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::CaptureSettings;
14use crate::encoders::software::{
15    EncodedStripe, Nv12Planes, StripeState, acknowledge_reference, encode_cpu_from, held_input,
16    invalidate_reference, release_input, 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/// Seconds a constant-rate cleanup through the rate control flows at most.
68const CONVERGE_S: f64 = 10.0;
69
70/// Frame budgets the rate control's frames of a still screen exceed for a trigger period when it
71/// is pinned at its coarsest quantizer, refining nothing within the rate: NVENC's H.264 codes a
72/// still screen of texture at 0.1 Mbit/s at 1080p in 3.3 kB frames, 16 budgets, at quantizer 51,
73/// where at 2 Mbit/s single frames after a change reach 4.5 budgets.
74const OVERSHOOT_BUDGETS: f64 = 2.0;
75
76/// Bytes a slice that refines nothing comes to at most: a frame of x264's four slices skipping
77/// every block takes 61 to 66 at 720p and 1080p with the wire header, a third of the budget at
78/// 0.1 Mbit/s, so never a small frame.
79pub(crate) const EMPTY_SLICE_BYTES: usize = 32;
80
81/// Seconds of such frames after which a constant-rate cleanup through x264 ends: x264 keeps its
82/// quantizer on a screen that does not change, and at 0.1 Mbit/s refines one in frames as far
83/// as 2.4 s apart. A session that holds no quantizer and measures nothing ends the same way
84/// once its frames at its coarsest have kept the size of the one before as long
85/// (`SAME_FRAME_SHARE`).
86pub(crate) const EMPTY_S: f64 = 3.0;
87
88/// The share of the frame before it within which a frame of a still screen codes nothing new,
89/// and the quality index from which its rate control sits at its coarsest: at 0.1 Mbit/s at
90/// 1080p libvpx's VP9 frames are 266 bytes each at index 51, or 220-221 at 46-47, and x265's
91/// 224 at 50-51, its strict constant rate padding them with filler, while the picture stays
92/// level, so never small against the 208-byte budget. From 1 Mbit/s their rate controls code a
93/// still screen at 36 and finer, x265's padded frames again of one size while it refines.
94const SAME_FRAME_SHARE: f64 = 0.125;
95const SAME_FRAME_QUALITY: u32 = 45;
96
97/// The same for a session that holds no quantizer but says the one it codes at or measures its
98/// pictures, which has no refresh to fall back to and ends its cleanup at the paint-over
99/// quantizer or once the picture stops improving: at 2 Mbit/s at 1080p x265's rate control
100/// refines a screen of text by 0.7 dB a second, still short of that quality after ten. A band
101/// sweep runs this long at most too.
102const REFINE_S: f64 = 30.0;
103
104/// Seconds the finest quantizer such a session has coded the still screen at stands before its
105/// rate control counts as done with the picture, where the session says its quantizer and does
106/// not measure: at 0.25 Mbit/s at 1080p x265 takes a step finer every 2 to 8 s for a tenth of
107/// a dB a second, and libvpx's VP9 one every 6 s after its first ten, each for all of `REFINE_S`
108/// at the full rate, where at 2 Mbit/s x265 takes one every 2 s for its first twenty.
109const LEVEL_S: f64 = 4.0;
110
111/// Where the encoder holds a band of the picture (`EncoderQuality::band`), the refresh that
112/// falls back from a constant-rate cleanup sweeps the picture a band a frame in raster order,
113/// the first `FIRST_BAND` of it and each next sized from the bytes of the last to
114/// `BAND_BUDGETS` frame budgets, so the stream keeps its rate: a whole refresh at the
115/// paint-over quantizer is 86 budgets of text at 2 Mbit/s at 1080p and queues for 240 ms on a
116/// 12 Mbit/s link.
117const FIRST_BAND: f64 = 1.0 / 64.0;
118
119/// The least share of the picture a band covers. A band that small costs next to nothing, so a
120/// held frame of one that comes out past its budget is the rest of the frame's cost.
121const LEAST_BAND: f64 = 1.0 / 4096.0;
122
123/// The share of a sweep's frames left that the rest of the picture may take at a quantizer step
124/// finer before the sweep steps finer: room for the bands' cost to vary down the picture.
125const SWEEP_FINER_SHARE: f64 = 0.8;
126const BAND_BUDGETS: f64 = 1.0;
127
128impl Damage {
129    /// The damage a set of rectangles on a `width` x `height` frame reports: their summed area,
130    /// clipped to the frame, overlaps counted twice.
131    pub fn of_rects<'a>(
132        rects: impl IntoIterator<Item = &'a smithay::utils::Rectangle<i32, smithay::utils::Physical>>,
133        width: i32,
134        height: i32,
135    ) -> Self {
136        let frame = f64::from(width.max(1)) * f64::from(height.max(1));
137        let mut area = 0f64;
138        for r in rects {
139            let w = (r.loc.x + r.size.w).min(width) - r.loc.x.max(0);
140            let h = (r.loc.y + r.size.h).min(height) - r.loc.y.max(0);
141            if w > 0 && h > 0 {
142                area += f64::from(w) * f64::from(h);
143            }
144        }
145        if area > 0.0 {
146            Damage::Area((area / frame).min(1.0) as f32)
147        } else {
148            Damage::None
149        }
150    }
151
152    /// Whether anything changed.
153    pub fn is_dirty(self) -> bool {
154        self != Damage::None
155    }
156
157    /// Whether the change is more than a small one, so the frame counts as motion.
158    pub(crate) fn is_motion(self) -> bool {
159        match self {
160            Damage::None => false,
161            Damage::Area(f) => f > SMALL_AREA,
162            Damage::Unknown => true,
163        }
164    }
165}
166
167/// What the cleanup policy decided for one region's frame.
168#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum Cleanup {
170    None,
171    /// Refresh predicted from the picture the client holds, at the paint-over quantizer.
172    Refresh,
173    /// Key frame at the paint-over quantizer.
174    Key,
175}
176
177/// The still-screen cleanup of one region (a whole frame, or a stripe): track how long and how
178/// much it has been changing, and say when it is due a cleanup and of which kind. `keys` false
179/// (JPEG, whose every stripe is a picture of its own, and a constant-rate session cleaned up
180/// through its rate control) never answers `Cleanup::Key`.
181///
182/// Static is read from the content (`damage`), never from what was sent, so a stream that
183/// encodes every frame (Turbo) sees the screen go still exactly like one that sends only changes.
184/// The cleanup comes in two steps. A region that changed is refreshed once it has held still for
185/// `trigger` frames: a frame predicted from the picture the client holds, which re-codes only
186/// what changed. Once the change since the last key-frame cleanup adds up to
187/// `KEY_CLEANUP_MASS` of a screen and the region has held still for `KEY_STILL_TRIGGERS` trigger
188/// periods, it is cleaned up by a key frame, which leaves nothing of the motion behind and
189/// resynchronizes every viewer, but costs a whole picture: so it waits for a screen still long
190/// enough that the user is reading it, and a caret or a clock, which change little, never pays
191/// for one. Nor does a region whose block ledger refined all but `KEY_CLEANUP_MASS` of it since
192/// the change (`BlockLedger::unrefined_share`): a video stopping beside text the ledger held
193/// clean was keyed at 99 frame budgets at 1080p and 8 Mbit/s, into a picture 2.4 dB worse than
194/// the refined one it replaced. A region that keeps changing never holds still, so once `LOW_MOTION_TRIGGERS`
195/// trigger periods have passed since it first changed and at most a quarter of its recent frames
196/// carried more than a small change (a blinking caret, a ticking clock) it is cleaned up then,
197/// by a key frame when the change adds up to one.
198///
199/// `improves` false (the paint-over is off, or a constant rate already codes the region finer)
200/// keeps the bookkeeping and answers `Cleanup::None`, and a cleanup that falls due then restarts
201/// the count of frames since the region first changed: the region is clean, so those frames are
202/// no low motion, and counted, they would have the first frame of the next large change keyed,
203/// the whole picture at the paint-over quantizer the moment a window opens. `now` false (a
204/// stripe whose cleanup is staggered to a later frame) answers `Cleanup::None` and leaves the
205/// cleanup pending. `start` is the still frames a refresh after motion waits for: `trigger`, or
206/// one where the refresh runs through the rate control (`refresh_start`).
207pub fn cleanup_due(
208    st: &mut StripeState,
209    trigger: u32,
210    start: u32,
211    improves: bool,
212    now: bool,
213    keys: bool,
214    damage: Damage,
215) -> Cleanup {
216    if damage.is_dirty() {
217        st.dirty_run = st.dirty_run.saturating_add(1);
218        st.no_motion_frame_count = 0;
219        st.paint_over_sent = false;
220        st.moved |= damage.is_motion();
221        st.change_mass += match damage {
222            Damage::Area(f) => f,
223            _ if st.dirty_run >= UNKNOWN_RUN => UNKNOWN_MASS,
224            _ => 0.0,
225        };
226    } else {
227        st.dirty_run = 0;
228        st.no_motion_frame_count = st.no_motion_frame_count.saturating_add(1);
229    }
230    if !st.paint_over_sent {
231        st.unclean_frames = st.unclean_frames.saturating_add(1);
232    }
233    st.motion = recent_motion(st.motion, trigger.max(1), damage.is_motion());
234    let due = due_cleanup(
235        st,
236        trigger.max(1),
237        start,
238        keys,
239        st.no_motion_frame_count,
240        st.unclean_frames,
241        st.motion,
242    );
243    if due == Cleanup::None {
244        return Cleanup::None;
245    }
246    if !improves {
247        st.unclean_frames = 0;
248        return Cleanup::None;
249    }
250    if !now {
251        return Cleanup::None;
252    }
253    st.paint_over_sent = true;
254    st.moved = false;
255    st.unclean_frames = 0;
256    st.cleanup_keyed = due == Cleanup::Key;
257    if due == Cleanup::Key {
258        st.change_mass = 0.0;
259    }
260    due
261}
262
263impl StripeState {
264    /// A consumer lost a frame of this region's stream (`invalidate_reference`). A cleanup already
265    /// sent may be among the frames it lost, and the frame predicting past them is coded at the
266    /// stream's own quality, from whatever frame is left to predict from: over a gate that held a
267    /// low frame rate's frames back on a long path, the anchor, seconds old, whose picture shows
268    /// through the recovery as a ghost. A region that was clean is due its cleanup again, of the
269    /// kind it lost (a key frame clears what the anchor left), or the picture would stay as the
270    /// loss left it until the region next changes.
271    pub fn lost_frame(&mut self) {
272        if self.paint_over_sent {
273            self.paint_over_sent = false;
274            self.settled = false;
275            self.sweep = None;
276            self.h264_burst_frames_remaining = 0;
277            if self.cleanup_keyed {
278                self.change_mass = self.change_mass.max(KEY_CLEANUP_MASS);
279            }
280        }
281    }
282
283    /// A frame of this region's stream was withheld (`ReferenceReport::Withheld`). A cleanup
284    /// running through the rate control codes past it, its next frames predicting from what the
285    /// consumers hold; one that ended or sweeps is due again as after a loss. Started over at each
286    /// drop of a rate budget, NVENC's cleanup of a texture at 0.1 Mbit/s never reached the one
287    /// held frame that ends it.
288    pub fn withheld_frame(&mut self) {
289        if self.h264_burst_frames_remaining <= 0 || self.sweep.is_some() {
290            self.lost_frame();
291        }
292    }
293
294    /// Whether this frame codes the region's scheduled key frame, `due` being the schedule's
295    /// (`periodic_idr_due`). One that falls due while the region holds still, or changes only a
296    /// little, waits for a frame that `moved`: a key frame is coded at the rate control's own
297    /// quality, so on a still screen it would throw away the picture the cleanup refined every
298    /// interval, where a decoder holding that picture gains nothing from it, and motion coarsens
299    /// the picture anyway. A key frame coded for any other reason pays the debt
300    /// (`decide_hw_fullframe`).
301    pub fn scheduled_key(&mut self, due: bool, moved: bool) -> bool {
302        self.key_owed |= due;
303        moved && std::mem::take(&mut self.key_owed)
304    }
305
306    /// What a report of the consumers does to this region's cleanup (`lost_frame`,
307    /// `withheld_frame`).
308    pub fn take_report(&mut self, report: crate::encoders::reference::ReferenceReport) {
309        use crate::encoders::reference::ReferenceReport;
310        match report {
311            ReferenceReport::Lost(_) => self.lost_frame(),
312            ReferenceReport::Withheld(_) => self.withheld_frame(),
313            ReferenceReport::Held(_) | ReferenceReport::Sent(_) => {}
314        }
315    }
316}
317
318/// The share of a region's recent frames that carried more than a small change, `motion` taking
319/// in one more frame: an average over `LOW_MOTION_TRIGGERS` trigger periods.
320fn recent_motion(motion: f32, trigger: u32, moved: bool) -> f32 {
321    motion + (f32::from(u8::from(moved)) - motion) / (trigger * LOW_MOTION_TRIGGERS) as f32
322}
323
324/// The cleanup a region is due with `still` frames held still, `unclean` frames since it first
325/// changed after its last cleanup, and `motion` the share of its recent frames in motion.
326fn due_cleanup(
327    st: &StripeState,
328    trigger: u32,
329    start: u32,
330    keys: bool,
331    still: u32,
332    unclean: u32,
333    motion: f32,
334) -> Cleanup {
335    let low_motion = !st.paint_over_sent
336        && unclean >= trigger * LOW_MOTION_TRIGGERS
337        && motion <= LOW_MOTION_SHARE;
338    if keys
339        && st.change_mass >= KEY_CLEANUP_MASS
340        && st.ledger.unrefined_share() >= KEY_CLEANUP_MASS
341        && (still >= trigger * KEY_STILL_TRIGGERS || low_motion)
342    {
343        Cleanup::Key
344    } else if !st.paint_over_sent && (still >= if st.moved { start } else { trigger } || low_motion)
345    {
346        Cleanup::Refresh
347    } else {
348        Cleanup::None
349    }
350}
351
352/// Whether `cleanup_due` would answer a cleanup for a frame that changed nothing, without moving
353/// the bookkeeping: what an idle frame has to know before it skips a region.
354pub fn cleanup_pending(
355    st: &StripeState,
356    trigger: u32,
357    start: u32,
358    enabled: bool,
359    keys: bool,
360) -> bool {
361    let unclean = st
362        .unclean_frames
363        .saturating_add(u32::from(!st.paint_over_sent));
364    let motion = recent_motion(st.motion, trigger.max(1), false);
365    enabled
366        && due_cleanup(
367            st,
368            trigger.max(1),
369            start,
370            keys,
371            st.no_motion_frame_count.saturating_add(1),
372            unclean,
373            motion,
374        ) != Cleanup::None
375}
376
377/// The still frames a refresh after the region moved waits for (`cleanup_due`'s `start`): a refresh
378/// through the rate control (`converges`) is that rate control's own frames, each within its
379/// budget, so it starts on the first still frame, where waiting `trigger` frames only left the
380/// picture as the motion did; a refresh held at the paint-over quantizer waits for the screen to
381/// stay still. A region that changed only a little (a caret, a keystroke) waits `trigger` either
382/// way, or a caret blinking on a clean screen would send every frame.
383pub fn refresh_start(trigger: u32, converges: bool) -> u32 {
384    if converges { 1 } else { trigger.max(1) }
385}
386
387/// What an encoder says about the frames it codes, which the cleanup reads: the quality index
388/// its rate control last coded a frame at, where it reports one (`FrameEncoder::last_quality`),
389/// the bytes of that frame, where it reports them too (`FrameEncoder::last_size`), whether it
390/// holds a frame at a quantizer it is asked for (`FrameEncoder::holds_quantizer`), whether a
391/// change of its constant quality re-opens it on a key frame (`FrameEncoder::reopens_on_quality`),
392/// whether its constant-rate cleanup ends on a key frame (`FrameEncoder::cleans_up_with_key`),
393/// where it holds a band of a frame at that quantizer (`FrameEncoder::band_size`), the
394/// bytes of its last held frame, where it measures a frame's reconstruction against its
395/// source when asked (`FrameEncoder::measures`), the last measurement, and where it took its last
396/// frame back for coming out past its cap and coded it again (`FrameEncoder::refit`), how far
397/// that frame's held band then reached, or 0 for a frame of its rate control's.
398#[derive(Clone, Copy, Debug, PartialEq)]
399pub struct EncoderQuality {
400    pub last: Option<u32>,
401    pub bytes: Option<usize>,
402    pub holds: bool,
403    pub reopens: bool,
404    pub keys: bool,
405    pub band: Option<usize>,
406    pub measures: bool,
407    pub psnr: Option<f32>,
408    pub refit: Option<f64>,
409    /// Whether it codes a still screen's cleanup frame at the rate its cap runs at
410    /// (`FrameEncoder::follows_cap`), so the frame is judged against the cleanup's budget.
411    pub follows_cap: bool,
412    /// The quantizer steps finer than the paint-over one at which its rate control's frames
413    /// come to the paint-over quality (`FrameEncoder::reach_finer`).
414    pub finer: u32,
415    /// Whether its cleanup through the rate control ends on a key frame held at the paint-over
416    /// quantizer (`FrameEncoder::ends_cleanup_on_key`).
417    pub ends_on_key: bool,
418}
419
420impl EncoderQuality {
421    /// What `encoder` says.
422    pub fn of(encoder: &FrameEncoder) -> Self {
423        Self {
424            last: encoder.last_quality(),
425            bytes: encoder.last_size(),
426            holds: encoder.holds_quantizer(),
427            reopens: encoder.reopens_on_quality(),
428            keys: encoder.cleans_up_with_key(),
429            band: encoder.band_size(),
430            measures: encoder.measures(),
431            psnr: encoder.last_psnr(),
432            refit: encoder.refit(),
433            follows_cap: encoder.follows_cap(),
434            finer: encoder.reach_finer(),
435            ends_on_key: encoder.ends_cleanup_on_key(),
436        }
437    }
438
439    /// Whether a constant-rate cleanup runs through this encoder's rate control: it says the
440    /// bytes of its frames, so the cleanup can tell when the rate control has stopped refining
441    /// a still screen, and with their quantizer whether that is at the paint-over quality.
442    pub fn converges(&self) -> bool {
443        self.bytes.is_some()
444    }
445}
446
447/// Where a rate control that the cleanup runs through stands after its last frame, of `bytes`
448/// at quality index `last`, against the paint-over quantizer and `budget` bytes a frame.
449#[derive(Clone, Copy, Debug, PartialEq)]
450pub enum Convergence {
451    /// Still refining: its last frame carried a share of the budget.
452    Refining,
453    /// A small frame at the paint-over quantizer or finer: nothing left to refine.
454    Converged,
455    /// A small frame at a coarser quantizer, or at one the encoder does not say: refining
456    /// nothing more this frame.
457    Idle,
458}
459
460/// Where the rate control stands (`Convergence`).
461pub fn convergence(
462    last: Option<u32>,
463    bytes: Option<usize>,
464    paint: u32,
465    budget: f64,
466) -> Convergence {
467    match bytes {
468        Some(b) if (b as f64) <= budget * CONVERGED_SHARE => {
469            if last.is_some_and(|q| q <= paint) {
470                Convergence::Converged
471            } else {
472                Convergence::Idle
473            }
474        }
475        _ => Convergence::Refining,
476    }
477}
478
479/// Trigger periods between two measurements of a picture a constant-rate cleanup is refining
480/// (`FrameEncoder::last_psnr`), the gain in dB under which one finds the picture level with
481/// the one before, and the level measurements in a row after which the rate control is done
482/// with it. A rate control that has a whole screen of dense text to refine at 2 Mbit/s at
483/// 1080p gains 0.2 dB a second at first, coding a few blocks of it a frame, and one that is
484/// done with a screen under 0.06.
485const PLATEAU_TRIGGERS: u32 = 4;
486const PLATEAU_GAIN_DB: f32 = 0.05;
487const PLATEAU_CHECKS: u32 = 2;
488
489/// Seconds of a link at the cleanup's rate a cleanup's frame may take to cross it (`cleanup_cap`):
490/// the session's buffer at 8 fps, under the backpressure gate's allowance.
491pub(crate) const CLEANUP_FRAME_S: f64 = 0.2;
492
493/// The most a cleanup runs at, in multiples of the session's rate (`cleanup_rate`): a sweep at
494/// 8 fps and 500 kbps over a fast path codes bands of two to six budgets.
495const CLEANUP_RATE_MAX: i32 = 4;
496
497/// Bytes a frame of the constant-rate target, at `kbps` and `fps`.
498pub fn frame_budget(kbps: i32, fps: f64) -> f64 {
499    f64::from(kbps.max(1)) * 125.0 / fps.max(1.0)
500}
501
502/// The bytes a constant-rate cleanup's frame may come to: the session's own buffer, what its rate
503/// control holds a frame to, or `CLEANUP_FRAME_S` of the cleanup's rate (`cleanup_rate`) where that
504/// is more. A still screen's frames past what the path carries queue on it, where congestion
505/// control reads them as the path falling behind; at 60 fps and 500 kbps NVENC's rate control
506/// refines a 1080p page a budget a frame, and now and then mispredicts one at fifty.
507fn cleanup_cap(settings: &CaptureSettings) -> usize {
508    let vbv = (crate::encoders::vbv_bits(
509        (settings.video_bitrate_kbps.max(1) as u32).saturating_mul(1000),
510        settings.target_fps,
511        settings.keyframe_interval_s,
512        settings.video_vbv_multiplier,
513    ) / 8) as usize;
514    vbv.max((f64::from(cleanup_rate(settings)) * 125.0 * CLEANUP_FRAME_S) as usize)
515}
516
517/// The rate (kbps) a constant-rate cleanup's sweep is paced to and its frames capped by: what the
518/// path carries (`CaptureSettings::video_link_kbps`), never under the session's rate nor over
519/// `CLEANUP_RATE_MAX` times it, and the session's rate where the path's is not known. A still
520/// screen's refresh at the paint-over quantizer is many seconds of a low rate (1080p text at
521/// 500 kbps, 20 s), which a path faster than the stream carries at once and one near its rate
522/// only at that rate.
523pub fn cleanup_rate(settings: &CaptureSettings) -> i32 {
524    let rate = settings.video_bitrate_kbps.max(1);
525    settings
526        .video_link_kbps
527        .clamp(rate, rate.saturating_mul(CLEANUP_RATE_MAX))
528}
529
530/// The share of the picture the next band of a sweep covers, after one of `size` whose held frame
531/// came out at `bytes` (`FIRST_BAND` onward): `BAND_BUDGETS` of `budget`, at most doubling or
532/// halving a frame.
533fn band_share(size: f64, bytes: Option<usize>, budget: f64) -> f64 {
534    let next = match bytes {
535        Some(b) if b > 0 => (size * BAND_BUDGETS * budget / b as f64).clamp(size / 2.0, size * 2.0),
536        _ => size,
537    };
538    next.clamp(LEAST_BAND, 1.0)
539}
540
541/// One frame of a cleanup that measures the picture it is refining, where the encoder can
542/// (`FrameEncoder::measure`): whether this frame is to be measured, one every `period`, and
543/// whether the measurement of the last such frame, `psnr`, was the `PLATEAU_CHECKS`th in a
544/// row to gain less than `PLATEAU_GAIN_DB` on the one before, the rate control having
545/// nothing left to give the picture.
546fn plateau(st: &mut StripeState, psnr: Option<f32>, period: u32) -> (bool, bool) {
547    if std::mem::take(&mut st.measuring)
548        && let Some(now) = psnr
549    {
550        let level = st
551            .measured
552            .is_some_and(|before| now - before < PLATEAU_GAIN_DB);
553        st.level_checks = if level { st.level_checks + 1 } else { 0 };
554        st.measured = Some(now);
555    }
556    if st.measure_in == 0 {
557        st.measure_in = period;
558        st.measuring = true;
559    } else {
560        st.measure_in -= 1;
561    }
562    (st.measuring, st.level_checks >= PLATEAU_CHECKS)
563}
564
565/// Frames a constant-rate cleanup through the rate control flows at most (`CONVERGE_S`).
566pub fn converge_frames(settings: &CaptureSettings) -> i32 {
567    (CONVERGE_S * settings.target_fps.max(1.0)).round() as i32
568}
569
570/// Frames a constant-rate cleanup through a rate control that says its quantizer flows at most,
571/// with no refresh to fall back to (`REFINE_S`).
572pub fn refine_frames(settings: &CaptureSettings) -> i32 {
573    (REFINE_S * settings.target_fps.max(1.0)).round() as i32
574}
575
576/// `converge_frames` for the session `encoder` describes (`REFINE_S`).
577fn cleanup_frames(settings: &CaptureSettings, encoder: EncoderQuality) -> i32 {
578    if encoder.holds || (encoder.last.is_none() && !encoder.measures) {
579        converge_frames(settings)
580    } else {
581        refine_frames(settings)
582    }
583}
584
585/// Whether a cleanup improves the picture at all: at a constant quality where the paint-over
586/// quality is finer than the session's (the cleanup then moves the session's quality, see
587/// `decide_constant_quality`); at a constant rate where it is finer than the quality index the
588/// rate control last coded at, less the steps its frames need to come to the paint-over quality
589/// (`EncoderQuality::finer`), or the encoder does not say. A rate control that has already
590/// refined a still screen past it (a constant rate refines every frame it sends) is left alone:
591/// holding the paint-over quantizer there would coarsen the picture. A constant-rate encoder
592/// that holds no quantizer is cleaned up by the rate control's own frames, a refresh and its
593/// burst, which it refines; a key frame there would come out at the rate control's quantizer,
594/// which a small buffer starves.
595pub fn paint_over_improves(settings: &CaptureSettings, encoder: EncoderQuality) -> bool {
596    let paint = settings.video_paintover_crf.max(0) as u32;
597    settings.cleans_up()
598        && if settings.video_cbr_mode {
599            encoder.last.is_none_or(|q| q + encoder.finer > paint)
600        } else {
601            settings.video_paintover_crf < settings.video_crf
602        }
603}
604
605/// Outcome of the full-frame send decision produced by `decide_hw_fullframe`.
606pub struct HwFrameDecision {
607    pub send: bool,
608    pub force_idr: bool,
609    /// The session quality index the frame is encoded at.
610    pub target_qp: u32,
611    /// The quality index this frame is held at whatever the rate control: the cleanup of a
612    /// still screen, handed to the encoder's `hold_quantizer`.
613    pub hold_qp: Option<u32>,
614    /// The share of the picture, from and to in raster order, `hold_qp` covers, the rest of the
615    /// frame held at the coarsest quantizer; `None` for the whole picture.
616    pub hold_band: Option<(f64, f64)>,
617    /// Whether the encoder is asked to measure this frame's reconstruction
618    /// (`FrameEncoder::measure`).
619    pub measure: bool,
620    /// The bytes a still screen's cleanup frame may come to (`FrameEncoder::cap`).
621    pub cap: Option<usize>,
622    /// The blocks this frame refines (`plan_refinement`).
623    pub refine: Option<crate::ledger::Refinement>,
624    /// The blocks this frame may code as skips without analysis, in raster order
625    /// (`plan_refinement`, `BlockLedger::constant`); `None` where none may be.
626    pub constant: Option<Vec<bool>>,
627    /// The coarse blocks of a capped constant quality whose peak let go that this frame codes
628    /// again at the session's own quality, and the ones left for later frames, which it holds
629    /// as skips (`BlockLedger::refresh`, `FrameEncoder::defer_blocks`).
630    pub refresh: Option<crate::ledger::BlockPlan>,
631    pub defer: Option<crate::ledger::BlockPlan>,
632}
633
634impl HwFrameDecision {
635    /// The quality the whole frame is coded at in place of the session's `normal` one: a
636    /// quantizer held for it, or a constant quality's paint-over one.
637    pub(crate) fn cleanup_quality(&self, normal: u32) -> Option<u32> {
638        self.hold_qp
639            .filter(|_| self.hold_band.is_none())
640            .or((self.target_qp != normal).then_some(self.target_qp))
641    }
642
643    /// Tell `encoder` what the decision asks of the frame it is about to code: the quantizer
644    /// it is held at and whether its reconstruction is measured.
645    pub fn prepare(&self, encoder: &mut FrameEncoder) {
646        if let Some(q) = self.hold_qp {
647            encoder.hold_quantizer(q, self.hold_band);
648        }
649        if let Some(bytes) = self.cap {
650            encoder.cap(bytes);
651        }
652        if self.measure {
653            encoder.measure();
654        }
655        if let Some(refinement) = &self.refine {
656            encoder.refine_blocks(refinement);
657        }
658        encoder.constant_blocks(self.constant.as_deref().unwrap_or(&[]));
659        encoder.defer_blocks(self.defer.as_ref());
660    }
661}
662
663/// The block ledger's part of a full-frame decision `d` (`ledger`): take this frame's change
664/// into `st`'s ledger, from where the source says it changed (`changed`, in the frame's
665/// coordinates, read with whether the region's hash saw it change, `StripeState::hash_changed`,
666/// through `BlockLedger::frame_reported`), or the whole frame where the source cannot say and
667/// `damage` says something changed; and where the frame goes at the session's own quality while the region moves, on a
668/// session that refines (`refines`, how: `ledger::Refiner`) with the cleanup armed, name the
669/// blocks it refines, the offset taken from the quality index the rate control last coded at
670/// (`last`) where a rate binds the session (`bound`, `FrameEncoder::rate_bound`), paced against
671/// the frame budget (`ledger::RefinePace`), or from the session's own at a constant quality no
672/// peak binds. A capped constant quality its peak let go codes again the blocks it coded while
673/// bound that held still since, so many a frame at its own quality (`BlockLedger::refresh`,
674/// `ledger::RefreshPace`), the rest held as skips, sending a still screen's frames for it, and
675/// first, at the paint-over quality, the blocks owed it (`BlockLedger::refresh_owed`): a
676/// cleanup it spent under the binding peak, or one falling due on blocks it has yet to refresh
677/// or refreshed at its own quality, which it takes in place of that cleanup's frame; it refines
678/// nothing on those frames and waits while the peak binds again.
679#[allow(clippy::too_many_arguments)]
680pub fn plan_refinement(
681    st: &mut StripeState,
682    settings: &CaptureSettings,
683    d: &mut HwFrameDecision,
684    damage: Damage,
685    changed: Option<&[Rectangle<i32, Physical>]>,
686    refines: Option<crate::ledger::Refiner>,
687    last: Option<u32>,
688    bound: bool,
689) {
690    st.ledger.fit(settings.width, settings.height);
691    match changed {
692        Some(rects) => st
693            .ledger
694            .frame_reported(rects, damage.is_dirty(), st.hash_changed),
695        None if damage.is_dirty() => st.ledger.frame_all(),
696        None => st.ledger.frame([]),
697    }
698    let normal = settings.video_crf as u32;
699    // A cleanup that falls due on a capped constant quality while its peak binds, or once it let
700    // go with blocks it coded bound yet to refresh or refreshed at its own quality since, is
701    // owed to the still blocks instead, which the refresh codes at the paint-over quality within
702    // the peak once it lets go: coded under the peak, the rate control walks it down over frames
703    // and leaves it short of the uncapped session's one pass; coded whole after, it would code
704    // the coarse picture again in one frame past the peak, or leave the refreshed one coarser
705    // than that pass, which starts from coarser.
706    let capped_bound = bound && !settings.video_cbr_mode;
707    if (capped_bound || st.ledger.coarse() || st.ledger.fresh())
708        && d.send
709        && !d.force_idr
710        && d.refresh.is_none()
711        && d.cleanup_quality(normal).is_some_and(|q| q < normal)
712    {
713        if st.ledger.spent() {
714            st.refresh = Default::default();
715        }
716        d.send = false;
717        d.hold_qp = None;
718        d.hold_band = None;
719        d.cap = None;
720        d.target_qp = normal;
721    }
722    let most = if !bound
723        && st.ledger.coarse()
724        && !d.force_idr
725        && d.hold_qp.is_none()
726        && (d.target_qp == normal || !d.send)
727    {
728        st.refresh.most(st.ledger.blocks())
729    } else {
730        0
731    };
732    // The blocks owed their paint-over (`BlockLedger::spent`) go first, at the paint-over
733    // quality as a refinement codes it, on a frame that changes nothing, so a frame that moves
734    // carries none of them (Turbo's still frames included); the rest at the session's own, a
735    // refresh's steps finer (`Codec::refresh_index`).
736    let owed = if most > 0 && !damage.is_dirty() {
737        st.ledger.refresh_owed(most)
738    } else {
739        None
740    };
741    let paint = owed.is_some();
742    if let Some((now, wait)) =
743        owed.or_else(|| (most > 0).then(|| st.ledger.refresh(most)).flatten())
744    {
745        d.send = true;
746        let quality = d.target_qp as i32;
747        let finer = match (paint, refines) {
748            (true, Some(r)) => settings.video_paintover_crf - r.finer as i32,
749            (true, None) => settings.video_paintover_crf,
750            (false, _) => settings.codec.refresh_index(quality),
751        };
752        // An encoder that refines blocks codes the refreshed ones finer than the frame; one that
753        // does not (libvpx's VP9) codes the frame finer, every still block but the refreshed
754        // ones held as a skip.
755        let constant: Vec<bool> = match refines {
756            Some(r) => {
757                d.refine = Some(crate::ledger::Refinement {
758                    plan: now.clone(),
759                    target: finer,
760                    from: quality,
761                    reach: r.reach,
762                });
763                st.ledger
764                    .constant()
765                    .enumerate()
766                    .map(|(i, c)| c || wait.refines(i % wait.cols, i / wait.cols))
767                    .collect()
768            }
769            None => {
770                d.target_qp = finer.max(0) as u32;
771                st.ledger
772                    .unchanged()
773                    .enumerate()
774                    .map(|(i, c)| c && !now.refines(i % now.cols, i / now.cols))
775                    .collect()
776            }
777        };
778        d.constant = constant.contains(&true).then_some(constant);
779        d.refresh = Some(now);
780        d.defer = (wait.count > 0).then_some(wait);
781        return;
782    }
783    let own_quality = settings.cleans_up()
784        && d.send
785        && !d.force_idr
786        && d.hold_qp.is_none()
787        && d.target_qp == normal;
788    if own_quality {
789        let constant: Vec<bool> = st.ledger.constant().collect();
790        if constant.contains(&true) {
791            d.constant = Some(constant);
792        }
793    }
794    let Some(refiner) = refines else {
795        return;
796    };
797    if !(own_quality && damage.is_dirty()) {
798        return;
799    }
800    let blocks = st.ledger.blocks();
801    let most = st.refine.most(blocks, bound);
802    if let Some(plan) = st
803        .ledger
804        .plan(settings.paint_over_trigger(), most, refiner.runs)
805    {
806        let from = match (bound, last) {
807            (true, Some(q)) => q as i32,
808            _ => normal as i32,
809        };
810        d.refine = Some(crate::ledger::Refinement {
811            plan,
812            target: settings.video_paintover_crf - refiner.finer as i32,
813            from,
814            reach: refiner.reach,
815        });
816    }
817}
818
819/// What a full-frame frame decided as `d` left in `st`'s ledger once coded in `bytes`, the
820/// encoder now reporting `quality`: its refined blocks are clean where the rate control's quality index put
821/// them at the target (`BlockLedger::refinement_coded`) and, where a rate binds the session
822/// (`bound`, as `plan_refinement` takes it), the pace follows the rate control
823/// (`RefinePace`); a frame held whole at the paint-over quality, or the region's cleanup having
824/// run its course, cleans the region, and a key frame at the rate control's own leaves it
825/// unclean. A frame coded under a capped constant quality's binding peak (`capped`,
826/// `FrameEncoder::coded_capped`) leaves the blocks it coded coarse and cleans nothing; one coded
827/// without it takes the blocks it refreshed (`HwFrameDecision::refresh`) off the coarse ones and
828/// paces the refresh against the peak's frame budget (`ledger::RefreshPace`), where it came out
829/// at all (`bytes` above 0). The last block owed the paint-over (`BlockLedger::spent`,
830/// `plan_refinement`) refreshed, the cleanup falls due again.
831#[allow(clippy::too_many_arguments)]
832pub fn refinement_coded(
833    st: &mut StripeState,
834    settings: &CaptureSettings,
835    d: &HwFrameDecision,
836    quality: EncoderQuality,
837    bytes: usize,
838    bound: bool,
839    capped: bool,
840) {
841    let normal = settings.video_crf as u32;
842    let refine = d.refine.as_ref().filter(|_| d.refresh.is_none());
843    // A frame the encoder refused, or coded to nothing, reached no one: the blocks it would have
844    // coded keep what the ledger had for them.
845    if d.send && bytes > 0 {
846        st.ledger.coded(capped, d.force_idr);
847        if let Some(plan) = d.refresh.as_ref().filter(|_| !capped) {
848            // The last owed block refreshed, the cleanup the peak spent falls due again: its
849            // frames refine the picture the refresh brought to the paint-over quality further,
850            // as the uncapped session's burst does, at a cost a frame of it already holds.
851            let owed = st.ledger.owed();
852            st.ledger.refreshed(plan);
853            if owed && !st.ledger.owed() {
854                st.paint_over_sent = false;
855            }
856            let blocks = st.ledger.blocks();
857            st.refresh.coded(
858                bytes,
859                frame_budget(settings.video_bitrate_kbps, settings.target_fps),
860                blocks,
861            );
862        }
863    }
864    // A capped constant quality bound to its peak codes no block finer than its own quality
865    // (NVENC's minQP, libvpx's floored quantizer): a refinement to a finer target leaves its
866    // blocks short of it, to be refined again once the peak lets go.
867    if let Some(refinement) =
868        refine.filter(|r| !bound || settings.video_cbr_mode || r.target >= normal as i32)
869    {
870        let coded = if bound {
871            quality.last.map_or(i32::MAX, |q| q as i32)
872        } else {
873            normal as i32
874        };
875        st.ledger.refinement_coded(refinement, coded);
876    }
877    if bound
878        && d.send
879        && d.hold_qp.is_none()
880        && !d.force_idr
881        && let Some(coded) = quality.last
882    {
883        let blocks = st.ledger.blocks();
884        st.refine.coded(
885            refine.is_some(),
886            coded as i32,
887            bytes,
888            frame_budget(settings.video_bitrate_kbps, settings.target_fps),
889            blocks,
890        );
891    }
892    let paint = settings.video_paintover_crf.max(0) as u32;
893    let cleanup_ended =
894        st.paint_over_sent && st.h264_burst_frames_remaining <= 0 && st.sweep.is_none();
895    if !capped
896        && d.refresh.is_none()
897        && (d.cleanup_quality(normal).is_some_and(|q| q <= paint) || cleanup_ended)
898    {
899        st.ledger.cleaned();
900    } else if d.force_idr {
901        st.ledger.coarsened();
902    }
903}
904
905/// Whether a scheduled keyframe is due this tick.
906///
907/// The default (`keyframe_interval_s <= 0`) is an infinite GOP with no scheduled IDRs. A positive
908/// interval buys a fixed ~N-second recovery cadence for consumers that cannot request one on demand.
909///
910/// # Arguments
911///
912/// * `settings` - Capture settings; reads `keyframe_interval_s` and `target_fps`.
913/// * `frame_counter` - Current frame number (wrapping `u16`).
914///
915/// # Returns
916///
917/// `true` if a periodic IDR should be forced on this frame.
918pub fn periodic_idr_due(settings: &CaptureSettings, frame_counter: u16) -> bool {
919    let secs = settings.keyframe_interval_s;
920    if secs <= 0.0 {
921        return false;
922    }
923    let safe_fps = settings.target_fps.max(1.0);
924    let interval = ((safe_fps * secs).round() as u64).max(1);
925    u64::from(frame_counter).is_multiple_of(interval)
926}
927
928/// The quality index a refresh held in one frame (a predicted frame) is coded at: the paint-over
929/// one, or, where a constant rate last coded far coarser, as much finer than that as
930/// `HELD_KEY_BUDGET_S` of the target allows at six steps a doubling, since a refresh of a whole
931/// screen at the paint-over quantizer after heavy motion at a low rate would cost many frames of
932/// budget at once. The bands of the sweep a cleanup through the rate control falls back to, each
933/// sized to a frame's budget, are coded at the paint-over quantizer instead: counted from the rate
934/// control's last quantizer, which a key frame after a loss leaves at the coarsest, the sweep
935/// ended up to 13 dB short of it at 8 fps.
936pub fn held_refresh_quality(settings: &CaptureSettings, encoder: EncoderQuality) -> u32 {
937    refresh_quality(settings, encoder, 1.0)
938}
939
940/// `held_refresh_quality` from a rate control's last frame of `spent` frame budgets rather than
941/// one: a rate control pinned at its coarsest quantizer (`OVERSHOOT_BUDGETS`) refreshed in one
942/// held frame, which then costs that much more.
943fn refresh_quality(settings: &CaptureSettings, encoder: EncoderQuality, spent: f64) -> u32 {
944    let paint = settings.video_paintover_crf.max(0) as u32;
945    match (settings.video_cbr_mode, encoder.last) {
946        (true, Some(last)) => {
947            let frames = (crate::encoders::HELD_KEY_BUDGET_S * settings.target_fps
948                / spent.max(1.0))
949            .max(1.0);
950            paint.max(last.saturating_sub((6.0 * frames.log2()).floor() as u32))
951        }
952        _ => paint,
953    }
954}
955
956/// The send / quality / keyframe policy every full-frame encoder obeys. A constant-quality
957/// session takes `decide_constant_quality`; what follows is the constant-rate one.
958///
959/// The GOP is left infinite and an IDR is forced only when a consumer genuinely needs a fresh
960/// decode entry point or a still screen is cleaned up. Every full-frame encoder shares this one
961/// function so they cannot drift apart; the CPU path applies the same policy per region inside
962/// `encode_cpu`.
963///
964/// 1. **Cleanup** (`cleanup_due`): once the content stops changing, whatever Turbo sends, a
965///    refresh and, after a large change and a longer stillness, a key frame where the encoder's
966///    cleanup ends on one (`EncoderQuality::keys`), each encoded at the paint-over quantizer held
967///    for it (`hold_qp`, which the encoder applies under any rate control; a refresh of a
968///    constant-rate session coarsened to `held_refresh_quality`, a key frame capped by the encoder
969///    at `HELD_KEY_BUDGET_S`) and followed by a recovery burst. A constant-rate session whose rate
970///    control reports its frames' bytes (`EncoderQuality::converges`) is cleaned up through that
971///    rate control instead, NVENC's like this:
972///    the frames keep flowing, each within the rate control's budget, until it codes the screen
973///    at the paint-over quantizer or finer in a frame under `CONVERGED_SHARE` of the budget
974///    (`convergence`), for `CONVERGE_S` at most. A frame held at the paint-over quantizer the
975///    size of a whole refresh would otherwise queue for up to half a second on a link near the
976///    stream's rate; the rate control reaches the same picture in about a second at the
977///    default rate with every frame within its budget. Only where it stops refining short of
978///    convergence, its finest quantizer standing or its frames small at a coarser one for
979///    `STALL_TRIGGERS` periods, as where the rate is low for the resolution, or a frame of it
980///    comes out past its cap (`cleanup_cap`), is the picture refreshed, which ends the cleanup: a
981///    band a frame at the paint-over quantizer where the encoder holds one (`FIRST_BAND`,
982///    `BAND_BUDGETS`) until the sweep has covered it or motion ends it, else one held frame at
983///    `held_refresh_quality`. The bands are sized to the rate the path carries (`cleanup_rate`) and kept to
984///    it, as the rate control's frames are outside Turbo, one past its budget waited out before the
985///    next, and each cleanup frame, the rate
986///    control's and the sweep's, is held to its cap: the encoder takes one back that comes out
987///    past it and codes it again, a band in a shorter one and the rate control's frame refining
988///    nothing (`FrameEncoder::cap`). At 8 fps and 500 kbps NVENC's rate control refines a 1080p
989///    page a quantizer step every two frames for seconds, each frame within a budget, and bands
990///    of three to five budgets queue on a link a quarter faster than the rate, where congestion
991///    control reads them as the path falling behind. A sweep whose rest would take it
992///    past `REFINE_S` at its bands' pace, once they are sized to the budget, is coded a step
993///    coarser a band until the pace fits, and ends where that reaches the rate control's own
994///    quantizer: dense text at 0.1 and 0.25 Mbit/s never finished in 40 s at the paint-over
995///    one. At 2 Mbit/s at 1080p a key frame
996///    capped to `HELD_KEY_BUDGET_S` of the target came out coarser than the picture it cleaned
997///    (35.9 dB), where the refresh reached 44.2. A rate control whose frames of the still screen
998///    run over `OVERSHOOT_BUDGETS` for a trigger period, none finer than the finest before (the
999///    frames refining a recovery key frame run as far over), is pinned at its coarsest quantizer
1000///    and refines nothing within the rate, and every frame of a sweep would carry the rest of the
1001///    picture at that cost: NVENC's at 0.1 Mbit/s codes a still 1080p screen of texture in
1002///    16-budget frames, and its sweep sent 4.7 MB in 40 s without ending. Such a screen is
1003///    refreshed at once in one held frame, coarsened to what `HELD_KEY_BUDGET_S` buys at those
1004///    frames' size and coded again to fit it where it comes out past `HELD_REFRESH_LIMIT_S`, as
1005///    dense text does, and a burst that has no refresh to fall back to ends. A session that reports
1006///    its frames' bytes but holds no quantizer at a constant rate (VA-API, x265, kvazaar, libvpx's
1007///    VP9) has no refresh to fall back to, so its frames flow, each the rate control's, until the
1008///    picture is done: where the session measures its reconstruction against its source (VA-API,
1009///    `plateau`) until that stops improving, else after a trigger period of frames at the
1010///    paint-over quantizer, or as much finer as its frames need to come to the paint-over
1011///    quality (`EncoderQuality::finer`: six steps for x265 and kvazaar), or once the finest
1012///    quantizer it has coded at has stood for `LEVEL_S`; and in either case after
1013///    `STALL_TRIGGERS` periods of small frames or `REFINE_S`. Where its rate control cannot
1014///    refine the picture at all (`EncoderQuality::ends_on_key`: radeonsi at a frame budget
1015///    of a few hundredths of a bit a pixel), a key frame held at the paint-over quantizer, with
1016///    a budget of its own, ends it after a large change and a longer stillness, as the key-frame
1017///    cleanup of a session that holds its frames does. A fixed burst left such a screen where
1018///    the motion did (19 to 27 dB at
1019///    2 Mbit/s at 1080p on iHD, x265, and SVT-AV1, each frame within its budget), and a
1020///    reported quantizer alone ends it early or never: iHD's H.264 average reads 26 through
1021///    a refinement from 23 to 50 dB. That cleanup then counts as settled
1022///    (`StripeState::settled`), and only a change of more than a small area arms another, so
1023///    a caret blinking on a clean screen costs its own frames and no more. A session that
1024///    reports no bytes (Tegra, a stateful V4L2 device) gets the refresh and burst at its rate
1025///    control's own quality, and no key frame. A change of more than a small area ends a
1026///    cleanup through the rate control; a caret blinking or a clock ticking on the screen
1027///    does not, so constant low motion is cleaned up the same way.
1028/// 2. **Recovery keyframe**: a requested or scheduled IDR, at the rate control's own quality: it
1029///    answers a join or a loss, often on a link that just fell behind, where a key frame the
1030///    size of a cleanup would only fall behind again. A scheduled one waits for motion
1031///    (`StripeState::scheduled_key`), so only a requested one lands on a still screen, and
1032///    any key frame coded pays a scheduled one owed. On a still screen it opens the burst,
1033///    which lasts until the rate control converges where the cleanup runs through it, or is
1034///    pinned (`OVERSHOOT_BUDGETS`).
1035/// 3. **Recovery burst**: the frames after a keyframe or cleanup on a still screen keep flowing
1036///    so rate control settles, until motion resumes: after a cleanup held at the refresh's
1037///    quantizer where the paint-over one still improves on the rate control's, so they go on
1038///    refining the picture (a key frame capped to its budget below the paint-over quality
1039///    included), and so after any keyframe of a constant-quality session; after a recovery key
1040///    frame of a constant-rate session they are the rate control's.
1041/// 4. **Motion**, Turbo (`video_streaming_mode`), and animated overlays send at the session's
1042///    quality; a still screen outside all of the above sends nothing.
1043///
1044/// # Arguments
1045///
1046/// * `st` - Per-region state carrying the cleanup and burst bookkeeping.
1047/// * `settings` - Capture settings; reads CRF, paint-over CRF, burst frames, trigger frames,
1048///   streaming mode, and keyframe interval.
1049/// * `frame_counter` - Current frame number (wrapping `u16`).
1050/// * `damage` - What changed, from compositor damage, the X server, the driver, or a content hash.
1051/// * `is_animated` - Forces a send for animated overlays.
1052/// * `requested_idr` - On-demand IDR request (client join / reset / recording cadence).
1053/// * `encoder` - What the encoder says of its frames (`EncoderQuality`).
1054///
1055/// # Returns
1056///
1057/// [`HwFrameDecision`] with `send`, `force_idr`, `target_qp` (the session quality index),
1058/// `hold_qp` (the quantizer a cleanup holds its frame at), and `hold_band` (the share of the
1059/// picture it covers).
1060pub fn decide_hw_fullframe(
1061    st: &mut StripeState,
1062    settings: &CaptureSettings,
1063    frame_counter: u16,
1064    damage: Damage,
1065    is_animated: bool,
1066    requested_idr: bool,
1067    encoder: EncoderQuality,
1068) -> HwFrameDecision {
1069    let scheduled = st.scheduled_key(
1070        periodic_idr_due(settings, frame_counter),
1071        damage.is_motion(),
1072    );
1073    let recovery_idr = requested_idr || scheduled;
1074    let d = if settings.video_cbr_mode {
1075        decide_constant_rate(
1076            st,
1077            settings,
1078            damage,
1079            is_animated,
1080            requested_idr,
1081            recovery_idr,
1082            encoder,
1083        )
1084    } else {
1085        decide_constant_quality(st, settings, damage, is_animated, recovery_idr, encoder)
1086    };
1087    if d.force_idr {
1088        st.key_owed = false;
1089    }
1090    d
1091}
1092
1093/// `decide_hw_fullframe` for a constant-rate session, `recovery_idr` the key frame a consumer
1094/// asked for or the schedule's.
1095fn decide_constant_rate(
1096    st: &mut StripeState,
1097    settings: &CaptureSettings,
1098    damage: Damage,
1099    is_animated: bool,
1100    requested_idr: bool,
1101    recovery_idr: bool,
1102    encoder: EncoderQuality,
1103) -> HwFrameDecision {
1104    let normal_qp = settings.video_crf as u32;
1105    let paint_qp = settings.video_paintover_crf as u32;
1106    let burst = settings.video_paintover_burst_frames;
1107    let improves = paint_over_improves(settings, encoder);
1108    let converges = encoder.converges();
1109    let holds = encoder.holds && !converges;
1110    // A session that holds its refresh and a band of a frame (libvpx's VP8) sweeps the refresh
1111    // a band a frame, as a converging one's fallback does, rather than holding it whole.
1112    let sweeps = holds && encoder.band.is_some();
1113    let cleanup = cleanup_due(
1114        st,
1115        settings.paint_over_trigger(),
1116        refresh_start(settings.paint_over_trigger(), converges),
1117        improves,
1118        true,
1119        (holds && encoder.keys) || (converges && encoder.ends_on_key),
1120        damage,
1121    );
1122    let refresh_qp = held_refresh_quality(settings, encoder);
1123    // The bands of a sweep, each sized to a frame's budget, are coded at the paint-over quantizer:
1124    // the fallback of a cleanup through the rate control, and a held refresh swept outside Turbo
1125    // (libvpx's VP8 at 8 fps 0.5 Mbit/s 37.6 -> 42.8 dB). Under Turbo, whose frames between the
1126    // bands flow at the rate control's quality, VP8's bands pinned that fine had those frames come
1127    // out at up to 242 kB, so a held refresh's sweep there keeps the refresh's quantizer.
1128    let band_qp = if converges || !settings.video_streaming_mode {
1129        paint_qp
1130    } else {
1131        refresh_qp
1132    };
1133    let cleanup_qp = match cleanup {
1134        Cleanup::Refresh if holds => Some(refresh_qp),
1135        Cleanup::Key => Some(paint_qp),
1136        _ => None,
1137    };
1138    let mut d = HwFrameDecision {
1139        send: false,
1140        force_idr: false,
1141        target_qp: normal_qp,
1142        hold_qp: None,
1143        hold_band: None,
1144        measure: false,
1145        cap: None,
1146        refine: None,
1147        constant: None,
1148        refresh: None,
1149        defer: None,
1150    };
1151    if damage.is_dirty() {
1152        if !converges || damage.is_motion() {
1153            st.h264_burst_frames_remaining = 0;
1154        }
1155        // Motion ends a sweep. A small change (a caret) leaves it to finish: one that started
1156        // over at every blink would never cover the picture.
1157        if damage.is_motion() {
1158            st.sweep = None;
1159            st.settled = false;
1160        }
1161        d.send = true;
1162        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
1163        d.hold_qp = cleanup_qp;
1164        if sweeps && cleanup == Cleanup::Refresh && !d.force_idr {
1165            sweep_refresh(st, &mut d, band_qp);
1166        }
1167        return d;
1168    }
1169    if cleanup != Cleanup::None || recovery_idr {
1170        // A key frame asked for codes a clean region again at the stream's own quality: it is due
1171        // its cleanup again, as after a loss, a trigger period on. A periodic one is not: a cleanup
1172        // each interval would cost a held key frame each interval where the cleanup keys.
1173        if requested_idr && cleanup == Cleanup::None {
1174            st.lost_frame();
1175        }
1176        d.send = true;
1177        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
1178        d.hold_qp = cleanup_qp;
1179        // A key frame ends a cleanup through the rate control: it restores the picture whole.
1180        if converges && cleanup == Cleanup::Key && !recovery_idr {
1181            st.h264_burst_frames_remaining = 0;
1182            st.settled = true;
1183            return d;
1184        }
1185        if recovery_idr {
1186            st.sweep = None;
1187        } else if sweeps && cleanup == Cleanup::Refresh && !d.force_idr {
1188            sweep_refresh(st, &mut d, band_qp);
1189        }
1190        if converges && cleanup != Cleanup::None && !d.force_idr {
1191            d.cap = Some(cleanup_cap(settings));
1192            st.cleanup_paced = true;
1193        }
1194        if converges && (cleanup != Cleanup::None || (burst > 0 && recovery_idr)) {
1195            if (st.h264_burst_frames_remaining <= 0 && !st.settled) || recovery_idr {
1196                st.h264_burst_frames_remaining = cleanup_frames(settings, encoder);
1197                st.idle_frames = 0;
1198                st.over_frames = 0;
1199                st.same_frames = 0;
1200                st.rc_bytes = 0;
1201                // The frame that made the region due its cleanup may be the overshoot repaid in it.
1202                st.rc_debt = (encoder.bytes.unwrap_or(0) as f64
1203                    - frame_budget(settings.video_bitrate_kbps, settings.target_fps))
1204                .max(0.0);
1205                st.finest = None;
1206                st.finest_frames = 0;
1207                st.fine_frames = 0;
1208                st.measured = None;
1209                st.level_checks = 0;
1210                st.measuring = false;
1211                st.measure_in = 0;
1212            }
1213            st.burst_held = false;
1214        } else if burst > 0 && (d.force_idr || cleanup != Cleanup::None) && st.sweep.is_none() {
1215            // A sweep spends a budget a frame and leaves the rate control nothing to settle, so no
1216            // burst follows it, which would hold whole frames again once it ends.
1217            st.h264_burst_frames_remaining = burst;
1218            st.burst_held =
1219                holds && (cleanup != Cleanup::None || (improves && !settings.video_cbr_mode));
1220        }
1221        return d;
1222    }
1223    // A band the encoder took back for coming out past its cap covered only so far. One it took
1224    // back before the last band began (a band two frames old, its report late) leaves the sweep
1225    // the least band to size the next from: a negative one panicked `band_share`'s clamp, which
1226    // ended the capture and froze the stream.
1227    if let Some((from, size)) = st.sweep.as_mut()
1228        && let Some(end) = encoder.refit.filter(|&end| end > 0.0 && end < *from)
1229    {
1230        *size = (*size - (*from - end)).max(LEAST_BAND);
1231        *from = end;
1232    }
1233    // A cleanup's frames, a sweep's bands and outside Turbo the rate control's, are kept to the
1234    // cleanup's rate: a budget they ran past is waited out before the next, the wait counting
1235    // toward the sweep's and the burst's time. At 0.1 Mbit/s NVENC's rate control codes a still
1236    // 720p texture in 5 kB frames, 14 budgets, and a band sent right behind one ran past what
1237    // WebRTC's sender lets through, which dropped it.
1238    let pace = frame_budget(cleanup_rate(settings), settings.target_fps);
1239    let last = if st.sweep.is_some() {
1240        encoder.band
1241    } else {
1242        encoder.bytes
1243    };
1244    let spent = if std::mem::take(&mut st.cleanup_paced) {
1245        last.unwrap_or(0)
1246    } else {
1247        0
1248    };
1249    st.cleanup_debt = (st.cleanup_debt + spent as f64 - pace).max(0.0);
1250    let bursting =
1251        converges && st.h264_burst_frames_remaining > 0 && !settings.video_streaming_mode;
1252    if (st.sweep.is_some() || bursting) && st.cleanup_debt >= pace {
1253        if st.sweep.is_some() {
1254            st.sweep_frames = st.sweep_frames.saturating_add(1);
1255        } else {
1256            // The burst's last frame is left to end it.
1257            st.h264_burst_frames_remaining = (st.h264_burst_frames_remaining - 1).max(1);
1258        }
1259        d.send = settings.video_streaming_mode || is_animated;
1260        return d;
1261    }
1262    if let Some((from, size)) = st.sweep {
1263        let held = std::mem::take(&mut st.sweep_held).then(|| encoder.band.unwrap_or(0));
1264        // What the whole picture comes to at the refresh's quantizer by the bands so far, each at
1265        // what it came out, six steps coarser a halving.
1266        if let Some(bytes) = held.filter(|&b| b > 0 && size > 0.0) {
1267            let cost = bytes as f64 / size * 2f64.powf(f64::from(st.sweep_coarser) / 6.0);
1268            if st.sweep_cost == 0.0 {
1269                st.sweep_first = cost;
1270            }
1271            st.sweep_cost = if st.sweep_cost > 0.0 {
1272                (st.sweep_cost + cost) / 2.0
1273            } else {
1274                cost
1275            };
1276        }
1277        // A least band whose frame still comes out past its budget, at more than twice what the
1278        // band comes to at the first band's cost, pays for the rest of the frame: NVENC's HEVC
1279        // codes a still 1080p texture in 2.8 kB frames whatever the band, five budgets at 0.25
1280        // Mbit/s, and its sweep crawled on without end; at 720p and 0.5 Mbit/s it stepped coarser
1281        // to the rate control's quantizer and ended at 8.9 levels. The rest of the picture is
1282        // refreshed in one held frame instead, as a pinned rate control's is.
1283        let band = st.sweep_first * size * 0.5f64.powf(f64::from(st.sweep_coarser) / 6.0);
1284        if let Some(bytes) = held.filter(|&b| {
1285            size <= LEAST_BAND && b as f64 > BAND_BUDGETS * pace && b as f64 > 2.0 * band
1286        }) {
1287            let target = frame_budget(settings.video_bitrate_kbps, settings.target_fps);
1288            st.sweep = None;
1289            d.send = true;
1290            d.hold_qp = Some(refresh_quality(settings, encoder, bytes as f64 / target));
1291            if converges {
1292                d.cap = Some(cleanup_cap(settings));
1293            }
1294            return d;
1295        }
1296        let size = band_share(size, encoder.band, pace);
1297        let to = (from + size).min(1.0);
1298        st.sweep_frames = st.sweep_frames.saturating_add(1);
1299        let left = (REFINE_S * settings.target_fps.max(1.0)).round() - f64::from(st.sweep_frames);
1300        let paced = encoder
1301            .band
1302            .is_some_and(|b| b as f64 <= 2.0 * BAND_BUDGETS * pace);
1303        // The rest of the picture at that cost against the frames left: a step coarser where it
1304        // would run past them, a step finer where a step finer still ends well within them. At
1305        // 0.2 Mbit/s at 1080p a first band far past its budget had the bands after it, sized down
1306        // a halving a frame, read as more than the window holds frame after frame, which stepped
1307        // them to quantizer 43 and ended the sweep at 32.5 dB in 9 s of a 30 s window.
1308        if paced && st.sweep_cost > 0.0 {
1309            let frames = |coarser: u32| {
1310                (1.0 - to) * st.sweep_cost * 0.5f64.powf(f64::from(coarser) / 6.0) / pace
1311            };
1312            if frames(st.sweep_coarser) > left.max(1.0) {
1313                st.sweep_coarser += 1;
1314            } else if st.sweep_coarser > 0
1315                && frames(st.sweep_coarser - 1) < SWEEP_FINER_SHARE * left
1316            {
1317                st.sweep_coarser -= 1;
1318            }
1319        }
1320        let q = band_qp + st.sweep_coarser;
1321        if encoder.last.is_some_and(|last| q >= last) {
1322            st.sweep = None;
1323            return d;
1324        }
1325        st.sweep = (to < 1.0).then_some((to, size));
1326        st.sweep_held = true;
1327        st.cleanup_paced = true;
1328        d.send = true;
1329        d.hold_qp = Some(q);
1330        d.hold_band = Some((from, to));
1331        d.cap = Some(cleanup_cap(settings));
1332        return d;
1333    }
1334    // A frame coded at the cleanup's rate is measured against that rate's budget.
1335    let budget = frame_budget(
1336        if encoder.follows_cap {
1337            cleanup_rate(settings)
1338        } else {
1339            settings.video_bitrate_kbps
1340        },
1341        settings.target_fps,
1342    );
1343    let trigger = settings.paint_over_trigger();
1344    if converges && st.h264_burst_frames_remaining > 0 {
1345        // Only a frame no finer than the burst's finest refines nothing: after a recovery key frame
1346        // coded at the coarsest quantizer, the rate control's frames run as far over their budgets
1347        // while it refines the picture from it.
1348        st.over_frames = if encoder
1349            .bytes
1350            .is_some_and(|b| b as f64 > budget * OVERSHOOT_BUDGETS)
1351            && encoder
1352                .last
1353                .is_none_or(|q| st.finest.is_none_or(|finest| q >= finest))
1354        {
1355            st.over_frames.saturating_add(1)
1356        } else {
1357            0
1358        };
1359    }
1360    let pinned = converges && st.h264_burst_frames_remaining > 0 && st.over_frames >= trigger;
1361    if pinned && !(improves && encoder.holds) {
1362        st.h264_burst_frames_remaining = 0;
1363        st.settled = !encoder.holds;
1364    }
1365    // A rate control repays a frame it coded far over the target with ones under it at its
1366    // coarsest, which say nothing of where it stops refining: kvazaar's at 60 fps 0.5 Mbit/s
1367    // spends 59 budgets on the frame after a scroll and then holds quantizer 51 for a second. One
1368    // whose coarsest stays over the target is not repaying.
1369    if converges && st.h264_burst_frames_remaining > 0 {
1370        st.rc_debt = (st.rc_debt + encoder.bytes.unwrap_or(0) as f64 - budget).max(0.0);
1371    }
1372    let repaying = st.rc_debt > budget && encoder.bytes.is_some_and(|b| (b as f64) < budget);
1373    if converges && st.h264_burst_frames_remaining > 0 {
1374        // The quantizer the rate control's frames come to the paint-over quality at.
1375        let reach_qp = paint_qp.saturating_sub(encoder.finer);
1376        match convergence(encoder.last, encoder.bytes, reach_qp, budget) {
1377            Convergence::Converged if !encoder.measures => {
1378                st.h264_burst_frames_remaining = 0;
1379                st.change_mass = 0.0;
1380                st.settled = !encoder.holds;
1381            }
1382            Convergence::Idle if repaying => {}
1383            Convergence::Converged | Convergence::Idle => {
1384                st.idle_frames = st.idle_frames.saturating_add(1)
1385            }
1386            Convergence::Refining => st.idle_frames = 0,
1387        }
1388        st.fine_frames = if encoder.last.is_some_and(|q| q <= reach_qp) {
1389            st.fine_frames.saturating_add(1)
1390        } else {
1391            0
1392        };
1393    }
1394    if st.h264_burst_frames_remaining > 0 {
1395        st.h264_burst_frames_remaining -= 1;
1396        d.send = true;
1397        d.hold_qp = (st.burst_held && improves).then_some(refresh_qp);
1398        let stalled = st.idle_frames >= STALL_TRIGGERS * trigger;
1399        if converges && let Some(q) = encoder.last {
1400            if st.finest.is_none_or(|finest| q < finest) {
1401                st.finest = Some(q);
1402                st.finest_frames = 0;
1403            } else {
1404                st.finest_frames = st.finest_frames.saturating_add(1);
1405            }
1406        }
1407        if converges && !encoder.holds {
1408            let reached = settings.cleans_up() && st.fine_frames >= trigger;
1409            let (measure, level) = plateau(st, encoder.psnr, PLATEAU_TRIGGERS * trigger);
1410            d.measure = measure;
1411            let done = if encoder.measures { level } else { reached };
1412            let bytes = encoder.bytes.unwrap_or(0);
1413            st.same_frames = if bytes > 0
1414                && bytes.abs_diff(st.rc_bytes) as f64 <= st.rc_bytes as f64 * SAME_FRAME_SHARE
1415            {
1416                st.same_frames.saturating_add(1)
1417            } else {
1418                0
1419            };
1420            st.rc_bytes = bytes;
1421            // A session that says no quantizer and measures nothing (radeonsi's VCE) has the
1422            // sizes alone: at a constant rate it codes frames of the same size once it is done
1423            // with a still screen (60 fps at 8 Mbit/s: 16 kB frames for 8 s after the picture
1424            // stopped at 54 dB), where frames it still refines with vary.
1425            let same = !repaying
1426                && f64::from(st.same_frames) >= EMPTY_S * settings.target_fps
1427                && encoder.last.is_none_or(|q| q >= SAME_FRAME_QUALITY);
1428            let level = f64::from(st.finest_frames) >= LEVEL_S * settings.target_fps;
1429            if ((stalled || same || level) && !encoder.measures)
1430                || done
1431                || st.h264_burst_frames_remaining == 0
1432            {
1433                st.h264_burst_frames_remaining = 0;
1434                st.settled = true;
1435            }
1436        } else if converges
1437            && improves
1438            && (stalled
1439                || pinned
1440                || st.finest_frames >= STALL_TRIGGERS * trigger
1441                || encoder.refit == Some(0.0))
1442        {
1443            d.hold_qp = Some(if pinned {
1444                refresh_quality(
1445                    settings,
1446                    encoder,
1447                    encoder.bytes.unwrap_or(0) as f64 / budget,
1448                )
1449            } else {
1450                refresh_qp
1451            });
1452            st.h264_burst_frames_remaining = 0;
1453            if encoder.band.is_some() && !pinned {
1454                start_sweep(st, &mut d, band_qp);
1455            }
1456        }
1457        if converges {
1458            d.cap = Some(cleanup_cap(settings));
1459            st.cleanup_paced = true;
1460        }
1461        return d;
1462    }
1463    d.send = settings.video_streaming_mode || is_animated;
1464    d
1465}
1466
1467/// Start a refresh sweep at the top of the picture: its first band in this frame at `qp`, the rest
1468/// a band a frame after it (`FIRST_BAND`, `band_share`).
1469fn start_sweep(st: &mut StripeState, d: &mut HwFrameDecision, qp: u32) {
1470    d.hold_qp = Some(qp);
1471    d.hold_band = Some((0.0, FIRST_BAND));
1472    st.sweep = Some((FIRST_BAND, FIRST_BAND));
1473    st.sweep_frames = 0;
1474    st.sweep_coarser = 0;
1475    st.cleanup_debt = 0.0;
1476    st.sweep_cost = 0.0;
1477    st.sweep_first = 0.0;
1478    st.sweep_held = true;
1479    st.cleanup_paced = true;
1480}
1481
1482/// A refresh due on a session that sweeps it (`decide_hw_fullframe`'s `sweeps`): a sweep from the
1483/// top, or, with one under way, which is already refreshing the picture, nothing held, the frame
1484/// going at the rate control's quality: a sweep started over at every change of a blinking caret
1485/// would never cover the picture, and a whole held frame is what the sweep spreads.
1486fn sweep_refresh(st: &mut StripeState, d: &mut HwFrameDecision, qp: u32) {
1487    if st.sweep.is_some() {
1488        d.hold_qp = None;
1489    } else {
1490        start_sweep(st, d, qp);
1491    }
1492}
1493
1494/// `decide_hw_fullframe` for a constant-quality session: the cleanup moves the session's own
1495/// quality to the paint-over one, as main's paint-over did, rather than holding a frame at a
1496/// quantizer. Each encoder then codes the cleanup the way it codes any frame at that quality:
1497/// x264's and x265's rate factor give a key frame their intra offset (about three steps finer
1498/// than the paint-over index), SVT-AV1 its key-frame qindex, and the encoders whose quality
1499/// change re-opens the session (x265, SVT-AV1, kvazaar) open on a key frame at it. A frame held
1500/// at the paint-over quantizer itself came out coarser than main's (SVT-AV1 49.6 against 61.0 dB,
1501/// x264 53.7 against 56.5 dB).
1502///
1503/// The session stays at the paint-over quality while the region stays clean: from its cleanup
1504/// until motion (more than a small change), so the frames Turbo sends of a still screen, or a
1505/// caret blinking on it, do not take a re-opening encoder back to the session's quality, a key
1506/// frame at it, after `QP_HYSTERESIS_LIMIT` frames. The refresh and the bursts come when
1507/// `cleanup_due` says, as at a constant rate, and no key-frame cleanup follows: on NVENC at CRF 25
1508/// and paint-over 18 it lifted the end 0.8 dB for five times the bytes, one key frame of 415 kB
1509/// where the refresh is 100. A requested or scheduled key frame is coded at the session's own
1510/// quality, as on main, and its burst at the paint-over one. An encoder whose quality change
1511/// re-opens it (`EncoderQuality::reopens`: x265, kvazaar, SVT-AV1) codes the refresh that moves it
1512/// as a key frame, as main's paint-over did.
1513fn decide_constant_quality(
1514    st: &mut StripeState,
1515    settings: &CaptureSettings,
1516    damage: Damage,
1517    is_animated: bool,
1518    recovery_idr: bool,
1519    encoder: EncoderQuality,
1520) -> HwFrameDecision {
1521    let normal_qp = settings.video_crf as u32;
1522    let paint_qp = settings.video_paintover_crf as u32;
1523    let burst = settings.video_paintover_burst_frames;
1524    let improves =
1525        settings.use_paint_over_quality && settings.video_paintover_crf < settings.video_crf;
1526    let mut cleanup = cleanup_due(
1527        st,
1528        settings.paint_over_trigger(),
1529        settings.paint_over_trigger(),
1530        improves,
1531        true,
1532        false,
1533        damage,
1534    );
1535    if damage.is_motion() || !improves {
1536        st.clean_quality = false;
1537    }
1538    if cleanup == Cleanup::Refresh && encoder.reopens && !st.clean_quality {
1539        cleanup = Cleanup::Key;
1540        st.change_mass = 0.0;
1541    }
1542    if cleanup != Cleanup::None {
1543        st.clean_quality = true;
1544    }
1545    let quality = if st.clean_quality {
1546        paint_qp
1547    } else {
1548        normal_qp
1549    };
1550    let mut d = HwFrameDecision {
1551        send: false,
1552        force_idr: false,
1553        target_qp: quality,
1554        hold_qp: None,
1555        hold_band: None,
1556        measure: false,
1557        cap: None,
1558        refine: None,
1559        constant: None,
1560        refresh: None,
1561        defer: None,
1562    };
1563    if recovery_idr && cleanup == Cleanup::None {
1564        d.target_qp = normal_qp;
1565    }
1566    if damage.is_dirty() {
1567        st.h264_burst_frames_remaining = 0;
1568        d.send = true;
1569        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
1570        return d;
1571    }
1572    if cleanup != Cleanup::None || recovery_idr {
1573        d.send = true;
1574        d.force_idr = recovery_idr || cleanup == Cleanup::Key;
1575        if burst > 0 {
1576            st.h264_burst_frames_remaining = burst;
1577            st.burst_held = improves;
1578        }
1579        return d;
1580    }
1581    if st.h264_burst_frames_remaining > 0 {
1582        st.h264_burst_frames_remaining -= 1;
1583        d.send = true;
1584        if st.burst_held {
1585            d.target_qp = paint_qp;
1586        }
1587        return d;
1588    }
1589    d.send = settings.video_streaming_mode || is_animated;
1590    d
1591}
1592
1593/// Rows per band of the content hash a full-frame X11 session reads its damage from: one band is
1594/// under `SMALL_AREA` of a screen from 720 rows up, so a caret or a clock reads as the small
1595/// change it is.
1596pub(crate) const DAMAGE_BAND_ROWS: usize = 32;
1597
1598/// Which bands of `DAMAGE_BAND_ROWS` rows of a host frame (`stride` bytes per row, `height` rows)
1599/// changed against the frame before, read from a content hash per band
1600/// (`StripeState::content_dirty`, one state per band in `bands`, with its damage blocks). The
1601/// bands hash in turn on the calling thread: a band takes tens of microseconds, less than waking
1602/// the rayon pool for it costs, which on a many-core host spent several times the hash in CPU
1603/// and added milliseconds waiting on the slowest worker.
1604fn hash_bands(
1605    bands: &mut Vec<StripeState>,
1606    pixels: &[u8],
1607    stride: usize,
1608    height: usize,
1609    threshold: u32,
1610    duration: i32,
1611) -> Vec<bool> {
1612    let n = height.div_ceil(DAMAGE_BAND_ROWS).max(1);
1613    if bands.len() != n {
1614        bands.clear();
1615        bands.resize_with(n, StripeState::default);
1616    }
1617    bands
1618        .iter_mut()
1619        .enumerate()
1620        .map(|(i, band)| {
1621            let end = ((i + 1) * DAMAGE_BAND_ROWS).min(height) * stride;
1622            let bytes =
1623                &pixels[(i * DAMAGE_BAND_ROWS * stride).min(pixels.len())..end.min(pixels.len())];
1624            band.content_dirty(bytes, threshold, duration)
1625        })
1626        .collect()
1627}
1628
1629/// The bands that changed by hashes taken where the frame was uploaded (`hash_next_upload`),
1630/// read through each band's state as the CPU's own hashes are (`StripeState::hash_dirty`).
1631fn hashed_bands(
1632    bands: &mut Vec<StripeState>,
1633    hashes: &[u64],
1634    threshold: u32,
1635    duration: i32,
1636) -> Vec<bool> {
1637    if bands.len() != hashes.len() {
1638        bands.clear();
1639        bands.resize_with(hashes.len(), StripeState::default);
1640    }
1641    bands
1642        .iter_mut()
1643        .zip(hashes)
1644        .map(|(band, &hash)| band.hash_dirty(hash, threshold, duration))
1645        .collect()
1646}
1647
1648/// The damage a band hash (`hash_bands`) reads: the fraction of the bands that changed. No bands
1649/// read yet, before a capture's first hash, is all new.
1650fn band_damage(dirty: &[bool]) -> Damage {
1651    let changed = dirty.iter().filter(|&&d| d).count();
1652    if dirty.is_empty() {
1653        Damage::Area(1.0)
1654    } else if changed == 0 {
1655        Damage::None
1656    } else {
1657        Damage::Area(changed as f32 / dirty.len() as f32)
1658    }
1659}
1660
1661/// The damage of a frame whose bands were hashed where it lies (NvFBC's, on the GPU) against the
1662/// hashes of the frame before, which `last` keeps: the share of the bands that changed. No hashes
1663/// before is all new.
1664pub(crate) fn hashed_damage(last: &mut Vec<u64>, hashes: Vec<u64>) -> Damage {
1665    let dirty: Vec<bool> = if last.len() == hashes.len() {
1666        hashes
1667            .iter()
1668            .zip(last.iter())
1669            .map(|(a, b)| a != b)
1670            .collect()
1671    } else {
1672        Vec::new()
1673    };
1674    *last = hashes;
1675    band_damage(&dirty)
1676}
1677
1678/// The damage of a host frame against the one before it, from its band hash (`hash_bands`).
1679fn hash_damage(
1680    bands: &mut Vec<StripeState>,
1681    pixels: &[u8],
1682    stride: usize,
1683    height: usize,
1684    threshold: u32,
1685    duration: i32,
1686) -> Damage {
1687    band_damage(&hash_bands(
1688        bands, pixels, stride, height, threshold, duration,
1689    ))
1690}
1691
1692/// The rows a band hash (`hash_bands`) found changed in a `width` x `height` frame, as
1693/// full-width rectangles, one per run of changed bands: the CPU path's dirty map. No bands
1694/// read yet is the whole frame.
1695fn band_rects(dirty: &[bool], width: i32, height: i32) -> Vec<Rectangle<i32, Physical>> {
1696    if dirty.is_empty() {
1697        return vec![Rectangle::new((0, 0).into(), (width, height).into())];
1698    }
1699    let mut rects = Vec::new();
1700    let mut run = None;
1701    for (i, &changed) in dirty.iter().chain(std::iter::once(&false)).enumerate() {
1702        match (changed, run) {
1703            (true, None) => run = Some(i),
1704            (false, Some(first)) => {
1705                let top = (first * DAMAGE_BAND_ROWS) as i32;
1706                let bottom = ((i * DAMAGE_BAND_ROWS) as i32).min(height);
1707                rects.push(Rectangle::new(
1708                    (0, top).into(),
1709                    (width, bottom - top).into(),
1710                ));
1711                run = None;
1712            }
1713            _ => {}
1714        }
1715    }
1716    rects
1717}
1718
1719/// Run `encode` on this thread while a thread of its own hashes the frame's bands (`hash_bands`),
1720/// and return what `encode` returned with the bands that changed: a Turbo frame's hash kept off
1721/// its encode's path. A scoped thread per frame rather than the rayon pool, whose idle workers
1722/// spin after every wake; where no thread starts, the hash runs here after the encode. The
1723/// thread keeps to the CPUs sharing this one's last-level cache (`share_cache_with`).
1724#[allow(clippy::too_many_arguments)]
1725fn hash_beside<R>(
1726    bands: &mut Vec<StripeState>,
1727    pixels: &[u8],
1728    stride: usize,
1729    height: usize,
1730    threshold: u32,
1731    duration: i32,
1732    encode: impl FnOnce() -> R,
1733) -> (R, Vec<bool>) {
1734    let cpu = unsafe { libc::sched_getcpu() };
1735    let (out, hashed) = std::thread::scope(|s| {
1736        let hashing = std::thread::Builder::new()
1737            .name("pxf-x11-hash".into())
1738            .spawn_scoped(s, || {
1739                share_cache_with(cpu);
1740                hash_bands(bands, pixels, stride, height, threshold, duration)
1741            });
1742        let out = encode();
1743        (out, hashing.ok().map(|h| h.join().unwrap_or_default()))
1744    });
1745    let dirty =
1746        hashed.unwrap_or_else(|| hash_bands(bands, pixels, stride, height, threshold, duration));
1747    (out, dirty)
1748}
1749
1750/// Keep this thread to the CPUs it may run on that share `cpu`'s last-level cache.
1751///
1752/// A thread started per frame is placed on the least loaded CPU, on a host of several caches
1753/// often under another one: its read of a frame leaves the frame's lines there, and the X
1754/// server's next grab into that surface waits on every one of them (a 1280x720 grab took 2.0 ms
1755/// against 0.9 on a four-node EPYC, the frame published that much later), while the read
1756/// evicts whatever else runs there. Beside the encode, which has just read the same frame, it
1757/// costs neither. A CPU whose caches the kernel does not list, or a cache of no CPU this thread
1758/// may use, leaves it where it is.
1759fn share_cache_with(cpu: i32) {
1760    static CACHES: std::sync::Mutex<std::collections::BTreeMap<i32, Option<libc::cpu_set_t>>> =
1761        std::sync::Mutex::new(std::collections::BTreeMap::new());
1762    if cpu < 0 {
1763        return;
1764    }
1765    let Some(cache) = *CACHES
1766        .lock()
1767        .unwrap_or_else(|e| e.into_inner())
1768        .entry(cpu)
1769        .or_insert_with(|| last_cache_cpus(cpu))
1770    else {
1771        return;
1772    };
1773    let size = std::mem::size_of::<libc::cpu_set_t>();
1774    unsafe {
1775        let mut allowed: libc::cpu_set_t = std::mem::zeroed();
1776        if libc::sched_getaffinity(0, size, &mut allowed) != 0 {
1777            return;
1778        }
1779        let mut keep: libc::cpu_set_t = std::mem::zeroed();
1780        for c in 0..libc::CPU_SETSIZE as usize {
1781            if libc::CPU_ISSET(c, &cache) && libc::CPU_ISSET(c, &allowed) {
1782                libc::CPU_SET(c, &mut keep);
1783            }
1784        }
1785        if libc::CPU_COUNT(&keep) > 0 && !libc::CPU_EQUAL(&keep, &allowed) {
1786            libc::sched_setaffinity(0, size, &keep);
1787        }
1788    }
1789}
1790
1791/// The CPUs sharing `cpu`'s last-level cache, the highest level its sysfs cache entries list.
1792fn last_cache_cpus(cpu: i32) -> Option<libc::cpu_set_t> {
1793    let (_, list) = std::fs::read_dir(format!("/sys/devices/system/cpu/cpu{cpu}/cache"))
1794        .ok()?
1795        .flatten()
1796        .filter_map(|entry| {
1797            let dir = entry.path();
1798            let level: u32 = std::fs::read_to_string(dir.join("level"))
1799                .ok()?
1800                .trim()
1801                .parse()
1802                .ok()?;
1803            Some((
1804                level,
1805                std::fs::read_to_string(dir.join("shared_cpu_list")).ok()?,
1806            ))
1807        })
1808        .max_by_key(|(level, _)| *level)?;
1809    cpu_list(&list)
1810}
1811
1812/// A kernel CPU list ("0-3,32-35") as a set.
1813fn cpu_list(list: &str) -> Option<libc::cpu_set_t> {
1814    let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() };
1815    for part in list.trim().split(',').filter(|p| !p.is_empty()) {
1816        let (first, last) = part.split_once('-').unwrap_or((part, part));
1817        for c in first.parse::<usize>().ok()?..=last.parse::<usize>().ok()? {
1818            if c < libc::CPU_SETSIZE as usize {
1819                unsafe { libc::CPU_SET(c, &mut set) };
1820            }
1821        }
1822    }
1823    (unsafe { libc::CPU_COUNT(&set) } > 0).then_some(set)
1824}
1825
1826/// Everything the X11 host-ARGB path has to remember between frames.
1827///
1828/// Unlike the Wayland backend, X11 capture over XShm has no compositor to report every change, so
1829/// this context holds the state that stands in for that missing damage signal: the per-stripe
1830/// hashes and the persistent encoder session that let `process()` discover damage by comparing
1831/// content frame-to-frame. A source that does report every change (the shared framebuffer's
1832/// watch) is read through `process_reported` instead, as Wayland's compositor damage is. A
1833/// full-frame encoder runs through `decide_hw_fullframe`; the CPU path (striped JPEG, or software
1834/// H.264) runs through `encode_cpu` with `hash_damage=true`.
1835///
1836/// Recording fan-out (socket sink and MP4 recorder) is handled at the delivery layer; a
1837/// consumer needing a keyframe goes through [`X11Pipeline::request_idr`] like everyone else.
1838pub struct X11Pipeline {
1839    settings: CaptureSettings,
1840    stripes: Vec<StripeState>,
1841    /// The full-frame encoder, or `None` for the CPU path.
1842    hw: Option<FrameEncoder>,
1843    hw_state: StripeState,
1844    /// The content hashes of a full-frame session, one per band of `DAMAGE_BAND_ROWS` rows.
1845    bands: Vec<StripeState>,
1846    /// The bands of the Turbo frames hashed on the CPU before their encode where the session
1847    /// hashes its frames on the device (`FrameEncoder::hash_next_upload`): kept apart from
1848    /// `bands`, which holds the device's hashes, a different function of the same picture.
1849    checked: Vec<StripeState>,
1850    /// The bands the content hash found changed in the frame before this one, which a Turbo
1851    /// frame reads (`process`); empty until the first hash.
1852    turbo_dirty: Vec<bool>,
1853    /// Whether the next frame whose source reports every change (`process_reported`) counts as
1854    /// changed whole whatever it reports: the first this pipeline codes, or the first at a new
1855    /// size, which the source reports against a picture the pipeline never had.
1856    report_whole: bool,
1857    frame_counter: u16,
1858    pending_force_idr: bool,
1859    /// Consecutive hardware encode failures and whether this pipeline already spent its one
1860    /// rebuild; together they drive `recover_hw`.
1861    hw_error_streak: u32,
1862    hw_rebuilt: bool,
1863    /// The cleanup quality (`HwFrameDecision::cleanup_quality`) and key frame the last full frame
1864    /// was decided on, and the ones it was coded with.
1865    #[cfg(test)]
1866    hold: [(Option<u32>, bool); 2],
1867}
1868
1869impl X11Pipeline {
1870    /// Build the context, choosing the full-frame encoder for the X11 host-BGRA path through
1871    /// the shared ladder (`select_frame_encoder`): the hardware backend the encode node's driver
1872    /// selects, then the codec's software encoder, or the CPU path for JPEG and
1873    /// H.264. A codec no backend serves falls through to the video codecs the host does serve,
1874    /// JPEG last.
1875    pub fn new(mut settings: CaptureSettings) -> Self {
1876        let hw = encoders::select_frame_encoder(
1877            &mut settings,
1878            FrameSource::Host { rgba: false },
1879            None,
1880            "X11",
1881        );
1882        let pipeline = Self {
1883            settings,
1884            stripes: Vec::new(),
1885            hw,
1886            hw_state: StripeState::default(),
1887            bands: Vec::new(),
1888            checked: Vec::new(),
1889            turbo_dirty: Vec::new(),
1890            report_whole: true,
1891            frame_counter: 0,
1892            pending_force_idr: false,
1893            hw_error_streak: 0,
1894            hw_rebuilt: false,
1895            #[cfg(test)]
1896            hold: [(None, false); 2],
1897        };
1898        pipeline.record_stream();
1899        pipeline
1900    }
1901
1902    /// Put the session this pipeline settled on into the capture's report.
1903    fn record_stream(&self) {
1904        crate::report::stream(
1905            &self.settings,
1906            encoders::software::stripe_count(self.settings.height, self.settings.codec),
1907            self.hw
1908                .as_ref()
1909                .map(|enc| (enc.backend_name(), enc.is_hardware())),
1910            encoders::session_fullcolor(self.hw.as_ref(), &self.settings),
1911            encoders::session_full_range(self.hw.as_ref(), &self.settings),
1912        );
1913        crate::report::bit_depth(encoders::session_bit_depth(
1914            self.hw.as_ref(),
1915            &self.settings,
1916        ));
1917        crate::report::capped(encoders::session_capped(self.hw.as_ref(), &self.settings));
1918    }
1919
1920    /// React to a streak of hardware encode failures: rebuild the session once with the startup
1921    /// selection, and demote to the software encoder when a fresh session fails the same way.
1922    /// A session whose encodes keep failing still constructs, so the rebuild only counts as
1923    /// recovery until an encode succeeds; otherwise the stream would rebuild in a loop and never
1924    /// demote. Streaming nothing forever is not an option.
1925    ///
1926    /// The demote is the last rung: the software path has no encode-error streak of its own, so
1927    /// once the pipeline lands there it is never re-entered.
1928    fn recover_hw(&mut self) {
1929        self.hw_error_streak = 0;
1930        if self.hw_rebuilt {
1931            eprintln!(
1932                "[X11] HW encoder unrecoverable; demoting to software encoding ({}).",
1933                crate::encoders::software_library(self.settings.codec)
1934            );
1935            // The broken session is released before its replacement is built: these failures
1936            // are usually device memory pressure, and holding both at once is what would make
1937            // the replacement fail too.
1938            self.hw = None;
1939            self.settings.use_cpu = true;
1940            self.hw = encoders::select_frame_encoder(
1941                &mut self.settings,
1942                FrameSource::Host { rgba: false },
1943                None,
1944                "X11",
1945            );
1946            crate::report::encoder_reason(
1947                "the hardware encoder failed repeatedly and was given up",
1948            );
1949        } else {
1950            eprintln!("[X11] rebuilding HW encoder after repeated encode errors.");
1951            self.hw = None;
1952            self.hw = encoders::select_frame_encoder(
1953                &mut self.settings,
1954                FrameSource::Host { rgba: false },
1955                None,
1956                "X11",
1957            );
1958            self.hw_rebuilt = true;
1959        }
1960        self.record_stream();
1961        self.hw_state = StripeState::default();
1962        self.stripes.clear();
1963        self.pending_force_idr = true;
1964    }
1965
1966    /// Request an on-demand keyframe on the next processed frame.
1967    pub fn request_idr(&mut self) {
1968        self.pending_force_idr = true;
1969    }
1970
1971    /// Apply what the consumers say of a frame (`FrameEncoder::take_report`): a loss leaves it and
1972    /// every frame after it out of the predictions, a key frame where the encoder cannot.
1973    pub fn take_report(&mut self, report: crate::encoders::reference::ReferenceReport) {
1974        use crate::encoders::reference::ReferenceReport;
1975        self.hw_state.take_report(report);
1976        self.stripes
1977            .iter_mut()
1978            .for_each(|st| st.take_report(report));
1979        let taken = match (self.hw.as_mut(), report) {
1980            (Some(enc), report) => enc.take_report(report),
1981            (None, ReferenceReport::Lost(frame_id) | ReferenceReport::Withheld(frame_id)) => {
1982                invalidate_reference(&mut self.stripes, frame_id)
1983            }
1984            (None, ReferenceReport::Held(frame_id)) => {
1985                acknowledge_reference(&mut self.stripes, frame_id, true);
1986                true
1987            }
1988            (None, ReferenceReport::Sent(frame_id)) => {
1989                acknowledge_reference(&mut self.stripes, frame_id, false);
1990                true
1991            }
1992        };
1993        if !taken {
1994            self.pending_force_idr = true;
1995        }
1996    }
1997
1998    /// The encoder's name for the stream log: the hardware backend, the software library of a
1999    /// full-frame session, or `CPU` with the library of the CPU path.
2000    pub fn encoder_name(&self) -> String {
2001        match &self.hw {
2002            Some(enc) if enc.is_hardware() => enc.backend_name().to_string(),
2003            Some(enc) => format!("CPU ({})", enc.backend_name()),
2004            None if !self.settings.codec.is_video() => "CPU (turbojpeg)".to_string(),
2005            None => format!("CPU ({})", crate::encoders::software_library(Codec::H264)),
2006        }
2007    }
2008
2009    /// The codec the pipeline actually emits, after any demotion.
2010    pub fn codec(&self) -> Codec {
2011        self.settings.codec
2012    }
2013
2014    /// Whether the pipeline encodes on a GPU.
2015    pub fn is_hardware(&self) -> bool {
2016        self.hw.as_ref().is_some_and(|enc| enc.is_hardware())
2017    }
2018
2019    /// Whether the session runs its constant quality under the peak of its bitrate
2020    /// (`encoders::session_capped`), for this pipeline's stream log.
2021    pub fn capped(&self) -> bool {
2022        encoders::session_capped(self.hw.as_ref(), &self.settings)
2023    }
2024
2025    /// The encoder's one rate control where its backend has one alone
2026    /// (`FrameEncoder::fixed_rate_control`), for this pipeline's stream log.
2027    pub fn fixed_rate_control(&self) -> Option<&'static str> {
2028        self.hw.as_ref().and_then(FrameEncoder::fixed_rate_control)
2029    }
2030
2031    /// Whether the encoder holds a cleanup at the quantizer asked for
2032    /// (`FrameEncoder::holds_quantizer`, `software::stripes_hold_quantizer`), for this
2033    /// pipeline's stream log.
2034    pub fn holds_quantizer(&self) -> bool {
2035        self.hw.as_ref().map_or_else(
2036            || crate::encoders::software::stripes_hold_quantizer(&self.settings),
2037            FrameEncoder::holds_quantizer,
2038        )
2039    }
2040
2041    /// The `Colorspace:` field for this pipeline's stream log, describing what its encoder settled
2042    /// on: a hardware session only reaches 4:4:4 when the device carries it, the software path
2043    /// only when the build's encoder for the codec does.
2044    pub fn colorspace_desc(&self) -> &'static str {
2045        let fullcolor = encoders::session_fullcolor(self.hw.as_ref(), &self.settings);
2046        crate::encoders::colorspace_desc(
2047            fullcolor,
2048            encoders::session_full_range(self.hw.as_ref(), &self.settings),
2049        )
2050    }
2051
2052    /// Adapt the live pipeline to recreated capture surfaces without rebuilding it.
2053    ///
2054    /// # Arguments
2055    ///
2056    /// * `settings` - New geometry plus current live rates.
2057    /// * `size_changed` - Whether the capture dimensions changed.
2058    ///
2059    /// # Returns
2060    ///
2061    /// `true` if the pipeline was successfully adapted in place; `false` when the active encoder
2062    /// cannot follow (VAAPI on resize), or the CPU path stands where an engine
2063    /// may take the new size (one that refused the old size, past its maximum), and the caller
2064    /// must rebuild.
2065    pub fn reshape(&mut self, settings: &CaptureSettings, size_changed: bool) -> bool {
2066        // A demotion (`recover_hw`) holds for the pipeline's life.
2067        let demoted = self.settings.use_cpu;
2068        self.report_whole |= size_changed;
2069        if !size_changed {
2070            if let Some(FrameEncoder::Nvenc(enc)) = &mut self.hw {
2071                enc.release_pinned_hosts();
2072            }
2073            self.settings = settings.clone();
2074            self.settings.use_cpu |= demoted;
2075            return true;
2076        }
2077        match &mut self.hw {
2078            Some(FrameEncoder::Nvenc(enc)) => {
2079                if let Err(e) = enc.reconfigure_resolution(settings) {
2080                    eprintln!("[X11] NVENC in-place resize unavailable ({e}); rebuilding");
2081                    return false;
2082                }
2083            }
2084            None => {
2085                if self.settings.codec.is_video()
2086                    && self.settings.codec == settings.codec
2087                    && !demoted
2088                    && encoders::engine_may_serve(settings)
2089                {
2090                    return false;
2091                }
2092            }
2093            _ => return false,
2094        }
2095        let codec = self.settings.codec;
2096        self.settings = settings.clone();
2097        self.settings.codec = codec;
2098        self.settings.use_cpu |= demoted;
2099        self.stripes.clear();
2100        self.hw_state = StripeState::default();
2101        self.turbo_dirty.clear();
2102        true
2103    }
2104
2105    /// Apply a runtime rate-control / framerate change: the CBR target bitrate + VBV (kbps /
2106    /// kb; ignored unless CBR is active) and the target fps. NVENC and libvpx reconfigure their
2107    /// live session; SVT-AV1 takes a new bitrate live where the library can and re-opens
2108    /// otherwise, as x265 and kvazaar always do; a VA-API session takes a constant-rate target
2109    /// with the next picture and starts a new sequence for any other change; the CPU path
2110    /// picks the new values up on the next `process()` (encode_cpu reads the updated settings
2111    /// and reconfigures its encoder).
2112    pub fn update_rate(&mut self, bitrate_kbps: i32, vbv_multiplier: f64, fps: f64) {
2113        self.settings.video_bitrate_kbps = bitrate_kbps;
2114        self.settings.video_vbv_multiplier = vbv_multiplier;
2115        if fps > 0.0 {
2116            self.settings.target_fps = fps;
2117        }
2118        if let Some(enc) = self.hw.as_mut()
2119            && let Err(e) = enc.reconfigure_rate(&self.settings)
2120        {
2121            // The failed re-open left the session without a codec context, so it goes
2122            // through the rebuild-or-demote ladder instead of being encoded into.
2123            eprintln!("[X11] rate reconfigure failed: {e}");
2124            self.recover_hw();
2125        }
2126    }
2127
2128    /// Take the rate the path carries (`CaptureSettings::video_link_kbps`), which the next
2129    /// process() reads.
2130    pub fn set_link_rate(&mut self, kbps: i32) {
2131        self.settings.video_link_kbps = kbps;
2132    }
2133
2134    /// Apply live per-frame tunables (quality, paint-over, streaming mode, keyframe
2135    /// cadence); every encoder re-reads them from the settings on the next process().
2136    pub fn update_tunables(&mut self, t: &crate::LiveTunables) {
2137        t.apply_to(&mut self.settings);
2138    }
2139
2140    /// Encode one host-ARGB frame and return the encoded stripes.
2141    ///
2142    /// Damage is read from the content hash (`hash_bands`). Without Turbo the hash decides whether
2143    /// a frame (or a stripe) is sent, so it runs first. Turbo sends every video frame, and only
2144    /// its cleanup reads the damage, so there the frame's hash runs beside its encode
2145    /// (`hash_beside`) and the cleanup reads the frame before's (`turbo_dirty`; the CPU path
2146    /// as the rows it maps onto its regions): the hash, a millisecond and a half at 1080p and six
2147    /// at 4K, stays off the path to the client. A frame the cleanup codes at its own quality is
2148    /// hashed before its encode instead, so a change landing on it is coded at the session's.
2149    /// With the paint-over off nothing reads it, and Turbo hashes nothing.
2150    ///
2151    /// # Arguments
2152    ///
2153    /// * `argb` - Packed BGRA pixel buffer (B,G,R,A byte order, `stride` bytes per row).
2154    /// * `stride` - Bytes per row (must equal `width * 4` for the software path).
2155    ///
2156    /// # Returns
2157    ///
2158    /// Vec of [`EncodedStripe`] — empty when nothing changed.
2159    pub fn process(&mut self, argb: &[u8], stride: usize) -> Vec<EncodedStripe> {
2160        self.process_damaged(argb, stride, &[])
2161    }
2162
2163    /// [`Self::process`] for a frame whose source says where it changed (`changed`, in the
2164    /// frame's coordinates), which places the change within each region for its block ledger
2165    /// (`ledger`); the content hash still decides which regions changed, and one it finds
2166    /// changed that no rectangle reaches counts as changed whole.
2167    pub fn process_damaged(
2168        &mut self,
2169        argb: &[u8],
2170        stride: usize,
2171        changed: &[Rectangle<i32, Physical>],
2172    ) -> Vec<EncodedStripe> {
2173        self.process_frame(argb, stride, None, changed, false)
2174    }
2175
2176    /// [`Self::process`] for a frame whose source reports every change it made (`changed`):
2177    /// the rectangles decide what changed, as a compositor's damage does on Wayland, and no
2178    /// content hash is taken. Hashing the frame only to learn what the source already said cost
2179    /// 0.84 ms of every 2560x1600 frame on the capture's critical path.
2180    pub fn process_reported(
2181        &mut self,
2182        argb: &[u8],
2183        stride: usize,
2184        changed: &[Rectangle<i32, Physical>],
2185    ) -> Vec<EncodedStripe> {
2186        self.process_frame(argb, stride, None, changed, true)
2187    }
2188
2189    /// [`Self::process_reported`] for a frame its capture already converted to the picture the
2190    /// session encodes (`planes`, where [`Self::takes_host_planes`]); `argb` is left unread. The
2191    /// session reads the picture through the next frame's encode ([`Self::held_input`]).
2192    pub fn process_reported_planes(
2193        &mut self,
2194        argb: &[u8],
2195        stride: usize,
2196        planes: &Nv12Planes,
2197        changed: &[Rectangle<i32, Physical>],
2198    ) -> Vec<EncodedStripe> {
2199        self.process_frame(argb, stride, Some(planes), changed, true)
2200    }
2201
2202    /// The picture the session still reads after the last frame's encode returned, by its
2203    /// [`Nv12Planes::id`], which its owner leaves unchanged until a later encode stops naming it
2204    /// or [`Self::release_input`].
2205    pub fn held_input(&self) -> Option<*const u8> {
2206        held_input(&self.stripes)
2207    }
2208
2209    /// Stop the session reading the picture it holds ([`Self::held_input`]).
2210    pub fn release_input(&mut self) {
2211        release_input(&mut self.stripes);
2212    }
2213
2214    /// Whether this pipeline encodes 8-bit 4:2:0 pictures a capture converts itself, as NV12
2215    /// laid out for x264 (`Nv12Planes`): the x264 session of the CPU path.
2216    pub fn takes_host_planes(&self) -> bool {
2217        cfg!(feature = "gpl")
2218            && self.hw.is_none()
2219            && self.settings.codec == Codec::H264
2220            && !self.settings.video_fullcolor
2221            && encoders::software::h264_bit_depth(&self.settings) == 8
2222    }
2223
2224    /// [`Self::process_damaged`], or [`Self::process_reported`] where `reported`, from `planes`
2225    /// where the capture converted the frame itself.
2226    fn process_frame(
2227        &mut self,
2228        argb: &[u8],
2229        stride: usize,
2230        planes: Option<&Nv12Planes>,
2231        changed: &[Rectangle<i32, Physical>],
2232        reported: bool,
2233    ) -> Vec<EncodedStripe> {
2234        let width = self.settings.width;
2235        let height = self.settings.height;
2236        let whole = [Rectangle::new((0, 0).into(), (width, height).into())];
2237        let changed = if reported && std::mem::take(&mut self.report_whole) {
2238            &whole[..]
2239        } else {
2240            changed
2241        };
2242        let requested = self.pending_force_idr;
2243        let threshold = self.settings.damage_block_threshold;
2244        let duration = self.settings.damage_block_duration as i32;
2245
2246        let out = if self.hw.is_some() {
2247            let turbo = self.settings.video_streaming_mode;
2248            let damage = if reported {
2249                Damage::of_rects(changed, width, height)
2250            } else if turbo {
2251                band_damage(&self.turbo_dirty)
2252            } else {
2253                hash_damage(
2254                    &mut self.bands,
2255                    argb,
2256                    stride,
2257                    height as usize,
2258                    threshold,
2259                    duration,
2260                )
2261            };
2262            if self
2263                .hw
2264                .as_mut()
2265                .is_some_and(FrameEncoder::take_lost_cleanup)
2266            {
2267                self.hw_state.lost_frame();
2268            }
2269            let quality = EncoderQuality::of(self.hw.as_ref().unwrap());
2270            let mut d = decide_hw_fullframe(
2271                &mut self.hw_state,
2272                &self.settings,
2273                self.frame_counter,
2274                damage,
2275                false,
2276                requested,
2277                quality,
2278            );
2279            // A Turbo frame is decided on the hash of the one before, so a frame coded whole at
2280            // a cleanup's quality, or capped and coded again at the coarsest, could carry a change
2281            // it was not decided for, coded at that quality. Such a frame is hashed before it is
2282            // encoded instead of beside it, and one that moved is coded as motion is: at the
2283            // session's quality, uncapped, and a key frame only where one is due. A held band
2284            // needs none of this: the rest of its frame is held at the coarsest quantizer.
2285            let normal = self.settings.video_crf as u32;
2286            #[cfg(test)]
2287            let decided = (d.cleanup_quality(normal), d.force_idr);
2288            let mut hashed = None;
2289            let on_device = turbo
2290                && !reported
2291                && self.settings.cleans_up()
2292                && d.send
2293                && self
2294                    .hw
2295                    .as_mut()
2296                    .unwrap()
2297                    .hash_next_upload(DAMAGE_BAND_ROWS as u32);
2298            if turbo
2299                && !reported
2300                && self.settings.cleans_up()
2301                && d.send
2302                && (d.cleanup_quality(normal).is_some() || d.cap.is_some() && d.hold_band.is_none())
2303            {
2304                let dirty = hash_bands(
2305                    if on_device {
2306                        &mut self.checked
2307                    } else {
2308                        &mut self.bands
2309                    },
2310                    argb,
2311                    stride,
2312                    height as usize,
2313                    threshold,
2314                    duration,
2315                );
2316                if band_damage(&dirty).is_motion() {
2317                    d.hold_qp = None;
2318                    d.cap = None;
2319                    d.target_qp = normal;
2320                    d.force_idr = requested
2321                        || self.hw_state.scheduled_key(
2322                            periodic_idr_due(&self.settings, self.frame_counter),
2323                            true,
2324                        );
2325                }
2326                hashed = Some(dirty);
2327            }
2328            #[cfg(test)]
2329            {
2330                self.hold = [decided, (d.cleanup_quality(normal), d.force_idr)];
2331            }
2332            self.hw_state.hash_changed = self
2333                .bands
2334                .iter()
2335                .chain(&self.checked)
2336                .any(|band| band.hash_changed);
2337            // Whether a rate binds the frame, read once before it is coded: the encode may bind
2338            // or release the session, and the frame was coded as it stood before.
2339            let bound = self
2340                .hw
2341                .as_ref()
2342                .unwrap()
2343                .rate_bound(self.settings.video_cbr_mode);
2344            {
2345                let enc = self.hw.as_ref().unwrap();
2346                plan_refinement(
2347                    &mut self.hw_state,
2348                    &self.settings,
2349                    &mut d,
2350                    damage,
2351                    Some(changed),
2352                    enc.refines(),
2353                    enc.last_quality(),
2354                    bound,
2355                );
2356            }
2357            let fc = u64::from(self.frame_counter);
2358            if !d.send && !self.hw.as_ref().unwrap().holds_frame() {
2359                self.hw.as_mut().unwrap().idle();
2360            }
2361            if d.send || self.hw.as_ref().unwrap().holds_frame() {
2362                let force_idr = d.force_idr;
2363                let enc = self.hw.as_mut().unwrap();
2364                d.prepare(enc);
2365                // A Turbo paint-over reads the bands the frame before changed: hashed on the
2366                // device where NVENC uploads the frame, beside its encode, else on a thread
2367                // beside it (`hash_beside`).
2368                let beside = hashed.is_none() && turbo && !reported && self.settings.cleans_up();
2369                let mut encode = || {
2370                    if d.send {
2371                        enc.encode_host(argb, stride, false, fc, d.target_qp, force_idr)
2372                    } else {
2373                        enc.push_held(fc)
2374                    }
2375                };
2376                let res = if on_device {
2377                    let res = encode();
2378                    self.turbo_dirty = match (enc.take_upload_hashes(), hashed) {
2379                        (Some(hashes), _) => {
2380                            hashed_bands(&mut self.bands, &hashes, threshold, duration)
2381                        }
2382                        (None, Some(dirty)) => dirty,
2383                        (None, None) => hash_bands(
2384                            &mut self.bands,
2385                            argb,
2386                            stride,
2387                            height as usize,
2388                            threshold,
2389                            duration,
2390                        ),
2391                    };
2392                    res
2393                } else if let Some(dirty) = hashed {
2394                    self.turbo_dirty = dirty;
2395                    encode()
2396                } else if beside {
2397                    let (res, dirty) = hash_beside(
2398                        &mut self.bands,
2399                        argb,
2400                        stride,
2401                        height as usize,
2402                        threshold,
2403                        duration,
2404                        encode,
2405                    );
2406                    self.turbo_dirty = dirty;
2407                    res
2408                } else {
2409                    self.turbo_dirty.clear();
2410                    encode()
2411                };
2412                let bytes = res.as_ref().map_or(0, Vec::len);
2413                refinement_coded(
2414                    &mut self.hw_state,
2415                    &self.settings,
2416                    &d,
2417                    EncoderQuality::of(enc),
2418                    bytes,
2419                    bound,
2420                    enc.coded_capped(),
2421                );
2422                match res {
2423                    Ok(data) if !data.is_empty() => {
2424                        self.hw_error_streak = 0;
2425                        self.hw_rebuilt = false;
2426                        let codec = self.settings.codec;
2427                        enc.delivered_units(data, self.frame_counter)
2428                            .into_iter()
2429                            .map(|(data, id, reference)| EncodedStripe {
2430                                data: Arc::new(data),
2431                                codec,
2432                                stripe_y_start: 0,
2433                                stripe_height: height,
2434                                frame_id: i32::from(id),
2435                                timing: Default::default(),
2436                                reference,
2437                            })
2438                            .collect()
2439                    }
2440                    Ok(_) => {
2441                        self.hw_error_streak = 0;
2442                        self.hw_rebuilt = false;
2443                        Vec::new()
2444                    }
2445                    Err(e) => {
2446                        // One line per recovery window: a session failing at frame rate would
2447                        // otherwise write a line per frame for the life of the capture.
2448                        if self
2449                            .hw_error_streak
2450                            .is_multiple_of(crate::HW_ERROR_RECOVERY_THRESHOLD)
2451                        {
2452                            eprintln!("[X11] HW encode error: {e}");
2453                        }
2454                        self.hw_error_streak = self.hw_error_streak.saturating_add(1);
2455                        if self.hw_error_streak >= crate::HW_ERROR_RECOVERY_THRESHOLD {
2456                            self.recover_hw();
2457                        }
2458                        Vec::new()
2459                    }
2460                }
2461            } else {
2462                Vec::new()
2463            }
2464        } else {
2465            debug_assert_eq!(
2466                stride,
2467                width as usize * 4,
2468                "software encode path assumes tightly-packed rows (stride == width*4)"
2469            );
2470            if self.settings.codec.is_video() && self.settings.video_streaming_mode && !reported {
2471                // As a full frame above: a frame that codes a stripe at the paint-over quality
2472                // while it holds still is hashed before it is encoded, and decided on its own
2473                // change as well as the one before. Only that frame's rectangles are its own
2474                // change, outside which the encoder may code blocks as constant.
2475                let hashed = (self.settings.cleans_up()
2476                    && stripes_held_still(&self.stripes, &self.settings))
2477                .then(|| {
2478                    hash_bands(
2479                        &mut self.bands,
2480                        argb,
2481                        stride,
2482                        height as usize,
2483                        threshold,
2484                        duration,
2485                    )
2486                });
2487                let rects = match &hashed {
2488                    Some(dirty) => {
2489                        let seen: Vec<bool> = dirty
2490                            .iter()
2491                            .enumerate()
2492                            .map(|(i, &d)| d || self.turbo_dirty.get(i).is_none_or(|&b| b))
2493                            .collect();
2494                        band_rects(&seen, width, height)
2495                    }
2496                    None => band_rects(&self.turbo_dirty, width, height),
2497                };
2498                let (stripes, settings) = (&mut self.stripes, &self.settings);
2499                let current = hashed.is_some();
2500                let mut encode = || {
2501                    encode_cpu_from(
2502                        stripes,
2503                        argb,
2504                        None,
2505                        width,
2506                        height,
2507                        &rects,
2508                        current,
2509                        settings,
2510                        self.frame_counter,
2511                        false,
2512                        false,
2513                        requested,
2514                    )
2515                };
2516                if let Some(dirty) = hashed {
2517                    self.turbo_dirty = dirty;
2518                    encode()
2519                } else if self.settings.cleans_up() {
2520                    let (out, dirty) = hash_beside(
2521                        &mut self.bands,
2522                        argb,
2523                        stride,
2524                        height as usize,
2525                        threshold,
2526                        duration,
2527                        encode,
2528                    );
2529                    self.turbo_dirty = dirty;
2530                    out
2531                } else {
2532                    self.turbo_dirty.clear();
2533                    encode()
2534                }
2535            } else {
2536                encode_cpu_from(
2537                    &mut self.stripes,
2538                    argb,
2539                    planes,
2540                    width,
2541                    height,
2542                    changed,
2543                    true,
2544                    &self.settings,
2545                    self.frame_counter,
2546                    false,
2547                    !reported,
2548                    requested,
2549                )
2550            }
2551        };
2552
2553        // An unserved request stays armed: on an infinite GOP an IDR lost to an encode
2554        // error or skip would never self-heal, leaving a joining consumer with an
2555        // undecodable stream. A rebuilt or demoted encoder arms one the same way, which
2556        // is what the second read of the flag picks up.
2557        self.pending_force_idr = (requested || self.pending_force_idr) && out.is_empty();
2558        self.frame_counter = self.frame_counter.wrapping_add(1);
2559        out
2560    }
2561}
2562
2563#[cfg(test)]
2564mod tests {
2565    use super::*;
2566
2567    /// An encoder that holds a quantizer and reports no quality of its own.
2568    const HOLDS: EncoderQuality = EncoderQuality {
2569        last: None,
2570        bytes: None,
2571        holds: true,
2572        reopens: false,
2573        keys: true,
2574        band: None,
2575        measures: false,
2576        psnr: None,
2577        refit: None,
2578        follows_cap: false,
2579        finer: 0,
2580        ends_on_key: false,
2581    };
2582
2583    /// A settings block for the hardware full-frame policy: a constant rate, whose cleanup holds
2584    /// its frames (`HOLDS` reports no quality of its own, so the cleanup always improves),
2585    /// paint-over on and clearly finer than normal, a short burst, a short trigger, and no
2586    /// scheduled IDR. `crf_settings` is the constant-quality one.
2587    fn hw_settings() -> CaptureSettings {
2588        CaptureSettings {
2589            codec: Codec::H264,
2590            video_cbr_mode: true,
2591            video_crf: 25,
2592            video_paintover_crf: 10,
2593            video_paintover_burst_frames: 3,
2594            paint_over_trigger_frames: 2,
2595            use_paint_over_quality: true,
2596            video_streaming_mode: false,
2597            keyframe_interval_s: 0.0,
2598            target_fps: 60.0,
2599            ..Default::default()
2600        }
2601    }
2602
2603    /// `hw_settings` at a constant quality.
2604    fn crf_settings() -> CaptureSettings {
2605        CaptureSettings {
2606            video_cbr_mode: false,
2607            ..hw_settings()
2608        }
2609    }
2610
2611    /// Drive the policy through `frames` frames of `damage`, returning the decisions.
2612    fn run(
2613        st: &mut StripeState,
2614        s: &CaptureSettings,
2615        damage: Damage,
2616        frames: usize,
2617    ) -> Vec<HwFrameDecision> {
2618        (0..frames)
2619            .map(|i| decide_hw_fullframe(st, s, i as u16 + 1, damage, false, false, HOLDS))
2620            .collect()
2621    }
2622
2623    /// Once motion stops a constant-rate session's screen is cleaned up whether Turbo sends every
2624    /// frame or not: a refresh held at the paint-over quantizer after the trigger, a key frame
2625    /// held there once it has held still four trigger periods, each followed by the recovery
2626    /// burst, then silence (or Turbo's frames at the session quality).
2627    #[test]
2628    fn a_kernel_cpu_list_reads_as_its_cpus() {
2629        let set = cpu_list("0-3,32-35\n").unwrap();
2630        let cpus: Vec<usize> = (0..64)
2631            .filter(|&c| unsafe { libc::CPU_ISSET(c, &set) })
2632            .collect();
2633        assert_eq!(cpus, [0, 1, 2, 3, 32, 33, 34, 35]);
2634        assert!(cpu_list("").is_none());
2635        assert!(cpu_list("x").is_none());
2636    }
2637
2638    #[test]
2639    fn the_hash_thread_keeps_to_cpus_it_was_allowed() {
2640        let size = std::mem::size_of::<libc::cpu_set_t>();
2641        let cpu = unsafe { libc::sched_getcpu() };
2642        let (before, after) = std::thread::spawn(move || unsafe {
2643            let mut before: libc::cpu_set_t = std::mem::zeroed();
2644            libc::sched_getaffinity(0, size, &mut before);
2645            share_cache_with(cpu);
2646            let mut after: libc::cpu_set_t = std::mem::zeroed();
2647            libc::sched_getaffinity(0, size, &mut after);
2648            (before, after)
2649        })
2650        .join()
2651        .unwrap();
2652        let within = |set: &libc::cpu_set_t, of: &libc::cpu_set_t| {
2653            (0..libc::CPU_SETSIZE as usize)
2654                .all(|c| unsafe { !libc::CPU_ISSET(c, set) || libc::CPU_ISSET(c, of) })
2655        };
2656        assert!(unsafe { libc::CPU_COUNT(&after) } > 0 && within(&after, &before));
2657        if let Some(cache) = last_cache_cpus(cpu) {
2658            assert!(within(&after, &cache) || unsafe { libc::CPU_EQUAL(&after, &before) });
2659        }
2660    }
2661
2662    #[test]
2663    fn a_still_screen_is_cleaned_up_whatever_turbo_sends() {
2664        for turbo in [false, true] {
2665            let s = CaptureSettings {
2666                video_streaming_mode: turbo,
2667                ..hw_settings()
2668            };
2669            let paint = Some(s.video_paintover_crf as u32);
2670            let mut st = StripeState::default();
2671            let moving = run(&mut st, &s, Damage::Area(1.0), 3);
2672            assert!(
2673                moving
2674                    .iter()
2675                    .all(|d| d.send && !d.force_idr && d.hold_qp.is_none()),
2676                "motion sends at the session quality"
2677            );
2678            let still = run(&mut st, &s, Damage::None, 16);
2679            let sent: Vec<(usize, bool, Option<u32>)> = still
2680                .iter()
2681                .enumerate()
2682                .filter(|(_, d)| d.send)
2683                .map(|(i, d)| (i + 1, d.force_idr, d.hold_qp))
2684                .collect();
2685            let cleanups = vec![
2686                (2, false, paint),
2687                (3, false, paint),
2688                (4, false, paint),
2689                (5, false, paint),
2690                (8, true, paint),
2691                (9, false, paint),
2692                (10, false, paint),
2693                (11, false, paint),
2694            ];
2695            if !turbo {
2696                assert_eq!(
2697                    sent, cleanups,
2698                    "a refresh at the trigger and a key four triggers in, each with its burst held"
2699                );
2700            } else {
2701                assert_eq!(sent.len(), 16, "Turbo sends every frame");
2702                let held: Vec<(usize, bool, Option<u32>)> =
2703                    sent.into_iter().filter(|r| r.2.is_some()).collect();
2704                assert_eq!(held, cleanups, "the same cleanups under Turbo");
2705            }
2706            assert!(
2707                still.iter().all(|d| d.target_qp == s.video_crf as u32),
2708                "the session's own quality is left alone"
2709            );
2710        }
2711    }
2712
2713    /// Under a constant rate the refresh and its burst are held at what the budget allows below the
2714    /// rate control's last quantizer, the key frame at the paint-over one (the encoder caps it to
2715    /// its budget), and the burst behind the key frame at the refresh's again, so a key frame the
2716    /// cap coarsened is refined back.
2717    #[test]
2718    fn a_constant_rate_cleanup_keeps_to_its_budget() {
2719        let s = CaptureSettings {
2720            video_cbr_mode: true,
2721            ..hw_settings()
2722        };
2723        let coarse = EncoderQuality {
2724            last: Some(40),
2725            bytes: None,
2726            holds: true,
2727            reopens: false,
2728            keys: true,
2729            band: None,
2730            measures: false,
2731            psnr: None,
2732            refit: None,
2733            follows_cap: false,
2734            finer: 0,
2735            ends_on_key: false,
2736        };
2737        let mut st = StripeState::default();
2738        for i in 0..3u16 {
2739            decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, coarse);
2740        }
2741        let still: Vec<HwFrameDecision> = (0..12u16)
2742            .map(|i| decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, coarse))
2743            .collect();
2744        let refresh_qp = held_refresh_quality(&s, coarse);
2745        assert_eq!(
2746            refresh_qp, 10,
2747            "35 steps under the rate control's 40 is past the paint-over quality"
2748        );
2749        assert_eq!(
2750            still[1].hold_qp,
2751            Some(refresh_qp),
2752            "the refresh at the budget's quality"
2753        );
2754        assert!(
2755            (2..5).all(|i| still[i].hold_qp == Some(refresh_qp)),
2756            "its burst at the same"
2757        );
2758        assert!(
2759            still[7].force_idr && still[7].hold_qp == Some(s.video_paintover_crf as u32),
2760            "the key at the paint-over one"
2761        );
2762        assert!(
2763            (8..11).all(|i| still[i].send && still[i].hold_qp == Some(refresh_qp)),
2764            "its burst at the refresh's"
2765        );
2766    }
2767
2768    /// A small change is cleaned up by a refresh alone; one covering half a screen since the last
2769    /// key frame earns a key frame once the screen holds still long enough.
2770    #[test]
2771    fn a_small_change_is_refreshed_and_a_large_one_keyed() {
2772        let s = CaptureSettings {
2773            video_paintover_burst_frames: 0,
2774            ..hw_settings()
2775        };
2776        let keys = |st: &mut StripeState, damage: Damage, frames: usize| -> (usize, usize) {
2777            run(st, &s, damage, frames);
2778            let still = run(st, &s, Damage::None, 12);
2779            (
2780                still
2781                    .iter()
2782                    .filter(|d| d.hold_qp.is_some() && !d.force_idr)
2783                    .count(),
2784                still.iter().filter(|d| d.force_idr).count(),
2785            )
2786        };
2787        let mut st = StripeState::default();
2788        assert_eq!(
2789            keys(&mut st, Damage::Area(1.0), 2),
2790            (1, 1),
2791            "a scroll is refreshed, then keyed"
2792        );
2793        assert_eq!(
2794            keys(&mut st, Damage::Area(0.001), 1),
2795            (1, 0),
2796            "a caret is refreshed, never keyed"
2797        );
2798        assert_eq!(
2799            keys(&mut st, Damage::Area(0.3), 2),
2800            (1, 1),
2801            "0.6 of a screen changed since the last key frame"
2802        );
2803    }
2804
2805    /// An encoder whose held refresh restores the picture whole (libvpx's VP8) is never keyed:
2806    /// after a large change and a long stillness its refresh and burst are the whole cleanup,
2807    /// where one that holds only key frames (SVT-AV1) ends on a key frame.
2808    #[test]
2809    fn a_refresh_that_restores_the_picture_is_not_followed_by_a_key_frame() {
2810        let s = CaptureSettings {
2811            video_paintover_burst_frames: 0,
2812            ..hw_settings()
2813        };
2814        for (encoder, keyed) in [
2815            (
2816                EncoderQuality {
2817                    keys: false,
2818                    ..HOLDS
2819                },
2820                0,
2821            ),
2822            (HOLDS, 1),
2823        ] {
2824            let mut st = StripeState::default();
2825            for i in 0..2u16 {
2826                decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, encoder);
2827            }
2828            let still: Vec<HwFrameDecision> = (0..40u16)
2829                .map(|i| {
2830                    decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, encoder)
2831                })
2832                .collect();
2833            assert_eq!(
2834                still
2835                    .iter()
2836                    .filter(|d| d.hold_qp.is_some() && !d.force_idr)
2837                    .count(),
2838                1,
2839                "keys {}: one refresh",
2840                encoder.keys
2841            );
2842            assert_eq!(
2843                still.iter().filter(|d| d.force_idr).count(),
2844                keyed,
2845                "keys {}: the key frames",
2846                encoder.keys
2847            );
2848        }
2849    }
2850
2851    /// The trigger is counted at 60 fps: a slower session waits as long for its cleanup, not as
2852    /// many frames.
2853    #[test]
2854    fn a_slower_session_waits_as_long_for_its_cleanup() {
2855        for (fps, frames) in [(120.0, 15), (60.0, 15), (30.0, 8), (8.0, 2), (1.0, 1)] {
2856            let s = CaptureSettings {
2857                paint_over_trigger_frames: 15,
2858                target_fps: fps,
2859                ..hw_settings()
2860            };
2861            assert_eq!(s.paint_over_trigger(), frames, "{fps} fps");
2862            let mut st = StripeState::default();
2863            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, HOLDS);
2864            let first = (1..40u16)
2865                .position(|i| {
2866                    decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, HOLDS).send
2867                })
2868                .expect("a cleanup");
2869            assert_eq!(first + 1, frames as usize, "{fps} fps: due at the trigger");
2870        }
2871    }
2872
2873    /// A cleanup through the rate control starts on the first still frame after motion, its frames
2874    /// the rate control's own within its budget, where a held one, and one after a change as small
2875    /// as a caret's, still waits for the trigger.
2876    #[test]
2877    fn a_cleanup_through_the_rate_control_starts_on_the_first_still_frame() {
2878        let s = CaptureSettings {
2879            paint_over_trigger_frames: 15,
2880            ..hw_settings()
2881        };
2882        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2883        let rate = EncoderQuality {
2884            last: Some(40),
2885            bytes: Some(budget),
2886            ..HOLDS
2887        };
2888        for (change, encoder, first) in [(1.0, rate, 1), (1.0, HOLDS, 15), (0.01, rate, 15)] {
2889            let mut st = StripeState::default();
2890            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(change), false, false, encoder);
2891            let sent = (1..40u16)
2892                .position(|i| {
2893                    decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, encoder).send
2894                })
2895                .expect("a cleanup");
2896            assert_eq!(sent + 1, first, "{change} {encoder:?}");
2897        }
2898    }
2899
2900    /// A consumer that lost frames after a cleanup gets the cleanup again on the next still frame:
2901    /// the frame predicting past its losses carries the stream's own quality, and without this the
2902    /// picture would stay as the motion left it until the screen next changes.
2903    #[test]
2904    fn a_cleanup_a_consumer_lost_is_coded_again() {
2905        let s = hw_settings();
2906        let still = |st: &mut StripeState, from: u16| -> Vec<bool> {
2907            (from..from + 300)
2908                .map(|i| decide_hw_fullframe(st, &s, i, Damage::None, false, false, HOLDS).send)
2909                .collect()
2910        };
2911        let mut st = StripeState::default();
2912        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, HOLDS);
2913        let first = still(&mut st, 1);
2914        let cleaned = first.iter().filter(|&&sent| sent).count();
2915        assert!(cleaned > 0, "a cleanup");
2916        assert!(
2917            first[240..].iter().all(|&sent| !sent),
2918            "then a quiet still screen"
2919        );
2920        // A whole screen changed, so the cleanup ended on a key frame after its refresh.
2921        let keyed = st.cleanup_keyed;
2922        assert!(keyed, "the last cleanup a key frame");
2923        st.lost_frame();
2924        st.lost_frame();
2925        let again = decide_hw_fullframe(&mut st, &s, 301, Damage::None, false, false, HOLDS);
2926        assert!(
2927            again.send && again.hold_qp.is_some(),
2928            "the lost cleanup is coded again: {} {:?}",
2929            again.send,
2930            again.hold_qp
2931        );
2932        assert_eq!(again.force_idr, keyed, "and as the kind it was");
2933        let rest = still(&mut st, 302);
2934        assert!(
2935            rest[240..].iter().all(|&sent| !sent),
2936            "and the screen is quiet again"
2937        );
2938        assert!(
2939            rest.iter().filter(|&&sent| sent).count() < cleaned,
2940            "two losses re-arm one cleanup, no larger than the first"
2941        );
2942        // A loss while the region still changes owes nothing yet: its cleanup is still to come.
2943        let mut moving = StripeState::default();
2944        decide_hw_fullframe(&mut moving, &s, 0, Damage::Area(1.0), false, false, HOLDS);
2945        moving.lost_frame();
2946        assert!(!moving.paint_over_sent);
2947    }
2948
2949    /// A still screen the rate control codes finer than the paint-over quantizer owes no cleanup,
2950    /// so its still frames are no low motion: the first frames of the next large change (a
2951    /// window opening) go out at the session quality, and the key frame waits for the screen to
2952    /// hold still after it.
2953    #[test]
2954    fn a_constant_rate_cleanup_runs_through_a_rate_control_that_converges() {
2955        let s = hw_settings();
2956        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2957        let trigger = s.paint_over_trigger_frames as u16;
2958        let refining = |i: u16| EncoderQuality {
2959            last: Some(40 - u32::from(i / trigger)),
2960            bytes: Some(budget),
2961            holds: true,
2962            reopens: false,
2963            keys: true,
2964            band: None,
2965            measures: false,
2966            psnr: None,
2967            refit: None,
2968            follows_cap: false,
2969            finer: 0,
2970            ends_on_key: false,
2971        };
2972        let converged = EncoderQuality {
2973            last: Some(8),
2974            bytes: Some(budget / 8),
2975            holds: true,
2976            reopens: false,
2977            keys: true,
2978            band: None,
2979            measures: false,
2980            psnr: None,
2981            refit: None,
2982            follows_cap: false,
2983            finer: 0,
2984            ends_on_key: false,
2985        };
2986        let window = 1 + 6 * trigger;
2987        let mut st = StripeState::default();
2988        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, refining(0));
2989        let still: Vec<HwFrameDecision> = (1..window)
2990            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, refining(i)))
2991            .collect();
2992        assert!(
2993            still.iter().all(|d| d.hold_qp.is_none() && !d.force_idr),
2994            "no frame held, no key frame"
2995        );
2996        let first = still.iter().position(|d| d.send).expect("a cleanup");
2997        assert_eq!(first, 0, "due on the first still frame");
2998        assert!(
2999            still[first..].iter().all(|d| d.send),
3000            "the frames keep flowing while it refines"
3001        );
3002        let d = decide_hw_fullframe(&mut st, &s, window, Damage::None, false, false, converged);
3003        assert!(
3004            !d.send,
3005            "and stop once it codes a small frame at the paint-over quality"
3006        );
3007        assert!((window + 1..window + 100).all(|i| {
3008            !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, converged).send
3009        }));
3010    }
3011
3012    /// A rate control that cannot refine a still screen (`EncoderQuality::ends_on_key`) runs
3013    /// its cleanup until a large change has held still for `KEY_STILL_TRIGGERS` periods, then
3014    /// ends it on a key frame held at the paint-over quantizer, and nothing follows it; one that
3015    /// can never keys.
3016    #[test]
3017    fn a_rate_control_that_cannot_refine_ends_its_cleanup_on_a_key_frame() {
3018        let s = hw_settings();
3019        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3020        let paint = s.video_paintover_crf as u32;
3021        let encoder = |ends_on_key: bool| EncoderQuality {
3022            last: None,
3023            bytes: Some(budget / 2),
3024            holds: false,
3025            reopens: false,
3026            keys: true,
3027            band: None,
3028            measures: false,
3029            psnr: None,
3030            refit: None,
3031            follows_cap: true,
3032            finer: 0,
3033            ends_on_key,
3034        };
3035        let run = |ends_on_key: bool| {
3036            let mut st = StripeState::default();
3037            decide_hw_fullframe(
3038                &mut st,
3039                &s,
3040                0,
3041                Damage::Area(1.0),
3042                false,
3043                false,
3044                encoder(true),
3045            );
3046            (1..400u16)
3047                .map(|i| {
3048                    decide_hw_fullframe(
3049                        &mut st,
3050                        &s,
3051                        i,
3052                        Damage::None,
3053                        false,
3054                        false,
3055                        encoder(ends_on_key),
3056                    )
3057                })
3058                .collect::<Vec<_>>()
3059        };
3060        let keyed = run(true);
3061        let key = keyed.iter().position(|d| d.force_idr).expect("a key frame");
3062        let trigger = s.paint_over_trigger() as usize;
3063        assert!(
3064            key + 1 >= KEY_STILL_TRIGGERS as usize * trigger,
3065            "after the stillness ({key})"
3066        );
3067        assert_eq!(keyed[key].hold_qp, Some(paint));
3068        assert!(keyed[..key].iter().all(|d| d.send && d.hold_qp.is_none()));
3069        assert!(keyed[key + 1..].iter().all(|d| !d.send), "nothing after it");
3070        assert!(run(false).iter().all(|d| !d.force_idr));
3071    }
3072
3073    /// A rate control whose frames need a quantizer finer than the paint-over one to come to its
3074    /// quality (`EncoderQuality::finer`, x265's and kvazaar's) is refined past it: a trigger
3075    /// period of frames at the paint-over quantizer goes on, one at as much finer ends the
3076    /// cleanup, and a still screen coded between the two still owes one.
3077    #[test]
3078    fn a_rate_control_that_needs_a_finer_quantizer_is_refined_past_the_paint_over_one() {
3079        let s = hw_settings();
3080        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3081        let at = |last: u32| EncoderQuality {
3082            last: Some(last),
3083            bytes: Some(budget),
3084            holds: false,
3085            reopens: false,
3086            keys: true,
3087            band: None,
3088            measures: false,
3089            psnr: None,
3090            refit: None,
3091            follows_cap: false,
3092            finer: 6,
3093            ends_on_key: false,
3094        };
3095        let trigger = s.paint_over_trigger_frames as u16;
3096        let paint = s.video_paintover_crf as u32;
3097        let mut st = StripeState::default();
3098        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(30));
3099        for i in 1..4 * trigger {
3100            decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(30));
3101        }
3102        let start = 4 * trigger;
3103        assert!(
3104            (start..start + 4 * trigger).all(|i| {
3105                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(paint)).send
3106            }),
3107            "frames at the paint-over quantizer go on"
3108        );
3109        let finer = start + 4 * trigger;
3110        assert!(
3111            (finer..finer + trigger).all(|i| {
3112                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(paint - 6)).send
3113            }),
3114            "a trigger period of frames six steps finer"
3115        );
3116        assert!(
3117            (finer + trigger..finer + 100).all(|i| {
3118                !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(paint - 6)).send
3119            }),
3120            "ends it"
3121        );
3122        assert!(paint_over_improves(&s, at(paint)));
3123        assert!(!paint_over_improves(&s, at(paint - 6)));
3124    }
3125
3126    /// A session that holds no quantizer has no refresh to fall back to: its cleanup flows
3127    /// through the rate control past the periods of a level quantizer a holding one is given,
3128    /// and ends after a trigger period of frames coded at the paint-over quantizer, whatever
3129    /// their size, or once its finest quantizer has stood for `LEVEL_S`.
3130    #[test]
3131    fn a_session_that_holds_no_quantizer_is_refined_until_it_reaches_the_paint_over_quality() {
3132        let s = hw_settings();
3133        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3134        let at = |last: u32| EncoderQuality {
3135            last: Some(last),
3136            bytes: Some(budget),
3137            holds: false,
3138            reopens: false,
3139            keys: true,
3140            band: None,
3141            measures: false,
3142            psnr: None,
3143            refit: None,
3144            follows_cap: false,
3145            finer: 0,
3146            ends_on_key: false,
3147        };
3148        let trigger = s.paint_over_trigger_frames as u16;
3149        let past = 13 * trigger;
3150        let mut st = StripeState::default();
3151        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(40));
3152        let still: Vec<HwFrameDecision> = (1..past)
3153            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(40)))
3154            .collect();
3155        assert!(still.iter().all(|d| d.hold_qp.is_none() && !d.force_idr));
3156        assert!(still[trigger as usize - 1..].iter().all(|d| d.send));
3157        let fine = s.video_paintover_crf as u32;
3158        assert!(
3159            (past..past + trigger).all(|i| {
3160                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(fine)).send
3161            }),
3162            "a trigger period of frames at it"
3163        );
3164        assert!((past + trigger..past + 100).all(|i| {
3165            !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(fine)).send
3166        }));
3167        let window = |encoder: EncoderQuality| {
3168            let mut st = StripeState::default();
3169            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, encoder);
3170            (1..u16::MAX)
3171                .filter(|&i| {
3172                    decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, encoder).send
3173                })
3174                .count() as f64
3175                / s.target_fps
3176        };
3177        assert!(
3178            (window(at(40)) - LEVEL_S).abs() < 1.0,
3179            "a quantizer that stands ends it"
3180        );
3181        let mut st = StripeState::default();
3182        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(44));
3183        let step = (2.0 * s.target_fps) as u32;
3184        let refining = (1..u16::MAX)
3185            .filter(|&i| {
3186                let q = 44 - (u32::from(i) / step).min(20);
3187                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(q)).send
3188            })
3189            .count() as f64
3190            / s.target_fps;
3191        assert!(
3192            (refining - REFINE_S).abs() < 1.0,
3193            "one a step finer every two seconds runs the window"
3194        );
3195        // Frames of one size end it sooner (`same_sized_frames_end_a_cleanup_that_says_no_quantizer`)
3196        let mut st = StripeState::default();
3197        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(40));
3198        let silent = (1..u16::MAX)
3199            .filter(|&i| {
3200                let varying = EncoderQuality {
3201                    last: None,
3202                    bytes: Some(budget / (1 + i as usize % 2)),
3203                    ..at(40)
3204                };
3205                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, varying).send
3206            })
3207            .count() as f64
3208            / s.target_fps;
3209        assert!(
3210            (silent - CONVERGE_S).abs() < 1.0,
3211            "one that says no quantizer gets the shorter window"
3212        );
3213    }
3214
3215    /// A region whose change adds up to a key-frame cleanup is keyed once it holds still, unless
3216    /// its block ledger refined all but a small share of it, where its refresh is the cleanup.
3217    #[test]
3218    fn a_key_cleanup_skips_a_region_its_ledger_refined() {
3219        let cleanups = |refined: bool| {
3220            let mut st = StripeState {
3221                ledger: crate::ledger::BlockLedger::new(64, 64),
3222                ..Default::default()
3223            };
3224            let trigger = 4;
3225            let due = |st: &mut StripeState, damage| {
3226                cleanup_due(st, trigger, trigger, true, true, true, damage)
3227            };
3228            for _ in 0..20 {
3229                due(&mut st, Damage::Area(0.5));
3230            }
3231            if refined {
3232                let plan = st.ledger.plan(0, usize::MAX, usize::MAX).unwrap();
3233                st.ledger.refined(&plan);
3234            }
3235            (0..trigger * KEY_STILL_TRIGGERS + 1)
3236                .map(|_| due(&mut st, Damage::None))
3237                .collect::<Vec<_>>()
3238        };
3239        assert!(cleanups(false).contains(&Cleanup::Key));
3240        let refined = cleanups(true);
3241        assert!(!refined.contains(&Cleanup::Key));
3242        assert!(refined.contains(&Cleanup::Refresh));
3243    }
3244
3245    /// Such a session whose frames run small has nothing left to refine: the cleanup ends, and
3246    /// a caret blinking on the settled screen starts no other; motion does.
3247    #[test]
3248    fn a_settled_cleanup_is_not_restarted_by_a_caret() {
3249        let s = hw_settings();
3250        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3251        let small = EncoderQuality {
3252            last: None,
3253            bytes: Some(budget / 8),
3254            holds: false,
3255            reopens: false,
3256            keys: true,
3257            band: None,
3258            measures: false,
3259            psnr: None,
3260            refit: None,
3261            follows_cap: false,
3262            finer: 0,
3263            ends_on_key: false,
3264        };
3265        let trigger = s.paint_over_trigger_frames;
3266        let mut st = StripeState::default();
3267        let mut frame = 0u16;
3268        let mut run = |st: &mut StripeState, damage: Damage, frames: u32| {
3269            (0..frames)
3270                .filter(|_| {
3271                    frame += 1;
3272                    decide_hw_fullframe(st, &s, frame, damage, false, false, small).send
3273                })
3274                .count() as u32
3275        };
3276        run(&mut st, Damage::Area(1.0), 1);
3277        let cleanup = run(&mut st, Damage::None, 20 * trigger);
3278        assert_eq!(cleanup, STALL_TRIGGERS * trigger + 1);
3279        let mut blinking = 0;
3280        for _ in 0..10 {
3281            blinking += run(&mut st, Damage::Area(0.001), 1);
3282            blinking += run(&mut st, Damage::None, 2 * trigger);
3283        }
3284        assert_eq!(blinking, 20, "each blink and its refresh, and no more");
3285        run(&mut st, Damage::Area(1.0), 1);
3286        assert_eq!(run(&mut st, Damage::None, 20 * trigger), cleanup);
3287    }
3288
3289    #[test]
3290    fn a_rate_control_that_does_not_converge_gets_one_held_refresh() {
3291        let s = hw_settings();
3292        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3293        let crawling = EncoderQuality {
3294            last: Some(38),
3295            bytes: Some(budget / 2),
3296            holds: true,
3297            reopens: false,
3298            keys: true,
3299            band: None,
3300            measures: false,
3301            psnr: None,
3302            refit: None,
3303            follows_cap: false,
3304            finer: 0,
3305            ends_on_key: false,
3306        };
3307        let mut st = StripeState::default();
3308        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, crawling);
3309        let still: Vec<HwFrameDecision> = (1..=200)
3310            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, crawling))
3311            .collect();
3312        assert!(still.iter().all(|d| !d.force_idr), "no key frame");
3313        let held: Vec<usize> = still
3314            .iter()
3315            .enumerate()
3316            .filter(|(_, d)| d.hold_qp.is_some())
3317            .map(|(i, _)| i)
3318            .collect();
3319        assert_eq!(held.len(), 1, "{held:?}");
3320        let trigger = s.paint_over_trigger_frames as usize;
3321        assert_eq!(
3322            held[0],
3323            1 + (STALL_TRIGGERS as usize) * trigger,
3324            "once its quantizer has stood"
3325        );
3326        assert_eq!(
3327            still[held[0]].hold_qp,
3328            Some(held_refresh_quality(&s, crawling))
3329        );
3330        assert!(still[held[0] + 1..].iter().all(|d| !d.send));
3331    }
3332
3333    /// A rate control pinned at its coarsest quantizer on a still screen, its frames many budgets
3334    /// each, is refreshed by one held frame at once, coarsened to what a second of the target buys
3335    /// at the bytes its frames take, and the cleanup ends: a band sweep would carry the rest of
3336    /// every frame at that quantizer.
3337    #[test]
3338    fn a_rate_control_pinned_at_its_coarsest_quantizer_is_refreshed_once() {
3339        let s = CaptureSettings {
3340            video_bitrate_kbps: 100,
3341            ..hw_settings()
3342        };
3343        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3344        let pinned = EncoderQuality {
3345            last: Some(51),
3346            bytes: Some((budget * 16.0) as usize),
3347            holds: true,
3348            reopens: false,
3349            keys: true,
3350            band: Some(0),
3351            measures: false,
3352            psnr: None,
3353            refit: None,
3354            follows_cap: false,
3355            finer: 0,
3356            ends_on_key: false,
3357        };
3358        let mut st = StripeState::default();
3359        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
3360        let still: Vec<HwFrameDecision> = (1..=200)
3361            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned))
3362            .collect();
3363        let sent: Vec<usize> = still
3364            .iter()
3365            .enumerate()
3366            .filter(|(_, d)| d.send)
3367            .map(|(i, _)| i)
3368            .collect();
3369        let trigger = s.paint_over_trigger_frames as usize;
3370        let paid = |k: usize| (k as f64 * pinned.bytes.unwrap() as f64 / budget) as usize;
3371        let refresh = paid(trigger);
3372        assert_eq!(
3373            sent,
3374            (0..=trigger).map(paid).collect::<Vec<_>>(),
3375            "a trigger period through the rate control, each frame waited out, then the refresh"
3376        );
3377        assert_eq!(
3378            (still[refresh].hold_qp, still[refresh].hold_band),
3379            (Some(40), None),
3380            "frames of 16 budgets leave 11 steps of the 35 a second buys"
3381        );
3382    }
3383
3384    /// That rate control's frames withheld by a sender as they go out are coded past: the refresh
3385    /// comes after the same trigger period. Taken as losses, each started the cleanup over and the
3386    /// refresh never came.
3387    #[test]
3388    fn a_pinned_rate_control_withheld_is_refreshed_all_the_same() {
3389        use crate::encoders::reference::ReferenceReport;
3390        let s = CaptureSettings {
3391            video_bitrate_kbps: 100,
3392            ..hw_settings()
3393        };
3394        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3395        let pinned = EncoderQuality {
3396            last: Some(51),
3397            bytes: Some((budget * 16.0) as usize),
3398            holds: true,
3399            reopens: false,
3400            keys: true,
3401            band: Some(0),
3402            measures: false,
3403            psnr: None,
3404            refit: None,
3405            follows_cap: false,
3406            finer: 0,
3407            ends_on_key: false,
3408        };
3409        let refresh = |report: fn(u16) -> ReferenceReport| {
3410            let mut st = StripeState::default();
3411            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
3412            (1..=200).position(|i| {
3413                let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned);
3414                if d.send && d.hold_qp.is_none() {
3415                    st.take_report(report(i));
3416                }
3417                d.hold_qp.is_some()
3418            })
3419        };
3420        let trigger = s.paint_over_trigger_frames as usize;
3421        let paid = (trigger as f64 * pinned.bytes.unwrap() as f64 / budget) as usize;
3422        assert_eq!(refresh(ReferenceReport::Withheld), Some(paid));
3423        assert_eq!(refresh(ReferenceReport::Lost), None);
3424    }
3425
3426    /// A key frame asked for on a still screen with nothing for a cleanup to improve (the session
3427    /// asks for no finer than the coarsest quantizer) opens a burst through the rate control,
3428    /// which ends at once where the rate control is pinned at its coarsest quantizer.
3429    #[test]
3430    fn a_pinned_rate_control_ends_a_recovery_burst() {
3431        let s = CaptureSettings {
3432            video_bitrate_kbps: 100,
3433            video_paintover_crf: 51,
3434            use_paint_over_quality: false,
3435            ..hw_settings()
3436        };
3437        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3438        let pinned = EncoderQuality {
3439            last: Some(51),
3440            bytes: Some((budget * 16.0) as usize),
3441            holds: true,
3442            reopens: false,
3443            keys: true,
3444            band: Some(0),
3445            measures: false,
3446            psnr: None,
3447            refit: None,
3448            follows_cap: false,
3449            finer: 0,
3450            ends_on_key: false,
3451        };
3452        let mut st = StripeState::default();
3453        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
3454        assert!((1..=20).all(|i| {
3455            !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned).send
3456        }));
3457        let key = decide_hw_fullframe(&mut st, &s, 21, Damage::None, false, true, pinned);
3458        assert!(key.send && key.force_idr);
3459        let trigger = s.paint_over_trigger_frames as u16;
3460        assert!(
3461            (22..21 + trigger).all(|i| decide_hw_fullframe(
3462                &mut st,
3463                &s,
3464                i,
3465                Damage::None,
3466                false,
3467                false,
3468                pinned
3469            )
3470            .send),
3471            "its burst until a trigger period over the budget"
3472        );
3473        assert!(
3474            (21 + trigger..=200).all(|i| !decide_hw_fullframe(
3475                &mut st,
3476                &s,
3477                i,
3478                Damage::None,
3479                false,
3480                false,
3481                pinned
3482            )
3483            .send),
3484            "and no more"
3485        );
3486    }
3487
3488    /// A session that says no quantizer and measures nothing (radeonsi's VCE) ends a cleanup it
3489    /// codes in frames of one size for `EMPTY_S`, a constant rate's frames once there is nothing
3490    /// left to refine; one that says a coarse quantizer goes on until it stops getting finer
3491    /// (`LEVEL_S`).
3492    #[test]
3493    fn same_sized_frames_end_a_cleanup_that_says_no_quantizer() {
3494        let s = CaptureSettings {
3495            video_bitrate_kbps: 2000,
3496            ..hw_settings()
3497        };
3498        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3499        let frames_sent = |last: Option<u32>| {
3500            let encoder = EncoderQuality {
3501                last,
3502                bytes: Some(budget as usize),
3503                holds: false,
3504                reopens: false,
3505                keys: true,
3506                band: None,
3507                measures: false,
3508                psnr: None,
3509                refit: None,
3510                follows_cap: false,
3511                finer: 0,
3512                ends_on_key: false,
3513            };
3514            let mut st = StripeState::default();
3515            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, encoder);
3516            (1..=1200u16)
3517                .filter(|&i| {
3518                    decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, encoder).send
3519                })
3520                .count()
3521        };
3522        let window = (EMPTY_S * s.target_fps) as usize;
3523        let (unsaid, coarse) = (frames_sent(None), frames_sent(Some(30)));
3524        assert!(
3525            unsaid <= window + 2 * s.paint_over_trigger() as usize
3526                && coarse >= (LEVEL_S * s.target_fps) as usize,
3527            "{unsaid} frames without a quantizer, {coarse} at a coarse one"
3528        );
3529    }
3530
3531    /// A session that codes a still screen's cleanup frames at the rate their cap runs at
3532    /// (`EncoderQuality::follows_cap`, VA-API) has them judged against the cleanup's budget: three
3533    /// session budgets a frame at a coarse quantizer, which is a rate control pinned past its
3534    /// target at the session's rate, is a share of the cleanup's at a 2 Mbit/s path's, and the
3535    /// cleanup carries on.
3536    #[test]
3537    fn a_cleanup_frame_coded_at_the_cleanup_rate_is_judged_by_its_budget() {
3538        let s = CaptureSettings {
3539            video_bitrate_kbps: 500,
3540            video_link_kbps: 2000,
3541            ..hw_settings()
3542        };
3543        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3544        let frames_sent = |follows_cap: bool| {
3545            let encoder = EncoderQuality {
3546                last: Some(40),
3547                bytes: Some((budget * 3.0) as usize),
3548                holds: false,
3549                reopens: false,
3550                keys: true,
3551                band: None,
3552                measures: false,
3553                psnr: None,
3554                refit: None,
3555                follows_cap,
3556                finer: 0,
3557                ends_on_key: false,
3558            };
3559            let mut st = StripeState::default();
3560            decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, encoder);
3561            (1..=120u16)
3562                .filter(|&i| {
3563                    decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, encoder).send
3564                })
3565                .count()
3566        };
3567        let (session, cleanup) = (frames_sent(false), frames_sent(true));
3568        assert!(
3569            session < 10 && cleanup > 60,
3570            "{session} frames judged by the session's budget, {cleanup} by the cleanup's"
3571        );
3572    }
3573
3574    /// A key frame asked for on a clean still screen comes out at the coarsest quantizer, and the
3575    /// rate control's frames after it run over their budgets while they refine it: the cleanup
3576    /// carries on into the band sweep, not the one refresh at that quantizer a rate control
3577    /// pinned at its coarsest gets (NVENC at 0.5 Mbit/s, 8 fps, 1080p text: 25.7 dB for good),
3578    /// and the sweep's bands are coded at the paint-over quantizer, not one counted from the rate
3579    /// control's (at 8 fps, 18 steps under it: 25 here, 33 from the key frame's 51).
3580    #[test]
3581    fn a_rate_control_refining_a_recovery_key_frame_is_not_pinned() {
3582        let s = CaptureSettings {
3583            video_bitrate_kbps: 500,
3584            target_fps: 8.0,
3585            video_paintover_crf: 18,
3586            paint_over_trigger_frames: 15,
3587            ..hw_settings()
3588        };
3589        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3590        let rc = |last: u32, budgets: f64, refit: Option<f64>| EncoderQuality {
3591            last: Some(last),
3592            bytes: Some((budget * budgets) as usize),
3593            holds: true,
3594            reopens: false,
3595            keys: true,
3596            band: Some(700),
3597            measures: false,
3598            psnr: None,
3599            refit,
3600            follows_cap: false,
3601            finer: 0,
3602            ends_on_key: false,
3603        };
3604        let mut st = StripeState {
3605            paint_over_sent: true,
3606            no_motion_frame_count: 100,
3607            ..Default::default()
3608        };
3609        let key = decide_hw_fullframe(&mut st, &s, 0, Damage::None, false, true, rc(42, 0.1, None));
3610        assert!(key.send && key.force_idr);
3611        let after = decide_hw_fullframe(
3612            &mut st,
3613            &s,
3614            1,
3615            Damage::None,
3616            false,
3617            false,
3618            rc(51, 4.3, None),
3619        );
3620        assert!(
3621            after.send && after.hold_qp.is_none(),
3622            "the burst through the rate control"
3623        );
3624        // The frame of 7.5 budgets is waited out first.
3625        let (n, refit) = (2..)
3626            .map(|i| {
3627                let d = decide_hw_fullframe(
3628                    &mut st,
3629                    &s,
3630                    i,
3631                    Damage::None,
3632                    false,
3633                    false,
3634                    rc(43, 7.5, Some(0.0)),
3635                );
3636                (i, d)
3637            })
3638            .find(|(_, d)| d.send)
3639            .unwrap();
3640        assert_eq!(n, 8);
3641        assert_eq!(
3642            (refit.hold_qp, refit.hold_band),
3643            (Some(18), Some((0.0, FIRST_BAND))),
3644            "a frame taken back for its cap starts the sweep, at the paint-over quantizer"
3645        );
3646        let band = decide_hw_fullframe(
3647            &mut st,
3648            &s,
3649            n + 1,
3650            Damage::None,
3651            false,
3652            false,
3653            rc(43, 0.1, None),
3654        );
3655        assert!(
3656            band.send
3657                && band.hold_qp == Some(18)
3658                && band.hold_band.is_some_and(|(from, _)| from == FIRST_BAND),
3659            "and so is the next band"
3660        );
3661    }
3662
3663    #[test]
3664    fn a_blinking_caret_does_not_hold_off_a_converging_cleanup() {
3665        let s = hw_settings();
3666        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3667        let crawling = EncoderQuality {
3668            last: Some(38),
3669            bytes: Some(budget / 2),
3670            holds: true,
3671            reopens: false,
3672            keys: true,
3673            band: None,
3674            measures: false,
3675            psnr: None,
3676            refit: None,
3677            follows_cap: false,
3678            finer: 0,
3679            ends_on_key: false,
3680        };
3681        let mut st = StripeState::default();
3682        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, crawling);
3683        let trigger = s.paint_over_trigger_frames as u16;
3684        let decisions: Vec<HwFrameDecision> = (1..300)
3685            .map(|i| {
3686                let damage = if i % 3 == 0 {
3687                    Damage::Area(0.001)
3688                } else {
3689                    Damage::None
3690                };
3691                decide_hw_fullframe(&mut st, &s, i, damage, false, false, crawling)
3692            })
3693            .collect();
3694        assert!(decisions.iter().all(|d| !d.force_idr), "no key frame");
3695        let first = decisions
3696            .iter()
3697            .position(|d| d.hold_qp.is_some())
3698            .expect("a held refresh") as u16
3699            + 1;
3700        let bound = trigger * (LOW_MOTION_TRIGGERS + 2 * STALL_TRIGGERS) as u16 * 3 / 2;
3701        assert!(
3702            first <= bound,
3703            "the caret does not hold it off: {first} > {bound}"
3704        );
3705        let held = decisions.iter().filter(|d| d.hold_qp.is_some()).count();
3706        assert!(
3707            held <= 300 / (trigger as usize * LOW_MOTION_TRIGGERS as usize),
3708            "{held} held refreshes"
3709        );
3710    }
3711
3712    /// A rate control that coded the frame before a still screen far over its target repays it
3713    /// with frames under the target at its coarsest, which is no stall: the cleanup runs past the
3714    /// stall's run until it is repaid, as kvazaar's at 60 fps 0.5 Mbit/s holds quantizer 51 for a
3715    /// second after 59 budgets, and still ends.
3716    #[test]
3717    fn a_rate_control_repaying_an_overshoot_is_not_stalled() {
3718        let s = hw_settings();
3719        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3720        let quality = |bytes: f64, q: u32| EncoderQuality {
3721            last: Some(q),
3722            bytes: Some(bytes as usize),
3723            holds: false,
3724            reopens: false,
3725            keys: true,
3726            band: None,
3727            measures: false,
3728            psnr: None,
3729            refit: None,
3730            follows_cap: false,
3731            finer: 0,
3732            ends_on_key: false,
3733        };
3734        let mut st = StripeState::default();
3735        let mut last = quality(budget * 60.0, 35);
3736        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, last);
3737        let sent = (1..=400u16)
3738            .filter(|&i| {
3739                let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, last);
3740                if d.send {
3741                    last = quality(budget / 10.0, 51);
3742                }
3743                d.send
3744            })
3745            .count();
3746        let stall = (STALL_TRIGGERS * s.paint_over_trigger_frames) as usize;
3747        assert!(
3748            sent > 50 + stall,
3749            "{sent} frames: the cleanup ran past the repayment"
3750        );
3751        assert!(
3752            sent < (REFINE_S * s.target_fps) as usize,
3753            "{sent} frames: it still ends"
3754        );
3755    }
3756
3757    #[test]
3758    fn a_stalled_rate_control_is_held_once_at_the_paint_over_quantizer() {
3759        let s = hw_settings();
3760        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
3761        let idle = EncoderQuality {
3762            last: Some(38),
3763            bytes: Some(budget / 10),
3764            holds: true,
3765            reopens: false,
3766            keys: true,
3767            band: None,
3768            measures: false,
3769            psnr: None,
3770            refit: None,
3771            follows_cap: false,
3772            finer: 0,
3773            ends_on_key: false,
3774        };
3775        let mut st = StripeState::default();
3776        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle);
3777        let still: Vec<HwFrameDecision> = (1..=200)
3778            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle))
3779            .collect();
3780        let held: Vec<usize> = still
3781            .iter()
3782            .enumerate()
3783            .filter(|(_, d)| d.hold_qp.is_some())
3784            .map(|(i, _)| i)
3785            .collect();
3786        assert_eq!(held.len(), 1, "one held frame: {held:?}");
3787        assert_eq!(still[held[0]].hold_qp, Some(held_refresh_quality(&s, idle)));
3788        assert!(!still[held[0]].force_idr, "a refresh, not a key frame");
3789        let stall = (STALL_TRIGGERS * s.paint_over_trigger_frames) as usize;
3790        assert!(held[0] + 1 > stall, "after the stall, not at its start");
3791        assert!(
3792            still[held[0] + 1..].iter().all(|d| !d.send),
3793            "which ends the cleanup"
3794        );
3795    }
3796
3797    #[test]
3798    fn a_stalled_rate_control_that_holds_a_band_sweeps_the_picture_within_the_budget() {
3799        let s = CaptureSettings {
3800            video_link_kbps: hw_settings().video_bitrate_kbps,
3801            ..hw_settings()
3802        };
3803        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3804        let idle = |held: usize| EncoderQuality {
3805            last: Some(38),
3806            bytes: Some(budget as usize / 10),
3807            holds: true,
3808            reopens: false,
3809            keys: true,
3810            band: Some(held),
3811            measures: false,
3812            psnr: None,
3813            refit: None,
3814            follows_cap: false,
3815            finer: 0,
3816            ends_on_key: false,
3817        };
3818        let mut st = StripeState::default();
3819        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
3820        let mut bands: Vec<(f64, f64)> = Vec::new();
3821        let mut sent_after = 0;
3822        for i in 1..=600u16 {
3823            let held = bands
3824                .last()
3825                .map_or(0, |(a, b)| ((b - a) * 128.0 * budget) as usize);
3826            let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
3827            assert!(!d.force_idr, "no key frame");
3828            match d.hold_band {
3829                Some(band) => {
3830                    assert!(d.send && d.hold_qp == Some(held_refresh_quality(&s, idle(0))));
3831                    bands.push(band);
3832                }
3833                None => {
3834                    assert!(d.hold_qp.is_none(), "every held frame is a band");
3835                    sent_after += u32::from(!bands.is_empty() && d.send);
3836                }
3837            }
3838        }
3839        assert_eq!(
3840            bands[0],
3841            (0.0, FIRST_BAND),
3842            "the sweep starts at the stall, from the top"
3843        );
3844        assert!(
3845            bands.windows(2).all(|w| w[0].1 == w[1].0),
3846            "contiguous bands: {bands:?}"
3847        );
3848        assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
3849        assert!(
3850            bands[2..bands.len() - 1]
3851                .iter()
3852                .all(|(a, b)| ((b - a) * 128.0 - 1.0).abs() < 1e-3),
3853            "each a budget: {bands:?}"
3854        );
3855        assert_eq!(sent_after, 0, "which ends the cleanup");
3856    }
3857
3858    /// A cleanup runs at what the path carries, never under the session's rate nor over
3859    /// `CLEANUP_RATE_MAX` times it, and at the session's rate where the path is not known; its
3860    /// sweep keeps to that rate, a band past its budget waited out before the next.
3861    #[test]
3862    fn a_sweep_keeps_to_the_rate_the_path_carries() {
3863        let at = |link: i32| CaptureSettings {
3864            video_link_kbps: link,
3865            ..hw_settings()
3866        };
3867        let rate = hw_settings().video_bitrate_kbps;
3868        assert_eq!(cleanup_rate(&at(0)), rate);
3869        assert_eq!(cleanup_rate(&at(rate / 2)), rate);
3870        assert_eq!(cleanup_rate(&at(rate * 2)), rate * 2);
3871        assert_eq!(cleanup_rate(&at(rate * 100)), rate * CLEANUP_RATE_MAX);
3872        let s = at(rate);
3873        let budget = frame_budget(rate, s.target_fps);
3874        let over = |held: usize| EncoderQuality {
3875            last: Some(38),
3876            bytes: Some(budget as usize / 10),
3877            holds: true,
3878            reopens: false,
3879            keys: true,
3880            band: Some(held),
3881            measures: false,
3882            psnr: None,
3883            refit: None,
3884            follows_cap: false,
3885            finer: 0,
3886            ends_on_key: false,
3887        };
3888        let mut st = StripeState::default();
3889        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, over(0));
3890        let sent: Vec<bool> = (1..=400u16)
3891            .map(|i| {
3892                let d = decide_hw_fullframe(
3893                    &mut st,
3894                    &s,
3895                    i,
3896                    Damage::None,
3897                    false,
3898                    false,
3899                    over(3 * budget as usize),
3900                );
3901                d.hold_band.is_some()
3902            })
3903            .collect();
3904        let first = sent.iter().position(|&b| b).expect("a sweep");
3905        let bands: Vec<usize> = sent[first..]
3906            .iter()
3907            .enumerate()
3908            .filter(|(_, b)| **b)
3909            .map(|(i, _)| i)
3910            .collect();
3911        assert!(bands.len() > 3, "{bands:?}");
3912        assert!(
3913            bands[1..].windows(2).all(|w| w[1] - w[0] == 3),
3914            "a band of three budgets every third frame: {bands:?}"
3915        );
3916    }
3917
3918    /// Every frame of a constant-rate cleanup, the rate control's and the sweep's, carries a cap,
3919    /// the session's buffer or `CLEANUP_FRAME_S` of the cleanup's rate; motion and a key frame
3920    /// carry none. A frame of the rate control's taken back for coming out past it starts the
3921    /// sweep on the next frame, and a band taken back resumes where it ended, so the bands still
3922    /// cover the picture.
3923    /// A band taken back by a report that ends before the last band began (the encoder's report
3924    /// of a band two frames old) restarts the sweep there with the least band, where the rewound
3925    /// band came out negative and `band_share`'s clamp panicked, ending the capture.
3926    #[test]
3927    fn a_band_taken_back_before_the_last_band_began_restarts_the_sweep() {
3928        let s = hw_settings();
3929        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3930        let rc = |held: usize, refit: Option<f64>| EncoderQuality {
3931            last: Some(38),
3932            bytes: Some(budget as usize),
3933            holds: true,
3934            reopens: false,
3935            keys: true,
3936            band: Some(held),
3937            measures: false,
3938            psnr: None,
3939            refit,
3940            follows_cap: false,
3941            finer: 0,
3942            ends_on_key: false,
3943        };
3944        let mut st = StripeState::default();
3945        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, rc(0, None));
3946        decide_hw_fullframe(&mut st, &s, 1, Damage::None, false, false, rc(0, None));
3947        decide_hw_fullframe(&mut st, &s, 2, Damage::None, false, false, rc(0, None));
3948        let swept =
3949            decide_hw_fullframe(&mut st, &s, 3, Damage::None, false, false, rc(0, Some(0.0)));
3950        assert_eq!(swept.hold_band, Some((0.0, FIRST_BAND)));
3951        let mut bands = vec![(0.0, FIRST_BAND)];
3952        for i in 4..=8u16 {
3953            let (a, b) = *bands.last().unwrap();
3954            let held = ((b - a) * 128.0 * budget) as usize;
3955            let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, rc(held, None));
3956            bands.push(d.hold_band.expect("a band a frame"));
3957        }
3958        let (a, _) = *bands.last().unwrap();
3959        let end = bands[1].0 / 2.0;
3960        assert!(end < a, "{bands:?}");
3961        let d = decide_hw_fullframe(
3962            &mut st,
3963            &s,
3964            9,
3965            Damage::None,
3966            false,
3967            false,
3968            rc(budget as usize, Some(end)),
3969        );
3970        let (from, to) = d.hold_band.expect("the sweep goes on");
3971        assert_eq!(from, end, "it resumes where the band taken back ended");
3972        assert!(to > from, "with a band of its own: {from}..{to}");
3973    }
3974
3975    #[test]
3976    fn a_cleanup_frame_past_its_cap_is_taken_back() {
3977        let s = hw_settings();
3978        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
3979        let cap = Some(cleanup_cap(&s));
3980        let link = f64::from(cleanup_rate(&s)) * 125.0 * CLEANUP_FRAME_S;
3981        assert!(
3982            (cleanup_cap(&s) as f64 - link.max(budget * 1.5)).abs() <= 1.0,
3983            "{cap:?}"
3984        );
3985        let rc = |last: u32, held: usize, refit: Option<f64>| EncoderQuality {
3986            last: Some(last),
3987            bytes: Some(budget as usize),
3988            holds: true,
3989            reopens: false,
3990            keys: true,
3991            band: Some(held),
3992            measures: false,
3993            psnr: None,
3994            refit,
3995            follows_cap: false,
3996            finer: 0,
3997            ends_on_key: false,
3998        };
3999        let mut st = StripeState::default();
4000        let moving = decide_hw_fullframe(
4001            &mut st,
4002            &s,
4003            0,
4004            Damage::Area(1.0),
4005            false,
4006            false,
4007            rc(40, 0, None),
4008        );
4009        assert_eq!(moving.cap, None, "motion carries no cap");
4010        let first =
4011            decide_hw_fullframe(&mut st, &s, 1, Damage::None, false, false, rc(40, 0, None));
4012        assert!(first.send && first.hold_qp.is_none() && first.cap == cap);
4013        let next = decide_hw_fullframe(&mut st, &s, 2, Damage::None, false, false, rc(39, 0, None));
4014        assert!(next.send && next.hold_band.is_none() && next.cap == cap);
4015        let swept = decide_hw_fullframe(
4016            &mut st,
4017            &s,
4018            3,
4019            Damage::None,
4020            false,
4021            false,
4022            rc(38, 0, Some(0.0)),
4023        );
4024        assert_eq!(
4025            swept.hold_band,
4026            Some((0.0, FIRST_BAND)),
4027            "the frame taken back starts the sweep"
4028        );
4029        assert_eq!(swept.cap, cap);
4030        let mut bands = vec![(0.0, FIRST_BAND)];
4031        for i in 4..=400u16 {
4032            let (a, b) = *bands.last().unwrap();
4033            let (held, refit) = if bands.len() == 3 {
4034                (budget as usize, Some(a + (b - a) / 4.0))
4035            } else {
4036                (((b - a) * 128.0 * budget) as usize, None)
4037            };
4038            let d = decide_hw_fullframe(
4039                &mut st,
4040                &s,
4041                i,
4042                Damage::None,
4043                false,
4044                false,
4045                rc(38, held, refit),
4046            );
4047            let Some(band) = d.hold_band else {
4048                if st.sweep.is_none() {
4049                    break;
4050                }
4051                continue;
4052            };
4053            assert_eq!(d.cap, cap);
4054            if let Some(end) = refit {
4055                assert_eq!(band.0, end, "a band taken back resumes where it ended");
4056                bands.last_mut().unwrap().1 = end;
4057            }
4058            bands.push(band);
4059        }
4060        assert!(
4061            bands.windows(2).all(|w| w[0].1 == w[1].0),
4062            "contiguous bands: {bands:?}"
4063        );
4064        assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
4065        let mut st = StripeState::default();
4066        decide_hw_fullframe(
4067            &mut st,
4068            &s,
4069            0,
4070            Damage::Area(1.0),
4071            false,
4072            false,
4073            rc(40, 0, None),
4074        );
4075        let key = decide_hw_fullframe(&mut st, &s, 1, Damage::None, false, true, rc(40, 0, None));
4076        assert!(
4077            key.force_idr && key.cap.is_none(),
4078            "a key frame carries none"
4079        );
4080    }
4081
4082    /// A session that holds its refresh rather than converging to it, and holds a band of a frame
4083    /// (libvpx's VP8), sweeps that refresh a band a frame from the top, each a budget, until it
4084    /// covers the picture, with no whole held frame; a caret blinking meanwhile leaves the sweep
4085    /// to finish.
4086    /// Under Turbo a held refresh's sweep keeps the refresh's quantizer: the rate control's frames
4087    /// flow between its bands, and VP8's came out at up to 242 kB beside bands pinned at the
4088    /// paint-over quantizer.
4089    #[test]
4090    fn a_held_refresh_swept_under_turbo_keeps_the_refresh_quantizer() {
4091        let s = CaptureSettings {
4092            video_streaming_mode: true,
4093            ..hw_settings()
4094        };
4095        let vp8 = EncoderQuality {
4096            last: Some(38),
4097            bytes: None,
4098            holds: true,
4099            reopens: false,
4100            keys: false,
4101            band: Some(0),
4102            measures: false,
4103            psnr: None,
4104            refit: None,
4105            follows_cap: false,
4106            finer: 0,
4107            ends_on_key: false,
4108        };
4109        let mut st = StripeState::default();
4110        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, vp8);
4111        let band = (1..=200u16)
4112            .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, vp8))
4113            .find(|d| d.hold_band.is_some())
4114            .expect("a sweep");
4115        assert_eq!(band.hold_qp, Some(held_refresh_quality(&s, vp8)));
4116    }
4117
4118    #[test]
4119    fn a_held_refresh_that_holds_a_band_sweeps_the_picture() {
4120        let s = hw_settings();
4121        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
4122        let vp8 = |held: usize| EncoderQuality {
4123            last: Some(38),
4124            bytes: None,
4125            holds: true,
4126            reopens: false,
4127            keys: false,
4128            band: Some(held),
4129            measures: false,
4130            psnr: None,
4131            refit: None,
4132            follows_cap: false,
4133            finer: 0,
4134            ends_on_key: false,
4135        };
4136        let mut st = StripeState::default();
4137        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, vp8(0));
4138        let mut bands: Vec<(f64, f64)> = Vec::new();
4139        let mut caret_in_sweep = false;
4140        let mut blinked = false;
4141        for i in 1..=600u16 {
4142            let held = bands
4143                .last()
4144                .map_or(0, |(a, b)| ((b - a) * 128.0 * budget) as usize);
4145            let caret = !blinked && bands.len() == 20;
4146            blinked |= caret;
4147            caret_in_sweep |= caret && st.sweep.is_some();
4148            let damage = if caret {
4149                Damage::Area(0.001)
4150            } else {
4151                Damage::None
4152            };
4153            let d = decide_hw_fullframe(&mut st, &s, i, damage, false, false, vp8(held));
4154            assert!(!d.force_idr, "no key frame");
4155            match d.hold_band {
4156                Some(band) => {
4157                    assert!(d.send && d.hold_qp == Some(s.video_paintover_crf as u32));
4158                    bands.push(band);
4159                }
4160                None => assert!(d.hold_qp.is_none(), "no whole held frame, frame {i}"),
4161            }
4162        }
4163        assert!(caret_in_sweep, "the caret blinked mid-sweep");
4164        assert_eq!(bands[0], (0.0, FIRST_BAND), "the sweep starts from the top");
4165        assert!(
4166            bands.windows(2).all(|w| w[0].1 == w[1].0),
4167            "contiguous bands, the caret's frame between two: {bands:?}"
4168        );
4169        assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
4170    }
4171
4172    /// A sweep whose bands, each a budget, would take longer than `REFINE_S` to cover the picture
4173    /// is coded a step coarser a band until its pace fits, and holds that pace: here a picture of
4174    /// four times as many budgets at the refresh's quantizer, half that six steps coarser.
4175    #[test]
4176    fn a_sweep_that_would_run_past_refine_s_is_coded_coarser() {
4177        let s = hw_settings();
4178        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
4179        let idle = |held: usize| EncoderQuality {
4180            last: Some(38),
4181            bytes: Some(budget as usize / 10),
4182            holds: true,
4183            reopens: false,
4184            keys: true,
4185            band: Some(held),
4186            measures: false,
4187            psnr: None,
4188            refit: None,
4189            follows_cap: false,
4190            finer: 0,
4191            ends_on_key: false,
4192        };
4193        let refresh = held_refresh_quality(&s, idle(0));
4194        let mut st = StripeState::default();
4195        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
4196        let window = (REFINE_S * s.target_fps).round() as usize;
4197        let mut bands: Vec<(f64, f64, u32)> = Vec::new();
4198        for i in 1..=(2 * window) as u16 {
4199            let held = bands.last().map_or(0, |(a, b, q)| {
4200                let cost = 4.0 * window as f64 * 2f64.powf(-f64::from(q - refresh) / 6.0);
4201                ((b - a) * cost * budget) as usize
4202            });
4203            let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
4204            if let (Some((a, b)), Some(q)) = (d.hold_band, d.hold_qp) {
4205                bands.push((a, b, q));
4206            }
4207        }
4208        assert_eq!(bands.last().unwrap().1, 1.0, "the sweep covers the picture");
4209        assert!(
4210            bands.len() <= window + window / 10,
4211            "within REFINE_S: {} bands",
4212            bands.len()
4213        );
4214        let mut counts = std::collections::BTreeMap::new();
4215        for b in &bands {
4216            *counts.entry(b.2).or_insert(0usize) += 1;
4217        }
4218        let (&pace, &held) = counts.iter().max_by_key(|(_, n)| **n).unwrap();
4219        assert!(
4220            (12..=15).contains(&(pace - refresh)) && held > bands.len() * 3 / 4,
4221            "most bands about the 12 steps that quarter the picture's cost: {counts:?}"
4222        );
4223        assert!(
4224            bands.iter().all(|b| b.2 <= pace + 1),
4225            "none past the pace: {counts:?}"
4226        );
4227    }
4228
4229    /// A sweep whose frames stay past the budget at the least band, the rest of each frame costing
4230    /// what the band does not, refreshes the rest of the picture in one held frame and ends.
4231    #[test]
4232    fn a_sweep_past_its_budget_at_the_least_band_is_refreshed_whole() {
4233        let s = hw_settings();
4234        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
4235        let idle = |held: usize| EncoderQuality {
4236            last: Some(38),
4237            bytes: Some(budget as usize / 10),
4238            holds: true,
4239            reopens: false,
4240            keys: true,
4241            band: Some(held),
4242            measures: false,
4243            psnr: None,
4244            refit: None,
4245            follows_cap: false,
4246            finer: 0,
4247            ends_on_key: false,
4248        };
4249        let floor = (3.0 * budget) as usize;
4250        let mut st = StripeState::default();
4251        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
4252        let mut bands = Vec::new();
4253        let mut whole = None;
4254        for i in 1..=600u16 {
4255            let held = if bands.is_empty() { 0 } else { floor };
4256            let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
4257            match (d.hold_band, d.hold_qp) {
4258                (Some((a, b)), Some(_)) => bands.push(b - a),
4259                (None, Some(q)) if !bands.is_empty() => {
4260                    whole = Some((i, q));
4261                    break;
4262                }
4263                _ => {}
4264            }
4265        }
4266        let (at, q) = whole.expect("a whole held refresh");
4267        assert!(
4268            bands.len() <= 8 && bands.last().is_some_and(|&b| b <= 2.0 * LEAST_BAND),
4269            "bands halved down to the least: {bands:?}"
4270        );
4271        assert!(
4272            st.sweep.is_none() && q < 38,
4273            "the sweep ends in a refresh at {q}"
4274        );
4275        let after = (at + 1..=at + 600)
4276            .filter(|&i| {
4277                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(floor)).send
4278            })
4279            .count();
4280        assert_eq!(after, 0, "and nothing goes out after it");
4281    }
4282
4283    /// A session that holds no quantizer and measures nothing, whose frames of a still screen keep
4284    /// the size of the one before (libvpx's VP9 at 0.1 Mbit/s, over the budget and so never
4285    /// small), ends its cleanup after `EMPTY_S` of them rather than `REFINE_S`.
4286    #[test]
4287    fn a_cleanup_whose_frames_keep_one_size_ends() {
4288        let s = CaptureSettings {
4289            video_bitrate_kbps: 100,
4290            ..hw_settings()
4291        };
4292        let level = EncoderQuality {
4293            last: Some(51),
4294            bytes: Some(266),
4295            holds: false,
4296            reopens: false,
4297            keys: true,
4298            band: None,
4299            measures: false,
4300            psnr: None,
4301            refit: None,
4302            follows_cap: false,
4303            finer: 0,
4304            ends_on_key: false,
4305        };
4306        let mut st = StripeState::default();
4307        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, level);
4308        let sent = (1..=2400u16)
4309            .filter(|&i| {
4310                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, level).send
4311            })
4312            .count();
4313        let run = (EMPTY_S * s.target_fps) as usize;
4314        assert!(
4315            (run..=run + 4).contains(&sent),
4316            "{sent} frames, not the {} of REFINE_S",
4317            (REFINE_S * s.target_fps) as usize
4318        );
4319    }
4320
4321    #[test]
4322    fn motion_ends_a_band_sweep() {
4323        let s = hw_settings();
4324        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
4325        let idle = EncoderQuality {
4326            last: Some(38),
4327            bytes: Some(budget / 10),
4328            holds: true,
4329            reopens: false,
4330            keys: true,
4331            band: Some(budget),
4332            measures: false,
4333            psnr: None,
4334            refit: None,
4335            follows_cap: false,
4336            finer: 0,
4337            ends_on_key: false,
4338        };
4339        let mut st = StripeState::default();
4340        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle);
4341        let first = (1..=200u16)
4342            .find(|&i| {
4343                decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle)
4344                    .hold_band
4345                    .is_some()
4346            })
4347            .expect("a sweep");
4348        let moving = decide_hw_fullframe(
4349            &mut st,
4350            &s,
4351            first + 1,
4352            Damage::Area(0.5),
4353            false,
4354            false,
4355            idle,
4356        );
4357        assert!(moving.send && moving.hold_qp.is_none() && moving.hold_band.is_none());
4358        assert!(st.sweep.is_none());
4359        let caret = decide_hw_fullframe(
4360            &mut st,
4361            &s,
4362            first + 2,
4363            Damage::Area(0.001),
4364            false,
4365            false,
4366            idle,
4367        );
4368        assert!(caret.hold_band.is_none());
4369    }
4370
4371    #[test]
4372    fn a_recovery_key_frame_is_refined_until_the_rate_control_converges() {
4373        let s = hw_settings();
4374        let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
4375        let coarse = |i: u16| EncoderQuality {
4376            last: Some(40 - u32::from(i - 201)),
4377            bytes: Some(budget),
4378            holds: true,
4379            reopens: false,
4380            keys: true,
4381            band: None,
4382            measures: false,
4383            psnr: None,
4384            refit: None,
4385            follows_cap: false,
4386            finer: 0,
4387            ends_on_key: false,
4388        };
4389        let clean = EncoderQuality {
4390            last: Some(8),
4391            bytes: Some(budget / 8),
4392            holds: true,
4393            reopens: false,
4394            keys: true,
4395            band: None,
4396            measures: false,
4397            psnr: None,
4398            refit: None,
4399            follows_cap: false,
4400            finer: 0,
4401            ends_on_key: false,
4402        };
4403        let mut st = StripeState::default();
4404        assert!(
4405            (0..200).all(|i| !decide_hw_fullframe(
4406                &mut st,
4407                &s,
4408                i,
4409                Damage::None,
4410                false,
4411                false,
4412                clean
4413            )
4414            .send)
4415        );
4416        let join = decide_hw_fullframe(&mut st, &s, 200, Damage::None, false, true, clean);
4417        assert!(join.send && join.force_idr && join.hold_qp.is_none());
4418        let past_burst = 201 + s.video_paintover_burst_frames as u16 + 3;
4419        assert!(
4420            (201..past_burst).all(|i| {
4421                let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, coarse(i));
4422                d.send && !d.force_idr && d.hold_qp.is_none()
4423            }),
4424            "the rate control keeps refining the key frame past the configured burst"
4425        );
4426        assert!(
4427            !decide_hw_fullframe(&mut st, &s, past_burst, Damage::None, false, false, clean).send,
4428            "until it converges"
4429        );
4430    }
4431
4432    #[test]
4433    fn a_large_change_after_a_clean_still_screen_is_not_keyed_as_it_lands() {
4434        let s = CaptureSettings {
4435            video_paintover_burst_frames: 0,
4436            ..hw_settings()
4437        };
4438        let fine = EncoderQuality {
4439            last: Some(5),
4440            bytes: None,
4441            holds: true,
4442            reopens: false,
4443            keys: true,
4444            band: None,
4445            measures: false,
4446            psnr: None,
4447            refit: None,
4448            follows_cap: false,
4449            finer: 0,
4450            ends_on_key: false,
4451        };
4452        let coarse = EncoderQuality {
4453            last: Some(40),
4454            bytes: None,
4455            holds: true,
4456            reopens: false,
4457            keys: true,
4458            band: None,
4459            measures: false,
4460            psnr: None,
4461            refit: None,
4462            follows_cap: false,
4463            finer: 0,
4464            ends_on_key: false,
4465        };
4466        let mut st = StripeState::default();
4467        let mut frame = 0u16;
4468        let mut step = |st: &mut StripeState, damage: Damage, q: EncoderQuality| {
4469            frame = frame.wrapping_add(1);
4470            decide_hw_fullframe(st, &s, frame, damage, false, false, q)
4471        };
4472        step(&mut st, Damage::Area(1.0), coarse);
4473        assert!(
4474            (0..100).all(|_| step(&mut st, Damage::None, fine).hold_qp.is_none()),
4475            "a finer rate control is left alone"
4476        );
4477        let landing = [
4478            step(&mut st, Damage::Area(1.0), fine),
4479            step(&mut st, Damage::Area(1.0), coarse),
4480        ];
4481        assert!(
4482            landing
4483                .iter()
4484                .all(|d| d.send && !d.force_idr && d.hold_qp.is_none()),
4485            "the change goes out at the session quality"
4486        );
4487        assert!(
4488            (0..12).any(|_| step(&mut st, Damage::None, coarse).force_idr),
4489            "and is keyed once the screen holds still"
4490        );
4491    }
4492
4493    /// A screen that never holds still for the trigger (a caret blinking every other frame)
4494    /// still gets its cleanup, once the low-motion window passes, and the rest of the screen
4495    /// with it.
4496    #[test]
4497    fn low_motion_does_not_hold_off_the_cleanup_forever() {
4498        let s = CaptureSettings {
4499            paint_over_trigger_frames: 4,
4500            ..hw_settings()
4501        };
4502        let mut st = StripeState::default();
4503        run(&mut st, &s, Damage::Area(1.0), 3);
4504        let mut fired = None;
4505        for i in 0..40u16 {
4506            let damage = if i % 2 == 0 {
4507                Damage::Area(0.0005)
4508            } else {
4509                Damage::None
4510            };
4511            let d = decide_hw_fullframe(&mut st, &s, 100 + i, damage, false, false, HOLDS);
4512            if d.hold_qp.is_some() {
4513                fired = Some((i, d.force_idr));
4514                break;
4515            }
4516        }
4517        let (at, key) = fired.expect("a cleanup under low motion");
4518        assert_eq!(
4519            u32::from(at) + 4,
4520            s.paint_over_trigger_frames * LOW_MOTION_TRIGGERS,
4521            "at the end of the window since the scroll began"
4522        );
4523        assert!(key, "the scroll before it makes it a key frame");
4524
4525        // After a long scroll the caret gets the cleanup once the scroll has aged out of the
4526        // window, not only after a quarter of every frame since it began.
4527        let mut st = StripeState::default();
4528        run(&mut st, &s, Damage::Area(1.0), 200);
4529        let window = s.paint_over_trigger_frames * LOW_MOTION_TRIGGERS;
4530        let fired = (0..200u16).find(|&i| {
4531            let damage = if i % 2 == 0 {
4532                Damage::Area(0.0005)
4533            } else {
4534                Damage::None
4535            };
4536            decide_hw_fullframe(&mut st, &s, 300 + i, damage, false, false, HOLDS).force_idr
4537        });
4538        let at = u32::from(fired.expect("a key frame under low motion after a long scroll"));
4539        assert!(
4540            at >= window && at <= window * 2,
4541            "{at} frames after the scroll stopped, window {window}"
4542        );
4543    }
4544
4545    /// Sustained motion is not low motion: a screen changing every frame gets no cleanup until
4546    /// it stops.
4547    #[test]
4548    fn sustained_motion_gets_no_cleanup_until_it_stops() {
4549        let s = hw_settings();
4550        let mut st = StripeState::default();
4551        assert!(
4552            run(&mut st, &s, Damage::Area(0.5), 60)
4553                .iter()
4554                .all(|d| d.hold_qp.is_none())
4555        );
4556        assert!(
4557            run(&mut st, &s, Damage::Unknown, 60)
4558                .iter()
4559                .all(|d| d.hold_qp.is_none())
4560        );
4561    }
4562
4563    /// A change of unknown extent counts toward a key frame only in runs: isolated ones (a
4564    /// caret on a capture that cannot say where it changed) are refreshed, never keyed.
4565    #[test]
4566    fn unknown_changes_count_toward_a_key_frame_only_in_runs() {
4567        let s = hw_settings();
4568        let mut st = StripeState::default();
4569        for _ in 0..20 {
4570            run(&mut st, &s, Damage::Unknown, 1);
4571            let still = run(&mut st, &s, Damage::None, 10);
4572            assert!(
4573                still[1].hold_qp.is_some() && !still[1].force_idr,
4574                "isolated changes are refreshed"
4575            );
4576            assert!(still.iter().all(|d| !d.force_idr), "and never keyed");
4577        }
4578        run(&mut st, &s, Damage::Unknown, 12);
4579        assert!(
4580            run(&mut st, &s, Damage::None, 10)
4581                .iter()
4582                .any(|d| d.force_idr),
4583            "a run of ten counts for more than half a screen"
4584        );
4585    }
4586
4587    /// Motion outranks the burst: it sends at normal quality, drops a burst in flight, and forces
4588    /// no keyframe of its own.
4589    #[test]
4590    fn motion_sends_at_normal_quality_and_cancels_the_burst() {
4591        let s = hw_settings();
4592        let mut st = StripeState {
4593            h264_burst_frames_remaining: 3,
4594            paint_over_sent: true,
4595            no_motion_frame_count: 9,
4596            ..Default::default()
4597        };
4598        let d = decide_hw_fullframe(&mut st, &s, 1, Damage::Area(0.2), false, false, HOLDS);
4599        assert!(d.send && !d.force_idr && d.hold_qp.is_none());
4600        assert_eq!(d.target_qp, s.video_crf as u32);
4601        assert_eq!(st.h264_burst_frames_remaining, 0, "the burst is dropped");
4602        assert_eq!(st.no_motion_frame_count, 0);
4603        assert!(!st.paint_over_sent);
4604    }
4605
4606    /// A requested key frame is coded at the rate control's own quality whatever the screen does:
4607    /// it answers a join or a loss, often on a link that fell behind, where a larger key frame
4608    /// would only fall behind again. On a still screen of a constant-quality session the burst
4609    /// behind it refines at the paint-over quality, a clean screen's included; under a constant
4610    /// rate it is not held.
4611    #[test]
4612    fn a_requested_key_frame_is_the_rate_controls_own() {
4613        let s = CaptureSettings {
4614            paint_over_trigger_frames: 30,
4615            ..crf_settings()
4616        };
4617        let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
4618        let mut st = StripeState::default();
4619        run(&mut st, &s, Damage::Area(1.0), 1);
4620        let join = decide_hw_fullframe(&mut st, &s, 2, Damage::None, false, true, HOLDS);
4621        assert!(join.send && join.force_idr && join.hold_qp.is_none() && join.target_qp == normal);
4622        assert_eq!(
4623            st.h264_burst_frames_remaining,
4624            s.video_paintover_burst_frames
4625        );
4626        let burst = decide_hw_fullframe(&mut st, &s, 3, Damage::None, false, false, HOLDS);
4627        assert!(
4628            burst.send && burst.hold_qp.is_none() && burst.target_qp == paint,
4629            "the burst refines at the paint-over quality"
4630        );
4631
4632        let s = crf_settings();
4633        let mut st = StripeState::default();
4634        run(&mut st, &s, Damage::Area(1.0), 1);
4635        run(&mut st, &s, Damage::None, 12);
4636        assert!(st.clean_quality, "cleaned up");
4637        let join = decide_hw_fullframe(&mut st, &s, 30, Damage::None, false, true, HOLDS);
4638        assert!(
4639            join.force_idr && join.target_qp == normal,
4640            "a key frame on a clean screen at the session's quality"
4641        );
4642        let burst = decide_hw_fullframe(&mut st, &s, 31, Damage::None, false, false, HOLDS);
4643        assert_eq!(burst.target_qp, paint);
4644
4645        let cbr = CaptureSettings {
4646            video_cbr_mode: true,
4647            paint_over_trigger_frames: 30,
4648            ..hw_settings()
4649        };
4650        let coarse = EncoderQuality {
4651            last: Some(40),
4652            bytes: None,
4653            holds: true,
4654            reopens: false,
4655            keys: true,
4656            band: None,
4657            measures: false,
4658            psnr: None,
4659            refit: None,
4660            follows_cap: false,
4661            finer: 0,
4662            ends_on_key: false,
4663        };
4664        let mut st = StripeState::default();
4665        decide_hw_fullframe(&mut st, &cbr, 1, Damage::Area(1.0), false, false, coarse);
4666        let join = decide_hw_fullframe(&mut st, &cbr, 2, Damage::None, false, true, coarse);
4667        assert!(join.force_idr && join.hold_qp.is_none());
4668        let burst = decide_hw_fullframe(&mut st, &cbr, 3, Damage::None, false, false, coarse);
4669        assert!(
4670            burst.send && burst.hold_qp.is_none(),
4671            "a constant rate refines the burst itself"
4672        );
4673
4674        let mut moving = StripeState::default();
4675        let dirty = decide_hw_fullframe(&mut moving, &s, 1, Damage::Area(1.0), false, true, HOLDS);
4676        assert!(dirty.send && dirty.force_idr && dirty.hold_qp.is_none());
4677        assert_eq!(
4678            moving.h264_burst_frames_remaining, 0,
4679            "motion carries the refinement, so no burst is opened"
4680        );
4681    }
4682
4683    /// At a constant quality the cleanup applies where the paint-over quality is finer than the
4684    /// session's, through the session's quality, whatever the encoder holds. At a constant rate
4685    /// it applies where the paint-over quality is finer than what the rate control last coded
4686    /// at, or the encoder does not say: held where the encoder holds a quantizer, and otherwise a
4687    /// refresh and its burst at the rate control's own quality, never a key frame. With the
4688    /// paint-over off a constant rate cleans up alike, to the paint-over quality, and a constant
4689    /// quality does not.
4690    #[test]
4691    fn the_cleanup_follows_the_rate_control_and_the_switch() {
4692        #[derive(Debug, PartialEq)]
4693        enum Seen {
4694            Held,
4695            Session,
4696            Unheld,
4697            Nothing,
4698        }
4699        for (cbr, paint, on, last, holds, seen) in [
4700            (true, 25, true, None, true, Seen::Held),
4701            (true, 18, true, Some(30), true, Seen::Held),
4702            (true, 18, true, Some(9), true, Seen::Nothing),
4703            (true, 18, true, Some(18), true, Seen::Nothing),
4704            (true, 18, true, Some(30), false, Seen::Unheld),
4705            (true, 18, true, None, false, Seen::Unheld),
4706            (true, 18, true, Some(9), false, Seen::Nothing),
4707            (false, 25, true, None, true, Seen::Nothing),
4708            (false, 18, true, None, true, Seen::Session),
4709            (false, 18, true, None, false, Seen::Session),
4710            (false, 18, false, None, true, Seen::Nothing),
4711            (true, 18, false, None, true, Seen::Held),
4712            (true, 18, false, Some(30), true, Seen::Held),
4713            (true, 18, false, Some(20), true, Seen::Held),
4714            (true, 18, false, Some(18), true, Seen::Nothing),
4715            (true, 18, false, Some(30), false, Seen::Unheld),
4716        ] {
4717            let s = CaptureSettings {
4718                video_cbr_mode: cbr,
4719                video_paintover_crf: paint,
4720                use_paint_over_quality: on,
4721                ..hw_settings()
4722            };
4723            let mut st = StripeState::default();
4724            let mut got = Seen::Nothing;
4725            for i in 0..40u16 {
4726                let damage = if i == 0 {
4727                    Damage::Area(1.0)
4728                } else {
4729                    Damage::None
4730                };
4731                let d = decide_hw_fullframe(
4732                    &mut st,
4733                    &s,
4734                    i,
4735                    damage,
4736                    false,
4737                    false,
4738                    EncoderQuality {
4739                        last,
4740                        bytes: None,
4741                        holds,
4742                        reopens: false,
4743                        keys: true,
4744                        band: None,
4745                        measures: false,
4746                        psnr: None,
4747                        refit: None,
4748                        follows_cap: false,
4749                        finer: 0,
4750                        ends_on_key: false,
4751                    },
4752                );
4753                if i > 0 && d.send {
4754                    assert!(
4755                        !cbr || holds || !d.force_idr,
4756                        "a key frame where no quantizer is held: last={last:?}"
4757                    );
4758                    if let Some(q) = d.hold_qp {
4759                        assert!(
4760                            q >= s.video_paintover_crf as u32,
4761                            "held finer than the cleanup's {q}"
4762                        );
4763                        got = Seen::Held;
4764                    } else if d.target_qp != s.video_crf as u32 {
4765                        assert!(!cbr, "a constant rate keeps its session quality");
4766                        got = Seen::Session;
4767                    } else if got == Seen::Nothing {
4768                        got = Seen::Unheld;
4769                    }
4770                }
4771            }
4772            assert_eq!(
4773                got, seen,
4774                "cbr={cbr} paint={paint} on={on} last={last:?} holds={holds}"
4775            );
4776        }
4777    }
4778
4779    /// At a constant quality the cleanup moves the session's quality to the paint-over one,
4780    /// holding no frame and coding no key frame: the refresh at the trigger and its burst, and
4781    /// under Turbo every frame from the refresh on, since a region stays at the paint-over quality
4782    /// until it moves again.
4783    #[test]
4784    fn a_constant_quality_cleanup_moves_the_session_quality() {
4785        for turbo in [false, true] {
4786            let s = CaptureSettings {
4787                video_streaming_mode: turbo,
4788                ..crf_settings()
4789            };
4790            let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
4791            let mut st = StripeState::default();
4792            let moving = run(&mut st, &s, Damage::Area(1.0), 3);
4793            assert!(
4794                moving.iter().all(|d| d.send
4795                    && !d.force_idr
4796                    && d.hold_qp.is_none()
4797                    && d.target_qp == normal)
4798            );
4799            let still = run(&mut st, &s, Damage::None, 16);
4800            assert!(
4801                still.iter().all(|d| d.hold_qp.is_none()),
4802                "no frame is held"
4803            );
4804            let sent: Vec<(usize, bool, u32)> = still
4805                .iter()
4806                .enumerate()
4807                .filter(|(_, d)| d.send)
4808                .map(|(i, d)| (i + 1, d.force_idr, d.target_qp))
4809                .collect();
4810            if !turbo {
4811                let cleanups = vec![
4812                    (2, false, paint),
4813                    (3, false, paint),
4814                    (4, false, paint),
4815                    (5, false, paint),
4816                ];
4817                assert_eq!(sent, cleanups, "a refresh at the trigger and its burst");
4818            } else {
4819                assert_eq!(sent.len(), 16, "Turbo sends every frame");
4820                assert_eq!(
4821                    sent[0],
4822                    (1, false, normal),
4823                    "the session's quality until the cleanup"
4824                );
4825                assert!(
4826                    sent[1..].iter().all(|r| r.2 == paint),
4827                    "the paint-over quality from the refresh on: {sent:?}"
4828                );
4829                assert!(sent.iter().all(|r| !r.1), "no key frame: {sent:?}");
4830            }
4831        }
4832    }
4833
4834    /// An encoder whose quality change re-opens it codes the refresh that moves it as a key
4835    /// frame, which then counts as the key-frame cleanup: one key frame for one change, as main's
4836    /// paint-over sent, where the four-trigger key frame would have been a second.
4837    #[test]
4838    fn a_reopening_encoder_counts_its_refresh_as_the_key_frame() {
4839        let s = crf_settings();
4840        let reopens = EncoderQuality {
4841            last: None,
4842            bytes: None,
4843            holds: true,
4844            reopens: true,
4845            keys: true,
4846            band: None,
4847            measures: false,
4848            psnr: None,
4849            refit: None,
4850            follows_cap: false,
4851            finer: 0,
4852            ends_on_key: false,
4853        };
4854        let mut st = StripeState::default();
4855        for i in 0..3u16 {
4856            decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, reopens);
4857        }
4858        let keys: Vec<usize> = (0..16u16)
4859            .map(|i| decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, reopens))
4860            .enumerate()
4861            .filter(|(_, d)| d.force_idr)
4862            .map(|(i, _)| i + 1)
4863            .collect();
4864        assert_eq!(
4865            keys,
4866            vec![2],
4867            "the refresh at the trigger is the key frame, and the only one"
4868        );
4869    }
4870
4871    /// A clean region keeps the paint-over quality through a small change (a caret), so an
4872    /// encoder whose quality change re-opens it does not go back and forth; motion, or a change
4873    /// of unknown extent, takes it back to the session's quality until its next cleanup.
4874    #[test]
4875    fn a_clean_region_keeps_its_quality_until_it_moves() {
4876        let s = CaptureSettings {
4877            video_streaming_mode: true,
4878            ..crf_settings()
4879        };
4880        let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
4881        let mut st = StripeState::default();
4882        run(&mut st, &s, Damage::Area(1.0), 3);
4883        run(&mut st, &s, Damage::None, 12);
4884        let caret = decide_hw_fullframe(&mut st, &s, 100, Damage::Area(0.001), false, false, HOLDS);
4885        assert_eq!(caret.target_qp, paint, "a caret keeps it");
4886        assert_eq!(
4887            decide_hw_fullframe(&mut st, &s, 101, Damage::None, false, false, HOLDS).target_qp,
4888            paint
4889        );
4890        let scroll = decide_hw_fullframe(&mut st, &s, 102, Damage::Area(0.5), false, false, HOLDS);
4891        assert_eq!(scroll.target_qp, normal, "motion takes it back");
4892        assert_eq!(
4893            decide_hw_fullframe(&mut st, &s, 103, Damage::None, false, false, HOLDS).target_qp,
4894            normal,
4895            "until the next cleanup"
4896        );
4897        run(&mut st, &s, Damage::None, 12);
4898        assert_eq!(
4899            decide_hw_fullframe(&mut st, &s, 120, Damage::Unknown, false, false, HOLDS).target_qp,
4900            normal
4901        );
4902
4903        let off = CaptureSettings {
4904            use_paint_over_quality: false,
4905            ..s
4906        };
4907        let mut st = StripeState::default();
4908        run(&mut st, &off, Damage::Area(1.0), 3);
4909        assert!(
4910            run(&mut st, &off, Damage::None, 20)
4911                .iter()
4912                .all(|d| d.target_qp == normal),
4913            "paint-over off"
4914        );
4915    }
4916
4917    /// Streaming and animated modes send every frame, and a scheduled interval forces the
4918    /// keyframe `periodic_idr_due` picks even with nothing else happening.
4919    #[test]
4920    fn hw_fullframe_streaming_and_scheduled_keyframes_send_unconditionally() {
4921        let mut s = hw_settings();
4922        s.video_streaming_mode = true;
4923        let mut st = StripeState::default();
4924        assert!(
4925            decide_hw_fullframe(&mut st, &s, 1, Damage::None, false, false, HOLDS).send,
4926            "streaming mode sends a static frame"
4927        );
4928
4929        s.video_streaming_mode = false;
4930        let mut animated = StripeState::default();
4931        assert!(
4932            decide_hw_fullframe(&mut animated, &s, 1, Damage::None, true, false, HOLDS).send,
4933            "an animated overlay sends a static frame"
4934        );
4935
4936        // 1 s at 60 fps: frame 60 is due, 61 is not.
4937        s.keyframe_interval_s = 1.0;
4938        let mut scheduled = StripeState::default();
4939        assert!(
4940            decide_hw_fullframe(
4941                &mut scheduled,
4942                &s,
4943                60,
4944                Damage::Area(1.0),
4945                false,
4946                false,
4947                HOLDS
4948            )
4949            .force_idr
4950        );
4951        let mut off_beat = StripeState::default();
4952        assert!(
4953            !decide_hw_fullframe(
4954                &mut off_beat,
4955                &s,
4956                61,
4957                Damage::Area(1.0),
4958                false,
4959                false,
4960                HOLDS
4961            )
4962            .force_idr
4963        );
4964    }
4965
4966    /// A scheduled key frame is coded at the rate control's own quality, so on a still screen
4967    /// the cleanup refined it would throw that picture away each interval (x264 at 18 Mbit/s,
4968    /// 2560x1600 text: 52.5 dB down to 35-38 at every 2 s key frame). It waits for motion
4969    /// instead, which coarsens the picture anyway, and a caret is no motion; a key frame a
4970    /// consumer asks for is coded at once.
4971    #[test]
4972    fn a_scheduled_key_frame_waits_for_motion() {
4973        for base in [hw_settings(), crf_settings()] {
4974            let s = CaptureSettings {
4975                keyframe_interval_s: 1.0,
4976                ..base
4977            };
4978            let mut st = StripeState::default();
4979            let mut decide = |fc: u16, damage: Damage, asked: bool| {
4980                decide_hw_fullframe(&mut st, &s, fc, damage, false, asked, HOLDS)
4981            };
4982            decide(1, Damage::Area(1.0), false);
4983            for fc in 2..60 {
4984                decide(fc, Damage::None, false);
4985            }
4986            assert!(
4987                (60..200).all(|fc| !decide(fc, Damage::None, false).force_idr),
4988                "a still screen codes no scheduled key frame (cbr {})",
4989                s.video_cbr_mode
4990            );
4991            assert!(
4992                !decide(200, Damage::Area(0.001), false).force_idr,
4993                "nor does a caret"
4994            );
4995            assert!(
4996                decide(201, Damage::Area(1.0), false).force_idr,
4997                "the motion after it does"
4998            );
4999            assert!(!decide(202, Damage::Area(1.0), false).force_idr, "once");
5000            assert!(
5001                decide(203, Damage::None, true).force_idr,
5002                "a key frame asked for is coded on a still screen"
5003            );
5004        }
5005    }
5006
5007    /// Software JPEG path emits on change and stays silent while static: the first frame
5008    /// sends every stripe (all dirty vs init), an identical static frame sends nothing, and a
5009    /// frame with changed top rows re-sends the dirty stripes. `use_cpu` forces the software path
5010    /// and paint-over is disabled to keep the static-frame assertions clean.
5011    #[test]
5012    fn x11_software_emits_on_change_and_stays_quiet_when_static() {
5013        let s = CaptureSettings {
5014            width: 128,
5015            height: 128,
5016            codec: Codec::Jpeg,
5017            use_cpu: true,
5018            jpeg_quality: 60,
5019            use_paint_over_quality: false,
5020            ..Default::default()
5021        };
5022        let mut p = X11Pipeline::new(s);
5023        let stride = 128 * 4;
5024        let frame_a = vec![10u8; stride * 128];
5025        let mut frame_b = frame_a.clone();
5026        for px in frame_b.iter_mut().take(stride * 40) {
5027            *px = 200;
5028        }
5029        let n1 = p.process(&frame_a, stride).len();
5030        let n2 = p.process(&frame_a, stride).len();
5031        let n3 = p.process(&frame_b, stride).len();
5032        assert!(
5033            n1 > 0,
5034            "first frame should emit (all stripes dirty vs init)"
5035        );
5036        assert_eq!(n2, 0, "identical static frame should emit nothing");
5037        assert!(n3 > 0, "changed frame should emit dirty stripes");
5038    }
5039
5040    /// Software H.264 (the build's encoder), paint-over off, non-streaming: a requested IDR on
5041    /// a static screen is followed by a short recovery burst so rate control can refine the
5042    /// keyframe, instead of the stream going silent and stranding an unrefined keyframe. The
5043    /// stream goes quiet again once the burst ends.
5044    #[test]
5045    fn x11_software_h264_streams_recovery_burst_after_requested_idr() {
5046        let s = CaptureSettings {
5047            width: 128,
5048            height: 128,
5049            codec: Codec::H264,
5050            use_cpu: true,
5051            video_crf: 25,
5052            video_paintover_burst_frames: 5,
5053            use_paint_over_quality: false,
5054            video_streaming_mode: false,
5055            target_fps: 60.0,
5056            ..Default::default()
5057        };
5058        let mut p = X11Pipeline::new(s);
5059        let stride = 128 * 4;
5060        let frame = vec![10u8; stride * 128];
5061        assert!(!p.process(&frame, stride).is_empty(), "first frame emits");
5062        for _ in 0..4 {
5063            let _ = p.process(&frame, stride);
5064        }
5065        assert!(
5066            p.process(&frame, stride).is_empty(),
5067            "static screen is quiet before the request"
5068        );
5069        p.request_idr();
5070        assert!(
5071            !p.process(&frame, stride).is_empty(),
5072            "requested IDR emits on a static screen"
5073        );
5074        for i in 0..5 {
5075            assert!(
5076                !p.process(&frame, stride).is_empty(),
5077                "recovery burst frame {i} streams while static"
5078            );
5079        }
5080        assert!(
5081            p.process(&frame, stride).is_empty(),
5082            "stream goes quiet again after the recovery burst"
5083        );
5084    }
5085
5086    /// A noise screen of 640x384 at `seed`: a rate control codes it still coarser than a cleanup's
5087    /// quantizer.
5088    fn noise(seed: u8) -> Vec<u8> {
5089        (0..640 * 4 * 384)
5090            .map(|i| {
5091                (i as u32)
5092                    .wrapping_mul(2654435761)
5093                    .wrapping_add(u32::from(seed) * 97) as u8
5094            })
5095            .collect()
5096    }
5097
5098    /// A 640x384 screen of hashed bytes at `seed`, which no motion search predicts from another.
5099    ///
5100    /// `noise` repeats every 64 pixels, so a change of its seed is a shift x264's 64-pixel search
5101    /// finds, and what that costs depends on the motion predictors the session's history left.
5102    fn scattered(seed: u8) -> Vec<u8> {
5103        (0..640 * 4 * 384u32)
5104            .map(|i| {
5105                let mut x = i ^ u32::from(seed).wrapping_mul(0x9e37_79b9);
5106                x ^= x >> 16;
5107                x = x.wrapping_mul(0x7feb_352d);
5108                x ^= x >> 15;
5109                x = x.wrapping_mul(0x846c_a68b);
5110                (x ^ (x >> 16)) as u8
5111            })
5112            .collect()
5113    }
5114
5115    /// A Turbo frame is decided on the hash of the frame before, which says nothing of where
5116    /// this one changed: a block that held still through the frame before and moves in this one
5117    /// is coded, not skipped as constant. The quantizer is pinned so x264's skip of a constant
5118    /// macroblock, which takes a reference no coarser than the frame, would hold.
5119    #[cfg(feature = "gpl")]
5120    #[test]
5121    fn a_turbo_change_after_a_still_frame_is_coded_not_skipped() {
5122        use openh264::decoder::Decoder;
5123        use openh264::formats::YUVSource;
5124        const W: usize = 256;
5125        const H: usize = 128;
5126        let mut p = X11Pipeline::new(CaptureSettings {
5127            width: W as i32,
5128            height: H as i32,
5129            codec: Codec::H264,
5130            use_cpu: true,
5131            video_streaming_mode: true,
5132            video_cbr_mode: true,
5133            video_bitrate_kbps: 20000,
5134            video_min_qp: 30,
5135            video_max_qp: 30,
5136            video_crf: 25,
5137            video_paintover_crf: 18,
5138            paint_over_trigger_frames: 15,
5139            use_paint_over_quality: true,
5140            keyframe_interval_s: 0.0,
5141            target_fps: 60.0,
5142            ..Default::default()
5143        });
5144        assert!(p.hw.is_none(), "the CPU path codes the frames");
5145        let stride = W * 4;
5146        let still = vec![60u8; stride * H];
5147        let mut moved = still.clone();
5148        for row in moved.chunks_mut(stride).take(80).skip(48) {
5149            row[96 * 4..128 * 4].fill(240);
5150        }
5151        let mut dec = Decoder::new().expect("decoder");
5152        let mut luma = 0u8;
5153        for frame in [&still, &still, &still, &moved] {
5154            for stripe in p.process(frame, stride) {
5155                let au = &stripe.data[crate::encoders::codec::VIDEO_HEADER_LEN..];
5156                if let Some(img) = dec.decode(au).expect("decode") {
5157                    let y_stride = img.strides().0;
5158                    luma = img.y()[64 * y_stride + 112];
5159                }
5160            }
5161        }
5162        assert!(
5163            luma > 180,
5164            "the block that moved after a still frame decodes as it moved: luma {luma}"
5165        );
5166    }
5167
5168    /// A still screen x264 refines nothing more of at a constant rate, its frames slice headers
5169    /// alone, ends its cleanup after `EMPTY_S` of them rather than `CONVERGE_S`. The quantizer
5170    /// floor holds x264 at its coarsest, as at 0.1 Mbit/s, on every run and thread count.
5171    #[cfg(feature = "gpl")]
5172    #[test]
5173    fn an_x264_cleanup_whose_frames_refine_nothing_ends() {
5174        let mut p = X11Pipeline::new(CaptureSettings {
5175            width: 640,
5176            height: 384,
5177            codec: Codec::H264,
5178            use_cpu: true,
5179            video_cbr_mode: true,
5180            video_bitrate_kbps: 100,
5181            video_min_qp: 51,
5182            video_paintover_crf: 18,
5183            paint_over_trigger_frames: 15,
5184            use_paint_over_quality: true,
5185            target_fps: 60.0,
5186            ..Default::default()
5187        });
5188        for i in 0..12u8 {
5189            p.process(&noise(i), 640 * 4);
5190        }
5191        let window = converge_frames(&p.settings) as usize;
5192        let frames: Vec<Vec<usize>> = (0..window + 60)
5193            .map(|_| {
5194                p.process(&noise(42), 640 * 4)
5195                    .iter()
5196                    .map(|e| e.data.len())
5197                    .collect()
5198            })
5199            .collect();
5200        let last = frames
5201            .iter()
5202            .rposition(|f| !f.is_empty())
5203            .expect("a cleanup");
5204        let empty = crate::encoders::codec::VIDEO_HEADER_LEN + 4 * EMPTY_SLICE_BYTES;
5205        let run = (EMPTY_S * 60.0) as usize;
5206        assert!(last < window, "the cleanup ends at {last} of {window}");
5207        assert!(
5208            frames[last + 1 - run..=last]
5209                .iter()
5210                .flatten()
5211                .all(|&b| b <= empty),
5212            "on frames that carry nothing: {:?}",
5213            &frames[last + 1 - run..=last]
5214        );
5215    }
5216
5217    /// The probes (`X11Pipeline::hold`) of a Turbo session's first still frame coded at a
5218    /// cleanup's quality, as a key frame where `key`, and of the same frame in a second session
5219    /// where the screen moves on it.
5220    fn cleanup_frame_that_moved(s: &CaptureSettings, key: bool) -> [[(Option<u32>, bool); 2]; 2] {
5221        let still = noise(42);
5222        let lead = |p: &mut X11Pipeline| {
5223            for i in 0..3u8 {
5224                p.process(&noise(i), 640 * 4);
5225            }
5226        };
5227        let mut p = X11Pipeline::new(s.clone());
5228        lead(&mut p);
5229        let at = (0..300)
5230            .find(|_| {
5231                p.process(&still, 640 * 4);
5232                p.hold[0].0.is_some() && p.hold[0].1 == key
5233            })
5234            .expect("a still screen is cleaned up");
5235        let mut q = X11Pipeline::new(s.clone());
5236        lead(&mut q);
5237        for _ in 0..at {
5238            q.process(&still, 640 * 4);
5239        }
5240        assert!(
5241            !q.process(&noise(9), 640 * 4).is_empty(),
5242            "the moved frame is sent"
5243        );
5244        [p.hold, q.hold]
5245    }
5246
5247    /// A Turbo frame is decided on the hash of the frame before it, so a change can land on a
5248    /// frame decided to be held whole at the cleanup's quantizer (SVT-AV1's held frames, from the
5249    /// release that takes a new target with a picture). That frame is hashed before it is
5250    /// encoded and goes to the rate control, a refresh or a key frame alike.
5251    #[test]
5252    fn a_turbo_frame_decided_held_that_moved_goes_to_the_rate_control() {
5253        let s = CaptureSettings {
5254            width: 640,
5255            height: 384,
5256            codec: Codec::Av1,
5257            use_cpu: true,
5258            video_cbr_mode: true,
5259            video_bitrate_kbps: 100,
5260            video_crf: 25,
5261            video_paintover_crf: 18,
5262            paint_over_trigger_frames: 15,
5263            use_paint_over_quality: true,
5264            video_streaming_mode: true,
5265            target_fps: 60.0,
5266            ..Default::default()
5267        };
5268        let p = X11Pipeline::new(s.clone());
5269        assert!(p.hw.is_some(), "AV1 runs as a full-frame session");
5270        if !codec_sys::svtav1::HAS_EVENTS {
5271            assert!(!EncoderQuality::of(p.hw.as_ref().unwrap()).holds);
5272            return;
5273        }
5274        for key in [false, true] {
5275            let [still, moved] = cleanup_frame_that_moved(&s, key);
5276            assert_eq!(
5277                still[1], still[0],
5278                "a frame that stood still keeps its hold"
5279            );
5280            assert!(moved[0].0.is_some() && moved[0].1 == key, "{moved:?}");
5281            assert_eq!(
5282                moved[1],
5283                (None, false),
5284                "a held frame that moved is coded under the rate control"
5285            );
5286        }
5287    }
5288
5289    /// The same at a constant quality, whose cleanup moves the session's quality to the
5290    /// paint-over one while the screen stays clean (`decide_constant_quality`): a frame that
5291    /// moved is coded at the session's own.
5292    #[test]
5293    fn a_turbo_frame_at_the_paint_over_quality_that_moved_is_coded_at_the_session_quality() {
5294        let s = CaptureSettings {
5295            width: 640,
5296            height: 384,
5297            codec: Codec::Vp9,
5298            use_cpu: true,
5299            video_cbr_mode: false,
5300            video_crf: 25,
5301            video_paintover_crf: 18,
5302            paint_over_trigger_frames: 15,
5303            use_paint_over_quality: true,
5304            video_streaming_mode: true,
5305            target_fps: 60.0,
5306            ..Default::default()
5307        };
5308        assert!(
5309            X11Pipeline::new(s.clone()).hw.is_some(),
5310            "VP9 runs as a full-frame session"
5311        );
5312        let [still, moved] = cleanup_frame_that_moved(&s, false);
5313        assert_eq!(
5314            still,
5315            [(Some(18), false); 2],
5316            "a frame that stood still keeps it"
5317        );
5318        assert_eq!(
5319            moved,
5320            [(Some(18), false), (None, false)],
5321            "a frame that moved does not"
5322        );
5323    }
5324
5325    /// The CPU path decides its cleanup inside its encode (`encode_cpu`), so a Turbo frame that
5326    /// codes a constant-quality software H.264 session at the paint-over quality
5327    /// while it holds still (`software::stripes_held_still`) is hashed before it is encoded: a
5328    /// change landing on its refresh or on a frame after it is coded at the session's quality,
5329    /// as without Turbo, and no key frame cleans the still screen up after the refresh. The
5330    /// change is `scattered`, so its size reads the quantizer it was coded at rather than what
5331    /// the motion search found.
5332    #[test]
5333    fn a_turbo_software_h264_frame_at_the_paint_over_quality_codes_a_change_at_the_session_quality()
5334    {
5335        use crate::encoders::codec::{FRAME_KEY, parse_video_type};
5336        let session = |turbo: bool| {
5337            let mut p = X11Pipeline::new(CaptureSettings {
5338                width: 640,
5339                height: 384,
5340                codec: Codec::H264,
5341                use_cpu: true,
5342                video_cbr_mode: false,
5343                video_crf: 25,
5344                video_paintover_crf: 18,
5345                paint_over_trigger_frames: 15,
5346                use_paint_over_quality: true,
5347                video_streaming_mode: turbo,
5348                target_fps: 60.0,
5349                ..Default::default()
5350            });
5351            assert!(p.hw.is_none(), "software H.264 runs on the CPU path");
5352            for i in 0..12u8 {
5353                p.process(&noise(i), 640 * 4);
5354            }
5355            p
5356        };
5357        let mut p = session(true);
5358        let (mut held, mut keys) = (None, Vec::new());
5359        for i in 0..300 {
5360            if held.is_none() && stripes_held_still(&p.stripes, &p.settings) {
5361                held = Some(i);
5362            }
5363            let out = p.process(&noise(42), 640 * 4);
5364            if out
5365                .iter()
5366                .any(|e| parse_video_type(e.data[1]).is_some_and(|(_, k)| k == FRAME_KEY))
5367            {
5368                keys.push(i);
5369            }
5370        }
5371        let held = held.expect("a still stripe is cleaned up");
5372        // OpenH264 moves its quantizer only by a rebuild, which opens on a key frame, so its
5373        // refresh at the paint-over quality is the one key frame the still screen costs.
5374        if cfg!(feature = "gpl") {
5375            assert!(
5376                keys.is_empty(),
5377                "no key frame cleans up the still screen: {keys:?}"
5378            );
5379        } else {
5380            assert!(
5381                keys.len() == 1 && keys[0] >= held,
5382                "only the rebuild at the refresh keys the still screen (held at {held}): {keys:?}"
5383            );
5384        }
5385        for at in [held, held + 15] {
5386            let jump = |turbo: bool| -> usize {
5387                let mut p = session(turbo);
5388                for _ in 0..at {
5389                    p.process(&noise(42), 640 * 4);
5390                }
5391                p.process(&scattered(9), 640 * 4)
5392                    .iter()
5393                    .map(|e| e.data.len())
5394                    .sum()
5395            };
5396            let (turbo, plain) = (jump(true), jump(false));
5397            assert!(
5398                turbo as f64 <= plain as f64 * 1.2,
5399                "a change {at} frames still is coded at the session's quality: {turbo} bytes \
5400                 against {plain}"
5401            );
5402        }
5403    }
5404
5405    /// A constant-rate x264 session given more rate than its moving picture needs codes it no
5406    /// finer than the paint-over quantizer, unless the caller floors it elsewhere, and once
5407    /// the screen holds still its cleanup codes finer than that floor.
5408    #[cfg(all(feature = "gpl", x264_qp_offsets))]
5409    #[test]
5410    fn a_constant_rate_software_h264_session_floors_motion_at_the_paint_over_quantizer() {
5411        let session = |min_qp: i32| {
5412            X11Pipeline::new(CaptureSettings {
5413                width: 640,
5414                height: 384,
5415                codec: Codec::H264,
5416                use_cpu: true,
5417                video_cbr_mode: true,
5418                video_bitrate_kbps: 200_000,
5419                video_paintover_crf: 18,
5420                video_min_qp: min_qp,
5421                paint_over_trigger_frames: 15,
5422                use_paint_over_quality: true,
5423                target_fps: 60.0,
5424                ..Default::default()
5425            })
5426        };
5427        let qp = |p: &X11Pipeline| {
5428            p.stripes[0]
5429                .h264_encoder
5430                .as_ref()
5431                .and_then(|e| e.last_qp())
5432                .expect("the rate control says its quantizer")
5433        };
5434        let mut floored = session(0);
5435        let mut own = session(1);
5436        for i in 0..40u8 {
5437            floored.process(&noise(i), 640 * 4);
5438            own.process(&noise(i), 640 * 4);
5439        }
5440        assert!(
5441            qp(&floored) >= 17,
5442            "a moving frame is floored at the paint-over quantizer: {}",
5443            qp(&floored)
5444        );
5445        assert!(
5446            qp(&own) < 17,
5447            "a floor the caller names is kept instead: {}",
5448            qp(&own)
5449        );
5450        let mut finest = qp(&floored);
5451        for _ in 0..240 {
5452            floored.process(&noise(39), 640 * 4);
5453            finest = finest.min(qp(&floored));
5454        }
5455        assert!(
5456            finest < 17,
5457            "the cleanup codes the still screen finer than the floor: {finest}"
5458        );
5459    }
5460
5461    /// The software path carries a 4:4:4 request exactly when the build's encoder does —
5462    /// libx264 at full range, so its log line says so; OpenH264 never, reporting the 4:2:0 it
5463    /// encodes — and without the request it reports 4:2:0. This is the same string the Wayland log
5464    /// builds from the same shared helper, so an identical session reads identically on both
5465    /// backends.
5466    #[test]
5467    fn x11_colorspace_desc_reports_what_the_software_encoder_carries() {
5468        let carries_444 = crate::encoders::software_fullcolor(Codec::H264);
5469        assert_eq!(
5470            carries_444,
5471            crate::encoders::software_library(Codec::H264) == "x264"
5472        );
5473        let i444 = if carries_444 {
5474            "I444 (Full Range)"
5475        } else {
5476            "I420 (Limited Range)"
5477        };
5478        for (fullcolor, expected) in [(true, i444), (false, "I420 (Limited Range)")] {
5479            let settings = CaptureSettings {
5480                width: 64,
5481                height: 64,
5482                codec: Codec::H264,
5483                use_cpu: true,
5484                video_fullcolor: fullcolor,
5485                ..Default::default()
5486            };
5487            let p = X11Pipeline::new(settings.clone());
5488            assert_eq!(
5489                p.encoder_name(),
5490                format!("CPU ({})", crate::encoders::software_library(Codec::H264))
5491            );
5492            assert_eq!(p.colorspace_desc(), expected);
5493            // The chroma and the range are each read from the session rather than assumed
5494            // equal: a 4:2:0 session converted at full range exists, so the two are no longer
5495            // the same answer.
5496            assert_eq!(
5497                p.colorspace_desc(),
5498                crate::encoders::colorspace_desc(
5499                    crate::encoders::session_fullcolor(None, &settings),
5500                    crate::encoders::session_full_range(None, &settings),
5501                ),
5502                "X11 and Wayland must describe the same session identically"
5503            );
5504        }
5505    }
5506
5507    /// A frame whose source reports every change is decided on its rectangles alone, on the
5508    /// CPU path and a full-frame session alike: the first a pipeline codes is sent whole
5509    /// whatever it reports, a change the rectangles name is sent, and one they do not name is
5510    /// not, since no hash is taken.
5511    #[test]
5512    fn a_reported_frame_is_decided_on_its_rectangles() {
5513        for codec in [Codec::H264, Codec::Vp8] {
5514            let mut p = X11Pipeline::new(CaptureSettings {
5515                width: 64,
5516                height: 64,
5517                codec,
5518                use_cpu: true,
5519                use_paint_over_quality: false,
5520                ..Default::default()
5521            });
5522            let mut pixels = vec![0x80u8; 64 * 64 * 4];
5523            let rect = [Rectangle::new((0, 0).into(), (16, 16).into())];
5524            assert!(
5525                !p.process_reported(&pixels, 256, &[]).is_empty(),
5526                "{codec:?} first"
5527            );
5528            assert!(
5529                p.process_reported(&pixels, 256, &[]).is_empty(),
5530                "{codec:?} still"
5531            );
5532            pixels[0] = 0;
5533            assert!(
5534                p.process_reported(&pixels, 256, &[]).is_empty(),
5535                "{codec:?} unnamed"
5536            );
5537            pixels[4] = 0;
5538            assert!(
5539                !p.process_reported(&pixels, 256, &rect).is_empty(),
5540                "{codec:?} named"
5541            );
5542        }
5543    }
5544
5545    /// A pipeline built with a report bound describes itself in it, and a frame it encodes is
5546    /// tallied with its bytes and its encode time.
5547    #[test]
5548    fn x11_pipeline_reports_the_session_it_settled_on() {
5549        let report = crate::report::StreamReport::new("x11");
5550        let _scope = crate::report::enter(&report);
5551        let mut p = X11Pipeline::new(CaptureSettings {
5552            width: 64,
5553            height: 64,
5554            codec: Codec::H264,
5555            use_cpu: true,
5556            ..Default::default()
5557        });
5558        let info = report.info();
5559        assert_eq!(info.encoder, crate::encoders::software_library(Codec::H264));
5560        assert!(!info.hardware);
5561        assert_eq!(info.encoder_reason, "software encoding selected");
5562        assert_eq!(info.codec, "h264");
5563        assert_eq!(info.stripes, 1);
5564        assert!(info.gpu.is_empty());
5565
5566        let pixels = vec![0x80u8; 64 * 64 * 4];
5567        let mut stripes = p.process(&pixels, 64 * 4);
5568        assert!(!stripes.is_empty(), "the first frame of a pipeline is sent");
5569        let start = crate::wayland::host::now_ns();
5570        crate::encoders::software::FrameTiming::stamp(
5571            &mut stripes,
5572            start - 2_000_000,
5573            start - 1_000_000,
5574        );
5575        report.tally(&stripes);
5576        let totals = report.totals();
5577        assert_eq!(totals.frames, 1);
5578        assert_eq!(
5579            totals.bytes,
5580            stripes.iter().map(|s| s.data.len() as u64).sum::<u64>()
5581        );
5582        assert!(totals.encode_ns >= 1_000_000 && totals.pipeline_ns >= 2_000_000);
5583    }
5584
5585    /// A software H.264 session where no engine may take the stream reshapes in place on a resize:
5586    /// software forced, encoding off the node, and a demotion, which a reshape at the same size
5587    /// keeps.
5588    #[test]
5589    fn x11_software_h264_reshapes_in_place_where_no_engine_may_take_it() {
5590        let s = CaptureSettings {
5591            width: 640,
5592            height: 384,
5593            codec: Codec::H264,
5594            use_cpu: true,
5595            ..Default::default()
5596        };
5597        let resized = CaptureSettings {
5598            width: 1280,
5599            height: 720,
5600            ..s.clone()
5601        };
5602        let mut p = X11Pipeline::new(s.clone());
5603        assert!(p.hw.is_none());
5604        assert!(p.reshape(&resized, true), "software was asked for");
5605
5606        let off_node = CaptureSettings {
5607            use_cpu: false,
5608            encode_node_index: -1,
5609            ..s.clone()
5610        };
5611        let mut p = X11Pipeline::new(off_node.clone());
5612        assert!(p.reshape(
5613            &CaptureSettings {
5614                width: 1280,
5615                height: 720,
5616                ..off_node
5617            },
5618            true
5619        ));
5620
5621        // The demotion `recover_hw` leaves.
5622        let mut p = X11Pipeline::new(s.clone());
5623        let live = CaptureSettings {
5624            use_cpu: false,
5625            ..s
5626        };
5627        assert!(p.reshape(&live, false));
5628        assert!(p.settings.use_cpu, "a reshape keeps the demotion");
5629        assert!(p.reshape(
5630            &CaptureSettings {
5631                width: 1280,
5632                height: 720,
5633                ..live
5634            },
5635            true
5636        ));
5637    }
5638
5639    /// A capture larger than the engine's maximum H.264 picture comes up in software, and a
5640    /// resize into the engine's range rebuilds it there. Ignored by default; run with the GPU set.
5641    #[test]
5642    #[ignore]
5643    fn gpu_x11_h264_past_the_engine_maximum_takes_the_engine_once_resized() {
5644        let big = CaptureSettings {
5645            width: 8192,
5646            height: 4096,
5647            ..crate::x11::gpu_test_support::settings(Codec::H264)
5648        };
5649        let mut p = X11Pipeline::new(big.clone());
5650        assert!(
5651            p.hw.is_none(),
5652            "8192x4096 is past every engine's H.264 maximum"
5653        );
5654        let resized = CaptureSettings {
5655            width: 1362,
5656            height: 1036,
5657            ..big
5658        };
5659        assert!(
5660            !p.reshape(&resized, true),
5661            "the engine may take the new size"
5662        );
5663        let rebuilt = X11Pipeline::new(resized);
5664        assert!(rebuilt.hw.as_ref().is_some_and(FrameEncoder::is_hardware));
5665    }
5666
5667    /// A JPEG stream's log line names the library that encodes it, as its report does.
5668    #[test]
5669    fn x11_jpeg_pipeline_names_its_encoder() {
5670        let report = crate::report::StreamReport::new("x11");
5671        let _scope = crate::report::enter(&report);
5672        let p = X11Pipeline::new(CaptureSettings {
5673            width: 64,
5674            height: 64,
5675            codec: Codec::Jpeg,
5676            use_cpu: true,
5677            ..Default::default()
5678        });
5679        assert_eq!(p.encoder_name(), format!("CPU ({})", report.info().encoder));
5680    }
5681
5682    /// On a host with a hardware encoder the report names it, the GPU it runs on, and the node.
5683    #[test]
5684    #[ignore]
5685    fn gpu_x11_pipeline_reports_the_hardware_session() {
5686        let report = crate::report::StreamReport::new("x11");
5687        let _scope = crate::report::enter(&report);
5688        let p = X11Pipeline::new(CaptureSettings {
5689            width: 1280,
5690            height: 720,
5691            codec: Codec::H264,
5692            target_fps: 60.0,
5693            video_crf: 25,
5694            ..Default::default()
5695        });
5696        assert!(p.is_hardware(), "needs a hardware encoder on render node 0");
5697        let info = report.info();
5698        println!("{info:?}");
5699        assert!(info.hardware);
5700        assert_eq!(info.encoder, p.encoder_name());
5701        assert!(info.encoder_reason.is_empty());
5702        assert_eq!(info.encode_node, 0);
5703        assert!(!info.driver.is_empty());
5704    }
5705
5706    /// On NVENC, which hashes a Turbo frame on the device: a still screen reads as still through
5707    /// its cleanup, whose capped frames are hashed on the CPU before they are coded, and cleans
5708    /// up. Mixed into the device's band state, the CPU hashes read every band as changed two
5709    /// frames in three, and the cleanup never came due. Ignored by default.
5710    #[test]
5711    #[ignore]
5712    fn gpu_a_turbo_still_screen_reads_still_through_its_cleanup() {
5713        let (w, h) = (1280usize, 720usize);
5714        let mut p = X11Pipeline::new(CaptureSettings {
5715            width: w as i32,
5716            height: h as i32,
5717            codec: Codec::H264,
5718            target_fps: 60.0,
5719            video_cbr_mode: true,
5720            video_bitrate_kbps: 500,
5721            video_streaming_mode: true,
5722            use_paint_over_quality: true,
5723            ..crate::x11::gpu_test_support::settings(Codec::H264)
5724        });
5725        assert!(p.is_hardware(), "needs NVENC");
5726        let mut seed = 0x9e37_79b9_7f4a_7c15u64;
5727        let picture: Vec<u8> = (0..w * h * 4)
5728            .map(|_| {
5729                seed ^= seed << 13;
5730                seed ^= seed >> 7;
5731                seed ^= seed << 17;
5732                seed as u8
5733            })
5734            .collect();
5735        let (mut changed, mut held) = (0, 0);
5736        for n in 0..360 {
5737            let _ = p.process(&picture, w * 4);
5738            if n >= 60 && band_damage(&p.turbo_dirty).is_dirty() {
5739                changed += 1;
5740            }
5741            held += p.hw_state.h264_burst_frames_remaining.min(1);
5742        }
5743        assert_eq!(changed, 0, "a still picture read as changed");
5744        assert!(held > 0, "no cleanup ran");
5745    }
5746
5747    /// With no motion, no request, and nothing for a cleanup to improve (the rate control codes
5748    /// at the paint-over quality), nothing is emitted at any frame-counter position: the GOP is
5749    /// infinite, so there is no scheduled IDR to break the silence.
5750    #[test]
5751    fn static_frames_stay_silent_without_request() {
5752        let s = CaptureSettings {
5753            use_paint_over_quality: false,
5754            ..hw_settings()
5755        };
5756        let clean = EncoderQuality {
5757            last: Some(s.video_paintover_crf as u32),
5758            ..HOLDS
5759        };
5760        let mut st = StripeState::default();
5761        for fc in [0u16, 1, 120, 240] {
5762            let d = decide_hw_fullframe(&mut st, &s, fc, Damage::None, false, false, clean);
5763            assert!(!d.send && !d.force_idr, "frame {fc} should stay idle");
5764        }
5765    }
5766
5767    /// With paint-over off, a forced keyframe on a still screen still opens the recovery burst,
5768    /// so a constant-rate session can refine it.
5769    #[test]
5770    fn requested_idr_recovers_even_without_paint_over() {
5771        let s = CaptureSettings {
5772            use_paint_over_quality: false,
5773            video_paintover_burst_frames: 5,
5774            ..hw_settings()
5775        };
5776        let mut st = StripeState::default();
5777        let d = decide_hw_fullframe(&mut st, &s, 5, Damage::None, false, true, HOLDS);
5778        assert!(d.send && d.force_idr && d.hold_qp.is_none());
5779        assert_eq!(
5780            st.h264_burst_frames_remaining, 5,
5781            "recovery burst armed without paint-over"
5782        );
5783        let d = decide_hw_fullframe(&mut st, &s, 6, Damage::None, false, false, HOLDS);
5784        assert!(d.send && !d.force_idr);
5785        assert_eq!(d.target_qp, 25);
5786    }
5787
5788    /// A key frame asked for on a still screen already cleaned up codes it again at the rate
5789    /// control's quality, so the screen is due its cleanup again a trigger period on, as after a
5790    /// loss; otherwise it stays as the key frame left it (VP8 and SVT-AV1 at 0.5 Mbit/s: 23-33 dB
5791    /// for good where the cleanup had reached 37-48).
5792    #[test]
5793    fn a_key_frame_asked_for_on_a_clean_screen_is_cleaned_up_again() {
5794        let s = hw_settings();
5795        let held = EncoderQuality {
5796            last: Some(30),
5797            ..HOLDS
5798        };
5799        let mut st = StripeState::default();
5800        decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, held);
5801        let cleaned = |st: &mut StripeState, from: u16| {
5802            (from..from + 40)
5803                .filter(|&i| {
5804                    decide_hw_fullframe(st, &s, i, Damage::None, false, false, held)
5805                        .hold_qp
5806                        .is_some()
5807                })
5808                .count()
5809        };
5810        assert!(cleaned(&mut st, 1) > 0, "the still screen is cleaned up");
5811        assert!(
5812            (41..80).all(|i| {
5813                !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, held).send
5814            }),
5815            "and goes quiet"
5816        );
5817        let key = decide_hw_fullframe(&mut st, &s, 80, Damage::None, false, true, held);
5818        assert!(
5819            key.send && key.force_idr && key.hold_qp.is_none(),
5820            "the key frame asked for at the rate control's quality"
5821        );
5822        assert!(cleaned(&mut st, 81) > 0, "then the cleanup again");
5823    }
5824
5825    #[test]
5826    fn configured_interval_restores_scheduled_keyframes() {
5827        let mut s = hw_settings();
5828        s.keyframe_interval_s = 2.0;
5829        assert!(periodic_idr_due(&s, 0));
5830        assert!(!periodic_idr_due(&s, 1));
5831        assert!(periodic_idr_due(&s, 120));
5832        s.keyframe_interval_s = 0.0;
5833        assert!(!periodic_idr_due(&s, 0) && !periodic_idr_due(&s, 120));
5834    }
5835
5836    /// A held refresh of a constant-rate session is coarsened from the rate control's last
5837    /// quantizer to what a second of the target buys; a constant quality, or a rate control
5838    /// that coded near the paint-over quantizer, gets the paint-over one.
5839    #[test]
5840    fn a_held_refresh_fits_the_budget_a_constant_rate_leaves() {
5841        let cbr = CaptureSettings {
5842            video_cbr_mode: true,
5843            video_paintover_crf: 18,
5844            target_fps: 30.0,
5845            ..hw_settings()
5846        };
5847        let q = |s: &CaptureSettings, last| {
5848            held_refresh_quality(
5849                s,
5850                EncoderQuality {
5851                    last,
5852                    bytes: None,
5853                    holds: true,
5854                    reopens: false,
5855                    keys: true,
5856                    band: None,
5857                    measures: false,
5858                    psnr: None,
5859                    refit: None,
5860                    follows_cap: false,
5861                    finer: 0,
5862                    ends_on_key: false,
5863                },
5864            )
5865        };
5866        assert_eq!(
5867            q(&cbr, Some(51)),
5868            22,
5869            "thirty frames of budget are 29 steps finer than the rate control"
5870        );
5871        assert_eq!(q(&cbr, Some(45)), 18);
5872        assert_eq!(q(&cbr, Some(30)), 18);
5873        assert_eq!(q(&cbr, None), 18);
5874        let crf = CaptureSettings {
5875            video_cbr_mode: false,
5876            ..cbr
5877        };
5878        assert_eq!(
5879            q(&crf, Some(45)),
5880            18,
5881            "a constant quality has no budget to fit"
5882        );
5883    }
5884
5885    /// Damage rectangles report the share of the frame they cover, clipped to it.
5886    #[test]
5887    fn damage_rectangles_report_their_share_of_the_frame() {
5888        use smithay::utils::Rectangle;
5889        assert_eq!(Damage::of_rects(&[], 100, 100), Damage::None);
5890        let quarter = [Rectangle::new((0, 0).into(), (50, 50).into())];
5891        assert_eq!(Damage::of_rects(&quarter, 100, 100), Damage::Area(0.25));
5892        let past = [Rectangle::new((80, 80).into(), (50, 50).into())];
5893        assert_eq!(
5894            Damage::of_rects(&past, 100, 100),
5895            Damage::Area(0.04),
5896            "clipped to the frame"
5897        );
5898        let whole = [Rectangle::new((0, 0).into(), (200, 200).into())];
5899        assert_eq!(Damage::of_rects(&whole, 100, 100), Damage::Area(1.0));
5900    }
5901
5902    /// The banded content hash reports the share of bands that changed, and nothing when the
5903    /// frame is the same.
5904    #[test]
5905    fn hash_damage_reports_the_bands_that_changed() {
5906        let (w, h) = (32usize, 256usize);
5907        let mut bands = Vec::new();
5908        let frame = vec![7u8; w * 4 * h];
5909        assert_eq!(
5910            hash_damage(&mut bands, &frame, w * 4, h, 10, 20),
5911            Damage::Area(1.0),
5912            "the first frame is all new"
5913        );
5914        assert_eq!(
5915            hash_damage(&mut bands, &frame, w * 4, h, 10, 20),
5916            Damage::None
5917        );
5918        let mut caret = frame.clone();
5919        caret[w * 4 * (DAMAGE_BAND_ROWS + 1)] = 0;
5920        let n = h.div_ceil(DAMAGE_BAND_ROWS);
5921        assert_eq!(
5922            hash_damage(&mut bands, &caret, w * 4, h, 10, 20),
5923            Damage::Area(1.0 / n as f32),
5924            "one band of {n}"
5925        );
5926    }
5927
5928    /// The rows a band hash found changed map onto full-width rectangles, one per run of bands,
5929    /// the last clipped to the frame; before any hash the whole frame is dirty.
5930    #[test]
5931    fn band_rects_cover_the_runs_of_changed_bands() {
5932        let r = |y: i32, h: i32| Rectangle::new((0, y).into(), (64, h).into());
5933        assert_eq!(band_rects(&[], 64, 100), vec![r(0, 100)]);
5934        assert_eq!(band_rects(&[false, false, false, false], 64, 100), vec![]);
5935        assert_eq!(
5936            band_rects(&[true, true, false, true], 64, 100),
5937            vec![r(0, 64), r(96, 4)]
5938        );
5939        assert_eq!(band_damage(&[true, true, false, true]), Damage::Area(0.75));
5940        assert_eq!(band_damage(&[]), Damage::Area(1.0));
5941    }
5942
5943    /// Band hashes taken elsewhere read as the share of the bands that changed: a caret's band of
5944    /// a 1080p screen is a small change and three are motion; a first frame, or one of another
5945    /// height, is all new.
5946    #[test]
5947    fn hashed_damage_reports_the_bands_that_changed() {
5948        let n = 1080usize.div_ceil(DAMAGE_BAND_ROWS);
5949        let frame: Vec<u64> = (0..n as u64).collect();
5950        let mut last = Vec::new();
5951        assert_eq!(hashed_damage(&mut last, frame.clone()), Damage::Area(1.0));
5952        assert_eq!(hashed_damage(&mut last, frame.clone()), Damage::None);
5953        let mut caret = frame.clone();
5954        caret[6] ^= 1;
5955        let d = hashed_damage(&mut last, caret.clone());
5956        assert_eq!(d, Damage::Area(1.0 / n as f32));
5957        assert!(!d.is_motion());
5958        let mut window = caret.clone();
5959        for band in &mut window[10..13] {
5960            *band ^= 1;
5961        }
5962        assert!(hashed_damage(&mut last, window).is_motion());
5963        assert_eq!(
5964            hashed_damage(&mut last, frame[..n - 1].to_vec()),
5965            Damage::Area(1.0)
5966        );
5967    }
5968}
5969
5970#[cfg(test)]
5971mod refresh_tests {
5972    use super::*;
5973    use crate::encoders::codec::{FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
5974    use crate::webcam::decode::{Decoder as _, VideoDecoder};
5975
5976    const W: usize = 320;
5977    const H: usize = 240;
5978    const SCROLL: usize = 30;
5979
5980    /// Code a page through the X11 pipeline, in software or on the GPU (`cpu`): scrolled for
5981    /// `SCROLL` frames under a peak of `peak` kbps (0 uncapped), then held still under `then`,
5982    /// for `frames` in all, a caret blinking in its corner every other still frame but the last
5983    /// where `caret`, reported as a change of the whole frame, as a capture that cannot place one
5984    /// reports it (NvFBC's); the PSNR of the last picture shown against the page, the frames sent
5985    /// while still, the key frames after the first, and whether coarse blocks are left.
5986    fn scroll_then_still(
5987        codec: Codec,
5988        cpu: bool,
5989        peak: i32,
5990        then: i32,
5991        frames: usize,
5992        caret: bool,
5993    ) -> (f64, usize, usize, bool) {
5994        let (psnr, still, keys, coarse, _) =
5995            scroll_then_still_painted(codec, cpu, peak, then, frames, caret, false, 0);
5996        (psnr, still, keys, coarse)
5997    }
5998
5999    /// `scroll_then_still` with the paint-over armed or not (`paint`, at quality 18), the page
6000    /// held still under `peak` for `hold` frames before `then` takes over, also returning the
6001    /// bytes the still frames came to.
6002    #[allow(clippy::too_many_arguments)]
6003    fn scroll_then_still_painted(
6004        codec: Codec,
6005        cpu: bool,
6006        peak: i32,
6007        then: i32,
6008        frames: usize,
6009        caret: bool,
6010        paint: bool,
6011        hold: usize,
6012    ) -> (f64, usize, usize, bool, usize) {
6013        let vbv = if peak > 0 {
6014            crate::encoders::CAPPED_VBV_FRAMES
6015        } else {
6016            0.0
6017        };
6018        let mut p = X11Pipeline::new(CaptureSettings {
6019            width: W as i32,
6020            height: H as i32,
6021            codec,
6022            use_cpu: cpu,
6023            target_fps: 60.0,
6024            video_crf: 25,
6025            video_paintover_crf: 18,
6026            use_paint_over_quality: paint,
6027            video_bitrate_kbps: if peak > 0 { peak } else { 8000 },
6028            video_vbv_multiplier: vbv,
6029            ..Default::default()
6030        });
6031        let whole = [Rectangle::new((0, 0).into(), (W as i32, H as i32).into())];
6032        let mut dec = VideoDecoder::new(codec).unwrap();
6033        let (mut still, mut keys, mut bytes) = (0, 0, 0);
6034        for t in 0..frames {
6035            if t == SCROLL + 1 + hold && peak > 0 {
6036                p.update_rate(then, vbv, 60.0);
6037            }
6038            let mut frame = crate::encoders::scrolled_text(W, H, t.min(SCROLL));
6039            let blinks = caret && t > SCROLL;
6040            if blinks && (t - SCROLL) % 2 == 1 && t + 1 < frames {
6041                for y in H - 16..H {
6042                    frame[(y * W + W - 16) * 4..(y * W + W) * 4].fill(255);
6043                }
6044            }
6045            let changed: &[_] = if t <= SCROLL || blinks { &whole } else { &[] };
6046            for f in p.process_reported(&frame, W * 4, changed) {
6047                still += usize::from(t > SCROLL);
6048                if t > SCROLL {
6049                    bytes += f.data.len() - VIDEO_HEADER_LEN;
6050                }
6051                keys += usize::from(
6052                    t > 0 && parse_video_type(f.data[1]).map(|(_, k)| k) == Some(FRAME_KEY),
6053                );
6054                assert!(dec.decode(&f.data[VIDEO_HEADER_LEN..]).expect("decode"));
6055            }
6056        }
6057        let shown = dec.frame().unwrap();
6058        let page = crate::encoders::scrolled_text(W, H, SCROLL);
6059        let mut mse = 0.0;
6060        for y in 0..H {
6061            for x in 0..W {
6062                let want = 16.0 + f64::from(page[(y * W + x) * 4]) * 219.0 / 255.0;
6063                let d = f64::from(shown.y[y * shown.y_stride + x]) - want;
6064                mse += d * d;
6065            }
6066        }
6067        let psnr = 10.0 * (255.0f64 * 255.0 / (mse / (W * H) as f64)).log10();
6068        (psnr, still, keys, p.hw_state.ledger.coarse(), bytes)
6069    }
6070
6071    /// A capped constant quality bound to its peak counts no block clean that a refinement past
6072    /// its own quality planned: its rate control floors the blocks there, and they are refined
6073    /// again once the peak lets go; under a constant rate, or released, they are.
6074    #[test]
6075    fn a_bound_capped_quality_counts_no_block_clean_past_its_own_quality() {
6076        let settings = CaptureSettings {
6077            width: 64,
6078            height: 16,
6079            video_crf: 25,
6080            video_bitrate_kbps: 4000,
6081            target_fps: 60.0,
6082            ..Default::default()
6083        };
6084        let quality = EncoderQuality {
6085            last: Some(51),
6086            bytes: None,
6087            holds: false,
6088            reopens: false,
6089            keys: false,
6090            band: None,
6091            measures: false,
6092            psnr: None,
6093            refit: None,
6094            follows_cap: false,
6095            finer: 0,
6096            ends_on_key: false,
6097        };
6098        for (bound, cbr, clean) in [
6099            (true, false, false),
6100            (true, true, true),
6101            (false, false, true),
6102        ] {
6103            let mut st = StripeState {
6104                ledger: crate::ledger::BlockLedger::new(64, 16),
6105                ..Default::default()
6106            };
6107            st.ledger.frame([]);
6108            let plan = st
6109                .ledger
6110                .plan(0, usize::MAX, usize::MAX)
6111                .expect("every block");
6112            let d = HwFrameDecision {
6113                send: true,
6114                force_idr: false,
6115                target_qp: 25,
6116                hold_qp: None,
6117                hold_band: None,
6118                measure: false,
6119                cap: None,
6120                refine: Some(crate::ledger::Refinement {
6121                    plan,
6122                    target: 15,
6123                    from: if bound { 51 } else { 25 },
6124                    reach: u32::MAX,
6125                }),
6126                constant: None,
6127                refresh: None,
6128                defer: None,
6129            };
6130            let settings = CaptureSettings {
6131                video_cbr_mode: cbr,
6132                ..settings.clone()
6133            };
6134            refinement_coded(&mut st, &settings, &d, quality, 100, bound, bound && !cbr);
6135            assert_eq!(
6136                st.ledger.settled(),
6137                clean,
6138                "bound {bound}, constant rate {cbr}"
6139            );
6140        }
6141    }
6142
6143    /// A refreshing frame the encoder refused (no bytes) refreshes nothing: its blocks stay
6144    /// coarse for the next refresh frame, and the pace does not read it as one under budget.
6145    #[test]
6146    fn a_refresh_frame_that_came_out_as_nothing_refreshes_nothing() {
6147        let settings = CaptureSettings {
6148            width: 64,
6149            height: 16,
6150            video_crf: 25,
6151            video_bitrate_kbps: 4000,
6152            target_fps: 60.0,
6153            ..Default::default()
6154        };
6155        let mut st = StripeState {
6156            ledger: crate::ledger::BlockLedger::new(64, 16),
6157            ..Default::default()
6158        };
6159        st.ledger
6160            .frame([&Rectangle::new((0, 0).into(), (64, 16).into())]);
6161        st.ledger.coded(true, false);
6162        st.ledger.frame([]);
6163        let most = st.refresh.most(st.ledger.blocks());
6164        let (now, _) = st.ledger.refresh(usize::MAX).expect("coarse blocks");
6165        let d = HwFrameDecision {
6166            send: true,
6167            force_idr: false,
6168            target_qp: 25,
6169            hold_qp: None,
6170            hold_band: None,
6171            measure: false,
6172            cap: None,
6173            refine: None,
6174            constant: None,
6175            refresh: Some(now),
6176            defer: None,
6177        };
6178        let quality = EncoderQuality {
6179            last: None,
6180            bytes: None,
6181            holds: false,
6182            reopens: false,
6183            keys: false,
6184            band: None,
6185            measures: false,
6186            psnr: None,
6187            refit: None,
6188            follows_cap: false,
6189            finer: 0,
6190            ends_on_key: false,
6191        };
6192        refinement_coded(&mut st, &settings, &d, quality, 0, false, false);
6193        assert!(
6194            st.ledger.coarse(),
6195            "a frame of no bytes refreshed its blocks"
6196        );
6197        assert_eq!(st.refresh.most(st.ledger.blocks()), most, "the pace moved");
6198        refinement_coded(&mut st, &settings, &d, quality, 100, false, false);
6199        assert!(!st.ledger.coarse());
6200    }
6201
6202    /// A capped libvpx quality its peak bound while the page scrolled codes the still page it
6203    /// leaves again once the peak lets go, without a key frame, to the uncapped session's
6204    /// quality, and leaves no coarse block.
6205    #[test]
6206    fn a_capped_libvpx_quality_refreshes_what_it_coded_bound() {
6207        for codec in [Codec::Vp8, Codec::Vp9] {
6208            let (plain, plain_still, _, _, plain_bytes) =
6209                scroll_then_still_painted(codec, true, 0, 0, 150, false, false, 0);
6210            let (capped, still, keys, coarse, bytes) =
6211                scroll_then_still_painted(codec, true, 100, 100_000, 150, false, false, 0);
6212            println!(
6213                "{codec:?}: uncapped {plain:.2} dB ({plain_still} frames, {plain_bytes} bytes while \
6214                 still), capped and refreshed {capped:.2} dB ({still} frames, {bytes} bytes)"
6215            );
6216            assert_eq!(keys, 0, "{codec:?}: a key frame");
6217            assert!(!coarse, "{codec:?}: coarse blocks left");
6218            assert!(still > 0, "{codec:?}: nothing refreshed the still page");
6219            assert!(
6220                capped >= plain,
6221                "{codec:?}: refreshed to {capped:.2} dB against {plain:.2}"
6222            );
6223        }
6224    }
6225
6226    /// A capped SVT-AV1 quantizer its peak bound while the page scrolled codes the still page it
6227    /// leaves again once the peak lets go, without a key frame, to the uncapped session's
6228    /// quality, and leaves no coarse block.
6229    #[test]
6230    #[cfg(svtav1_sequence_qp)]
6231    fn a_capped_svtav1_quantizer_refreshes_what_it_coded_bound() {
6232        let (plain, plain_still, _, _, plain_bytes) =
6233            scroll_then_still_painted(Codec::Av1, true, 0, 0, 150, false, false, 0);
6234        let (capped, still, keys, coarse, bytes) =
6235            scroll_then_still_painted(Codec::Av1, true, 100, 100_000, 150, false, false, 0);
6236        println!(
6237            "Av1: uncapped {plain:.2} dB ({plain_still} frames, {plain_bytes} bytes while still), \
6238             capped and refreshed {capped:.2} dB ({still} frames, {bytes} bytes)"
6239        );
6240        assert_eq!(keys, 0, "a key frame");
6241        assert!(!coarse, "coarse blocks left");
6242        assert!(still > 0, "nothing refreshed the still page");
6243        assert!(
6244            capped >= plain,
6245            "refreshed to {capped:.2} dB against {plain:.2}"
6246        );
6247    }
6248
6249    /// `comes_to_the_paint_over` on SVT-AV1.
6250    #[test]
6251    #[cfg(svtav1_sequence_qp)]
6252    fn a_capped_svtav1_quantizer_comes_to_the_paint_over_once_its_peak_lets_go() {
6253        comes_to_the_paint_over(Codec::Av1, true);
6254    }
6255
6256    /// NVENC's capped constant quantizer, bound while the page scrolled, codes the still page
6257    /// again once the peak lets go, without a key frame, to the uncapped session's quality.
6258    /// Ignored by default.
6259    #[test]
6260    #[ignore]
6261    fn gpu_a_capped_nvenc_quantizer_refreshes_what_it_coded_bound() {
6262        let (plain, _, _, _) = scroll_then_still(Codec::H264, false, 0, 0, 150, false);
6263        let (capped, still, keys, coarse) =
6264            scroll_then_still(Codec::H264, false, 100, 100_000, 150, false);
6265        println!(
6266            "NVENC: uncapped {plain:.2} dB, capped and refreshed {capped:.2} dB, {still} frames sent while still"
6267        );
6268        assert_eq!(keys, 0, "a key frame");
6269        assert!(!coarse, "coarse blocks left");
6270        assert!(still > 0, "nothing refreshed the still page");
6271        assert!(
6272            capped >= plain - 1.0,
6273            "refreshed to {capped:.2} dB against {plain:.2}"
6274        );
6275    }
6276
6277    /// NVENC's capped constant quantizer, bound while the page scrolled, codes the page again
6278    /// under a change the capture cannot place once the peak lets go: the ledger reads it as a
6279    /// change of the whole frame, which the encoder codes at the session's quantizer from the
6280    /// coarse picture before it, to the uncapped session's quality, without a key frame.
6281    /// Ignored by default.
6282    #[test]
6283    #[ignore]
6284    fn gpu_a_capped_nvenc_quantizer_codes_again_under_a_change_it_cannot_place() {
6285        let (plain, _, _, _) = scroll_then_still(Codec::H264, false, 0, 0, 150, true);
6286        let (capped, _, keys, coarse) =
6287            scroll_then_still(Codec::H264, false, 100, 100_000, 150, true);
6288        println!("NVENC under a caret: uncapped {plain:.2} dB, capped {capped:.2} dB");
6289        assert_eq!(keys, 0, "a key frame");
6290        assert!(!coarse, "coarse blocks left");
6291        assert!(
6292            capped >= plain - 1.0,
6293            "coded again to {capped:.2} dB against {plain:.2}"
6294        );
6295    }
6296    /// A capped quality its peak bound while the page scrolled, with the paint-over armed,
6297    /// ends where the uncapped session's paint-over leaves the page once a fast link's peak lets
6298    /// go, with no key frame: where the paint-over falls due after the peak let go, and where it
6299    /// falls due under the binding peak (held still 60 frames under it), spent short of its
6300    /// target, the still blocks owed it. `codec` through the X11 pipeline in software or on the
6301    /// GPU (`cpu`).
6302    fn comes_to_the_paint_over(codec: Codec, cpu: bool) {
6303        let (plain, plain_still, _, _, plain_bytes) =
6304            scroll_then_still_painted(codec, cpu, 0, 0, 300, false, true, 0);
6305        for hold in [0, 60] {
6306            let (capped, still, keys, coarse, bytes) =
6307                scroll_then_still_painted(codec, cpu, 100, 100_000, 300, false, true, hold);
6308            println!(
6309                "{codec:?} paint-over: uncapped {plain:.2} dB ({plain_still} frames, \
6310                 {plain_bytes} bytes while still), capped, held {hold} frames, {capped:.2} dB \
6311                 ({still} frames, {bytes} bytes)"
6312            );
6313            assert_eq!(keys, 0, "{codec:?}: a key frame");
6314            assert!(!coarse, "{codec:?}: coarse blocks left");
6315            assert!(
6316                capped >= plain - 0.5,
6317                "{codec:?}, held {hold}: {capped:.2} dB against the uncapped paint-over's {plain:.2}"
6318            );
6319        }
6320    }
6321
6322    #[test]
6323    fn a_capped_libvpx_quality_comes_to_the_paint_over_once_its_peak_lets_go() {
6324        for codec in [Codec::Vp8, Codec::Vp9] {
6325            comes_to_the_paint_over(codec, true);
6326        }
6327    }
6328
6329    /// `comes_to_the_paint_over` on NVENC. Ignored by default.
6330    #[test]
6331    #[ignore]
6332    fn gpu_a_capped_nvenc_quantizer_comes_to_the_paint_over_once_its_peak_lets_go() {
6333        comes_to_the_paint_over(Codec::H264, false);
6334    }
6335}
6336
6337#[cfg(test)]
6338mod vbv_tests {
6339    /// VBV sizing policy: an infinite GOP uses 1.5 frames of headroom, scheduled keyframes
6340    /// relax to 3 frames, and an explicit multiplier overrides both and rescales with bitrate.
6341    #[test]
6342    fn vbv_policy() {
6343        use crate::encoders::vbv_bits;
6344        let frame = 4_000_000f64 / 60.0;
6345        assert_eq!(
6346            vbv_bits(4_000_000, 60.0, 0.0, 0.0),
6347            (frame * 1.5).round() as u32
6348        );
6349        assert_eq!(
6350            vbv_bits(4_000_000, 60.0, 2.0, 0.0),
6351            (frame * 3.0).round() as u32
6352        );
6353        assert_eq!(vbv_bits(4_000_000, 60.0, 2.0, 1.0), frame.round() as u32);
6354        assert_eq!(
6355            vbv_bits(8_000_000, 60.0, 0.0, 1.0),
6356            (2.0 * frame).round() as u32
6357        );
6358    }
6359}