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//! CPU-based striped encoder: H.264 through the build's software encoder — libx264 with the
8//! `gpl` feature, Cisco OpenH264 without it (`software_encoder`) — and turbojpeg for JPEG.
9//!
10//! Frames are split into horizontal stripes processed in parallel via rayon. Each stripe is
11//! independently hashed against the previous frame for change detection, and only dirty stripes
12//! are encoded. The H.264 path maintains per-stripe encoder state across frames for
13//! inter-prediction, and both libraries emit the same per-stripe wire framing; the JPEG path is
14//! stateless.
15
16use super::codec::{Codec, push_jpeg_header};
17#[cfg(feature = "gpl")]
18use super::codec::{
19    FRAME_DELTA, FRAME_INTRA, FRAME_KEY, h264_dpb_frames, h264_level, h264_reference_level,
20    push_video_header,
21};
22#[cfg(feature = "gpl")]
23use super::frame_rate::FrameRate;
24use super::reference::Reference;
25#[cfg(feature = "gpl")]
26use super::reference::{Invalidation, ReferenceWindow};
27#[cfg(feature = "gpl")]
28use super::sps::{WideFrameNum, h264_frame_num_range};
29use crate::RustCaptureSettings;
30use crate::pipeline::{Cleanup, Damage};
31use rayon::prelude::*;
32use smithay::utils::{Physical, Rectangle};
33#[cfg(feature = "gpl")]
34use std::ffi::CString;
35#[cfg(feature = "gpl")]
36use std::ptr;
37use std::sync::Arc;
38use yuv::{BufferStoreMut, YuvConversionMode, YuvPlanarImageMut, YuvRange, YuvStandardMatrix};
39
40/// Upper bound on the horizontal stripes the CPU encoder splits a frame into, so the
41/// persistent per-stripe state vector can be reserved to a fixed capacity once, up front.
42///
43/// With the vector reserved to this size at startup, the per-frame resize to the actual stripe count
44/// stays a cheap in-place adjustment that preserves each stripe's reused encoder and scratch buffers,
45/// rather than a reallocation that would churn them whenever the count changes.
46pub const MAX_STRIPE_CAPACITY: usize = 64;
47
48/// Convert a packed BGRA/RGBA buffer to planar YUV (4:2:0 or 4:4:4) for the software H.264
49/// encoders, spreading the conversion across up to `bands` threads so it never bottlenecks a frame.
50///
51/// **Why the band split exists.** Color conversion is a non-trivial slice of per-frame CPU. The
52/// striped path already parallelizes it for free — each stripe converts on its own rayon worker —
53/// but a single full-frame consumer (the whole-frame x264 stripe, a full-frame OpenH264
54/// instance, a whole-frame session of another codec) would otherwise convert its entire image on one thread and
55/// stall the frame there. Splitting into horizontal bands hands that lone conversion the same
56/// multi-threading the striped path enjoys. The cut is horizontal because YUV planes are
57/// row-major, so a horizontal boundary yields contiguous, non-overlapping plane sub-slices with no
58/// per-row seam bookkeeping.
59///
60/// 1. **Plane strides**: `strides` gives the Y and chroma row pitches of the output planes
61///    (tightly packed for the stripe buffers, padded for an AVFrame); the chroma planes are
62///    `width` wide for 4:4:4 (`i444 == true`) or `width / 2` for 4:2:0. `rgba_input` selects
63///    the source byte order and `i444` the subsampling; `full_range` selects the signal range,
64///    **Full** for the 4:4:4 stream x264 and x265 declare and **Limited** for everything else.
65///    The matrix is **BT.709**, whose primaries and transfer the sRGB desktop source already
66///    carries, unless `bt601` marks a codec whose bitstream can name no other matrix (VP8).
67///    Every encoder declares the matrix it was fed. All four `yuv` crate routines run in the
68///    **Fast** conversion mode.
69/// 2. **Band split**: `band_h` is `height / bands` floored to an even number and at least 2 rows
70///    (a band under 2 rows is not worth a task). Keeping band boundaries even ensures a 4:2:0
71///    chroma pair never straddles a seam. When `bands <= 1` or the whole image fits one band, the
72///    conversion runs on the calling thread in place.
73/// 3. **Parallel bands**: otherwise `src` and the three output planes are carved into contiguous
74///    per-band sub-slices (chroma rows scaled by `uv_rows` — full height for 4:4:4, half for
75///    4:2:0) and converted on the rayon pool, whose workers already exist, so a frame spawns no
76///    thread. The final band absorbs any leftover rows, taking all remaining rows whenever fewer
77///    than `band_h + 2` are left. The first error wins.
78#[allow(clippy::too_many_arguments)]
79pub(crate) fn convert_to_yuv_mt(
80    src: &[u8],
81    src_stride: u32,
82    width: usize,
83    height: usize,
84    rgba_input: bool,
85    i444: bool,
86    full_range: bool,
87    bt601: bool,
88    y_buf: &mut [u8],
89    u_buf: &mut [u8],
90    v_buf: &mut [u8],
91    strides: (usize, usize),
92    bands: usize,
93) -> Result<(), yuv::YuvError> {
94    let (y_stride, uv_stride) = strides;
95    let range = if full_range {
96        YuvRange::Full
97    } else {
98        YuvRange::Limited
99    };
100    let matrix = if bt601 {
101        YuvStandardMatrix::Bt601
102    } else {
103        YuvStandardMatrix::Bt709
104    };
105
106    let convert_band = |src_band: &[u8], y: &mut [u8], u: &mut [u8], v: &mut [u8], h: usize| {
107        let mut img = YuvPlanarImageMut {
108            y_plane: BufferStoreMut::Borrowed(y),
109            y_stride: y_stride as u32,
110            u_plane: BufferStoreMut::Borrowed(u),
111            u_stride: uv_stride as u32,
112            v_plane: BufferStoreMut::Borrowed(v),
113            v_stride: uv_stride as u32,
114            width: width as u32,
115            height: h as u32,
116        };
117        match (i444, rgba_input) {
118            (true, true) => yuv::rgba_to_yuv444(
119                &mut img,
120                src_band,
121                src_stride,
122                range,
123                matrix,
124                YuvConversionMode::Fast,
125            ),
126            (true, false) => yuv::bgra_to_yuv444(
127                &mut img,
128                src_band,
129                src_stride,
130                range,
131                matrix,
132                YuvConversionMode::Fast,
133            ),
134            (false, true) => yuv::rgba_to_yuv420(
135                &mut img,
136                src_band,
137                src_stride,
138                range,
139                matrix,
140                YuvConversionMode::Fast,
141            ),
142            (false, false) => yuv::bgra_to_yuv420(
143                &mut img,
144                src_band,
145                src_stride,
146                range,
147                matrix,
148                YuvConversionMode::Fast,
149            ),
150        }
151    };
152
153    let band_h = ((height / bands.max(1)) & !1).max(2);
154    if bands <= 1 || height <= band_h {
155        return convert_band(src, y_buf, u_buf, v_buf, height);
156    }
157
158    let uv_rows = |rows: usize| if i444 { rows } else { rows / 2 };
159    let mut jobs = Vec::new();
160    let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
161    let mut row = 0;
162    while row < height {
163        let h = if height - row < band_h + 2 {
164            height - row
165        } else {
166            band_h
167        };
168        let (src_band, s_next) = src_rest.split_at(h * src_stride as usize);
169        let (y_band, y_next) = y_rest.split_at_mut(h * y_stride);
170        let (u_band, u_next) = u_rest.split_at_mut(uv_rows(h) * uv_stride);
171        let (v_band, v_next) = v_rest.split_at_mut(uv_rows(h) * uv_stride);
172        src_rest = s_next;
173        y_rest = y_next;
174        u_rest = u_next;
175        v_rest = v_next;
176        row += h;
177        jobs.push((src_band, y_band, u_band, v_band, h));
178    }
179    jobs.into_par_iter()
180        .map(|(src_band, y_band, u_band, v_band, h)| {
181            convert_band(src_band, y_band, u_band, v_band, h)
182        })
183        .collect::<Result<Vec<()>, _>>()
184        .map(|_| ())
185}
186
187/// The fixed-point scale of the 10-bit conversion's coefficients.
188const YUV10_SHIFT: u32 = 14;
189
190/// The BT.709 coefficients that take 8-bit R, G, B to 10-bit Y, Cb, Cr at `full_range` or
191/// limited range, scaled by `1 << YUV10_SHIFT`: a row each for Y, Cb, and Cr, then the luma
192/// offset. The chroma offset is the 10-bit midpoint at either range.
193fn yuv10_coefficients(full_range: bool) -> ([[i32; 3]; 3], i32) {
194    const KR: f64 = 0.2126;
195    const KB: f64 = 0.0722;
196    const KG: f64 = 1.0 - KR - KB;
197    let (luma, chroma, offset) = if full_range {
198        (1023.0 / 255.0, 1023.0 / 255.0, 0)
199    } else {
200        (876.0 / 255.0, 896.0 / 255.0, 64)
201    };
202    let fixed = |k: f64| (k * (1 << YUV10_SHIFT) as f64).round() as i32;
203    let (cb, cr) = (chroma / (2.0 * (1.0 - KB)), chroma / (2.0 * (1.0 - KR)));
204    (
205        [
206            [fixed(luma * KR), fixed(luma * KG), fixed(luma * KB)],
207            [fixed(-cb * KR), fixed(-cb * KG), fixed(cb * (1.0 - KB))],
208            [fixed(cr * (1.0 - KR)), fixed(-cr * KG), fixed(-cr * KB)],
209        ],
210        offset,
211    )
212}
213
214/// Convert a packed BGRA/RGBA buffer to planar 10-bit YUV (4:2:0 or 4:4:4) in 16-bit samples,
215/// for the software encoders' 10-bit sessions, across up to `bands` threads as
216/// `convert_to_yuv_mt` splits them.
217///
218/// The 10-bit samples are computed from the 8-bit source directly rather than widened from an
219/// 8-bit conversion, so the two bits the encoder gains carry the precision an 8-bit Y, Cb, Cr
220/// rounds away. The matrix is BT.709 at `full_range` or limited range, and a 4:2:0 chroma
221/// sample is the conversion of the mean of its 2x2 block, which sites it at the block's
222/// center as the 8-bit conversion does. `strides` are in samples.
223#[allow(clippy::too_many_arguments)]
224pub(crate) fn convert_to_yuv10_mt(
225    src: &[u8],
226    src_stride: usize,
227    width: usize,
228    height: usize,
229    rgba_input: bool,
230    i444: bool,
231    full_range: bool,
232    y_buf: &mut [u16],
233    u_buf: &mut [u16],
234    v_buf: &mut [u16],
235    strides: (usize, usize),
236    bands: usize,
237) {
238    let (y_stride, uv_stride) = strides;
239    let (k, y_offset) = yuv10_coefficients(full_range);
240    let (ri, bi) = if rgba_input { (0, 2) } else { (2, 0) };
241    let half = 1 << (YUV10_SHIFT - 1);
242    let luma = move |r: i32, g: i32, b: i32| {
243        ((k[0][0] * r + k[0][1] * g + k[0][2] * b + half) >> YUV10_SHIFT) + y_offset
244    };
245    let chroma = move |row: &[i32; 3], r: i32, g: i32, b: i32, shift: u32| {
246        (((row[0] * r + row[1] * g + row[2] * b + (1 << (shift - 1))) >> shift) + 512)
247            .clamp(0, 1023) as u16
248    };
249
250    let convert_band = move |src: &[u8], y: &mut [u16], u: &mut [u16], v: &mut [u16], h: usize| {
251        for row in 0..h {
252            let line = &src[row * src_stride..row * src_stride + width * 4];
253            let out = &mut y[row * y_stride..row * y_stride + width];
254            for (px, y) in line.as_chunks::<4>().0.iter().zip(out.iter_mut()) {
255                *y = luma(px[ri] as i32, px[1] as i32, px[bi] as i32) as u16;
256            }
257        }
258        if i444 {
259            for row in 0..h {
260                let line = &src[row * src_stride..row * src_stride + width * 4];
261                let cb = &mut u[row * uv_stride..row * uv_stride + width];
262                let cr = &mut v[row * uv_stride..row * uv_stride + width];
263                for ((px, cb), cr) in line
264                    .as_chunks::<4>()
265                    .0
266                    .iter()
267                    .zip(cb.iter_mut())
268                    .zip(cr.iter_mut())
269                {
270                    let (r, g, b) = (px[ri] as i32, px[1] as i32, px[bi] as i32);
271                    *cb = chroma(&k[1], r, g, b, YUV10_SHIFT);
272                    *cr = chroma(&k[2], r, g, b, YUV10_SHIFT);
273                }
274            }
275            return;
276        }
277        let cw = width.div_ceil(2);
278        for row in 0..h.div_ceil(2) {
279            let top = &src[2 * row * src_stride..2 * row * src_stride + width * 4];
280            let below = (2 * row + 1).min(h - 1);
281            let bottom = &src[below * src_stride..below * src_stride + width * 4];
282            let cb = &mut u[row * uv_stride..row * uv_stride + cw];
283            let cr = &mut v[row * uv_stride..row * uv_stride + cw];
284            for (col, (cb, cr)) in cb.iter_mut().zip(cr.iter_mut()).enumerate() {
285                let left = 8 * col;
286                let right = (left + 4).min(width * 4 - 4);
287                let sum = |i: usize| {
288                    top[left + i] as i32
289                        + top[right + i] as i32
290                        + bottom[left + i] as i32
291                        + bottom[right + i] as i32
292                };
293                let (r, g, b) = (sum(ri), sum(1), sum(bi));
294                *cb = chroma(&k[1], r, g, b, YUV10_SHIFT + 2);
295                *cr = chroma(&k[2], r, g, b, YUV10_SHIFT + 2);
296            }
297        }
298    };
299
300    let band_h = ((height / bands.max(1)) & !1).max(2);
301    if bands <= 1 || height <= band_h {
302        convert_band(src, y_buf, u_buf, v_buf, height);
303        return;
304    }
305    let uv_rows = |rows: usize| if i444 { rows } else { rows.div_ceil(2) };
306    let mut jobs = Vec::new();
307    let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
308    let mut row = 0;
309    while row < height {
310        let h = if height - row < band_h + 2 {
311            height - row
312        } else {
313            band_h
314        };
315        let last = row + h >= height;
316        let take = |len: usize, rows: usize, stride: usize| if last { len } else { rows * stride };
317        let (src_band, s_next) = src_rest.split_at(take(src_rest.len(), h, src_stride));
318        let (y_band, y_next) = y_rest.split_at_mut(take(y_rest.len(), h, y_stride));
319        let (u_band, u_next) = u_rest.split_at_mut(take(u_rest.len(), uv_rows(h), uv_stride));
320        let (v_band, v_next) = v_rest.split_at_mut(take(v_rest.len(), uv_rows(h), uv_stride));
321        src_rest = s_next;
322        y_rest = y_next;
323        u_rest = u_next;
324        v_rest = v_next;
325        row += h;
326        jobs.push((src_band, y_band, u_band, v_band, h));
327    }
328    jobs.into_par_iter()
329        .for_each(|(src_band, y_band, u_band, v_band, h)| {
330            convert_band(src_band, y_band, u_band, v_band, h)
331        });
332}
333
334thread_local! {
335    /// Reused libjpeg-turbo compressor kept per worker thread to avoid paying a
336    /// `tjInitCompress`/`tjDestroy` round trip for every stripe of every frame.
337    ///
338    /// The striped JPEG path compresses one stripe per rayon worker, so the compressor is
339    /// thread-local rather than shared: each worker creates its own lazily on first use and then
340    /// holds it for the process lifetime. Making it thread-local also sidesteps the locking a shared
341    /// compressor would otherwise need across the parallel stripe encoders.
342    static JPEG_COMPRESSOR: std::cell::RefCell<Option<turbojpeg::Compressor>> =
343        const { std::cell::RefCell::new(None) };
344}
345
346/// Process-global lock that serializes libx264 encoder open/close, because those calls are
347/// not thread-safe yet the striped path opens encoders concurrently from many stripe workers.
348///
349/// libx264 mutates process-global state inside `x264_encoder_open`/`x264_encoder_close`, so two
350/// stripe encoders opening at once — or two capture instances sharing one process — can race that
351/// state and corrupt the heap. The lock is deliberately held only around open and close, never
352/// around `x264_encoder_encode`, so serializing setup costs nothing in the hot per-stripe encode
353/// path where the real parallelism lives.
354#[cfg(feature = "gpl")]
355static X264_OPEN_CLOSE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
356
357/// What libx264 adds to a quantizer above 8 bits: its quantizer bounds, a frame's forced
358/// quantizer, and the one it reports are all on a scale that starts six below zero for every
359/// bit past eight, while the rate factor is not. Added going in and taken off coming out, so a
360/// session's quantizers mean what they do at 8 bits whatever its depth.
361#[cfg(feature = "gpl")]
362fn qp_offset(bit_depth: u32) -> i32 {
363    6 * (bit_depth as i32 - 8)
364}
365
366/// The rate an x264 session is opened at: the capture's, or 30 frames per second where it names
367/// less than one.
368#[cfg(feature = "gpl")]
369fn x264_frame_rate(fps: f64) -> FrameRate {
370    FrameRate::of(if fps < 1.0 { 30.0 } else { fps })
371}
372
373/// One long-lived libx264 session for a stripe, holding the raw `x264_t` handle alongside a
374/// mirror of its live parameters so the encoder can be retuned per frame instead of rebuilt.
375///
376/// Rebuilding an x264 encoder is expensive and forces a fresh IDR, so a stripe keeps its instance
377/// across frames and only nudges CRF, bitrate, VBV, and frame rate live; the tracked `current_*`
378/// fields are that mirror, letting a reconfigure skip the FFI call whenever nothing actually changed.
379/// `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
380/// one of the few things that forces a full rebuild; `is_cbr` records which rate-control mode was
381/// chosen at open and gates which of the live reconfigures apply. The manual `Send` impl exists only
382/// because a raw pointer is not `Send` by default and the handle must move onto the rayon stripe
383/// workers; `Drop` closes it under the global open/close lock for the same reason that lock exists.
384#[cfg(feature = "gpl")]
385pub struct H264EncoderWrapper {
386    encoder: *mut x264_sys::x264_t,
387    pub width: i32,
388    pub height: i32,
389    current_crf: i32,
390    pub is_i444: bool,
391    /// The bits per sample the session was opened at, baked in as the chroma format is.
392    pub bit_depth: u32,
393    is_cbr: bool,
394    current_bitrate: i32,
395    current_vbv: i32,
396    current_fps: FrameRate,
397    /// Open-time parameters retained so a frame-rate change can reopen the session: x264's live
398    /// reconfigure cannot alter the frame rate, and CBR/VBV budgets are derived from it.
399    threads: i32,
400    min_qp: i32,
401    max_qp: i32,
402    /// The frames the decoder holds, so a lost one can be left out of the predictions and
403    /// each frame can name what it predicts from.
404    references: ReferenceWindow,
405    /// The most frames the decoded picture buffer keeps, 0 for the level's
406    /// (`video_reference_frames`), kept for a reopen.
407    reference_frames: u32,
408    last_reference: Reference,
409    /// The stream's `frame_num`, widened from x264's sixteen values to 4096.
410    frame_num: WideFrameNum,
411    /// The quantizer the next frame is held at whatever the rate control (`hold_quantizer`).
412    held_qp: Option<i32>,
413    /// The quantizer the rate control last coded a frame at, held frames aside.
414    last_qp: Option<u32>,
415}
416
417#[cfg(feature = "gpl")]
418unsafe impl Send for H264EncoderWrapper {}
419
420#[cfg(feature = "gpl")]
421impl Drop for H264EncoderWrapper {
422    fn drop(&mut self) {
423        if !self.encoder.is_null() {
424            let _guard = X264_OPEN_CLOSE_LOCK
425                .lock()
426                .unwrap_or_else(|e| e.into_inner());
427            unsafe { x264_sys::x264_encoder_close(self.encoder) };
428            self.encoder = ptr::null_mut();
429        }
430    }
431}
432
433#[cfg(feature = "gpl")]
434impl H264EncoderWrapper {
435    /// Open an x264 encoder tuned for real-time screen streaming, or `None` on failure.
436    ///
437    /// **Why this configuration.** These frames are captured live and must ship immediately, so the
438    /// encoder is optimized for latency over compression ratio: the `ultrafast` preset keeps encode
439    /// time under the frame budget, and `zerolatency` bars the frame reordering and lookahead
440    /// buffering that would otherwise add pipeline delay. Everything below then bends x264 toward the
441    /// pipeline's own keyframe and color model instead of its broadcast-oriented defaults.
442    ///
443    /// 1. **Preset/tune**: starts from the `ultrafast` preset with the `zerolatency` tune, then
444    ///    overrides resolution, frame rate (floored to 30 fps when under 1), and thread count.
445    /// 2. **Infinite GOP**: `i_keyint_max` is set to x264's infinite sentinel and adaptive scene-cut
446    ///    is disabled (`i_scenecut_threshold = 0`), so the encoder never injects an unrequested IDR
447    ///    on a scene change — keyframes are purely on-demand via the forced-IDR path, matching the
448    ///    strict infinite-GOP model.
449    /// 3. **Rate control**:
450    ///    - **CBR** (`cbr_mode`): ABR targeting `bitrate_kbps` with a VBV cap pinned to the same
451    ///      value (buffer `vbv_kbit`, precomputed by the caller from the frame-time multiplier
452    ///      policy) and filler disabled. `max_qp` is the legibility floor (caps how ugly a
453    ///      rate-starved frame gets) and `min_qp` the waste ceiling (stops over-spending on easy
454    ///      content); both are clamped to 51, and the ceiling is 51 when unset because x264's
455    ///      own default admits out-of-spec quantizers above it that exist only to force skips
456    ///      when the VBV underflows, which leaves rows of the picture frozen on old content. A
457    ///      budget the content cannot meet overshoots instead, as NVENC and libvpx do.
458    ///    - **CRF** (default): constant-quality with `f_rf_constant = crf`. A positive
459    ///      `vbv_kbit` caps it with a VBV of that buffer at a peak of `bitrate_kbps` (x264's
460    ///      capped CRF): the rate factor still picks the quantizer and the VBV bounds the size
461    ///      of a frame, a key frame above all, under CBR's ceiling of 51, so content the cap
462    ///      starves overshoots it rather than freezes. `vbv_kbit == 0` leaves it uncapped.
463    /// 4. **Color**: I444 at full range or I420 at limited range, a VUI declaring that range
464    ///    with the BT.709 primaries, transfer, and matrix the sRGB source and the conversion
465    ///    carry, and the matching `high444` / `baseline` profile.
466    /// 5. **Coding tools**: CABAC and the 8x8 transform are disabled, matching the low-latency
467    ///    baseline profile — CAVLC entropy coding with no 8x8 DCT — for minimal encode cost.
468    /// 6. **Output**: repeated headers (SPS/PPS before each keyframe) and Annex-B framing, with
469    ///    x264's own logging silenced.
470    /// 7. **References**: a decoded picture buffer of `REFERENCE_FRAMES` (`i_dpb_size`), declaring
471    ///    the lowest level up to 5.2 that holds them (`h264_reference_level`: 5.0 at 1080p, where
472    ///    4.2 holds four), so `invalidate_reference` can leave a frame a client lost out of the
473    ///    predictions with earlier frames still there to predict from, for a report up to eight
474    ///    frames late (133 ms at 60 fps). Motion search keeps its single reference.
475    /// 8. **`frame_num`**: x264 counts it in sixteen values for that buffer, and a loss covering
476    ///    the frame where it wraps costs a key frame (`ReferenceWindow`), so the stream carries it
477    ///    a byte wider (`WideFrameNum`): 4096 values, one such frame in 68 s at 60 fps.
478    ///
479    /// The `x264_encoder_open` call is serialized under `X264_OPEN_CLOSE_LOCK` because it mutates
480    /// libx264 global state.
481    #[allow(clippy::too_many_arguments)]
482    pub fn new(
483        width: i32,
484        height: i32,
485        crf: i32,
486        is_i444: bool,
487        fps: f64,
488        threads: i32,
489        cbr_mode: bool,
490        bitrate_kbps: i32,
491        vbv_kbit: i32,
492        min_qp: i32,
493        max_qp: i32,
494    ) -> Option<Self> {
495        Self::open(
496            width,
497            height,
498            crf,
499            is_i444,
500            8,
501            fps,
502            threads,
503            cbr_mode,
504            bitrate_kbps,
505            vbv_kbit,
506            min_qp,
507            max_qp,
508            0,
509            None,
510        )
511    }
512
513    /// `new` at `bit_depth` bits per sample: 10 opens a High 10 session, or High 4:4:4
514    /// Predictive at 10 bits, which reads its planes as 16-bit samples; its decoded picture
515    /// buffer keeps at most `reference_frames`, 0 for the level's (`video_reference_frames`).
516    #[allow(clippy::too_many_arguments)]
517    pub fn with_depth(
518        width: i32,
519        height: i32,
520        crf: i32,
521        is_i444: bool,
522        bit_depth: u32,
523        fps: f64,
524        threads: i32,
525        cbr_mode: bool,
526        bitrate_kbps: i32,
527        vbv_kbit: i32,
528        min_qp: i32,
529        max_qp: i32,
530        reference_frames: u32,
531    ) -> Option<Self> {
532        Self::open(
533            width,
534            height,
535            crf,
536            is_i444,
537            bit_depth,
538            fps,
539            threads,
540            cbr_mode,
541            bitrate_kbps,
542            vbv_kbit,
543            min_qp,
544            max_qp,
545            reference_frames,
546            None,
547        )
548    }
549
550    /// Whether the linked libx264 opens a 10-bit session, which is a property of how the
551    /// library was built, asked once of a session too small to cost anything.
552    pub fn ten_bit() -> bool {
553        static TEN_BIT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
554        *TEN_BIT.get_or_init(|| {
555            Self::with_depth(64, 64, 25, false, 10, 30.0, 1, false, 0, 0, 0, 0, 0).is_some()
556        })
557    }
558
559    /// `new`, with a constant-rate session's first frame given a budget of its own where `key`
560    /// names one, `(budget_kbit, level_idc)`: the rate is that budget a frame and the VBV buffer
561    /// that budget, full, until `restore_rate`, and the level is pinned to the one the session it
562    /// replaces declared, which the raised rate would otherwise lift.
563    #[allow(clippy::too_many_arguments)]
564    fn open(
565        width: i32,
566        height: i32,
567        crf: i32,
568        is_i444: bool,
569        bit_depth: u32,
570        fps: f64,
571        threads: i32,
572        cbr_mode: bool,
573        bitrate_kbps: i32,
574        vbv_kbit: i32,
575        min_qp: i32,
576        max_qp: i32,
577        reference_frames: u32,
578        key: Option<(i32, i32)>,
579    ) -> Option<Self> {
580        unsafe {
581            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
582            let preset = CString::new("ultrafast").unwrap();
583            let tune = CString::new("zerolatency").unwrap();
584
585            if x264_sys::x264_param_default_preset(&mut param, preset.as_ptr(), tune.as_ptr()) < 0 {
586                return None;
587            }
588
589            let frame_rate = x264_frame_rate(fps);
590            param.i_width = width;
591            param.i_height = height;
592            param.i_fps_num = frame_rate.num;
593            param.i_fps_den = frame_rate.den;
594            param.i_keyint_max = x264_sys::X264_KEYINT_MAX_INFINITE as i32;
595            param.i_scenecut_threshold = 0;
596            let bitrate_bps = if cbr_mode {
597                bitrate_kbps.saturating_abs() as u64 * 1000
598            } else {
599                0
600            };
601            let dpb = h264_dpb_frames(
602                h264_reference_level(
603                    h264_level(width as u32, height as u32, frame_rate.ceil(), bitrate_bps),
604                    width as u32,
605                    height as u32,
606                ),
607                width as u32,
608                height as u32,
609            );
610            let dpb = match reference_frames {
611                0 => dpb,
612                n => dpb.min(n),
613            };
614            param.i_dpb_size = dpb as i32;
615            if cbr_mode {
616                let bk = bitrate_kbps.saturating_abs();
617                param.rc.i_rc_method = x264_sys::X264_RC_ABR as i32;
618                param.rc.i_bitrate = bk;
619                param.rc.i_vbv_max_bitrate = bk;
620                param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
621                if let Some((budget, level)) = key {
622                    let rate = ((budget as f64 * frame_rate.fps()) as i32).max(bk);
623                    param.rc.i_bitrate = rate;
624                    param.rc.i_vbv_max_bitrate = rate;
625                    param.rc.i_vbv_buffer_size = budget.max(1);
626                    param.rc.f_vbv_buffer_init = 1.0;
627                    param.i_level_idc = level;
628                }
629                param.rc.b_filler = 0;
630                if min_qp > 0 {
631                    param.rc.i_qp_min = min_qp.min(51) + qp_offset(bit_depth);
632                }
633                param.rc.i_qp_max =
634                    if max_qp > 0 { max_qp.min(51) } else { 51 } + qp_offset(bit_depth);
635            } else {
636                param.rc.i_rc_method = x264_sys::X264_RC_CRF as i32;
637                param.rc.f_rf_constant = crf as f32;
638                if vbv_kbit > 0 {
639                    param.rc.i_vbv_max_bitrate = bitrate_kbps.saturating_abs();
640                    param.rc.i_vbv_buffer_size = vbv_kbit;
641                    param.rc.i_qp_max = 51 + qp_offset(bit_depth);
642                }
643            }
644            let depth_flag = if bit_depth > 8 {
645                x264_sys::X264_CSP_HIGH_DEPTH
646            } else {
647                0
648            };
649            param.i_csp = (if is_i444 {
650                x264_sys::X264_CSP_I444
651            } else {
652                x264_sys::X264_CSP_I420
653            } | depth_flag) as i32;
654            param.i_bitdepth = bit_depth as i32;
655            param.vui.b_fullrange = if is_i444 { 1 } else { 0 };
656            param.vui.i_colorprim = 1;
657            param.vui.i_transfer = 1;
658            param.vui.i_colmatrix = 1;
659
660            let profile = CString::new(match (is_i444, bit_depth > 8) {
661                (true, _) => "high444",
662                (false, true) => "high10",
663                (false, false) => "baseline",
664            })
665            .unwrap();
666            x264_sys::x264_param_apply_profile(&mut param, profile.as_ptr());
667
668            param.i_threads = threads;
669            param.b_repeat_headers = 1;
670            param.b_annexb = 1;
671            param.i_log_level = x264_sys::X264_LOG_NONE;
672
673            let encoder = {
674                let _guard = X264_OPEN_CLOSE_LOCK
675                    .lock()
676                    .unwrap_or_else(|e| e.into_inner());
677                x264_sys::x264_encoder_open(&mut param)
678            };
679            if encoder.is_null() {
680                None
681            } else {
682                Some(Self {
683                    encoder,
684                    width,
685                    height,
686                    current_crf: crf,
687                    is_i444,
688                    bit_depth,
689                    is_cbr: cbr_mode,
690                    current_bitrate: bitrate_kbps.saturating_abs(),
691                    current_vbv: vbv_kbit,
692                    current_fps: frame_rate,
693                    threads,
694                    min_qp,
695                    max_qp,
696                    references: ReferenceWindow::new(dpb),
697                    reference_frames,
698                    last_reference: Reference::Untracked,
699                    frame_num: WideFrameNum::default(),
700                    held_qp: None,
701                    last_qp: None,
702                })
703            }
704        }
705    }
706
707    /// Retune the constant-quality CRF on the running encoder, so a quality change costs a
708    /// parameter push rather than tearing down and rebuilding the session (a rebuild would force an
709    /// IDR and drop encoder state).
710    ///
711    /// It is a no-op in CBR mode, where rate is bitrate-controlled and CRF simply does not apply, and
712    /// a no-op when the value is unchanged — the tracked `current_crf` is what makes that cheap
713    /// early-out possible. Otherwise it reads the encoder's live parameters, overwrites
714    /// `f_rf_constant`, and pushes the change via `x264_encoder_reconfig`, advancing the tracked CRF
715    /// only once the reconfig has actually succeeded so the mirror never drifts from the encoder.
716    pub fn reconfigure_crf(&mut self, new_crf: i32) {
717        if self.is_cbr || self.current_crf == new_crf {
718            return;
719        }
720        unsafe {
721            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
722            x264_sys::x264_encoder_parameters(self.encoder, &mut param);
723            param.rc.f_rf_constant = new_crf as f32;
724            if x264_sys::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
725                self.current_crf = new_crf;
726            }
727        }
728    }
729
730    /// Retune bitrate/VBV (CBR, or a capped CRF) and/or frame rate to match the live settings,
731    /// structured to be called unconditionally every frame so the caller need not track what
732    /// changed itself.
733    ///
734    /// Because `encode_cpu` fires it on every frame, it first computes the would-be values and bails
735    /// before touching the encoder when neither the bitrate/VBV nor the frame rate differs from
736    /// what is live — that self-gating keeps a per-frame call nearly free.
737    ///
738    /// A frame-rate change reopens the encoder rather than reconfiguring it: `x264_encoder_reconfig`
739    /// does not apply `i_fps_*`, and the CBR/VBV per-frame budget is `bitrate / fps`, so a session
740    /// left at its old rate ships roughly half the configured bitrate once fps halves. The reopen
741    /// carries the new bitrate/VBV too, and a fresh session emits an IDR on its first frame; a failed
742    /// reopen keeps the working session instead of nulling the handle. Capping a CRF session or
743    /// lifting its cap reopens it too, since x264 retunes a VBV in place only while one is on. A
744    /// bitrate/VBV-only change otherwise stays a live `x264_encoder_reconfig`, and the tracked
745    /// mirror advances only on success so it cannot drift from the encoder's real state.
746    pub fn reconfigure_rate(&mut self, bitrate_kbps: i32, vbv_kbit: i32, fps: f64) {
747        let bk = bitrate_kbps.saturating_abs();
748        let new_fps = x264_frame_rate(fps);
749        let vbv_applies = self.is_cbr || vbv_kbit > 0;
750        let rate_changed =
751            vbv_applies && (self.current_bitrate != bk || self.current_vbv != vbv_kbit);
752        let reopen = self.current_fps != new_fps
753            || (!self.is_cbr && (self.current_vbv > 0) != (vbv_kbit > 0));
754        if !rate_changed && !reopen {
755            return;
756        }
757        if reopen {
758            if let Some(fresh) = H264EncoderWrapper::with_depth(
759                self.width,
760                self.height,
761                self.current_crf,
762                self.is_i444,
763                self.bit_depth,
764                new_fps.fps(),
765                self.threads,
766                self.is_cbr,
767                bk,
768                vbv_kbit,
769                self.min_qp,
770                self.max_qp,
771                self.reference_frames,
772            ) {
773                *self = fresh;
774            }
775            return;
776        }
777        unsafe {
778            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
779            x264_sys::x264_encoder_parameters(self.encoder, &mut param);
780            param.rc.i_bitrate = bk;
781            if vbv_applies {
782                param.rc.i_vbv_max_bitrate = bk;
783                param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
784            }
785            if x264_sys::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
786                self.current_bitrate = bk;
787                self.current_vbv = vbv_kbit;
788            }
789        }
790    }
791
792    /// The frame the last encoded frame predicted from.
793    pub fn last_reference(&self) -> Reference {
794        self.last_reference
795    }
796
797    /// The quantizer the rate control last coded a frame at, held frames aside.
798    pub fn last_qp(&self) -> Option<u32> {
799        self.last_qp
800    }
801
802    /// Encode the next frame at quantizer `qp` whatever the rate control, leaving the session's
803    /// own for the frame after: x264's per-picture forced quantizer.
804    ///
805    /// Under a constant rate x264's row-level VBV control moves a forced quantizer back toward
806    /// the rate control's own for the frame, so a held key frame there, a fresh start for the
807    /// decoder anyway, starts a fresh session (`open_for_held_key`) whose rate control plans that
808    /// frame with `HELD_KEY_BUDGET_S` of the target, which also bounds its size, and takes the
809    /// session's own rate back for the frame after.
810    pub fn hold_quantizer(&mut self, qp: i32) {
811        self.held_qp = Some(qp.clamp(0, 51));
812    }
813
814    /// Replace the session with one whose first frame, the held key frame about to be encoded,
815    /// has the budget of `HELD_KEY_BUDGET_S` of the target; true where it opened, and
816    /// `restore_rate` is owed after the frame.
817    fn open_for_held_key(&mut self) -> bool {
818        let level = unsafe {
819            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
820            x264_sys::x264_encoder_parameters(self.encoder, &mut param);
821            param.i_level_idc
822        };
823        let budget = (self.current_bitrate as f64 * super::HELD_KEY_BUDGET_S).round() as i32;
824        let fresh = Self::open(
825            self.width,
826            self.height,
827            self.current_crf,
828            self.is_i444,
829            self.bit_depth,
830            self.current_fps.fps(),
831            self.threads,
832            self.is_cbr,
833            self.current_bitrate,
834            self.current_vbv,
835            self.min_qp,
836            self.max_qp,
837            self.reference_frames,
838            Some((budget, level)),
839        );
840        let Some(mut fresh) = fresh else { return false };
841        fresh.held_qp = self.held_qp;
842        *self = fresh;
843        true
844    }
845
846    /// Put the session's own rate and VBV buffer back after a held key frame.
847    fn restore_rate(&mut self) {
848        unsafe {
849            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
850            x264_sys::x264_encoder_parameters(self.encoder, &mut param);
851            param.rc.i_bitrate = self.current_bitrate;
852            param.rc.i_vbv_max_bitrate = self.current_bitrate;
853            param.rc.i_vbv_buffer_size = self.current_vbv.max(1);
854            x264_sys::x264_encoder_reconfig(self.encoder, &mut param);
855        }
856    }
857
858    /// Leave frame `frame_id` and every frame after it out of the predictions. False when x264
859    /// refuses, and the caller codes a key frame instead.
860    pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
861        match self.references.invalidate(frame_id) {
862            Invalidation::Forget(pts) => unsafe {
863                x264_sys::x264_encoder_invalidate_reference(self.encoder, pts as i64) == 0
864            },
865            Invalidation::KeyFrame | Invalidation::Ignored => true,
866        }
867    }
868
869    /// Encode one YUV frame into H.264 and frame it for the wire, reporting whether the
870    /// encoder actually emitted a bitstream this call.
871    ///
872    /// The boolean return is load-bearing: `x264_encoder_encode` can legitimately produce nothing on
873    /// a given call, and the caller must forward a stripe only when real bytes exist — never an empty
874    /// or header-only packet. Framing is conditional because the transport needs the pipeline's small
875    /// wire header to route the stripe, while `omit_headers` consumers take the bare Annex-B
876    /// elementary stream.
877    ///
878    /// 1. **Picture setup**: wraps the borrowed Y/U/V planes and their strides in an
879    ///    `x264_picture_t` with the encoder's CSP, stamps the presentation timestamp with the
880    ///    session's count of encoded frames (the one an invalidation names), and requests an IDR
881    ///    when `force_idr` is set or no reference is left to predict from (otherwise
882    ///    `X264_TYPE_AUTO`).
883    /// 2. **Encode**: calls `x264_encoder_encode`; a non-positive returned size means no frame was
884    ///    emitted this call, so the function returns `false` without writing output.
885    /// 3. **Framing**: `output_buf` is cleared and refilled. Unless `omit_headers` is set, the wire
886    ///    header is prepended with a frame kind read from the *actual* output picture type rather
887    ///    than from `force_idr`, because the encoder may not honor a keyframe request and the
888    ///    client keys its decode-recovery on the kind it truly received. With `omit_headers` the
889    ///    output is bare Annex-B.
890    /// 4. **Payload**: every NAL payload is appended to `output_buf` after the optional header,
891    ///    its `frame_num` widened (`WideFrameNum`), so the bytes past the wire header are always a
892    ///    contiguous Annex-B access unit.
893    #[allow(clippy::too_many_arguments)]
894    pub fn encode_with_headers(
895        &mut self,
896        y: &[u8],
897        u: &[u8],
898        v: &[u8],
899        y_stride: i32,
900        u_stride: i32,
901        v_stride: i32,
902        frame_id: u16,
903        y_start: u16,
904        force_idr: bool,
905        omit_headers: bool,
906        output_buf: &mut Vec<u8>,
907    ) -> bool {
908        let key_budget =
909            self.is_cbr && force_idr && self.held_qp.is_some() && self.open_for_held_key();
910        let coded = self.encode_picture(
911            y,
912            u,
913            v,
914            y_stride,
915            u_stride,
916            v_stride,
917            frame_id,
918            y_start,
919            force_idr,
920            omit_headers,
921            output_buf,
922        );
923        if key_budget {
924            self.restore_rate();
925        }
926        coded
927    }
928
929    /// `encode_with_headers` past the choice of session.
930    #[allow(clippy::too_many_arguments)]
931    fn encode_picture(
932        &mut self,
933        y: &[u8],
934        u: &[u8],
935        v: &[u8],
936        y_stride: i32,
937        u_stride: i32,
938        v_stride: i32,
939        frame_id: u16,
940        y_start: u16,
941        force_idr: bool,
942        omit_headers: bool,
943        output_buf: &mut Vec<u8>,
944    ) -> bool {
945        unsafe {
946            let mut pic_in: x264_sys::x264_picture_t = std::mem::zeroed();
947            x264_sys::x264_picture_init(&mut pic_in);
948
949            pic_in.img.i_csp = (if self.is_i444 {
950                x264_sys::X264_CSP_I444
951            } else {
952                x264_sys::X264_CSP_I420
953            } | if self.bit_depth > 8 {
954                x264_sys::X264_CSP_HIGH_DEPTH
955            } else {
956                0
957            }) as i32;
958            pic_in.img.i_plane = 3;
959            pic_in.img.plane[0] = y.as_ptr() as *mut u8;
960            pic_in.img.plane[1] = u.as_ptr() as *mut u8;
961            pic_in.img.plane[2] = v.as_ptr() as *mut u8;
962            pic_in.img.i_stride[0] = y_stride;
963            pic_in.img.i_stride[1] = u_stride;
964            pic_in.img.i_stride[2] = v_stride;
965            pic_in.i_pts = self.references.next_pts() as i64;
966            pic_in.i_type = if force_idr || !self.references.has_reference() {
967                x264_sys::X264_TYPE_IDR
968            } else {
969                x264_sys::X264_TYPE_AUTO
970            } as i32;
971            let held = self.held_qp.take();
972            let offset = qp_offset(self.bit_depth);
973            pic_in.i_qpplus1 = held.map_or(x264_sys::X264_QP_AUTO as i32, |q| q + offset + 1);
974
975            let mut pic_out: x264_sys::x264_picture_t = std::mem::zeroed();
976            let mut nals: *mut x264_sys::x264_nal_t = ptr::null_mut();
977            let mut i_nals: i32 = 0;
978
979            let frame_size = x264_sys::x264_encoder_encode(
980                self.encoder,
981                &mut nals,
982                &mut i_nals,
983                &mut pic_in,
984                &mut pic_out,
985            );
986
987            if frame_size > 0 {
988                if held.is_none() {
989                    self.last_qp = (pic_out.i_qpplus1 > 0)
990                        .then(|| (pic_out.i_qpplus1 - 1 - offset).max(0) as u32);
991                }
992                let frame_type = if pic_out.i_type == x264_sys::X264_TYPE_IDR as i32 {
993                    FRAME_KEY
994                } else if pic_out.i_type == x264_sys::X264_TYPE_I as i32 {
995                    FRAME_INTRA
996                } else {
997                    FRAME_DELTA
998                };
999                self.last_reference = self.references.record(frame_id, frame_type != FRAME_DELTA);
1000                output_buf.clear();
1001                output_buf.reserve(super::codec::VIDEO_HEADER_LEN + frame_size as usize);
1002                if !omit_headers {
1003                    push_video_header(
1004                        output_buf,
1005                        Codec::H264,
1006                        frame_type,
1007                        frame_id,
1008                        y_start,
1009                        self.width as u16,
1010                        self.height as u16,
1011                        self.last_reference,
1012                    );
1013                }
1014
1015                let nal_slice = std::slice::from_raw_parts(nals, i_nals as usize);
1016                let mut widened = true;
1017                for nal in nal_slice {
1018                    let payload = std::slice::from_raw_parts(nal.p_payload, nal.i_payload as usize);
1019                    widened &= self.frame_num.push(payload, output_buf);
1020                }
1021                if !widened {
1022                    // Its slices no longer match the set they went out under, so the next frame
1023                    // is a key frame.
1024                    self.references.reset();
1025                }
1026                if frame_type == FRAME_KEY {
1027                    let stream = &output_buf[if omit_headers {
1028                        0
1029                    } else {
1030                        super::codec::VIDEO_HEADER_LEN
1031                    }..];
1032                    self.references
1033                        .set_frame_num_range(h264_frame_num_range(stream).unwrap_or(0));
1034                }
1035                return true;
1036            }
1037        }
1038        false
1039    }
1040}
1041
1042/// Everything one horizontal stripe must remember between frames: its reused buffers, its own
1043/// live encoder, and the motion / paint-over / damage bookkeeping that drives its send decision.
1044///
1045/// The frame is striped so independent screen regions can encode in parallel and an unchanged region
1046/// can be skipped on its own, and that only works if each stripe carries its *own* cross-frame
1047/// history. So one instance lives per stripe for the whole session and nothing per-stripe is rebuilt
1048/// or recomputed from scratch each frame:
1049/// - **Reused buffers**: `y_buf` / `u_buf` / `v_buf` hold the stripe's YUV planes and `packet_buf`
1050///   the encoded output, grown in place rather than reallocated per frame.
1051/// - **Encoder**: `h264_encoder` is the stripe's software H.264 instance — libx264 in a `gpl`
1052///   build, OpenH264 otherwise — reused until its geometry (or, for x264, chroma format) changes.
1053/// - **Cleanup / recovery**: `no_motion_frame_count` counts consecutive static frames,
1054///   `paint_over_sent` records that the region was cleaned up since it last changed,
1055///   `h264_burst_frames_remaining` tracks a post-cleanup or recovery streaming burst (held at the
1056///   paint-over quantizer where `burst_held`), and `dirty_run`, `change_mass`, `unclean_frames`,
1057///   and `motion` (the share of recent frames in motion) are what `pipeline::cleanup_due` weighs
1058///   to pick the cleanup's moment and kind; `clean_quality` keeps a constant-quality session at
1059///   the paint-over quality from a cleanup until the region changes again; `rc_bytes` (the bytes of
1060///   the stripe's last frame under its rate control), `idle_frames` (a constant-rate cleanup's run
1061///   of small frames short of the paint-over quality, of empty ones for x264), `same_frames` (of
1062///   frames the size of the one before), `finest` and `finest_frames` (the finest quality index
1063///   its rate control has coded at and the frames since), and `over_frames` (its run of frames over
1064///   `pipeline::OVERSHOOT_BUDGETS`) tell a cleanup that runs through the rate control when it has
1065///   converged, stalled, or been pinned at its coarsest quantizer, `fine_frames` counts its run of
1066///   frames at the paint-over quantizer or finer, `measured`, `measuring`, `level_checks`, and
1067///   `measure_in` are the last measurement of the picture it is refining, whether the frame before
1068///   was measured, the measurements in a row that found it level, and the frames to the next
1069///   (`pipeline::plateau`), `settled` records
1070///   that such a cleanup ran its course under a rate control it cannot hold a quantizer
1071///   under, so no other starts before the region moves, and `sweep` (the next band's start
1072///   and the last band's size, as shares of the picture) carries the band sweep a
1073///   full-frame refresh falls back to (`pipeline::decide_hw_fullframe`), `sweep_frames` its
1074///   bands so far and `sweep_coarser` the steps they are coded coarser than the refresh.
1075/// - **Content-hash damage** (only for sources without external damage, i.e. X11): `last_hash` is
1076///   the previous frame's content hash, `consecutive_changes` counts changed frames toward the
1077///   damage-block threshold, and `in_damage_block` / `damage_block_frames_remaining` drive the
1078///   sustained-motion damage block managed by `content_dirty`.
1079#[derive(Default)]
1080pub struct StripeState {
1081    pub no_motion_frame_count: u32,
1082    pub paint_over_sent: bool,
1083    pub dirty_run: u32,
1084    pub change_mass: f32,
1085    pub unclean_frames: u32,
1086    pub motion: f32,
1087    pub burst_held: bool,
1088    pub clean_quality: bool,
1089    pub rc_bytes: usize,
1090    pub idle_frames: u32,
1091    pub over_frames: u32,
1092    pub same_frames: u32,
1093    pub finest: Option<u32>,
1094    pub finest_frames: u32,
1095    pub settled: bool,
1096    pub fine_frames: u32,
1097    pub measured: Option<f32>,
1098    pub measuring: bool,
1099    pub level_checks: u32,
1100    pub measure_in: u32,
1101    pub sweep: Option<(f64, f64)>,
1102    pub sweep_frames: u32,
1103    pub sweep_coarser: u32,
1104    #[cfg(feature = "gpl")]
1105    pub h264_encoder: Option<H264EncoderWrapper>,
1106    #[cfg(not(feature = "gpl"))]
1107    pub h264_encoder: Option<crate::encoders::oh264::Openh264Encoder>,
1108    pub h264_burst_frames_remaining: i32,
1109    #[cfg(feature = "gpl")]
1110    pub y_buf: Vec<u8>,
1111    #[cfg(feature = "gpl")]
1112    pub u_buf: Vec<u8>,
1113    #[cfg(feature = "gpl")]
1114    pub v_buf: Vec<u8>,
1115    /// The planes of a 10-bit stripe, as 16-bit samples.
1116    #[cfg(feature = "gpl")]
1117    pub yuv16: [Vec<u16>; 3],
1118    pub packet_buf: Vec<u8>,
1119    pub last_hash: u64,
1120    pub consecutive_changes: u32,
1121    pub in_damage_block: bool,
1122    pub damage_block_frames_remaining: i32,
1123}
1124
1125/// The bits per sample the striped H.264 path codes `settings` at: 10 where they ask for it
1126/// and the build's libx264 opens such a session, else 8.
1127pub fn stripe_bit_depth(settings: &RustCaptureSettings) -> u32 {
1128    #[cfg(feature = "gpl")]
1129    if settings.codec == Codec::H264
1130        && settings.video_bit_depth >= 10
1131        && H264EncoderWrapper::ten_bit()
1132    {
1133        return 10;
1134    }
1135    let _ = settings;
1136    8
1137}
1138
1139/// Fast, non-cryptographic 64-bit content hash used only for in-memory change detection.
1140///
1141/// Uses xxh3: a SIMD-friendly hash that processes 64-byte blocks with parallel lanes,
1142/// delivering near memory-bandwidth throughput. The value is never persisted or sent on the
1143/// wire, so only the property that identical bytes hash identically matters. A collision
1144/// between two distinct stripes is ~2^-64, and the next real content change or a requested
1145/// keyframe repaints any missed update anyway.
1146fn fast_hash(bytes: &[u8]) -> u64 {
1147    xxhash_rust::xxh3::xxh3_64_with_seed(bytes, 0)
1148}
1149
1150impl StripeState {
1151    /// Stand in for the compositor damage that X11 capture does not provide: hash this stripe
1152    /// to decide whether it changed since last frame, and once it is clearly in motion, stop
1153    /// re-hashing it every frame by committing to a sustained-motion "damage block".
1154    ///
1155    /// The hash is not free, and a region that changes every frame would otherwise be re-hashed
1156    /// forever while always reporting dirty anyway. So after `threshold` consecutive changes the
1157    /// stripe enters a damage block that just reports dirty for `duration` frames and re-hashes only
1158    /// on its last two frames, to decide whether to extend the block or let it lapse — trading a
1159    /// little extra sending for far fewer hashes on exactly the regions that need them least:
1160    ///
1161    /// 1. **Inside a damage block**: the stripe is treated as dirty without re-hashing, and the
1162    ///    block's remaining-frame counter is decremented. The stripe is hashed on the block's last
1163    ///    two frames: if they differ the region is still moving and the block is renewed for
1164    ///    another `duration` frames, otherwise the block exits and the change counter resets, so
1165    ///    motion that stopped inside a block is read as stopped when that block ends, not a block
1166    ///    later. This keeps a continuously-moving region streaming for `duration` frames per
1167    ///    re-check rather than hashing every frame.
1168    /// 2. **Outside a block**: the stripe is hashed and compared to the previous frame. A change
1169    ///    increments `consecutive_changes`, and reaching `threshold` consecutive changes opens a new
1170    ///    damage block; an unchanged frame resets the counter to zero.
1171    ///
1172    /// Returns `true` whenever the stripe is considered dirty (always true while inside a block).
1173    pub fn content_dirty(&mut self, bytes: &[u8], threshold: u32, duration: i32) -> bool {
1174        self.dirty_by(|| fast_hash(bytes), threshold, duration)
1175    }
1176
1177    /// `content_dirty` for a stripe hashed elsewhere (on the GPU, where the frame was uploaded):
1178    /// the same state, read off `hash`.
1179    pub fn hash_dirty(&mut self, hash: u64, threshold: u32, duration: i32) -> bool {
1180        self.dirty_by(|| hash, threshold, duration)
1181    }
1182
1183    fn dirty_by(&mut self, fast_hash: impl FnOnce() -> u64, threshold: u32, duration: i32) -> bool {
1184        if self.in_damage_block {
1185            self.damage_block_frames_remaining -= 1;
1186            if self.damage_block_frames_remaining == 1 {
1187                self.last_hash = fast_hash();
1188            } else if self.damage_block_frames_remaining <= 0 {
1189                let h = fast_hash();
1190                if h != self.last_hash {
1191                    self.damage_block_frames_remaining = duration;
1192                } else {
1193                    self.in_damage_block = false;
1194                    self.consecutive_changes = 0;
1195                }
1196                self.last_hash = h;
1197            }
1198            return true;
1199        }
1200        let h = fast_hash();
1201        let changed = h != self.last_hash;
1202        self.last_hash = h;
1203        if changed {
1204            self.consecutive_changes += 1;
1205            if self.consecutive_changes >= threshold {
1206                self.in_damage_block = true;
1207                self.damage_block_frames_remaining = duration;
1208            }
1209        } else {
1210            self.consecutive_changes = 0;
1211        }
1212        changed
1213    }
1214}
1215
1216/// One encoded stripe: the compressed bytes plus geometry and identity metadata.
1217///
1218/// The consumer can place and attribute the stripe even when the payload has no header. In
1219/// `omit_headers` mode the per-stripe wire header is stripped from the bytes; the struct fields
1220/// carry that information out-of-band.
1221///
1222/// # Fields
1223///
1224/// * `data` - Compressed payload (JPEG, or the video codec's bitstream). `Arc`-shared so every
1225///   delivery-layer consumer can retain the frame without copying the bytes.
1226/// * `codec` - The codec of the payload.
1227/// * `stripe_y_start` - Y pixel coordinate of the stripe's top edge within the frame.
1228/// * `stripe_height` - Height of the stripe in pixels.
1229/// * `frame_id` - Frame sequence number this stripe belongs to.
1230/// * `reference` - The frame this stripe predicts from.
1231pub struct EncodedStripe {
1232    pub data: Arc<Vec<u8>>,
1233    pub codec: Codec,
1234    pub stripe_y_start: i32,
1235    pub stripe_height: i32,
1236    pub frame_id: i32,
1237    pub timing: FrameTiming,
1238    pub reference: Reference,
1239}
1240
1241/// When a frame was captured and when its encode began and ended, as CLOCK_MONOTONIC
1242/// nanoseconds, so a consumer can attribute a frame's age to the host rather than the
1243/// network or the decoder. An encoder leaves them zero; the capture that ran it stamps
1244/// every stripe of the frame.
1245#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1246pub struct FrameTiming {
1247    pub capture_ns: i64,
1248    pub encode_start_ns: i64,
1249    pub encode_end_ns: i64,
1250}
1251
1252impl FrameTiming {
1253    /// Stamps the stripes of one frame, captured at `capture_ns` and encoded from
1254    /// `encode_start_ns` until now.
1255    pub fn stamp(stripes: &mut [EncodedStripe], capture_ns: i64, encode_start_ns: i64) {
1256        let timing = FrameTiming {
1257            capture_ns,
1258            encode_start_ns,
1259            encode_end_ns: crate::wayland::host::now_ns(),
1260        };
1261        for stripe in stripes {
1262            stripe.timing = timing;
1263        }
1264    }
1265}
1266
1267/// No stripe is shorter than a macroblock row.
1268const MIN_STRIPE_HEIGHT: i32 = 64;
1269/// The frames the stripes' cleanups of one still screen are spread over (`encode_cpu`).
1270const CLEANUP_STAGGER_FRAMES: usize = 4;
1271/// How fast the smoothed count of budget-carrying stripes follows the frame's.
1272const CARRY_RISE: f32 = 0.3;
1273const CARRY_FALL: f32 = 0.05;
1274
1275/// Whether the striped software path holds a stripe's cleanup at the quantizer asked for:
1276/// libx264 does under either rate control, and OpenH264 moves its quantizer only at a constant
1277/// quality (`Openh264Encoder::update_qp`), so a stripe that holds none is cleaned up by a refresh
1278/// and burst at its rate control's own quality, never a key frame, which that would starve.
1279pub fn stripes_hold_quantizer(settings: &RustCaptureSettings) -> bool {
1280    cfg!(feature = "gpl") || !settings.video_cbr_mode
1281}
1282
1283/// Whether a frame that holds still codes a stripe at the paint-over quality instead of the
1284/// session's (`encode_cpu`): at a constant quality, a stripe whose cleanup falls due, whose burst
1285/// is held, or which keeps the quality it was cleaned up at. A constant rate holds none there.
1286pub fn stripes_held_still(stripes: &[StripeState], settings: &RustCaptureSettings) -> bool {
1287    !settings.video_cbr_mode
1288        && settings.use_paint_over_quality
1289        && settings.video_paintover_crf < settings.video_crf
1290        && stripes.iter().any(|st| {
1291            st.clean_quality
1292                || (st.burst_held && st.h264_burst_frames_remaining > 0)
1293                || crate::pipeline::cleanup_pending(
1294                    st,
1295                    settings.paint_over_trigger_frames,
1296                    true,
1297                    true,
1298                )
1299        })
1300}
1301
1302/// The software encoder's per-frame entry point: split the frame into horizontal stripes,
1303/// decide per stripe whether it needs sending, and encode only those as JPEG or H.264 (libx264
1304/// or OpenH264, by build) across the rayon pool.
1305///
1306/// Two pressures drive the design: CPU H.264/JPEG is expensive, so the frame is cut into
1307/// parallel stripes; bandwidth is precious, so unchanged stripes are skipped. Each stripe is
1308/// independently hashed against the previous frame for change detection, and only dirty stripes
1309/// are encoded. The H.264 path maintains per-stripe encoder state across frames for
1310/// inter-prediction; the JPEG path is stateless.
1311///
1312/// # Arguments
1313///
1314/// * `stripes` - Persistent per-stripe state vector (resized as needed, encoder state preserved).
1315/// * `raw_pixels` - Packed BGRA/RGBA pixel buffer (`width * height * 4` bytes).
1316/// * `width` - Frame width in pixels.
1317/// * `height` - Frame height in pixels.
1318/// * `damage_rects` - Wayland damage rectangles (empty for X11 hash-based detection).
1319/// * `settings` - Capture settings (quality, mode, rate control, etc.).
1320/// * `frame_counter` - Current frame number (wrapping `u16`).
1321/// * `use_gpu` - `true` when the source is RGBA (GLES readback); `false` for BGRA (X11 host).
1322/// * `hash_damage` - `true` for X11 stripe-hash change detection; `false` when damage rects
1323///   are provided.
1324/// * `force_idr_all` - Force a keyframe on every stripe (client join / reset / periodic IDR).
1325///
1326/// # Returns
1327///
1328/// Vec of [`EncodedStripe`] — empty when nothing changed.
1329/// repainting a stalled region at full quality, and letting a freshly-joined or reset client recover
1330/// a clean picture. Persistent `StripeState` is what makes both affordable: encoders and buffers
1331/// survive across frames instead of being rebuilt, and the motion/paint-over history the decision
1332/// needs lives right beside them. The per-stripe decision mirrors `decide_hw_fullframe`'s policy for
1333/// the hardware full-frame encoders; it is kept as separate code here because the striped path also
1334/// chooses JPEG-vs-H.264 and derives its own damage.
1335///
1336/// 1. **Stripe count**: the core count, held to `MAX_STRIPES` because the client decodes one
1337///    picture per stripe, but collapses to a single full-frame stripe when H.264 full-frame is
1338///    requested or the frame is shorter than the 64-row minimum, and no stripe is thinner than 64 rows —
1339///    below that the per-stripe encoder and thread overhead outweighs the parallelism and the tiny
1340///    H.264 slices compress poorly. The persistent `stripes` vector is resized to match, preserving
1341///    per-stripe state across frames.
1342/// 2. **Idle fast path**: a frame on which no stripe can emit anything (no damage / clean
1343///    hashes, no cleanup due, no burst, no recovery IDR, not streaming) only advances the
1344///    per-stripe cleanup bookkeeping inline and returns without dispatching the stripe
1345///    fan-out, so a static capture never wakes the rayon pool.
1346/// 3. **Dirty map**: with external compositor damage (`hash_damage == false`) each `damage_rects`
1347///    rectangle marks every stripe whose row range it overlaps. With `hash_damage == true` (X11,
1348///    which has no compositor damage) per-stripe content hashing drives dirtiness instead, in
1349///    streaming H.264 too, since the cleanup reads a still stripe from its content.
1350/// 4. **Per-stripe cleanup** (`pipeline::cleanup_due`, one region per stripe): a stripe that
1351///    changed is cleaned up once it has held still for `paint_over_trigger_frames`, or has kept
1352///    changing only a little for four times that, at the paint-over JPEG quality or, for H.264,
1353///    at the paint-over quality: under CBR through libx264's rate control, the stripe sent until it
1354///    codes at the paint-over quantizer or finer in a small frame (`pipeline::convergence`), or its
1355///    frames have been slice headers alone for `pipeline::EMPTY_S`, with no key frame, and where
1356///    the stripe's encoder reports no quantizer (OpenH264) as a refresh and burst at its rate
1357///    control's quality; under CRF as the stripe's rate factor until it changes again, as main's
1358///    paint-over was, as a key frame after a run of changes and a refresh otherwise, then a burst.
1359///    The stripes whose cleanups fall due on one
1360///    frame are spread over `CLEANUP_STAGGER_FRAMES` frames, so a screen going still costs no
1361///    one frame the whole screen's cleanup. A stripe is otherwise sent when it is dirty, while
1362///    its burst runs, or when streaming mode is on, at the base quality; a newly dirty frame
1363///    cancels its burst.
1364/// 5. **Recovery IDR** (`force_idr_all`): forces a send on every stripe even when static so a
1365///    reconnecting client can resume. For H.264 it forces an IDR at the stripe's own quality and
1366///    arms the burst (held at the paint-over quantizer at a constant quality, as in
1367///    `pipeline::decide_hw_fullframe`); for JPEG, where every stripe is already intra, it resends
1368///    a painted-over stripe at the paint-over quality already on screen so a joining viewer does
1369///    not see a downgrade.
1370/// 6. **Encoding**:
1371///    - **JPEG**: source byte order is RGBA on the GPU readback path and BGRA on
1372///      X11; each worker thread reuses its thread-local TurboJPEG compressor. Header-less output
1373///      hands the compressed buffer straight through; otherwise a 6-byte stripe header (`0x03` tag,
1374///      a reserved byte, frame number, y-start) is prepended to match the H.264 path's native
1375///      framing so the transport can forward the buffer without re-framing.
1376///    - **H.264**: the stripe's encoder is reused unless the width, height, or
1377///      (x264) chroma format changed, in which case it is rebuilt and an IDR forced; otherwise CRF
1378///      and rate are reconfigured live. With libx264, ARGB is converted to YUV here (a conversion
1379///      failure skips the stripe rather than encoding garbage) and an 8-byte fixed header (frame
1380///      number, y-start, width, height) is emitted; OpenH264 converts and frames inside
1381///      `encode_stripe_argb` with the same header layout, and encodes a 4:4:4 request 4:2:0 (said
1382///      once per process). The live CBR budget is recomputed here from the bitrate/fps so it
1383///      rescales with live changes.
1384/// 7. **Dispatch**: a single full-frame stripe runs inline (sequential — empirically faster than a
1385///    one-element rayon job) with one fewer encode thread than the available cores, clamped to
1386///    `[1, 4]` (x264 with a single-band color conversion; OpenH264 adds four slices and a four-band
1387///    conversion of its own). The slice threads keep the in-frame encode latency inside the frame
1388///    budget at high resolutions; the cap is four because `zerolatency` makes x264 slice-threaded
1389///    and more than four slices trips decode glitches in some Chromium builds, and the minus-one
1390///    leaves headroom for the capture thread. Multiple stripes instead run across the rayon pool
1391///    with a single encode thread and one conversion band each, since the parallelism there
1392///    already comes from encoding the stripes concurrently.
1393#[allow(clippy::too_many_arguments)]
1394pub fn encode_cpu(
1395    stripes: &mut Vec<StripeState>,
1396    carrying: &mut f32,
1397    raw_pixels: &[u8],
1398    width: i32,
1399    height: i32,
1400    damage_rects: &[Rectangle<i32, Physical>],
1401    settings: &RustCaptureSettings,
1402    frame_counter: u16,
1403    use_gpu: bool,
1404    hash_damage: bool,
1405    force_idr_all: bool,
1406) -> Vec<EncodedStripe> {
1407    let codec = settings.codec;
1408    let n_processing_stripes = stripe_count(height, codec, settings.video_fullframe);
1409
1410    if stripes.len() != n_processing_stripes {
1411        stripes.resize_with(n_processing_stripes, StripeState::default);
1412    }
1413
1414    let stripe_geometries = compute_stripe_geometries(height as usize, n_processing_stripes, codec);
1415
1416    // Idle fast path: a static frame must still advance every stripe's paint-over countdown,
1417    // but nothing else — so when no stripe can emit anything this frame, do that bookkeeping
1418    // inline and return before the rayon fan-out. Waking the whole worker pool 60x/s for
1419    // no-op stripes is the dominant idle cost (tens of percent of a core), dwarfing the real
1420    // per-frame work. "Static" is known up front for damage-authoritative sources (Wayland:
1421    // empty damage list); hash-damage sources (X11) instead take a sequential early-exit
1422    // hash scan, probing the most-recently-dirty stripe first so live content bails out
1423    // after a single stripe hash. A clean scan performs exactly the state transitions
1424    // `content_dirty` would (hash unchanged, change streak reset), so the damage-block
1425    // machinery observes no difference.
1426    let coded_quality = |st: &StripeState| -> Option<u32> {
1427        cfg_if::cfg_if! {
1428            if #[cfg(feature = "gpl")] {
1429                st.h264_encoder.as_ref().and_then(H264EncoderWrapper::last_qp)
1430            } else {
1431                let _ = st;
1432                None
1433            }
1434        }
1435    };
1436    let holds = stripes_hold_quantizer(settings);
1437    let converges = codec.is_video() && settings.video_cbr_mode && cfg!(feature = "gpl");
1438    let keys = codec.is_video() && holds && !converges;
1439    let paint_over_armed = |st: &StripeState| {
1440        if !codec.is_video() {
1441            return settings.use_paint_over_quality
1442                && settings.paint_over_jpeg_quality > settings.jpeg_quality;
1443        }
1444        crate::pipeline::paint_over_improves(
1445            settings,
1446            crate::pipeline::EncoderQuality {
1447                last: coded_quality(st),
1448                bytes: None,
1449                holds,
1450                reopens: false,
1451                keys: true,
1452                band: None,
1453                measures: false,
1454                psnr: None,
1455            },
1456        )
1457    };
1458    let trigger_frames = settings.paint_over_trigger_frames;
1459    let idle_candidate = damage_rects.is_empty()
1460        && !force_idr_all
1461        && !(codec.is_video() && settings.video_streaming_mode);
1462    if idle_candidate {
1463        let no_pending_send = |st: &StripeState| {
1464            (!codec.is_video() || st.h264_burst_frames_remaining <= 0)
1465                && !crate::pipeline::cleanup_pending(st, trigger_frames, paint_over_armed(st), keys)
1466        };
1467        let quiescent = if !hash_damage {
1468            stripes.iter().all(no_pending_send)
1469        } else {
1470            let width_bytes = width as usize * 4;
1471            let hint = stripes
1472                .iter()
1473                .enumerate()
1474                .min_by_key(|(_, st)| st.no_motion_frame_count)
1475                .map(|(i, _)| i)
1476                .unwrap_or(0);
1477            let clean = |i: usize| {
1478                let st = &stripes[i];
1479                if !no_pending_send(st) || st.in_damage_block {
1480                    return false;
1481                }
1482                let (y, h) = stripe_geometries[i];
1483                let bytes = &raw_pixels[y * width_bytes..(y + h) * width_bytes];
1484                fast_hash(bytes) == st.last_hash
1485            };
1486            clean(hint) && (0..stripes.len()).filter(|&i| i != hint).all(clean)
1487        };
1488        if quiescent {
1489            for st in stripes.iter_mut() {
1490                let armed = paint_over_armed(st);
1491                crate::pipeline::cleanup_due(st, trigger_frames, armed, true, keys, Damage::None);
1492                st.consecutive_changes = 0;
1493            }
1494            return Vec::new();
1495        }
1496    }
1497    let mut stripe_is_dirty = vec![false; n_processing_stripes];
1498    if !damage_rects.is_empty() {
1499        for rect in damage_rects {
1500            let r_y_start = rect.loc.y.max(0) as usize;
1501            let r_y_end = (rect.loc.y + rect.size.h).min(height) as usize;
1502            if r_y_start < r_y_end {
1503                for (i, &(s_y, s_h)) in stripe_geometries.iter().enumerate() {
1504                    let s_end = s_y + s_h;
1505                    if r_y_start < s_end && r_y_end > s_y {
1506                        stripe_is_dirty[i] = true;
1507                    }
1508                }
1509            }
1510        }
1511    }
1512
1513    let width_usize = width as usize;
1514    let video = codec.is_video();
1515    let video_crf = settings.video_crf;
1516    let video_po_crf = settings.video_paintover_crf;
1517    let video_burst = settings.video_paintover_burst_frames;
1518    let video_fullcolor = settings.video_fullcolor;
1519    #[cfg(feature = "gpl")]
1520    let bit_depth = stripe_bit_depth(settings);
1521    let video_streaming = settings.video_streaming_mode;
1522    let jpeg_q = settings.jpeg_quality;
1523    let paint_q = settings.paint_over_jpeg_quality;
1524    let target_fps = settings.target_fps;
1525    let omit_headers = settings.omit_stripe_headers;
1526    let damage_block_threshold = settings.damage_block_threshold;
1527    let damage_block_duration = settings.damage_block_duration as i32;
1528    let video_cbr = settings.video_cbr_mode;
1529    // The requested rate is a whole-screen budget, and CRF needs no division
1530    // at all (a per-quality target). OpenH264 sizes its own buffer, so only
1531    // x264 reads the VBV share.
1532    #[cfg_attr(not(feature = "gpl"), allow(unused_variables))]
1533    let (video_bitrate, video_vbv) = stripe_rate_control(settings, *carrying, n_processing_stripes);
1534    #[cfg(feature = "gpl")]
1535    let cleanup_vbv = cleanup_vbv_kbit(settings, video_bitrate).max(video_vbv);
1536    // Full-frame x264 threads: one fewer than the cores (headroom for the
1537    // capture thread), clamped to [1, 4] to match the four-slice ceiling below.
1538    // A full-frame OpenH264 instance applies the same policy internally.
1539    #[cfg(feature = "gpl")]
1540    let h264_threads = if n_processing_stripes == 1 {
1541        std::thread::available_parallelism()
1542            .map(|n| n.get())
1543            .unwrap_or(1)
1544            .saturating_sub(1)
1545            .clamp(1, 4) as i32
1546    } else {
1547        1
1548    };
1549    // A frame of x264's slices, one a thread, that refines nothing.
1550    #[cfg(feature = "gpl")]
1551    let empty_bytes = crate::encoders::codec::VIDEO_HEADER_LEN
1552        + crate::pipeline::EMPTY_SLICE_BYTES * h264_threads as usize;
1553    #[cfg(not(feature = "gpl"))]
1554    let empty_bytes = 0;
1555    #[cfg(feature = "gpl")]
1556    let csc_bands = 1;
1557    if video && video_fullcolor && !crate::encoders::software_fullcolor(Codec::H264) {
1558        static FULLCOLOR_LOGGED: std::sync::atomic::AtomicBool =
1559            std::sync::atomic::AtomicBool::new(false);
1560        if !FULLCOLOR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
1561            eprintln!(
1562                "[software] 4:4:4 full-color requested; OpenH264 is 4:2:0-only, encoding 4:2:0."
1563            );
1564        }
1565    }
1566
1567    // A still screen reaches every stripe's cleanup on the same frame; spread them over
1568    // `CLEANUP_STAGGER_FRAMES` frames so no one frame carries the whole screen's.
1569    let per_frame = stripes.len().div_ceil(CLEANUP_STAGGER_FRAMES).max(1);
1570    let mut granted = 0;
1571    let cleanup_allowed: Vec<bool> = stripes
1572        .iter()
1573        .map(|st| {
1574            if !crate::pipeline::cleanup_pending(st, trigger_frames, paint_over_armed(st), keys) {
1575                return true;
1576            }
1577            granted += 1;
1578            granted <= per_frame
1579        })
1580        .collect();
1581
1582    let stripe_body = |(i, stripe_state): (usize, &mut StripeState)| -> Option<EncodedStripe> {
1583        if i >= stripe_geometries.len() {
1584            return None;
1585        }
1586        let (y_start, actual_height) = stripe_geometries[i];
1587        let start_idx = y_start * width_usize * 4;
1588        let end_idx = start_idx + (actual_height * width_usize * 4);
1589        let stripe_bytes = &raw_pixels[start_idx..end_idx];
1590
1591        let is_dirty = if !hash_damage {
1592            stripe_is_dirty[i]
1593        } else {
1594            stripe_state.content_dirty(stripe_bytes, damage_block_threshold, damage_block_duration)
1595        };
1596        let armed = paint_over_armed(stripe_state);
1597        let cleanup = crate::pipeline::cleanup_due(
1598            stripe_state,
1599            trigger_frames,
1600            armed,
1601            cleanup_allowed[i],
1602            keys,
1603            if is_dirty {
1604                Damage::Unknown
1605            } else {
1606                Damage::None
1607            },
1608        );
1609        let mut send_this_stripe = is_dirty || cleanup != Cleanup::None || force_idr_all;
1610        let mut quality_or_crf = if !video { jpeg_q } else { video_crf };
1611        let mut force_idr = video && (force_idr_all || cleanup == Cleanup::Key);
1612        let mut hold = None;
1613        let quality = crate::pipeline::EncoderQuality {
1614            last: coded_quality(stripe_state),
1615            bytes: None,
1616            holds,
1617            reopens: false,
1618            keys: true,
1619            band: None,
1620            measures: false,
1621            psnr: None,
1622        };
1623        let refresh_crf = crate::pipeline::held_refresh_quality(settings, quality) as i32;
1624        if cleanup != Cleanup::None {
1625            if !video {
1626                quality_or_crf = paint_q;
1627            } else if cleanup == Cleanup::Key {
1628                hold = Some(video_po_crf);
1629            } else if holds && !converges {
1630                hold = Some(refresh_crf);
1631            }
1632        } else if force_idr_all && !is_dirty && !video && armed && stripe_state.paint_over_sent {
1633            quality_or_crf = paint_q;
1634        }
1635        if converges
1636            && stripe_state.h264_burst_frames_remaining > 0
1637            && !is_dirty
1638            && cleanup == Cleanup::None
1639        {
1640            let budget = crate::pipeline::frame_budget(video_bitrate, target_fps);
1641            let bytes = (stripe_state.rc_bytes > 0).then_some(stripe_state.rc_bytes);
1642            stripe_state.idle_frames = if bytes.is_some_and(|b| b <= empty_bytes) {
1643                stripe_state.idle_frames.saturating_add(1)
1644            } else {
1645                0
1646            };
1647            if crate::pipeline::convergence(quality.last, bytes, video_po_crf.max(0) as u32, budget)
1648                == crate::pipeline::Convergence::Converged
1649                || stripe_state.idle_frames as f64 >= crate::pipeline::EMPTY_S * target_fps
1650            {
1651                stripe_state.h264_burst_frames_remaining = 0;
1652            }
1653        }
1654        if is_dirty {
1655            stripe_state.h264_burst_frames_remaining = 0;
1656        } else if video
1657            && converges
1658            && (cleanup != Cleanup::None || (force_idr && video_burst > 0 && armed))
1659        {
1660            stripe_state.h264_burst_frames_remaining = crate::pipeline::converge_frames(settings);
1661            stripe_state.burst_held = false;
1662            stripe_state.idle_frames = 0;
1663        } else if video && (force_idr || cleanup != Cleanup::None) && video_burst > 0 {
1664            stripe_state.h264_burst_frames_remaining = video_burst;
1665            stripe_state.burst_held =
1666                holds && (cleanup != Cleanup::None || (armed && !settings.video_cbr_mode));
1667        } else if video && stripe_state.h264_burst_frames_remaining > 0 {
1668            stripe_state.h264_burst_frames_remaining -= 1;
1669            send_this_stripe = true;
1670            if stripe_state.burst_held && armed {
1671                hold = Some(refresh_crf);
1672            }
1673        }
1674        // A constant-quality stripe is cleaned up through its session quality, as main's
1675        // paint-over was (`pipeline::decide_constant_quality`): x264's rate factor or
1676        // OpenH264's rebuild at the held index, where a key frame takes the library's intra
1677        // offset. It stays there until the stripe changes again; a requested key frame is
1678        // coded at the session's own quality.
1679        if video && !video_cbr {
1680            if is_dirty || !armed {
1681                stripe_state.clean_quality = false;
1682            }
1683            if cleanup != Cleanup::None {
1684                stripe_state.clean_quality = true;
1685            }
1686            if stripe_state.clean_quality && hold.is_none() && !force_idr_all {
1687                hold = Some(video_po_crf);
1688            }
1689        }
1690        if video && video_streaming {
1691            send_this_stripe = true;
1692        }
1693
1694        if send_this_stripe {
1695            if !video {
1696                let pixel_format = if use_gpu {
1697                    turbojpeg::PixelFormat::RGBA
1698                } else {
1699                    turbojpeg::PixelFormat::BGRA
1700                };
1701                let img = turbojpeg::Image {
1702                    pixels: stripe_bytes,
1703                    width: width_usize,
1704                    pitch: width_usize * 4,
1705                    height: actual_height,
1706                    format: pixel_format,
1707                };
1708                JPEG_COMPRESSOR.with(|cell| -> Option<EncodedStripe> {
1709                    let mut slot = cell.borrow_mut();
1710                    if slot.is_none() {
1711                        *slot = Some(turbojpeg::Compressor::new().ok()?);
1712                    }
1713                    let compressor = slot.as_mut().unwrap();
1714                    compressor.set_quality(quality_or_crf).ok()?;
1715                    let jpeg = compressor.compress_to_vec(img).ok()?;
1716                    let data = if omit_headers {
1717                        jpeg
1718                    } else {
1719                        stripe_state.packet_buf.clear();
1720                        push_jpeg_header(
1721                            &mut stripe_state.packet_buf,
1722                            frame_counter,
1723                            y_start as u16,
1724                        );
1725                        stripe_state.packet_buf.extend_from_slice(&jpeg);
1726                        std::mem::take(&mut stripe_state.packet_buf)
1727                    };
1728                    Some(EncodedStripe {
1729                        data: Arc::new(data),
1730                        codec: Codec::Jpeg,
1731                        stripe_y_start: y_start as i32,
1732                        stripe_height: actual_height as i32,
1733                        frame_id: frame_counter as i32,
1734                        timing: FrameTiming::default(),
1735                        reference: Reference::Untracked,
1736                    })
1737                })
1738            } else {
1739                cfg_if::cfg_if! {
1740                    if #[cfg(feature = "gpl")] {
1741                let needs_reinit = if let Some(ref enc) = stripe_state.h264_encoder {
1742                    enc.width != width_usize as i32
1743                        || enc.height != actual_height as i32
1744                        || enc.is_i444 != video_fullcolor
1745                        || enc.bit_depth != bit_depth
1746                } else {
1747                    true
1748                };
1749
1750                // A constant rate holds the frame at its quantizer (`hold_quantizer`); a
1751                // constant quality codes it at that rate factor.
1752                let x264_crf = if video_cbr { quality_or_crf } else { hold.unwrap_or(quality_or_crf) };
1753                if needs_reinit {
1754                    stripe_state.h264_encoder = H264EncoderWrapper::with_depth(
1755                        width_usize as i32,
1756                        actual_height as i32,
1757                        x264_crf,
1758                        video_fullcolor,
1759                        bit_depth,
1760                        target_fps,
1761                        h264_threads,
1762                        video_cbr,
1763                        video_bitrate,
1764                        video_vbv,
1765                        settings.video_min_qp,
1766                        settings.video_max_qp,
1767                        settings.video_reference_frames,
1768                    );
1769                    force_idr = true;
1770                } else if let Some(ref mut enc) = stripe_state.h264_encoder {
1771                    enc.reconfigure_crf(x264_crf);
1772                    let cleaning = converges && stripe_state.h264_burst_frames_remaining > 0;
1773                    let vbv = if cleaning { cleanup_vbv } else { video_vbv };
1774                    enc.reconfigure_rate(video_bitrate, vbv, target_fps);
1775                }
1776
1777                if let Some(ref mut enc) = stripe_state.h264_encoder {
1778                    if let Some(q) = hold.filter(|_| video_cbr) {
1779                        enc.hold_quantizer(q);
1780                    }
1781                    let y_size = width_usize * actual_height;
1782                    let uv_size = if video_fullcolor { y_size } else { y_size / 4 };
1783                    let y_stride = width_usize as i32;
1784                    let uv_stride =
1785                        (if video_fullcolor { width_usize } else { width_usize / 2 }) as i32;
1786                    let wide = bit_depth > 8;
1787                    let conversion_result = if wide {
1788                        let [y16, u16, v16] = &mut stripe_state.yuv16;
1789                        y16.resize(y_size, 0);
1790                        u16.resize(uv_size, 0);
1791                        v16.resize(uv_size, 0);
1792                        convert_to_yuv10_mt(
1793                            stripe_bytes,
1794                            width_usize * 4,
1795                            width_usize,
1796                            actual_height,
1797                            use_gpu,
1798                            video_fullcolor,
1799                            video_fullcolor,
1800                            y16,
1801                            u16,
1802                            v16,
1803                            (y_stride as usize, uv_stride as usize),
1804                            csc_bands,
1805                        );
1806                        Ok(())
1807                    } else {
1808                        if stripe_state.y_buf.len() != y_size {
1809                            stripe_state.y_buf.resize(y_size, 0);
1810                        }
1811                        if stripe_state.u_buf.len() != uv_size {
1812                            stripe_state.u_buf.resize(uv_size, 0);
1813                        }
1814                        if stripe_state.v_buf.len() != uv_size {
1815                            stripe_state.v_buf.resize(uv_size, 0);
1816                        }
1817                        convert_to_yuv_mt(
1818                            stripe_bytes,
1819                            (width_usize * 4) as u32,
1820                            width_usize,
1821                            actual_height,
1822                            use_gpu,
1823                            video_fullcolor,
1824                            video_fullcolor,
1825                            false,
1826                            &mut stripe_state.y_buf,
1827                            &mut stripe_state.u_buf,
1828                            &mut stripe_state.v_buf,
1829                            (y_stride as usize, uv_stride as usize),
1830                            csc_bands,
1831                        )
1832                    };
1833
1834                    if let Err(e) = conversion_result {
1835                        eprintln!(
1836                            "[software] YUV conversion failed for {}x{} stripe: {:?}; skipping",
1837                            width_usize, actual_height, e
1838                        );
1839                        return None;
1840                    }
1841
1842                    let bytes = |plane: &[u16]| unsafe {
1843                        std::slice::from_raw_parts(plane.as_ptr().cast::<u8>(), plane.len() * 2)
1844                    };
1845                    let (y, u, v, scale) = if wide {
1846                        let [y16, u16, v16] = &stripe_state.yuv16;
1847                        (bytes(y16), bytes(u16), bytes(v16), 2)
1848                    } else {
1849                        (
1850                            stripe_state.y_buf.as_slice(),
1851                            stripe_state.u_buf.as_slice(),
1852                            stripe_state.v_buf.as_slice(),
1853                            1,
1854                        )
1855                    };
1856                    if enc.encode_with_headers(
1857                        y,
1858                        u,
1859                        v,
1860                        y_stride * scale,
1861                        uv_stride * scale,
1862                        uv_stride * scale,
1863                        frame_counter,
1864                        y_start as u16,
1865                        force_idr,
1866                        omit_headers,
1867                        &mut stripe_state.packet_buf,
1868                    ) {
1869                        if hold.is_none() || !video_cbr {
1870                            stripe_state.rc_bytes = stripe_state.packet_buf.len();
1871                        }
1872                        Some(EncodedStripe {
1873                            data: Arc::new(std::mem::take(&mut stripe_state.packet_buf)),
1874                            codec: Codec::H264,
1875                            stripe_y_start: y_start as i32,
1876                            stripe_height: actual_height as i32,
1877                            frame_id: frame_counter as i32,
1878                            timing: FrameTiming::default(),
1879                            reference: enc.last_reference(),
1880                        })
1881                    } else {
1882                        None
1883                    }
1884                } else {
1885                    None
1886                }
1887                    } else {
1888                use crate::encoders::oh264::Openh264Encoder;
1889                let needs_reinit = stripe_state.h264_encoder.as_ref().is_none_or(|enc| {
1890                    enc.width() != width_usize || enc.height() != actual_height
1891                });
1892                if needs_reinit {
1893                    stripe_state.h264_encoder = Openh264Encoder::new_stripe(
1894                        settings,
1895                        width_usize,
1896                        actual_height,
1897                        quality_or_crf,
1898                        video_bitrate,
1899                        n_processing_stripes == 1,
1900                    );
1901                    if stripe_state.h264_encoder.is_none() {
1902                        // Once per process: a geometry OpenH264 refuses (wider than
1903                        // 3840, say) would otherwise log on every stripe of every frame.
1904                        static INIT_FAILED_LOGGED: std::sync::atomic::AtomicBool =
1905                            std::sync::atomic::AtomicBool::new(false);
1906                        if !INIT_FAILED_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
1907                            eprintln!(
1908                                "[software] OpenH264 init failed for a {}x{} stripe; no software H.264 for it",
1909                                width_usize, actual_height
1910                            );
1911                        }
1912                    }
1913                    force_idr = true;
1914                } else if let Some(ref mut enc) = stripe_state.h264_encoder {
1915                    // OpenH264 moves its quantizer only by a rebuild, which opens on a key
1916                    // frame, so a held quantizer is that key frame, and the stripe's key-frame
1917                    // cleanup with it; a still stripe keeps the quantizer it was cleaned at,
1918                    // since moving it back would rebuild a picture nothing changed in.
1919                    if let Some(q) = hold {
1920                        if enc.update_qp(q.max(0) as u32) {
1921                            stripe_state.change_mass = 0.0;
1922                        }
1923                    } else if is_dirty {
1924                        enc.update_qp(quality_or_crf.max(0) as u32);
1925                    }
1926                    enc.reconfigure_rate(video_bitrate, target_fps);
1927                }
1928
1929                let enc = stripe_state.h264_encoder.as_mut()?;
1930                match enc.encode_stripe_argb(
1931                    stripe_bytes,
1932                    width_usize * 4,
1933                    frame_counter as u64,
1934                    y_start as u16,
1935                    force_idr,
1936                    use_gpu,
1937                ) {
1938                    Ok(data) if !data.is_empty() => Some(EncodedStripe {
1939                        data: Arc::new(data),
1940                        codec: Codec::H264,
1941                        stripe_y_start: y_start as i32,
1942                        stripe_height: actual_height as i32,
1943                        frame_id: frame_counter as i32,
1944                        timing: FrameTiming::default(),
1945                        reference: enc.last_reference(),
1946                    }),
1947                    Ok(_) => None,
1948                    Err(e) => {
1949                        eprintln!("[software] OpenH264 encode failed for stripe at y={y_start}: {e}");
1950                        None
1951                    }
1952                }
1953                    }
1954                }
1955            }
1956        } else {
1957            None
1958        }
1959    };
1960    let encoded: Vec<EncodedStripe> = if n_processing_stripes <= 1 {
1961        stripes
1962            .iter_mut()
1963            .enumerate()
1964            .filter_map(&stripe_body)
1965            .collect()
1966    } else {
1967        stripes
1968            .par_iter_mut()
1969            .enumerate()
1970            .filter_map(&stripe_body)
1971            .collect()
1972    };
1973    // Follow motion spreading out quickly and narrowing slowly: the budget is
1974    // better spent late than overshot the moment a screen goes still again.
1975    let sent = encoded.len() as f32;
1976    let alpha = if sent > *carrying {
1977        CARRY_RISE
1978    } else {
1979        CARRY_FALL
1980    };
1981    *carrying += (sent - *carrying) * alpha;
1982    encoded
1983}
1984
1985/// How many horizontal stripes a frame of `height` is split into, which is the choice of how
1986/// much encode parallelism to spend on it.
1987///
1988/// A full-frame session is one contiguous stream and so a single stripe; otherwise the frame
1989/// fans out across cores, held to `MAX_STRIPES` and bounded so no stripe is shorter than a
1990/// macroblock row. Both the encoder and the settings line report from here, so what is logged is
1991/// what is encoded.
1992/// Leave frame `frame_id` and every frame after it out of every stripe's predictions. False
1993/// when an encoder cannot, and the caller codes a key frame instead.
1994pub fn invalidate_reference(stripes: &mut [StripeState], frame_id: u16) -> bool {
1995    cfg_if::cfg_if! {
1996        if #[cfg(feature = "gpl")] {
1997            // Every stripe is its own stream, so each one is told; a refusal from any of
1998            // them still leaves the rest to forget the frame.
1999            let mut forgotten = true;
2000            for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
2001                forgotten &= enc.invalidate_reference(frame_id);
2002            }
2003            forgotten
2004        } else {
2005            // OpenH264 predicts past a loss only from a long-term reference every consumer
2006            // holds, which only a full-frame instance keeps; a stripe of several codes a key
2007            // frame.
2008            let mut forgotten = true;
2009            for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
2010                forgotten &= enc.invalidate_reference(frame_id);
2011            }
2012            forgotten
2013        }
2014    }
2015}
2016
2017/// Every consumer holds frame `frame_id`, or where not `held` was sent it: OpenH264 confirms the
2018/// long-term reference it marked there. x264 keeps none.
2019pub fn acknowledge_reference(stripes: &mut [StripeState], frame_id: u16, held: bool) {
2020    #[cfg(not(feature = "gpl"))]
2021    for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
2022        enc.acknowledge_reference(frame_id, held);
2023    }
2024    #[cfg(feature = "gpl")]
2025    let _ = (stripes, frame_id, held);
2026}
2027
2028/// Most stripes a frame is cut into, whatever the encoder host's core count.
2029///
2030/// A stripe is a picture of its own on the wire, and the client decodes one per stripe: the
2031/// count is spent on the viewer's machine, which is not the one it was derived from. Measured
2032/// decoding a 1080p frame split N ways, WebKit -- the engine a phone or tablet runs -- loses
2033/// throughput with every stripe added (18.9 fps whole, 9.4 at eight, 3.8 at thirty-two), while
2034/// Chromium gains up to four and holds through twelve. Eight is where both sit near their best
2035/// and where the JPEG payload is smallest, so a host with the cores to cut thirty-two no longer
2036/// hands a tablet a frame it cannot assemble. One stripe is one encode job, so this bounds the
2037/// encoder's own parallelism with it.
2038const MAX_STRIPES: usize = 8;
2039
2040pub fn stripe_count(height: i32, codec: Codec, fullframe: bool) -> usize {
2041    if !codec.stripes() || (codec.is_video() && fullframe) || height < MIN_STRIPE_HEIGHT {
2042        return 1;
2043    }
2044    let cores = std::thread::available_parallelism()
2045        .map(|n| n.get())
2046        .unwrap_or(1);
2047    cores
2048        .min(MAX_STRIPES)
2049        .min((height / MIN_STRIPE_HEIGHT) as usize)
2050        .max(1)
2051}
2052
2053/// Frames of the target a constant-rate x264 stripe is given as its buffer while its cleanup
2054/// runs through the rate control: in the session's buffer of a frame and a half x264's
2055/// row-level control prices a still screen's whole residual against the buffer and coarsens
2056/// the rows it names a fine quantizer for, so a screen of text at 2 Mbit/s at 1080p sat at
2057/// 31 dB while the frame quantizer read 19; in four it reached 46 dB in five seconds with no
2058/// frame over a budget and a half.
2059#[cfg(feature = "gpl")]
2060const CLEANUP_VBV_FRAMES: f64 = 4.0;
2061
2062/// The buffer, in kbit, of a stripe at `bitrate_kbps` while it is cleaned up
2063/// (`CLEANUP_VBV_FRAMES`).
2064#[cfg(feature = "gpl")]
2065fn cleanup_vbv_kbit(settings: &RustCaptureSettings, bitrate_kbps: i32) -> i32 {
2066    (crate::encoders::vbv_bits(
2067        (bitrate_kbps as u32).saturating_mul(1000),
2068        settings.target_fps,
2069        0.0,
2070        CLEANUP_VBV_FRAMES,
2071    ) / 1000)
2072        .max(1) as i32
2073}
2074
2075/// Split the configured rate budget across the stripes carrying it, returning the
2076/// `(bitrate_kbps, vbv_kbit)` each stripe's encoder is programmed with: the CBR target, or the
2077/// peak of a CRF a positive `video_vbv_multiplier` caps. An uncapped CRF gets a buffer of 0.
2078///
2079/// Every stripe runs its own encoder and rate control is per instance, metered against the
2080/// declared frame rate rather than against the frames that stripe was actually sent. So the
2081/// screen's rate is one stripe's rate times the number of stripes that carry motion, and the
2082/// budget is divided by that number — not by the stripe count, which on a screen where one
2083/// corner moves would spend a fraction of what was configured. The divisor is the smoothed
2084/// count so it changes on the scale of a moving average and not every frame: a rate that
2085/// swings frame to frame leaves the encoder chasing it and delivers less than either rate would.
2086fn stripe_rate_control(
2087    settings: &RustCaptureSettings,
2088    carrying: f32,
2089    n_stripes: usize,
2090) -> (i32, i32) {
2091    let divisor = (carrying.round().max(1.0) as usize).min(n_stripes.max(1)) as i32;
2092    let bitrate = (settings.video_bitrate_kbps / divisor).max(1);
2093    if !settings.video_cbr_mode && settings.video_vbv_multiplier <= 0.0 {
2094        return (bitrate, 0);
2095    }
2096    let vbv = (crate::encoders::vbv_bits(
2097        (bitrate as u32).saturating_mul(1000),
2098        settings.target_fps,
2099        settings.keyframe_interval_s,
2100        settings.video_vbv_multiplier,
2101    ) / 1000)
2102        .max(1) as i32;
2103    (bitrate, vbv)
2104}
2105
2106/// Divide `height` into `n` contiguous stripes as `(y_start, stripe_height)`, with the split
2107/// rule differing by codec because only H.264 constrains stripe height.
2108///
2109/// - **JPEG**: JPEG has no vertical subsampling, so stripes may be any height; the heights differ
2110///   by at most one row — the first `remainder` stripes take one extra each — and every row of
2111///   the frame is covered.
2112/// - **Video**: 4:2:0 pairs chroma rows vertically, so every stripe height is forced even and the
2113///   remainder is handed out two rows at a time. The deliberate cost is that a single trailing
2114///   odd row may be left uncovered — preferable to an odd-height stripe the encoder cannot
2115///   represent.
2116fn compute_stripe_geometries(height: usize, n: usize, codec: Codec) -> Vec<(usize, usize)> {
2117    let mut geoms = Vec::with_capacity(n);
2118    let mut current_y = 0;
2119    if !codec.is_video() {
2120        let base_h = height / n;
2121        let remainder = height - base_h * n;
2122        for i in 0..n {
2123            let s_h = base_h + if i < remainder { 1 } else { 0 };
2124            geoms.push((current_y, s_h));
2125            current_y += s_h;
2126        }
2127    } else {
2128        let base_h = (height / n) & !1;
2129        let remainder = height - base_h * n;
2130        let stripes_with_extra = remainder / 2;
2131        for i in 0..n {
2132            let s_h = base_h + if i < stripes_with_extra { 2 } else { 0 };
2133            geoms.push((current_y, s_h));
2134            current_y += s_h;
2135        }
2136    }
2137    geoms
2138}
2139
2140#[cfg(test)]
2141mod stripe_count_tests {
2142    use super::{Codec, MAX_STRIPES, MIN_STRIPE_HEIGHT, stripe_count};
2143
2144    /// The count is what the client decodes, so a host with many cores cannot raise it: a 4K
2145    /// frame has room for thirty-three stripes at the minimum height and still gets at most
2146    /// `MAX_STRIPES`, on the striped video codec as much as on JPEG.
2147    #[test]
2148    fn a_tall_frame_is_not_cut_into_one_stripe_per_core() {
2149        for codec in [Codec::Jpeg, Codec::H264] {
2150            let n = stripe_count(2160, codec, false);
2151            assert!(
2152                n <= MAX_STRIPES,
2153                "{codec:?} cut a 4K frame into {n} stripes"
2154            );
2155            assert!(n >= 1, "{codec:?} cut a 4K frame into none");
2156        }
2157    }
2158
2159    /// A stripe still never falls below the minimum height, so a short frame is cut by its own
2160    /// height rather than by the ceiling.
2161    #[test]
2162    fn a_short_frame_is_cut_by_its_height() {
2163        assert_eq!(stripe_count(MIN_STRIPE_HEIGHT - 1, Codec::Jpeg, false), 1);
2164        assert!(stripe_count(MIN_STRIPE_HEIGHT * 2, Codec::Jpeg, false) <= 2);
2165    }
2166
2167    /// A full-frame video session carries one picture, and a codec that does not stripe never
2168    /// gets more than one whatever the height.
2169    #[test]
2170    fn a_whole_frame_is_one_stripe() {
2171        assert_eq!(stripe_count(2160, Codec::H264, true), 1);
2172        assert_eq!(stripe_count(2160, Codec::Av1, false), 1);
2173    }
2174}
2175
2176#[cfg(test)]
2177mod tests {
2178    /// The configured bitrate is a budget for the screen, not for each stripe: every stripe
2179    /// runs its own rate control, so what reaches an encoder is the budget over the number of
2180    /// stripes carrying motion. Dividing by the stripe count instead spends a fraction of the
2181    /// configured rate whenever only part of the screen moves, and dividing by nothing at all
2182    /// spends a multiple of it whenever the whole screen does.
2183    #[test]
2184    fn cbr_budget_is_split_across_the_stripes_carrying_it() {
2185        use crate::RustCaptureSettings;
2186        for &kbps in &[500i32, 4000, 8000, 20000] {
2187            let settings = RustCaptureSettings {
2188                video_cbr_mode: true,
2189                video_bitrate_kbps: kbps,
2190                ..Default::default()
2191            };
2192            for &n in &[1usize, 2, 4, 12, 64] {
2193                let (all, _) = super::stripe_rate_control(&settings, n as f32, n);
2194                let total = all * n as i32;
2195                assert!(
2196                    total <= kbps && kbps - total < n as i32,
2197                    "{n} stripes at {all} kbps must sum to the configured {kbps}"
2198                );
2199                let (one, _) = super::stripe_rate_control(&settings, 1.0, n);
2200                assert_eq!(one, kbps, "a lone moving stripe carries the whole budget");
2201                let (over, _) = super::stripe_rate_control(&settings, n as f32 * 4.0, n);
2202                assert_eq!(
2203                    over, all,
2204                    "the divisor never exceeds the stripes that exist"
2205                );
2206                let (under, _) = super::stripe_rate_control(&settings, 0.0, n);
2207                assert_eq!(under, kbps, "and never falls below one");
2208            }
2209            let (vbv_one, whole) = super::stripe_rate_control(&settings, 1.0, 8);
2210            let (_, share) = super::stripe_rate_control(&settings, 8.0, 8);
2211            assert_eq!(vbv_one, kbps);
2212            assert!(
2213                (share * 8 - whole).abs() <= 9,
2214                "each stripe's buffer is its share of the whole-screen one: {share}x8 vs {whole}"
2215            );
2216        }
2217    }
2218
2219    /// A CRF stripe is capped only where a VBV multiplier asks for it, with the whole-screen
2220    /// buffer that multiplier names at the configured rate as its peak; CBR always has one.
2221    #[test]
2222    fn crf_stripes_take_a_vbv_only_where_a_multiplier_asks() {
2223        use crate::RustCaptureSettings;
2224        let crf = |mult| RustCaptureSettings {
2225            video_bitrate_kbps: 8000,
2226            video_vbv_multiplier: mult,
2227            target_fps: 30.0,
2228            ..Default::default()
2229        };
2230        assert_eq!(super::stripe_rate_control(&crf(0.0), 1.0, 4), (8000, 0));
2231        assert_eq!(super::stripe_rate_control(&crf(12.0), 1.0, 4), (8000, 3200));
2232        assert_eq!(super::stripe_rate_control(&crf(12.0), 4.0, 4), (2000, 800));
2233        let cbr = RustCaptureSettings {
2234            video_cbr_mode: true,
2235            ..crf(0.0)
2236        };
2237        assert!(super::stripe_rate_control(&cbr, 1.0, 4).1 > 0);
2238    }
2239
2240    /// The divisor follows the screen rather than the configuration: full-screen motion moves
2241    /// it to the stripe count within a few frames, and it comes back down when the motion
2242    /// stops. A divisor recomputed per frame would swing between those two ends every frame,
2243    /// which leaves the encoder chasing a square wave and delivering less than either rate.
2244    #[test]
2245    fn the_budget_divisor_follows_motion_and_is_smoothed() {
2246        use crate::RustCaptureSettings;
2247        let (w, h) = (64, 512);
2248        let settings = RustCaptureSettings {
2249            width: w,
2250            height: h,
2251            codec: Codec::Jpeg,
2252            jpeg_quality: 40,
2253            use_paint_over_quality: false,
2254            ..Default::default()
2255        };
2256        let full = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
2257        let stripes_n = super::stripe_count(h, settings.codec, settings.video_fullframe);
2258        if stripes_n < 2 {
2259            return;
2260        }
2261        let mut stripes = Vec::new();
2262        let mut carrying = 1.0f32;
2263        for frame in 0..40u16 {
2264            let shade = 40u8.wrapping_add(frame.wrapping_mul(7) as u8);
2265            let px = vec![shade; (w * h * 4) as usize];
2266            super::encode_cpu(
2267                &mut stripes,
2268                &mut carrying,
2269                &px,
2270                w,
2271                h,
2272                &full,
2273                &settings,
2274                frame,
2275                false,
2276                false,
2277                false,
2278            );
2279        }
2280        assert!(
2281            carrying > stripes_n as f32 * 0.75,
2282            "full-screen motion must move the divisor toward the {stripes_n} stripes it uses, \
2283             not leave it at {carrying}"
2284        );
2285        let moved = carrying;
2286        // Motion that narrows to one corner narrows the divisor with it, so the budget
2287        // follows the stripes that are actually spending it. A frame with no motion at all
2288        // encodes nothing and carries nothing, so it leaves the divisor where it was.
2289        let band = [smithay::utils::Rectangle::new(
2290            (0, 0).into(),
2291            (w, 64).into(),
2292        )];
2293        for frame in 40..120u16 {
2294            let shade = 40u8.wrapping_add(frame.wrapping_mul(11) as u8);
2295            let mut px = vec![200u8; (w * h * 4) as usize];
2296            for byte in px.iter_mut().take((w * 64 * 4) as usize) {
2297                *byte = shade;
2298            }
2299            super::encode_cpu(
2300                &mut stripes,
2301                &mut carrying,
2302                &px,
2303                w,
2304                h,
2305                &band,
2306                &settings,
2307                frame,
2308                false,
2309                false,
2310                false,
2311            );
2312        }
2313        assert!(
2314            carrying < moved * 0.5,
2315            "motion in one stripe must bring the divisor back down: {carrying} vs {moved}"
2316        );
2317    }
2318    use super::{Codec, StripeState, compute_stripe_geometries};
2319
2320    /// Without `gpl` the striped H.264 path runs one OpenH264 instance per stripe and speaks
2321    /// the x264 stripes' protocol: the first frame emits every stripe as an IDR whose wire header
2322    /// carries that stripe's y-start and geometry, each stripe is an independently decodable
2323    /// stream (a decoder fed only that stripe's bytes yields a picture of the stripe's size), a
2324    /// static follow-up frame sends nothing, and motion confined to the top rows re-sends only
2325    /// the top stripe, as a delta frame.
2326    #[cfg(not(feature = "gpl"))]
2327    #[test]
2328    fn openh264_stripes_are_independent_streams() {
2329        use crate::RustCaptureSettings;
2330        use openh264::decoder::Decoder;
2331        use openh264::formats::YUVSource;
2332        let (w, h) = (128, 512);
2333        let settings = RustCaptureSettings {
2334            width: w,
2335            height: h,
2336            codec: Codec::H264,
2337            video_crf: 25,
2338            use_paint_over_quality: false,
2339            video_streaming_mode: false,
2340            ..Default::default()
2341        };
2342        let n = super::stripe_count(h, settings.codec, settings.video_fullframe);
2343        if n < 2 {
2344            return;
2345        }
2346        let mut stripes = Vec::new();
2347        let mut carrying = 1.0f32;
2348        let px: Vec<u8> = (0..(w * h * 4) as usize).map(|i| (i % 251) as u8).collect();
2349        let first = super::encode_cpu(
2350            &mut stripes,
2351            &mut carrying,
2352            &px,
2353            w,
2354            h,
2355            &[],
2356            &settings,
2357            0,
2358            false,
2359            true,
2360            false,
2361        );
2362        assert_eq!(first.len(), n, "every stripe is sent on the first frame");
2363        for (stripe, (y, sh)) in
2364            first
2365                .iter()
2366                .zip(compute_stripe_geometries(h as usize, n, Codec::H264))
2367        {
2368            let d = &stripe.data;
2369            assert_eq!(d[0], 0x04, "H.264 stripe tag");
2370            assert_eq!(d[1], 0x11, "first frame of a stripe is an H.264 key frame");
2371            assert_eq!(u16::from_be_bytes([d[2], d[3]]), 0, "frame number");
2372            assert_eq!(u16::from_be_bytes([d[4], d[5]]) as usize, y, "y-start");
2373            assert_eq!(u16::from_be_bytes([d[6], d[7]]) as i32, w, "width");
2374            assert_eq!(
2375                u16::from_be_bytes([d[8], d[9]]) as usize,
2376                sh,
2377                "stripe height"
2378            );
2379            assert_eq!(
2380                (
2381                    stripe.stripe_y_start as usize,
2382                    stripe.stripe_height as usize
2383                ),
2384                (y, sh)
2385            );
2386            let mut dec = Decoder::new().expect("decoder");
2387            let img = dec
2388                .decode(&d[crate::encoders::codec::VIDEO_HEADER_LEN..])
2389                .expect("decode")
2390                .expect("an IDR decodes on its own");
2391            assert_eq!(
2392                img.dimensions(),
2393                (w as usize, sh),
2394                "each stripe is its own stream"
2395            );
2396        }
2397        let quiet = super::encode_cpu(
2398            &mut stripes,
2399            &mut carrying,
2400            &px,
2401            w,
2402            h,
2403            &[],
2404            &settings,
2405            1,
2406            false,
2407            true,
2408            false,
2409        );
2410        assert!(quiet.is_empty(), "a static frame sends nothing");
2411        let mut moved = px.clone();
2412        for b in moved.iter_mut().take((w * 8 * 4) as usize) {
2413            *b = b.wrapping_add(97);
2414        }
2415        let top = super::encode_cpu(
2416            &mut stripes,
2417            &mut carrying,
2418            &moved,
2419            w,
2420            h,
2421            &[],
2422            &settings,
2423            2,
2424            false,
2425            true,
2426            false,
2427        );
2428        assert_eq!(
2429            top.len(),
2430            1,
2431            "motion in the top rows re-sends the top stripe alone"
2432        );
2433        assert_eq!(top[0].stripe_y_start, 0);
2434        assert_eq!(
2435            top[0].data[1], 0x10,
2436            "an unforced follow-up is an H.264 delta frame"
2437        );
2438        assert_eq!(
2439            u16::from_be_bytes([top[0].data[2], top[0].data[3]]),
2440            2,
2441            "frame number"
2442        );
2443    }
2444
2445    /// A stripe hashed elsewhere reads as one hashed here: the same changes, damage blocks and
2446    /// their renewal for the same sequence of hashes, whatever produced them.
2447    #[test]
2448    fn hash_dirty_reads_as_content_dirty() {
2449        let frames: Vec<Vec<u8>> = [1u8, 1, 2, 1, 1, 1, 1, 1, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7]
2450            .iter()
2451            .map(|&v| vec![v; 256])
2452            .collect();
2453        let (mut here, mut there) = (StripeState::default(), StripeState::default());
2454        for frame in &frames {
2455            assert_eq!(
2456                here.content_dirty(frame, 2, 3),
2457                there.hash_dirty(super::fast_hash(frame), 2, 3)
2458            );
2459            assert_eq!(here.in_damage_block, there.in_damage_block);
2460        }
2461    }
2462
2463    /// With `threshold = 2` and `duration = 3`, a first change reads dirty and two consecutive
2464    /// changes open a damage block that holds dirty for three frames without re-hashing; once content
2465    /// has gone static, the end-of-block re-hash exits the block and the stripe reads clean again.
2466    #[test]
2467    fn content_dirty_detects_change_and_damage_block() {
2468        let mut st = StripeState::default();
2469        let a = vec![1u8; 256];
2470        let b = vec![2u8; 256];
2471        assert!(st.content_dirty(&a, 2, 3));
2472        assert!(!st.content_dirty(&a, 2, 3));
2473        assert!(st.content_dirty(&b, 2, 3));
2474        assert!(st.content_dirty(&a, 2, 3));
2475        assert!(st.in_damage_block);
2476        assert!(st.content_dirty(&a, 2, 3));
2477        assert!(st.content_dirty(&a, 2, 3));
2478        assert!(st.content_dirty(&a, 2, 3));
2479        assert!(!st.in_damage_block);
2480        assert!(!st.content_dirty(&a, 2, 3));
2481
2482        // Motion through a whole block renews it; motion that stops inside one ends with it.
2483        let frames: Vec<Vec<u8>> = (0..12u8).map(|i| vec![i; 256]).collect();
2484        let mut st = StripeState::default();
2485        for f in &frames {
2486            assert!(st.content_dirty(f, 2, 3));
2487        }
2488        assert!(
2489            st.in_damage_block,
2490            "a region moving every frame stays in its block"
2491        );
2492        let still = frames.last().unwrap();
2493        let dirty: Vec<bool> = (0..6).map(|_| st.content_dirty(still, 2, 3)).collect();
2494        assert_eq!(
2495            dirty,
2496            [true, true, false, false, false, false],
2497            "clean once the block that saw it stop ends"
2498        );
2499    }
2500
2501    /// With compositor damage as the authority (Wayland), a clean frame must still advance the
2502    /// paint-over countdown and fire the repaint at the trigger, and once every stripe has
2503    /// latched (`paint_over_sent`) further clean frames must produce nothing — that quiescent
2504    /// tail is the idle fast path, which skips the stripe fan-out entirely.
2505    #[test]
2506    fn clean_frames_countdown_fire_paintover_then_go_quiescent() {
2507        use crate::RustCaptureSettings;
2508        let (w, h) = (64, 128);
2509        let pixels = vec![128u8; (w * h * 4) as usize];
2510        let settings = RustCaptureSettings {
2511            width: w,
2512            height: h,
2513            codec: Codec::Jpeg,
2514            jpeg_quality: 60,
2515            paint_over_jpeg_quality: 90,
2516            use_paint_over_quality: true,
2517            paint_over_trigger_frames: 5,
2518            ..Default::default()
2519        };
2520        let mut stripes = Vec::new();
2521        let mut carrying = 1.0f32;
2522        let full = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
2523        let dirty = super::encode_cpu(
2524            &mut stripes,
2525            &mut carrying,
2526            &pixels,
2527            w,
2528            h,
2529            &full,
2530            &settings,
2531            0,
2532            false,
2533            false,
2534            false,
2535        );
2536        assert!(!dirty.is_empty(), "damaged frame must encode");
2537
2538        let mut painted = Vec::new();
2539        for frame in 1..=20u16 {
2540            let out = super::encode_cpu(
2541                &mut stripes,
2542                &mut carrying,
2543                &pixels,
2544                w,
2545                h,
2546                &[],
2547                &settings,
2548                frame,
2549                false,
2550                false,
2551                false,
2552            );
2553            painted.extend(out.iter().map(|s| (frame, s.stripe_y_start)));
2554        }
2555        let per_frame = stripes.len().div_ceil(super::CLEANUP_STAGGER_FRAMES).max(1);
2556        assert_eq!(
2557            painted.len(),
2558            stripes.len(),
2559            "each stripe is painted over exactly once: {painted:?}"
2560        );
2561        assert_eq!(
2562            painted[0].0, settings.paint_over_trigger_frames as u16,
2563            "starting at the trigger"
2564        );
2565        for (n, (frame, _)) in painted.iter().enumerate() {
2566            assert_eq!(
2567                *frame as usize,
2568                settings.paint_over_trigger_frames as usize + n / per_frame,
2569                "{per_frame} a frame"
2570            );
2571        }
2572        assert!(
2573            stripes.iter().all(|st| st.paint_over_sent),
2574            "all stripes latched after the repaint"
2575        );
2576    }
2577
2578    /// Hash-damage sources (X11) take the sequential-scan fast path: static frames advance
2579    /// the countdown and fire the paint-over exactly once, the quiescent tail emits nothing,
2580    /// and a subsequent content change is still detected and encoded (streak state reset by
2581    /// the fast path must not swallow the wake-up).
2582    #[test]
2583    fn hash_scan_idles_after_paintover_and_wakes_on_change() {
2584        use crate::RustCaptureSettings;
2585        let (w, h) = (64, 128);
2586        let static_px = vec![128u8; (w * h * 4) as usize];
2587        let changed_px = vec![200u8; (w * h * 4) as usize];
2588        let settings = RustCaptureSettings {
2589            width: w,
2590            height: h,
2591            codec: Codec::Jpeg,
2592            jpeg_quality: 60,
2593            paint_over_jpeg_quality: 90,
2594            use_paint_over_quality: true,
2595            paint_over_trigger_frames: 5,
2596            damage_block_threshold: 10,
2597            damage_block_duration: 10,
2598            ..Default::default()
2599        };
2600        let mut stripes = Vec::new();
2601        let mut carrying = 1.0f32;
2602        let first = super::encode_cpu(
2603            &mut stripes,
2604            &mut carrying,
2605            &static_px,
2606            w,
2607            h,
2608            &[],
2609            &settings,
2610            0,
2611            false,
2612            true,
2613            false,
2614        );
2615        assert!(
2616            !first.is_empty(),
2617            "first frame hashes as changed and encodes"
2618        );
2619
2620        let mut painted = Vec::new();
2621        for frame in 1..=20u16 {
2622            let out = super::encode_cpu(
2623                &mut stripes,
2624                &mut carrying,
2625                &static_px,
2626                w,
2627                h,
2628                &[],
2629                &settings,
2630                frame,
2631                false,
2632                true,
2633                false,
2634            );
2635            painted.extend(out.iter().map(|s| (frame, s.stripe_y_start)));
2636        }
2637        assert_eq!(
2638            painted.len(),
2639            stripes.len(),
2640            "each stripe is painted over exactly once while static: {painted:?}"
2641        );
2642        assert_eq!(painted[0].0, settings.paint_over_trigger_frames as u16);
2643
2644        let woke = super::encode_cpu(
2645            &mut stripes,
2646            &mut carrying,
2647            &changed_px,
2648            w,
2649            h,
2650            &[],
2651            &settings,
2652            21,
2653            false,
2654            true,
2655            false,
2656        );
2657        assert!(!woke.is_empty(), "content change after idle must encode");
2658    }
2659
2660    /// A caret changing one stripe every fourth frame keeps it from ever holding still for the
2661    /// trigger, yet that stripe is cleaned up once the low-motion window passes, and the stripes
2662    /// around it are cleaned up at the trigger as if it were not there.
2663    #[test]
2664    fn a_blinking_caret_starves_no_stripe_of_its_cleanup() {
2665        use crate::RustCaptureSettings;
2666        let (w, h) = (64i32, 256i32);
2667        let settings = RustCaptureSettings {
2668            width: w,
2669            height: h,
2670            codec: Codec::Jpeg,
2671            jpeg_quality: 40,
2672            paint_over_jpeg_quality: 90,
2673            use_paint_over_quality: true,
2674            paint_over_trigger_frames: 5,
2675            damage_block_threshold: 10,
2676            damage_block_duration: 10,
2677            ..Default::default()
2678        };
2679        let mut stripes = Vec::new();
2680        let mut carrying = 1.0f32;
2681        let frame = |caret: bool| {
2682            let mut px = vec![128u8; (w * h * 4) as usize];
2683            if caret {
2684                px[(w * 4 * 3) as usize..(w * 4 * 3) as usize + 8].fill(0);
2685            }
2686            px
2687        };
2688        // The luma DC quantizer of a stripe's JPEG: 3 at the paint-over quality, 20 at the base one.
2689        let dc_quant = |data: &[u8]| {
2690            let at = data
2691                .windows(2)
2692                .position(|m| m == [0xFF, 0xDB])
2693                .expect("a quantization table");
2694            data[at + 5]
2695        };
2696        let mut cleaned = std::collections::HashMap::new();
2697        for n in 0..60u16 {
2698            let out = super::encode_cpu(
2699                &mut stripes,
2700                &mut carrying,
2701                &frame((n / 4) % 2 == 0),
2702                w,
2703                h,
2704                &[],
2705                &settings,
2706                n,
2707                false,
2708                true,
2709                false,
2710            );
2711            for s in &out {
2712                if dc_quant(&s.data) < 8 {
2713                    cleaned.entry(s.stripe_y_start).or_insert(n);
2714                }
2715            }
2716        }
2717        let rows = stripes.len();
2718        assert!(rows > 1, "the frame is striped");
2719        let caret_stripe = 0;
2720        let others: Vec<u16> = cleaned
2721            .iter()
2722            .filter(|(y, _)| **y != caret_stripe)
2723            .map(|(_, n)| *n)
2724            .collect();
2725        assert_eq!(
2726            others.len(),
2727            rows - 1,
2728            "every still stripe is cleaned up: {cleaned:?}"
2729        );
2730        assert!(
2731            others
2732                .iter()
2733                .all(|&n| n <= 5 + super::CLEANUP_STAGGER_FRAMES as u16),
2734            "at the trigger: {cleaned:?}"
2735        );
2736        let at = *cleaned
2737            .get(&caret_stripe)
2738            .expect("the caret's stripe is cleaned up too");
2739        assert!(
2740            (19..=23).contains(&at),
2741            "once the low-motion window passes: {at}"
2742        );
2743    }
2744
2745    /// Total rows covered by a geometry — the sum of all stripe heights.
2746    fn covered(geoms: &[(usize, usize)]) -> usize {
2747        geoms.iter().map(|&(_, h)| h).sum()
2748    }
2749
2750    /// Assert the stripes tile the frame with no gaps or overlap: each stripe's `y_start`
2751    /// equals the running sum of the preceding heights.
2752    fn assert_contiguous(geoms: &[(usize, usize)]) {
2753        let mut y = 0;
2754        for &(sy, sh) in geoms {
2755            assert_eq!(sy, y, "stripes must be contiguous");
2756            y += sh;
2757        }
2758    }
2759
2760    /// JPEG geometry covers the full frame height with contiguous stripes, across a range of
2761    /// heights (odd ones included) and stripe counts.
2762    #[test]
2763    fn jpeg_covers_every_row_including_odd() {
2764        for &h in &[1usize, 63, 720, 721, 1079, 1080, 1081] {
2765            for &n in &[1usize, 2, 3, 8, 16] {
2766                let g = compute_stripe_geometries(h, n, Codec::Jpeg);
2767                assert_eq!(g.len(), n);
2768                assert_eq!(
2769                    covered(&g),
2770                    h,
2771                    "JPEG must cover full height h={} n={}",
2772                    h,
2773                    n
2774                );
2775                assert_contiguous(&g);
2776            }
2777        }
2778    }
2779
2780    /// H.264 geometry yields even, contiguous stripe heights that cover the whole frame
2781    /// except at most one trailing odd row, across a range of heights and stripe counts.
2782    #[test]
2783    fn h264_stripes_even_and_within_bounds() {
2784        for &h in &[64usize, 720, 721, 1080, 1081] {
2785            for &n in &[1usize, 2, 8] {
2786                let g = compute_stripe_geometries(h, n, Codec::H264);
2787                assert_eq!(g.len(), n);
2788                for &(_, sh) in &g {
2789                    assert_eq!(
2790                        sh % 2,
2791                        0,
2792                        "H.264 stripe heights must be even h={} n={}",
2793                        h,
2794                        n
2795                    );
2796                }
2797                assert_contiguous(&g);
2798                assert!(covered(&g) <= h);
2799                assert!(
2800                    h - covered(&g) <= 1,
2801                    "at most one trailing odd row uncovered"
2802                );
2803            }
2804        }
2805    }
2806
2807    /// The host convert sites chroma at the center of the block on both its paths, the
2808    /// single-threaded one and the banded one, and a band boundary never splits a chroma pair.
2809    #[test]
2810    fn the_host_convert_sites_chroma_at_the_block_center() {
2811        use super::convert_to_yuv_mt;
2812        let (w, h) = (64usize, 64usize);
2813        let bgra = crate::encoders::chroma_siting::bgra(w, h);
2814        for bands in [1usize, 4, 7] {
2815            let (mut yp, mut up, mut vp) =
2816                (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
2817            convert_to_yuv_mt(
2818                &bgra,
2819                (w * 4) as u32,
2820                w,
2821                h,
2822                false,
2823                false,
2824                false,
2825                false,
2826                &mut yp,
2827                &mut up,
2828                &mut vp,
2829                (w, w / 2),
2830                bands,
2831            )
2832            .expect("convert");
2833            let worst = up
2834                .iter()
2835                .zip(&vp)
2836                .map(|(&u, &v)| (f64::from(u) - 128.0).hypot(f64::from(v) - 128.0))
2837                .fold(0.0f64, f64::max);
2838            assert!(
2839                worst <= 2.0,
2840                "{bands} bands: chroma sits {worst:.1} off neutral"
2841            );
2842        }
2843    }
2844
2845    /// The JPEG stripes a WebSockets session sends by default hand BGRA to libjpeg-turbo, which
2846    /// subsamples chroma itself, so the tile is held to the same neutral chroma there.
2847    #[test]
2848    fn the_jpeg_path_sites_chroma_at_the_block_center() {
2849        use super::Codec;
2850        use crate::webcam::decode::new_decoder;
2851        let (w, h) = (64usize, 64usize);
2852        let bgra = crate::encoders::chroma_siting::bgra(w, h);
2853        let mut comp = turbojpeg::Compressor::new().expect("turbojpeg compressor");
2854        comp.set_quality(90).expect("quality");
2855        let img = turbojpeg::Image {
2856            pixels: &bgra[..],
2857            width: w,
2858            pitch: w * 4,
2859            height: h,
2860            format: turbojpeg::PixelFormat::BGRA,
2861        };
2862        let jpeg = comp.compress_to_vec(img).expect("compress");
2863        let mut dec = new_decoder(Codec::Jpeg).expect("jpeg decoder");
2864        assert!(
2865            dec.decode(&jpeg).expect("decode"),
2866            "the stripe decoded nothing"
2867        );
2868        let worst = crate::encoders::chroma_siting::worst(&dec.frame().expect("frame"));
2869        assert!(worst <= 4.0, "JPEG chroma sits {worst:.1} off neutral");
2870    }
2871
2872    /// The 10-bit conversion against the BT.709 arithmetic it implements, at both ranges and
2873    /// both chroma formats: every sample within one code value, a 4:2:0 chroma sample being
2874    /// the conversion of its block's mean, and the same picture whatever the band count.
2875    #[test]
2876    fn ten_bit_conversion_matches_bt709() {
2877        use super::convert_to_yuv10_mt;
2878        let (w, h) = (34usize, 18usize);
2879        let mut bgra = vec![0u8; w * h * 4];
2880        let mut seed = 0x2545_f491u32;
2881        for px in bgra.as_chunks_mut::<4>().0 {
2882            for c in px.iter_mut().take(3) {
2883                seed ^= seed << 13;
2884                seed ^= seed >> 17;
2885                seed ^= seed << 5;
2886                *c = (seed >> 11) as u8;
2887            }
2888            px[3] = 255;
2889        }
2890        let reference = |r: f64, g: f64, b: f64, full: bool| {
2891            let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
2892            let (cb, cr) = ((b - y) / 1.8556, (r - y) / 1.5748);
2893            if full {
2894                (
2895                    y * 1023.0 / 255.0,
2896                    512.0 + cb * 1023.0 / 255.0,
2897                    512.0 + cr * 1023.0 / 255.0,
2898                )
2899            } else {
2900                (
2901                    64.0 + y * 876.0 / 255.0,
2902                    512.0 + cb * 896.0 / 255.0,
2903                    512.0 + cr * 896.0 / 255.0,
2904                )
2905            }
2906        };
2907        let rgb = |x: usize, y: usize| {
2908            let p = &bgra[(y * w + x) * 4..];
2909            (p[2] as f64, p[1] as f64, p[0] as f64)
2910        };
2911        for full in [false, true] {
2912            for i444 in [false, true] {
2913                let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
2914                let convert = |bands: usize| {
2915                    let (mut y, mut u, mut v) =
2916                        (vec![0u16; w * h], vec![0u16; cw * ch], vec![0u16; cw * ch]);
2917                    convert_to_yuv10_mt(
2918                        &bgra,
2919                        w * 4,
2920                        w,
2921                        h,
2922                        false,
2923                        i444,
2924                        full,
2925                        &mut y,
2926                        &mut u,
2927                        &mut v,
2928                        (w, cw),
2929                        bands,
2930                    );
2931                    (y, u, v)
2932                };
2933                let (y, u, v) = convert(1);
2934                assert_eq!(
2935                    (y.clone(), u.clone(), v.clone()),
2936                    convert(4),
2937                    "bands change nothing"
2938                );
2939                for row in 0..h {
2940                    for col in 0..w {
2941                        let (r, g, b) = rgb(col, row);
2942                        let want = reference(r, g, b, full).0;
2943                        assert!((y[row * w + col] as f64 - want).abs() <= 1.0, "luma");
2944                    }
2945                }
2946                for row in 0..ch {
2947                    for col in 0..cw {
2948                        let (r, g, b) = if i444 {
2949                            rgb(col, row)
2950                        } else {
2951                            let mut sum = (0.0, 0.0, 0.0);
2952                            for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
2953                                let p = rgb(2 * col + dx, 2 * row + dy);
2954                                sum = (sum.0 + p.0 / 4.0, sum.1 + p.1 / 4.0, sum.2 + p.2 / 4.0);
2955                            }
2956                            sum
2957                        };
2958                        let (_, cb, cr) = reference(r, g, b, full);
2959                        assert!((u[row * cw + col] as f64 - cb).abs() <= 1.0, "cb");
2960                        assert!((v[row * cw + col] as f64 - cr).abs() <= 1.0, "cr");
2961                    }
2962                }
2963            }
2964        }
2965    }
2966
2967    /// A 10-bit x264 session takes and reports quantizers on the scale an 8-bit one does: the
2968    /// rate factor's first frame reads the same quantizer at either depth, and a key frame held
2969    /// at a coarse quantizer comes out as small.
2970    #[cfg(feature = "gpl")]
2971    #[test]
2972    fn x264_quantizers_keep_their_scale_at_ten_bits() {
2973        use super::H264EncoderWrapper;
2974        if !H264EncoderWrapper::ten_bit() {
2975            println!("this libx264 was built for 8 bits alone; nothing to check");
2976            return;
2977        }
2978        let (w, h) = (640usize, 360usize);
2979        let mut seed = 7u32;
2980        let mut noise = |n: usize, amplitude: u32, base: u32| -> Vec<u32> {
2981            (0..n)
2982                .map(|_| {
2983                    seed ^= seed << 13;
2984                    seed ^= seed >> 17;
2985                    seed ^= seed << 5;
2986                    base + seed % amplitude
2987                })
2988                .collect()
2989        };
2990        let (luma, chroma) = (noise(w * h, 60, 90), noise(w * h / 4, 20, 118));
2991        let mut seen = Vec::new();
2992        for depth in [8u32, 10] {
2993            let plane = |samples: &[u32]| -> Vec<u8> {
2994                if depth == 10 {
2995                    samples
2996                        .iter()
2997                        .flat_map(|s| ((s * 4) as u16).to_le_bytes())
2998                        .collect()
2999                } else {
3000                    samples.iter().map(|s| *s as u8).collect()
3001                }
3002            };
3003            let (y, c) = (plane(&luma), plane(&chroma));
3004            let bytes = (depth as usize).div_ceil(8);
3005            let mut enc = H264EncoderWrapper::with_depth(
3006                w as i32, h as i32, 25, false, depth, 30.0, 1, false, 0, 0, 0, 0, 0,
3007            )
3008            .expect("x264 init");
3009            let encode = |enc: &mut H264EncoderWrapper, id: u16| {
3010                let mut out = Vec::new();
3011                assert!(enc.encode_with_headers(
3012                    &y,
3013                    &c,
3014                    &c,
3015                    (w * bytes) as i32,
3016                    (w / 2 * bytes) as i32,
3017                    (w / 2 * bytes) as i32,
3018                    id,
3019                    0,
3020                    true,
3021                    true,
3022                    &mut out,
3023                ));
3024                out.len()
3025            };
3026            encode(&mut enc, 0);
3027            let rate_factor_qp = enc.last_qp().expect("a quantizer");
3028            enc.hold_quantizer(45);
3029            seen.push((rate_factor_qp, encode(&mut enc, 1)));
3030        }
3031        let ((qp8, held8), (qp10, held10)) = (seen[0], seen[1]);
3032        assert!(
3033            qp8.abs_diff(qp10) <= 2,
3034            "rate factor quantizers {qp8} and {qp10}"
3035        );
3036        assert!(
3037            held10 < 2 * held8,
3038            "held key frames of {held8} and {held10} bytes"
3039        );
3040    }
3041
3042    /// A 10-bit x264 session declares High 10, or High 4:4:4 Predictive, and codes the
3043    /// 16-bit planes it is handed.
3044    #[cfg(feature = "gpl")]
3045    #[test]
3046    fn x264_codes_ten_bits() {
3047        use super::H264EncoderWrapper;
3048        if !H264EncoderWrapper::ten_bit() {
3049            println!("this libx264 was built for 8 bits alone; nothing to check");
3050            return;
3051        }
3052        let (w, h) = (64usize, 64usize);
3053        for (i444, profile) in [(false, 110u8), (true, 244u8)] {
3054            let mut enc = H264EncoderWrapper::with_depth(
3055                w as i32, h as i32, 25, i444, 10, 30.0, 1, false, 0, 0, 0, 0, 0,
3056            )
3057            .expect("x264 init");
3058            assert_eq!(enc.bit_depth, 10);
3059            let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
3060            let planes = [
3061                vec![400u16; w * h],
3062                vec![512u16; cw * ch],
3063                vec![600u16; cw * ch],
3064            ];
3065            let bytes = |p: &[u16]| unsafe {
3066                std::slice::from_raw_parts(p.as_ptr().cast::<u8>(), p.len() * 2).to_vec()
3067            };
3068            let (y, u, v) = (bytes(&planes[0]), bytes(&planes[1]), bytes(&planes[2]));
3069            let mut out = Vec::new();
3070            assert!(enc.encode_with_headers(
3071                &y,
3072                &u,
3073                &v,
3074                (w * 2) as i32,
3075                (cw * 2) as i32,
3076                (cw * 2) as i32,
3077                0,
3078                0,
3079                true,
3080                true,
3081                &mut out,
3082            ));
3083            let sps = crate::encoders::codec::annexb_nals(&out)
3084                .find(|n| n[0] & 0x1f == 7)
3085                .expect("a sequence parameter set");
3086            assert_eq!(sps[1], profile, "the profile declared");
3087        }
3088    }
3089}
3090
3091#[cfg(test)]
3092mod qp_bound_sweep {
3093    //! Invariants under test: the CBR QP clamp reaches libx264/OpenH264 (a max clamp must
3094    //! raise worst-case fidelity on rate-starved text at the cost of bitrate overshoot;
3095    //! a min clamp must cut spend on over-budgeted content) and defaults (0) leave the
3096    //! encoders' own behavior untouched. Each encoder is swept separately: the OpenH264
3097    //! sweep runs in every build (the crate is a dev-dependency), the x264 one needs `gpl`.
3098    #[cfg(feature = "gpl")]
3099    use super::H264EncoderWrapper;
3100    use crate::RustCaptureSettings;
3101    use crate::encoders::Codec;
3102    use crate::encoders::oh264::Openh264Encoder;
3103    use openh264::decoder::Decoder;
3104    use openh264::formats::YUVSource;
3105
3106    const W: usize = 1280;
3107    const H: usize = 720;
3108    const FRAMES: usize = 60;
3109
3110    /// Build a scrolling terminal-like luma frame: an 8x12 glyph grid seeded by an LCG and
3111    /// scrolled 4 px per frame — the worst case for screen-share rate control, with dense
3112    /// high-contrast detail (~40% lit pixels per glyph row) under full-frame motion.
3113    fn text_luma(frame: usize) -> Vec<u8> {
3114        let mut y = vec![18u8; W * H];
3115        let scroll = frame * 4;
3116        for row in 0..H {
3117            let srow = row + scroll;
3118            let cell_y = srow / 12;
3119            let in_glyph_y = srow % 12;
3120            if in_glyph_y >= 10 {
3121                continue;
3122            }
3123            for col in 0..W {
3124                let cell_x = col / 8;
3125                let in_glyph_x = col % 8;
3126                if in_glyph_x >= 7 {
3127                    continue;
3128                }
3129                let mut s = (cell_x as u32)
3130                    .wrapping_mul(2654435761)
3131                    .wrapping_add((cell_y as u32).wrapping_mul(40503))
3132                    .wrapping_add(1);
3133                s ^= s << 13;
3134                s ^= s >> 17;
3135                s ^= s << 5;
3136                if (s >> ((in_glyph_y * 3 + in_glyph_x) % 29)) & 1 == 1 {
3137                    y[row * W + col] = 224;
3138                }
3139            }
3140        }
3141        y
3142    }
3143
3144    /// Encode `FRAMES` scrolling-text luma frames through the x264 stripe encoder at the
3145    /// given rate-control settings (constant gray chroma), returning each frame's raw bitstream.
3146    #[cfg(feature = "gpl")]
3147    fn encode_x264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
3148        let mut enc = H264EncoderWrapper::new(
3149            W as i32, H as i32, crf, false, 60.0, 4, cbr, kbps, 50, min_qp, max_qp,
3150        )
3151        .expect("x264 init");
3152        let u = vec![128u8; (W / 2) * (H / 2)];
3153        let v = vec![128u8; (W / 2) * (H / 2)];
3154        (0..FRAMES)
3155            .map(|i| {
3156                let y = text_luma(i);
3157                let mut out = Vec::new();
3158                enc.encode_with_headers(
3159                    &y,
3160                    &u,
3161                    &v,
3162                    W as i32,
3163                    (W / 2) as i32,
3164                    (W / 2) as i32,
3165                    i as u16,
3166                    0,
3167                    i == 0,
3168                    true,
3169                    &mut out,
3170                );
3171                out
3172            })
3173            .collect()
3174    }
3175
3176    /// The x264 stream declares the signal its input was converted with: the BT.709 matrix in
3177    /// both chroma formats, at full range for I444 and limited for I420.
3178    #[cfg(feature = "gpl")]
3179    #[test]
3180    fn x264_declares_the_conversion_matrix() {
3181        use crate::encoders::codec::annexb_nals;
3182        use crate::encoders::sps::read_color;
3183        use crate::webcam::decode::{ColorTags, Decoder as _, VideoDecoder};
3184        for (i444, want) in [
3185            (false, ColorTags::BT709_LIMITED),
3186            (true, ColorTags::BT709_FULL),
3187        ] {
3188            let (w, h) = (128usize, 96usize);
3189            let mut enc =
3190                H264EncoderWrapper::new(w as i32, h as i32, 25, i444, 30.0, 1, false, 0, 0, 0, 0)
3191                    .expect("x264 init");
3192            let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
3193            let y = vec![90u8; w * h];
3194            let u = vec![128u8; cw * ch];
3195            let v = vec![160u8; cw * ch];
3196            let mut out = Vec::new();
3197            assert!(enc.encode_with_headers(
3198                &y, &u, &v, w as i32, cw as i32, cw as i32, 0, 0, true, true, &mut out
3199            ));
3200            let sps = annexb_nals(&out)
3201                .find(|n| n[0] & 0x1f == 7)
3202                .expect("an SPS");
3203            let declared = read_color(sps).map(|s| ColorTags {
3204                matrix: s.matrix,
3205                full_range: s.full_range,
3206            });
3207            assert_eq!(declared, Some(want), "i444={i444}");
3208            if !i444 {
3209                let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
3210                assert!(dec.decode(&out).expect("decode"));
3211                assert_eq!(dec.color_tags(), Some(want));
3212            }
3213        }
3214    }
3215
3216    /// x264 is opened at the capture's rate as the fraction it names, its SPS declares that
3217    /// rate, and a session moved to another rate reopens at it.
3218    #[cfg(feature = "gpl")]
3219    #[test]
3220    fn x264_takes_the_frame_rate_as_its_fraction() {
3221        use crate::encoders::frame_rate::FrameRate;
3222        use crate::encoders::sps::h264_timing;
3223        let (w, h) = (64usize, 64usize);
3224        let (y, uv) = (vec![90u8; w * h], vec![128u8; w * h / 4]);
3225        for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
3226            let mut enc = H264EncoderWrapper::new(
3227                w as i32,
3228                h as i32,
3229                25,
3230                false,
3231                num as f64 / den as f64,
3232                1,
3233                true,
3234                4000,
3235                100,
3236                0,
3237                0,
3238            )
3239            .expect("x264 init");
3240            let param = unsafe {
3241                let mut param: x264_sys::x264_param_t = std::mem::zeroed();
3242                x264_sys::x264_encoder_parameters(enc.encoder, &mut param);
3243                param
3244            };
3245            assert_eq!((param.i_fps_num, param.i_fps_den), (num, den));
3246            let mut out = Vec::new();
3247            assert!(enc.encode_with_headers(
3248                &y,
3249                &uv,
3250                &uv,
3251                w as i32,
3252                (w / 2) as i32,
3253                (w / 2) as i32,
3254                0,
3255                0,
3256                true,
3257                true,
3258                &mut out
3259            ));
3260            assert_eq!(
3261                h264_timing(&out),
3262                Some((den, 2 * num)),
3263                "{num}/{den}: the SPS declares the rate"
3264            );
3265        }
3266        let mut enc = H264EncoderWrapper::new(
3267            w as i32, h as i32, 25, false, 60.0, 1, true, 4000, 100, 0, 0,
3268        )
3269        .expect("x264 init");
3270        enc.reconfigure_rate(4000, 100, 60000.0 / 1001.0);
3271        assert_eq!(
3272            enc.current_fps,
3273            FrameRate {
3274                num: 60000,
3275                den: 1001
3276            },
3277            "60 fps moved to 59.94"
3278        );
3279    }
3280
3281    /// The color chart, converted by the host path and encoded by x264, decodes back to the
3282    /// color that was painted when the BT.709 the stream declares is inverted — the check a
3283    /// client's presentation path performs on every frame, here with no browser in the way.
3284    #[cfg(feature = "gpl")]
3285    #[test]
3286    fn x264_paints_the_chart_it_converts() {
3287        use super::convert_to_yuv_mt;
3288        use crate::encoders::chroma_siting::{BT709, chart_bgra, chart_error};
3289        use crate::webcam::decode::{Decoder as _, VideoDecoder};
3290        let (w, h) = (256usize, 128usize);
3291        let bgra = chart_bgra(w, h);
3292        let (mut y, mut u, mut v) = (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
3293        convert_to_yuv_mt(
3294            &bgra,
3295            (w * 4) as u32,
3296            w,
3297            h,
3298            false,
3299            false,
3300            false,
3301            false,
3302            &mut y,
3303            &mut u,
3304            &mut v,
3305            (w, w / 2),
3306            4,
3307        )
3308        .expect("convert");
3309        let mut enc =
3310            H264EncoderWrapper::new(w as i32, h as i32, 20, false, 30.0, 1, false, 0, 0, 0, 0)
3311                .expect("x264 init");
3312        let mut out = Vec::new();
3313        assert!(enc.encode_with_headers(
3314            &y,
3315            &u,
3316            &v,
3317            w as i32,
3318            (w / 2) as i32,
3319            (w / 2) as i32,
3320            0,
3321            0,
3322            true,
3323            true,
3324            &mut out
3325        ));
3326        let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
3327        assert!(dec.decode(&out).expect("decode"));
3328        let worst = chart_error(&dec.frame().expect("frame"), BT709);
3329        println!("[chart] x264: worst |dRGB| {worst:.1}");
3330        assert!(
3331            worst <= 12.0,
3332            "the software H.264 path paints {worst:.1} off the chart"
3333        );
3334    }
3335
3336    /// Every x264 session bounds reordering at zero, 4:2:0 and 4:4:4, at a constant quantizer
3337    /// and a constant rate: the full-frame session is one stripe of the same machinery.
3338    #[cfg(feature = "gpl")]
3339    #[test]
3340    fn x264_bounds_reordering_at_zero() {
3341        use crate::encoders::sps::fixtures::assert_no_reorder;
3342        for (fullcolor, cbr) in [(false, false), (false, true), (true, false), (true, true)] {
3343            let mut enc = H264EncoderWrapper::new(
3344                W as i32, H as i32, 20, fullcolor, 60.0, 4, cbr, 8000, 0, 0, 0,
3345            )
3346            .expect("x264 init");
3347            let chroma = if fullcolor { W * H } else { W * H / 4 };
3348            let (u, v) = (vec![128u8; chroma], vec![128u8; chroma]);
3349            let stride = if fullcolor { W } else { W / 2 } as i32;
3350            let mut out = Vec::new();
3351            assert!(enc.encode_with_headers(
3352                &text_luma(0),
3353                &u,
3354                &v,
3355                W as i32,
3356                stride,
3357                stride,
3358                0,
3359                0,
3360                true,
3361                true,
3362                &mut out
3363            ));
3364            assert_no_reorder(&out, &format!("x264 fullcolor {fullcolor} cbr {cbr}"));
3365        }
3366    }
3367
3368    /// A CRF session given a VBV codes its key frame within the buffer, where the same session
3369    /// without one codes it at whatever size the rate factor asks.
3370    #[cfg(feature = "gpl")]
3371    #[test]
3372    fn x264_crf_bounds_a_key_frame_by_its_vbv() {
3373        const VBV_KBIT: i32 = 1500;
3374        let key = |vbv_kbit: i32| {
3375            let mut enc = H264EncoderWrapper::new(
3376                W as i32, H as i32, 12, false, 60.0, 4, false, 8000, vbv_kbit, 0, 0,
3377            )
3378            .expect("x264 init");
3379            let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3380            let mut out = Vec::new();
3381            assert!(enc.encode_with_headers(
3382                &text_luma(0),
3383                &u,
3384                &v,
3385                W as i32,
3386                (W / 2) as i32,
3387                (W / 2) as i32,
3388                0,
3389                0,
3390                true,
3391                true,
3392                &mut out
3393            ));
3394            out.len() * 8
3395        };
3396        let (uncapped, capped) = (key(0), key(VBV_KBIT));
3397        assert!(
3398            capped <= VBV_KBIT as usize * 1000,
3399            "a {capped}-bit key frame overflows the {VBV_KBIT} kbit buffer"
3400        );
3401        assert!(
3402            uncapped > 2 * capped,
3403            "the uncapped key frame ({uncapped} bits) is not the rate factor's own"
3404        );
3405    }
3406
3407    /// A capped CRF session its content starves overshoots the buffer rather than code past
3408    /// quantizer 51, where x264 forces skips that leave rows frozen on old content.
3409    #[cfg(feature = "gpl")]
3410    #[test]
3411    fn x264_capped_crf_codes_no_quantizer_past_51() {
3412        let mut enc =
3413            H264EncoderWrapper::new(W as i32, H as i32, 23, false, 60.0, 4, false, 300, 10, 0, 0)
3414                .expect("x264 init");
3415        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3416        let mut worst = 0;
3417        for i in 0..FRAMES {
3418            let mut out = Vec::new();
3419            enc.encode_with_headers(
3420                &text_luma(i),
3421                &u,
3422                &v,
3423                W as i32,
3424                (W / 2) as i32,
3425                (W / 2) as i32,
3426                i as u16,
3427                0,
3428                i == 0,
3429                true,
3430                &mut out,
3431            );
3432            worst = worst.max(enc.last_qp().unwrap_or(0));
3433        }
3434        assert!(worst <= 51, "a starved capped CRF coded quantizer {worst}");
3435    }
3436
3437    /// A CRF session capped or uncapped live codes its next key frame by the new setting.
3438    #[cfg(feature = "gpl")]
3439    #[test]
3440    fn x264_crf_takes_a_cap_set_or_lifted_live() {
3441        const VBV_KBIT: i32 = 1500;
3442        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3443        let mut enc =
3444            H264EncoderWrapper::new(W as i32, H as i32, 12, false, 60.0, 4, false, 8000, 0, 0, 0)
3445                .expect("x264 init");
3446        let key = |enc: &mut H264EncoderWrapper| {
3447            let mut out = Vec::new();
3448            assert!(enc.encode_with_headers(
3449                &text_luma(0),
3450                &u,
3451                &v,
3452                W as i32,
3453                (W / 2) as i32,
3454                (W / 2) as i32,
3455                0,
3456                0,
3457                true,
3458                true,
3459                &mut out
3460            ));
3461            out.len() * 8
3462        };
3463        let uncapped = key(&mut enc);
3464        enc.reconfigure_rate(8000, VBV_KBIT, 60.0);
3465        let capped = key(&mut enc);
3466        enc.reconfigure_rate(8000, 0, 60.0);
3467        let lifted = key(&mut enc);
3468        assert!(
3469            capped <= VBV_KBIT as usize * 1000,
3470            "a {capped}-bit key frame after capping overflows the {VBV_KBIT} kbit buffer ({uncapped} bits before)"
3471        );
3472        assert!(
3473            lifted > 2 * capped,
3474            "the key frame after lifting the cap ({lifted} bits) is still capped"
3475        );
3476    }
3477
3478    /// A frame a client lost is left out of the predictions: the next frame predicts from the
3479    /// newest frame before it and names it, a decoder that never saw the lost frames decodes it
3480    /// as one that saw everything does, and a loss the window no longer covers becomes a key
3481    /// frame. The stream declares the decoded picture buffer this needs.
3482    #[cfg(feature = "gpl")]
3483    #[test]
3484    fn x264_predicts_past_a_lost_frame() {
3485        use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
3486        use crate::encoders::reference::{REFERENCE_FRAMES, Reference};
3487        use crate::encoders::sps::h264_max_num_ref_frames;
3488        use crate::webcam::decode::{Decoder as _, VideoDecoder};
3489        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3490        let mut enc =
3491            H264EncoderWrapper::new(W as i32, H as i32, 20, false, 60.0, 4, false, 0, 0, 0, 0)
3492                .expect("x264 init");
3493        let encode = |enc: &mut H264EncoderWrapper, i: usize| {
3494            let y = text_luma(i);
3495            let mut out = Vec::new();
3496            assert!(enc.encode_with_headers(
3497                &y,
3498                &u,
3499                &v,
3500                W as i32,
3501                (W / 2) as i32,
3502                (W / 2) as i32,
3503                i as u16,
3504                0,
3505                i == 0,
3506                true,
3507                &mut out
3508            ));
3509            (out, enc.last_reference())
3510        };
3511        let mut frames: Vec<Vec<u8>> = Vec::new();
3512        for i in 0..8 {
3513            let (out, reference) = encode(&mut enc, i);
3514            assert_eq!(
3515                reference,
3516                if i == 0 {
3517                    Reference::None
3518                } else {
3519                    Reference::Frame(i as u16 - 1)
3520                }
3521            );
3522            frames.push(out);
3523        }
3524        assert_eq!(
3525            h264_max_num_ref_frames(&frames[0]),
3526            Some(REFERENCE_FRAMES),
3527            "the SPS declares the DPB"
3528        );
3529        // Frame 5 is reported lost once 6 and 7 have gone out.
3530        assert!(enc.invalidate_reference(5));
3531        let (out, reference) = encode(&mut enc, 8);
3532        assert_eq!(reference, Reference::Frame(4));
3533        assert_eq!(h264_frame_type(&out), FRAME_DELTA);
3534        frames.push(out);
3535        let (out, reference) = encode(&mut enc, 9);
3536        assert_eq!(reference, Reference::Frame(8));
3537        frames.push(out);
3538        let (mut whole, mut lossy) = (
3539            VideoDecoder::new(Codec::H264).unwrap(),
3540            VideoDecoder::new(Codec::H264).unwrap(),
3541        );
3542        for (i, f) in frames.iter().enumerate() {
3543            assert!(whole.decode(f).expect("decode"), "frame {i}");
3544            if !(5..8).contains(&i) {
3545                assert!(lossy.decode(f).expect("decode without 5-7"), "frame {i}");
3546            }
3547        }
3548        let apart = luma_distance(&whole.frame().unwrap(), &lossy.frame().unwrap());
3549        assert!(
3550            apart < 0.5,
3551            "the decoder that lost frames 5-7 shows frame 9 {apart:.2} off the one that saw them"
3552        );
3553        let source = text_luma(9);
3554        let off = lossy
3555            .frame()
3556            .unwrap()
3557            .y
3558            .chunks(lossy.frame().unwrap().y_stride)
3559            .take(H)
3560            .zip(source.chunks(W))
3561            .flat_map(|(row, src)| {
3562                row[..W]
3563                    .iter()
3564                    .zip(src)
3565                    .map(|(&a, &b)| (a as f64 - b as f64).abs())
3566            })
3567            .sum::<f64>()
3568            / (W * H) as f64;
3569        assert!(
3570            off < 6.0,
3571            "frame 9 decoded without frames 5-7 is {off:.2} off the picture painted"
3572        );
3573        for i in 10..20 {
3574            frames.push(encode(&mut enc, i).0);
3575        }
3576        // Frame 9 has left an 8-frame window holding 12..19, so every reference predicts
3577        // through it: the next frame is a key frame.
3578        assert!(enc.invalidate_reference(9));
3579        let (out, reference) = encode(&mut enc, 20);
3580        assert_eq!(reference, Reference::None);
3581        assert_eq!(h264_frame_type(&out), FRAME_KEY);
3582    }
3583
3584    /// A session told to keep one reference frame (`video_reference_frames`) declares a decoded
3585    /// picture buffer of one, and a loss of the frame before the last is a key frame.
3586    #[cfg(feature = "gpl")]
3587    #[test]
3588    fn x264_keeps_the_reference_frames_it_is_given() {
3589        use crate::encoders::codec::{FRAME_KEY, h264_frame_type};
3590        use crate::encoders::sps::h264_max_num_ref_frames;
3591        let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3592        let mut enc = H264EncoderWrapper::with_depth(
3593            W as i32, H as i32, 20, false, 8, 60.0, 1, false, 0, 0, 0, 0, 1,
3594        )
3595        .expect("x264 init");
3596        let mut frames = Vec::new();
3597        for i in 0..4usize {
3598            let mut out = Vec::new();
3599            assert!(enc.encode_with_headers(
3600                &text_luma(i),
3601                &u,
3602                &v,
3603                W as i32,
3604                (W / 2) as i32,
3605                (W / 2) as i32,
3606                i as u16,
3607                0,
3608                i == 0,
3609                true,
3610                &mut out
3611            ));
3612            frames.push(out);
3613        }
3614        assert_eq!(h264_max_num_ref_frames(&frames[0]), Some(1));
3615        assert!(enc.invalidate_reference(3));
3616        let mut out = Vec::new();
3617        assert!(enc.encode_with_headers(
3618            &text_luma(4),
3619            &u,
3620            &v,
3621            W as i32,
3622            (W / 2) as i32,
3623            (W / 2) as i32,
3624            4,
3625            0,
3626            false,
3627            true,
3628            &mut out
3629        ));
3630        assert_eq!(h264_frame_type(&out), FRAME_KEY, "nothing older is held");
3631    }
3632
3633    /// x264 counts `frame_num` in sixteen values and the stream in 4096 (`WideFrameNum`): a loss
3634    /// covering frame 16, where x264's own count wraps, is predicted past, and one covering frame
3635    /// 4096 is answered with a key frame, since past a gap across the stream's wrap FFmpeg's
3636    /// decoder drops about a range of pictures.
3637    #[test]
3638    #[cfg(feature = "gpl")]
3639    fn x264_answers_a_loss_at_the_frame_num_wrap_with_a_key_frame() {
3640        use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
3641        use crate::encoders::reference::Reference;
3642        use crate::encoders::sps::h264_frame_num_range;
3643        use crate::webcam::decode::{Decoder as _, VideoDecoder};
3644        let (w, h) = (160usize, 96usize);
3645        let (u, v) = (vec![128u8; w * h / 4], vec![128u8; w * h / 4]);
3646        let mut enc =
3647            H264EncoderWrapper::new(w as i32, h as i32, 20, false, 60.0, 1, false, 0, 0, 0, 0)
3648                .expect("x264 init");
3649        let encode = |enc: &mut H264EncoderWrapper, i: usize| {
3650            let y: Vec<u8> = (0..w * h)
3651                .map(|p| ((p % w + p / w + 3 * i) * 5 % 256) as u8)
3652                .collect();
3653            let mut out = Vec::new();
3654            assert!(enc.encode_with_headers(
3655                &y,
3656                &u,
3657                &v,
3658                w as i32,
3659                (w / 2) as i32,
3660                (w / 2) as i32,
3661                i as u16,
3662                0,
3663                i == 0,
3664                true,
3665                &mut out
3666            ));
3667            (out, enc.last_reference())
3668        };
3669        let (mut whole, mut lossy) = (
3670            VideoDecoder::new(Codec::H264).unwrap(),
3671            VideoDecoder::new(Codec::H264).unwrap(),
3672        );
3673        let (first, _) = encode(&mut enc, 0);
3674        let range = h264_frame_num_range(&first).expect("the key frame carries the SPS") as usize;
3675        assert_eq!(range, 4096, "x264's sixteen values, a byte wider");
3676        assert!(whole.decode(&first).expect("decode") && lossy.decode(&first).expect("decode"));
3677        for i in 1..=18 {
3678            let (out, _) = encode(&mut enc, i);
3679            assert!(whole.decode(&out).expect("decode"), "frame {i}");
3680            if i < 16 {
3681                assert!(lossy.decode(&out).expect("decode"), "frame {i}");
3682            }
3683        }
3684        assert!(enc.invalidate_reference(16), "x264's wrap frame is lost");
3685        let (out, reference) = encode(&mut enc, 19);
3686        assert_eq!(reference, Reference::Frame(15));
3687        assert_eq!(h264_frame_type(&out), FRAME_DELTA);
3688        assert!(whole.decode(&out).expect("decode"));
3689        assert!(lossy.decode(&out).expect("decode past x264's wrap"));
3690        let apart = luma_distance(&whole.frame().unwrap(), &lossy.frame().unwrap());
3691        assert!(
3692            apart < 0.5,
3693            "the decoder that lost frames 16-18 shows frame 19 {apart:.2} off the one that saw them"
3694        );
3695        for i in 20..=range {
3696            let (out, _) = encode(&mut enc, i);
3697            if i < range {
3698                assert!(lossy.decode(&out).expect("decode"), "frame {i}");
3699            }
3700        }
3701        assert!(
3702            enc.invalidate_reference(range as u16),
3703            "the stream's wrap frame is reported lost"
3704        );
3705        let (out, reference) = encode(&mut enc, range + 1);
3706        assert_eq!(reference, Reference::None);
3707        assert_eq!(h264_frame_type(&out), FRAME_KEY);
3708        assert!(
3709            lossy.decode(&out).expect("decode past the wrap"),
3710            "the decoder that never saw the wrap frame shows the next one"
3711        );
3712        assert_eq!(
3713            encode(&mut enc, range + 2).1,
3714            Reference::Frame(range as u16 + 1)
3715        );
3716        assert!(
3717            enc.invalidate_reference(range as u16 + 2),
3718            "the count restarted at the key frame"
3719        );
3720        assert_eq!(
3721            encode(&mut enc, range + 3).1,
3722            Reference::Frame(range as u16 + 1)
3723        );
3724    }
3725
3726    /// Mean absolute luma difference between two decoded pictures.
3727    #[cfg(feature = "gpl")]
3728    fn luma_distance(
3729        a: &crate::webcam::convert::I420View<'_>,
3730        b: &crate::webcam::convert::I420View<'_>,
3731    ) -> f64 {
3732        let rows =
3733            a.y.chunks(a.y_stride)
3734                .zip(b.y.chunks(b.y_stride))
3735                .take(a.height);
3736        rows.flat_map(|(ra, rb)| {
3737            ra[..a.width]
3738                .iter()
3739                .zip(&rb[..a.width])
3740                .map(|(&x, &y)| (x as f64 - y as f64).abs())
3741        })
3742        .sum::<f64>()
3743            / (a.width * a.height) as f64
3744    }
3745
3746    /// Encode the same scrolling-text sequence through the OpenH264 full-frame encoder (luma
3747    /// broadcast to a gray BGRA frame), returning each frame's bitstream for comparison with the
3748    /// x264 run.
3749    fn encode_oh264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
3750        let s = RustCaptureSettings {
3751            width: W as i32,
3752            height: H as i32,
3753            target_fps: 60.0,
3754            codec: Codec::H264,
3755            video_cbr_mode: cbr,
3756            video_bitrate_kbps: kbps,
3757            video_crf: crf,
3758            video_min_qp: min_qp,
3759            video_max_qp: max_qp,
3760            ..Default::default()
3761        };
3762        let mut enc = Openh264Encoder::new(&s).expect("oh264 init");
3763        (0..FRAMES)
3764            .map(|i| {
3765                let y = text_luma(i);
3766                let mut bgra = vec![255u8; W * H * 4];
3767                for (px, &l) in bgra.as_chunks_mut::<4>().0.iter_mut().zip(y.iter()) {
3768                    px[0] = l;
3769                    px[1] = l;
3770                    px[2] = l;
3771                }
3772                enc.encode_host_argb(&bgra, W * 4, i as u64, i == 0, false)
3773                    .expect("oh264 encode")
3774            })
3775            .collect()
3776    }
3777
3778    /// Decode a sequence of H.264 frames back to tightly-packed luma planes (dropping empty
3779    /// frames and the decoded chroma) for PSNR comparison.
3780    fn decode_luma(frames: &[Vec<u8>]) -> Vec<Vec<u8>> {
3781        let mut dec = Decoder::new().expect("decoder");
3782        let mut out = Vec::new();
3783        for f in frames {
3784            if f.is_empty() {
3785                continue;
3786            }
3787            if let Ok(Some(img)) = dec.decode(f) {
3788                let (w, h) = img.dimensions();
3789                let stride = img.strides().0;
3790                let mut y = vec![0u8; w * h];
3791                for r in 0..h {
3792                    y[r * w..r * w + w].copy_from_slice(&img.y()[r * stride..r * stride + w]);
3793                }
3794                out.push(y);
3795            }
3796        }
3797        out
3798    }
3799
3800    /// Mean per-frame luma PSNR (dB) between two decoded sequences, treating a zero-MSE frame
3801    /// as 99 dB.
3802    fn mean_psnr(a: &[Vec<u8>], b: &[Vec<u8>]) -> f64 {
3803        let n = a.len().min(b.len());
3804        let mut acc = 0.0;
3805        for i in 0..n {
3806            let mse: f64 = a[i]
3807                .iter()
3808                .zip(b[i].iter())
3809                .map(|(&x, &y)| {
3810                    let d = x as f64 - y as f64;
3811                    d * d
3812                })
3813                .sum::<f64>()
3814                / a[i].len() as f64;
3815            acc += if mse <= 0.0 {
3816                99.0
3817            } else {
3818                10.0 * (255.0f64 * 255.0 / mse).log10()
3819            };
3820        }
3821        acc / n.max(1) as f64
3822    }
3823
3824    /// Average encoded bitrate (kbps) of a frame sequence, assuming 60 fps playback.
3825    fn kbps(frames: &[Vec<u8>]) -> f64 {
3826        frames.iter().map(|f| f.len()).sum::<usize>() as f64 * 8.0 * 60.0 / FRAMES as f64 / 1000.0
3827    }
3828
3829    /// Diagnostic that the CBR QP clamp is actually plumbed through to x264, printing a
3830    /// bitrate/PSNR table on scrolling text and asserting the effect.
3831    ///
3832    /// Encodes worst-case scrolling text at 2 Mbps CBR across a sweep of `max_qp` values (plus a
3833    /// separate `min_qp` sweep on an over-provisioned 12 Mbps budget), measuring luma PSNR against a
3834    /// near-lossless CRF-12 reference from the same encoder so color-conversion differences cancel
3835    /// out. Capping `max_qp` at 30 on rate-starved content must lift fidelity by more than 0.5 dB
3836    /// over the unclamped run — proving the clamp reaches the encoder rather than being silently
3837    /// dropped (paid for in bitrate overshoot).
3838    #[cfg(feature = "gpl")]
3839    #[test]
3840    fn cbr_qp_bound_sweep_x264() {
3841        let reference = decode_luma(&encode_x264(false, 0, 12, 0, 0));
3842
3843        println!("scrolling-text 720p60 @ 2 Mbps CBR, x264 (PSNR vs own CRF-12 decode):");
3844        let mut rows = Vec::new();
3845        for &max_qp in &[0i32, 45, 40, 35, 30] {
3846            let x = encode_x264(true, 2000, 25, 0, max_qp);
3847            let psnr = mean_psnr(&decode_luma(&x), &reference);
3848            println!(
3849                "  max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
3850                max_qp,
3851                kbps(&x),
3852                psnr
3853            );
3854            rows.push((max_qp, psnr));
3855        }
3856        println!("scrolling-text 720p60 @ 12 Mbps CBR, x264 min-QP sweep:");
3857        for &min_qp in &[0i32, 10, 15] {
3858            let x = encode_x264(true, 12000, 25, min_qp, 0);
3859            let psnr = mean_psnr(&decode_luma(&x), &reference);
3860            println!(
3861                "  min_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
3862                min_qp,
3863                kbps(&x),
3864                psnr
3865            );
3866        }
3867
3868        let base = rows[0].1;
3869        let capped = rows.last().unwrap().1;
3870        assert!(
3871            capped > base + 0.5,
3872            "x264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
3873        );
3874    }
3875
3876    /// The OpenH264 counterpart of [`cbr_qp_bound_sweep_x264`]: the software H.264 sweep of a
3877    /// build without the `gpl` feature, where OpenH264 is the encoder behind every stripe.
3878    ///
3879    /// Same scrolling-text workload and 2 Mbps CBR `max_qp` sweep, with luma PSNR measured against
3880    /// this encoder's own near-lossless QP-12 reference. Capping `max_qp` at 30 must lift fidelity
3881    /// by more than 0.5 dB over the unclamped run.
3882    #[test]
3883    fn cbr_qp_bound_sweep_openh264() {
3884        let reference = decode_luma(&encode_oh264(false, 0, 12, 0, 0));
3885
3886        println!("scrolling-text 720p60 @ 2 Mbps CBR, oh264 (PSNR vs own QP-12 decode):");
3887        let mut rows = Vec::new();
3888        for &max_qp in &[0i32, 45, 40, 35, 30] {
3889            let o = encode_oh264(true, 2000, 25, 0, max_qp);
3890            let psnr = mean_psnr(&decode_luma(&o), &reference);
3891            println!(
3892                "  max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
3893                max_qp,
3894                kbps(&o),
3895                psnr
3896            );
3897            rows.push((max_qp, psnr));
3898        }
3899
3900        let base = rows[0].1;
3901        let capped = rows.last().unwrap().1;
3902        assert!(
3903            capped > base + 0.5,
3904            "oh264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
3905        );
3906    }
3907
3908    /// After a live frame-rate change the CBR stream still tracks the configured bitrate.
3909    ///
3910    /// x264's per-frame CBR budget is `bitrate / fps`, so halving the frame rate at a fixed kbps
3911    /// budget must roughly double each encoded frame while the per-second bitrate holds. This encodes
3912    /// incompressible full-frame noise at 20 Mbps CBR (content the rate controller cannot undershoot,
3913    /// so per-frame size sits at the budget; a budget the noise cannot meet within the quantizer
3914    /// ceiling would read the encoder's shortfall instead), measures the mean encoded frame size at 60 fps, drops
3915    /// to 30 fps through `reconfigure_rate`, and requires the per-frame size to roughly double — so
3916    /// the per-second bitrate is preserved rather than collapsing to half, which is what the pre-fix
3917    /// path did by leaving the session budgeting for 60 fps (`x264_encoder_reconfig` never applies a
3918    /// frame-rate change). Single-threaded so the rate-control measurement is deterministic; warmup
3919    /// frames are discarded so the ABR controller and the post-reopen IDR do not skew the mean.
3920    #[cfg(feature = "gpl")]
3921    #[test]
3922    fn cbr_bitrate_tracks_configured_rate_after_fps_change() {
3923        const TARGET_KBPS: i32 = 20000;
3924        const WARMUP: usize = 24;
3925        const MEASURED: usize = 96;
3926        let u = vec![128u8; (W / 2) * (H / 2)];
3927        let v = vec![128u8; (W / 2) * (H / 2)];
3928        let vbv = |fps: f64| {
3929            (crate::encoders::vbv_bits((TARGET_KBPS as u32) * 1000, fps, 0.0, 0.0) / 1000).max(1)
3930                as i32
3931        };
3932        // A fresh incompressible luma plane every frame, so inter-prediction cannot cheapen a
3933        // frame and CBR must spend its whole per-frame budget: the per-frame size then reads the
3934        // budget directly, which is exactly what a frame-rate change is supposed to move.
3935        let noise_luma = |frame: usize| -> Vec<u8> {
3936            let mut y = vec![0u8; W * H];
3937            let mut s = (frame as u32).wrapping_mul(2654435761).wrapping_add(1);
3938            for p in y.iter_mut() {
3939                s ^= s << 13;
3940                s ^= s >> 17;
3941                s ^= s << 5;
3942                *p = (s >> 24) as u8;
3943            }
3944            y
3945        };
3946
3947        let mut enc = H264EncoderWrapper::new(
3948            W as i32,
3949            H as i32,
3950            25,
3951            false,
3952            60.0,
3953            1,
3954            true,
3955            TARGET_KBPS,
3956            vbv(60.0),
3957            0,
3958            0,
3959        )
3960        .expect("x264 init");
3961
3962        let measure = |enc: &mut H264EncoderWrapper, start: usize| -> f64 {
3963            let mut bytes = 0usize;
3964            let mut counted = 0usize;
3965            for i in start..start + WARMUP + MEASURED {
3966                let y = noise_luma(i);
3967                let mut out = Vec::new();
3968                enc.encode_with_headers(
3969                    &y,
3970                    &u,
3971                    &v,
3972                    W as i32,
3973                    (W / 2) as i32,
3974                    (W / 2) as i32,
3975                    i as u16,
3976                    0,
3977                    i == start,
3978                    true,
3979                    &mut out,
3980                );
3981                if i >= start + WARMUP && !out.is_empty() {
3982                    bytes += out.len();
3983                    counted += 1;
3984                }
3985            }
3986            bytes as f64 / counted.max(1) as f64
3987        };
3988
3989        let per_frame_60 = measure(&mut enc, 0);
3990        enc.reconfigure_rate(TARGET_KBPS, vbv(30.0), 30.0);
3991        let per_frame_30 = measure(&mut enc, 1000);
3992
3993        let eff_60 = per_frame_60 * 8.0 * 60.0 / 1000.0;
3994        let eff_30 = per_frame_30 * 8.0 * 30.0 / 1000.0;
3995        println!(
3996            "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)"
3997        );
3998
3999        let ratio = per_frame_30 / per_frame_60.max(1.0);
4000        assert!(
4001            (1.5..2.6).contains(&ratio),
4002            "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"
4003        );
4004        assert!(
4005            eff_30 > eff_60 * 0.75,
4006            "30fps effective bitrate {eff_30:.0} kbps collapsed from the 60fps {eff_60:.0} kbps; fps change did not re-budget the bitrate"
4007        );
4008    }
4009}