1use super::codec::{Codec, push_jpeg_header};
17#[cfg(feature = "gpl")]
18use super::codec::{
19 FRAME_DELTA, FRAME_INTRA, FRAME_KEY, h264_dpb_frames, h264_level, h264_reference_level,
20 push_video_header,
21};
22#[cfg(feature = "gpl")]
23use super::frame_rate::FrameRate;
24use super::reference::Reference;
25#[cfg(feature = "gpl")]
26use super::reference::{Invalidation, ReferenceWindow};
27#[cfg(feature = "gpl")]
28use super::sps::{WideFrameNum, h264_frame_num_range};
29use crate::RustCaptureSettings;
30use crate::pipeline::{Cleanup, Damage};
31use rayon::prelude::*;
32use smithay::utils::{Physical, Rectangle};
33#[cfg(feature = "gpl")]
34use std::ffi::CString;
35#[cfg(feature = "gpl")]
36use std::ptr;
37use std::sync::Arc;
38use yuv::{BufferStoreMut, YuvConversionMode, YuvPlanarImageMut, YuvRange, YuvStandardMatrix};
39
40pub const MAX_STRIPE_CAPACITY: usize = 64;
47
48#[allow(clippy::too_many_arguments)]
79pub(crate) fn convert_to_yuv_mt(
80 src: &[u8],
81 src_stride: u32,
82 width: usize,
83 height: usize,
84 rgba_input: bool,
85 i444: bool,
86 full_range: bool,
87 bt601: bool,
88 y_buf: &mut [u8],
89 u_buf: &mut [u8],
90 v_buf: &mut [u8],
91 strides: (usize, usize),
92 bands: usize,
93) -> Result<(), yuv::YuvError> {
94 let (y_stride, uv_stride) = strides;
95 let range = if full_range {
96 YuvRange::Full
97 } else {
98 YuvRange::Limited
99 };
100 let matrix = if bt601 {
101 YuvStandardMatrix::Bt601
102 } else {
103 YuvStandardMatrix::Bt709
104 };
105
106 let convert_band = |src_band: &[u8], y: &mut [u8], u: &mut [u8], v: &mut [u8], h: usize| {
107 let mut img = YuvPlanarImageMut {
108 y_plane: BufferStoreMut::Borrowed(y),
109 y_stride: y_stride as u32,
110 u_plane: BufferStoreMut::Borrowed(u),
111 u_stride: uv_stride as u32,
112 v_plane: BufferStoreMut::Borrowed(v),
113 v_stride: uv_stride as u32,
114 width: width as u32,
115 height: h as u32,
116 };
117 match (i444, rgba_input) {
118 (true, true) => yuv::rgba_to_yuv444(
119 &mut img,
120 src_band,
121 src_stride,
122 range,
123 matrix,
124 YuvConversionMode::Fast,
125 ),
126 (true, false) => yuv::bgra_to_yuv444(
127 &mut img,
128 src_band,
129 src_stride,
130 range,
131 matrix,
132 YuvConversionMode::Fast,
133 ),
134 (false, true) => yuv::rgba_to_yuv420(
135 &mut img,
136 src_band,
137 src_stride,
138 range,
139 matrix,
140 YuvConversionMode::Fast,
141 ),
142 (false, false) => yuv::bgra_to_yuv420(
143 &mut img,
144 src_band,
145 src_stride,
146 range,
147 matrix,
148 YuvConversionMode::Fast,
149 ),
150 }
151 };
152
153 let band_h = ((height / bands.max(1)) & !1).max(2);
154 if bands <= 1 || height <= band_h {
155 return convert_band(src, y_buf, u_buf, v_buf, height);
156 }
157
158 let uv_rows = |rows: usize| if i444 { rows } else { rows / 2 };
159 let mut jobs = Vec::new();
160 let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
161 let mut row = 0;
162 while row < height {
163 let h = if height - row < band_h + 2 {
164 height - row
165 } else {
166 band_h
167 };
168 let (src_band, s_next) = src_rest.split_at(h * src_stride as usize);
169 let (y_band, y_next) = y_rest.split_at_mut(h * y_stride);
170 let (u_band, u_next) = u_rest.split_at_mut(uv_rows(h) * uv_stride);
171 let (v_band, v_next) = v_rest.split_at_mut(uv_rows(h) * uv_stride);
172 src_rest = s_next;
173 y_rest = y_next;
174 u_rest = u_next;
175 v_rest = v_next;
176 row += h;
177 jobs.push((src_band, y_band, u_band, v_band, h));
178 }
179 jobs.into_par_iter()
180 .map(|(src_band, y_band, u_band, v_band, h)| {
181 convert_band(src_band, y_band, u_band, v_band, h)
182 })
183 .collect::<Result<Vec<()>, _>>()
184 .map(|_| ())
185}
186
187const YUV10_SHIFT: u32 = 14;
189
190fn yuv10_coefficients(full_range: bool) -> ([[i32; 3]; 3], i32) {
194 const KR: f64 = 0.2126;
195 const KB: f64 = 0.0722;
196 const KG: f64 = 1.0 - KR - KB;
197 let (luma, chroma, offset) = if full_range {
198 (1023.0 / 255.0, 1023.0 / 255.0, 0)
199 } else {
200 (876.0 / 255.0, 896.0 / 255.0, 64)
201 };
202 let fixed = |k: f64| (k * (1 << YUV10_SHIFT) as f64).round() as i32;
203 let (cb, cr) = (chroma / (2.0 * (1.0 - KB)), chroma / (2.0 * (1.0 - KR)));
204 (
205 [
206 [fixed(luma * KR), fixed(luma * KG), fixed(luma * KB)],
207 [fixed(-cb * KR), fixed(-cb * KG), fixed(cb * (1.0 - KB))],
208 [fixed(cr * (1.0 - KR)), fixed(-cr * KG), fixed(-cr * KB)],
209 ],
210 offset,
211 )
212}
213
214#[allow(clippy::too_many_arguments)]
224pub(crate) fn convert_to_yuv10_mt(
225 src: &[u8],
226 src_stride: usize,
227 width: usize,
228 height: usize,
229 rgba_input: bool,
230 i444: bool,
231 full_range: bool,
232 y_buf: &mut [u16],
233 u_buf: &mut [u16],
234 v_buf: &mut [u16],
235 strides: (usize, usize),
236 bands: usize,
237) {
238 let (y_stride, uv_stride) = strides;
239 let (k, y_offset) = yuv10_coefficients(full_range);
240 let (ri, bi) = if rgba_input { (0, 2) } else { (2, 0) };
241 let half = 1 << (YUV10_SHIFT - 1);
242 let luma = move |r: i32, g: i32, b: i32| {
243 ((k[0][0] * r + k[0][1] * g + k[0][2] * b + half) >> YUV10_SHIFT) + y_offset
244 };
245 let chroma = move |row: &[i32; 3], r: i32, g: i32, b: i32, shift: u32| {
246 (((row[0] * r + row[1] * g + row[2] * b + (1 << (shift - 1))) >> shift) + 512)
247 .clamp(0, 1023) as u16
248 };
249
250 let convert_band = move |src: &[u8], y: &mut [u16], u: &mut [u16], v: &mut [u16], h: usize| {
251 for row in 0..h {
252 let line = &src[row * src_stride..row * src_stride + width * 4];
253 let out = &mut y[row * y_stride..row * y_stride + width];
254 for (px, y) in line.as_chunks::<4>().0.iter().zip(out.iter_mut()) {
255 *y = luma(px[ri] as i32, px[1] as i32, px[bi] as i32) as u16;
256 }
257 }
258 if i444 {
259 for row in 0..h {
260 let line = &src[row * src_stride..row * src_stride + width * 4];
261 let cb = &mut u[row * uv_stride..row * uv_stride + width];
262 let cr = &mut v[row * uv_stride..row * uv_stride + width];
263 for ((px, cb), cr) in line
264 .as_chunks::<4>()
265 .0
266 .iter()
267 .zip(cb.iter_mut())
268 .zip(cr.iter_mut())
269 {
270 let (r, g, b) = (px[ri] as i32, px[1] as i32, px[bi] as i32);
271 *cb = chroma(&k[1], r, g, b, YUV10_SHIFT);
272 *cr = chroma(&k[2], r, g, b, YUV10_SHIFT);
273 }
274 }
275 return;
276 }
277 let cw = width.div_ceil(2);
278 for row in 0..h.div_ceil(2) {
279 let top = &src[2 * row * src_stride..2 * row * src_stride + width * 4];
280 let below = (2 * row + 1).min(h - 1);
281 let bottom = &src[below * src_stride..below * src_stride + width * 4];
282 let cb = &mut u[row * uv_stride..row * uv_stride + cw];
283 let cr = &mut v[row * uv_stride..row * uv_stride + cw];
284 for (col, (cb, cr)) in cb.iter_mut().zip(cr.iter_mut()).enumerate() {
285 let left = 8 * col;
286 let right = (left + 4).min(width * 4 - 4);
287 let sum = |i: usize| {
288 top[left + i] as i32
289 + top[right + i] as i32
290 + bottom[left + i] as i32
291 + bottom[right + i] as i32
292 };
293 let (r, g, b) = (sum(ri), sum(1), sum(bi));
294 *cb = chroma(&k[1], r, g, b, YUV10_SHIFT + 2);
295 *cr = chroma(&k[2], r, g, b, YUV10_SHIFT + 2);
296 }
297 }
298 };
299
300 let band_h = ((height / bands.max(1)) & !1).max(2);
301 if bands <= 1 || height <= band_h {
302 convert_band(src, y_buf, u_buf, v_buf, height);
303 return;
304 }
305 let uv_rows = |rows: usize| if i444 { rows } else { rows.div_ceil(2) };
306 let mut jobs = Vec::new();
307 let (mut src_rest, mut y_rest, mut u_rest, mut v_rest) = (src, y_buf, u_buf, v_buf);
308 let mut row = 0;
309 while row < height {
310 let h = if height - row < band_h + 2 {
311 height - row
312 } else {
313 band_h
314 };
315 let last = row + h >= height;
316 let take = |len: usize, rows: usize, stride: usize| if last { len } else { rows * stride };
317 let (src_band, s_next) = src_rest.split_at(take(src_rest.len(), h, src_stride));
318 let (y_band, y_next) = y_rest.split_at_mut(take(y_rest.len(), h, y_stride));
319 let (u_band, u_next) = u_rest.split_at_mut(take(u_rest.len(), uv_rows(h), uv_stride));
320 let (v_band, v_next) = v_rest.split_at_mut(take(v_rest.len(), uv_rows(h), uv_stride));
321 src_rest = s_next;
322 y_rest = y_next;
323 u_rest = u_next;
324 v_rest = v_next;
325 row += h;
326 jobs.push((src_band, y_band, u_band, v_band, h));
327 }
328 jobs.into_par_iter()
329 .for_each(|(src_band, y_band, u_band, v_band, h)| {
330 convert_band(src_band, y_band, u_band, v_band, h)
331 });
332}
333
334thread_local! {
335 static JPEG_COMPRESSOR: std::cell::RefCell<Option<turbojpeg::Compressor>> =
343 const { std::cell::RefCell::new(None) };
344}
345
346#[cfg(feature = "gpl")]
355static X264_OPEN_CLOSE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
356
357#[cfg(feature = "gpl")]
362fn qp_offset(bit_depth: u32) -> i32 {
363 6 * (bit_depth as i32 - 8)
364}
365
366#[cfg(feature = "gpl")]
369fn x264_frame_rate(fps: f64) -> FrameRate {
370 FrameRate::of(if fps < 1.0 { 30.0 } else { fps })
371}
372
373#[cfg(feature = "gpl")]
385pub struct H264EncoderWrapper {
386 encoder: *mut x264_sys::x264_t,
387 pub width: i32,
388 pub height: i32,
389 current_crf: i32,
390 pub is_i444: bool,
391 pub bit_depth: u32,
393 is_cbr: bool,
394 current_bitrate: i32,
395 current_vbv: i32,
396 current_fps: FrameRate,
397 threads: i32,
400 min_qp: i32,
401 max_qp: i32,
402 references: ReferenceWindow,
405 reference_frames: u32,
408 last_reference: Reference,
409 frame_num: WideFrameNum,
411 held_qp: Option<i32>,
413 last_qp: Option<u32>,
415}
416
417#[cfg(feature = "gpl")]
418unsafe impl Send for H264EncoderWrapper {}
419
420#[cfg(feature = "gpl")]
421impl Drop for H264EncoderWrapper {
422 fn drop(&mut self) {
423 if !self.encoder.is_null() {
424 let _guard = X264_OPEN_CLOSE_LOCK
425 .lock()
426 .unwrap_or_else(|e| e.into_inner());
427 unsafe { x264_sys::x264_encoder_close(self.encoder) };
428 self.encoder = ptr::null_mut();
429 }
430 }
431}
432
433#[cfg(feature = "gpl")]
434impl H264EncoderWrapper {
435 #[allow(clippy::too_many_arguments)]
482 pub fn new(
483 width: i32,
484 height: i32,
485 crf: i32,
486 is_i444: bool,
487 fps: f64,
488 threads: i32,
489 cbr_mode: bool,
490 bitrate_kbps: i32,
491 vbv_kbit: i32,
492 min_qp: i32,
493 max_qp: i32,
494 ) -> Option<Self> {
495 Self::open(
496 width,
497 height,
498 crf,
499 is_i444,
500 8,
501 fps,
502 threads,
503 cbr_mode,
504 bitrate_kbps,
505 vbv_kbit,
506 min_qp,
507 max_qp,
508 0,
509 None,
510 )
511 }
512
513 #[allow(clippy::too_many_arguments)]
517 pub fn with_depth(
518 width: i32,
519 height: i32,
520 crf: i32,
521 is_i444: bool,
522 bit_depth: u32,
523 fps: f64,
524 threads: i32,
525 cbr_mode: bool,
526 bitrate_kbps: i32,
527 vbv_kbit: i32,
528 min_qp: i32,
529 max_qp: i32,
530 reference_frames: u32,
531 ) -> Option<Self> {
532 Self::open(
533 width,
534 height,
535 crf,
536 is_i444,
537 bit_depth,
538 fps,
539 threads,
540 cbr_mode,
541 bitrate_kbps,
542 vbv_kbit,
543 min_qp,
544 max_qp,
545 reference_frames,
546 None,
547 )
548 }
549
550 pub fn ten_bit() -> bool {
553 static TEN_BIT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
554 *TEN_BIT.get_or_init(|| {
555 Self::with_depth(64, 64, 25, false, 10, 30.0, 1, false, 0, 0, 0, 0, 0).is_some()
556 })
557 }
558
559 #[allow(clippy::too_many_arguments)]
564 fn open(
565 width: i32,
566 height: i32,
567 crf: i32,
568 is_i444: bool,
569 bit_depth: u32,
570 fps: f64,
571 threads: i32,
572 cbr_mode: bool,
573 bitrate_kbps: i32,
574 vbv_kbit: i32,
575 min_qp: i32,
576 max_qp: i32,
577 reference_frames: u32,
578 key: Option<(i32, i32)>,
579 ) -> Option<Self> {
580 unsafe {
581 let mut param: x264_sys::x264_param_t = std::mem::zeroed();
582 let preset = CString::new("ultrafast").unwrap();
583 let tune = CString::new("zerolatency").unwrap();
584
585 if x264_sys::x264_param_default_preset(&mut param, preset.as_ptr(), tune.as_ptr()) < 0 {
586 return None;
587 }
588
589 let frame_rate = x264_frame_rate(fps);
590 param.i_width = width;
591 param.i_height = height;
592 param.i_fps_num = frame_rate.num;
593 param.i_fps_den = frame_rate.den;
594 param.i_keyint_max = x264_sys::X264_KEYINT_MAX_INFINITE as i32;
595 param.i_scenecut_threshold = 0;
596 let bitrate_bps = if cbr_mode {
597 bitrate_kbps.saturating_abs() as u64 * 1000
598 } else {
599 0
600 };
601 let dpb = h264_dpb_frames(
602 h264_reference_level(
603 h264_level(width as u32, height as u32, frame_rate.ceil(), bitrate_bps),
604 width as u32,
605 height as u32,
606 ),
607 width as u32,
608 height as u32,
609 );
610 let dpb = match reference_frames {
611 0 => dpb,
612 n => dpb.min(n),
613 };
614 param.i_dpb_size = dpb as i32;
615 if cbr_mode {
616 let bk = bitrate_kbps.saturating_abs();
617 param.rc.i_rc_method = x264_sys::X264_RC_ABR as i32;
618 param.rc.i_bitrate = bk;
619 param.rc.i_vbv_max_bitrate = bk;
620 param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
621 if let Some((budget, level)) = key {
622 let rate = ((budget as f64 * frame_rate.fps()) as i32).max(bk);
623 param.rc.i_bitrate = rate;
624 param.rc.i_vbv_max_bitrate = rate;
625 param.rc.i_vbv_buffer_size = budget.max(1);
626 param.rc.f_vbv_buffer_init = 1.0;
627 param.i_level_idc = level;
628 }
629 param.rc.b_filler = 0;
630 if min_qp > 0 {
631 param.rc.i_qp_min = min_qp.min(51) + qp_offset(bit_depth);
632 }
633 param.rc.i_qp_max =
634 if max_qp > 0 { max_qp.min(51) } else { 51 } + qp_offset(bit_depth);
635 } else {
636 param.rc.i_rc_method = x264_sys::X264_RC_CRF as i32;
637 param.rc.f_rf_constant = crf as f32;
638 if vbv_kbit > 0 {
639 param.rc.i_vbv_max_bitrate = bitrate_kbps.saturating_abs();
640 param.rc.i_vbv_buffer_size = vbv_kbit;
641 param.rc.i_qp_max = 51 + qp_offset(bit_depth);
642 }
643 }
644 let depth_flag = if bit_depth > 8 {
645 x264_sys::X264_CSP_HIGH_DEPTH
646 } else {
647 0
648 };
649 param.i_csp = (if is_i444 {
650 x264_sys::X264_CSP_I444
651 } else {
652 x264_sys::X264_CSP_I420
653 } | depth_flag) as i32;
654 param.i_bitdepth = bit_depth as i32;
655 param.vui.b_fullrange = if is_i444 { 1 } else { 0 };
656 param.vui.i_colorprim = 1;
657 param.vui.i_transfer = 1;
658 param.vui.i_colmatrix = 1;
659
660 let profile = CString::new(match (is_i444, bit_depth > 8) {
661 (true, _) => "high444",
662 (false, true) => "high10",
663 (false, false) => "baseline",
664 })
665 .unwrap();
666 x264_sys::x264_param_apply_profile(&mut param, profile.as_ptr());
667
668 param.i_threads = threads;
669 param.b_repeat_headers = 1;
670 param.b_annexb = 1;
671 param.i_log_level = x264_sys::X264_LOG_NONE;
672
673 let encoder = {
674 let _guard = X264_OPEN_CLOSE_LOCK
675 .lock()
676 .unwrap_or_else(|e| e.into_inner());
677 x264_sys::x264_encoder_open(&mut param)
678 };
679 if encoder.is_null() {
680 None
681 } else {
682 Some(Self {
683 encoder,
684 width,
685 height,
686 current_crf: crf,
687 is_i444,
688 bit_depth,
689 is_cbr: cbr_mode,
690 current_bitrate: bitrate_kbps.saturating_abs(),
691 current_vbv: vbv_kbit,
692 current_fps: frame_rate,
693 threads,
694 min_qp,
695 max_qp,
696 references: ReferenceWindow::new(dpb),
697 reference_frames,
698 last_reference: Reference::Untracked,
699 frame_num: WideFrameNum::default(),
700 held_qp: None,
701 last_qp: None,
702 })
703 }
704 }
705 }
706
707 pub fn reconfigure_crf(&mut self, new_crf: i32) {
717 if self.is_cbr || self.current_crf == new_crf {
718 return;
719 }
720 unsafe {
721 let mut param: x264_sys::x264_param_t = std::mem::zeroed();
722 x264_sys::x264_encoder_parameters(self.encoder, &mut param);
723 param.rc.f_rf_constant = new_crf as f32;
724 if x264_sys::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
725 self.current_crf = new_crf;
726 }
727 }
728 }
729
730 pub fn reconfigure_rate(&mut self, bitrate_kbps: i32, vbv_kbit: i32, fps: f64) {
747 let bk = bitrate_kbps.saturating_abs();
748 let new_fps = x264_frame_rate(fps);
749 let vbv_applies = self.is_cbr || vbv_kbit > 0;
750 let rate_changed =
751 vbv_applies && (self.current_bitrate != bk || self.current_vbv != vbv_kbit);
752 let reopen = self.current_fps != new_fps
753 || (!self.is_cbr && (self.current_vbv > 0) != (vbv_kbit > 0));
754 if !rate_changed && !reopen {
755 return;
756 }
757 if reopen {
758 if let Some(fresh) = H264EncoderWrapper::with_depth(
759 self.width,
760 self.height,
761 self.current_crf,
762 self.is_i444,
763 self.bit_depth,
764 new_fps.fps(),
765 self.threads,
766 self.is_cbr,
767 bk,
768 vbv_kbit,
769 self.min_qp,
770 self.max_qp,
771 self.reference_frames,
772 ) {
773 *self = fresh;
774 }
775 return;
776 }
777 unsafe {
778 let mut param: x264_sys::x264_param_t = std::mem::zeroed();
779 x264_sys::x264_encoder_parameters(self.encoder, &mut param);
780 param.rc.i_bitrate = bk;
781 if vbv_applies {
782 param.rc.i_vbv_max_bitrate = bk;
783 param.rc.i_vbv_buffer_size = vbv_kbit.max(1);
784 }
785 if x264_sys::x264_encoder_reconfig(self.encoder, &mut param) == 0 {
786 self.current_bitrate = bk;
787 self.current_vbv = vbv_kbit;
788 }
789 }
790 }
791
792 pub fn last_reference(&self) -> Reference {
794 self.last_reference
795 }
796
797 pub fn last_qp(&self) -> Option<u32> {
799 self.last_qp
800 }
801
802 pub fn hold_quantizer(&mut self, qp: i32) {
811 self.held_qp = Some(qp.clamp(0, 51));
812 }
813
814 fn open_for_held_key(&mut self) -> bool {
818 let level = unsafe {
819 let mut param: x264_sys::x264_param_t = std::mem::zeroed();
820 x264_sys::x264_encoder_parameters(self.encoder, &mut param);
821 param.i_level_idc
822 };
823 let budget = (self.current_bitrate as f64 * super::HELD_KEY_BUDGET_S).round() as i32;
824 let fresh = Self::open(
825 self.width,
826 self.height,
827 self.current_crf,
828 self.is_i444,
829 self.bit_depth,
830 self.current_fps.fps(),
831 self.threads,
832 self.is_cbr,
833 self.current_bitrate,
834 self.current_vbv,
835 self.min_qp,
836 self.max_qp,
837 self.reference_frames,
838 Some((budget, level)),
839 );
840 let Some(mut fresh) = fresh else { return false };
841 fresh.held_qp = self.held_qp;
842 *self = fresh;
843 true
844 }
845
846 fn restore_rate(&mut self) {
848 unsafe {
849 let mut param: x264_sys::x264_param_t = std::mem::zeroed();
850 x264_sys::x264_encoder_parameters(self.encoder, &mut param);
851 param.rc.i_bitrate = self.current_bitrate;
852 param.rc.i_vbv_max_bitrate = self.current_bitrate;
853 param.rc.i_vbv_buffer_size = self.current_vbv.max(1);
854 x264_sys::x264_encoder_reconfig(self.encoder, &mut param);
855 }
856 }
857
858 pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
861 match self.references.invalidate(frame_id) {
862 Invalidation::Forget(pts) => unsafe {
863 x264_sys::x264_encoder_invalidate_reference(self.encoder, pts as i64) == 0
864 },
865 Invalidation::KeyFrame | Invalidation::Ignored => true,
866 }
867 }
868
869 #[allow(clippy::too_many_arguments)]
894 pub fn encode_with_headers(
895 &mut self,
896 y: &[u8],
897 u: &[u8],
898 v: &[u8],
899 y_stride: i32,
900 u_stride: i32,
901 v_stride: i32,
902 frame_id: u16,
903 y_start: u16,
904 force_idr: bool,
905 omit_headers: bool,
906 output_buf: &mut Vec<u8>,
907 ) -> bool {
908 let key_budget =
909 self.is_cbr && force_idr && self.held_qp.is_some() && self.open_for_held_key();
910 let coded = self.encode_picture(
911 y,
912 u,
913 v,
914 y_stride,
915 u_stride,
916 v_stride,
917 frame_id,
918 y_start,
919 force_idr,
920 omit_headers,
921 output_buf,
922 );
923 if key_budget {
924 self.restore_rate();
925 }
926 coded
927 }
928
929 #[allow(clippy::too_many_arguments)]
931 fn encode_picture(
932 &mut self,
933 y: &[u8],
934 u: &[u8],
935 v: &[u8],
936 y_stride: i32,
937 u_stride: i32,
938 v_stride: i32,
939 frame_id: u16,
940 y_start: u16,
941 force_idr: bool,
942 omit_headers: bool,
943 output_buf: &mut Vec<u8>,
944 ) -> bool {
945 unsafe {
946 let mut pic_in: x264_sys::x264_picture_t = std::mem::zeroed();
947 x264_sys::x264_picture_init(&mut pic_in);
948
949 pic_in.img.i_csp = (if self.is_i444 {
950 x264_sys::X264_CSP_I444
951 } else {
952 x264_sys::X264_CSP_I420
953 } | if self.bit_depth > 8 {
954 x264_sys::X264_CSP_HIGH_DEPTH
955 } else {
956 0
957 }) as i32;
958 pic_in.img.i_plane = 3;
959 pic_in.img.plane[0] = y.as_ptr() as *mut u8;
960 pic_in.img.plane[1] = u.as_ptr() as *mut u8;
961 pic_in.img.plane[2] = v.as_ptr() as *mut u8;
962 pic_in.img.i_stride[0] = y_stride;
963 pic_in.img.i_stride[1] = u_stride;
964 pic_in.img.i_stride[2] = v_stride;
965 pic_in.i_pts = self.references.next_pts() as i64;
966 pic_in.i_type = if force_idr || !self.references.has_reference() {
967 x264_sys::X264_TYPE_IDR
968 } else {
969 x264_sys::X264_TYPE_AUTO
970 } as i32;
971 let held = self.held_qp.take();
972 let offset = qp_offset(self.bit_depth);
973 pic_in.i_qpplus1 = held.map_or(x264_sys::X264_QP_AUTO as i32, |q| q + offset + 1);
974
975 let mut pic_out: x264_sys::x264_picture_t = std::mem::zeroed();
976 let mut nals: *mut x264_sys::x264_nal_t = ptr::null_mut();
977 let mut i_nals: i32 = 0;
978
979 let frame_size = x264_sys::x264_encoder_encode(
980 self.encoder,
981 &mut nals,
982 &mut i_nals,
983 &mut pic_in,
984 &mut pic_out,
985 );
986
987 if frame_size > 0 {
988 if held.is_none() {
989 self.last_qp = (pic_out.i_qpplus1 > 0)
990 .then(|| (pic_out.i_qpplus1 - 1 - offset).max(0) as u32);
991 }
992 let frame_type = if pic_out.i_type == x264_sys::X264_TYPE_IDR as i32 {
993 FRAME_KEY
994 } else if pic_out.i_type == x264_sys::X264_TYPE_I as i32 {
995 FRAME_INTRA
996 } else {
997 FRAME_DELTA
998 };
999 self.last_reference = self.references.record(frame_id, frame_type != FRAME_DELTA);
1000 output_buf.clear();
1001 output_buf.reserve(super::codec::VIDEO_HEADER_LEN + frame_size as usize);
1002 if !omit_headers {
1003 push_video_header(
1004 output_buf,
1005 Codec::H264,
1006 frame_type,
1007 frame_id,
1008 y_start,
1009 self.width as u16,
1010 self.height as u16,
1011 self.last_reference,
1012 );
1013 }
1014
1015 let nal_slice = std::slice::from_raw_parts(nals, i_nals as usize);
1016 let mut widened = true;
1017 for nal in nal_slice {
1018 let payload = std::slice::from_raw_parts(nal.p_payload, nal.i_payload as usize);
1019 widened &= self.frame_num.push(payload, output_buf);
1020 }
1021 if !widened {
1022 self.references.reset();
1025 }
1026 if frame_type == FRAME_KEY {
1027 let stream = &output_buf[if omit_headers {
1028 0
1029 } else {
1030 super::codec::VIDEO_HEADER_LEN
1031 }..];
1032 self.references
1033 .set_frame_num_range(h264_frame_num_range(stream).unwrap_or(0));
1034 }
1035 return true;
1036 }
1037 }
1038 false
1039 }
1040}
1041
1042#[derive(Default)]
1080pub struct StripeState {
1081 pub no_motion_frame_count: u32,
1082 pub paint_over_sent: bool,
1083 pub dirty_run: u32,
1084 pub change_mass: f32,
1085 pub unclean_frames: u32,
1086 pub motion: f32,
1087 pub burst_held: bool,
1088 pub clean_quality: bool,
1089 pub rc_bytes: usize,
1090 pub idle_frames: u32,
1091 pub over_frames: u32,
1092 pub same_frames: u32,
1093 pub finest: Option<u32>,
1094 pub finest_frames: u32,
1095 pub settled: bool,
1096 pub fine_frames: u32,
1097 pub measured: Option<f32>,
1098 pub measuring: bool,
1099 pub level_checks: u32,
1100 pub measure_in: u32,
1101 pub sweep: Option<(f64, f64)>,
1102 pub sweep_frames: u32,
1103 pub sweep_coarser: u32,
1104 #[cfg(feature = "gpl")]
1105 pub h264_encoder: Option<H264EncoderWrapper>,
1106 #[cfg(not(feature = "gpl"))]
1107 pub h264_encoder: Option<crate::encoders::oh264::Openh264Encoder>,
1108 pub h264_burst_frames_remaining: i32,
1109 #[cfg(feature = "gpl")]
1110 pub y_buf: Vec<u8>,
1111 #[cfg(feature = "gpl")]
1112 pub u_buf: Vec<u8>,
1113 #[cfg(feature = "gpl")]
1114 pub v_buf: Vec<u8>,
1115 #[cfg(feature = "gpl")]
1117 pub yuv16: [Vec<u16>; 3],
1118 pub packet_buf: Vec<u8>,
1119 pub last_hash: u64,
1120 pub consecutive_changes: u32,
1121 pub in_damage_block: bool,
1122 pub damage_block_frames_remaining: i32,
1123}
1124
1125pub fn stripe_bit_depth(settings: &RustCaptureSettings) -> u32 {
1128 #[cfg(feature = "gpl")]
1129 if settings.codec == Codec::H264
1130 && settings.video_bit_depth >= 10
1131 && H264EncoderWrapper::ten_bit()
1132 {
1133 return 10;
1134 }
1135 let _ = settings;
1136 8
1137}
1138
1139fn fast_hash(bytes: &[u8]) -> u64 {
1147 xxhash_rust::xxh3::xxh3_64_with_seed(bytes, 0)
1148}
1149
1150impl StripeState {
1151 pub fn content_dirty(&mut self, bytes: &[u8], threshold: u32, duration: i32) -> bool {
1174 self.dirty_by(|| fast_hash(bytes), threshold, duration)
1175 }
1176
1177 pub fn hash_dirty(&mut self, hash: u64, threshold: u32, duration: i32) -> bool {
1180 self.dirty_by(|| hash, threshold, duration)
1181 }
1182
1183 fn dirty_by(&mut self, fast_hash: impl FnOnce() -> u64, threshold: u32, duration: i32) -> bool {
1184 if self.in_damage_block {
1185 self.damage_block_frames_remaining -= 1;
1186 if self.damage_block_frames_remaining == 1 {
1187 self.last_hash = fast_hash();
1188 } else if self.damage_block_frames_remaining <= 0 {
1189 let h = fast_hash();
1190 if h != self.last_hash {
1191 self.damage_block_frames_remaining = duration;
1192 } else {
1193 self.in_damage_block = false;
1194 self.consecutive_changes = 0;
1195 }
1196 self.last_hash = h;
1197 }
1198 return true;
1199 }
1200 let h = fast_hash();
1201 let changed = h != self.last_hash;
1202 self.last_hash = h;
1203 if changed {
1204 self.consecutive_changes += 1;
1205 if self.consecutive_changes >= threshold {
1206 self.in_damage_block = true;
1207 self.damage_block_frames_remaining = duration;
1208 }
1209 } else {
1210 self.consecutive_changes = 0;
1211 }
1212 changed
1213 }
1214}
1215
1216pub struct EncodedStripe {
1232 pub data: Arc<Vec<u8>>,
1233 pub codec: Codec,
1234 pub stripe_y_start: i32,
1235 pub stripe_height: i32,
1236 pub frame_id: i32,
1237 pub timing: FrameTiming,
1238 pub reference: Reference,
1239}
1240
1241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1246pub struct FrameTiming {
1247 pub capture_ns: i64,
1248 pub encode_start_ns: i64,
1249 pub encode_end_ns: i64,
1250}
1251
1252impl FrameTiming {
1253 pub fn stamp(stripes: &mut [EncodedStripe], capture_ns: i64, encode_start_ns: i64) {
1256 let timing = FrameTiming {
1257 capture_ns,
1258 encode_start_ns,
1259 encode_end_ns: crate::wayland::host::now_ns(),
1260 };
1261 for stripe in stripes {
1262 stripe.timing = timing;
1263 }
1264 }
1265}
1266
1267const MIN_STRIPE_HEIGHT: i32 = 64;
1269const CLEANUP_STAGGER_FRAMES: usize = 4;
1271const CARRY_RISE: f32 = 0.3;
1273const CARRY_FALL: f32 = 0.05;
1274
1275pub fn stripes_hold_quantizer(settings: &RustCaptureSettings) -> bool {
1280 cfg!(feature = "gpl") || !settings.video_cbr_mode
1281}
1282
1283pub fn stripes_held_still(stripes: &[StripeState], settings: &RustCaptureSettings) -> bool {
1287 !settings.video_cbr_mode
1288 && settings.use_paint_over_quality
1289 && settings.video_paintover_crf < settings.video_crf
1290 && stripes.iter().any(|st| {
1291 st.clean_quality
1292 || (st.burst_held && st.h264_burst_frames_remaining > 0)
1293 || crate::pipeline::cleanup_pending(
1294 st,
1295 settings.paint_over_trigger_frames,
1296 true,
1297 true,
1298 )
1299 })
1300}
1301
1302#[allow(clippy::too_many_arguments)]
1394pub fn encode_cpu(
1395 stripes: &mut Vec<StripeState>,
1396 carrying: &mut f32,
1397 raw_pixels: &[u8],
1398 width: i32,
1399 height: i32,
1400 damage_rects: &[Rectangle<i32, Physical>],
1401 settings: &RustCaptureSettings,
1402 frame_counter: u16,
1403 use_gpu: bool,
1404 hash_damage: bool,
1405 force_idr_all: bool,
1406) -> Vec<EncodedStripe> {
1407 let codec = settings.codec;
1408 let n_processing_stripes = stripe_count(height, codec, settings.video_fullframe);
1409
1410 if stripes.len() != n_processing_stripes {
1411 stripes.resize_with(n_processing_stripes, StripeState::default);
1412 }
1413
1414 let stripe_geometries = compute_stripe_geometries(height as usize, n_processing_stripes, codec);
1415
1416 let coded_quality = |st: &StripeState| -> Option<u32> {
1427 cfg_if::cfg_if! {
1428 if #[cfg(feature = "gpl")] {
1429 st.h264_encoder.as_ref().and_then(H264EncoderWrapper::last_qp)
1430 } else {
1431 let _ = st;
1432 None
1433 }
1434 }
1435 };
1436 let holds = stripes_hold_quantizer(settings);
1437 let converges = codec.is_video() && settings.video_cbr_mode && cfg!(feature = "gpl");
1438 let keys = codec.is_video() && holds && !converges;
1439 let paint_over_armed = |st: &StripeState| {
1440 if !codec.is_video() {
1441 return settings.use_paint_over_quality
1442 && settings.paint_over_jpeg_quality > settings.jpeg_quality;
1443 }
1444 crate::pipeline::paint_over_improves(
1445 settings,
1446 crate::pipeline::EncoderQuality {
1447 last: coded_quality(st),
1448 bytes: None,
1449 holds,
1450 reopens: false,
1451 keys: true,
1452 band: None,
1453 measures: false,
1454 psnr: None,
1455 },
1456 )
1457 };
1458 let trigger_frames = settings.paint_over_trigger_frames;
1459 let idle_candidate = damage_rects.is_empty()
1460 && !force_idr_all
1461 && !(codec.is_video() && settings.video_streaming_mode);
1462 if idle_candidate {
1463 let no_pending_send = |st: &StripeState| {
1464 (!codec.is_video() || st.h264_burst_frames_remaining <= 0)
1465 && !crate::pipeline::cleanup_pending(st, trigger_frames, paint_over_armed(st), keys)
1466 };
1467 let quiescent = if !hash_damage {
1468 stripes.iter().all(no_pending_send)
1469 } else {
1470 let width_bytes = width as usize * 4;
1471 let hint = stripes
1472 .iter()
1473 .enumerate()
1474 .min_by_key(|(_, st)| st.no_motion_frame_count)
1475 .map(|(i, _)| i)
1476 .unwrap_or(0);
1477 let clean = |i: usize| {
1478 let st = &stripes[i];
1479 if !no_pending_send(st) || st.in_damage_block {
1480 return false;
1481 }
1482 let (y, h) = stripe_geometries[i];
1483 let bytes = &raw_pixels[y * width_bytes..(y + h) * width_bytes];
1484 fast_hash(bytes) == st.last_hash
1485 };
1486 clean(hint) && (0..stripes.len()).filter(|&i| i != hint).all(clean)
1487 };
1488 if quiescent {
1489 for st in stripes.iter_mut() {
1490 let armed = paint_over_armed(st);
1491 crate::pipeline::cleanup_due(st, trigger_frames, armed, true, keys, Damage::None);
1492 st.consecutive_changes = 0;
1493 }
1494 return Vec::new();
1495 }
1496 }
1497 let mut stripe_is_dirty = vec![false; n_processing_stripes];
1498 if !damage_rects.is_empty() {
1499 for rect in damage_rects {
1500 let r_y_start = rect.loc.y.max(0) as usize;
1501 let r_y_end = (rect.loc.y + rect.size.h).min(height) as usize;
1502 if r_y_start < r_y_end {
1503 for (i, &(s_y, s_h)) in stripe_geometries.iter().enumerate() {
1504 let s_end = s_y + s_h;
1505 if r_y_start < s_end && r_y_end > s_y {
1506 stripe_is_dirty[i] = true;
1507 }
1508 }
1509 }
1510 }
1511 }
1512
1513 let width_usize = width as usize;
1514 let video = codec.is_video();
1515 let video_crf = settings.video_crf;
1516 let video_po_crf = settings.video_paintover_crf;
1517 let video_burst = settings.video_paintover_burst_frames;
1518 let video_fullcolor = settings.video_fullcolor;
1519 #[cfg(feature = "gpl")]
1520 let bit_depth = stripe_bit_depth(settings);
1521 let video_streaming = settings.video_streaming_mode;
1522 let jpeg_q = settings.jpeg_quality;
1523 let paint_q = settings.paint_over_jpeg_quality;
1524 let target_fps = settings.target_fps;
1525 let omit_headers = settings.omit_stripe_headers;
1526 let damage_block_threshold = settings.damage_block_threshold;
1527 let damage_block_duration = settings.damage_block_duration as i32;
1528 let video_cbr = settings.video_cbr_mode;
1529 #[cfg_attr(not(feature = "gpl"), allow(unused_variables))]
1533 let (video_bitrate, video_vbv) = stripe_rate_control(settings, *carrying, n_processing_stripes);
1534 #[cfg(feature = "gpl")]
1535 let cleanup_vbv = cleanup_vbv_kbit(settings, video_bitrate).max(video_vbv);
1536 #[cfg(feature = "gpl")]
1540 let h264_threads = if n_processing_stripes == 1 {
1541 std::thread::available_parallelism()
1542 .map(|n| n.get())
1543 .unwrap_or(1)
1544 .saturating_sub(1)
1545 .clamp(1, 4) as i32
1546 } else {
1547 1
1548 };
1549 #[cfg(feature = "gpl")]
1551 let empty_bytes = crate::encoders::codec::VIDEO_HEADER_LEN
1552 + crate::pipeline::EMPTY_SLICE_BYTES * h264_threads as usize;
1553 #[cfg(not(feature = "gpl"))]
1554 let empty_bytes = 0;
1555 #[cfg(feature = "gpl")]
1556 let csc_bands = 1;
1557 if video && video_fullcolor && !crate::encoders::software_fullcolor(Codec::H264) {
1558 static FULLCOLOR_LOGGED: std::sync::atomic::AtomicBool =
1559 std::sync::atomic::AtomicBool::new(false);
1560 if !FULLCOLOR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
1561 eprintln!(
1562 "[software] 4:4:4 full-color requested; OpenH264 is 4:2:0-only, encoding 4:2:0."
1563 );
1564 }
1565 }
1566
1567 let per_frame = stripes.len().div_ceil(CLEANUP_STAGGER_FRAMES).max(1);
1570 let mut granted = 0;
1571 let cleanup_allowed: Vec<bool> = stripes
1572 .iter()
1573 .map(|st| {
1574 if !crate::pipeline::cleanup_pending(st, trigger_frames, paint_over_armed(st), keys) {
1575 return true;
1576 }
1577 granted += 1;
1578 granted <= per_frame
1579 })
1580 .collect();
1581
1582 let stripe_body = |(i, stripe_state): (usize, &mut StripeState)| -> Option<EncodedStripe> {
1583 if i >= stripe_geometries.len() {
1584 return None;
1585 }
1586 let (y_start, actual_height) = stripe_geometries[i];
1587 let start_idx = y_start * width_usize * 4;
1588 let end_idx = start_idx + (actual_height * width_usize * 4);
1589 let stripe_bytes = &raw_pixels[start_idx..end_idx];
1590
1591 let is_dirty = if !hash_damage {
1592 stripe_is_dirty[i]
1593 } else {
1594 stripe_state.content_dirty(stripe_bytes, damage_block_threshold, damage_block_duration)
1595 };
1596 let armed = paint_over_armed(stripe_state);
1597 let cleanup = crate::pipeline::cleanup_due(
1598 stripe_state,
1599 trigger_frames,
1600 armed,
1601 cleanup_allowed[i],
1602 keys,
1603 if is_dirty {
1604 Damage::Unknown
1605 } else {
1606 Damage::None
1607 },
1608 );
1609 let mut send_this_stripe = is_dirty || cleanup != Cleanup::None || force_idr_all;
1610 let mut quality_or_crf = if !video { jpeg_q } else { video_crf };
1611 let mut force_idr = video && (force_idr_all || cleanup == Cleanup::Key);
1612 let mut hold = None;
1613 let quality = crate::pipeline::EncoderQuality {
1614 last: coded_quality(stripe_state),
1615 bytes: None,
1616 holds,
1617 reopens: false,
1618 keys: true,
1619 band: None,
1620 measures: false,
1621 psnr: None,
1622 };
1623 let refresh_crf = crate::pipeline::held_refresh_quality(settings, quality) as i32;
1624 if cleanup != Cleanup::None {
1625 if !video {
1626 quality_or_crf = paint_q;
1627 } else if cleanup == Cleanup::Key {
1628 hold = Some(video_po_crf);
1629 } else if holds && !converges {
1630 hold = Some(refresh_crf);
1631 }
1632 } else if force_idr_all && !is_dirty && !video && armed && stripe_state.paint_over_sent {
1633 quality_or_crf = paint_q;
1634 }
1635 if converges
1636 && stripe_state.h264_burst_frames_remaining > 0
1637 && !is_dirty
1638 && cleanup == Cleanup::None
1639 {
1640 let budget = crate::pipeline::frame_budget(video_bitrate, target_fps);
1641 let bytes = (stripe_state.rc_bytes > 0).then_some(stripe_state.rc_bytes);
1642 stripe_state.idle_frames = if bytes.is_some_and(|b| b <= empty_bytes) {
1643 stripe_state.idle_frames.saturating_add(1)
1644 } else {
1645 0
1646 };
1647 if crate::pipeline::convergence(quality.last, bytes, video_po_crf.max(0) as u32, budget)
1648 == crate::pipeline::Convergence::Converged
1649 || stripe_state.idle_frames as f64 >= crate::pipeline::EMPTY_S * target_fps
1650 {
1651 stripe_state.h264_burst_frames_remaining = 0;
1652 }
1653 }
1654 if is_dirty {
1655 stripe_state.h264_burst_frames_remaining = 0;
1656 } else if video
1657 && converges
1658 && (cleanup != Cleanup::None || (force_idr && video_burst > 0 && armed))
1659 {
1660 stripe_state.h264_burst_frames_remaining = crate::pipeline::converge_frames(settings);
1661 stripe_state.burst_held = false;
1662 stripe_state.idle_frames = 0;
1663 } else if video && (force_idr || cleanup != Cleanup::None) && video_burst > 0 {
1664 stripe_state.h264_burst_frames_remaining = video_burst;
1665 stripe_state.burst_held =
1666 holds && (cleanup != Cleanup::None || (armed && !settings.video_cbr_mode));
1667 } else if video && stripe_state.h264_burst_frames_remaining > 0 {
1668 stripe_state.h264_burst_frames_remaining -= 1;
1669 send_this_stripe = true;
1670 if stripe_state.burst_held && armed {
1671 hold = Some(refresh_crf);
1672 }
1673 }
1674 if video && !video_cbr {
1680 if is_dirty || !armed {
1681 stripe_state.clean_quality = false;
1682 }
1683 if cleanup != Cleanup::None {
1684 stripe_state.clean_quality = true;
1685 }
1686 if stripe_state.clean_quality && hold.is_none() && !force_idr_all {
1687 hold = Some(video_po_crf);
1688 }
1689 }
1690 if video && video_streaming {
1691 send_this_stripe = true;
1692 }
1693
1694 if send_this_stripe {
1695 if !video {
1696 let pixel_format = if use_gpu {
1697 turbojpeg::PixelFormat::RGBA
1698 } else {
1699 turbojpeg::PixelFormat::BGRA
1700 };
1701 let img = turbojpeg::Image {
1702 pixels: stripe_bytes,
1703 width: width_usize,
1704 pitch: width_usize * 4,
1705 height: actual_height,
1706 format: pixel_format,
1707 };
1708 JPEG_COMPRESSOR.with(|cell| -> Option<EncodedStripe> {
1709 let mut slot = cell.borrow_mut();
1710 if slot.is_none() {
1711 *slot = Some(turbojpeg::Compressor::new().ok()?);
1712 }
1713 let compressor = slot.as_mut().unwrap();
1714 compressor.set_quality(quality_or_crf).ok()?;
1715 let jpeg = compressor.compress_to_vec(img).ok()?;
1716 let data = if omit_headers {
1717 jpeg
1718 } else {
1719 stripe_state.packet_buf.clear();
1720 push_jpeg_header(
1721 &mut stripe_state.packet_buf,
1722 frame_counter,
1723 y_start as u16,
1724 );
1725 stripe_state.packet_buf.extend_from_slice(&jpeg);
1726 std::mem::take(&mut stripe_state.packet_buf)
1727 };
1728 Some(EncodedStripe {
1729 data: Arc::new(data),
1730 codec: Codec::Jpeg,
1731 stripe_y_start: y_start as i32,
1732 stripe_height: actual_height as i32,
1733 frame_id: frame_counter as i32,
1734 timing: FrameTiming::default(),
1735 reference: Reference::Untracked,
1736 })
1737 })
1738 } else {
1739 cfg_if::cfg_if! {
1740 if #[cfg(feature = "gpl")] {
1741 let needs_reinit = if let Some(ref enc) = stripe_state.h264_encoder {
1742 enc.width != width_usize as i32
1743 || enc.height != actual_height as i32
1744 || enc.is_i444 != video_fullcolor
1745 || enc.bit_depth != bit_depth
1746 } else {
1747 true
1748 };
1749
1750 let x264_crf = if video_cbr { quality_or_crf } else { hold.unwrap_or(quality_or_crf) };
1753 if needs_reinit {
1754 stripe_state.h264_encoder = H264EncoderWrapper::with_depth(
1755 width_usize as i32,
1756 actual_height as i32,
1757 x264_crf,
1758 video_fullcolor,
1759 bit_depth,
1760 target_fps,
1761 h264_threads,
1762 video_cbr,
1763 video_bitrate,
1764 video_vbv,
1765 settings.video_min_qp,
1766 settings.video_max_qp,
1767 settings.video_reference_frames,
1768 );
1769 force_idr = true;
1770 } else if let Some(ref mut enc) = stripe_state.h264_encoder {
1771 enc.reconfigure_crf(x264_crf);
1772 let cleaning = converges && stripe_state.h264_burst_frames_remaining > 0;
1773 let vbv = if cleaning { cleanup_vbv } else { video_vbv };
1774 enc.reconfigure_rate(video_bitrate, vbv, target_fps);
1775 }
1776
1777 if let Some(ref mut enc) = stripe_state.h264_encoder {
1778 if let Some(q) = hold.filter(|_| video_cbr) {
1779 enc.hold_quantizer(q);
1780 }
1781 let y_size = width_usize * actual_height;
1782 let uv_size = if video_fullcolor { y_size } else { y_size / 4 };
1783 let y_stride = width_usize as i32;
1784 let uv_stride =
1785 (if video_fullcolor { width_usize } else { width_usize / 2 }) as i32;
1786 let wide = bit_depth > 8;
1787 let conversion_result = if wide {
1788 let [y16, u16, v16] = &mut stripe_state.yuv16;
1789 y16.resize(y_size, 0);
1790 u16.resize(uv_size, 0);
1791 v16.resize(uv_size, 0);
1792 convert_to_yuv10_mt(
1793 stripe_bytes,
1794 width_usize * 4,
1795 width_usize,
1796 actual_height,
1797 use_gpu,
1798 video_fullcolor,
1799 video_fullcolor,
1800 y16,
1801 u16,
1802 v16,
1803 (y_stride as usize, uv_stride as usize),
1804 csc_bands,
1805 );
1806 Ok(())
1807 } else {
1808 if stripe_state.y_buf.len() != y_size {
1809 stripe_state.y_buf.resize(y_size, 0);
1810 }
1811 if stripe_state.u_buf.len() != uv_size {
1812 stripe_state.u_buf.resize(uv_size, 0);
1813 }
1814 if stripe_state.v_buf.len() != uv_size {
1815 stripe_state.v_buf.resize(uv_size, 0);
1816 }
1817 convert_to_yuv_mt(
1818 stripe_bytes,
1819 (width_usize * 4) as u32,
1820 width_usize,
1821 actual_height,
1822 use_gpu,
1823 video_fullcolor,
1824 video_fullcolor,
1825 false,
1826 &mut stripe_state.y_buf,
1827 &mut stripe_state.u_buf,
1828 &mut stripe_state.v_buf,
1829 (y_stride as usize, uv_stride as usize),
1830 csc_bands,
1831 )
1832 };
1833
1834 if let Err(e) = conversion_result {
1835 eprintln!(
1836 "[software] YUV conversion failed for {}x{} stripe: {:?}; skipping",
1837 width_usize, actual_height, e
1838 );
1839 return None;
1840 }
1841
1842 let bytes = |plane: &[u16]| unsafe {
1843 std::slice::from_raw_parts(plane.as_ptr().cast::<u8>(), plane.len() * 2)
1844 };
1845 let (y, u, v, scale) = if wide {
1846 let [y16, u16, v16] = &stripe_state.yuv16;
1847 (bytes(y16), bytes(u16), bytes(v16), 2)
1848 } else {
1849 (
1850 stripe_state.y_buf.as_slice(),
1851 stripe_state.u_buf.as_slice(),
1852 stripe_state.v_buf.as_slice(),
1853 1,
1854 )
1855 };
1856 if enc.encode_with_headers(
1857 y,
1858 u,
1859 v,
1860 y_stride * scale,
1861 uv_stride * scale,
1862 uv_stride * scale,
1863 frame_counter,
1864 y_start as u16,
1865 force_idr,
1866 omit_headers,
1867 &mut stripe_state.packet_buf,
1868 ) {
1869 if hold.is_none() || !video_cbr {
1870 stripe_state.rc_bytes = stripe_state.packet_buf.len();
1871 }
1872 Some(EncodedStripe {
1873 data: Arc::new(std::mem::take(&mut stripe_state.packet_buf)),
1874 codec: Codec::H264,
1875 stripe_y_start: y_start as i32,
1876 stripe_height: actual_height as i32,
1877 frame_id: frame_counter as i32,
1878 timing: FrameTiming::default(),
1879 reference: enc.last_reference(),
1880 })
1881 } else {
1882 None
1883 }
1884 } else {
1885 None
1886 }
1887 } else {
1888 use crate::encoders::oh264::Openh264Encoder;
1889 let needs_reinit = stripe_state.h264_encoder.as_ref().is_none_or(|enc| {
1890 enc.width() != width_usize || enc.height() != actual_height
1891 });
1892 if needs_reinit {
1893 stripe_state.h264_encoder = Openh264Encoder::new_stripe(
1894 settings,
1895 width_usize,
1896 actual_height,
1897 quality_or_crf,
1898 video_bitrate,
1899 n_processing_stripes == 1,
1900 );
1901 if stripe_state.h264_encoder.is_none() {
1902 static INIT_FAILED_LOGGED: std::sync::atomic::AtomicBool =
1905 std::sync::atomic::AtomicBool::new(false);
1906 if !INIT_FAILED_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
1907 eprintln!(
1908 "[software] OpenH264 init failed for a {}x{} stripe; no software H.264 for it",
1909 width_usize, actual_height
1910 );
1911 }
1912 }
1913 force_idr = true;
1914 } else if let Some(ref mut enc) = stripe_state.h264_encoder {
1915 if let Some(q) = hold {
1920 if enc.update_qp(q.max(0) as u32) {
1921 stripe_state.change_mass = 0.0;
1922 }
1923 } else if is_dirty {
1924 enc.update_qp(quality_or_crf.max(0) as u32);
1925 }
1926 enc.reconfigure_rate(video_bitrate, target_fps);
1927 }
1928
1929 let enc = stripe_state.h264_encoder.as_mut()?;
1930 match enc.encode_stripe_argb(
1931 stripe_bytes,
1932 width_usize * 4,
1933 frame_counter as u64,
1934 y_start as u16,
1935 force_idr,
1936 use_gpu,
1937 ) {
1938 Ok(data) if !data.is_empty() => Some(EncodedStripe {
1939 data: Arc::new(data),
1940 codec: Codec::H264,
1941 stripe_y_start: y_start as i32,
1942 stripe_height: actual_height as i32,
1943 frame_id: frame_counter as i32,
1944 timing: FrameTiming::default(),
1945 reference: enc.last_reference(),
1946 }),
1947 Ok(_) => None,
1948 Err(e) => {
1949 eprintln!("[software] OpenH264 encode failed for stripe at y={y_start}: {e}");
1950 None
1951 }
1952 }
1953 }
1954 }
1955 }
1956 } else {
1957 None
1958 }
1959 };
1960 let encoded: Vec<EncodedStripe> = if n_processing_stripes <= 1 {
1961 stripes
1962 .iter_mut()
1963 .enumerate()
1964 .filter_map(&stripe_body)
1965 .collect()
1966 } else {
1967 stripes
1968 .par_iter_mut()
1969 .enumerate()
1970 .filter_map(&stripe_body)
1971 .collect()
1972 };
1973 let sent = encoded.len() as f32;
1976 let alpha = if sent > *carrying {
1977 CARRY_RISE
1978 } else {
1979 CARRY_FALL
1980 };
1981 *carrying += (sent - *carrying) * alpha;
1982 encoded
1983}
1984
1985pub fn invalidate_reference(stripes: &mut [StripeState], frame_id: u16) -> bool {
1995 cfg_if::cfg_if! {
1996 if #[cfg(feature = "gpl")] {
1997 let mut forgotten = true;
2000 for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
2001 forgotten &= enc.invalidate_reference(frame_id);
2002 }
2003 forgotten
2004 } else {
2005 let mut forgotten = true;
2009 for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
2010 forgotten &= enc.invalidate_reference(frame_id);
2011 }
2012 forgotten
2013 }
2014 }
2015}
2016
2017pub fn acknowledge_reference(stripes: &mut [StripeState], frame_id: u16, held: bool) {
2020 #[cfg(not(feature = "gpl"))]
2021 for enc in stripes.iter_mut().filter_map(|s| s.h264_encoder.as_mut()) {
2022 enc.acknowledge_reference(frame_id, held);
2023 }
2024 #[cfg(feature = "gpl")]
2025 let _ = (stripes, frame_id, held);
2026}
2027
2028const MAX_STRIPES: usize = 8;
2039
2040pub fn stripe_count(height: i32, codec: Codec, fullframe: bool) -> usize {
2041 if !codec.stripes() || (codec.is_video() && fullframe) || height < MIN_STRIPE_HEIGHT {
2042 return 1;
2043 }
2044 let cores = std::thread::available_parallelism()
2045 .map(|n| n.get())
2046 .unwrap_or(1);
2047 cores
2048 .min(MAX_STRIPES)
2049 .min((height / MIN_STRIPE_HEIGHT) as usize)
2050 .max(1)
2051}
2052
2053#[cfg(feature = "gpl")]
2060const CLEANUP_VBV_FRAMES: f64 = 4.0;
2061
2062#[cfg(feature = "gpl")]
2065fn cleanup_vbv_kbit(settings: &RustCaptureSettings, bitrate_kbps: i32) -> i32 {
2066 (crate::encoders::vbv_bits(
2067 (bitrate_kbps as u32).saturating_mul(1000),
2068 settings.target_fps,
2069 0.0,
2070 CLEANUP_VBV_FRAMES,
2071 ) / 1000)
2072 .max(1) as i32
2073}
2074
2075fn stripe_rate_control(
2087 settings: &RustCaptureSettings,
2088 carrying: f32,
2089 n_stripes: usize,
2090) -> (i32, i32) {
2091 let divisor = (carrying.round().max(1.0) as usize).min(n_stripes.max(1)) as i32;
2092 let bitrate = (settings.video_bitrate_kbps / divisor).max(1);
2093 if !settings.video_cbr_mode && settings.video_vbv_multiplier <= 0.0 {
2094 return (bitrate, 0);
2095 }
2096 let vbv = (crate::encoders::vbv_bits(
2097 (bitrate as u32).saturating_mul(1000),
2098 settings.target_fps,
2099 settings.keyframe_interval_s,
2100 settings.video_vbv_multiplier,
2101 ) / 1000)
2102 .max(1) as i32;
2103 (bitrate, vbv)
2104}
2105
2106fn compute_stripe_geometries(height: usize, n: usize, codec: Codec) -> Vec<(usize, usize)> {
2117 let mut geoms = Vec::with_capacity(n);
2118 let mut current_y = 0;
2119 if !codec.is_video() {
2120 let base_h = height / n;
2121 let remainder = height - base_h * n;
2122 for i in 0..n {
2123 let s_h = base_h + if i < remainder { 1 } else { 0 };
2124 geoms.push((current_y, s_h));
2125 current_y += s_h;
2126 }
2127 } else {
2128 let base_h = (height / n) & !1;
2129 let remainder = height - base_h * n;
2130 let stripes_with_extra = remainder / 2;
2131 for i in 0..n {
2132 let s_h = base_h + if i < stripes_with_extra { 2 } else { 0 };
2133 geoms.push((current_y, s_h));
2134 current_y += s_h;
2135 }
2136 }
2137 geoms
2138}
2139
2140#[cfg(test)]
2141mod stripe_count_tests {
2142 use super::{Codec, MAX_STRIPES, MIN_STRIPE_HEIGHT, stripe_count};
2143
2144 #[test]
2148 fn a_tall_frame_is_not_cut_into_one_stripe_per_core() {
2149 for codec in [Codec::Jpeg, Codec::H264] {
2150 let n = stripe_count(2160, codec, false);
2151 assert!(
2152 n <= MAX_STRIPES,
2153 "{codec:?} cut a 4K frame into {n} stripes"
2154 );
2155 assert!(n >= 1, "{codec:?} cut a 4K frame into none");
2156 }
2157 }
2158
2159 #[test]
2162 fn a_short_frame_is_cut_by_its_height() {
2163 assert_eq!(stripe_count(MIN_STRIPE_HEIGHT - 1, Codec::Jpeg, false), 1);
2164 assert!(stripe_count(MIN_STRIPE_HEIGHT * 2, Codec::Jpeg, false) <= 2);
2165 }
2166
2167 #[test]
2170 fn a_whole_frame_is_one_stripe() {
2171 assert_eq!(stripe_count(2160, Codec::H264, true), 1);
2172 assert_eq!(stripe_count(2160, Codec::Av1, false), 1);
2173 }
2174}
2175
2176#[cfg(test)]
2177mod tests {
2178 #[test]
2184 fn cbr_budget_is_split_across_the_stripes_carrying_it() {
2185 use crate::RustCaptureSettings;
2186 for &kbps in &[500i32, 4000, 8000, 20000] {
2187 let settings = RustCaptureSettings {
2188 video_cbr_mode: true,
2189 video_bitrate_kbps: kbps,
2190 ..Default::default()
2191 };
2192 for &n in &[1usize, 2, 4, 12, 64] {
2193 let (all, _) = super::stripe_rate_control(&settings, n as f32, n);
2194 let total = all * n as i32;
2195 assert!(
2196 total <= kbps && kbps - total < n as i32,
2197 "{n} stripes at {all} kbps must sum to the configured {kbps}"
2198 );
2199 let (one, _) = super::stripe_rate_control(&settings, 1.0, n);
2200 assert_eq!(one, kbps, "a lone moving stripe carries the whole budget");
2201 let (over, _) = super::stripe_rate_control(&settings, n as f32 * 4.0, n);
2202 assert_eq!(
2203 over, all,
2204 "the divisor never exceeds the stripes that exist"
2205 );
2206 let (under, _) = super::stripe_rate_control(&settings, 0.0, n);
2207 assert_eq!(under, kbps, "and never falls below one");
2208 }
2209 let (vbv_one, whole) = super::stripe_rate_control(&settings, 1.0, 8);
2210 let (_, share) = super::stripe_rate_control(&settings, 8.0, 8);
2211 assert_eq!(vbv_one, kbps);
2212 assert!(
2213 (share * 8 - whole).abs() <= 9,
2214 "each stripe's buffer is its share of the whole-screen one: {share}x8 vs {whole}"
2215 );
2216 }
2217 }
2218
2219 #[test]
2222 fn crf_stripes_take_a_vbv_only_where_a_multiplier_asks() {
2223 use crate::RustCaptureSettings;
2224 let crf = |mult| RustCaptureSettings {
2225 video_bitrate_kbps: 8000,
2226 video_vbv_multiplier: mult,
2227 target_fps: 30.0,
2228 ..Default::default()
2229 };
2230 assert_eq!(super::stripe_rate_control(&crf(0.0), 1.0, 4), (8000, 0));
2231 assert_eq!(super::stripe_rate_control(&crf(12.0), 1.0, 4), (8000, 3200));
2232 assert_eq!(super::stripe_rate_control(&crf(12.0), 4.0, 4), (2000, 800));
2233 let cbr = RustCaptureSettings {
2234 video_cbr_mode: true,
2235 ..crf(0.0)
2236 };
2237 assert!(super::stripe_rate_control(&cbr, 1.0, 4).1 > 0);
2238 }
2239
2240 #[test]
2245 fn the_budget_divisor_follows_motion_and_is_smoothed() {
2246 use crate::RustCaptureSettings;
2247 let (w, h) = (64, 512);
2248 let settings = RustCaptureSettings {
2249 width: w,
2250 height: h,
2251 codec: Codec::Jpeg,
2252 jpeg_quality: 40,
2253 use_paint_over_quality: false,
2254 ..Default::default()
2255 };
2256 let full = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
2257 let stripes_n = super::stripe_count(h, settings.codec, settings.video_fullframe);
2258 if stripes_n < 2 {
2259 return;
2260 }
2261 let mut stripes = Vec::new();
2262 let mut carrying = 1.0f32;
2263 for frame in 0..40u16 {
2264 let shade = 40u8.wrapping_add(frame.wrapping_mul(7) as u8);
2265 let px = vec![shade; (w * h * 4) as usize];
2266 super::encode_cpu(
2267 &mut stripes,
2268 &mut carrying,
2269 &px,
2270 w,
2271 h,
2272 &full,
2273 &settings,
2274 frame,
2275 false,
2276 false,
2277 false,
2278 );
2279 }
2280 assert!(
2281 carrying > stripes_n as f32 * 0.75,
2282 "full-screen motion must move the divisor toward the {stripes_n} stripes it uses, \
2283 not leave it at {carrying}"
2284 );
2285 let moved = carrying;
2286 let band = [smithay::utils::Rectangle::new(
2290 (0, 0).into(),
2291 (w, 64).into(),
2292 )];
2293 for frame in 40..120u16 {
2294 let shade = 40u8.wrapping_add(frame.wrapping_mul(11) as u8);
2295 let mut px = vec![200u8; (w * h * 4) as usize];
2296 for byte in px.iter_mut().take((w * 64 * 4) as usize) {
2297 *byte = shade;
2298 }
2299 super::encode_cpu(
2300 &mut stripes,
2301 &mut carrying,
2302 &px,
2303 w,
2304 h,
2305 &band,
2306 &settings,
2307 frame,
2308 false,
2309 false,
2310 false,
2311 );
2312 }
2313 assert!(
2314 carrying < moved * 0.5,
2315 "motion in one stripe must bring the divisor back down: {carrying} vs {moved}"
2316 );
2317 }
2318 use super::{Codec, StripeState, compute_stripe_geometries};
2319
2320 #[cfg(not(feature = "gpl"))]
2327 #[test]
2328 fn openh264_stripes_are_independent_streams() {
2329 use crate::RustCaptureSettings;
2330 use openh264::decoder::Decoder;
2331 use openh264::formats::YUVSource;
2332 let (w, h) = (128, 512);
2333 let settings = RustCaptureSettings {
2334 width: w,
2335 height: h,
2336 codec: Codec::H264,
2337 video_crf: 25,
2338 use_paint_over_quality: false,
2339 video_streaming_mode: false,
2340 ..Default::default()
2341 };
2342 let n = super::stripe_count(h, settings.codec, settings.video_fullframe);
2343 if n < 2 {
2344 return;
2345 }
2346 let mut stripes = Vec::new();
2347 let mut carrying = 1.0f32;
2348 let px: Vec<u8> = (0..(w * h * 4) as usize).map(|i| (i % 251) as u8).collect();
2349 let first = super::encode_cpu(
2350 &mut stripes,
2351 &mut carrying,
2352 &px,
2353 w,
2354 h,
2355 &[],
2356 &settings,
2357 0,
2358 false,
2359 true,
2360 false,
2361 );
2362 assert_eq!(first.len(), n, "every stripe is sent on the first frame");
2363 for (stripe, (y, sh)) in
2364 first
2365 .iter()
2366 .zip(compute_stripe_geometries(h as usize, n, Codec::H264))
2367 {
2368 let d = &stripe.data;
2369 assert_eq!(d[0], 0x04, "H.264 stripe tag");
2370 assert_eq!(d[1], 0x11, "first frame of a stripe is an H.264 key frame");
2371 assert_eq!(u16::from_be_bytes([d[2], d[3]]), 0, "frame number");
2372 assert_eq!(u16::from_be_bytes([d[4], d[5]]) as usize, y, "y-start");
2373 assert_eq!(u16::from_be_bytes([d[6], d[7]]) as i32, w, "width");
2374 assert_eq!(
2375 u16::from_be_bytes([d[8], d[9]]) as usize,
2376 sh,
2377 "stripe height"
2378 );
2379 assert_eq!(
2380 (
2381 stripe.stripe_y_start as usize,
2382 stripe.stripe_height as usize
2383 ),
2384 (y, sh)
2385 );
2386 let mut dec = Decoder::new().expect("decoder");
2387 let img = dec
2388 .decode(&d[crate::encoders::codec::VIDEO_HEADER_LEN..])
2389 .expect("decode")
2390 .expect("an IDR decodes on its own");
2391 assert_eq!(
2392 img.dimensions(),
2393 (w as usize, sh),
2394 "each stripe is its own stream"
2395 );
2396 }
2397 let quiet = super::encode_cpu(
2398 &mut stripes,
2399 &mut carrying,
2400 &px,
2401 w,
2402 h,
2403 &[],
2404 &settings,
2405 1,
2406 false,
2407 true,
2408 false,
2409 );
2410 assert!(quiet.is_empty(), "a static frame sends nothing");
2411 let mut moved = px.clone();
2412 for b in moved.iter_mut().take((w * 8 * 4) as usize) {
2413 *b = b.wrapping_add(97);
2414 }
2415 let top = super::encode_cpu(
2416 &mut stripes,
2417 &mut carrying,
2418 &moved,
2419 w,
2420 h,
2421 &[],
2422 &settings,
2423 2,
2424 false,
2425 true,
2426 false,
2427 );
2428 assert_eq!(
2429 top.len(),
2430 1,
2431 "motion in the top rows re-sends the top stripe alone"
2432 );
2433 assert_eq!(top[0].stripe_y_start, 0);
2434 assert_eq!(
2435 top[0].data[1], 0x10,
2436 "an unforced follow-up is an H.264 delta frame"
2437 );
2438 assert_eq!(
2439 u16::from_be_bytes([top[0].data[2], top[0].data[3]]),
2440 2,
2441 "frame number"
2442 );
2443 }
2444
2445 #[test]
2448 fn hash_dirty_reads_as_content_dirty() {
2449 let frames: Vec<Vec<u8>> = [1u8, 1, 2, 1, 1, 1, 1, 1, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7]
2450 .iter()
2451 .map(|&v| vec![v; 256])
2452 .collect();
2453 let (mut here, mut there) = (StripeState::default(), StripeState::default());
2454 for frame in &frames {
2455 assert_eq!(
2456 here.content_dirty(frame, 2, 3),
2457 there.hash_dirty(super::fast_hash(frame), 2, 3)
2458 );
2459 assert_eq!(here.in_damage_block, there.in_damage_block);
2460 }
2461 }
2462
2463 #[test]
2467 fn content_dirty_detects_change_and_damage_block() {
2468 let mut st = StripeState::default();
2469 let a = vec![1u8; 256];
2470 let b = vec![2u8; 256];
2471 assert!(st.content_dirty(&a, 2, 3));
2472 assert!(!st.content_dirty(&a, 2, 3));
2473 assert!(st.content_dirty(&b, 2, 3));
2474 assert!(st.content_dirty(&a, 2, 3));
2475 assert!(st.in_damage_block);
2476 assert!(st.content_dirty(&a, 2, 3));
2477 assert!(st.content_dirty(&a, 2, 3));
2478 assert!(st.content_dirty(&a, 2, 3));
2479 assert!(!st.in_damage_block);
2480 assert!(!st.content_dirty(&a, 2, 3));
2481
2482 let frames: Vec<Vec<u8>> = (0..12u8).map(|i| vec![i; 256]).collect();
2484 let mut st = StripeState::default();
2485 for f in &frames {
2486 assert!(st.content_dirty(f, 2, 3));
2487 }
2488 assert!(
2489 st.in_damage_block,
2490 "a region moving every frame stays in its block"
2491 );
2492 let still = frames.last().unwrap();
2493 let dirty: Vec<bool> = (0..6).map(|_| st.content_dirty(still, 2, 3)).collect();
2494 assert_eq!(
2495 dirty,
2496 [true, true, false, false, false, false],
2497 "clean once the block that saw it stop ends"
2498 );
2499 }
2500
2501 #[test]
2506 fn clean_frames_countdown_fire_paintover_then_go_quiescent() {
2507 use crate::RustCaptureSettings;
2508 let (w, h) = (64, 128);
2509 let pixels = vec![128u8; (w * h * 4) as usize];
2510 let settings = RustCaptureSettings {
2511 width: w,
2512 height: h,
2513 codec: Codec::Jpeg,
2514 jpeg_quality: 60,
2515 paint_over_jpeg_quality: 90,
2516 use_paint_over_quality: true,
2517 paint_over_trigger_frames: 5,
2518 ..Default::default()
2519 };
2520 let mut stripes = Vec::new();
2521 let mut carrying = 1.0f32;
2522 let full = [smithay::utils::Rectangle::new((0, 0).into(), (w, h).into())];
2523 let dirty = super::encode_cpu(
2524 &mut stripes,
2525 &mut carrying,
2526 &pixels,
2527 w,
2528 h,
2529 &full,
2530 &settings,
2531 0,
2532 false,
2533 false,
2534 false,
2535 );
2536 assert!(!dirty.is_empty(), "damaged frame must encode");
2537
2538 let mut painted = Vec::new();
2539 for frame in 1..=20u16 {
2540 let out = super::encode_cpu(
2541 &mut stripes,
2542 &mut carrying,
2543 &pixels,
2544 w,
2545 h,
2546 &[],
2547 &settings,
2548 frame,
2549 false,
2550 false,
2551 false,
2552 );
2553 painted.extend(out.iter().map(|s| (frame, s.stripe_y_start)));
2554 }
2555 let per_frame = stripes.len().div_ceil(super::CLEANUP_STAGGER_FRAMES).max(1);
2556 assert_eq!(
2557 painted.len(),
2558 stripes.len(),
2559 "each stripe is painted over exactly once: {painted:?}"
2560 );
2561 assert_eq!(
2562 painted[0].0, settings.paint_over_trigger_frames as u16,
2563 "starting at the trigger"
2564 );
2565 for (n, (frame, _)) in painted.iter().enumerate() {
2566 assert_eq!(
2567 *frame as usize,
2568 settings.paint_over_trigger_frames as usize + n / per_frame,
2569 "{per_frame} a frame"
2570 );
2571 }
2572 assert!(
2573 stripes.iter().all(|st| st.paint_over_sent),
2574 "all stripes latched after the repaint"
2575 );
2576 }
2577
2578 #[test]
2583 fn hash_scan_idles_after_paintover_and_wakes_on_change() {
2584 use crate::RustCaptureSettings;
2585 let (w, h) = (64, 128);
2586 let static_px = vec![128u8; (w * h * 4) as usize];
2587 let changed_px = vec![200u8; (w * h * 4) as usize];
2588 let settings = RustCaptureSettings {
2589 width: w,
2590 height: h,
2591 codec: Codec::Jpeg,
2592 jpeg_quality: 60,
2593 paint_over_jpeg_quality: 90,
2594 use_paint_over_quality: true,
2595 paint_over_trigger_frames: 5,
2596 damage_block_threshold: 10,
2597 damage_block_duration: 10,
2598 ..Default::default()
2599 };
2600 let mut stripes = Vec::new();
2601 let mut carrying = 1.0f32;
2602 let first = super::encode_cpu(
2603 &mut stripes,
2604 &mut carrying,
2605 &static_px,
2606 w,
2607 h,
2608 &[],
2609 &settings,
2610 0,
2611 false,
2612 true,
2613 false,
2614 );
2615 assert!(
2616 !first.is_empty(),
2617 "first frame hashes as changed and encodes"
2618 );
2619
2620 let mut painted = Vec::new();
2621 for frame in 1..=20u16 {
2622 let out = super::encode_cpu(
2623 &mut stripes,
2624 &mut carrying,
2625 &static_px,
2626 w,
2627 h,
2628 &[],
2629 &settings,
2630 frame,
2631 false,
2632 true,
2633 false,
2634 );
2635 painted.extend(out.iter().map(|s| (frame, s.stripe_y_start)));
2636 }
2637 assert_eq!(
2638 painted.len(),
2639 stripes.len(),
2640 "each stripe is painted over exactly once while static: {painted:?}"
2641 );
2642 assert_eq!(painted[0].0, settings.paint_over_trigger_frames as u16);
2643
2644 let woke = super::encode_cpu(
2645 &mut stripes,
2646 &mut carrying,
2647 &changed_px,
2648 w,
2649 h,
2650 &[],
2651 &settings,
2652 21,
2653 false,
2654 true,
2655 false,
2656 );
2657 assert!(!woke.is_empty(), "content change after idle must encode");
2658 }
2659
2660 #[test]
2664 fn a_blinking_caret_starves_no_stripe_of_its_cleanup() {
2665 use crate::RustCaptureSettings;
2666 let (w, h) = (64i32, 256i32);
2667 let settings = RustCaptureSettings {
2668 width: w,
2669 height: h,
2670 codec: Codec::Jpeg,
2671 jpeg_quality: 40,
2672 paint_over_jpeg_quality: 90,
2673 use_paint_over_quality: true,
2674 paint_over_trigger_frames: 5,
2675 damage_block_threshold: 10,
2676 damage_block_duration: 10,
2677 ..Default::default()
2678 };
2679 let mut stripes = Vec::new();
2680 let mut carrying = 1.0f32;
2681 let frame = |caret: bool| {
2682 let mut px = vec![128u8; (w * h * 4) as usize];
2683 if caret {
2684 px[(w * 4 * 3) as usize..(w * 4 * 3) as usize + 8].fill(0);
2685 }
2686 px
2687 };
2688 let dc_quant = |data: &[u8]| {
2690 let at = data
2691 .windows(2)
2692 .position(|m| m == [0xFF, 0xDB])
2693 .expect("a quantization table");
2694 data[at + 5]
2695 };
2696 let mut cleaned = std::collections::HashMap::new();
2697 for n in 0..60u16 {
2698 let out = super::encode_cpu(
2699 &mut stripes,
2700 &mut carrying,
2701 &frame((n / 4) % 2 == 0),
2702 w,
2703 h,
2704 &[],
2705 &settings,
2706 n,
2707 false,
2708 true,
2709 false,
2710 );
2711 for s in &out {
2712 if dc_quant(&s.data) < 8 {
2713 cleaned.entry(s.stripe_y_start).or_insert(n);
2714 }
2715 }
2716 }
2717 let rows = stripes.len();
2718 assert!(rows > 1, "the frame is striped");
2719 let caret_stripe = 0;
2720 let others: Vec<u16> = cleaned
2721 .iter()
2722 .filter(|(y, _)| **y != caret_stripe)
2723 .map(|(_, n)| *n)
2724 .collect();
2725 assert_eq!(
2726 others.len(),
2727 rows - 1,
2728 "every still stripe is cleaned up: {cleaned:?}"
2729 );
2730 assert!(
2731 others
2732 .iter()
2733 .all(|&n| n <= 5 + super::CLEANUP_STAGGER_FRAMES as u16),
2734 "at the trigger: {cleaned:?}"
2735 );
2736 let at = *cleaned
2737 .get(&caret_stripe)
2738 .expect("the caret's stripe is cleaned up too");
2739 assert!(
2740 (19..=23).contains(&at),
2741 "once the low-motion window passes: {at}"
2742 );
2743 }
2744
2745 fn covered(geoms: &[(usize, usize)]) -> usize {
2747 geoms.iter().map(|&(_, h)| h).sum()
2748 }
2749
2750 fn assert_contiguous(geoms: &[(usize, usize)]) {
2753 let mut y = 0;
2754 for &(sy, sh) in geoms {
2755 assert_eq!(sy, y, "stripes must be contiguous");
2756 y += sh;
2757 }
2758 }
2759
2760 #[test]
2763 fn jpeg_covers_every_row_including_odd() {
2764 for &h in &[1usize, 63, 720, 721, 1079, 1080, 1081] {
2765 for &n in &[1usize, 2, 3, 8, 16] {
2766 let g = compute_stripe_geometries(h, n, Codec::Jpeg);
2767 assert_eq!(g.len(), n);
2768 assert_eq!(
2769 covered(&g),
2770 h,
2771 "JPEG must cover full height h={} n={}",
2772 h,
2773 n
2774 );
2775 assert_contiguous(&g);
2776 }
2777 }
2778 }
2779
2780 #[test]
2783 fn h264_stripes_even_and_within_bounds() {
2784 for &h in &[64usize, 720, 721, 1080, 1081] {
2785 for &n in &[1usize, 2, 8] {
2786 let g = compute_stripe_geometries(h, n, Codec::H264);
2787 assert_eq!(g.len(), n);
2788 for &(_, sh) in &g {
2789 assert_eq!(
2790 sh % 2,
2791 0,
2792 "H.264 stripe heights must be even h={} n={}",
2793 h,
2794 n
2795 );
2796 }
2797 assert_contiguous(&g);
2798 assert!(covered(&g) <= h);
2799 assert!(
2800 h - covered(&g) <= 1,
2801 "at most one trailing odd row uncovered"
2802 );
2803 }
2804 }
2805 }
2806
2807 #[test]
2810 fn the_host_convert_sites_chroma_at_the_block_center() {
2811 use super::convert_to_yuv_mt;
2812 let (w, h) = (64usize, 64usize);
2813 let bgra = crate::encoders::chroma_siting::bgra(w, h);
2814 for bands in [1usize, 4, 7] {
2815 let (mut yp, mut up, mut vp) =
2816 (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
2817 convert_to_yuv_mt(
2818 &bgra,
2819 (w * 4) as u32,
2820 w,
2821 h,
2822 false,
2823 false,
2824 false,
2825 false,
2826 &mut yp,
2827 &mut up,
2828 &mut vp,
2829 (w, w / 2),
2830 bands,
2831 )
2832 .expect("convert");
2833 let worst = up
2834 .iter()
2835 .zip(&vp)
2836 .map(|(&u, &v)| (f64::from(u) - 128.0).hypot(f64::from(v) - 128.0))
2837 .fold(0.0f64, f64::max);
2838 assert!(
2839 worst <= 2.0,
2840 "{bands} bands: chroma sits {worst:.1} off neutral"
2841 );
2842 }
2843 }
2844
2845 #[test]
2848 fn the_jpeg_path_sites_chroma_at_the_block_center() {
2849 use super::Codec;
2850 use crate::webcam::decode::new_decoder;
2851 let (w, h) = (64usize, 64usize);
2852 let bgra = crate::encoders::chroma_siting::bgra(w, h);
2853 let mut comp = turbojpeg::Compressor::new().expect("turbojpeg compressor");
2854 comp.set_quality(90).expect("quality");
2855 let img = turbojpeg::Image {
2856 pixels: &bgra[..],
2857 width: w,
2858 pitch: w * 4,
2859 height: h,
2860 format: turbojpeg::PixelFormat::BGRA,
2861 };
2862 let jpeg = comp.compress_to_vec(img).expect("compress");
2863 let mut dec = new_decoder(Codec::Jpeg).expect("jpeg decoder");
2864 assert!(
2865 dec.decode(&jpeg).expect("decode"),
2866 "the stripe decoded nothing"
2867 );
2868 let worst = crate::encoders::chroma_siting::worst(&dec.frame().expect("frame"));
2869 assert!(worst <= 4.0, "JPEG chroma sits {worst:.1} off neutral");
2870 }
2871
2872 #[test]
2876 fn ten_bit_conversion_matches_bt709() {
2877 use super::convert_to_yuv10_mt;
2878 let (w, h) = (34usize, 18usize);
2879 let mut bgra = vec![0u8; w * h * 4];
2880 let mut seed = 0x2545_f491u32;
2881 for px in bgra.as_chunks_mut::<4>().0 {
2882 for c in px.iter_mut().take(3) {
2883 seed ^= seed << 13;
2884 seed ^= seed >> 17;
2885 seed ^= seed << 5;
2886 *c = (seed >> 11) as u8;
2887 }
2888 px[3] = 255;
2889 }
2890 let reference = |r: f64, g: f64, b: f64, full: bool| {
2891 let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
2892 let (cb, cr) = ((b - y) / 1.8556, (r - y) / 1.5748);
2893 if full {
2894 (
2895 y * 1023.0 / 255.0,
2896 512.0 + cb * 1023.0 / 255.0,
2897 512.0 + cr * 1023.0 / 255.0,
2898 )
2899 } else {
2900 (
2901 64.0 + y * 876.0 / 255.0,
2902 512.0 + cb * 896.0 / 255.0,
2903 512.0 + cr * 896.0 / 255.0,
2904 )
2905 }
2906 };
2907 let rgb = |x: usize, y: usize| {
2908 let p = &bgra[(y * w + x) * 4..];
2909 (p[2] as f64, p[1] as f64, p[0] as f64)
2910 };
2911 for full in [false, true] {
2912 for i444 in [false, true] {
2913 let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
2914 let convert = |bands: usize| {
2915 let (mut y, mut u, mut v) =
2916 (vec![0u16; w * h], vec![0u16; cw * ch], vec![0u16; cw * ch]);
2917 convert_to_yuv10_mt(
2918 &bgra,
2919 w * 4,
2920 w,
2921 h,
2922 false,
2923 i444,
2924 full,
2925 &mut y,
2926 &mut u,
2927 &mut v,
2928 (w, cw),
2929 bands,
2930 );
2931 (y, u, v)
2932 };
2933 let (y, u, v) = convert(1);
2934 assert_eq!(
2935 (y.clone(), u.clone(), v.clone()),
2936 convert(4),
2937 "bands change nothing"
2938 );
2939 for row in 0..h {
2940 for col in 0..w {
2941 let (r, g, b) = rgb(col, row);
2942 let want = reference(r, g, b, full).0;
2943 assert!((y[row * w + col] as f64 - want).abs() <= 1.0, "luma");
2944 }
2945 }
2946 for row in 0..ch {
2947 for col in 0..cw {
2948 let (r, g, b) = if i444 {
2949 rgb(col, row)
2950 } else {
2951 let mut sum = (0.0, 0.0, 0.0);
2952 for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
2953 let p = rgb(2 * col + dx, 2 * row + dy);
2954 sum = (sum.0 + p.0 / 4.0, sum.1 + p.1 / 4.0, sum.2 + p.2 / 4.0);
2955 }
2956 sum
2957 };
2958 let (_, cb, cr) = reference(r, g, b, full);
2959 assert!((u[row * cw + col] as f64 - cb).abs() <= 1.0, "cb");
2960 assert!((v[row * cw + col] as f64 - cr).abs() <= 1.0, "cr");
2961 }
2962 }
2963 }
2964 }
2965 }
2966
2967 #[cfg(feature = "gpl")]
2971 #[test]
2972 fn x264_quantizers_keep_their_scale_at_ten_bits() {
2973 use super::H264EncoderWrapper;
2974 if !H264EncoderWrapper::ten_bit() {
2975 println!("this libx264 was built for 8 bits alone; nothing to check");
2976 return;
2977 }
2978 let (w, h) = (640usize, 360usize);
2979 let mut seed = 7u32;
2980 let mut noise = |n: usize, amplitude: u32, base: u32| -> Vec<u32> {
2981 (0..n)
2982 .map(|_| {
2983 seed ^= seed << 13;
2984 seed ^= seed >> 17;
2985 seed ^= seed << 5;
2986 base + seed % amplitude
2987 })
2988 .collect()
2989 };
2990 let (luma, chroma) = (noise(w * h, 60, 90), noise(w * h / 4, 20, 118));
2991 let mut seen = Vec::new();
2992 for depth in [8u32, 10] {
2993 let plane = |samples: &[u32]| -> Vec<u8> {
2994 if depth == 10 {
2995 samples
2996 .iter()
2997 .flat_map(|s| ((s * 4) as u16).to_le_bytes())
2998 .collect()
2999 } else {
3000 samples.iter().map(|s| *s as u8).collect()
3001 }
3002 };
3003 let (y, c) = (plane(&luma), plane(&chroma));
3004 let bytes = (depth as usize).div_ceil(8);
3005 let mut enc = H264EncoderWrapper::with_depth(
3006 w as i32, h as i32, 25, false, depth, 30.0, 1, false, 0, 0, 0, 0, 0,
3007 )
3008 .expect("x264 init");
3009 let encode = |enc: &mut H264EncoderWrapper, id: u16| {
3010 let mut out = Vec::new();
3011 assert!(enc.encode_with_headers(
3012 &y,
3013 &c,
3014 &c,
3015 (w * bytes) as i32,
3016 (w / 2 * bytes) as i32,
3017 (w / 2 * bytes) as i32,
3018 id,
3019 0,
3020 true,
3021 true,
3022 &mut out,
3023 ));
3024 out.len()
3025 };
3026 encode(&mut enc, 0);
3027 let rate_factor_qp = enc.last_qp().expect("a quantizer");
3028 enc.hold_quantizer(45);
3029 seen.push((rate_factor_qp, encode(&mut enc, 1)));
3030 }
3031 let ((qp8, held8), (qp10, held10)) = (seen[0], seen[1]);
3032 assert!(
3033 qp8.abs_diff(qp10) <= 2,
3034 "rate factor quantizers {qp8} and {qp10}"
3035 );
3036 assert!(
3037 held10 < 2 * held8,
3038 "held key frames of {held8} and {held10} bytes"
3039 );
3040 }
3041
3042 #[cfg(feature = "gpl")]
3045 #[test]
3046 fn x264_codes_ten_bits() {
3047 use super::H264EncoderWrapper;
3048 if !H264EncoderWrapper::ten_bit() {
3049 println!("this libx264 was built for 8 bits alone; nothing to check");
3050 return;
3051 }
3052 let (w, h) = (64usize, 64usize);
3053 for (i444, profile) in [(false, 110u8), (true, 244u8)] {
3054 let mut enc = H264EncoderWrapper::with_depth(
3055 w as i32, h as i32, 25, i444, 10, 30.0, 1, false, 0, 0, 0, 0, 0,
3056 )
3057 .expect("x264 init");
3058 assert_eq!(enc.bit_depth, 10);
3059 let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
3060 let planes = [
3061 vec![400u16; w * h],
3062 vec![512u16; cw * ch],
3063 vec![600u16; cw * ch],
3064 ];
3065 let bytes = |p: &[u16]| unsafe {
3066 std::slice::from_raw_parts(p.as_ptr().cast::<u8>(), p.len() * 2).to_vec()
3067 };
3068 let (y, u, v) = (bytes(&planes[0]), bytes(&planes[1]), bytes(&planes[2]));
3069 let mut out = Vec::new();
3070 assert!(enc.encode_with_headers(
3071 &y,
3072 &u,
3073 &v,
3074 (w * 2) as i32,
3075 (cw * 2) as i32,
3076 (cw * 2) as i32,
3077 0,
3078 0,
3079 true,
3080 true,
3081 &mut out,
3082 ));
3083 let sps = crate::encoders::codec::annexb_nals(&out)
3084 .find(|n| n[0] & 0x1f == 7)
3085 .expect("a sequence parameter set");
3086 assert_eq!(sps[1], profile, "the profile declared");
3087 }
3088 }
3089}
3090
3091#[cfg(test)]
3092mod qp_bound_sweep {
3093 #[cfg(feature = "gpl")]
3099 use super::H264EncoderWrapper;
3100 use crate::RustCaptureSettings;
3101 use crate::encoders::Codec;
3102 use crate::encoders::oh264::Openh264Encoder;
3103 use openh264::decoder::Decoder;
3104 use openh264::formats::YUVSource;
3105
3106 const W: usize = 1280;
3107 const H: usize = 720;
3108 const FRAMES: usize = 60;
3109
3110 fn text_luma(frame: usize) -> Vec<u8> {
3114 let mut y = vec![18u8; W * H];
3115 let scroll = frame * 4;
3116 for row in 0..H {
3117 let srow = row + scroll;
3118 let cell_y = srow / 12;
3119 let in_glyph_y = srow % 12;
3120 if in_glyph_y >= 10 {
3121 continue;
3122 }
3123 for col in 0..W {
3124 let cell_x = col / 8;
3125 let in_glyph_x = col % 8;
3126 if in_glyph_x >= 7 {
3127 continue;
3128 }
3129 let mut s = (cell_x as u32)
3130 .wrapping_mul(2654435761)
3131 .wrapping_add((cell_y as u32).wrapping_mul(40503))
3132 .wrapping_add(1);
3133 s ^= s << 13;
3134 s ^= s >> 17;
3135 s ^= s << 5;
3136 if (s >> ((in_glyph_y * 3 + in_glyph_x) % 29)) & 1 == 1 {
3137 y[row * W + col] = 224;
3138 }
3139 }
3140 }
3141 y
3142 }
3143
3144 #[cfg(feature = "gpl")]
3147 fn encode_x264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
3148 let mut enc = H264EncoderWrapper::new(
3149 W as i32, H as i32, crf, false, 60.0, 4, cbr, kbps, 50, min_qp, max_qp,
3150 )
3151 .expect("x264 init");
3152 let u = vec![128u8; (W / 2) * (H / 2)];
3153 let v = vec![128u8; (W / 2) * (H / 2)];
3154 (0..FRAMES)
3155 .map(|i| {
3156 let y = text_luma(i);
3157 let mut out = Vec::new();
3158 enc.encode_with_headers(
3159 &y,
3160 &u,
3161 &v,
3162 W as i32,
3163 (W / 2) as i32,
3164 (W / 2) as i32,
3165 i as u16,
3166 0,
3167 i == 0,
3168 true,
3169 &mut out,
3170 );
3171 out
3172 })
3173 .collect()
3174 }
3175
3176 #[cfg(feature = "gpl")]
3179 #[test]
3180 fn x264_declares_the_conversion_matrix() {
3181 use crate::encoders::codec::annexb_nals;
3182 use crate::encoders::sps::read_color;
3183 use crate::webcam::decode::{ColorTags, Decoder as _, VideoDecoder};
3184 for (i444, want) in [
3185 (false, ColorTags::BT709_LIMITED),
3186 (true, ColorTags::BT709_FULL),
3187 ] {
3188 let (w, h) = (128usize, 96usize);
3189 let mut enc =
3190 H264EncoderWrapper::new(w as i32, h as i32, 25, i444, 30.0, 1, false, 0, 0, 0, 0)
3191 .expect("x264 init");
3192 let (cw, ch) = if i444 { (w, h) } else { (w / 2, h / 2) };
3193 let y = vec![90u8; w * h];
3194 let u = vec![128u8; cw * ch];
3195 let v = vec![160u8; cw * ch];
3196 let mut out = Vec::new();
3197 assert!(enc.encode_with_headers(
3198 &y, &u, &v, w as i32, cw as i32, cw as i32, 0, 0, true, true, &mut out
3199 ));
3200 let sps = annexb_nals(&out)
3201 .find(|n| n[0] & 0x1f == 7)
3202 .expect("an SPS");
3203 let declared = read_color(sps).map(|s| ColorTags {
3204 matrix: s.matrix,
3205 full_range: s.full_range,
3206 });
3207 assert_eq!(declared, Some(want), "i444={i444}");
3208 if !i444 {
3209 let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
3210 assert!(dec.decode(&out).expect("decode"));
3211 assert_eq!(dec.color_tags(), Some(want));
3212 }
3213 }
3214 }
3215
3216 #[cfg(feature = "gpl")]
3219 #[test]
3220 fn x264_takes_the_frame_rate_as_its_fraction() {
3221 use crate::encoders::frame_rate::FrameRate;
3222 use crate::encoders::sps::h264_timing;
3223 let (w, h) = (64usize, 64usize);
3224 let (y, uv) = (vec![90u8; w * h], vec![128u8; w * h / 4]);
3225 for (num, den) in [(60000u32, 1001u32), (120000, 1001), (144000, 1001), (60, 1)] {
3226 let mut enc = H264EncoderWrapper::new(
3227 w as i32,
3228 h as i32,
3229 25,
3230 false,
3231 num as f64 / den as f64,
3232 1,
3233 true,
3234 4000,
3235 100,
3236 0,
3237 0,
3238 )
3239 .expect("x264 init");
3240 let param = unsafe {
3241 let mut param: x264_sys::x264_param_t = std::mem::zeroed();
3242 x264_sys::x264_encoder_parameters(enc.encoder, &mut param);
3243 param
3244 };
3245 assert_eq!((param.i_fps_num, param.i_fps_den), (num, den));
3246 let mut out = Vec::new();
3247 assert!(enc.encode_with_headers(
3248 &y,
3249 &uv,
3250 &uv,
3251 w as i32,
3252 (w / 2) as i32,
3253 (w / 2) as i32,
3254 0,
3255 0,
3256 true,
3257 true,
3258 &mut out
3259 ));
3260 assert_eq!(
3261 h264_timing(&out),
3262 Some((den, 2 * num)),
3263 "{num}/{den}: the SPS declares the rate"
3264 );
3265 }
3266 let mut enc = H264EncoderWrapper::new(
3267 w as i32, h as i32, 25, false, 60.0, 1, true, 4000, 100, 0, 0,
3268 )
3269 .expect("x264 init");
3270 enc.reconfigure_rate(4000, 100, 60000.0 / 1001.0);
3271 assert_eq!(
3272 enc.current_fps,
3273 FrameRate {
3274 num: 60000,
3275 den: 1001
3276 },
3277 "60 fps moved to 59.94"
3278 );
3279 }
3280
3281 #[cfg(feature = "gpl")]
3285 #[test]
3286 fn x264_paints_the_chart_it_converts() {
3287 use super::convert_to_yuv_mt;
3288 use crate::encoders::chroma_siting::{BT709, chart_bgra, chart_error};
3289 use crate::webcam::decode::{Decoder as _, VideoDecoder};
3290 let (w, h) = (256usize, 128usize);
3291 let bgra = chart_bgra(w, h);
3292 let (mut y, mut u, mut v) = (vec![0u8; w * h], vec![0u8; w * h / 4], vec![0u8; w * h / 4]);
3293 convert_to_yuv_mt(
3294 &bgra,
3295 (w * 4) as u32,
3296 w,
3297 h,
3298 false,
3299 false,
3300 false,
3301 false,
3302 &mut y,
3303 &mut u,
3304 &mut v,
3305 (w, w / 2),
3306 4,
3307 )
3308 .expect("convert");
3309 let mut enc =
3310 H264EncoderWrapper::new(w as i32, h as i32, 20, false, 30.0, 1, false, 0, 0, 0, 0)
3311 .expect("x264 init");
3312 let mut out = Vec::new();
3313 assert!(enc.encode_with_headers(
3314 &y,
3315 &u,
3316 &v,
3317 w as i32,
3318 (w / 2) as i32,
3319 (w / 2) as i32,
3320 0,
3321 0,
3322 true,
3323 true,
3324 &mut out
3325 ));
3326 let mut dec = VideoDecoder::new(Codec::H264).expect("decoder");
3327 assert!(dec.decode(&out).expect("decode"));
3328 let worst = chart_error(&dec.frame().expect("frame"), BT709);
3329 println!("[chart] x264: worst |dRGB| {worst:.1}");
3330 assert!(
3331 worst <= 12.0,
3332 "the software H.264 path paints {worst:.1} off the chart"
3333 );
3334 }
3335
3336 #[cfg(feature = "gpl")]
3339 #[test]
3340 fn x264_bounds_reordering_at_zero() {
3341 use crate::encoders::sps::fixtures::assert_no_reorder;
3342 for (fullcolor, cbr) in [(false, false), (false, true), (true, false), (true, true)] {
3343 let mut enc = H264EncoderWrapper::new(
3344 W as i32, H as i32, 20, fullcolor, 60.0, 4, cbr, 8000, 0, 0, 0,
3345 )
3346 .expect("x264 init");
3347 let chroma = if fullcolor { W * H } else { W * H / 4 };
3348 let (u, v) = (vec![128u8; chroma], vec![128u8; chroma]);
3349 let stride = if fullcolor { W } else { W / 2 } as i32;
3350 let mut out = Vec::new();
3351 assert!(enc.encode_with_headers(
3352 &text_luma(0),
3353 &u,
3354 &v,
3355 W as i32,
3356 stride,
3357 stride,
3358 0,
3359 0,
3360 true,
3361 true,
3362 &mut out
3363 ));
3364 assert_no_reorder(&out, &format!("x264 fullcolor {fullcolor} cbr {cbr}"));
3365 }
3366 }
3367
3368 #[cfg(feature = "gpl")]
3371 #[test]
3372 fn x264_crf_bounds_a_key_frame_by_its_vbv() {
3373 const VBV_KBIT: i32 = 1500;
3374 let key = |vbv_kbit: i32| {
3375 let mut enc = H264EncoderWrapper::new(
3376 W as i32, H as i32, 12, false, 60.0, 4, false, 8000, vbv_kbit, 0, 0,
3377 )
3378 .expect("x264 init");
3379 let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3380 let mut out = Vec::new();
3381 assert!(enc.encode_with_headers(
3382 &text_luma(0),
3383 &u,
3384 &v,
3385 W as i32,
3386 (W / 2) as i32,
3387 (W / 2) as i32,
3388 0,
3389 0,
3390 true,
3391 true,
3392 &mut out
3393 ));
3394 out.len() * 8
3395 };
3396 let (uncapped, capped) = (key(0), key(VBV_KBIT));
3397 assert!(
3398 capped <= VBV_KBIT as usize * 1000,
3399 "a {capped}-bit key frame overflows the {VBV_KBIT} kbit buffer"
3400 );
3401 assert!(
3402 uncapped > 2 * capped,
3403 "the uncapped key frame ({uncapped} bits) is not the rate factor's own"
3404 );
3405 }
3406
3407 #[cfg(feature = "gpl")]
3410 #[test]
3411 fn x264_capped_crf_codes_no_quantizer_past_51() {
3412 let mut enc =
3413 H264EncoderWrapper::new(W as i32, H as i32, 23, false, 60.0, 4, false, 300, 10, 0, 0)
3414 .expect("x264 init");
3415 let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3416 let mut worst = 0;
3417 for i in 0..FRAMES {
3418 let mut out = Vec::new();
3419 enc.encode_with_headers(
3420 &text_luma(i),
3421 &u,
3422 &v,
3423 W as i32,
3424 (W / 2) as i32,
3425 (W / 2) as i32,
3426 i as u16,
3427 0,
3428 i == 0,
3429 true,
3430 &mut out,
3431 );
3432 worst = worst.max(enc.last_qp().unwrap_or(0));
3433 }
3434 assert!(worst <= 51, "a starved capped CRF coded quantizer {worst}");
3435 }
3436
3437 #[cfg(feature = "gpl")]
3439 #[test]
3440 fn x264_crf_takes_a_cap_set_or_lifted_live() {
3441 const VBV_KBIT: i32 = 1500;
3442 let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3443 let mut enc =
3444 H264EncoderWrapper::new(W as i32, H as i32, 12, false, 60.0, 4, false, 8000, 0, 0, 0)
3445 .expect("x264 init");
3446 let key = |enc: &mut H264EncoderWrapper| {
3447 let mut out = Vec::new();
3448 assert!(enc.encode_with_headers(
3449 &text_luma(0),
3450 &u,
3451 &v,
3452 W as i32,
3453 (W / 2) as i32,
3454 (W / 2) as i32,
3455 0,
3456 0,
3457 true,
3458 true,
3459 &mut out
3460 ));
3461 out.len() * 8
3462 };
3463 let uncapped = key(&mut enc);
3464 enc.reconfigure_rate(8000, VBV_KBIT, 60.0);
3465 let capped = key(&mut enc);
3466 enc.reconfigure_rate(8000, 0, 60.0);
3467 let lifted = key(&mut enc);
3468 assert!(
3469 capped <= VBV_KBIT as usize * 1000,
3470 "a {capped}-bit key frame after capping overflows the {VBV_KBIT} kbit buffer ({uncapped} bits before)"
3471 );
3472 assert!(
3473 lifted > 2 * capped,
3474 "the key frame after lifting the cap ({lifted} bits) is still capped"
3475 );
3476 }
3477
3478 #[cfg(feature = "gpl")]
3483 #[test]
3484 fn x264_predicts_past_a_lost_frame() {
3485 use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
3486 use crate::encoders::reference::{REFERENCE_FRAMES, Reference};
3487 use crate::encoders::sps::h264_max_num_ref_frames;
3488 use crate::webcam::decode::{Decoder as _, VideoDecoder};
3489 let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3490 let mut enc =
3491 H264EncoderWrapper::new(W as i32, H as i32, 20, false, 60.0, 4, false, 0, 0, 0, 0)
3492 .expect("x264 init");
3493 let encode = |enc: &mut H264EncoderWrapper, i: usize| {
3494 let y = text_luma(i);
3495 let mut out = Vec::new();
3496 assert!(enc.encode_with_headers(
3497 &y,
3498 &u,
3499 &v,
3500 W as i32,
3501 (W / 2) as i32,
3502 (W / 2) as i32,
3503 i as u16,
3504 0,
3505 i == 0,
3506 true,
3507 &mut out
3508 ));
3509 (out, enc.last_reference())
3510 };
3511 let mut frames: Vec<Vec<u8>> = Vec::new();
3512 for i in 0..8 {
3513 let (out, reference) = encode(&mut enc, i);
3514 assert_eq!(
3515 reference,
3516 if i == 0 {
3517 Reference::None
3518 } else {
3519 Reference::Frame(i as u16 - 1)
3520 }
3521 );
3522 frames.push(out);
3523 }
3524 assert_eq!(
3525 h264_max_num_ref_frames(&frames[0]),
3526 Some(REFERENCE_FRAMES),
3527 "the SPS declares the DPB"
3528 );
3529 assert!(enc.invalidate_reference(5));
3531 let (out, reference) = encode(&mut enc, 8);
3532 assert_eq!(reference, Reference::Frame(4));
3533 assert_eq!(h264_frame_type(&out), FRAME_DELTA);
3534 frames.push(out);
3535 let (out, reference) = encode(&mut enc, 9);
3536 assert_eq!(reference, Reference::Frame(8));
3537 frames.push(out);
3538 let (mut whole, mut lossy) = (
3539 VideoDecoder::new(Codec::H264).unwrap(),
3540 VideoDecoder::new(Codec::H264).unwrap(),
3541 );
3542 for (i, f) in frames.iter().enumerate() {
3543 assert!(whole.decode(f).expect("decode"), "frame {i}");
3544 if !(5..8).contains(&i) {
3545 assert!(lossy.decode(f).expect("decode without 5-7"), "frame {i}");
3546 }
3547 }
3548 let apart = luma_distance(&whole.frame().unwrap(), &lossy.frame().unwrap());
3549 assert!(
3550 apart < 0.5,
3551 "the decoder that lost frames 5-7 shows frame 9 {apart:.2} off the one that saw them"
3552 );
3553 let source = text_luma(9);
3554 let off = lossy
3555 .frame()
3556 .unwrap()
3557 .y
3558 .chunks(lossy.frame().unwrap().y_stride)
3559 .take(H)
3560 .zip(source.chunks(W))
3561 .flat_map(|(row, src)| {
3562 row[..W]
3563 .iter()
3564 .zip(src)
3565 .map(|(&a, &b)| (a as f64 - b as f64).abs())
3566 })
3567 .sum::<f64>()
3568 / (W * H) as f64;
3569 assert!(
3570 off < 6.0,
3571 "frame 9 decoded without frames 5-7 is {off:.2} off the picture painted"
3572 );
3573 for i in 10..20 {
3574 frames.push(encode(&mut enc, i).0);
3575 }
3576 assert!(enc.invalidate_reference(9));
3579 let (out, reference) = encode(&mut enc, 20);
3580 assert_eq!(reference, Reference::None);
3581 assert_eq!(h264_frame_type(&out), FRAME_KEY);
3582 }
3583
3584 #[cfg(feature = "gpl")]
3587 #[test]
3588 fn x264_keeps_the_reference_frames_it_is_given() {
3589 use crate::encoders::codec::{FRAME_KEY, h264_frame_type};
3590 use crate::encoders::sps::h264_max_num_ref_frames;
3591 let (u, v) = (vec![128u8; W * H / 4], vec![128u8; W * H / 4]);
3592 let mut enc = H264EncoderWrapper::with_depth(
3593 W as i32, H as i32, 20, false, 8, 60.0, 1, false, 0, 0, 0, 0, 1,
3594 )
3595 .expect("x264 init");
3596 let mut frames = Vec::new();
3597 for i in 0..4usize {
3598 let mut out = Vec::new();
3599 assert!(enc.encode_with_headers(
3600 &text_luma(i),
3601 &u,
3602 &v,
3603 W as i32,
3604 (W / 2) as i32,
3605 (W / 2) as i32,
3606 i as u16,
3607 0,
3608 i == 0,
3609 true,
3610 &mut out
3611 ));
3612 frames.push(out);
3613 }
3614 assert_eq!(h264_max_num_ref_frames(&frames[0]), Some(1));
3615 assert!(enc.invalidate_reference(3));
3616 let mut out = Vec::new();
3617 assert!(enc.encode_with_headers(
3618 &text_luma(4),
3619 &u,
3620 &v,
3621 W as i32,
3622 (W / 2) as i32,
3623 (W / 2) as i32,
3624 4,
3625 0,
3626 false,
3627 true,
3628 &mut out
3629 ));
3630 assert_eq!(h264_frame_type(&out), FRAME_KEY, "nothing older is held");
3631 }
3632
3633 #[test]
3638 #[cfg(feature = "gpl")]
3639 fn x264_answers_a_loss_at_the_frame_num_wrap_with_a_key_frame() {
3640 use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, h264_frame_type};
3641 use crate::encoders::reference::Reference;
3642 use crate::encoders::sps::h264_frame_num_range;
3643 use crate::webcam::decode::{Decoder as _, VideoDecoder};
3644 let (w, h) = (160usize, 96usize);
3645 let (u, v) = (vec![128u8; w * h / 4], vec![128u8; w * h / 4]);
3646 let mut enc =
3647 H264EncoderWrapper::new(w as i32, h as i32, 20, false, 60.0, 1, false, 0, 0, 0, 0)
3648 .expect("x264 init");
3649 let encode = |enc: &mut H264EncoderWrapper, i: usize| {
3650 let y: Vec<u8> = (0..w * h)
3651 .map(|p| ((p % w + p / w + 3 * i) * 5 % 256) as u8)
3652 .collect();
3653 let mut out = Vec::new();
3654 assert!(enc.encode_with_headers(
3655 &y,
3656 &u,
3657 &v,
3658 w as i32,
3659 (w / 2) as i32,
3660 (w / 2) as i32,
3661 i as u16,
3662 0,
3663 i == 0,
3664 true,
3665 &mut out
3666 ));
3667 (out, enc.last_reference())
3668 };
3669 let (mut whole, mut lossy) = (
3670 VideoDecoder::new(Codec::H264).unwrap(),
3671 VideoDecoder::new(Codec::H264).unwrap(),
3672 );
3673 let (first, _) = encode(&mut enc, 0);
3674 let range = h264_frame_num_range(&first).expect("the key frame carries the SPS") as usize;
3675 assert_eq!(range, 4096, "x264's sixteen values, a byte wider");
3676 assert!(whole.decode(&first).expect("decode") && lossy.decode(&first).expect("decode"));
3677 for i in 1..=18 {
3678 let (out, _) = encode(&mut enc, i);
3679 assert!(whole.decode(&out).expect("decode"), "frame {i}");
3680 if i < 16 {
3681 assert!(lossy.decode(&out).expect("decode"), "frame {i}");
3682 }
3683 }
3684 assert!(enc.invalidate_reference(16), "x264's wrap frame is lost");
3685 let (out, reference) = encode(&mut enc, 19);
3686 assert_eq!(reference, Reference::Frame(15));
3687 assert_eq!(h264_frame_type(&out), FRAME_DELTA);
3688 assert!(whole.decode(&out).expect("decode"));
3689 assert!(lossy.decode(&out).expect("decode past x264's wrap"));
3690 let apart = luma_distance(&whole.frame().unwrap(), &lossy.frame().unwrap());
3691 assert!(
3692 apart < 0.5,
3693 "the decoder that lost frames 16-18 shows frame 19 {apart:.2} off the one that saw them"
3694 );
3695 for i in 20..=range {
3696 let (out, _) = encode(&mut enc, i);
3697 if i < range {
3698 assert!(lossy.decode(&out).expect("decode"), "frame {i}");
3699 }
3700 }
3701 assert!(
3702 enc.invalidate_reference(range as u16),
3703 "the stream's wrap frame is reported lost"
3704 );
3705 let (out, reference) = encode(&mut enc, range + 1);
3706 assert_eq!(reference, Reference::None);
3707 assert_eq!(h264_frame_type(&out), FRAME_KEY);
3708 assert!(
3709 lossy.decode(&out).expect("decode past the wrap"),
3710 "the decoder that never saw the wrap frame shows the next one"
3711 );
3712 assert_eq!(
3713 encode(&mut enc, range + 2).1,
3714 Reference::Frame(range as u16 + 1)
3715 );
3716 assert!(
3717 enc.invalidate_reference(range as u16 + 2),
3718 "the count restarted at the key frame"
3719 );
3720 assert_eq!(
3721 encode(&mut enc, range + 3).1,
3722 Reference::Frame(range as u16 + 1)
3723 );
3724 }
3725
3726 #[cfg(feature = "gpl")]
3728 fn luma_distance(
3729 a: &crate::webcam::convert::I420View<'_>,
3730 b: &crate::webcam::convert::I420View<'_>,
3731 ) -> f64 {
3732 let rows =
3733 a.y.chunks(a.y_stride)
3734 .zip(b.y.chunks(b.y_stride))
3735 .take(a.height);
3736 rows.flat_map(|(ra, rb)| {
3737 ra[..a.width]
3738 .iter()
3739 .zip(&rb[..a.width])
3740 .map(|(&x, &y)| (x as f64 - y as f64).abs())
3741 })
3742 .sum::<f64>()
3743 / (a.width * a.height) as f64
3744 }
3745
3746 fn encode_oh264(cbr: bool, kbps: i32, crf: i32, min_qp: i32, max_qp: i32) -> Vec<Vec<u8>> {
3750 let s = RustCaptureSettings {
3751 width: W as i32,
3752 height: H as i32,
3753 target_fps: 60.0,
3754 codec: Codec::H264,
3755 video_cbr_mode: cbr,
3756 video_bitrate_kbps: kbps,
3757 video_crf: crf,
3758 video_min_qp: min_qp,
3759 video_max_qp: max_qp,
3760 ..Default::default()
3761 };
3762 let mut enc = Openh264Encoder::new(&s).expect("oh264 init");
3763 (0..FRAMES)
3764 .map(|i| {
3765 let y = text_luma(i);
3766 let mut bgra = vec![255u8; W * H * 4];
3767 for (px, &l) in bgra.as_chunks_mut::<4>().0.iter_mut().zip(y.iter()) {
3768 px[0] = l;
3769 px[1] = l;
3770 px[2] = l;
3771 }
3772 enc.encode_host_argb(&bgra, W * 4, i as u64, i == 0, false)
3773 .expect("oh264 encode")
3774 })
3775 .collect()
3776 }
3777
3778 fn decode_luma(frames: &[Vec<u8>]) -> Vec<Vec<u8>> {
3781 let mut dec = Decoder::new().expect("decoder");
3782 let mut out = Vec::new();
3783 for f in frames {
3784 if f.is_empty() {
3785 continue;
3786 }
3787 if let Ok(Some(img)) = dec.decode(f) {
3788 let (w, h) = img.dimensions();
3789 let stride = img.strides().0;
3790 let mut y = vec![0u8; w * h];
3791 for r in 0..h {
3792 y[r * w..r * w + w].copy_from_slice(&img.y()[r * stride..r * stride + w]);
3793 }
3794 out.push(y);
3795 }
3796 }
3797 out
3798 }
3799
3800 fn mean_psnr(a: &[Vec<u8>], b: &[Vec<u8>]) -> f64 {
3803 let n = a.len().min(b.len());
3804 let mut acc = 0.0;
3805 for i in 0..n {
3806 let mse: f64 = a[i]
3807 .iter()
3808 .zip(b[i].iter())
3809 .map(|(&x, &y)| {
3810 let d = x as f64 - y as f64;
3811 d * d
3812 })
3813 .sum::<f64>()
3814 / a[i].len() as f64;
3815 acc += if mse <= 0.0 {
3816 99.0
3817 } else {
3818 10.0 * (255.0f64 * 255.0 / mse).log10()
3819 };
3820 }
3821 acc / n.max(1) as f64
3822 }
3823
3824 fn kbps(frames: &[Vec<u8>]) -> f64 {
3826 frames.iter().map(|f| f.len()).sum::<usize>() as f64 * 8.0 * 60.0 / FRAMES as f64 / 1000.0
3827 }
3828
3829 #[cfg(feature = "gpl")]
3839 #[test]
3840 fn cbr_qp_bound_sweep_x264() {
3841 let reference = decode_luma(&encode_x264(false, 0, 12, 0, 0));
3842
3843 println!("scrolling-text 720p60 @ 2 Mbps CBR, x264 (PSNR vs own CRF-12 decode):");
3844 let mut rows = Vec::new();
3845 for &max_qp in &[0i32, 45, 40, 35, 30] {
3846 let x = encode_x264(true, 2000, 25, 0, max_qp);
3847 let psnr = mean_psnr(&decode_luma(&x), &reference);
3848 println!(
3849 " max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
3850 max_qp,
3851 kbps(&x),
3852 psnr
3853 );
3854 rows.push((max_qp, psnr));
3855 }
3856 println!("scrolling-text 720p60 @ 12 Mbps CBR, x264 min-QP sweep:");
3857 for &min_qp in &[0i32, 10, 15] {
3858 let x = encode_x264(true, 12000, 25, min_qp, 0);
3859 let psnr = mean_psnr(&decode_luma(&x), &reference);
3860 println!(
3861 " min_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
3862 min_qp,
3863 kbps(&x),
3864 psnr
3865 );
3866 }
3867
3868 let base = rows[0].1;
3869 let capped = rows.last().unwrap().1;
3870 assert!(
3871 capped > base + 0.5,
3872 "x264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
3873 );
3874 }
3875
3876 #[test]
3883 fn cbr_qp_bound_sweep_openh264() {
3884 let reference = decode_luma(&encode_oh264(false, 0, 12, 0, 0));
3885
3886 println!("scrolling-text 720p60 @ 2 Mbps CBR, oh264 (PSNR vs own QP-12 decode):");
3887 let mut rows = Vec::new();
3888 for &max_qp in &[0i32, 45, 40, 35, 30] {
3889 let o = encode_oh264(true, 2000, 25, 0, max_qp);
3890 let psnr = mean_psnr(&decode_luma(&o), &reference);
3891 println!(
3892 " max_qp {:>2}: {:>8.1} kbps / {:>5.2} dB",
3893 max_qp,
3894 kbps(&o),
3895 psnr
3896 );
3897 rows.push((max_qp, psnr));
3898 }
3899
3900 let base = rows[0].1;
3901 let capped = rows.last().unwrap().1;
3902 assert!(
3903 capped > base + 0.5,
3904 "oh264 max-QP clamp had no effect: {capped:.2} vs {base:.2} dB"
3905 );
3906 }
3907
3908 #[cfg(feature = "gpl")]
3921 #[test]
3922 fn cbr_bitrate_tracks_configured_rate_after_fps_change() {
3923 const TARGET_KBPS: i32 = 20000;
3924 const WARMUP: usize = 24;
3925 const MEASURED: usize = 96;
3926 let u = vec![128u8; (W / 2) * (H / 2)];
3927 let v = vec![128u8; (W / 2) * (H / 2)];
3928 let vbv = |fps: f64| {
3929 (crate::encoders::vbv_bits((TARGET_KBPS as u32) * 1000, fps, 0.0, 0.0) / 1000).max(1)
3930 as i32
3931 };
3932 let noise_luma = |frame: usize| -> Vec<u8> {
3936 let mut y = vec![0u8; W * H];
3937 let mut s = (frame as u32).wrapping_mul(2654435761).wrapping_add(1);
3938 for p in y.iter_mut() {
3939 s ^= s << 13;
3940 s ^= s >> 17;
3941 s ^= s << 5;
3942 *p = (s >> 24) as u8;
3943 }
3944 y
3945 };
3946
3947 let mut enc = H264EncoderWrapper::new(
3948 W as i32,
3949 H as i32,
3950 25,
3951 false,
3952 60.0,
3953 1,
3954 true,
3955 TARGET_KBPS,
3956 vbv(60.0),
3957 0,
3958 0,
3959 )
3960 .expect("x264 init");
3961
3962 let measure = |enc: &mut H264EncoderWrapper, start: usize| -> f64 {
3963 let mut bytes = 0usize;
3964 let mut counted = 0usize;
3965 for i in start..start + WARMUP + MEASURED {
3966 let y = noise_luma(i);
3967 let mut out = Vec::new();
3968 enc.encode_with_headers(
3969 &y,
3970 &u,
3971 &v,
3972 W as i32,
3973 (W / 2) as i32,
3974 (W / 2) as i32,
3975 i as u16,
3976 0,
3977 i == start,
3978 true,
3979 &mut out,
3980 );
3981 if i >= start + WARMUP && !out.is_empty() {
3982 bytes += out.len();
3983 counted += 1;
3984 }
3985 }
3986 bytes as f64 / counted.max(1) as f64
3987 };
3988
3989 let per_frame_60 = measure(&mut enc, 0);
3990 enc.reconfigure_rate(TARGET_KBPS, vbv(30.0), 30.0);
3991 let per_frame_30 = measure(&mut enc, 1000);
3992
3993 let eff_60 = per_frame_60 * 8.0 * 60.0 / 1000.0;
3994 let eff_30 = per_frame_30 * 8.0 * 30.0 / 1000.0;
3995 println!(
3996 "x264 CBR {TARGET_KBPS} kbps: 60fps {eff_60:.0} kbps ({per_frame_60:.0} B/frame), 30fps {eff_30:.0} kbps ({per_frame_30:.0} B/frame)"
3997 );
3998
3999 let ratio = per_frame_30 / per_frame_60.max(1.0);
4000 assert!(
4001 (1.5..2.6).contains(&ratio),
4002 "30fps per-frame size {per_frame_30:.0} B vs 60fps {per_frame_60:.0} B (ratio {ratio:.2}); halving fps should roughly double each frame"
4003 );
4004 assert!(
4005 eff_30 > eff_60 * 0.75,
4006 "30fps effective bitrate {eff_30:.0} kbps collapsed from the 60fps {eff_60:.0} kbps; fps change did not re-budget the bitrate"
4007 );
4008 }
4009}