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