1use crate::RustCaptureSettings;
14use crate::encoders::software::{
15 EncodedStripe, StripeState, acknowledge_reference, encode_cpu, invalidate_reference,
16 stripes_held_still,
17};
18use crate::encoders::{self, Codec, FrameEncoder, FrameSource};
19use smithay::utils::{Physical, Rectangle};
20use std::sync::Arc;
21
22#[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 FALLBACK_TRIGGERS: u32 = 6;
71
72const CONVERGE_S: f64 = 10.0;
74
75const OVERSHOOT_BUDGETS: f64 = 2.0;
80
81pub(crate) const EMPTY_SLICE_BYTES: usize = 32;
85
86pub(crate) const EMPTY_S: f64 = 3.0;
92
93const SAME_FRAME_SHARE: f64 = 0.125;
100const SAME_FRAME_QUALITY: u32 = 45;
101
102const REFINE_S: f64 = 30.0;
108
109const LEVEL_S: f64 = 4.0;
115
116const FIRST_BAND: f64 = 1.0 / 64.0;
123const BAND_BUDGETS: f64 = 1.0;
124
125impl Damage {
126 pub fn of_rects<'a>(
129 rects: impl IntoIterator<Item = &'a smithay::utils::Rectangle<i32, smithay::utils::Physical>>,
130 width: i32,
131 height: i32,
132 ) -> Self {
133 let frame = (width.max(1) as f64) * (height.max(1) as f64);
134 let mut area = 0f64;
135 for r in rects {
136 let w = (r.loc.x + r.size.w).min(width) - r.loc.x.max(0);
137 let h = (r.loc.y + r.size.h).min(height) - r.loc.y.max(0);
138 if w > 0 && h > 0 {
139 area += w as f64 * h as f64;
140 }
141 }
142 if area > 0.0 {
143 Damage::Area((area / frame).min(1.0) as f32)
144 } else {
145 Damage::None
146 }
147 }
148
149 pub fn is_dirty(self) -> bool {
151 self != Damage::None
152 }
153
154 pub(crate) fn is_motion(self) -> bool {
156 match self {
157 Damage::None => false,
158 Damage::Area(f) => f > SMALL_AREA,
159 Damage::Unknown => true,
160 }
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq)]
166pub enum Cleanup {
167 None,
168 Refresh,
170 Key,
172}
173
174pub fn cleanup_due(
201 st: &mut StripeState,
202 trigger: u32,
203 improves: bool,
204 now: bool,
205 keys: bool,
206 damage: Damage,
207) -> Cleanup {
208 if damage.is_dirty() {
209 st.dirty_run = st.dirty_run.saturating_add(1);
210 st.no_motion_frame_count = 0;
211 st.paint_over_sent = false;
212 st.change_mass += match damage {
213 Damage::Area(f) => f,
214 _ if st.dirty_run >= UNKNOWN_RUN => UNKNOWN_MASS,
215 _ => 0.0,
216 };
217 } else {
218 st.dirty_run = 0;
219 st.no_motion_frame_count = st.no_motion_frame_count.saturating_add(1);
220 }
221 if !st.paint_over_sent {
222 st.unclean_frames = st.unclean_frames.saturating_add(1);
223 }
224 st.motion = recent_motion(st.motion, trigger.max(1), damage.is_motion());
225 let due = due_cleanup(
226 st,
227 trigger.max(1),
228 keys,
229 st.no_motion_frame_count,
230 st.unclean_frames,
231 st.motion,
232 );
233 if due == Cleanup::None {
234 return Cleanup::None;
235 }
236 if !improves {
237 st.unclean_frames = 0;
238 return Cleanup::None;
239 }
240 if !now {
241 return Cleanup::None;
242 }
243 st.paint_over_sent = true;
244 st.unclean_frames = 0;
245 if due == Cleanup::Key {
246 st.change_mass = 0.0;
247 }
248 due
249}
250
251fn recent_motion(motion: f32, trigger: u32, moved: bool) -> f32 {
254 motion + (moved as u8 as f32 - motion) / (trigger * LOW_MOTION_TRIGGERS) as f32
255}
256
257fn due_cleanup(
260 st: &StripeState,
261 trigger: u32,
262 keys: bool,
263 still: u32,
264 unclean: u32,
265 motion: f32,
266) -> Cleanup {
267 let low_motion = !st.paint_over_sent
268 && unclean >= trigger * LOW_MOTION_TRIGGERS
269 && motion <= LOW_MOTION_SHARE;
270 if keys
271 && st.change_mass >= KEY_CLEANUP_MASS
272 && (still >= trigger * KEY_STILL_TRIGGERS || low_motion)
273 {
274 Cleanup::Key
275 } else if !st.paint_over_sent && (still >= trigger || low_motion) {
276 Cleanup::Refresh
277 } else {
278 Cleanup::None
279 }
280}
281
282pub fn cleanup_pending(st: &StripeState, trigger: u32, enabled: bool, keys: bool) -> bool {
285 let unclean = st.unclean_frames.saturating_add(!st.paint_over_sent as u32);
286 let motion = recent_motion(st.motion, trigger.max(1), false);
287 enabled
288 && due_cleanup(
289 st,
290 trigger.max(1),
291 keys,
292 st.no_motion_frame_count.saturating_add(1),
293 unclean,
294 motion,
295 ) != Cleanup::None
296}
297
298#[derive(Clone, Copy, Debug, PartialEq)]
308pub struct EncoderQuality {
309 pub last: Option<u32>,
310 pub bytes: Option<usize>,
311 pub holds: bool,
312 pub reopens: bool,
313 pub keys: bool,
314 pub band: Option<usize>,
315 pub measures: bool,
316 pub psnr: Option<f32>,
317}
318
319impl EncoderQuality {
320 pub fn of(encoder: &FrameEncoder) -> Self {
322 Self {
323 last: encoder.last_quality(),
324 bytes: encoder.last_size(),
325 holds: encoder.holds_quantizer(),
326 reopens: encoder.reopens_on_quality(),
327 keys: encoder.cleans_up_with_key(),
328 band: encoder.band_size(),
329 measures: encoder.measures(),
330 psnr: encoder.last_psnr(),
331 }
332 }
333
334 pub fn converges(&self) -> bool {
338 self.bytes.is_some()
339 }
340}
341
342#[derive(Clone, Copy, Debug, PartialEq)]
345pub enum Convergence {
346 Refining,
348 Converged,
350 Idle,
353}
354
355pub fn convergence(
357 last: Option<u32>,
358 bytes: Option<usize>,
359 paint: u32,
360 budget: f64,
361) -> Convergence {
362 match bytes {
363 Some(b) if (b as f64) <= budget * CONVERGED_SHARE => {
364 if last.is_some_and(|q| q <= paint) {
365 Convergence::Converged
366 } else {
367 Convergence::Idle
368 }
369 }
370 _ => Convergence::Refining,
371 }
372}
373
374const PLATEAU_TRIGGERS: u32 = 4;
381const PLATEAU_GAIN_DB: f32 = 0.05;
382const PLATEAU_CHECKS: u32 = 2;
383
384pub fn frame_budget(kbps: i32, fps: f64) -> f64 {
386 kbps.max(1) as f64 * 125.0 / fps.max(1.0)
387}
388
389fn band_share(size: f64, bytes: Option<usize>, budget: f64) -> f64 {
393 let next = match bytes {
394 Some(b) if b > 0 => (size * BAND_BUDGETS * budget / b as f64).clamp(size / 2.0, size * 2.0),
395 _ => size,
396 };
397 next.clamp(1.0 / 4096.0, 1.0)
398}
399
400fn plateau(st: &mut StripeState, psnr: Option<f32>, period: u32) -> (bool, bool) {
406 if std::mem::take(&mut st.measuring)
407 && let Some(now) = psnr
408 {
409 let level = st
410 .measured
411 .is_some_and(|before| now - before < PLATEAU_GAIN_DB);
412 st.level_checks = if level { st.level_checks + 1 } else { 0 };
413 st.measured = Some(now);
414 }
415 if st.measure_in == 0 {
416 st.measure_in = period;
417 st.measuring = true;
418 } else {
419 st.measure_in -= 1;
420 }
421 (st.measuring, st.level_checks >= PLATEAU_CHECKS)
422}
423
424pub fn converge_frames(settings: &RustCaptureSettings) -> i32 {
426 (CONVERGE_S * settings.target_fps.max(1.0)).round() as i32
427}
428
429fn cleanup_frames(settings: &RustCaptureSettings, encoder: EncoderQuality) -> i32 {
431 if encoder.holds || (encoder.last.is_none() && !encoder.measures) {
432 converge_frames(settings)
433 } else {
434 (REFINE_S * settings.target_fps.max(1.0)).round() as i32
435 }
436}
437
438pub fn paint_over_improves(settings: &RustCaptureSettings, encoder: EncoderQuality) -> bool {
448 let paint = settings.video_paintover_crf.max(0) as u32;
449 settings.use_paint_over_quality
450 && if settings.video_cbr_mode {
451 encoder.last.is_none_or(|q| q > paint)
452 } else {
453 settings.video_paintover_crf < settings.video_crf
454 }
455}
456
457pub struct HwFrameDecision {
459 pub send: bool,
460 pub force_idr: bool,
461 pub target_qp: u32,
463 pub hold_qp: Option<u32>,
466 pub hold_band: Option<(f64, f64)>,
469 pub measure: bool,
472}
473
474impl HwFrameDecision {
475 pub(crate) fn cleanup_quality(&self, normal: u32) -> Option<u32> {
478 self.hold_qp
479 .filter(|_| self.hold_band.is_none())
480 .or((self.target_qp != normal).then_some(self.target_qp))
481 }
482
483 pub fn prepare(&self, encoder: &mut FrameEncoder) {
486 if let Some(q) = self.hold_qp {
487 encoder.hold_quantizer(q, self.hold_band);
488 }
489 if self.measure {
490 encoder.measure();
491 }
492 }
493}
494
495pub fn periodic_idr_due(settings: &RustCaptureSettings, frame_counter: u16) -> bool {
509 let secs = settings.keyframe_interval_s;
510 if secs <= 0.0 {
511 return false;
512 }
513 let safe_fps = settings.target_fps.max(1.0);
514 let interval = ((safe_fps * secs).round() as u64).max(1);
515 (frame_counter as u64).is_multiple_of(interval)
516}
517
518pub fn held_refresh_quality(settings: &RustCaptureSettings, encoder: EncoderQuality) -> u32 {
524 refresh_quality(settings, encoder, 1.0)
525}
526
527fn refresh_quality(settings: &RustCaptureSettings, encoder: EncoderQuality, spent: f64) -> u32 {
531 let paint = settings.video_paintover_crf.max(0) as u32;
532 match (settings.video_cbr_mode, encoder.last) {
533 (true, Some(last)) => {
534 let frames = (crate::encoders::HELD_KEY_BUDGET_S * settings.target_fps
535 / spent.max(1.0))
536 .max(1.0);
537 paint.max(last.saturating_sub((6.0 * frames.log2()).floor() as u32))
538 }
539 _ => paint,
540 }
541}
542
543pub fn decide_hw_fullframe(
630 st: &mut StripeState,
631 settings: &RustCaptureSettings,
632 frame_counter: u16,
633 damage: Damage,
634 is_animated: bool,
635 requested_idr: bool,
636 encoder: EncoderQuality,
637) -> HwFrameDecision {
638 if !settings.video_cbr_mode {
639 return decide_constant_quality(
640 st,
641 settings,
642 frame_counter,
643 damage,
644 is_animated,
645 requested_idr,
646 encoder,
647 );
648 }
649 let normal_qp = settings.video_crf as u32;
650 let paint_qp = settings.video_paintover_crf as u32;
651 let burst = settings.video_paintover_burst_frames;
652 let improves = paint_over_improves(settings, encoder);
653 let recovery_idr = requested_idr || periodic_idr_due(settings, frame_counter);
654 let converges = encoder.converges();
655 let holds = encoder.holds && !converges;
656 let sweeps = holds && encoder.band.is_some();
659 let cleanup = cleanup_due(
660 st,
661 settings.paint_over_trigger_frames,
662 improves,
663 true,
664 holds && encoder.keys,
665 damage,
666 );
667 let refresh_qp = held_refresh_quality(settings, encoder);
668 let cleanup_qp = match cleanup {
669 Cleanup::Refresh if holds => Some(refresh_qp),
670 Cleanup::Key => Some(paint_qp),
671 _ => None,
672 };
673 let mut d = HwFrameDecision {
674 send: false,
675 force_idr: false,
676 target_qp: normal_qp,
677 hold_qp: None,
678 hold_band: None,
679 measure: false,
680 };
681 if damage.is_dirty() {
682 if !converges || damage.is_motion() {
683 st.h264_burst_frames_remaining = 0;
684 }
685 if damage.is_motion() {
688 st.sweep = None;
689 st.settled = false;
690 }
691 d.send = true;
692 d.force_idr = recovery_idr || cleanup == Cleanup::Key;
693 d.hold_qp = cleanup_qp;
694 if sweeps && cleanup == Cleanup::Refresh && !d.force_idr {
695 sweep_refresh(st, &mut d);
696 }
697 return d;
698 }
699 if cleanup != Cleanup::None || recovery_idr {
700 d.send = true;
701 d.force_idr = recovery_idr || cleanup == Cleanup::Key;
702 d.hold_qp = cleanup_qp;
703 if recovery_idr {
704 st.sweep = None;
705 } else if sweeps && cleanup == Cleanup::Refresh && !d.force_idr {
706 sweep_refresh(st, &mut d);
707 }
708 if converges && (cleanup != Cleanup::None || (burst > 0 && recovery_idr)) {
709 if (st.h264_burst_frames_remaining <= 0 && !st.settled) || recovery_idr {
710 st.h264_burst_frames_remaining = cleanup_frames(settings, encoder);
711 st.idle_frames = 0;
712 st.over_frames = 0;
713 st.same_frames = 0;
714 st.rc_bytes = 0;
715 st.finest = None;
716 st.finest_frames = 0;
717 st.fine_frames = 0;
718 st.measured = None;
719 st.level_checks = 0;
720 st.measuring = false;
721 st.measure_in = 0;
722 }
723 st.burst_held = false;
724 } else if burst > 0 && (d.force_idr || cleanup != Cleanup::None) && st.sweep.is_none() {
725 st.h264_burst_frames_remaining = burst;
728 st.burst_held =
729 holds && (cleanup != Cleanup::None || (improves && !settings.video_cbr_mode));
730 }
731 return d;
732 }
733 if let Some((from, size)) = st.sweep {
734 let budget = frame_budget(settings.video_bitrate_kbps, settings.target_fps);
735 let size = band_share(size, encoder.band, budget);
736 let to = (from + size).min(1.0);
737 st.sweep_frames = st.sweep_frames.saturating_add(1);
738 let left = (REFINE_S * settings.target_fps.max(1.0)).round() - st.sweep_frames as f64;
739 let paced = encoder
740 .band
741 .is_some_and(|b| b as f64 <= 2.0 * BAND_BUDGETS * budget);
742 if paced && (1.0 - to) / size > left.max(1.0) {
743 st.sweep_coarser += 1;
744 }
745 let q = refresh_qp + st.sweep_coarser;
746 if encoder.last.is_some_and(|last| q >= last) {
747 st.sweep = None;
748 return d;
749 }
750 st.sweep = (to < 1.0).then_some((to, size));
751 d.send = true;
752 d.hold_qp = Some(q);
753 d.hold_band = Some((from, to));
754 return d;
755 }
756 let budget = frame_budget(settings.video_bitrate_kbps, settings.target_fps);
757 let trigger = settings.paint_over_trigger_frames.max(1);
758 if converges && st.h264_burst_frames_remaining > 0 {
759 st.over_frames = if encoder
760 .bytes
761 .is_some_and(|b| b as f64 > budget * OVERSHOOT_BUDGETS)
762 {
763 st.over_frames.saturating_add(1)
764 } else {
765 0
766 };
767 }
768 let pinned = converges && st.h264_burst_frames_remaining > 0 && st.over_frames >= trigger;
769 if pinned && !(improves && encoder.holds) {
770 st.h264_burst_frames_remaining = 0;
771 st.settled = !encoder.holds;
772 }
773 if converges && st.h264_burst_frames_remaining > 0 {
774 match convergence(encoder.last, encoder.bytes, paint_qp, budget) {
775 Convergence::Converged if !encoder.measures => {
776 st.h264_burst_frames_remaining = 0;
777 st.change_mass = 0.0;
778 st.settled = !encoder.holds;
779 }
780 Convergence::Converged | Convergence::Idle => {
781 st.idle_frames = st.idle_frames.saturating_add(1)
782 }
783 Convergence::Refining => st.idle_frames = 0,
784 }
785 st.fine_frames = if encoder.last.is_some_and(|q| q <= paint_qp) {
786 st.fine_frames.saturating_add(1)
787 } else {
788 0
789 };
790 }
791 if st.h264_burst_frames_remaining > 0 {
792 st.h264_burst_frames_remaining -= 1;
793 d.send = true;
794 d.hold_qp = (st.burst_held && improves).then_some(refresh_qp);
795 let elapsed = (converge_frames(settings) - st.h264_burst_frames_remaining).max(0) as u32;
796 let stalled = st.idle_frames >= STALL_TRIGGERS * trigger;
797 if converges && !encoder.holds {
798 let reached = settings.use_paint_over_quality && st.fine_frames >= trigger;
799 let (measure, level) = plateau(st, encoder.psnr, PLATEAU_TRIGGERS * trigger);
800 d.measure = measure;
801 let done = if encoder.measures { level } else { reached };
802 let bytes = encoder.bytes.unwrap_or(0);
803 st.same_frames = if bytes > 0
804 && bytes.abs_diff(st.rc_bytes) as f64 <= st.rc_bytes as f64 * SAME_FRAME_SHARE
805 {
806 st.same_frames.saturating_add(1)
807 } else {
808 0
809 };
810 st.rc_bytes = bytes;
811 let same = st.same_frames as f64 >= EMPTY_S * settings.target_fps
812 && encoder.last.is_some_and(|q| q >= SAME_FRAME_QUALITY);
813 if let Some(q) = encoder.last {
814 if st.finest.is_none_or(|finest| q < finest) {
815 st.finest = Some(q);
816 st.finest_frames = 0;
817 } else {
818 st.finest_frames = st.finest_frames.saturating_add(1);
819 }
820 }
821 let level = st.finest_frames as f64 >= LEVEL_S * settings.target_fps;
822 if ((stalled || same || level) && !encoder.measures)
823 || done
824 || st.h264_burst_frames_remaining == 0
825 {
826 st.h264_burst_frames_remaining = 0;
827 st.settled = true;
828 }
829 } else if converges
830 && improves
831 && (stalled || pinned || elapsed >= FALLBACK_TRIGGERS * trigger)
832 {
833 d.hold_qp = Some(if pinned {
834 refresh_quality(
835 settings,
836 encoder,
837 encoder.bytes.unwrap_or(0) as f64 / budget,
838 )
839 } else {
840 refresh_qp
841 });
842 st.h264_burst_frames_remaining = 0;
843 if encoder.band.is_some() && !pinned {
844 start_sweep(st, &mut d);
845 }
846 }
847 return d;
848 }
849 d.send = settings.video_streaming_mode || is_animated;
850 d
851}
852
853fn start_sweep(st: &mut StripeState, d: &mut HwFrameDecision) {
856 d.hold_band = Some((0.0, FIRST_BAND));
857 st.sweep = Some((FIRST_BAND, FIRST_BAND));
858 st.sweep_frames = 0;
859 st.sweep_coarser = 0;
860}
861
862fn sweep_refresh(st: &mut StripeState, d: &mut HwFrameDecision) {
867 if st.sweep.is_some() {
868 d.hold_qp = None;
869 } else {
870 start_sweep(st, d);
871 }
872}
873
874fn decide_constant_quality(
893 st: &mut StripeState,
894 settings: &RustCaptureSettings,
895 frame_counter: u16,
896 damage: Damage,
897 is_animated: bool,
898 requested_idr: bool,
899 encoder: EncoderQuality,
900) -> HwFrameDecision {
901 let normal_qp = settings.video_crf as u32;
902 let paint_qp = settings.video_paintover_crf as u32;
903 let burst = settings.video_paintover_burst_frames;
904 let improves =
905 settings.use_paint_over_quality && settings.video_paintover_crf < settings.video_crf;
906 let recovery_idr = requested_idr || periodic_idr_due(settings, frame_counter);
907 let mut cleanup = cleanup_due(
908 st,
909 settings.paint_over_trigger_frames,
910 improves,
911 true,
912 true,
913 damage,
914 );
915 if damage.is_motion() || !improves {
916 st.clean_quality = false;
917 }
918 if cleanup == Cleanup::Refresh && encoder.reopens && !st.clean_quality {
919 cleanup = Cleanup::Key;
920 st.change_mass = 0.0;
921 }
922 if cleanup != Cleanup::None {
923 st.clean_quality = true;
924 }
925 let quality = if st.clean_quality {
926 paint_qp
927 } else {
928 normal_qp
929 };
930 let mut d = HwFrameDecision {
931 send: false,
932 force_idr: false,
933 target_qp: quality,
934 hold_qp: None,
935 hold_band: None,
936 measure: false,
937 };
938 if recovery_idr && cleanup == Cleanup::None {
939 d.target_qp = normal_qp;
940 }
941 if damage.is_dirty() {
942 st.h264_burst_frames_remaining = 0;
943 d.send = true;
944 d.force_idr = recovery_idr || cleanup == Cleanup::Key;
945 return d;
946 }
947 if cleanup != Cleanup::None || recovery_idr {
948 d.send = true;
949 d.force_idr = recovery_idr || cleanup == Cleanup::Key;
950 if burst > 0 {
951 st.h264_burst_frames_remaining = burst;
952 st.burst_held = improves;
953 }
954 return d;
955 }
956 if st.h264_burst_frames_remaining > 0 {
957 st.h264_burst_frames_remaining -= 1;
958 d.send = true;
959 if st.burst_held {
960 d.target_qp = paint_qp;
961 }
962 return d;
963 }
964 d.send = settings.video_streaming_mode || is_animated;
965 d
966}
967
968pub(crate) const DAMAGE_BAND_ROWS: usize = 32;
972
973fn hash_bands(
980 bands: &mut Vec<StripeState>,
981 pixels: &[u8],
982 stride: usize,
983 height: usize,
984 threshold: u32,
985 duration: i32,
986) -> Vec<bool> {
987 let n = height.div_ceil(DAMAGE_BAND_ROWS).max(1);
988 if bands.len() != n {
989 bands.clear();
990 bands.resize_with(n, StripeState::default);
991 }
992 bands
993 .iter_mut()
994 .enumerate()
995 .map(|(i, band)| {
996 let end = ((i + 1) * DAMAGE_BAND_ROWS).min(height) * stride;
997 let bytes =
998 &pixels[(i * DAMAGE_BAND_ROWS * stride).min(pixels.len())..end.min(pixels.len())];
999 band.content_dirty(bytes, threshold, duration)
1000 })
1001 .collect()
1002}
1003
1004fn hashed_bands(
1007 bands: &mut Vec<StripeState>,
1008 hashes: &[u64],
1009 threshold: u32,
1010 duration: i32,
1011) -> Vec<bool> {
1012 if bands.len() != hashes.len() {
1013 bands.clear();
1014 bands.resize_with(hashes.len(), StripeState::default);
1015 }
1016 bands
1017 .iter_mut()
1018 .zip(hashes)
1019 .map(|(band, &hash)| band.hash_dirty(hash, threshold, duration))
1020 .collect()
1021}
1022
1023fn band_damage(dirty: &[bool]) -> Damage {
1026 let changed = dirty.iter().filter(|&&d| d).count();
1027 if dirty.is_empty() {
1028 Damage::Area(1.0)
1029 } else if changed == 0 {
1030 Damage::None
1031 } else {
1032 Damage::Area(changed as f32 / dirty.len() as f32)
1033 }
1034}
1035
1036pub(crate) fn hashed_damage(last: &mut Vec<u64>, hashes: Vec<u64>) -> Damage {
1040 let dirty: Vec<bool> = if last.len() == hashes.len() {
1041 hashes
1042 .iter()
1043 .zip(last.iter())
1044 .map(|(a, b)| a != b)
1045 .collect()
1046 } else {
1047 Vec::new()
1048 };
1049 *last = hashes;
1050 band_damage(&dirty)
1051}
1052
1053fn hash_damage(
1055 bands: &mut Vec<StripeState>,
1056 pixels: &[u8],
1057 stride: usize,
1058 height: usize,
1059 threshold: u32,
1060 duration: i32,
1061) -> Damage {
1062 band_damage(&hash_bands(
1063 bands, pixels, stride, height, threshold, duration,
1064 ))
1065}
1066
1067fn band_rects(dirty: &[bool], width: i32, height: i32) -> Vec<Rectangle<i32, Physical>> {
1071 if dirty.is_empty() {
1072 return vec![Rectangle::new((0, 0).into(), (width, height).into())];
1073 }
1074 let mut rects = Vec::new();
1075 let mut run = None;
1076 for (i, &changed) in dirty.iter().chain(std::iter::once(&false)).enumerate() {
1077 match (changed, run) {
1078 (true, None) => run = Some(i),
1079 (false, Some(first)) => {
1080 let top = (first * DAMAGE_BAND_ROWS) as i32;
1081 let bottom = ((i * DAMAGE_BAND_ROWS) as i32).min(height);
1082 rects.push(Rectangle::new(
1083 (0, top).into(),
1084 (width, bottom - top).into(),
1085 ));
1086 run = None;
1087 }
1088 _ => {}
1089 }
1090 }
1091 rects
1092}
1093
1094#[allow(clippy::too_many_arguments)]
1099fn hash_beside<R>(
1100 bands: &mut Vec<StripeState>,
1101 pixels: &[u8],
1102 stride: usize,
1103 height: usize,
1104 threshold: u32,
1105 duration: i32,
1106 encode: impl FnOnce() -> R,
1107) -> (R, Vec<bool>) {
1108 let (out, hashed) = std::thread::scope(|s| {
1109 let hashing = std::thread::Builder::new()
1110 .name("pxf-x11-hash".into())
1111 .spawn_scoped(s, || {
1112 hash_bands(bands, pixels, stride, height, threshold, duration)
1113 });
1114 let out = encode();
1115 (out, hashing.ok().map(|h| h.join().unwrap_or_default()))
1116 });
1117 let dirty =
1118 hashed.unwrap_or_else(|| hash_bands(bands, pixels, stride, height, threshold, duration));
1119 (out, dirty)
1120}
1121
1122pub struct X11Pipeline {
1134 settings: RustCaptureSettings,
1135 stripes: Vec<StripeState>,
1136 stripes_carrying: f32,
1138 hw: Option<FrameEncoder>,
1140 hw_state: StripeState,
1141 bands: Vec<StripeState>,
1143 turbo_dirty: Vec<bool>,
1146 frame_counter: u16,
1147 pending_force_idr: bool,
1148 hw_error_streak: u32,
1151 hw_rebuilt: bool,
1152 #[cfg(test)]
1155 hold: [(Option<u32>, bool); 2],
1156}
1157
1158impl X11Pipeline {
1159 pub fn new(mut settings: RustCaptureSettings) -> Self {
1165 let hw = encoders::select_frame_encoder(
1166 &mut settings,
1167 FrameSource::Host { rgba: false },
1168 None,
1169 "X11",
1170 );
1171 let pipeline = Self {
1172 settings,
1173 stripes: Vec::new(),
1174 stripes_carrying: 1.0,
1175 hw,
1176 hw_state: StripeState::default(),
1177 bands: Vec::new(),
1178 turbo_dirty: Vec::new(),
1179 frame_counter: 0,
1180 pending_force_idr: false,
1181 hw_error_streak: 0,
1182 hw_rebuilt: false,
1183 #[cfg(test)]
1184 hold: [(None, false); 2],
1185 };
1186 pipeline.record_stream();
1187 pipeline
1188 }
1189
1190 fn record_stream(&self) {
1192 let fullframe = self.hw.is_some() || self.settings.video_fullframe;
1193 crate::report::stream(
1194 &self.settings,
1195 encoders::software::stripe_count(self.settings.height, self.settings.codec, fullframe),
1196 self.hw
1197 .as_ref()
1198 .map(|enc| (enc.backend_name(), enc.is_hardware())),
1199 encoders::session_fullcolor(self.hw.as_ref(), &self.settings),
1200 encoders::session_full_range(self.hw.as_ref(), &self.settings),
1201 );
1202 crate::report::bit_depth(encoders::session_bit_depth(
1203 self.hw.as_ref(),
1204 &self.settings,
1205 ));
1206 }
1207
1208 fn recover_hw(&mut self) {
1217 self.hw_error_streak = 0;
1218 if self.hw_rebuilt {
1219 eprintln!(
1220 "[X11] HW encoder unrecoverable; demoting to software encoding ({}).",
1221 crate::encoders::software_library(self.settings.codec)
1222 );
1223 self.hw = None;
1227 self.settings.use_cpu = true;
1228 self.hw = encoders::select_frame_encoder(
1229 &mut self.settings,
1230 FrameSource::Host { rgba: false },
1231 None,
1232 "X11",
1233 );
1234 crate::report::encoder_reason(
1235 "the hardware encoder failed repeatedly and was given up",
1236 );
1237 } else {
1238 eprintln!("[X11] rebuilding HW encoder after repeated encode errors.");
1239 self.hw = None;
1240 self.hw = encoders::select_frame_encoder(
1241 &mut self.settings,
1242 FrameSource::Host { rgba: false },
1243 None,
1244 "X11",
1245 );
1246 self.hw_rebuilt = true;
1247 }
1248 self.record_stream();
1249 self.hw_state = StripeState::default();
1250 self.stripes.clear();
1251 self.pending_force_idr = true;
1252 }
1253
1254 pub fn request_idr(&mut self) {
1256 self.pending_force_idr = true;
1257 }
1258
1259 pub fn take_report(&mut self, report: crate::encoders::reference::ReferenceReport) {
1262 use crate::encoders::reference::ReferenceReport;
1263 let taken = match (self.hw.as_mut(), report) {
1264 (Some(enc), report) => enc.take_report(report),
1265 (None, ReferenceReport::Lost(frame_id)) => {
1266 invalidate_reference(&mut self.stripes, frame_id)
1267 }
1268 (None, ReferenceReport::Held(frame_id)) => {
1269 acknowledge_reference(&mut self.stripes, frame_id, true);
1270 true
1271 }
1272 (None, ReferenceReport::Sent(frame_id)) => {
1273 acknowledge_reference(&mut self.stripes, frame_id, false);
1274 true
1275 }
1276 };
1277 if !taken {
1278 self.pending_force_idr = true;
1279 }
1280 }
1281
1282 pub fn encoder_name(&self) -> String {
1285 match &self.hw {
1286 Some(enc) if enc.is_hardware() => enc.backend_name().to_string(),
1287 Some(enc) => format!("CPU ({})", enc.backend_name()),
1288 None if !self.settings.codec.is_video() => "CPU (turbojpeg)".to_string(),
1289 None => format!("CPU ({})", crate::encoders::software_library(Codec::H264)),
1290 }
1291 }
1292
1293 pub fn codec(&self) -> Codec {
1295 self.settings.codec
1296 }
1297
1298 pub fn is_hardware(&self) -> bool {
1300 self.hw.as_ref().is_some_and(|enc| enc.is_hardware())
1301 }
1302
1303 pub fn fixed_rate_control(&self) -> Option<&'static str> {
1306 self.hw.as_ref().and_then(FrameEncoder::fixed_rate_control)
1307 }
1308
1309 pub fn holds_quantizer(&self) -> bool {
1313 self.hw.as_ref().map_or_else(
1314 || crate::encoders::software::stripes_hold_quantizer(&self.settings),
1315 FrameEncoder::holds_quantizer,
1316 )
1317 }
1318
1319 pub fn colorspace_desc(&self) -> &'static str {
1323 let fullcolor = encoders::session_fullcolor(self.hw.as_ref(), &self.settings);
1324 crate::encoders::colorspace_desc(
1325 fullcolor,
1326 encoders::session_full_range(self.hw.as_ref(), &self.settings),
1327 )
1328 }
1329
1330 pub fn reshape(&mut self, settings: &RustCaptureSettings, size_changed: bool) -> bool {
1342 if !size_changed {
1343 if let Some(FrameEncoder::Nvenc(enc)) = &mut self.hw {
1344 enc.release_pinned_hosts();
1345 }
1346 self.settings = settings.clone();
1347 return true;
1348 }
1349 match &mut self.hw {
1350 Some(FrameEncoder::Nvenc(enc)) => {
1351 if let Err(e) = enc.reconfigure_resolution(settings) {
1352 eprintln!("[X11] NVENC in-place resize unavailable ({e}); rebuilding");
1353 return false;
1354 }
1355 }
1356 None => {}
1357 _ => return false,
1358 }
1359 let codec = self.settings.codec;
1360 self.settings = settings.clone();
1361 self.settings.codec = codec;
1362 self.stripes.clear();
1363 self.hw_state = StripeState::default();
1364 self.turbo_dirty.clear();
1365 true
1366 }
1367
1368 pub fn update_rate(&mut self, bitrate_kbps: i32, vbv_multiplier: f64, fps: f64) {
1375 self.settings.video_bitrate_kbps = bitrate_kbps;
1376 self.settings.video_vbv_multiplier = vbv_multiplier;
1377 if fps > 0.0 {
1378 self.settings.target_fps = fps;
1379 }
1380 if let Some(enc) = self.hw.as_mut()
1381 && let Err(e) = enc.reconfigure_rate(&self.settings)
1382 {
1383 eprintln!("[X11] rate reconfigure failed: {e}");
1386 self.recover_hw();
1387 }
1388 }
1389
1390 pub fn update_tunables(&mut self, t: &crate::LiveTunables) {
1393 t.apply_to(&mut self.settings);
1394 }
1395
1396 pub fn process(&mut self, argb: &[u8], stride: usize) -> Vec<EncodedStripe> {
1416 let width = self.settings.width;
1417 let height = self.settings.height;
1418 let requested = self.pending_force_idr;
1419 let threshold = self.settings.damage_block_threshold;
1420 let duration = self.settings.damage_block_duration as i32;
1421
1422 let out = if self.hw.is_some() {
1423 let turbo = self.settings.video_streaming_mode;
1424 let damage = if turbo {
1425 band_damage(&self.turbo_dirty)
1426 } else {
1427 hash_damage(
1428 &mut self.bands,
1429 argb,
1430 stride,
1431 height as usize,
1432 threshold,
1433 duration,
1434 )
1435 };
1436 let quality = EncoderQuality::of(self.hw.as_ref().unwrap());
1437 let mut d = decide_hw_fullframe(
1438 &mut self.hw_state,
1439 &self.settings,
1440 self.frame_counter,
1441 damage,
1442 false,
1443 requested,
1444 quality,
1445 );
1446 let normal = self.settings.video_crf as u32;
1453 #[cfg(test)]
1454 let decided = (d.cleanup_quality(normal), d.force_idr);
1455 let mut hashed = None;
1456 if turbo
1457 && self.settings.use_paint_over_quality
1458 && d.send
1459 && d.cleanup_quality(normal).is_some()
1460 {
1461 let dirty = hash_bands(
1462 &mut self.bands,
1463 argb,
1464 stride,
1465 height as usize,
1466 threshold,
1467 duration,
1468 );
1469 if band_damage(&dirty).is_motion() {
1470 d.hold_qp = None;
1471 d.target_qp = normal;
1472 d.force_idr = requested || periodic_idr_due(&self.settings, self.frame_counter);
1473 }
1474 hashed = Some(dirty);
1475 }
1476 #[cfg(test)]
1477 {
1478 self.hold = [decided, (d.cleanup_quality(normal), d.force_idr)];
1479 }
1480 let fc = self.frame_counter as u64;
1481 if d.send || self.hw.as_ref().unwrap().holds_frame() {
1482 let force_idr = d.force_idr;
1483 let enc = self.hw.as_mut().unwrap();
1484 d.prepare(enc);
1485 let beside = hashed.is_none() && turbo && self.settings.use_paint_over_quality;
1489 let on_device = beside && d.send && enc.hash_next_upload(DAMAGE_BAND_ROWS as u32);
1490 let mut encode = || {
1491 if d.send {
1492 enc.encode_host(argb, stride, false, fc, d.target_qp, force_idr)
1493 } else {
1494 enc.push_held(fc)
1495 }
1496 };
1497 let res = if let Some(dirty) = hashed {
1498 self.turbo_dirty = dirty;
1499 encode()
1500 } else if on_device {
1501 let res = encode();
1502 self.turbo_dirty = match enc.take_upload_hashes() {
1503 Some(hashes) => hashed_bands(&mut self.bands, &hashes, threshold, duration),
1504 None => hash_bands(
1505 &mut self.bands,
1506 argb,
1507 stride,
1508 height as usize,
1509 threshold,
1510 duration,
1511 ),
1512 };
1513 res
1514 } else if beside {
1515 let (res, dirty) = hash_beside(
1516 &mut self.bands,
1517 argb,
1518 stride,
1519 height as usize,
1520 threshold,
1521 duration,
1522 encode,
1523 );
1524 self.turbo_dirty = dirty;
1525 res
1526 } else {
1527 self.turbo_dirty.clear();
1528 encode()
1529 };
1530 match res {
1531 Ok(data) if !data.is_empty() => {
1532 self.hw_error_streak = 0;
1533 self.hw_rebuilt = false;
1534 let codec = self.settings.codec;
1535 enc.delivered_units(data, self.frame_counter)
1536 .into_iter()
1537 .map(|(data, id, reference)| EncodedStripe {
1538 data: Arc::new(data),
1539 codec,
1540 stripe_y_start: 0,
1541 stripe_height: height,
1542 frame_id: id as i32,
1543 timing: Default::default(),
1544 reference,
1545 })
1546 .collect()
1547 }
1548 Ok(_) => {
1549 self.hw_error_streak = 0;
1550 self.hw_rebuilt = false;
1551 Vec::new()
1552 }
1553 Err(e) => {
1554 if self
1557 .hw_error_streak
1558 .is_multiple_of(crate::HW_ERROR_RECOVERY_THRESHOLD)
1559 {
1560 eprintln!("[X11] HW encode error: {e}");
1561 }
1562 self.hw_error_streak = self.hw_error_streak.saturating_add(1);
1563 if self.hw_error_streak >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1564 self.recover_hw();
1565 }
1566 Vec::new()
1567 }
1568 }
1569 } else {
1570 Vec::new()
1571 }
1572 } else {
1573 debug_assert_eq!(
1574 stride,
1575 width as usize * 4,
1576 "software encode path assumes tightly-packed rows (stride == width*4)"
1577 );
1578 let force_idr_all = requested
1579 || (self.settings.codec.is_video()
1580 && periodic_idr_due(&self.settings, self.frame_counter));
1581 if self.settings.codec.is_video() && self.settings.video_streaming_mode {
1582 let hashed = (self.settings.use_paint_over_quality
1586 && stripes_held_still(&self.stripes, &self.settings))
1587 .then(|| {
1588 hash_bands(
1589 &mut self.bands,
1590 argb,
1591 stride,
1592 height as usize,
1593 threshold,
1594 duration,
1595 )
1596 });
1597 let rects = match &hashed {
1598 Some(dirty) => {
1599 let seen: Vec<bool> = dirty
1600 .iter()
1601 .enumerate()
1602 .map(|(i, &d)| d || self.turbo_dirty.get(i).is_none_or(|&b| b))
1603 .collect();
1604 band_rects(&seen, width, height)
1605 }
1606 None => band_rects(&self.turbo_dirty, width, height),
1607 };
1608 let (stripes, carrying, settings) = (
1609 &mut self.stripes,
1610 &mut self.stripes_carrying,
1611 &self.settings,
1612 );
1613 let mut encode = || {
1614 encode_cpu(
1615 stripes,
1616 carrying,
1617 argb,
1618 width,
1619 height,
1620 &rects,
1621 settings,
1622 self.frame_counter,
1623 false,
1624 false,
1625 force_idr_all,
1626 )
1627 };
1628 if let Some(dirty) = hashed {
1629 self.turbo_dirty = dirty;
1630 encode()
1631 } else if self.settings.use_paint_over_quality {
1632 let (out, dirty) = hash_beside(
1633 &mut self.bands,
1634 argb,
1635 stride,
1636 height as usize,
1637 threshold,
1638 duration,
1639 encode,
1640 );
1641 self.turbo_dirty = dirty;
1642 out
1643 } else {
1644 self.turbo_dirty.clear();
1645 encode()
1646 }
1647 } else {
1648 encode_cpu(
1649 &mut self.stripes,
1650 &mut self.stripes_carrying,
1651 argb,
1652 width,
1653 height,
1654 &[],
1655 &self.settings,
1656 self.frame_counter,
1657 false,
1658 true,
1659 force_idr_all,
1660 )
1661 }
1662 };
1663
1664 self.pending_force_idr = (requested || self.pending_force_idr) && out.is_empty();
1669 self.frame_counter = self.frame_counter.wrapping_add(1);
1670 out
1671 }
1672}
1673
1674#[cfg(test)]
1675mod tests {
1676 use super::*;
1677
1678 const HOLDS: EncoderQuality = EncoderQuality {
1680 last: None,
1681 bytes: None,
1682 holds: true,
1683 reopens: false,
1684 keys: true,
1685 band: None,
1686 measures: false,
1687 psnr: None,
1688 };
1689
1690 fn hw_settings() -> RustCaptureSettings {
1695 RustCaptureSettings {
1696 codec: Codec::H264,
1697 video_cbr_mode: true,
1698 video_crf: 25,
1699 video_paintover_crf: 10,
1700 video_paintover_burst_frames: 3,
1701 paint_over_trigger_frames: 2,
1702 use_paint_over_quality: true,
1703 video_streaming_mode: false,
1704 keyframe_interval_s: 0.0,
1705 target_fps: 60.0,
1706 ..Default::default()
1707 }
1708 }
1709
1710 fn crf_settings() -> RustCaptureSettings {
1712 RustCaptureSettings {
1713 video_cbr_mode: false,
1714 ..hw_settings()
1715 }
1716 }
1717
1718 fn run(
1720 st: &mut StripeState,
1721 s: &RustCaptureSettings,
1722 damage: Damage,
1723 frames: usize,
1724 ) -> Vec<HwFrameDecision> {
1725 (0..frames)
1726 .map(|i| decide_hw_fullframe(st, s, i as u16 + 1, damage, false, false, HOLDS))
1727 .collect()
1728 }
1729
1730 #[test]
1735 fn a_still_screen_is_cleaned_up_whatever_turbo_sends() {
1736 for turbo in [false, true] {
1737 let s = RustCaptureSettings {
1738 video_streaming_mode: turbo,
1739 ..hw_settings()
1740 };
1741 let paint = Some(s.video_paintover_crf as u32);
1742 let mut st = StripeState::default();
1743 let moving = run(&mut st, &s, Damage::Area(1.0), 3);
1744 assert!(
1745 moving
1746 .iter()
1747 .all(|d| d.send && !d.force_idr && d.hold_qp.is_none()),
1748 "motion sends at the session quality"
1749 );
1750 let still = run(&mut st, &s, Damage::None, 16);
1751 let sent: Vec<(usize, bool, Option<u32>)> = still
1752 .iter()
1753 .enumerate()
1754 .filter(|(_, d)| d.send)
1755 .map(|(i, d)| (i + 1, d.force_idr, d.hold_qp))
1756 .collect();
1757 let cleanups = vec![
1758 (2, false, paint),
1759 (3, false, paint),
1760 (4, false, paint),
1761 (5, false, paint),
1762 (8, true, paint),
1763 (9, false, paint),
1764 (10, false, paint),
1765 (11, false, paint),
1766 ];
1767 if !turbo {
1768 assert_eq!(
1769 sent, cleanups,
1770 "a refresh at the trigger and a key four triggers in, each with its burst held"
1771 );
1772 } else {
1773 assert_eq!(sent.len(), 16, "Turbo sends every frame");
1774 let held: Vec<(usize, bool, Option<u32>)> =
1775 sent.into_iter().filter(|r| r.2.is_some()).collect();
1776 assert_eq!(held, cleanups, "the same cleanups under Turbo");
1777 }
1778 assert!(
1779 still.iter().all(|d| d.target_qp == s.video_crf as u32),
1780 "the session's own quality is left alone"
1781 );
1782 }
1783 }
1784
1785 #[test]
1790 fn a_constant_rate_cleanup_keeps_to_its_budget() {
1791 let s = RustCaptureSettings {
1792 video_cbr_mode: true,
1793 ..hw_settings()
1794 };
1795 let coarse = EncoderQuality {
1796 last: Some(40),
1797 bytes: None,
1798 holds: true,
1799 reopens: false,
1800 keys: true,
1801 band: None,
1802 measures: false,
1803 psnr: None,
1804 };
1805 let mut st = StripeState::default();
1806 for i in 0..3u16 {
1807 decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, coarse);
1808 }
1809 let still: Vec<HwFrameDecision> = (0..12u16)
1810 .map(|i| decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, coarse))
1811 .collect();
1812 let refresh_qp = held_refresh_quality(&s, coarse);
1813 assert_eq!(
1814 refresh_qp, 10,
1815 "35 steps under the rate control's 40 is past the paint-over quality"
1816 );
1817 assert_eq!(
1818 still[1].hold_qp,
1819 Some(refresh_qp),
1820 "the refresh at the budget's quality"
1821 );
1822 assert!(
1823 (2..5).all(|i| still[i].hold_qp == Some(refresh_qp)),
1824 "its burst at the same"
1825 );
1826 assert!(
1827 still[7].force_idr && still[7].hold_qp == Some(s.video_paintover_crf as u32),
1828 "the key at the paint-over one"
1829 );
1830 assert!(
1831 (8..11).all(|i| still[i].send && still[i].hold_qp == Some(refresh_qp)),
1832 "its burst at the refresh's"
1833 );
1834 }
1835
1836 #[test]
1839 fn a_small_change_is_refreshed_and_a_large_one_keyed() {
1840 let s = RustCaptureSettings {
1841 video_paintover_burst_frames: 0,
1842 ..hw_settings()
1843 };
1844 let keys = |st: &mut StripeState, damage: Damage, frames: usize| -> (usize, usize) {
1845 run(st, &s, damage, frames);
1846 let still = run(st, &s, Damage::None, 12);
1847 (
1848 still
1849 .iter()
1850 .filter(|d| d.hold_qp.is_some() && !d.force_idr)
1851 .count(),
1852 still.iter().filter(|d| d.force_idr).count(),
1853 )
1854 };
1855 let mut st = StripeState::default();
1856 assert_eq!(
1857 keys(&mut st, Damage::Area(1.0), 2),
1858 (1, 1),
1859 "a scroll is refreshed, then keyed"
1860 );
1861 assert_eq!(
1862 keys(&mut st, Damage::Area(0.001), 1),
1863 (1, 0),
1864 "a caret is refreshed, never keyed"
1865 );
1866 assert_eq!(
1867 keys(&mut st, Damage::Area(0.3), 2),
1868 (1, 1),
1869 "0.6 of a screen changed since the last key frame"
1870 );
1871 }
1872
1873 #[test]
1877 fn a_refresh_that_restores_the_picture_is_not_followed_by_a_key_frame() {
1878 let s = RustCaptureSettings {
1879 video_paintover_burst_frames: 0,
1880 ..hw_settings()
1881 };
1882 for (encoder, keyed) in [
1883 (
1884 EncoderQuality {
1885 keys: false,
1886 ..HOLDS
1887 },
1888 0,
1889 ),
1890 (HOLDS, 1),
1891 ] {
1892 let mut st = StripeState::default();
1893 for i in 0..2u16 {
1894 decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, encoder);
1895 }
1896 let still: Vec<HwFrameDecision> = (0..40u16)
1897 .map(|i| {
1898 decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, encoder)
1899 })
1900 .collect();
1901 assert_eq!(
1902 still
1903 .iter()
1904 .filter(|d| d.hold_qp.is_some() && !d.force_idr)
1905 .count(),
1906 1,
1907 "keys {}: one refresh",
1908 encoder.keys
1909 );
1910 assert_eq!(
1911 still.iter().filter(|d| d.force_idr).count(),
1912 keyed,
1913 "keys {}: the key frames",
1914 encoder.keys
1915 );
1916 }
1917 }
1918
1919 #[test]
1924 fn a_constant_rate_cleanup_runs_through_a_rate_control_that_converges() {
1925 let s = hw_settings();
1926 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
1927 let refining = EncoderQuality {
1928 last: Some(40),
1929 bytes: Some(budget),
1930 holds: true,
1931 reopens: false,
1932 keys: true,
1933 band: None,
1934 measures: false,
1935 psnr: None,
1936 };
1937 let converged = EncoderQuality {
1938 last: Some(8),
1939 bytes: Some(budget / 8),
1940 holds: true,
1941 reopens: false,
1942 keys: true,
1943 band: None,
1944 measures: false,
1945 psnr: None,
1946 };
1947 let trigger = s.paint_over_trigger_frames as u16;
1948 let window = trigger + (FALLBACK_TRIGGERS as u16) * trigger;
1949 let mut st = StripeState::default();
1950 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, refining);
1951 let still: Vec<HwFrameDecision> = (1..window)
1952 .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, refining))
1953 .collect();
1954 assert!(
1955 still.iter().all(|d| d.hold_qp.is_none() && !d.force_idr),
1956 "no frame held, no key frame"
1957 );
1958 let first = still.iter().position(|d| d.send).expect("a cleanup");
1959 assert_eq!(first + 1, trigger as usize, "due at the trigger");
1960 assert!(
1961 still[first..].iter().all(|d| d.send),
1962 "the frames keep flowing while it refines"
1963 );
1964 let d = decide_hw_fullframe(&mut st, &s, window, Damage::None, false, false, converged);
1965 assert!(
1966 !d.send,
1967 "and stop once it codes a small frame at the paint-over quality"
1968 );
1969 assert!((window + 1..window + 100).all(|i| {
1970 !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, converged).send
1971 }));
1972 }
1973
1974 #[test]
1979 fn a_session_that_holds_no_quantizer_is_refined_until_it_reaches_the_paint_over_quality() {
1980 let s = hw_settings();
1981 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
1982 let at = |last: u32| EncoderQuality {
1983 last: Some(last),
1984 bytes: Some(budget),
1985 holds: false,
1986 reopens: false,
1987 keys: true,
1988 band: None,
1989 measures: false,
1990 psnr: None,
1991 };
1992 let trigger = s.paint_over_trigger_frames as u16;
1993 let past = trigger + 2 * (FALLBACK_TRIGGERS as u16) * trigger;
1994 let mut st = StripeState::default();
1995 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(40));
1996 let still: Vec<HwFrameDecision> = (1..past)
1997 .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(40)))
1998 .collect();
1999 assert!(still.iter().all(|d| d.hold_qp.is_none() && !d.force_idr));
2000 assert!(still[trigger as usize - 1..].iter().all(|d| d.send));
2001 let fine = s.video_paintover_crf as u32;
2002 assert!(
2003 (past..past + trigger).all(|i| {
2004 decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(fine)).send
2005 }),
2006 "a trigger period of frames at it"
2007 );
2008 assert!((past + trigger..past + 100).all(|i| {
2009 !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(fine)).send
2010 }));
2011 let window = |encoder: EncoderQuality| {
2012 let mut st = StripeState::default();
2013 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, encoder);
2014 (1..u16::MAX)
2015 .filter(|&i| {
2016 decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, encoder).send
2017 })
2018 .count() as f64
2019 / s.target_fps
2020 };
2021 assert!(
2022 (window(at(40)) - LEVEL_S).abs() < 1.0,
2023 "a quantizer that stands ends it"
2024 );
2025 let mut st = StripeState::default();
2026 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, at(44));
2027 let step = (2.0 * s.target_fps) as u32;
2028 let refining = (1..u16::MAX)
2029 .filter(|&i| {
2030 let q = 44 - (i as u32 / step).min(20);
2031 decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, at(q)).send
2032 })
2033 .count() as f64
2034 / s.target_fps;
2035 assert!(
2036 (refining - REFINE_S).abs() < 1.0,
2037 "one a step finer every two seconds runs the window"
2038 );
2039 let silent = EncoderQuality {
2040 last: None,
2041 ..at(40)
2042 };
2043 assert!(
2044 (window(silent) - CONVERGE_S).abs() < 1.0,
2045 "one that says no quantizer gets the shorter window"
2046 );
2047 }
2048
2049 #[test]
2052 fn a_settled_cleanup_is_not_restarted_by_a_caret() {
2053 let s = hw_settings();
2054 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2055 let small = EncoderQuality {
2056 last: None,
2057 bytes: Some(budget / 8),
2058 holds: false,
2059 reopens: false,
2060 keys: true,
2061 band: None,
2062 measures: false,
2063 psnr: None,
2064 };
2065 let trigger = s.paint_over_trigger_frames;
2066 let mut st = StripeState::default();
2067 let mut frame = 0u16;
2068 let mut run = |st: &mut StripeState, damage: Damage, frames: u32| {
2069 (0..frames)
2070 .filter(|_| {
2071 frame += 1;
2072 decide_hw_fullframe(st, &s, frame, damage, false, false, small).send
2073 })
2074 .count() as u32
2075 };
2076 run(&mut st, Damage::Area(1.0), 1);
2077 let cleanup = run(&mut st, Damage::None, 20 * trigger);
2078 assert_eq!(cleanup, STALL_TRIGGERS * trigger + 1);
2079 let mut blinking = 0;
2080 for _ in 0..10 {
2081 blinking += run(&mut st, Damage::Area(0.001), 1);
2082 blinking += run(&mut st, Damage::None, 2 * trigger);
2083 }
2084 assert_eq!(blinking, 20, "each blink and its refresh, and no more");
2085 run(&mut st, Damage::Area(1.0), 1);
2086 assert_eq!(run(&mut st, Damage::None, 20 * trigger), cleanup);
2087 }
2088
2089 #[test]
2090 fn a_rate_control_that_does_not_converge_gets_one_held_refresh() {
2091 let s = hw_settings();
2092 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2093 let crawling = EncoderQuality {
2094 last: Some(38),
2095 bytes: Some(budget / 2),
2096 holds: true,
2097 reopens: false,
2098 keys: true,
2099 band: None,
2100 measures: false,
2101 psnr: None,
2102 };
2103 let mut st = StripeState::default();
2104 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, crawling);
2105 let still: Vec<HwFrameDecision> = (1..=200)
2106 .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, crawling))
2107 .collect();
2108 assert!(still.iter().all(|d| !d.force_idr), "no key frame");
2109 let held: Vec<usize> = still
2110 .iter()
2111 .enumerate()
2112 .filter(|(_, d)| d.hold_qp.is_some())
2113 .map(|(i, _)| i)
2114 .collect();
2115 assert_eq!(held.len(), 1, "{held:?}");
2116 let trigger = s.paint_over_trigger_frames as usize;
2117 assert_eq!(
2118 held[0] + 1,
2119 trigger + (FALLBACK_TRIGGERS as usize) * trigger,
2120 "after the window"
2121 );
2122 assert_eq!(
2123 still[held[0]].hold_qp,
2124 Some(held_refresh_quality(&s, crawling))
2125 );
2126 assert!(still[held[0] + 1..].iter().all(|d| !d.send));
2127 }
2128
2129 #[test]
2134 fn a_rate_control_pinned_at_its_coarsest_quantizer_is_refreshed_once() {
2135 let s = RustCaptureSettings {
2136 video_bitrate_kbps: 100,
2137 ..hw_settings()
2138 };
2139 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2140 let pinned = EncoderQuality {
2141 last: Some(51),
2142 bytes: Some((budget * 16.0) as usize),
2143 holds: true,
2144 reopens: false,
2145 keys: true,
2146 band: Some(0),
2147 measures: false,
2148 psnr: None,
2149 };
2150 let mut st = StripeState::default();
2151 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
2152 let still: Vec<HwFrameDecision> = (1..=200)
2153 .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned))
2154 .collect();
2155 let sent: Vec<usize> = still
2156 .iter()
2157 .enumerate()
2158 .filter(|(_, d)| d.send)
2159 .map(|(i, _)| i)
2160 .collect();
2161 let trigger = s.paint_over_trigger_frames as usize;
2162 let refresh = 2 * trigger - 1;
2163 assert_eq!(
2164 sent,
2165 (trigger - 1..=refresh).collect::<Vec<_>>(),
2166 "a trigger period through the rate control, then the refresh"
2167 );
2168 assert_eq!(
2169 (still[refresh].hold_qp, still[refresh].hold_band),
2170 (Some(40), None),
2171 "frames of 16 budgets leave 11 steps of the 35 a second buys"
2172 );
2173 }
2174
2175 #[test]
2178 fn a_pinned_rate_control_ends_a_recovery_burst() {
2179 let s = RustCaptureSettings {
2180 video_bitrate_kbps: 100,
2181 use_paint_over_quality: false,
2182 ..hw_settings()
2183 };
2184 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2185 let pinned = EncoderQuality {
2186 last: Some(51),
2187 bytes: Some((budget * 16.0) as usize),
2188 holds: true,
2189 reopens: false,
2190 keys: true,
2191 band: Some(0),
2192 measures: false,
2193 psnr: None,
2194 };
2195 let mut st = StripeState::default();
2196 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, pinned);
2197 assert!((1..=20).all(|i| {
2198 !decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, pinned).send
2199 }));
2200 let key = decide_hw_fullframe(&mut st, &s, 21, Damage::None, false, true, pinned);
2201 assert!(key.send && key.force_idr);
2202 let trigger = s.paint_over_trigger_frames as u16;
2203 assert!(
2204 (22..21 + trigger).all(|i| decide_hw_fullframe(
2205 &mut st,
2206 &s,
2207 i,
2208 Damage::None,
2209 false,
2210 false,
2211 pinned
2212 )
2213 .send),
2214 "its burst until a trigger period over the budget"
2215 );
2216 assert!(
2217 (21 + trigger..=200).all(|i| !decide_hw_fullframe(
2218 &mut st,
2219 &s,
2220 i,
2221 Damage::None,
2222 false,
2223 false,
2224 pinned
2225 )
2226 .send),
2227 "and no more"
2228 );
2229 }
2230
2231 #[test]
2232 fn a_blinking_caret_does_not_hold_off_a_converging_cleanup() {
2233 let s = hw_settings();
2234 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2235 let crawling = EncoderQuality {
2236 last: Some(38),
2237 bytes: Some(budget / 2),
2238 holds: true,
2239 reopens: false,
2240 keys: true,
2241 band: None,
2242 measures: false,
2243 psnr: None,
2244 };
2245 let mut st = StripeState::default();
2246 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, crawling);
2247 let trigger = s.paint_over_trigger_frames as u16;
2248 let decisions: Vec<HwFrameDecision> = (1..300)
2249 .map(|i| {
2250 let damage = if i % 3 == 0 {
2251 Damage::Area(0.001)
2252 } else {
2253 Damage::None
2254 };
2255 decide_hw_fullframe(&mut st, &s, i, damage, false, false, crawling)
2256 })
2257 .collect();
2258 assert!(decisions.iter().all(|d| !d.force_idr), "no key frame");
2259 let first = decisions
2260 .iter()
2261 .position(|d| d.hold_qp.is_some())
2262 .expect("a held refresh") as u16
2263 + 1;
2264 let bound = trigger * (LOW_MOTION_TRIGGERS + 2 * FALLBACK_TRIGGERS) as u16 * 3 / 2;
2265 assert!(
2266 first <= bound,
2267 "the caret does not hold it off: {first} > {bound}"
2268 );
2269 let held = decisions.iter().filter(|d| d.hold_qp.is_some()).count();
2270 assert!(
2271 held <= 300 / (trigger as usize * LOW_MOTION_TRIGGERS as usize),
2272 "{held} held refreshes"
2273 );
2274 }
2275
2276 #[test]
2277 fn a_stalled_rate_control_is_held_once_at_the_paint_over_quantizer() {
2278 let s = hw_settings();
2279 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2280 let idle = EncoderQuality {
2281 last: Some(38),
2282 bytes: Some(budget / 10),
2283 holds: true,
2284 reopens: false,
2285 keys: true,
2286 band: None,
2287 measures: false,
2288 psnr: None,
2289 };
2290 let mut st = StripeState::default();
2291 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle);
2292 let still: Vec<HwFrameDecision> = (1..=200)
2293 .map(|i| decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle))
2294 .collect();
2295 let held: Vec<usize> = still
2296 .iter()
2297 .enumerate()
2298 .filter(|(_, d)| d.hold_qp.is_some())
2299 .map(|(i, _)| i)
2300 .collect();
2301 assert_eq!(held.len(), 1, "one held frame: {held:?}");
2302 assert_eq!(still[held[0]].hold_qp, Some(held_refresh_quality(&s, idle)));
2303 assert!(!still[held[0]].force_idr, "a refresh, not a key frame");
2304 let stall = (STALL_TRIGGERS * s.paint_over_trigger_frames) as usize;
2305 assert!(
2306 held[0] + 1 >= s.paint_over_trigger_frames as usize + stall,
2307 "after the stall, not at the trigger"
2308 );
2309 assert!(
2310 still[held[0] + 1..].iter().all(|d| !d.send),
2311 "which ends the cleanup"
2312 );
2313 }
2314
2315 #[test]
2316 fn a_stalled_rate_control_that_holds_a_band_sweeps_the_picture_within_the_budget() {
2317 let s = hw_settings();
2318 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2319 let idle = |held: usize| EncoderQuality {
2320 last: Some(38),
2321 bytes: Some(budget as usize / 10),
2322 holds: true,
2323 reopens: false,
2324 keys: true,
2325 band: Some(held),
2326 measures: false,
2327 psnr: None,
2328 };
2329 let mut st = StripeState::default();
2330 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
2331 let mut bands: Vec<(f64, f64)> = Vec::new();
2332 let mut sent_after = 0;
2333 for i in 1..=600u16 {
2334 let held = bands
2335 .last()
2336 .map_or(0, |(a, b)| ((b - a) * 128.0 * budget) as usize);
2337 let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
2338 assert!(!d.force_idr, "no key frame");
2339 match d.hold_band {
2340 Some(band) => {
2341 assert!(d.send && d.hold_qp == Some(held_refresh_quality(&s, idle(0))));
2342 bands.push(band);
2343 }
2344 None => {
2345 assert!(d.hold_qp.is_none(), "every held frame is a band");
2346 sent_after += (!bands.is_empty() && d.send) as u32;
2347 }
2348 }
2349 }
2350 assert_eq!(
2351 bands[0],
2352 (0.0, FIRST_BAND),
2353 "the sweep starts at the stall, from the top"
2354 );
2355 assert!(
2356 bands.windows(2).all(|w| w[0].1 == w[1].0),
2357 "contiguous bands: {bands:?}"
2358 );
2359 assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
2360 assert!(
2361 bands[2..bands.len() - 1]
2362 .iter()
2363 .all(|(a, b)| ((b - a) * 128.0 - 1.0).abs() < 1e-3),
2364 "each a budget: {bands:?}"
2365 );
2366 assert_eq!(sent_after, 0, "which ends the cleanup");
2367 }
2368
2369 #[test]
2374 fn a_held_refresh_that_holds_a_band_sweeps_the_picture() {
2375 let s = hw_settings();
2376 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2377 let vp8 = |held: usize| EncoderQuality {
2378 last: Some(38),
2379 bytes: None,
2380 holds: true,
2381 reopens: false,
2382 keys: false,
2383 band: Some(held),
2384 measures: false,
2385 psnr: None,
2386 };
2387 let mut st = StripeState::default();
2388 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, vp8(0));
2389 let mut bands: Vec<(f64, f64)> = Vec::new();
2390 let mut caret_in_sweep = false;
2391 let mut blinked = false;
2392 for i in 1..=600u16 {
2393 let held = bands
2394 .last()
2395 .map_or(0, |(a, b)| ((b - a) * 128.0 * budget) as usize);
2396 let caret = !blinked && bands.len() == 20;
2397 blinked |= caret;
2398 caret_in_sweep |= caret && st.sweep.is_some();
2399 let damage = if caret {
2400 Damage::Area(0.001)
2401 } else {
2402 Damage::None
2403 };
2404 let d = decide_hw_fullframe(&mut st, &s, i, damage, false, false, vp8(held));
2405 assert!(!d.force_idr, "no key frame");
2406 match d.hold_band {
2407 Some(band) => {
2408 assert!(d.send && d.hold_qp == Some(held_refresh_quality(&s, vp8(0))));
2409 bands.push(band);
2410 }
2411 None => assert!(d.hold_qp.is_none(), "no whole held frame, frame {i}"),
2412 }
2413 }
2414 assert!(caret_in_sweep, "the caret blinked mid-sweep");
2415 assert_eq!(bands[0], (0.0, FIRST_BAND), "the sweep starts from the top");
2416 assert!(
2417 bands.windows(2).all(|w| w[0].1 == w[1].0),
2418 "contiguous bands, the caret's frame between two: {bands:?}"
2419 );
2420 assert_eq!(bands.last().unwrap().1, 1.0, "that cover the picture");
2421 }
2422
2423 #[test]
2427 fn a_sweep_that_would_run_past_refine_s_is_coded_coarser() {
2428 let s = hw_settings();
2429 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps);
2430 let idle = |held: usize| EncoderQuality {
2431 last: Some(38),
2432 bytes: Some(budget as usize / 10),
2433 holds: true,
2434 reopens: false,
2435 keys: true,
2436 band: Some(held),
2437 measures: false,
2438 psnr: None,
2439 };
2440 let refresh = held_refresh_quality(&s, idle(0));
2441 let mut st = StripeState::default();
2442 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle(0));
2443 let window = (REFINE_S * s.target_fps).round() as usize;
2444 let mut bands: Vec<(f64, f64, u32)> = Vec::new();
2445 for i in 1..=(2 * window) as u16 {
2446 let held = bands.last().map_or(0, |(a, b, q)| {
2447 let cost = 4.0 * window as f64 * 2f64.powf(-((q - refresh) as f64) / 6.0);
2448 ((b - a) * cost * budget) as usize
2449 });
2450 let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle(held));
2451 if let (Some((a, b)), Some(q)) = (d.hold_band, d.hold_qp) {
2452 bands.push((a, b, q));
2453 }
2454 }
2455 let coarser = bands.last().unwrap().2 - refresh;
2456 assert_eq!(bands.last().unwrap().1, 1.0, "the sweep covers the picture");
2457 assert!(
2458 bands.len() <= window + window / 10,
2459 "within REFINE_S: {} bands",
2460 bands.len()
2461 );
2462 assert!(
2463 bands.windows(2).all(|w| w[0].2 <= w[1].2),
2464 "never finer again"
2465 );
2466 assert!(
2467 (12..=15).contains(&coarser),
2468 "about the 12 steps that quarter the picture's cost: {coarser}"
2469 );
2470 }
2471
2472 #[test]
2476 fn a_cleanup_whose_frames_keep_one_size_ends() {
2477 let s = RustCaptureSettings {
2478 video_bitrate_kbps: 100,
2479 ..hw_settings()
2480 };
2481 let level = EncoderQuality {
2482 last: Some(51),
2483 bytes: Some(266),
2484 holds: false,
2485 reopens: false,
2486 keys: true,
2487 band: None,
2488 measures: false,
2489 psnr: None,
2490 };
2491 let mut st = StripeState::default();
2492 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, level);
2493 let sent = (1..=2400u16)
2494 .filter(|&i| {
2495 decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, level).send
2496 })
2497 .count();
2498 let run = (EMPTY_S * s.target_fps) as usize;
2499 assert!(
2500 (run..=run + 4).contains(&sent),
2501 "{sent} frames, not the {} of REFINE_S",
2502 (REFINE_S * s.target_fps) as usize
2503 );
2504 }
2505
2506 #[test]
2507 fn motion_ends_a_band_sweep() {
2508 let s = hw_settings();
2509 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2510 let idle = EncoderQuality {
2511 last: Some(38),
2512 bytes: Some(budget / 10),
2513 holds: true,
2514 reopens: false,
2515 keys: true,
2516 band: Some(budget),
2517 measures: false,
2518 psnr: None,
2519 };
2520 let mut st = StripeState::default();
2521 decide_hw_fullframe(&mut st, &s, 0, Damage::Area(1.0), false, false, idle);
2522 let first = (1..=200u16)
2523 .find(|&i| {
2524 decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, idle)
2525 .hold_band
2526 .is_some()
2527 })
2528 .expect("a sweep");
2529 let moving = decide_hw_fullframe(
2530 &mut st,
2531 &s,
2532 first + 1,
2533 Damage::Area(0.5),
2534 false,
2535 false,
2536 idle,
2537 );
2538 assert!(moving.send && moving.hold_qp.is_none() && moving.hold_band.is_none());
2539 assert!(st.sweep.is_none());
2540 let caret = decide_hw_fullframe(
2541 &mut st,
2542 &s,
2543 first + 2,
2544 Damage::Area(0.001),
2545 false,
2546 false,
2547 idle,
2548 );
2549 assert!(caret.hold_band.is_none());
2550 }
2551
2552 #[test]
2553 fn a_recovery_key_frame_is_refined_until_the_rate_control_converges() {
2554 let s = hw_settings();
2555 let budget = frame_budget(s.video_bitrate_kbps, s.target_fps) as usize;
2556 let coarse = EncoderQuality {
2557 last: Some(40),
2558 bytes: Some(budget),
2559 holds: true,
2560 reopens: false,
2561 keys: true,
2562 band: None,
2563 measures: false,
2564 psnr: None,
2565 };
2566 let clean = EncoderQuality {
2567 last: Some(8),
2568 bytes: Some(budget / 8),
2569 holds: true,
2570 reopens: false,
2571 keys: true,
2572 band: None,
2573 measures: false,
2574 psnr: None,
2575 };
2576 let mut st = StripeState::default();
2577 assert!(
2578 (0..200).all(|i| !decide_hw_fullframe(
2579 &mut st,
2580 &s,
2581 i,
2582 Damage::None,
2583 false,
2584 false,
2585 clean
2586 )
2587 .send)
2588 );
2589 let join = decide_hw_fullframe(&mut st, &s, 200, Damage::None, false, true, clean);
2590 assert!(join.send && join.force_idr && join.hold_qp.is_none());
2591 let past_burst = 201 + s.video_paintover_burst_frames as u16 + 3;
2592 assert!(
2593 (201..past_burst).all(|i| {
2594 let d = decide_hw_fullframe(&mut st, &s, i, Damage::None, false, false, coarse);
2595 d.send && !d.force_idr && d.hold_qp.is_none()
2596 }),
2597 "the rate control keeps refining the key frame past the configured burst"
2598 );
2599 assert!(
2600 !decide_hw_fullframe(&mut st, &s, past_burst, Damage::None, false, false, clean).send,
2601 "until it converges"
2602 );
2603 }
2604
2605 #[test]
2606 fn a_large_change_after_a_clean_still_screen_is_not_keyed_as_it_lands() {
2607 let s = RustCaptureSettings {
2608 video_paintover_burst_frames: 0,
2609 ..hw_settings()
2610 };
2611 let fine = EncoderQuality {
2612 last: Some(5),
2613 bytes: None,
2614 holds: true,
2615 reopens: false,
2616 keys: true,
2617 band: None,
2618 measures: false,
2619 psnr: None,
2620 };
2621 let coarse = EncoderQuality {
2622 last: Some(40),
2623 bytes: None,
2624 holds: true,
2625 reopens: false,
2626 keys: true,
2627 band: None,
2628 measures: false,
2629 psnr: None,
2630 };
2631 let mut st = StripeState::default();
2632 let mut frame = 0u16;
2633 let mut step = |st: &mut StripeState, damage: Damage, q: EncoderQuality| {
2634 frame = frame.wrapping_add(1);
2635 decide_hw_fullframe(st, &s, frame, damage, false, false, q)
2636 };
2637 step(&mut st, Damage::Area(1.0), coarse);
2638 assert!(
2639 (0..100).all(|_| step(&mut st, Damage::None, fine).hold_qp.is_none()),
2640 "a finer rate control is left alone"
2641 );
2642 let landing = [
2643 step(&mut st, Damage::Area(1.0), fine),
2644 step(&mut st, Damage::Area(1.0), coarse),
2645 ];
2646 assert!(
2647 landing
2648 .iter()
2649 .all(|d| d.send && !d.force_idr && d.hold_qp.is_none()),
2650 "the change goes out at the session quality"
2651 );
2652 assert!(
2653 (0..12).any(|_| step(&mut st, Damage::None, coarse).force_idr),
2654 "and is keyed once the screen holds still"
2655 );
2656 }
2657
2658 #[test]
2662 fn low_motion_does_not_hold_off_the_cleanup_forever() {
2663 let s = RustCaptureSettings {
2664 paint_over_trigger_frames: 4,
2665 ..hw_settings()
2666 };
2667 let mut st = StripeState::default();
2668 run(&mut st, &s, Damage::Area(1.0), 3);
2669 let mut fired = None;
2670 for i in 0..40u16 {
2671 let damage = if i % 2 == 0 {
2672 Damage::Area(0.0005)
2673 } else {
2674 Damage::None
2675 };
2676 let d = decide_hw_fullframe(&mut st, &s, 100 + i, damage, false, false, HOLDS);
2677 if d.hold_qp.is_some() {
2678 fired = Some((i, d.force_idr));
2679 break;
2680 }
2681 }
2682 let (at, key) = fired.expect("a cleanup under low motion");
2683 assert_eq!(
2684 at as u32 + 4,
2685 s.paint_over_trigger_frames * LOW_MOTION_TRIGGERS,
2686 "at the end of the window since the scroll began"
2687 );
2688 assert!(key, "the scroll before it makes it a key frame");
2689
2690 let mut st = StripeState::default();
2693 run(&mut st, &s, Damage::Area(1.0), 200);
2694 let window = s.paint_over_trigger_frames * LOW_MOTION_TRIGGERS;
2695 let fired = (0..200u16).find(|&i| {
2696 let damage = if i % 2 == 0 {
2697 Damage::Area(0.0005)
2698 } else {
2699 Damage::None
2700 };
2701 decide_hw_fullframe(&mut st, &s, 300 + i, damage, false, false, HOLDS).force_idr
2702 });
2703 let at = fired.expect("a key frame under low motion after a long scroll") as u32;
2704 assert!(
2705 at >= window && at <= window * 2,
2706 "{at} frames after the scroll stopped, window {window}"
2707 );
2708 }
2709
2710 #[test]
2713 fn sustained_motion_gets_no_cleanup_until_it_stops() {
2714 let s = hw_settings();
2715 let mut st = StripeState::default();
2716 assert!(
2717 run(&mut st, &s, Damage::Area(0.5), 60)
2718 .iter()
2719 .all(|d| d.hold_qp.is_none())
2720 );
2721 assert!(
2722 run(&mut st, &s, Damage::Unknown, 60)
2723 .iter()
2724 .all(|d| d.hold_qp.is_none())
2725 );
2726 }
2727
2728 #[test]
2731 fn unknown_changes_count_toward_a_key_frame_only_in_runs() {
2732 let s = hw_settings();
2733 let mut st = StripeState::default();
2734 for _ in 0..20 {
2735 run(&mut st, &s, Damage::Unknown, 1);
2736 let still = run(&mut st, &s, Damage::None, 10);
2737 assert!(
2738 still[1].hold_qp.is_some() && !still[1].force_idr,
2739 "isolated changes are refreshed"
2740 );
2741 assert!(still.iter().all(|d| !d.force_idr), "and never keyed");
2742 }
2743 run(&mut st, &s, Damage::Unknown, 12);
2744 assert!(
2745 run(&mut st, &s, Damage::None, 10)
2746 .iter()
2747 .any(|d| d.force_idr),
2748 "a run of ten counts for more than half a screen"
2749 );
2750 }
2751
2752 #[test]
2755 fn motion_sends_at_normal_quality_and_cancels_the_burst() {
2756 let s = hw_settings();
2757 let mut st = StripeState {
2758 h264_burst_frames_remaining: 3,
2759 paint_over_sent: true,
2760 no_motion_frame_count: 9,
2761 ..Default::default()
2762 };
2763 let d = decide_hw_fullframe(&mut st, &s, 1, Damage::Area(0.2), false, false, HOLDS);
2764 assert!(d.send && !d.force_idr && d.hold_qp.is_none());
2765 assert_eq!(d.target_qp, s.video_crf as u32);
2766 assert_eq!(st.h264_burst_frames_remaining, 0, "the burst is dropped");
2767 assert_eq!(st.no_motion_frame_count, 0);
2768 assert!(!st.paint_over_sent);
2769 }
2770
2771 #[test]
2777 fn a_requested_key_frame_is_the_rate_controls_own() {
2778 let s = RustCaptureSettings {
2779 paint_over_trigger_frames: 30,
2780 ..crf_settings()
2781 };
2782 let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
2783 let mut st = StripeState::default();
2784 run(&mut st, &s, Damage::Area(1.0), 1);
2785 let join = decide_hw_fullframe(&mut st, &s, 2, Damage::None, false, true, HOLDS);
2786 assert!(join.send && join.force_idr && join.hold_qp.is_none() && join.target_qp == normal);
2787 assert_eq!(
2788 st.h264_burst_frames_remaining,
2789 s.video_paintover_burst_frames
2790 );
2791 let burst = decide_hw_fullframe(&mut st, &s, 3, Damage::None, false, false, HOLDS);
2792 assert!(
2793 burst.send && burst.hold_qp.is_none() && burst.target_qp == paint,
2794 "the burst refines at the paint-over quality"
2795 );
2796
2797 let s = crf_settings();
2798 let mut st = StripeState::default();
2799 run(&mut st, &s, Damage::Area(1.0), 1);
2800 run(&mut st, &s, Damage::None, 12);
2801 assert!(st.clean_quality, "cleaned up");
2802 let join = decide_hw_fullframe(&mut st, &s, 30, Damage::None, false, true, HOLDS);
2803 assert!(
2804 join.force_idr && join.target_qp == normal,
2805 "a key frame on a clean screen at the session's quality"
2806 );
2807 let burst = decide_hw_fullframe(&mut st, &s, 31, Damage::None, false, false, HOLDS);
2808 assert_eq!(burst.target_qp, paint);
2809
2810 let cbr = RustCaptureSettings {
2811 video_cbr_mode: true,
2812 paint_over_trigger_frames: 30,
2813 ..hw_settings()
2814 };
2815 let coarse = EncoderQuality {
2816 last: Some(40),
2817 bytes: None,
2818 holds: true,
2819 reopens: false,
2820 keys: true,
2821 band: None,
2822 measures: false,
2823 psnr: None,
2824 };
2825 let mut st = StripeState::default();
2826 decide_hw_fullframe(&mut st, &cbr, 1, Damage::Area(1.0), false, false, coarse);
2827 let join = decide_hw_fullframe(&mut st, &cbr, 2, Damage::None, false, true, coarse);
2828 assert!(join.force_idr && join.hold_qp.is_none());
2829 let burst = decide_hw_fullframe(&mut st, &cbr, 3, Damage::None, false, false, coarse);
2830 assert!(
2831 burst.send && burst.hold_qp.is_none(),
2832 "a constant rate refines the burst itself"
2833 );
2834
2835 let mut moving = StripeState::default();
2836 let dirty = decide_hw_fullframe(&mut moving, &s, 1, Damage::Area(1.0), false, true, HOLDS);
2837 assert!(dirty.send && dirty.force_idr && dirty.hold_qp.is_none());
2838 assert_eq!(
2839 moving.h264_burst_frames_remaining, 0,
2840 "motion carries the refinement, so no burst is opened"
2841 );
2842 }
2843
2844 #[test]
2851 fn the_cleanup_follows_the_rate_control_and_the_switch() {
2852 #[derive(Debug, PartialEq)]
2853 enum Seen {
2854 Held,
2855 Session,
2856 Unheld,
2857 Nothing,
2858 }
2859 for (cbr, paint, on, last, holds, seen) in [
2860 (true, 25, true, None, true, Seen::Held),
2861 (true, 18, true, Some(30), true, Seen::Held),
2862 (true, 18, true, Some(9), true, Seen::Nothing),
2863 (true, 18, true, Some(18), true, Seen::Nothing),
2864 (true, 18, true, Some(30), false, Seen::Unheld),
2865 (true, 18, true, None, false, Seen::Unheld),
2866 (true, 18, true, Some(9), false, Seen::Nothing),
2867 (false, 25, true, None, true, Seen::Nothing),
2868 (false, 18, true, None, true, Seen::Session),
2869 (false, 18, true, None, false, Seen::Session),
2870 (false, 18, false, None, true, Seen::Nothing),
2871 (true, 18, false, None, true, Seen::Nothing),
2872 ] {
2873 let s = RustCaptureSettings {
2874 video_cbr_mode: cbr,
2875 video_paintover_crf: paint,
2876 use_paint_over_quality: on,
2877 ..hw_settings()
2878 };
2879 let mut st = StripeState::default();
2880 let mut got = Seen::Nothing;
2881 for i in 0..40u16 {
2882 let damage = if i == 0 {
2883 Damage::Area(1.0)
2884 } else {
2885 Damage::None
2886 };
2887 let d = decide_hw_fullframe(
2888 &mut st,
2889 &s,
2890 i,
2891 damage,
2892 false,
2893 false,
2894 EncoderQuality {
2895 last,
2896 bytes: None,
2897 holds,
2898 reopens: false,
2899 keys: true,
2900 band: None,
2901 measures: false,
2902 psnr: None,
2903 },
2904 );
2905 if i > 0 && d.send {
2906 assert!(
2907 !cbr || holds || !d.force_idr,
2908 "a key frame where no quantizer is held: last={last:?}"
2909 );
2910 if d.hold_qp.is_some() {
2911 got = Seen::Held;
2912 } else if d.target_qp != s.video_crf as u32 {
2913 assert!(!cbr, "a constant rate keeps its session quality");
2914 got = Seen::Session;
2915 } else if got == Seen::Nothing {
2916 got = Seen::Unheld;
2917 }
2918 }
2919 }
2920 assert_eq!(
2921 got, seen,
2922 "cbr={cbr} paint={paint} on={on} last={last:?} holds={holds}"
2923 );
2924 }
2925 }
2926
2927 #[test]
2932 fn a_constant_quality_cleanup_moves_the_session_quality() {
2933 for turbo in [false, true] {
2934 let s = RustCaptureSettings {
2935 video_streaming_mode: turbo,
2936 ..crf_settings()
2937 };
2938 let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
2939 let mut st = StripeState::default();
2940 let moving = run(&mut st, &s, Damage::Area(1.0), 3);
2941 assert!(
2942 moving.iter().all(|d| d.send
2943 && !d.force_idr
2944 && d.hold_qp.is_none()
2945 && d.target_qp == normal)
2946 );
2947 let still = run(&mut st, &s, Damage::None, 16);
2948 assert!(
2949 still.iter().all(|d| d.hold_qp.is_none()),
2950 "no frame is held"
2951 );
2952 let sent: Vec<(usize, bool, u32)> = still
2953 .iter()
2954 .enumerate()
2955 .filter(|(_, d)| d.send)
2956 .map(|(i, d)| (i + 1, d.force_idr, d.target_qp))
2957 .collect();
2958 if !turbo {
2959 let cleanups = vec![
2960 (2, false, paint),
2961 (3, false, paint),
2962 (4, false, paint),
2963 (5, false, paint),
2964 (8, true, paint),
2965 (9, false, paint),
2966 (10, false, paint),
2967 (11, false, paint),
2968 ];
2969 assert_eq!(
2970 sent, cleanups,
2971 "a refresh at the trigger and a key four triggers in, each with its burst"
2972 );
2973 } else {
2974 assert_eq!(sent.len(), 16, "Turbo sends every frame");
2975 assert_eq!(
2976 sent[0],
2977 (1, false, normal),
2978 "the session's quality until the cleanup"
2979 );
2980 assert!(
2981 sent[1..].iter().all(|r| r.2 == paint),
2982 "the paint-over quality from the refresh on: {sent:?}"
2983 );
2984 assert_eq!(
2985 sent.iter().filter(|r| r.1).map(|r| r.0).collect::<Vec<_>>(),
2986 vec![8],
2987 "one key frame"
2988 );
2989 }
2990 }
2991 }
2992
2993 #[test]
2997 fn a_reopening_encoder_counts_its_refresh_as_the_key_frame() {
2998 let s = crf_settings();
2999 let reopens = EncoderQuality {
3000 last: None,
3001 bytes: None,
3002 holds: true,
3003 reopens: true,
3004 keys: true,
3005 band: None,
3006 measures: false,
3007 psnr: None,
3008 };
3009 let mut st = StripeState::default();
3010 for i in 0..3u16 {
3011 decide_hw_fullframe(&mut st, &s, i, Damage::Area(1.0), false, false, reopens);
3012 }
3013 let keys: Vec<usize> = (0..16u16)
3014 .map(|i| decide_hw_fullframe(&mut st, &s, 10 + i, Damage::None, false, false, reopens))
3015 .enumerate()
3016 .filter(|(_, d)| d.force_idr)
3017 .map(|(i, _)| i + 1)
3018 .collect();
3019 assert_eq!(
3020 keys,
3021 vec![2],
3022 "the refresh at the trigger is the key frame, and the only one"
3023 );
3024 }
3025
3026 #[test]
3030 fn a_clean_region_keeps_its_quality_until_it_moves() {
3031 let s = RustCaptureSettings {
3032 video_streaming_mode: true,
3033 ..crf_settings()
3034 };
3035 let (normal, paint) = (s.video_crf as u32, s.video_paintover_crf as u32);
3036 let mut st = StripeState::default();
3037 run(&mut st, &s, Damage::Area(1.0), 3);
3038 run(&mut st, &s, Damage::None, 12);
3039 let caret = decide_hw_fullframe(&mut st, &s, 100, Damage::Area(0.001), false, false, HOLDS);
3040 assert_eq!(caret.target_qp, paint, "a caret keeps it");
3041 assert_eq!(
3042 decide_hw_fullframe(&mut st, &s, 101, Damage::None, false, false, HOLDS).target_qp,
3043 paint
3044 );
3045 let scroll = decide_hw_fullframe(&mut st, &s, 102, Damage::Area(0.5), false, false, HOLDS);
3046 assert_eq!(scroll.target_qp, normal, "motion takes it back");
3047 assert_eq!(
3048 decide_hw_fullframe(&mut st, &s, 103, Damage::None, false, false, HOLDS).target_qp,
3049 normal,
3050 "until the next cleanup"
3051 );
3052 run(&mut st, &s, Damage::None, 12);
3053 assert_eq!(
3054 decide_hw_fullframe(&mut st, &s, 120, Damage::Unknown, false, false, HOLDS).target_qp,
3055 normal
3056 );
3057
3058 let off = RustCaptureSettings {
3059 use_paint_over_quality: false,
3060 ..s
3061 };
3062 let mut st = StripeState::default();
3063 run(&mut st, &off, Damage::Area(1.0), 3);
3064 assert!(
3065 run(&mut st, &off, Damage::None, 20)
3066 .iter()
3067 .all(|d| d.target_qp == normal),
3068 "paint-over off"
3069 );
3070 }
3071
3072 #[test]
3075 fn hw_fullframe_streaming_and_scheduled_keyframes_send_unconditionally() {
3076 let mut s = hw_settings();
3077 s.video_streaming_mode = true;
3078 let mut st = StripeState::default();
3079 assert!(
3080 decide_hw_fullframe(&mut st, &s, 1, Damage::None, false, false, HOLDS).send,
3081 "streaming mode sends a static frame"
3082 );
3083
3084 s.video_streaming_mode = false;
3085 let mut animated = StripeState::default();
3086 assert!(
3087 decide_hw_fullframe(&mut animated, &s, 1, Damage::None, true, false, HOLDS).send,
3088 "an animated overlay sends a static frame"
3089 );
3090
3091 s.keyframe_interval_s = 1.0;
3093 let mut scheduled = StripeState::default();
3094 assert!(
3095 decide_hw_fullframe(&mut scheduled, &s, 60, Damage::None, false, false, HOLDS)
3096 .force_idr
3097 );
3098 let mut off_beat = StripeState::default();
3099 assert!(
3100 !decide_hw_fullframe(&mut off_beat, &s, 61, Damage::None, false, false, HOLDS)
3101 .force_idr
3102 );
3103 }
3104
3105 #[test]
3110 fn x11_software_emits_on_change_and_stays_quiet_when_static() {
3111 let s = RustCaptureSettings {
3112 width: 128,
3113 height: 128,
3114 codec: Codec::Jpeg,
3115 use_cpu: true,
3116 jpeg_quality: 60,
3117 use_paint_over_quality: false,
3118 ..Default::default()
3119 };
3120 let mut p = X11Pipeline::new(s);
3121 let stride = 128 * 4;
3122 let frame_a = vec![10u8; stride * 128];
3123 let mut frame_b = frame_a.clone();
3124 for px in frame_b.iter_mut().take(stride * 40) {
3125 *px = 200;
3126 }
3127 let n1 = p.process(&frame_a, stride).len();
3128 let n2 = p.process(&frame_a, stride).len();
3129 let n3 = p.process(&frame_b, stride).len();
3130 assert!(
3131 n1 > 0,
3132 "first frame should emit (all stripes dirty vs init)"
3133 );
3134 assert_eq!(n2, 0, "identical static frame should emit nothing");
3135 assert!(n3 > 0, "changed frame should emit dirty stripes");
3136 }
3137
3138 #[test]
3143 fn x11_software_h264_streams_recovery_burst_after_requested_idr() {
3144 let s = RustCaptureSettings {
3145 width: 128,
3146 height: 128,
3147 codec: Codec::H264,
3148 use_cpu: true,
3149 video_crf: 25,
3150 video_paintover_burst_frames: 5,
3151 use_paint_over_quality: false,
3152 video_streaming_mode: false,
3153 target_fps: 60.0,
3154 ..Default::default()
3155 };
3156 let mut p = X11Pipeline::new(s);
3157 let stride = 128 * 4;
3158 let frame = vec![10u8; stride * 128];
3159 assert!(!p.process(&frame, stride).is_empty(), "first frame emits");
3160 for _ in 0..4 {
3161 let _ = p.process(&frame, stride);
3162 }
3163 assert!(
3164 p.process(&frame, stride).is_empty(),
3165 "static screen is quiet before the request"
3166 );
3167 p.request_idr();
3168 assert!(
3169 !p.process(&frame, stride).is_empty(),
3170 "requested IDR emits on a static screen"
3171 );
3172 for i in 0..5 {
3173 assert!(
3174 !p.process(&frame, stride).is_empty(),
3175 "recovery burst frame {i} streams while static"
3176 );
3177 }
3178 assert!(
3179 p.process(&frame, stride).is_empty(),
3180 "stream goes quiet again after the recovery burst"
3181 );
3182 }
3183
3184 fn noise(seed: u8) -> Vec<u8> {
3187 (0..640 * 4 * 384)
3188 .map(|i| {
3189 (i as u32)
3190 .wrapping_mul(2654435761)
3191 .wrapping_add(seed as u32 * 97) as u8
3192 })
3193 .collect()
3194 }
3195
3196 #[cfg(feature = "gpl")]
3200 #[test]
3201 fn an_x264_cleanup_whose_frames_refine_nothing_ends() {
3202 let mut p = X11Pipeline::new(RustCaptureSettings {
3203 width: 640,
3204 height: 384,
3205 codec: Codec::H264,
3206 use_cpu: true,
3207 video_fullframe: true,
3208 video_cbr_mode: true,
3209 video_bitrate_kbps: 100,
3210 video_min_qp: 51,
3211 video_paintover_crf: 18,
3212 paint_over_trigger_frames: 15,
3213 use_paint_over_quality: true,
3214 target_fps: 60.0,
3215 ..Default::default()
3216 });
3217 for i in 0..12u8 {
3218 p.process(&noise(i), 640 * 4);
3219 }
3220 let window = converge_frames(&p.settings) as usize;
3221 let frames: Vec<Vec<usize>> = (0..window + 60)
3222 .map(|_| {
3223 p.process(&noise(42), 640 * 4)
3224 .iter()
3225 .map(|e| e.data.len())
3226 .collect()
3227 })
3228 .collect();
3229 let last = frames
3230 .iter()
3231 .rposition(|f| !f.is_empty())
3232 .expect("a cleanup");
3233 let empty = crate::encoders::codec::VIDEO_HEADER_LEN + 4 * EMPTY_SLICE_BYTES;
3234 let run = (EMPTY_S * 60.0) as usize;
3235 assert!(last < window, "the cleanup ends at {last} of {window}");
3236 assert!(
3237 frames[last + 1 - run..=last]
3238 .iter()
3239 .flatten()
3240 .all(|&b| b <= empty),
3241 "on frames that carry nothing: {:?}",
3242 &frames[last + 1 - run..=last]
3243 );
3244 }
3245
3246 fn cleanup_frame_that_moved(
3250 s: &RustCaptureSettings,
3251 key: bool,
3252 ) -> [[(Option<u32>, bool); 2]; 2] {
3253 let still = noise(42);
3254 let lead = |p: &mut X11Pipeline| {
3255 for i in 0..3u8 {
3256 p.process(&noise(i), 640 * 4);
3257 }
3258 };
3259 let mut p = X11Pipeline::new(s.clone());
3260 lead(&mut p);
3261 let at = (0..300)
3262 .find(|_| {
3263 p.process(&still, 640 * 4);
3264 p.hold[0].0.is_some() && p.hold[0].1 == key
3265 })
3266 .expect("a still screen is cleaned up");
3267 let mut q = X11Pipeline::new(s.clone());
3268 lead(&mut q);
3269 for _ in 0..at {
3270 q.process(&still, 640 * 4);
3271 }
3272 assert!(
3273 !q.process(&noise(9), 640 * 4).is_empty(),
3274 "the moved frame is sent"
3275 );
3276 [p.hold, q.hold]
3277 }
3278
3279 #[test]
3284 fn a_turbo_frame_decided_held_that_moved_goes_to_the_rate_control() {
3285 let s = RustCaptureSettings {
3286 width: 640,
3287 height: 384,
3288 codec: Codec::Av1,
3289 use_cpu: true,
3290 video_cbr_mode: true,
3291 video_bitrate_kbps: 100,
3292 video_crf: 25,
3293 video_paintover_crf: 18,
3294 paint_over_trigger_frames: 15,
3295 use_paint_over_quality: true,
3296 video_streaming_mode: true,
3297 target_fps: 60.0,
3298 ..Default::default()
3299 };
3300 let p = X11Pipeline::new(s.clone());
3301 assert!(p.hw.is_some(), "AV1 runs as a full-frame session");
3302 if !codec_sys::svtav1::HAS_EVENTS {
3303 assert!(!EncoderQuality::of(p.hw.as_ref().unwrap()).holds);
3304 return;
3305 }
3306 for key in [false, true] {
3307 let [still, moved] = cleanup_frame_that_moved(&s, key);
3308 assert_eq!(
3309 still[1], still[0],
3310 "a frame that stood still keeps its hold"
3311 );
3312 assert!(moved[0].0.is_some() && moved[0].1 == key, "{moved:?}");
3313 assert_eq!(
3314 moved[1],
3315 (None, false),
3316 "a held frame that moved is coded under the rate control"
3317 );
3318 }
3319 }
3320
3321 #[test]
3325 fn a_turbo_frame_at_the_paint_over_quality_that_moved_is_coded_at_the_session_quality() {
3326 let s = RustCaptureSettings {
3327 width: 640,
3328 height: 384,
3329 codec: Codec::Vp9,
3330 use_cpu: true,
3331 video_cbr_mode: false,
3332 video_crf: 25,
3333 video_paintover_crf: 18,
3334 paint_over_trigger_frames: 15,
3335 use_paint_over_quality: true,
3336 video_streaming_mode: true,
3337 target_fps: 60.0,
3338 ..Default::default()
3339 };
3340 assert!(
3341 X11Pipeline::new(s.clone()).hw.is_some(),
3342 "VP9 runs as a full-frame session"
3343 );
3344 for key in [false, true] {
3345 let [still, moved] = cleanup_frame_that_moved(&s, key);
3346 assert_eq!(
3347 still,
3348 [(Some(18), key); 2],
3349 "a frame that stood still keeps it"
3350 );
3351 assert_eq!(
3352 moved,
3353 [(Some(18), key), (None, false)],
3354 "a frame that moved does not"
3355 );
3356 }
3357 }
3358
3359 #[test]
3365 fn a_turbo_stripe_at_the_paint_over_quality_codes_a_change_at_the_session_quality() {
3366 use crate::encoders::codec::{FRAME_KEY, parse_video_type};
3367 let session = |turbo: bool| {
3368 let mut p = X11Pipeline::new(RustCaptureSettings {
3369 width: 640,
3370 height: 384,
3371 codec: Codec::H264,
3372 use_cpu: true,
3373 video_fullframe: true,
3374 video_cbr_mode: false,
3375 video_crf: 25,
3376 video_paintover_crf: 18,
3377 paint_over_trigger_frames: 15,
3378 use_paint_over_quality: true,
3379 video_streaming_mode: turbo,
3380 target_fps: 60.0,
3381 ..Default::default()
3382 });
3383 assert!(p.hw.is_none(), "software H.264 runs striped");
3384 for i in 0..12u8 {
3385 p.process(&noise(i), 640 * 4);
3386 }
3387 p
3388 };
3389 let mut p = session(true);
3390 let (mut held, mut key) = (None, None);
3391 for i in 0..300 {
3392 if held.is_none() && stripes_held_still(&p.stripes, &p.settings) {
3393 held = Some(i);
3394 }
3395 let out = p.process(&noise(42), 640 * 4);
3396 let keyed = out
3397 .iter()
3398 .any(|e| parse_video_type(e.data[1]).is_some_and(|(_, k)| k == FRAME_KEY));
3399 if held.is_some() && keyed {
3400 key = Some(i);
3401 break;
3402 }
3403 }
3404 let held = held.expect("a still stripe is cleaned up");
3405 let key = key.expect("a large change is cleaned up by a key frame");
3406 for at in [held, held + 15, key] {
3407 let jump = |turbo: bool| -> usize {
3408 let mut p = session(turbo);
3409 for _ in 0..at {
3410 p.process(&noise(42), 640 * 4);
3411 }
3412 p.process(&noise(9), 640 * 4)
3413 .iter()
3414 .map(|e| e.data.len())
3415 .sum()
3416 };
3417 let (turbo, plain) = (jump(true), jump(false));
3418 assert!(
3419 turbo as f64 <= plain as f64 * 1.2,
3420 "a change {at} frames still is coded at the session's quality: {turbo} bytes \
3421 against {plain}"
3422 );
3423 }
3424 }
3425
3426 #[test]
3432 fn x11_colorspace_desc_reports_what_the_software_encoder_carries() {
3433 let carries_444 = crate::encoders::software_fullcolor(Codec::H264);
3434 assert_eq!(
3435 carries_444,
3436 crate::encoders::software_library(Codec::H264) == "x264"
3437 );
3438 let i444 = if carries_444 {
3439 "I444 (Full Range)"
3440 } else {
3441 "I420 (Limited Range)"
3442 };
3443 for (fullcolor, expected) in [(true, i444), (false, "I420 (Limited Range)")] {
3444 let settings = RustCaptureSettings {
3445 width: 64,
3446 height: 64,
3447 codec: Codec::H264,
3448 use_cpu: true,
3449 video_fullcolor: fullcolor,
3450 ..Default::default()
3451 };
3452 let p = X11Pipeline::new(settings.clone());
3453 assert_eq!(
3454 p.encoder_name(),
3455 format!("CPU ({})", crate::encoders::software_library(Codec::H264))
3456 );
3457 assert_eq!(p.colorspace_desc(), expected);
3458 assert_eq!(
3462 p.colorspace_desc(),
3463 crate::encoders::colorspace_desc(
3464 crate::encoders::session_fullcolor(None, &settings),
3465 crate::encoders::session_full_range(None, &settings),
3466 ),
3467 "X11 and Wayland must describe the same session identically"
3468 );
3469 }
3470 }
3471
3472 #[test]
3475 fn x11_pipeline_reports_the_session_it_settled_on() {
3476 let report = crate::report::StreamReport::new("x11");
3477 let _scope = crate::report::enter(&report);
3478 let mut p = X11Pipeline::new(RustCaptureSettings {
3479 width: 64,
3480 height: 64,
3481 codec: Codec::H264,
3482 use_cpu: true,
3483 video_fullframe: true,
3484 ..Default::default()
3485 });
3486 let info = report.info();
3487 assert_eq!(info.encoder, crate::encoders::software_library(Codec::H264));
3488 assert!(!info.hardware);
3489 assert_eq!(info.encoder_reason, "software encoding selected");
3490 assert_eq!(info.codec, "h264");
3491 assert_eq!(info.stripes, 1);
3492 assert!(info.gpu.is_empty());
3493
3494 let pixels = vec![0x80u8; 64 * 64 * 4];
3495 let mut stripes = p.process(&pixels, 64 * 4);
3496 assert!(!stripes.is_empty(), "the first frame of a pipeline is sent");
3497 let start = crate::wayland::host::now_ns();
3498 crate::encoders::software::FrameTiming::stamp(
3499 &mut stripes,
3500 start - 2_000_000,
3501 start - 1_000_000,
3502 );
3503 report.tally(&stripes);
3504 let totals = report.totals();
3505 assert_eq!(totals.frames, 1);
3506 assert_eq!(
3507 totals.bytes,
3508 stripes.iter().map(|s| s.data.len() as u64).sum::<u64>()
3509 );
3510 assert!(totals.encode_ns >= 1_000_000 && totals.pipeline_ns >= 2_000_000);
3511 }
3512
3513 #[test]
3515 fn x11_jpeg_pipeline_names_its_encoder() {
3516 let report = crate::report::StreamReport::new("x11");
3517 let _scope = crate::report::enter(&report);
3518 let p = X11Pipeline::new(RustCaptureSettings {
3519 width: 64,
3520 height: 64,
3521 codec: Codec::Jpeg,
3522 use_cpu: true,
3523 ..Default::default()
3524 });
3525 assert_eq!(p.encoder_name(), format!("CPU ({})", report.info().encoder));
3526 }
3527
3528 #[test]
3530 #[ignore]
3531 fn gpu_x11_pipeline_reports_the_hardware_session() {
3532 let report = crate::report::StreamReport::new("x11");
3533 let _scope = crate::report::enter(&report);
3534 let p = X11Pipeline::new(RustCaptureSettings {
3535 width: 1280,
3536 height: 720,
3537 codec: Codec::H264,
3538 target_fps: 60.0,
3539 video_crf: 25,
3540 ..Default::default()
3541 });
3542 assert!(p.is_hardware(), "needs a hardware encoder on render node 0");
3543 let info = report.info();
3544 println!("{info:?}");
3545 assert!(info.hardware);
3546 assert_eq!(info.encoder, p.encoder_name());
3547 assert!(info.encoder_reason.is_empty());
3548 assert_eq!(info.encode_node, 0);
3549 assert!(!info.driver.is_empty());
3550 }
3551
3552 #[test]
3555 fn static_frames_stay_silent_without_request() {
3556 let s = RustCaptureSettings {
3557 use_paint_over_quality: false,
3558 ..hw_settings()
3559 };
3560 let mut st = StripeState::default();
3561 for fc in [0u16, 1, 120, 240] {
3562 let d = decide_hw_fullframe(&mut st, &s, fc, Damage::None, false, false, HOLDS);
3563 assert!(!d.send && !d.force_idr, "frame {fc} should stay idle");
3564 }
3565 }
3566
3567 #[test]
3570 fn requested_idr_recovers_even_without_paint_over() {
3571 let s = RustCaptureSettings {
3572 use_paint_over_quality: false,
3573 video_paintover_burst_frames: 5,
3574 ..hw_settings()
3575 };
3576 let mut st = StripeState::default();
3577 let d = decide_hw_fullframe(&mut st, &s, 5, Damage::None, false, true, HOLDS);
3578 assert!(d.send && d.force_idr && d.hold_qp.is_none());
3579 assert_eq!(
3580 st.h264_burst_frames_remaining, 5,
3581 "recovery burst armed without paint-over"
3582 );
3583 let d = decide_hw_fullframe(&mut st, &s, 6, Damage::None, false, false, HOLDS);
3584 assert!(d.send && !d.force_idr);
3585 assert_eq!(d.target_qp, 25);
3586 }
3587
3588 #[test]
3589 fn configured_interval_restores_scheduled_keyframes() {
3590 let mut s = hw_settings();
3591 s.keyframe_interval_s = 2.0;
3592 assert!(periodic_idr_due(&s, 0));
3593 assert!(!periodic_idr_due(&s, 1));
3594 assert!(periodic_idr_due(&s, 120));
3595 let mut st = StripeState::default();
3596 let d = decide_hw_fullframe(&mut st, &s, 120, Damage::None, false, false, HOLDS);
3597 assert!(
3598 d.send && d.force_idr,
3599 "interval keyframe fires on a static screen"
3600 );
3601 s.keyframe_interval_s = 0.0;
3602 assert!(!periodic_idr_due(&s, 0) && !periodic_idr_due(&s, 120));
3603 }
3604
3605 #[test]
3609 fn a_held_refresh_fits_the_budget_a_constant_rate_leaves() {
3610 let cbr = RustCaptureSettings {
3611 video_cbr_mode: true,
3612 video_paintover_crf: 18,
3613 target_fps: 30.0,
3614 ..hw_settings()
3615 };
3616 let q = |s: &RustCaptureSettings, last| {
3617 held_refresh_quality(
3618 s,
3619 EncoderQuality {
3620 last,
3621 bytes: None,
3622 holds: true,
3623 reopens: false,
3624 keys: true,
3625 band: None,
3626 measures: false,
3627 psnr: None,
3628 },
3629 )
3630 };
3631 assert_eq!(
3632 q(&cbr, Some(51)),
3633 22,
3634 "thirty frames of budget are 29 steps finer than the rate control"
3635 );
3636 assert_eq!(q(&cbr, Some(45)), 18);
3637 assert_eq!(q(&cbr, Some(30)), 18);
3638 assert_eq!(q(&cbr, None), 18);
3639 let crf = RustCaptureSettings {
3640 video_cbr_mode: false,
3641 ..cbr.clone()
3642 };
3643 assert_eq!(
3644 q(&crf, Some(45)),
3645 18,
3646 "a constant quality has no budget to fit"
3647 );
3648 }
3649
3650 #[test]
3652 fn damage_rectangles_report_their_share_of_the_frame() {
3653 use smithay::utils::Rectangle;
3654 assert_eq!(Damage::of_rects(&[], 100, 100), Damage::None);
3655 let quarter = [Rectangle::new((0, 0).into(), (50, 50).into())];
3656 assert_eq!(Damage::of_rects(&quarter, 100, 100), Damage::Area(0.25));
3657 let past = [Rectangle::new((80, 80).into(), (50, 50).into())];
3658 assert_eq!(
3659 Damage::of_rects(&past, 100, 100),
3660 Damage::Area(0.04),
3661 "clipped to the frame"
3662 );
3663 let whole = [Rectangle::new((0, 0).into(), (200, 200).into())];
3664 assert_eq!(Damage::of_rects(&whole, 100, 100), Damage::Area(1.0));
3665 }
3666
3667 #[test]
3670 fn hash_damage_reports_the_bands_that_changed() {
3671 let (w, h) = (32usize, 256usize);
3672 let mut bands = Vec::new();
3673 let frame = vec![7u8; w * 4 * h];
3674 assert_eq!(
3675 hash_damage(&mut bands, &frame, w * 4, h, 10, 20),
3676 Damage::Area(1.0),
3677 "the first frame is all new"
3678 );
3679 assert_eq!(
3680 hash_damage(&mut bands, &frame, w * 4, h, 10, 20),
3681 Damage::None
3682 );
3683 let mut caret = frame.clone();
3684 caret[w * 4 * (DAMAGE_BAND_ROWS + 1)] = 0;
3685 let n = h.div_ceil(DAMAGE_BAND_ROWS);
3686 assert_eq!(
3687 hash_damage(&mut bands, &caret, w * 4, h, 10, 20),
3688 Damage::Area(1.0 / n as f32),
3689 "one band of {n}"
3690 );
3691 }
3692
3693 #[test]
3696 fn band_rects_cover_the_runs_of_changed_bands() {
3697 let r = |y: i32, h: i32| Rectangle::new((0, y).into(), (64, h).into());
3698 assert_eq!(band_rects(&[], 64, 100), vec![r(0, 100)]);
3699 assert_eq!(band_rects(&[false, false, false, false], 64, 100), vec![]);
3700 assert_eq!(
3701 band_rects(&[true, true, false, true], 64, 100),
3702 vec![r(0, 64), r(96, 4)]
3703 );
3704 assert_eq!(band_damage(&[true, true, false, true]), Damage::Area(0.75));
3705 assert_eq!(band_damage(&[]), Damage::Area(1.0));
3706 }
3707
3708 #[test]
3712 fn hashed_damage_reports_the_bands_that_changed() {
3713 let n = 1080usize.div_ceil(DAMAGE_BAND_ROWS);
3714 let frame: Vec<u64> = (0..n as u64).collect();
3715 let mut last = Vec::new();
3716 assert_eq!(hashed_damage(&mut last, frame.clone()), Damage::Area(1.0));
3717 assert_eq!(hashed_damage(&mut last, frame.clone()), Damage::None);
3718 let mut caret = frame.clone();
3719 caret[6] ^= 1;
3720 let d = hashed_damage(&mut last, caret.clone());
3721 assert_eq!(d, Damage::Area(1.0 / n as f32));
3722 assert!(!d.is_motion());
3723 let mut window = caret.clone();
3724 for band in &mut window[10..13] {
3725 *band ^= 1;
3726 }
3727 assert!(hashed_damage(&mut last, window).is_motion());
3728 assert_eq!(
3729 hashed_damage(&mut last, frame[..n - 1].to_vec()),
3730 Damage::Area(1.0)
3731 );
3732 }
3733}
3734
3735#[cfg(test)]
3736mod vbv_tests {
3737 #[test]
3740 fn vbv_policy() {
3741 use crate::encoders::vbv_bits;
3742 let frame = 4_000_000f64 / 60.0;
3743 assert_eq!(
3744 vbv_bits(4_000_000, 60.0, 0.0, 0.0),
3745 (frame * 1.5).round() as u32
3746 );
3747 assert_eq!(
3748 vbv_bits(4_000_000, 60.0, 2.0, 0.0),
3749 (frame * 3.0).round() as u32
3750 );
3751 assert_eq!(vbv_bits(4_000_000, 60.0, 2.0, 1.0), frame.round() as u32);
3752 assert_eq!(
3753 vbv_bits(8_000_000, 60.0, 0.0, 1.0),
3754 (2.0 * frame).round() as u32
3755 );
3756 }
3757}