Skip to main content

pixelflux/encoders/
hevc.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//! Software HEVC: x265 with the `gpl` feature, kvazaar without it, each at the fastest setting
8//! its library offers in real time (ultrafast, wavefront threads, no lookahead, no reordering)
9//! with the parameter sets repeated on every key frame. x265 carries 4:4:4 at full range like
10//! x264; kvazaar encodes 4:2:0. Neither library lets a session steer what a frame predicts
11//! from, so the session names no reference and refuses an invalidation, and the caller codes
12//! a key frame instead.
13//!
14//! With the patches under `patches/x265`, x265 encodes a picture laid out as its own frame where
15//! it lies (`x265_param_input_layout`, `copy-pic=0`), two pictures converted into in turn since
16//! it reads one through the encode it is passed to; the blocks the ledger marks still and clean
17//! reach it as its constant-block map (`constant_blocks`, `x265_picture.constantBlocks`), coded
18//! as skips without analysis and keeping their lowres intra estimates; and its per-block
19//! quantizer offsets apply without adaptive quantization's variance pass (`qp-offsets`). A
20//! still 1080p screen took 2.8 ms a frame on eight threads where it took 5.7, and 2560x1600
21//! 4.6 where it took 10.8. An x265 a distribution packages copies each picture from plain
22//! planes, analyses every block, and weighs the offsets against adaptive quantization. kvazaar
23//! converts into the picture it allocates and encodes from.
24//!
25//! The constant quality runs uncapped where a caller asks for a capped one
26//! (`RateSettings::capped`). x265's capped CRF (a VBV at the peak) moves its peak live only
27//! within the level and tier it derives from that peak: `x265_encoder_reconfig` keeps the old
28//! VBV, reporting success, where a move changes either (4.2: "Cannot reconfigure
29//! rate-control"), so a peak cut from the High tier's range into the Main one would cost a
30//! key frame, as a capped session at a pinned level's Main tier would cost the picture
31//! uncapped content takes past that tier's rate. kvazaar's rate control targets a rate and has
32//! no capped quality.
33
34use std::ffi::CString;
35
36use super::codec::{Codec, VIDEO_HEADER_LEN, h265_frame_type, push_video_header};
37use super::reference::Reference;
38use super::session::{Pending, Planes, Quality, RateSettings, encode_threads};
39use crate::CaptureSettings;
40
41/// One software HEVC session for one capture.
42pub struct HevcEncoder {
43    backend: Backend,
44    planes: Planes,
45    threads: i32,
46    quality: Quality,
47    rate: RateSettings,
48    omit_headers: bool,
49    /// Whether the next frame is the first after an open, which the encoder codes as a key
50    /// frame on its own.
51    fresh: bool,
52    next_pts: u64,
53    pending: Pending,
54    /// The quantizer the next frame is held at whatever the rate control (`hold_quantizer`).
55    held: Option<u32>,
56    /// The blocks the next frame of the rate control's refines (`refine_blocks`), and the
57    /// quantizer offset of each 16x16 block of every frame, 0 where it refines nothing.
58    refinement: Option<crate::ledger::Refinement>,
59    offsets: Vec<f32>,
60    /// The 16x16 blocks of the next frame that held still and are clean, one byte each in
61    /// raster order, nonzero where constant (`constant_blocks`); empty where none is.
62    constant: Vec<u8>,
63    /// The quantizer the rate control last coded a frame at, held frames aside.
64    last_quality: Option<u32>,
65    /// The bytes of that frame.
66    last_bytes: Option<usize>,
67}
68
69unsafe impl Send for HevcEncoder {}
70
71/// `HevcEncoder::reach_finer`: the predicted frames of either library at a quantizer leave the
72/// motion's coarse blocks as they are. x265's (4.2) cleanup of 1080p text ended at 45.0-46.3 dB
73/// at the paint-over quantizer and at 49.7-52.4 six steps finer at 1 and 2 Mbit/s, where its key
74/// frame at the paint-over quantizer decodes at 50.0, on a fifth to three fifths more bytes over
75/// a few seconds more (twelve came to 50.4-55.9 over up to 29 s); kvazaar's at 8 fps 0.5 and 1
76/// Mbit/s at 44.4-45.0 against 47.4-47.9, its rate control settling about the paint-over
77/// quantizer at 2 Mbit/s whatever the cleanup asks (44.8-46.8 dB either way).
78const HEVC_REACH_FINER: u32 = 6;
79
80/// Whether the build's software HEVC encoder codes 10 bits: x265 where it was built with its
81/// 10-bit encoder linked in.
82pub fn ten_bit() -> bool {
83    Backend::ten_bit()
84}
85
86/// What a backend answered for one submitted picture, with the quantizer it was coded at.
87struct Coded {
88    bytes: Vec<u8>,
89    pts: u64,
90    qp: u32,
91    /// The bytes the rate control spent on the frame, the filler it pads a constant rate with
92    /// included, which `bytes` leaves out.
93    spent: usize,
94}
95
96impl HevcEncoder {
97    /// Open a session on host frames; `rgba` names the byte order they arrive in.
98    pub fn new(settings: &CaptureSettings, rgba: bool) -> Result<Self, String> {
99        let _ = rgba;
100        let fullcolor = settings.video_fullcolor && Backend::FULLCOLOR;
101        let bit_depth = if settings.video_bit_depth >= 10 && Backend::ten_bit() {
102            10
103        } else {
104            8
105        };
106        let threads = encode_threads(settings);
107        let rate = RateSettings::new(settings).uncapped();
108        let quality = Quality::new(Codec::H265.quantizer(settings.video_crf));
109        let width = settings.width.max(1) as usize;
110        let height = settings.height.max(1) as usize;
111        let backend = Backend::open(
112            width,
113            height,
114            threads,
115            fullcolor,
116            bit_depth,
117            rate,
118            quality.current,
119        )?;
120        Ok(Self {
121            backend,
122            planes: Planes::new(width, height, fullcolor, bit_depth),
123            threads,
124            quality,
125            rate,
126            omit_headers: settings.omit_stripe_headers,
127            fresh: true,
128            next_pts: 0,
129            pending: Pending::default(),
130            held: None,
131            refinement: None,
132            offsets: Vec::new(),
133            constant: Vec::new(),
134            last_quality: None,
135            last_bytes: None,
136        })
137    }
138
139    pub fn codec(&self) -> Codec {
140        Codec::H265
141    }
142
143    /// The library behind the session, as the logs name it.
144    pub fn library(&self) -> &'static str {
145        Backend::LIBRARY
146    }
147
148    pub fn is_fullcolor(&self) -> bool {
149        self.planes.i444
150    }
151
152    /// The bits per sample the session codes.
153    pub fn bit_depth(&self) -> u32 {
154        self.planes.bit_depth
155    }
156
157    /// The software 4:4:4 of x264's kind signals full range.
158    pub fn is_full_range(&self) -> bool {
159        self.planes.i444
160    }
161
162    pub fn last_reference(&self) -> Reference {
163        Reference::Untracked
164    }
165
166    /// Whether `hold_quantizer` holds a frame at its quantizer: x265 at a constant rate factor,
167    /// kvazaar never. At a constant rate x265's rate control moves a forced quantizer back to
168    /// what its buffer affords (a key frame held at 18 came out 23.2 kB at 26.6 dB at 8 Mbit/s
169    /// 1080p), and a fresh session whose buffer is the key frame's budget plans it from a
170    /// fraction of that buffer (102.5 kB at 34 dB where the budget was 250 kB and the picture
171    /// before it 53.9 dB), so a constant-rate session holds none.
172    pub fn holds_quantizer(&self) -> bool {
173        Backend::HOLDS_QUANTIZER && !self.rate.cbr
174    }
175
176    /// The quantizer steps finer than the paint-over one at which a constant rate's frames come
177    /// to the paint-over quality (`FrameEncoder::reach_finer`): `HEVC_REACH_FINER`.
178    pub fn reach_finer(&self) -> u32 {
179        if self.rate.cbr { HEVC_REACH_FINER } else { 0 }
180    }
181
182    /// Whether the session refines blocks of a frame of its rate control's (`refine_blocks`):
183    /// x265, through its per-block quantizer offsets, and kvazaar, through its map of a delta a
184    /// 64x64 tree block, the finest of the blocks it holds.
185    pub fn refines(&self) -> bool {
186        Backend::REFINES
187    }
188
189    /// Quality index steps finer than the paint-over one a refined block is coded at
190    /// (`FrameEncoder::refines`): the library's (`Backend::REFINE_FINER`).
191    pub fn refine_finer(&self) -> u32 {
192        Backend::REFINE_FINER
193    }
194
195    /// Code the blocks `refinement` names in the next frame of the rate control's at its target
196    /// quality (`ledger`): each 16x16 block it refines at the quantizer steps from the rate
197    /// control's last to the target, off the frame's own.
198    pub fn refine_blocks(&mut self, refinement: &crate::ledger::Refinement) {
199        self.refinement = self.refines().then(|| refinement.clone());
200    }
201
202    /// Mark the blocks of the next frame, in raster order, that held still and are clean
203    /// (`BlockLedger::constant`), which x265 codes as skips without analysis
204    /// (`x265_picture.constantBlocks`) on a frame of its rate control's; kvazaar takes no such
205    /// map. An empty slice marks none.
206    pub fn constant_blocks(&mut self, constant: &[bool]) {
207        let blocks = self.planes.width.div_ceil(16) * self.planes.height.div_ceil(16);
208        self.constant.clear();
209        if Backend::CONSTANT_BLOCKS && constant.len() == blocks && constant.contains(&true) {
210            self.constant.extend(constant.iter().map(|&c| u8::from(c)));
211        }
212    }
213
214    /// The quality index (HEVC's quantizer) the rate control last coded a frame at, held frames
215    /// aside.
216    pub fn last_quality(&self) -> Option<u32> {
217        self.last_quality
218    }
219
220    /// The bytes the rate control spent on its last frame, held frames aside: its filler counted,
221    /// as the rate control counts it, though the frame goes without it (`Coded::spent`).
222    pub fn last_size(&self) -> Option<usize> {
223        self.last_bytes
224    }
225
226    /// The library keeps its reference lists to itself: refused, so the caller codes a key frame.
227    pub fn invalidate_reference(&mut self, _frame_id: u16) -> bool {
228        false
229    }
230
231    /// Open a fresh encoder with the live settings, whose first frame is a key frame.
232    fn reopen(&mut self) -> Result<(), String> {
233        self.backend = Backend::open(
234            self.planes.width,
235            self.planes.height,
236            self.threads,
237            self.planes.i444,
238            self.planes.bit_depth,
239            self.rate,
240            self.quality.current,
241        )?;
242        self.fresh = true;
243        Ok(())
244    }
245
246    /// Encode the next frame at the quantizer the quality index `crf` selects, leaving the
247    /// session's own quality for the frame after: the cleanup of a still screen, at a constant
248    /// rate factor, where x265 takes it as the frame's forced quantizer. A constant-rate session
249    /// codes the frame under its rate control (`holds_quantizer`). kvazaar takes no quantizer
250    /// but the one it opens at, and a re-open for the frame would need another to come back,
251    /// each a key frame, so it codes the frame at its own.
252    pub fn hold_quantizer(&mut self, crf: u32) {
253        if !self.rate.cbr {
254            self.held = Some(Codec::H265.quantizer(crf as i32));
255        }
256    }
257
258    /// Re-open the encoder when a rate or frame-rate setting changed: neither library's rate
259    /// control takes a new target while it runs.
260    pub fn reconfigure_rate(&mut self, settings: &CaptureSettings) -> Result<(), String> {
261        let Some(rate) = self.rate.changed(settings) else {
262            return Ok(());
263        };
264        self.rate = rate;
265        self.reopen()
266    }
267
268    /// Encode one packed host frame at the quality index `crf`, as a key frame when `force_idr`.
269    pub fn encode_host(
270        &mut self,
271        pixels: &[u8],
272        stride: usize,
273        rgba: bool,
274        frame_number: u64,
275        crf: u32,
276        force_idr: bool,
277    ) -> Result<Vec<u8>, String> {
278        if !self.rate.cbr
279            && self
280                .quality
281                .update(Codec::H265.quantizer(crf as i32))
282                .is_some()
283        {
284            self.reopen()?;
285        }
286        let held = self.held.take();
287        if force_idr && !self.fresh && !Backend::KEY_ON_REQUEST {
288            self.reopen()?;
289        }
290        let key = force_idr || self.fresh;
291        let full_range = self.is_full_range();
292        let pts = self.next_pts;
293        let refinement = self.refinement.take().filter(|_| held.is_none() && !key);
294        let offset = refinement.as_ref().map_or(0.0, |r| {
295            let q = |index: i32| Codec::H265.quantizer(index) as f32;
296            (q(r.target) - q(r.from)).min(0.0)
297        });
298        let cols = self.planes.width.div_ceil(16);
299        let rows = self.planes.height.div_ceil(16);
300        self.offsets.clear();
301        self.offsets.extend((0..rows).flat_map(|row| {
302            let refinement = refinement.as_ref();
303            (0..cols).map(move |col| {
304                if refinement.is_some_and(|r| r.plan.refines(col, row)) {
305                    offset
306                } else {
307                    0.0
308                }
309            })
310        }));
311        let constant = (!key && held.is_none() && !self.constant.is_empty())
312            .then_some(self.constant.as_mut_slice());
313        let coded = self.backend.encode(
314            &mut self.planes,
315            pixels,
316            stride,
317            rgba,
318            full_range,
319            self.threads as usize,
320            pts,
321            key,
322            held,
323            Some(self.offsets.as_mut_slice()),
324            constant,
325        );
326        self.constant.clear();
327        let coded = coded?;
328        self.next_pts += 1;
329        self.fresh = false;
330        self.pending.push(pts, frame_number as u16);
331        let Some(coded) = coded else {
332            return Ok(Vec::new());
333        };
334        if held.is_none() {
335            self.last_quality = Some(coded.qp);
336            self.last_bytes = Some(coded.spent);
337        }
338        let id = self.pending.take(coded.pts).unwrap_or(frame_number as u16);
339        let mut output = Vec::with_capacity(VIDEO_HEADER_LEN + coded.bytes.len());
340        if !self.omit_headers {
341            push_video_header(
342                &mut output,
343                Codec::H265,
344                h265_frame_type(&coded.bytes),
345                id,
346                0,
347                self.planes.width as u16,
348                self.planes.height as u16,
349                Reference::Untracked,
350            );
351        }
352        output.extend_from_slice(&coded.bytes);
353        Ok(output)
354    }
355}
356
357/// The CBR quantizer bounds in HEVC's domain, zero where the settings name none.
358fn qp_bounds(rate: RateSettings) -> (u32, u32) {
359    (
360        Codec::H265.quantizer_bound(rate.min_qp),
361        Codec::H265.quantizer_bound(rate.max_qp),
362    )
363}
364
365#[cfg(not(feature = "gpl"))]
366use kvazaar::Backend;
367#[cfg(feature = "gpl")]
368use x265::Backend;
369
370#[cfg(feature = "gpl")]
371mod x265 {
372    //! x265 through its versioned API table, configured by name so the session reads no field
373    //! of a parameter structure that moves between releases.
374    use std::ffi::c_int;
375    use std::ptr;
376
377    use codec_sys::x265::*;
378
379    use super::*;
380
381    /// HEVC's filler data NAL unit type (FD_NUT).
382    const FILLER_NAL: u32 = 38;
383
384    /// How a session lays out a source picture: the layout a patched x265 names
385    /// (`x265_param_input_layout`), which it encodes in place, or plain planes an unpatched one
386    /// copies from. Per plane, the bytes per row, the offset of the first pixel, and the buffer's
387    /// size; and the alignment every buffer takes.
388    #[derive(Clone, Copy, Default)]
389    struct InputLayout {
390        stride: [c_int; 3],
391        offset: [c_int; 3],
392        size: [c_int; 3],
393        align: c_int,
394    }
395
396    impl InputLayout {
397        /// Plain planes of a `width` x `height` picture, each row starting 64-byte aligned.
398        #[cfg(not(x265_input_layout))]
399        fn plain(width: usize, height: usize, i444: bool, bit_depth: u32) -> Self {
400            let bytes = if bit_depth > 8 { 2 } else { 1 };
401            let (cw, ch) = if i444 {
402                (width, height)
403            } else {
404                (width.div_ceil(2), height.div_ceil(2))
405            };
406            let (luma, chroma) = (
407                (width * bytes).next_multiple_of(64),
408                (cw * bytes).next_multiple_of(64),
409            );
410            InputLayout {
411                stride: [luma as c_int, chroma as c_int, chroma as c_int],
412                offset: [0; 3],
413                size: [
414                    (luma * height) as c_int,
415                    (chroma * ch) as c_int,
416                    (chroma * ch) as c_int,
417                ],
418                align: 64,
419            }
420        }
421    }
422
423    pub struct Backend {
424        api: *const x265_api,
425        params: *mut x265_param,
426        encoder: *mut x265_encoder,
427        /// How the session lays out a source picture, which `pictures` follow.
428        layout: InputLayout,
429        /// Two pictures, three planes each, converted into in turn: x265 reads a picture through
430        /// the encode it is passed to, and the other holds the frame before while it is read.
431        /// Each plane is 64-byte aligned within its buffer at `aligned`.
432        pictures: [[Vec<u64>; 3]; 2],
433        aligned: [[usize; 3]; 2],
434        current: usize,
435    }
436
437    impl Drop for Backend {
438        fn drop(&mut self) {
439            unsafe {
440                let api = &*self.api;
441                if !self.encoder.is_null() {
442                    (api.encoder_close.unwrap())(self.encoder);
443                }
444                if !self.params.is_null() {
445                    (api.param_free.unwrap())(self.params);
446                }
447            }
448        }
449    }
450
451    impl Backend {
452        pub const LIBRARY: &'static str = "x265";
453        pub const FULLCOLOR: bool = true;
454
455        /// Whether the linked x265 carries a 10-bit encoder beside its 8-bit one, which is
456        /// a property of how the library was built.
457        pub fn ten_bit() -> bool {
458            !unsafe { x265_api_get(10) }.is_null()
459        }
460
461        pub const KEY_ON_REQUEST: bool = true;
462        pub const HOLDS_QUANTIZER: bool = true;
463        pub const REFINES: bool = true;
464        pub const CONSTANT_BLOCKS: bool = true;
465        /// Quality index steps finer than the paint-over one x265 codes a refined block at
466        /// (`FrameEncoder::refines`). Its ultrafast analysis settles many a still block on its merge
467        /// candidate before the offset's quantizer counts, so refining takes 1080p text beside a video
468        /// less far than its cleanup of the still screen does, and every step finer gains: at 8 Mbit/s
469        /// from 31.1 dB to 34.2, 35.5, and 36.3 at 0, 3, and 6 steps finer, against 46.4 still; at a
470        /// constant quality from 37.7 to 40.7, 41.6, and 42.7, against 49.6; the video losing 0.3 dB.
471        pub const REFINE_FINER: u32 = 6;
472
473        /// The frame rate the library was configured at, as `(num, den)`.
474        #[cfg(test)]
475        pub fn frame_rate(&self) -> (u32, u32) {
476            unsafe { ((*self.params).fpsNum, (*self.params).fpsDenom) }
477        }
478
479        /// The smallest coding unit the library was configured with, in pixels.
480        #[cfg(test)]
481        pub fn min_cu_size(&self) -> u32 {
482            unsafe { (*self.params).minCUSize }
483        }
484
485        fn set(&self, name: &str, value: &str) -> Result<(), String> {
486            let (n, v) = (CString::new(name).unwrap(), CString::new(value).unwrap());
487            match unsafe { ((*self.api).param_parse.unwrap())(self.params, n.as_ptr(), v.as_ptr()) }
488            {
489                0 => Ok(()),
490                X265_PARAM_BAD_NAME => Err(format!("x265 knows no option {name}")),
491                _ => Err(format!("x265 refused {name}={value}")),
492            }
493        }
494
495        /// The rate-control options of `rate` at quantizer `q`: a constant rate held to its
496        /// VBV with the quantizer capped at 51, since the library's default ceiling admits
497        /// out-of-spec quantizers that only force skips on a VBV underflow; else a constant
498        /// rate factor.
499        fn rate_options(rate: RateSettings, q: u32) -> Vec<(&'static str, String)> {
500            let mut options = Vec::new();
501            if rate.cbr {
502                let kbps = (rate.bps() / 1000).max(1);
503                let vbv_kbit = (rate.vbv() / 1000).max(1);
504                options.push(("bitrate", kbps.to_string()));
505                options.push(("vbv-maxrate", kbps.to_string()));
506                options.push(("vbv-bufsize", vbv_kbit.to_string()));
507                if rate.vbv() > 1000 {
508                    options.push(("vbv-init", vbv_kbit.to_string()));
509                }
510                options.push(("strict-cbr", "1".into()));
511                let (lo, hi) = qp_bounds(rate);
512                if lo > 0 {
513                    options.push(("qpmin", lo.to_string()));
514                }
515                options.push(("qpmax", if hi > 0 { hi } else { 51 }.to_string()));
516            } else {
517                options.push(("crf", q.to_string()));
518            }
519            options
520        }
521
522        pub fn open(
523            width: usize,
524            height: usize,
525            threads: i32,
526            i444: bool,
527            bit_depth: u32,
528            rate: RateSettings,
529            q: u32,
530        ) -> Result<Self, String> {
531            let api = unsafe { x265_api_get(bit_depth as c_int) };
532            if api.is_null() {
533                return Err(format!("x265 carries no {bit_depth}-bit encoder"));
534            }
535            let params = unsafe { ((*api).param_alloc.unwrap())() };
536            if params.is_null() {
537                return Err("x265 allocated no parameters".into());
538            }
539            let mut me = Self {
540                api,
541                params,
542                encoder: ptr::null_mut(),
543                layout: InputLayout::default(),
544                pictures: Default::default(),
545                aligned: [[0; 3]; 2],
546                current: 0,
547            };
548            let (preset, tune) = (c"ultrafast", c"zerolatency");
549            if unsafe {
550                ((*api).param_default_preset.unwrap())(params, preset.as_ptr(), tune.as_ptr())
551            } < 0
552            {
553                return Err("x265 refused the ultrafast zerolatency preset".into());
554            }
555            if width < 16 || height < 16 {
556                return Err(format!(
557                    "x265 encodes no picture as small as {width}x{height}"
558                ));
559            }
560            let mut options: Vec<(&str, String)> = vec![
561                ("log-level", "none".into()),
562                ("input-res", format!("{width}x{height}")),
563                ("input-csp", if i444 { "i444" } else { "i420" }.into()),
564                ("fps", format!("{}/{}", rate.fps.num, rate.fps.den)),
565                ("range", if i444 { "full" } else { "limited" }.into()),
566                ("colorprim", "bt709".into()),
567                ("transfer", "bt709".into()),
568                ("colormatrix", "bt709".into()),
569                ("keyint", "-1".into()),
570                ("scenecut", "0".into()),
571                ("repeat-headers", "1".into()),
572                ("annexb", "1".into()),
573                ("aud", "0".into()),
574                ("rc-lookahead", "0".into()),
575                ("bframes", "0".into()),
576                ("frame-threads", "1".into()),
577                ("pools", threads.to_string()),
578                ("wpp", "1".into()),
579            ];
580            if width < 32 || height < 32 {
581                options.push(("ctu", "16".into()));
582            } else if width < 64 || height < 64 {
583                options.push(("ctu", "32".into()));
584            }
585            // ultrafast's 16x16 coding units cannot follow text: a 1080p page's key frame at
586            // quantizer 18 came out at 750 kB and 49.3 dB with them, at 395 kB and 53.5 dB with 8x8
587            // ones, and a still screen's cleanup at 60 fps 8 Mbit/s ended at 47.6 dB against 53.1
588            // on 58% fewer bytes. A 1080p scroll codes as fast, one at 4K a tenth slower.
589            options.push(("min-cu-size", "8".into()));
590            // A frame's per-block offsets (`refine_blocks`) reach the quantizer only under
591            // adaptive quantization; qp-offsets keeps it at strength 0, where its variance pass
592            // over every block is skipped and the offsets apply as given.
593            options.push(("aq-mode", "1".into()));
594            options.push(("aq-strength", "0".into()));
595            #[cfg(x265_qp_offsets)]
596            options.push(("qp-offsets", "1".into()));
597            #[cfg(x265_input_layout)]
598            options.push(("copy-pic", "0".into()));
599            options.extend(Self::rate_options(rate, q));
600            for (name, value) in &options {
601                me.set(name, value)?;
602            }
603            let profile = match (i444, bit_depth) {
604                (true, 10) => Some(c"main444-10"),
605                (true, _) => Some(c"main444-8"),
606                (false, 10) => Some(c"main10"),
607                (false, _) => None,
608            };
609            if let Some(profile) = profile
610                && unsafe { ((*api).param_apply_profile.unwrap())(params, profile.as_ptr()) } < 0
611            {
612                return Err(format!(
613                    "x265 refused the {} profile",
614                    profile.to_string_lossy()
615                ));
616            }
617            me.encoder = unsafe { ((*api).encoder_open.unwrap())(params) };
618            if me.encoder.is_null() {
619                return Err("x265 refused the session".into());
620            }
621            // The layout follows the parameters as the open encoder holds them.
622            #[cfg(x265_input_layout)]
623            {
624                let mut opened: x265_param = unsafe { std::mem::zeroed() };
625                unsafe { ((*api).encoder_parameters.unwrap())(me.encoder, &mut opened) };
626                let mut layout: x265_input_layout = unsafe { std::mem::zeroed() };
627                if unsafe { ((*api).param_input_layout.unwrap())(&opened, &mut layout) } < 0 {
628                    return Err("x265 named no input layout".into());
629                }
630                me.layout = InputLayout {
631                    stride: layout.stride,
632                    offset: layout.offset,
633                    size: layout.size,
634                    align: layout.align,
635                };
636            }
637            #[cfg(not(x265_input_layout))]
638            {
639                me.layout = InputLayout::plain(width, height, i444, bit_depth);
640            }
641            let align = (me.layout.align.max(1) as usize).max(8);
642            for (picture, aligned) in me.pictures.iter_mut().zip(me.aligned.iter_mut()) {
643                for p in 0..3 {
644                    let size = me.layout.size[p].max(0) as usize;
645                    picture[p] = vec![0u64; (size + align).div_ceil(8)];
646                    let start = picture[p].as_ptr() as usize;
647                    aligned[p] = start.next_multiple_of(align) - start;
648                }
649            }
650            Ok(me)
651        }
652
653        /// The first pixel of plane `p` of picture `picture`, and the bytes from it to the
654        /// buffer's end.
655        fn plane(&mut self, picture: usize, p: usize) -> (*mut u8, usize) {
656            let offset = self.aligned[picture][p] + self.layout.offset[p].max(0) as usize;
657            let buf = &mut self.pictures[picture][p];
658            let len = buf.len() * 8 - offset;
659            (unsafe { buf.as_mut_ptr().cast::<u8>().add(offset) }, len)
660        }
661
662        #[allow(clippy::too_many_arguments)]
663        /// Convert the host frame into the next in-place picture and encode it there.
664        /// `constant` marks the 16x16 blocks x265 codes as skips without analysis.
665        #[allow(clippy::too_many_arguments)]
666        pub fn encode(
667            &mut self,
668            planes: &mut Planes,
669            pixels: &[u8],
670            stride: usize,
671            rgba: bool,
672            full_range: bool,
673            threads: usize,
674            pts: u64,
675            key: bool,
676            held: Option<u32>,
677            offsets: Option<&mut [f32]>,
678            constant: Option<&mut [u8]>,
679        ) -> Result<Option<Coded>, String> {
680            let picture = self.current;
681            self.current ^= 1;
682            let (w, h) = (planes.width, planes.height);
683            let ch = if planes.i444 { h } else { h.div_ceil(2) };
684            let strides = (
685                self.layout.stride[0] as usize,
686                self.layout.stride[1] as usize,
687            );
688            let [(y, ylen), (u, ulen), (v, vlen)] = [
689                self.plane(picture, 0),
690                self.plane(picture, 1),
691                self.plane(picture, 2),
692            ];
693            super::super::session::check_host_frame(pixels, stride, w, h)?;
694            if planes.bit_depth > 8 {
695                let (ys, cs) = (strides.0 / 2, strides.1 / 2);
696                // SAFETY: each plane buffer is x265's own layout for this picture, whose offset of
697                // the first pixel is a whole sample, and holds ys * h and cs * ch samples past it.
698                let (y16, u16, v16) = unsafe {
699                    (
700                        std::slice::from_raw_parts_mut(y.cast::<u16>(), (ys * h).min(ylen / 2)),
701                        std::slice::from_raw_parts_mut(u.cast::<u16>(), (cs * ch).min(ulen / 2)),
702                        std::slice::from_raw_parts_mut(v.cast::<u16>(), (cs * ch).min(vlen / 2)),
703                    )
704                };
705                crate::encoders::software::convert_to_yuv10_mt(
706                    pixels,
707                    stride,
708                    w,
709                    h,
710                    rgba,
711                    planes.i444,
712                    full_range,
713                    y16,
714                    u16,
715                    v16,
716                    (ys, cs),
717                    threads,
718                );
719            } else {
720                // SAFETY: as above, with byte samples.
721                let (y8, u8s, v8) = unsafe {
722                    (
723                        std::slice::from_raw_parts_mut(y, (strides.0 * h).min(ylen)),
724                        std::slice::from_raw_parts_mut(u, (strides.1 * ch).min(ulen)),
725                        std::slice::from_raw_parts_mut(v, (strides.1 * ch).min(vlen)),
726                    )
727                };
728                super::super::session::convert_into(
729                    pixels,
730                    stride,
731                    w,
732                    h,
733                    rgba,
734                    planes.i444,
735                    full_range,
736                    false,
737                    threads,
738                    y8,
739                    u8s,
740                    v8,
741                    strides,
742                )?;
743            }
744            let api = unsafe { &*self.api };
745            let mut pic: x265_picture = unsafe { std::mem::zeroed() };
746            let mut out: [x265_picture; 2] = unsafe { std::mem::zeroed() };
747            unsafe {
748                (api.picture_init.unwrap())(self.params, &mut pic);
749                for o in &mut out {
750                    (api.picture_init.unwrap())(self.params, o);
751                }
752            }
753            // Written per index: x265 4 declares a fourth plane for alpha, x265 3 does not.
754            pic.planes[0] = y.cast();
755            pic.planes[1] = u.cast();
756            pic.planes[2] = v.cast();
757            pic.stride[0] = self.layout.stride[0];
758            pic.stride[1] = self.layout.stride[1];
759            pic.stride[2] = self.layout.stride[2];
760            // Only a patched x265 reads a constant-block map.
761            #[cfg(x265_constant_blocks)]
762            if let Some(constant) = constant {
763                pic.constantBlocks = constant.as_mut_ptr();
764            }
765            #[cfg(not(x265_constant_blocks))]
766            let _ = constant;
767            pic.pts = pts as i64;
768            pic.bitDepth = planes.bit_depth as c_int;
769            pic.sliceType = if key {
770                X265_TYPE_IDR as c_int
771            } else {
772                X265_TYPE_AUTO as c_int
773            };
774            pic.forceqp = held.map_or(0, |q| q as c_int + 1);
775            // x265 copies the offsets as the frame is submitted, within this call, into a buffer
776            // the frame object it recycles has only where its first frame came with offsets: a
777            // session hands them with every frame, zero where it refines nothing, or a recycled
778            // frame without one is written through a null pointer.
779            if let Some(offsets) = offsets {
780                pic.quantOffsets = offsets.as_mut_ptr();
781            }
782            let mut nals: *mut x265_nal = ptr::null_mut();
783            let mut count: u32 = 0;
784            let ret = unsafe {
785                encoder_encode(
786                    api,
787                    self.encoder,
788                    &mut nals,
789                    &mut count,
790                    &mut pic,
791                    out.as_mut_ptr(),
792                )
793            };
794            if ret < 0 {
795                return Err("x265 refused the frame".into());
796            }
797            if count == 0 {
798                return Ok(None);
799            }
800            // A constant rate pads every frame short of its budget with filler data (`strict-cbr`),
801            // which tells a decoder nothing: 44% of a still screen's stream at 60 fps and 54% at
802            // 8 fps at 0.5 Mbit/s at 1080p. It stays off the wire.
803            let mut bytes = Vec::new();
804            let mut spent = 0;
805            for nal in unsafe { std::slice::from_raw_parts(nals, count as usize) } {
806                spent += nal.sizeBytes as usize;
807                if nal.type_ != FILLER_NAL {
808                    bytes.extend_from_slice(unsafe {
809                        std::slice::from_raw_parts(nal.payload, nal.sizeBytes as usize)
810                    });
811                }
812            }
813            Ok(Some(Coded {
814                bytes,
815                pts: out[0].pts.max(0) as u64,
816                qp: out[0].frameData.qp.round().clamp(0.0, 51.0) as u32,
817                spent,
818            }))
819        }
820    }
821}
822
823#[cfg(not(feature = "gpl"))]
824mod kvazaar {
825    //! kvazaar through its API table. The library takes no per-frame key-frame request and no
826    //! live rate change, so both re-open the encoder.
827    use std::ptr;
828
829    use codec_sys::kvazaar::*;
830
831    use super::*;
832
833    pub struct Backend {
834        api: *const kvz_api,
835        config: *mut kvz_config,
836        encoder: *mut kvz_encoder,
837    }
838
839    impl Drop for Backend {
840        fn drop(&mut self) {
841            unsafe {
842                let api = &*self.api;
843                if !self.encoder.is_null() {
844                    (api.encoder_close.unwrap())(self.encoder);
845                }
846                if !self.config.is_null() {
847                    (api.config_destroy.unwrap())(self.config);
848                }
849            }
850        }
851    }
852
853    impl Backend {
854        pub const LIBRARY: &'static str = "kvazaar";
855        pub const FULLCOLOR: bool = false;
856
857        /// kvazaar's sample depth is fixed when it is built, and the build here is 8-bit.
858        pub fn ten_bit() -> bool {
859            false
860        }
861        pub const KEY_ON_REQUEST: bool = false;
862        pub const HOLDS_QUANTIZER: bool = false;
863        pub const REFINES: bool = true;
864        pub const CONSTANT_BLOCKS: bool = false;
865        /// Quality index steps finer than the paint-over one kvazaar codes a refined block at
866        /// (`FrameEncoder::refines`): 1080p text beside a video went from 38.5 dB to 43.2, 45.8,
867        /// and 48.0 at a constant quality at 0, 3, and 6 steps finer, where kvazaar's cleanup of
868        /// the still screen reaches 44.3. At a constant rate the refined tree blocks take the
869        /// frame's quantizer off its rate control, which spends what they save elsewhere: 31.8
870        /// dB with the map and without at 8 Mbit/s.
871        pub const REFINE_FINER: u32 = 0;
872
873        /// The frame rate the library was configured at, as `(num, den)`.
874        #[cfg(test)]
875        pub fn frame_rate(&self) -> (u32, u32) {
876            unsafe {
877                (
878                    (*self.config).framerate_num as u32,
879                    (*self.config).framerate_denom as u32,
880                )
881            }
882        }
883
884        fn set(&self, name: &str, value: &str) -> Result<(), String> {
885            let (n, v) = (CString::new(name).unwrap(), CString::new(value).unwrap());
886            if unsafe { ((*self.api).config_parse.unwrap())(self.config, n.as_ptr(), v.as_ptr()) }
887                == 0
888            {
889                return Err(format!("kvazaar refused {name}={value}"));
890            }
891            Ok(())
892        }
893
894        pub fn open(
895            width: usize,
896            height: usize,
897            threads: i32,
898            i444: bool,
899            bit_depth: u32,
900            rate: RateSettings,
901            q: u32,
902        ) -> Result<Self, String> {
903            let _ = (i444, bit_depth);
904            if !width.is_multiple_of(8) || !height.is_multiple_of(8) {
905                return Err(format!(
906                    "kvazaar takes only pictures a multiple of eight, not {width}x{height}"
907                ));
908            }
909            let api = unsafe { kvz_api_get(8) };
910            if api.is_null() {
911                return Err("kvazaar carries no 8-bit encoder".into());
912            }
913            let config = unsafe { ((*api).config_alloc.unwrap())() };
914            if config.is_null() {
915                return Err("kvazaar allocated no configuration".into());
916            }
917            let me = Self {
918                api,
919                config,
920                encoder: ptr::null_mut(),
921            };
922            unsafe {
923                if ((*api).config_init.unwrap())(config) == 0 {
924                    return Err("kvazaar refused to initialize its configuration".into());
925                }
926                let cfg = &mut *config;
927                cfg.width = width as i32;
928                cfg.height = height as i32;
929                cfg.framerate_num = rate.fps.num as i32;
930                cfg.framerate_denom = rate.fps.den as i32;
931                cfg.vui.fullrange = 0;
932                cfg.vui.colorprim = 1;
933                cfg.vui.transfer = 1;
934                cfg.vui.colormatrix = 1;
935            }
936            let mut options: Vec<(&str, String)> = vec![
937                ("preset", "ultrafast".into()),
938                ("gop", "0".into()),
939                ("period", "0".into()),
940                ("vps-period", "0".into()),
941                ("threads", threads.to_string()),
942                ("owf", "0".into()),
943                ("wpp", "1".into()),
944            ];
945            if rate.cbr {
946                options.push(("bitrate", rate.bps().to_string()));
947                options.push(("rc-algorithm", "oba".into()));
948            } else {
949                options.push(("qp", q.to_string()));
950            }
951            for (name, value) in &options {
952                me.set(name, value)?;
953            }
954            let mut me = me;
955            me.encoder = unsafe { ((*api).encoder_open.unwrap())(config) };
956            if me.encoder.is_null() {
957                return Err("kvazaar refused the session".into());
958            }
959            Ok(me)
960        }
961
962        #[allow(clippy::too_many_arguments)]
963        pub fn encode(
964            &mut self,
965            planes: &mut Planes,
966            pixels: &[u8],
967            stride: usize,
968            rgba: bool,
969            full_range: bool,
970            threads: usize,
971            pts: u64,
972            _key: bool,
973            _held: Option<u32>,
974            offsets: Option<&mut [f32]>,
975            _constant: Option<&mut [u8]>,
976        ) -> Result<Option<Coded>, String> {
977            let api = unsafe { &*self.api };
978            let pic =
979                unsafe { (api.picture_alloc.unwrap())(planes.width as i32, planes.height as i32) };
980            if pic.is_null() {
981                return Err("kvazaar allocated no picture".into());
982            }
983            // kvazaar decides at the key frame whether the stream carries a quantizer delta per
984            // coding unit, from whether that picture has a map, so every picture carries one,
985            // 0 where nothing is refined; it frees the map with the picture.
986            if let Some(offsets) = offsets {
987                let (cols16, cols, rows) = (
988                    planes.width.div_ceil(16),
989                    planes.width.div_ceil(64),
990                    planes.height.div_ceil(64),
991                );
992                let rows16 = planes.height.div_ceil(16);
993                let map = unsafe { libc::malloc(cols * rows) as *mut i8 };
994                if !map.is_null() {
995                    for row in 0..rows {
996                        for col in 0..cols {
997                            let mut dqp = 0f32;
998                            for r in row * 4..(row * 4 + 4).min(rows16) {
999                                for c in col * 4..(col * 4 + 4).min(cols16) {
1000                                    dqp = dqp.min(offsets[r * cols16 + c]);
1001                                }
1002                            }
1003                            unsafe { *map.add(row * cols + col) = dqp.round() as i8 };
1004                        }
1005                    }
1006                    unsafe {
1007                        (*pic).roi.width = cols as i32;
1008                        (*pic).roi.height = rows as i32;
1009                        (*pic).roi.roi_array = map;
1010                    }
1011                }
1012            }
1013            let result = unsafe {
1014                let p = &mut *pic;
1015                let luma_stride = p.stride as usize;
1016                let chroma_stride = luma_stride / 2;
1017                let (w, h) = (planes.width, planes.height);
1018                let y = std::slice::from_raw_parts_mut(p.y, luma_stride * h);
1019                let u = std::slice::from_raw_parts_mut(p.u, chroma_stride * h.div_ceil(2));
1020                let v = std::slice::from_raw_parts_mut(p.v, chroma_stride * h.div_ceil(2));
1021                p.pts = pts as i64;
1022                super::super::session::convert_into(
1023                    pixels,
1024                    stride,
1025                    w,
1026                    h,
1027                    rgba,
1028                    false,
1029                    full_range,
1030                    false,
1031                    threads,
1032                    y,
1033                    u,
1034                    v,
1035                    (luma_stride, chroma_stride),
1036                )
1037            };
1038            if let Err(e) = result {
1039                unsafe { (api.picture_free.unwrap())(pic) };
1040                return Err(e);
1041            }
1042            let mut data: *mut kvz_data_chunk = ptr::null_mut();
1043            let mut len: u32 = 0;
1044            let mut recon: *mut kvz_picture = ptr::null_mut();
1045            let mut info: kvz_frame_info = unsafe { std::mem::zeroed() };
1046            let ok = unsafe {
1047                (api.encoder_encode.unwrap())(
1048                    self.encoder,
1049                    pic,
1050                    &mut data,
1051                    &mut len,
1052                    &mut recon,
1053                    ptr::null_mut(),
1054                    &mut info,
1055                )
1056            };
1057            let mut coded = None;
1058            if ok != 0 && !data.is_null() {
1059                let mut bytes = Vec::with_capacity(len as usize);
1060                let mut chunk = data;
1061                while !chunk.is_null() {
1062                    let c = unsafe { &*chunk };
1063                    bytes.extend_from_slice(&c.data[..c.len as usize]);
1064                    chunk = c.next;
1065                }
1066                let out_pts = if recon.is_null() {
1067                    pts
1068                } else {
1069                    unsafe { (*recon).pts.max(0) as u64 }
1070                };
1071                coded = Some(Coded {
1072                    spent: bytes.len(),
1073                    bytes,
1074                    pts: out_pts,
1075                    qp: info.qp.clamp(0, 51) as u32,
1076                });
1077            }
1078            unsafe {
1079                (api.picture_free.unwrap())(pic);
1080                (api.picture_free.unwrap())(recon);
1081                (api.chunk_free.unwrap())(data);
1082            }
1083            if ok == 0 {
1084                return Err("kvazaar refused the frame".into());
1085            }
1086            Ok(coded)
1087        }
1088    }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094
1095    /// A session's first frame is a key frame, a forced one mid-stream is too, and the ones
1096    /// between are deltas that name no reference.
1097    #[test]
1098    fn key_frames_come_on_request_and_deltas_name_no_reference() {
1099        let settings = CaptureSettings {
1100            width: 64,
1101            height: 48,
1102            target_fps: 30.0,
1103            codec: Codec::H265,
1104            video_crf: 25,
1105            ..Default::default()
1106        };
1107        let mut enc = HevcEncoder::new(&settings, false).expect("session");
1108        assert_eq!(enc.library(), Backend::LIBRARY);
1109        let bgra = vec![128u8; 64 * 48 * 4];
1110        let mut kinds = Vec::new();
1111        for t in 0..4u64 {
1112            let out = enc
1113                .encode_host(&bgra, 64 * 4, false, t, 25, t == 2)
1114                .expect("encode");
1115            kinds.push(super::super::codec::parse_video_type(out[1]).map(|(_, k)| k));
1116            assert_eq!(enc.last_reference(), Reference::Untracked);
1117        }
1118        assert_eq!(
1119            kinds,
1120            [
1121                Some(super::super::codec::FRAME_KEY),
1122                Some(super::super::codec::FRAME_DELTA),
1123                Some(super::super::codec::FRAME_KEY),
1124                Some(super::super::codec::FRAME_DELTA)
1125            ]
1126        );
1127        assert!(!enc.invalidate_reference(1));
1128    }
1129
1130    /// The blocks marked constant are coded as skips without analysis: a still screen's frame
1131    /// comes out smaller with the map than without, where x265 takes one, at 8 bits and, where
1132    /// the build carries the encoder, at 10, each picture converted into x265's own layout.
1133    #[cfg(all(feature = "gpl", x265_constant_blocks))]
1134    #[test]
1135    fn constant_blocks_code_a_still_frame_as_skips() {
1136        for depth in [8, 10] {
1137            if depth == 10 && !ten_bit() {
1138                continue;
1139            }
1140            constant_blocks_skip_at(depth);
1141        }
1142    }
1143
1144    #[cfg(all(feature = "gpl", x265_constant_blocks))]
1145    fn constant_blocks_skip_at(depth: u32) {
1146        let (w, h) = (320usize, 240usize);
1147        let settings = CaptureSettings {
1148            width: w as i32,
1149            height: h as i32,
1150            target_fps: 30.0,
1151            codec: Codec::H265,
1152            video_crf: 25,
1153            video_bit_depth: depth as i32,
1154            ..Default::default()
1155        };
1156        let mut bgra = vec![0u8; w * h * 4];
1157        for (i, px) in bgra.as_chunks_mut::<4>().0.iter_mut().enumerate() {
1158            let (x, y) = (i % w, i / w);
1159            let text = (x / 7 + y / 11) % 3 == 0 && x % 7 < 5 && y % 11 < 8;
1160            px.copy_from_slice(if text {
1161                &[30, 30, 30, 255]
1162            } else {
1163                &[235, 235, 235, 255]
1164            });
1165        }
1166        let mut plain = HevcEncoder::new(&settings, false).expect("session");
1167        let mut mapped = HevcEncoder::new(&settings, false).expect("session");
1168        let blocks = w.div_ceil(16) * h.div_ceil(16);
1169        let (mut plain_bytes, mut mapped_bytes) = (0, 0);
1170        for n in 0..6u64 {
1171            plain_bytes = plain
1172                .encode_host(&bgra, w * 4, false, n, 25, n == 0)
1173                .expect("encode")
1174                .len();
1175            mapped.constant_blocks(&vec![true; blocks]);
1176            mapped_bytes = mapped
1177                .encode_host(&bgra, w * 4, false, n, 25, n == 0)
1178                .expect("encode")
1179                .len();
1180        }
1181        assert_eq!(mapped.bit_depth(), depth);
1182        assert!(
1183            mapped_bytes < plain_bytes,
1184            "{depth} bits: {mapped_bytes} bytes with the map, {plain_bytes} without"
1185        );
1186    }
1187
1188    /// A box that changes every frame beside blocks marked constant, at a size no coding unit
1189    /// divides: the coding units the box touches and the partial ones along the edges are
1190    /// analyzed as before, the rest skipped, and the box and the still screen decode as close to
1191    /// the source as without the map, each picture converted in place into x265's own layout.
1192    #[cfg(all(feature = "gpl", x265_constant_blocks))]
1193    #[test]
1194    fn constant_blocks_leave_a_change_beside_them_coded() {
1195        use super::super::mixed_screen;
1196        let settings = CaptureSettings {
1197            width: mixed_screen::W as i32,
1198            height: mixed_screen::H as i32,
1199            target_fps: 30.0,
1200            codec: Codec::H265,
1201            video_crf: 25,
1202            ..Default::default()
1203        };
1204        let mut plain = HevcEncoder::new(&settings, false).expect("session");
1205        let mut mapped = HevcEncoder::new(&settings, false).expect("session");
1206        mixed_screen::assert_decodes_alike(
1207            Codec::H265,
1208            "x265",
1209            |src, n| {
1210                plain
1211                    .encode_host(src, mixed_screen::W * 4, false, n, 25, n == 0)
1212                    .expect("encode")
1213            },
1214            |src, n, map| {
1215                mapped.constant_blocks(map);
1216                mapped
1217                    .encode_host(src, mixed_screen::W * 4, false, n, 25, n == 0)
1218                    .expect("encode")
1219            },
1220        );
1221    }
1222
1223    /// A constant-rate session's frames come to the paint-over quality six quantizer steps finer
1224    /// than the paint-over quantizer, a constant quality's at it.
1225    #[test]
1226    fn a_constant_rate_cleanup_reaches_six_steps_finer() {
1227        let mut settings = CaptureSettings {
1228            width: 64,
1229            height: 48,
1230            target_fps: 30.0,
1231            codec: Codec::H265,
1232            video_crf: 25,
1233            ..Default::default()
1234        };
1235        assert_eq!(
1236            HevcEncoder::new(&settings, false)
1237                .expect("session")
1238                .reach_finer(),
1239            0
1240        );
1241        settings.video_cbr_mode = true;
1242        settings.video_bitrate_kbps = 500;
1243        assert_eq!(
1244            HevcEncoder::new(&settings, false)
1245                .expect("session")
1246                .reach_finer(),
1247            6
1248        );
1249    }
1250
1251    /// An x265 session codes with 8x8 coding units, finer than its preset's 16x16, at every size
1252    /// down to the smallest picture it codes.
1253    #[cfg(feature = "gpl")]
1254    #[test]
1255    fn x265_codes_with_8x8_coding_units() {
1256        for (width, height) in [(1920, 1080), (64, 48), (16, 16)] {
1257            let settings = CaptureSettings {
1258                width,
1259                height,
1260                target_fps: 30.0,
1261                codec: Codec::H265,
1262                video_crf: 25,
1263                ..Default::default()
1264            };
1265            let enc = HevcEncoder::new(&settings, false).expect("session");
1266            assert_eq!(enc.backend.min_cu_size(), 8, "{width}x{height}");
1267        }
1268    }
1269
1270    /// A constant rate's still frames go without the filler x265 pads them to their budget with,
1271    /// which the cleanup still reads as spent: the frames on the wire come to a fraction of it.
1272    #[cfg(feature = "gpl")]
1273    #[test]
1274    fn a_constant_rates_filler_stays_off_the_wire() {
1275        let settings = CaptureSettings {
1276            width: 320,
1277            height: 240,
1278            target_fps: 30.0,
1279            codec: Codec::H265,
1280            video_cbr_mode: true,
1281            video_bitrate_kbps: 1000,
1282            ..Default::default()
1283        };
1284        let mut enc = HevcEncoder::new(&settings, false).expect("session");
1285        let bgra: Vec<u8> = (0..320 * 240 * 4).map(|i| ((i * 7) % 251) as u8).collect();
1286        let (mut sent, mut spent) = (0, 0);
1287        for t in 0..60u64 {
1288            let out = enc
1289                .encode_host(&bgra, 320 * 4, false, t, 25, false)
1290                .expect("encode");
1291            if t >= 30 && !out.is_empty() {
1292                let payload = &out[VIDEO_HEADER_LEN..];
1293                let mut at = 0;
1294                while let Some(i) = payload[at..].windows(3).position(|w| w == [0, 0, 1]) {
1295                    at += i + 3;
1296                    assert_ne!((payload[at] >> 1) & 0x3f, 38, "frame {t} carries filler");
1297                }
1298                sent += payload.len();
1299                spent += enc.last_size().unwrap_or(0);
1300            }
1301        }
1302        assert!(spent > 2 * sent, "{sent} bytes sent of {spent} spent");
1303    }
1304
1305    /// The library is configured at the capture's rate as the fraction it names.
1306    #[test]
1307    fn the_frame_rate_reaches_the_library_as_its_fraction() {
1308        for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
1309            let settings = CaptureSettings {
1310                width: 64,
1311                height: 48,
1312                target_fps: f64::from(num) / f64::from(den),
1313                codec: Codec::H265,
1314                video_cbr_mode: true,
1315                video_bitrate_kbps: 2000,
1316                ..Default::default()
1317            };
1318            let enc = HevcEncoder::new(&settings, false).expect("session");
1319            assert_eq!(enc.backend.frame_rate(), (num, den), "{}", Backend::LIBRARY);
1320        }
1321    }
1322}