Skip to main content

pixelflux/
pipeline.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//! Frame-processing policy shared by the two capture backends. It lives in its own module for one
8//! reason: the Wayland path (dmabuf, compositor damage) and the X11 path (host-ARGB, stripe-hash
9//! damage) capture pixels in completely different ways, but a viewer must never be able to tell
10//! which one produced a frame — a paint-over refresh or a recovery keyframe has to behave
11//! identically either way. Keeping the decision logic here, source-agnostic, is what guarantees it.
12
13use crate::RustCaptureSettings;
14use crate::encoders::software::{
15    EncodedStripe, StripeState, acknowledge_reference, encode_cpu, invalidate_reference,
16    stripes_held_still,
17};
18use crate::encoders::{self, Codec, FrameEncoder, FrameSource};
19use smithay::utils::{Physical, Rectangle};
20use std::sync::Arc;
21
22/// How much of a frame changed since the frame before it, as far as its capture can tell.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub enum Damage {
25    /// Nothing changed.
26    None,
27    /// This fraction of the frame's area changed, in `(0, 1]`.
28    Area(f32),
29    /// Something changed, and the capture cannot say how much (NvFBC's new-frame report).
30    Unknown,
31}
32
33/// A change covering at most this fraction of the frame is small: it does not count as motion
34/// toward the low-motion cleanup.
35const SMALL_AREA: f32 = 1.0 / 16.0;
36
37/// How much of a screen a change of unknown extent counts for toward a key-frame cleanup, from
38/// the third frame of a run of changed frames on: sustained motion, where an isolated change
39/// (a caret, a clock, a keystroke) counts for nothing.
40const UNKNOWN_MASS: f32 = 1.0 / 16.0;
41const UNKNOWN_RUN: u32 = 3;
42
43/// The change since the last key-frame cleanup, in screens, past which a region still for
44/// `KEY_STILL_TRIGGERS` trigger periods is cleaned up by a key frame.
45const KEY_CLEANUP_MASS: f32 = 0.5;
46
47/// The trigger periods a region holds still before its key-frame cleanup.
48const KEY_STILL_TRIGGERS: u32 = 4;
49
50/// A region that keeps changing is still due a cleanup once this many trigger periods passed
51/// since it first changed, where at most a quarter of its frames carry more than a small change,
52/// on an average over as many frames (`StripeState::motion`), so the motion that went before
53/// (a scroll) ages out of it while a caret blinks.
54const LOW_MOTION_TRIGGERS: u32 = 4;
55const LOW_MOTION_SHARE: f32 = 0.25;
56
57/// A constant-rate cleanup that runs through the rate control keeps the frames flowing until the
58/// rate control codes the region at the paint-over quantizer or finer in a frame under this share
59/// of the frame budget: it has nothing left to refine.
60const CONVERGED_SHARE: f64 = 0.25;
61
62/// Trigger periods of such small frames at a quantizer still coarser than the paint-over one after
63/// which the rate control counts as stalled: it stopped refining short of the paint-over quality,
64/// as NVENC's does at 2 Mbit/s at 1080p.
65const STALL_TRIGGERS: u32 = 2;
66
67/// Trigger periods a constant-rate cleanup through the rate control has to converge before it
68/// falls back to a held refresh: at 8 Mbit/s NVENC converges on a 1080p screen of text within
69/// four, and where the rate is low for the resolution it crawls for seconds.
70const FALLBACK_TRIGGERS: u32 = 6;
71
72/// Seconds a constant-rate cleanup through the rate control flows at most.
73const CONVERGE_S: f64 = 10.0;
74
75/// Frame budgets the rate control's frames of a still screen exceed for a trigger period when it
76/// is pinned at its coarsest quantizer, refining nothing within the rate: NVENC's H.264 codes a
77/// still screen of texture at 0.1 Mbit/s at 1080p in 3.3 kB frames, 16 budgets, at quantizer 51,
78/// where at 2 Mbit/s single frames after a change reach 4.5 budgets.
79const OVERSHOOT_BUDGETS: f64 = 2.0;
80
81/// Bytes a slice that refines nothing comes to at most: a frame of x264's four slices skipping
82/// every block takes 61 to 66 at 720p and 1080p with the wire header, a third of the budget at
83/// 0.1 Mbit/s, so never a small frame.
84pub(crate) const EMPTY_SLICE_BYTES: usize = 32;
85
86/// Seconds of such frames after which a constant-rate cleanup through x264 ends: x264 keeps its
87/// quantizer on a screen that does not change, and at 0.1 Mbit/s refines one in frames as far
88/// as 2.4 s apart. A session that holds no quantizer and measures nothing ends the same way
89/// once its frames at its coarsest have kept the size of the one before as long
90/// (`SAME_FRAME_SHARE`).
91pub(crate) const EMPTY_S: f64 = 3.0;
92
93/// The share of the frame before it within which a frame of a still screen codes nothing new,
94/// and the quality index from which its rate control sits at its coarsest: at 0.1 Mbit/s at
95/// 1080p libvpx's VP9 frames are 266 bytes each at index 51, or 220-221 at 46-47, and x265's
96/// 224 at 50-51, its strict constant rate padding them with filler, while the picture stays
97/// level, so never small against the 208-byte budget. From 1 Mbit/s their rate controls code a
98/// still screen at 36 and finer, x265's padded frames again of one size while it refines.
99const SAME_FRAME_SHARE: f64 = 0.125;
100const SAME_FRAME_QUALITY: u32 = 45;
101
102/// The same for a session that holds no quantizer but says the one it codes at or measures its
103/// pictures, which has no refresh to fall back to and ends its cleanup at the paint-over
104/// quantizer or once the picture stops improving: at 2 Mbit/s at 1080p x265's rate control
105/// refines a screen of text by 0.7 dB a second, still short of that quality after ten. A band
106/// sweep runs this long at most too.
107const REFINE_S: f64 = 30.0;
108
109/// Seconds the finest quantizer such a session has coded the still screen at stands before its
110/// rate control counts as done with the picture, where the session says its quantizer and does
111/// not measure: at 0.25 Mbit/s at 1080p x265 takes a step finer every 2 to 8 s for a tenth of
112/// a dB a second, and libvpx's VP9 one every 6 s after its first ten, each for all of `REFINE_S`
113/// at the full rate, where at 2 Mbit/s x265 takes one every 2 s for its first twenty.
114const LEVEL_S: f64 = 4.0;
115
116/// Where the encoder holds a band of the picture (`EncoderQuality::band`), the refresh that
117/// falls back from a constant-rate cleanup sweeps the picture a band a frame in raster order,
118/// the first `FIRST_BAND` of it and each next sized from the bytes of the last to
119/// `BAND_BUDGETS` frame budgets, so the stream keeps its rate: a whole refresh at the
120/// paint-over quantizer is 86 budgets of text at 2 Mbit/s at 1080p and queues for 240 ms on a
121/// 12 Mbit/s link.
122const FIRST_BAND: f64 = 1.0 / 64.0;
123const BAND_BUDGETS: f64 = 1.0;
124
125impl Damage {
126    /// The damage a set of rectangles on a `width` x `height` frame reports: their summed area,
127    /// clipped to the frame, overlaps counted twice.
128    pub fn of_rects<'a>(
129        rects: impl IntoIterator<Item = &'a smithay::utils::Rectangle<i32, smithay::utils::Physical>>,
130        width: i32,
131        height: i32,
132    ) -> Self {
133        let frame = (width.max(1) as f64) * (height.max(1) as f64);
134        let mut area = 0f64;
135        for r in rects {
136            let w = (r.loc.x + r.size.w).min(width) - r.loc.x.max(0);
137            let h = (r.loc.y + r.size.h).min(height) - r.loc.y.max(0);
138            if w > 0 && h > 0 {
139                area += w as f64 * h as f64;
140            }
141        }
142        if area > 0.0 {
143            Damage::Area((area / frame).min(1.0) as f32)
144        } else {
145            Damage::None
146        }
147    }
148
149    /// Whether anything changed.
150    pub fn is_dirty(self) -> bool {
151        self != Damage::None
152    }
153
154    /// Whether the change is more than a small one, so the frame counts as motion.
155    pub(crate) fn is_motion(self) -> bool {
156        match self {
157            Damage::None => false,
158            Damage::Area(f) => f > SMALL_AREA,
159            Damage::Unknown => true,
160        }
161    }
162}
163
164/// What the cleanup policy decided for one region's frame.
165#[derive(Clone, Copy, Debug, PartialEq)]
166pub enum Cleanup {
167    None,
168    /// Refresh predicted from the picture the client holds, at the paint-over quantizer.
169    Refresh,
170    /// Key frame at the paint-over quantizer.
171    Key,
172}
173
174/// The still-screen cleanup of one region (a whole frame, or a stripe): track how long and how
175/// much it has been changing, and say when it is due a cleanup and of which kind. `keys` false
176/// (JPEG, whose every stripe is a picture of its own, and a constant-rate session cleaned up
177/// through its rate control) never answers `Cleanup::Key`.
178///
179/// Static is read from the content (`damage`), never from what was sent, so a stream that
180/// encodes every frame (Turbo) sees the screen go still exactly like one that sends only changes.
181/// The cleanup comes in two steps. A region that changed is refreshed once it has held still for
182/// `trigger` frames: a frame predicted from the picture the client holds, which re-codes only
183/// what changed. Once the change since the last key-frame cleanup adds up to
184/// `KEY_CLEANUP_MASS` of a screen and the region has held still for `KEY_STILL_TRIGGERS` trigger
185/// periods, it is cleaned up by a key frame, which leaves nothing of the motion behind and
186/// resynchronizes every viewer, but costs a whole picture: so it waits for a screen still long
187/// enough that the user is reading it, and a caret or a clock, which change little, never pays
188/// for one. A region that keeps changing never holds still, so once `LOW_MOTION_TRIGGERS`
189/// trigger periods have passed since it first changed and at most a quarter of its recent frames
190/// carried more than a small change (a blinking caret, a ticking clock) it is cleaned up then,
191/// by a key frame when the change adds up to one.
192///
193/// `improves` false (the paint-over is off, or a constant rate already codes the region finer)
194/// keeps the bookkeeping and answers `Cleanup::None`, and a cleanup that falls due then restarts
195/// the count of frames since the region first changed: the region is clean, so those frames are
196/// no low motion, and counted, they would have the first frame of the next large change keyed,
197/// the whole picture at the paint-over quantizer the moment a window opens. `now` false (a
198/// stripe whose cleanup is staggered to a later frame) answers `Cleanup::None` and leaves the
199/// cleanup pending.
200pub fn cleanup_due(
201    st: &mut StripeState,
202    trigger: u32,
203    improves: bool,
204    now: bool,
205    keys: bool,
206    damage: Damage,
207) -> Cleanup {
208    if damage.is_dirty() {
209        st.dirty_run = st.dirty_run.saturating_add(1);
210        st.no_motion_frame_count = 0;
211        st.paint_over_sent = false;
212        st.change_mass += match damage {
213            Damage::Area(f) => f,
214            _ if st.dirty_run >= UNKNOWN_RUN => UNKNOWN_MASS,
215            _ => 0.0,
216        };
217    } else {
218        st.dirty_run = 0;
219        st.no_motion_frame_count = st.no_motion_frame_count.saturating_add(1);
220    }
221    if !st.paint_over_sent {
222        st.unclean_frames = st.unclean_frames.saturating_add(1);
223    }
224    st.motion = recent_motion(st.motion, trigger.max(1), damage.is_motion());
225    let due = due_cleanup(
226        st,
227        trigger.max(1),
228        keys,
229        st.no_motion_frame_count,
230        st.unclean_frames,
231        st.motion,
232    );
233    if due == Cleanup::None {
234        return Cleanup::None;
235    }
236    if !improves {
237        st.unclean_frames = 0;
238        return Cleanup::None;
239    }
240    if !now {
241        return Cleanup::None;
242    }
243    st.paint_over_sent = true;
244    st.unclean_frames = 0;
245    if due == Cleanup::Key {
246        st.change_mass = 0.0;
247    }
248    due
249}
250
251/// The share of a region's recent frames that carried more than a small change, `motion` taking
252/// in one more frame: an average over `LOW_MOTION_TRIGGERS` trigger periods.
253fn recent_motion(motion: f32, trigger: u32, moved: bool) -> f32 {
254    motion + (moved as u8 as f32 - motion) / (trigger * LOW_MOTION_TRIGGERS) as f32
255}
256
257/// The cleanup a region is due with `still` frames held still, `unclean` frames since it first
258/// changed after its last cleanup, and `motion` the share of its recent frames in motion.
259fn due_cleanup(
260    st: &StripeState,
261    trigger: u32,
262    keys: bool,
263    still: u32,
264    unclean: u32,
265    motion: f32,
266) -> Cleanup {
267    let low_motion = !st.paint_over_sent
268        && unclean >= trigger * LOW_MOTION_TRIGGERS
269        && motion <= LOW_MOTION_SHARE;
270    if keys
271        && st.change_mass >= KEY_CLEANUP_MASS
272        && (still >= trigger * KEY_STILL_TRIGGERS || low_motion)
273    {
274        Cleanup::Key
275    } else if !st.paint_over_sent && (still >= trigger || low_motion) {
276        Cleanup::Refresh
277    } else {
278        Cleanup::None
279    }
280}
281
282/// Whether `cleanup_due` would answer a cleanup for a frame that changed nothing, without moving
283/// the bookkeeping: what an idle frame has to know before it skips a region.
284pub fn cleanup_pending(st: &StripeState, trigger: u32, enabled: bool, keys: bool) -> bool {
285    let unclean = st.unclean_frames.saturating_add(!st.paint_over_sent as u32);
286    let motion = recent_motion(st.motion, trigger.max(1), false);
287    enabled
288        && due_cleanup(
289            st,
290            trigger.max(1),
291            keys,
292            st.no_motion_frame_count.saturating_add(1),
293            unclean,
294            motion,
295        ) != Cleanup::None
296}
297
298/// What an encoder says about the frames it codes, which the cleanup reads: the quality index
299/// its rate control last coded a frame at, where it reports one (`FrameEncoder::last_quality`),
300/// the bytes of that frame, where it reports them too (`FrameEncoder::last_size`), whether it
301/// holds a frame at a quantizer it is asked for (`FrameEncoder::holds_quantizer`), whether a
302/// change of its constant quality re-opens it on a key frame (`FrameEncoder::reopens_on_quality`),
303/// whether its constant-rate cleanup ends on a key frame (`FrameEncoder::cleans_up_with_key`),
304/// where it holds a band of a frame at that quantizer (`FrameEncoder::band_size`), the
305/// bytes of its last held frame, and, where it measures a frame's reconstruction against its
306/// source when asked (`FrameEncoder::measures`), the last measurement.
307#[derive(Clone, Copy, Debug, PartialEq)]
308pub struct EncoderQuality {
309    pub last: Option<u32>,
310    pub bytes: Option<usize>,
311    pub holds: bool,
312    pub reopens: bool,
313    pub keys: bool,
314    pub band: Option<usize>,
315    pub measures: bool,
316    pub psnr: Option<f32>,
317}
318
319impl EncoderQuality {
320    /// What `encoder` says.
321    pub fn of(encoder: &FrameEncoder) -> Self {
322        Self {
323            last: encoder.last_quality(),
324            bytes: encoder.last_size(),
325            holds: encoder.holds_quantizer(),
326            reopens: encoder.reopens_on_quality(),
327            keys: encoder.cleans_up_with_key(),
328            band: encoder.band_size(),
329            measures: encoder.measures(),
330            psnr: encoder.last_psnr(),
331        }
332    }
333
334    /// Whether a constant-rate cleanup runs through this encoder's rate control: it says the
335    /// bytes of its frames, so the cleanup can tell when the rate control has stopped refining
336    /// a still screen, and with their quantizer whether that is at the paint-over quality.
337    pub fn converges(&self) -> bool {
338        self.bytes.is_some()
339    }
340}
341
342/// Where a rate control that the cleanup runs through stands after its last frame, of `bytes`
343/// at quality index `last`, against the paint-over quantizer and `budget` bytes a frame.
344#[derive(Clone, Copy, Debug, PartialEq)]
345pub enum Convergence {
346    /// Still refining: its last frame carried a share of the budget.
347    Refining,
348    /// A small frame at the paint-over quantizer or finer: nothing left to refine.
349    Converged,
350    /// A small frame at a coarser quantizer, or at one the encoder does not say: refining
351    /// nothing more this frame.
352    Idle,
353}
354
355/// Where the rate control stands (`Convergence`).
356pub fn convergence(
357    last: Option<u32>,
358    bytes: Option<usize>,
359    paint: u32,
360    budget: f64,
361) -> Convergence {
362    match bytes {
363        Some(b) if (b as f64) <= budget * CONVERGED_SHARE => {
364            if last.is_some_and(|q| q <= paint) {
365                Convergence::Converged
366            } else {
367                Convergence::Idle
368            }
369        }
370        _ => Convergence::Refining,
371    }
372}
373
374/// Trigger periods between two measurements of a picture a constant-rate cleanup is refining
375/// (`FrameEncoder::last_psnr`), the gain in dB under which one finds the picture level with
376/// the one before, and the level measurements in a row after which the rate control is done
377/// with it. A rate control that has a whole screen of dense text to refine at 2 Mbit/s at
378/// 1080p gains 0.2 dB a second at first, coding a few blocks of it a frame, and one that is
379/// done with a screen under 0.06.
380const PLATEAU_TRIGGERS: u32 = 4;
381const PLATEAU_GAIN_DB: f32 = 0.05;
382const PLATEAU_CHECKS: u32 = 2;
383
384/// Bytes a frame of the constant-rate target, at `kbps` and `fps`.
385pub fn frame_budget(kbps: i32, fps: f64) -> f64 {
386    kbps.max(1) as f64 * 125.0 / fps.max(1.0)
387}
388
389/// The share of the picture the next band of a sweep covers, after one of `size` whose held frame
390/// came out at `bytes` (`FIRST_BAND` onward): `BAND_BUDGETS` of `budget`, at most doubling or
391/// halving a frame.
392fn band_share(size: f64, bytes: Option<usize>, budget: f64) -> f64 {
393    let next = match bytes {
394        Some(b) if b > 0 => (size * BAND_BUDGETS * budget / b as f64).clamp(size / 2.0, size * 2.0),
395        _ => size,
396    };
397    next.clamp(1.0 / 4096.0, 1.0)
398}
399
400/// One frame of a cleanup that measures the picture it is refining, where the encoder can
401/// (`FrameEncoder::measure`): whether this frame is to be measured, one every `period`, and
402/// whether the measurement of the last such frame, `psnr`, was the `PLATEAU_CHECKS`th in a
403/// row to gain less than `PLATEAU_GAIN_DB` on the one before, the rate control having
404/// nothing left to give the picture.
405fn plateau(st: &mut StripeState, psnr: Option<f32>, period: u32) -> (bool, bool) {
406    if std::mem::take(&mut st.measuring)
407        && let Some(now) = psnr
408    {
409        let level = st
410            .measured
411            .is_some_and(|before| now - before < PLATEAU_GAIN_DB);
412        st.level_checks = if level { st.level_checks + 1 } else { 0 };
413        st.measured = Some(now);
414    }
415    if st.measure_in == 0 {
416        st.measure_in = period;
417        st.measuring = true;
418    } else {
419        st.measure_in -= 1;
420    }
421    (st.measuring, st.level_checks >= PLATEAU_CHECKS)
422}
423
424/// Frames a constant-rate cleanup through the rate control flows at most (`CONVERGE_S`).
425pub fn converge_frames(settings: &RustCaptureSettings) -> i32 {
426    (CONVERGE_S * settings.target_fps.max(1.0)).round() as i32
427}
428
429/// `converge_frames` for the session `encoder` describes (`REFINE_S`).
430fn cleanup_frames(settings: &RustCaptureSettings, encoder: EncoderQuality) -> i32 {
431    if encoder.holds || (encoder.last.is_none() && !encoder.measures) {
432        converge_frames(settings)
433    } else {
434        (REFINE_S * settings.target_fps.max(1.0)).round() as i32
435    }
436}
437
438/// Whether a cleanup improves the picture at all: at a constant quality where the paint-over
439/// quality is finer than the session's (the cleanup then moves the session's quality, see
440/// `decide_constant_quality`); at a constant rate where it is finer than the quality index the
441/// rate control last coded at, or the encoder does not say. A rate control that has already
442/// refined a still screen past it (a constant rate refines every frame it sends) is left alone:
443/// holding the paint-over quantizer there would coarsen the picture. A constant-rate encoder
444/// that holds no quantizer is cleaned up by the rate control's own frames, a refresh and its
445/// burst, which it refines; a key frame there would come out at the rate control's quantizer,
446/// which a small buffer starves.
447pub fn paint_over_improves(settings: &RustCaptureSettings, encoder: EncoderQuality) -> bool {
448    let paint = settings.video_paintover_crf.max(0) as u32;
449    settings.use_paint_over_quality
450        && if settings.video_cbr_mode {
451            encoder.last.is_none_or(|q| q > paint)
452        } else {
453            settings.video_paintover_crf < settings.video_crf
454        }
455}
456
457/// Outcome of the full-frame send decision produced by `decide_hw_fullframe`.
458pub struct HwFrameDecision {
459    pub send: bool,
460    pub force_idr: bool,
461    /// The session quality index the frame is encoded at.
462    pub target_qp: u32,
463    /// The quality index this frame is held at whatever the rate control: the cleanup of a
464    /// still screen, handed to the encoder's `hold_quantizer`.
465    pub hold_qp: Option<u32>,
466    /// The share of the picture, from and to in raster order, `hold_qp` covers, the rest of the
467    /// frame held at the coarsest quantizer; `None` for the whole picture.
468    pub hold_band: Option<(f64, f64)>,
469    /// Whether the encoder is asked to measure this frame's reconstruction
470    /// (`FrameEncoder::measure`).
471    pub measure: bool,
472}
473
474impl HwFrameDecision {
475    /// The quality the whole frame is coded at in place of the session's `normal` one: a
476    /// quantizer held for it, or a constant quality's paint-over one.
477    pub(crate) fn cleanup_quality(&self, normal: u32) -> Option<u32> {
478        self.hold_qp
479            .filter(|_| self.hold_band.is_none())
480            .or((self.target_qp != normal).then_some(self.target_qp))
481    }
482
483    /// Tell `encoder` what the decision asks of the frame it is about to code: the quantizer
484    /// it is held at and whether its reconstruction is measured.
485    pub fn prepare(&self, encoder: &mut FrameEncoder) {
486        if let Some(q) = self.hold_qp {
487            encoder.hold_quantizer(q, self.hold_band);
488        }
489        if self.measure {
490            encoder.measure();
491        }
492    }
493}
494
495/// Whether a scheduled keyframe is due this tick.
496///
497/// The default (`keyframe_interval_s <= 0`) is an infinite GOP with no scheduled IDRs. A positive
498/// interval buys a fixed ~N-second recovery cadence for consumers that cannot request one on demand.
499///
500/// # Arguments
501///
502/// * `settings` - Capture settings; reads `keyframe_interval_s` and `target_fps`.
503/// * `frame_counter` - Current frame number (wrapping `u16`).
504///
505/// # Returns
506///
507/// `true` if a periodic IDR should be forced on this frame.
508pub fn periodic_idr_due(settings: &RustCaptureSettings, frame_counter: u16) -> bool {
509    let secs = settings.keyframe_interval_s;
510    if secs <= 0.0 {
511        return false;
512    }
513    let safe_fps = settings.target_fps.max(1.0);
514    let interval = ((safe_fps * secs).round() as u64).max(1);
515    (frame_counter as u64).is_multiple_of(interval)
516}
517
518/// The quality index a held refresh (a predicted frame) is coded at: the paint-over one, or,
519/// where a constant rate last coded far coarser, as much finer than that as
520/// `HELD_KEY_BUDGET_S` of the target allows at six steps a doubling, since a refresh of a whole
521/// screen at the paint-over quantizer after heavy motion at a low rate would cost many frames of
522/// budget at once.
523pub fn held_refresh_quality(settings: &RustCaptureSettings, encoder: EncoderQuality) -> u32 {
524    refresh_quality(settings, encoder, 1.0)
525}
526
527/// `held_refresh_quality` from a rate control's last frame of `spent` frame budgets rather than
528/// one: a rate control pinned at its coarsest quantizer (`OVERSHOOT_BUDGETS`) refreshed in one
529/// held frame, which then costs that much more.
530fn refresh_quality(settings: &RustCaptureSettings, encoder: EncoderQuality, spent: f64) -> u32 {
531    let paint = settings.video_paintover_crf.max(0) as u32;
532    match (settings.video_cbr_mode, encoder.last) {
533        (true, Some(last)) => {
534            let frames = (crate::encoders::HELD_KEY_BUDGET_S * settings.target_fps
535                / spent.max(1.0))
536            .max(1.0);
537            paint.max(last.saturating_sub((6.0 * frames.log2()).floor() as u32))
538        }
539        _ => paint,
540    }
541}
542
543/// The send / quality / keyframe policy every full-frame encoder obeys. A constant-quality
544/// session takes `decide_constant_quality`; what follows is the constant-rate one.
545///
546/// The GOP is left infinite and an IDR is forced only when a consumer genuinely needs a fresh
547/// decode entry point or a still screen is cleaned up. Every full-frame encoder shares this one
548/// function so they cannot drift apart; the striped software path applies the same policy per
549/// stripe inside `encode_cpu`.
550///
551/// 1. **Cleanup** (`cleanup_due`): once the content stops changing, whatever Turbo sends, a
552///    refresh and, after a large change and a longer stillness, a key frame where the encoder's
553///    cleanup ends on one (`EncoderQuality::keys`), each encoded at the paint-over quantizer held
554///    for it (`hold_qp`, which the encoder applies under any rate control; a refresh of a
555///    constant-rate session coarsened to `held_refresh_quality`, a key frame capped by the encoder
556///    at `HELD_KEY_BUDGET_S`) and followed by a recovery burst. A constant-rate session whose rate
557///    control reports its frames' bytes (`EncoderQuality::converges`) is cleaned up through that
558///    rate control instead, NVENC's like this:
559///    the frames keep flowing, each within the rate control's budget, until it codes the screen
560///    at the paint-over quantizer or finer in a frame under `CONVERGED_SHARE` of the budget
561///    (`convergence`), for `CONVERGE_S` at most. A frame held at the paint-over quantizer the
562///    size of a whole refresh would otherwise queue for up to half a second on a link near the
563///    stream's rate; the rate control reaches the same picture in about a second at the
564///    default rate with every frame within its budget. Only where it has not converged within
565///    `FALLBACK_TRIGGERS` periods, or stalls short of the paint-over quality (`STALL_TRIGGERS`
566///    periods of small frames at a coarser quantizer), as where the rate is low for the
567///    resolution, is the picture refreshed at `held_refresh_quality`, which ends the cleanup: a
568///    band a frame where the encoder holds one (`FIRST_BAND`, `BAND_BUDGETS`) until the sweep
569///    has covered it or motion ends it, else one held frame. A sweep whose rest would take it
570///    past `REFINE_S` at its bands' pace, once they are sized to the budget, is coded a step
571///    coarser a band until the pace fits, and ends where that reaches the rate control's own
572///    quantizer: dense text at 0.1 and 0.25 Mbit/s never finished in 40 s at the paint-over
573///    one. At 2 Mbit/s at 1080p a key frame
574///    capped to `HELD_KEY_BUDGET_S` of the target came out coarser than the picture it cleaned
575///    (35.9 dB), where the refresh reached 44.2. A rate control whose frames of the still screen
576///    run over `OVERSHOOT_BUDGETS` for a trigger period is pinned at its coarsest quantizer and
577///    refines nothing within the rate, and every frame of a sweep would carry the rest of the
578///    picture at that cost: NVENC's at 0.1 Mbit/s codes a still 1080p screen of texture in
579///    16-budget frames, and its sweep sent 4.7 MB in 40 s without ending. Such a screen is
580///    refreshed at once in one held frame, coarsened to what `HELD_KEY_BUDGET_S` buys at those
581///    frames' size and coded again to fit it where it comes out past `HELD_REFRESH_LIMIT_S`, as
582///    dense text does, and a burst that has no refresh to fall back to ends. A session that reports
583///    its frames' bytes but holds no quantizer at a constant rate (VA-API, x265, kvazaar, libvpx's
584///    VP9) has no refresh to fall back to, so its frames flow, each the rate control's, until the
585///    picture is done: where the session measures its reconstruction against its source (VA-API,
586///    `plateau`) until that stops improving, else after a trigger period of frames at the
587///    paint-over quantizer or once the finest quantizer it has coded at has stood for
588///    `LEVEL_S`; and in either case after `STALL_TRIGGERS` periods of small frames or
589///    `REFINE_S`. A fixed burst left such a screen where the motion did (19 to 27 dB at
590///    2 Mbit/s at 1080p on iHD, x265, and SVT-AV1, each frame within its budget), and a
591///    reported quantizer alone ends it early or never: iHD's H.264 average reads 26 through
592///    a refinement from 23 to 50 dB. That cleanup then counts as settled
593///    (`StripeState::settled`), and only a change of more than a small area arms another, so
594///    a caret blinking on a clean screen costs its own frames and no more. A session that
595///    reports no bytes (Tegra, a stateful V4L2 device) gets the refresh and burst at its rate
596///    control's own quality, and no key frame. A change of more than a small area ends a
597///    cleanup through the rate control; a caret blinking or a clock ticking on the screen
598///    does not, so constant low motion is cleaned up the same way.
599/// 2. **Recovery keyframe**: a requested or scheduled IDR, at the rate control's own quality: it
600///    answers a join or a loss, often on a link that just fell behind, where a key frame the
601///    size of a cleanup would only fall behind again. On a still screen it opens the burst,
602///    which lasts until the rate control converges where the cleanup runs through it, or is
603///    pinned (`OVERSHOOT_BUDGETS`).
604/// 3. **Recovery burst**: the frames after a keyframe or cleanup on a still screen keep flowing
605///    so rate control settles, until motion resumes: after a cleanup held at the refresh's
606///    quantizer where the paint-over one still improves on the rate control's, so they go on
607///    refining the picture (a key frame capped to its budget below the paint-over quality
608///    included), and so after any keyframe of a constant-quality session; after a recovery key
609///    frame of a constant-rate session they are the rate control's.
610/// 4. **Motion**, Turbo (`video_streaming_mode`), and animated overlays send at the session's
611///    quality; a still screen outside all of the above sends nothing.
612///
613/// # Arguments
614///
615/// * `st` - Per-region state carrying the cleanup and burst bookkeeping.
616/// * `settings` - Capture settings; reads CRF, paint-over CRF, burst frames, trigger frames,
617///   streaming mode, and keyframe interval.
618/// * `frame_counter` - Current frame number (wrapping `u16`).
619/// * `damage` - What changed, from compositor damage, the X server, the driver, or a content hash.
620/// * `is_animated` - Forces a send for animated overlays.
621/// * `requested_idr` - On-demand IDR request (client join / reset / recording cadence).
622/// * `encoder` - What the encoder says of its frames (`EncoderQuality`).
623///
624/// # Returns
625///
626/// [`HwFrameDecision`] with `send`, `force_idr`, `target_qp` (the session quality index),
627/// `hold_qp` (the quantizer a cleanup holds its frame at), and `hold_band` (the share of the
628/// picture it covers).
629pub fn decide_hw_fullframe(
630    st: &mut StripeState,
631    settings: &RustCaptureSettings,
632    frame_counter: u16,
633    damage: Damage,
634    is_animated: bool,
635    requested_idr: bool,
636    encoder: EncoderQuality,
637) -> HwFrameDecision {
638    if !settings.video_cbr_mode {
639        return decide_constant_quality(
640            st,
641            settings,
642            frame_counter,
643            damage,
644            is_animated,
645            requested_idr,
646            encoder,
647        );
648    }
649    let normal_qp = settings.video_crf as u32;
650    let paint_qp = settings.video_paintover_crf as u32;
651    let burst = settings.video_paintover_burst_frames;
652    let improves = paint_over_improves(settings, encoder);
653    let recovery_idr = requested_idr || periodic_idr_due(settings, frame_counter);
654    let converges = encoder.converges();
655    let holds = encoder.holds && !converges;
656    // A session that holds its refresh and a band of a frame (libvpx's VP8) sweeps the refresh
657    // a band a frame, as a converging one's fallback does, rather than holding it whole.
658    let sweeps = holds && encoder.band.is_some();
659    let cleanup = cleanup_due(
660        st,
661        settings.paint_over_trigger_frames,
662        improves,
663        true,
664        holds && encoder.keys,
665        damage,
666    );
667    let refresh_qp = held_refresh_quality(settings, encoder);
668    let cleanup_qp = match cleanup {
669        Cleanup::Refresh if holds => Some(refresh_qp),
670        Cleanup::Key => Some(paint_qp),
671        _ => None,
672    };
673    let mut d = HwFrameDecision {
674        send: false,
675        force_idr: false,
676        target_qp: normal_qp,
677        hold_qp: None,
678        hold_band: None,
679        measure: false,
680    };
681    if damage.is_dirty() {
682        if !converges || damage.is_motion() {
683            st.h264_burst_frames_remaining = 0;
684        }
685        // Motion ends a sweep. A small change (a caret) leaves it to finish: one that started
686        // over at every blink would never cover the picture.
687        if damage.is_motion() {
688            st.sweep = None;
689            st.settled = false;
690        }
691        d.send = true;
692        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
693        d.hold_qp = cleanup_qp;
694        if sweeps && cleanup == Cleanup::Refresh && !d.force_idr {
695            sweep_refresh(st, &mut d);
696        }
697        return d;
698    }
699    if cleanup != Cleanup::None || recovery_idr {
700        d.send = true;
701        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
702        d.hold_qp = cleanup_qp;
703        if recovery_idr {
704            st.sweep = None;
705        } else if sweeps && cleanup == Cleanup::Refresh && !d.force_idr {
706            sweep_refresh(st, &mut d);
707        }
708        if converges && (cleanup != Cleanup::None || (burst > 0 && recovery_idr)) {
709            if (st.h264_burst_frames_remaining <= 0 && !st.settled) || recovery_idr {
710                st.h264_burst_frames_remaining = cleanup_frames(settings, encoder);
711                st.idle_frames = 0;
712                st.over_frames = 0;
713                st.same_frames = 0;
714                st.rc_bytes = 0;
715                st.finest = None;
716                st.finest_frames = 0;
717                st.fine_frames = 0;
718                st.measured = None;
719                st.level_checks = 0;
720                st.measuring = false;
721                st.measure_in = 0;
722            }
723            st.burst_held = false;
724        } else if burst > 0 && (d.force_idr || cleanup != Cleanup::None) && st.sweep.is_none() {
725            // A sweep spends a budget a frame and leaves the rate control nothing to settle, so no
726            // burst follows it, which would hold whole frames again once it ends.
727            st.h264_burst_frames_remaining = burst;
728            st.burst_held =
729                holds && (cleanup != Cleanup::None || (improves && !settings.video_cbr_mode));
730        }
731        return d;
732    }
733    if let Some((from, size)) = st.sweep {
734        let budget = frame_budget(settings.video_bitrate_kbps, settings.target_fps);
735        let size = band_share(size, encoder.band, budget);
736        let to = (from + size).min(1.0);
737        st.sweep_frames = st.sweep_frames.saturating_add(1);
738        let left = (REFINE_S * settings.target_fps.max(1.0)).round() - st.sweep_frames as f64;
739        let paced = encoder
740            .band
741            .is_some_and(|b| b as f64 <= 2.0 * BAND_BUDGETS * budget);
742        if paced && (1.0 - to) / size > left.max(1.0) {
743            st.sweep_coarser += 1;
744        }
745        let q = refresh_qp + st.sweep_coarser;
746        if encoder.last.is_some_and(|last| q >= last) {
747            st.sweep = None;
748            return d;
749        }
750        st.sweep = (to < 1.0).then_some((to, size));
751        d.send = true;
752        d.hold_qp = Some(q);
753        d.hold_band = Some((from, to));
754        return d;
755    }
756    let budget = frame_budget(settings.video_bitrate_kbps, settings.target_fps);
757    let trigger = settings.paint_over_trigger_frames.max(1);
758    if converges && st.h264_burst_frames_remaining > 0 {
759        st.over_frames = if encoder
760            .bytes
761            .is_some_and(|b| b as f64 > budget * OVERSHOOT_BUDGETS)
762        {
763            st.over_frames.saturating_add(1)
764        } else {
765            0
766        };
767    }
768    let pinned = converges && st.h264_burst_frames_remaining > 0 && st.over_frames >= trigger;
769    if pinned && !(improves && encoder.holds) {
770        st.h264_burst_frames_remaining = 0;
771        st.settled = !encoder.holds;
772    }
773    if converges && st.h264_burst_frames_remaining > 0 {
774        match convergence(encoder.last, encoder.bytes, paint_qp, budget) {
775            Convergence::Converged if !encoder.measures => {
776                st.h264_burst_frames_remaining = 0;
777                st.change_mass = 0.0;
778                st.settled = !encoder.holds;
779            }
780            Convergence::Converged | Convergence::Idle => {
781                st.idle_frames = st.idle_frames.saturating_add(1)
782            }
783            Convergence::Refining => st.idle_frames = 0,
784        }
785        st.fine_frames = if encoder.last.is_some_and(|q| q <= paint_qp) {
786            st.fine_frames.saturating_add(1)
787        } else {
788            0
789        };
790    }
791    if st.h264_burst_frames_remaining > 0 {
792        st.h264_burst_frames_remaining -= 1;
793        d.send = true;
794        d.hold_qp = (st.burst_held && improves).then_some(refresh_qp);
795        let elapsed = (converge_frames(settings) - st.h264_burst_frames_remaining).max(0) as u32;
796        let stalled = st.idle_frames >= STALL_TRIGGERS * trigger;
797        if converges && !encoder.holds {
798            let reached = settings.use_paint_over_quality && st.fine_frames >= trigger;
799            let (measure, level) = plateau(st, encoder.psnr, PLATEAU_TRIGGERS * trigger);
800            d.measure = measure;
801            let done = if encoder.measures { level } else { reached };
802            let bytes = encoder.bytes.unwrap_or(0);
803            st.same_frames = if bytes > 0
804                && bytes.abs_diff(st.rc_bytes) as f64 <= st.rc_bytes as f64 * SAME_FRAME_SHARE
805            {
806                st.same_frames.saturating_add(1)
807            } else {
808                0
809            };
810            st.rc_bytes = bytes;
811            let same = st.same_frames as f64 >= EMPTY_S * settings.target_fps
812                && encoder.last.is_some_and(|q| q >= SAME_FRAME_QUALITY);
813            if let Some(q) = encoder.last {
814                if st.finest.is_none_or(|finest| q < finest) {
815                    st.finest = Some(q);
816                    st.finest_frames = 0;
817                } else {
818                    st.finest_frames = st.finest_frames.saturating_add(1);
819                }
820            }
821            let level = st.finest_frames as f64 >= LEVEL_S * settings.target_fps;
822            if ((stalled || same || level) && !encoder.measures)
823                || done
824                || st.h264_burst_frames_remaining == 0
825            {
826                st.h264_burst_frames_remaining = 0;
827                st.settled = true;
828            }
829        } else if converges
830            && improves
831            && (stalled || pinned || elapsed >= FALLBACK_TRIGGERS * trigger)
832        {
833            d.hold_qp = Some(if pinned {
834                refresh_quality(
835                    settings,
836                    encoder,
837                    encoder.bytes.unwrap_or(0) as f64 / budget,
838                )
839            } else {
840                refresh_qp
841            });
842            st.h264_burst_frames_remaining = 0;
843            if encoder.band.is_some() && !pinned {
844                start_sweep(st, &mut d);
845            }
846        }
847        return d;
848    }
849    d.send = settings.video_streaming_mode || is_animated;
850    d
851}
852
853/// Start a refresh sweep at the top of the picture: its first band in this frame, the rest a band
854/// a frame after it (`FIRST_BAND`, `band_share`).
855fn start_sweep(st: &mut StripeState, d: &mut HwFrameDecision) {
856    d.hold_band = Some((0.0, FIRST_BAND));
857    st.sweep = Some((FIRST_BAND, FIRST_BAND));
858    st.sweep_frames = 0;
859    st.sweep_coarser = 0;
860}
861
862/// A refresh due on a session that sweeps it (`decide_hw_fullframe`'s `sweeps`): a sweep from the
863/// top, or, with one under way, which is already refreshing the picture, nothing held, the frame
864/// going at the rate control's quality: a sweep started over at every change of a blinking caret
865/// would never cover the picture, and a whole held frame is what the sweep spreads.
866fn sweep_refresh(st: &mut StripeState, d: &mut HwFrameDecision) {
867    if st.sweep.is_some() {
868        d.hold_qp = None;
869    } else {
870        start_sweep(st, d);
871    }
872}
873
874/// `decide_hw_fullframe` for a constant-quality session: the cleanup moves the session's own
875/// quality to the paint-over one, as main's paint-over did, rather than holding a frame at a
876/// quantizer. Each encoder then codes the cleanup the way it codes any frame at that quality:
877/// x264's and x265's rate factor give a key frame their intra offset (about three steps finer
878/// than the paint-over index), SVT-AV1 its key-frame qindex, and the encoders whose quality
879/// change re-opens the session (x265, SVT-AV1, kvazaar) open on a key frame at it. A frame held
880/// at the paint-over quantizer itself came out coarser than main's (SVT-AV1 49.6 against 61.0 dB,
881/// x264 53.7 against 56.5 dB).
882///
883/// The session stays at the paint-over quality while the region stays clean: from its cleanup
884/// until motion (more than a small change), so the frames Turbo sends of a still screen, or a
885/// caret blinking on it, do not take a re-opening encoder back to the session's quality, a key
886/// frame at it, after `QP_HYSTERESIS_LIMIT` frames. The refresh, the key frame, and the bursts
887/// come when `cleanup_due` says, as at a constant rate; a requested or scheduled key frame is
888/// coded at the session's own quality, as on main, and its burst at the paint-over one. An
889/// encoder whose quality change re-opens it (`EncoderQuality::reopens`: x265, kvazaar, SVT-AV1)
890/// codes the refresh that moves it as a key frame, as main's paint-over did, so that refresh is
891/// its key-frame cleanup and no second key frame follows for the same change.
892fn decide_constant_quality(
893    st: &mut StripeState,
894    settings: &RustCaptureSettings,
895    frame_counter: u16,
896    damage: Damage,
897    is_animated: bool,
898    requested_idr: bool,
899    encoder: EncoderQuality,
900) -> HwFrameDecision {
901    let normal_qp = settings.video_crf as u32;
902    let paint_qp = settings.video_paintover_crf as u32;
903    let burst = settings.video_paintover_burst_frames;
904    let improves =
905        settings.use_paint_over_quality && settings.video_paintover_crf < settings.video_crf;
906    let recovery_idr = requested_idr || periodic_idr_due(settings, frame_counter);
907    let mut cleanup = cleanup_due(
908        st,
909        settings.paint_over_trigger_frames,
910        improves,
911        true,
912        true,
913        damage,
914    );
915    if damage.is_motion() || !improves {
916        st.clean_quality = false;
917    }
918    if cleanup == Cleanup::Refresh && encoder.reopens && !st.clean_quality {
919        cleanup = Cleanup::Key;
920        st.change_mass = 0.0;
921    }
922    if cleanup != Cleanup::None {
923        st.clean_quality = true;
924    }
925    let quality = if st.clean_quality {
926        paint_qp
927    } else {
928        normal_qp
929    };
930    let mut d = HwFrameDecision {
931        send: false,
932        force_idr: false,
933        target_qp: quality,
934        hold_qp: None,
935        hold_band: None,
936        measure: false,
937    };
938    if recovery_idr && cleanup == Cleanup::None {
939        d.target_qp = normal_qp;
940    }
941    if damage.is_dirty() {
942        st.h264_burst_frames_remaining = 0;
943        d.send = true;
944        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
945        return d;
946    }
947    if cleanup != Cleanup::None || recovery_idr {
948        d.send = true;
949        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
950        if burst > 0 {
951            st.h264_burst_frames_remaining = burst;
952            st.burst_held = improves;
953        }
954        return d;
955    }
956    if st.h264_burst_frames_remaining > 0 {
957        st.h264_burst_frames_remaining -= 1;
958        d.send = true;
959        if st.burst_held {
960            d.target_qp = paint_qp;
961        }
962        return d;
963    }
964    d.send = settings.video_streaming_mode || is_animated;
965    d
966}
967
968/// Rows per band of the content hash a full-frame X11 session reads its damage from: one band is
969/// under `SMALL_AREA` of a screen from 720 rows up, so a caret or a clock reads as the small
970/// change it is.
971pub(crate) const DAMAGE_BAND_ROWS: usize = 32;
972
973/// Which bands of `DAMAGE_BAND_ROWS` rows of a host frame (`stride` bytes per row, `height` rows)
974/// changed against the frame before, read from a content hash per band
975/// (`StripeState::content_dirty`, one state per band in `bands`, with its damage blocks). The
976/// bands hash in turn on the calling thread: a band takes tens of microseconds, less than waking
977/// the rayon pool for it costs, which on a many-core host spent several times the hash in CPU
978/// and added milliseconds waiting on the slowest worker.
979fn hash_bands(
980    bands: &mut Vec<StripeState>,
981    pixels: &[u8],
982    stride: usize,
983    height: usize,
984    threshold: u32,
985    duration: i32,
986) -> Vec<bool> {
987    let n = height.div_ceil(DAMAGE_BAND_ROWS).max(1);
988    if bands.len() != n {
989        bands.clear();
990        bands.resize_with(n, StripeState::default);
991    }
992    bands
993        .iter_mut()
994        .enumerate()
995        .map(|(i, band)| {
996            let end = ((i + 1) * DAMAGE_BAND_ROWS).min(height) * stride;
997            let bytes =
998                &pixels[(i * DAMAGE_BAND_ROWS * stride).min(pixels.len())..end.min(pixels.len())];
999            band.content_dirty(bytes, threshold, duration)
1000        })
1001        .collect()
1002}
1003
1004/// The bands that changed by hashes taken where the frame was uploaded (`hash_next_upload`),
1005/// read through each band's state as the CPU's own hashes are (`StripeState::hash_dirty`).
1006fn hashed_bands(
1007    bands: &mut Vec<StripeState>,
1008    hashes: &[u64],
1009    threshold: u32,
1010    duration: i32,
1011) -> Vec<bool> {
1012    if bands.len() != hashes.len() {
1013        bands.clear();
1014        bands.resize_with(hashes.len(), StripeState::default);
1015    }
1016    bands
1017        .iter_mut()
1018        .zip(hashes)
1019        .map(|(band, &hash)| band.hash_dirty(hash, threshold, duration))
1020        .collect()
1021}
1022
1023/// The damage a band hash (`hash_bands`) reads: the fraction of the bands that changed. No bands
1024/// read yet, before a capture's first hash, is all new.
1025fn band_damage(dirty: &[bool]) -> Damage {
1026    let changed = dirty.iter().filter(|&&d| d).count();
1027    if dirty.is_empty() {
1028        Damage::Area(1.0)
1029    } else if changed == 0 {
1030        Damage::None
1031    } else {
1032        Damage::Area(changed as f32 / dirty.len() as f32)
1033    }
1034}
1035
1036/// The damage of a frame whose bands were hashed where it lies (NvFBC's, on the GPU) against the
1037/// hashes of the frame before, which `last` keeps: the share of the bands that changed. No hashes
1038/// before is all new.
1039pub(crate) fn hashed_damage(last: &mut Vec<u64>, hashes: Vec<u64>) -> Damage {
1040    let dirty: Vec<bool> = if last.len() == hashes.len() {
1041        hashes
1042            .iter()
1043            .zip(last.iter())
1044            .map(|(a, b)| a != b)
1045            .collect()
1046    } else {
1047        Vec::new()
1048    };
1049    *last = hashes;
1050    band_damage(&dirty)
1051}
1052
1053/// The damage of a host frame against the one before it, from its band hash (`hash_bands`).
1054fn hash_damage(
1055    bands: &mut Vec<StripeState>,
1056    pixels: &[u8],
1057    stride: usize,
1058    height: usize,
1059    threshold: u32,
1060    duration: i32,
1061) -> Damage {
1062    band_damage(&hash_bands(
1063        bands, pixels, stride, height, threshold, duration,
1064    ))
1065}
1066
1067/// The rows a band hash (`hash_bands`) found changed in a `width` x `height` frame, as
1068/// full-width rectangles, one per run of changed bands: the striped path's dirty map. No bands
1069/// read yet is the whole frame.
1070fn band_rects(dirty: &[bool], width: i32, height: i32) -> Vec<Rectangle<i32, Physical>> {
1071    if dirty.is_empty() {
1072        return vec![Rectangle::new((0, 0).into(), (width, height).into())];
1073    }
1074    let mut rects = Vec::new();
1075    let mut run = None;
1076    for (i, &changed) in dirty.iter().chain(std::iter::once(&false)).enumerate() {
1077        match (changed, run) {
1078            (true, None) => run = Some(i),
1079            (false, Some(first)) => {
1080                let top = (first * DAMAGE_BAND_ROWS) as i32;
1081                let bottom = ((i * DAMAGE_BAND_ROWS) as i32).min(height);
1082                rects.push(Rectangle::new(
1083                    (0, top).into(),
1084                    (width, bottom - top).into(),
1085                ));
1086                run = None;
1087            }
1088            _ => {}
1089        }
1090    }
1091    rects
1092}
1093
1094/// Run `encode` on this thread while a thread of its own hashes the frame's bands (`hash_bands`),
1095/// and return what `encode` returned with the bands that changed: a Turbo frame's hash kept off
1096/// its encode's path. A scoped thread per frame rather than the rayon pool, whose idle workers
1097/// spin after every wake; where no thread starts, the hash runs here after the encode.
1098#[allow(clippy::too_many_arguments)]
1099fn hash_beside<R>(
1100    bands: &mut Vec<StripeState>,
1101    pixels: &[u8],
1102    stride: usize,
1103    height: usize,
1104    threshold: u32,
1105    duration: i32,
1106    encode: impl FnOnce() -> R,
1107) -> (R, Vec<bool>) {
1108    let (out, hashed) = std::thread::scope(|s| {
1109        let hashing = std::thread::Builder::new()
1110            .name("pxf-x11-hash".into())
1111            .spawn_scoped(s, || {
1112                hash_bands(bands, pixels, stride, height, threshold, duration)
1113            });
1114        let out = encode();
1115        (out, hashing.ok().map(|h| h.join().unwrap_or_default()))
1116    });
1117    let dirty =
1118        hashed.unwrap_or_else(|| hash_bands(bands, pixels, stride, height, threshold, duration));
1119    (out, dirty)
1120}
1121
1122/// Everything the X11 host-ARGB path has to remember between frames.
1123///
1124/// Unlike the Wayland backend, X11 capture has no compositor to report what changed, so this
1125/// context exists to hold the state that stands in for that missing damage signal: the per-stripe
1126/// hashes and the persistent encoder session that let `process()` discover damage by comparing
1127/// content frame-to-frame. A full-frame encoder runs through `decide_hw_fullframe`; the striped
1128/// software path (JPEG, striped or full-frame software H.264) runs through `encode_cpu` with
1129/// `hash_damage=true`.
1130///
1131/// Recording fan-out (socket sink and MP4 recorder) is handled at the delivery layer; a
1132/// consumer needing a keyframe goes through [`X11Pipeline::request_idr`] like everyone else.
1133pub struct X11Pipeline {
1134    settings: RustCaptureSettings,
1135    stripes: Vec<StripeState>,
1136    /// Smoothed number of stripes carrying the encode budget (see `stripe_rate_control`).
1137    stripes_carrying: f32,
1138    /// The full-frame encoder, or `None` for the striped software path.
1139    hw: Option<FrameEncoder>,
1140    hw_state: StripeState,
1141    /// The content hashes of a full-frame session, one per band of `DAMAGE_BAND_ROWS` rows.
1142    bands: Vec<StripeState>,
1143    /// The bands the content hash found changed in the frame before this one, which a Turbo
1144    /// frame reads (`process`); empty until the first hash.
1145    turbo_dirty: Vec<bool>,
1146    frame_counter: u16,
1147    pending_force_idr: bool,
1148    /// Consecutive hardware encode failures and whether this pipeline already spent its one
1149    /// rebuild; together they drive `recover_hw`.
1150    hw_error_streak: u32,
1151    hw_rebuilt: bool,
1152    /// The cleanup quality (`HwFrameDecision::cleanup_quality`) and key frame the last full frame
1153    /// was decided on, and the ones it was coded with.
1154    #[cfg(test)]
1155    hold: [(Option<u32>, bool); 2],
1156}
1157
1158impl X11Pipeline {
1159    /// Build the context, choosing the full-frame encoder for the X11 host-BGRA path through
1160    /// the shared ladder (`select_frame_encoder`): the hardware backend the encode node's driver
1161    /// selects, then the codec's software encoder, or the striped software path for JPEG and
1162    /// H.264. A codec no backend serves falls through to the video codecs the host does serve,
1163    /// JPEG last.
1164    pub fn new(mut settings: RustCaptureSettings) -> Self {
1165        let hw = encoders::select_frame_encoder(
1166            &mut settings,
1167            FrameSource::Host { rgba: false },
1168            None,
1169            "X11",
1170        );
1171        let pipeline = Self {
1172            settings,
1173            stripes: Vec::new(),
1174            stripes_carrying: 1.0,
1175            hw,
1176            hw_state: StripeState::default(),
1177            bands: Vec::new(),
1178            turbo_dirty: Vec::new(),
1179            frame_counter: 0,
1180            pending_force_idr: false,
1181            hw_error_streak: 0,
1182            hw_rebuilt: false,
1183            #[cfg(test)]
1184            hold: [(None, false); 2],
1185        };
1186        pipeline.record_stream();
1187        pipeline
1188    }
1189
1190    /// Put the session this pipeline settled on into the capture's report.
1191    fn record_stream(&self) {
1192        let fullframe = self.hw.is_some() || self.settings.video_fullframe;
1193        crate::report::stream(
1194            &self.settings,
1195            encoders::software::stripe_count(self.settings.height, self.settings.codec, fullframe),
1196            self.hw
1197                .as_ref()
1198                .map(|enc| (enc.backend_name(), enc.is_hardware())),
1199            encoders::session_fullcolor(self.hw.as_ref(), &self.settings),
1200            encoders::session_full_range(self.hw.as_ref(), &self.settings),
1201        );
1202        crate::report::bit_depth(encoders::session_bit_depth(
1203            self.hw.as_ref(),
1204            &self.settings,
1205        ));
1206    }
1207
1208    /// React to a streak of hardware encode failures: rebuild the session once with the startup
1209    /// selection, and demote to the software encoder when a fresh session fails the same way.
1210    /// A session whose encodes keep failing still constructs, so the rebuild only counts as
1211    /// recovery until an encode succeeds; otherwise the stream would rebuild in a loop and never
1212    /// demote. Streaming nothing forever is not an option.
1213    ///
1214    /// The demote is the last rung: the software path has no encode-error streak of its own, so
1215    /// once the pipeline lands there it is never re-entered.
1216    fn recover_hw(&mut self) {
1217        self.hw_error_streak = 0;
1218        if self.hw_rebuilt {
1219            eprintln!(
1220                "[X11] HW encoder unrecoverable; demoting to software encoding ({}).",
1221                crate::encoders::software_library(self.settings.codec)
1222            );
1223            // The broken session is released before its replacement is built: these failures
1224            // are usually device memory pressure, and holding both at once is what would make
1225            // the replacement fail too.
1226            self.hw = None;
1227            self.settings.use_cpu = true;
1228            self.hw = encoders::select_frame_encoder(
1229                &mut self.settings,
1230                FrameSource::Host { rgba: false },
1231                None,
1232                "X11",
1233            );
1234            crate::report::encoder_reason(
1235                "the hardware encoder failed repeatedly and was given up",
1236            );
1237        } else {
1238            eprintln!("[X11] rebuilding HW encoder after repeated encode errors.");
1239            self.hw = None;
1240            self.hw = encoders::select_frame_encoder(
1241                &mut self.settings,
1242                FrameSource::Host { rgba: false },
1243                None,
1244                "X11",
1245            );
1246            self.hw_rebuilt = true;
1247        }
1248        self.record_stream();
1249        self.hw_state = StripeState::default();
1250        self.stripes.clear();
1251        self.pending_force_idr = true;
1252    }
1253
1254    /// Request an on-demand keyframe on the next processed frame.
1255    pub fn request_idr(&mut self) {
1256        self.pending_force_idr = true;
1257    }
1258
1259    /// Apply what the consumers say of a frame (`FrameEncoder::take_report`): a loss leaves it and
1260    /// every frame after it out of the predictions, a key frame where the encoder cannot.
1261    pub fn take_report(&mut self, report: crate::encoders::reference::ReferenceReport) {
1262        use crate::encoders::reference::ReferenceReport;
1263        let taken = match (self.hw.as_mut(), report) {
1264            (Some(enc), report) => enc.take_report(report),
1265            (None, ReferenceReport::Lost(frame_id)) => {
1266                invalidate_reference(&mut self.stripes, frame_id)
1267            }
1268            (None, ReferenceReport::Held(frame_id)) => {
1269                acknowledge_reference(&mut self.stripes, frame_id, true);
1270                true
1271            }
1272            (None, ReferenceReport::Sent(frame_id)) => {
1273                acknowledge_reference(&mut self.stripes, frame_id, false);
1274                true
1275            }
1276        };
1277        if !taken {
1278            self.pending_force_idr = true;
1279        }
1280    }
1281
1282    /// The encoder's name for the stream log: the hardware backend, the software library of a
1283    /// full-frame session, or `CPU` with the library of the striped software path.
1284    pub fn encoder_name(&self) -> String {
1285        match &self.hw {
1286            Some(enc) if enc.is_hardware() => enc.backend_name().to_string(),
1287            Some(enc) => format!("CPU ({})", enc.backend_name()),
1288            None if !self.settings.codec.is_video() => "CPU (turbojpeg)".to_string(),
1289            None => format!("CPU ({})", crate::encoders::software_library(Codec::H264)),
1290        }
1291    }
1292
1293    /// The codec the pipeline actually emits, after any demotion.
1294    pub fn codec(&self) -> Codec {
1295        self.settings.codec
1296    }
1297
1298    /// Whether the pipeline encodes on a GPU.
1299    pub fn is_hardware(&self) -> bool {
1300        self.hw.as_ref().is_some_and(|enc| enc.is_hardware())
1301    }
1302
1303    /// The encoder's one rate control where its backend has one alone
1304    /// (`FrameEncoder::fixed_rate_control`), for this pipeline's stream log.
1305    pub fn fixed_rate_control(&self) -> Option<&'static str> {
1306        self.hw.as_ref().and_then(FrameEncoder::fixed_rate_control)
1307    }
1308
1309    /// Whether the encoder holds a cleanup at the quantizer asked for
1310    /// (`FrameEncoder::holds_quantizer`, `software::stripes_hold_quantizer`), for this
1311    /// pipeline's stream log.
1312    pub fn holds_quantizer(&self) -> bool {
1313        self.hw.as_ref().map_or_else(
1314            || crate::encoders::software::stripes_hold_quantizer(&self.settings),
1315            FrameEncoder::holds_quantizer,
1316        )
1317    }
1318
1319    /// The `Colorspace:` field for this pipeline's stream log, describing what its encoder settled
1320    /// on: a hardware session only reaches 4:4:4 when the device carries it, the software path
1321    /// only when the build's encoder for the codec does.
1322    pub fn colorspace_desc(&self) -> &'static str {
1323        let fullcolor = encoders::session_fullcolor(self.hw.as_ref(), &self.settings);
1324        crate::encoders::colorspace_desc(
1325            fullcolor,
1326            encoders::session_full_range(self.hw.as_ref(), &self.settings),
1327        )
1328    }
1329
1330    /// Adapt the live pipeline to recreated capture surfaces without rebuilding it.
1331    ///
1332    /// # Arguments
1333    ///
1334    /// * `settings` - New geometry plus current live rates.
1335    /// * `size_changed` - Whether the capture dimensions changed.
1336    ///
1337    /// # Returns
1338    ///
1339    /// `true` if the pipeline was successfully adapted in place; `false` when the active encoder
1340    /// cannot follow (VAAPI on resize) and the caller must rebuild.
1341    pub fn reshape(&mut self, settings: &RustCaptureSettings, size_changed: bool) -> bool {
1342        if !size_changed {
1343            if let Some(FrameEncoder::Nvenc(enc)) = &mut self.hw {
1344                enc.release_pinned_hosts();
1345            }
1346            self.settings = settings.clone();
1347            return true;
1348        }
1349        match &mut self.hw {
1350            Some(FrameEncoder::Nvenc(enc)) => {
1351                if let Err(e) = enc.reconfigure_resolution(settings) {
1352                    eprintln!("[X11] NVENC in-place resize unavailable ({e}); rebuilding");
1353                    return false;
1354                }
1355            }
1356            None => {}
1357            _ => return false,
1358        }
1359        let codec = self.settings.codec;
1360        self.settings = settings.clone();
1361        self.settings.codec = codec;
1362        self.stripes.clear();
1363        self.hw_state = StripeState::default();
1364        self.turbo_dirty.clear();
1365        true
1366    }
1367
1368    /// Apply a runtime rate-control / framerate change: the CBR target bitrate + VBV (kbps /
1369    /// kb; ignored unless CBR is active) and the target fps. NVENC and libvpx reconfigure their
1370    /// live session, as x265 does unless the frame rate moved; x265 then re-opens, as kvazaar
1371    /// and SVT-AV1 always do; a VA-API session starts a new sequence; the striped software path
1372    /// picks the new values up on the next `process()` (encode_cpu reads the updated settings
1373    /// and reconfigures each stripe's encoder).
1374    pub fn update_rate(&mut self, bitrate_kbps: i32, vbv_multiplier: f64, fps: f64) {
1375        self.settings.video_bitrate_kbps = bitrate_kbps;
1376        self.settings.video_vbv_multiplier = vbv_multiplier;
1377        if fps > 0.0 {
1378            self.settings.target_fps = fps;
1379        }
1380        if let Some(enc) = self.hw.as_mut()
1381            && let Err(e) = enc.reconfigure_rate(&self.settings)
1382        {
1383            // The failed re-open left the session without a codec context, so it goes
1384            // through the rebuild-or-demote ladder instead of being encoded into.
1385            eprintln!("[X11] rate reconfigure failed: {e}");
1386            self.recover_hw();
1387        }
1388    }
1389
1390    /// Apply live per-frame tunables (quality, paint-over, streaming mode, keyframe
1391    /// cadence); every encoder re-reads them from the settings on the next process().
1392    pub fn update_tunables(&mut self, t: &crate::LiveTunables) {
1393        t.apply_to(&mut self.settings);
1394    }
1395
1396    /// Encode one host-ARGB frame and return the encoded stripes.
1397    ///
1398    /// Damage is read from the content hash (`hash_bands`). Without Turbo the hash decides whether
1399    /// a frame (or a stripe) is sent, so it runs first. Turbo sends every video frame, and only
1400    /// its cleanup reads the damage, so there the frame's hash runs beside its encode
1401    /// (`hash_beside`) and the cleanup reads the frame before's (`turbo_dirty`; the striped path
1402    /// as the rows it maps onto its stripes): the hash, a millisecond and a half at 1080p and six
1403    /// at 4K, stays off the path to the client. A frame the cleanup codes at its own quality is
1404    /// hashed before its encode instead, so a change landing on it is coded at the session's.
1405    /// With the paint-over off nothing reads it, and Turbo hashes nothing.
1406    ///
1407    /// # Arguments
1408    ///
1409    /// * `argb` - Packed BGRA pixel buffer (B,G,R,A byte order, `stride` bytes per row).
1410    /// * `stride` - Bytes per row (must equal `width * 4` for the software path).
1411    ///
1412    /// # Returns
1413    ///
1414    /// Vec of [`EncodedStripe`] — empty when nothing changed.
1415    pub fn process(&mut self, argb: &[u8], stride: usize) -> Vec<EncodedStripe> {
1416        let width = self.settings.width;
1417        let height = self.settings.height;
1418        let requested = self.pending_force_idr;
1419        let threshold = self.settings.damage_block_threshold;
1420        let duration = self.settings.damage_block_duration as i32;
1421
1422        let out = if self.hw.is_some() {
1423            let turbo = self.settings.video_streaming_mode;
1424            let damage = if turbo {
1425                band_damage(&self.turbo_dirty)
1426            } else {
1427                hash_damage(
1428                    &mut self.bands,
1429                    argb,
1430                    stride,
1431                    height as usize,
1432                    threshold,
1433                    duration,
1434                )
1435            };
1436            let quality = EncoderQuality::of(self.hw.as_ref().unwrap());
1437            let mut d = decide_hw_fullframe(
1438                &mut self.hw_state,
1439                &self.settings,
1440                self.frame_counter,
1441                damage,
1442                false,
1443                requested,
1444                quality,
1445            );
1446            // A Turbo frame is decided on the hash of the one before, so a frame coded whole at
1447            // a cleanup's quality could carry a change it was not decided for, coded at that
1448            // quality. Such a frame is hashed before it is encoded instead of beside it, and one
1449            // that moved is coded as motion is: at the session's quality, and a key frame only
1450            // where one is due. A held band needs none of this: the rest of its frame is held at
1451            // the coarsest quantizer.
1452            let normal = self.settings.video_crf as u32;
1453            #[cfg(test)]
1454            let decided = (d.cleanup_quality(normal), d.force_idr);
1455            let mut hashed = None;
1456            if turbo
1457                && self.settings.use_paint_over_quality
1458                && d.send
1459                && d.cleanup_quality(normal).is_some()
1460            {
1461                let dirty = hash_bands(
1462                    &mut self.bands,
1463                    argb,
1464                    stride,
1465                    height as usize,
1466                    threshold,
1467                    duration,
1468                );
1469                if band_damage(&dirty).is_motion() {
1470                    d.hold_qp = None;
1471                    d.target_qp = normal;
1472                    d.force_idr = requested || periodic_idr_due(&self.settings, self.frame_counter);
1473                }
1474                hashed = Some(dirty);
1475            }
1476            #[cfg(test)]
1477            {
1478                self.hold = [decided, (d.cleanup_quality(normal), d.force_idr)];
1479            }
1480            let fc = self.frame_counter as u64;
1481            if d.send || self.hw.as_ref().unwrap().holds_frame() {
1482                let force_idr = d.force_idr;
1483                let enc = self.hw.as_mut().unwrap();
1484                d.prepare(enc);
1485                // A Turbo paint-over reads the bands the frame before changed: hashed on the
1486                // device where NVENC uploads the frame, beside its encode, else on a thread
1487                // beside it (`hash_beside`).
1488                let beside = hashed.is_none() && turbo && self.settings.use_paint_over_quality;
1489                let on_device = beside && d.send && enc.hash_next_upload(DAMAGE_BAND_ROWS as u32);
1490                let mut encode = || {
1491                    if d.send {
1492                        enc.encode_host(argb, stride, false, fc, d.target_qp, force_idr)
1493                    } else {
1494                        enc.push_held(fc)
1495                    }
1496                };
1497                let res = if let Some(dirty) = hashed {
1498                    self.turbo_dirty = dirty;
1499                    encode()
1500                } else if on_device {
1501                    let res = encode();
1502                    self.turbo_dirty = match enc.take_upload_hashes() {
1503                        Some(hashes) => hashed_bands(&mut self.bands, &hashes, threshold, duration),
1504                        None => hash_bands(
1505                            &mut self.bands,
1506                            argb,
1507                            stride,
1508                            height as usize,
1509                            threshold,
1510                            duration,
1511                        ),
1512                    };
1513                    res
1514                } else if beside {
1515                    let (res, dirty) = hash_beside(
1516                        &mut self.bands,
1517                        argb,
1518                        stride,
1519                        height as usize,
1520                        threshold,
1521                        duration,
1522                        encode,
1523                    );
1524                    self.turbo_dirty = dirty;
1525                    res
1526                } else {
1527                    self.turbo_dirty.clear();
1528                    encode()
1529                };
1530                match res {
1531                    Ok(data) if !data.is_empty() => {
1532                        self.hw_error_streak = 0;
1533                        self.hw_rebuilt = false;
1534                        let codec = self.settings.codec;
1535                        enc.delivered_units(data, self.frame_counter)
1536                            .into_iter()
1537                            .map(|(data, id, reference)| EncodedStripe {
1538                                data: Arc::new(data),
1539                                codec,
1540                                stripe_y_start: 0,
1541                                stripe_height: height,
1542                                frame_id: id as i32,
1543                                timing: Default::default(),
1544                                reference,
1545                            })
1546                            .collect()
1547                    }
1548                    Ok(_) => {
1549                        self.hw_error_streak = 0;
1550                        self.hw_rebuilt = false;
1551                        Vec::new()
1552                    }
1553                    Err(e) => {
1554                        // One line per recovery window: a session failing at frame rate would
1555                        // otherwise write a line per frame for the life of the capture.
1556                        if self
1557                            .hw_error_streak
1558                            .is_multiple_of(crate::HW_ERROR_RECOVERY_THRESHOLD)
1559                        {
1560                            eprintln!("[X11] HW encode error: {e}");
1561                        }
1562                        self.hw_error_streak = self.hw_error_streak.saturating_add(1);
1563                        if self.hw_error_streak >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1564                            self.recover_hw();
1565                        }
1566                        Vec::new()
1567                    }
1568                }
1569            } else {
1570                Vec::new()
1571            }
1572        } else {
1573            debug_assert_eq!(
1574                stride,
1575                width as usize * 4,
1576                "software encode path assumes tightly-packed rows (stride == width*4)"
1577            );
1578            let force_idr_all = requested
1579                || (self.settings.codec.is_video()
1580                    && periodic_idr_due(&self.settings, self.frame_counter));
1581            if self.settings.codec.is_video() && self.settings.video_streaming_mode {
1582                // As a full frame above: a frame that codes a stripe at the paint-over quality
1583                // while it holds still is hashed before it is encoded, and decided on its own
1584                // change as well as the one before.
1585                let hashed = (self.settings.use_paint_over_quality
1586                    && stripes_held_still(&self.stripes, &self.settings))
1587                .then(|| {
1588                    hash_bands(
1589                        &mut self.bands,
1590                        argb,
1591                        stride,
1592                        height as usize,
1593                        threshold,
1594                        duration,
1595                    )
1596                });
1597                let rects = match &hashed {
1598                    Some(dirty) => {
1599                        let seen: Vec<bool> = dirty
1600                            .iter()
1601                            .enumerate()
1602                            .map(|(i, &d)| d || self.turbo_dirty.get(i).is_none_or(|&b| b))
1603                            .collect();
1604                        band_rects(&seen, width, height)
1605                    }
1606                    None => band_rects(&self.turbo_dirty, width, height),
1607                };
1608                let (stripes, carrying, settings) = (
1609                    &mut self.stripes,
1610                    &mut self.stripes_carrying,
1611                    &self.settings,
1612                );
1613                let mut encode = || {
1614                    encode_cpu(
1615                        stripes,
1616                        carrying,
1617                        argb,
1618                        width,
1619                        height,
1620                        &rects,
1621                        settings,
1622                        self.frame_counter,
1623                        false,
1624                        false,
1625                        force_idr_all,
1626                    )
1627                };
1628                if let Some(dirty) = hashed {
1629                    self.turbo_dirty = dirty;
1630                    encode()
1631                } else if self.settings.use_paint_over_quality {
1632                    let (out, dirty) = hash_beside(
1633                        &mut self.bands,
1634                        argb,
1635                        stride,
1636                        height as usize,
1637                        threshold,
1638                        duration,
1639                        encode,
1640                    );
1641                    self.turbo_dirty = dirty;
1642                    out
1643                } else {
1644                    self.turbo_dirty.clear();
1645                    encode()
1646                }
1647            } else {
1648                encode_cpu(
1649                    &mut self.stripes,
1650                    &mut self.stripes_carrying,
1651                    argb,
1652                    width,
1653                    height,
1654                    &[],
1655                    &self.settings,
1656                    self.frame_counter,
1657                    false,
1658                    true,
1659                    force_idr_all,
1660                )
1661            }
1662        };
1663
1664        // An unserved request stays armed: on an infinite GOP an IDR lost to an encode
1665        // error or skip would never self-heal, leaving a joining consumer with an
1666        // undecodable stream. A rebuilt or demoted encoder arms one the same way, which
1667        // is what the second read of the flag picks up.
1668        self.pending_force_idr = (requested || self.pending_force_idr) && out.is_empty();
1669        self.frame_counter = self.frame_counter.wrapping_add(1);
1670        out
1671    }
1672}
1673
1674#[cfg(test)]
1675mod tests {
1676    use super::*;
1677
1678    /// An encoder that holds a quantizer and reports no quality of its own.
1679    const HOLDS: EncoderQuality = EncoderQuality {
1680        last: None,
1681        bytes: None,
1682        holds: true,
1683        reopens: false,
1684        keys: true,
1685        band: None,
1686        measures: false,
1687        psnr: None,
1688    };
1689
1690    /// A settings block for the hardware full-frame policy: a constant rate, whose cleanup holds
1691    /// its frames (`HOLDS` reports no quality of its own, so the cleanup always improves),
1692    /// paint-over on and clearly finer than normal, a short burst, a short trigger, and no
1693    /// scheduled IDR. `crf_settings` is the constant-quality one.
1694    fn hw_settings() -> RustCaptureSettings {
1695        RustCaptureSettings {
1696            codec: Codec::H264,
1697            video_cbr_mode: true,
1698            video_crf: 25,
1699            video_paintover_crf: 10,
1700            video_paintover_burst_frames: 3,
1701            paint_over_trigger_frames: 2,
1702            use_paint_over_quality: true,
1703            video_streaming_mode: false,
1704            keyframe_interval_s: 0.0,
1705            target_fps: 60.0,
1706            ..Default::default()
1707        }
1708    }
1709
1710    /// `hw_settings` at a constant quality.
1711    fn crf_settings() -> RustCaptureSettings {
1712        RustCaptureSettings {
1713            video_cbr_mode: false,
1714            ..hw_settings()
1715        }
1716    }
1717
1718    /// Drive the policy through `frames` frames of `damage`, returning the decisions.
1719    fn run(
1720        st: &mut StripeState,
1721        s: &RustCaptureSettings,
1722        damage: Damage,
1723        frames: usize,
1724    ) -> Vec<HwFrameDecision> {
1725        (0..frames)
1726            .map(|i| decide_hw_fullframe(st, s, i as u16 + 1, damage, false, false, HOLDS))
1727            .collect()
1728    }
1729
1730    /// Once motion stops a constant-rate session's screen is cleaned up whether Turbo sends every
1731    /// frame or not: a refresh held at the paint-over quantizer after the trigger, a key frame
1732    /// held there once it has held still four trigger periods, each followed by the recovery
1733    /// burst, then silence (or Turbo's frames at the session quality).
1734    #[test]
1735    fn a_still_screen_is_cleaned_up_whatever_turbo_sends() {
1736        for turbo in [false, true] {
1737            let s = RustCaptureSettings {
1738                video_streaming_mode: turbo,
1739                ..hw_settings()
1740            };
1741            let paint = Some(s.video_paintover_crf as u32);
1742            let mut st = StripeState::default();
1743            let moving = run(&mut st, &s, Damage::Area(1.0), 3);
1744            assert!(
1745                moving
1746                    .iter()
1747                    .all(|d| d.send && !d.force_idr && d.hold_qp.is_none()),
1748                "motion sends at the session quality"
1749            );
1750            let still = run(&mut st, &s, Damage::None, 16);
1751            let sent: Vec<(usize, bool, Option<u32>)> = still
1752                .iter()
1753                .enumerate()
1754                .filter(|(_, d)| d.send)
1755                .map(|(i, d)| (i + 1, d.force_idr, d.hold_qp))
1756                .collect();
1757            let cleanups = vec![
1758                (2, false, paint),
1759                (3, false, paint),
1760                (4, false, paint),
1761                (5, false, paint),
1762                (8, true, paint),
1763                (9, false, paint),
1764                (10, false, paint),
1765                (11, false, paint),
1766            ];
1767            if !turbo {
1768                assert_eq!(
1769                    sent, cleanups,
1770                    "a refresh at the trigger and a key four triggers in, each with its burst held"
1771                );
1772            } else {
1773                assert_eq!(sent.len(), 16, "Turbo sends every frame");
1774                let held: Vec<(usize, bool, Option<u32>)> =
1775                    sent.into_iter().filter(|r| r.2.is_some()).collect();
1776                assert_eq!(held, cleanups, "the same cleanups under Turbo");
1777            }
1778            assert!(
1779                still.iter().all(|d| d.target_qp == s.video_crf as u32),
1780                "the session's own quality is left alone"
1781            );
1782        }
1783    }
1784
1785    /// Under a constant rate the refresh and its burst are held at what the budget allows below the
1786    /// rate control's last quantizer, the key frame at the paint-over one (the encoder caps it to
1787    /// its budget), and the burst behind the key frame at the refresh's again, so a key frame the
1788    /// cap coarsened is refined back.
1789    #[test]
1790    fn a_constant_rate_cleanup_keeps_to_its_budget() {
1791        let s = RustCaptureSettings {
1792            video_cbr_mode: true,
1793            ..hw_settings()
1794        };
1795        let coarse = EncoderQuality {
1796            last: Some(40),
1797            bytes: None,
1798            holds: true,
1799            reopens: false,
1800            keys: true,
1801            band: None,
1802            measures: false,
1803            psnr: None,
1804        };
1805        let mut st = StripeState::default();
1806        for i in 0..3u16 {
1807            decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, coarse);
1808        }
1809        let still: Vec<HwFrameDecision> = (0..12u16)
1810            .map(|i| decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, coarse))
1811            .collect();
1812        let refresh_qp = held_refresh_quality(&s, coarse);
1813        assert_eq!(
1814            refresh_qp, 10,
1815            "35 steps under the rate control's 40 is past the paint-over quality"
1816        );
1817        assert_eq!(
1818            still[1].hold_qp,
1819            Some(refresh_qp),
1820            "the refresh at the budget's quality"
1821        );
1822        assert!(
1823            (2..5).all(|i| still[i].hold_qp == Some(refresh_qp)),
1824            "its burst at the same"
1825        );
1826        assert!(
1827            still[7].force_idr && still[7].hold_qp == Some(s.video_paintover_crf as u32),
1828            "the key at the paint-over one"
1829        );
1830        assert!(
1831            (8..11).all(|i| still[i].send && still[i].hold_qp == Some(refresh_qp)),
1832            "its burst at the refresh's"
1833        );
1834    }
1835
1836    /// A small change is cleaned up by a refresh alone; one covering half a screen since the last
1837    /// key frame earns a key frame once the screen holds still long enough.
1838    #[test]
1839    fn a_small_change_is_refreshed_and_a_large_one_keyed() {
1840        let s = RustCaptureSettings {
1841            video_paintover_burst_frames: 0,
1842            ..hw_settings()
1843        };
1844        let keys = |st: &mut StripeState, damage: Damage, frames: usize| -> (usize, usize) {
1845            run(st, &s, damage, frames);
1846            let still = run(st, &s, Damage::None, 12);
1847            (
1848                still
1849                    .iter()
1850                    .filter(|d| d.hold_qp.is_some() && !d.force_idr)
1851                    .count(),
1852                still.iter().filter(|d| d.force_idr).count(),
1853            )
1854        };
1855        let mut st = StripeState::default();
1856        assert_eq!(
1857            keys(&mut st, Damage::Area(1.0), 2),
1858            (1, 1),
1859            "a scroll is refreshed, then keyed"
1860        );
1861        assert_eq!(
1862            keys(&mut st, Damage::Area(0.001), 1),
1863            (1, 0),
1864            "a caret is refreshed, never keyed"
1865        );
1866        assert_eq!(
1867            keys(&mut st, Damage::Area(0.3), 2),
1868            (1, 1),
1869            "0.6 of a screen changed since the last key frame"
1870        );
1871    }
1872
1873    /// An encoder whose held refresh restores the picture whole (libvpx's VP8) is never keyed:
1874    /// after a large change and a long stillness its refresh and burst are the whole cleanup,
1875    /// where one that holds only key frames (SVT-AV1) ends on a key frame.
1876    #[test]
1877    fn a_refresh_that_restores_the_picture_is_not_followed_by_a_key_frame() {
1878        let s = RustCaptureSettings {
1879            video_paintover_burst_frames: 0,
1880            ..hw_settings()
1881        };
1882        for (encoder, keyed) in [
1883            (
1884                EncoderQuality {
1885                    keys: false,
1886                    ..HOLDS
1887                },
1888                0,
1889            ),
1890            (HOLDS, 1),
1891        ] {
1892            let mut st = StripeState::default();
1893            for i in 0..2u16 {
1894                decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, encoder);
1895            }
1896            let still: Vec<HwFrameDecision> = (0..40u16)
1897                .map(|i| {
1898                    decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, encoder)
1899                })
1900                .collect();
1901            assert_eq!(
1902                still
1903                    .iter()
1904                    .filter(|d| d.hold_qp.is_some() && !d.force_idr)
1905                    .count(),
1906                1,
1907                "keys {}: one refresh",
1908                encoder.keys
1909            );
1910            assert_eq!(
1911                still.iter().filter(|d| d.force_idr).count(),
1912                keyed,
1913                "keys {}: the key frames",
1914                encoder.keys
1915            );
1916        }
1917    }
1918
1919    /// A still screen the rate control codes finer than the paint-over quantizer owes no cleanup,
1920    /// so its still frames are no low motion: the first frames of the next large change (a
1921    /// window opening) go out at the session quality, and the key frame waits for the screen to
1922    /// hold still after it.
1923    #[test]
1924    fn a_constant_rate_cleanup_runs_through_a_rate_control_that_converges() {
1925        let s = hw_settings();
1926        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
1927        let refining = EncoderQuality {
1928            last: Some(40),
1929            bytes: Some(budget),
1930            holds: true,
1931            reopens: false,
1932            keys: true,
1933            band: None,
1934            measures: false,
1935            psnr: None,
1936        };
1937        let converged = EncoderQuality {
1938            last: Some(8),
1939            bytes: Some(budget / 8),
1940            holds: true,
1941            reopens: false,
1942            keys: true,
1943            band: None,
1944            measures: false,
1945            psnr: None,
1946        };
1947        let trigger = s.paint_over_trigger_frames as u16;
1948        let window = trigger + (FALLBACK_TRIGGERS as u16) * trigger;
1949        let mut st = StripeState::default();
1950        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, refining);
1951        let still: Vec<HwFrameDecision> = (1..window)
1952            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, refining))
1953            .collect();
1954        assert!(
1955            still.iter().all(|d| d.hold_qp.is_none() && !d.force_idr),
1956            "no frame held, no key frame"
1957        );
1958        let first = still.iter().position(|d| d.send).expect("a cleanup");
1959        assert_eq!(first + 1, trigger as usize, "due at the trigger");
1960        assert!(
1961            still[first..].iter().all(|d| d.send),
1962            "the frames keep flowing while it refines"
1963        );
1964        let d = decide_hw_fullframe(&mut st, &s, window, Damage::None, false, false, converged);
1965        assert!(
1966            !d.send,
1967            "and stop once it codes a small frame at the paint-over quality"
1968        );
1969        assert!((window + 1..window + 100).all(|i| {
1970            !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, converged).send
1971        }));
1972    }
1973
1974    /// A session that holds no quantizer has no refresh to fall back to: its cleanup flows
1975    /// through the rate control past the window a holding one is given, and ends after a
1976    /// trigger period of frames coded at the paint-over quantizer, whatever their size, or
1977    /// once its finest quantizer has stood for `LEVEL_S`.
1978    #[test]
1979    fn a_session_that_holds_no_quantizer_is_refined_until_it_reaches_the_paint_over_quality() {
1980        let s = hw_settings();
1981        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
1982        let at = |last: u32| EncoderQuality {
1983            last: Some(last),
1984            bytes: Some(budget),
1985            holds: false,
1986            reopens: false,
1987            keys: true,
1988            band: None,
1989            measures: false,
1990            psnr: None,
1991        };
1992        let trigger = s.paint_over_trigger_frames as u16;
1993        let past = trigger + 2 * (FALLBACK_TRIGGERS as u16) * trigger;
1994        let mut st = StripeState::default();
1995        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(40));
1996        let still: Vec<HwFrameDecision> = (1..past)
1997            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(40)))
1998            .collect();
1999        assert!(still.iter().all(|d| d.hold_qp.is_none() && !d.force_idr));
2000        assert!(still[trigger as usize - 1..].iter().all(|d| d.send));
2001        let fine = s.video_paintover_crf as u32;
2002        assert!(
2003            (past..past + trigger).all(|i| {
2004                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(fine)).send
2005            }),
2006            "a trigger period of frames at it"
2007        );
2008        assert!((past + trigger..past + 100).all(|i| {
2009            !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(fine)).send
2010        }));
2011        let window = |encoder: EncoderQuality| {
2012            let mut st = StripeState::default();
2013            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, encoder);
2014            (1..u16::MAX)
2015                .filter(|&i| {
2016                    decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, encoder).send
2017                })
2018                .count() as f64
2019                / s.target_fps
2020        };
2021        assert!(
2022            (window(at(40)) - LEVEL_S).abs() < 1.0,
2023            "a quantizer that stands ends it"
2024        );
2025        let mut st = StripeState::default();
2026        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(44));
2027        let step = (2.0 * s.target_fps) as u32;
2028        let refining = (1..u16::MAX)
2029            .filter(|&i| {
2030                let q = 44 - (i as u32 / step).min(20);
2031                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(q)).send
2032            })
2033            .count() as f64
2034            / s.target_fps;
2035        assert!(
2036            (refining - REFINE_S).abs() < 1.0,
2037            "one a step finer every two seconds runs the window"
2038        );
2039        let silent = EncoderQuality {
2040            last: None,
2041            ..at(40)
2042        };
2043        assert!(
2044            (window(silent) - CONVERGE_S).abs() < 1.0,
2045            "one that says no quantizer gets the shorter window"
2046        );
2047    }
2048
2049    /// Such a session whose frames run small has nothing left to refine: the cleanup ends, and
2050    /// a caret blinking on the settled screen starts no other; motion does.
2051    #[test]
2052    fn a_settled_cleanup_is_not_restarted_by_a_caret() {
2053        let s = hw_settings();
2054        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2055        let small = EncoderQuality {
2056            last: None,
2057            bytes: Some(budget / 8),
2058            holds: false,
2059            reopens: false,
2060            keys: true,
2061            band: None,
2062            measures: false,
2063            psnr: None,
2064        };
2065        let trigger = s.paint_over_trigger_frames;
2066        let mut st = StripeState::default();
2067        let mut frame = 0u16;
2068        let mut run = |st: &mut StripeState, damage: Damage, frames: u32| {
2069            (0..frames)
2070                .filter(|_| {
2071                    frame += 1;
2072                    decide_hw_fullframe(st, &s, frame, damage, false, false, small).send
2073                })
2074                .count() as u32
2075        };
2076        run(&mut st, Damage::Area(1.0), 1);
2077        let cleanup = run(&mut st, Damage::None, 20 * trigger);
2078        assert_eq!(cleanup, STALL_TRIGGERS * trigger + 1);
2079        let mut blinking = 0;
2080        for _ in 0..10 {
2081            blinking += run(&mut st, Damage::Area(0.001), 1);
2082            blinking += run(&mut st, Damage::None, 2 * trigger);
2083        }
2084        assert_eq!(blinking, 20, "each blink and its refresh, and no more");
2085        run(&mut st, Damage::Area(1.0), 1);
2086        assert_eq!(run(&mut st, Damage::None, 20 * trigger), cleanup);
2087    }
2088
2089    #[test]
2090    fn a_rate_control_that_does_not_converge_gets_one_held_refresh() {
2091        let s = hw_settings();
2092        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2093        let crawling = EncoderQuality {
2094            last: Some(38),
2095            bytes: Some(budget / 2),
2096            holds: true,
2097            reopens: false,
2098            keys: true,
2099            band: None,
2100            measures: false,
2101            psnr: None,
2102        };
2103        let mut st = StripeState::default();
2104        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, crawling);
2105        let still: Vec<HwFrameDecision> = (1..=200)
2106            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, crawling))
2107            .collect();
2108        assert!(still.iter().all(|d| !d.force_idr), "no key frame");
2109        let held: Vec<usize> = still
2110            .iter()
2111            .enumerate()
2112            .filter(|(_, d)| d.hold_qp.is_some())
2113            .map(|(i, _)| i)
2114            .collect();
2115        assert_eq!(held.len(), 1, "{held:?}");
2116        let trigger = s.paint_over_trigger_frames as usize;
2117        assert_eq!(
2118            held[0] + 1,
2119            trigger + (FALLBACK_TRIGGERS as usize) * trigger,
2120            "after the window"
2121        );
2122        assert_eq!(
2123            still[held[0]].hold_qp,
2124            Some(held_refresh_quality(&s, crawling))
2125        );
2126        assert!(still[held[0] + 1..].iter().all(|d| !d.send));
2127    }
2128
2129    /// A rate control pinned at its coarsest quantizer on a still screen, its frames many budgets
2130    /// each, is refreshed by one held frame at once, coarsened to what a second of the target buys
2131    /// at the bytes its frames take, and the cleanup ends: a band sweep would carry the rest of
2132    /// every frame at that quantizer.
2133    #[test]
2134    fn a_rate_control_pinned_at_its_coarsest_quantizer_is_refreshed_once() {
2135        let s = RustCaptureSettings {
2136            video_bitrate_kbps: 100,
2137            ..hw_settings()
2138        };
2139        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2140        let pinned = EncoderQuality {
2141            last: Some(51),
2142            bytes: Some((budget * 16.0) as usize),
2143            holds: true,
2144            reopens: false,
2145            keys: true,
2146            band: Some(0),
2147            measures: false,
2148            psnr: None,
2149        };
2150        let mut st = StripeState::default();
2151        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
2152        let still: Vec<HwFrameDecision> = (1..=200)
2153            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned))
2154            .collect();
2155        let sent: Vec<usize> = still
2156            .iter()
2157            .enumerate()
2158            .filter(|(_, d)| d.send)
2159            .map(|(i, _)| i)
2160            .collect();
2161        let trigger = s.paint_over_trigger_frames as usize;
2162        let refresh = 2 * trigger - 1;
2163        assert_eq!(
2164            sent,
2165            (trigger - 1..=refresh).collect::<Vec<_>>(),
2166            "a trigger period through the rate control, then the refresh"
2167        );
2168        assert_eq!(
2169            (still[refresh].hold_qp, still[refresh].hold_band),
2170            (Some(40), None),
2171            "frames of 16 budgets leave 11 steps of the 35 a second buys"
2172        );
2173    }
2174
2175    /// A key frame asked for on a still screen with the paint-over off opens a burst through the
2176    /// rate control, which ends at once where the rate control is pinned at its coarsest quantizer.
2177    #[test]
2178    fn a_pinned_rate_control_ends_a_recovery_burst() {
2179        let s = RustCaptureSettings {
2180            video_bitrate_kbps: 100,
2181            use_paint_over_quality: false,
2182            ..hw_settings()
2183        };
2184        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2185        let pinned = EncoderQuality {
2186            last: Some(51),
2187            bytes: Some((budget * 16.0) as usize),
2188            holds: true,
2189            reopens: false,
2190            keys: true,
2191            band: Some(0),
2192            measures: false,
2193            psnr: None,
2194        };
2195        let mut st = StripeState::default();
2196        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
2197        assert!((1..=20).all(|i| {
2198            !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned).send
2199        }));
2200        let key = decide_hw_fullframe(&mut st, &s, 21, Damage::None, false, true, pinned);
2201        assert!(key.send && key.force_idr);
2202        let trigger = s.paint_over_trigger_frames as u16;
2203        assert!(
2204            (22..21 + trigger).all(|i| decide_hw_fullframe(
2205                &mut st,
2206                &s,
2207                i,
2208                Damage::None,
2209                false,
2210                false,
2211                pinned
2212            )
2213            .send),
2214            "its burst until a trigger period over the budget"
2215        );
2216        assert!(
2217            (21 + trigger..=200).all(|i| !decide_hw_fullframe(
2218                &mut st,
2219                &s,
2220                i,
2221                Damage::None,
2222                false,
2223                false,
2224                pinned
2225            )
2226            .send),
2227            "and no more"
2228        );
2229    }
2230
2231    #[test]
2232    fn a_blinking_caret_does_not_hold_off_a_converging_cleanup() {
2233        let s = hw_settings();
2234        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2235        let crawling = EncoderQuality {
2236            last: Some(38),
2237            bytes: Some(budget / 2),
2238            holds: true,
2239            reopens: false,
2240            keys: true,
2241            band: None,
2242            measures: false,
2243            psnr: None,
2244        };
2245        let mut st = StripeState::default();
2246        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, crawling);
2247        let trigger = s.paint_over_trigger_frames as u16;
2248        let decisions: Vec<HwFrameDecision> = (1..300)
2249            .map(|i| {
2250                let damage = if i % 3 == 0 {
2251                    Damage::Area(0.001)
2252                } else {
2253                    Damage::None
2254                };
2255                decide_hw_fullframe(&mut st, &s, i, damage, false, false, crawling)
2256            })
2257            .collect();
2258        assert!(decisions.iter().all(|d| !d.force_idr), "no key frame");
2259        let first = decisions
2260            .iter()
2261            .position(|d| d.hold_qp.is_some())
2262            .expect("a held refresh") as u16
2263            + 1;
2264        let bound = trigger * (LOW_MOTION_TRIGGERS + 2 * FALLBACK_TRIGGERS) as u16 * 3 / 2;
2265        assert!(
2266            first <= bound,
2267            "the caret does not hold it off: {first} > {bound}"
2268        );
2269        let held = decisions.iter().filter(|d| d.hold_qp.is_some()).count();
2270        assert!(
2271            held <= 300 / (trigger as usize * LOW_MOTION_TRIGGERS as usize),
2272            "{held} held refreshes"
2273        );
2274    }
2275
2276    #[test]
2277    fn a_stalled_rate_control_is_held_once_at_the_paint_over_quantizer() {
2278        let s = hw_settings();
2279        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2280        let idle = EncoderQuality {
2281            last: Some(38),
2282            bytes: Some(budget / 10),
2283            holds: true,
2284            reopens: false,
2285            keys: true,
2286            band: None,
2287            measures: false,
2288            psnr: None,
2289        };
2290        let mut st = StripeState::default();
2291        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle);
2292        let still: Vec<HwFrameDecision> = (1..=200)
2293            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle))
2294            .collect();
2295        let held: Vec<usize> = still
2296            .iter()
2297            .enumerate()
2298            .filter(|(_, d)| d.hold_qp.is_some())
2299            .map(|(i, _)| i)
2300            .collect();
2301        assert_eq!(held.len(), 1, "one held frame: {held:?}");
2302        assert_eq!(still[held[0]].hold_qp, Some(held_refresh_quality(&s, idle)));
2303        assert!(!still[held[0]].force_idr, "a refresh, not a key frame");
2304        let stall = (STALL_TRIGGERS * s.paint_over_trigger_frames) as usize;
2305        assert!(
2306            held[0] + 1 >= s.paint_over_trigger_frames as usize + stall,
2307            "after the stall, not at the trigger"
2308        );
2309        assert!(
2310            still[held[0] + 1..].iter().all(|d| !d.send),
2311            "which ends the cleanup"
2312        );
2313    }
2314
2315    #[test]
2316    fn a_stalled_rate_control_that_holds_a_band_sweeps_the_picture_within_the_budget() {
2317        let s = hw_settings();
2318        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2319        let idle = |held: usize| EncoderQuality {
2320            last: Some(38),
2321            bytes: Some(budget as usize / 10),
2322            holds: true,
2323            reopens: false,
2324            keys: true,
2325            band: Some(held),
2326            measures: false,
2327            psnr: None,
2328        };
2329        let mut st = StripeState::default();
2330        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
2331        let mut bands: Vec<(f64, f64)> = Vec::new();
2332        let mut sent_after = 0;
2333        for i in 1..=600u16 {
2334            let held = bands
2335                .last()
2336                .map_or(0, |(a, b)| ((b - a) * 128.0 * budget) as usize);
2337            let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
2338            assert!(!d.force_idr, "no key frame");
2339            match d.hold_band {
2340                Some(band) => {
2341                    assert!(d.send && d.hold_qp == Some(held_refresh_quality(&s, idle(0))));
2342                    bands.push(band);
2343                }
2344                None => {
2345                    assert!(d.hold_qp.is_none(), "every held frame is a band");
2346                    sent_after += (!bands.is_empty() && d.send) as u32;
2347                }
2348            }
2349        }
2350        assert_eq!(
2351            bands[0],
2352            (0.0, FIRST_BAND),
2353            "the sweep starts at the stall, from the top"
2354        );
2355        assert!(
2356            bands.windows(2).all(|w| w[0].1 == w[1].0),
2357            "contiguous bands: {bands:?}"
2358        );
2359        assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
2360        assert!(
2361            bands[2..bands.len() - 1]
2362                .iter()
2363                .all(|(a, b)| ((b - a) * 128.0 - 1.0).abs() < 1e-3),
2364            "each a budget: {bands:?}"
2365        );
2366        assert_eq!(sent_after, 0, "which ends the cleanup");
2367    }
2368
2369    /// A session that holds its refresh rather than converging to it, and holds a band of a frame
2370    /// (libvpx's VP8), sweeps that refresh a band a frame from the top, each a budget, until it
2371    /// covers the picture, with no whole held frame; a caret blinking meanwhile leaves the sweep
2372    /// to finish.
2373    #[test]
2374    fn a_held_refresh_that_holds_a_band_sweeps_the_picture() {
2375        let s = hw_settings();
2376        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2377        let vp8 = |held: usize| EncoderQuality {
2378            last: Some(38),
2379            bytes: None,
2380            holds: true,
2381            reopens: false,
2382            keys: false,
2383            band: Some(held),
2384            measures: false,
2385            psnr: None,
2386        };
2387        let mut st = StripeState::default();
2388        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, vp8(0));
2389        let mut bands: Vec<(f64, f64)> = Vec::new();
2390        let mut caret_in_sweep = false;
2391        let mut blinked = false;
2392        for i in 1..=600u16 {
2393            let held = bands
2394                .last()
2395                .map_or(0, |(a, b)| ((b - a) * 128.0 * budget) as usize);
2396            let caret = !blinked && bands.len() == 20;
2397            blinked |= caret;
2398            caret_in_sweep |= caret && st.sweep.is_some();
2399            let damage = if caret {
2400                Damage::Area(0.001)
2401            } else {
2402                Damage::None
2403            };
2404            let d = decide_hw_fullframe(&mut st, &s, i, damage, false, false, vp8(held));
2405            assert!(!d.force_idr, "no key frame");
2406            match d.hold_band {
2407                Some(band) => {
2408                    assert!(d.send && d.hold_qp == Some(held_refresh_quality(&s, vp8(0))));
2409                    bands.push(band);
2410                }
2411                None => assert!(d.hold_qp.is_none(), "no whole held frame, frame {i}"),
2412            }
2413        }
2414        assert!(caret_in_sweep, "the caret blinked mid-sweep");
2415        assert_eq!(bands[0], (0.0, FIRST_BAND), "the sweep starts from the top");
2416        assert!(
2417            bands.windows(2).all(|w| w[0].1 == w[1].0),
2418            "contiguous bands, the caret's frame between two: {bands:?}"
2419        );
2420        assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
2421    }
2422
2423    /// A sweep whose bands, each a budget, would take longer than `REFINE_S` to cover the picture
2424    /// is coded a step coarser a band until its pace fits: here a picture of four times as many
2425    /// budgets at the refresh's quantizer, half that six steps coarser.
2426    #[test]
2427    fn a_sweep_that_would_run_past_refine_s_is_coded_coarser() {
2428        let s = hw_settings();
2429        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2430        let idle = |held: usize| EncoderQuality {
2431            last: Some(38),
2432            bytes: Some(budget as usize / 10),
2433            holds: true,
2434            reopens: false,
2435            keys: true,
2436            band: Some(held),
2437            measures: false,
2438            psnr: None,
2439        };
2440        let refresh = held_refresh_quality(&s, idle(0));
2441        let mut st = StripeState::default();
2442        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
2443        let window = (REFINE_S * s.target_fps).round() as usize;
2444        let mut bands: Vec<(f64, f64, u32)> = Vec::new();
2445        for i in 1..=(2 * window) as u16 {
2446            let held = bands.last().map_or(0, |(a, b, q)| {
2447                let cost = 4.0 * window as f64 * 2f64.powf(-((q - refresh) as f64) / 6.0);
2448                ((b - a) * cost * budget) as usize
2449            });
2450            let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
2451            if let (Some((a, b)), Some(q)) = (d.hold_band, d.hold_qp) {
2452                bands.push((a, b, q));
2453            }
2454        }
2455        let coarser = bands.last().unwrap().2 - refresh;
2456        assert_eq!(bands.last().unwrap().1, 1.0, "the sweep covers the picture");
2457        assert!(
2458            bands.len() <= window + window / 10,
2459            "within REFINE_S: {} bands",
2460            bands.len()
2461        );
2462        assert!(
2463            bands.windows(2).all(|w| w[0].2 <= w[1].2),
2464            "never finer again"
2465        );
2466        assert!(
2467            (12..=15).contains(&coarser),
2468            "about the 12 steps that quarter the picture's cost: {coarser}"
2469        );
2470    }
2471
2472    /// A session that holds no quantizer and measures nothing, whose frames of a still screen keep
2473    /// the size of the one before (libvpx's VP9 at 0.1 Mbit/s, over the budget and so never
2474    /// small), ends its cleanup after `EMPTY_S` of them rather than `REFINE_S`.
2475    #[test]
2476    fn a_cleanup_whose_frames_keep_one_size_ends() {
2477        let s = RustCaptureSettings {
2478            video_bitrate_kbps: 100,
2479            ..hw_settings()
2480        };
2481        let level = EncoderQuality {
2482            last: Some(51),
2483            bytes: Some(266),
2484            holds: false,
2485            reopens: false,
2486            keys: true,
2487            band: None,
2488            measures: false,
2489            psnr: None,
2490        };
2491        let mut st = StripeState::default();
2492        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, level);
2493        let sent = (1..=2400u16)
2494            .filter(|&i| {
2495                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, level).send
2496            })
2497            .count();
2498        let run = (EMPTY_S * s.target_fps) as usize;
2499        assert!(
2500            (run..=run + 4).contains(&sent),
2501            "{sent} frames, not the {} of REFINE_S",
2502            (REFINE_S * s.target_fps) as usize
2503        );
2504    }
2505
2506    #[test]
2507    fn motion_ends_a_band_sweep() {
2508        let s = hw_settings();
2509        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2510        let idle = EncoderQuality {
2511            last: Some(38),
2512            bytes: Some(budget / 10),
2513            holds: true,
2514            reopens: false,
2515            keys: true,
2516            band: Some(budget),
2517            measures: false,
2518            psnr: None,
2519        };
2520        let mut st = StripeState::default();
2521        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle);
2522        let first = (1..=200u16)
2523            .find(|&i| {
2524                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle)
2525                    .hold_band
2526                    .is_some()
2527            })
2528            .expect("a sweep");
2529        let moving = decide_hw_fullframe(
2530            &mut st,
2531            &s,
2532            first + 1,
2533            Damage::Area(0.5),
2534            false,
2535            false,
2536            idle,
2537        );
2538        assert!(moving.send && moving.hold_qp.is_none() && moving.hold_band.is_none());
2539        assert!(st.sweep.is_none());
2540        let caret = decide_hw_fullframe(
2541            &mut st,
2542            &s,
2543            first + 2,
2544            Damage::Area(0.001),
2545            false,
2546            false,
2547            idle,
2548        );
2549        assert!(caret.hold_band.is_none());
2550    }
2551
2552    #[test]
2553    fn a_recovery_key_frame_is_refined_until_the_rate_control_converges() {
2554        let s = hw_settings();
2555        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2556        let coarse = EncoderQuality {
2557            last: Some(40),
2558            bytes: Some(budget),
2559            holds: true,
2560            reopens: false,
2561            keys: true,
2562            band: None,
2563            measures: false,
2564            psnr: None,
2565        };
2566        let clean = EncoderQuality {
2567            last: Some(8),
2568            bytes: Some(budget / 8),
2569            holds: true,
2570            reopens: false,
2571            keys: true,
2572            band: None,
2573            measures: false,
2574            psnr: None,
2575        };
2576        let mut st = StripeState::default();
2577        assert!(
2578            (0..200).all(|i| !decide_hw_fullframe(
2579                &mut st,
2580                &s,
2581                i,
2582                Damage::None,
2583                false,
2584                false,
2585                clean
2586            )
2587            .send)
2588        );
2589        let join = decide_hw_fullframe(&mut st, &s, 200, Damage::None, false, true, clean);
2590        assert!(join.send && join.force_idr && join.hold_qp.is_none());
2591        let past_burst = 201 + s.video_paintover_burst_frames as u16 + 3;
2592        assert!(
2593            (201..past_burst).all(|i| {
2594                let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, coarse);
2595                d.send && !d.force_idr && d.hold_qp.is_none()
2596            }),
2597            "the rate control keeps refining the key frame past the configured burst"
2598        );
2599        assert!(
2600            !decide_hw_fullframe(&mut st, &s, past_burst, Damage::None, false, false, clean).send,
2601            "until it converges"
2602        );
2603    }
2604
2605    #[test]
2606    fn a_large_change_after_a_clean_still_screen_is_not_keyed_as_it_lands() {
2607        let s = RustCaptureSettings {
2608            video_paintover_burst_frames: 0,
2609            ..hw_settings()
2610        };
2611        let fine = EncoderQuality {
2612            last: Some(5),
2613            bytes: None,
2614            holds: true,
2615            reopens: false,
2616            keys: true,
2617            band: None,
2618            measures: false,
2619            psnr: None,
2620        };
2621        let coarse = EncoderQuality {
2622            last: Some(40),
2623            bytes: None,
2624            holds: true,
2625            reopens: false,
2626            keys: true,
2627            band: None,
2628            measures: false,
2629            psnr: None,
2630        };
2631        let mut st = StripeState::default();
2632        let mut frame = 0u16;
2633        let mut step = |st: &mut StripeState, damage: Damage, q: EncoderQuality| {
2634            frame = frame.wrapping_add(1);
2635            decide_hw_fullframe(st, &s, frame, damage, false, false, q)
2636        };
2637        step(&mut st, Damage::Area(1.0), coarse);
2638        assert!(
2639            (0..100).all(|_| step(&mut st, Damage::None, fine).hold_qp.is_none()),
2640            "a finer rate control is left alone"
2641        );
2642        let landing = [
2643            step(&mut st, Damage::Area(1.0), fine),
2644            step(&mut st, Damage::Area(1.0), coarse),
2645        ];
2646        assert!(
2647            landing
2648                .iter()
2649                .all(|d| d.send && !d.force_idr && d.hold_qp.is_none()),
2650            "the change goes out at the session quality"
2651        );
2652        assert!(
2653            (0..12).any(|_| step(&mut st, Damage::None, coarse).force_idr),
2654            "and is keyed once the screen holds still"
2655        );
2656    }
2657
2658    /// A screen that never holds still for the trigger (a caret blinking every other frame)
2659    /// still gets its cleanup, once the low-motion window passes, and the rest of the screen
2660    /// with it.
2661    #[test]
2662    fn low_motion_does_not_hold_off_the_cleanup_forever() {
2663        let s = RustCaptureSettings {
2664            paint_over_trigger_frames: 4,
2665            ..hw_settings()
2666        };
2667        let mut st = StripeState::default();
2668        run(&mut st, &s, Damage::Area(1.0), 3);
2669        let mut fired = None;
2670        for i in 0..40u16 {
2671            let damage = if i % 2 == 0 {
2672                Damage::Area(0.0005)
2673            } else {
2674                Damage::None
2675            };
2676            let d = decide_hw_fullframe(&mut st, &s, 100 + i, damage, false, false, HOLDS);
2677            if d.hold_qp.is_some() {
2678                fired = Some((i, d.force_idr));
2679                break;
2680            }
2681        }
2682        let (at, key) = fired.expect("a cleanup under low motion");
2683        assert_eq!(
2684            at as u32 + 4,
2685            s.paint_over_trigger_frames * LOW_MOTION_TRIGGERS,
2686            "at the end of the window since the scroll began"
2687        );
2688        assert!(key, "the scroll before it makes it a key frame");
2689
2690        // After a long scroll the caret gets the cleanup once the scroll has aged out of the
2691        // window, not only after a quarter of every frame since it began.
2692        let mut st = StripeState::default();
2693        run(&mut st, &s, Damage::Area(1.0), 200);
2694        let window = s.paint_over_trigger_frames * LOW_MOTION_TRIGGERS;
2695        let fired = (0..200u16).find(|&i| {
2696            let damage = if i % 2 == 0 {
2697                Damage::Area(0.0005)
2698            } else {
2699                Damage::None
2700            };
2701            decide_hw_fullframe(&mut st, &s, 300 + i, damage, false, false, HOLDS).force_idr
2702        });
2703        let at = fired.expect("a key frame under low motion after a long scroll") as u32;
2704        assert!(
2705            at >= window && at <= window * 2,
2706            "{at} frames after the scroll stopped, window {window}"
2707        );
2708    }
2709
2710    /// Sustained motion is not low motion: a screen changing every frame gets no cleanup until
2711    /// it stops.
2712    #[test]
2713    fn sustained_motion_gets_no_cleanup_until_it_stops() {
2714        let s = hw_settings();
2715        let mut st = StripeState::default();
2716        assert!(
2717            run(&mut st, &s, Damage::Area(0.5), 60)
2718                .iter()
2719                .all(|d| d.hold_qp.is_none())
2720        );
2721        assert!(
2722            run(&mut st, &s, Damage::Unknown, 60)
2723                .iter()
2724                .all(|d| d.hold_qp.is_none())
2725        );
2726    }
2727
2728    /// A change of unknown extent counts toward a key frame only in runs: isolated ones (a
2729    /// caret on a capture that cannot say where it changed) are refreshed, never keyed.
2730    #[test]
2731    fn unknown_changes_count_toward_a_key_frame_only_in_runs() {
2732        let s = hw_settings();
2733        let mut st = StripeState::default();
2734        for _ in 0..20 {
2735            run(&mut st, &s, Damage::Unknown, 1);
2736            let still = run(&mut st, &s, Damage::None, 10);
2737            assert!(
2738                still[1].hold_qp.is_some() && !still[1].force_idr,
2739                "isolated changes are refreshed"
2740            );
2741            assert!(still.iter().all(|d| !d.force_idr), "and never keyed");
2742        }
2743        run(&mut st, &s, Damage::Unknown, 12);
2744        assert!(
2745            run(&mut st, &s, Damage::None, 10)
2746                .iter()
2747                .any(|d| d.force_idr),
2748            "a run of ten counts for more than half a screen"
2749        );
2750    }
2751
2752    /// Motion outranks the burst: it sends at normal quality, drops a burst in flight, and forces
2753    /// no keyframe of its own.
2754    #[test]
2755    fn motion_sends_at_normal_quality_and_cancels_the_burst() {
2756        let s = hw_settings();
2757        let mut st = StripeState {
2758            h264_burst_frames_remaining: 3,
2759            paint_over_sent: true,
2760            no_motion_frame_count: 9,
2761            ..Default::default()
2762        };
2763        let d = decide_hw_fullframe(&mut st, &s, 1, Damage::Area(0.2), false, false, HOLDS);
2764        assert!(d.send && !d.force_idr && d.hold_qp.is_none());
2765        assert_eq!(d.target_qp, s.video_crf as u32);
2766        assert_eq!(st.h264_burst_frames_remaining, 0, "the burst is dropped");
2767        assert_eq!(st.no_motion_frame_count, 0);
2768        assert!(!st.paint_over_sent);
2769    }
2770
2771    /// A requested key frame is coded at the rate control's own quality whatever the screen does:
2772    /// it answers a join or a loss, often on a link that fell behind, where a larger key frame
2773    /// would only fall behind again. On a still screen of a constant-quality session the burst
2774    /// behind it refines at the paint-over quality, a clean screen's included; under a constant
2775    /// rate it is not held.
2776    #[test]
2777    fn a_requested_key_frame_is_the_rate_controls_own() {
2778        let s = RustCaptureSettings {
2779            paint_over_trigger_frames: 30,
2780            ..crf_settings()
2781        };
2782        let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
2783        let mut st = StripeState::default();
2784        run(&mut st, &s, Damage::Area(1.0), 1);
2785        let join = decide_hw_fullframe(&mut st, &s, 2, Damage::None, false, true, HOLDS);
2786        assert!(join.send && join.force_idr && join.hold_qp.is_none() && join.target_qp == normal);
2787        assert_eq!(
2788            st.h264_burst_frames_remaining,
2789            s.video_paintover_burst_frames
2790        );
2791        let burst = decide_hw_fullframe(&mut st, &s, 3, Damage::None, false, false, HOLDS);
2792        assert!(
2793            burst.send && burst.hold_qp.is_none() && burst.target_qp == paint,
2794            "the burst refines at the paint-over quality"
2795        );
2796
2797        let s = crf_settings();
2798        let mut st = StripeState::default();
2799        run(&mut st, &s, Damage::Area(1.0), 1);
2800        run(&mut st, &s, Damage::None, 12);
2801        assert!(st.clean_quality, "cleaned up");
2802        let join = decide_hw_fullframe(&mut st, &s, 30, Damage::None, false, true, HOLDS);
2803        assert!(
2804            join.force_idr && join.target_qp == normal,
2805            "a key frame on a clean screen at the session's quality"
2806        );
2807        let burst = decide_hw_fullframe(&mut st, &s, 31, Damage::None, false, false, HOLDS);
2808        assert_eq!(burst.target_qp, paint);
2809
2810        let cbr = RustCaptureSettings {
2811            video_cbr_mode: true,
2812            paint_over_trigger_frames: 30,
2813            ..hw_settings()
2814        };
2815        let coarse = EncoderQuality {
2816            last: Some(40),
2817            bytes: None,
2818            holds: true,
2819            reopens: false,
2820            keys: true,
2821            band: None,
2822            measures: false,
2823            psnr: None,
2824        };
2825        let mut st = StripeState::default();
2826        decide_hw_fullframe(&mut st, &cbr, 1, Damage::Area(1.0), false, false, coarse);
2827        let join = decide_hw_fullframe(&mut st, &cbr, 2, Damage::None, false, true, coarse);
2828        assert!(join.force_idr && join.hold_qp.is_none());
2829        let burst = decide_hw_fullframe(&mut st, &cbr, 3, Damage::None, false, false, coarse);
2830        assert!(
2831            burst.send && burst.hold_qp.is_none(),
2832            "a constant rate refines the burst itself"
2833        );
2834
2835        let mut moving = StripeState::default();
2836        let dirty = decide_hw_fullframe(&mut moving, &s, 1, Damage::Area(1.0), false, true, HOLDS);
2837        assert!(dirty.send && dirty.force_idr && dirty.hold_qp.is_none());
2838        assert_eq!(
2839            moving.h264_burst_frames_remaining, 0,
2840            "motion carries the refinement, so no burst is opened"
2841        );
2842    }
2843
2844    /// At a constant quality the cleanup applies where the paint-over quality is finer than the
2845    /// session's, through the session's quality, whatever the encoder holds. At a constant rate
2846    /// it applies where the paint-over quality is finer than what the rate control last coded
2847    /// at, or the encoder does not say: held where the encoder holds a quantizer, and otherwise a
2848    /// refresh and its burst at the rate control's own quality, never a key frame. Never with
2849    /// paint-over off.
2850    #[test]
2851    fn the_cleanup_follows_the_rate_control_and_the_switch() {
2852        #[derive(Debug, PartialEq)]
2853        enum Seen {
2854            Held,
2855            Session,
2856            Unheld,
2857            Nothing,
2858        }
2859        for (cbr, paint, on, last, holds, seen) in [
2860            (true, 25, true, None, true, Seen::Held),
2861            (true, 18, true, Some(30), true, Seen::Held),
2862            (true, 18, true, Some(9), true, Seen::Nothing),
2863            (true, 18, true, Some(18), true, Seen::Nothing),
2864            (true, 18, true, Some(30), false, Seen::Unheld),
2865            (true, 18, true, None, false, Seen::Unheld),
2866            (true, 18, true, Some(9), false, Seen::Nothing),
2867            (false, 25, true, None, true, Seen::Nothing),
2868            (false, 18, true, None, true, Seen::Session),
2869            (false, 18, true, None, false, Seen::Session),
2870            (false, 18, false, None, true, Seen::Nothing),
2871            (true, 18, false, None, true, Seen::Nothing),
2872        ] {
2873            let s = RustCaptureSettings {
2874                video_cbr_mode: cbr,
2875                video_paintover_crf: paint,
2876                use_paint_over_quality: on,
2877                ..hw_settings()
2878            };
2879            let mut st = StripeState::default();
2880            let mut got = Seen::Nothing;
2881            for i in 0..40u16 {
2882                let damage = if i == 0 {
2883                    Damage::Area(1.0)
2884                } else {
2885                    Damage::None
2886                };
2887                let d = decide_hw_fullframe(
2888                    &mut st,
2889                    &s,
2890                    i,
2891                    damage,
2892                    false,
2893                    false,
2894                    EncoderQuality {
2895                        last,
2896                        bytes: None,
2897                        holds,
2898                        reopens: false,
2899                        keys: true,
2900                        band: None,
2901                        measures: false,
2902                        psnr: None,
2903                    },
2904                );
2905                if i > 0 && d.send {
2906                    assert!(
2907                        !cbr || holds || !d.force_idr,
2908                        "a key frame where no quantizer is held: last={last:?}"
2909                    );
2910                    if d.hold_qp.is_some() {
2911                        got = Seen::Held;
2912                    } else if d.target_qp != s.video_crf as u32 {
2913                        assert!(!cbr, "a constant rate keeps its session quality");
2914                        got = Seen::Session;
2915                    } else if got == Seen::Nothing {
2916                        got = Seen::Unheld;
2917                    }
2918                }
2919            }
2920            assert_eq!(
2921                got, seen,
2922                "cbr={cbr} paint={paint} on={on} last={last:?} holds={holds}"
2923            );
2924        }
2925    }
2926
2927    /// At a constant quality the cleanup moves the session's quality to the paint-over one,
2928    /// holding no frame: the refresh at the trigger and its burst, the key frame four trigger
2929    /// periods in and its burst, and under Turbo every frame from the refresh on, since a region
2930    /// stays at the paint-over quality until it moves again.
2931    #[test]
2932    fn a_constant_quality_cleanup_moves_the_session_quality() {
2933        for turbo in [false, true] {
2934            let s = RustCaptureSettings {
2935                video_streaming_mode: turbo,
2936                ..crf_settings()
2937            };
2938            let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
2939            let mut st = StripeState::default();
2940            let moving = run(&mut st, &s, Damage::Area(1.0), 3);
2941            assert!(
2942                moving.iter().all(|d| d.send
2943                    && !d.force_idr
2944                    && d.hold_qp.is_none()
2945                    && d.target_qp == normal)
2946            );
2947            let still = run(&mut st, &s, Damage::None, 16);
2948            assert!(
2949                still.iter().all(|d| d.hold_qp.is_none()),
2950                "no frame is held"
2951            );
2952            let sent: Vec<(usize, bool, u32)> = still
2953                .iter()
2954                .enumerate()
2955                .filter(|(_, d)| d.send)
2956                .map(|(i, d)| (i + 1, d.force_idr, d.target_qp))
2957                .collect();
2958            if !turbo {
2959                let cleanups = vec![
2960                    (2, false, paint),
2961                    (3, false, paint),
2962                    (4, false, paint),
2963                    (5, false, paint),
2964                    (8, true, paint),
2965                    (9, false, paint),
2966                    (10, false, paint),
2967                    (11, false, paint),
2968                ];
2969                assert_eq!(
2970                    sent, cleanups,
2971                    "a refresh at the trigger and a key four triggers in, each with its burst"
2972                );
2973            } else {
2974                assert_eq!(sent.len(), 16, "Turbo sends every frame");
2975                assert_eq!(
2976                    sent[0],
2977                    (1, false, normal),
2978                    "the session's quality until the cleanup"
2979                );
2980                assert!(
2981                    sent[1..].iter().all(|r| r.2 == paint),
2982                    "the paint-over quality from the refresh on: {sent:?}"
2983                );
2984                assert_eq!(
2985                    sent.iter().filter(|r| r.1).map(|r| r.0).collect::<Vec<_>>(),
2986                    vec![8],
2987                    "one key frame"
2988                );
2989            }
2990        }
2991    }
2992
2993    /// An encoder whose quality change re-opens it codes the refresh that moves it as a key
2994    /// frame, which then counts as the key-frame cleanup: one key frame for one change, as main's
2995    /// paint-over sent, where the four-trigger key frame would have been a second.
2996    #[test]
2997    fn a_reopening_encoder_counts_its_refresh_as_the_key_frame() {
2998        let s = crf_settings();
2999        let reopens = EncoderQuality {
3000            last: None,
3001            bytes: None,
3002            holds: true,
3003            reopens: true,
3004            keys: true,
3005            band: None,
3006            measures: false,
3007            psnr: None,
3008        };
3009        let mut st = StripeState::default();
3010        for i in 0..3u16 {
3011            decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, reopens);
3012        }
3013        let keys: Vec<usize> = (0..16u16)
3014            .map(|i| decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, reopens))
3015            .enumerate()
3016            .filter(|(_, d)| d.force_idr)
3017            .map(|(i, _)| i + 1)
3018            .collect();
3019        assert_eq!(
3020            keys,
3021            vec![2],
3022            "the refresh at the trigger is the key frame, and the only one"
3023        );
3024    }
3025
3026    /// A clean region keeps the paint-over quality through a small change (a caret), so an
3027    /// encoder whose quality change re-opens it does not go back and forth; motion, or a change
3028    /// of unknown extent, takes it back to the session's quality until its next cleanup.
3029    #[test]
3030    fn a_clean_region_keeps_its_quality_until_it_moves() {
3031        let s = RustCaptureSettings {
3032            video_streaming_mode: true,
3033            ..crf_settings()
3034        };
3035        let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
3036        let mut st = StripeState::default();
3037        run(&mut st, &s, Damage::Area(1.0), 3);
3038        run(&mut st, &s, Damage::None, 12);
3039        let caret = decide_hw_fullframe(&mut st, &s, 100, Damage::Area(0.001), false, false, HOLDS);
3040        assert_eq!(caret.target_qp, paint, "a caret keeps it");
3041        assert_eq!(
3042            decide_hw_fullframe(&mut st, &s, 101, Damage::None, false, false, HOLDS).target_qp,
3043            paint
3044        );
3045        let scroll = decide_hw_fullframe(&mut st, &s, 102, Damage::Area(0.5), false, false, HOLDS);
3046        assert_eq!(scroll.target_qp, normal, "motion takes it back");
3047        assert_eq!(
3048            decide_hw_fullframe(&mut st, &s, 103, Damage::None, false, false, HOLDS).target_qp,
3049            normal,
3050            "until the next cleanup"
3051        );
3052        run(&mut st, &s, Damage::None, 12);
3053        assert_eq!(
3054            decide_hw_fullframe(&mut st, &s, 120, Damage::Unknown, false, false, HOLDS).target_qp,
3055            normal
3056        );
3057
3058        let off = RustCaptureSettings {
3059            use_paint_over_quality: false,
3060            ..s
3061        };
3062        let mut st = StripeState::default();
3063        run(&mut st, &off, Damage::Area(1.0), 3);
3064        assert!(
3065            run(&mut st, &off, Damage::None, 20)
3066                .iter()
3067                .all(|d| d.target_qp == normal),
3068            "paint-over off"
3069        );
3070    }
3071
3072    /// Streaming and animated modes send every frame, and a scheduled interval forces the
3073    /// keyframe `periodic_idr_due` picks even with nothing else happening.
3074    #[test]
3075    fn hw_fullframe_streaming_and_scheduled_keyframes_send_unconditionally() {
3076        let mut s = hw_settings();
3077        s.video_streaming_mode = true;
3078        let mut st = StripeState::default();
3079        assert!(
3080            decide_hw_fullframe(&mut st, &s, 1, Damage::None, false, false, HOLDS).send,
3081            "streaming mode sends a static frame"
3082        );
3083
3084        s.video_streaming_mode = false;
3085        let mut animated = StripeState::default();
3086        assert!(
3087            decide_hw_fullframe(&mut animated, &s, 1, Damage::None, true, false, HOLDS).send,
3088            "an animated overlay sends a static frame"
3089        );
3090
3091        // 1 s at 60 fps: frame 60 is due, 61 is not.
3092        s.keyframe_interval_s = 1.0;
3093        let mut scheduled = StripeState::default();
3094        assert!(
3095            decide_hw_fullframe(&mut scheduled, &s, 60, Damage::None, false, false, HOLDS)
3096                .force_idr
3097        );
3098        let mut off_beat = StripeState::default();
3099        assert!(
3100            !decide_hw_fullframe(&mut off_beat, &s, 61, Damage::None, false, false, HOLDS)
3101                .force_idr
3102        );
3103    }
3104
3105    /// Software JPEG path emits on change and stays silent while static: the first frame
3106    /// sends every stripe (all dirty vs init), an identical static frame sends nothing, and a
3107    /// frame with changed top rows re-sends the dirty stripes. `use_cpu` forces the software path
3108    /// and paint-over is disabled to keep the static-frame assertions clean.
3109    #[test]
3110    fn x11_software_emits_on_change_and_stays_quiet_when_static() {
3111        let s = RustCaptureSettings {
3112            width: 128,
3113            height: 128,
3114            codec: Codec::Jpeg,
3115            use_cpu: true,
3116            jpeg_quality: 60,
3117            use_paint_over_quality: false,
3118            ..Default::default()
3119        };
3120        let mut p = X11Pipeline::new(s);
3121        let stride = 128 * 4;
3122        let frame_a = vec![10u8; stride * 128];
3123        let mut frame_b = frame_a.clone();
3124        for px in frame_b.iter_mut().take(stride * 40) {
3125            *px = 200;
3126        }
3127        let n1 = p.process(&frame_a, stride).len();
3128        let n2 = p.process(&frame_a, stride).len();
3129        let n3 = p.process(&frame_b, stride).len();
3130        assert!(
3131            n1 > 0,
3132            "first frame should emit (all stripes dirty vs init)"
3133        );
3134        assert_eq!(n2, 0, "identical static frame should emit nothing");
3135        assert!(n3 > 0, "changed frame should emit dirty stripes");
3136    }
3137
3138    /// Software H.264 (the build's encoder), paint-over off, non-streaming: a requested IDR on
3139    /// a static screen is followed by a short recovery burst so rate control can refine the
3140    /// keyframe, instead of the stream going silent and stranding an unrefined keyframe. The
3141    /// stream goes quiet again once the burst ends.
3142    #[test]
3143    fn x11_software_h264_streams_recovery_burst_after_requested_idr() {
3144        let s = RustCaptureSettings {
3145            width: 128,
3146            height: 128,
3147            codec: Codec::H264,
3148            use_cpu: true,
3149            video_crf: 25,
3150            video_paintover_burst_frames: 5,
3151            use_paint_over_quality: false,
3152            video_streaming_mode: false,
3153            target_fps: 60.0,
3154            ..Default::default()
3155        };
3156        let mut p = X11Pipeline::new(s);
3157        let stride = 128 * 4;
3158        let frame = vec![10u8; stride * 128];
3159        assert!(!p.process(&frame, stride).is_empty(), "first frame emits");
3160        for _ in 0..4 {
3161            let _ = p.process(&frame, stride);
3162        }
3163        assert!(
3164            p.process(&frame, stride).is_empty(),
3165            "static screen is quiet before the request"
3166        );
3167        p.request_idr();
3168        assert!(
3169            !p.process(&frame, stride).is_empty(),
3170            "requested IDR emits on a static screen"
3171        );
3172        for i in 0..5 {
3173            assert!(
3174                !p.process(&frame, stride).is_empty(),
3175                "recovery burst frame {i} streams while static"
3176            );
3177        }
3178        assert!(
3179            p.process(&frame, stride).is_empty(),
3180            "stream goes quiet again after the recovery burst"
3181        );
3182    }
3183
3184    /// A noise screen of 640x384 at `seed`: a rate control codes it still coarser than a cleanup's
3185    /// quantizer.
3186    fn noise(seed: u8) -> Vec<u8> {
3187        (0..640 * 4 * 384)
3188            .map(|i| {
3189                (i as u32)
3190                    .wrapping_mul(2654435761)
3191                    .wrapping_add(seed as u32 * 97) as u8
3192            })
3193            .collect()
3194    }
3195
3196    /// A still screen x264 refines nothing more of at a constant rate, its frames slice headers
3197    /// alone, ends its cleanup after `EMPTY_S` of them rather than `CONVERGE_S`. The quantizer
3198    /// floor holds x264 at its coarsest, as at 0.1 Mbit/s, on every run and thread count.
3199    #[cfg(feature = "gpl")]
3200    #[test]
3201    fn an_x264_cleanup_whose_frames_refine_nothing_ends() {
3202        let mut p = X11Pipeline::new(RustCaptureSettings {
3203            width: 640,
3204            height: 384,
3205            codec: Codec::H264,
3206            use_cpu: true,
3207            video_fullframe: true,
3208            video_cbr_mode: true,
3209            video_bitrate_kbps: 100,
3210            video_min_qp: 51,
3211            video_paintover_crf: 18,
3212            paint_over_trigger_frames: 15,
3213            use_paint_over_quality: true,
3214            target_fps: 60.0,
3215            ..Default::default()
3216        });
3217        for i in 0..12u8 {
3218            p.process(&noise(i), 640 * 4);
3219        }
3220        let window = converge_frames(&p.settings) as usize;
3221        let frames: Vec<Vec<usize>> = (0..window + 60)
3222            .map(|_| {
3223                p.process(&noise(42), 640 * 4)
3224                    .iter()
3225                    .map(|e| e.data.len())
3226                    .collect()
3227            })
3228            .collect();
3229        let last = frames
3230            .iter()
3231            .rposition(|f| !f.is_empty())
3232            .expect("a cleanup");
3233        let empty = crate::encoders::codec::VIDEO_HEADER_LEN + 4 * EMPTY_SLICE_BYTES;
3234        let run = (EMPTY_S * 60.0) as usize;
3235        assert!(last < window, "the cleanup ends at {last} of {window}");
3236        assert!(
3237            frames[last + 1 - run..=last]
3238                .iter()
3239                .flatten()
3240                .all(|&b| b <= empty),
3241            "on frames that carry nothing: {:?}",
3242            &frames[last + 1 - run..=last]
3243        );
3244    }
3245
3246    /// The probes (`X11Pipeline::hold`) of a Turbo session's first still frame coded at a
3247    /// cleanup's quality, as a key frame where `key`, and of the same frame in a second session
3248    /// where the screen moves on it.
3249    fn cleanup_frame_that_moved(
3250        s: &RustCaptureSettings,
3251        key: bool,
3252    ) -> [[(Option<u32>, bool); 2]; 2] {
3253        let still = noise(42);
3254        let lead = |p: &mut X11Pipeline| {
3255            for i in 0..3u8 {
3256                p.process(&noise(i), 640 * 4);
3257            }
3258        };
3259        let mut p = X11Pipeline::new(s.clone());
3260        lead(&mut p);
3261        let at = (0..300)
3262            .find(|_| {
3263                p.process(&still, 640 * 4);
3264                p.hold[0].0.is_some() && p.hold[0].1 == key
3265            })
3266            .expect("a still screen is cleaned up");
3267        let mut q = X11Pipeline::new(s.clone());
3268        lead(&mut q);
3269        for _ in 0..at {
3270            q.process(&still, 640 * 4);
3271        }
3272        assert!(
3273            !q.process(&noise(9), 640 * 4).is_empty(),
3274            "the moved frame is sent"
3275        );
3276        [p.hold, q.hold]
3277    }
3278
3279    /// A Turbo frame is decided on the hash of the frame before it, so a change can land on a
3280    /// frame decided to be held whole at the cleanup's quantizer (SVT-AV1's held frames, from the
3281    /// release that takes a new target with a picture). That frame is hashed before it is
3282    /// encoded and goes to the rate control, a refresh or a key frame alike.
3283    #[test]
3284    fn a_turbo_frame_decided_held_that_moved_goes_to_the_rate_control() {
3285        let s = RustCaptureSettings {
3286            width: 640,
3287            height: 384,
3288            codec: Codec::Av1,
3289            use_cpu: true,
3290            video_cbr_mode: true,
3291            video_bitrate_kbps: 100,
3292            video_crf: 25,
3293            video_paintover_crf: 18,
3294            paint_over_trigger_frames: 15,
3295            use_paint_over_quality: true,
3296            video_streaming_mode: true,
3297            target_fps: 60.0,
3298            ..Default::default()
3299        };
3300        let p = X11Pipeline::new(s.clone());
3301        assert!(p.hw.is_some(), "AV1 runs as a full-frame session");
3302        if !codec_sys::svtav1::HAS_EVENTS {
3303            assert!(!EncoderQuality::of(p.hw.as_ref().unwrap()).holds);
3304            return;
3305        }
3306        for key in [false, true] {
3307            let [still, moved] = cleanup_frame_that_moved(&s, key);
3308            assert_eq!(
3309                still[1], still[0],
3310                "a frame that stood still keeps its hold"
3311            );
3312            assert!(moved[0].0.is_some() && moved[0].1 == key, "{moved:?}");
3313            assert_eq!(
3314                moved[1],
3315                (None, false),
3316                "a held frame that moved is coded under the rate control"
3317            );
3318        }
3319    }
3320
3321    /// The same at a constant quality, whose cleanup moves the session's quality to the
3322    /// paint-over one while the screen stays clean (`decide_constant_quality`): a frame that
3323    /// moved is coded at the session's own.
3324    #[test]
3325    fn a_turbo_frame_at_the_paint_over_quality_that_moved_is_coded_at_the_session_quality() {
3326        let s = RustCaptureSettings {
3327            width: 640,
3328            height: 384,
3329            codec: Codec::Vp9,
3330            use_cpu: true,
3331            video_cbr_mode: false,
3332            video_crf: 25,
3333            video_paintover_crf: 18,
3334            paint_over_trigger_frames: 15,
3335            use_paint_over_quality: true,
3336            video_streaming_mode: true,
3337            target_fps: 60.0,
3338            ..Default::default()
3339        };
3340        assert!(
3341            X11Pipeline::new(s.clone()).hw.is_some(),
3342            "VP9 runs as a full-frame session"
3343        );
3344        for key in [false, true] {
3345            let [still, moved] = cleanup_frame_that_moved(&s, key);
3346            assert_eq!(
3347                still,
3348                [(Some(18), key); 2],
3349                "a frame that stood still keeps it"
3350            );
3351            assert_eq!(
3352                moved,
3353                [(Some(18), key), (None, false)],
3354                "a frame that moved does not"
3355            );
3356        }
3357    }
3358
3359    /// The striped path decides each stripe's cleanup inside its encode (`encode_cpu`), so a
3360    /// Turbo frame that codes a stripe of a constant-quality session at the paint-over quality
3361    /// while it holds still (`software::stripes_held_still`) is hashed before it is encoded: a
3362    /// change landing on its refresh, on a frame after it, or on its key frame is coded at the
3363    /// session's quality, as without Turbo.
3364    #[test]
3365    fn a_turbo_stripe_at_the_paint_over_quality_codes_a_change_at_the_session_quality() {
3366        use crate::encoders::codec::{FRAME_KEY, parse_video_type};
3367        let session = |turbo: bool| {
3368            let mut p = X11Pipeline::new(RustCaptureSettings {
3369                width: 640,
3370                height: 384,
3371                codec: Codec::H264,
3372                use_cpu: true,
3373                video_fullframe: true,
3374                video_cbr_mode: false,
3375                video_crf: 25,
3376                video_paintover_crf: 18,
3377                paint_over_trigger_frames: 15,
3378                use_paint_over_quality: true,
3379                video_streaming_mode: turbo,
3380                target_fps: 60.0,
3381                ..Default::default()
3382            });
3383            assert!(p.hw.is_none(), "software H.264 runs striped");
3384            for i in 0..12u8 {
3385                p.process(&noise(i), 640 * 4);
3386            }
3387            p
3388        };
3389        let mut p = session(true);
3390        let (mut held, mut key) = (None, None);
3391        for i in 0..300 {
3392            if held.is_none() && stripes_held_still(&p.stripes, &p.settings) {
3393                held = Some(i);
3394            }
3395            let out = p.process(&noise(42), 640 * 4);
3396            let keyed = out
3397                .iter()
3398                .any(|e| parse_video_type(e.data[1]).is_some_and(|(_, k)| k == FRAME_KEY));
3399            if held.is_some() && keyed {
3400                key = Some(i);
3401                break;
3402            }
3403        }
3404        let held = held.expect("a still stripe is cleaned up");
3405        let key = key.expect("a large change is cleaned up by a key frame");
3406        for at in [held, held + 15, key] {
3407            let jump = |turbo: bool| -> usize {
3408                let mut p = session(turbo);
3409                for _ in 0..at {
3410                    p.process(&noise(42), 640 * 4);
3411                }
3412                p.process(&noise(9), 640 * 4)
3413                    .iter()
3414                    .map(|e| e.data.len())
3415                    .sum()
3416            };
3417            let (turbo, plain) = (jump(true), jump(false));
3418            assert!(
3419                turbo as f64 <= plain as f64 * 1.2,
3420                "a change {at} frames still is coded at the session's quality: {turbo} bytes \
3421                 against {plain}"
3422            );
3423        }
3424    }
3425
3426    /// The software path carries a 4:4:4 request exactly when the build's encoder does —
3427    /// libx264 at full range, so its log line says so; OpenH264 never, reporting the 4:2:0 it
3428    /// encodes — and without the request it reports 4:2:0. This is the same string the Wayland log
3429    /// builds from the same shared helper, so an identical session reads identically on both
3430    /// backends.
3431    #[test]
3432    fn x11_colorspace_desc_reports_what_the_software_encoder_carries() {
3433        let carries_444 = crate::encoders::software_fullcolor(Codec::H264);
3434        assert_eq!(
3435            carries_444,
3436            crate::encoders::software_library(Codec::H264) == "x264"
3437        );
3438        let i444 = if carries_444 {
3439            "I444 (Full Range)"
3440        } else {
3441            "I420 (Limited Range)"
3442        };
3443        for (fullcolor, expected) in [(true, i444), (false, "I420 (Limited Range)")] {
3444            let settings = RustCaptureSettings {
3445                width: 64,
3446                height: 64,
3447                codec: Codec::H264,
3448                use_cpu: true,
3449                video_fullcolor: fullcolor,
3450                ..Default::default()
3451            };
3452            let p = X11Pipeline::new(settings.clone());
3453            assert_eq!(
3454                p.encoder_name(),
3455                format!("CPU ({})", crate::encoders::software_library(Codec::H264))
3456            );
3457            assert_eq!(p.colorspace_desc(), expected);
3458            // The chroma and the range are each read from the session rather than assumed
3459            // equal: a 4:2:0 session converted at full range exists, so the two are no longer
3460            // the same answer.
3461            assert_eq!(
3462                p.colorspace_desc(),
3463                crate::encoders::colorspace_desc(
3464                    crate::encoders::session_fullcolor(None, &settings),
3465                    crate::encoders::session_full_range(None, &settings),
3466                ),
3467                "X11 and Wayland must describe the same session identically"
3468            );
3469        }
3470    }
3471
3472    /// A pipeline built with a report bound describes itself in it, and a frame it encodes is
3473    /// tallied with its bytes and its encode time.
3474    #[test]
3475    fn x11_pipeline_reports_the_session_it_settled_on() {
3476        let report = crate::report::StreamReport::new("x11");
3477        let _scope = crate::report::enter(&report);
3478        let mut p = X11Pipeline::new(RustCaptureSettings {
3479            width: 64,
3480            height: 64,
3481            codec: Codec::H264,
3482            use_cpu: true,
3483            video_fullframe: true,
3484            ..Default::default()
3485        });
3486        let info = report.info();
3487        assert_eq!(info.encoder, crate::encoders::software_library(Codec::H264));
3488        assert!(!info.hardware);
3489        assert_eq!(info.encoder_reason, "software encoding selected");
3490        assert_eq!(info.codec, "h264");
3491        assert_eq!(info.stripes, 1);
3492        assert!(info.gpu.is_empty());
3493
3494        let pixels = vec![0x80u8; 64 * 64 * 4];
3495        let mut stripes = p.process(&pixels, 64 * 4);
3496        assert!(!stripes.is_empty(), "the first frame of a pipeline is sent");
3497        let start = crate::wayland::host::now_ns();
3498        crate::encoders::software::FrameTiming::stamp(
3499            &mut stripes,
3500            start - 2_000_000,
3501            start - 1_000_000,
3502        );
3503        report.tally(&stripes);
3504        let totals = report.totals();
3505        assert_eq!(totals.frames, 1);
3506        assert_eq!(
3507            totals.bytes,
3508            stripes.iter().map(|s| s.data.len() as u64).sum::<u64>()
3509        );
3510        assert!(totals.encode_ns >= 1_000_000 && totals.pipeline_ns >= 2_000_000);
3511    }
3512
3513    /// A JPEG stream's log line names the library that encodes it, as its report does.
3514    #[test]
3515    fn x11_jpeg_pipeline_names_its_encoder() {
3516        let report = crate::report::StreamReport::new("x11");
3517        let _scope = crate::report::enter(&report);
3518        let p = X11Pipeline::new(RustCaptureSettings {
3519            width: 64,
3520            height: 64,
3521            codec: Codec::Jpeg,
3522            use_cpu: true,
3523            ..Default::default()
3524        });
3525        assert_eq!(p.encoder_name(), format!("CPU ({})", report.info().encoder));
3526    }
3527
3528    /// On a host with a hardware encoder the report names it, the GPU it runs on, and the node.
3529    #[test]
3530    #[ignore]
3531    fn gpu_x11_pipeline_reports_the_hardware_session() {
3532        let report = crate::report::StreamReport::new("x11");
3533        let _scope = crate::report::enter(&report);
3534        let p = X11Pipeline::new(RustCaptureSettings {
3535            width: 1280,
3536            height: 720,
3537            codec: Codec::H264,
3538            target_fps: 60.0,
3539            video_crf: 25,
3540            ..Default::default()
3541        });
3542        assert!(p.is_hardware(), "needs a hardware encoder on render node 0");
3543        let info = report.info();
3544        println!("{info:?}");
3545        assert!(info.hardware);
3546        assert_eq!(info.encoder, p.encoder_name());
3547        assert!(info.encoder_reason.is_empty());
3548        assert_eq!(info.encode_node, 0);
3549        assert!(!info.driver.is_empty());
3550    }
3551
3552    /// With no motion, no request, and paint-over off, nothing is emitted at any frame-counter
3553    /// position: the GOP is infinite, so there is no scheduled IDR to break the silence.
3554    #[test]
3555    fn static_frames_stay_silent_without_request() {
3556        let s = RustCaptureSettings {
3557            use_paint_over_quality: false,
3558            ..hw_settings()
3559        };
3560        let mut st = StripeState::default();
3561        for fc in [0u16, 1, 120, 240] {
3562            let d = decide_hw_fullframe(&mut st, &s, fc, Damage::None, false, false, HOLDS);
3563            assert!(!d.send && !d.force_idr, "frame {fc} should stay idle");
3564        }
3565    }
3566
3567    /// With paint-over off, a forced keyframe on a still screen still opens the recovery burst,
3568    /// so a constant-rate session can refine it.
3569    #[test]
3570    fn requested_idr_recovers_even_without_paint_over() {
3571        let s = RustCaptureSettings {
3572            use_paint_over_quality: false,
3573            video_paintover_burst_frames: 5,
3574            ..hw_settings()
3575        };
3576        let mut st = StripeState::default();
3577        let d = decide_hw_fullframe(&mut st, &s, 5, Damage::None, false, true, HOLDS);
3578        assert!(d.send && d.force_idr && d.hold_qp.is_none());
3579        assert_eq!(
3580            st.h264_burst_frames_remaining, 5,
3581            "recovery burst armed without paint-over"
3582        );
3583        let d = decide_hw_fullframe(&mut st, &s, 6, Damage::None, false, false, HOLDS);
3584        assert!(d.send && !d.force_idr);
3585        assert_eq!(d.target_qp, 25);
3586    }
3587
3588    #[test]
3589    fn configured_interval_restores_scheduled_keyframes() {
3590        let mut s = hw_settings();
3591        s.keyframe_interval_s = 2.0;
3592        assert!(periodic_idr_due(&s, 0));
3593        assert!(!periodic_idr_due(&s, 1));
3594        assert!(periodic_idr_due(&s, 120));
3595        let mut st = StripeState::default();
3596        let d = decide_hw_fullframe(&mut st, &s, 120, Damage::None, false, false, HOLDS);
3597        assert!(
3598            d.send && d.force_idr,
3599            "interval keyframe fires on a static screen"
3600        );
3601        s.keyframe_interval_s = 0.0;
3602        assert!(!periodic_idr_due(&s, 0) && !periodic_idr_due(&s, 120));
3603    }
3604
3605    /// A held refresh of a constant-rate session is coarsened from the rate control's last
3606    /// quantizer to what a second of the target buys; a constant quality, or a rate control
3607    /// that coded near the paint-over quantizer, gets the paint-over one.
3608    #[test]
3609    fn a_held_refresh_fits_the_budget_a_constant_rate_leaves() {
3610        let cbr = RustCaptureSettings {
3611            video_cbr_mode: true,
3612            video_paintover_crf: 18,
3613            target_fps: 30.0,
3614            ..hw_settings()
3615        };
3616        let q = |s: &RustCaptureSettings, last| {
3617            held_refresh_quality(
3618                s,
3619                EncoderQuality {
3620                    last,
3621                    bytes: None,
3622                    holds: true,
3623                    reopens: false,
3624                    keys: true,
3625                    band: None,
3626                    measures: false,
3627                    psnr: None,
3628                },
3629            )
3630        };
3631        assert_eq!(
3632            q(&cbr, Some(51)),
3633            22,
3634            "thirty frames of budget are 29 steps finer than the rate control"
3635        );
3636        assert_eq!(q(&cbr, Some(45)), 18);
3637        assert_eq!(q(&cbr, Some(30)), 18);
3638        assert_eq!(q(&cbr, None), 18);
3639        let crf = RustCaptureSettings {
3640            video_cbr_mode: false,
3641            ..cbr.clone()
3642        };
3643        assert_eq!(
3644            q(&crf, Some(45)),
3645            18,
3646            "a constant quality has no budget to fit"
3647        );
3648    }
3649
3650    /// Damage rectangles report the share of the frame they cover, clipped to it.
3651    #[test]
3652    fn damage_rectangles_report_their_share_of_the_frame() {
3653        use smithay::utils::Rectangle;
3654        assert_eq!(Damage::of_rects(&[], 100, 100), Damage::None);
3655        let quarter = [Rectangle::new((0, 0).into(), (50, 50).into())];
3656        assert_eq!(Damage::of_rects(&quarter, 100, 100), Damage::Area(0.25));
3657        let past = [Rectangle::new((80, 80).into(), (50, 50).into())];
3658        assert_eq!(
3659            Damage::of_rects(&past, 100, 100),
3660            Damage::Area(0.04),
3661            "clipped to the frame"
3662        );
3663        let whole = [Rectangle::new((0, 0).into(), (200, 200).into())];
3664        assert_eq!(Damage::of_rects(&whole, 100, 100), Damage::Area(1.0));
3665    }
3666
3667    /// The banded content hash reports the share of bands that changed, and nothing when the
3668    /// frame is the same.
3669    #[test]
3670    fn hash_damage_reports_the_bands_that_changed() {
3671        let (w, h) = (32usize, 256usize);
3672        let mut bands = Vec::new();
3673        let frame = vec![7u8; w * 4 * h];
3674        assert_eq!(
3675            hash_damage(&mut bands, &frame, w * 4, h, 10, 20),
3676            Damage::Area(1.0),
3677            "the first frame is all new"
3678        );
3679        assert_eq!(
3680            hash_damage(&mut bands, &frame, w * 4, h, 10, 20),
3681            Damage::None
3682        );
3683        let mut caret = frame.clone();
3684        caret[w * 4 * (DAMAGE_BAND_ROWS + 1)] = 0;
3685        let n = h.div_ceil(DAMAGE_BAND_ROWS);
3686        assert_eq!(
3687            hash_damage(&mut bands, &caret, w * 4, h, 10, 20),
3688            Damage::Area(1.0 / n as f32),
3689            "one band of {n}"
3690        );
3691    }
3692
3693    /// The rows a band hash found changed map onto full-width rectangles, one per run of bands,
3694    /// the last clipped to the frame; before any hash the whole frame is dirty.
3695    #[test]
3696    fn band_rects_cover_the_runs_of_changed_bands() {
3697        let r = |y: i32, h: i32| Rectangle::new((0, y).into(), (64, h).into());
3698        assert_eq!(band_rects(&[], 64, 100), vec![r(0, 100)]);
3699        assert_eq!(band_rects(&[false, false, false, false], 64, 100), vec![]);
3700        assert_eq!(
3701            band_rects(&[true, true, false, true], 64, 100),
3702            vec![r(0, 64), r(96, 4)]
3703        );
3704        assert_eq!(band_damage(&[true, true, false, true]), Damage::Area(0.75));
3705        assert_eq!(band_damage(&[]), Damage::Area(1.0));
3706    }
3707
3708    /// Band hashes taken elsewhere read as the share of the bands that changed: a caret's band of
3709    /// a 1080p screen is a small change and three are motion; a first frame, or one of another
3710    /// height, is all new.
3711    #[test]
3712    fn hashed_damage_reports_the_bands_that_changed() {
3713        let n = 1080usize.div_ceil(DAMAGE_BAND_ROWS);
3714        let frame: Vec<u64> = (0..n as u64).collect();
3715        let mut last = Vec::new();
3716        assert_eq!(hashed_damage(&mut last, frame.clone()), Damage::Area(1.0));
3717        assert_eq!(hashed_damage(&mut last, frame.clone()), Damage::None);
3718        let mut caret = frame.clone();
3719        caret[6] ^= 1;
3720        let d = hashed_damage(&mut last, caret.clone());
3721        assert_eq!(d, Damage::Area(1.0 / n as f32));
3722        assert!(!d.is_motion());
3723        let mut window = caret.clone();
3724        for band in &mut window[10..13] {
3725            *band ^= 1;
3726        }
3727        assert!(hashed_damage(&mut last, window).is_motion());
3728        assert_eq!(
3729            hashed_damage(&mut last, frame[..n - 1].to_vec()),
3730            Damage::Area(1.0)
3731        );
3732    }
3733}
3734
3735#[cfg(test)]
3736mod vbv_tests {
3737    /// VBV sizing policy: an infinite GOP uses 1.5 frames of headroom, scheduled keyframes
3738    /// relax to 3 frames, and an explicit multiplier overrides both and rescales with bitrate.
3739    #[test]
3740    fn vbv_policy() {
3741        use crate::encoders::vbv_bits;
3742        let frame = 4_000_000f64 / 60.0;
3743        assert_eq!(
3744            vbv_bits(4_000_000, 60.0, 0.0, 0.0),
3745            (frame * 1.5).round() as u32
3746        );
3747        assert_eq!(
3748            vbv_bits(4_000_000, 60.0, 2.0, 0.0),
3749            (frame * 3.0).round() as u32
3750        );
3751        assert_eq!(vbv_bits(4_000_000, 60.0, 2.0, 1.0), frame.round() as u32);
3752        assert_eq!(
3753            vbv_bits(8_000_000, 60.0, 0.0, 1.0),
3754            (2.0 * frame).round() as u32
3755        );
3756    }
3757}