1use 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#[derive(Clone, Copy, Debug, PartialEq)]
24pub enum Damage {
25 None,
27 Area(f32),
29 Unknown,
31}
32
33const SMALL_AREA: f32 = 1.0 / 16.0;
36
37const UNKNOWN_MASS: f32 = 1.0 / 16.0;
41const UNKNOWN_RUN: u32 = 3;
42
43const KEY_CLEANUP_MASS: f32 = 0.5;
46
47const KEY_STILL_TRIGGERS: u32 = 4;
49
50const LOW_MOTION_TRIGGERS: u32 = 4;
55const LOW_MOTION_SHARE: f32 = 0.25;
56
57const CONVERGED_SHARE: f64 = 0.25;
61
62const STALL_TRIGGERS: u32 = 2;
66
67const CONVERGE_S: f64 = 10.0;
69
70const OVERSHOOT_BUDGETS: f64 = 2.0;
75
76pub(crate) const EMPTY_SLICE_BYTES: usize = 32;
80
81pub(crate) const EMPTY_S: f64 = 3.0;
87
88const SAME_FRAME_SHARE: f64 = 0.125;
95const SAME_FRAME_QUALITY: u32 = 45;
96
97const REFINE_S: f64 = 30.0;
103
104const LEVEL_S: f64 = 4.0;
110
111const FIRST_BAND: f64 = 1.0 / 64.0;
118
119const LEAST_BAND: f64 = 1.0 / 4096.0;
122
123const SWEEP_FINER_SHARE: f64 = 0.8;
126const BAND_BUDGETS: f64 = 1.0;
127
128impl Damage {
129 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 pub fn is_dirty(self) -> bool {
154 self != Damage::None
155 }
156
157 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#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum Cleanup {
170 None,
171 Refresh,
173 Key,
175}
176
177pub 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 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 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 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 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
318fn 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
324fn 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
352pub 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
377pub fn refresh_start(trigger: u32, converges: bool) -> u32 {
384 if converges { 1 } else { trigger.max(1) }
385}
386
387#[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 pub follows_cap: bool,
412 pub finer: u32,
415 pub ends_on_key: bool,
418}
419
420impl EncoderQuality {
421 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 pub fn converges(&self) -> bool {
443 self.bytes.is_some()
444 }
445}
446
447#[derive(Clone, Copy, Debug, PartialEq)]
450pub enum Convergence {
451 Refining,
453 Converged,
455 Idle,
458}
459
460pub 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
479const PLATEAU_TRIGGERS: u32 = 4;
486const PLATEAU_GAIN_DB: f32 = 0.05;
487const PLATEAU_CHECKS: u32 = 2;
488
489pub(crate) const CLEANUP_FRAME_S: f64 = 0.2;
492
493const CLEANUP_RATE_MAX: i32 = 4;
496
497pub fn frame_budget(kbps: i32, fps: f64) -> f64 {
499 f64::from(kbps.max(1)) * 125.0 / fps.max(1.0)
500}
501
502fn 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
517pub 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
530fn 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
541fn 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
565pub fn converge_frames(settings: &CaptureSettings) -> i32 {
567 (CONVERGE_S * settings.target_fps.max(1.0)).round() as i32
568}
569
570pub fn refine_frames(settings: &CaptureSettings) -> i32 {
573 (REFINE_S * settings.target_fps.max(1.0)).round() as i32
574}
575
576fn 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
585pub 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
605pub struct HwFrameDecision {
607 pub send: bool,
608 pub force_idr: bool,
609 pub target_qp: u32,
611 pub hold_qp: Option<u32>,
614 pub hold_band: Option<(f64, f64)>,
617 pub measure: bool,
620 pub cap: Option<usize>,
622 pub refine: Option<crate::ledger::Refinement>,
624 pub constant: Option<Vec<bool>>,
627 pub refresh: Option<crate::ledger::BlockPlan>,
631 pub defer: Option<crate::ledger::BlockPlan>,
632}
633
634impl HwFrameDecision {
635 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 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#[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 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 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 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#[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 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 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 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
905pub 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
928pub fn held_refresh_quality(settings: &CaptureSettings, encoder: EncoderQuality) -> u32 {
937 refresh_quality(settings, encoder, 1.0)
938}
939
940fn 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
956pub 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
1093fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1467fn 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
1482fn 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
1494fn 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
1593pub(crate) const DAMAGE_BAND_ROWS: usize = 32;
1597
1598fn 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
1629fn 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
1648fn 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
1661pub(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
1678fn 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
1692fn 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#[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
1750fn 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
1791fn 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
1812fn 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
1826pub struct X11Pipeline {
1839 settings: CaptureSettings,
1840 stripes: Vec<StripeState>,
1841 hw: Option<FrameEncoder>,
1843 hw_state: StripeState,
1844 bands: Vec<StripeState>,
1846 checked: Vec<StripeState>,
1850 turbo_dirty: Vec<bool>,
1853 report_whole: bool,
1857 frame_counter: u16,
1858 pending_force_idr: bool,
1859 hw_error_streak: u32,
1862 hw_rebuilt: bool,
1863 #[cfg(test)]
1866 hold: [(Option<u32>, bool); 2],
1867}
1868
1869impl X11Pipeline {
1870 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 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 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 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 pub fn request_idr(&mut self) {
1968 self.pending_force_idr = true;
1969 }
1970
1971 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 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 pub fn codec(&self) -> Codec {
2011 self.settings.codec
2012 }
2013
2014 pub fn is_hardware(&self) -> bool {
2016 self.hw.as_ref().is_some_and(|enc| enc.is_hardware())
2017 }
2018
2019 pub fn capped(&self) -> bool {
2022 encoders::session_capped(self.hw.as_ref(), &self.settings)
2023 }
2024
2025 pub fn fixed_rate_control(&self) -> Option<&'static str> {
2028 self.hw.as_ref().and_then(FrameEncoder::fixed_rate_control)
2029 }
2030
2031 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 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 pub fn reshape(&mut self, settings: &CaptureSettings, size_changed: bool) -> bool {
2066 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 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 eprintln!("[X11] rate reconfigure failed: {e}");
2124 self.recover_hw();
2125 }
2126 }
2127
2128 pub fn set_link_rate(&mut self, kbps: i32) {
2131 self.settings.video_link_kbps = kbps;
2132 }
2133
2134 pub fn update_tunables(&mut self, t: &crate::LiveTunables) {
2137 t.apply_to(&mut self.settings);
2138 }
2139
2140 pub fn process(&mut self, argb: &[u8], stride: usize) -> Vec<EncodedStripe> {
2160 self.process_damaged(argb, stride, &[])
2161 }
2162
2163 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 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 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 pub fn held_input(&self) -> Option<*const u8> {
2206 held_input(&self.stripes)
2207 }
2208
2209 pub fn release_input(&mut self) {
2211 release_input(&mut self.stripes);
2212 }
2213
2214 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 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 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 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 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 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 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 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 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 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 fn crf_settings() -> CaptureSettings {
2605 CaptureSettings {
2606 video_cbr_mode: false,
2607 ..hw_settings()
2608 }
2609 }
2610
2611 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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}