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_H264_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 crate::RustCaptureSettings;
17use rayon::prelude::*;
18use smithay::utils::{Physical, Rectangle};
19#[cfg(feature = "gpl")]
20use std::ffi::CString;
21#[cfg(feature = "gpl")]
22use std::ptr;
23use std::sync::Arc;
24use yuv::{BufferStoreMut, YuvConversionMode, YuvPlanarImageMut, YuvRange, YuvStandardMatrix};
25
26/// Upper bound on the horizontal stripes the CPU encoder splits a frame into, so the
27/// persistent per-stripe state vector can be reserved to a fixed capacity once, up front.
28///
29/// With the vector reserved to this size at startup, the per-frame resize to the actual stripe count
30/// stays a cheap in-place adjustment that preserves each stripe's reused encoder and scratch buffers,
31/// rather than a reallocation that would churn them whenever the count changes.
32pub const MAX_STRIPE_CAPACITY: usize = 64;
33
34/// Convert a packed BGRA/RGBA buffer to planar YUV (4:2:0 or 4:4:4) for the software H.264
35/// encoders, spreading the conversion across up to `bands` threads so it never bottlenecks a frame.
36///
37/// **Why the band split exists.** Colour conversion is a non-trivial slice of per-frame CPU. The
38/// striped path already parallelizes it for free — each stripe converts on its own rayon worker —
39/// but a single full-frame consumer (the whole-frame x264 stripe, or a full-frame OpenH264
40/// instance, which passes `bands = 4`) would otherwise convert its entire image on one thread and
41/// stall the frame there. Splitting into horizontal bands hands that lone conversion the same
42/// multi-threading the striped path enjoys. The cut is horizontal because YUV planes are
43/// row-major, so a horizontal boundary yields contiguous, non-overlapping plane sub-slices with no
44/// per-row seam bookkeeping.
45///
46/// 1. **Plane strides**: the Y plane is `width` wide; the chroma planes are `width` for 4:4:4
47///    (`i444 == true`) or `width / 2` for 4:2:0. `rgba_input` selects the source byte order and
48///    `i444` the subsampling, together choosing one of four `yuv` crate routines — 4:4:4 uses
49///    **Full** range, 4:2:0 uses **Limited** range, and both use the **BT.709** matrix and the
50///    **Fast** conversion mode.
51/// 2. **Band split**: `band_h` is `height / bands` floored to an even number and at least 2 rows
52///    (a band under 2 rows is not worth a thread). Keeping band boundaries even ensures a 4:2:0
53///    chroma pair never straddles a seam. When `bands <= 1` or the whole image fits one band, the
54///    conversion runs single-threaded in place.
55/// 3. **Parallel bands**: otherwise a `std::thread::scope` carves `src` and the three output planes
56///    into contiguous per-band sub-slices (chroma rows scaled by `uv_rows` — full height for 4:4:4,
57///    half for 4:2:0) and spawns one thread per band. The final band absorbs any leftover rows,
58///    taking all remaining rows whenever fewer than `band_h + 2` are left. Each thread's result is
59///    joined and collected; a panicked join degrades to a `PointerOverflow` error, and the first
60///    error wins.
61#[allow(clippy::too_many_arguments)]
62pub(crate) fn convert_to_yuv_mt(
63    src: &[u8],
64    src_stride: u32,
65    width: usize,
66    height: usize,
67    rgba_input: bool,
68    i444: bool,
69    y_buf: &mut [u8],
70    u_buf: &mut [u8],
71    v_buf: &mut [u8],
72    bands: usize,
73) -> Result<(), yuv::YuvError> {
74    let y_stride = width;
75    let uv_stride = if i444 { width } else { width / 2 };
76
77    let convert_band = |src_band: &[u8], y: &mut [u8], u: &mut [u8], v: &mut [u8], h: usize| {
78        let mut img = YuvPlanarImageMut {
79            y_plane: BufferStoreMut::Borrowed(y),
80            y_stride: y_stride as u32,
81            u_plane: BufferStoreMut::Borrowed(u),
82            u_stride: uv_stride as u32,
83            v_plane: BufferStoreMut::Borrowed(v),
84            v_stride: uv_stride as u32,
85            width: width as u32,
86            height: h as u32,
87        };
88        match (i444, rgba_input) {
89            (true, true) => yuv::rgba_to_yuv444(
90                &mut img, src_band, src_stride, YuvRange::Full,
91                YuvStandardMatrix::Bt709, YuvConversionMode::Fast,
92            ),
93            (true, false) => yuv::bgra_to_yuv444(
94                &mut img, src_band, src_stride, YuvRange::Full,
95                YuvStandardMatrix::Bt709, YuvConversionMode::Fast,
96            ),
97            (false, true) => yuv::rgba_to_yuv420(
98                &mut img, src_band, src_stride, YuvRange::Limited,
99                YuvStandardMatrix::Bt709, YuvConversionMode::Fast,
100            ),
101            (false, false) => yuv::bgra_to_yuv420(
102                &mut img, src_band, src_stride, YuvRange::Limited,
103                YuvStandardMatrix::Bt709, YuvConversionMode::Fast,
104            ),
105        }
106    };
107
108    let band_h = ((height / bands.max(1)) & !1).max(2);
109    if bands <= 1 || height <= band_h {
110        return convert_band(src, y_buf, u_buf, v_buf, height);
111    }
112
113    let uv_rows = |rows: usize| if i444 { rows } else { rows / 2 };
114    let mut results: Vec<Result<(), yuv::YuvError>> = Vec::new();
115    std::thread::scope(|s| {
116        let mut handles = Vec::new();
117        let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
118        let mut row = 0;
119        while row < height {
120            let h = if height - row < band_h + 2 { height - row } else { band_h };
121            let (src_band, s_next) = src_rest.split_at(h * src_stride as usize);
122            let (y_band, y_next) = y_rest.split_at_mut(h * y_stride);
123            let (u_band, u_next) = u_rest.split_at_mut(uv_rows(h) * uv_stride);
124            let (v_band, v_next) = v_rest.split_at_mut(uv_rows(h) * uv_stride);
125            src_rest = s_next;
126            y_rest = y_next;
127            u_rest = u_next;
128            v_rest = v_next;
129            row += h;
130            handles.push(s.spawn(move || convert_band(src_band, y_band, u_band, v_band, h)));
131        }
132        for hnd in handles {
133            results.push(hnd.join().unwrap_or(Err(yuv::YuvError::PointerOverflow)));
134        }
135    });
136    results.into_iter().collect()
137}
138
139thread_local! {
140    /// Reused libjpeg-turbo compressor kept per worker thread to avoid paying a
141    /// `tjInitCompress`/`tjDestroy` round trip for every stripe of every frame.
142    ///
143    /// The striped JPEG path compresses one stripe per rayon worker, so the compressor is
144    /// thread-local rather than shared: each worker creates its own lazily on first use and then
145    /// holds it for the process lifetime. Making it thread-local also sidesteps the locking a shared
146    /// compressor would otherwise need across the parallel stripe encoders.
147    static JPEG_COMPRESSOR: std::cell::RefCell<Option<turbojpeg::Compressor>> =
148        const { std::cell::RefCell::new(None) };
149}
150
151/// Process-global lock that serializes libx264 encoder open/close, because those calls are
152/// not thread-safe yet the striped path opens encoders concurrently from many stripe workers.
153///
154/// libx264 mutates process-global state inside `x264_encoder_open`/`x264_encoder_close`, so two
155/// stripe encoders opening at once — or two capture instances sharing one process — can race that
156/// state and corrupt the heap. The lock is deliberately held only around open and close, never
157/// around `x264_encoder_encode`, so serializing setup costs nothing in the hot per-stripe encode
158/// path where the real parallelism lives.
159#[cfg(feature = "gpl")]
160static X264_OPEN_CLOSE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
161
162/// One long-lived libx264 session for a stripe, holding the raw `x264_t` handle alongside a
163/// mirror of its live parameters so the encoder can be retuned per frame instead of rebuilt.
164///
165/// Rebuilding an x264 encoder is expensive and forces a fresh IDR, so a stripe keeps its instance
166/// across frames and only nudges CRF, bitrate, VBV, and frame rate live; the tracked `current_*`
167/// fields are that mirror, letting a reconfigure skip the FFI call whenever nothing actually changed.
168/// `is_i444` (4:4:4 vs 4:2:0) is baked into the encoder's colour space at open, so a change to it is
169/// one of the few things that forces a full rebuild; `is_cbr` records which rate-control mode was
170/// chosen at open and gates which of the live reconfigures apply. The manual `Send` impl exists only
171/// because a raw pointer is not `Send` by default and the handle must move onto the rayon stripe
172/// workers; `Drop` closes it under the global open/close lock for the same reason that lock exists.
173#[cfg(feature = "gpl")]
174pub struct H264EncoderWrapper {
175    encoder: *mut x264_sys::x264_t,
176    pub width: i32,
177    pub height: i32,
178    current_crf: i32,
179    pub is_i444: bool,
180    is_cbr: bool,
181    current_bitrate: i32,
182    current_vbv: i32,
183    current_fps: u32,
184    #[allow(dead_code)]
185    full_range: bool,
186    /// Open-time parameters retained so a frame-rate change can reopen the session: x264's live
187    /// reconfigure cannot alter the frame rate, and CBR/VBV budgets are derived from it.
188    threads: i32,
189    min_qp: i32,
190    max_qp: i32,
191}
192
193#[cfg(feature = "gpl")]
194unsafe impl Send for H264EncoderWrapper {}
195
196#[cfg(feature = "gpl")]
197impl Drop for H264EncoderWrapper {
198    fn drop(&mut self) {
199        if !self.encoder.is_null() {
200            let _guard = X264_OPEN_CLOSE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
201            unsafe { x264_sys::x264_encoder_close(self.encoder) };
202            self.encoder = ptr::null_mut();
203        }
204    }
205}
206
207#[cfg(feature = "gpl")]
208impl H264EncoderWrapper {
209    /// Open an x264 encoder tuned for real-time screen streaming, or `None` on failure.
210    ///
211    /// **Why this configuration.** These frames are captured live and must ship immediately, so the
212    /// encoder is optimized for latency over compression ratio: the `ultrafast` preset keeps encode
213    /// time under the frame budget, and `zerolatency` bars the frame reordering and lookahead
214    /// buffering that would otherwise add pipeline delay. Everything below then bends x264 toward the
215    /// pipeline's own keyframe and colour model instead of its broadcast-oriented defaults.
216    ///
217    /// 1. **Preset/tune**: starts from the `ultrafast` preset with the `zerolatency` tune, then
218    ///    overrides resolution, frame rate (floored to 30 fps when under 1), and thread count.
219    /// 2. **Infinite GOP**: `i_keyint_max` is set to x264's infinite sentinel and adaptive scene-cut
220    ///    is disabled (`i_scenecut_threshold = 0`), so the encoder never injects an unrequested IDR
221    ///    on a scene change — keyframes are purely on-demand via the forced-IDR path, matching the
222    ///    strict infinite-GOP model.
223    /// 3. **Rate control**:
224    ///    - **CBR** (`cbr_mode`): ABR targeting `bitrate_kbps` with a VBV cap pinned to the same
225    ///      value (buffer `vbv_kbit`, precomputed by the caller from the frame-time multiplier
226    ///      policy) and filler disabled. Optional QP clamps apply only when non-zero — `max_qp` is
227    ///      the legibility floor (caps how ugly a rate-starved frame gets) and `min_qp` the waste
228    ///      ceiling (stops over-spending on easy content); both are clamped to 51.
229    ///    - **CRF** (default): constant-quality with `f_rf_constant = crf`.
230    /// 4. **Colour**: I444 (full range) or I420 (limited range) CSP, BT.709 VUI primaries/transfer/
231    ///    matrix, and the matching `high444` / `baseline` profile.
232    /// 5. **Coding tools**: CABAC and the 8x8 transform are disabled, matching the low-latency
233    ///    baseline profile — CAVLC entropy coding with no 8x8 DCT — for minimal encode cost.
234    /// 6. **Output**: repeated headers (SPS/PPS before each keyframe) and Annex-B framing, with
235    ///    x264's own logging silenced.
236    ///
237    /// The `x264_encoder_open` call is serialized under `X264_OPEN_CLOSE_LOCK` because it mutates
238    /// libx264 global state.
239    #[allow(clippy::too_many_arguments)]
240    pub fn new(width: i32, height: i32, crf: i32, is_i444: bool, fps: f64, threads: i32,
241               cbr_mode: bool, bitrate_kbps: i32, vbv_kbit: i32,
242               min_qp: i32, max_qp: i32) -> Option<Self> {
243        unsafe {
244            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
245            let preset = CString::new("ultrafast").unwrap();
246            let tune = CString::new("zerolatency").unwrap();
247
248            if x264_sys::x264_param_default_preset(&mut param, preset.as_ptr(), tune.as_ptr()) < 0 {
249                return None;
250            }
251
252            param.i_width = width;
253            param.i_height = height;
254            param.i_fps_num = if fps < 1.0 { 30 } else { fps as u32 };
255            param.i_fps_den = 1;
256            param.i_keyint_max = x264_sys::X264_KEYINT_MAX_INFINITE as i32;
257            param.i_scenecut_threshold = 0;
258            if cbr_mode {
259                let bk = bitrate_kbps.saturating_abs();
260                param.rc.i_rc_method = x264_sys::X264_RC_ABR as i32;
261                param.rc.i_bitrate = bk;
262                param.rc.i_vbv_max_bitrate = bk;
263                param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
264                param.rc.b_filler = 0;
265                if min_qp > 0 {
266                    param.rc.i_qp_min = min_qp.min(51);
267                }
268                if max_qp > 0 {
269                    param.rc.i_qp_max = max_qp.min(51);
270                }
271            } else {
272                param.rc.i_rc_method = x264_sys::X264_RC_CRF as i32;
273                param.rc.f_rf_constant = crf as f32;
274            }
275            param.i_csp = if is_i444 {
276                x264_sys::X264_CSP_I444
277            } else {
278                x264_sys::X264_CSP_I420
279            } as i32;
280            param.vui.b_fullrange = if is_i444 { 1 } else { 0 };
281            param.vui.i_colorprim = 1;
282            param.vui.i_transfer = 1;
283            param.vui.i_colmatrix = 1;
284
285            let profile = CString::new(if is_i444 { "high444" } else { "baseline" }).unwrap();
286            x264_sys::x264_param_apply_profile(&mut param, profile.as_ptr());
287
288            param.i_threads = threads;
289            param.b_repeat_headers = 1;
290            param.b_annexb = 1;
291            param.i_log_level = x264_sys::X264_LOG_NONE;
292
293            let encoder = {
294                let _guard = X264_OPEN_CLOSE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
295                x264_sys::x264_encoder_open(&mut param)
296            };
297            if encoder.is_null() {
298                None
299            } else {
300                Some(Self {
301                    encoder,
302                    width,
303                    height,
304                    current_crf: crf,
305                    is_i444,
306                    is_cbr: cbr_mode,
307                    current_bitrate: bitrate_kbps.saturating_abs(),
308                    current_vbv: vbv_kbit,
309                    current_fps: if fps < 1.0 { 30 } else { fps as u32 },
310                    full_range: param.vui.b_fullrange == 1,
311                    threads,
312                    min_qp,
313                    max_qp,
314                })
315            }
316        }
317    }
318
319    /// Retune the constant-quality CRF on the running encoder, so a quality change costs a
320    /// parameter push rather than tearing down and rebuilding the session (a rebuild would force an
321    /// IDR and drop encoder state).
322    ///
323    /// It is a no-op in CBR mode, where rate is bitrate-controlled and CRF simply does not apply, and
324    /// a no-op when the value is unchanged — the tracked `current_crf` is what makes that cheap
325    /// early-out possible. Otherwise it reads the encoder's live parameters, overwrites
326    /// `f_rf_constant`, and pushes the change via `x264_encoder_reconfig`, advancing the tracked CRF
327    /// only once the reconfig has actually succeeded so the mirror never drifts from the encoder.
328    pub fn reconfigure_crf(&mut self, new_crf: i32) {
329        if self.is_cbr || self.current_crf == new_crf {
330            return;
331        }
332        unsafe {
333            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
334            x264_sys::x264_encoder_parameters(self.encoder, &mut param);
335            param.rc.f_rf_constant = new_crf as f32;
336            if x264_sys::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
337                self.current_crf = new_crf;
338            }
339        }
340    }
341
342    /// Retune bitrate/VBV (CBR only) and/or frame rate to match the live settings, structured to
343    /// be called unconditionally every frame so the caller need not track what changed itself.
344    ///
345    /// Because `encode_cpu` fires it on every frame, it first computes the would-be values and bails
346    /// before touching the encoder when neither the CBR bitrate/VBV nor the frame rate differs from
347    /// what is live — that self-gating keeps a per-frame call nearly free.
348    ///
349    /// A frame-rate change reopens the encoder rather than reconfiguring it: `x264_encoder_reconfig`
350    /// does not apply `i_fps_*`, and the CBR/VBV per-frame budget is `bitrate / fps`, so a session
351    /// left at its old rate ships roughly half the configured bitrate once fps halves. The reopen
352    /// carries the new bitrate/VBV too, and a fresh session emits an IDR on its first frame; a failed
353    /// reopen keeps the working session instead of nulling the handle. A bitrate/VBV-only change
354    /// (CBR) stays a live `x264_encoder_reconfig`, and the tracked mirror advances only on success so
355    /// it cannot drift from the encoder's real state.
356    pub fn reconfigure_rate(&mut self, bitrate_kbps: i32, vbv_kbit: i32, fps: f64) {
357        let bk = bitrate_kbps.saturating_abs();
358        let new_fps = if fps < 1.0 { 30 } else { fps as u32 };
359        let rate_changed =
360            self.is_cbr && (self.current_bitrate != bk || self.current_vbv != vbv_kbit);
361        let fps_changed = self.current_fps != new_fps;
362        if !rate_changed && !fps_changed {
363            return;
364        }
365        if fps_changed {
366            if let Some(fresh) = H264EncoderWrapper::new(
367                self.width,
368                self.height,
369                self.current_crf,
370                self.is_i444,
371                new_fps as f64,
372                self.threads,
373                self.is_cbr,
374                bk,
375                vbv_kbit,
376                self.min_qp,
377                self.max_qp,
378            ) {
379                *self = fresh;
380            }
381            return;
382        }
383        unsafe {
384            let mut param: x264_sys::x264_param_t = std::mem::zeroed();
385            x264_sys::x264_encoder_parameters(self.encoder, &mut param);
386            param.rc.i_bitrate = bk;
387            param.rc.i_vbv_max_bitrate = bk;
388            param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
389            if x264_sys::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
390                self.current_bitrate = bk;
391                self.current_vbv = vbv_kbit;
392            }
393        }
394    }
395
396    /// Encode one YUV frame into H.264 and frame it for the wire, reporting whether the
397    /// encoder actually emitted a bitstream this call.
398    ///
399    /// The boolean return is load-bearing: `x264_encoder_encode` can legitimately produce nothing on
400    /// a given call, and the caller must forward a stripe only when real bytes exist — never an empty
401    /// or header-only packet. Framing is conditional because the transport needs the pipeline's small
402    /// wire header to route the stripe, while `omit_headers` consumers take the bare Annex-B
403    /// elementary stream.
404    ///
405    /// 1. **Picture setup**: wraps the borrowed Y/U/V planes and their strides in an
406    ///    `x264_picture_t` with the encoder's CSP, stamps the presentation timestamp with `frame_id`,
407    ///    and requests an IDR when `force_idr` is set (otherwise `X264_TYPE_AUTO`).
408    /// 2. **Encode**: calls `x264_encoder_encode`; a non-positive returned size means no frame was
409    ///    emitted this call, so the function returns `false` without writing output.
410    /// 3. **Framing**: `output_buf` is cleared and refilled. Unless `omit_headers` is set, a header
411    ///    is prepended — a `0x04` codec tag, then a type byte read from the *actual* output picture
412    ///    type rather than from `force_idr`, because the encoder may not honor a keyframe request and
413    ///    the client keys its decode-recovery on the frame type it truly received (IDR = `0x01`,
414    ///    I = `0x02`, else `0x00`), then the caller's `fixed_header` (frame number, y-start, width,
415    ///    height). With `omit_headers` the output is bare Annex-B.
416    /// 4. **Payload**: every NAL payload is appended to `output_buf` after the optional header,
417    ///    so the bytes past the wire header are always a contiguous Annex-B access unit.
418    #[allow(clippy::too_many_arguments)]
419    pub fn encode_with_headers(
420        &mut self,
421        y: &[u8],
422        u: &[u8],
423        v: &[u8],
424        y_stride: i32,
425        u_stride: i32,
426        v_stride: i32,
427        frame_id: i64,
428        force_idr: bool,
429        fixed_header: &[u8],
430        omit_headers: bool,
431        output_buf: &mut Vec<u8>,
432    ) -> bool {
433        unsafe {
434            let mut pic_in: x264_sys::x264_picture_t = std::mem::zeroed();
435            x264_sys::x264_picture_init(&mut pic_in);
436
437            pic_in.img.i_csp = if self.is_i444 {
438                x264_sys::X264_CSP_I444
439            } else {
440                x264_sys::X264_CSP_I420
441            } as i32;
442            pic_in.img.i_plane = 3;
443            pic_in.img.plane[0] = y.as_ptr() as *mut u8;
444            pic_in.img.plane[1] = u.as_ptr() as *mut u8;
445            pic_in.img.plane[2] = v.as_ptr() as *mut u8;
446            pic_in.img.i_stride[0] = y_stride;
447            pic_in.img.i_stride[1] = u_stride;
448            pic_in.img.i_stride[2] = v_stride;
449            pic_in.i_pts = frame_id;
450            pic_in.i_type = if force_idr {
451                x264_sys::X264_TYPE_IDR
452            } else {
453                x264_sys::X264_TYPE_AUTO
454            } as i32;
455
456            let mut pic_out: x264_sys::x264_picture_t = std::mem::zeroed();
457            let mut nals: *mut x264_sys::x264_nal_t = ptr::null_mut();
458            let mut i_nals: i32 = 0;
459
460            let frame_size = x264_sys::x264_encoder_encode(
461                self.encoder,
462                &mut nals,
463                &mut i_nals,
464                &mut pic_in,
465                &mut pic_out,
466            );
467
468            if frame_size > 0 {
469                let header_len = if omit_headers { 0 } else { 2 + fixed_header.len() };
470                let total_len = header_len + frame_size as usize;
471
472                output_buf.clear();
473                output_buf.reserve(total_len);
474
475                if !omit_headers {
476                    output_buf.push(0x04);
477                    let type_byte = if pic_out.i_type == x264_sys::X264_TYPE_IDR as i32 {
478                        0x01
479                    } else if pic_out.i_type == x264_sys::X264_TYPE_I as i32 {
480                        0x02
481                    } else {
482                        0x00
483                    };
484                    output_buf.push(type_byte);
485                    output_buf.extend_from_slice(fixed_header);
486                }
487
488                let nal_slice = std::slice::from_raw_parts(nals, i_nals as usize);
489                for nal in nal_slice {
490                    let payload = std::slice::from_raw_parts(nal.p_payload, nal.i_payload as usize);
491                    output_buf.extend_from_slice(payload);
492                }
493                return true;
494            }
495        }
496        false
497    }
498}
499
500/// Everything one horizontal stripe must remember between frames: its reused buffers, its own
501/// live encoder, and the motion / paint-over / damage bookkeeping that drives its send decision.
502///
503/// The frame is striped so independent screen regions can encode in parallel and an unchanged region
504/// can be skipped on its own, and that only works if each stripe carries its *own* cross-frame
505/// history. So one instance lives per stripe for the whole session and nothing per-stripe is rebuilt
506/// or recomputed from scratch each frame:
507/// - **Reused buffers**: `y_buf` / `u_buf` / `v_buf` hold the stripe's YUV planes and `packet_buf`
508///   the encoded output, grown in place rather than reallocated per frame.
509/// - **Encoder**: `h264_encoder` is the stripe's software H.264 instance — libx264 in a `gpl`
510///   build, OpenH264 otherwise — reused until its geometry (or, for x264, chroma format) changes.
511/// - **Paint-over / recovery**: `no_motion_frame_count` counts consecutive static frames,
512///   `paint_over_sent` guards against re-sending a high-quality repaint of a still region, and
513///   `h264_burst_frames_remaining` tracks a post-repaint or recovery streaming burst.
514/// - **Content-hash damage** (only for sources without external damage, i.e. X11): `last_hash` is
515///   the previous frame's content hash, `consecutive_changes` counts changed frames toward the
516///   damage-block threshold, and `in_damage_block` / `damage_block_frames_remaining` /
517///   `hash_at_block_start` drive the sustained-motion damage block managed by `content_dirty`.
518#[derive(Default)]
519pub struct StripeState {
520    pub no_motion_frame_count: u32,
521    pub paint_over_sent: bool,
522    #[cfg(feature = "gpl")]
523    pub h264_encoder: Option<H264EncoderWrapper>,
524    #[cfg(not(feature = "gpl"))]
525    pub h264_encoder: Option<crate::encoders::oh264::Openh264Encoder>,
526    pub h264_burst_frames_remaining: i32,
527    #[cfg(feature = "gpl")]
528    pub y_buf: Vec<u8>,
529    #[cfg(feature = "gpl")]
530    pub u_buf: Vec<u8>,
531    #[cfg(feature = "gpl")]
532    pub v_buf: Vec<u8>,
533    pub packet_buf: Vec<u8>,
534    pub last_hash: u64,
535    pub consecutive_changes: u32,
536    pub in_damage_block: bool,
537    pub damage_block_frames_remaining: i32,
538    pub hash_at_block_start: u64,
539}
540
541/// Fast, non-cryptographic 64-bit content hash used only for in-memory change detection.
542///
543/// Uses xxh3: a SIMD-friendly hash that processes 64-byte blocks with parallel lanes,
544/// delivering near memory-bandwidth throughput. The value is never persisted or sent on the
545/// wire, so only the property that identical bytes hash identically matters. A collision
546/// between two distinct stripes is ~2^-64, and the next real content change or a requested
547/// keyframe repaints any missed update anyway.
548fn fast_hash(bytes: &[u8]) -> u64 {
549    xxhash_rust::xxh3::xxh3_64_with_seed(bytes, 0)
550}
551
552impl StripeState {
553    /// Stand in for the compositor damage that X11 capture does not provide: hash this stripe
554    /// to decide whether it changed since last frame, and once it is clearly in motion, stop
555    /// re-hashing it every frame by committing to a sustained-motion "damage block".
556    ///
557    /// The hash is not free, and a region that changes every frame would otherwise be re-hashed
558    /// forever while always reporting dirty anyway. So after `threshold` consecutive changes the
559    /// stripe enters a damage block that just reports dirty for `duration` frames and re-hashes only
560    /// once, at the end, to decide whether to extend the block or let it lapse — trading a little
561    /// extra sending for far fewer hashes on exactly the regions that need them least:
562    ///
563    /// 1. **Inside a damage block**: the stripe is treated as dirty without re-hashing, and the
564    ///    block's remaining-frame counter is decremented. Only when the counter reaches zero is the
565    ///    stripe re-hashed — if it differs from the hash captured at block start the block is renewed
566    ///    for another `duration` frames, otherwise the block exits and the change counter resets.
567    ///    This keeps a continuously-moving region streaming for `duration` frames per re-check rather
568    ///    than hashing every frame.
569    /// 2. **Outside a block**: the stripe is hashed and compared to the previous frame. A change
570    ///    increments `consecutive_changes`, and reaching `threshold` consecutive changes opens a new
571    ///    damage block; an unchanged frame resets the counter to zero.
572    ///
573    /// Returns `true` whenever the stripe is considered dirty (always true while inside a block).
574    pub fn content_dirty(&mut self, bytes: &[u8], threshold: u32, duration: i32) -> bool {
575        if self.in_damage_block {
576            self.damage_block_frames_remaining -= 1;
577            if self.damage_block_frames_remaining <= 0 {
578                let h = fast_hash(bytes);
579                if h != self.hash_at_block_start {
580                    self.damage_block_frames_remaining = duration;
581                    self.hash_at_block_start = h;
582                } else {
583                    self.in_damage_block = false;
584                    self.consecutive_changes = 0;
585                }
586                self.last_hash = h;
587            }
588            return true;
589        }
590        let h = fast_hash(bytes);
591        let changed = h != self.last_hash;
592        self.last_hash = h;
593        if changed {
594            self.consecutive_changes += 1;
595            if self.consecutive_changes >= threshold {
596                self.in_damage_block = true;
597                self.damage_block_frames_remaining = duration;
598                self.hash_at_block_start = h;
599            }
600        } else {
601            self.consecutive_changes = 0;
602        }
603        changed
604    }
605}
606
607/// One encoded stripe: the compressed bytes plus geometry and identity metadata.
608///
609/// The consumer can place and attribute the stripe even when the payload has no header. In
610/// `omit_headers` mode the per-stripe wire header is stripped from the bytes; the struct fields
611/// carry that information out-of-band.
612///
613/// # Fields
614///
615/// * `data` - Compressed payload (JPEG or H.264 NAL units). `Arc`-shared so every
616///   delivery-layer consumer can retain the frame without copying the bytes.
617/// * `data_type` - Codec tag: **1 = JPEG**, **2 = H.264**.
618/// * `stripe_y_start` - Y pixel coordinate of the stripe's top edge within the frame.
619/// * `stripe_height` - Height of the stripe in pixels.
620/// * `frame_id` - Frame sequence number this stripe belongs to.
621pub struct EncodedStripe {
622    pub data: Arc<Vec<u8>>,
623    pub data_type: i32,
624    pub stripe_y_start: i32,
625    pub stripe_height: i32,
626    pub frame_id: i32,
627}
628
629/// The software encoder's per-frame entry point: split the frame into horizontal stripes,
630/// decide per stripe whether it needs sending, and encode only those as JPEG or H.264 (libx264
631/// or OpenH264, by build) across the rayon pool.
632///
633/// Two pressures drive the design: CPU H.264/JPEG is expensive, so the frame is cut into
634/// parallel stripes; bandwidth is precious, so unchanged stripes are skipped. Each stripe is
635/// independently hashed against the previous frame for change detection, and only dirty stripes
636/// are encoded. The H.264 path maintains per-stripe encoder state across frames for
637/// inter-prediction; the JPEG path is stateless.
638///
639/// # Arguments
640///
641/// * `stripes` - Persistent per-stripe state vector (resized as needed, encoder state preserved).
642/// * `raw_pixels` - Packed BGRA/RGBA pixel buffer (`width * height * 4` bytes).
643/// * `width` - Frame width in pixels.
644/// * `height` - Frame height in pixels.
645/// * `damage_rects` - Wayland damage rectangles (empty for X11 hash-based detection).
646/// * `settings` - Capture settings (quality, mode, rate control, etc.).
647/// * `frame_counter` - Current frame number (wrapping `u16`).
648/// * `use_gpu` - `true` when the source is RGBA (GLES readback); `false` for BGRA (X11 host).
649/// * `hash_damage` - `true` for X11 stripe-hash change detection; `false` when damage rects
650///   are provided.
651/// * `force_idr_all` - Force a keyframe on every stripe (client join / reset / periodic IDR).
652///
653/// # Returns
654///
655/// Vec of [`EncodedStripe`] — empty when nothing changed.
656/// repainting a stalled region at full quality, and letting a freshly-joined or reset client recover
657/// a clean picture. Persistent `StripeState` is what makes both affordable: encoders and buffers
658/// survive across frames instead of being rebuilt, and the motion/paint-over history the decision
659/// needs lives right beside them. The per-stripe decision mirrors `decide_hw_fullframe`'s policy for
660/// the hardware full-frame encoders; it is kept as separate code here because the striped path also
661/// chooses JPEG-vs-H.264 and derives its own damage.
662///
663/// 1. **Stripe count**: defaults to the core count so the fan-out matches the hardware, but
664///    collapses to a single full-frame stripe when H.264 full-frame is requested or the frame is
665///    shorter than the 64-row minimum, and is otherwise capped so no stripe is thinner than 64 rows —
666///    below that the per-stripe encoder and thread overhead outweighs the parallelism and the tiny
667///    H.264 slices compress poorly. The persistent `stripes` vector is resized to match, preserving
668///    per-stripe state across frames.
669/// 2. **Idle fast path**: a frame on which no stripe can emit anything (no damage / clean
670///    hashes, no paint-over due, no burst, no recovery IDR, not streaming) only advances the
671///    per-stripe no-motion bookkeeping inline and returns without dispatching the stripe
672///    fan-out, so a static capture never wakes the rayon pool.
673/// 3. **Dirty map**: with external compositor damage (`hash_damage == false`) each `damage_rects`
674///    rectangle marks every stripe whose row range it overlaps. With `hash_damage == true` (X11,
675///    which has no compositor damage) per-stripe content hashing drives dirtiness instead — except
676///    in streaming H.264, where every stripe is sent unconditionally so the hash is skipped.
677/// 4. **Per-stripe decision** (in `stripe_body`): a stripe is sent when it is dirty, when a
678///    paint-over / recovery burst is in flight, when streaming mode is on, or when `force_idr_all`
679///    is set. Quality is chosen per case — base JPEG quality / base CRF for live content, the
680///    paint-over quality/CRF after `paint_over_trigger_frames` static frames (once per still region,
681///    guarded by `paint_over_sent`), and `burst_crf` during a burst (the paint-over CRF when it is
682///    enabled and actually lower, else the base CRF, since a recovery burst still needs to stream so
683///    CBR can refine it). A newly dirty frame cancels any pending burst or paint-over and reverts to
684///    base quality.
685/// 5. **Recovery IDR** (`force_idr_all`): forces a send on every stripe even when static so a
686///    reconnecting client can resume. For H.264 it forces an IDR and arms a short streaming burst
687///    (unless one is already pending, so it cannot preempt an in-flight burst) because the keyframe
688///    is base-quality — worsened further by CBR — and a damage-gated static stream would otherwise
689///    never refine it; for JPEG, where every stripe is already intra, it resends a
690///    previously-painted-over stripe at the paint-over quality already on screen so a joining viewer
691///    does not see a downgrade.
692/// 6. **Encoding**:
693///    - **JPEG** (`output_mode 0`): source byte order is RGBA on the GPU readback path and BGRA on
694///      X11; each worker thread reuses its thread-local TurboJPEG compressor. Header-less output
695///      hands the compressed buffer straight through; otherwise a 6-byte stripe header (`0x03` tag,
696///      a reserved byte, frame number, y-start) is prepended to match the H.264 path's native
697///      framing so the transport can forward the buffer without re-framing.
698///    - **H.264** (`output_mode 1`): the stripe's encoder is reused unless the width, height, or
699///      (x264) chroma format changed, in which case it is rebuilt and an IDR forced; otherwise CRF
700///      and rate are reconfigured live. With libx264, ARGB is converted to YUV here (a conversion
701///      failure skips the stripe rather than encoding garbage) and an 8-byte fixed header (frame
702///      number, y-start, width, height) is emitted; OpenH264 converts and frames inside
703///      `encode_stripe_argb` with the same header layout, and encodes a 4:4:4 request 4:2:0 (said
704///      once per process). The live CBR budget is recomputed here from the bitrate/fps so it
705///      rescales with live changes.
706/// 7. **Dispatch**: a single full-frame stripe runs inline (sequential — empirically faster than a
707///    one-element rayon job) with one fewer encode thread than the available cores, clamped to
708///    `[1, 4]` (x264 with a single-band colour conversion; OpenH264 adds four slices and a four-band
709///    conversion of its own). The slice threads keep the in-frame encode latency inside the frame
710///    budget at high resolutions; the cap is four because `zerolatency` makes x264 slice-threaded
711///    and more than four slices trips decode glitches in some Chromium builds, and the minus-one
712///    leaves headroom for the capture thread. Multiple stripes instead run across the rayon pool
713///    with a single encode thread and one conversion band each, since the parallelism there
714///    already comes from encoding the stripes concurrently.
715#[allow(clippy::too_many_arguments)]
716/// No stripe is shorter than a macroblock row.
717const MIN_STRIPE_HEIGHT: i32 = 64;
718/// How fast the smoothed count of budget-carrying stripes follows the frame's.
719const CARRY_RISE: f32 = 0.3;
720const CARRY_FALL: f32 = 0.05;
721
722pub fn encode_cpu(
723    stripes: &mut Vec<StripeState>,
724    carrying: &mut f32,
725    raw_pixels: &[u8],
726    width: i32,
727    height: i32,
728    damage_rects: &[Rectangle<i32, Physical>],
729    settings: &RustCaptureSettings,
730    frame_counter: u16,
731    use_gpu: bool,
732    hash_damage: bool,
733    force_idr_all: bool,
734) -> Vec<EncodedStripe> {
735    let n_processing_stripes =
736        stripe_count(height, settings.output_mode, settings.video_fullframe);
737
738    if stripes.len() != n_processing_stripes {
739        stripes.resize_with(n_processing_stripes, StripeState::default);
740    }
741
742    let stripe_geometries =
743        compute_stripe_geometries(height as usize, n_processing_stripes, settings.output_mode);
744
745    // Idle fast path: a static frame must still advance every stripe's paint-over countdown,
746    // but nothing else — so when no stripe can emit anything this frame, do that bookkeeping
747    // inline and return before the rayon fan-out. Waking the whole worker pool 60x/s for
748    // no-op stripes is the dominant idle cost (tens of percent of a core), dwarfing the real
749    // per-frame work. "Static" is known up front for damage-authoritative sources (Wayland:
750    // empty damage list); hash-damage sources (X11) instead take a sequential early-exit
751    // hash scan, probing the most-recently-dirty stripe first so live content bails out
752    // after a single stripe hash. A clean scan performs exactly the state transitions
753    // `content_dirty` would (hash unchanged, change streak reset), so the damage-block
754    // machinery observes no difference.
755    let idle_candidate = damage_rects.is_empty()
756        && !force_idr_all
757        && !(settings.output_mode == 1 && settings.video_streaming_mode);
758    if idle_candidate {
759        let paint_over_armed = settings.use_paint_over_quality
760            && if settings.output_mode == 0 {
761                settings.paint_over_jpeg_quality > settings.jpeg_quality
762            } else {
763                settings.video_paintover_crf < settings.video_crf
764            };
765        let no_pending_send = |st: &StripeState| {
766            (settings.output_mode == 0 || st.h264_burst_frames_remaining <= 0)
767                && (!paint_over_armed
768                    || st.paint_over_sent
769                    || st.no_motion_frame_count.saturating_add(1)
770                        < settings.paint_over_trigger_frames)
771        };
772        let quiescent = if !hash_damage {
773            stripes.iter().all(no_pending_send)
774        } else {
775            let width_bytes = width as usize * 4;
776            let hint = stripes
777                .iter()
778                .enumerate()
779                .min_by_key(|(_, st)| st.no_motion_frame_count)
780                .map(|(i, _)| i)
781                .unwrap_or(0);
782            let clean = |i: usize| {
783                let st = &stripes[i];
784                if !no_pending_send(st) || st.in_damage_block {
785                    return false;
786                }
787                let (y, h) = stripe_geometries[i];
788                let bytes = &raw_pixels[y * width_bytes..(y + h) * width_bytes];
789                fast_hash(bytes) == st.last_hash
790            };
791            clean(hint) && (0..stripes.len()).filter(|&i| i != hint).all(clean)
792        };
793        if quiescent {
794            for st in stripes.iter_mut() {
795                st.no_motion_frame_count = st.no_motion_frame_count.saturating_add(1);
796                st.consecutive_changes = 0;
797            }
798            return Vec::new();
799        }
800    }
801    let mut stripe_is_dirty = vec![false; n_processing_stripes];
802    if !damage_rects.is_empty() {
803        for rect in damage_rects {
804            let r_y_start = rect.loc.y.max(0) as usize;
805            let r_y_end = (rect.loc.y + rect.size.h).min(height) as usize;
806            if r_y_start < r_y_end {
807                for (i, &(s_y, s_h)) in stripe_geometries.iter().enumerate() {
808                    let s_end = s_y + s_h;
809                    if r_y_start < s_end && r_y_end > s_y {
810                        stripe_is_dirty[i] = true;
811                    }
812                }
813            }
814        }
815    }
816
817    let width_usize = width as usize;
818    let output_mode = settings.output_mode;
819    let video_crf = settings.video_crf;
820    let video_po_crf = settings.video_paintover_crf;
821    let video_burst = settings.video_paintover_burst_frames;
822    let video_fullcolor = settings.video_fullcolor;
823    let video_streaming = settings.video_streaming_mode;
824    let jpeg_q = settings.jpeg_quality;
825    let paint_q = settings.paint_over_jpeg_quality;
826    let trigger_frames = settings.paint_over_trigger_frames;
827    let use_paint_over = settings.use_paint_over_quality;
828    let burst_crf = if use_paint_over && video_po_crf < video_crf { video_po_crf } else { video_crf };
829    let target_fps = settings.target_fps;
830    let omit_headers = settings.omit_stripe_headers;
831    let damage_block_threshold = settings.damage_block_threshold;
832    let damage_block_duration = settings.damage_block_duration as i32;
833    #[cfg(feature = "gpl")]
834    let video_cbr = settings.video_cbr_mode;
835    // The requested rate is a whole-screen budget, and CRF needs no division
836    // at all (a per-quality target). OpenH264 sizes its own buffer, so only
837    // x264 reads the VBV share.
838    #[cfg_attr(not(feature = "gpl"), allow(unused_variables))]
839    let (video_bitrate, video_vbv) =
840        stripe_rate_control(settings, *carrying, n_processing_stripes);
841    // Full-frame x264 threads: one fewer than the cores (headroom for the
842    // capture thread), clamped to [1, 4] to match the four-slice ceiling below.
843    // A full-frame OpenH264 instance applies the same policy internally.
844    #[cfg(feature = "gpl")]
845    let h264_threads = if n_processing_stripes == 1 {
846        std::thread::available_parallelism()
847            .map(|n| n.get())
848            .unwrap_or(1)
849            .saturating_sub(1)
850            .clamp(1, 4) as i32
851    } else {
852        1
853    };
854    #[cfg(feature = "gpl")]
855    let csc_bands = 1;
856    if output_mode == 1 && video_fullcolor && !crate::encoders::SOFTWARE_H264_FULLCOLOR {
857        static FULLCOLOR_LOGGED: std::sync::atomic::AtomicBool =
858            std::sync::atomic::AtomicBool::new(false);
859        if !FULLCOLOR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
860            eprintln!("[software] 4:4:4 full-color requested; OpenH264 is 4:2:0-only, encoding 4:2:0.");
861        }
862    }
863
864    let stripe_body = |(i, stripe_state): (usize, &mut StripeState)| -> Option<EncodedStripe> {
865            if i >= stripe_geometries.len() {
866                return None;
867            }
868            let (y_start, actual_height) = stripe_geometries[i];
869            let start_idx = y_start * width_usize * 4;
870            let end_idx = start_idx + (actual_height * width_usize * 4);
871            let stripe_bytes = &raw_pixels[start_idx..end_idx];
872
873            let mut send_this_stripe = false;
874            let mut quality_or_crf = if output_mode == 0 { jpeg_q } else { video_crf };
875            let mut force_idr = false;
876            let is_dirty = if !hash_damage {
877                stripe_is_dirty[i]
878            } else if output_mode == 1 && video_streaming {
879                false
880            } else {
881                stripe_state.content_dirty(stripe_bytes, damage_block_threshold, damage_block_duration)
882            };
883
884            if output_mode == 1 && stripe_state.h264_burst_frames_remaining > 0 {
885                send_this_stripe = true;
886                quality_or_crf = burst_crf;
887                stripe_state.h264_burst_frames_remaining -= 1;
888
889                if is_dirty {
890                    stripe_state.h264_burst_frames_remaining = 0;
891                    stripe_state.paint_over_sent = false;
892                    quality_or_crf = video_crf;
893                }
894            }
895
896            if !send_this_stripe && output_mode == 1 && video_streaming {
897                send_this_stripe = true;
898            }
899
900            if is_dirty {
901                send_this_stripe = true;
902                stripe_state.no_motion_frame_count = 0;
903                stripe_state.paint_over_sent = false;
904                stripe_state.h264_burst_frames_remaining = 0;
905                quality_or_crf = if output_mode == 0 { jpeg_q } else { video_crf };
906            } else if !send_this_stripe {
907                stripe_state.no_motion_frame_count += 1;
908
909                if use_paint_over
910                    && stripe_state.no_motion_frame_count >= trigger_frames
911                    && !stripe_state.paint_over_sent
912                {
913                    if output_mode == 0 && paint_q > jpeg_q {
914                        send_this_stripe = true;
915                        quality_or_crf = paint_q;
916                        stripe_state.paint_over_sent = true;
917                    } else if output_mode == 1 && video_po_crf < video_crf {
918                        send_this_stripe = true;
919                        stripe_state.paint_over_sent = true;
920                        quality_or_crf = video_po_crf;
921                        force_idr = true;
922                        stripe_state.h264_burst_frames_remaining = video_burst - 1;
923                    }
924                }
925            }
926
927            if force_idr_all {
928                send_this_stripe = true;
929                if output_mode == 1 {
930                    force_idr = true;
931                    if stripe_state.h264_burst_frames_remaining <= 0 && video_burst > 0 {
932                        stripe_state.paint_over_sent = true;
933                        stripe_state.h264_burst_frames_remaining = video_burst;
934                    }
935                } else if stripe_state.paint_over_sent && use_paint_over && paint_q > jpeg_q {
936                    quality_or_crf = paint_q;
937                }
938            }
939
940            if send_this_stripe {
941                if output_mode == 0 {
942                    let pixel_format = if use_gpu {
943                        turbojpeg::PixelFormat::RGBA
944                    } else {
945                        turbojpeg::PixelFormat::BGRA
946                    };
947                    let img = turbojpeg::Image {
948                        pixels: stripe_bytes,
949                        width: width_usize,
950                        pitch: width_usize * 4,
951                        height: actual_height,
952                        format: pixel_format,
953                    };
954                    JPEG_COMPRESSOR.with(|cell| -> Option<EncodedStripe> {
955                        let mut slot = cell.borrow_mut();
956                        if slot.is_none() {
957                            *slot = Some(turbojpeg::Compressor::new().ok()?);
958                        }
959                        let compressor = slot.as_mut().unwrap();
960                        compressor.set_quality(quality_or_crf).ok()?;
961                        let jpeg = compressor.compress_to_vec(img).ok()?;
962                        let data = if omit_headers {
963                            jpeg
964                        } else {
965                            stripe_state.packet_buf.clear();
966                            stripe_state.packet_buf.push(0x03);
967                            stripe_state.packet_buf.push(0x00);
968                            stripe_state
969                                .packet_buf
970                                .extend_from_slice(&frame_counter.to_be_bytes());
971                            stripe_state
972                                .packet_buf
973                                .extend_from_slice(&(y_start as u16).to_be_bytes());
974                            stripe_state.packet_buf.extend_from_slice(&jpeg);
975                            std::mem::take(&mut stripe_state.packet_buf)
976                        };
977                        Some(EncodedStripe {
978                            data: Arc::new(data),
979                            data_type: 1,
980                            stripe_y_start: y_start as i32,
981                            stripe_height: actual_height as i32,
982                            frame_id: frame_counter as i32,
983                        })
984                    })
985                } else {
986                    cfg_if::cfg_if! {
987                        if #[cfg(feature = "gpl")] {
988                    let needs_reinit = if let Some(ref enc) = stripe_state.h264_encoder {
989                        enc.width != width_usize as i32
990                            || enc.height != actual_height as i32
991                            || enc.is_i444 != video_fullcolor
992                    } else {
993                        true
994                    };
995
996                    if needs_reinit {
997                        stripe_state.h264_encoder = H264EncoderWrapper::new(
998                            width_usize as i32,
999                            actual_height as i32,
1000                            quality_or_crf,
1001                            video_fullcolor,
1002                            target_fps,
1003                            h264_threads,
1004                            video_cbr,
1005                            video_bitrate,
1006                            video_vbv,
1007                            settings.video_min_qp,
1008                            settings.video_max_qp,
1009                        );
1010                        force_idr = true;
1011                    } else if let Some(ref mut enc) = stripe_state.h264_encoder {
1012                        enc.reconfigure_crf(quality_or_crf);
1013                        enc.reconfigure_rate(video_bitrate, video_vbv, target_fps);
1014                    }
1015
1016                    if let Some(ref mut enc) = stripe_state.h264_encoder {
1017                        let y_size = width_usize * actual_height;
1018                        let uv_size = if video_fullcolor { y_size } else { y_size / 4 };
1019                        if stripe_state.y_buf.len() != y_size {
1020                            stripe_state.y_buf.resize(y_size, 0);
1021                        }
1022                        if stripe_state.u_buf.len() != uv_size {
1023                            stripe_state.u_buf.resize(uv_size, 0);
1024                        }
1025                        if stripe_state.v_buf.len() != uv_size {
1026                            stripe_state.v_buf.resize(uv_size, 0);
1027                        }
1028
1029                        let y_stride = width_usize as i32;
1030                        let uv_stride =
1031                            (if video_fullcolor { width_usize } else { width_usize / 2 }) as i32;
1032                        let conversion_result = convert_to_yuv_mt(
1033                            stripe_bytes,
1034                            (width_usize * 4) as u32,
1035                            width_usize,
1036                            actual_height,
1037                            use_gpu,
1038                            video_fullcolor,
1039                            &mut stripe_state.y_buf,
1040                            &mut stripe_state.u_buf,
1041                            &mut stripe_state.v_buf,
1042                            csc_bands,
1043                        );
1044
1045                        if let Err(e) = conversion_result {
1046                            eprintln!(
1047                                "[software] YUV conversion failed for {}x{} stripe: {:?}; skipping",
1048                                width_usize, actual_height, e
1049                            );
1050                            return None;
1051                        }
1052
1053                        let mut fixed_header = [0u8; 8];
1054                        fixed_header[0..2].copy_from_slice(&frame_counter.to_be_bytes());
1055                        fixed_header[2..4].copy_from_slice(&(y_start as u16).to_be_bytes());
1056                        fixed_header[4..6].copy_from_slice(&(width_usize as u16).to_be_bytes());
1057                        fixed_header[6..8].copy_from_slice(&(actual_height as u16).to_be_bytes());
1058
1059                        if enc.encode_with_headers(
1060                            &stripe_state.y_buf,
1061                            &stripe_state.u_buf,
1062                            &stripe_state.v_buf,
1063                            y_stride,
1064                            uv_stride,
1065                            uv_stride,
1066                            frame_counter as i64,
1067                            force_idr,
1068                            &fixed_header,
1069                            omit_headers,
1070                            &mut stripe_state.packet_buf,
1071                        ) {
1072                            Some(EncodedStripe {
1073                                data: Arc::new(std::mem::take(&mut stripe_state.packet_buf)),
1074                                data_type: 2,
1075                                stripe_y_start: y_start as i32,
1076                                stripe_height: actual_height as i32,
1077                                frame_id: frame_counter as i32,
1078                            })
1079                        } else {
1080                            None
1081                        }
1082                    } else {
1083                        None
1084                    }
1085                        } else {
1086                    use crate::encoders::oh264::Openh264Encoder;
1087                    let needs_reinit = stripe_state.h264_encoder.as_ref().is_none_or(|enc| {
1088                        enc.width() != width_usize || enc.height() != actual_height
1089                    });
1090                    if needs_reinit {
1091                        stripe_state.h264_encoder = Openh264Encoder::new_stripe(
1092                            settings,
1093                            width_usize,
1094                            actual_height,
1095                            quality_or_crf,
1096                            video_bitrate,
1097                            n_processing_stripes == 1,
1098                        );
1099                        if stripe_state.h264_encoder.is_none() {
1100                            // Once per process: a geometry OpenH264 refuses (wider than
1101                            // 3840, say) would otherwise log on every stripe of every frame.
1102                            static INIT_FAILED_LOGGED: std::sync::atomic::AtomicBool =
1103                                std::sync::atomic::AtomicBool::new(false);
1104                            if !INIT_FAILED_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
1105                                eprintln!(
1106                                    "[software] OpenH264 init failed for a {}x{} stripe; no software H.264 for it",
1107                                    width_usize, actual_height
1108                                );
1109                            }
1110                        }
1111                        force_idr = true;
1112                    } else if let Some(ref mut enc) = stripe_state.h264_encoder {
1113                        enc.update_qp(quality_or_crf.max(0) as u32);
1114                        enc.reconfigure_rate(video_bitrate, target_fps);
1115                    }
1116
1117                    let enc = stripe_state.h264_encoder.as_mut()?;
1118                    match enc.encode_stripe_argb(
1119                        stripe_bytes,
1120                        width_usize * 4,
1121                        frame_counter as u64,
1122                        y_start as u16,
1123                        force_idr,
1124                        use_gpu,
1125                    ) {
1126                        Ok(data) if !data.is_empty() => Some(EncodedStripe {
1127                            data: Arc::new(data),
1128                            data_type: 2,
1129                            stripe_y_start: y_start as i32,
1130                            stripe_height: actual_height as i32,
1131                            frame_id: frame_counter as i32,
1132                        }),
1133                        Ok(_) => None,
1134                        Err(e) => {
1135                            eprintln!("[software] OpenH264 encode failed for stripe at y={y_start}: {e}");
1136                            None
1137                        }
1138                    }
1139                        }
1140                    }
1141                }
1142            } else {
1143                None
1144            }
1145    };
1146    let encoded: Vec<EncodedStripe> = if n_processing_stripes <= 1 {
1147        stripes.iter_mut().enumerate().filter_map(&stripe_body).collect()
1148    } else {
1149        stripes.par_iter_mut().enumerate().filter_map(&stripe_body).collect()
1150    };
1151    // Follow motion spreading out quickly and narrowing slowly: the budget is
1152    // better spent late than overshot the moment a screen goes still again.
1153    let sent = encoded.len() as f32;
1154    let alpha = if sent > *carrying { CARRY_RISE } else { CARRY_FALL };
1155    *carrying += (sent - *carrying) * alpha;
1156    encoded
1157}
1158
1159/// How many horizontal stripes a frame of `height` is split into, which is the choice of how
1160/// much encode parallelism to spend on it.
1161///
1162/// A full-frame session is one contiguous stream and so a single stripe; otherwise the frame
1163/// fans out across cores, bounded so no stripe is shorter than a macroblock row. Both the
1164/// encoder and the settings line report from here, so what is logged is what is encoded.
1165pub fn stripe_count(height: i32, output_mode: i32, fullframe: bool) -> usize {
1166    let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
1167    if (output_mode == 1 && fullframe) || height < MIN_STRIPE_HEIGHT {
1168        return 1;
1169    }
1170    cores.min((height / MIN_STRIPE_HEIGHT) as usize).max(1)
1171}
1172
1173/// Split the configured CBR budget across the stripes carrying it, returning the
1174/// `(bitrate_kbps, vbv_kbit)` each stripe's encoder is programmed with.
1175///
1176/// Every stripe runs its own encoder and rate control is per instance, metered against the
1177/// declared frame rate rather than against the frames that stripe was actually sent. So the
1178/// screen's rate is one stripe's rate times the number of stripes that carry motion, and the
1179/// budget is divided by that number — not by the stripe count, which on a screen where one
1180/// corner moves would spend a fraction of what was configured. The divisor is the smoothed
1181/// count so it changes on the scale of a moving average and not every frame: a rate that
1182/// swings frame to frame leaves the encoder chasing it and delivers less than either rate would.
1183fn stripe_rate_control(
1184    settings: &RustCaptureSettings, carrying: f32, n_stripes: usize,
1185) -> (i32, i32) {
1186    let divisor = (carrying.round().max(1.0) as usize).min(n_stripes.max(1)) as i32;
1187    let bitrate = (settings.video_bitrate_kbps / divisor).max(1);
1188    let vbv = (crate::encoders::vbv_bits(
1189        (bitrate as u32).saturating_mul(1000),
1190        settings.target_fps,
1191        settings.keyframe_interval_s,
1192        settings.video_vbv_multiplier,
1193    ) / 1000)
1194        .max(1) as i32;
1195    (bitrate, vbv)
1196}
1197
1198/// Divide `height` into `n` contiguous stripes as `(y_start, stripe_height)`, with the split
1199/// rule differing by codec because only H.264 constrains stripe height.
1200///
1201/// - **JPEG** (`output_mode 0`): JPEG has no vertical subsampling, so stripes may be any height; the
1202///   heights differ by at most one row — the first `remainder` stripes take one extra each — and
1203///   every row of the frame is covered.
1204/// - **H.264** (`output_mode 1`): 4:2:0 pairs chroma rows vertically, so every stripe height is
1205///   forced even and the remainder is handed out two rows at a time. The deliberate cost is that a
1206///   single trailing odd row may be left uncovered — preferable to an odd-height stripe the encoder
1207///   cannot represent.
1208fn compute_stripe_geometries(height: usize, n: usize, output_mode: i32) -> Vec<(usize, usize)> {
1209    let mut geoms = Vec::with_capacity(n);
1210    let mut current_y = 0;
1211    if output_mode == 0 {
1212        let base_h = height / n;
1213        let remainder = height - base_h * n;
1214        for i in 0..n {
1215            let s_h = base_h + if i < remainder { 1 } else { 0 };
1216            geoms.push((current_y, s_h));
1217            current_y += s_h;
1218        }
1219    } else {
1220        let base_h = (height / n) & !1;
1221        let remainder = height - base_h * n;
1222        let stripes_with_extra = remainder / 2;
1223        for i in 0..n {
1224            let s_h = base_h + if i < stripes_with_extra { 2 } else { 0 };
1225            geoms.push((current_y, s_h));
1226            current_y += s_h;
1227        }
1228    }
1229    geoms
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234    /// The configured bitrate is a budget for the screen, not for each stripe: every stripe
1235    /// runs its own rate control, so what reaches an encoder is the budget over the number of
1236    /// stripes carrying motion. Dividing by the stripe count instead spends a fraction of the
1237    /// configured rate whenever only part of the screen moves, and dividing by nothing at all
1238    /// spends a multiple of it whenever the whole screen does.
1239    #[test]
1240    fn cbr_budget_is_split_across_the_stripes_carrying_it() {
1241        use crate::RustCaptureSettings;
1242        for &kbps in &[500i32, 4000, 8000, 20000] {
1243            let settings = RustCaptureSettings {
1244                video_cbr_mode: true,
1245                video_bitrate_kbps: kbps,
1246                ..Default::default()
1247            };
1248            for &n in &[1usize, 2, 4, 12, 64] {
1249                let (all, _) = super::stripe_rate_control(&settings, n as f32, n);
1250                let total = all * n as i32;
1251                assert!(
1252                    total <= kbps && kbps - total < n as i32,
1253                    "{n} stripes at {all} kbps must sum to the configured {kbps}"
1254                );
1255                let (one, _) = super::stripe_rate_control(&settings, 1.0, n);
1256                assert_eq!(one, kbps, "a lone moving stripe carries the whole budget");
1257                let (over, _) = super::stripe_rate_control(&settings, n as f32 * 4.0, n);
1258                assert_eq!(over, all, "the divisor never exceeds the stripes that exist");
1259                let (under, _) = super::stripe_rate_control(&settings, 0.0, n);
1260                assert_eq!(under, kbps, "and never falls below one");
1261            }
1262            let (vbv_one, whole) = super::stripe_rate_control(&settings, 1.0, 8);
1263            let (_, share) = super::stripe_rate_control(&settings, 8.0, 8);
1264            assert_eq!(vbv_one, kbps);
1265            assert!(
1266                (share * 8 - whole).abs() <= 9,
1267                "each stripe's buffer is its share of the whole-screen one: {share}x8 vs {whole}"
1268            );
1269        }
1270    }
1271
1272    /// The divisor follows the screen rather than the configuration: full-screen motion moves
1273    /// it to the stripe count within a few frames, and it comes back down when the motion
1274    /// stops. A divisor recomputed per frame would swing between those two ends every frame,
1275    /// which leaves the encoder chasing a square wave and delivering less than either rate.
1276    #[test]
1277    fn the_budget_divisor_follows_motion_and_is_smoothed() {
1278        use crate::RustCaptureSettings;
1279        let (w, h) = (64, 512);
1280        let settings = RustCaptureSettings {
1281            width: w,
1282            height: h,
1283            output_mode: 0,
1284            jpeg_quality: 40,
1285            use_paint_over_quality: false,
1286            ..Default::default()
1287        };
1288        let full = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
1289        let stripes_n = super::stripe_count(h, settings.output_mode, settings.video_fullframe);
1290        if stripes_n < 2 {
1291            return;
1292        }
1293        let mut stripes = Vec::new();
1294        let mut carrying = 1.0f32;
1295        for frame in 0..40u16 {
1296            let shade = 40u8.wrapping_add(frame.wrapping_mul(7) as u8);
1297            let px = vec![shade; (w * h * 4) as usize];
1298            super::encode_cpu(
1299                &mut stripes, &mut carrying, &px, w, h, &full, &settings, frame,
1300                false, false, false,
1301            );
1302        }
1303        assert!(
1304            carrying > stripes_n as f32 * 0.75,
1305            "full-screen motion must move the divisor toward the {stripes_n} stripes it uses, \
1306             not leave it at {carrying}"
1307        );
1308        let moved = carrying;
1309        // Motion that narrows to one corner narrows the divisor with it, so the budget
1310        // follows the stripes that are actually spending it. A frame with no motion at all
1311        // encodes nothing and carries nothing, so it leaves the divisor where it was.
1312        let band = [smithay::utils::Rectangle::new((0, 0).into(), (w, 64).into())];
1313        for frame in 40..120u16 {
1314            let shade = 40u8.wrapping_add(frame.wrapping_mul(11) as u8);
1315            let mut px = vec![200u8; (w * h * 4) as usize];
1316            for byte in px.iter_mut().take((w * 64 * 4) as usize) {
1317                *byte = shade;
1318            }
1319            super::encode_cpu(
1320                &mut stripes, &mut carrying, &px, w, h, &band, &settings, frame,
1321                false, false, false,
1322            );
1323        }
1324        assert!(
1325            carrying < moved * 0.5,
1326            "motion in one stripe must bring the divisor back down: {carrying} vs {moved}"
1327        );
1328    }
1329    use super::{compute_stripe_geometries, StripeState};
1330
1331    /// Without `gpl` the striped H.264 path runs one OpenH264 instance per stripe and speaks
1332    /// the x264 stripes' protocol: the first frame emits every stripe as an IDR whose wire header
1333    /// carries that stripe's y-start and geometry, each stripe is an independently decodable
1334    /// stream (a decoder fed only that stripe's bytes yields a picture of the stripe's size), a
1335    /// static follow-up frame sends nothing, and motion confined to the top rows re-sends only
1336    /// the top stripe, as a delta frame.
1337    #[cfg(not(feature = "gpl"))]
1338    #[test]
1339    fn openh264_stripes_are_independent_streams() {
1340        use crate::RustCaptureSettings;
1341        use openh264::decoder::Decoder;
1342        use openh264::formats::YUVSource;
1343        let (w, h) = (128, 512);
1344        let settings = RustCaptureSettings {
1345            width: w,
1346            height: h,
1347            output_mode: 1,
1348            video_crf: 25,
1349            use_paint_over_quality: false,
1350            video_streaming_mode: false,
1351            ..Default::default()
1352        };
1353        let n = super::stripe_count(h, settings.output_mode, settings.video_fullframe);
1354        if n < 2 {
1355            return;
1356        }
1357        let mut stripes = Vec::new();
1358        let mut carrying = 1.0f32;
1359        let px: Vec<u8> = (0..(w * h * 4) as usize).map(|i| (i % 251) as u8).collect();
1360        let first = super::encode_cpu(
1361            &mut stripes, &mut carrying, &px, w, h, &[], &settings, 0, false, true, false,
1362        );
1363        assert_eq!(first.len(), n, "every stripe is sent on the first frame");
1364        for (stripe, (y, sh)) in first.iter().zip(compute_stripe_geometries(h as usize, n, 1)) {
1365            let d = &stripe.data;
1366            assert_eq!(d[0], 0x04, "H.264 stripe tag");
1367            assert_eq!(d[1], 0x01, "first frame of a stripe is an IDR");
1368            assert_eq!(u16::from_be_bytes([d[2], d[3]]), 0, "frame number");
1369            assert_eq!(u16::from_be_bytes([d[4], d[5]]) as usize, y, "y-start");
1370            assert_eq!(u16::from_be_bytes([d[6], d[7]]) as i32, w, "width");
1371            assert_eq!(u16::from_be_bytes([d[8], d[9]]) as usize, sh, "stripe height");
1372            assert_eq!((stripe.stripe_y_start as usize, stripe.stripe_height as usize), (y, sh));
1373            let mut dec = Decoder::new().expect("decoder");
1374            let img = dec.decode(&d[10..]).expect("decode").expect("an IDR decodes on its own");
1375            assert_eq!(img.dimensions(), (w as usize, sh), "each stripe is its own stream");
1376        }
1377        let quiet = super::encode_cpu(
1378            &mut stripes, &mut carrying, &px, w, h, &[], &settings, 1, false, true, false,
1379        );
1380        assert!(quiet.is_empty(), "a static frame sends nothing");
1381        let mut moved = px.clone();
1382        for b in moved.iter_mut().take((w * 8 * 4) as usize) {
1383            *b = b.wrapping_add(97);
1384        }
1385        let top = super::encode_cpu(
1386            &mut stripes, &mut carrying, &moved, w, h, &[], &settings, 2, false, true, false,
1387        );
1388        assert_eq!(top.len(), 1, "motion in the top rows re-sends the top stripe alone");
1389        assert_eq!(top[0].stripe_y_start, 0);
1390        assert_eq!(top[0].data[1], 0x00, "an unforced follow-up is a delta frame");
1391        assert_eq!(u16::from_be_bytes([top[0].data[2], top[0].data[3]]), 2, "frame number");
1392    }
1393
1394    /// With `threshold = 2` and `duration = 3`, a first change reads dirty and two consecutive
1395    /// changes open a damage block that holds dirty for three frames without re-hashing; once content
1396    /// has gone static, the end-of-block re-hash exits the block and the stripe reads clean again.
1397    #[test]
1398    fn content_dirty_detects_change_and_damage_block() {
1399        let mut st = StripeState::default();
1400        let a = vec![1u8; 256];
1401        let b = vec![2u8; 256];
1402        assert!(st.content_dirty(&a, 2, 3));
1403        assert!(!st.content_dirty(&a, 2, 3));
1404        assert!(st.content_dirty(&b, 2, 3));
1405        assert!(st.content_dirty(&a, 2, 3));
1406        assert!(st.in_damage_block);
1407        assert!(st.content_dirty(&a, 2, 3));
1408        assert!(st.content_dirty(&a, 2, 3));
1409        assert!(st.content_dirty(&a, 2, 3));
1410        assert!(!st.in_damage_block);
1411        assert!(!st.content_dirty(&a, 2, 3));
1412    }
1413
1414    /// With compositor damage as the authority (Wayland), a clean frame must still advance the
1415    /// paint-over countdown and fire the repaint at the trigger, and once every stripe has
1416    /// latched (`paint_over_sent`) further clean frames must produce nothing — that quiescent
1417    /// tail is the idle fast path, which skips the stripe fan-out entirely.
1418    #[test]
1419    fn clean_frames_countdown_fire_paintover_then_go_quiescent() {
1420        use crate::RustCaptureSettings;
1421        let (w, h) = (64, 128);
1422        let pixels = vec![128u8; (w * h * 4) as usize];
1423        let settings = RustCaptureSettings {
1424            width: w,
1425            height: h,
1426            output_mode: 0,
1427            jpeg_quality: 60,
1428            paint_over_jpeg_quality: 90,
1429            use_paint_over_quality: true,
1430            paint_over_trigger_frames: 5,
1431            ..Default::default()
1432        };
1433        let mut stripes = Vec::new();
1434        let mut carrying = 1.0f32;
1435        let full = [smithay::utils::Rectangle::new(
1436            (0, 0).into(),
1437            (w, h).into(),
1438        )];
1439        let dirty = super::encode_cpu(
1440            &mut stripes, &mut carrying, &pixels, w, h, &full, &settings, 0, false, false, false,
1441        );
1442        assert!(!dirty.is_empty(), "damaged frame must encode");
1443
1444        let mut fired_at = None;
1445        for frame in 1..=20u16 {
1446            let out = super::encode_cpu(
1447                &mut stripes, &mut carrying, &pixels, w, h, &[], &settings, frame, false, false, false,
1448            );
1449            if !out.is_empty() {
1450                assert!(fired_at.is_none(), "paint-over must fire exactly once");
1451                fired_at = Some(frame);
1452            }
1453        }
1454        assert_eq!(fired_at, Some(settings.paint_over_trigger_frames as u16));
1455        assert!(
1456            stripes.iter().all(|st| st.paint_over_sent),
1457            "all stripes latched after the repaint"
1458        );
1459    }
1460
1461    /// Hash-damage sources (X11) take the sequential-scan fast path: static frames advance
1462    /// the countdown and fire the paint-over exactly once, the quiescent tail emits nothing,
1463    /// and a subsequent content change is still detected and encoded (streak state reset by
1464    /// the fast path must not swallow the wake-up).
1465    #[test]
1466    fn hash_scan_idles_after_paintover_and_wakes_on_change() {
1467        use crate::RustCaptureSettings;
1468        let (w, h) = (64, 128);
1469        let static_px = vec![128u8; (w * h * 4) as usize];
1470        let changed_px = vec![200u8; (w * h * 4) as usize];
1471        let settings = RustCaptureSettings {
1472            width: w,
1473            height: h,
1474            output_mode: 0,
1475            jpeg_quality: 60,
1476            paint_over_jpeg_quality: 90,
1477            use_paint_over_quality: true,
1478            paint_over_trigger_frames: 5,
1479            damage_block_threshold: 10,
1480            damage_block_duration: 10,
1481            ..Default::default()
1482        };
1483        let mut stripes = Vec::new();
1484        let mut carrying = 1.0f32;
1485        let first = super::encode_cpu(
1486            &mut stripes, &mut carrying, &static_px, w, h, &[], &settings, 0, false, true, false,
1487        );
1488        assert!(!first.is_empty(), "first frame hashes as changed and encodes");
1489
1490        let mut fired_at = None;
1491        for frame in 1..=20u16 {
1492            let out = super::encode_cpu(
1493                &mut stripes, &mut carrying, &static_px, w, h, &[], &settings, frame, false, true, false,
1494            );
1495            if !out.is_empty() {
1496                assert!(fired_at.is_none(), "paint-over must fire exactly once while static");
1497                fired_at = Some(frame);
1498            }
1499        }
1500        assert_eq!(fired_at, Some(settings.paint_over_trigger_frames as u16));
1501
1502        let woke = super::encode_cpu(
1503            &mut stripes, &mut carrying, &changed_px, w, h, &[], &settings, 21, false, true, false,
1504        );
1505        assert!(!woke.is_empty(), "content change after idle must encode");
1506    }
1507
1508    /// Total rows covered by a geometry — the sum of all stripe heights.
1509    fn covered(geoms: &[(usize, usize)]) -> usize {
1510        geoms.iter().map(|&(_, h)| h).sum()
1511    }
1512
1513    /// Assert the stripes tile the frame with no gaps or overlap: each stripe's `y_start`
1514    /// equals the running sum of the preceding heights.
1515    fn assert_contiguous(geoms: &[(usize, usize)]) {
1516        let mut y = 0;
1517        for &(sy, sh) in geoms {
1518            assert_eq!(sy, y, "stripes must be contiguous");
1519            y += sh;
1520        }
1521    }
1522
1523    /// JPEG geometry covers the full frame height with contiguous stripes, across a range of
1524    /// heights (odd ones included) and stripe counts.
1525    #[test]
1526    fn jpeg_covers_every_row_including_odd() {
1527        for &h in &[1usize, 63, 720, 721, 1079, 1080, 1081] {
1528            for &n in &[1usize, 2, 3, 8, 16] {
1529                let g = compute_stripe_geometries(h, n, 0);
1530                assert_eq!(g.len(), n);
1531                assert_eq!(covered(&g), h, "JPEG must cover full height h={} n={}", h, n);
1532                assert_contiguous(&g);
1533            }
1534        }
1535    }
1536
1537    /// H.264 geometry yields even, contiguous stripe heights that cover the whole frame
1538    /// except at most one trailing odd row, across a range of heights and stripe counts.
1539    #[test]
1540    fn h264_stripes_even_and_within_bounds() {
1541        for &h in &[64usize, 720, 721, 1080, 1081] {
1542            for &n in &[1usize, 2, 8] {
1543                let g = compute_stripe_geometries(h, n, 1);
1544                assert_eq!(g.len(), n);
1545                for &(_, sh) in &g {
1546                    assert_eq!(sh % 2, 0, "H.264 stripe heights must be even h={} n={}", h, n);
1547                }
1548                assert_contiguous(&g);
1549                assert!(covered(&g) <= h);
1550                assert!(h - covered(&g) <= 1, "at most one trailing odd row uncovered");
1551            }
1552        }
1553    }
1554}
1555
1556#[cfg(test)]
1557mod qp_bound_sweep {
1558    //! Invariants under test: the CBR QP clamp reaches libx264/OpenH264 (a max clamp must
1559    //! raise worst-case fidelity on rate-starved text at the cost of bitrate overshoot;
1560    //! a min clamp must cut spend on over-budgeted content) and defaults (0) leave the
1561    //! encoders' own behavior untouched. Each encoder is swept separately: the OpenH264
1562    //! sweep runs in every build (the crate is a dev-dependency), the x264 one needs `gpl`.
1563    #[cfg(feature = "gpl")]
1564    use super::H264EncoderWrapper;
1565    use crate::encoders::oh264::Openh264Encoder;
1566    use crate::RustCaptureSettings;
1567    use openh264::decoder::Decoder;
1568    use openh264::formats::YUVSource;
1569
1570    const W: usize = 1280;
1571    const H: usize = 720;
1572    const FRAMES: usize = 60;
1573
1574    /// Build a scrolling terminal-like luma frame: an 8x12 glyph grid seeded by an LCG and
1575    /// scrolled 4 px per frame — the worst case for screen-share rate control, with dense
1576    /// high-contrast detail (~40% lit pixels per glyph row) under full-frame motion.
1577    fn text_luma(frame: usize) -> Vec<u8> {
1578        let mut y = vec![18u8; W * H];
1579        let scroll = frame * 4;
1580        for row in 0..H {
1581            let srow = row + scroll;
1582            let cell_y = srow / 12;
1583            let in_glyph_y = srow % 12;
1584            if in_glyph_y >= 10 {
1585                continue;
1586            }
1587            for col in 0..W {
1588                let cell_x = col / 8;
1589                let in_glyph_x = col % 8;
1590                if in_glyph_x >= 7 {
1591                    continue;
1592                }
1593                let mut s = (cell_x as u32)
1594                    .wrapping_mul(2654435761)
1595                    .wrapping_add((cell_y as u32).wrapping_mul(40503))
1596                    .wrapping_add(1);
1597                s ^= s << 13;
1598                s ^= s >> 17;
1599                s ^= s << 5;
1600                if (s >> ((in_glyph_y * 3 + in_glyph_x) % 29)) & 1 == 1 {
1601                    y[row * W + col] = 224;
1602                }
1603            }
1604        }
1605        y
1606    }
1607
1608    /// Encode `FRAMES` scrolling-text luma frames through the x264 stripe encoder at the
1609    /// given rate-control settings (constant grey chroma), returning each frame's raw bitstream.
1610    #[cfg(feature = "gpl")]
1611    fn encode_x264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
1612        let mut enc = H264EncoderWrapper::new(
1613            W as i32, H as i32, crf, false, 60.0, 4, cbr, kbps, 50, min_qp, max_qp,
1614        )
1615        .expect("x264 init");
1616        let u = vec![128u8; (W / 2) * (H / 2)];
1617        let v = vec![128u8; (W / 2) * (H / 2)];
1618        (0..FRAMES)
1619            .map(|i| {
1620                let y = text_luma(i);
1621                let mut out = Vec::new();
1622                enc.encode_with_headers(
1623                    &y, &u, &v, W as i32, (W / 2) as i32, (W / 2) as i32,
1624                    i as i64, i == 0, &[], true, &mut out,
1625                );
1626                out
1627            })
1628            .collect()
1629    }
1630
1631    /// Encode the same scrolling-text sequence through the OpenH264 full-frame encoder (luma
1632    /// broadcast to a grey BGRA frame), returning each frame's bitstream for comparison with the
1633    /// x264 run.
1634    fn encode_oh264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
1635        let s = RustCaptureSettings {
1636            width: W as i32,
1637            height: H as i32,
1638            target_fps: 60.0,
1639            output_mode: 1,
1640            video_cbr_mode: cbr,
1641            video_bitrate_kbps: kbps,
1642            video_crf: crf,
1643            video_min_qp: min_qp,
1644            video_max_qp: max_qp,
1645            ..Default::default()
1646        };
1647        let mut enc = Openh264Encoder::new(&s).expect("oh264 init");
1648        (0..FRAMES)
1649            .map(|i| {
1650                let y = text_luma(i);
1651                let mut bgra = vec![255u8; W * H * 4];
1652                for (px, &l) in bgra.chunks_exact_mut(4).zip(y.iter()) {
1653                    px[0] = l;
1654                    px[1] = l;
1655                    px[2] = l;
1656                }
1657                enc.encode_host_argb(&bgra, W * 4, i as u64, i == 0, false)
1658                    .expect("oh264 encode")
1659            })
1660            .collect()
1661    }
1662
1663    /// Decode a sequence of H.264 frames back to tightly-packed luma planes (dropping empty
1664    /// frames and the decoded chroma) for PSNR comparison.
1665    fn decode_luma(frames: &[Vec<u8>]) -> Vec<Vec<u8>> {
1666        let mut dec = Decoder::new().expect("decoder");
1667        let mut out = Vec::new();
1668        for f in frames {
1669            if f.is_empty() {
1670                continue;
1671            }
1672            if let Ok(Some(img)) = dec.decode(f) {
1673                let (w, h) = img.dimensions();
1674                let stride = img.strides().0;
1675                let mut y = vec![0u8; w * h];
1676                for r in 0..h {
1677                    y[r * w..r * w + w].copy_from_slice(&img.y()[r * stride..r * stride + w]);
1678                }
1679                out.push(y);
1680            }
1681        }
1682        out
1683    }
1684
1685    /// Mean per-frame luma PSNR (dB) between two decoded sequences, treating a zero-MSE frame
1686    /// as 99 dB.
1687    fn mean_psnr(a: &[Vec<u8>], b: &[Vec<u8>]) -> f64 {
1688        let n = a.len().min(b.len());
1689        let mut acc = 0.0;
1690        for i in 0..n {
1691            let mse: f64 = a[i]
1692                .iter()
1693                .zip(b[i].iter())
1694                .map(|(&x, &y)| {
1695                    let d = x as f64 - y as f64;
1696                    d * d
1697                })
1698                .sum::<f64>()
1699                / a[i].len() as f64;
1700            acc += if mse <= 0.0 { 99.0 } else { 10.0 * (255.0f64 * 255.0 / mse).log10() };
1701        }
1702        acc / n.max(1) as f64
1703    }
1704
1705    /// Average encoded bitrate (kbps) of a frame sequence, assuming 60 fps playback.
1706    fn kbps(frames: &[Vec<u8>]) -> f64 {
1707        frames.iter().map(|f| f.len()).sum::<usize>() as f64 * 8.0 * 60.0
1708            / FRAMES as f64
1709            / 1000.0
1710    }
1711
1712    /// Diagnostic that the CBR QP clamp is actually plumbed through to x264, printing a
1713    /// bitrate/PSNR table on scrolling text and asserting the effect.
1714    ///
1715    /// Encodes worst-case scrolling text at 2 Mbps CBR across a sweep of `max_qp` values (plus a
1716    /// separate `min_qp` sweep on an over-provisioned 12 Mbps budget), measuring luma PSNR against a
1717    /// near-lossless CRF-12 reference from the same encoder so colour-conversion differences cancel
1718    /// out. Capping `max_qp` at 30 on rate-starved content must lift fidelity by more than 0.5 dB
1719    /// over the unclamped run — proving the clamp reaches the encoder rather than being silently
1720    /// dropped (paid for in bitrate overshoot).
1721    #[cfg(feature = "gpl")]
1722    #[test]
1723    fn cbr_qp_bound_sweep_x264() {
1724        let reference = decode_luma(&encode_x264(false, 0, 12, 0, 0));
1725
1726        println!("scrolling-text 720p60 @ 2 Mbps CBR, x264 (PSNR vs own CRF-12 decode):");
1727        let mut rows = Vec::new();
1728        for &max_qp in &[0i32, 45, 40, 35, 30] {
1729            let x = encode_x264(true, 2000, 25, 0, max_qp);
1730            let psnr = mean_psnr(&decode_luma(&x), &reference);
1731            println!("  max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB", max_qp, kbps(&x), psnr);
1732            rows.push((max_qp, psnr));
1733        }
1734        println!("scrolling-text 720p60 @ 12 Mbps CBR, x264 min-QP sweep:");
1735        for &min_qp in &[0i32, 10, 15] {
1736            let x = encode_x264(true, 12000, 25, min_qp, 0);
1737            let psnr = mean_psnr(&decode_luma(&x), &reference);
1738            println!("  min_qp {:>2}: {:>8.1} kbps / {:>5.2} dB", min_qp, kbps(&x), psnr);
1739        }
1740
1741        let base = rows[0].1;
1742        let capped = rows.last().unwrap().1;
1743        assert!(
1744            capped > base + 0.5,
1745            "x264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
1746        );
1747    }
1748
1749    /// The OpenH264 counterpart of [`cbr_qp_bound_sweep_x264`]: the software H.264 sweep of a
1750    /// build without the `gpl` feature, where OpenH264 is the encoder behind every stripe.
1751    ///
1752    /// Same scrolling-text workload and 2 Mbps CBR `max_qp` sweep, with luma PSNR measured against
1753    /// this encoder's own near-lossless QP-12 reference. Capping `max_qp` at 30 must lift fidelity
1754    /// by more than 0.5 dB over the unclamped run.
1755    #[test]
1756    fn cbr_qp_bound_sweep_openh264() {
1757        let reference = decode_luma(&encode_oh264(false, 0, 12, 0, 0));
1758
1759        println!("scrolling-text 720p60 @ 2 Mbps CBR, oh264 (PSNR vs own QP-12 decode):");
1760        let mut rows = Vec::new();
1761        for &max_qp in &[0i32, 45, 40, 35, 30] {
1762            let o = encode_oh264(true, 2000, 25, 0, max_qp);
1763            let psnr = mean_psnr(&decode_luma(&o), &reference);
1764            println!("  max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB", max_qp, kbps(&o), psnr);
1765            rows.push((max_qp, psnr));
1766        }
1767
1768        let base = rows[0].1;
1769        let capped = rows.last().unwrap().1;
1770        assert!(
1771            capped > base + 0.5,
1772            "oh264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
1773        );
1774    }
1775
1776    /// After a live frame-rate change the CBR stream still tracks the configured bitrate.
1777    ///
1778    /// x264's per-frame CBR budget is `bitrate / fps`, so halving the frame rate at a fixed kbps
1779    /// budget must roughly double each encoded frame while the per-second bitrate holds. This encodes
1780    /// incompressible full-frame noise at 4 Mbps CBR (content the rate controller cannot undershoot,
1781    /// so per-frame size sits at the budget), measures the mean encoded frame size at 60 fps, drops
1782    /// to 30 fps through `reconfigure_rate`, and requires the per-frame size to roughly double — so
1783    /// the per-second bitrate is preserved rather than collapsing to half, which is what the pre-fix
1784    /// path did by leaving the session budgeting for 60 fps (`x264_encoder_reconfig` never applies a
1785    /// frame-rate change). Single-threaded so the rate-control measurement is deterministic; warmup
1786    /// frames are discarded so the ABR controller and the post-reopen IDR do not skew the mean.
1787    #[cfg(feature = "gpl")]
1788    #[test]
1789    fn cbr_bitrate_tracks_configured_rate_after_fps_change() {
1790        const TARGET_KBPS: i32 = 4000;
1791        const WARMUP: usize = 24;
1792        const MEASURED: usize = 96;
1793        let u = vec![128u8; (W / 2) * (H / 2)];
1794        let v = vec![128u8; (W / 2) * (H / 2)];
1795        let vbv = |fps: f64| {
1796            (crate::encoders::vbv_bits((TARGET_KBPS as u32) * 1000, fps, 0.0, 0.0) / 1000).max(1)
1797                as i32
1798        };
1799        // A fresh incompressible luma plane every frame, so inter-prediction cannot cheapen a
1800        // frame and CBR must spend its whole per-frame budget: the per-frame size then reads the
1801        // budget directly, which is exactly what a frame-rate change is supposed to move.
1802        let noise_luma = |frame: usize| -> Vec<u8> {
1803            let mut y = vec![0u8; W * H];
1804            let mut s = (frame as u32).wrapping_mul(2654435761).wrapping_add(1);
1805            for p in y.iter_mut() {
1806                s ^= s << 13;
1807                s ^= s >> 17;
1808                s ^= s << 5;
1809                *p = (s >> 24) as u8;
1810            }
1811            y
1812        };
1813
1814        let mut enc =
1815            H264EncoderWrapper::new(W as i32, H as i32, 25, false, 60.0, 1, true, TARGET_KBPS, vbv(60.0), 0, 0)
1816                .expect("x264 init");
1817
1818        let measure = |enc: &mut H264EncoderWrapper, start: usize| -> f64 {
1819            let mut bytes = 0usize;
1820            let mut counted = 0usize;
1821            for i in start..start + WARMUP + MEASURED {
1822                let y = noise_luma(i);
1823                let mut out = Vec::new();
1824                enc.encode_with_headers(
1825                    &y, &u, &v, W as i32, (W / 2) as i32, (W / 2) as i32, i as i64, i == start,
1826                    &[], true, &mut out,
1827                );
1828                if i >= start + WARMUP && !out.is_empty() {
1829                    bytes += out.len();
1830                    counted += 1;
1831                }
1832            }
1833            bytes as f64 / counted.max(1) as f64
1834        };
1835
1836        let per_frame_60 = measure(&mut enc, 0);
1837        enc.reconfigure_rate(TARGET_KBPS, vbv(30.0), 30.0);
1838        let per_frame_30 = measure(&mut enc, 1000);
1839
1840        let eff_60 = per_frame_60 * 8.0 * 60.0 / 1000.0;
1841        let eff_30 = per_frame_30 * 8.0 * 30.0 / 1000.0;
1842        println!(
1843            "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)"
1844        );
1845
1846        let ratio = per_frame_30 / per_frame_60.max(1.0);
1847        assert!(
1848            (1.5..2.6).contains(&ratio),
1849            "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"
1850        );
1851        assert!(
1852            eff_30 > eff_60 * 0.75,
1853            "30fps effective bitrate {eff_30:.0} kbps collapsed from the 60fps {eff_60:.0} kbps; fps change did not re-budget the bitrate"
1854        );
1855    }
1856}