Skip to main content

pixelflux/encoders/
mod.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//! Encoder backends and what they share: the codec identities and wire framing, the
8//! rate-control policy, which software encoder a build resolves each codec to, and which
9//! hardware encoder a render node serves each with.
10
11/// The bit writer the packed headers of the VA-API session are written with.
12pub mod bits;
13/// The GPU semaphore an X server's blit signals for the encoder, made through Vulkan.
14pub(crate) mod blit_semaphore;
15/// Codec identities, wire framing, quantizer domains, level ladders, bitstream reads.
16pub mod codec;
17/// The identity of a dmabuf behind an fd, for the sessions that keep an import of each buffer.
18pub(crate) mod dmabuf_identity;
19/// A frame rate as the fraction encoder parameters and bitstream timing take.
20pub mod frame_rate;
21/// Software HEVC: x265 with the `gpl` feature, kvazaar without it.
22pub mod hevc;
23/// NVIDIA NVENC hardware H.264 / HEVC / AV1 encoder loaded via runtime `libcuda` /
24/// `libnvidia-encode`.
25pub mod nvenc;
26/// Cisco OpenH264 software H.264 encoder (BSD-licensed): the software H.264 encoder of a
27/// build without `gpl`, and always built for the test suite.
28#[cfg(any(feature = "openh264", test))]
29pub mod oh264;
30/// The long-term references of an OpenH264 session, read off its own slice headers.
31#[cfg(any(feature = "openh264", test))]
32pub mod oh264_refs;
33/// PNG watermark overlay composited onto frames before encoding.
34pub mod overlay;
35pub mod reference;
36/// What the full-frame software sessions share: planar input, quantizer, rate settings.
37pub mod session;
38/// The CPU path: striped JPEG and whole-frame software H.264 (libx264 or OpenH264, by build),
39/// with per-region change detection.
40pub mod software;
41/// The color an H.264 stream declares: read from a sequence parameter set, and written into
42/// one for a device that converts without saying what it converted with.
43pub mod sps;
44/// Software AV1 through SVT-AV1.
45pub mod svtav1;
46/// Tegra hardware video encoding through the vendor V4L2 encoder, loaded at runtime: the only path to a
47/// Jetson's encoder, which carries no `libnvidia-encode` and no render node driver. Built for
48/// `aarch64` alone — the vendor libraries and the encoder behind them exist on no other
49/// architecture, so an x86_64 build carries none of this.
50#[cfg(target_arch = "aarch64")]
51pub mod tegra;
52/// Hardware H.264 through a generic stateful V4L2 M2M encoder: boards whose encoder sits
53/// behind the kernel's own interface rather than a vendor library or a render node, such as
54/// a Raspberry Pi 4, RK356x, or i.MX8M. Built everywhere, since the interface is the kernel's.
55pub mod v4l2m2m;
56/// VA-API hardware sessions on a DRM render node, driven through libva directly.
57pub mod vaapi;
58/// Software VP8 and VP9 through libvpx.
59pub mod vpx;
60
61pub use codec::*;
62
63use std::collections::HashMap;
64use std::ffi::c_void;
65use std::sync::{Mutex, OnceLock};
66
67use nvenc::NvencEncoder;
68use smithay::backend::allocator::dmabuf::Dmabuf;
69
70use crate::CaptureSettings;
71
72#[cfg(not(any(feature = "gpl", feature = "openh264")))]
73compile_error!(
74    "pixelflux needs a software H.264 encoder: enable the `gpl` feature (libx264, the default) or `openh264`."
75);
76
77/// A software encoder the build runs for one codec: the library's name as reported to Python
78/// and the logs.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct SoftwareEncoder {
81    pub library: &'static str,
82}
83
84/// The codecs a render node encodes in hardware, each with the backend's name.
85/// The formats past 8-bit 4:2:0 an engine encodes a codec in: 4:4:4 for a `video_fullcolor`
86/// session, and 10 bits for a `video_bit_depth` one at 4:2:0 and at 4:4:4.
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct Formats {
89    pub fullcolor: bool,
90    pub ten_bit: [bool; 2],
91}
92
93impl Formats {
94    /// Each format as `chroma-depth`, 8-bit 4:2:0 first.
95    pub fn names(&self) -> Vec<String> {
96        let mut names = Vec::new();
97        for (fullcolor, chroma) in [(false, "420"), (true, "444")] {
98            for (ten_bit, depth) in [(false, 8), (true, 10)] {
99                let carried = match (fullcolor, ten_bit) {
100                    (false, false) => true,
101                    (true, false) => self.fullcolor,
102                    (_, true) => self.ten_bit[usize::from(fullcolor)],
103                };
104                if carried {
105                    names.push(format!("{chroma}-{depth}"));
106                }
107            }
108        }
109        names
110    }
111}
112
113/// Each video codec an engine on a node encodes, the backend's name, and the formats it
114/// encodes the codec in.
115pub type HardwareEncoders = Vec<(Codec, &'static str, Formats)>;
116
117/// The hardware backend that serves each video codec on an encode node, as the name a
118/// session logs it in lower case (`"nvenc"`, `"vaapi"`, or `"tegra"`), probed once per node and
119/// remembered for the life of the process: the ladder picks the backend by the node's
120/// driver exactly as `select_frame_encoder` does, and that backend lists the codecs its
121/// device has an engine for (`nvenc::probe_codecs`, `vaapi::probe_codecs`). A node whose
122/// backend cannot be brought up serves nothing, said once in the log, so a caller offers
123/// the codec only where a session would come up on hardware rather than demote. What a
124/// session is then refused for (a size past the engine's maximum, a 4:4:4 the engine lacks)
125/// is still the ladder's to fall through on.
126pub fn hardware_encoders(encode_node_index: i32) -> HardwareEncoders {
127    probe_node(encode_node_index).unwrap_or_default()
128}
129
130/// An encode node's probe answer: its hardware table, or the refusal of its NVENC or VA-API
131/// backend as `(backend, error)`.
132type ProbeAnswer = Result<HardwareEncoders, (&'static str, String)>;
133
134/// `hardware_encoders` with the refusal of a node whose backend could not be brought up. A
135/// device with no NVENC session to spare (`nvenc::SESSIONS_TAKEN`) says nothing lasting, so
136/// that answer is asked again.
137fn probe_node(encode_node_index: i32) -> ProbeAnswer {
138    static PROBED: OnceLock<Mutex<HashMap<i32, ProbeAnswer>>> = OnceLock::new();
139    let node = encode_node_index.max(0);
140    let mut probed = PROBED
141        .get_or_init(|| Mutex::new(HashMap::new()))
142        .lock()
143        .unwrap();
144    if let Some(answer) = probed.get(&node) {
145        return answer.clone();
146    }
147    #[cfg(target_arch = "aarch64")]
148    if tegra::available() {
149        let served: HardwareEncoders = tegra::served()
150            .into_iter()
151            .map(|c| (c, "tegra", Formats::default()))
152            .collect();
153        let names: Vec<&str> = served.iter().map(|(c, ..)| c.display()).collect();
154        println!(
155            "[pixelflux] Render node {node} encodes {} on tegra.",
156            names.join(", ")
157        );
158        probed.insert(node, Ok(served.clone()));
159        return Ok(served);
160    }
161    let driver = crate::get_gpu_driver(node);
162    let (backend, codecs) = if crate::driver_selects_nvenc(&driver) {
163        ("nvenc", nvenc::probe_codecs(node))
164    } else {
165        ("vaapi", vaapi::probe_codecs(node))
166    };
167    let answer = match codecs {
168        Ok(codecs) => Ok(codecs
169            .into_iter()
170            .map(|(codec, formats): vaapi::Served| (codec, backend, formats))
171            .collect()),
172        Err(e) => {
173            eprintln!("[pixelflux] No hardware encoder on render node {node} ({backend}): {e}");
174            Err((backend, e))
175        }
176    };
177    if answer.as_ref().map_or(true, Vec::is_empty) && v4l2m2m::available() {
178        // The node index names nothing here: an M2M encoder is not a render node, and the
179        // answer is the same whichever index was asked about. It is cached under the key all
180        // the same, so a caller asking twice is answered from the same probe.
181        let codecs = v4l2m2m::served();
182        let served: HardwareEncoders = codecs
183            .iter()
184            .map(|&c| (c, "v4l2m2m", Formats::default()))
185            .collect();
186        let names: Vec<&str> = codecs.iter().map(|c| c.display()).collect();
187        println!(
188            "[pixelflux] A stateful V4L2 M2M encoder serves {}.",
189            names.join(", ")
190        );
191        probed.insert(node, Ok(served.clone()));
192        return Ok(served);
193    }
194    if let Ok(served) = &answer
195        && !served.is_empty()
196    {
197        let names: Vec<&str> = served.iter().map(|(codec, ..)| codec.display()).collect();
198        println!(
199            "[pixelflux] Render node {node} encodes {} on {backend}.",
200            names.join(", ")
201        );
202    }
203    if !matches!(&answer, Err((_, e)) if e == nvenc::SESSIONS_TAKEN) {
204        probed.insert(node, answer.clone());
205    }
206    answer
207}
208
209/// Whether the software encoder of a codec carries a 4:4:4 (`video_fullcolor`) request: x264
210/// (High 4:4:4, full range), x265, and libvpx's VP9 (profile 1) do; OpenH264, kvazaar, VP8, and
211/// SVT-AV1 encode such a request 4:2:0.
212pub fn software_fullcolor(codec: Codec) -> bool {
213    match software_encoder(codec) {
214        Some(enc) => {
215            matches!(enc.library, "x264" | "x265")
216                || (codec == Codec::Vp9 && enc.library == "libvpx" && vpx::encodes_444())
217        }
218        None => false,
219    }
220}
221
222/// Whether the software encoder of a codec carries a 10-bit `video_bit_depth` request, at
223/// whichever chroma it codes: x264 (High 10, and High 4:4:4 Predictive), x265 where the build
224/// links its 10-bit encoder, libvpx's VP9 (profiles 2 and 3) where it was built with high bit
225/// depth, and SVT-AV1 do; OpenH264, kvazaar, and VP8 encode such a request at 8 bits.
226pub fn software_ten_bit(codec: Codec) -> bool {
227    match software_encoder(codec) {
228        Some(enc) => match (codec, enc.library) {
229            #[cfg(feature = "gpl")]
230            (Codec::H264, "x264") => software::H264EncoderWrapper::ten_bit(),
231            (Codec::H265, "x265") => hevc::ten_bit(),
232            (Codec::Vp9, "libvpx") => vpx::encodes_ten_bit(),
233            (Codec::Av1, _) => true,
234            _ => false,
235        },
236        None => false,
237    }
238}
239
240/// The formats the software encoder of a codec codes it in.
241pub fn software_formats(codec: Codec) -> Formats {
242    let ten_bit = software_ten_bit(codec);
243    let fullcolor = software_fullcolor(codec);
244    Formats {
245        fullcolor,
246        ten_bit: [ten_bit, ten_bit && fullcolor],
247    }
248}
249
250/// The name of the software encoder this build runs for `codec`, for logs; `"none"` for JPEG,
251/// which is striped stills.
252pub fn software_library(codec: Codec) -> &'static str {
253    software_encoder(codec).map_or("none", |enc| enc.library)
254}
255
256/// The software encoder this build runs for `codec` on this machine, or `None` for JPEG and for
257/// a codec whose encoder does not run here.
258///
259/// Every one is fixed by the crate features: libx264 and x265 whenever `gpl` is on (it wins
260/// even if `openh264` is also enabled), Cisco OpenH264 and kvazaar for a GPL-free build; libvpx
261/// serves VP8 and VP9 and SVT-AV1 serves AV1 in both. H.264 is what the CPU path encodes
262/// with, the software fallback under NVENC/VA-API included. Each
263/// full-frame encoder is opened on a frame in a forked child once, so one that takes its
264/// process down on this machine is one the build does not carry here.
265pub fn software_encoder(codec: Codec) -> Option<SoftwareEncoder> {
266    static ENCODES: OnceLock<[bool; 5]> = OnceLock::new();
267    let library = match codec {
268        Codec::Jpeg => return None,
269        Codec::H264 => {
270            if cfg!(feature = "gpl") {
271                "x264"
272            } else {
273                "openh264"
274            }
275        }
276        Codec::H265 => {
277            if cfg!(feature = "gpl") {
278                "x265"
279            } else {
280                "kvazaar"
281            }
282        }
283        Codec::Vp8 | Codec::Vp9 => "libvpx",
284        Codec::Av1 => "svt-av1",
285    };
286    let encodes =
287        ENCODES.get_or_init(|| Codec::VIDEO.map(|c| c == Codec::H264 || encodes_in_child(c)));
288    encodes[Codec::VIDEO.iter().position(|&c| c == codec).unwrap()]
289        .then_some(SoftwareEncoder { library })
290}
291
292/// Whether opening `codec`'s software session on a small frame and encoding one leaves a
293/// process alive, tried in a forked child: a library that faults on this machine, with an
294/// instruction the CPU lacks or a register the kernel does not emulate, takes the child down
295/// and not a session.
296fn encodes_in_child(codec: Codec) -> bool {
297    survives_in_child(|| {
298        // The child converts on a pool of its own: the parent's rayon workers do not exist in it.
299        // The pool is this thread, since a thread the child started would wait on the lock the
300        // standard library takes to set a thread up, which a parent thread may have held at the
301        // fork.
302        let Ok(pool) = rayon::ThreadPoolBuilder::new()
303            .num_threads(1)
304            .use_current_thread()
305            .build()
306        else {
307            return;
308        };
309        pool.install(|| {
310            let settings = CaptureSettings {
311                width: 256,
312                height: 128,
313                ..Default::default()
314            };
315            let Ok(mut session) = software_session(&settings, codec, false) else {
316                return;
317            };
318            let frame = vec![0u8; 256 * 128 * 4];
319            for n in 0..64 {
320                let packet = session.encode_host(
321                    &frame,
322                    256 * 4,
323                    false,
324                    n,
325                    settings.video_crf as u32,
326                    n == 0,
327                );
328                if !matches!(packet, Ok(p) if p.is_empty()) {
329                    break;
330                }
331            }
332        })
333    })
334}
335
336/// Whether a forked child outlives `f`: a fatal signal in it is the finding, an exit is not, and
337/// neither reaches the caller. The child has a minute, and one that neither returns nor dies
338/// in it has stalled, which is as final. What it prints goes nowhere, so a library that
339/// announces itself on open speaks for a session and not for the probe. The fork waits until
340/// no SVT-AV1 handle is alive (`svtav1::LIFECYCLE`), as at the import-time probe: one being
341/// created, released, or run holds process-wide state the child would inherit half-built or
342/// locked. One a sandbox refuses to fork counts as alive.
343fn survives_in_child(f: impl FnOnce()) -> bool {
344    let mut live = svtav1::LIFECYCLE.lock().unwrap_or_else(|e| e.into_inner());
345    while *live > 0 {
346        live = svtav1::RELEASED
347            .wait(live)
348            .unwrap_or_else(|e| e.into_inner());
349    }
350    let pid = unsafe { libc::fork() };
351    drop(live);
352    match pid {
353        0 => {
354            unsafe {
355                libc::signal(libc::SIGALRM, libc::SIG_DFL);
356                libc::alarm(60);
357                let null = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
358                if null >= 0 {
359                    libc::dup2(null, 1);
360                    libc::dup2(null, 2);
361                }
362            }
363            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
364            unsafe { libc::_exit(0) }
365        }
366        pid if pid > 0 => {
367            let mut status = 0;
368            while unsafe { libc::waitpid(pid, &mut status, 0) } < 0 {
369                if std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
370                    return true;
371                }
372            }
373            !libc::WIFSIGNALED(status)
374        }
375        _ => true,
376    }
377}
378
379/// Seconds of a constant-rate target a held frame (the cleanup of a still screen) may spend: a
380/// held key frame larger than that is coded again at a coarser quantizer (`held_key_retry`), or
381/// planned with that budget where the library's own rate control sizes it (x264, SVT-AV1),
382/// and a held refresh is coarsened to fit from the rate control's last quantizer
383/// (`pipeline::decide_hw_fullframe`), and on NVENC coded again to fit where it comes out past
384/// `HELD_REFRESH_LIMIT_S`. A second of the target drains in 0.4 s at the pace the WebRTC pacer
385/// holds video to, so a user acting just as the cleanup goes out waits that long at most for
386/// it. It holds a 1080p key frame at the paint-over quantizer down to 2 Mbit/s (NVENC 144 kB,
387/// x264 182 kB); a quarter second made that key frame coarser than the picture it was cleaning
388/// (x264 52 kB at 31.7 dB, NVENC 99 kB at 45.3 dB at 2 Mbit/s).
389pub(crate) const HELD_KEY_BUDGET_S: f64 = 1.0;
390
391/// Seconds of a constant-rate target a held refresh of the whole picture, a predicted frame, may
392/// spend before it is coded again to fit `HELD_KEY_BUDGET_S` where the encoder can take it back
393/// (NVENC): what a cleanup through the rate control spends at most (`pipeline`'s `CONVERGE_S`).
394/// A refresh under it stands, since coding it coarser costs the picture more than the time it
395/// saves: NVENC's of a 1080p texture at 0.25 and 0.1 Mbit/s, 1.8 and 3.1 s of the target, came
396/// out at 1.3 and 1.7 s six to ten steps coarser, 6.4-6.8 dB worse. One of dense text at 0.1
397/// Mbit/s at 1080p took 52 s.
398pub(crate) const HELD_REFRESH_LIMIT_S: f64 = 10.0;
399
400/// The quality index a held key frame of `len` bytes is coded again at, from `crf`, where it
401/// came out larger than a budget of `cap` bytes; `None` where it fits. A quality index is a
402/// quantizer on the H.26x scale, where six steps halve the bits.
403pub(crate) fn held_key_retry(crf: u32, len: usize, cap: usize) -> Option<u32> {
404    (len > cap && cap > 0)
405        .then(|| (crf + (6.0 * (len as f64 / cap as f64).log2()).ceil() as u32).min(51))
406}
407
408/// The quality index a held key frame of a constant-rate session is first coded at, from `crf`,
409/// where the session's last held key frame, `last` = (bytes, quality index), says one at `crf`
410/// would come out past a budget of `cap` bytes: as much coarser as `held_key_retry` would take
411/// it, so the frame is coded once rather than twice, since a rate control that runs through
412/// the held frame (libvpx's) charges its buffer with both. A key frame coded more than twelve
413/// steps from `crf` says too little about one at it, and the frame is tried at `crf`.
414pub(crate) fn held_key_start(crf: u32, last: Option<(usize, u32)>, cap: usize) -> u32 {
415    match last {
416        Some((len, q)) if q.abs_diff(crf) <= 12 => {
417            let at_crf = len as f64 * 2f64.powf((f64::from(q) - f64::from(crf)) / 6.0);
418            held_key_retry(crf, at_crf.round() as usize, cap).unwrap_or(crf)
419        }
420        _ => crf,
421    }
422}
423
424/// Damps visible quality "blinking": the number of consecutive frames a QP *increase* (a
425/// quality drop under sustained motion) must be requested before a fixed-QP encoder commits
426/// it. Moving the quantizer costs a codec re-open (x265, kvazaar, SVT-AV1) or a full encoder
427/// rebuild (OpenH264), and either forces a key frame, so acting on every transient increase —
428/// and then reversing it as motion settles — would make the picture pulse. Quality
429/// *increases* (a lower QP, e.g. a paint-over refresh) apply at once and never wait. Shared so
430/// the fixed-QP encoders cannot disagree about how long a drop must persist.
431pub(crate) const QP_HYSTERESIS_LIMIT: u32 = 60;
432
433/// The buffer, in frames of its peak, a capped constant-quality session takes
434/// (`CaptureSettings::video_vbv_multiplier`): software H.264's constant-rate buffer, so a cap
435/// that binds codes a frame as the constant rate congestion control steers does, and a cap
436/// that does not leaves the rate factor its picture.
437pub const CAPPED_VBV_FRAMES: f64 = software::CBR_VBV_FRAMES;
438
439/// Size the CBR VBV/HRD buffer so rate control has enough slack to hold quality steady
440/// without letting end-to-end latency drift upward.
441///
442/// The size is expressed as a multiple of one frame's bit budget (`bitrate_bps / fps`) rather
443/// than a fixed byte count so a live bitrate or framerate change rescales the buffer with it,
444/// preserving the same latency behavior at every operating point.
445///
446/// The 1.5-frame default holds for hardware rate control too: a one-frame buffer measured on
447/// NVENC (`gpu_bench_cbr_policy`, a V100) ran an 8 Mbit/s H.264 session at 13.3 to 13.7 Mbit/s
448/// on scene cuts at the same PSNR, made the frame after a single cut larger (408 against 288
449/// kbit) and bought nothing on steady content. The overshoot is the quarter-resolution first
450/// pass misjudging a scene cut with no buffer left to absorb the miss
451/// (`gpu_bench_cbr_rate_control`): full-resolution two-pass holds a one-frame buffer at
452/// 1.5 ms more per frame, single-pass halves the overshoot at a lower PSNR, and the GOP target
453/// and quantizer ceiling change nothing. HEVC holds either buffer.
454///
455/// # Arguments
456///
457/// * `bitrate_bps` - Target bitrate in bits per second.
458/// * `fps` - Target frames per second.
459/// * `keyframe_interval_s` - Seconds between scheduled keyframes; `<= 0` for infinite GOP.
460/// * `multiplier` - Explicit buffer multiplier; `<= 0` selects the policy default (1.5 on
461///   infinite GOP, 3 when keyframe interval is active).
462///
463/// # Returns
464///
465/// VBV buffer size in bits, clamped to `[1, u32::MAX]`.
466/// The most frames an H.264 or H.265 session keeps for reference (`video_reference_frames`).
467pub fn reference_frames(settings: &CaptureSettings) -> u32 {
468    match settings.video_reference_frames {
469        0 => reference::REFERENCE_FRAMES,
470        n => n.min(reference::REFERENCE_FRAMES),
471    }
472}
473
474pub fn vbv_bits(bitrate_bps: u32, fps: f64, keyframe_interval_s: f64, multiplier: f64) -> u32 {
475    let frame_bits = f64::from(bitrate_bps) / fps.max(1.0);
476    let mult = if multiplier > 0.0 {
477        multiplier
478    } else if keyframe_interval_s > 0.0 {
479        3.0
480    } else {
481        1.5
482    };
483    (frame_bits * mult)
484        .round()
485        .max(1.0)
486        .min(f64::from(u32::MAX)) as u32
487}
488
489/// The `Colorspace:` field of a stream log line, from what the session negotiated rather than
490/// what was asked for: a hardware encoder can refuse 4:4:4, software 4:4:4 of x264's kind
491/// carries full range, and a device converting in fixed function picks the range itself.
492/// Shared so the X11 and Wayland logs describe an identical session identically.
493pub fn colorspace_desc(fullcolor: bool, full_range: bool) -> &'static str {
494    match (fullcolor, full_range) {
495        (true, true) => "I444 (Full Range)",
496        (true, false) => "I444 (Limited Range)",
497        (false, true) => "I420 (Full Range)",
498        (false, false) => "I420 (Limited Range)",
499    }
500}
501
502/// The rate-control field of a video stream's log line, as the session applies it: a backend
503/// with one rate control (`fixed`, from `FrameEncoder::fixed_rate_control`) runs it at the
504/// bitrate target whatever the mode, and every other session runs its mode, a CRF one under the
505/// peak of its bitrate where it is `capped` (`session_capped`) and with no bitrate cap otherwise.
506pub fn rate_desc(settings: &CaptureSettings, fixed: Option<&str>, capped: bool) -> String {
507    match fixed {
508        Some(mode) => format!("{mode} {}", settings.video_bitrate_kbps),
509        None if settings.video_cbr_mode => format!("CBR {}", settings.video_bitrate_kbps),
510        None if capped => format!(
511            "CRF: {} (Peak {})",
512            settings.video_crf, settings.video_bitrate_kbps
513        ),
514        None => format!("CRF: {}", settings.video_crf),
515    }
516}
517
518/// The paint-over field of a stream's log line, or `None` where paint-over changes nothing. The
519/// cleanup reads a still screen from its content, so it acts with Turbo as without. JPEG resends
520/// a still stripe at the paint-over quality when it is above the stream's. At a constant quality
521/// the cleanup, where the paint-over CRF is below the stream's, moves the session to that CRF for
522/// its frames and their burst, except on a backend with one rate control (`fixed`), which takes no
523/// quality from the caller and sends them at its own. At a constant rate it acts wherever the rate
524/// control codes coarser than the paint-over quantizer: held at it where the session holds a frame
525/// at a quantizer (`holds`, `FrameEncoder::holds_quantizer`), and as a refresh and its burst at the
526/// rate control's own quality elsewhere.
527pub fn paint_over_desc(
528    settings: &CaptureSettings,
529    fixed: Option<&str>,
530    holds: bool,
531) -> Option<String> {
532    if !settings.cleans_up() {
533        return None;
534    }
535    if !settings.codec.is_video() {
536        return (settings.paint_over_jpeg_quality > settings.jpeg_quality).then(|| {
537            format!(
538                "PaintOver Q: {} (Trigger: {}f)",
539                settings.paint_over_jpeg_quality,
540                settings.paint_over_trigger()
541            )
542        });
543    }
544    let burst = settings.video_paintover_burst_frames;
545    let at_paint_over = if settings.video_cbr_mode {
546        holds
547    } else if settings.video_paintover_crf < settings.video_crf {
548        fixed.is_none()
549    } else {
550        return None;
551    };
552    Some(if at_paint_over {
553        format!(
554            "PaintOver CRF: {} (Burst: {burst}f)",
555            settings.video_paintover_crf
556        )
557    } else {
558        format!("PaintOver Burst: {burst}f")
559    })
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    /// The rate field says what the session runs: a backend with one rate control runs it at the
567    /// bitrate target in either mode, and every other session runs its mode, CRF naming its peak
568    /// where the session takes one and no bitrate otherwise.
569    #[test]
570    fn the_rate_field_names_what_the_backend_applies() {
571        let crf = CaptureSettings {
572            codec: Codec::H264,
573            video_crf: 25,
574            video_bitrate_kbps: 8000,
575            ..Default::default()
576        };
577        let cbr = CaptureSettings {
578            video_cbr_mode: true,
579            ..crf.clone()
580        };
581        assert_eq!(rate_desc(&crf, None, false), "CRF: 25");
582        assert_eq!(rate_desc(&crf, None, true), "CRF: 25 (Peak 8000)");
583        assert_eq!(rate_desc(&cbr, None, false), "CBR 8000");
584        for settings in [&crf, &cbr] {
585            assert_eq!(rate_desc(settings, Some("VBR"), false), "VBR 8000");
586            assert_eq!(rate_desc(settings, Some("CBR"), false), "CBR 8000");
587        }
588    }
589
590    /// A session reports the peak it takes: the CPU path's H.264 where x264 carries it, libvpx
591    /// where asked, and x265 never; a constant rate is no capped quality.
592    #[test]
593    fn a_session_reports_whether_it_takes_its_peak() {
594        let capped = CaptureSettings {
595            codec: Codec::H264,
596            width: 64,
597            height: 48,
598            target_fps: 30.0,
599            video_crf: 25,
600            video_bitrate_kbps: 4000,
601            video_vbv_multiplier: CAPPED_VBV_FRAMES,
602            ..Default::default()
603        };
604        assert_eq!(session_capped(None, &capped), cfg!(feature = "gpl"));
605        let cbr = CaptureSettings {
606            video_cbr_mode: true,
607            ..capped.clone()
608        };
609        assert!(!session_capped(None, &cbr));
610        let vpx = |codec| CaptureSettings {
611            codec,
612            ..capped.clone()
613        };
614        for codec in [Codec::Vp8, Codec::Vp9] {
615            let enc = FrameEncoder::Vpx(vpx::VpxEncoder::new(&vpx(codec), codec, false).unwrap());
616            assert!(session_capped(Some(&enc), &vpx(codec)), "{codec:?}");
617        }
618        let hevc = vpx(Codec::H265);
619        let enc = FrameEncoder::Hevc(hevc::HevcEncoder::new(&hevc, false).unwrap());
620        assert!(!session_capped(Some(&enc), &hevc));
621    }
622
623    /// Paint-over is named where the cleanup acts, Turbo or not: its CRF where the session codes
624    /// the cleanup at it, at a constant quality below the stream's on any backend that takes a
625    /// quality from the caller and at a constant rate where the session holds a quantizer; its
626    /// burst where the frames come at the backend's or the rate control's own quality; JPEG's
627    /// higher quality; and nothing where it cannot fire.
628    #[test]
629    fn the_paint_over_field_names_what_paint_over_does() {
630        let crf = CaptureSettings {
631            codec: Codec::H264,
632            video_crf: 25,
633            video_paintover_crf: 18,
634            video_paintover_burst_frames: 5,
635            use_paint_over_quality: true,
636            video_streaming_mode: false,
637            ..Default::default()
638        };
639        let cbr = CaptureSettings {
640            video_cbr_mode: true,
641            ..crf.clone()
642        };
643        let turbo = |s: &CaptureSettings| CaptureSettings {
644            video_streaming_mode: true,
645            ..s.clone()
646        };
647        for settings in [&crf, &turbo(&crf)] {
648            assert_eq!(
649                paint_over_desc(settings, None, true).as_deref(),
650                Some("PaintOver CRF: 18 (Burst: 5f)")
651            );
652            assert_eq!(
653                paint_over_desc(settings, None, false).as_deref(),
654                Some("PaintOver CRF: 18 (Burst: 5f)")
655            );
656            assert_eq!(
657                paint_over_desc(settings, Some("VBR"), false).as_deref(),
658                Some("PaintOver Burst: 5f")
659            );
660        }
661        for settings in [&cbr, &turbo(&cbr)] {
662            assert_eq!(
663                paint_over_desc(settings, None, true).as_deref(),
664                Some("PaintOver CRF: 18 (Burst: 5f)")
665            );
666            assert_eq!(
667                paint_over_desc(settings, None, false).as_deref(),
668                Some("PaintOver Burst: 5f")
669            );
670            assert_eq!(
671                paint_over_desc(settings, Some("CBR"), false).as_deref(),
672                Some("PaintOver Burst: 5f")
673            );
674        }
675        assert_eq!(
676            paint_over_desc(
677                &CaptureSettings {
678                    use_paint_over_quality: false,
679                    ..crf.clone()
680                },
681                None,
682                true
683            ),
684            None
685        );
686        // A constant rate cleans up with the paint-over off too, to the same quality.
687        let cbr_off = CaptureSettings {
688            use_paint_over_quality: false,
689            ..cbr.clone()
690        };
691        assert_eq!(
692            paint_over_desc(&cbr_off, None, false),
693            paint_over_desc(&cbr, None, false)
694        );
695        assert_eq!(
696            paint_over_desc(&cbr_off, None, true),
697            paint_over_desc(&cbr, None, true)
698        );
699        assert_eq!(
700            paint_over_desc(
701                &CaptureSettings {
702                    video_paintover_crf: 25,
703                    ..crf.clone()
704                },
705                None,
706                true
707            ),
708            None
709        );
710        assert_eq!(
711            paint_over_desc(
712                &CaptureSettings {
713                    video_paintover_crf: 25,
714                    ..cbr
715                },
716                None,
717                true
718            )
719            .as_deref(),
720            Some("PaintOver CRF: 25 (Burst: 5f)")
721        );
722        let jpeg = CaptureSettings {
723            codec: Codec::Jpeg,
724            jpeg_quality: 40,
725            paint_over_jpeg_quality: 90,
726            paint_over_trigger_frames: 15,
727            ..crf
728        };
729        assert_eq!(
730            paint_over_desc(&jpeg, None, false).as_deref(),
731            Some("PaintOver Q: 90 (Trigger: 15f)")
732        );
733        assert_eq!(
734            paint_over_desc(&turbo(&jpeg), None, false).as_deref(),
735            Some("PaintOver Q: 90 (Trigger: 15f)")
736        );
737        assert_eq!(
738            paint_over_desc(
739                &CaptureSettings {
740                    paint_over_jpeg_quality: 40,
741                    ..jpeg.clone()
742                },
743                None,
744                false
745            ),
746            None
747        );
748        assert_eq!(
749            paint_over_desc(
750                &CaptureSettings {
751                    use_paint_over_quality: false,
752                    ..jpeg
753                },
754                None,
755                false
756            ),
757            None
758        );
759    }
760
761    /// A held key frame past its budget is coded again six quantizer steps coarser for each
762    /// doubling past it, never past 51, and one inside it is kept.
763    #[test]
764    fn a_held_key_past_its_budget_is_coarsened_by_the_overshoot() {
765        assert_eq!(held_key_retry(18, 1000, 1000), None);
766        assert_eq!(held_key_retry(18, 2000, 1000), Some(24));
767        assert_eq!(held_key_retry(18, 3000, 1000), Some(28));
768        assert_eq!(held_key_retry(40, 64_000, 1000), Some(51));
769        assert_eq!(held_key_retry(18, 5000, 0), None, "no budget, no retry");
770    }
771
772    /// A held key frame starts where the last one says a frame of its size fits the budget: at
773    /// the paint-over index where it fit there, as much coarser as a retry would take it where
774    /// it did not, and at the paint-over index where there is no last one near enough to say.
775    #[test]
776    fn a_held_key_starts_where_the_last_one_says_it_fits() {
777        assert_eq!(held_key_start(18, None, 1000), 18);
778        assert_eq!(held_key_start(18, Some((800, 18)), 1000), 18);
779        assert_eq!(held_key_start(18, Some((2000, 18)), 1000), 24);
780        assert_eq!(
781            held_key_start(18, Some((1000, 24)), 1000),
782            24,
783            "a retried key's own index"
784        );
785        assert_eq!(
786            held_key_start(18, Some((100, 40)), 1000),
787            18,
788            "too far to say"
789        );
790    }
791
792    /// A session lands on the codec it asked for, or on the next video codec this host serves;
793    /// JPEG is the ladder's last leg, not its answer to one missing encoder.
794    #[test]
795    fn a_session_lands_on_a_video_codec_the_host_serves() {
796        let mut settings = CaptureSettings {
797            width: 256,
798            height: 128,
799            codec: Codec::Av1,
800            use_cpu: true,
801            ..Default::default()
802        };
803        let encoder = select_frame_encoder(
804            &mut settings,
805            FrameSource::Host { rgba: false },
806            None,
807            "test",
808        );
809        println!("asked for av1, landed on {}", settings.codec.display());
810        assert_ne!(
811            settings.codec,
812            Codec::Jpeg,
813            "this build encodes video in software"
814        );
815        assert!(software_encoder(settings.codec).is_some());
816        // H.264 comes back as the caller's own CPU path rather than a full-frame session.
817        assert!(encoder.is_some() || settings.codec == Codec::H264);
818    }
819
820    /// A codec with no path on this host falls through the video codecs it does serve, the
821    /// encode node's hardware ones first and most efficient first, the software ones by encode
822    /// time, software H.264 leading them.
823    #[test]
824    fn a_codec_without_a_path_falls_through_the_served_video_codecs() {
825        let software: Vec<Codec> = SOFTWARE_ORDER
826            .iter()
827            .copied()
828            .filter(|&codec| codec != Codec::Av1 && software_encoder(codec).is_some())
829            .collect();
830        assert_eq!(software.first(), Some(&Codec::H264));
831        assert_eq!(
832            fallback_codecs(Codec::Av1, &[]),
833            software,
834            "software H.264 leads the software rungs"
835        );
836        assert_eq!(
837            fallback_codecs(Codec::Av1, &[Codec::H264]),
838            software,
839            "an engine's H.264 leads either way"
840        );
841        assert!(!fallback_codecs(Codec::Av1, &[Codec::Av1]).contains(&Codec::Av1));
842        // An engine's codecs go most efficient first, whatever the build encodes in software.
843        let engine = [Codec::H264, Codec::H265, Codec::Av1];
844        assert_eq!(
845            &fallback_codecs(Codec::Vp8, &engine)[..3],
846            &[Codec::Av1, Codec::H265, Codec::H264]
847        );
848    }
849
850    /// The probe of every software encoder prints nothing where the host sees it, though a
851    /// library announces itself on open (kvazaar's preset and SIMD banner, SVT-AV1's
852    /// configuration, x265's NUMA warnings in a container).
853    #[test]
854    fn the_software_probe_prints_nothing() {
855        if std::env::var_os("PIXELFLUX_PROBE_CHILD").is_some() {
856            for codec in Codec::VIDEO {
857                encodes_in_child(codec);
858            }
859            return;
860        }
861        let out = std::process::Command::new(std::env::current_exe().unwrap())
862            .args([
863                "--exact",
864                "encoders::tests::the_software_probe_prints_nothing",
865                "--nocapture",
866                "--test-threads=1",
867            ])
868            .env("PIXELFLUX_PROBE_CHILD", "1")
869            .output()
870            .unwrap();
871        assert!(out.status.success() && String::from_utf8_lossy(&out.stdout).contains("1 passed"));
872        assert_eq!(
873            String::from_utf8_lossy(&out.stderr),
874            "",
875            "the probes wrote to the host's stderr"
876        );
877    }
878
879    /// A capture start takes what its node's probe settled rather than opening the backend
880    /// again: node 99 has no render device, so it reads as NVENC's, which has no VP8 engine.
881    /// A device whose sessions other processes hold settles nothing, so the probe is asked
882    /// until it does before the capture starts.
883    #[test]
884    fn a_session_takes_the_answer_its_node_probe_settled() {
885        let settled = (0..300)
886            .find_map(|_| match probe_node(99) {
887                Err((_, e)) if e == nvenc::SESSIONS_TAKEN => {
888                    std::thread::sleep(std::time::Duration::from_millis(100));
889                    None
890                }
891                Err((backend, e)) => {
892                    Some(format!("{} VP8 did not open: {e}", backend.to_uppercase()))
893                }
894                Ok(_) => Some("render node 99 has no VP8 engine".to_string()),
895            })
896            .expect("the device had no NVENC session to spare for 30 s");
897        let report = crate::report::StreamReport::new("x11");
898        let _scope = crate::report::enter(&report);
899        let mut settings = CaptureSettings {
900            width: 256,
901            height: 128,
902            codec: Codec::Vp8,
903            encode_node_index: 99,
904            ..Default::default()
905        };
906        let encoder = select_frame_encoder(
907            &mut settings,
908            FrameSource::Host { rgba: false },
909            None,
910            "test",
911        );
912        assert!(
913            encoder.is_some_and(|enc| !enc.is_hardware()),
914            "VP8 comes up in software"
915        );
916        assert_eq!(report.info().encoder_reason, settled);
917    }
918
919    /// The software encoders that take a 4:4:4 session: x264 and x265, and libvpx for VP9 alone;
920    /// AV1 and VP8 encode 4:2:0 whatever is asked.
921    #[test]
922    fn software_fullcolor_follows_the_library() {
923        assert_eq!(
924            software_fullcolor(Codec::H264),
925            software_library(Codec::H264) == "x264"
926        );
927        assert_eq!(
928            software_fullcolor(Codec::H265),
929            software_library(Codec::H265) == "x265"
930        );
931        assert_eq!(
932            software_fullcolor(Codec::Vp9),
933            software_library(Codec::Vp9) == "libvpx" && vpx::encodes_444()
934        );
935        assert!(!software_fullcolor(Codec::Av1) && !software_fullcolor(Codec::Vp8));
936    }
937
938    /// A child's fate is the finding: a fatal signal, and only that, reads as not surviving.
939    #[test]
940    fn a_child_survives_unless_a_signal_takes_it() {
941        assert!(survives_in_child(|| {}));
942        assert!(survives_in_child(|| unsafe { libc::_exit(3) }));
943        assert!(!survives_in_child(|| unsafe {
944            libc::raise(libc::SIGILL);
945        }));
946    }
947
948    /// A probe forked while other threads open and close SVT-AV1 sessions still finds the
949    /// encoder. The threads beside it stop opening sessions while one is pending, as nothing
950    /// opens one beside the import-time probe: a child forked while they run between sessions
951    /// inherits locks their threads hold and no thread of its own releases, and runs out its
952    /// minute.
953    #[test]
954    fn the_av1_probe_survives_sessions_opening_beside_it() {
955        use std::sync::atomic::{AtomicBool, Ordering};
956        let settings = CaptureSettings {
957            width: 64,
958            height: 64,
959            target_fps: 30.0,
960            codec: Codec::Av1,
961            ..Default::default()
962        };
963        let stop = AtomicBool::new(false);
964        let probing = AtomicBool::new(false);
965        std::thread::scope(|s| {
966            for _ in 0..4 {
967                s.spawn(|| {
968                    while !stop.load(Ordering::Relaxed) {
969                        if probing.load(Ordering::Acquire) {
970                            std::thread::sleep(std::time::Duration::from_millis(1));
971                            continue;
972                        }
973                        drop(software_session(&settings, Codec::Av1, false));
974                    }
975                });
976            }
977            let found = (0..4).all(|_| {
978                probing.store(true, Ordering::Release);
979                let found = encodes_in_child(Codec::Av1);
980                probing.store(false, Ordering::Release);
981                found
982            });
983            stop.store(true, Ordering::Relaxed);
984            assert!(found);
985        });
986    }
987
988    /// A probe forked while other threads start and stop still finds the encoder: a thread the
989    /// child started would wait on the lock the standard library holds while it sets a thread
990    /// up, which another thread of the parent may have held at the fork.
991    #[test]
992    fn the_av1_probe_survives_threads_starting_beside_it() {
993        use std::sync::atomic::{AtomicBool, Ordering};
994        let stop = AtomicBool::new(false);
995        std::thread::scope(|s| {
996            for _ in 0..16 {
997                s.spawn(|| {
998                    while !stop.load(Ordering::Relaxed) {
999                        let _ = std::thread::spawn(|| {}).join();
1000                    }
1001                });
1002            }
1003            let found = (0..200).all(|_| encodes_in_child(Codec::Av1));
1004            stop.store(true, Ordering::Relaxed);
1005            assert!(found);
1006        });
1007    }
1008
1009    /// The build serves every video codec in software, JPEG never, and the GPL-free build
1010    /// names neither x264 nor x265.
1011    #[test]
1012    fn software_encoders_follow_the_build() {
1013        use rayon::prelude::*;
1014        let _: u32 = (0..8u32).into_par_iter().sum();
1015        let h264 = software_encoder(Codec::H264).expect("H.264 is always served");
1016        assert_eq!(
1017            h264.library,
1018            if cfg!(feature = "gpl") {
1019                "x264"
1020            } else {
1021                "openh264"
1022            }
1023        );
1024        assert_eq!(software_library(Codec::H264), h264.library);
1025        assert_eq!(software_library(Codec::Jpeg), "none");
1026        assert_eq!(software_encoder(Codec::Jpeg), None);
1027        assert_eq!(
1028            software_library(Codec::H265),
1029            if cfg!(feature = "gpl") {
1030                "x265"
1031            } else {
1032                "kvazaar"
1033            }
1034        );
1035        assert_eq!(software_library(Codec::Vp8), "libvpx");
1036        assert_eq!(software_library(Codec::Vp9), "libvpx");
1037        assert_eq!(software_library(Codec::Av1), "svt-av1");
1038        assert_eq!(software_fullcolor(Codec::H264), cfg!(feature = "gpl"));
1039        assert_eq!(software_fullcolor(Codec::H265), cfg!(feature = "gpl"));
1040        assert!(!software_fullcolor(Codec::Vp8) && !software_fullcolor(Codec::Av1));
1041        assert_eq!(software_fullcolor(Codec::Vp9), vpx::encodes_444());
1042    }
1043
1044    /// A session's range is the session's to report, not something read off whether its
1045    /// encoder is hardware. The two part on a 4:2:0 software H.264 session, which is
1046    /// limited range while the encoder is software, so a description taking the second for
1047    /// the first names a range the bitstream does not carry. All four pairings have a name
1048    /// of their own for that reason, rather than 4:2:0 falling through to one.
1049    #[test]
1050    fn a_session_reports_the_range_it_converted_at() {
1051        assert_eq!(colorspace_desc(true, true), "I444 (Full Range)");
1052        assert_eq!(colorspace_desc(true, false), "I444 (Limited Range)");
1053        assert_eq!(colorspace_desc(false, true), "I420 (Full Range)");
1054        assert_eq!(colorspace_desc(false, false), "I420 (Limited Range)");
1055
1056        let mut settings = CaptureSettings {
1057            codec: Codec::H264,
1058            video_fullcolor: false,
1059            ..Default::default()
1060        };
1061        assert!(
1062            !session_full_range(None, &settings),
1063            "software H.264 at 4:2:0 converts at limited range"
1064        );
1065        assert_eq!(
1066            colorspace_desc(
1067                session_fullcolor(None, &settings),
1068                session_full_range(None, &settings)
1069            ),
1070            "I420 (Limited Range)"
1071        );
1072
1073        settings.video_fullcolor = true;
1074        assert_eq!(
1075            session_full_range(None, &settings),
1076            software_fullcolor(Codec::H264),
1077            "software H.264 at 4:4:4 converts at full range where the build carries it"
1078        );
1079    }
1080}
1081
1082/// One full-frame encoder session, whichever backend produced it, so the render and delivery
1083/// code passes "the frame encoder" around without caring which vendor path or library
1084/// produced the frames.
1085#[allow(clippy::large_enum_variant)]
1086pub enum FrameEncoder {
1087    Nvenc(NvencEncoder),
1088    /// A VA-API session on a render node, hardware.
1089    Vaapi(vaapi::VaapiEncoder),
1090    /// libvpx VP8 or VP9, software.
1091    Vpx(vpx::VpxEncoder),
1092    /// x265 or kvazaar, software.
1093    Hevc(hevc::HevcEncoder),
1094    /// SVT-AV1, software.
1095    Av1(svtav1::SvtAv1Encoder),
1096    /// Tegra's encoder, reached through the vendor V4L2 library.
1097    #[cfg(target_arch = "aarch64")]
1098    Tegra(tegra::TegraEncoder),
1099    /// A stateful V4L2 M2M encoder, driven through the kernel interface directly.
1100    V4l2m2m(v4l2m2m::V4l2M2mEncoder),
1101}
1102
1103/// Apply one expression to the session behind whichever variant `self` is.
1104macro_rules! each {
1105    ($self:expr, $enc:ident => $e:expr) => {
1106        match $self {
1107            FrameEncoder::Nvenc($enc) => $e,
1108            FrameEncoder::Vaapi($enc) => $e,
1109            FrameEncoder::Vpx($enc) => $e,
1110            FrameEncoder::Hevc($enc) => $e,
1111            FrameEncoder::Av1($enc) => $e,
1112            #[cfg(target_arch = "aarch64")]
1113            FrameEncoder::Tegra($enc) => $e,
1114            FrameEncoder::V4l2m2m($enc) => $e,
1115        }
1116    };
1117}
1118
1119impl FrameEncoder {
1120    /// The codec the session emits.
1121    pub fn codec(&self) -> Codec {
1122        each!(self, enc => enc.codec())
1123    }
1124
1125    /// Whether the session encodes on a GPU or a hardware engine.
1126    pub fn is_hardware(&self) -> bool {
1127        !matches!(
1128            self,
1129            FrameEncoder::Vpx(_) | FrameEncoder::Hevc(_) | FrameEncoder::Av1(_)
1130        )
1131    }
1132
1133    /// The rate control the session runs whatever mode it was given, for a backend that has one
1134    /// alone: a V4L2 M2M device offers variable bitrate only, and Tegra's encoder is driven at a
1135    /// constant one. `None` where the session runs the mode it was given.
1136    pub fn fixed_rate_control(&self) -> Option<&'static str> {
1137        match self {
1138            #[cfg(target_arch = "aarch64")]
1139            FrameEncoder::Tegra(_) => Some("CBR"),
1140            FrameEncoder::V4l2m2m(_) => Some("VBR"),
1141            _ => None,
1142        }
1143    }
1144
1145    /// The backend as the logs name it: `NVENC`, `VAAPI`, or the software library.
1146    pub fn backend_name(&self) -> &'static str {
1147        match self {
1148            FrameEncoder::Nvenc(_) => "NVENC",
1149            FrameEncoder::Vaapi(_) => "VAAPI",
1150            FrameEncoder::Vpx(enc) => enc.library(),
1151            FrameEncoder::Hevc(enc) => enc.library(),
1152            FrameEncoder::Av1(enc) => enc.library(),
1153            #[cfg(target_arch = "aarch64")]
1154            FrameEncoder::Tegra(_) => "TEGRA",
1155            FrameEncoder::V4l2m2m(_) => "V4L2M2M",
1156        }
1157    }
1158
1159    /// The planes of one dmabuf the session imports as it stands: VA-API's descriptor maps up to
1160    /// four, each to the object it lies in (a compressed layout's metadata is a plane beside the
1161    /// pixels); NVENC imports the first plane through EGL, so a buffer of more is composited.
1162    pub fn dmabuf_planes(&self) -> usize {
1163        match self {
1164            FrameEncoder::Vaapi(_) => 4,
1165            _ => 1,
1166        }
1167    }
1168
1169    /// Whether the session negotiated 4:4:4 chroma.
1170    pub fn is_fullcolor(&self) -> bool {
1171        each!(self, enc => enc.is_fullcolor())
1172    }
1173
1174    /// The bits per sample the session negotiated: 10 on a VA-API session that opened a
1175    /// 10-bit profile and on a software one whose library codes them, 8 everywhere else.
1176    pub fn bit_depth(&self) -> u32 {
1177        match self {
1178            FrameEncoder::Nvenc(enc) => enc.bit_depth(),
1179            FrameEncoder::Vaapi(enc) => enc.bit_depth(),
1180            FrameEncoder::Vpx(enc) => enc.bit_depth(),
1181            FrameEncoder::Hevc(enc) => enc.bit_depth(),
1182            FrameEncoder::Av1(enc) => enc.bit_depth(),
1183            _ => 8,
1184        }
1185    }
1186
1187    /// Whether the session signals full range: a software 4:4:4 of x264's kind, and the V4L2
1188    /// M2M sessions whose firmware converts at full range and offers no way to ask for another.
1189    pub fn is_full_range(&self) -> bool {
1190        each!(self, enc => enc.is_full_range())
1191    }
1192
1193    /// Apply a live bitrate / VBV / frame-rate change. `Err` means the session lost its codec
1194    /// context and has to be rebuilt.
1195    pub fn reconfigure_rate(&mut self, settings: &CaptureSettings) -> Result<(), String> {
1196        match self {
1197            FrameEncoder::Nvenc(enc) => {
1198                enc.reconfigure_rate(settings);
1199                Ok(())
1200            }
1201            FrameEncoder::Vaapi(enc) => enc.reconfigure_rate(settings),
1202            FrameEncoder::Vpx(enc) => enc.reconfigure_rate(settings),
1203            FrameEncoder::Hevc(enc) => enc.reconfigure_rate(settings),
1204            FrameEncoder::Av1(enc) => enc.reconfigure_rate(settings),
1205            #[cfg(target_arch = "aarch64")]
1206            FrameEncoder::Tegra(enc) => enc.reconfigure_rate(settings),
1207            FrameEncoder::V4l2m2m(enc) => enc.reconfigure_rate(settings),
1208        }
1209    }
1210
1211    /// Encode one packed host frame (`stride` bytes per row; `rgba` names R,G,B,A byte order,
1212    /// otherwise B,G,R,A) at quantizer `qp`, as a key frame when `force_idr`.
1213    pub fn encode_host(
1214        &mut self,
1215        pixels: &[u8],
1216        stride: usize,
1217        rgba: bool,
1218        frame_number: u64,
1219        qp: u32,
1220        force_idr: bool,
1221    ) -> Result<Vec<u8>, String> {
1222        match self {
1223            FrameEncoder::Nvenc(enc) => {
1224                enc.encode_cpu_packed(pixels, stride, rgba, frame_number, qp, force_idr)
1225            }
1226            FrameEncoder::Vaapi(enc) => {
1227                enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1228            }
1229            FrameEncoder::Vpx(enc) => {
1230                enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1231            }
1232            FrameEncoder::Hevc(enc) => {
1233                enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1234            }
1235            FrameEncoder::Av1(enc) => {
1236                enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1237            }
1238            #[cfg(target_arch = "aarch64")]
1239            FrameEncoder::Tegra(enc) => {
1240                enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1241            }
1242            FrameEncoder::V4l2m2m(enc) => {
1243                enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1244            }
1245        }
1246    }
1247
1248    /// `encode_host` for rows a client owns and may unmap after the call, as the wl_shm buffer
1249    /// a Wayland capture encodes as it stands: NVENC uploads them without page-locking them
1250    /// (`NvencEncoder::encode_client_packed`), and every other session reads them as
1251    /// `encode_host` does.
1252    pub fn encode_client_host(
1253        &mut self,
1254        pixels: &[u8],
1255        stride: usize,
1256        rgba: bool,
1257        frame_number: u64,
1258        qp: u32,
1259        force_idr: bool,
1260    ) -> Result<Vec<u8>, String> {
1261        match self {
1262            FrameEncoder::Nvenc(enc) => {
1263                enc.encode_client_packed(pixels, stride, rgba, frame_number, qp, force_idr)
1264            }
1265            _ => self.encode_host(pixels, stride, rgba, frame_number, qp, force_idr),
1266        }
1267    }
1268
1269    /// Hash the next host frame's bands of `rows` rows where the encoder holds it, beside its
1270    /// encode (NVENC: `NvencEncoder::hash_next_upload`); false where it cannot, which leaves the
1271    /// hash to the caller.
1272    pub fn hash_next_upload(&mut self, rows: u32) -> bool {
1273        match self {
1274            FrameEncoder::Nvenc(enc) => enc.hash_next_upload(rows),
1275            _ => false,
1276        }
1277    }
1278
1279    /// The band hashes `hash_next_upload` asked for, once the frame is coded.
1280    pub fn take_upload_hashes(&mut self) -> Option<Vec<u64>> {
1281        match self {
1282            FrameEncoder::Nvenc(enc) => enc.take_upload_hashes(),
1283            _ => None,
1284        }
1285    }
1286
1287    /// The quality index (the H.26x quantizer scale `video_crf` uses) the last frame was coded at,
1288    /// where the session reports its quantizer: what a constant-rate session's picture is worth
1289    /// against the paint-over quality.
1290    pub fn last_quality(&self) -> Option<u32> {
1291        match self {
1292            FrameEncoder::Nvenc(enc) => enc.last_quality(),
1293            FrameEncoder::Vaapi(enc) => enc.last_quality(),
1294            FrameEncoder::Vpx(enc) => enc.last_quality(),
1295            FrameEncoder::Hevc(enc) => enc.last_quality(),
1296            FrameEncoder::Av1(enc) => enc.last_quality(),
1297            _ => None,
1298        }
1299    }
1300
1301    /// The bytes of the last frame the rate control coded, held frames aside, where the session
1302    /// reports them (NVENC, VA-API, x265 and kvazaar, libvpx's VP9): what tells a constant-rate
1303    /// cleanup that runs through the rate control whether it has converged on a still screen
1304    /// (`pipeline::decide_hw_fullframe`), with `last_quality` where the session reports that
1305    /// too.
1306    pub fn last_size(&self) -> Option<usize> {
1307        match self {
1308            FrameEncoder::Nvenc(enc) => enc.last_size(),
1309            FrameEncoder::Vaapi(enc) => enc.last_size(),
1310            FrameEncoder::Vpx(enc) => enc.last_size(),
1311            FrameEncoder::Hevc(enc) => enc.last_size(),
1312            _ => None,
1313        }
1314    }
1315
1316    /// Have the next frame's reconstruction measured against its source, where the session
1317    /// can (`last_psnr`).
1318    pub fn measure(&mut self) {
1319        if let FrameEncoder::Vaapi(enc) = self {
1320            enc.measure();
1321        }
1322    }
1323
1324    /// Whether `measure` has the session measure a frame: VA-API, which reads its
1325    /// reconstruction back, where the driver's surfaces let it.
1326    pub fn measures(&self) -> bool {
1327        match self {
1328            FrameEncoder::Vaapi(enc) => enc.measures(),
1329            _ => false,
1330        }
1331    }
1332
1333    /// The luma PSNR of the last frame measured (`measure`), where the session reads its
1334    /// reconstruction back: VA-API. What a constant-rate cleanup through the rate control
1335    /// ends on where it has one, since a picture that stopped improving needs no more frames
1336    /// whatever quantizer the driver reports.
1337    pub fn last_psnr(&self) -> Option<f32> {
1338        match self {
1339            FrameEncoder::Vaapi(enc) => enc.last_psnr(),
1340            _ => None,
1341        }
1342    }
1343
1344    /// Take the next frame back where it comes out past `bytes` and code it again within them:
1345    /// a band held at a quantizer (`hold_quantizer`) in a shorter band, a frame of the rate
1346    /// control's at the coarsest quantizer, refining nothing. NVENC, which leaves the frame out of
1347    /// the predictions as one a client lost, does. VA-API codes the frame at the rate that spends
1348    /// `bytes` in the cleanup's frame time instead (`follows_cap`); elsewhere the frame stands as
1349    /// coded.
1350    pub fn cap(&mut self, bytes: usize) {
1351        match self {
1352            FrameEncoder::Nvenc(enc) => enc.cap(bytes),
1353            FrameEncoder::Vaapi(enc) => enc.cap(bytes),
1354            FrameEncoder::Vpx(enc) => enc.cap(bytes),
1355            _ => {}
1356        }
1357    }
1358
1359    /// Whether a still screen's cleanup frame is coded at the rate its cap runs at (`cap`), so
1360    /// the cleanup judges it against that rate's budget: VA-API at a constant rate, which takes
1361    /// a new target with a picture, and libvpx's VP9, which takes one live. NVENC takes a frame
1362    /// back instead.
1363    pub fn follows_cap(&self) -> bool {
1364        match self {
1365            FrameEncoder::Vaapi(enc) => enc.follows_cap(),
1366            FrameEncoder::Vpx(enc) => enc.follows_cap(),
1367            _ => false,
1368        }
1369    }
1370
1371    /// The quantizer steps finer than the paint-over one at which the rate control's frames come
1372    /// to the paint-over quality, where a constant-rate cleanup runs through it: the software
1373    /// HEVC encoders' predicted frames at a quantizer leave the motion's coarse blocks as they
1374    /// are, and x265's cleanup ended at 45-46 dB at the paint-over quantizer where its key frame
1375    /// there decodes at 50 (`HevcEncoder::reach_finer`). Elsewhere none.
1376    pub fn reach_finer(&self) -> u32 {
1377        match self {
1378            FrameEncoder::Hevc(enc) => enc.reach_finer(),
1379            _ => 0,
1380        }
1381    }
1382
1383    /// Whether a constant-rate cleanup through the rate control ends on a key frame held at the
1384    /// paint-over quantizer after a large change and a longer stillness: where the rate control
1385    /// cannot refine a still screen in the frames' budget and the session says nothing of how far
1386    /// it got (`VaapiEncoder::ends_cleanup_on_key`). Elsewhere a refresh or the rate control's
1387    /// own frames do, and a key frame would only cost more (libvpx's VP9 key frame of 1080p text
1388    /// came out at 231 kB and 30.9 dB at its coarsest).
1389    pub fn ends_cleanup_on_key(&self) -> bool {
1390        match self {
1391            FrameEncoder::Vaapi(enc) => enc.ends_cleanup_on_key(),
1392            _ => false,
1393        }
1394    }
1395
1396    /// Where the last frame was taken back and coded again (`cap`): how far its held band then
1397    /// reached, or 0 for a frame of the rate control's.
1398    pub fn refit(&self) -> Option<f64> {
1399        match self {
1400            FrameEncoder::Nvenc(enc) => enc.refit(),
1401            _ => None,
1402        }
1403    }
1404
1405    /// Whether a frame left a held band out of every prediction with no band of its own to take
1406    /// it back into since this was last asked: NVENC, whose anchors predict from the newest frame
1407    /// every client was sent (`NvencEncoder::take_lost_cleanup`).
1408    pub fn take_lost_cleanup(&mut self) -> bool {
1409        match self {
1410            FrameEncoder::Nvenc(enc) => enc.take_lost_cleanup(),
1411            _ => false,
1412        }
1413    }
1414
1415    /// The bytes of the last frame held at a quantizer (0 before one), where the session holds a
1416    /// band of a frame at it (`hold_quantizer`): NVENC's H.264, HEVC and AV1 sessions, through a
1417    /// QP delta map, and libvpx's VP8, through a region-of-interest map.
1418    pub fn band_size(&self) -> Option<usize> {
1419        match self {
1420            FrameEncoder::Nvenc(enc) => enc.band_size(),
1421            FrameEncoder::Vpx(enc) => enc.band_size(),
1422            _ => None,
1423        }
1424    }
1425
1426    /// How the session refines blocks of a frame of its rate control's at a quality of their
1427    /// own (`refine_blocks`, `ledger::Refiner`), so a refined block comes to what the region's
1428    /// own cleanup reaches; `None` where it does not: NVENC's H.264, HEVC, and AV1 through the
1429    /// QP delta map (`nvenc::REFINE_FINER`), libvpx's VP8 through its region-of-interest map
1430    /// (`vpx::REFINE_FINER`, which says why not VP9), x265 through its per-block quantizer
1431    /// offsets and kvazaar through its per-tree-block map (`HevcEncoder::refine_finer`). x264's
1432    /// session refines within `software::encode_cpu` (`ledger::REFINE_FINER`).
1433    pub fn refines(&self) -> Option<crate::ledger::Refiner> {
1434        use crate::ledger::Refiner;
1435        match self {
1436            FrameEncoder::Nvenc(enc) => {
1437                enc.refines().then_some(Refiner::finer(nvenc::REFINE_FINER))
1438            }
1439            FrameEncoder::Vpx(enc) => {
1440                (enc.codec() == Codec::Vp8).then_some(Refiner::finer(vpx::REFINE_FINER))
1441            }
1442            FrameEncoder::Hevc(enc) => enc.refines().then(|| Refiner::finer(enc.refine_finer())),
1443            FrameEncoder::Vaapi(enc) => enc.refines(),
1444            _ => None,
1445        }
1446    }
1447
1448    /// Mark the blocks of the next frame, in raster order, that held still and are clean
1449    /// (`BlockLedger::constant`), which a session codes as skips without analysis where its
1450    /// library takes such a map and the frame predicts from the one before: libvpx's VP8 and
1451    /// VP9 through their active map, x265 through its constant-block map, and SVT-AV1 through
1452    /// the map its input copy reads. An empty slice marks none, and a session takes the map
1453    /// only on a frame of its rate control's, never on a key frame or a held one.
1454    pub fn constant_blocks(&mut self, constant: &[bool]) {
1455        match self {
1456            FrameEncoder::Vpx(enc) => enc.constant_blocks(constant),
1457            FrameEncoder::Hevc(enc) => enc.constant_blocks(constant),
1458            FrameEncoder::Av1(enc) => enc.constant_blocks(constant),
1459            _ => {}
1460        }
1461    }
1462
1463    /// Code the blocks `refinement` names in the next frame, where it is the rate control's, at
1464    /// its target quality (`ledger`), where `refines`.
1465    pub fn refine_blocks(&mut self, refinement: &crate::ledger::Refinement) {
1466        match self {
1467            FrameEncoder::Nvenc(enc) => enc.refine_blocks(refinement),
1468            FrameEncoder::Vpx(enc) => enc.refine_blocks(refinement),
1469            FrameEncoder::Hevc(enc) => enc.refine_blocks(refinement),
1470            FrameEncoder::Vaapi(enc) => enc.refine_blocks(refinement),
1471            _ => {}
1472        }
1473    }
1474
1475    /// Whether `hold_quantizer` holds a frame at the quantizer asked for under the session's rate
1476    /// control: NVENC, libvpx's VP8, and SVT-AV1 at a constant rate (where the release takes a
1477    /// new target with a picture) do; x265, VA-API, and libvpx's VP9 only at a constant
1478    /// quantizer (their `holds_quantizer` and `hold_quantizer` say why not at a constant rate,
1479    /// VP9's that its rate control refines a still screen within the rate); kvazaar, Tegra,
1480    /// and a stateful V4L2 device take no quantizer from the caller. The cleanup of a
1481    /// constant-quality session moves the session's quality instead of holding a frame
1482    /// (`pipeline::decide_constant_quality`), so this is read at a constant rate.
1483    pub fn holds_quantizer(&self) -> bool {
1484        match self {
1485            FrameEncoder::Nvenc(_) => true,
1486            FrameEncoder::Vpx(enc) => enc.holds_quantizer(),
1487            FrameEncoder::Vaapi(enc) => !enc.is_cbr(),
1488            FrameEncoder::Hevc(enc) => enc.holds_quantizer(),
1489            FrameEncoder::Av1(enc) => enc.holds_quantizer(),
1490            #[cfg(target_arch = "aarch64")]
1491            FrameEncoder::Tegra(_) => false,
1492            FrameEncoder::V4l2m2m(_) => false,
1493        }
1494    }
1495
1496    /// Whether the session runs its constant quality under the peak it was asked for: NVENC,
1497    /// libvpx, and SVT-AV1 where asked, SVT-AV1 where the release forces a picture's quantizer
1498    /// and keeps the sequence's otherwise (`svtav1_sequence_qp`); x265, kvazaar, and VA-API never,
1499    /// and the V4L2 and Tegra devices run at the rate whatever the mode (their module docblocks).
1500    pub fn capped(&self) -> bool {
1501        match self {
1502            FrameEncoder::Nvenc(enc) => enc.capped(),
1503            FrameEncoder::Vpx(enc) => enc.capped(),
1504            FrameEncoder::Av1(enc) => enc.capped(),
1505            FrameEncoder::Vaapi(_) | FrameEncoder::Hevc(_) => false,
1506            #[cfg(target_arch = "aarch64")]
1507            FrameEncoder::Tegra(_) => false,
1508            FrameEncoder::V4l2m2m(_) => false,
1509        }
1510    }
1511
1512    /// Whether a rate binds the session: a constant rate, or a constant quality whose peak bound
1513    /// its last frame (NVENC through its constant rate, libvpx above its pinned quantizer, SVT-AV1
1514    /// at a quantizer the peak raised), each
1515    /// of which codes a frame coarser than its quality, so its refinement and block ledger follow
1516    /// what its rate control coded (`pipeline::plan_refinement`). Under a peak that does not bind,
1517    /// a capped session refines as its uncapped quality does.
1518    pub fn rate_bound(&self, cbr: bool) -> bool {
1519        cbr || match self {
1520            FrameEncoder::Nvenc(enc) => enc.peak_bound(),
1521            FrameEncoder::Vpx(enc) => enc.peak_bound(),
1522            FrameEncoder::Av1(enc) => enc.peak_bound(),
1523            _ => false,
1524        }
1525    }
1526
1527    /// Whether the frame just coded was coded under a capped constant quality's binding peak,
1528    /// coarser than the rate factor would have chosen (`ledger::BlockLedger::coded`): NVENC
1529    /// through its constant rate, libvpx above its pinned quantizer, SVT-AV1 at a quantizer the
1530    /// peak raised.
1531    pub fn coded_capped(&self) -> bool {
1532        match self {
1533            FrameEncoder::Nvenc(enc) => enc.coded_capped(),
1534            FrameEncoder::Vpx(enc) => enc.coded_capped(),
1535            FrameEncoder::Av1(enc) => enc.coded_capped(),
1536            _ => false,
1537        }
1538    }
1539
1540    /// A tick that codes no frame: a capped constant quality's peak drains for it as for an
1541    /// empty frame, so a still screen lets the peak go and its refresh starts
1542    /// (`ledger::BlockLedger::refresh`).
1543    pub fn idle(&mut self) {
1544        match self {
1545            FrameEncoder::Nvenc(enc) => enc.idle(),
1546            FrameEncoder::Vpx(enc) => enc.idle(),
1547            FrameEncoder::Av1(enc) => enc.idle(),
1548            _ => {}
1549        }
1550    }
1551
1552    /// Hold the blocks `plan` names as skips in the next frame: the coarse blocks a capped
1553    /// constant quality's refresh leaves for later frames (`ledger::BlockLedger::refresh`).
1554    /// NVENC, which takes no skips, holds them at its coarsest quantizer through its QP delta
1555    /// map; an encoder that takes skips (`constant_blocks`) has them among those.
1556    pub fn defer_blocks(&mut self, plan: Option<&crate::ledger::BlockPlan>) {
1557        if let FrameEncoder::Nvenc(enc) = self {
1558            enc.defer_blocks(plan);
1559        }
1560    }
1561
1562    /// Whether a change of the session's constant quality re-opens it, so the frame it applies
1563    /// to is a key frame: x265 and kvazaar take no new quality while they run, and SVT-AV1 none
1564    /// until a capped session's peak binds (`SvtAv1Encoder::reopens_on_quality`). The cleanup of a
1565    /// constant-quality session counts such a refresh as its key frame.
1566    pub fn reopens_on_quality(&self) -> bool {
1567        match self {
1568            FrameEncoder::Hevc(_) => true,
1569            FrameEncoder::Av1(enc) => enc.reopens_on_quality(),
1570            _ => false,
1571        }
1572    }
1573
1574    /// Whether a constant-rate cleanup ends on a key frame after a large change and a longer
1575    /// stillness (`pipeline::decide_hw_fullframe`). SVT-AV1 holds only a key frame, so that is its
1576    /// cleanup. libvpx's VP8 holds the refresh before it, predicted frames that restore the
1577    /// picture whole (in bands where the rate leaves room, `VpxEncoder::band_size`), and its key
1578    /// frame only sent the picture again: 1080p text at 0.25 Mbit/s
1579    /// went quiet on 1.3 MB without it against 2.7, at the same 42 dB, since the key frame came
1580    /// out capped coarser than the picture (216 kB at 21.6 dB) and a second refresh restored it.
1581    pub fn cleans_up_with_key(&self) -> bool {
1582        !matches!(self, FrameEncoder::Vpx(enc) if enc.codec() == Codec::Vp8)
1583    }
1584
1585    /// Encode the next frame at the quantizer the quality index `crf` selects whatever the rate
1586    /// control, and leave the session's own rate control and quality as they were for the frame
1587    /// after: the cleanup of a still screen, where `holds_quantizer`. A session whose engine takes
1588    /// no quantizer from the caller (Tegra, a stateful V4L2 device) codes that frame under its own
1589    /// rate control. `band`, the share of the picture from and to in raster order, confines the
1590    /// quantizer to that band where `band_size` says the session holds one, the rest of the frame
1591    /// held at the coarsest quantizer.
1592    pub fn hold_quantizer(&mut self, crf: u32, band: Option<(f64, f64)>) {
1593        match self {
1594            FrameEncoder::Nvenc(enc) => enc.hold_quantizer(crf, band),
1595            FrameEncoder::Vaapi(enc) => enc.hold_quantizer(crf),
1596            FrameEncoder::Vpx(enc) => enc.hold_quantizer(crf, band),
1597            FrameEncoder::Hevc(enc) => enc.hold_quantizer(crf),
1598            FrameEncoder::Av1(enc) => enc.hold_quantizer(crf),
1599            #[cfg(target_arch = "aarch64")]
1600            FrameEncoder::Tegra(_) => {}
1601            FrameEncoder::V4l2m2m(_) => {}
1602        }
1603    }
1604
1605    /// The data one call returned, cut at the units it carries, each with its own frame's id and
1606    /// reference: whole and labeled `encoded` from a session that hands back the frame it
1607    /// encoded, one unit per earlier frame from one that hands them back late (Tegra), so what a
1608    /// consumer reports lost is the unit it dropped and nothing else.
1609    pub fn delivered_units(
1610        &self,
1611        data: Vec<u8>,
1612        encoded: u16,
1613    ) -> Vec<(Vec<u8>, u16, reference::Reference)> {
1614        #[cfg(target_arch = "aarch64")]
1615        if let FrameEncoder::Tegra(enc) = self {
1616            match enc.delivered_units() {
1617                [] => {}
1618                &[(id, reference, _)] => return vec![(data, id, reference)],
1619                units => {
1620                    let mut start = 0;
1621                    return units
1622                        .iter()
1623                        .map(|&(id, reference, end)| {
1624                            let unit = data[start..end].to_vec();
1625                            start = end;
1626                            (unit, id, reference)
1627                        })
1628                        .collect();
1629                }
1630            }
1631        }
1632        vec![(data, encoded, self.last_reference())]
1633    }
1634
1635    /// The frame the last delivered frame predicted from.
1636    /// The access unit a still screen leaves inside an engine that emits a frame only once the
1637    /// next is queued: empty from every other backend, whose units come with their frame.
1638    #[cfg_attr(not(target_arch = "aarch64"), allow(unused_variables))]
1639    pub fn push_held(&mut self, frame_number: u64) -> Result<Vec<u8>, String> {
1640        match self {
1641            #[cfg(target_arch = "aarch64")]
1642            FrameEncoder::Tegra(enc) => enc.push_held(frame_number),
1643            _ => Ok(Vec::new()),
1644        }
1645    }
1646
1647    pub fn holds_frame(&self) -> bool {
1648        match self {
1649            #[cfg(target_arch = "aarch64")]
1650            FrameEncoder::Tegra(enc) => enc.holds_frame(),
1651            _ => false,
1652        }
1653    }
1654
1655    pub fn last_reference(&self) -> reference::Reference {
1656        each!(self, enc => enc.last_reference())
1657    }
1658
1659    /// Leave frame `frame_id` and every frame after it out of the predictions, so the next
1660    /// frame decodes for a client that lost it. False when the session cannot, and the caller
1661    /// codes a key frame instead.
1662    pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
1663        each!(self, enc => enc.invalidate_reference(frame_id))
1664    }
1665
1666    /// Apply what the consumers say of a frame: a loss as `invalidate_reference`, a frame every
1667    /// one of them holds or was sent to a session keeping long-term references
1668    /// (`ReferenceWindow::acknowledge`). False where the session codes a key frame instead.
1669    pub fn take_report(&mut self, report: reference::ReferenceReport) -> bool {
1670        match (self, report) {
1671            (FrameEncoder::Nvenc(enc), report) => enc.take_report(report),
1672            (FrameEncoder::Vaapi(enc), report) => enc.take_report(report),
1673            (FrameEncoder::Vpx(enc), reference::ReferenceReport::Held(id)) => {
1674                enc.acknowledge_reference(id, true);
1675                true
1676            }
1677            (FrameEncoder::Vpx(enc), reference::ReferenceReport::Sent(id)) => {
1678                enc.acknowledge_reference(id, false);
1679                true
1680            }
1681            (FrameEncoder::Av1(enc), reference::ReferenceReport::Held(id)) => {
1682                enc.acknowledge_reference(id, true);
1683                true
1684            }
1685            (FrameEncoder::Av1(enc), reference::ReferenceReport::Sent(id)) => {
1686                enc.acknowledge_reference(id, false);
1687                true
1688            }
1689            (
1690                enc,
1691                reference::ReferenceReport::Lost(frame_id)
1692                | reference::ReferenceReport::Withheld(frame_id),
1693            ) => enc.invalidate_reference(frame_id),
1694            _ => true,
1695        }
1696    }
1697
1698    /// Encode one Wayland dmabuf in place (a zero-copy session).
1699    pub fn encode_dmabuf(
1700        &mut self,
1701        dmabuf: &Dmabuf,
1702        frame_number: u64,
1703        qp: u32,
1704        force_idr: bool,
1705    ) -> Result<Vec<u8>, String> {
1706        match self {
1707            FrameEncoder::Nvenc(enc) => enc.encode(dmabuf, frame_number, qp, force_idr),
1708            FrameEncoder::Vaapi(enc) => enc.encode_dmabuf(dmabuf, frame_number, qp, force_idr),
1709            _ => Err(format!(
1710                "the {} session takes host frames",
1711                self.backend_name()
1712            )),
1713        }
1714    }
1715
1716    /// `encode_dmabuf`, for a buffer the X server signals the session's blit semaphore after
1717    /// blitting into (`blit_semaphore_fd`): the frame's GPU work waits on it on the GPU.
1718    pub fn encode_dmabuf_after_blit(
1719        &mut self,
1720        dmabuf: &Dmabuf,
1721        frame_number: u64,
1722        qp: u32,
1723        force_idr: bool,
1724    ) -> Result<Vec<u8>, String> {
1725        match self {
1726            FrameEncoder::Nvenc(enc) => enc.encode_after_blit(dmabuf, frame_number, qp, force_idr),
1727            _ => Err(format!(
1728                "the {} session waits on no blit semaphore",
1729                self.backend_name()
1730            )),
1731        }
1732    }
1733
1734    /// A semaphore for the X server to signal after each blit, as the fd it imports, for
1735    /// `encode_dmabuf_after_blit` to wait on; None for a session that cannot wait on one (NVENC
1736    /// alone can, through CUDA).
1737    pub fn blit_semaphore_fd(&mut self) -> Option<Result<std::os::fd::OwnedFd, String>> {
1738        match self {
1739            FrameEncoder::Nvenc(enc) => Some(enc.blit_semaphore_fd()),
1740            _ => None,
1741        }
1742    }
1743}
1744
1745/// The full-frame software session of `codec` on host frames in the byte order `rgba` names.
1746pub fn software_session(
1747    settings: &CaptureSettings,
1748    codec: Codec,
1749    rgba: bool,
1750) -> Result<FrameEncoder, String> {
1751    Ok(match codec {
1752        Codec::Vp8 | Codec::Vp9 => FrameEncoder::Vpx(vpx::VpxEncoder::new(settings, codec, rgba)?),
1753        Codec::H265 => FrameEncoder::Hevc(hevc::HevcEncoder::new(settings, rgba)?),
1754        Codec::Av1 => FrameEncoder::Av1(svtav1::SvtAv1Encoder::new(settings, rgba)?),
1755        Codec::H264 | Codec::Jpeg => {
1756            return Err(format!("{} takes the CPU path", codec.display()));
1757        }
1758    })
1759}
1760
1761/// The chroma sampling of a session as the logs name it.
1762pub fn chroma_name(fullcolor: bool) -> &'static str {
1763    if fullcolor { "4:4:4" } else { "4:2:0" }
1764}
1765
1766/// The kernel driver's name out of the sysfs link `get_gpu_driver` read, `unknown` without one.
1767pub fn driver_name(driver: &str) -> &str {
1768    match driver.rsplit('/').next() {
1769        Some(name) if !name.is_empty() => name,
1770        _ => "unknown",
1771    }
1772}
1773
1774/// How a session's frames arrive: Wayland dmabufs (with the EGL display NVENC imports them
1775/// through), or packed host pixels in R,G,B,A (`rgba`) or B,G,R,A byte order.
1776#[derive(Clone, Copy)]
1777pub enum FrameSource {
1778    Dmabuf { egl_display: *const c_void },
1779    Host { rgba: bool },
1780}
1781
1782/// How many threads a software session encodes on, for its `Encoder:` line.
1783fn threads_desc(threads: i32) -> String {
1784    if threads == 1 {
1785        "1 thread".to_string()
1786    } else {
1787        format!("{threads} threads")
1788    }
1789}
1790
1791/// Choose and build the full-frame encoder of a session, or `None` when the CPU path
1792/// (`software::encode_cpu`) serves it: JPEG, or H.264 through the build's software encoder. One ladder for X11, Wayland zero-copy, and Wayland readback, so the three
1793/// cannot pick differently for the same settings:
1794///
1795/// 1. Unless software encoding is forced (`use_cpu`, or encode node `-1`), the hardware
1796///    backend the encode node's driver selects — NVENC on the NVIDIA driver, VA-API otherwise,
1797///    and on a Jetson the vendor V4L2 encoder before either, since that board publishes no
1798///    render node driver to select on. A compatible NVENC session handed over in `prior` is
1799///    reconfigured in place instead of rebuilt. A hardware refusal is logged and falls through.
1800/// 2. A dmabuf source stops here: software cannot read dmabufs, and the caller's readback path
1801///    then runs this ladder again with host frames.
1802/// 3. The software encoder of the codec, except JPEG and H.264, whose software path is the CPU
1803///    path's.
1804/// 4. Where the codec has no path at all, the other video codecs this host serves, the encode
1805///    node's hardware ones before the build's software ones, software H.264 among the latter,
1806///    and JPEG only past all of them. `settings.codec` names what came up.
1807pub fn select_frame_encoder(
1808    settings: &mut CaptureSettings,
1809    source: FrameSource,
1810    prior: Option<FrameEncoder>,
1811    tag: &str,
1812) -> Option<FrameEncoder> {
1813    let requested = settings.codec;
1814    if !requested.is_video() {
1815        return None;
1816    }
1817    if let Some(enc) = select_for_codec(settings, source, prior, tag) {
1818        return Some(enc);
1819    }
1820    // A dmabuf source stops here, its readback path running the ladder again on host frames,
1821    // and H.264 has come up on the CPU path the caller encodes itself.
1822    let FrameSource::Host { .. } = source else {
1823        return None;
1824    };
1825    if requested == Codec::H264 {
1826        return None;
1827    }
1828    let hardware: Vec<Codec> = if settings.use_cpu || settings.encode_node_index == -1 {
1829        Vec::new()
1830    } else {
1831        hardware_encoders(settings.encode_node_index)
1832            .into_iter()
1833            .map(|(codec, ..)| codec)
1834            .collect()
1835    };
1836    eprintln!(
1837        "[{tag}] No {} path on this host; trying the video codecs it serves.",
1838        requested.display()
1839    );
1840    for codec in fallback_codecs(requested, &hardware) {
1841        settings.codec = codec;
1842        if let Some(enc) = select_for_codec(settings, source, None, tag) {
1843            return Some(enc);
1844        }
1845        // Software H.264 is the caller's own path.
1846        if codec == Codec::H264 && software_encoder(Codec::H264).is_some() {
1847            return None;
1848        }
1849    }
1850    eprintln!("[{tag}] No video encoder on this host. Encoding JPEG instead.");
1851    settings.codec = Codec::Jpeg;
1852    None
1853}
1854
1855/// The video codecs by compression efficiency: the order a fallthrough tries the ones an engine
1856/// on the encode node carries, where CPU is no constraint.
1857const HARDWARE_ORDER: [Codec; 5] = [Codec::Av1, Codec::H265, Codec::Vp9, Codec::H264, Codec::Vp8];
1858
1859/// The video codecs by the measured time per frame of their software encoders, x264 to libvpx
1860/// VP9: the order a fallthrough tries the ones the build encodes in software.
1861const SOFTWARE_ORDER: [Codec; 5] = [Codec::H264, Codec::Av1, Codec::Vp8, Codec::H265, Codec::Vp9];
1862
1863/// The video codecs a capture falls through to where the one it asked for has no path on this
1864/// host: those an engine on the encode node carries, named in `hardware`, in `HARDWARE_ORDER`,
1865/// then those the build encodes in software, in `SOFTWARE_ORDER`.
1866fn fallback_codecs(requested: Codec, hardware: &[Codec]) -> Vec<Codec> {
1867    let mut codecs: Vec<Codec> = HARDWARE_ORDER
1868        .iter()
1869        .copied()
1870        .filter(|&codec| codec != requested && hardware.contains(&codec))
1871        .collect();
1872    codecs.extend(SOFTWARE_ORDER.iter().copied().filter(|&codec| {
1873        codec != requested && !hardware.contains(&codec) && software_encoder(codec).is_some()
1874    }));
1875    codecs
1876}
1877
1878/// Whether a 10-bit request is the software encoder's to serve: the node's engine encodes the
1879/// codec without 10 bits at the chroma it would run, and the build's software encoder codes
1880/// them, so the session takes the software path as a 4:4:4 the engine lacks does rather than
1881/// streaming 8 bits on hardware.
1882fn hardware_lacks_ten_bit(served: &HardwareEncoders, settings: &CaptureSettings) -> bool {
1883    let codec = settings.codec;
1884    if settings.video_bit_depth < 10 || !software_ten_bit(codec) {
1885        return false;
1886    }
1887    served
1888        .iter()
1889        .find(|&&(c, ..)| c == codec)
1890        .is_some_and(|&(_, _, formats)| {
1891            let fullcolor = settings.video_fullcolor && codec.fullcolor() && formats.fullcolor;
1892            !formats.ten_bit[usize::from(fullcolor)]
1893        })
1894}
1895
1896/// `hardware_lacks_ten_bit` for the encode node `settings` names, for a capture path that opens
1897/// its engine's session itself (NvFBC).
1898pub(crate) fn ten_bit_is_softwares(settings: &CaptureSettings) -> bool {
1899    probe_node(settings.encode_node_index.max(0))
1900        .is_ok_and(|served| hardware_lacks_ten_bit(&served, settings))
1901}
1902
1903/// Whether an engine may take the codec `settings` names at another size: software is not
1904/// forced, and the encode node's probe found an engine for it, at the depth asked, or found every
1905/// session taken. A session the ladder left in software where this holds was refused for
1906/// something a resize can change (an engine's maximum size), so it runs the ladder again.
1907pub(crate) fn engine_may_serve(settings: &CaptureSettings) -> bool {
1908    if settings.use_cpu || settings.encode_node_index == -1 {
1909        return false;
1910    }
1911    #[cfg(target_arch = "aarch64")]
1912    if tegra::available() {
1913        return tegra::served().contains(&settings.codec);
1914    }
1915    match probe_node(settings.encode_node_index) {
1916        Ok(served) => {
1917            served.iter().any(|&(c, ..)| c == settings.codec)
1918                && !hardware_lacks_ten_bit(&served, settings)
1919        }
1920        Err((_, e)) => e == nvenc::SESSIONS_TAKEN,
1921    }
1922}
1923
1924/// The ladder for the one codec `settings` names; `None` where no backend of it opened, and for
1925/// H.264 on host frames, whose software path the caller encodes itself.
1926fn select_for_codec(
1927    settings: &mut CaptureSettings,
1928    source: FrameSource,
1929    prior: Option<FrameEncoder>,
1930    tag: &str,
1931) -> Option<FrameEncoder> {
1932    let codec = settings.codec;
1933    let software_forced = settings.use_cpu || settings.encode_node_index == -1;
1934    #[cfg(target_arch = "aarch64")]
1935    if let (false, Some(_), FrameSource::Host { rgba }) =
1936        (software_forced, tegra::coded_fourcc(codec), source)
1937    {
1938        // Tegra publishes no render node driver to probe and carries no libnvidia-encode, so the
1939        // vendor library is the only way to its encoder and this step comes before both; a
1940        // codec it does not serve, or refuses, goes straight to software, past the two backends
1941        // the board lacks.
1942        if tegra::available() {
1943            drop(prior);
1944            if tegra::served().contains(&codec) {
1945                match tegra::TegraEncoder::new(codec, settings, rgba) {
1946                    Ok(enc) => {
1947                        println!(
1948                            "[{tag}] Encoder: TEGRA {} {} on the vendor V4L2 encoder.",
1949                            codec.display(),
1950                            chroma_name(enc.is_fullcolor())
1951                        );
1952                        return Some(FrameEncoder::Tegra(enc));
1953                    }
1954                    Err(e) => {
1955                        eprintln!("[{tag}] Failed to init the Tegra encoder: {e}");
1956                        crate::report::encoder_reason(&format!(
1957                            "Tegra {} did not open: {e}",
1958                            codec.display()
1959                        ));
1960                    }
1961                }
1962            } else {
1963                crate::report::encoder_reason(&format!(
1964                    "the Tegra encoder has no {} engine",
1965                    codec.display()
1966                ));
1967            }
1968            return software_fallback(settings, rgba, tag);
1969        }
1970    }
1971    if !software_forced {
1972        let node = settings.encode_node_index.max(0);
1973        let driver = crate::get_gpu_driver(node);
1974        crate::log::debug!("[{tag}] Encode node {node}, driver {driver}.");
1975        // What the node's probe settled holds for every capture start: a backend it could not
1976        // bring up, or a device with no engine for the codec, is not opened again.
1977        let settled = match probe_node(node) {
1978            Err((backend, e)) if e != nvenc::SESSIONS_TAKEN => Some(format!(
1979                "{} {} did not open: {e}",
1980                backend.to_uppercase(),
1981                codec.display()
1982            )),
1983            Ok(served)
1984                if !served.iter().any(|&(c, backend, ..)| {
1985                    c == codec && matches!(backend, "nvenc" | "vaapi")
1986                }) =>
1987            {
1988                Some(format!(
1989                    "render node {node} has no {} engine",
1990                    codec.display()
1991                ))
1992            }
1993            Ok(served) if hardware_lacks_ten_bit(&served, settings) => Some(format!(
1994                "render node {node} encodes no 10-bit {}, which the software encoder does",
1995                codec.display()
1996            )),
1997            _ => None,
1998        };
1999        if let Some(reason) = settled {
2000            drop(prior);
2001            crate::report::encoder_reason(&reason);
2002        } else if crate::driver_selects_nvenc(&driver) {
2003            if let Some(FrameEncoder::Nvenc(mut enc)) = prior {
2004                match enc.reconfigure_resolution(settings) {
2005                    Ok(resized) => {
2006                        if resized {
2007                            crate::log::debug!("[{tag}] NVENC session reconfigured in place.");
2008                        }
2009                        crate::report::hardware_encoder(
2010                            enc.device_name(),
2011                            driver_name(&driver),
2012                            node,
2013                        );
2014                        return Some(FrameEncoder::Nvenc(enc));
2015                    }
2016                    Err(e) => eprintln!(
2017                        "[{tag}] NVENC in-place reconfigure unavailable ({e}); rebuilding."
2018                    ),
2019                }
2020            }
2021            let egl_display = match source {
2022                FrameSource::Dmabuf { egl_display } => egl_display,
2023                FrameSource::Host { .. } => std::ptr::null(),
2024            };
2025            match NvencEncoder::new(settings, egl_display) {
2026                Ok(enc) => {
2027                    println!(
2028                        "[{tag}] Encoder: NVENC {} {} {}-bit on {} (render node {node}, {} driver), {}.",
2029                        codec.display(),
2030                        chroma_name(enc.is_fullcolor()),
2031                        enc.bit_depth(),
2032                        enc.device_name(),
2033                        driver_name(&driver),
2034                        enc.split_summary()
2035                    );
2036                    crate::report::hardware_encoder(enc.device_name(), driver_name(&driver), node);
2037                    return Some(FrameEncoder::Nvenc(enc));
2038                }
2039                Err(e) => {
2040                    eprintln!("[{tag}] Failed to init NVENC {}: {e}", codec.display());
2041                    crate::report::encoder_reason(&format!(
2042                        "NVENC {} did not open: {e}",
2043                        codec.display()
2044                    ));
2045                }
2046            }
2047        } else {
2048            // Nothing below reconfigures a session in place, so the previous one is released
2049            // here: a single-context M2M node refuses to open while its own last session is
2050            // alive, and a window resize would then demote a working hardware path.
2051            drop(prior);
2052            let input = match source {
2053                FrameSource::Dmabuf { .. } => vaapi::Input::Dmabuf,
2054                FrameSource::Host { rgba } => vaapi::Input::Host { rgba },
2055            };
2056            match vaapi::VaapiEncoder::new(settings, codec, input) {
2057                Ok(enc) => {
2058                    println!(
2059                        "[{tag}] Encoder: VAAPI {} {} {}-bit on {} surfaces (render node {node}, {} driver).",
2060                        codec.display(),
2061                        chroma_name(enc.is_fullcolor()),
2062                        enc.bit_depth(),
2063                        enc.surface_format_name(),
2064                        driver_name(&driver)
2065                    );
2066                    crate::report::hardware_encoder("", driver_name(&driver), node);
2067                    return Some(FrameEncoder::Vaapi(enc));
2068                }
2069                Err(e) => {
2070                    eprintln!("[{tag}] Failed to init VAAPI {}: {e}", codec.display());
2071                    crate::report::encoder_reason(&format!(
2072                        "VAAPI {} did not open: {e}",
2073                        codec.display()
2074                    ));
2075                }
2076            }
2077        }
2078        // A stateful M2M encoder publishes no render node driver to select on, so it is
2079        // reached only once the two backends that do have refused. The size is checked before
2080        // the node is opened: a refusal here falls through to software, a refusal later would
2081        // leave a session that came up and produces nothing.
2082        if let (Some(_), FrameSource::Host { rgba }) = (v4l2m2m::coded_fourcc(codec), source)
2083            && v4l2m2m::encodes(codec, settings.width, settings.height)
2084        {
2085            match v4l2m2m::V4l2M2mEncoder::new(codec, settings, rgba) {
2086                Ok(enc) => {
2087                    println!(
2088                        "[{tag}] Encoder: V4L2M2M {} {} on a stateful M2M node.",
2089                        codec.display(),
2090                        chroma_name(enc.is_fullcolor())
2091                    );
2092                    return Some(FrameEncoder::V4l2m2m(enc));
2093                }
2094                Err(e) => eprintln!("[{tag}] Failed to init the V4L2 M2M encoder: {e}"),
2095            }
2096        }
2097    } else {
2098        crate::log::debug!("[{tag}] Software encoding selected (use_cpu or encode_node_index -1).");
2099        crate::report::encoder_reason("software encoding selected");
2100    }
2101    let FrameSource::Host { rgba } = source else {
2102        return None;
2103    };
2104    software_fallback(settings, rgba, tag)
2105}
2106
2107/// The codec's software encoder; `None` for H.264, whose software path is the CPU path the
2108/// caller encodes itself, and where the build carries none for the codec, which the ladder then
2109/// falls through on.
2110fn software_fallback(
2111    settings: &mut CaptureSettings,
2112    rgba: bool,
2113    tag: &str,
2114) -> Option<FrameEncoder> {
2115    let codec = settings.codec;
2116    if codec == Codec::H264 {
2117        println!(
2118            "[{tag}] Encoder: software {} ({}) {} {}-bit, {}.",
2119            codec.display(),
2120            software_library(Codec::H264),
2121            chroma_name(session_fullcolor(None, settings)),
2122            session_bit_depth(None, settings),
2123            threads_desc(session::h264_fullframe_threads(settings))
2124        );
2125        return None;
2126    }
2127    let session = software_encoder(codec)
2128        .ok_or_else(|| {
2129            format!(
2130                "the {} encoder does not run on this machine",
2131                codec.display()
2132            )
2133        })
2134        .and_then(|_| software_session(settings, codec, rgba));
2135    match session {
2136        Ok(enc) => {
2137            println!(
2138                "[{tag}] Encoder: software {} ({}) {} {}-bit, {}.",
2139                codec.display(),
2140                enc.backend_name(),
2141                chroma_name(enc.is_fullcolor()),
2142                enc.bit_depth(),
2143                threads_desc(session::encode_threads(settings))
2144            );
2145            Some(enc)
2146        }
2147        Err(e) => {
2148            eprintln!("[{tag}] No {} encoder available: {e}.", codec.display());
2149            None
2150        }
2151    }
2152}
2153
2154#[cfg(test)]
2155mod software_tests {
2156    //! The full-frame software sessions, driven end to end through the one ladder step that
2157    //! builds them: every frame they emit is wire-framed for its codec and decodes back to the
2158    //! picture that went in, key frames come on request and are self-contained, the quantizer
2159    //! and the CBR target reach the encoder, a live rate or quality change keeps the stream
2160    //! decodable, and each stream declares the matrix it was converted with.
2161    use super::codec::{FRAME_DELTA, FRAME_KEY, WIRE_VIDEO, parse_video_type};
2162    use super::*;
2163    use crate::webcam::convert::I420View;
2164    use crate::webcam::decode::{ColorTags, Decoder, VideoDecoder};
2165    use std::cell::Cell;
2166    use std::sync::{Mutex, MutexGuard};
2167
2168    const W: usize = 320;
2169    const H: usize = 240;
2170
2171    /// The video codecs the software ladder serves whole-frame.
2172    const SOFTWARE: [Codec; 4] = [Codec::H265, Codec::Vp8, Codec::Vp9, Codec::Av1];
2173
2174    /// One AV1 session at a time across the suite: SVT-AV1 before 2.x faults while a second
2175    /// session is live, and the whole life of the session is the unsafe window. Every other
2176    /// codec stays parallel, and a thread already holding the turn keeps it, since one check
2177    /// holds two sessions at once.
2178    static ONE_AV1: Mutex<()> = Mutex::new(());
2179
2180    thread_local! {
2181        static AV1_DEPTH: Cell<u32> = const { Cell::new(0) };
2182    }
2183
2184    /// The AV1 turn, held for as long as the session it was taken for.
2185    pub(super) struct Turn {
2186        _guard: Option<MutexGuard<'static, ()>>,
2187        counted: bool,
2188    }
2189
2190    impl Drop for Turn {
2191        fn drop(&mut self) {
2192            if self.counted {
2193                AV1_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
2194            }
2195        }
2196    }
2197
2198    /// Take the turn for `codec`, which is a no-op for anything but AV1.
2199    pub(super) fn turn(codec: Codec) -> Turn {
2200        let counted = codec == Codec::Av1;
2201        let first = counted
2202            && AV1_DEPTH.with(|depth| {
2203                let held = depth.get();
2204                depth.set(held + 1);
2205                held == 0
2206            });
2207        Turn {
2208            _guard: first.then(|| ONE_AV1.lock().unwrap_or_else(|e| e.into_inner())),
2209            counted,
2210        }
2211    }
2212
2213    /// A software session holding the AV1 turn for as long as it lives.
2214    pub(super) struct Session {
2215        encoder: FrameEncoder,
2216        _turn: Turn,
2217    }
2218
2219    impl std::ops::Deref for Session {
2220        type Target = FrameEncoder;
2221        fn deref(&self) -> &FrameEncoder {
2222            &self.encoder
2223        }
2224    }
2225
2226    impl std::ops::DerefMut for Session {
2227        fn deref_mut(&mut self) -> &mut FrameEncoder {
2228            &mut self.encoder
2229        }
2230    }
2231
2232    pub(super) fn settings(codec: Codec) -> CaptureSettings {
2233        CaptureSettings {
2234            width: W as i32,
2235            height: H as i32,
2236            target_fps: 30.0,
2237            codec,
2238            video_crf: 25,
2239            use_cpu: true,
2240            ..Default::default()
2241        }
2242    }
2243
2244    pub(super) fn session(codec: Codec, s: &CaptureSettings, rgba: bool) -> Session {
2245        let _turn = turn(codec);
2246        let encoder = software_session(s, codec, rgba)
2247            .unwrap_or_else(|e| panic!("{codec:?} software session: {e}"));
2248        Session { encoder, _turn }
2249    }
2250
2251    /// Frames a fresh session takes before its first packet, zero where one frame in is one
2252    /// picture out. The wire ids and the latency budget both assume zero; SVT-AV1 gives that
2253    /// only from 2.3.0, where its packet call became blocking for low delay, and before it
2254    /// fills two frames that it neither reports nor lets a caller shorten.
2255    fn pipeline_depth(codec: Codec) -> usize {
2256        let s = settings(codec);
2257        let mut enc = session(codec, &s, false);
2258        for t in 0..8usize {
2259            let out = enc
2260                .encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2261                .unwrap_or_default();
2262            if !out.is_empty() {
2263                return t;
2264            }
2265        }
2266        panic!("{codec:?}: no packet after eight frames");
2267    }
2268
2269    /// The software codecs whose encoder answers each frame with that frame's own picture,
2270    /// which is what the checks below read a frame id back from. One that pipelines is named
2271    /// rather than skipped silently, since the delay is the session's latency as well as the
2272    /// test's.
2273    fn lockstep_codecs() -> Vec<Codec> {
2274        SOFTWARE
2275            .into_iter()
2276            .filter(|&codec| {
2277                let depth = pipeline_depth(codec);
2278                if depth > 0 {
2279                    println!("[pipeline] {codec:?}: {depth} frames deep, frame-id checks skipped");
2280                }
2281                depth == 0
2282            })
2283            .collect()
2284    }
2285
2286    /// A desktop-like BGRA frame: a diagonal gradient with a grid of dark glyph cells and a
2287    /// bright block that moves with `t`, so inter frames carry real motion.
2288    pub(super) fn frame(t: usize) -> Vec<u8> {
2289        let mut f = vec![0u8; W * H * 4];
2290        let (bx, by) = ((t * 9) % (W - 40), (t * 5) % (H - 30));
2291        for y in 0..H {
2292            for x in 0..W {
2293                let i = (y * W + x) * 4;
2294                let g = ((x * 255) / W) as u8;
2295                let cell = (x / 8 + y / 12) % 3 == 0 && x % 8 < 6 && y % 12 < 9;
2296                let (b, gr, r) = if x >= bx && x < bx + 40 && y >= by && y < by + 30 {
2297                    (40, 220, 250)
2298                } else if cell {
2299                    (30, 30, 30)
2300                } else {
2301                    (g, 200 - g / 2, 120)
2302                };
2303                f[i] = b;
2304                f[i + 1] = gr;
2305                f[i + 2] = r;
2306                f[i + 3] = 255;
2307            }
2308        }
2309        f
2310    }
2311
2312    /// Incompressible content, so rate control has to spend its whole budget.
2313    fn noise(t: usize) -> Vec<u8> {
2314        let mut f = vec![255u8; W * H * 4];
2315        let mut s = (t as u32).wrapping_mul(2654435761).wrapping_add(7);
2316        for px in f.as_chunks_mut::<4>().0 {
2317            s ^= s << 13;
2318            s ^= s >> 17;
2319            s ^= s << 5;
2320            px[0] = (s >> 24) as u8;
2321            px[1] = (s >> 16) as u8;
2322            px[2] = (s >> 8) as u8;
2323        }
2324        f
2325    }
2326
2327    /// Luma PSNR of a decoded frame against the BGRA source it came from, with the source's
2328    /// luma derived by the BT.709 limited-range formula the encoder's conversion uses.
2329    fn luma_psnr(decoded: &I420View<'_>, bgra: &[u8]) -> f64 {
2330        assert_eq!((decoded.width, decoded.height), (W, H));
2331        let mut mse = 0f64;
2332        for y in 0..H {
2333            for x in 0..W {
2334                let i = (y * W + x) * 4;
2335                let (b, g, r) = (
2336                    f64::from(bgra[i]),
2337                    f64::from(bgra[i + 1]),
2338                    f64::from(bgra[i + 2]),
2339                );
2340                let luma = 16.0 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219.0 / 255.0;
2341                let d = f64::from(decoded.y[y * decoded.y_stride + x]) - luma;
2342                mse += d * d;
2343            }
2344        }
2345        mse /= (W * H) as f64;
2346        if mse <= 0.0 {
2347            99.0
2348        } else {
2349            10.0 * (255.0 * 255.0 / mse).log10()
2350        }
2351    }
2352
2353    fn decode_one(dec: &mut VideoDecoder, packet: &[u8]) -> bool {
2354        dec.decode(&packet[VIDEO_HEADER_LEN..])
2355            .unwrap_or_else(|e| panic!("decode: {e:?}"))
2356    }
2357
2358    /// Whether the session takes a new bitrate without a key frame: libvpx, and SVT-AV1 where the
2359    /// release takes one with a picture. A new quantizer reaches libvpx alone live.
2360    fn takes_a_live_rate(enc: &FrameEncoder) -> bool {
2361        match enc.backend_name() {
2362            "libvpx" => true,
2363            "svt-av1" => codec_sys::svtav1::HAS_EVENTS,
2364            _ => false,
2365        }
2366    }
2367
2368    /// Every codec's frames carry its own wire id and kind, decode back to the source picture,
2369    /// and a key frame forced mid-stream starts a fresh decoder on its own.
2370    #[test]
2371    fn software_frames_decode_back_to_the_source() {
2372        for codec in lockstep_codecs() {
2373            let s = settings(codec);
2374            let mut enc = session(codec, &s, false);
2375            let mut dec = VideoDecoder::new(codec).expect("decoder");
2376            for t in 0..6usize {
2377                let src = frame(t);
2378                let out = enc
2379                    .encode_host(&src, W * 4, false, t as u64, 25, t == 0)
2380                    .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2381                assert!(out.len() > VIDEO_HEADER_LEN, "{codec:?} frame {t} is empty");
2382                assert_eq!(out[0], WIRE_VIDEO);
2383                let kind = if t == 0 { FRAME_KEY } else { FRAME_DELTA };
2384                assert_eq!(
2385                    parse_video_type(out[1]),
2386                    Some((codec, kind)),
2387                    "{codec:?} frame {t}"
2388                );
2389                assert_eq!(u16::from_be_bytes([out[2], out[3]]) as usize, t);
2390                assert_eq!(
2391                    &out[4..10],
2392                    &[0, 0, (W >> 8) as u8, W as u8, (H >> 8) as u8, H as u8]
2393                );
2394                assert!(
2395                    decode_one(&mut dec, &out),
2396                    "{codec:?} frame {t} decoded nothing"
2397                );
2398                let psnr = luma_psnr(&dec.frame().unwrap(), &src);
2399                assert!(psnr > 28.0, "{codec:?} frame {t}: luma PSNR {psnr:.1} dB");
2400            }
2401            let src = frame(6);
2402            let key = enc
2403                .encode_host(&src, W * 4, false, 6, 25, true)
2404                .expect("forced key");
2405            assert_eq!(
2406                parse_video_type(key[1]),
2407                Some((codec, FRAME_KEY)),
2408                "{codec:?} forced key"
2409            );
2410            let mut fresh = VideoDecoder::new(codec).expect("decoder");
2411            assert!(
2412                decode_one(&mut fresh, &key),
2413                "{codec:?}: a forced key frame must decode alone"
2414            );
2415            assert!(luma_psnr(&fresh.frame().unwrap(), &src) > 28.0);
2416            let next = enc
2417                .encode_host(&frame(7), W * 4, false, 7, 25, false)
2418                .expect("delta after key");
2419            assert_eq!(parse_video_type(next[1]), Some((codec, FRAME_DELTA)));
2420            assert!(decode_one(&mut fresh, &next));
2421        }
2422    }
2423
2424    /// A key frame asked for mid-stream comes back as one on every codec, through whatever
2425    /// pipeline the encoder keeps, carrying the id of the frame it was asked for, and a fresh
2426    /// decoder starts on it. SVT-AV1 before 2.0 re-opens for it and hands it back two frames
2427    /// later, as kvazaar does.
2428    #[test]
2429    fn a_key_frame_asked_for_mid_stream_starts_a_fresh_decoder() {
2430        for codec in SOFTWARE {
2431            let s = settings(codec);
2432            let mut enc = session(codec, &s, false);
2433            for t in 0..6usize {
2434                enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2435                    .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2436            }
2437            let mut out = enc
2438                .encode_host(&frame(6), W * 4, false, 6, 25, true)
2439                .expect("forced key");
2440            let mut t = 7usize;
2441            while out.get(1).and_then(|&kind| parse_video_type(kind)) != Some((codec, FRAME_KEY)) {
2442                assert!(
2443                    t < 22,
2444                    "{codec:?}: no key frame came back within fifteen frames of the request"
2445                );
2446                out = enc
2447                    .encode_host(&frame(t), W * 4, false, t as u64, 25, false)
2448                    .expect("delta");
2449                t += 1;
2450            }
2451            assert_eq!(
2452                u16::from_be_bytes([out[2], out[3]]),
2453                6,
2454                "{codec:?}: the key frame carries the id it was asked for"
2455            );
2456            let mut fresh = VideoDecoder::new(codec).expect("decoder");
2457            assert!(
2458                decode_one(&mut fresh, &out),
2459                "{codec:?}: the key frame must decode alone"
2460            );
2461            assert!(
2462                luma_psnr(&fresh.frame().unwrap(), &frame(6)) > 28.0,
2463                "{codec:?}"
2464            );
2465        }
2466    }
2467
2468    /// A session whose library keeps its references to itself says so instead of pretending:
2469    /// it names no reference on any frame and refuses the request, which is what leaves the
2470    /// caller a key frame to code. x265, kvazaar, and a constant-quality SVT-AV1 session offer
2471    /// nothing to steer; libvpx does, and its sessions track every frame.
2472    #[test]
2473    fn a_session_that_cannot_invalidate_names_no_reference() {
2474        use super::reference::Reference;
2475        for codec in [Codec::H265, Codec::Av1] {
2476            let s = settings(codec);
2477            let mut enc = session(codec, &s, false);
2478            for t in 0..4usize {
2479                enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2480                    .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2481                assert_eq!(
2482                    enc.last_reference(),
2483                    Reference::Untracked,
2484                    "{codec:?} frame {t}"
2485                );
2486            }
2487            assert!(
2488                !enc.invalidate_reference(2),
2489                "{codec:?}: the refusal is what asks for the key frame"
2490            );
2491        }
2492        for codec in [Codec::Vp8, Codec::Vp9] {
2493            let s = settings(codec);
2494            let mut enc = session(codec, &s, false);
2495            enc.encode_host(&frame(0), W * 4, false, 0, 25, true)
2496                .unwrap();
2497            assert_eq!(enc.last_reference(), Reference::None, "{codec:?}");
2498            enc.encode_host(&frame(1), W * 4, false, 1, 25, false)
2499                .unwrap();
2500            assert_eq!(enc.last_reference(), Reference::Frame(0), "{codec:?}");
2501            assert!(enc.invalidate_reference(1), "{codec:?}");
2502        }
2503    }
2504
2505    /// A constant-rate AV1 session names its references where the release takes reference
2506    /// commands: a frame a client lost is predicted past from an anchor, a decoder that never saw
2507    /// the lost frames decodes what follows exactly as one that saw everything, and a key frame
2508    /// asked for still comes. On an earlier release the session names none and refuses.
2509    #[test]
2510    fn a_constant_rate_av1_session_predicts_past_a_lost_frame() {
2511        use super::reference::Reference;
2512        let mut s = settings(Codec::Av1);
2513        s.video_cbr_mode = true;
2514        s.video_bitrate_kbps = 2000;
2515        let mut enc = session(Codec::Av1, &s, false);
2516        let mut frames: Vec<Vec<u8>> = (0..8usize)
2517            .map(|t| {
2518                enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2519                    .unwrap()
2520            })
2521            .collect();
2522        if !codec_sys::svtav1::HAS_EVENTS {
2523            assert_eq!(enc.last_reference(), Reference::Untracked);
2524            assert!(!enc.invalidate_reference(5));
2525            return;
2526        }
2527        assert_eq!(enc.last_reference(), Reference::Frame(6));
2528        // Frame 5 is reported lost once 6 and 7 have gone out: GOLDEN holds 6, so the next frame
2529        // predicts from ALTREF, the key frame.
2530        assert!(enc.invalidate_reference(5));
2531        for t in 8..10usize {
2532            frames.push(
2533                enc.encode_host(&frame(t), W * 4, false, t as u64, 25, false)
2534                    .unwrap(),
2535            );
2536            assert_eq!(
2537                parse_video_type(frames[t][1]),
2538                Some((Codec::Av1, FRAME_DELTA)),
2539                "frame {t}"
2540            );
2541            assert_eq!(
2542                enc.last_reference(),
2543                Reference::Frame(if t == 8 { 0 } else { 8 }),
2544                "frame {t}"
2545            );
2546        }
2547        let (mut whole, mut lossy) = (
2548            VideoDecoder::new(Codec::Av1).unwrap(),
2549            VideoDecoder::new(Codec::Av1).unwrap(),
2550        );
2551        for (i, f) in frames.iter().enumerate() {
2552            assert!(decode_one(&mut whole, f), "frame {i}");
2553            if !(5..8).contains(&i) {
2554                assert!(decode_one(&mut lossy, f), "frame {i} without frames 5-7");
2555            }
2556        }
2557        let (a, b) = (whole.frame().unwrap(), lossy.frame().unwrap());
2558        let same =
2559            a.y.chunks(a.y_stride)
2560                .zip(b.y.chunks(b.y_stride))
2561                .take(H)
2562                .all(|(x, y)| x[..W] == y[..W]);
2563        assert!(
2564            same,
2565            "the decoder that lost frames 5-7 shows frame 9 unlike the one that saw them"
2566        );
2567        let key = enc
2568            .encode_host(&frame(10), W * 4, false, 10, 25, true)
2569            .unwrap();
2570        assert_eq!(parse_video_type(key[1]), Some((Codec::Av1, FRAME_KEY)));
2571        assert_eq!(enc.last_reference(), Reference::None);
2572        assert!(
2573            decode_one(&mut VideoDecoder::new(Codec::Av1).unwrap(), &key),
2574            "the key frame decodes alone"
2575        );
2576    }
2577
2578    /// A constant-rate AV1 session predicting a frame from an anchor alone codes what the picture
2579    /// changed since the anchor: test_slow_page's scene at 1280x720, a 160-pixel bar moving 12
2580    /// pixels a frame over flat color, with the frame four back reported lost every 13, decodes
2581    /// within 20 levels of its source in every 64-pixel block above its noise, where preset 11
2582    /// left blocks of the bar standing where they were in the anchor, 41 off.
2583    #[test]
2584    fn an_anchor_prediction_leaves_no_block_as_it_stood_in_the_anchor() {
2585        use super::reference::ReferenceReport;
2586        if !codec_sys::svtav1::HAS_EVENTS {
2587            return;
2588        }
2589        let (w, h) = (1280usize, 720usize);
2590        let mut noise = vec![0u8; w * 240 * 4];
2591        let mut seed = 1u64;
2592        for b in noise.iter_mut() {
2593            seed ^= seed << 13;
2594            seed ^= seed >> 7;
2595            seed ^= seed << 17;
2596            *b = seed as u8;
2597        }
2598        let scene = |t: usize| {
2599            let bar = (t * 12) % (w - 160);
2600            let mut f = vec![0u8; w * h * 4];
2601            for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
2602                let (x, y) = (i % w, i / w);
2603                let bgr: [u8; 3] = if y >= 480 {
2604                    let n = ((t % 4) * w * 240 + (y - 480) * w + x) * 4 % noise.len();
2605                    [noise[n], noise[n + 1], noise[n + 2]]
2606                } else if (96..160).contains(&y) {
2607                    [if ((x + t * 4) / 16 + (y - 96) / 16).is_multiple_of(2) {
2608                        255
2609                    } else {
2610                        0
2611                    }; 3]
2612                } else if y >= 160 && (bar..bar + 160).contains(&x) {
2613                    [0x28, 0x3c, 0xdc]
2614                } else {
2615                    [0x78, 0x28, 0x1e]
2616                };
2617                px[..3].copy_from_slice(&bgr);
2618            }
2619            f
2620        };
2621        let mut s = settings(Codec::Av1);
2622        s.width = w as i32;
2623        s.height = h as i32;
2624        s.target_fps = 60.0;
2625        s.video_cbr_mode = true;
2626        s.video_bitrate_kbps = 8000;
2627        let mut enc = session(Codec::Av1, &s, false);
2628        let mut dec = VideoDecoder::new(Codec::Av1).unwrap();
2629        let (mut previous, mut anchored) = (None, 0);
2630        for t in 0..84usize {
2631            if t > 10 && t % 13 == 0 {
2632                assert!(enc.take_report(ReferenceReport::Lost(t as u16 - 4)));
2633            }
2634            let src = scene(t);
2635            let out = enc
2636                .encode_host(&src, w * 4, false, t as u64, 25, t == 0)
2637                .unwrap();
2638            let id = u16::from_be_bytes([out[2], out[3]]);
2639            let reference = u16::from_be_bytes([out[10], out[11]]);
2640            assert_eq!(id as usize, t);
2641            if reference != id && Some(reference) != previous {
2642                anchored += 1;
2643            }
2644            previous = Some(id);
2645            assert!(decode_one(&mut dec, &out), "frame {id}");
2646            let pic = dec.frame().unwrap();
2647            for (bx, by) in (0..480)
2648                .step_by(64)
2649                .flat_map(|y| (0..w).step_by(64).map(move |x| (x, y)))
2650            {
2651                let (mut sum, mut n) = (0f64, 0);
2652                for y in (by..(by + 64).min(480)).step_by(2) {
2653                    for x in (bx..bx + 64).step_by(2) {
2654                        let i = (y * w + x) * 4;
2655                        let (b, g, r) = (
2656                            f64::from(src[i]),
2657                            f64::from(src[i + 1]),
2658                            f64::from(src[i + 2]),
2659                        );
2660                        let luma = 16.0 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219.0 / 255.0;
2661                        sum += (f64::from(pic.y[y * pic.y_stride + x]) - luma).abs();
2662                        n += 1;
2663                    }
2664                }
2665                let off = sum / f64::from(n);
2666                assert!(
2667                    off <= 20.0,
2668                    "frame {id} (from {reference}): the block at ({bx}, {by}) is {off:.1} off its source"
2669                );
2670            }
2671        }
2672        assert!(anchored >= 5, "{anchored} frames predicted from an anchor");
2673    }
2674
2675    /// The byte order a session is built for reaches the conversion: a red picture handed as
2676    /// B,G,R,A and as R,G,B,A decodes to the same red on both.
2677    #[test]
2678    fn host_byte_order_is_honored() {
2679        for codec in lockstep_codecs() {
2680            let s = settings(codec);
2681            let mut means = Vec::new();
2682            for rgba in [false, true] {
2683                let mut px = [0u8; 4];
2684                if rgba {
2685                    px[0] = 220
2686                } else {
2687                    px[2] = 220
2688                }
2689                px[3] = 255;
2690                let src: Vec<u8> = px.repeat(W * H);
2691                let mut enc = session(codec, &s, rgba);
2692                let out = enc
2693                    .encode_host(&src, W * 4, rgba, 0, 20, true)
2694                    .expect("encode");
2695                let mut dec = VideoDecoder::new(codec).expect("decoder");
2696                assert!(decode_one(&mut dec, &out));
2697                let f = dec.frame().unwrap();
2698                let cw = f.chroma_width();
2699                let ch = f.chroma_height();
2700                let v: f64 = (0..ch)
2701                    .flat_map(|y| (0..cw).map(move |x| (x, y)))
2702                    .map(|(x, y)| f64::from(f.v[y * f.uv_stride + x]))
2703                    .sum::<f64>()
2704                    / (cw * ch) as f64;
2705                means.push(v);
2706            }
2707            assert!(
2708                means[0] > 180.0,
2709                "{codec:?}: red must land high in V, got {:.0}",
2710                means[0]
2711            );
2712            assert!(
2713                (means[0] - means[1]).abs() < 6.0,
2714                "{codec:?}: BGRA {:.0} vs RGBA {:.0}",
2715                means[0],
2716                means[1]
2717            );
2718        }
2719    }
2720
2721    /// A higher session quality index (a coarser quantizer) shrinks the stream, and CBR holds a
2722    /// noise stream near its bitrate target.
2723    #[test]
2724    fn quantizer_and_bitrate_reach_the_encoder() {
2725        for codec in SOFTWARE {
2726            let run = |crf: i32| -> usize {
2727                let mut s = settings(codec);
2728                s.video_crf = crf;
2729                let mut enc = session(codec, &s, false);
2730                (0..12usize)
2731                    .map(|t| {
2732                        enc.encode_host(&frame(t), W * 4, false, t as u64, crf as u32, t == 0)
2733                            .unwrap()
2734                            .len()
2735                    })
2736                    .sum()
2737            };
2738            let (fine, coarse) = (run(15), run(45));
2739            assert!(
2740                coarse * 2 < fine,
2741                "{codec:?}: crf 45 = {coarse} bytes vs crf 15 = {fine}"
2742            );
2743
2744            const KBPS: i32 = 800;
2745            let mut s = settings(codec);
2746            s.video_cbr_mode = true;
2747            s.video_bitrate_kbps = KBPS;
2748            let mut enc = session(codec, &s, false);
2749            let mut bytes = 0usize;
2750            for t in 0..90usize {
2751                let out = enc
2752                    .encode_host(&noise(t), W * 4, false, t as u64, 25, t == 0)
2753                    .unwrap();
2754                // A constant-rate encoder is free to answer a frame with nothing -- SVT-AV1
2755                // drops one rather than overshoot its buffer -- and that frame carries no
2756                // payload to count rather than a negative one.
2757                if t >= 30 && out.len() > VIDEO_HEADER_LEN {
2758                    bytes += out.len() - VIDEO_HEADER_LEN;
2759                }
2760            }
2761            let kbps = bytes as f64 * 8.0 * 30.0 / 60.0 / 1000.0;
2762            println!("{codec:?} CBR {KBPS} kbps on noise: {kbps:.0} kbps");
2763            assert!(
2764                kbps > f64::from(KBPS) * 0.5 && kbps < f64::from(KBPS) * 1.6,
2765                "{codec:?}: {kbps:.0} kbps"
2766            );
2767        }
2768    }
2769
2770    /// A quality increase applies at once and keeps the stream decodable, a decrease waits out
2771    /// the hysteresis where a change costs a re-open, and a frame-rate change keeps the stream
2772    /// decodable too. A library that takes the change live spends no key frame on it; one that
2773    /// has to re-open starts the fresh encoder with one.
2774    #[test]
2775    fn live_quality_and_rate_changes_keep_the_stream_decodable() {
2776        for codec in lockstep_codecs() {
2777            let mut s = settings(codec);
2778            s.video_crf = 40;
2779            let mut enc = session(codec, &s, false);
2780            let mut dec = VideoDecoder::new(codec).expect("decoder");
2781            let coarse = enc
2782                .encode_host(&frame(0), W * 4, false, 0, 40, true)
2783                .unwrap();
2784            assert!(decode_one(&mut dec, &coarse));
2785            let fine = enc
2786                .encode_host(&frame(1), W * 4, false, 1, 15, false)
2787                .unwrap();
2788            let live = enc.backend_name() == "libvpx";
2789            let kind = if live { FRAME_DELTA } else { FRAME_KEY };
2790            assert_eq!(
2791                parse_video_type(fine[1]),
2792                Some((codec, kind)),
2793                "{codec:?}: a quality change {}",
2794                if live {
2795                    "is live"
2796                } else {
2797                    "re-opens with a key frame"
2798                }
2799            );
2800            assert!(decode_one(&mut dec, &fine));
2801            let held = enc
2802                .encode_host(&frame(2), W * 4, false, 2, 40, false)
2803                .unwrap();
2804            assert_eq!(
2805                parse_video_type(held[1]),
2806                Some((codec, FRAME_DELTA)),
2807                "{codec:?}: a single decrease waits out the hysteresis"
2808            );
2809            assert!(decode_one(&mut dec, &held));
2810            s.target_fps = 15.0;
2811            enc.reconfigure_rate(&s).expect("rate reconfigure");
2812            let after = enc
2813                .encode_host(&frame(3), W * 4, false, 3, 15, false)
2814                .unwrap();
2815            let kind = if enc.backend_name() == "libvpx" {
2816                FRAME_DELTA
2817            } else {
2818                FRAME_KEY
2819            };
2820            assert_eq!(
2821                parse_video_type(after[1]),
2822                Some((codec, kind)),
2823                "{codec:?}: a frame-rate change"
2824            );
2825            assert!(decode_one(&mut dec, &after));
2826            assert!(luma_psnr(&dec.frame().unwrap(), &frame(3)) > 28.0);
2827        }
2828    }
2829
2830    /// A finer quantizer reaches a running constant-quality session, live where the library takes
2831    /// one, and the frames after it spend more.
2832    #[test]
2833    fn a_quality_change_reaches_a_running_session() {
2834        for codec in lockstep_codecs() {
2835            let mut s = settings(codec);
2836            s.video_crf = 40;
2837            let mut enc = session(codec, &s, false);
2838            let (mut spent, mut changed) = ([0usize; 2], None);
2839            for t in 0..12usize {
2840                let crf = if t < 6 { 40 } else { 15 };
2841                let out = enc
2842                    .encode_host(&frame(t), W * 4, false, t as u64, crf, t == 0)
2843                    .unwrap();
2844                if t == 6 {
2845                    changed = parse_video_type(out[1]);
2846                }
2847                if (1..6).contains(&t) {
2848                    spent[0] += out.len();
2849                } else if t >= 7 {
2850                    spent[1] += out.len();
2851                }
2852            }
2853            assert!(
2854                spent[1] * 2 > spent[0] * 3,
2855                "{codec:?}: bytes over five deltas before and after {spent:?}"
2856            );
2857            let kind = if enc.backend_name() == "libvpx" {
2858                FRAME_DELTA
2859            } else {
2860                FRAME_KEY
2861            };
2862            assert_eq!(
2863                changed,
2864                Some((codec, kind)),
2865                "{codec:?}: the frame after the change"
2866            );
2867        }
2868    }
2869
2870    /// A new bitrate reaches a running constant-rate session, live where the library takes one,
2871    /// and the stream stays decodable across it.
2872    #[test]
2873    fn a_bitrate_change_reaches_a_running_session() {
2874        for codec in lockstep_codecs() {
2875            let mut s = settings(codec);
2876            s.video_cbr_mode = true;
2877            s.video_bitrate_kbps = 1500;
2878            let mut enc = session(codec, &s, false);
2879            let mut dec = VideoDecoder::new(codec).expect("decoder");
2880            let (mut spent, mut changed) = ([0usize; 2], None);
2881            for t in 0..60usize {
2882                if t == 30 {
2883                    s.video_bitrate_kbps = 4500;
2884                    enc.reconfigure_rate(&s).expect("rate reconfigure");
2885                }
2886                let out = enc
2887                    .encode_host(&noise(t), W * 4, false, t as u64, 25, t == 0)
2888                    .unwrap();
2889                if t == 30 {
2890                    changed = parse_video_type(out[1]);
2891                }
2892                if out.len() > VIDEO_HEADER_LEN {
2893                    assert!(decode_one(&mut dec, &out), "{codec:?} frame {t}");
2894                }
2895                if (15..30).contains(&t) {
2896                    spent[0] += out.len();
2897                } else if t >= 45 {
2898                    spent[1] += out.len();
2899                }
2900            }
2901            assert!(
2902                spent[1] > spent[0] * 2,
2903                "{codec:?}: bytes over fifteen frames before and after {spent:?}"
2904            );
2905            let kind = if takes_a_live_rate(&enc) {
2906                FRAME_DELTA
2907            } else {
2908                FRAME_KEY
2909            };
2910            assert_eq!(
2911                changed,
2912                Some((codec, kind)),
2913                "{codec:?}: the frame after the change"
2914            );
2915        }
2916    }
2917
2918    /// Every session declares the BT.709 matrix it converts with, at limited range for 4:2:0
2919    /// and full range for the x265 4:4:4 one, like x264. VP8 reads back as BT.470BG whatever it
2920    /// is handed: its keyframe header holds one color-space bit and BT.601 is its only defined
2921    /// value, so the transports carry the real matrix for that codec themselves.
2922    #[test]
2923    fn sessions_declare_the_matrix_they_convert_with() {
2924        for codec in lockstep_codecs() {
2925            let mut s = settings(codec);
2926            let mut enc = session(codec, &s, false);
2927            let out = enc
2928                .encode_host(&frame(0), W * 4, false, 0, 25, true)
2929                .expect("encode");
2930            let mut dec = VideoDecoder::new(codec).expect("decoder");
2931            assert!(decode_one(&mut dec, &out));
2932            let want = if codec == Codec::Vp8 {
2933                ColorTags::BT470BG_LIMITED
2934            } else {
2935                ColorTags::BT709_LIMITED
2936            };
2937            assert_eq!(dec.color_tags(), Some(want), "{codec:?}");
2938            if software_fullcolor(codec) {
2939                s.video_fullcolor = true;
2940                let mut enc = session(codec, &s, false);
2941                assert!(enc.is_fullcolor(), "{codec:?} carries the 4:4:4 request");
2942                let out = enc
2943                    .encode_host(&frame(0), W * 4, false, 0, 25, true)
2944                    .expect("encode");
2945                let mut dec = VideoDecoder::new(codec).expect("decoder");
2946                assert!(decode_one(&mut dec, &out));
2947                let want = if codec == Codec::Vp9 {
2948                    ColorTags::BT709_LIMITED
2949                } else {
2950                    ColorTags::BT709_FULL
2951                };
2952                assert_eq!(dec.color_tags(), Some(want), "{codec:?} 4:4:4");
2953            }
2954        }
2955    }
2956
2957    /// 4:4:4 is carried only where the software encoder does (x265, VP9), never quietly
2958    /// elsewhere.
2959    #[test]
2960    fn fullcolor_follows_the_software_encoder() {
2961        for codec in lockstep_codecs() {
2962            let mut s = settings(codec);
2963            s.video_fullcolor = true;
2964            let mut enc = session(codec, &s, false);
2965            assert_eq!(enc.is_fullcolor(), software_fullcolor(codec), "{codec:?}");
2966            let out = enc
2967                .encode_host(&frame(0), W * 4, false, 0, 25, true)
2968                .unwrap();
2969            let mut dec = VideoDecoder::new(codec).expect("decoder");
2970            assert!(decode_one(&mut dec, &out));
2971        }
2972    }
2973
2974    /// 10 bits are coded only where the software encoder codes them, at either chroma it
2975    /// carries, and the session says which it runs.
2976    #[test]
2977    fn ten_bit_follows_the_software_encoder() {
2978        for codec in lockstep_codecs() {
2979            for fullcolor in [false, true] {
2980                let mut s = settings(codec);
2981                s.video_bit_depth = 10;
2982                s.video_fullcolor = fullcolor;
2983                let mut enc = session(codec, &s, false);
2984                let want = if software_ten_bit(codec) { 10 } else { 8 };
2985                assert_eq!(enc.bit_depth(), want, "{codec:?}");
2986                assert_eq!(
2987                    enc.is_fullcolor(),
2988                    fullcolor && software_fullcolor(codec),
2989                    "{codec:?}"
2990                );
2991                assert_eq!(
2992                    software_formats(codec).ten_bit,
2993                    [want == 10, want == 10 && software_fullcolor(codec)],
2994                    "{codec:?}"
2995                );
2996                for t in 0..3 {
2997                    let out = enc
2998                        .encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2999                        .unwrap();
3000                    assert!(t > 0 || out.len() > VIDEO_HEADER_LEN, "{codec:?} key frame");
3001                }
3002            }
3003        }
3004    }
3005
3006    /// Four colors whose 2x2 average is gray, tiled: a decoded block's chroma comes out
3007    /// neutral only where the session sited chroma at the center of the block, and saturated
3008    /// wherever it kept one pixel, row, or column of it — the color a browser then shows along
3009    /// the glyph edges of subpixel-antialiased text.
3010    #[test]
3011    fn decoded_chroma_is_neutral_on_a_tile_that_averages_to_gray() {
3012        const N: usize = 128;
3013        let bgra = chroma_siting::bgra(N, N);
3014        let settings = CaptureSettings {
3015            width: N as i32,
3016            height: N as i32,
3017            target_fps: 30.0,
3018            video_crf: 20,
3019            ..Default::default()
3020        };
3021        for codec in SOFTWARE {
3022            let mut enc = session(codec, &settings, false);
3023            let out = drain_first(&mut enc, &bgra, N * 4, 20);
3024            assert!(!out.is_empty(), "{codec:?} encoded nothing");
3025            let mut dec = VideoDecoder::new(codec).expect("decoder");
3026            assert!(
3027                dec.decode(&out[VIDEO_HEADER_LEN..]).unwrap_or(false),
3028                "{codec:?} decoded nothing"
3029            );
3030            let worst = chroma_siting::worst(&dec.frame().expect("frame"));
3031            println!("[chroma-siting] {codec:?}: worst |C-128| {worst:.1}");
3032            assert!(
3033                worst <= 8.0,
3034                "{codec:?} sites chroma {worst:.1} off neutral"
3035            );
3036        }
3037    }
3038
3039    /// The eight-patch chart, encoded and decoded, comes back as the color that was painted
3040    /// when a receiver inverts the matrix the session declares — the check a client's
3041    /// presentation path performs on every frame. A convert or a declaration that name
3042    /// different matrices leaves the neutrals exact and the saturated patches tens of levels
3043    /// out, which is what the browsers show as washed-out or shifted color.
3044    #[test]
3045    fn the_chart_decodes_to_the_color_that_was_painted() {
3046        const N: usize = 256;
3047        let bgra = chroma_siting::chart_bgra(N, N / 2);
3048        let settings = CaptureSettings {
3049            width: N as i32,
3050            height: (N / 2) as i32,
3051            target_fps: 30.0,
3052            video_crf: 20,
3053            ..Default::default()
3054        };
3055        for codec in SOFTWARE {
3056            let mut enc = session(codec, &settings, false);
3057            let out = drain_first(&mut enc, &bgra, N * 4, 20);
3058            let mut dec = VideoDecoder::new(codec).expect("decoder");
3059            assert!(
3060                dec.decode(&out[VIDEO_HEADER_LEN..]).unwrap_or(false),
3061                "{codec:?}"
3062            );
3063            let declared = codec != Codec::Vp8;
3064            let (k, other, other_name) = if declared {
3065                (chroma_siting::BT709, chroma_siting::BT601, "BT.601")
3066            } else {
3067                (chroma_siting::BT601, chroma_siting::BT709, "BT.709")
3068            };
3069            let frame = dec.frame().expect("frame");
3070            let worst = chroma_siting::chart_error(&frame, k);
3071            let under_other = chroma_siting::chart_error(&frame, other);
3072            println!(
3073                "[chart] {codec:?}: worst |dRGB| {worst:.1} against the declared matrix, {under_other:.1} against {other_name}"
3074            );
3075            assert!(
3076                worst <= 12.0,
3077                "{codec:?} paints {worst:.1} off the chart against the matrix it declares, and {under_other:.1} against {other_name}: {}",
3078                if under_other < worst {
3079                    "it converted with that one and declared the other"
3080                } else {
3081                    "neither matrix explains it"
3082                }
3083            );
3084        }
3085    }
3086
3087    /// The first packet of a fresh session, feeding `bgra` until one arrives: an encoder that
3088    /// pipelines answers the opening frames with nothing.
3089    pub(super) fn drain_first(
3090        enc: &mut FrameEncoder,
3091        bgra: &[u8],
3092        stride: usize,
3093        qp: u32,
3094    ) -> Vec<u8> {
3095        for t in 0..8u64 {
3096            let out = enc
3097                .encode_host(bgra, stride, false, t, qp, t == 0)
3098                .unwrap_or_else(|e| panic!("encode: {e}"));
3099            if !out.is_empty() {
3100                return out;
3101            }
3102        }
3103        Vec::new()
3104    }
3105}
3106
3107/// The fixture the capped-quality checks share: a `w` x `h` BGRA page of glyphs scrolled `t` steps
3108/// of 4 rows, which costs its quantizer's bits every frame.
3109#[cfg(test)]
3110pub(crate) fn scrolled_text(w: usize, h: usize, t: usize) -> Vec<u8> {
3111    let mut f = vec![18u8; w * h * 4];
3112    for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3113        let (x, y) = (i % w, i / w + t * 4);
3114        let cell =
3115            ((x / 8) as u32).wrapping_mul(2654435761) ^ ((y / 12) as u32).wrapping_mul(40503);
3116        if x % 8 < 7 && y % 12 < 10 && (cell >> (((y % 12) * 3 + x % 8) % 29)) & 1 == 1 {
3117            px.copy_from_slice(&[224, 224, 224, 255]);
3118        }
3119    }
3120    f
3121}
3122
3123/// The fixture the constant-block checks of every software encoder share: a screen of text
3124/// with one box of it changing every frame, at a size that is no multiple of a block.
3125#[cfg(test)]
3126pub(crate) mod mixed_screen {
3127    use crate::webcam::decode::{Decoder as _, VideoDecoder};
3128
3129    /// A width and height that no 16x16 block, 64x64 superblock, or 8x8 coding unit divides, so
3130    /// the blocks along the right and bottom edges are partial and the chroma planes end on an
3131    /// odd row pair.
3132    pub const W: usize = 328;
3133    pub const H: usize = 244;
3134
3135    /// The side of the changing box, placed off every block boundary so the blocks it touches
3136    /// are partly still.
3137    const BOX: usize = 64;
3138    const BX: usize = W - BOX - 20;
3139    const BY: usize = H - BOX - 12;
3140
3141    /// Whether a pixel lies in the changing box.
3142    pub fn in_box(x: usize, y: usize) -> bool {
3143        (BX..BX + BOX).contains(&x) && (BY..BY + BOX).contains(&y)
3144    }
3145
3146    /// The BGRA picture at frame `t`: text on a light screen, the box a pattern that moves
3147    /// every frame.
3148    pub fn picture(t: usize) -> Vec<u8> {
3149        let mut f = vec![0u8; W * H * 4];
3150        for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3151            let (x, y) = (i % W, i / W);
3152            let text = (x / 7 + y / 11) % 3 == 0 && x % 7 < 5 && y % 11 < 8;
3153            let v = if in_box(x, y) {
3154                ((x * 7 + y * 13 + t * 41) % 251) as u8
3155            } else if text {
3156                30
3157            } else {
3158                235
3159            };
3160            px.copy_from_slice(&[v, v, v, 255]);
3161        }
3162        f
3163    }
3164
3165    /// The ledger's map of the screen: every 16x16 block constant but those the box touches,
3166    /// which is most of them.
3167    pub fn constant() -> Vec<bool> {
3168        let (cols, rows) = (W.div_ceil(16), H.div_ceil(16));
3169        let map: Vec<bool> = (0..rows)
3170            .flat_map(|row| {
3171                (0..cols).map(move |col| {
3172                    let (x0, y0) = (col * 16, row * 16);
3173                    !(x0 + 16 > BX && x0 < BX + BOX && y0 + 16 > BY && y0 < BY + BOX)
3174                })
3175            })
3176            .collect();
3177        assert!(map.iter().filter(|&&c| !c).count() < map.len() / 2);
3178        map
3179    }
3180
3181    /// The decoder's current picture as a `W`x`H` luma plane.
3182    pub fn luma(dec: &VideoDecoder) -> Vec<u8> {
3183        let f = dec.frame().expect("a decoded picture");
3184        f.y.chunks(f.y_stride)
3185            .take(H)
3186            .flat_map(|row| row[..W].iter().copied())
3187            .collect()
3188    }
3189
3190    /// The mean distance, in luma levels, of a decoded plane from the gray `source` picture
3191    /// over the box (`boxed`) or over the still screen around it.
3192    pub fn distance(decoded: &[u8], source: &[u8], boxed: bool) -> f64 {
3193        let (mut sum, mut n) = (0.0, 0usize);
3194        for y in 0..H {
3195            for x in 0..W {
3196                if in_box(x, y) == boxed {
3197                    sum +=
3198                        (f64::from(decoded[y * W + x]) - f64::from(source[(y * W + x) * 4])).abs();
3199                    n += 1;
3200                }
3201            }
3202        }
3203        sum / n as f64
3204    }
3205
3206    /// Encodes eight frames of the screen through `plain` without a map and through `mapped`
3207    /// with the ledger's, decodes both, and asserts the mapped stream lands as close to the
3208    /// source as the plain one, in the box and on the still screen, within one luma level.
3209    pub fn assert_decodes_alike(
3210        codec: super::Codec,
3211        label: &str,
3212        mut plain: impl FnMut(&[u8], u64) -> Vec<u8>,
3213        mut mapped: impl FnMut(&[u8], u64, &[bool]) -> Vec<u8>,
3214    ) {
3215        let map = constant();
3216        let mut plain_dec = VideoDecoder::new(codec).expect("decoder");
3217        let mut mapped_dec = VideoDecoder::new(codec).expect("decoder");
3218        let mut source = Vec::new();
3219        for n in 0..8u64 {
3220            source = picture(n as usize);
3221            let a = plain(&source, n);
3222            let b = mapped(&source, n, &map);
3223            assert!(
3224                plain_dec
3225                    .decode(&a[super::codec::VIDEO_HEADER_LEN..])
3226                    .expect("decode")
3227            );
3228            assert!(
3229                mapped_dec
3230                    .decode(&b[super::codec::VIDEO_HEADER_LEN..])
3231                    .expect("decode")
3232            );
3233        }
3234        let (pa, pb) = (luma(&plain_dec), luma(&mapped_dec));
3235        for boxed in [true, false] {
3236            let (da, db) = (distance(&pa, &source, boxed), distance(&pb, &source, boxed));
3237            assert!(
3238                db <= da + 1.0,
3239                "{label} {}: {db:.2} luma levels from the source with the map, {da:.2} without",
3240                if boxed {
3241                    "in the box"
3242                } else {
3243                    "on the still screen"
3244                }
3245            );
3246        }
3247    }
3248}
3249
3250/// The fixture the chroma-siting checks of every backend share.
3251#[cfg(test)]
3252pub(crate) mod chroma_siting {
3253    /// Four colors averaging to gray, of which no pixel, row pair, or column pair does: the
3254    /// chroma of a block comes out neutral only where all four were averaged. A 4:2:0 convert
3255    /// that keeps one pixel of the block, or one row or column of it, leaves the saturation
3256    /// subpixel-antialiased text carries on its glyph edges in the picture as visible color.
3257    pub const TILE: [[u8; 3]; 4] = [[0, 0, 128], [0, 255, 0], [128, 128, 255], [255, 0, 0]];
3258
3259    /// `TILE` laid out as a `w`x`h` BGRA frame.
3260    pub fn bgra(w: usize, h: usize) -> Vec<u8> {
3261        let mut buf = vec![255u8; w * h * 4];
3262        for y in 0..h {
3263            for x in 0..w {
3264                let p = TILE[(y % 2) * 2 + (x % 2)];
3265                buf[(y * w + x) * 4..][..3].copy_from_slice(&[p[2], p[1], p[0]]);
3266            }
3267        }
3268        buf
3269    }
3270
3271    /// The luma weights `(Kr, Kb)` of the matrix every session converts with and declares.
3272    pub const BT709: (f64, f64) = (0.2126, 0.0722);
3273
3274    /// BT.601's weights, the other matrix a receiver might invert, which the checks use as the
3275    /// contrast: a mis-declared stream lands tens of levels away under them.
3276    pub const BT601: (f64, f64) = (0.299, 0.114);
3277
3278    /// Limited-range Y/Cb/Cr of an RGB triple under the matrix `k` names.
3279    pub fn ycbcr(rgb: [f64; 3], k: (f64, f64)) -> [f64; 3] {
3280        let ([r, g, b], (kr, kb)) = (rgb, k);
3281        let y = kr * r + (1.0 - kr - kb) * g + kb * b;
3282        [
3283            16.0 + 219.0 * y / 255.0,
3284            128.0 + 224.0 * (b - y) / (2.0 * (1.0 - kb) * 255.0),
3285            128.0 + 224.0 * (r - y) / (2.0 * (1.0 - kr) * 255.0),
3286        ]
3287    }
3288
3289    /// Its chroma pair alone.
3290    pub fn chroma(rgb: [f64; 3], k: (f64, f64)) -> (f64, f64) {
3291        let c = ycbcr(rgb, k);
3292        (c[1], c[2])
3293    }
3294
3295    /// The inverse: the RGB a receiver paints from a limited-range Y/Cb/Cr under the matrix `k`
3296    /// names, which is what a client's presentation path computes.
3297    pub fn rgb(ycc: [f64; 3], k: (f64, f64)) -> [f64; 3] {
3298        let (kr, kb) = k;
3299        let (y, cb, cr) = (
3300            (ycc[0] - 16.0) / 219.0,
3301            (ycc[1] - 128.0) / 224.0,
3302            (ycc[2] - 128.0) / 224.0,
3303        );
3304        let r = y + 2.0 * (1.0 - kr) * cr;
3305        let b = y + 2.0 * (1.0 - kb) * cb;
3306        let g = (y - kr * r - kb * b) / (1.0 - kr - kb);
3307        [r, g, b].map(|c| (c * 255.0).clamp(0.0, 255.0))
3308    }
3309
3310    /// The eight-patch color chart the matrix checks paint: the neutrals, whose chroma a wrong
3311    /// matrix leaves alone, and the saturated corners, which it moves by tens of levels.
3312    pub const CHART: [[u8; 3]; 8] = [
3313        [255, 255, 255],
3314        [128, 128, 128],
3315        [0, 0, 0],
3316        [255, 0, 0],
3317        [0, 255, 0],
3318        [0, 0, 255],
3319        [255, 255, 0],
3320        [0, 255, 255],
3321    ];
3322
3323    /// `CHART` as a `w`x`h` BGRA frame of eight columns.
3324    pub fn chart_bgra(w: usize, h: usize) -> Vec<u8> {
3325        let mut buf = vec![255u8; w * h * 4];
3326        for y in 0..h {
3327            for x in 0..w {
3328                let p = CHART[(x * CHART.len() / w).min(CHART.len() - 1)];
3329                buf[(y * w + x) * 4..][..3].copy_from_slice(&[p[2], p[1], p[0]]);
3330            }
3331        }
3332        buf
3333    }
3334
3335    /// The worst channel error, over `CHART`'s patches, between the RGB a receiver paints from a
3336    /// decoded frame — inverting the matrix `k` names — and the RGB that was painted. Each
3337    /// patch is sampled well inside its column, so neither the 4:2:0 chroma edges nor the
3338    /// encoder's ringing at the boundaries counts.
3339    pub fn chart_error(f: &crate::webcam::convert::I420View<'_>, k: (f64, f64)) -> f64 {
3340        let cols = CHART.len();
3341        let mut worst = 0.0f64;
3342        for (i, want) in CHART.iter().enumerate() {
3343            let (x0, x1) = (f.width * i / cols, f.width * (i + 1) / cols);
3344            let (lo, hi) = (x0 + (x1 - x0) / 4, x1 - (x1 - x0) / 4);
3345            let (mut acc, mut n) = ([0.0f64; 3], 0.0f64);
3346            for y in f.height / 4..f.height * 3 / 4 {
3347                for x in lo..hi {
3348                    let ycc = [
3349                        f64::from(f.y[y * f.y_stride + x]),
3350                        f64::from(f.u[(y / 2) * f.uv_stride + x / 2]),
3351                        f64::from(f.v[(y / 2) * f.uv_stride + x / 2]),
3352                    ];
3353                    let got = rgb(ycc, k);
3354                    for c in 0..3 {
3355                        acc[c] += got[c];
3356                    }
3357                    n += 1.0;
3358                }
3359            }
3360            for c in 0..3 {
3361                worst = worst.max((acc[c] / n - f64::from(want[c])).abs());
3362            }
3363        }
3364        worst
3365    }
3366
3367    /// The worst distance from neutral chroma over a decoded frame's chroma planes.
3368    pub fn worst(f: &crate::webcam::convert::I420View<'_>) -> f64 {
3369        let mut worst = 0.0f64;
3370        for r in 0..f.chroma_height() {
3371            for c in 0..f.chroma_width() {
3372                let i = r * f.uv_stride + c;
3373                worst = worst.max((f64::from(f.u[i]) - 128.0).hypot(f64::from(f.v[i]) - 128.0));
3374            }
3375        }
3376        worst
3377    }
3378
3379    /// The tile is a fixture, so its premise is checked where it lives: the four chromas cancel,
3380    /// and every partial average a wrong siting would take is far from neutral.
3381    #[test]
3382    fn the_tile_separates_the_sitings() {
3383        let c: Vec<(f64, f64)> = TILE
3384            .iter()
3385            .map(|&p| chroma(p.map(f64::from), BT709))
3386            .collect();
3387        let mean = |of: &[usize]| {
3388            let (u, v) = of
3389                .iter()
3390                .fold((0.0, 0.0), |(u, v), &i| (u + c[i].0, v + c[i].1));
3391            let n = of.len() as f64;
3392            (u / n - 128.0).hypot(v / n - 128.0)
3393        };
3394        let all = mean(&[0, 1, 2, 3]);
3395        assert!(
3396            all < 0.5,
3397            "the whole tile must average to neutral chroma, off by {all:.1}"
3398        );
3399        for part in [
3400            vec![0],
3401            vec![1],
3402            vec![2],
3403            vec![3],
3404            vec![0, 1],
3405            vec![2, 3],
3406            vec![0, 2],
3407            vec![1, 3],
3408        ] {
3409            let d = mean(&part);
3410            assert!(
3411                d > 40.0,
3412                "pixels {part:?} average to chroma only {d:.1} from neutral"
3413            );
3414        }
3415    }
3416}
3417
3418/// The chroma format a session actually carries, which is not always the one requested: a
3419/// hardware session only when the device carries it, the software path only when the build's
3420/// encoder for the codec does. Every consumer of "is this stream 4:4:4" reads it from here.
3421pub fn session_fullcolor(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> bool {
3422    match encoder {
3423        Some(enc) => enc.is_fullcolor(),
3424        None => settings.video_fullcolor && software_fullcolor(settings.codec),
3425    }
3426}
3427
3428/// The bits per sample a session carries, which is not always what was asked for: a
3429/// full-frame session's own answer, and for the CPU path (`None`) what its H.264 encoder
3430/// opens at.
3431pub fn session_bit_depth(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> u32 {
3432    match encoder {
3433        Some(enc) => enc.bit_depth(),
3434        None => software::h264_bit_depth(settings),
3435    }
3436}
3437
3438/// Whether a session runs its constant quality under the peak it was asked for
3439/// (`RateSettings::capped`): a full-frame session's own answer, and for the CPU path (`None`)
3440/// x264's, which caps, where the build carries it.
3441pub fn session_capped(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> bool {
3442    match encoder {
3443        Some(enc) => enc.capped(),
3444        None => {
3445            cfg!(feature = "gpl")
3446                && settings.codec == Codec::H264
3447                && session::RateSettings::new(settings).capped
3448        }
3449    }
3450}
3451
3452/// Whether a session signals full range: a software 4:4:4 session of x264's kind, which the
3453/// CPU path (`None`) is whenever it carries 4:4:4, or a device that converted in fixed
3454/// function at a range it chose. The one answer every description of a session reads, so no
3455/// two of them can disagree.
3456pub fn session_full_range(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> bool {
3457    match encoder {
3458        Some(enc) => enc.is_full_range(),
3459        None => session_fullcolor(None, settings),
3460    }
3461}
3462
3463#[cfg(test)]
3464mod hardware_encoder_tests {
3465    //! The hardware encoder table of a render node, on a host with an engine behind its first
3466    //! node. Ignored by default; run with `cargo test gpu_ -- --ignored --test-threads=1`.
3467    use super::*;
3468
3469    /// The first node serves H.264 on the backend its driver selects, every entry names one of
3470    /// the two backends, and the second read is the remembered first.
3471    #[test]
3472    #[ignore]
3473    fn gpu_hardware_encoders_serve_h264_once_probed() {
3474        let served = hardware_encoders(0);
3475        assert!(
3476            served.iter().any(|(codec, ..)| *codec == Codec::H264),
3477            "node 0 serves {served:?}"
3478        );
3479        assert!(
3480            served
3481                .iter()
3482                .all(|(_, backend, ..)| matches!(*backend, "nvenc" | "vaapi"))
3483        );
3484        assert_eq!(hardware_encoders(0), served);
3485    }
3486}