Skip to main content

pixelflux/encoders/
software.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//! The CPU encode path: JPEG through turbojpeg in horizontal stripes, and H.264 through the
8//! build's software encoder — libx264 with the `gpl` feature, Cisco OpenH264 without it
9//! (`software_encoder`) — as one whole-frame session.
10//!
11//! JPEG frames are split into stripes coded in parallel via rayon; each stripe is hashed against
12//! the previous frame for change detection, and only dirty stripes are sent, statelessly. H.264
13//! is one region covering the frame, its encoder kept across frames for inter-prediction, and
14//! both libraries emit the same wire framing. Both run the same per-region send and cleanup
15//! decision (`encode_cpu`).
16
17use super::codec::{Codec, push_jpeg_header};
18#[cfg(feature = "gpl")]
19use super::codec::{
20    FRAME_DELTA, FRAME_INTRA, FRAME_KEY, h264_dpb_frames, h264_level, h264_reference_level,
21    push_video_header,
22};
23#[cfg(feature = "gpl")]
24use super::frame_rate::FrameRate;
25use super::reference::Reference;
26#[cfg(feature = "gpl")]
27use super::reference::{Invalidation, ReferenceWindow};
28#[cfg(feature = "gpl")]
29use super::session::PeakOffset;
30#[cfg(feature = "gpl")]
31use super::sps::{WideFrameNum, h264_frame_num_range};
32use crate::CaptureSettings;
33use crate::ledger::{BlockLedger, RefinePace, RefreshPace};
34use crate::pipeline::{Cleanup, Damage};
35use rayon::prelude::*;
36use smithay::utils::{Physical, Rectangle};
37#[cfg(feature = "gpl")]
38use std::ffi::CString;
39#[cfg(feature = "gpl")]
40use std::ptr;
41use std::sync::Arc;
42use yuv::{
43    BufferStoreMut, YuvBiPlanarImageMut, YuvConversionMode, YuvPlanarImageMut, YuvRange,
44    YuvStandardMatrix,
45};
46
47/// Upper bound on the horizontal stripes the CPU encoder splits a frame into, so the
48/// persistent per-stripe state vector can be reserved to a fixed capacity once, up front.
49///
50/// With the vector reserved to this size at startup, the per-frame resize to the actual stripe count
51/// stays a cheap in-place adjustment that preserves each stripe's reused encoder and scratch buffers,
52/// rather than a reallocation that would churn them whenever the count changes.
53pub const MAX_STRIPE_CAPACITY: usize = 64;
54
55/// Convert a packed BGRA/RGBA buffer to planar YUV (4:2:0 or 4:4:4) for the software H.264
56/// encoders, spreading the conversion across up to `bands` threads so it never bottlenecks a frame.
57///
58/// **Why the band split exists.** Color conversion is a non-trivial slice of per-frame CPU, and a
59/// whole-frame session (OpenH264, or a session of another codec) would otherwise convert its
60/// entire image on one thread and stall the frame there. Splitting into horizontal bands spreads
61/// that conversion over the rayon pool. The cut is horizontal because YUV planes are row-major, so
62/// a horizontal boundary yields contiguous, non-overlapping plane sub-slices with no per-row seam
63/// bookkeeping.
64///
65/// 1. **Plane strides**: `strides` gives the Y and chroma row pitches of the output planes
66///    (tightly packed for the session's own buffers, padded for an AVFrame); the chroma planes are
67///    `width` wide for 4:4:4 (`i444 == true`) or `width / 2` for 4:2:0. `rgba_input` selects
68///    the source byte order and `i444` the subsampling; `full_range` selects the signal range,
69///    **Full** for the 4:4:4 stream x264 and x265 declare and **Limited** for everything else.
70///    The matrix is **BT.709**, whose primaries and transfer the sRGB desktop source already
71///    carries, unless `bt601` marks a codec whose bitstream can name no other matrix (VP8).
72///    Every encoder declares the matrix it was fed. All four `yuv` crate routines run in the
73///    **Fast** conversion mode.
74/// 2. **Band split**: `band_h` is `height / bands` floored to an even number and at least 2 rows
75///    (a band under 2 rows is not worth a task). Keeping band boundaries even ensures a 4:2:0
76///    chroma pair never straddles a seam. When `bands <= 1` or the whole image fits one band, the
77///    conversion runs on the calling thread in place.
78/// 3. **Parallel bands**: otherwise `src` and the three output planes are carved into contiguous
79///    per-band sub-slices (chroma rows scaled by `uv_rows` — full height for 4:4:4, half for
80///    4:2:0) and converted on the pool of `bands` threads (`on_band_pool`), whose workers
81///    already exist, so a frame spawns no thread. The final band absorbs any leftover rows,
82///    taking all remaining rows whenever fewer than `band_h + 2` are left. The first error wins.
83#[allow(clippy::too_many_arguments)]
84pub(crate) fn convert_to_yuv_mt(
85    src: &[u8],
86    src_stride: u32,
87    width: usize,
88    height: usize,
89    rgba_input: bool,
90    i444: bool,
91    full_range: bool,
92    bt601: bool,
93    y_buf: &mut [u8],
94    u_buf: &mut [u8],
95    v_buf: &mut [u8],
96    strides: (usize, usize),
97    bands: usize,
98) -> Result<(), yuv::YuvError> {
99    let (y_stride, uv_stride) = strides;
100    let range = if full_range {
101        YuvRange::Full
102    } else {
103        YuvRange::Limited
104    };
105    let matrix = if bt601 {
106        YuvStandardMatrix::Bt601
107    } else {
108        YuvStandardMatrix::Bt709
109    };
110
111    let convert_band = |src_band: &[u8], y: &mut [u8], u: &mut [u8], v: &mut [u8], h: usize| {
112        let mut img = YuvPlanarImageMut {
113            y_plane: BufferStoreMut::Borrowed(y),
114            y_stride: y_stride as u32,
115            u_plane: BufferStoreMut::Borrowed(u),
116            u_stride: uv_stride as u32,
117            v_plane: BufferStoreMut::Borrowed(v),
118            v_stride: uv_stride as u32,
119            width: width as u32,
120            height: h as u32,
121        };
122        match (i444, rgba_input) {
123            (true, true) => yuv::rgba_to_yuv444(
124                &mut img,
125                src_band,
126                src_stride,
127                range,
128                matrix,
129                YuvConversionMode::Fast,
130            ),
131            (true, false) => yuv::bgra_to_yuv444(
132                &mut img,
133                src_band,
134                src_stride,
135                range,
136                matrix,
137                YuvConversionMode::Fast,
138            ),
139            (false, true) => yuv::rgba_to_yuv420(
140                &mut img,
141                src_band,
142                src_stride,
143                range,
144                matrix,
145                YuvConversionMode::Fast,
146            ),
147            (false, false) => yuv::bgra_to_yuv420(
148                &mut img,
149                src_band,
150                src_stride,
151                range,
152                matrix,
153                YuvConversionMode::Fast,
154            ),
155        }
156    };
157
158    let heights = band_heights(height, bands);
159    if heights.len() <= 1 {
160        return convert_band(src, y_buf, u_buf, v_buf, height);
161    }
162
163    let uv_rows = |rows: usize| if i444 { rows } else { rows / 2 };
164    let mut jobs = Vec::new();
165    let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
166    for h in heights {
167        let (src_band, s_next) = src_rest.split_at(h * src_stride as usize);
168        let (y_band, y_next) = y_rest.split_at_mut(h * y_stride);
169        let (u_band, u_next) = u_rest.split_at_mut(uv_rows(h) * uv_stride);
170        let (v_band, v_next) = v_rest.split_at_mut(uv_rows(h) * uv_stride);
171        src_rest = s_next;
172        y_rest = y_next;
173        u_rest = u_next;
174        v_rest = v_next;
175        jobs.push((src_band, y_band, u_band, v_band, h));
176    }
177    on_band_pool(bands, || {
178        jobs.into_par_iter()
179            .map(|(src_band, y_band, u_band, v_band, h)| {
180                convert_band(src_band, y_band, u_band, v_band, h)
181            })
182            .collect::<Result<Vec<()>, _>>()
183            .map(|_| ())
184    })
185}
186
187/// The rows of each band a conversion of `height` rows splits into for up to `bands` threads
188/// (`convert_to_yuv_mt`): `height / bands` floored to an even number and at least 2, the last band
189/// taking the rest wherever fewer than a band and 2 rows would be left after it. One band where
190/// `bands <= 1` or the image fits one.
191fn band_heights(height: usize, bands: usize) -> Vec<usize> {
192    let band_h = ((height / bands.max(1)) & !1).max(2);
193    if bands <= 1 || height <= band_h {
194        return vec![height];
195    }
196    let mut heights = Vec::with_capacity(bands + 1);
197    let mut row = 0;
198    while row < height {
199        let h = if height - row < band_h + 2 {
200            height - row
201        } else {
202            band_h
203        };
204        heights.push(h);
205        row += h;
206    }
207    heights
208}
209
210/// The most bands a conversion keeps a pool for (`on_band_pool`); more run on rayon's global
211/// pool. A session converts in as many bands as its encode threads, eight at most
212/// (`session::threads_up_to`).
213const MAX_POOLED_BANDS: usize = 16;
214
215/// Run `convert`, a conversion's parallel bands, on a pool of `bands` threads: one a band
216/// count, built the first time a conversion asks for it and kept for the process.
217///
218/// rayon's global pool has a thread per CPU. Handed a frame's bands, it also wakes the threads
219/// it has no band for, and those spin before they sleep again, a cost that grows with the CPUs
220/// rather than the bands. On a pool of as many threads as bands, each thread takes a band.
221/// Where the pool cannot be built, or `bands` is over `MAX_POOLED_BANDS`, the bands run on the
222/// global pool.
223///
224/// A conversion called on a rayon pool's thread keeps to that pool: the forked child that
225/// `encodes_in_child` probes an encoder in installs one, since a pool built before the fork
226/// has no threads in the child.
227fn on_band_pool<R: Send>(bands: usize, convert: impl FnOnce() -> R + Send) -> R {
228    if rayon::current_thread_index().is_some() {
229        return convert();
230    }
231    static POOLS: [std::sync::OnceLock<Option<rayon::ThreadPool>>; MAX_POOLED_BANDS + 1] =
232        [const { std::sync::OnceLock::new() }; MAX_POOLED_BANDS + 1];
233    let pool = POOLS.get(bands).and_then(|pool| {
234        pool.get_or_init(|| {
235            rayon::ThreadPoolBuilder::new()
236                .num_threads(bands)
237                .thread_name(|_| "pixelflux-csc".into())
238                .build()
239                .map_err(|e| eprintln!("[software] no {bands}-thread conversion pool: {e}"))
240                .ok()
241        })
242        .as_ref()
243    });
244    match pool {
245        Some(pool) => pool.install(convert),
246        None => convert(),
247    }
248}
249
250/// Convert a packed buffer, B,G,R,A or with `rgba_input` R,G,B,A, to 8-bit 4:2:0 BT.709 limited
251/// range as NV12, a luma plane and an interleaved chroma one `strides` bytes a row, the picture
252/// x264 encodes in place (`Nv12Planes`), in the bands `convert_to_yuv_mt` splits it into. Its
253/// samples are the ones `convert_to_yuv_mt` writes to I420, the chroma interleaved.
254#[allow(clippy::too_many_arguments)]
255pub(crate) fn convert_to_nv12_mt(
256    src: &[u8],
257    src_stride: u32,
258    width: usize,
259    height: usize,
260    rgba_input: bool,
261    y_buf: &mut [u8],
262    uv_buf: &mut [u8],
263    strides: (usize, usize),
264    bands: usize,
265) -> Result<(), yuv::YuvError> {
266    let (y_stride, uv_stride) = strides;
267    let convert_band = |src_band: &[u8], y: &mut [u8], uv: &mut [u8], h: usize| {
268        let mut img = YuvBiPlanarImageMut {
269            y_plane: BufferStoreMut::Borrowed(y),
270            y_stride: y_stride as u32,
271            uv_plane: BufferStoreMut::Borrowed(uv),
272            uv_stride: uv_stride as u32,
273            width: width as u32,
274            height: h as u32,
275        };
276        let convert = if rgba_input {
277            yuv::rgba_to_yuv_nv12
278        } else {
279            yuv::bgra_to_yuv_nv12
280        };
281        convert(
282            &mut img,
283            src_band,
284            src_stride,
285            YuvRange::Limited,
286            YuvStandardMatrix::Bt709,
287            YuvConversionMode::Fast,
288        )
289    };
290    let heights = band_heights(height, bands);
291    if heights.len() <= 1 {
292        return convert_band(src, y_buf, uv_buf, height);
293    }
294    let mut jobs = Vec::with_capacity(heights.len());
295    let (mut src_rest, mut y_rest, mut uv_rest) = (src, y_buf, uv_buf);
296    for h in heights {
297        let (src_band, s_next) = src_rest.split_at(h * src_stride as usize);
298        let (y_band, y_next) = y_rest.split_at_mut(h * y_stride);
299        let (uv_band, uv_next) = uv_rest.split_at_mut(h / 2 * uv_stride);
300        src_rest = s_next;
301        y_rest = y_next;
302        uv_rest = uv_next;
303        jobs.push((src_band, y_band, uv_band, h));
304    }
305    on_band_pool(bands, || {
306        jobs.into_par_iter()
307            .map(|(src_band, y_band, uv_band, h)| convert_band(src_band, y_band, uv_band, h))
308            .collect::<Result<Vec<()>, _>>()
309            .map(|_| ())
310    })
311}
312
313/// The fixed-point scale of the 10-bit conversion's coefficients.
314const YUV10_SHIFT: u32 = 14;
315
316/// The BT.709 coefficients that take 8-bit R, G, B to 10-bit Y, Cb, Cr at `full_range` or
317/// limited range, scaled by `1 << YUV10_SHIFT`: a row each for Y, Cb, and Cr, then the luma
318/// offset. The chroma offset is the 10-bit midpoint at either range.
319fn yuv10_coefficients(full_range: bool) -> ([[i32; 3]; 3], i32) {
320    const KR: f64 = 0.2126;
321    const KB: f64 = 0.0722;
322    const KG: f64 = 1.0 - KR - KB;
323    let (luma, chroma, offset) = if full_range {
324        (1023.0 / 255.0, 1023.0 / 255.0, 0)
325    } else {
326        (876.0 / 255.0, 896.0 / 255.0, 64)
327    };
328    let fixed = |k: f64| (k * f64::from(1 << YUV10_SHIFT)).round() as i32;
329    let (cb, cr) = (chroma / (2.0 * (1.0 - KB)), chroma / (2.0 * (1.0 - KR)));
330    (
331        [
332            [fixed(luma * KR), fixed(luma * KG), fixed(luma * KB)],
333            [fixed(-cb * KR), fixed(-cb * KG), fixed(cb * (1.0 - KB))],
334            [fixed(cr * (1.0 - KR)), fixed(-cr * KG), fixed(-cr * KB)],
335        ],
336        offset,
337    )
338}
339
340/// Convert a packed BGRA/RGBA buffer to planar 10-bit YUV (4:2:0 or 4:4:4) in 16-bit samples,
341/// for the software encoders' 10-bit sessions, across up to `bands` threads as
342/// `convert_to_yuv_mt` splits them.
343///
344/// The 10-bit samples are computed from the 8-bit source directly rather than widened from an
345/// 8-bit conversion, so the two bits the encoder gains carry the precision an 8-bit Y, Cb, Cr
346/// rounds away. The matrix is BT.709 at `full_range` or limited range, and a 4:2:0 chroma
347/// sample is the conversion of the mean of its 2x2 block, which sites it at the block's
348/// center as the 8-bit conversion does. `strides` are in samples.
349#[allow(clippy::too_many_arguments)]
350pub(crate) fn convert_to_yuv10_mt(
351    src: &[u8],
352    src_stride: usize,
353    width: usize,
354    height: usize,
355    rgba_input: bool,
356    i444: bool,
357    full_range: bool,
358    y_buf: &mut [u16],
359    u_buf: &mut [u16],
360    v_buf: &mut [u16],
361    strides: (usize, usize),
362    bands: usize,
363) {
364    let (y_stride, uv_stride) = strides;
365    let (k, y_offset) = yuv10_coefficients(full_range);
366    let (ri, bi) = if rgba_input { (0, 2) } else { (2, 0) };
367    let half = 1 << (YUV10_SHIFT - 1);
368    let luma = move |r: i32, g: i32, b: i32| {
369        ((k[0][0] * r + k[0][1] * g + k[0][2] * b + half) >> YUV10_SHIFT) + y_offset
370    };
371    let chroma = move |row: &[i32; 3], r: i32, g: i32, b: i32, shift: u32| {
372        (((row[0] * r + row[1] * g + row[2] * b + (1 << (shift - 1))) >> shift) + 512)
373            .clamp(0, 1023) as u16
374    };
375
376    let convert_band = move |src: &[u8], y: &mut [u16], u: &mut [u16], v: &mut [u16], h: usize| {
377        for row in 0..h {
378            let line = &src[row * src_stride..row * src_stride + width * 4];
379            let out = &mut y[row * y_stride..row * y_stride + width];
380            for (px, y) in line.as_chunks::<4>().0.iter().zip(out.iter_mut()) {
381                *y = luma(i32::from(px[ri]), i32::from(px[1]), i32::from(px[bi])) as u16;
382            }
383        }
384        if i444 {
385            for row in 0..h {
386                let line = &src[row * src_stride..row * src_stride + width * 4];
387                let cb = &mut u[row * uv_stride..row * uv_stride + width];
388                let cr = &mut v[row * uv_stride..row * uv_stride + width];
389                for ((px, cb), cr) in line
390                    .as_chunks::<4>()
391                    .0
392                    .iter()
393                    .zip(cb.iter_mut())
394                    .zip(cr.iter_mut())
395                {
396                    let (r, g, b) = (i32::from(px[ri]), i32::from(px[1]), i32::from(px[bi]));
397                    *cb = chroma(&k[1], r, g, b, YUV10_SHIFT);
398                    *cr = chroma(&k[2], r, g, b, YUV10_SHIFT);
399                }
400            }
401            return;
402        }
403        let cw = width.div_ceil(2);
404        for row in 0..h.div_ceil(2) {
405            let top = &src[2 * row * src_stride..2 * row * src_stride + width * 4];
406            let below = (2 * row + 1).min(h - 1);
407            let bottom = &src[below * src_stride..below * src_stride + width * 4];
408            let cb = &mut u[row * uv_stride..row * uv_stride + cw];
409            let cr = &mut v[row * uv_stride..row * uv_stride + cw];
410            for (col, (cb, cr)) in cb.iter_mut().zip(cr.iter_mut()).enumerate() {
411                let left = 8 * col;
412                let right = (left + 4).min(width * 4 - 4);
413                let sum = |i: usize| {
414                    i32::from(top[left + i])
415                        + i32::from(top[right + i])
416                        + i32::from(bottom[left + i])
417                        + i32::from(bottom[right + i])
418                };
419                let (r, g, b) = (sum(ri), sum(1), sum(bi));
420                *cb = chroma(&k[1], r, g, b, YUV10_SHIFT + 2);
421                *cr = chroma(&k[2], r, g, b, YUV10_SHIFT + 2);
422            }
423        }
424    };
425
426    let heights = band_heights(height, bands);
427    if heights.len() <= 1 {
428        convert_band(src, y_buf, u_buf, v_buf, height);
429        return;
430    }
431    let uv_rows = |rows: usize| if i444 { rows } else { rows.div_ceil(2) };
432    let mut jobs = Vec::new();
433    let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
434    let mut row = 0;
435    for h in heights {
436        let last = row + h >= height;
437        let take = |len: usize, rows: usize, stride: usize| if last { len } else { rows * stride };
438        let (src_band, s_next) = src_rest.split_at(take(src_rest.len(), h, src_stride));
439        let (y_band, y_next) = y_rest.split_at_mut(take(y_rest.len(), h, y_stride));
440        let (u_band, u_next) = u_rest.split_at_mut(take(u_rest.len(), uv_rows(h), uv_stride));
441        let (v_band, v_next) = v_rest.split_at_mut(take(v_rest.len(), uv_rows(h), uv_stride));
442        src_rest = s_next;
443        y_rest = y_next;
444        u_rest = u_next;
445        v_rest = v_next;
446        row += h;
447        jobs.push((src_band, y_band, u_band, v_band, h));
448    }
449    on_band_pool(bands, || {
450        jobs.into_par_iter()
451            .for_each(|(src_band, y_band, u_band, v_band, h)| {
452                convert_band(src_band, y_band, u_band, v_band, h)
453            })
454    });
455}
456
457thread_local! {
458    /// Reused libjpeg-turbo compressor kept per worker thread to avoid paying a
459    /// `tjInitCompress`/`tjDestroy` round trip for every stripe of every frame.
460    ///
461    /// The striped JPEG path compresses one stripe per rayon worker, so the compressor is
462    /// thread-local rather than shared: each worker creates its own lazily on first use and then
463    /// holds it for the process lifetime. Making it thread-local also sidesteps the locking a shared
464    /// compressor would otherwise need across the parallel stripe encoders.
465    static JPEG_COMPRESSOR: std::cell::RefCell<Option<turbojpeg::Compressor>> =
466        const { std::cell::RefCell::new(None) };
467}
468
469/// Process-global lock that serializes libx264 encoder open/close, because those calls are
470/// not thread-safe yet several captures in one process open their sessions concurrently.
471///
472/// libx264 mutates process-global state inside `x264_encoder_open`/`x264_encoder_close`, so two
473/// capture instances sharing one process can race that state and corrupt the heap. The lock is
474/// deliberately held only around open and close, never around `x264_encoder_encode`, so
475/// serializing setup costs nothing in the hot per-frame encode path.
476#[cfg(feature = "gpl")]
477static X264_OPEN_CLOSE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
478
479/// What libx264 adds to a quantizer above 8 bits: its quantizer bounds, a frame's forced
480/// quantizer, and the one it reports are all on a scale that starts six below zero for every
481/// bit past eight, while the rate factor is not. Added going in and taken off coming out, so a
482/// session's quantizers mean what they do at 8 bits whatever its depth.
483#[cfg(feature = "gpl")]
484fn qp_offset(bit_depth: u32) -> i32 {
485    6 * (bit_depth as i32 - 8)
486}
487
488/// The rate an x264 session is opened at: the capture's, or 30 frames per second where it names
489/// less than one.
490#[cfg(feature = "gpl")]
491fn x264_frame_rate(fps: f64) -> FrameRate {
492    FrameRate::of(if fps < 1.0 { 30.0 } else { fps })
493}
494
495/// Quantizer steps a halving of a capped CRF's key frame is taken to need where it is aimed into
496/// the bucket's room (`H264EncoderWrapper::aim_key`, `fit_key`), an encode at most aimed: text
497/// keeps 17 steps a halving between quantizers 12 and 35 (10.1 to 4.1 Mbit at 1280x720) where
498/// the quantizer's own scale says 6, so an aim of 6 lands a second attempt past the room.
499#[cfg(feature = "gpl")]
500const KEY_STEPS: f64 = 10.0;
501
502/// Quantizer steps finer than the paint-over one a capped CRF codes a block owed its paint-over
503/// at (`ledger::BlockLedger::refresh_owed`): a refinement's, and two more, the predicted frame
504/// over a coarser picture landing short of it: a 720p page under a 4 Mbit/s peak came to
505/// 48.1 dB at one more and 49.4 at two, where the uncapped session's paint-over leaves it at
506/// 48.5.
507#[cfg(feature = "gpl")]
508const OWED_FINER: u32 = crate::ledger::REFINE_FINER + 2;
509
510/// Quality index steps finer than the session's own a capped CRF refreshes its coarse blocks at
511/// (`ledger::BlockLedger::refresh`), one past `ledger::REFRESH_FINER`: refreshing a 720p page of
512/// text it coded at quantizer 51, x264 came to 37.7 dB three steps finer, 0.9 dB under the
513/// uncapped session's on one to three slice threads, and to 38.5-38.6 four steps finer, within
514/// 0.3 dB of it on any, for 3 % more bytes. The uncapped session's own picture moves with its
515/// slice threads (37.9-38.8 dB) rather than with the quantizers its scroll ends at, which are
516/// coarser than the refresh's (26-29 against 22).
517#[cfg(feature = "gpl")]
518const X264_REFRESH_FINER: u32 = crate::ledger::REFRESH_FINER + 1;
519
520/// One long-lived libx264 session for a frame, holding the raw `x264_t` handle alongside a
521/// mirror of its live parameters so the encoder can be retuned per frame instead of rebuilt.
522///
523/// Rebuilding an x264 encoder is expensive and forces a fresh IDR, so a session keeps its instance
524/// across frames and only nudges CRF, bitrate, VBV, and frame rate live; the tracked `current_*`
525/// fields are that mirror, letting a reconfigure skip the FFI call whenever nothing actually changed.
526/// `is_i444` (4:4:4 vs 4:2:0) is baked into the encoder's color space at open, so a change to it is
527/// one of the few things that forces a full rebuild; `is_cbr` records which rate-control mode was
528/// chosen at open and gates which of the live reconfigures apply. The manual `Send` impl exists only
529/// because a raw pointer is not `Send` by default and the handle must move onto the encode
530/// thread; `Drop` closes it under the global open/close lock for the same reason that lock exists.
531#[cfg(feature = "gpl")]
532pub struct H264EncoderWrapper {
533    encoder: *mut codec_sys::x264::x264_t,
534    pub width: i32,
535    pub height: i32,
536    current_crf: i32,
537    pub is_i444: bool,
538    /// The bits per sample the session was opened at, baked in as the chroma format is.
539    pub bit_depth: u32,
540    is_cbr: bool,
541    current_bitrate: i32,
542    current_vbv: i32,
543    current_fps: FrameRate,
544    /// Open-time parameters retained so a frame-rate change can reopen the session: x264's live
545    /// reconfigure cannot alter the frame rate, and CBR/VBV budgets are derived from it.
546    threads: i32,
547    min_qp: i32,
548    max_qp: i32,
549    /// The frames the decoder holds, so a lost one can be left out of the predictions and
550    /// each frame can name what it predicts from.
551    references: ReferenceWindow,
552    /// The most frames the decoded picture buffer keeps, 0 for the level's
553    /// (`video_reference_frames`), kept for a reopen.
554    reference_frames: u32,
555    last_reference: Reference,
556    /// The stream's `frame_num`, widened from x264's sixteen values to 4096.
557    frame_num: WideFrameNum,
558    /// The floor the rate control's quantizer stands at now (`reconfigure_floor`), on the
559    /// H.264 scale; 0 is x264's own.
560    current_min_qp: i32,
561    /// The quantizer the next frame is held at whatever the rate control (`hold_quantizer`).
562    held_qp: Option<i32>,
563    /// The mean quantizer the rate control last coded a frame's rows at, held frames aside.
564    last_qp: Option<u32>,
565    /// The live rate factor, which x264 reports a frame's mean quantizer against (`last_qp`).
566    rf_constant: f64,
567    /// A capped CRF session's peak (`PeakOffset`): x264's own cap, a VBV, changes how it codes
568    /// every frame whether the peak binds or not (a 720p scroll's key frame at quantizer 27 for 22
569    /// under a 100 Mbit/s peak it never reached), and is taken only at open, so the session opens
570    /// uncapped and the peak is held beside it. A key frame is aimed at half the bucket's room,
571    /// as x264's VBV holds a frame to half its buffer's fill: from the start where the last key
572    /// frame says it would not fit (`H264EncoderWrapper::aim_key`), else by one encode more where
573    /// its first comes out past the room (`H264EncoderWrapper::fit_key`). The quantizer ceiling it
574    /// opened with, which
575    /// the peak lowers to 51 while it binds.
576    peak: Option<PeakOffset>,
577    open_qp_max: i32,
578    /// The frame the last encode coded (`Coded`), and whether it was coded coarser than the
579    /// rate factor for the peak (`coded_capped`).
580    coded: Option<Coded>,
581    last_capped: bool,
582    /// Whether the peak has bound a frame of the session (`holds_cleanup`).
583    bound_once: bool,
584    /// The last key frame's first attempt (`aim_key`).
585    last_key: Option<LastKey>,
586    /// The pictures x264 has been handed, attempts again included.
587    #[cfg(test)]
588    encodes: usize,
589    /// The quantizer offset of each macroblock of the next frame, empty where it has none
590    /// (`refine_blocks`).
591    quant_offsets: Vec<f32>,
592    /// The next frame's macroblocks that did not change (`constant_blocks`), as x264's
593    /// `mb_info` flags; empty where the source cannot say.
594    mb_info: Vec<u8>,
595    /// The picture x264 still reads after its last encode (`held_input`).
596    held_input: Option<*const u8>,
597}
598
599/// A key frame's first attempt (`H264EncoderWrapper::aim_key`): its bits, the quantizer it was
600/// planned or held at, the rate factor it was coded under, and whether it was held there.
601#[cfg(feature = "gpl")]
602#[derive(Clone, Copy, Debug)]
603struct LastKey {
604    bits: f64,
605    qp: f64,
606    rf: f64,
607    held: bool,
608}
609
610/// A frame x264 coded: its bits, the quantizer it was planned or held at, whether it was a key
611/// frame, and whether it was held (`hold_quantizer`).
612#[cfg(feature = "gpl")]
613#[derive(Clone, Copy, Debug)]
614struct Coded {
615    bits: f64,
616    qp: f64,
617    key: bool,
618    held: bool,
619}
620
621/// The planes of a picture handed to x264: 4:2:0 or 4:4:4 planes of either depth, each its
622/// stride in bytes, or an 8-bit 4:2:0 picture as NV12.
623#[cfg(feature = "gpl")]
624#[derive(Clone, Copy)]
625enum Input<'a> {
626    Planar {
627        planes: [&'a [u8]; 3],
628        strides: [i32; 3],
629    },
630    Nv12(&'a Nv12Planes),
631}
632
633#[cfg(feature = "gpl")]
634unsafe impl Send for H264EncoderWrapper {}
635
636#[cfg(feature = "gpl")]
637impl Drop for H264EncoderWrapper {
638    fn drop(&mut self) {
639        if !self.encoder.is_null() {
640            let _guard = X264_OPEN_CLOSE_LOCK
641                .lock()
642                .unwrap_or_else(|e| e.into_inner());
643            unsafe { codec_sys::x264::x264_encoder_close(self.encoder) };
644            self.encoder = ptr::null_mut();
645        }
646    }
647}
648
649#[cfg(feature = "gpl")]
650impl H264EncoderWrapper {
651    /// Open an x264 encoder tuned for real-time screen streaming, or `None` on failure.
652    ///
653    /// **Why this configuration.** These frames are captured live and must ship immediately, so the
654    /// encoder is optimized for latency over compression ratio: the `ultrafast` preset keeps encode
655    /// time under the frame budget, and `zerolatency` bars the frame reordering and lookahead
656    /// buffering that would otherwise add pipeline delay. Everything below then bends x264 toward the
657    /// pipeline's own keyframe and color model instead of its broadcast-oriented defaults.
658    ///
659    /// 1. **Preset/tune**: starts from the `ultrafast` preset with the `zerolatency` tune, then
660    ///    overrides resolution, frame rate (floored to 30 fps when under 1), and thread count.
661    /// 2. **Infinite GOP**: `i_keyint_max` is set to x264's infinite sentinel and adaptive scene-cut
662    ///    is disabled (`i_scenecut_threshold = 0`), so the encoder never injects an unrequested IDR
663    ///    on a scene change — keyframes are purely on-demand via the forced-IDR path, matching the
664    ///    strict infinite-GOP model.
665    /// 3. **Rate control**:
666    ///    - **CBR** (`cbr_mode`): ABR targeting `bitrate_kbps` with a VBV cap pinned to the same
667    ///      value (buffer `vbv_kbit`, precomputed by the caller from the frame-time multiplier
668    ///      policy) and filler disabled. `max_qp` is the legibility floor (caps how ugly a
669    ///      rate-starved frame gets) and `min_qp` the waste ceiling (stops over-spending on easy
670    ///      content); both are clamped to 51, and the ceiling is 51 when unset because x264's
671    ///      own default admits out-of-spec quantizers above it that exist only to force skips
672    ///      when the VBV underflows, which leaves rows of the picture frozen on old content. A
673    ///      budget the content cannot meet overshoots instead, as NVENC and libvpx do.
674    ///    - **CRF** (default): constant-quality with `f_rf_constant = crf`. A positive
675    ///      `vbv_kbit` and `bitrate_kbps` cap it at that peak with a buffer of that size, held
676    ///      beside x264 (`PeakOffset`): the session opens as an uncapped one, and where the peak
677    ///      binds the rate factor rises, up to 51, so content the cap starves overshoots it
678    ///      rather than freezes. A `vbv_kbit` or `bitrate_kbps` of 0 leaves it uncapped.
679    /// 4. **Color**: I444 at full range or I420 at limited range, a VUI declaring that range
680    ///    with the BT.709 primaries, transfer, and matrix the sRGB source and the conversion
681    ///    carry, and the matching `high444` / `baseline` profile.
682    /// 5. **Coding tools**: CABAC and the 8x8 transform are disabled, matching the low-latency
683    ///    baseline profile — CAVLC entropy coding with no 8x8 DCT — for minimal encode cost.
684    ///    Motion search is the uneven multi-hexagon one over 64 pixels in place of `ultrafast`'s
685    ///    diamond over 16, which misses a scroll or a moved window wherever its starting
686    ///    prediction is poor (a slice's first row, the edge of a still region) and codes the
687    ///    shifted pixels as new: at 2560x1600 it took a scrolling full-screen change from 5.0 to
688    ///    3.5 Mbit/s at the same PSNR for 0.14 ms more of encode, and a quarter of the screen from
689    ///    5.0 to 4.4 for 0.09 ms, the search running only on the macroblocks `mb_info` leaves.
690    /// 6. **Output**: repeated headers (SPS/PPS before each keyframe) and Annex-B framing, with
691    ///    x264's own logging silenced.
692    /// 7. **References**: a decoded picture buffer of `REFERENCE_FRAMES` (`i_dpb_size`), or of
693    ///    `reference_frames` where the caller caps it (`video_reference_frames`), declaring
694    ///    the lowest level up to 5.2 that holds them (`h264_reference_level`: 5.0 at 1080p, where
695    ///    4.2 holds four), so `invalidate_reference` can leave a frame a client lost out of the
696    ///    predictions with earlier frames still there to predict from, for a report up to eight
697    ///    frames late (133 ms at 60 fps). Motion search keeps its single reference.
698    /// 8. **`frame_num`**: x264 counts it in sixteen values for that buffer, and a loss covering
699    ///    the frame where it wraps costs a key frame (`ReferenceWindow`), so the stream carries it
700    ///    a byte wider (`WideFrameNum`): 4096 values, one such frame in 68 s at 60 fps.
701    ///
702    /// The `x264_encoder_open` call is serialized under `X264_OPEN_CLOSE_LOCK` because it mutates
703    /// libx264 global state.
704    #[allow(clippy::too_many_arguments)]
705    pub fn new(
706        width: i32,
707        height: i32,
708        crf: i32,
709        is_i444: bool,
710        fps: f64,
711        threads: i32,
712        cbr_mode: bool,
713        bitrate_kbps: i32,
714        vbv_kbit: i32,
715        min_qp: i32,
716        max_qp: i32,
717    ) -> Option<Self> {
718        Self::open(
719            width,
720            height,
721            crf,
722            is_i444,
723            8,
724            fps,
725            threads,
726            cbr_mode,
727            bitrate_kbps,
728            vbv_kbit,
729            min_qp,
730            max_qp,
731            0,
732            None,
733        )
734    }
735
736    /// `new` at `bit_depth` bits per sample: 10 opens a High 10 session, or High 4:4:4
737    /// Predictive at 10 bits, which reads its planes as 16-bit samples; its decoded picture
738    /// buffer keeps at most `reference_frames`, 0 for the level's (`video_reference_frames`).
739    #[allow(clippy::too_many_arguments)]
740    pub fn with_depth(
741        width: i32,
742        height: i32,
743        crf: i32,
744        is_i444: bool,
745        bit_depth: u32,
746        fps: f64,
747        threads: i32,
748        cbr_mode: bool,
749        bitrate_kbps: i32,
750        vbv_kbit: i32,
751        min_qp: i32,
752        max_qp: i32,
753        reference_frames: u32,
754    ) -> Option<Self> {
755        Self::open(
756            width,
757            height,
758            crf,
759            is_i444,
760            bit_depth,
761            fps,
762            threads,
763            cbr_mode,
764            bitrate_kbps,
765            vbv_kbit,
766            min_qp,
767            max_qp,
768            reference_frames,
769            None,
770        )
771    }
772
773    /// Whether the linked libx264 opens a 10-bit session, which is a property of how the
774    /// library was built, asked once of a session too small to cost anything.
775    pub fn ten_bit() -> bool {
776        static TEN_BIT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
777        *TEN_BIT.get_or_init(|| {
778            Self::with_depth(64, 64, 25, false, 10, 30.0, 1, false, 0, 0, 0, 0, 0).is_some()
779        })
780    }
781
782    /// `new`, with a constant-rate session's first frame given a budget of its own where `key`
783    /// names one, `(budget_kbit, level_idc)`: the rate is that budget a frame and the VBV buffer
784    /// that budget, full, until `restore_rate`, and the level is pinned to the one the session it
785    /// replaces declared, which the raised rate would otherwise lift.
786    #[allow(clippy::too_many_arguments)]
787    fn open(
788        width: i32,
789        height: i32,
790        crf: i32,
791        is_i444: bool,
792        bit_depth: u32,
793        fps: f64,
794        threads: i32,
795        cbr_mode: bool,
796        bitrate_kbps: i32,
797        vbv_kbit: i32,
798        min_qp: i32,
799        max_qp: i32,
800        reference_frames: u32,
801        key: Option<(i32, i32)>,
802    ) -> Option<Self> {
803        unsafe {
804            let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
805            let preset = CString::new("ultrafast").unwrap();
806            let tune = CString::new("zerolatency").unwrap();
807
808            if codec_sys::x264::x264_param_default_preset(
809                &mut param,
810                preset.as_ptr(),
811                tune.as_ptr(),
812            ) < 0
813            {
814                return None;
815            }
816
817            let frame_rate = x264_frame_rate(fps);
818            param.i_width = width;
819            param.i_height = height;
820            param.i_fps_num = frame_rate.num;
821            param.i_fps_den = frame_rate.den;
822            param.i_keyint_max = codec_sys::x264::X264_KEYINT_MAX_INFINITE as i32;
823            param.i_scenecut_threshold = 0;
824            let bitrate_bps = if cbr_mode {
825                bitrate_kbps.saturating_abs() as u64 * 1000
826            } else {
827                0
828            };
829            let dpb = h264_dpb_frames(
830                h264_reference_level(
831                    h264_level(width as u32, height as u32, frame_rate.ceil(), bitrate_bps),
832                    width as u32,
833                    height as u32,
834                ),
835                width as u32,
836                height as u32,
837            );
838            let dpb = match reference_frames {
839                0 => dpb,
840                n => dpb.min(n),
841            };
842            param.i_dpb_size = dpb as i32;
843            if cbr_mode {
844                let bk = bitrate_kbps.saturating_abs();
845                param.rc.i_rc_method = codec_sys::x264::X264_RC_ABR as i32;
846                param.rc.i_bitrate = bk;
847                param.rc.i_vbv_max_bitrate = bk;
848                param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
849                if let Some((budget, level)) = key {
850                    let rate = ((f64::from(budget) * frame_rate.fps()) as i32).max(bk);
851                    param.rc.i_bitrate = rate;
852                    param.rc.i_vbv_max_bitrate = rate;
853                    param.rc.i_vbv_buffer_size = budget.max(1);
854                    param.rc.f_vbv_buffer_init = 1.0;
855                    param.i_level_idc = level;
856                }
857                param.rc.b_filler = 0;
858                if min_qp > 0 {
859                    param.rc.i_qp_min = min_qp.min(51) + qp_offset(bit_depth);
860                }
861                param.rc.i_qp_max =
862                    if max_qp > 0 { max_qp.min(51) } else { 51 } + qp_offset(bit_depth);
863            } else {
864                param.rc.i_rc_method = codec_sys::x264::X264_RC_CRF as i32;
865                param.rc.f_rf_constant = crf as f32;
866            }
867            let depth_flag = if bit_depth > 8 {
868                codec_sys::x264::X264_CSP_HIGH_DEPTH
869            } else {
870                0
871            };
872            param.i_csp = (if is_i444 {
873                codec_sys::x264::X264_CSP_I444
874            } else {
875                codec_sys::x264::X264_CSP_I420
876            } | depth_flag) as i32;
877            param.i_bitdepth = bit_depth as i32;
878            param.vui.b_fullrange = if is_i444 { 1 } else { 0 };
879            param.vui.i_colorprim = 1;
880            param.vui.i_transfer = 1;
881            param.vui.i_colmatrix = 1;
882
883            let profile = CString::new(match (is_i444, bit_depth > 8) {
884                (true, _) => "high444",
885                (false, true) => "high10",
886                (false, false) => "baseline",
887            })
888            .unwrap();
889            codec_sys::x264::x264_param_apply_profile(&mut param, profile.as_ptr());
890            // A patched x264 applies the per-block offsets as given (`patches/x264`); an
891            // unpatched one weighs them against its adaptive quantization.
892            #[cfg(x264_qp_offsets)]
893            {
894                param.rc.b_qp_offsets = 1;
895            }
896            param.analyse.b_mb_info = 1;
897            param.analyse.i_me_method = codec_sys::x264::X264_ME_UMH as i32;
898            param.analyse.i_me_range = 64;
899
900            param.i_threads = threads;
901            param.b_repeat_headers = 1;
902            param.b_annexb = 1;
903            param.i_log_level = codec_sys::x264::X264_LOG_NONE;
904
905            let encoder = {
906                let _guard = X264_OPEN_CLOSE_LOCK
907                    .lock()
908                    .unwrap_or_else(|e| e.into_inner());
909                codec_sys::x264::x264_encoder_open(&mut param)
910            };
911            if encoder.is_null() {
912                None
913            } else {
914                codec_sys::x264::x264_encoder_parameters(encoder, &mut param);
915                Some(Self {
916                    encoder,
917                    width,
918                    height,
919                    current_crf: crf,
920                    is_i444,
921                    bit_depth,
922                    is_cbr: cbr_mode,
923                    current_bitrate: bitrate_kbps.saturating_abs(),
924                    current_vbv: if cbr_mode { vbv_kbit } else { 0 },
925                    current_fps: frame_rate,
926                    threads,
927                    min_qp,
928                    max_qp,
929                    references: ReferenceWindow::new(dpb),
930                    reference_frames,
931                    last_reference: Reference::Untracked,
932                    frame_num: WideFrameNum::default(),
933                    current_min_qp: min_qp,
934                    held_qp: None,
935                    last_qp: None,
936                    rf_constant: f64::from(param.rc.f_rf_constant),
937                    peak: (!cbr_mode && vbv_kbit > 0 && bitrate_kbps != 0).then(|| {
938                        PeakOffset::new(bitrate_kbps.saturating_abs(), vbv_kbit, frame_rate.fps())
939                    }),
940                    open_qp_max: param.rc.i_qp_max,
941                    coded: None,
942                    last_capped: false,
943                    bound_once: false,
944                    last_key: None,
945                    #[cfg(test)]
946                    encodes: 0,
947                    quant_offsets: Vec::new(),
948                    mb_info: Vec::new(),
949                    held_input: None,
950                })
951            }
952        }
953    }
954
955    /// Retune the constant-quality CRF on the running encoder, so a quality change costs a
956    /// parameter push rather than tearing down and rebuilding the session (a rebuild would force an
957    /// IDR and drop encoder state).
958    ///
959    /// It is a no-op in CBR mode, where rate is bitrate-controlled and CRF simply does not apply, and
960    /// a no-op when the value is unchanged — the tracked `current_crf` is what makes that cheap
961    /// early-out possible. Otherwise it reads the encoder's live parameters, overwrites
962    /// `f_rf_constant`, and pushes the change via `x264_encoder_reconfig`, advancing the tracked CRF
963    /// only once the reconfig has actually succeeded so the mirror never drifts from the encoder.
964    pub fn reconfigure_crf(&mut self, new_crf: i32) {
965        if self.is_cbr || self.current_crf == new_crf {
966            return;
967        }
968        let offset = self.peak.map_or(0.0, |p| p.offset);
969        if self.set_rate_factor(new_crf, offset) {
970            self.current_crf = new_crf;
971        }
972    }
973
974    /// Put the rate factor at `crf` plus a peak's `offset`, the quantizer ceiling at 51 while an
975    /// offset holds and at the session's own otherwise; true where x264 took it.
976    fn set_rate_factor(&mut self, crf: i32, offset: f64) -> bool {
977        unsafe {
978            let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
979            codec_sys::x264::x264_encoder_parameters(self.encoder, &mut param);
980            param.rc.f_rf_constant = (f64::from(crf) + offset) as f32;
981            param.rc.i_qp_max = if offset > 0.0 {
982                51 + qp_offset(self.bit_depth)
983            } else {
984                self.open_qp_max
985            };
986            let taken = codec_sys::x264::x264_encoder_reconfig(self.encoder, &mut param) == 0;
987            if taken {
988                self.rf_constant = f64::from(param.rc.f_rf_constant);
989            }
990            taken
991        }
992    }
993
994    /// Retune bitrate/VBV (CBR, or a capped CRF's peak) and/or frame rate to match the live settings,
995    /// structured to be called unconditionally every frame so the caller need not track what
996    /// changed itself.
997    ///
998    /// Because `encode_cpu` fires it on every frame, it first computes the would-be values and bails
999    /// before touching the encoder when neither the bitrate/VBV nor the frame rate differs from
1000    /// what is live — that self-gating keeps a per-frame call nearly free.
1001    ///
1002    /// A frame-rate change reopens the encoder rather than reconfiguring it: `x264_encoder_reconfig`
1003    /// does not apply `i_fps_*`, and the CBR/VBV per-frame budget is `bitrate / fps`, so a session
1004    /// left at its old rate ships roughly half the configured bitrate once fps halves. The reopen
1005    /// carries the new bitrate/VBV too, and a fresh session emits an IDR on its first frame; a failed
1006    /// reopen keeps the working session instead of nulling the handle. A CRF session's peak, set,
1007    /// moved, or lifted, never reaches x264 (`follow_peak`). A CBR bitrate/VBV-only change
1008    /// otherwise stays a live `x264_encoder_reconfig`, and the tracked mirror advances only on
1009    /// success so it cannot drift from the encoder's real state.
1010    pub fn reconfigure_rate(&mut self, bitrate_kbps: i32, vbv_kbit: i32, fps: f64) {
1011        let bk = bitrate_kbps.saturating_abs();
1012        let new_fps = x264_frame_rate(fps);
1013        if !self.is_cbr {
1014            self.follow_peak(bk, vbv_kbit, new_fps.fps());
1015        }
1016        let rate_changed =
1017            self.is_cbr && (self.current_bitrate != bk || self.current_vbv != vbv_kbit);
1018        let reopen = self.current_fps != new_fps;
1019        if !rate_changed && !reopen {
1020            return;
1021        }
1022        if reopen {
1023            if let Some(fresh) = H264EncoderWrapper::with_depth(
1024                self.width,
1025                self.height,
1026                self.current_crf,
1027                self.is_i444,
1028                self.bit_depth,
1029                new_fps.fps(),
1030                self.threads,
1031                self.is_cbr,
1032                bk,
1033                vbv_kbit,
1034                self.min_qp,
1035                self.max_qp,
1036                self.reference_frames,
1037            ) {
1038                // The bucket carries over to the key frame that opens it.
1039                let peak = self.peak;
1040                *self = fresh;
1041                if let (Some(fresh), Some(peak)) = (self.peak.as_mut(), peak) {
1042                    fresh.fill = peak.fill;
1043                }
1044            }
1045            return;
1046        }
1047        unsafe {
1048            let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
1049            codec_sys::x264::x264_encoder_parameters(self.encoder, &mut param);
1050            param.rc.i_bitrate = bk;
1051            param.rc.i_vbv_max_bitrate = bk;
1052            param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
1053            if codec_sys::x264::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
1054                self.current_bitrate = bk;
1055                self.current_vbv = vbv_kbit;
1056            }
1057        }
1058    }
1059
1060    /// The frame the last encoded frame predicted from.
1061    pub fn last_reference(&self) -> Reference {
1062        self.last_reference
1063    }
1064
1065    /// The mean quantizer the rate control last coded a frame's rows at, held frames aside.
1066    pub fn last_qp(&self) -> Option<u32> {
1067        self.last_qp
1068    }
1069
1070    /// Whether the peak of a capped CRF session binds it: the rate factor stands above the
1071    /// session's own (`PeakOffset`).
1072    pub fn peak_bound(&self) -> bool {
1073        self.peak.is_some_and(|p| p.offset > 0.0)
1074    }
1075
1076    /// Whether the session runs its CRF under a peak (`PeakOffset`).
1077    pub fn capped(&self) -> bool {
1078        self.peak.is_some()
1079    }
1080
1081    /// A tick that codes no frame, under the live `settings`: the peak follows them and its
1082    /// bucket drains as for an empty frame, the rate factor stepping back toward the session's
1083    /// own, so a still screen lets the peak go.
1084    pub fn idle(&mut self, settings: &CaptureSettings) {
1085        if !self.is_cbr {
1086            let (bitrate, vbv) = rate_control(settings);
1087            self.follow_peak(bitrate, vbv, x264_frame_rate(settings.target_fps).fps());
1088        }
1089        if self.peak.is_some() {
1090            self.fold_frame(Coded {
1091                bits: 0.0,
1092                qp: self.rf_constant,
1093                key: false,
1094                held: false,
1095            });
1096        }
1097    }
1098
1099    /// Whether the last frame was coded coarser than the session's rate factor for its peak:
1100    /// under a raised rate factor, or a key frame coded again within the bucket (`fit_key`).
1101    pub fn coded_capped(&self) -> bool {
1102        self.last_capped
1103    }
1104
1105    /// Code the next frame as a refresh of coarse blocks (`ledger::BlockLedger::refresh_keyed`):
1106    /// held at quantizer `qp` where one is named, the macroblocks `now` names `finer` steps finer
1107    /// (`X264_REFRESH_FINER`), the ones of them a key frame left coarse (`key`) `key_finer`
1108    /// steps finer, and the ones `wait` names past the coarsest quantizer, where x264 codes a
1109    /// block its reference holds (`constant_blocks`) as a skip.
1110    #[allow(clippy::too_many_arguments)]
1111    pub fn refresh_blocks(
1112        &mut self,
1113        qp: Option<i32>,
1114        now: &crate::ledger::BlockPlan,
1115        finer: f32,
1116        key: &crate::ledger::BlockPlan,
1117        key_finer: f32,
1118        wait: &crate::ledger::BlockPlan,
1119    ) {
1120        if let Some(qp) = qp {
1121            self.hold_quantizer(qp);
1122        }
1123        let cols = (self.width as usize).div_ceil(16);
1124        let rows = (self.height as usize).div_ceil(16);
1125        self.quant_offsets.clear();
1126        self.quant_offsets.extend((0..rows).flat_map(|row| {
1127            (0..cols).map(move |col| {
1128                if key.refines(col, row) {
1129                    -key_finer
1130                } else if now.refines(col, row) {
1131                    -finer
1132                } else if wait.refines(col, row) {
1133                    51.0
1134                } else {
1135                    0.0
1136                }
1137            })
1138        }));
1139    }
1140
1141    /// Cap a CRF session at a peak of `bitrate_kbps` with a buffer of `vbv_kbit`, or lift its cap
1142    /// where either is 0, live and outside x264 (`PeakOffset`).
1143    fn follow_peak(&mut self, bitrate_kbps: i32, vbv_kbit: i32, fps: f64) {
1144        self.current_bitrate = bitrate_kbps;
1145        match (self.peak.as_mut(), vbv_kbit > 0 && bitrate_kbps > 0) {
1146            (Some(peak), true) => peak.follow(bitrate_kbps, vbv_kbit, fps),
1147            (None, true) => self.peak = Some(PeakOffset::new(bitrate_kbps, vbv_kbit, fps)),
1148            (Some(peak), false) => {
1149                let bound = peak.offset > 0.0;
1150                self.peak = None;
1151                if bound {
1152                    self.set_rate_factor(self.current_crf, 0.0);
1153                }
1154            }
1155            (None, false) => {}
1156        }
1157    }
1158
1159    /// Move the floor of a constant-rate session's quantizer live, `min_qp` on the H.264 scale
1160    /// and 0 for x264's own, through `x264_encoder_reconfig` with the bounds the carried patch
1161    /// lets it take. A floor at the paint-over quality while the screen moves holds the rate
1162    /// control from spending a generous budget on coding motion finer than the still picture
1163    /// it leaves (a 1440p scroll at 30 Mbit/s: 22.3 Mbit/s at 57 dB against 13.5 at 46 dB),
1164    /// lifted while a still screen is cleaned up, which then reaches what the rate control
1165    /// gives it; the blocks a moving frame refines code below the floor, as the patch keeps
1166    /// the caller's offsets. An x264 without the patches keeps the bounds it opened with.
1167    pub fn reconfigure_floor(&mut self, min_qp: i32) {
1168        if !self.is_cbr || min_qp == self.current_min_qp {
1169            return;
1170        }
1171        unsafe {
1172            let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
1173            codec_sys::x264::x264_encoder_parameters(self.encoder, &mut param);
1174            param.rc.i_qp_min = if min_qp > 0 {
1175                min_qp.min(51) + qp_offset(self.bit_depth)
1176            } else {
1177                0
1178            };
1179            if codec_sys::x264::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
1180                self.current_min_qp = min_qp;
1181            }
1182        }
1183    }
1184
1185    /// Encode the next frame at quantizer `qp` whatever the rate control, leaving the session's
1186    /// own for the frame after: x264's per-picture forced quantizer.
1187    ///
1188    /// Under a constant rate x264's row-level VBV control moves a forced quantizer back toward
1189    /// the rate control's own for the frame, so a held key frame there, a fresh start for the
1190    /// decoder anyway, starts a fresh session (`open_for_held_key`) whose rate control plans that
1191    /// frame with `HELD_KEY_BUDGET_S` of the target, which also bounds its size, and takes the
1192    /// session's own rate back for the frame after.
1193    pub fn hold_quantizer(&mut self, qp: i32) {
1194        self.held_qp = Some(qp.clamp(0, 51));
1195    }
1196
1197    /// Replace the session with one whose first frame, the held key frame about to be encoded,
1198    /// has the budget of `HELD_KEY_BUDGET_S` of the target; true where it opened, and
1199    /// `restore_rate` is owed after the frame.
1200    fn open_for_held_key(&mut self) -> bool {
1201        let level = unsafe {
1202            let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
1203            codec_sys::x264::x264_encoder_parameters(self.encoder, &mut param);
1204            param.i_level_idc
1205        };
1206        let budget = (f64::from(self.current_bitrate) * super::HELD_KEY_BUDGET_S).round() as i32;
1207        let fresh = Self::open(
1208            self.width,
1209            self.height,
1210            self.current_crf,
1211            self.is_i444,
1212            self.bit_depth,
1213            self.current_fps.fps(),
1214            self.threads,
1215            self.is_cbr,
1216            self.current_bitrate,
1217            self.current_vbv,
1218            self.min_qp,
1219            self.max_qp,
1220            self.reference_frames,
1221            Some((budget, level)),
1222        );
1223        let Some(mut fresh) = fresh else { return false };
1224        fresh.held_qp = self.held_qp;
1225        *self = fresh;
1226        true
1227    }
1228
1229    /// Code the macroblocks of the next frame that `plan` refines `offset` quantizer steps off
1230    /// the frame's own, and the rest at it: the ledger's refinement of blocks that held still
1231    /// while others move (`ledger`). The plan's blocks are the macroblocks of this session's
1232    /// picture.
1233    pub fn refine_blocks(&mut self, plan: &crate::ledger::BlockPlan, offset: f32) {
1234        let cols = (self.width as usize).div_ceil(16);
1235        let rows = (self.height as usize).div_ceil(16);
1236        self.quant_offsets.clear();
1237        self.quant_offsets.extend((0..rows).flat_map(|row| {
1238            (0..cols).map(move |col| if plan.refines(col, row) { offset } else { 0.0 })
1239        }));
1240    }
1241
1242    /// Mark the macroblocks of the next frame that did not change since the frame before
1243    /// (`unchanged`, in raster order), which x264 codes as skips without analysing them where
1244    /// its reference holds them at the frame's quantizer or finer.
1245    pub fn constant_blocks(&mut self, unchanged: impl Iterator<Item = bool>) {
1246        self.mb_info.clear();
1247        self.mb_info.extend(unchanged.map(|c| {
1248            if c {
1249                codec_sys::x264::X264_MBINFO_CONSTANT as u8
1250            } else {
1251                0
1252            }
1253        }));
1254    }
1255
1256    /// Put the session's own rate and VBV buffer back after a held key frame.
1257    fn restore_rate(&mut self) {
1258        unsafe {
1259            let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
1260            codec_sys::x264::x264_encoder_parameters(self.encoder, &mut param);
1261            param.rc.i_bitrate = self.current_bitrate;
1262            param.rc.i_vbv_max_bitrate = self.current_bitrate;
1263            param.rc.i_vbv_buffer_size = self.current_vbv.max(1);
1264            codec_sys::x264::x264_encoder_reconfig(self.encoder, &mut param);
1265        }
1266    }
1267
1268    /// Take a frame just coded into a capped CRF's peak, and move the rate factor where the
1269    /// peak says to.
1270    fn fold_frame(&mut self, frame: Coded) {
1271        let max_offset = 51.0 - f64::from(self.current_crf);
1272        let Some(peak) = self.peak.as_mut() else {
1273            return;
1274        };
1275        let was = peak.offset;
1276        let offset = peak.note(frame.bits, !frame.key && !frame.held, max_offset);
1277        if offset != was {
1278            self.set_rate_factor(self.current_crf, offset);
1279        }
1280    }
1281
1282    /// Leave frame `frame_id` and every frame after it out of the predictions. False when x264
1283    /// refuses, and the caller codes a key frame instead.
1284    pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
1285        match self.references.invalidate(frame_id) {
1286            Invalidation::Forget(pts) => unsafe {
1287                codec_sys::x264::x264_encoder_invalidate_reference(self.encoder, pts as i64) == 0
1288            },
1289            Invalidation::KeyFrame | Invalidation::Ignored => true,
1290        }
1291    }
1292
1293    /// Encode one YUV frame into H.264 and frame it for the wire, reporting whether the
1294    /// encoder actually emitted a bitstream this call.
1295    ///
1296    /// The boolean return is load-bearing: `x264_encoder_encode` can legitimately produce nothing on
1297    /// a given call, and the caller must forward a frame only when real bytes exist — never an empty
1298    /// or header-only packet. Framing is conditional because the transport needs the pipeline's small
1299    /// wire header to route the frame, while `omit_headers` consumers take the bare Annex-B
1300    /// elementary stream.
1301    ///
1302    /// 1. **Picture setup**: wraps the borrowed Y/U/V planes and their strides in an
1303    ///    `x264_picture_t` with the encoder's CSP, stamps the presentation timestamp with the
1304    ///    session's count of encoded frames (the one an invalidation names), and requests an IDR
1305    ///    when `force_idr` is set or no reference is left to predict from (otherwise
1306    ///    `X264_TYPE_AUTO`).
1307    /// 2. **Encode**: calls `x264_encoder_encode`; a non-positive returned size means no frame was
1308    ///    emitted this call, so the function returns `false` without writing output.
1309    /// 3. **Framing**: `output_buf` is cleared and refilled. Unless `omit_headers` is set, the wire
1310    ///    header is prepended with a frame kind read from the *actual* output picture type rather
1311    ///    than from `force_idr`, because the encoder may not honor a keyframe request and the
1312    ///    client keys its decode-recovery on the kind it truly received. With `omit_headers` the
1313    ///    output is bare Annex-B.
1314    /// 4. **Payload**: every NAL payload is appended to `output_buf` after the optional header,
1315    ///    its `frame_num` widened (`WideFrameNum`), so the bytes past the wire header are always a
1316    ///    contiguous Annex-B access unit.
1317    #[allow(clippy::too_many_arguments)]
1318    pub fn encode_with_headers(
1319        &mut self,
1320        y: &[u8],
1321        u: &[u8],
1322        v: &[u8],
1323        y_stride: i32,
1324        u_stride: i32,
1325        v_stride: i32,
1326        frame_id: u16,
1327        force_idr: bool,
1328        omit_headers: bool,
1329        output_buf: &mut Vec<u8>,
1330    ) -> bool {
1331        self.encode_input(
1332            Input::Planar {
1333                planes: [y, u, v],
1334                strides: [y_stride, u_stride, v_stride],
1335            },
1336            frame_id,
1337            force_idr,
1338            omit_headers,
1339            output_buf,
1340        )
1341    }
1342
1343    /// `encode_with_headers` for an 8-bit 4:2:0 session's NV12 picture, which x264 encodes in
1344    /// place where it is laid out for it: x264 reads it through the next encode, which takes it
1345    /// as the previous input, so it is left unchanged until that encode returns or until
1346    /// `release_input` (`held_input`).
1347    pub fn encode_nv12_with_headers(
1348        &mut self,
1349        planes: &Nv12Planes,
1350        frame_id: u16,
1351        force_idr: bool,
1352        omit_headers: bool,
1353        output_buf: &mut Vec<u8>,
1354    ) -> bool {
1355        self.encode_input(
1356            Input::Nv12(planes),
1357            frame_id,
1358            force_idr,
1359            omit_headers,
1360            output_buf,
1361        )
1362    }
1363
1364    /// The picture x264 still reads, as the previous input, after its last encode returned: the
1365    /// [`Nv12Planes::id`] of the last one it was handed in place, `None` where the last encode
1366    /// copied its picture.
1367    pub fn held_input(&self) -> Option<*const u8> {
1368        self.held_input
1369    }
1370
1371    /// Stop x264 reading the picture it holds (`held_input`), so its buffers can change.
1372    pub fn release_input(&mut self) {
1373        // Only a patched x264 holds an input (`x264_in_place`); an unpatched one copied it.
1374        #[cfg(x264_in_place)]
1375        unsafe {
1376            codec_sys::x264::x264_encoder_release_input(self.encoder)
1377        };
1378        self.held_input = None;
1379    }
1380
1381    fn encode_input(
1382        &mut self,
1383        input: Input<'_>,
1384        frame_id: u16,
1385        force_idr: bool,
1386        omit_headers: bool,
1387        output_buf: &mut Vec<u8>,
1388    ) -> bool {
1389        let key_budget =
1390            self.is_cbr && force_idr && self.held_qp.is_some() && self.open_for_held_key();
1391        self.last_capped = self.peak_bound();
1392        let key = force_idr || !self.references.has_reference();
1393        let aimed = (key && self.held_qp.is_none())
1394            .then(|| self.aim_key())
1395            .flatten();
1396        if let Some(q) = aimed {
1397            self.held_qp = Some(q as i32);
1398        }
1399        let rf = self.rf_constant;
1400        let mut coded = self.encode_picture(input, frame_id, force_idr, omit_headers, output_buf);
1401        if key_budget {
1402            self.restore_rate();
1403        }
1404        // The key frame's first attempt, as the rate factor coded it or as it was aimed.
1405        if let Some(frame) = self.coded.filter(|c| c.key) {
1406            self.last_key = Some(LastKey {
1407                bits: frame.bits,
1408                qp: frame.qp,
1409                rf,
1410                held: aimed.is_some(),
1411            });
1412        }
1413        let mut held = aimed;
1414        if coded && aimed.is_none() {
1415            (coded, held) = self.fit_key(input, frame_id, omit_headers, output_buf);
1416        }
1417        if let (Some(q), Some(peak)) = (held, self.peak.as_mut()) {
1418            // The rate factor stands at the key frame's quantizer from here, so the frames after
1419            // it refine the picture as the peak lets them rather than code it again whole.
1420            self.last_capped = true;
1421            let crf = f64::from(self.current_crf);
1422            let offset = (q - crf).clamp(peak.offset, (51.0 - crf).max(0.0));
1423            if offset != peak.offset {
1424                peak.offset = offset;
1425                self.set_rate_factor(self.current_crf, offset);
1426            }
1427        }
1428        self.bound_once |= self.last_capped;
1429        if let Some(frame) = self.coded.take() {
1430            self.fold_frame(frame);
1431        }
1432        coded
1433    }
1434
1435    /// Whether the bucket of a capped CRF's peak has room in one frame for a cleanup at rate
1436    /// factor `crf` (`encode_cpu_from`): for as much as the last key frame's first attempt
1437    /// (`LastKey`) would come to at it, a cleanup of a still page coding much of it again, and
1438    /// after a refresh held at a quantizer all of it. True uncapped, on a session no peak has
1439    /// bound, which codes as an uncapped one, or before a key frame.
1440    pub fn holds_cleanup(&self, crf: i32) -> bool {
1441        let (Some(peak), Some(last)) = (self.peak.filter(|_| self.bound_once), self.last_key)
1442        else {
1443            return true;
1444        };
1445        let from = if last.held { last.qp } else { last.rf };
1446        last.bits * 2f64.powf((from - f64::from(crf)) / KEY_STEPS) <= peak.room()
1447    }
1448
1449    /// The quantizer a capped CRF's key frame is held at from the start, where the last key
1450    /// frame's first attempt (`LastKey`) says one at the rate factor now would overflow the room
1451    /// the bucket has left: aimed at half that room, at `KEY_STEPS` quantizer steps a halving
1452    /// (`PeakOffset`), one encode. `None` where it would fit, or where no key frame came before,
1453    /// whose first attempt `fit_key` measures instead.
1454    fn aim_key(&self) -> Option<f64> {
1455        let room = self.peak?.room().max(1.0);
1456        let last = self.last_key?;
1457        // What the last key frame would have come to at the rate factor now: from the rate factor
1458        // it was coded under, or from the quantizer it was held at.
1459        let from = if last.held { last.qp } else { last.rf };
1460        let expected = last.bits * 2f64.powf((from - self.rf_constant) / KEY_STEPS);
1461        (expected > room).then(|| {
1462            (last.qp + KEY_STEPS * (2.0 * last.bits / room).log2())
1463                .ceil()
1464                .min(51.0)
1465        })
1466    }
1467
1468    /// Code a capped CRF's key frame that overflows the room its peak has left once more, held at
1469    /// a quantizer aimed at half that room from the first attempt's size, at `KEY_STEPS`
1470    /// quantizer steps a halving (`PeakOffset`); the second attempt is the one that goes out,
1471    /// overflowing or not, so a key frame costs at most two encodes. Returns whether the frame
1472    /// coded, and the quantizer it was held at where it was coded again.
1473    fn fit_key(
1474        &mut self,
1475        input: Input<'_>,
1476        frame_id: u16,
1477        omit_headers: bool,
1478        output_buf: &mut Vec<u8>,
1479    ) -> (bool, Option<f64>) {
1480        let Some(room) = self.peak.map(|p| p.room().max(1.0)) else {
1481            return (true, None);
1482        };
1483        let Some(first) = self.coded.filter(|c| c.key && c.bits > room && c.qp < 51.0) else {
1484            return (true, None);
1485        };
1486        let q = (first.qp + KEY_STEPS * (2.0 * first.bits / room).log2())
1487            .ceil()
1488            .min(51.0);
1489        self.held_qp = Some(q as i32);
1490        let coded = self.encode_picture(input, frame_id, true, omit_headers, output_buf);
1491        (coded, Some(q))
1492    }
1493
1494    /// `encode_input` past the choice of session.
1495    fn encode_picture(
1496        &mut self,
1497        input: Input<'_>,
1498        frame_id: u16,
1499        force_idr: bool,
1500        omit_headers: bool,
1501        output_buf: &mut Vec<u8>,
1502    ) -> bool {
1503        #[cfg(test)]
1504        {
1505            self.encodes += 1;
1506        }
1507        unsafe {
1508            let mut pic_in: codec_sys::x264::x264_picture_t = std::mem::zeroed();
1509            codec_sys::x264::x264_picture_init(&mut pic_in);
1510
1511            self.held_input = None;
1512            self.coded = None;
1513            match input {
1514                Input::Planar { planes, strides } => {
1515                    pic_in.img.i_csp = (if self.is_i444 {
1516                        codec_sys::x264::X264_CSP_I444
1517                    } else {
1518                        codec_sys::x264::X264_CSP_I420
1519                    } | if self.bit_depth > 8 {
1520                        codec_sys::x264::X264_CSP_HIGH_DEPTH
1521                    } else {
1522                        0
1523                    }) as i32;
1524                    pic_in.img.i_plane = 3;
1525                    for p in 0..3 {
1526                        pic_in.img.plane[p] = planes[p].as_ptr() as *mut u8;
1527                        pic_in.img.i_stride[p] = strides[p];
1528                    }
1529                }
1530                Input::Nv12(nv12) => {
1531                    // A picture is laid out in place only where the linked x264 named a layout.
1532                    #[cfg(x264_in_place)]
1533                    let in_place = if nv12.in_place {
1534                        codec_sys::x264::X264_CSP_IN_PLACE
1535                    } else {
1536                        0
1537                    };
1538                    #[cfg(not(x264_in_place))]
1539                    let in_place = 0;
1540                    pic_in.img.i_csp = (codec_sys::x264::X264_CSP_NV12 | in_place) as i32;
1541                    pic_in.img.i_plane = 2;
1542                    let (y, uv) = nv12.planes();
1543                    pic_in.img.plane[0] = y.as_ptr() as *mut u8;
1544                    pic_in.img.plane[1] = uv.as_ptr() as *mut u8;
1545                    pic_in.img.i_stride[0] = nv12.stride[0] as i32;
1546                    pic_in.img.i_stride[1] = nv12.stride[1] as i32;
1547                    self.held_input = nv12.in_place.then(|| nv12.id());
1548                }
1549            }
1550            pic_in.i_pts = self.references.next_pts() as i64;
1551            pic_in.i_type = if force_idr || !self.references.has_reference() {
1552                codec_sys::x264::X264_TYPE_IDR
1553            } else {
1554                codec_sys::x264::X264_TYPE_AUTO
1555            } as i32;
1556            let held = self.held_qp.take();
1557            let offset = qp_offset(self.bit_depth);
1558            pic_in.i_qpplus1 =
1559                held.map_or(codec_sys::x264::X264_QP_AUTO as i32, |q| q + offset + 1);
1560            // x264 reads the offsets as the frame is submitted, within this call.
1561            if !self.quant_offsets.is_empty() {
1562                pic_in.prop.quant_offsets = self.quant_offsets.as_mut_ptr();
1563            }
1564            let blocks = (self.width as usize).div_ceil(16) * (self.height as usize).div_ceil(16);
1565            if self.mb_info.len() == blocks {
1566                pic_in.prop.mb_info = self.mb_info.as_mut_ptr();
1567            }
1568
1569            let mut pic_out: codec_sys::x264::x264_picture_t = std::mem::zeroed();
1570            let mut nals: *mut codec_sys::x264::x264_nal_t = ptr::null_mut();
1571            let mut i_nals: i32 = 0;
1572
1573            let frame_size = codec_sys::x264::x264_encoder_encode(
1574                self.encoder,
1575                &mut nals,
1576                &mut i_nals,
1577                &mut pic_in,
1578                &mut pic_out,
1579            );
1580            self.quant_offsets.clear();
1581
1582            if frame_size > 0 {
1583                // x264 says the quantizer it planned the frame at, while its row-level control
1584                // codes the rows that would overrun the buffer coarser, up to 51: on a still screen
1585                // the plan falls to the paint-over quantizer and below with the rows still coarse.
1586                // `f_crf_avg` is the rate factor plus the rows' mean less the pre-VBV quantizer,
1587                // which the plan stands in for (never under it, so the mean never reads finer).
1588                if held.is_none() {
1589                    self.last_qp = (pic_out.i_qpplus1 > 0).then(|| {
1590                        let planned = f64::from(pic_out.i_qpplus1 - 1);
1591                        let coded = planned + pic_out.prop.f_crf_avg - self.rf_constant;
1592                        (coded.round() as i32 - offset).clamp(0, 51) as u32
1593                    });
1594                }
1595                let frame_type = if pic_out.i_type == codec_sys::x264::X264_TYPE_IDR as i32 {
1596                    FRAME_KEY
1597                } else if pic_out.i_type == codec_sys::x264::X264_TYPE_I as i32 {
1598                    FRAME_INTRA
1599                } else {
1600                    FRAME_DELTA
1601                };
1602                self.coded = Some(Coded {
1603                    bits: f64::from(frame_size) * 8.0,
1604                    qp: f64::from(pic_out.i_qpplus1 - 1 - offset),
1605                    key: frame_type == FRAME_KEY,
1606                    held: held.is_some(),
1607                });
1608                self.last_reference = self.references.record(frame_id, frame_type != FRAME_DELTA);
1609                output_buf.clear();
1610                output_buf.reserve(super::codec::VIDEO_HEADER_LEN + frame_size as usize);
1611                if !omit_headers {
1612                    push_video_header(
1613                        output_buf,
1614                        Codec::H264,
1615                        frame_type,
1616                        frame_id,
1617                        0,
1618                        self.width as u16,
1619                        self.height as u16,
1620                        self.last_reference,
1621                    );
1622                }
1623
1624                let nal_slice = std::slice::from_raw_parts(nals, i_nals as usize);
1625                let mut widened = true;
1626                for nal in nal_slice {
1627                    let payload = std::slice::from_raw_parts(nal.p_payload, nal.i_payload as usize);
1628                    widened &= self.frame_num.push(payload, output_buf);
1629                }
1630                if !widened {
1631                    // Its slices no longer match the set they went out under, so the next frame
1632                    // is a key frame.
1633                    self.references.reset();
1634                }
1635                if frame_type == FRAME_KEY {
1636                    let stream = &output_buf[if omit_headers {
1637                        0
1638                    } else {
1639                        super::codec::VIDEO_HEADER_LEN
1640                    }..];
1641                    self.references
1642                        .set_frame_num_range(h264_frame_num_range(stream).unwrap_or(0));
1643                }
1644                return true;
1645            }
1646        }
1647        false
1648    }
1649}
1650
1651/// Everything one region of the frame must remember between frames: its reused buffers, its own
1652/// live encoder, and the motion / paint-over / damage bookkeeping that drives its send decision.
1653///
1654/// A region is a JPEG stripe, the whole frame of a software H.264 session, or the frame of a
1655/// full-frame session (`pipeline::decide_hw_fullframe`). JPEG stripes encode in parallel and an
1656/// unchanged one is skipped on its own, which only works if each carries its *own* cross-frame
1657/// history. So one instance lives per region for the whole session and nothing in it is rebuilt or
1658/// recomputed from scratch each frame:
1659/// - **Reused buffers**: `nv12` holds an 8-bit 4:2:0 x264 frame's pictures, `y_buf` / `u_buf` /
1660///   `v_buf` and `yuv16` another frame's YUV planes, and `packet_buf` the encoded output, grown
1661///   in place rather than reallocated per frame.
1662/// - **Encoder**: `h264_encoder` is the software H.264 session — libx264 in a `gpl` build,
1663///   OpenH264 otherwise — reused until its geometry (or, for x264, chroma format) changes.
1664/// - **Cleanup / recovery**: `no_motion_frame_count` counts consecutive static frames,
1665///   `paint_over_sent` records that the region was cleaned up since it last changed,
1666///   `h264_burst_frames_remaining` tracks a post-cleanup or recovery streaming burst (held at the
1667///   paint-over quantizer where `burst_held`), and `dirty_run`, `change_mass`, `unclean_frames`,
1668///   and `motion` (the share of recent frames in motion) are what `pipeline::cleanup_due` weighs
1669///   to pick the cleanup's moment and kind; `clean_quality` keeps a constant-quality session at
1670///   the paint-over quality from a cleanup until the region changes again; `rc_bytes` (the bytes of
1671///   the region's last frame under its rate control), `rc_debt` (the bytes a cleanup's rate control
1672///   has spent past its target and not yet repaid), `idle_frames` (a constant-rate cleanup's run
1673///   of small frames short of the paint-over quality, of empty ones for x264), `same_frames` (of
1674///   frames the size of the one before), `finest` and `finest_frames` (the finest quality index
1675///   its rate control has coded at and the frames since), and `over_frames` (its run of frames over
1676///   `pipeline::OVERSHOOT_BUDGETS` and no finer than `finest`) tell a cleanup that runs through
1677///   the rate control when it has converged, stalled, or been pinned at its coarsest quantizer,
1678///   `fine_frames` counts its run of frames at the paint-over quantizer or finer, `measured`,
1679///   `measuring`, `level_checks`, and
1680///   `measure_in` are the last measurement of the picture it is refining, whether the frame before
1681///   was measured, the measurements in a row that found it level, and the frames to the next
1682///   (`pipeline::plateau`), `settled` records
1683///   that such a cleanup ran its course under a rate control it cannot hold a quantizer
1684///   under, so no other starts before the region moves, and `sweep` (the next band's start
1685///   and the last band's size, as shares of the picture) carries the band sweep a
1686///   full-frame refresh falls back to (`pipeline::decide_hw_fullframe`), `sweep_frames` its
1687///   bands so far, `sweep_coarser` the steps they are coded coarser than the refresh,
1688///   `sweep_held` whether the last frame was one of them, and `sweep_cost` the bytes the whole
1689///   picture would come to at the refresh's quantizer, by its bands so far (`sweep_first`, by its
1690///   first); `cleanup_debt` is the
1691///   bytes a cleanup's frames, the rate control's and the bands, came to past the cleanup's
1692///   budget a frame, which it waits out (`cleanup_paced`: the last frame was one of them).
1693/// - **Content-hash damage** (only for sources without external damage, i.e. X11): `last_hash` is
1694///   the previous frame's content hash, `consecutive_changes` counts changed frames toward the
1695///   damage-block threshold, and `in_damage_block` / `damage_block_frames_remaining` drive the
1696///   sustained-motion damage block managed by `content_dirty`.
1697#[derive(Default)]
1698pub struct StripeState {
1699    pub no_motion_frame_count: u32,
1700    pub paint_over_sent: bool,
1701    pub dirty_run: u32,
1702    pub change_mass: f32,
1703    /// Whether the region moved (`Damage::is_motion`) since its last cleanup.
1704    pub moved: bool,
1705    /// Whether a scheduled key frame fell due that waits for the region's next motion
1706    /// (`StripeState::scheduled_key`).
1707    pub key_owed: bool,
1708    pub unclean_frames: u32,
1709    /// Whether the last cleanup was a key frame (`pipeline::lost_frame` repeats its kind).
1710    pub cleanup_keyed: bool,
1711    pub motion: f32,
1712    pub burst_held: bool,
1713    pub clean_quality: bool,
1714    pub rc_bytes: usize,
1715    pub rc_debt: f64,
1716    pub idle_frames: u32,
1717    pub over_frames: u32,
1718    pub same_frames: u32,
1719    pub finest: Option<u32>,
1720    pub finest_frames: u32,
1721    pub settled: bool,
1722    pub fine_frames: u32,
1723    pub measured: Option<f32>,
1724    pub measuring: bool,
1725    pub level_checks: u32,
1726    pub measure_in: u32,
1727    pub sweep: Option<(f64, f64)>,
1728    pub sweep_frames: u32,
1729    pub sweep_coarser: u32,
1730    pub sweep_cost: f64,
1731    pub sweep_first: f64,
1732    pub sweep_held: bool,
1733    pub cleanup_debt: f64,
1734    pub cleanup_paced: bool,
1735    #[cfg(feature = "gpl")]
1736    pub h264_encoder: Option<H264EncoderWrapper>,
1737    #[cfg(not(feature = "gpl"))]
1738    pub h264_encoder: Option<crate::encoders::oh264::Openh264Encoder>,
1739    pub h264_burst_frames_remaining: i32,
1740    #[cfg(feature = "gpl")]
1741    pub y_buf: Vec<u8>,
1742    #[cfg(feature = "gpl")]
1743    pub u_buf: Vec<u8>,
1744    #[cfg(feature = "gpl")]
1745    pub v_buf: Vec<u8>,
1746    /// The planes of a 10-bit frame, as 16-bit samples.
1747    #[cfg(feature = "gpl")]
1748    pub yuv16: [Vec<u16>; 3],
1749    /// The two pictures an 8-bit 4:2:0 x264 session converts its frames into in turn, x264
1750    /// reading the one before through the next encode (`H264EncoderWrapper::held_input`).
1751    #[cfg(feature = "gpl")]
1752    pub nv12: Vec<Nv12Planes>,
1753    pub packet_buf: Vec<u8>,
1754    pub last_hash: u64,
1755    pub consecutive_changes: u32,
1756    pub in_damage_block: bool,
1757    pub damage_block_frames_remaining: i32,
1758    /// Whether the last `content_dirty` hashed the region and found it changed: a damage block's
1759    /// other frames read dirty without a hash.
1760    pub hash_changed: bool,
1761    /// The region's blocks, which held still and which are clean (`ledger`), for refining the
1762    /// ones that held still while others move.
1763    pub ledger: BlockLedger,
1764    /// How many of them a frame refines (`ledger::RefinePace`), and how many coarse ones a
1765    /// capped constant quality refreshes once its peak lets go (`ledger::RefreshPace`).
1766    pub refine: RefinePace,
1767    pub refresh: RefreshPace,
1768}
1769
1770/// The bits per sample the software H.264 session codes `settings` at: 10 where they ask for it
1771/// and the build's libx264 opens such a session, else 8.
1772pub fn h264_bit_depth(settings: &CaptureSettings) -> u32 {
1773    #[cfg(feature = "gpl")]
1774    if settings.codec == Codec::H264
1775        && settings.video_bit_depth >= 10
1776        && H264EncoderWrapper::ten_bit()
1777    {
1778        return 10;
1779    }
1780    let _ = settings;
1781    8
1782}
1783
1784/// Fast, non-cryptographic 64-bit content hash used only for in-memory change detection.
1785///
1786/// Uses xxh3: a SIMD-friendly hash that processes 64-byte blocks with parallel lanes,
1787/// delivering near memory-bandwidth throughput. The value is never persisted or sent on the
1788/// wire, so only the property that identical bytes hash identically matters. A collision
1789/// between two distinct stripes is ~2^-64, and the next real content change or a requested
1790/// keyframe repaints any missed update anyway.
1791fn fast_hash(bytes: &[u8]) -> u64 {
1792    xxhash_rust::xxh3::xxh3_64_with_seed(bytes, 0)
1793}
1794
1795impl StripeState {
1796    /// Stand in for the compositor damage that X11 capture does not provide: hash this stripe
1797    /// to decide whether it changed since last frame, and once it is clearly in motion, stop
1798    /// re-hashing it every frame by committing to a sustained-motion "damage block".
1799    ///
1800    /// The hash is not free, and a region that changes every frame would otherwise be re-hashed
1801    /// forever while always reporting dirty anyway. So after `threshold` consecutive changes the
1802    /// stripe enters a damage block that just reports dirty for `duration` frames and re-hashes only
1803    /// on its last two frames, to decide whether to extend the block or let it lapse — trading a
1804    /// little extra sending for far fewer hashes on exactly the regions that need them least:
1805    ///
1806    /// 1. **Inside a damage block**: the stripe is treated as dirty without re-hashing, and the
1807    ///    block's remaining-frame counter is decremented. The stripe is hashed on the block's last
1808    ///    two frames: if they differ the region is still moving and the block is renewed for
1809    ///    another `duration` frames, otherwise the block exits and the change counter resets, so
1810    ///    motion that stopped inside a block is read as stopped when that block ends, not a block
1811    ///    later. This keeps a continuously-moving region streaming for `duration` frames per
1812    ///    re-check rather than hashing every frame.
1813    /// 2. **Outside a block**: the stripe is hashed and compared to the previous frame. A change
1814    ///    increments `consecutive_changes`, and reaching `threshold` consecutive changes opens a new
1815    ///    damage block; an unchanged frame resets the counter to zero.
1816    ///
1817    /// Returns `true` whenever the stripe is considered dirty (always true while inside a block).
1818    pub fn content_dirty(&mut self, bytes: &[u8], threshold: u32, duration: i32) -> bool {
1819        self.dirty_by(|| fast_hash(bytes), threshold, duration)
1820    }
1821
1822    /// `content_dirty` for a stripe hashed elsewhere (on the GPU, where the frame was uploaded):
1823    /// the same state, read off `hash`.
1824    pub fn hash_dirty(&mut self, hash: u64, threshold: u32, duration: i32) -> bool {
1825        self.dirty_by(|| hash, threshold, duration)
1826    }
1827
1828    fn dirty_by(&mut self, fast_hash: impl FnOnce() -> u64, threshold: u32, duration: i32) -> bool {
1829        self.hash_changed = false;
1830        if self.in_damage_block {
1831            self.damage_block_frames_remaining -= 1;
1832            if self.damage_block_frames_remaining == 1 {
1833                self.last_hash = fast_hash();
1834            } else if self.damage_block_frames_remaining <= 0 {
1835                let h = fast_hash();
1836                if h != self.last_hash {
1837                    self.hash_changed = true;
1838                    self.damage_block_frames_remaining = duration;
1839                } else {
1840                    self.in_damage_block = false;
1841                    self.consecutive_changes = 0;
1842                }
1843                self.last_hash = h;
1844            }
1845            return true;
1846        }
1847        let h = fast_hash();
1848        let changed = h != self.last_hash;
1849        self.last_hash = h;
1850        self.hash_changed = changed;
1851        if changed {
1852            self.consecutive_changes += 1;
1853            if self.consecutive_changes >= threshold {
1854                self.in_damage_block = true;
1855                self.damage_block_frames_remaining = duration;
1856            }
1857        } else {
1858            self.consecutive_changes = 0;
1859        }
1860        changed
1861    }
1862}
1863
1864/// One encoded stripe: the compressed bytes plus geometry and identity metadata.
1865///
1866/// The consumer can place and attribute the stripe even when the payload has no header. In
1867/// `omit_headers` mode the per-stripe wire header is stripped from the bytes; the struct fields
1868/// carry that information out-of-band.
1869///
1870/// # Fields
1871///
1872/// * `data` - Compressed payload (JPEG, or the video codec's bitstream). `Arc`-shared so every
1873///   delivery-layer consumer can retain the frame without copying the bytes.
1874/// * `codec` - The codec of the payload.
1875/// * `stripe_y_start` - Y pixel coordinate of the stripe's top edge within the frame.
1876/// * `stripe_height` - Height of the stripe in pixels.
1877/// * `frame_id` - Frame sequence number this stripe belongs to.
1878/// * `reference` - The frame this stripe predicts from.
1879pub struct EncodedStripe {
1880    pub data: Arc<Vec<u8>>,
1881    pub codec: Codec,
1882    pub stripe_y_start: i32,
1883    pub stripe_height: i32,
1884    pub frame_id: i32,
1885    pub timing: FrameTiming,
1886    pub reference: Reference,
1887}
1888
1889impl EncodedStripe {
1890    /// The encoded bytes, shared with the encoder's buffer.
1891    pub fn as_bytes(&self) -> &[u8] {
1892        &self.data
1893    }
1894
1895    /// The codec tag the wire carries: the codec id.
1896    pub fn data_type(&self) -> i32 {
1897        self.codec.data_type()
1898    }
1899
1900    /// The frame this one predicts from: its id, -1 for a frame that decodes on its own,
1901    /// -2 where the encoder does not track its references.
1902    pub fn reference_frame_id(&self) -> i32 {
1903        self.reference.frame_id()
1904    }
1905}
1906
1907/// When a frame was captured and when its encode began and ended, as CLOCK_MONOTONIC
1908/// nanoseconds, so a consumer can attribute a frame's age to the host rather than the
1909/// network or the decoder. An encoder leaves them zero; the capture that ran it stamps
1910/// every stripe of the frame.
1911#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1912pub struct FrameTiming {
1913    pub capture_ns: i64,
1914    pub encode_start_ns: i64,
1915    pub encode_end_ns: i64,
1916}
1917
1918impl FrameTiming {
1919    /// Stamps the stripes of one frame, captured at `capture_ns` and encoded from
1920    /// `encode_start_ns` until now.
1921    pub fn stamp(stripes: &mut [EncodedStripe], capture_ns: i64, encode_start_ns: i64) {
1922        let timing = FrameTiming {
1923            capture_ns,
1924            encode_start_ns,
1925            encode_end_ns: crate::wayland::host::now_ns(),
1926        };
1927        for stripe in stripes {
1928            stripe.timing = timing;
1929        }
1930    }
1931}
1932
1933/// No JPEG stripe is shorter than this many rows.
1934const MIN_STRIPE_HEIGHT: i32 = 64;
1935/// The frames the JPEG stripes' cleanups of one still screen are spread over (`encode_cpu`).
1936const CLEANUP_STAGGER_FRAMES: usize = 4;
1937
1938/// Whether the software H.264 session holds its cleanup at the quantizer asked for: libx264
1939/// does under either rate control, and OpenH264 moves its quantizer only at a constant quality
1940/// (`Openh264Encoder::update_qp`), so a session that holds none is cleaned up through its rate
1941/// control (`encode_cpu`), never by a key frame, which that would starve.
1942pub fn stripes_hold_quantizer(settings: &CaptureSettings) -> bool {
1943    cfg!(feature = "gpl") || !settings.video_cbr_mode
1944}
1945
1946/// Whether a frame that holds still codes the software H.264 session at the paint-over quality
1947/// instead of its own (`encode_cpu`): at a constant quality, where its cleanup falls due, its
1948/// burst is held, or it keeps the quality it was cleaned up at. A constant rate holds none there.
1949pub fn stripes_held_still(stripes: &[StripeState], settings: &CaptureSettings) -> bool {
1950    !settings.video_cbr_mode
1951        && settings.use_paint_over_quality
1952        && settings.video_paintover_crf < settings.video_crf
1953        && stripes.iter().any(|st| {
1954            st.clean_quality
1955                || (st.burst_held && st.h264_burst_frames_remaining > 0)
1956                || crate::pipeline::cleanup_pending(
1957                    st,
1958                    settings.paint_over_trigger(),
1959                    settings.paint_over_trigger(),
1960                    true,
1961                    true,
1962                )
1963        })
1964}
1965
1966/// One frame of a region's `ledger`, the region at rows `y` to `y + height`: the rectangles of
1967/// `changed` that reach it, in its own coordinates, read with whether it changed (`dirty`) and
1968/// whether its hash saw it change (`observed`, `BlockLedger::frame_reported`).
1969fn ledger_frame(
1970    ledger: &mut BlockLedger,
1971    changed: &[Rectangle<i32, Physical>],
1972    (y, height): (i32, i32),
1973    dirty: bool,
1974    observed: bool,
1975) {
1976    let local: Vec<Rectangle<i32, Physical>> = changed
1977        .iter()
1978        .filter(|r| r.loc.y < y + height && r.loc.y + r.size.h > y)
1979        .map(|r| Rectangle::new((r.loc.x, r.loc.y - y).into(), r.size))
1980        .collect();
1981    ledger.frame_reported(&local, dirty, observed);
1982}
1983
1984/// The CPU path's per-frame entry point: decide per region whether it needs sending, and encode
1985/// those as JPEG stripes across the rayon pool or as one H.264 frame through the build's software
1986/// encoder (libx264 or OpenH264).
1987///
1988/// JPEG is cut into parallel stripes so its CPU cost spreads over the cores and an unchanged
1989/// stripe is skipped on its own; H.264 is one region covering the frame, its encoder kept across
1990/// frames for inter-prediction. Persistent `StripeState` keeps encoders, buffers, and the
1991/// motion/paint-over history the decision needs alive across frames. The per-region decision
1992/// mirrors `decide_hw_fullframe`'s policy for the full-frame sessions; it is kept as separate code
1993/// because this path also derives its own damage and cleans H.264 up through its software rate
1994/// control.
1995///
1996/// # Arguments
1997///
1998/// * `stripes` - Persistent per-region state vector (resized as needed, encoder state preserved).
1999/// * `raw_pixels` - Packed BGRA/RGBA pixel buffer (`width * height * 4` bytes).
2000/// * `width` - Frame width in pixels.
2001/// * `height` - Frame height in pixels.
2002/// * `damage_rects` - Where the source says the frame changed: what marks a region dirty without
2003///   `hash_damage` (Wayland), and with it where the change lies within each region the hash
2004///   finds dirty, for its block ledger (empty where the source cannot say).
2005/// * `settings` - Capture settings (quality, mode, rate control, etc.).
2006/// * `frame_counter` - Current frame number (wrapping `u16`).
2007/// * `use_gpu` - `true` when the source is RGBA (GLES readback); `false` for BGRA (X11 host).
2008/// * `hash_damage` - `true` for X11 content-hash change detection; `false` when damage rects
2009///   are provided.
2010/// * `requested_idr` - Force a keyframe (client join / reset).
2011///
2012/// # Returns
2013///
2014/// Vec of [`EncodedStripe`] — empty when nothing changed.
2015///
2016/// 1. **Regions**: JPEG takes the core count of stripes, held to `MAX_STRIPES` because the client
2017///    decodes one picture per stripe, with no stripe thinner than 64 rows (`stripe_count`); H.264
2018///    takes one region covering the frame. The persistent `stripes` vector is resized to match,
2019///    preserving per-region state across frames.
2020/// 2. **Idle fast path**: a frame on which no region can emit anything (no damage / clean
2021///    hashes, no cleanup due, no burst, no recovery IDR, not streaming) only advances the
2022///    per-region cleanup bookkeeping inline and returns without dispatching the stripe
2023///    fan-out, so a static capture never wakes the rayon pool.
2024/// 3. **Dirty map**: with external compositor damage (`hash_damage == false`) each `damage_rects`
2025///    rectangle marks every region whose row range it overlaps. With `hash_damage == true` (X11,
2026///    which has no compositor damage) per-region content hashing drives dirtiness instead, in
2027///    streaming H.264 too, since the cleanup reads a still region from its content.
2028/// 4. **Per-region cleanup** (`pipeline::cleanup_due`): a region that changed is cleaned up once
2029///    it has held still for `paint_over_trigger`, or has kept changing only a little for four
2030///    times that, at the paint-over JPEG quality or, for H.264, at the paint-over quality: under
2031///    CBR through libx264's rate control, the frame sent until it codes at the paint-over
2032///    quantizer or finer in a small frame (`pipeline::convergence`), or its frames have been slice
2033///    headers alone for `pipeline::EMPTY_S`, with no key frame, and where the encoder reports no
2034///    quantizer (OpenH264) as a refresh and burst at its rate control's quality; under CRF as the
2035///    session's rate factor until it changes again, as main's paint-over was, a refresh and
2036///    then a burst, with no key frame either, as `pipeline::decide_constant_quality` keeps it:
2037///    the frames held at the rate factor refine the picture past what a key frame at it codes
2038///    (x264 at CRF 24 and paint-over 16 on a scrolled 1440p page, 51.3 dB after the refresh,
2039///    48.3 after the 2.7 MB key frame that followed it). The JPEG stripes whose
2040///    cleanups fall due on one frame are spread over `CLEANUP_STAGGER_FRAMES` frames, so a screen
2041///    going still costs no one frame the whole screen's cleanup. A region is otherwise sent when
2042///    it is dirty, while its burst runs, or when streaming mode is on, at the base quality; a
2043///    newly dirty frame cancels its burst. A constant-rate x264 session is floored at the
2044///    paint-over quantizer while it moves and the floor lifted for its cleanup
2045///    (`H264EncoderWrapper::reconfigure_floor`), unless the caller names a floor of its own
2046///    (`video_min_qp`): the rate control otherwise spends a generous budget coding motion finer
2047///    than the still picture it leaves (a 1440p scroll at 30 Mbit/s, 22.3 Mbit/s at 57 dB
2048///    against 13.5 at 46 dB), where the still screen reaches what the rate control gives it
2049///    either way, and the blocks a moving frame refines code below the floor.
2050/// 5. **Recovery IDR** (`requested_idr`): forces a send even when static so a reconnecting
2051///    client can resume. A scheduled one (`pipeline::periodic_idr_due`) waits for the region to
2052///    change (`StripeState::scheduled_key`): a hashed change has no known extent, so any counts as
2053///    motion. For H.264 it forces an IDR at the session's own quality and arms the burst (held at
2054///    the paint-over quantizer at a constant quality, as in `pipeline::decide_hw_fullframe`); for
2055///    JPEG, where every stripe is already intra, it resends a painted-over stripe at the
2056///    paint-over quality already on screen so a joining viewer does not see a downgrade.
2057/// 6. **Encoding**:
2058///    - **JPEG**: source byte order is RGBA on the GPU readback path and BGRA on
2059///      X11; each worker thread reuses its thread-local TurboJPEG compressor. Header-less output
2060///      hands the compressed buffer straight through; otherwise a 6-byte stripe header (`0x03` tag,
2061///      a reserved byte, frame number, y-start) is prepended so the transport can forward the
2062///      buffer without re-framing.
2063///    - **H.264**: the encoder is reused unless the width, height, or (x264) chroma format or
2064///      depth changed, in which case it is rebuilt and an IDR forced; otherwise CRF and rate are
2065///      reconfigured live. With libx264, ARGB is converted to YUV here (a conversion failure skips
2066///      the frame rather than encoding garbage) and the video header is emitted; an 8-bit 4:2:0
2067///      frame is converted to NV12 laid out as x264's own frames, which x264 encodes where it
2068///      lies instead of copying it, into the session's two pictures in turn, since x264 reads the
2069///      one before through the next encode; OpenH264 converts
2070///      and frames inside `encode_host_argb` with the same header layout, and encodes a 4:4:4
2071///      request 4:2:0 (said once per process). The live CBR budget is recomputed here from the
2072///      bitrate/fps (`rate_control`) so it rescales with live changes.
2073/// 7. **Dispatch**: H.264 runs inline with `h264_fullframe_threads` encode threads, its color
2074///    conversion split into as many bands (x264: a whole 2560x1600 frame took 0.98 ms on one
2075///    thread and 0.62 on four; OpenH264 adds four slices and a four-band conversion of its own),
2076///    or none where the capture hands over the planes ([`encode_cpu_from`]). The slice threads keep the in-frame encode latency inside the frame budget at high
2077///    resolutions; the cap is four because `zerolatency` makes x264 slice-threaded and more than
2078///    four slices trips decode glitches in some Chromium builds, and the minus-one leaves
2079///    headroom for the capture thread. JPEG stripes instead run across the rayon pool, or one
2080///    after another without `cpu_encode_threading` (`stripes_at_once`).
2081#[allow(clippy::too_many_arguments)]
2082pub fn encode_cpu(
2083    stripes: &mut Vec<StripeState>,
2084    raw_pixels: &[u8],
2085    width: i32,
2086    height: i32,
2087    damage_rects: &[Rectangle<i32, Physical>],
2088    settings: &CaptureSettings,
2089    frame_counter: u16,
2090    use_gpu: bool,
2091    hash_damage: bool,
2092    requested_idr: bool,
2093) -> Vec<EncodedStripe> {
2094    encode_cpu_from(
2095        stripes,
2096        raw_pixels,
2097        None,
2098        width,
2099        height,
2100        damage_rects,
2101        true,
2102        settings,
2103        frame_counter,
2104        use_gpu,
2105        hash_damage,
2106        requested_idr,
2107    )
2108}
2109
2110/// A frame's 8-bit 4:2:0 picture as NV12, a luma plane and an interleaved chroma one, laid out
2111/// as an x264 session encodes it in place (`x264_param_input_layout`): each plane `stride` bytes
2112/// a row within a buffer padded and aligned as x264's own frames are, which x264 reads without
2113/// copying it and writes the padding of. Tightly packed where the build has no x264, whose
2114/// OpenH264 session takes no such picture.
2115pub struct Nv12Planes {
2116    buf: Vec<u8>,
2117    start: [usize; 2],
2118    pub stride: [usize; 2],
2119    pub width: usize,
2120    pub height: usize,
2121    #[cfg(feature = "gpl")]
2122    in_place: bool,
2123}
2124
2125impl Nv12Planes {
2126    /// Black planes for a `width` x `height` picture, both even.
2127    pub fn new(width: usize, height: usize) -> Self {
2128        let layout = x264_input_layout(width, height);
2129        #[cfg(feature = "gpl")]
2130        let in_place = layout.is_some();
2131        let layout = layout.unwrap_or(InputLayout {
2132            stride: [width, width],
2133            offset: [0, 0],
2134            size: [width * height, width * height / 2],
2135        });
2136        const ALIGN: usize = 64;
2137        let luma = layout.size[0].next_multiple_of(ALIGN);
2138        let mut buf = vec![128u8; ALIGN + luma + layout.size[1]];
2139        let base = buf.as_ptr().align_offset(ALIGN);
2140        buf[base..base + layout.size[0]].fill(16);
2141        Self {
2142            buf,
2143            start: [base + layout.offset[0], base + luma + layout.offset[1]],
2144            stride: layout.stride,
2145            width,
2146            height,
2147            #[cfg(feature = "gpl")]
2148            in_place,
2149        }
2150    }
2151
2152    /// The luma and chroma planes from their first pixel, `stride` bytes a row for the picture's
2153    /// rows.
2154    pub fn planes(&self) -> (&[u8], &[u8]) {
2155        let (y, uv) = self.extents();
2156        (&self.buf[y], &self.buf[uv])
2157    }
2158
2159    /// [`Self::planes`], to write.
2160    pub fn planes_mut(&mut self) -> (&mut [u8], &mut [u8]) {
2161        let (y, uv) = self.extents();
2162        let (head, tail) = self.buf.split_at_mut(uv.start);
2163        (&mut head[y], &mut tail[..uv.len()])
2164    }
2165
2166    /// The address of the first luma pixel, which names the picture to the session holding it
2167    /// (`H264EncoderWrapper::held_input`).
2168    pub fn id(&self) -> *const u8 {
2169        self.buf[self.start[0]..].as_ptr()
2170    }
2171
2172    fn extents(&self) -> (std::ops::Range<usize>, std::ops::Range<usize>) {
2173        let y = self.start[0]..self.start[0] + self.stride[0] * self.height;
2174        let uv = self.start[1]..self.start[1] + self.stride[1] * self.height / 2;
2175        (y, uv)
2176    }
2177}
2178
2179/// Where each plane of an NV12 picture lies in its buffer: `stride` bytes a row, the first pixel
2180/// `offset` bytes in, the buffer `size` bytes.
2181struct InputLayout {
2182    stride: [usize; 2],
2183    offset: [usize; 2],
2184    size: [usize; 2],
2185}
2186
2187/// The planes an 8-bit 4:2:0 x264 session of `width` x `height` encodes in place, which the
2188/// library lays out for the CPU it runs on; `None` where the linked x264 is not patched to.
2189#[cfg(all(feature = "gpl", x264_in_place))]
2190fn x264_input_layout(width: usize, height: usize) -> Option<InputLayout> {
2191    unsafe {
2192        let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
2193        codec_sys::x264::x264_param_default(&mut param);
2194        param.i_width = width as i32;
2195        param.i_height = height as i32;
2196        param.i_csp = codec_sys::x264::X264_CSP_I420 as i32;
2197        param.i_bitdepth = 8;
2198        let (mut stride, mut offset, mut size) = ([0i32; 2], [0i32; 2], [0i64; 2]);
2199        if codec_sys::x264::x264_param_input_layout(
2200            &param,
2201            stride.as_mut_ptr(),
2202            offset.as_mut_ptr(),
2203            size.as_mut_ptr(),
2204        ) < 0
2205        {
2206            return None;
2207        }
2208        Some(InputLayout {
2209            stride: stride.map(|v| v as usize),
2210            offset: offset.map(|v| v as usize),
2211            size: size.map(|v| v as usize),
2212        })
2213    }
2214}
2215
2216#[cfg(not(all(feature = "gpl", x264_in_place)))]
2217fn x264_input_layout(_width: usize, _height: usize) -> Option<InputLayout> {
2218    None
2219}
2220
2221/// [`encode_cpu`] with the frame's picture already converted (`planes`): an 8-bit 4:2:0 x264
2222/// session encodes it in place and leaves `raw_pixels` unread, which a capture converting straight
2223/// from the screen's memory never wrote. x264 reads the picture through the next encode
2224/// ([`held_input`]).
2225///
2226/// `current` says whether `damage_rects` holds every change of this frame against the one
2227/// before, as a compositor's damage or this frame's own hash does. Only then does x264 code the
2228/// blocks they leave out as skips without analysis (`H264EncoderWrapper::constant_blocks`):
2229/// rectangles read from the frame before's hash (a Turbo frame's) leave out a block that held
2230/// still there and changes in this frame, which a skip would show as it was.
2231#[allow(clippy::too_many_arguments)]
2232pub fn encode_cpu_from(
2233    stripes: &mut Vec<StripeState>,
2234    raw_pixels: &[u8],
2235    planes: Option<&Nv12Planes>,
2236    width: i32,
2237    height: i32,
2238    damage_rects: &[Rectangle<i32, Physical>],
2239    current: bool,
2240    settings: &CaptureSettings,
2241    frame_counter: u16,
2242    use_gpu: bool,
2243    hash_damage: bool,
2244    requested_idr: bool,
2245) -> Vec<EncodedStripe> {
2246    #[cfg(not(feature = "gpl"))]
2247    let _ = planes;
2248    let codec = settings.codec;
2249    let scheduled = codec.is_video() && crate::pipeline::periodic_idr_due(settings, frame_counter);
2250    let n_processing_stripes = stripe_count(height, codec);
2251
2252    if stripes.len() != n_processing_stripes {
2253        stripes.resize_with(n_processing_stripes, StripeState::default);
2254    }
2255
2256    let stripe_geometries = compute_stripe_geometries(height as usize, n_processing_stripes, codec);
2257
2258    // Idle fast path: a static frame must still advance every stripe's paint-over countdown,
2259    // but nothing else — so when no stripe can emit anything this frame, do that bookkeeping
2260    // inline and return before the rayon fan-out. Waking the whole worker pool 60x/s for
2261    // no-op stripes is the dominant idle cost (tens of percent of a core), dwarfing the real
2262    // per-frame work. "Static" is known up front for damage-authoritative sources (Wayland:
2263    // empty damage list); hash-damage sources (X11) instead take a sequential early-exit
2264    // hash scan, probing the most-recently-dirty stripe first so live content bails out
2265    // after a single stripe hash. A clean scan performs exactly the state transitions
2266    // `content_dirty` would (hash unchanged, change streak reset), so the damage-block
2267    // machinery observes no difference.
2268    let coded_quality = |st: &StripeState| -> Option<u32> {
2269        cfg_if::cfg_if! {
2270            if #[cfg(feature = "gpl")] {
2271                st.h264_encoder.as_ref().and_then(H264EncoderWrapper::last_qp)
2272            } else {
2273                let _ = st;
2274                None
2275            }
2276        }
2277    };
2278    let holds = stripes_hold_quantizer(settings);
2279    // A constant-rate stripe is cleaned up through its rate control: x264 refines a still screen
2280    // within its buffer, and OpenH264, which moves its quantizer only by a rebuild, past its frame
2281    // skip, a skipped frame telling nothing of the picture (`rc_bytes` 0).
2282    let converges = codec.is_video() && settings.video_cbr_mode;
2283    let motion_floor = if settings.video_min_qp > 0 || !converges {
2284        settings.video_min_qp
2285    } else {
2286        settings.video_paintover_crf.max(0)
2287    };
2288    let paint_over_armed = |st: &StripeState| {
2289        if !codec.is_video() {
2290            return settings.use_paint_over_quality
2291                && settings.paint_over_jpeg_quality > settings.jpeg_quality;
2292        }
2293        crate::pipeline::paint_over_improves(
2294            settings,
2295            crate::pipeline::EncoderQuality {
2296                last: coded_quality(st),
2297                bytes: None,
2298                holds,
2299                reopens: false,
2300                keys: true,
2301                band: None,
2302                measures: false,
2303                psnr: None,
2304                refit: None,
2305                follows_cap: false,
2306                finer: 0,
2307                ends_on_key: false,
2308            },
2309        )
2310    };
2311    let trigger_frames = settings.paint_over_trigger();
2312    // A stripe's change is read from its hash, of no known extent, so a caret reads as motion:
2313    // its cleanup waits the trigger, through the rate control or not.
2314    let start_frames = trigger_frames;
2315    let idle_candidate = damage_rects.is_empty()
2316        && !requested_idr
2317        && !(codec.is_video() && settings.video_streaming_mode);
2318    if idle_candidate {
2319        // A capped CRF its peak let go with coarse blocks left has a refresh to send
2320        // (`BlockLedger::refresh`).
2321        let refresh_due = |st: &StripeState| {
2322            cfg_if::cfg_if! {
2323                if #[cfg(feature = "gpl")] {
2324                    st.ledger.coarse()
2325                        && st.h264_encoder.as_ref().is_some_and(|e| e.capped() && !e.peak_bound())
2326                } else {
2327                    let _ = st;
2328                    false
2329                }
2330            }
2331        };
2332        let no_pending_send = |st: &StripeState| {
2333            (!codec.is_video() || st.h264_burst_frames_remaining <= 0)
2334                && !crate::pipeline::cleanup_pending(
2335                    st,
2336                    trigger_frames,
2337                    start_frames,
2338                    paint_over_armed(st),
2339                    false,
2340                )
2341                && !refresh_due(st)
2342        };
2343        let quiescent = if !hash_damage {
2344            stripes.iter().all(no_pending_send)
2345        } else {
2346            let width_bytes = width as usize * 4;
2347            let hint = stripes
2348                .iter()
2349                .enumerate()
2350                .min_by_key(|(_, st)| st.no_motion_frame_count)
2351                .map(|(i, _)| i)
2352                .unwrap_or(0);
2353            let clean = |i: usize| {
2354                let st = &stripes[i];
2355                if !no_pending_send(st) || st.in_damage_block {
2356                    return false;
2357                }
2358                let (y, h) = stripe_geometries[i];
2359                let bytes = &raw_pixels[y * width_bytes..(y + h) * width_bytes];
2360                fast_hash(bytes) == st.last_hash
2361            };
2362            clean(hint) && (0..stripes.len()).filter(|&i| i != hint).all(clean)
2363        };
2364        if quiescent {
2365            for st in stripes.iter_mut() {
2366                let armed = paint_over_armed(st);
2367                crate::pipeline::cleanup_due(
2368                    st,
2369                    trigger_frames,
2370                    start_frames,
2371                    armed,
2372                    true,
2373                    false,
2374                    Damage::None,
2375                );
2376                st.consecutive_changes = 0;
2377                st.key_owed |= scheduled;
2378                st.ledger.frame([]);
2379                #[cfg(feature = "gpl")]
2380                if let Some(enc) = st.h264_encoder.as_mut() {
2381                    enc.idle(settings);
2382                }
2383            }
2384            return Vec::new();
2385        }
2386    }
2387    let mut stripe_is_dirty = vec![false; n_processing_stripes];
2388    if !damage_rects.is_empty() {
2389        for rect in damage_rects {
2390            let r_y_start = rect.loc.y.max(0) as usize;
2391            let r_y_end = (rect.loc.y + rect.size.h).min(height) as usize;
2392            if r_y_start < r_y_end {
2393                for (i, &(s_y, s_h)) in stripe_geometries.iter().enumerate() {
2394                    let s_end = s_y + s_h;
2395                    if r_y_start < s_end && r_y_end > s_y {
2396                        stripe_is_dirty[i] = true;
2397                    }
2398                }
2399            }
2400        }
2401    }
2402
2403    let width_usize = width as usize;
2404    let video = codec.is_video();
2405    let video_crf = settings.video_crf;
2406    let video_po_crf = settings.video_paintover_crf;
2407    let video_burst = settings.video_paintover_burst_frames;
2408    let video_fullcolor = settings.video_fullcolor;
2409    #[cfg(feature = "gpl")]
2410    let bit_depth = h264_bit_depth(settings);
2411    let video_streaming = settings.video_streaming_mode;
2412    let jpeg_q = settings.jpeg_quality;
2413    let paint_q = settings.paint_over_jpeg_quality;
2414    let target_fps = settings.target_fps;
2415    let omit_headers = settings.omit_stripe_headers;
2416    let damage_block_threshold = settings.damage_block_threshold;
2417    let damage_block_duration = settings.damage_block_duration as i32;
2418    let video_cbr = settings.video_cbr_mode;
2419    // OpenH264 sizes its own buffer, so only x264 reads the VBV.
2420    #[cfg_attr(not(feature = "gpl"), allow(unused_variables))]
2421    let (video_bitrate, video_vbv) = rate_control(settings);
2422    #[cfg(feature = "gpl")]
2423    let cleanup_vbv = cleanup_vbv_kbit(settings, video_bitrate).max(video_vbv);
2424    // x264 runs the H.264 thread policy, as OpenH264 does internally.
2425    #[cfg(feature = "gpl")]
2426    let h264_threads = crate::encoders::session::h264_fullframe_threads(settings);
2427    // A frame of x264's slices, one a thread, that refines nothing.
2428    #[cfg(feature = "gpl")]
2429    let empty_bytes = crate::encoders::codec::VIDEO_HEADER_LEN
2430        + crate::pipeline::EMPTY_SLICE_BYTES * h264_threads as usize;
2431    #[cfg(not(feature = "gpl"))]
2432    let empty_bytes = 0;
2433    #[cfg(feature = "gpl")]
2434    let csc_bands = h264_threads.max(1) as usize;
2435    if video && video_fullcolor && !crate::encoders::software_fullcolor(Codec::H264) {
2436        static FULLCOLOR_LOGGED: std::sync::atomic::AtomicBool =
2437            std::sync::atomic::AtomicBool::new(false);
2438        if !FULLCOLOR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
2439            eprintln!(
2440                "[software] 4:4:4 full-color requested; OpenH264 is 4:2:0-only, encoding 4:2:0."
2441            );
2442        }
2443    }
2444
2445    // A still screen reaches every JPEG stripe's cleanup on the same frame; spread them over
2446    // `CLEANUP_STAGGER_FRAMES` frames so no one frame carries the whole screen's. An H.264 session
2447    // refining through its rate control takes no other cleanup until that one ends.
2448    let per_frame = stripes.len().div_ceil(CLEANUP_STAGGER_FRAMES).max(1);
2449    let refining = stripes
2450        .iter()
2451        .filter(|st| converges && st.h264_burst_frames_remaining > 0)
2452        .count();
2453    let mut granted = 0;
2454    let cleanup_allowed: Vec<bool> = stripes
2455        .iter()
2456        .map(|st| {
2457            if !crate::pipeline::cleanup_pending(
2458                st,
2459                trigger_frames,
2460                start_frames,
2461                paint_over_armed(st),
2462                false,
2463            ) {
2464                return true;
2465            }
2466            granted += 1;
2467            granted <= per_frame && refining + granted <= stripes.len()
2468        })
2469        .collect();
2470
2471    let stripe_body = |(i, stripe_state): (usize, &mut StripeState)| -> Option<EncodedStripe> {
2472        if i >= stripe_geometries.len() {
2473            return None;
2474        }
2475        let (y_start, actual_height) = stripe_geometries[i];
2476        let start_idx = y_start * width_usize * 4;
2477        let end_idx = start_idx + (actual_height * width_usize * 4);
2478        let stripe_bytes = &raw_pixels[start_idx..end_idx];
2479
2480        let is_dirty = if !hash_damage {
2481            stripe_is_dirty[i]
2482        } else {
2483            stripe_state.content_dirty(stripe_bytes, damage_block_threshold, damage_block_duration)
2484        };
2485        stripe_state.ledger.fit(width, actual_height as i32);
2486        let observed = if hash_damage {
2487            stripe_state.hash_changed
2488        } else {
2489            is_dirty
2490        };
2491        ledger_frame(
2492            &mut stripe_state.ledger,
2493            damage_rects,
2494            (y_start as i32, actual_height as i32),
2495            is_dirty,
2496            observed,
2497        );
2498        let armed = paint_over_armed(stripe_state);
2499        let cleanup = crate::pipeline::cleanup_due(
2500            stripe_state,
2501            trigger_frames,
2502            start_frames,
2503            armed,
2504            cleanup_allowed[i],
2505            false,
2506            if is_dirty {
2507                Damage::Unknown
2508            } else {
2509                Damage::None
2510            },
2511        );
2512        // A paint-over that falls due on a capped CRF while its peak binds, which would code it
2513        // coarse, or once it let go on blocks it coded bound and has yet to refresh or refreshed
2514        // at its own quality since, or that the room its bucket has left cannot take in one frame
2515        // (`H264EncoderWrapper::holds_cleanup`), is owed to the still blocks instead
2516        // (`BlockLedger::spent`), which the refresh codes at the paint-over quality within the
2517        // peak once it lets go: x264's rate factor reads a still frame over a refreshed page as
2518        // next to no complexity and codes the paint-over in one frame past it.
2519        #[cfg(feature = "gpl")]
2520        let cleanup = if cleanup == Cleanup::Refresh
2521            && video
2522            && !video_cbr
2523            && stripe_state.h264_encoder.as_ref().is_some_and(|e| {
2524                e.capped()
2525                    && (e.peak_bound()
2526                        || stripe_state.ledger.coarse()
2527                        || stripe_state.ledger.fresh()
2528                        || !e.holds_cleanup(video_po_crf))
2529            }) {
2530            if stripe_state.ledger.spent() {
2531                stripe_state.refresh = Default::default();
2532            }
2533            Cleanup::None
2534        } else {
2535            cleanup
2536        };
2537        let force_idr_all = requested_idr || stripe_state.scheduled_key(scheduled, is_dirty);
2538        let mut send_this_stripe = is_dirty || cleanup != Cleanup::None || force_idr_all;
2539        let mut quality_or_crf = if !video { jpeg_q } else { video_crf };
2540        let mut force_idr = video && (force_idr_all || cleanup == Cleanup::Key);
2541        if force_idr {
2542            stripe_state.key_owed = false;
2543        }
2544        // A key frame asked for codes a clean stripe again at its rate control's quality: it is due
2545        // its cleanup again, as after a loss, a trigger period on (`pipeline::decide_hw_fullframe`).
2546        if requested_idr && video && cleanup == Cleanup::None {
2547            stripe_state.lost_frame();
2548        }
2549        let mut hold = None;
2550        let quality = crate::pipeline::EncoderQuality {
2551            last: coded_quality(stripe_state),
2552            bytes: None,
2553            holds,
2554            reopens: false,
2555            keys: true,
2556            band: None,
2557            measures: false,
2558            psnr: None,
2559            refit: None,
2560            follows_cap: false,
2561            finer: 0,
2562            ends_on_key: false,
2563        };
2564        let refresh_crf = crate::pipeline::held_refresh_quality(settings, quality) as i32;
2565        if cleanup != Cleanup::None {
2566            if !video {
2567                quality_or_crf = paint_q;
2568            } else if cleanup == Cleanup::Key {
2569                hold = Some(video_po_crf);
2570            } else if holds && !converges {
2571                hold = Some(refresh_crf);
2572            }
2573        } else if force_idr_all && !is_dirty && !video && armed && stripe_state.paint_over_sent {
2574            quality_or_crf = paint_q;
2575        }
2576        if converges
2577            && stripe_state.h264_burst_frames_remaining > 0
2578            && !is_dirty
2579            && cleanup == Cleanup::None
2580        {
2581            let budget = crate::pipeline::frame_budget(video_bitrate, target_fps);
2582            let bytes = (stripe_state.rc_bytes > 0).then_some(stripe_state.rc_bytes);
2583            stripe_state.idle_frames = if bytes.is_some_and(|b| b <= empty_bytes) {
2584                stripe_state.idle_frames.saturating_add(1)
2585            } else {
2586                0
2587            };
2588            // A run of a trigger period: after a key frame coded coarser, x264 codes the frames that
2589            // follow at a fine quantizer while skipping most of the picture, so one small one says
2590            // nothing of the picture.
2591            stripe_state.fine_frames = if crate::pipeline::convergence(
2592                quality.last,
2593                bytes,
2594                settings.video_paintover_crf.max(0) as u32,
2595                budget,
2596            ) == crate::pipeline::Convergence::Converged
2597            {
2598                stripe_state.fine_frames.saturating_add(1)
2599            } else {
2600                0
2601            };
2602            if stripe_state.fine_frames >= trigger_frames {
2603                stripe_state.ledger.cleaned();
2604            }
2605            if stripe_state.fine_frames >= trigger_frames
2606                || f64::from(stripe_state.idle_frames) >= crate::pipeline::EMPTY_S * target_fps
2607            {
2608                stripe_state.h264_burst_frames_remaining = 0;
2609            }
2610        }
2611        // Whether this frame is the cleanup's or its burst's, which a refresh leaves alone.
2612        let mut cleaning = cleanup != Cleanup::None;
2613        if is_dirty {
2614            stripe_state.h264_burst_frames_remaining = 0;
2615        } else if video
2616            && converges
2617            && (cleanup != Cleanup::None || (force_idr && video_burst > 0 && armed))
2618        {
2619            // Neither encoder has a refresh after its rate control: it refines for as long as it
2620            // gains, x264 at 0.5 Mbit/s at 8 fps 1.7 dB a second still at 10 s, and OpenH264 at
2621            // 1080p first waits out seconds of frame skip after the frame that changed the page.
2622            stripe_state.h264_burst_frames_remaining = crate::pipeline::refine_frames(settings);
2623            stripe_state.burst_held = false;
2624            stripe_state.idle_frames = 0;
2625            stripe_state.fine_frames = 0;
2626        } else if video && (force_idr || cleanup != Cleanup::None) && video_burst > 0 {
2627            stripe_state.h264_burst_frames_remaining = video_burst;
2628            stripe_state.burst_held =
2629                holds && (cleanup != Cleanup::None || (armed && !settings.video_cbr_mode));
2630        } else if video && stripe_state.h264_burst_frames_remaining > 0 {
2631            stripe_state.h264_burst_frames_remaining -= 1;
2632            send_this_stripe = true;
2633            cleaning = true;
2634            if stripe_state.burst_held && armed {
2635                hold = Some(refresh_crf);
2636            }
2637        }
2638        // A constant-quality stripe is cleaned up through its session quality, as main's
2639        // paint-over was (`pipeline::decide_constant_quality`): x264's rate factor or
2640        // OpenH264's rebuild at the held index, where a key frame takes the library's intra
2641        // offset. It stays there until the stripe changes again; a requested key frame is
2642        // coded at the session's own quality.
2643        if video && !video_cbr {
2644            if is_dirty || !armed {
2645                stripe_state.clean_quality = false;
2646            }
2647            if cleanup != Cleanup::None {
2648                stripe_state.clean_quality = true;
2649            }
2650            if stripe_state.clean_quality && hold.is_none() && !force_idr_all {
2651                hold = Some(video_po_crf);
2652            }
2653        }
2654        if video && video_streaming {
2655            send_this_stripe = true;
2656        }
2657        // A capped CRF its peak let go codes again the blocks it coded while bound that held
2658        // still since, so many a frame (`BlockLedger::refresh`), the rest held as skips, the
2659        // ones owed their paint-over first and at it on a still stripe's frame
2660        // (`BlockLedger::refresh_owed`); a still stripe's frames are sent for it. A cleanup already
2661        // running, and its burst, run as they would without.
2662        #[cfg(feature = "gpl")]
2663        let refresh = if video && !video_cbr && !force_idr && !cleaning {
2664            let unbound = stripe_state
2665                .h264_encoder
2666                .as_ref()
2667                .is_some_and(|e| e.capped() && !e.peak_bound());
2668            let blocks = stripe_state.ledger.blocks();
2669            let most = if unbound && stripe_state.ledger.coarse() {
2670                stripe_state.refresh.most(blocks)
2671            } else {
2672                0
2673            };
2674            if most == 0 {
2675                None
2676            } else if !is_dirty && let Some((now, wait)) = stripe_state.ledger.refresh_owed(most) {
2677                let key = now.none_like();
2678                Some((now, key, wait, true))
2679            } else {
2680                stripe_state
2681                    .ledger
2682                    .refresh_keyed(most)
2683                    .map(|(now, key, wait)| (now, key, wait, false))
2684            }
2685        } else {
2686            None
2687        };
2688        #[cfg(feature = "gpl")]
2689        if refresh.is_some() {
2690            send_this_stripe = true;
2691        }
2692        #[cfg(feature = "gpl")]
2693        if video
2694            && !send_this_stripe
2695            && let Some(enc) = stripe_state.h264_encoder.as_mut()
2696        {
2697            enc.idle(settings);
2698        }
2699
2700        if send_this_stripe {
2701            if !video {
2702                let pixel_format = if use_gpu {
2703                    turbojpeg::PixelFormat::RGBA
2704                } else {
2705                    turbojpeg::PixelFormat::BGRA
2706                };
2707                let img = turbojpeg::Image {
2708                    pixels: stripe_bytes,
2709                    width: width_usize,
2710                    pitch: width_usize * 4,
2711                    height: actual_height,
2712                    format: pixel_format,
2713                };
2714                JPEG_COMPRESSOR.with(|cell| -> Option<EncodedStripe> {
2715                    let mut slot = cell.borrow_mut();
2716                    if slot.is_none() {
2717                        *slot = Some(turbojpeg::Compressor::new().ok()?);
2718                    }
2719                    let compressor = slot.as_mut().unwrap();
2720                    compressor.set_quality(quality_or_crf).ok()?;
2721                    let jpeg = compressor.compress_to_vec(img).ok()?;
2722                    let data = if omit_headers {
2723                        jpeg
2724                    } else {
2725                        stripe_state.packet_buf.clear();
2726                        push_jpeg_header(
2727                            &mut stripe_state.packet_buf,
2728                            frame_counter,
2729                            y_start as u16,
2730                        );
2731                        stripe_state.packet_buf.extend_from_slice(&jpeg);
2732                        std::mem::take(&mut stripe_state.packet_buf)
2733                    };
2734                    Some(EncodedStripe {
2735                        data: Arc::new(data),
2736                        codec: Codec::Jpeg,
2737                        stripe_y_start: y_start as i32,
2738                        stripe_height: actual_height as i32,
2739                        frame_id: i32::from(frame_counter),
2740                        timing: FrameTiming::default(),
2741                        reference: Reference::Untracked,
2742                    })
2743                })
2744            } else {
2745                cfg_if::cfg_if! {
2746                    if #[cfg(feature = "gpl")] {
2747                let needs_reinit = if let Some(ref enc) = stripe_state.h264_encoder {
2748                    enc.width != width_usize as i32
2749                        || enc.height != actual_height as i32
2750                        || enc.is_i444 != video_fullcolor
2751                        || enc.bit_depth != bit_depth
2752                        || enc.reference_frames != settings.video_reference_frames
2753                } else {
2754                    true
2755                };
2756
2757                // A constant rate holds the frame at its quantizer (`hold_quantizer`); a
2758                // constant quality codes it at that rate factor.
2759                let x264_crf = if video_cbr { quality_or_crf } else { hold.unwrap_or(quality_or_crf) };
2760                if needs_reinit {
2761                    stripe_state.h264_encoder = H264EncoderWrapper::with_depth(
2762                        width_usize as i32,
2763                        actual_height as i32,
2764                        x264_crf,
2765                        video_fullcolor,
2766                        bit_depth,
2767                        target_fps,
2768                        h264_threads,
2769                        video_cbr,
2770                        video_bitrate,
2771                        video_vbv,
2772                        settings.video_min_qp,
2773                        settings.video_max_qp,
2774                        settings.video_reference_frames,
2775                    );
2776                    force_idr = true;
2777                } else if let Some(ref mut enc) = stripe_state.h264_encoder {
2778                    enc.reconfigure_crf(x264_crf);
2779                    let cleaning = converges && stripe_state.h264_burst_frames_remaining > 0;
2780                    let vbv = if cleaning { cleanup_vbv } else { video_vbv };
2781                    enc.reconfigure_rate(video_bitrate, vbv, target_fps);
2782                    enc.reconfigure_floor(if cleaning { settings.video_min_qp } else { motion_floor });
2783                }
2784
2785                // A stripe moving at the session's quality refines the blocks of it that held
2786                // still and are not clean, at the paint-over quality: a quantizer offset from
2787                // the rate control's last, or from the session's rate factor. A capped CRF
2788                // refines nothing while its peak binds: the offsets would code the blocks from a
2789                // rate factor the peak has raised, coarser than the quality they count clean at.
2790                let peak_bound = stripe_state
2791                    .h264_encoder
2792                    .as_ref()
2793                    .is_some_and(H264EncoderWrapper::peak_bound);
2794                let refines = video
2795                    && armed
2796                    && is_dirty
2797                    && cleanup == Cleanup::None
2798                    && hold.is_none()
2799                    && !force_idr
2800                    && !peak_bound
2801                    && refresh.is_none();
2802                let plan = refines
2803                    .then(|| {
2804                        let blocks = stripe_state.ledger.blocks();
2805                        let most = stripe_state.refine.most(blocks, video_cbr);
2806                        stripe_state.ledger.plan(trigger_frames, most, usize::MAX)
2807                    })
2808                    .flatten();
2809                if let Some(ref mut enc) = stripe_state.h264_encoder {
2810                    if let Some(q) = hold.filter(|_| video_cbr) {
2811                        enc.hold_quantizer(q);
2812                    }
2813                    let refinement = plan.map(|plan| {
2814                        let refinement = crate::ledger::Refinement {
2815                            plan,
2816                            target: video_po_crf - crate::ledger::REFINE_FINER as i32,
2817                            reach: u32::MAX,
2818                            from: if video_cbr {
2819                                enc.last_qp().map_or(video_crf, |q| q as i32)
2820                            } else {
2821                                video_crf
2822                            },
2823                        };
2824                        enc.refine_blocks(&refinement.plan, refinement.offset() as f32);
2825                        refinement
2826                    });
2827                    // A still refresh frame is held at the stripe's quality as a quantizer, the
2828                    // session's own or the paint-over one it was cleaned up to: x264's rate factor
2829                    // reads a still frame's complexity as next to none and holds the frames after a
2830                    // peak let go at quantizers anywhere from 51 to 6. One that also moves is the
2831                    // rate factor's, so the peak's bucket steers it and binds again.
2832                    // A block owed its paint-over is coded `OWED_FINER` steps past it.
2833                    if let Some((now, key, wait, paint)) = &refresh {
2834                        let (quality, finer) = if *paint {
2835                            (
2836                                video_po_crf,
2837                                OWED_FINER as i32,
2838                            )
2839                        } else {
2840                            (x264_crf, X264_REFRESH_FINER as i32)
2841                        };
2842                        let finer = if is_dirty {
2843                            (x264_crf - quality).max(0) + finer
2844                        } else {
2845                            finer
2846                        };
2847                        enc.refresh_blocks(
2848                            (!is_dirty).then_some(quality),
2849                            now,
2850                            finer as f32,
2851                            key,
2852                            (crate::ledger::REFRESH_FINER + crate::ledger::REFRESH_KEY_FINER)
2853                                as f32,
2854                            wait,
2855                        );
2856                    }
2857                    if hash_damage || !current {
2858                        enc.constant_blocks(std::iter::empty());
2859                    } else {
2860                        enc.constant_blocks(stripe_state.ledger.unchanged());
2861                    }
2862                    let y_size = width_usize * actual_height;
2863                    let uv_size = if video_fullcolor { y_size } else { y_size / 4 };
2864                    let y_stride = width_usize as i32;
2865                    let uv_stride =
2866                        (if video_fullcolor { width_usize } else { width_usize / 2 }) as i32;
2867                    let wide = bit_depth > 8;
2868                    let nv12 = !wide && !video_fullcolor;
2869                    let planes = planes.filter(|p| {
2870                        nv12 && y_start == 0 && p.width == width_usize && p.height == actual_height
2871                    });
2872                    // x264 reads the picture it encoded last through this encode, so the frame
2873                    // is converted into the session's other one.
2874                    let own = if nv12 && planes.is_none() {
2875                        let held = enc.held_input();
2876                        let buffers = &mut stripe_state.nv12;
2877                        buffers.retain(|p| p.width == width_usize && p.height == actual_height);
2878                        while buffers.len() < 2 {
2879                            buffers.push(Nv12Planes::new(width_usize, actual_height));
2880                        }
2881                        Some(usize::from(held == Some(buffers[0].id())))
2882                    } else {
2883                        None
2884                    };
2885                    let conversion_result = if planes.is_some() {
2886                        Ok(())
2887                    } else if let Some(i) = own {
2888                        let target = &mut stripe_state.nv12[i];
2889                        let strides = (target.stride[0], target.stride[1]);
2890                        let (y, uv) = target.planes_mut();
2891                        convert_to_nv12_mt(
2892                            stripe_bytes,
2893                            (width_usize * 4) as u32,
2894                            width_usize,
2895                            actual_height,
2896                            use_gpu,
2897                            y,
2898                            uv,
2899                            strides,
2900                            csc_bands,
2901                        )
2902                    } else if wide {
2903                        let [y16, u16, v16] = &mut stripe_state.yuv16;
2904                        y16.resize(y_size, 0);
2905                        u16.resize(uv_size, 0);
2906                        v16.resize(uv_size, 0);
2907                        convert_to_yuv10_mt(
2908                            stripe_bytes,
2909                            width_usize * 4,
2910                            width_usize,
2911                            actual_height,
2912                            use_gpu,
2913                            video_fullcolor,
2914                            video_fullcolor,
2915                            y16,
2916                            u16,
2917                            v16,
2918                            (y_stride as usize, uv_stride as usize),
2919                            csc_bands,
2920                        );
2921                        Ok(())
2922                    } else {
2923                        if stripe_state.y_buf.len() != y_size {
2924                            stripe_state.y_buf.resize(y_size, 0);
2925                        }
2926                        if stripe_state.u_buf.len() != uv_size {
2927                            stripe_state.u_buf.resize(uv_size, 0);
2928                        }
2929                        if stripe_state.v_buf.len() != uv_size {
2930                            stripe_state.v_buf.resize(uv_size, 0);
2931                        }
2932                        convert_to_yuv_mt(
2933                            stripe_bytes,
2934                            (width_usize * 4) as u32,
2935                            width_usize,
2936                            actual_height,
2937                            use_gpu,
2938                            video_fullcolor,
2939                            video_fullcolor,
2940                            false,
2941                            &mut stripe_state.y_buf,
2942                            &mut stripe_state.u_buf,
2943                            &mut stripe_state.v_buf,
2944                            (y_stride as usize, uv_stride as usize),
2945                            csc_bands,
2946                        )
2947                    };
2948
2949                    if let Err(e) = conversion_result {
2950                        eprintln!(
2951                            "[software] YUV conversion failed for {}x{} stripe: {:?}; skipping",
2952                            width_usize, actual_height, e
2953                        );
2954                        return None;
2955                    }
2956
2957                    let bytes = |plane: &[u16]| unsafe {
2958                        std::slice::from_raw_parts(plane.as_ptr().cast::<u8>(), plane.len() * 2)
2959                    };
2960                    let coded = if let Some(p) = planes.or(own.map(|i| &stripe_state.nv12[i])) {
2961                        enc.encode_nv12_with_headers(
2962                            p,
2963                            frame_counter,
2964                            force_idr,
2965                            omit_headers,
2966                            &mut stripe_state.packet_buf,
2967                        )
2968                    } else {
2969                        let (y, u, v, scale) = if wide {
2970                            let [y16, u16, v16] = &stripe_state.yuv16;
2971                            (bytes(y16), bytes(u16), bytes(v16), 2)
2972                        } else {
2973                            (
2974                                stripe_state.y_buf.as_slice(),
2975                                stripe_state.u_buf.as_slice(),
2976                                stripe_state.v_buf.as_slice(),
2977                                1,
2978                            )
2979                        };
2980                        enc.encode_with_headers(
2981                            y,
2982                            u,
2983                            v,
2984                            y_stride * scale,
2985                            uv_stride * scale,
2986                            uv_stride * scale,
2987                            frame_counter,
2988                            force_idr,
2989                            omit_headers,
2990                            &mut stripe_state.packet_buf,
2991                        )
2992                    };
2993                    if coded {
2994                        if hold.is_none() || !video_cbr {
2995                            stripe_state.rc_bytes = stripe_state.packet_buf.len();
2996                        }
2997                        let capped = enc.coded_capped();
2998                        stripe_state.ledger.coded(capped, force_idr);
2999                        if let Some((now, _, _, _)) = refresh.as_ref().filter(|_| !capped) {
3000                            stripe_state.ledger.refreshed(now);
3001                            let blocks = stripe_state.ledger.blocks();
3002                            stripe_state.refresh.coded(
3003                                stripe_state.packet_buf.len(),
3004                                crate::pipeline::frame_budget(video_bitrate, target_fps),
3005                                blocks,
3006                            );
3007                        }
3008                        if let Some(refinement) = &refinement {
3009                            let coded = if video_cbr {
3010                                enc.last_qp().map_or(i32::MAX, |q| q as i32)
3011                            } else {
3012                                video_crf
3013                            };
3014                            stripe_state.ledger.refinement_coded(refinement, coded);
3015                        }
3016                        if video_cbr
3017                            && hold.is_none()
3018                            && !force_idr
3019                            && let Some(coded) = enc.last_qp().map(|q| q as i32)
3020                        {
3021                            let budget = crate::pipeline::frame_budget(video_bitrate, target_fps);
3022                            let blocks = stripe_state.ledger.blocks();
3023                            stripe_state.refine.coded(
3024                                refinement.is_some(),
3025                                coded,
3026                                stripe_state.packet_buf.len(),
3027                                budget,
3028                                blocks,
3029                            );
3030                        }
3031                        if hold.is_some_and(|q| q <= video_po_crf) && !capped && refresh.is_none() {
3032                            stripe_state.ledger.cleaned();
3033                        } else if force_idr {
3034                            stripe_state.ledger.coarsened();
3035                        }
3036                        // A cleanup the binding peak held coarse is spent short of its target:
3037                        // its still blocks are owed the paint-over once the peak lets go.
3038                        if peak_bound
3039                            && !video_cbr
3040                            && cleaning
3041                            && hold.is_some_and(|q| q < video_crf)
3042                            && stripe_state.ledger.spent()
3043                        {
3044                            stripe_state.refresh = Default::default();
3045                        }
3046                        Some(EncodedStripe {
3047                            data: Arc::new(std::mem::take(&mut stripe_state.packet_buf)),
3048                            codec: Codec::H264,
3049                            stripe_y_start: y_start as i32,
3050                            stripe_height: actual_height as i32,
3051                            frame_id: i32::from(frame_counter),
3052                            timing: FrameTiming::default(),
3053                            reference: enc.last_reference(),
3054                        })
3055                    } else {
3056                        None
3057                    }
3058                } else {
3059                    None
3060                }
3061                    } else {
3062                use crate::encoders::oh264::Openh264Encoder;
3063                let needs_reinit = stripe_state.h264_encoder.as_ref().is_none_or(|enc| {
3064                    enc.width() != width_usize || enc.height() != actual_height
3065                });
3066                if needs_reinit {
3067                    stripe_state.h264_encoder = Openh264Encoder::with_size(
3068                        settings,
3069                        width_usize,
3070                        actual_height,
3071                        quality_or_crf,
3072                        video_bitrate,
3073                    );
3074                    if stripe_state.h264_encoder.is_none() {
3075                        // Once per process: a geometry OpenH264 refuses (wider than
3076                        // 3840, say) would otherwise log on every stripe of every frame.
3077                        static INIT_FAILED_LOGGED: std::sync::atomic::AtomicBool =
3078                            std::sync::atomic::AtomicBool::new(false);
3079                        if !INIT_FAILED_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
3080                            eprintln!(
3081                                "[software] OpenH264 init failed for a {}x{} frame; no software H.264 for it",
3082                                width_usize, actual_height
3083                            );
3084                        }
3085                    }
3086                    force_idr = true;
3087                } else if let Some(ref mut enc) = stripe_state.h264_encoder {
3088                    // OpenH264 moves its quantizer only by a rebuild, which opens on a key
3089                    // frame, so a held quantizer is that key frame, and the stripe's key-frame
3090                    // cleanup with it; a still stripe keeps the quantizer it was cleaned at,
3091                    // since moving it back would rebuild a picture nothing changed in.
3092                    if let Some(q) = hold {
3093                        if enc.update_qp(q.max(0) as u32) {
3094                            stripe_state.change_mass = 0.0;
3095                        }
3096                    } else if is_dirty {
3097                        enc.update_qp(quality_or_crf.max(0) as u32);
3098                    }
3099                    enc.reconfigure_rate(video_bitrate, target_fps);
3100                }
3101
3102                let coded = stripe_state.h264_encoder.as_mut()?.encode_host_argb(
3103                    stripe_bytes,
3104                    width_usize * 4,
3105                    u64::from(frame_counter),
3106                    force_idr,
3107                    use_gpu,
3108                );
3109                stripe_state.rc_bytes = coded.as_ref().map_or(0, Vec::len);
3110                let enc = stripe_state.h264_encoder.as_mut()?;
3111                match coded {
3112                    Ok(data) if !data.is_empty() => Some(EncodedStripe {
3113                        data: Arc::new(data),
3114                        codec: Codec::H264,
3115                        stripe_y_start: y_start as i32,
3116                        stripe_height: actual_height as i32,
3117                        frame_id: i32::from(frame_counter),
3118                        timing: FrameTiming::default(),
3119                        reference: enc.last_reference(),
3120                    }),
3121                    Ok(_) => None,
3122                    Err(e) => {
3123                        eprintln!("[software] OpenH264 encode failed: {e}");
3124                        None
3125                    }
3126                }
3127                    }
3128                }
3129            }
3130        } else {
3131            None
3132        }
3133    };
3134    if !stripes_at_once(settings, n_processing_stripes) {
3135        stripes
3136            .iter_mut()
3137            .enumerate()
3138            .filter_map(&stripe_body)
3139            .collect()
3140    } else {
3141        stripes
3142            .par_iter_mut()
3143            .enumerate()
3144            .filter_map(&stripe_body)
3145            .collect()
3146    }
3147}
3148
3149/// Whether a frame's `n` JPEG stripes are coded at once across the rayon pool, or one after
3150/// another on the encode thread: one region, or a session without `cpu_encode_threading`, takes
3151/// no other.
3152fn stripes_at_once(settings: &CaptureSettings, n: usize) -> bool {
3153    n > 1 && settings.cpu_encode_threading
3154}
3155
3156/// Leave frame `frame_id` and every frame after it out of the software H.264 session's
3157/// predictions: x264 through its reference window, OpenH264 from a long-term reference every
3158/// consumer holds. False when the encoder cannot, and the caller codes a key frame instead.
3159pub fn invalidate_reference(stripes: &mut [StripeState], frame_id: u16) -> bool {
3160    let mut forgotten = true;
3161    for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
3162        forgotten &= enc.invalidate_reference(frame_id);
3163    }
3164    forgotten
3165}
3166
3167/// The picture the software H.264 session still reads after its last encode returned
3168/// (`H264EncoderWrapper::held_input`), which its owner leaves unchanged: `None` for any other
3169/// session.
3170pub fn held_input(stripes: &[StripeState]) -> Option<*const u8> {
3171    #[cfg(feature = "gpl")]
3172    return stripes
3173        .iter()
3174        .find_map(|s| s.h264_encoder.as_ref()?.held_input());
3175    #[cfg(not(feature = "gpl"))]
3176    {
3177        let _ = stripes;
3178        None
3179    }
3180}
3181
3182/// Stop the software H.264 session reading the picture it holds ([`held_input`]).
3183pub fn release_input(stripes: &mut [StripeState]) {
3184    #[cfg(feature = "gpl")]
3185    for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
3186        enc.release_input();
3187    }
3188    #[cfg(not(feature = "gpl"))]
3189    let _ = stripes;
3190}
3191
3192/// Every consumer holds frame `frame_id`, or where not `held` was sent it: OpenH264 confirms the
3193/// long-term reference it marked there. x264 keeps none.
3194pub fn acknowledge_reference(stripes: &mut [StripeState], frame_id: u16, held: bool) {
3195    #[cfg(not(feature = "gpl"))]
3196    for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
3197        enc.acknowledge_reference(frame_id, held);
3198    }
3199    #[cfg(feature = "gpl")]
3200    let _ = (stripes, frame_id, held);
3201}
3202
3203/// Most stripes a JPEG frame is cut into, whatever the encoder host's core count.
3204///
3205/// A stripe is a picture of its own on the wire, and the client decodes one per stripe: the
3206/// count is spent on the viewer's machine, which is not the one it was derived from. Measured
3207/// decoding a 1080p frame split N ways, WebKit -- the engine a phone or tablet runs -- loses
3208/// throughput with every stripe added (18.9 fps whole, 9.4 at eight, 3.8 at thirty-two), while
3209/// Chromium gains up to four and holds through twelve. Eight is where both sit near their best
3210/// and where the JPEG payload is smallest, so a host with the cores to cut thirty-two no longer
3211/// hands a tablet a frame it cannot assemble. One stripe is one encode job, so this bounds the
3212/// encoder's own parallelism with it.
3213const MAX_STRIPES: usize = 8;
3214
3215/// How many horizontal regions a frame of `height` is split into, which is the choice of how
3216/// much encode parallelism to spend on it.
3217///
3218/// A video codec streams whole frames, one region; JPEG fans out across cores, held to
3219/// `MAX_STRIPES` and bounded so no stripe is shorter than `MIN_STRIPE_HEIGHT` rows. Both the
3220/// encoder and the settings line report from here, so what is logged is what is encoded.
3221pub fn stripe_count(height: i32, codec: Codec) -> usize {
3222    if !codec.stripes() || height < MIN_STRIPE_HEIGHT {
3223        return 1;
3224    }
3225    let cores = std::thread::available_parallelism()
3226        .map(|n| n.get())
3227        .unwrap_or(1);
3228    cores
3229        .min(MAX_STRIPES)
3230        .min((height / MIN_STRIPE_HEIGHT) as usize)
3231        .max(1)
3232}
3233
3234/// Frames of the target a constant-rate x264 session is given as its buffer while its cleanup
3235/// runs through the rate control, or `CLEANUP_FRAME_S` of its rate where that is more: in the
3236/// session's buffer of a frame and a half x264's row-level control prices a still screen's whole
3237/// residual against the buffer and coarsens the rows it names a fine quantizer for, so a screen
3238/// of text at 2 Mbit/s at 1080p sat at 31 dB while the quantizer x264 planned read 19; in four
3239/// it reached 46 dB in five seconds with no frame over a budget and a half. A row finer than its
3240/// reference is priced at its whole intra cost against a slice's share of the buffer: at 60 fps
3241/// four frames held a 1080p page at 0.5 Mbit/s at quantizer 30 and 25-35 dB, or at 51 after a
3242/// scroll, where 0.2 s reached 55.7 dB in 30 s.
3243#[cfg(feature = "gpl")]
3244const CLEANUP_VBV_FRAMES: f64 = 4.0;
3245
3246/// The buffer, in kbit, of an x264 session at `bitrate_kbps` while it is cleaned up
3247/// (`CLEANUP_VBV_FRAMES`).
3248#[cfg(feature = "gpl")]
3249fn cleanup_vbv_kbit(settings: &CaptureSettings, bitrate_kbps: i32) -> i32 {
3250    let frames = crate::encoders::vbv_bits(
3251        (bitrate_kbps as u32).saturating_mul(1000),
3252        settings.target_fps,
3253        0.0,
3254        CLEANUP_VBV_FRAMES,
3255    ) / 1000;
3256    let seconds =
3257        (f64::from(bitrate_kbps.max(0)) * crate::pipeline::CLEANUP_FRAME_S).round() as u32;
3258    frames.max(seconds).max(1) as i32
3259}
3260
3261/// Frames of the target a constant-rate x264 session is given as its buffer where the caller
3262/// names no `video_vbv_multiplier`. x264's row-level control prices each row against a slice's
3263/// share of the buffer, so in the frame and a half the other encoders take it coded a moving
3264/// 1080p window on a 2560x1600 screen at half its target: 1.95 Mbit/s of 4 at 30.4 dB, 2.96 in
3265/// four frames, 3.52 at 34.6 dB in eight, 3.68 in fifteen, with no latency lost on a LAN (6.2 ms
3266/// to draw and decode at 1.5 frames, 5.5 at 8). Eight frames hold a frame's worst case to 133 ms
3267/// of the rate at 60 fps, where fifteen added queue on a slow link for 4% more of the target.
3268pub const CBR_VBV_FRAMES: f64 = 8.0;
3269
3270/// The `(bitrate_kbps, vbv_kbit)` the software H.264 encoder is programmed with: the CBR
3271/// target, with a buffer of `CBR_VBV_FRAMES` unless `video_vbv_multiplier` names one, or the
3272/// peak of a CRF a positive `video_vbv_multiplier` caps. An uncapped CRF gets a buffer of 0.
3273fn rate_control(settings: &CaptureSettings) -> (i32, i32) {
3274    let bitrate = settings.video_bitrate_kbps.max(1);
3275    if !settings.video_cbr_mode && settings.video_vbv_multiplier <= 0.0 {
3276        return (bitrate, 0);
3277    }
3278    let vbv = (crate::encoders::vbv_bits(
3279        (bitrate as u32).saturating_mul(1000),
3280        settings.target_fps,
3281        settings.keyframe_interval_s,
3282        if settings.video_cbr_mode && settings.video_vbv_multiplier <= 0.0 {
3283            CBR_VBV_FRAMES
3284        } else {
3285            settings.video_vbv_multiplier
3286        },
3287    ) / 1000)
3288        .max(1) as i32;
3289    (bitrate, vbv)
3290}
3291
3292/// Divide `height` into `n` contiguous regions as `(y_start, height)`, by codec.
3293///
3294/// - **JPEG**: JPEG has no vertical subsampling, so stripes may be any height; the heights differ
3295///   by at most one row — the first `remainder` stripes take one extra each — and every row of
3296///   the frame is covered.
3297/// - **Video**: one region, its height forced even since 4:2:0 pairs chroma rows vertically. The
3298///   deliberate cost is that a single trailing odd row may be left uncovered — preferable to an
3299///   odd-height picture the encoder cannot represent.
3300fn compute_stripe_geometries(height: usize, n: usize, codec: Codec) -> Vec<(usize, usize)> {
3301    if codec.is_video() {
3302        return vec![(0, height & !1)];
3303    }
3304    let mut geoms = Vec::with_capacity(n);
3305    let mut current_y = 0;
3306    let base_h = height / n;
3307    let remainder = height - base_h * n;
3308    for i in 0..n {
3309        let s_h = base_h + if i < remainder { 1 } else { 0 };
3310        geoms.push((current_y, s_h));
3311        current_y += s_h;
3312    }
3313    geoms
3314}
3315
3316#[cfg(test)]
3317mod stripe_count_tests {
3318    use super::{Codec, MAX_STRIPES, MIN_STRIPE_HEIGHT, stripe_count};
3319
3320    /// The count is what the client decodes, so a host with many cores cannot raise it: a 4K
3321    /// frame has room for thirty-three stripes at the minimum height and still gets at most
3322    /// `MAX_STRIPES`.
3323    #[test]
3324    fn a_tall_frame_is_not_cut_into_one_stripe_per_core() {
3325        let n = stripe_count(2160, Codec::Jpeg);
3326        assert!(n <= MAX_STRIPES, "a 4K frame cut into {n} stripes");
3327        assert!(n >= 1, "a 4K frame cut into none");
3328    }
3329
3330    /// A stripe still never falls below the minimum height, so a short frame is cut by its own
3331    /// height rather than by the ceiling.
3332    #[test]
3333    fn a_short_frame_is_cut_by_its_height() {
3334        assert_eq!(stripe_count(MIN_STRIPE_HEIGHT - 1, Codec::Jpeg), 1);
3335        assert!(stripe_count(MIN_STRIPE_HEIGHT * 2, Codec::Jpeg) <= 2);
3336    }
3337
3338    /// A video codec carries one picture a frame whatever the height.
3339    #[test]
3340    fn a_video_frame_is_one_region() {
3341        for codec in Codec::VIDEO {
3342            assert_eq!(stripe_count(2160, codec), 1, "{codec:?}");
3343        }
3344    }
3345}
3346
3347#[cfg(test)]
3348mod tests {
3349    /// Stripes are coded at once only where there are several and `cpu_encode_threading` is on.
3350    #[test]
3351    fn stripes_are_coded_at_once_only_with_cpu_encode_threading() {
3352        use super::stripes_at_once;
3353        use crate::CaptureSettings;
3354        let on = CaptureSettings::default();
3355        let off = CaptureSettings {
3356            cpu_encode_threading: false,
3357            ..Default::default()
3358        };
3359        assert!(stripes_at_once(&on, 8));
3360        assert!(!stripes_at_once(&on, 1));
3361        assert!(!stripes_at_once(&off, 8));
3362        assert!(!stripes_at_once(&off, 1));
3363    }
3364
3365    /// A CRF session is capped only where a VBV multiplier asks for it, with the buffer that
3366    /// multiplier names at the configured rate as its peak; CBR always has one.
3367    #[test]
3368    fn a_crf_session_takes_a_vbv_only_where_a_multiplier_asks() {
3369        use crate::CaptureSettings;
3370        let crf = |mult| CaptureSettings {
3371            video_bitrate_kbps: 8000,
3372            video_vbv_multiplier: mult,
3373            target_fps: 30.0,
3374            ..Default::default()
3375        };
3376        assert_eq!(super::rate_control(&crf(0.0)), (8000, 0));
3377        assert_eq!(super::rate_control(&crf(12.0)), (8000, 3200));
3378        let cbr = CaptureSettings {
3379            video_cbr_mode: true,
3380            ..crf(0.0)
3381        };
3382        assert!(super::rate_control(&cbr).1 > 0);
3383    }
3384
3385    use super::{Codec, StripeState, compute_stripe_geometries};
3386
3387    /// Without `gpl` a constant-rate stripe is cleaned up through OpenH264's rate control past the
3388    /// frames it skips: a page coded far over the target is followed by seconds of skipped frames,
3389    /// which say nothing of the picture, and the cleanup refines once they end, where a burst of
3390    /// a few frames ran out inside them and left the picture as the page's own frame had it.
3391    #[cfg(not(feature = "gpl"))]
3392    #[test]
3393    fn openh264_cleans_up_past_the_frames_it_skips() {
3394        use crate::CaptureSettings;
3395        use openh264::decoder::Decoder;
3396        use openh264::formats::YUVSource;
3397        let (w, h) = (640, 352);
3398        let settings = CaptureSettings {
3399            width: w,
3400            height: h,
3401            codec: Codec::H264,
3402            target_fps: 30.0,
3403            video_cbr_mode: true,
3404            video_bitrate_kbps: 150,
3405            video_streaming_mode: false,
3406            omit_stripe_headers: true,
3407            ..Default::default()
3408        };
3409        let flat = vec![128u8; (w * h * 4) as usize];
3410        let page: Vec<u8> = (0..(w * h) as usize)
3411            .flat_map(|i| {
3412                let v = if ((i * 2654435761usize) >> 7).is_multiple_of(3) {
3413                    230
3414                } else {
3415                    20
3416                };
3417                [v, v, v, 255]
3418            })
3419            .collect();
3420        let mut stripes = Vec::new();
3421        let mut dec = Decoder::new().expect("decoder");
3422        let (mut sent, mut skipped_before_last, mut last) = (0usize, false, None);
3423        for i in 0..900u16 {
3424            let px = if i < 10 { &flat } else { &page };
3425            let out = super::encode_cpu(
3426                &mut stripes,
3427                px,
3428                w,
3429                h,
3430                &[],
3431                &settings,
3432                i,
3433                false,
3434                true,
3435                i == 0,
3436            );
3437            for stripe in &out {
3438                if i >= 10 {
3439                    sent += 1;
3440                }
3441                if let Ok(Some(img)) = dec.decode(&stripe.data) {
3442                    let stride = img.strides().0;
3443                    last = Some(
3444                        (0..h as usize)
3445                            .flat_map(|r| img.y()[r * stride..r * stride + w as usize].to_vec())
3446                            .collect::<Vec<u8>>(),
3447                    );
3448                }
3449            }
3450            if i > 10
3451                && out.is_empty()
3452                && stripes
3453                    .first()
3454                    .is_some_and(|st| st.h264_burst_frames_remaining > 0)
3455            {
3456                skipped_before_last = true;
3457            }
3458        }
3459        let luma: Vec<u8> = page.chunks(4).map(|p| p[0]).collect();
3460        let picture = last.expect("a picture");
3461        let mse = picture
3462            .iter()
3463            .zip(&luma)
3464            .map(|(&a, &b)| {
3465                let d = (f64::from(a) - 16.0) * 255.0 / 219.0 - f64::from(b);
3466                d * d
3467            })
3468            .sum::<f64>()
3469            / luma.len() as f64;
3470        let psnr = 10.0 * (255.0f64 * 255.0 / mse.max(1e-9)).log10();
3471        println!(
3472            "frames sent after the page: {sent}, skipped inside the cleanup: {skipped_before_last}, {psnr:.1} dB"
3473        );
3474        assert!(
3475            skipped_before_last,
3476            "the rate control skipped frames inside the cleanup"
3477        );
3478        assert!(
3479            sent > 20,
3480            "the cleanup refined past the skipped frames: {sent} frames"
3481        );
3482        assert!(psnr > 30.0, "{psnr:.1} dB");
3483    }
3484
3485    /// A scheduled key frame waits for the software H.264 session to change: a still screen
3486    /// sends nothing through its intervals, and the first change after one keys the frame, once.
3487    #[test]
3488    fn a_scheduled_key_frame_waits_for_the_change() {
3489        use crate::CaptureSettings;
3490        let (w, h) = (128, 512);
3491        let settings = CaptureSettings {
3492            width: w,
3493            height: h,
3494            codec: Codec::H264,
3495            video_crf: 25,
3496            use_paint_over_quality: false,
3497            video_streaming_mode: false,
3498            keyframe_interval_s: 1.0,
3499            target_fps: 60.0,
3500            ..Default::default()
3501        };
3502        let mut stripes = Vec::new();
3503        let px: Vec<u8> = (0..(w * h * 4) as usize).map(|i| (i % 251) as u8).collect();
3504        let mut encode = |pixels: &[u8], frame: u16| {
3505            super::encode_cpu(
3506                &mut stripes,
3507                pixels,
3508                w,
3509                h,
3510                &[],
3511                &settings,
3512                frame,
3513                false,
3514                true,
3515                false,
3516            )
3517        };
3518        assert_eq!(encode(&px, 1).len(), 1, "the first frame is sent");
3519        assert!(
3520            (2..200).all(|frame| encode(&px, frame).is_empty()),
3521            "a still screen sends nothing through its intervals"
3522        );
3523        let mut moved = px.clone();
3524        for b in moved.iter_mut().take((w * 8 * 4) as usize) {
3525            *b = b.wrapping_add(97);
3526        }
3527        let key = encode(&moved, 200);
3528        assert_eq!(key.len(), 1, "the change sends the frame");
3529        assert_eq!(key[0].data[1], 0x11, "as the key frame owed");
3530        for b in moved.iter_mut().take((w * 8 * 4) as usize) {
3531            *b = b.wrapping_add(31);
3532        }
3533        let next = encode(&moved, 201);
3534        assert_eq!(next.len(), 1);
3535        assert_ne!(next[0].data[1], 0x11, "once");
3536    }
3537
3538    /// Without `gpl` the software H.264 session is one OpenH264 instance for the whole frame,
3539    /// framed as every video session is: the first frame is an IDR whose wire header carries
3540    /// y-start 0 and the frame's geometry and decodes on its own to a picture of the frame's
3541    /// size, a static follow-up sends nothing, and motion confined to the top rows re-sends the
3542    /// frame as a delta.
3543    #[cfg(not(feature = "gpl"))]
3544    #[test]
3545    fn openh264_codes_the_whole_frame() {
3546        use crate::CaptureSettings;
3547        use openh264::decoder::Decoder;
3548        use openh264::formats::YUVSource;
3549        let (w, h) = (128, 512);
3550        let settings = CaptureSettings {
3551            width: w,
3552            height: h,
3553            codec: Codec::H264,
3554            video_crf: 25,
3555            use_paint_over_quality: false,
3556            video_streaming_mode: false,
3557            ..Default::default()
3558        };
3559        let mut stripes = Vec::new();
3560        let px: Vec<u8> = (0..(w * h * 4) as usize).map(|i| (i % 251) as u8).collect();
3561        let mut encode = |pixels: &[u8], frame: u16| {
3562            super::encode_cpu(
3563                &mut stripes,
3564                pixels,
3565                w,
3566                h,
3567                &[],
3568                &settings,
3569                frame,
3570                false,
3571                true,
3572                false,
3573            )
3574        };
3575        let first = encode(&px, 0);
3576        assert_eq!(first.len(), 1, "the first frame is sent whole");
3577        let d = &first[0].data;
3578        assert_eq!(d[0], 0x04, "video tag");
3579        assert_eq!(d[1], 0x11, "the first frame is an H.264 key frame");
3580        assert_eq!(u16::from_be_bytes([d[2], d[3]]), 0, "frame number");
3581        assert_eq!(u16::from_be_bytes([d[4], d[5]]), 0, "y-start");
3582        assert_eq!(i32::from(u16::from_be_bytes([d[6], d[7]])), w, "width");
3583        assert_eq!(i32::from(u16::from_be_bytes([d[8], d[9]])), h, "height");
3584        assert_eq!((first[0].stripe_y_start, first[0].stripe_height), (0, h));
3585        let mut dec = Decoder::new().expect("decoder");
3586        let img = dec
3587            .decode(&d[crate::encoders::codec::VIDEO_HEADER_LEN..])
3588            .expect("decode")
3589            .expect("an IDR decodes on its own");
3590        assert_eq!(img.dimensions(), (w as usize, h as usize));
3591        assert!(encode(&px, 1).is_empty(), "a static frame sends nothing");
3592        let mut moved = px.clone();
3593        for b in moved.iter_mut().take((w * 8 * 4) as usize) {
3594            *b = b.wrapping_add(97);
3595        }
3596        let top = encode(&moved, 2);
3597        assert_eq!(top.len(), 1, "motion in the top rows re-sends the frame");
3598        assert_eq!(
3599            top[0].data[1], 0x10,
3600            "an unforced follow-up is an H.264 delta frame"
3601        );
3602        assert_eq!(
3603            u16::from_be_bytes([top[0].data[2], top[0].data[3]]),
3604            2,
3605            "frame number"
3606        );
3607    }
3608
3609    /// A stripe hashed elsewhere reads as one hashed here: the same changes, damage blocks and
3610    /// their renewal for the same sequence of hashes, whatever produced them.
3611    #[test]
3612    fn hash_dirty_reads_as_content_dirty() {
3613        let frames: Vec<Vec<u8>> = [1u8, 1, 2, 1, 1, 1, 1, 1, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7]
3614            .iter()
3615            .map(|&v| vec![v; 256])
3616            .collect();
3617        let (mut here, mut there) = (StripeState::default(), StripeState::default());
3618        for frame in &frames {
3619            assert_eq!(
3620                here.content_dirty(frame, 2, 3),
3621                there.hash_dirty(super::fast_hash(frame), 2, 3)
3622            );
3623            assert_eq!(here.in_damage_block, there.in_damage_block);
3624        }
3625    }
3626
3627    /// With `threshold = 2` and `duration = 3`, a first change reads dirty and two consecutive
3628    /// changes open a damage block that holds dirty for three frames without re-hashing; once content
3629    /// has gone static, the end-of-block re-hash exits the block and the stripe reads clean again.
3630    #[test]
3631    fn content_dirty_detects_change_and_damage_block() {
3632        let mut st = StripeState::default();
3633        let a = vec![1u8; 256];
3634        let b = vec![2u8; 256];
3635        assert!(st.content_dirty(&a, 2, 3));
3636        assert!(!st.content_dirty(&a, 2, 3));
3637        assert!(st.content_dirty(&b, 2, 3));
3638        assert!(st.content_dirty(&a, 2, 3));
3639        assert!(st.in_damage_block);
3640        assert!(st.content_dirty(&a, 2, 3));
3641        assert!(st.content_dirty(&a, 2, 3));
3642        assert!(st.content_dirty(&a, 2, 3));
3643        assert!(!st.in_damage_block);
3644        assert!(!st.content_dirty(&a, 2, 3));
3645
3646        // Motion through a whole block renews it; motion that stops inside one ends with it.
3647        let frames: Vec<Vec<u8>> = (0..12u8).map(|i| vec![i; 256]).collect();
3648        let mut st = StripeState::default();
3649        for f in &frames {
3650            assert!(st.content_dirty(f, 2, 3));
3651        }
3652        assert!(
3653            st.in_damage_block,
3654            "a region moving every frame stays in its block"
3655        );
3656        let still = frames.last().unwrap();
3657        let dirty: Vec<bool> = (0..6).map(|_| st.content_dirty(still, 2, 3)).collect();
3658        assert_eq!(
3659            dirty,
3660            [true, true, false, false, false, false],
3661            "clean once the block that saw it stop ends"
3662        );
3663    }
3664
3665    /// With compositor damage as the authority (Wayland), a clean frame must still advance the
3666    /// paint-over countdown and fire the repaint at the trigger, and once every stripe has
3667    /// latched (`paint_over_sent`) further clean frames must produce nothing — that quiescent
3668    /// tail is the idle fast path, which skips the stripe fan-out entirely.
3669    #[test]
3670    fn clean_frames_countdown_fire_paintover_then_go_quiescent() {
3671        use crate::CaptureSettings;
3672        let (w, h) = (64, 128);
3673        let pixels = vec![128u8; (w * h * 4) as usize];
3674        let settings = CaptureSettings {
3675            width: w,
3676            height: h,
3677            codec: Codec::Jpeg,
3678            jpeg_quality: 60,
3679            paint_over_jpeg_quality: 90,
3680            use_paint_over_quality: true,
3681            paint_over_trigger_frames: 5,
3682            ..Default::default()
3683        };
3684        let mut stripes = Vec::new();
3685        let full = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
3686        let dirty = super::encode_cpu(
3687            &mut stripes,
3688            &pixels,
3689            w,
3690            h,
3691            &full,
3692            &settings,
3693            0,
3694            false,
3695            false,
3696            false,
3697        );
3698        assert!(!dirty.is_empty(), "damaged frame must encode");
3699
3700        let mut painted = Vec::new();
3701        for frame in 1..=20u16 {
3702            let out = super::encode_cpu(
3703                &mut stripes,
3704                &pixels,
3705                w,
3706                h,
3707                &[],
3708                &settings,
3709                frame,
3710                false,
3711                false,
3712                false,
3713            );
3714            painted.extend(out.iter().map(|s| (frame, s.stripe_y_start)));
3715        }
3716        let per_frame = stripes.len().div_ceil(super::CLEANUP_STAGGER_FRAMES).max(1);
3717        assert_eq!(
3718            painted.len(),
3719            stripes.len(),
3720            "each stripe is painted over exactly once: {painted:?}"
3721        );
3722        assert_eq!(
3723            painted[0].0, settings.paint_over_trigger_frames as u16,
3724            "starting at the trigger"
3725        );
3726        for (n, (frame, _)) in painted.iter().enumerate() {
3727            assert_eq!(
3728                *frame as usize,
3729                settings.paint_over_trigger_frames as usize + n / per_frame,
3730                "{per_frame} a frame"
3731            );
3732        }
3733        assert!(
3734            stripes.iter().all(|st| st.paint_over_sent),
3735            "all stripes latched after the repaint"
3736        );
3737    }
3738
3739    /// Hash-damage sources (X11) take the sequential-scan fast path: static frames advance
3740    /// the countdown and fire the paint-over exactly once, the quiescent tail emits nothing,
3741    /// and a subsequent content change is still detected and encoded (streak state reset by
3742    /// the fast path must not swallow the wake-up).
3743    #[test]
3744    fn hash_scan_idles_after_paintover_and_wakes_on_change() {
3745        use crate::CaptureSettings;
3746        let (w, h) = (64, 128);
3747        let static_px = vec![128u8; (w * h * 4) as usize];
3748        let changed_px = vec![200u8; (w * h * 4) as usize];
3749        let settings = CaptureSettings {
3750            width: w,
3751            height: h,
3752            codec: Codec::Jpeg,
3753            jpeg_quality: 60,
3754            paint_over_jpeg_quality: 90,
3755            use_paint_over_quality: true,
3756            paint_over_trigger_frames: 5,
3757            damage_block_threshold: 10,
3758            damage_block_duration: 10,
3759            ..Default::default()
3760        };
3761        let mut stripes = Vec::new();
3762        let first = super::encode_cpu(
3763            &mut stripes,
3764            &static_px,
3765            w,
3766            h,
3767            &[],
3768            &settings,
3769            0,
3770            false,
3771            true,
3772            false,
3773        );
3774        assert!(
3775            !first.is_empty(),
3776            "first frame hashes as changed and encodes"
3777        );
3778
3779        let mut painted = Vec::new();
3780        for frame in 1..=20u16 {
3781            let out = super::encode_cpu(
3782                &mut stripes,
3783                &static_px,
3784                w,
3785                h,
3786                &[],
3787                &settings,
3788                frame,
3789                false,
3790                true,
3791                false,
3792            );
3793            painted.extend(out.iter().map(|s| (frame, s.stripe_y_start)));
3794        }
3795        assert_eq!(
3796            painted.len(),
3797            stripes.len(),
3798            "each stripe is painted over exactly once while static: {painted:?}"
3799        );
3800        assert_eq!(painted[0].0, settings.paint_over_trigger_frames as u16);
3801
3802        let woke = super::encode_cpu(
3803            &mut stripes,
3804            &changed_px,
3805            w,
3806            h,
3807            &[],
3808            &settings,
3809            21,
3810            false,
3811            true,
3812            false,
3813        );
3814        assert!(!woke.is_empty(), "content change after idle must encode");
3815    }
3816
3817    /// A caret changing one stripe every fourth frame keeps it from ever holding still for the
3818    /// trigger, yet that stripe is cleaned up once the low-motion window passes, and the stripes
3819    /// around it are cleaned up at the trigger as if it were not there.
3820    #[test]
3821    fn a_blinking_caret_starves_no_stripe_of_its_cleanup() {
3822        use crate::CaptureSettings;
3823        let (w, h) = (64i32, 256i32);
3824        let settings = CaptureSettings {
3825            width: w,
3826            height: h,
3827            codec: Codec::Jpeg,
3828            jpeg_quality: 40,
3829            paint_over_jpeg_quality: 90,
3830            use_paint_over_quality: true,
3831            paint_over_trigger_frames: 5,
3832            damage_block_threshold: 10,
3833            damage_block_duration: 10,
3834            ..Default::default()
3835        };
3836        let mut stripes = Vec::new();
3837        let frame = |caret: bool| {
3838            let mut px = vec![128u8; (w * h * 4) as usize];
3839            if caret {
3840                px[(w * 4 * 3) as usize..(w * 4 * 3) as usize + 8].fill(0);
3841            }
3842            px
3843        };
3844        // The luma DC quantizer of a stripe's JPEG: 3 at the paint-over quality, 20 at the base one.
3845        let dc_quant = |data: &[u8]| {
3846            let at = data
3847                .windows(2)
3848                .position(|m| m == [0xFF, 0xDB])
3849                .expect("a quantization table");
3850            data[at + 5]
3851        };
3852        let mut cleaned = std::collections::HashMap::new();
3853        for n in 0..60u16 {
3854            let out = super::encode_cpu(
3855                &mut stripes,
3856                &frame((n / 4) % 2 == 0),
3857                w,
3858                h,
3859                &[],
3860                &settings,
3861                n,
3862                false,
3863                true,
3864                false,
3865            );
3866            for s in &out {
3867                if dc_quant(&s.data) < 8 {
3868                    cleaned.entry(s.stripe_y_start).or_insert(n);
3869                }
3870            }
3871        }
3872        let rows = stripes.len();
3873        assert!(rows > 1, "the frame is striped");
3874        let caret_stripe = 0;
3875        let others: Vec<u16> = cleaned
3876            .iter()
3877            .filter(|(y, _)| **y != caret_stripe)
3878            .map(|(_, n)| *n)
3879            .collect();
3880        assert_eq!(
3881            others.len(),
3882            rows - 1,
3883            "every still stripe is cleaned up: {cleaned:?}"
3884        );
3885        assert!(
3886            others
3887                .iter()
3888                .all(|&n| n <= 5 + super::CLEANUP_STAGGER_FRAMES as u16),
3889            "at the trigger: {cleaned:?}"
3890        );
3891        let at = *cleaned
3892            .get(&caret_stripe)
3893            .expect("the caret's stripe is cleaned up too");
3894        assert!(
3895            (19..=23).contains(&at),
3896            "once the low-motion window passes: {at}"
3897        );
3898    }
3899
3900    /// Total rows covered by a geometry — the sum of all stripe heights.
3901    fn covered(geoms: &[(usize, usize)]) -> usize {
3902        geoms.iter().map(|&(_, h)| h).sum()
3903    }
3904
3905    /// Assert the stripes tile the frame with no gaps or overlap: each stripe's `y_start`
3906    /// equals the running sum of the preceding heights.
3907    fn assert_contiguous(geoms: &[(usize, usize)]) {
3908        let mut y = 0;
3909        for &(sy, sh) in geoms {
3910            assert_eq!(sy, y, "stripes must be contiguous");
3911            y += sh;
3912        }
3913    }
3914
3915    /// JPEG geometry covers the full frame height with contiguous stripes, across a range of
3916    /// heights (odd ones included) and stripe counts.
3917    #[test]
3918    fn jpeg_covers_every_row_including_odd() {
3919        for &h in &[1usize, 63, 720, 721, 1079, 1080, 1081] {
3920            for &n in &[1usize, 2, 3, 8, 16] {
3921                let g = compute_stripe_geometries(h, n, Codec::Jpeg);
3922                assert_eq!(g.len(), n);
3923                assert_eq!(
3924                    covered(&g),
3925                    h,
3926                    "JPEG must cover full height h={} n={}",
3927                    h,
3928                    n
3929                );
3930                assert_contiguous(&g);
3931            }
3932        }
3933    }
3934
3935    /// Video geometry is one region of even height covering the whole frame except at most one
3936    /// trailing odd row, whatever the count asked for.
3937    #[test]
3938    fn a_video_frame_is_one_even_region() {
3939        for &h in &[64usize, 720, 721, 1080, 1081] {
3940            for &n in &[1usize, 2, 8] {
3941                let g = compute_stripe_geometries(h, n, Codec::H264);
3942                assert_eq!(g, vec![(0, h & !1)], "h={h} n={n}");
3943            }
3944        }
3945    }
3946
3947    /// A conversion's bands run on a pool of as many threads; a count past the pooled ones runs
3948    /// on the global pool, and a conversion already on a pool, such as the forked encoder
3949    /// probe's, stays on it.
3950    #[test]
3951    fn bands_convert_on_a_pool_of_as_many_threads() {
3952        use super::{MAX_POOLED_BANDS, on_band_pool};
3953        let on_pool = || {
3954            (
3955                rayon::current_num_threads(),
3956                rayon::current_thread_index().is_some(),
3957            )
3958        };
3959        for bands in [2usize, 4, 7] {
3960            assert_eq!(on_band_pool(bands, on_pool), (bands, true), "{bands} bands");
3961            let named = on_band_pool(bands, || std::thread::current().name().map(str::to_owned));
3962            assert_eq!(named.as_deref(), Some("pixelflux-csc"));
3963        }
3964        let past = MAX_POOLED_BANDS + 1;
3965        assert_eq!(
3966            on_band_pool(past, on_pool),
3967            (rayon::current_num_threads(), false),
3968            "{past} bands run where they are called"
3969        );
3970        let own = rayon::ThreadPoolBuilder::new()
3971            .num_threads(1)
3972            .build()
3973            .expect("pool");
3974        assert_eq!(
3975            own.install(|| on_band_pool(4, on_pool)),
3976            (1, true),
3977            "a conversion on a pool keeps to it"
3978        );
3979    }
3980
3981    /// The host convert sites chroma at the center of the block on both its paths, the
3982    /// single-threaded one and the banded one, and a band boundary never splits a chroma pair.
3983    #[test]
3984    fn the_host_convert_sites_chroma_at_the_block_center() {
3985        use super::convert_to_yuv_mt;
3986        let (w, h) = (64usize, 64usize);
3987        let bgra = crate::encoders::chroma_siting::bgra(w, h);
3988        for bands in [1usize, 4, 7] {
3989            let (mut yp, mut up, mut vp) =
3990                (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
3991            convert_to_yuv_mt(
3992                &bgra,
3993                (w * 4) as u32,
3994                w,
3995                h,
3996                false,
3997                false,
3998                false,
3999                false,
4000                &mut yp,
4001                &mut up,
4002                &mut vp,
4003                (w, w / 2),
4004                bands,
4005            )
4006            .expect("convert");
4007            let worst = up
4008                .iter()
4009                .zip(&vp)
4010                .map(|(&u, &v)| (f64::from(u) - 128.0).hypot(f64::from(v) - 128.0))
4011                .fold(0.0f64, f64::max);
4012            assert!(
4013                worst <= 2.0,
4014                "{bands} bands: chroma sits {worst:.1} off neutral"
4015            );
4016        }
4017    }
4018
4019    /// The JPEG stripes a WebSockets session sends by default hand BGRA to libjpeg-turbo, which
4020    /// subsamples chroma itself, so the tile is held to the same neutral chroma there.
4021    #[test]
4022    fn the_jpeg_path_sites_chroma_at_the_block_center() {
4023        use super::Codec;
4024        use crate::webcam::decode::new_decoder;
4025        let (w, h) = (64usize, 64usize);
4026        let bgra = crate::encoders::chroma_siting::bgra(w, h);
4027        let mut comp = turbojpeg::Compressor::new().expect("turbojpeg compressor");
4028        comp.set_quality(90).expect("quality");
4029        let img = turbojpeg::Image {
4030            pixels: &bgra[..],
4031            width: w,
4032            pitch: w * 4,
4033            height: h,
4034            format: turbojpeg::PixelFormat::BGRA,
4035        };
4036        let jpeg = comp.compress_to_vec(img).expect("compress");
4037        let mut dec = new_decoder(Codec::Jpeg).expect("jpeg decoder");
4038        assert!(
4039            dec.decode(&jpeg).expect("decode"),
4040            "the stripe decoded nothing"
4041        );
4042        let worst = crate::encoders::chroma_siting::worst(&dec.frame().expect("frame"));
4043        assert!(worst <= 4.0, "JPEG chroma sits {worst:.1} off neutral");
4044    }
4045
4046    /// The 10-bit conversion against the BT.709 arithmetic it implements, at both ranges and
4047    /// both chroma formats: every sample within one code value, a 4:2:0 chroma sample being
4048    /// the conversion of its block's mean, and the same picture whatever the band count.
4049    #[test]
4050    fn ten_bit_conversion_matches_bt709() {
4051        use super::convert_to_yuv10_mt;
4052        let (w, h) = (34usize, 18usize);
4053        let mut bgra = vec![0u8; w * h * 4];
4054        let mut seed = 0x2545_f491u32;
4055        for px in bgra.as_chunks_mut::<4>().0 {
4056            for c in px.iter_mut().take(3) {
4057                seed ^= seed << 13;
4058                seed ^= seed >> 17;
4059                seed ^= seed << 5;
4060                *c = (seed >> 11) as u8;
4061            }
4062            px[3] = 255;
4063        }
4064        let reference = |r: f64, g: f64, b: f64, full: bool| {
4065            let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
4066            let (cb, cr) = ((b - y) / 1.8556, (r - y) / 1.5748);
4067            if full {
4068                (
4069                    y * 1023.0 / 255.0,
4070                    512.0 + cb * 1023.0 / 255.0,
4071                    512.0 + cr * 1023.0 / 255.0,
4072                )
4073            } else {
4074                (
4075                    64.0 + y * 876.0 / 255.0,
4076                    512.0 + cb * 896.0 / 255.0,
4077                    512.0 + cr * 896.0 / 255.0,
4078                )
4079            }
4080        };
4081        let rgb = |x: usize, y: usize| {
4082            let p = &bgra[(y * w + x) * 4..];
4083            (f64::from(p[2]), f64::from(p[1]), f64::from(p[0]))
4084        };
4085        for full in [false, true] {
4086            for i444 in [false, true] {
4087                let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
4088                let convert = |bands: usize| {
4089                    let (mut y, mut u, mut v) =
4090                        (vec![0u16; w * h], vec![0u16; cw * ch], vec![0u16; cw * ch]);
4091                    convert_to_yuv10_mt(
4092                        &bgra,
4093                        w * 4,
4094                        w,
4095                        h,
4096                        false,
4097                        i444,
4098                        full,
4099                        &mut y,
4100                        &mut u,
4101                        &mut v,
4102                        (w, cw),
4103                        bands,
4104                    );
4105                    (y, u, v)
4106                };
4107                let (y, u, v) = convert(1);
4108                assert_eq!(
4109                    (y.clone(), u.clone(), v.clone()),
4110                    convert(4),
4111                    "bands change nothing"
4112                );
4113                for row in 0..h {
4114                    for col in 0..w {
4115                        let (r, g, b) = rgb(col, row);
4116                        let want = reference(r, g, b, full).0;
4117                        assert!((f64::from(y[row * w + col]) - want).abs() <= 1.0, "luma");
4118                    }
4119                }
4120                for row in 0..ch {
4121                    for col in 0..cw {
4122                        let (r, g, b) = if i444 {
4123                            rgb(col, row)
4124                        } else {
4125                            let mut sum = (0.0, 0.0, 0.0);
4126                            for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
4127                                let p = rgb(2 * col + dx, 2 * row + dy);
4128                                sum = (sum.0 + p.0 / 4.0, sum.1 + p.1 / 4.0, sum.2 + p.2 / 4.0);
4129                            }
4130                            sum
4131                        };
4132                        let (_, cb, cr) = reference(r, g, b, full);
4133                        assert!((f64::from(u[row * cw + col]) - cb).abs() <= 1.0, "cb");
4134                        assert!((f64::from(v[row * cw + col]) - cr).abs() <= 1.0, "cr");
4135                    }
4136                }
4137            }
4138        }
4139    }
4140
4141    /// A 10-bit x264 session takes and reports quantizers on the scale an 8-bit one does: the
4142    /// rate factor's first frame reads the same quantizer at either depth, and a key frame held
4143    /// at a coarse quantizer comes out as small.
4144    #[cfg(feature = "gpl")]
4145    #[test]
4146    fn x264_quantizers_keep_their_scale_at_ten_bits() {
4147        use super::H264EncoderWrapper;
4148        if !H264EncoderWrapper::ten_bit() {
4149            println!("this libx264 was built for 8 bits alone; nothing to check");
4150            return;
4151        }
4152        let (w, h) = (640usize, 360usize);
4153        let mut seed = 7u32;
4154        let mut noise = |n: usize, amplitude: u32, base: u32| -> Vec<u32> {
4155            (0..n)
4156                .map(|_| {
4157                    seed ^= seed << 13;
4158                    seed ^= seed >> 17;
4159                    seed ^= seed << 5;
4160                    base + seed % amplitude
4161                })
4162                .collect()
4163        };
4164        let (luma, chroma) = (noise(w * h, 60, 90), noise(w * h / 4, 20, 118));
4165        let mut seen = Vec::new();
4166        for depth in [8u32, 10] {
4167            let plane = |samples: &[u32]| -> Vec<u8> {
4168                if depth == 10 {
4169                    samples
4170                        .iter()
4171                        .flat_map(|s| ((s * 4) as u16).to_le_bytes())
4172                        .collect()
4173                } else {
4174                    samples.iter().map(|s| *s as u8).collect()
4175                }
4176            };
4177            let (y, c) = (plane(&luma), plane(&chroma));
4178            let bytes = (depth as usize).div_ceil(8);
4179            let mut enc = H264EncoderWrapper::with_depth(
4180                w as i32, h as i32, 25, false, depth, 30.0, 1, false, 0, 0, 0, 0, 0,
4181            )
4182            .expect("x264 init");
4183            let encode = |enc: &mut H264EncoderWrapper, id: u16| {
4184                let mut out = Vec::new();
4185                assert!(enc.encode_with_headers(
4186                    &y,
4187                    &c,
4188                    &c,
4189                    (w * bytes) as i32,
4190                    (w / 2 * bytes) as i32,
4191                    (w / 2 * bytes) as i32,
4192                    id,
4193                    true,
4194                    true,
4195                    &mut out,
4196                ));
4197                out.len()
4198            };
4199            encode(&mut enc, 0);
4200            let rate_factor_qp = enc.last_qp().expect("a quantizer");
4201            enc.hold_quantizer(45);
4202            seen.push((rate_factor_qp, encode(&mut enc, 1)));
4203        }
4204        let ((qp8, held8), (qp10, held10)) = (seen[0], seen[1]);
4205        assert!(
4206            qp8.abs_diff(qp10) <= 2,
4207            "rate factor quantizers {qp8} and {qp10}"
4208        );
4209        assert!(
4210            held10 < 2 * held8,
4211            "held key frames of {held8} and {held10} bytes"
4212        );
4213    }
4214
4215    /// A 10-bit x264 session declares High 10, or High 4:4:4 Predictive, and codes the
4216    /// 16-bit planes it is handed.
4217    #[cfg(feature = "gpl")]
4218    #[test]
4219    fn x264_codes_ten_bits() {
4220        use super::H264EncoderWrapper;
4221        if !H264EncoderWrapper::ten_bit() {
4222            println!("this libx264 was built for 8 bits alone; nothing to check");
4223            return;
4224        }
4225        let (w, h) = (64usize, 64usize);
4226        for (i444, profile) in [(false, 110u8), (true, 244u8)] {
4227            let mut enc = H264EncoderWrapper::with_depth(
4228                w as i32, h as i32, 25, i444, 10, 30.0, 1, false, 0, 0, 0, 0, 0,
4229            )
4230            .expect("x264 init");
4231            assert_eq!(enc.bit_depth, 10);
4232            let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
4233            let planes = [
4234                vec![400u16; w * h],
4235                vec![512u16; cw * ch],
4236                vec![600u16; cw * ch],
4237            ];
4238            let bytes = |p: &[u16]| unsafe {
4239                std::slice::from_raw_parts(p.as_ptr().cast::<u8>(), p.len() * 2).to_vec()
4240            };
4241            let (y, u, v) = (bytes(&planes[0]), bytes(&planes[1]), bytes(&planes[2]));
4242            let mut out = Vec::new();
4243            assert!(enc.encode_with_headers(
4244                &y,
4245                &u,
4246                &v,
4247                (w * 2) as i32,
4248                (cw * 2) as i32,
4249                (cw * 2) as i32,
4250                0,
4251                true,
4252                true,
4253                &mut out,
4254            ));
4255            let sps = crate::encoders::codec::annexb_nals(&out)
4256                .find(|n| n[0] & 0x1f == 7)
4257                .expect("a sequence parameter set");
4258            assert_eq!(sps[1], profile, "the profile declared");
4259        }
4260    }
4261
4262    /// The NV12 conversion x264 encodes in place writes the samples the I420 one does, its
4263    /// chroma interleaved, from either byte order, at any band count and into padded rows.
4264    #[test]
4265    fn the_nv12_convert_writes_the_i420_samples_interleaved() {
4266        use super::{Nv12Planes, convert_to_nv12_mt, convert_to_yuv_mt};
4267        let mut seed = 0x2545_f491_4f6c_dd1du64;
4268        for (w, h) in [(64usize, 64usize), (130, 66), (1920, 1080)] {
4269            let pixels: Vec<u8> = (0..w * h * 4)
4270                .map(|_| {
4271                    seed ^= seed << 13;
4272                    seed ^= seed >> 7;
4273                    seed ^= seed << 17;
4274                    seed as u8
4275                })
4276                .collect();
4277            for rgba in [false, true] {
4278                let (mut yp, mut up, mut vp) =
4279                    (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
4280                convert_to_yuv_mt(
4281                    &pixels,
4282                    (w * 4) as u32,
4283                    w,
4284                    h,
4285                    rgba,
4286                    false,
4287                    false,
4288                    false,
4289                    &mut yp,
4290                    &mut up,
4291                    &mut vp,
4292                    (w, w / 2),
4293                    1,
4294                )
4295                .expect("I420");
4296                for bands in [1usize, 4, 7] {
4297                    let mut nv12 = Nv12Planes::new(w, h);
4298                    let strides = (nv12.stride[0], nv12.stride[1]);
4299                    let (y, uv) = nv12.planes_mut();
4300                    convert_to_nv12_mt(&pixels, (w * 4) as u32, w, h, rgba, y, uv, strides, bands)
4301                        .expect("NV12");
4302                    let (y, uv) = nv12.planes();
4303                    for row in 0..h {
4304                        assert_eq!(
4305                            &y[row * strides.0..][..w],
4306                            &yp[row * w..][..w],
4307                            "{w}x{h} rgba {rgba}: luma row {row}"
4308                        );
4309                    }
4310                    for row in 0..h / 2 {
4311                        let line = &uv[row * strides.1..][..w];
4312                        let (u, v) = (&up[row * w / 2..][..w / 2], &vp[row * w / 2..][..w / 2]);
4313                        assert!(
4314                            line.chunks(2)
4315                                .zip(u.iter().zip(v))
4316                                .all(|(c, (u, v))| c == [*u, *v]),
4317                            "{w}x{h} rgba {rgba}, {bands} bands: chroma row {row}"
4318                        );
4319                    }
4320                }
4321            }
4322        }
4323    }
4324
4325    /// An NV12 picture laid out as x264 asks is encoded where it lies, reading through the next
4326    /// encode the picture before (`held_input`), and codes the stream the same planes copied
4327    /// as I420 code, across a release.
4328    #[cfg(feature = "gpl")]
4329    #[test]
4330    #[cfg(x264_in_place)]
4331    fn x264_encodes_an_nv12_picture_in_place_as_it_codes_a_copy() {
4332        use super::{H264EncoderWrapper, Nv12Planes, convert_to_nv12_mt, convert_to_yuv_mt};
4333        let (w, h) = (200usize, 120usize);
4334        let frames: Vec<Vec<u8>> = (0..6)
4335            .map(|f| {
4336                (0..w * h * 4)
4337                    .map(|i| {
4338                        let (x, y) = ((i / 4) % w, (i / 4) / w);
4339                        ((x + 3 * f) * 7 + y * 3 + (i % 4) * 50) as u8
4340                    })
4341                    .collect()
4342            })
4343            .collect();
4344        let open = || {
4345            H264EncoderWrapper::new(w as i32, h as i32, 25, false, 30.0, 4, false, 0, 0, 0, 0)
4346                .expect("x264 init")
4347        };
4348        let (mut copied, mut in_place) = (open(), open());
4349        let mut pictures = [Nv12Planes::new(w, h), Nv12Planes::new(w, h)];
4350        assert!(
4351            pictures[0].in_place,
4352            "the linked libx264 lays out a picture to encode in place"
4353        );
4354        let layout = super::x264_input_layout(w, h).expect("layout");
4355        let align = codec_sys::x264::X264_IN_PLACE_ALIGN as usize;
4356        for p in &pictures {
4357            let (y, uv) = p.planes();
4358            assert!(
4359                (y.as_ptr().addr() - layout.offset[0]).is_multiple_of(align)
4360                    && (uv.as_ptr().addr() - layout.offset[1]).is_multiple_of(align),
4361                "each plane lies its layout offset into a buffer aligned as x264 asks"
4362            );
4363        }
4364        let (mut a, mut b) = (Vec::new(), Vec::new());
4365        for (n, bgra) in frames.iter().enumerate() {
4366            let (mut yp, mut up, mut vp) =
4367                (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
4368            convert_to_yuv_mt(
4369                bgra,
4370                (w * 4) as u32,
4371                w,
4372                h,
4373                false,
4374                false,
4375                false,
4376                false,
4377                &mut yp,
4378                &mut up,
4379                &mut vp,
4380                (w, w / 2),
4381                1,
4382            )
4383            .unwrap();
4384            let mut out = Vec::new();
4385            assert!(copied.encode_with_headers(
4386                &yp,
4387                &up,
4388                &vp,
4389                w as i32,
4390                (w / 2) as i32,
4391                (w / 2) as i32,
4392                n as u16,
4393                false,
4394                true,
4395                &mut out,
4396            ));
4397            a.extend_from_slice(&out);
4398
4399            let mb_w = w.div_ceil(16) * 16;
4400            let picture = &mut pictures[n % 2];
4401            let strides = (picture.stride[0], picture.stride[1]);
4402            let (y, uv) = picture.planes_mut();
4403            convert_to_nv12_mt(bgra, (w * 4) as u32, w, h, false, y, uv, strides, 2).unwrap();
4404            y[mb_w] = !y[w - 1];
4405            if n == 4 {
4406                in_place.release_input();
4407                assert_eq!(in_place.held_input(), None);
4408            }
4409            let picture = &pictures[n % 2];
4410            assert!(in_place.encode_nv12_with_headers(picture, n as u16, false, true, &mut out));
4411            b.extend_from_slice(&out);
4412            assert_eq!(in_place.held_input(), Some(picture.id()));
4413            let (y, _) = picture.planes();
4414            assert_eq!(
4415                y[mb_w],
4416                y[w - 1],
4417                "x264 padded the picture where it lies, past its last macroblock"
4418            );
4419        }
4420        assert!(a == b, "the stream the copy codes");
4421    }
4422}
4423
4424#[cfg(test)]
4425mod qp_bound_sweep {
4426    //! Invariants under test: the CBR QP clamp reaches libx264/OpenH264 (a max clamp must
4427    //! raise worst-case fidelity on rate-starved text at the cost of bitrate overshoot;
4428    //! a min clamp must cut spend on over-budgeted content) and defaults (0) leave the
4429    //! encoders' own behavior untouched. Each encoder is swept separately: the OpenH264
4430    //! sweep runs in every build (the crate is a dev-dependency), the x264 one needs `gpl`.
4431    #[cfg(feature = "gpl")]
4432    use super::H264EncoderWrapper;
4433    use crate::CaptureSettings;
4434    use crate::encoders::Codec;
4435    use crate::encoders::oh264::Openh264Encoder;
4436    use openh264::decoder::Decoder;
4437    use openh264::formats::YUVSource;
4438
4439    const W: usize = 1280;
4440    const H: usize = 720;
4441    const FRAMES: usize = 60;
4442
4443    /// Build a scrolling terminal-like luma frame: an 8x12 glyph grid seeded by an LCG and
4444    /// scrolled 4 px per frame — the worst case for screen-share rate control, with dense
4445    /// high-contrast detail (~40% lit pixels per glyph row) under full-frame motion.
4446    fn text_luma(frame: usize) -> Vec<u8> {
4447        let mut y = vec![18u8; W * H];
4448        let scroll = frame * 4;
4449        for row in 0..H {
4450            let srow = row + scroll;
4451            let cell_y = srow / 12;
4452            let in_glyph_y = srow % 12;
4453            if in_glyph_y >= 10 {
4454                continue;
4455            }
4456            for col in 0..W {
4457                let cell_x = col / 8;
4458                let in_glyph_x = col % 8;
4459                if in_glyph_x >= 7 {
4460                    continue;
4461                }
4462                let mut s = (cell_x as u32)
4463                    .wrapping_mul(2654435761)
4464                    .wrapping_add((cell_y as u32).wrapping_mul(40503))
4465                    .wrapping_add(1);
4466                s ^= s << 13;
4467                s ^= s >> 17;
4468                s ^= s << 5;
4469                if (s >> ((in_glyph_y * 3 + in_glyph_x) % 29)) & 1 == 1 {
4470                    y[row * W + col] = 224;
4471                }
4472            }
4473        }
4474        y
4475    }
4476
4477    /// Encode `FRAMES` scrolling-text luma frames through the x264 session at the
4478    /// given rate-control settings (constant gray chroma), returning each frame's raw bitstream.
4479    #[cfg(feature = "gpl")]
4480    fn encode_x264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
4481        let mut enc = H264EncoderWrapper::new(
4482            W as i32, H as i32, crf, false, 60.0, 4, cbr, kbps, 50, min_qp, max_qp,
4483        )
4484        .expect("x264 init");
4485        let u = vec![128u8; (W / 2) * (H / 2)];
4486        let v = vec![128u8; (W / 2) * (H / 2)];
4487        (0..FRAMES)
4488            .map(|i| {
4489                let y = text_luma(i);
4490                let mut out = Vec::new();
4491                enc.encode_with_headers(
4492                    &y,
4493                    &u,
4494                    &v,
4495                    W as i32,
4496                    (W / 2) as i32,
4497                    (W / 2) as i32,
4498                    i as u16,
4499                    i == 0,
4500                    true,
4501                    &mut out,
4502                );
4503                out
4504            })
4505            .collect()
4506    }
4507
4508    /// The x264 stream declares the signal its input was converted with: the BT.709 matrix in
4509    /// both chroma formats, at full range for I444 and limited for I420.
4510    #[cfg(feature = "gpl")]
4511    #[test]
4512    fn x264_declares_the_conversion_matrix() {
4513        use crate::encoders::codec::annexb_nals;
4514        use crate::encoders::sps::read_color;
4515        use crate::webcam::decode::{ColorTags, Decoder as _, VideoDecoder};
4516        for (i444, want) in [
4517            (false, ColorTags::BT709_LIMITED),
4518            (true, ColorTags::BT709_FULL),
4519        ] {
4520            let (w, h) = (128usize, 96usize);
4521            let mut enc =
4522                H264EncoderWrapper::new(w as i32, h as i32, 25, i444, 30.0, 1, false, 0, 0, 0, 0)
4523                    .expect("x264 init");
4524            let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
4525            let y = vec![90u8; w * h];
4526            let u = vec![128u8; cw * ch];
4527            let v = vec![160u8; cw * ch];
4528            let mut out = Vec::new();
4529            assert!(enc.encode_with_headers(
4530                &y, &u, &v, w as i32, cw as i32, cw as i32, 0, true, true, &mut out
4531            ));
4532            let sps = annexb_nals(&out)
4533                .find(|n| n[0] & 0x1f == 7)
4534                .expect("an SPS");
4535            let declared = read_color(sps).map(|s| ColorTags {
4536                matrix: s.matrix,
4537                full_range: s.full_range,
4538            });
4539            assert_eq!(declared, Some(want), "i444={i444}");
4540            if !i444 {
4541                let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
4542                assert!(dec.decode(&out).expect("decode"));
4543                assert_eq!(dec.color_tags(), Some(want));
4544            }
4545        }
4546    }
4547
4548    /// x264 is opened at the capture's rate as the fraction it names, its SPS declares that
4549    /// rate, and a session moved to another rate reopens at it.
4550    #[cfg(feature = "gpl")]
4551    #[test]
4552    fn x264_takes_the_frame_rate_as_its_fraction() {
4553        use crate::encoders::frame_rate::FrameRate;
4554        use crate::encoders::sps::h264_timing;
4555        let (w, h) = (64usize, 64usize);
4556        let (y, uv) = (vec![90u8; w * h], vec![128u8; w * h / 4]);
4557        for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
4558            let mut enc = H264EncoderWrapper::new(
4559                w as i32,
4560                h as i32,
4561                25,
4562                false,
4563                f64::from(num) / f64::from(den),
4564                1,
4565                true,
4566                4000,
4567                100,
4568                0,
4569                0,
4570            )
4571            .expect("x264 init");
4572            let param = unsafe {
4573                let mut param: codec_sys::x264::x264_param_t = std::mem::zeroed();
4574                codec_sys::x264::x264_encoder_parameters(enc.encoder, &mut param);
4575                param
4576            };
4577            assert_eq!((param.i_fps_num, param.i_fps_den), (num, den));
4578            let mut out = Vec::new();
4579            assert!(enc.encode_with_headers(
4580                &y,
4581                &uv,
4582                &uv,
4583                w as i32,
4584                (w / 2) as i32,
4585                (w / 2) as i32,
4586                0,
4587                true,
4588                true,
4589                &mut out
4590            ));
4591            assert_eq!(
4592                h264_timing(&out),
4593                Some((den, 2 * num)),
4594                "{num}/{den}: the SPS declares the rate"
4595            );
4596        }
4597        let mut enc = H264EncoderWrapper::new(
4598            w as i32, h as i32, 25, false, 60.0, 1, true, 4000, 100, 0, 0,
4599        )
4600        .expect("x264 init");
4601        enc.reconfigure_rate(4000, 100, 60000.0 / 1001.0);
4602        assert_eq!(
4603            enc.current_fps,
4604            FrameRate {
4605                num: 60000,
4606                den: 1001
4607            },
4608            "60 fps moved to 59.94"
4609        );
4610    }
4611
4612    /// The color chart, converted by the host path and encoded by x264, decodes back to the
4613    /// color that was painted when the BT.709 the stream declares is inverted — the check a
4614    /// client's presentation path performs on every frame, here with no browser in the way.
4615    #[cfg(feature = "gpl")]
4616    #[test]
4617    fn x264_paints_the_chart_it_converts() {
4618        use super::convert_to_yuv_mt;
4619        use crate::encoders::chroma_siting::{BT709, chart_bgra, chart_error};
4620        use crate::webcam::decode::{Decoder as _, VideoDecoder};
4621        let (w, h) = (256usize, 128usize);
4622        let bgra = chart_bgra(w, h);
4623        let (mut y, mut u, mut v) = (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
4624        convert_to_yuv_mt(
4625            &bgra,
4626            (w * 4) as u32,
4627            w,
4628            h,
4629            false,
4630            false,
4631            false,
4632            false,
4633            &mut y,
4634            &mut u,
4635            &mut v,
4636            (w, w / 2),
4637            4,
4638        )
4639        .expect("convert");
4640        let mut enc =
4641            H264EncoderWrapper::new(w as i32, h as i32, 20, false, 30.0, 1, false, 0, 0, 0, 0)
4642                .expect("x264 init");
4643        let mut out = Vec::new();
4644        assert!(enc.encode_with_headers(
4645            &y,
4646            &u,
4647            &v,
4648            w as i32,
4649            (w / 2) as i32,
4650            (w / 2) as i32,
4651            0,
4652            true,
4653            true,
4654            &mut out
4655        ));
4656        let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
4657        assert!(dec.decode(&out).expect("decode"));
4658        let worst = chart_error(&dec.frame().expect("frame"), BT709);
4659        println!("[chart] x264: worst |dRGB| {worst:.1}");
4660        assert!(
4661            worst <= 12.0,
4662            "the software H.264 path paints {worst:.1} off the chart"
4663        );
4664    }
4665
4666    /// Every x264 session bounds reordering at zero, 4:2:0 and 4:4:4, at a constant quantizer
4667    /// and a constant rate.
4668    #[cfg(feature = "gpl")]
4669    #[test]
4670    fn x264_bounds_reordering_at_zero() {
4671        use crate::encoders::sps::fixtures::assert_no_reorder;
4672        for (fullcolor, cbr) in [(false, false), (false, true), (true, false), (true, true)] {
4673            let mut enc = H264EncoderWrapper::new(
4674                W as i32, H as i32, 20, fullcolor, 60.0, 4, cbr, 8000, 0, 0, 0,
4675            )
4676            .expect("x264 init");
4677            let chroma = if fullcolor { W * H } else { W * H / 4 };
4678            let (u, v) = (vec![128u8; chroma], vec![128u8; chroma]);
4679            let stride = if fullcolor { W } else { W / 2 } as i32;
4680            let mut out = Vec::new();
4681            assert!(enc.encode_with_headers(
4682                &text_luma(0),
4683                &u,
4684                &v,
4685                W as i32,
4686                stride,
4687                stride,
4688                0,
4689                true,
4690                true,
4691                &mut out
4692            ));
4693            assert_no_reorder(&out, &format!("x264 fullcolor {fullcolor} cbr {cbr}"));
4694        }
4695    }
4696
4697    /// A capped CRF's key frame costs at most two encodes, the first of a session measuring
4698    /// itself and coding once more aimed into the bucket, and one after a key frame before it,
4699    /// aimed from the start: each within the buffer.
4700    #[cfg(feature = "gpl")]
4701    #[test]
4702    fn x264_capped_crf_codes_a_key_frame_in_at_most_two_encodes() {
4703        const VBV_KBIT: i32 = 1500;
4704        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
4705        let mut enc = H264EncoderWrapper::new(
4706            W as i32, H as i32, 12, false, 60.0, 4, false, 8000, VBV_KBIT, 0, 0,
4707        )
4708        .expect("x264 init");
4709        let key = |enc: &mut H264EncoderWrapper, i: usize| {
4710            let before = enc.encodes;
4711            let mut out = Vec::new();
4712            assert!(enc.encode_with_headers(
4713                &text_luma(i),
4714                &u,
4715                &v,
4716                W as i32,
4717                (W / 2) as i32,
4718                (W / 2) as i32,
4719                i as u16,
4720                true,
4721                true,
4722                &mut out
4723            ));
4724            (enc.encodes - before, out.len() * 8)
4725        };
4726        let (first, first_bits) = key(&mut enc, 0);
4727        for _ in 0..120 {
4728            enc.idle(&CaptureSettings {
4729                video_bitrate_kbps: 8000,
4730                video_vbv_multiplier: f64::from(VBV_KBIT) * 1000.0 / (8_000_000.0 / 60.0),
4731                target_fps: 60.0,
4732                ..Default::default()
4733            });
4734        }
4735        let (later, later_bits) = key(&mut enc, 1);
4736        println!("first key: {first} encodes, {first_bits} bits; later: {later}, {later_bits}");
4737        assert!(first <= 2, "the first key frame took {first} encodes");
4738        assert_eq!(later, 1, "a later key frame took {later} encodes");
4739        assert!(first_bits <= VBV_KBIT as usize * 1000 && later_bits <= VBV_KBIT as usize * 1000);
4740    }
4741
4742    /// A CRF session given a VBV codes its key frame within the buffer, where the same session
4743    /// without one codes it at whatever size the rate factor asks.
4744    #[cfg(feature = "gpl")]
4745    #[test]
4746    fn x264_crf_bounds_a_key_frame_by_its_vbv() {
4747        const VBV_KBIT: i32 = 1500;
4748        let key = |vbv_kbit: i32| {
4749            let mut enc = H264EncoderWrapper::new(
4750                W as i32, H as i32, 12, false, 60.0, 4, false, 8000, vbv_kbit, 0, 0,
4751            )
4752            .expect("x264 init");
4753            let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
4754            let mut out = Vec::new();
4755            assert!(enc.encode_with_headers(
4756                &text_luma(0),
4757                &u,
4758                &v,
4759                W as i32,
4760                (W / 2) as i32,
4761                (W / 2) as i32,
4762                0,
4763                true,
4764                true,
4765                &mut out
4766            ));
4767            out.len() * 8
4768        };
4769        let (uncapped, capped) = (key(0), key(VBV_KBIT));
4770        assert!(
4771            capped <= VBV_KBIT as usize * 1000,
4772            "a {capped}-bit key frame overflows the {VBV_KBIT} kbit buffer"
4773        );
4774        assert!(
4775            uncapped > 2 * capped,
4776            "the uncapped key frame ({uncapped} bits) is not the rate factor's own"
4777        );
4778    }
4779
4780    /// A capped CRF session its content starves overshoots the buffer rather than code past
4781    /// quantizer 51, where x264 forces skips that leave rows frozen on old content.
4782    #[cfg(feature = "gpl")]
4783    #[test]
4784    fn x264_capped_crf_codes_no_quantizer_past_51() {
4785        let mut enc =
4786            H264EncoderWrapper::new(W as i32, H as i32, 23, false, 60.0, 4, false, 300, 10, 0, 0)
4787                .expect("x264 init");
4788        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
4789        let mut worst = 0;
4790        for i in 0..FRAMES {
4791            let mut out = Vec::new();
4792            enc.encode_with_headers(
4793                &text_luma(i),
4794                &u,
4795                &v,
4796                W as i32,
4797                (W / 2) as i32,
4798                (W / 2) as i32,
4799                i as u16,
4800                i == 0,
4801                true,
4802                &mut out,
4803            );
4804            worst = worst.max(enc.last_qp().unwrap_or(0));
4805        }
4806        assert!(worst <= 51, "a starved capped CRF coded quantizer {worst}");
4807    }
4808
4809    /// A constant-rate session on a still screen of text after a scroll says the quantizer its
4810    /// rows were coded at: at 1 Mbit/s at 720p x264 plans its frames at 17 while its row-level
4811    /// control holds every row at 51, coding nothing, so a frame said to be that fine holds a
4812    /// picture that fine.
4813    #[cfg(feature = "gpl")]
4814    #[test]
4815    fn x264_says_the_quantizer_a_still_screens_rows_were_coded_at() {
4816        const KBPS: i32 = 1000;
4817        const SCROLL: usize = 30;
4818        const STILL: usize = 300;
4819        const PAINT_QP: u32 = 18;
4820        let mut enc = H264EncoderWrapper::new(
4821            W as i32,
4822            H as i32,
4823            25,
4824            false,
4825            60.0,
4826            4,
4827            true,
4828            KBPS,
4829            KBPS * 4 / 60,
4830            0,
4831            0,
4832        )
4833        .expect("x264 init");
4834        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
4835        let still = text_luma(SCROLL);
4836        let mut dec = Decoder::new().expect("decoder");
4837        let mut picture = vec![0u8; W * H];
4838        let mut misread = Vec::new();
4839        for i in 0..SCROLL + STILL {
4840            let y = if i < SCROLL {
4841                text_luma(i)
4842            } else {
4843                still.clone()
4844            };
4845            let mut out = Vec::new();
4846            enc.encode_with_headers(
4847                &y,
4848                &u,
4849                &v,
4850                W as i32,
4851                (W / 2) as i32,
4852                (W / 2) as i32,
4853                i as u16,
4854                i == 0,
4855                true,
4856                &mut out,
4857            );
4858            if let Some(img) = dec.decode(&out).expect("decode") {
4859                let stride = img.strides().0;
4860                for r in 0..H {
4861                    picture[r * W..r * W + W].copy_from_slice(&img.y()[r * stride..r * stride + W]);
4862                }
4863            }
4864            let psnr = mean_psnr(std::slice::from_ref(&picture), std::slice::from_ref(&still));
4865            if i >= SCROLL && enc.last_qp().is_some_and(|q| q <= PAINT_QP) && psnr < 40.0 {
4866                misread.push((i, enc.last_qp(), out.len(), psnr as i32));
4867            }
4868        }
4869        assert!(
4870            misread.is_empty(),
4871            "frames said to be at the paint-over quantizer: {:?}",
4872            &misread[..misread.len().min(8)]
4873        );
4874    }
4875
4876    /// A constant-rate session refines a still screen of text after a scroll in a cleanup's
4877    /// buffer at 60 fps: in four frames x264's row-level control prices every row at its whole
4878    /// intra cost against a slice's share and codes none, leaving 720p at 1 Mbit/s at 12 dB,
4879    /// where in 0.2 s it reaches 39 dB in ten seconds.
4880    #[cfg(feature = "gpl")]
4881    #[test]
4882    fn x264_refines_a_still_screen_after_a_scroll_in_a_cleanups_buffer() {
4883        const KBPS: i32 = 1000;
4884        const SCROLL: usize = 30;
4885        const STILL: usize = 600;
4886        let settings = CaptureSettings {
4887            target_fps: 60.0,
4888            ..Default::default()
4889        };
4890        let session_vbv =
4891            (crate::encoders::vbv_bits(KBPS as u32 * 1000, 60.0, 0.0, 0.0) / 1000) as i32;
4892        let mut enc = H264EncoderWrapper::new(
4893            W as i32,
4894            H as i32,
4895            25,
4896            false,
4897            60.0,
4898            4,
4899            true,
4900            KBPS,
4901            session_vbv,
4902            0,
4903            0,
4904        )
4905        .expect("x264 init");
4906        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
4907        let still = text_luma(SCROLL);
4908        let mut dec = Decoder::new().expect("decoder");
4909        let mut picture = vec![0u8; W * H];
4910        for i in 0..SCROLL + STILL {
4911            if i == SCROLL {
4912                enc.reconfigure_rate(KBPS, super::cleanup_vbv_kbit(&settings, KBPS), 60.0);
4913            }
4914            let y = if i < SCROLL {
4915                text_luma(i)
4916            } else {
4917                still.clone()
4918            };
4919            let mut out = Vec::new();
4920            enc.encode_with_headers(
4921                &y,
4922                &u,
4923                &v,
4924                W as i32,
4925                (W / 2) as i32,
4926                (W / 2) as i32,
4927                i as u16,
4928                i == 0,
4929                true,
4930                &mut out,
4931            );
4932            if let Some(img) = dec.decode(&out).expect("decode") {
4933                let stride = img.strides().0;
4934                for r in 0..H {
4935                    picture[r * W..r * W + W].copy_from_slice(&img.y()[r * stride..r * stride + W]);
4936                }
4937            }
4938        }
4939        let psnr = mean_psnr(std::slice::from_ref(&picture), std::slice::from_ref(&still));
4940        assert!(psnr > 35.0, "a still screen refined to {psnr:.1} dB");
4941    }
4942
4943    /// A capped CRF session under a peak its picture never reaches codes every frame as the
4944    /// uncapped one does, byte for byte: the key frame that opens it and the frames after it, a
4945    /// scroll, the cleanup of the still screen after it at the paint-over quality, a key frame a
4946    /// client asks for in that cleanup, and the scroll after.
4947    #[cfg(feature = "gpl")]
4948    #[test]
4949    fn x264_capped_crf_codes_as_uncapped_under_a_peak_it_never_reaches() {
4950        use crate::encoders::codec::{FRAME_KEY, parse_video_type};
4951        let (w, h) = (W as i32, H as i32);
4952        let uncapped = CaptureSettings {
4953            width: w,
4954            height: h,
4955            codec: Codec::H264,
4956            target_fps: 60.0,
4957            video_crf: 25,
4958            video_paintover_crf: 18,
4959            use_paint_over_quality: true,
4960            video_bitrate_kbps: 8000,
4961            ..Default::default()
4962        };
4963        let capped = CaptureSettings {
4964            video_bitrate_kbps: 100_000,
4965            video_vbv_multiplier: crate::encoders::CAPPED_VBV_FRAMES,
4966            ..uncapped.clone()
4967        };
4968        let whole = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
4969        let (mut a, mut b) = (Vec::new(), Vec::new());
4970        let (mut keys, mut cleanup) = (0, 0);
4971        for t in 0..150u16 {
4972            let still = (30..90).contains(&t);
4973            let at = if still {
4974                30
4975            } else if t < 30 {
4976                t
4977            } else {
4978                t - 60
4979            };
4980            let frame = crate::encoders::scrolled_text(W, H, at as usize);
4981            let changed: &[_] = if still { &[] } else { &whole };
4982            let key = t == 0 || t == 60;
4983            let one = super::encode_cpu(
4984                &mut a, &frame, w, h, changed, &uncapped, t, false, false, key,
4985            );
4986            let two =
4987                super::encode_cpu(&mut b, &frame, w, h, changed, &capped, t, false, false, key);
4988            assert_eq!(one.len(), two.len(), "frame {t} coded on one side only");
4989            for (x, y) in one.iter().zip(&two) {
4990                assert!(
4991                    x.data == y.data,
4992                    "frame {t}: {} bytes uncapped, {} capped",
4993                    x.data.len(),
4994                    y.data.len()
4995                );
4996                keys += usize::from(parse_video_type(x.data[1]).map(|(_, k)| k) == Some(FRAME_KEY));
4997                cleanup += usize::from(still);
4998            }
4999        }
5000        assert!(keys >= 2, "{keys} key frames");
5001        assert!(cleanup > 0, "the still screen was never cleaned up");
5002    }
5003
5004    /// Code a 720p page through the stripe path: scrolled for `SCROLL` frames under a peak of
5005    /// `peak` kbps (0 uncapped), then held still under `then` kbps, the paint-over armed or not
5006    /// (`paint`), returning the decoded pictures, each frame's bytes, the key frames after the
5007    /// first, and whether coarse blocks are left (`BlockLedger::coarse`).
5008    #[cfg(feature = "gpl")]
5009    fn scroll_then_still(
5010        peak: i32,
5011        then: i32,
5012        frames: usize,
5013        paint: bool,
5014    ) -> (Vec<Vec<u8>>, Vec<usize>, usize, bool) {
5015        let peaks = |t: usize| if t <= 60 || peak == 0 { peak } else { then };
5016        scroll_then_still_under(&peaks, frames, paint)
5017    }
5018
5019    /// `scroll_then_still` under the peak `peaks` names for each frame (0 uncapped throughout).
5020    #[cfg(feature = "gpl")]
5021    fn scroll_then_still_under(
5022        peaks: &dyn Fn(usize) -> i32,
5023        frames: usize,
5024        paint: bool,
5025    ) -> (Vec<Vec<u8>>, Vec<usize>, usize, bool) {
5026        use crate::encoders::codec::{FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
5027        const SCROLL: usize = 60;
5028        let (w, h) = (W as i32, H as i32);
5029        let peak = peaks(0);
5030        let mut s = CaptureSettings {
5031            width: w,
5032            height: h,
5033            codec: Codec::H264,
5034            target_fps: 60.0,
5035            video_crf: 25,
5036            video_paintover_crf: 18,
5037            use_paint_over_quality: paint,
5038            video_bitrate_kbps: peak.max(8000),
5039            video_vbv_multiplier: if peak > 0 {
5040                crate::encoders::CAPPED_VBV_FRAMES
5041            } else {
5042                0.0
5043            },
5044            ..Default::default()
5045        };
5046        if peak > 0 {
5047            s.video_bitrate_kbps = peak;
5048        }
5049        let whole = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
5050        let (mut stripes, mut stream, mut sizes, mut keys) =
5051            (Vec::new(), Vec::new(), Vec::new(), 0);
5052        for t in 0..frames {
5053            if peak > 0 {
5054                s.video_bitrate_kbps = peaks(t);
5055            }
5056            let frame = crate::encoders::scrolled_text(W, H, t.min(SCROLL));
5057            let changed: &[_] = if t <= SCROLL { &whole } else { &[] };
5058            let out = super::encode_cpu(
5059                &mut stripes,
5060                &frame,
5061                w,
5062                h,
5063                changed,
5064                &s,
5065                t as u16,
5066                false,
5067                false,
5068                t == 0,
5069            );
5070            let mut bytes = 0;
5071            for st in &out {
5072                keys += usize::from(
5073                    t > 0 && parse_video_type(st.data[1]).map(|(_, k)| k) == Some(FRAME_KEY),
5074                );
5075                bytes += st.data.len() - VIDEO_HEADER_LEN;
5076                stream.push(st.data[VIDEO_HEADER_LEN..].to_vec());
5077            }
5078            sizes.push(bytes);
5079        }
5080        let coarse = stripes
5081            .first()
5082            .is_some_and(|st: &super::StripeState| st.ledger.coarse());
5083        (decode_luma(&stream), sizes, keys, coarse)
5084    }
5085
5086    /// The luma of the still page `scroll_then_still` ends on, as the stripe path converts it.
5087    #[cfg(feature = "gpl")]
5088    fn still_page_luma() -> Vec<u8> {
5089        crate::encoders::scrolled_text(W, H, 60)
5090            .as_chunks::<4>()
5091            .0
5092            .iter()
5093            .map(|px| (16.0 + f64::from(px[0]) * 219.0 / 255.0).round() as u8)
5094            .collect()
5095    }
5096
5097    /// A capped CRF stripe its peak bound while the page scrolled codes the still page it
5098    /// leaves again once the peak lets go, without a key frame: every coarse block is refreshed
5099    /// once, and the page comes to within `NEAR_DB` of the uncapped session's quality. The
5100    /// refresh lands at 38.5-38.6 dB on any host; the uncapped rate factor's own result moves
5101    /// with the host's cores, which set x264's slice threads (37.9 dB on four, 38.6-38.8 on one
5102    /// to three).
5103    #[cfg(feature = "gpl")]
5104    #[test]
5105    fn x264_capped_crf_refreshes_what_it_coded_bound_once_its_peak_lets_go() {
5106        const NEAR_DB: f64 = 0.5;
5107        const FRAMES: usize = 200;
5108        let page = still_page_luma();
5109        let (plain, _, _, _) = scroll_then_still(0, 0, FRAMES, false);
5110        let (capped, sizes, keys, coarse) = scroll_then_still(300, 100_000, FRAMES, false);
5111        let psnr = |p: &[Vec<u8>]| mean_psnr(&p[p.len() - 1..], std::slice::from_ref(&page));
5112        let (a, b) = (psnr(&plain), psnr(&capped));
5113        let refreshing = sizes[61..].iter().filter(|&&n| n > 0).count();
5114        println!(
5115            "uncapped {a:.2} dB, capped and refreshed {b:.2} dB, {refreshing} frames sent while still"
5116        );
5117        assert_eq!(keys, 0, "a key frame");
5118        assert!(!coarse, "coarse blocks left");
5119        assert!(refreshing > 0, "nothing refreshed the still page");
5120        assert!(
5121            b >= a - NEAR_DB,
5122            "refreshed to {b:.2} dB against {a:.2} uncapped"
5123        );
5124    }
5125
5126    /// A still page's paint-over cleanup runs on a capped CRF stripe whose peak let go as on an
5127    /// uncapped one, the refresh leaving it alone: the page comes to the paint-over quality.
5128    #[cfg(feature = "gpl")]
5129    #[test]
5130    fn x264_capped_crf_refresh_leaves_the_paint_over_to_run() {
5131        const FRAMES: usize = 300;
5132        let page = still_page_luma();
5133        let psnr = |p: &[Vec<u8>]| mean_psnr(&p[p.len() - 1..], std::slice::from_ref(&page));
5134        let (plain, _, _, _) = scroll_then_still(0, 0, FRAMES, true);
5135        let (capped, _, keys, coarse) = scroll_then_still(300, 100_000, FRAMES, true);
5136        let (a, b) = (psnr(&plain), psnr(&capped));
5137        println!("paint-over: uncapped {a:.2} dB, capped and refreshed {b:.2} dB");
5138        assert_eq!(keys, 0, "a key frame");
5139        assert!(!coarse, "coarse blocks left");
5140        assert!(
5141            b >= a - 0.5,
5142            "{b:.2} dB against the uncapped paint-over's {a:.2}"
5143        );
5144    }
5145
5146    /// On a link that holds the peak low, the paint-over that falls due on the page the peak
5147    /// left coarse is owed to the still blocks and refreshed at its quality within the peak: no
5148    /// still frame past the bucket's room, and the page comes to the uncapped paint-over's
5149    /// quality.
5150    #[cfg(feature = "gpl")]
5151    #[test]
5152    fn x264_capped_crf_paints_over_within_the_peak() {
5153        const FRAMES: usize = 600;
5154        const THEN: i32 = 4000;
5155        let page = still_page_luma();
5156        let psnr = |p: &[Vec<u8>]| mean_psnr(&p[p.len() - 1..], std::slice::from_ref(&page));
5157        let (plain, plain_sizes, _, _) = scroll_then_still(0, 0, FRAMES, true);
5158        let (capped, sizes, keys, coarse) = scroll_then_still(300, THEN, FRAMES, true);
5159        let room =
5160            (crate::encoders::CAPPED_VBV_FRAMES * f64::from(THEN) * 1000.0 / 8.0 / 60.0) as usize;
5161        let still = &sizes[61..];
5162        let largest = still.iter().max().copied().unwrap_or(0);
5163        let sent = still.iter().filter(|&&n| n > 0).count();
5164        let bytes = |s: &[usize]| s[61..].iter().sum::<usize>();
5165        let (a, b) = (psnr(&plain), psnr(&capped));
5166        println!(
5167            "paint-over under {THEN} kbps: uncapped {a:.2} dB in {} bytes still, capped {b:.2} dB \
5168             in {sent} frames of {} bytes, the largest {largest} (room {room})",
5169            bytes(&plain_sizes),
5170            bytes(&sizes)
5171        );
5172        assert_eq!(keys, 0, "a key frame");
5173        assert!(!coarse, "coarse blocks left");
5174        assert!(
5175            largest <= room,
5176            "a still frame of {largest} bytes past the room"
5177        );
5178        assert!(
5179            b >= a - 0.5,
5180            "{b:.2} dB against the uncapped paint-over's {a:.2}"
5181        );
5182    }
5183
5184    /// A capped CRF whose peak bound once, its page coded again at its own quality under a peak
5185    /// that took it, and then still under a peak that holds low: the paint-over the bucket's
5186    /// room cannot take in one frame is owed to the still blocks and refreshed at its quality
5187    /// within the peak, no still frame past the room, and the page comes to the uncapped
5188    /// paint-over's quality.
5189    #[cfg(feature = "gpl")]
5190    #[test]
5191    fn x264_capped_crf_holds_its_paint_over_to_the_room() {
5192        const FRAMES: usize = 600;
5193        const THEN: i32 = 4000;
5194        let page = still_page_luma();
5195        let psnr = |p: &[Vec<u8>]| mean_psnr(&p[p.len() - 1..], std::slice::from_ref(&page));
5196        let (plain, plain_sizes, _, _) = scroll_then_still(0, 0, FRAMES, true);
5197        let peaks = |t: usize| match t {
5198            0..5 => 300,
5199            5..=60 => 50_000,
5200            _ => THEN,
5201        };
5202        let (capped, sizes, keys, coarse) = scroll_then_still_under(&peaks, FRAMES, true);
5203        let room =
5204            (crate::encoders::CAPPED_VBV_FRAMES * f64::from(THEN) * 1000.0 / 8.0 / 60.0) as usize;
5205        let still = &sizes[61..];
5206        let largest = still.iter().max().copied().unwrap_or(0);
5207        let sent = still.iter().filter(|&&n| n > 0).count();
5208        let bytes = |s: &[usize]| s[61..].iter().sum::<usize>();
5209        let (a, b) = (psnr(&plain), psnr(&capped));
5210        println!(
5211            "paint-over under {THEN} kbps after a page coded at its own quality: uncapped {a:.2} \
5212             dB in {} bytes still, capped {b:.2} dB in {sent} frames of {} bytes, the largest \
5213             {largest} (room {room})",
5214            bytes(&plain_sizes),
5215            bytes(&sizes)
5216        );
5217        assert_eq!(keys, 0, "a key frame");
5218        assert!(!coarse, "coarse blocks left");
5219        assert!(
5220            largest <= room,
5221            "a still frame of {largest} bytes past the room"
5222        );
5223        assert!(
5224            b >= a - 0.5,
5225            "{b:.2} dB against the uncapped paint-over's {a:.2}"
5226        );
5227    }
5228
5229    /// The refresh of a peak that let go on a slow link spreads over frames within the peak:
5230    /// the page's still frames average no more than it, and the refresh still ends.
5231    #[cfg(feature = "gpl")]
5232    #[test]
5233    fn x264_capped_crf_paces_its_refresh_within_the_peak() {
5234        const FRAMES: usize = 600;
5235        const THEN: i32 = 2000;
5236        let (_, sizes, keys, coarse) = scroll_then_still(300, THEN, FRAMES, false);
5237        let sent: Vec<usize> = sizes[61..].iter().copied().filter(|&n| n > 0).collect();
5238        let kbps =
5239            sizes[61..].iter().sum::<usize>() as f64 * 8.0 * 60.0 / (FRAMES - 61) as f64 / 1000.0;
5240        let largest = sent.iter().copied().max().unwrap_or(0);
5241        println!(
5242            "{} still frames sent, largest {largest} bytes, {kbps:.0} kbps under a {THEN} kbps peak",
5243            sent.len()
5244        );
5245        assert_eq!(keys, 0, "a key frame");
5246        assert!(!coarse, "the refresh did not end");
5247        assert!(
5248            kbps <= 1.1 * f64::from(THEN),
5249            "{kbps:.0} kbps under a {THEN} kbps peak"
5250        );
5251    }
5252
5253    /// A refresh under way when the page moves again on a slow link lets the peak bind the
5254    /// moving frames as before: its frames on the still page each fit the bucket, and the scroll
5255    /// after runs within the peak. The scroll's first frame codes the page the bind left coarse
5256    /// again, past the bucket, as it does without a refresh (556 kB at quantizer 27; 702 kB at
5257    /// 20 after the refresh's frames, held at the session's quantizer, left x264's rate factor
5258    /// reading less complexity), and the peak binds from the next.
5259    #[cfg(feature = "gpl")]
5260    #[test]
5261    fn x264_capped_crf_refresh_yields_to_motion_within_the_peak() {
5262        use crate::encoders::codec::{FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
5263        const THEN: i32 = 2000;
5264        let (w, h) = (W as i32, H as i32);
5265        let mut s = CaptureSettings {
5266            width: w,
5267            height: h,
5268            codec: Codec::H264,
5269            target_fps: 60.0,
5270            video_crf: 25,
5271            use_paint_over_quality: false,
5272            video_bitrate_kbps: 300,
5273            video_vbv_multiplier: crate::encoders::CAPPED_VBV_FRAMES,
5274            ..Default::default()
5275        };
5276        let whole = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
5277        let (mut stripes, mut sizes, mut keys) = (Vec::new(), Vec::new(), 0);
5278        let mut refreshing = false;
5279        for t in 0..300usize {
5280            if t == 61 {
5281                s.video_bitrate_kbps = THEN;
5282            }
5283            // Scrolled, held still while the refresh starts, then scrolled again.
5284            let (at, moving) = match t {
5285                0..=60 => (t, true),
5286                61..=90 => (60, false),
5287                _ => (t - 30, true),
5288            };
5289            let frame = crate::encoders::scrolled_text(W, H, at);
5290            let changed: &[_] = if moving { &whole } else { &[] };
5291            let out = super::encode_cpu(
5292                &mut stripes,
5293                &frame,
5294                w,
5295                h,
5296                changed,
5297                &s,
5298                t as u16,
5299                false,
5300                false,
5301                t == 0,
5302            );
5303            let mut bytes = 0;
5304            for st in &out {
5305                keys += usize::from(
5306                    t > 0 && parse_video_type(st.data[1]).map(|(_, k)| k) == Some(FRAME_KEY),
5307                );
5308                bytes += st.data.len() - VIDEO_HEADER_LEN;
5309            }
5310            refreshing |= t == 90 && stripes[0].ledger.coarse();
5311            sizes.push(bytes);
5312        }
5313        let kbps = sizes[120..].iter().sum::<usize>() as f64 * 8.0 * 60.0 / 180.0 / 1000.0;
5314        let largest = sizes[61..=90].iter().copied().max().unwrap_or(0);
5315        let bucket = crate::encoders::vbv_bits(
5316            THEN as u32 * 1000,
5317            60.0,
5318            0.0,
5319            crate::encoders::CAPPED_VBV_FRAMES,
5320        ) as usize
5321            / 8;
5322        println!(
5323            "{kbps:.0} kbps under a {THEN} kbps peak, largest refreshing frame {largest} bytes \
5324             (bucket {bucket}), the scroll's first {} bytes",
5325            sizes[91]
5326        );
5327        assert_eq!(keys, 0, "a key frame");
5328        assert!(
5329            refreshing,
5330            "the refresh had ended before the page moved again"
5331        );
5332        assert!(
5333            kbps <= 1.1 * f64::from(THEN),
5334            "{kbps:.0} kbps under a {THEN} kbps peak"
5335        );
5336        assert!(
5337            largest <= bucket,
5338            "a {largest}-byte refreshing frame past the {bucket}-byte bucket"
5339        );
5340    }
5341
5342    /// A capped CRF stripe whose peak binds refines nothing of its still half, so the moving half
5343    /// stays within the peak, paint-over armed or not.
5344    #[cfg(feature = "gpl")]
5345    #[test]
5346    fn x264_capped_crf_refines_nothing_while_its_peak_binds() {
5347        use crate::CaptureSettings;
5348        const KBPS: i32 = 1500;
5349        let (w, h) = (W as i32, H as i32);
5350        let settings = CaptureSettings {
5351            width: w,
5352            height: h,
5353            codec: Codec::H264,
5354            target_fps: 60.0,
5355            video_crf: 25,
5356            video_paintover_crf: 18,
5357            use_paint_over_quality: true,
5358            video_bitrate_kbps: KBPS,
5359            video_vbv_multiplier: crate::encoders::CAPPED_VBV_FRAMES,
5360            ..Default::default()
5361        };
5362        let still = crate::encoders::scrolled_text(W, H, 0);
5363        let moving = [smithay::utils::Rectangle::new(
5364            (0, 0).into(),
5365            (w, h / 2).into(),
5366        )];
5367        let mut stripes = Vec::new();
5368        let mut bits = 0usize;
5369        for t in 0..300u16 {
5370            let mut frame = still.clone();
5371            let scrolled = crate::encoders::scrolled_text(W, H, t as usize + 1);
5372            frame[..W * H * 2].copy_from_slice(&scrolled[..W * H * 2]);
5373            let out = super::encode_cpu(
5374                &mut stripes,
5375                &frame,
5376                w,
5377                h,
5378                if t == 0 { &[] } else { &moving },
5379                &settings,
5380                t,
5381                false,
5382                false,
5383                t == 0,
5384            );
5385            if t >= 120 {
5386                bits += out.iter().map(|s| s.data.len() * 8).sum::<usize>();
5387            }
5388        }
5389        let kbps = bits as f64 * 60.0 / 180.0 / 1000.0;
5390        assert!(
5391            kbps <= 1.05 * f64::from(KBPS),
5392            "{kbps:.0} kbps under a {KBPS} kbps peak"
5393        );
5394    }
5395
5396    /// A capped CRF session takes each new peak congestion control steers it to live: no key
5397    /// frame, and a scroll after a cut codes within the lower peak, where before it the rate
5398    /// factor's picture fit under the higher one.
5399    #[cfg(feature = "gpl")]
5400    #[test]
5401    fn x264_capped_crf_moves_its_peak_without_a_key_frame() {
5402        use crate::encoders::codec::{FRAME_KEY, h264_frame_type};
5403        const CUT: usize = 30;
5404        const LOW_KBPS: i32 = 1000;
5405        let vbv = |kbps: i32| {
5406            (crate::encoders::vbv_bits(
5407                kbps as u32 * 1000,
5408                60.0,
5409                0.0,
5410                crate::encoders::CAPPED_VBV_FRAMES,
5411            ) / 1000) as i32
5412        };
5413        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
5414        let mut enc = H264EncoderWrapper::new(
5415            W as i32,
5416            H as i32,
5417            25,
5418            false,
5419            60.0,
5420            4,
5421            false,
5422            100_000,
5423            vbv(100_000),
5424            0,
5425            0,
5426        )
5427        .expect("x264 init");
5428        let mut sizes = Vec::new();
5429        for i in 0..CUT + FRAMES {
5430            if i == CUT {
5431                enc.reconfigure_rate(LOW_KBPS, vbv(LOW_KBPS), 60.0);
5432            }
5433            let mut out = Vec::new();
5434            assert!(enc.encode_with_headers(
5435                &text_luma(i),
5436                &u,
5437                &v,
5438                W as i32,
5439                (W / 2) as i32,
5440                (W / 2) as i32,
5441                i as u16,
5442                i == 0,
5443                true,
5444                &mut out
5445            ));
5446            assert!(
5447                i == 0 || h264_frame_type(&out) != FRAME_KEY,
5448                "frame {i} is a key frame"
5449            );
5450            sizes.push(out.len() * 8);
5451        }
5452        let kbps = |frames: &[usize]| {
5453            frames.iter().sum::<usize>() as f64 * 60.0 / frames.len() as f64 / 1000.0
5454        };
5455        let (before, after) = (kbps(&sizes[1..CUT]), kbps(&sizes[CUT + 10..]));
5456        assert!(
5457            before > 2.0 * f64::from(LOW_KBPS),
5458            "the scroll ran at {before:.0} kbps under the high peak"
5459        );
5460        assert!(
5461            after <= 1.1 * f64::from(LOW_KBPS),
5462            "the scroll ran at {after:.0} kbps under a {LOW_KBPS} kbps peak"
5463        );
5464    }
5465
5466    /// A CRF session capped or uncapped live codes its next key frame by the new setting.
5467    #[cfg(feature = "gpl")]
5468    #[test]
5469    fn x264_crf_takes_a_cap_set_or_lifted_live() {
5470        const VBV_KBIT: i32 = 1500;
5471        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
5472        let mut enc =
5473            H264EncoderWrapper::new(W as i32, H as i32, 12, false, 60.0, 4, false, 8000, 0, 0, 0)
5474                .expect("x264 init");
5475        let key = |enc: &mut H264EncoderWrapper| {
5476            let mut out = Vec::new();
5477            assert!(enc.encode_with_headers(
5478                &text_luma(0),
5479                &u,
5480                &v,
5481                W as i32,
5482                (W / 2) as i32,
5483                (W / 2) as i32,
5484                0,
5485                true,
5486                true,
5487                &mut out
5488            ));
5489            out.len() * 8
5490        };
5491        let uncapped = key(&mut enc);
5492        enc.reconfigure_rate(8000, VBV_KBIT, 60.0);
5493        let capped = key(&mut enc);
5494        enc.reconfigure_rate(8000, 0, 60.0);
5495        let lifted = key(&mut enc);
5496        assert!(
5497            capped <= VBV_KBIT as usize * 1000,
5498            "a {capped}-bit key frame after capping overflows the {VBV_KBIT} kbit buffer ({uncapped} bits before)"
5499        );
5500        assert!(
5501            lifted > 2 * capped,
5502            "the key frame after lifting the cap ({lifted} bits) is still capped"
5503        );
5504    }
5505
5506    /// A frame a client lost is left out of the predictions: the next frame predicts from the
5507    /// newest frame before it and names it, a decoder that never saw the lost frames decodes it
5508    /// as one that saw everything does, and a loss the window no longer covers becomes a key
5509    /// frame. The stream declares the decoded picture buffer this needs.
5510    #[cfg(feature = "gpl")]
5511    #[test]
5512    fn x264_predicts_past_a_lost_frame() {
5513        use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
5514        use crate::encoders::reference::{REFERENCE_FRAMES, Reference};
5515        use crate::encoders::sps::h264_max_num_ref_frames;
5516        use crate::webcam::decode::{Decoder as _, VideoDecoder};
5517        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
5518        let mut enc =
5519            H264EncoderWrapper::new(W as i32, H as i32, 20, false, 60.0, 4, false, 0, 0, 0, 0)
5520                .expect("x264 init");
5521        let encode = |enc: &mut H264EncoderWrapper, i: usize| {
5522            let y = text_luma(i);
5523            let mut out = Vec::new();
5524            assert!(enc.encode_with_headers(
5525                &y,
5526                &u,
5527                &v,
5528                W as i32,
5529                (W / 2) as i32,
5530                (W / 2) as i32,
5531                i as u16,
5532                i == 0,
5533                true,
5534                &mut out
5535            ));
5536            (out, enc.last_reference())
5537        };
5538        let mut frames: Vec<Vec<u8>> = Vec::new();
5539        for i in 0..8 {
5540            let (out, reference) = encode(&mut enc, i);
5541            assert_eq!(
5542                reference,
5543                if i == 0 {
5544                    Reference::None
5545                } else {
5546                    Reference::Frame(i as u16 - 1)
5547                }
5548            );
5549            frames.push(out);
5550        }
5551        assert_eq!(
5552            h264_max_num_ref_frames(&frames[0]),
5553            Some(REFERENCE_FRAMES),
5554            "the SPS declares the DPB"
5555        );
5556        // Frame 5 is reported lost once 6 and 7 have gone out.
5557        assert!(enc.invalidate_reference(5));
5558        let (out, reference) = encode(&mut enc, 8);
5559        assert_eq!(reference, Reference::Frame(4));
5560        assert_eq!(h264_frame_type(&out), FRAME_DELTA);
5561        frames.push(out);
5562        let (out, reference) = encode(&mut enc, 9);
5563        assert_eq!(reference, Reference::Frame(8));
5564        frames.push(out);
5565        let (mut whole, mut lossy) = (
5566            VideoDecoder::new(Codec::H264).unwrap(),
5567            VideoDecoder::new(Codec::H264).unwrap(),
5568        );
5569        for (i, f) in frames.iter().enumerate() {
5570            assert!(whole.decode(f).expect("decode"), "frame {i}");
5571            if !(5..8).contains(&i) {
5572                assert!(lossy.decode(f).expect("decode without 5-7"), "frame {i}");
5573            }
5574        }
5575        let apart = luma_distance(&whole.frame().unwrap(), &lossy.frame().unwrap());
5576        assert!(
5577            apart < 0.5,
5578            "the decoder that lost frames 5-7 shows frame 9 {apart:.2} off the one that saw them"
5579        );
5580        let source = text_luma(9);
5581        let off = lossy
5582            .frame()
5583            .unwrap()
5584            .y
5585            .chunks(lossy.frame().unwrap().y_stride)
5586            .take(H)
5587            .zip(source.chunks(W))
5588            .flat_map(|(row, src)| {
5589                row[..W]
5590                    .iter()
5591                    .zip(src)
5592                    .map(|(&a, &b)| (f64::from(a) - f64::from(b)).abs())
5593            })
5594            .sum::<f64>()
5595            / (W * H) as f64;
5596        assert!(
5597            off < 6.0,
5598            "frame 9 decoded without frames 5-7 is {off:.2} off the picture painted"
5599        );
5600        for i in 10..20 {
5601            frames.push(encode(&mut enc, i).0);
5602        }
5603        // Frame 9 has left an 8-frame window holding 12..19, so every reference predicts
5604        // through it: the next frame is a key frame.
5605        assert!(enc.invalidate_reference(9));
5606        let (out, reference) = encode(&mut enc, 20);
5607        assert_eq!(reference, Reference::None);
5608        assert_eq!(h264_frame_type(&out), FRAME_KEY);
5609    }
5610
5611    /// A session told to keep one reference frame (`video_reference_frames`) declares a decoded
5612    /// picture buffer of one, and a loss of the frame before the last is a key frame.
5613    #[cfg(feature = "gpl")]
5614    #[test]
5615    fn x264_keeps_the_reference_frames_it_is_given() {
5616        use crate::encoders::codec::{FRAME_KEY, h264_frame_type};
5617        use crate::encoders::sps::h264_max_num_ref_frames;
5618        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
5619        let mut enc = H264EncoderWrapper::with_depth(
5620            W as i32, H as i32, 20, false, 8, 60.0, 1, false, 0, 0, 0, 0, 1,
5621        )
5622        .expect("x264 init");
5623        let mut frames = Vec::new();
5624        for i in 0..4usize {
5625            let mut out = Vec::new();
5626            assert!(enc.encode_with_headers(
5627                &text_luma(i),
5628                &u,
5629                &v,
5630                W as i32,
5631                (W / 2) as i32,
5632                (W / 2) as i32,
5633                i as u16,
5634                i == 0,
5635                true,
5636                &mut out
5637            ));
5638            frames.push(out);
5639        }
5640        assert_eq!(h264_max_num_ref_frames(&frames[0]), Some(1));
5641        assert!(enc.invalidate_reference(3));
5642        let mut out = Vec::new();
5643        assert!(enc.encode_with_headers(
5644            &text_luma(4),
5645            &u,
5646            &v,
5647            W as i32,
5648            (W / 2) as i32,
5649            (W / 2) as i32,
5650            4,
5651            false,
5652            true,
5653            &mut out
5654        ));
5655        assert_eq!(h264_frame_type(&out), FRAME_KEY, "nothing older is held");
5656    }
5657
5658    /// A reference depth changed live (`LiveTunables::video_reference_frames`) reopens the
5659    /// x264 session at it on a key frame, and an unchanged one codes on.
5660    #[cfg(feature = "gpl")]
5661    #[test]
5662    fn x264_reopens_at_a_changed_reference_depth() {
5663        use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
5664        use crate::encoders::sps::h264_max_num_ref_frames;
5665        let mut settings = CaptureSettings {
5666            width: W as i32,
5667            height: H as i32,
5668            codec: Codec::H264,
5669            target_fps: 60.0,
5670            video_streaming_mode: true,
5671            omit_stripe_headers: true,
5672            video_reference_frames: 1,
5673            ..Default::default()
5674        };
5675        let mut stripes = Vec::new();
5676        let mut encode = |settings: &CaptureSettings, i: usize| {
5677            let argb: Vec<u8> = text_luma(i).iter().flat_map(|&l| [l, l, l, 255]).collect();
5678            let out = super::encode_cpu(
5679                &mut stripes,
5680                &argb,
5681                W as i32,
5682                H as i32,
5683                &[],
5684                settings,
5685                i as u16,
5686                false,
5687                true,
5688                i == 0,
5689            );
5690            assert_eq!(out.len(), 1, "frame {i} is one full-frame stripe");
5691            out[0].data.to_vec()
5692        };
5693        let first = encode(&settings, 0);
5694        assert_eq!(h264_max_num_ref_frames(&first), Some(1));
5695        assert_eq!(h264_frame_type(&encode(&settings, 1)), FRAME_DELTA);
5696        settings.video_reference_frames = 0;
5697        let reopened = encode(&settings, 2);
5698        assert_eq!(
5699            h264_frame_type(&reopened),
5700            FRAME_KEY,
5701            "the reopen codes a key frame"
5702        );
5703        assert!(
5704            h264_max_num_ref_frames(&reopened).is_some_and(|n| n > 1),
5705            "at the session's own depth"
5706        );
5707        assert_eq!(
5708            h264_frame_type(&encode(&settings, 3)),
5709            FRAME_DELTA,
5710            "and codes on"
5711        );
5712    }
5713
5714    /// x264 counts `frame_num` in sixteen values and the stream in 4096 (`WideFrameNum`): a loss
5715    /// covering frame 16, where x264's own count wraps, is predicted past, and one covering frame
5716    /// 4096 is answered with a key frame, since past a gap across the stream's wrap FFmpeg's
5717    /// decoder drops about a range of pictures.
5718    #[test]
5719    #[cfg(feature = "gpl")]
5720    fn x264_answers_a_loss_at_the_frame_num_wrap_with_a_key_frame() {
5721        use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
5722        use crate::encoders::reference::Reference;
5723        use crate::encoders::sps::h264_frame_num_range;
5724        use crate::webcam::decode::{Decoder as _, VideoDecoder};
5725        let (w, h) = (160usize, 96usize);
5726        let (u, v) = (vec![128u8; w * h / 4], vec![128u8; w * h / 4]);
5727        let mut enc =
5728            H264EncoderWrapper::new(w as i32, h as i32, 20, false, 60.0, 1, false, 0, 0, 0, 0)
5729                .expect("x264 init");
5730        let encode = |enc: &mut H264EncoderWrapper, i: usize| {
5731            let y: Vec<u8> = (0..w * h)
5732                .map(|p| ((p % w + p / w + 3 * i) * 5 % 256) as u8)
5733                .collect();
5734            let mut out = Vec::new();
5735            assert!(enc.encode_with_headers(
5736                &y,
5737                &u,
5738                &v,
5739                w as i32,
5740                (w / 2) as i32,
5741                (w / 2) as i32,
5742                i as u16,
5743                i == 0,
5744                true,
5745                &mut out
5746            ));
5747            (out, enc.last_reference())
5748        };
5749        let (mut whole, mut lossy) = (
5750            VideoDecoder::new(Codec::H264).unwrap(),
5751            VideoDecoder::new(Codec::H264).unwrap(),
5752        );
5753        let (first, _) = encode(&mut enc, 0);
5754        let range = h264_frame_num_range(&first).expect("the key frame carries the SPS") as usize;
5755        assert_eq!(range, 4096, "x264's sixteen values, a byte wider");
5756        assert!(whole.decode(&first).expect("decode") && lossy.decode(&first).expect("decode"));
5757        for i in 1..=18 {
5758            let (out, _) = encode(&mut enc, i);
5759            assert!(whole.decode(&out).expect("decode"), "frame {i}");
5760            if i < 16 {
5761                assert!(lossy.decode(&out).expect("decode"), "frame {i}");
5762            }
5763        }
5764        assert!(enc.invalidate_reference(16), "x264's wrap frame is lost");
5765        let (out, reference) = encode(&mut enc, 19);
5766        assert_eq!(reference, Reference::Frame(15));
5767        assert_eq!(h264_frame_type(&out), FRAME_DELTA);
5768        assert!(whole.decode(&out).expect("decode"));
5769        assert!(lossy.decode(&out).expect("decode past x264's wrap"));
5770        let apart = luma_distance(&whole.frame().unwrap(), &lossy.frame().unwrap());
5771        assert!(
5772            apart < 0.5,
5773            "the decoder that lost frames 16-18 shows frame 19 {apart:.2} off the one that saw them"
5774        );
5775        for i in 20..=range {
5776            let (out, _) = encode(&mut enc, i);
5777            if i < range {
5778                assert!(lossy.decode(&out).expect("decode"), "frame {i}");
5779            }
5780        }
5781        assert!(
5782            enc.invalidate_reference(range as u16),
5783            "the stream's wrap frame is reported lost"
5784        );
5785        let (out, reference) = encode(&mut enc, range + 1);
5786        assert_eq!(reference, Reference::None);
5787        assert_eq!(h264_frame_type(&out), FRAME_KEY);
5788        assert!(
5789            lossy.decode(&out).expect("decode past the wrap"),
5790            "the decoder that never saw the wrap frame shows the next one"
5791        );
5792        assert_eq!(
5793            encode(&mut enc, range + 2).1,
5794            Reference::Frame(range as u16 + 1)
5795        );
5796        assert!(
5797            enc.invalidate_reference(range as u16 + 2),
5798            "the count restarted at the key frame"
5799        );
5800        assert_eq!(
5801            encode(&mut enc, range + 3).1,
5802            Reference::Frame(range as u16 + 1)
5803        );
5804    }
5805
5806    /// Mean absolute luma difference between two decoded pictures.
5807    #[cfg(feature = "gpl")]
5808    fn luma_distance(
5809        a: &crate::webcam::convert::I420View<'_>,
5810        b: &crate::webcam::convert::I420View<'_>,
5811    ) -> f64 {
5812        let rows =
5813            a.y.chunks(a.y_stride)
5814                .zip(b.y.chunks(b.y_stride))
5815                .take(a.height);
5816        rows.flat_map(|(ra, rb)| {
5817            ra[..a.width]
5818                .iter()
5819                .zip(&rb[..a.width])
5820                .map(|(&x, &y)| (f64::from(x) - f64::from(y)).abs())
5821        })
5822        .sum::<f64>()
5823            / (a.width * a.height) as f64
5824    }
5825
5826    /// Encode the same scrolling-text sequence through the OpenH264 full-frame encoder (luma
5827    /// broadcast to a gray BGRA frame), returning each frame's bitstream for comparison with the
5828    /// x264 run.
5829    fn encode_oh264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
5830        let s = CaptureSettings {
5831            width: W as i32,
5832            height: H as i32,
5833            target_fps: 60.0,
5834            codec: Codec::H264,
5835            video_cbr_mode: cbr,
5836            video_bitrate_kbps: kbps,
5837            video_crf: crf,
5838            video_min_qp: min_qp,
5839            video_max_qp: max_qp,
5840            ..Default::default()
5841        };
5842        let mut enc = Openh264Encoder::new(&s).expect("oh264 init");
5843        (0..FRAMES)
5844            .map(|i| {
5845                let y = text_luma(i);
5846                let mut bgra = vec![255u8; W * H * 4];
5847                for (px, &l) in bgra.as_chunks_mut::<4>().0.iter_mut().zip(y.iter()) {
5848                    px[0] = l;
5849                    px[1] = l;
5850                    px[2] = l;
5851                }
5852                enc.encode_host_argb(&bgra, W * 4, i as u64, i == 0, false)
5853                    .expect("oh264 encode")
5854            })
5855            .collect()
5856    }
5857
5858    /// Decode a sequence of H.264 frames back to tightly-packed luma planes (dropping empty
5859    /// frames and the decoded chroma) for PSNR comparison.
5860    fn decode_luma(frames: &[Vec<u8>]) -> Vec<Vec<u8>> {
5861        let mut dec = Decoder::new().expect("decoder");
5862        let mut out = Vec::new();
5863        for f in frames {
5864            if f.is_empty() {
5865                continue;
5866            }
5867            if let Ok(Some(img)) = dec.decode(f) {
5868                let (w, h) = img.dimensions();
5869                let stride = img.strides().0;
5870                let mut y = vec![0u8; w * h];
5871                for r in 0..h {
5872                    y[r * w..r * w + w].copy_from_slice(&img.y()[r * stride..r * stride + w]);
5873                }
5874                out.push(y);
5875            }
5876        }
5877        out
5878    }
5879
5880    /// Mean per-frame luma PSNR (dB) between two decoded sequences, treating a zero-MSE frame
5881    /// as 99 dB.
5882    fn mean_psnr(a: &[Vec<u8>], b: &[Vec<u8>]) -> f64 {
5883        let n = a.len().min(b.len());
5884        let mut acc = 0.0;
5885        for i in 0..n {
5886            let mse: f64 = a[i]
5887                .iter()
5888                .zip(b[i].iter())
5889                .map(|(&x, &y)| {
5890                    let d = f64::from(x) - f64::from(y);
5891                    d * d
5892                })
5893                .sum::<f64>()
5894                / a[i].len() as f64;
5895            acc += if mse <= 0.0 {
5896                99.0
5897            } else {
5898                10.0 * (255.0f64 * 255.0 / mse).log10()
5899            };
5900        }
5901        acc / n.max(1) as f64
5902    }
5903
5904    /// Average encoded bitrate (kbps) of a frame sequence, assuming 60 fps playback.
5905    fn kbps(frames: &[Vec<u8>]) -> f64 {
5906        frames.iter().map(|f| f.len()).sum::<usize>() as f64 * 8.0 * 60.0 / FRAMES as f64 / 1000.0
5907    }
5908
5909    /// Diagnostic that the CBR QP clamp is actually plumbed through to x264, printing a
5910    /// bitrate/PSNR table on scrolling text and asserting the effect.
5911    ///
5912    /// Encodes worst-case scrolling text at 2 Mbps CBR across a sweep of `max_qp` values (plus a
5913    /// separate `min_qp` sweep on an over-provisioned 12 Mbps budget), measuring luma PSNR against a
5914    /// near-lossless CRF-12 reference from the same encoder so color-conversion differences cancel
5915    /// out. Capping `max_qp` at 30 on rate-starved content must lift fidelity by more than 0.5 dB
5916    /// over the unclamped run — proving the clamp reaches the encoder rather than being silently
5917    /// dropped (paid for in bitrate overshoot).
5918    #[cfg(feature = "gpl")]
5919    #[test]
5920    fn cbr_qp_bound_sweep_x264() {
5921        let reference = decode_luma(&encode_x264(false, 0, 12, 0, 0));
5922
5923        println!("scrolling-text 720p60 @ 2 Mbps CBR, x264 (PSNR vs own CRF-12 decode):");
5924        let mut rows = Vec::new();
5925        for &max_qp in &[0i32, 45, 40, 35, 30] {
5926            let x = encode_x264(true, 2000, 25, 0, max_qp);
5927            let psnr = mean_psnr(&decode_luma(&x), &reference);
5928            println!(
5929                "  max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
5930                max_qp,
5931                kbps(&x),
5932                psnr
5933            );
5934            rows.push((max_qp, psnr));
5935        }
5936        println!("scrolling-text 720p60 @ 12 Mbps CBR, x264 min-QP sweep:");
5937        for &min_qp in &[0i32, 10, 15] {
5938            let x = encode_x264(true, 12000, 25, min_qp, 0);
5939            let psnr = mean_psnr(&decode_luma(&x), &reference);
5940            println!(
5941                "  min_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
5942                min_qp,
5943                kbps(&x),
5944                psnr
5945            );
5946        }
5947
5948        let base = rows[0].1;
5949        let capped = rows.last().unwrap().1;
5950        assert!(
5951            capped > base + 0.5,
5952            "x264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
5953        );
5954    }
5955
5956    /// The OpenH264 counterpart of [`cbr_qp_bound_sweep_x264`]: the software H.264 sweep of a
5957    /// build without the `gpl` feature, where OpenH264 is the encoder behind every stripe.
5958    ///
5959    /// Same scrolling-text workload and 2 Mbps CBR `max_qp` sweep, with luma PSNR measured against
5960    /// this encoder's own near-lossless QP-12 reference. Capping `max_qp` at 30 must lift fidelity
5961    /// by more than 0.5 dB over the unclamped run.
5962    #[test]
5963    fn cbr_qp_bound_sweep_openh264() {
5964        let reference = decode_luma(&encode_oh264(false, 0, 12, 0, 0));
5965
5966        println!("scrolling-text 720p60 @ 2 Mbps CBR, oh264 (PSNR vs own QP-12 decode):");
5967        let mut rows = Vec::new();
5968        for &max_qp in &[0i32, 45, 40, 35, 30] {
5969            let o = encode_oh264(true, 2000, 25, 0, max_qp);
5970            let psnr = mean_psnr(&decode_luma(&o), &reference);
5971            println!(
5972                "  max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
5973                max_qp,
5974                kbps(&o),
5975                psnr
5976            );
5977            rows.push((max_qp, psnr));
5978        }
5979
5980        let base = rows[0].1;
5981        let capped = rows.last().unwrap().1;
5982        assert!(
5983            capped > base + 0.5,
5984            "oh264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
5985        );
5986    }
5987
5988    /// After a live frame-rate change the CBR stream still tracks the configured bitrate.
5989    ///
5990    /// x264's per-frame CBR budget is `bitrate / fps`, so halving the frame rate at a fixed kbps
5991    /// budget must roughly double each encoded frame while the per-second bitrate holds. This encodes
5992    /// incompressible full-frame noise at 20 Mbps CBR (content the rate controller cannot undershoot,
5993    /// so per-frame size sits at the budget; a budget the noise cannot meet within the quantizer
5994    /// ceiling would read the encoder's shortfall instead), measures the mean encoded frame size at 60 fps, drops
5995    /// to 30 fps through `reconfigure_rate`, and requires the per-frame size to roughly double — so
5996    /// the per-second bitrate is preserved rather than collapsing to half, which is what the pre-fix
5997    /// path did by leaving the session budgeting for 60 fps (`x264_encoder_reconfig` never applies a
5998    /// frame-rate change). Single-threaded so the rate-control measurement is deterministic; warmup
5999    /// frames are discarded so the ABR controller and the post-reopen IDR do not skew the mean.
6000    #[cfg(feature = "gpl")]
6001    #[test]
6002    fn cbr_bitrate_tracks_configured_rate_after_fps_change() {
6003        const TARGET_KBPS: i32 = 20000;
6004        const WARMUP: usize = 24;
6005        const MEASURED: usize = 96;
6006        let u = vec![128u8; (W / 2) * (H / 2)];
6007        let v = vec![128u8; (W / 2) * (H / 2)];
6008        let vbv = |fps: f64| {
6009            (crate::encoders::vbv_bits((TARGET_KBPS as u32) * 1000, fps, 0.0, 0.0) / 1000).max(1)
6010                as i32
6011        };
6012        // A fresh incompressible luma plane every frame, so inter-prediction cannot cheapen a
6013        // frame and CBR must spend its whole per-frame budget: the per-frame size then reads the
6014        // budget directly, which is exactly what a frame-rate change is supposed to move.
6015        let noise_luma = |frame: usize| -> Vec<u8> {
6016            let mut y = vec![0u8; W * H];
6017            let mut s = (frame as u32).wrapping_mul(2654435761).wrapping_add(1);
6018            for p in y.iter_mut() {
6019                s ^= s << 13;
6020                s ^= s >> 17;
6021                s ^= s << 5;
6022                *p = (s >> 24) as u8;
6023            }
6024            y
6025        };
6026
6027        let mut enc = H264EncoderWrapper::new(
6028            W as i32,
6029            H as i32,
6030            25,
6031            false,
6032            60.0,
6033            1,
6034            true,
6035            TARGET_KBPS,
6036            vbv(60.0),
6037            0,
6038            0,
6039        )
6040        .expect("x264 init");
6041
6042        let measure = |enc: &mut H264EncoderWrapper, start: usize| -> f64 {
6043            let mut bytes = 0usize;
6044            let mut counted = 0usize;
6045            for i in start..start + WARMUP + MEASURED {
6046                let y = noise_luma(i);
6047                let mut out = Vec::new();
6048                enc.encode_with_headers(
6049                    &y,
6050                    &u,
6051                    &v,
6052                    W as i32,
6053                    (W / 2) as i32,
6054                    (W / 2) as i32,
6055                    i as u16,
6056                    i == start,
6057                    true,
6058                    &mut out,
6059                );
6060                if i >= start + WARMUP && !out.is_empty() {
6061                    bytes += out.len();
6062                    counted += 1;
6063                }
6064            }
6065            bytes as f64 / counted.max(1) as f64
6066        };
6067
6068        let per_frame_60 = measure(&mut enc, 0);
6069        enc.reconfigure_rate(TARGET_KBPS, vbv(30.0), 30.0);
6070        let per_frame_30 = measure(&mut enc, 1000);
6071
6072        let eff_60 = per_frame_60 * 8.0 * 60.0 / 1000.0;
6073        let eff_30 = per_frame_30 * 8.0 * 30.0 / 1000.0;
6074        println!(
6075            "x264 CBR {TARGET_KBPS} kbps: 60fps {eff_60:.0} kbps ({per_frame_60:.0} B/frame), 30fps {eff_30:.0} kbps ({per_frame_30:.0} B/frame)"
6076        );
6077
6078        let ratio = per_frame_30 / per_frame_60.max(1.0);
6079        assert!(
6080            (1.5..2.6).contains(&ratio),
6081            "30fps per-frame size {per_frame_30:.0} B vs 60fps {per_frame_60:.0} B (ratio {ratio:.2}); halving fps should roughly double each frame"
6082        );
6083        assert!(
6084            eff_30 > eff_60 * 0.75,
6085            "30fps effective bitrate {eff_30:.0} kbps collapsed from the 60fps {eff_60:.0} kbps; fps change did not re-budget the bitrate"
6086        );
6087    }
6088}