1use crate::encoders::nvenc::NvencEncoder;
14use crate::encoders::software::{encode_cpu, EncodedStripe, StripeState};
15use crate::encoders::vaapi::VaapiEncoder;
16use crate::RustCaptureSettings;
17use std::sync::Arc;
18
19pub struct HwFrameDecision {
21 pub send: bool,
22 pub force_idr: bool,
23 pub target_qp: u32,
24}
25
26pub fn periodic_idr_due(settings: &RustCaptureSettings, frame_counter: u16) -> bool {
40 let secs = settings.keyframe_interval_s;
41 if secs <= 0.0 {
42 return false;
43 }
44 let safe_fps = settings.target_fps.max(1.0);
45 let interval = ((safe_fps * secs).round() as u64).max(1);
46 (frame_counter as u64).is_multiple_of(interval)
47}
48
49pub fn decide_hw_fullframe(
76 st: &mut StripeState,
77 settings: &RustCaptureSettings,
78 frame_counter: u16,
79 is_dirty: bool,
80 is_animated: bool,
81 requested_idr: bool,
82) -> HwFrameDecision {
83 let normal_qp = settings.video_crf as u32;
84 let paint_qp = settings.video_paintover_crf as u32;
85 let trigger_frames = settings.paint_over_trigger_frames;
86 let use_paint_over = settings.use_paint_over_quality;
87 let burst = settings.video_paintover_burst_frames;
88 let streaming = settings.video_streaming_mode;
89 let burst_qp = if use_paint_over && paint_qp < normal_qp { paint_qp } else { normal_qp };
90
91 let mut send_frame = false;
92 let mut force_idr = false;
93 let mut target_qp = normal_qp;
94
95 if st.h264_burst_frames_remaining > 0 {
96 send_frame = true;
97 target_qp = burst_qp;
98 st.h264_burst_frames_remaining -= 1;
99
100 if is_dirty {
101 st.h264_burst_frames_remaining = 0;
102 st.paint_over_sent = false;
103 target_qp = normal_qp;
104 }
105 }
106
107 if !send_frame && (streaming || is_animated) {
108 send_frame = true;
109 }
110
111 let recovery_idr = requested_idr || periodic_idr_due(settings, frame_counter);
112
113 if is_dirty {
114 send_frame = true;
115 force_idr = recovery_idr;
116 st.no_motion_frame_count = 0;
117 st.paint_over_sent = false;
118 st.h264_burst_frames_remaining = 0;
119 target_qp = normal_qp;
120 } else if recovery_idr {
121 send_frame = true;
122 force_idr = true;
123 if st.h264_burst_frames_remaining <= 0 {
124 target_qp = normal_qp;
125 if burst > 0 {
126 st.paint_over_sent = true;
127 st.h264_burst_frames_remaining = burst;
128 }
129 }
130 } else if !send_frame {
131 st.no_motion_frame_count += 1;
132
133 if use_paint_over
134 && st.no_motion_frame_count >= trigger_frames
135 && !st.paint_over_sent
136 && paint_qp < normal_qp
137 {
138 send_frame = true;
139 st.paint_over_sent = true;
140 target_qp = paint_qp;
141 st.h264_burst_frames_remaining = burst - 1;
142 }
143 }
144
145 HwFrameDecision { send: send_frame, force_idr, target_qp }
146}
147
148#[allow(clippy::large_enum_variant)]
152enum X11Encoder {
153 None,
154 Nvenc(NvencEncoder),
155 Vaapi(VaapiEncoder),
156}
157
158fn select_encoder(settings: &RustCaptureSettings) -> X11Encoder {
162 if settings.output_mode == 1 && !settings.use_cpu && settings.encode_node_index != -1 {
163 let encode_driver = crate::get_gpu_driver(settings.encode_node_index.max(0));
164 println!(
165 "[x11] Encode Node Index: {} | Driver: {}",
166 settings.encode_node_index.max(0), encode_driver
167 );
168 if !crate::driver_selects_nvenc(&encode_driver) {
169 println!("[x11] Initializing Unified VAAPI Encoder...");
170 match VaapiEncoder::new_host(settings) {
171 Ok(e) => {
172 println!(
173 "[x11] VAAPI Encoder initialized successfully ({}).",
174 if e.is_fullcolor() { "4:4:4" } else { "4:2:0" }
175 );
176 return X11Encoder::Vaapi(e);
177 }
178 Err(err) => {
179 eprintln!("[x11] Failed to init VAAPI: {err}. Falling back to CPU ({}).", crate::encoders::SOFTWARE_H264_ENCODER);
180 return X11Encoder::None;
181 }
182 }
183 }
184 println!("[x11] Nvidia Encoder detected. Initializing NVENC...");
185 return match NvencEncoder::new(settings, std::ptr::null()) {
186 Ok(e) => {
187 println!("[x11] NVENC Encoder initialized successfully.");
188 X11Encoder::Nvenc(e)
189 }
190 Err(err) => {
191 eprintln!("[x11] Failed to init NVENC: {err}. Falling back to CPU ({}).", crate::encoders::SOFTWARE_H264_ENCODER);
192 X11Encoder::None
193 }
194 };
195 }
196 if settings.output_mode == 1 {
197 println!(
198 "[x11] No GPU Encoder available -> Using CPU Software Encoding ({}).",
199 crate::encoders::SOFTWARE_H264_ENCODER
200 );
201 }
202 X11Encoder::None
203}
204
205pub struct X11Pipeline {
217 settings: RustCaptureSettings,
218 stripes: Vec<StripeState>,
219 stripes_carrying: f32,
221 hw: X11Encoder,
222 hw_state: StripeState,
223 frame_counter: u16,
224 pending_force_idr: bool,
225 hw_error_streak: u32,
228 hw_rebuilt: bool,
229}
230
231impl X11Pipeline {
232 pub fn new(settings: RustCaptureSettings) -> Self {
250 let hw = select_encoder(&settings);
251 Self {
252 settings,
253 stripes: Vec::new(),
254 stripes_carrying: 1.0,
255 hw,
256 hw_state: StripeState::default(),
257 frame_counter: 0,
258 pending_force_idr: false,
259 hw_error_streak: 0,
260 hw_rebuilt: false,
261 }
262 }
263
264 fn recover_hw(&mut self) {
273 self.hw_error_streak = 0;
274 if self.hw_rebuilt {
275 eprintln!(
276 "[x11] HW encoder unrecoverable; demoting to software encoding ({}).",
277 crate::encoders::SOFTWARE_H264_ENCODER
278 );
279 self.hw = X11Encoder::None;
283 } else {
284 eprintln!("[x11] rebuilding HW encoder after repeated encode errors.");
285 self.hw = X11Encoder::None;
286 self.hw = select_encoder(&self.settings);
287 self.hw_rebuilt = true;
288 }
289 self.hw_state = StripeState::default();
290 self.stripes.clear();
291 self.pending_force_idr = true;
292 }
293
294 pub fn request_idr(&mut self) {
296 self.pending_force_idr = true;
297 }
298
299 pub fn encoder_name(&self) -> &str {
301 match &self.hw {
302 X11Encoder::Nvenc(_) => "NVENC",
303 X11Encoder::Vaapi(_) => "VAAPI",
304 X11Encoder::None => "CPU",
305 }
306 }
307
308 pub fn colorspace_desc(&self) -> &'static str {
312 let fullcolor = match &self.hw {
313 X11Encoder::Vaapi(enc) => enc.is_fullcolor(),
314 X11Encoder::Nvenc(_) => self.settings.video_fullcolor,
315 X11Encoder::None => {
316 self.settings.video_fullcolor && crate::encoders::SOFTWARE_H264_FULLCOLOR
317 }
318 };
319 crate::encoders::colorspace_desc(fullcolor, matches!(self.hw, X11Encoder::None))
320 }
321
322 pub fn reshape(&mut self, settings: &RustCaptureSettings, size_changed: bool) -> bool {
334 if !size_changed {
335 if let X11Encoder::Nvenc(enc) = &mut self.hw {
336 enc.release_pinned_hosts();
337 }
338 self.settings = settings.clone();
339 return true;
340 }
341 match &mut self.hw {
342 X11Encoder::Nvenc(enc) => {
343 if let Err(e) = enc.reconfigure_resolution(settings) {
344 eprintln!("[x11] NVENC in-place resize unavailable ({e}); rebuilding");
345 return false;
346 }
347 }
348 X11Encoder::None => {}
349 _ => return false,
350 }
351 self.settings = settings.clone();
352 self.stripes.clear();
353 self.hw_state = StripeState::default();
354 true
355 }
356
357 pub fn update_rate(&mut self, bitrate_kbps: i32, vbv_multiplier: f64, fps: f64) {
363 self.settings.video_bitrate_kbps = bitrate_kbps;
364 self.settings.video_vbv_multiplier = vbv_multiplier;
365 if fps > 0.0 {
366 self.settings.target_fps = fps;
367 }
368 let rate_error = match &mut self.hw {
369 X11Encoder::Nvenc(enc) => {
370 enc.reconfigure_rate(&self.settings);
371 None
372 }
373 X11Encoder::Vaapi(enc) => enc.reconfigure_rate(&self.settings).err(),
374 X11Encoder::None => None,
375 };
376 if let Some(e) = rate_error {
377 eprintln!("[x11] VAAPI rate reconfigure failed: {e}");
380 self.recover_hw();
381 }
382 }
383
384 pub fn update_tunables(&mut self, t: &crate::LiveTunables) {
387 t.apply_to(&mut self.settings);
388 }
389
390 pub fn process(&mut self, argb: &[u8], stride: usize) -> Vec<EncodedStripe> {
401 let width = self.settings.width;
402 let height = self.settings.height;
403 let requested = self.pending_force_idr;
404 let threshold = self.settings.damage_block_threshold;
405 let duration = self.settings.damage_block_duration as i32;
406
407 let out = if !matches!(self.hw, X11Encoder::None) {
408 let is_dirty = if self.settings.video_streaming_mode {
409 false
410 } else {
411 self.hw_state.content_dirty(argb, threshold, duration)
412 };
413 let d = decide_hw_fullframe(
414 &mut self.hw_state,
415 &self.settings,
416 self.frame_counter,
417 is_dirty,
418 false,
419 requested,
420 );
421 if d.send {
422 let fc = self.frame_counter as u64;
423 let force_idr = d.force_idr;
424 let res = match &mut self.hw {
425 X11Encoder::Nvenc(enc) => {
426 enc.encode_cpu_argb(argb, stride, fc, d.target_qp, force_idr)
427 }
428 X11Encoder::Vaapi(enc) => {
429 enc.encode_host_argb(argb, stride, fc, d.target_qp, force_idr)
430 }
431 X11Encoder::None => unreachable!(),
432 };
433 match res {
434 Ok(data) if !data.is_empty() => {
435 self.hw_error_streak = 0;
436 self.hw_rebuilt = false;
437 vec![EncodedStripe {
438 data: Arc::new(data),
439 data_type: 2,
440 stripe_y_start: 0,
441 stripe_height: height,
442 frame_id: self.frame_counter as i32,
443 }]
444 }
445 Ok(_) => {
446 self.hw_error_streak = 0;
447 self.hw_rebuilt = false;
448 Vec::new()
449 }
450 Err(e) => {
451 if self.hw_error_streak % crate::HW_ERROR_RECOVERY_THRESHOLD == 0 {
454 eprintln!("[x11] HW encode error: {e}");
455 }
456 self.hw_error_streak = self.hw_error_streak.saturating_add(1);
457 if self.hw_error_streak >= crate::HW_ERROR_RECOVERY_THRESHOLD {
458 self.recover_hw();
459 }
460 Vec::new()
461 }
462 }
463 } else {
464 Vec::new()
465 }
466 } else {
467 debug_assert_eq!(
468 stride,
469 width as usize * 4,
470 "software encode path assumes tightly-packed rows (stride == width*4)"
471 );
472 let force_idr_all = requested
473 || (self.settings.output_mode == 1
474 && periodic_idr_due(&self.settings, self.frame_counter));
475 encode_cpu(
476 &mut self.stripes,
477 &mut self.stripes_carrying,
478 argb,
479 width,
480 height,
481 &[],
482 &self.settings,
483 self.frame_counter,
484 false,
485 true,
486 force_idr_all,
487 )
488 };
489
490 self.pending_force_idr = (requested || self.pending_force_idr) && out.is_empty();
495 self.frame_counter = self.frame_counter.wrapping_add(1);
496 out
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
509 fn x11_software_emits_on_change_and_stays_quiet_when_static() {
510 let s = RustCaptureSettings {
511 width: 128,
512 height: 128,
513 output_mode: 0,
514 use_cpu: true,
515 jpeg_quality: 60,
516 use_paint_over_quality: false,
517 ..Default::default()
518 };
519 let mut p = X11Pipeline::new(s);
520 let stride = 128 * 4;
521 let frame_a = vec![10u8; stride * 128];
522 let mut frame_b = frame_a.clone();
523 for px in frame_b.iter_mut().take(stride * 40) {
524 *px = 200;
525 }
526 let n1 = p.process(&frame_a, stride).len();
527 let n2 = p.process(&frame_a, stride).len();
528 let n3 = p.process(&frame_b, stride).len();
529 assert!(n1 > 0, "first frame should emit (all stripes dirty vs init)");
530 assert_eq!(n2, 0, "identical static frame should emit nothing");
531 assert!(n3 > 0, "changed frame should emit dirty stripes");
532 }
533
534 #[test]
539 fn x11_software_h264_streams_recovery_burst_after_requested_idr() {
540 let s = RustCaptureSettings {
541 width: 128,
542 height: 128,
543 output_mode: 1,
544 use_cpu: true,
545 video_crf: 25,
546 video_paintover_burst_frames: 5,
547 use_paint_over_quality: false,
548 video_streaming_mode: false,
549 target_fps: 60.0,
550 ..Default::default()
551 };
552 let mut p = X11Pipeline::new(s);
553 let stride = 128 * 4;
554 let frame = vec![10u8; stride * 128];
555 assert!(!p.process(&frame, stride).is_empty(), "first frame emits");
556 for _ in 0..4 {
557 let _ = p.process(&frame, stride);
558 }
559 assert!(p.process(&frame, stride).is_empty(), "static screen is quiet before the request");
560 p.request_idr();
561 assert!(!p.process(&frame, stride).is_empty(), "requested IDR emits on a static screen");
562 for i in 0..5 {
563 assert!(!p.process(&frame, stride).is_empty(), "recovery burst frame {i} streams while static");
564 }
565 assert!(p.process(&frame, stride).is_empty(), "stream goes quiet again after the recovery burst");
566 }
567
568 #[test]
574 fn x11_colorspace_desc_reports_what_the_software_encoder_carries() {
575 let carries_444 = crate::encoders::SOFTWARE_H264_FULLCOLOR;
576 assert_eq!(carries_444, crate::encoders::SOFTWARE_H264_ENCODER == "x264");
577 let i444 = if carries_444 { "I444 (Full Range)" } else { "I420 (Limited Range)" };
578 for (fullcolor, expected) in [(true, i444), (false, "I420 (Limited Range)")] {
579 let p = X11Pipeline::new(RustCaptureSettings {
580 width: 64,
581 height: 64,
582 output_mode: 1,
583 use_cpu: true,
584 video_fullcolor: fullcolor,
585 ..Default::default()
586 });
587 assert_eq!(p.encoder_name(), "CPU");
588 assert_eq!(p.colorspace_desc(), expected);
589 assert_eq!(
590 p.colorspace_desc(),
591 crate::encoders::colorspace_desc(fullcolor && carries_444, true),
592 "X11 and Wayland must describe the same session identically"
593 );
594 }
595 }
596
597 fn settings() -> RustCaptureSettings {
598 RustCaptureSettings {
599 video_crf: 25,
600 video_paintover_crf: 18,
601 paint_over_trigger_frames: 3,
602 use_paint_over_quality: true,
603 video_paintover_burst_frames: 5,
604 video_streaming_mode: false,
605 target_fps: 60.0,
606 ..Default::default()
607 }
608 }
609
610 #[test]
613 fn static_frames_stay_silent_without_request() {
614 let mut s = settings();
615 s.use_paint_over_quality = false;
616 let mut st = StripeState::default();
617 for fc in [0u16, 1, 120, 240] {
618 let d = decide_hw_fullframe(&mut st, &s, fc, false, false, false);
619 assert!(!d.send && !d.force_idr, "frame {fc} should stay idle");
620 }
621 }
622
623 #[test]
627 fn requested_idr_forces_send_even_on_static() {
628 let s = settings();
629 let mut st = StripeState::default();
630 let d = decide_hw_fullframe(&mut st, &s, 5, false, false, true);
631 assert!(d.send && d.force_idr);
632 assert_eq!(d.target_qp, 25);
633 assert_eq!(st.h264_burst_frames_remaining, 5);
634 let d = decide_hw_fullframe(&mut st, &s, 6, false, false, false);
635 assert!(d.send && !d.force_idr);
636 assert_eq!(d.target_qp, 18);
637 assert_eq!(st.h264_burst_frames_remaining, 4);
638 let d = decide_hw_fullframe(&mut st, &s, 7, true, false, false);
639 assert!(d.send && !d.force_idr);
640 assert_eq!(d.target_qp, 25);
641 assert_eq!(st.h264_burst_frames_remaining, 0);
642 }
643
644 #[test]
648 fn requested_idr_recovers_even_without_paint_over() {
649 let mut s = settings();
650 s.use_paint_over_quality = false;
651 let mut st = StripeState::default();
652 let d = decide_hw_fullframe(&mut st, &s, 5, false, false, true);
653 assert!(d.send && d.force_idr);
654 assert_eq!(st.h264_burst_frames_remaining, 5, "recovery burst armed without paint-over");
655 let d = decide_hw_fullframe(&mut st, &s, 6, false, false, false);
656 assert!(d.send && !d.force_idr);
657 assert_eq!(d.target_qp, 25);
658 }
659
660 #[test]
661 fn configured_interval_restores_scheduled_keyframes() {
662 let mut s = settings();
663 s.keyframe_interval_s = 2.0;
664 assert!(periodic_idr_due(&s, 0));
665 assert!(!periodic_idr_due(&s, 1));
666 assert!(periodic_idr_due(&s, 120));
667 let mut st = StripeState::default();
668 let d = decide_hw_fullframe(&mut st, &s, 120, false, false, false);
669 assert!(d.send && d.force_idr, "interval keyframe fires on a static screen");
670 s.keyframe_interval_s = 0.0;
671 assert!(!periodic_idr_due(&s, 0) && !periodic_idr_due(&s, 120));
672 }
673
674 #[test]
678 fn paint_over_fires_after_trigger_then_bursts() {
679 let s = settings();
680 let mut st = StripeState::default();
681 for fc in 1..=2 {
682 let d = decide_hw_fullframe(&mut st, &s, fc, false, false, false);
683 assert!(!d.send, "frame {fc} should stay idle");
684 }
685 let d = decide_hw_fullframe(&mut st, &s, 3, false, false, false);
686 assert!(d.send && !d.force_idr);
687 assert_eq!(d.target_qp, 18);
688 assert_eq!(st.h264_burst_frames_remaining, 4);
689 let d = decide_hw_fullframe(&mut st, &s, 4, false, false, false);
690 assert!(d.send && !d.force_idr);
691 assert_eq!(d.target_qp, 18);
692 assert_eq!(st.h264_burst_frames_remaining, 3);
693 }
694
695 #[test]
696 fn motion_resets_paintover_and_uses_normal_qp() {
697 let s = settings();
698 let mut st = StripeState {
699 paint_over_sent: true,
700 h264_burst_frames_remaining: 2,
701 no_motion_frame_count: 9,
702 ..Default::default()
703 };
704 let d = decide_hw_fullframe(&mut st, &s, 7, true, false, false);
705 assert!(d.send);
706 assert_eq!(d.target_qp, 25);
707 assert!(!st.paint_over_sent);
708 assert_eq!(st.h264_burst_frames_remaining, 0);
709 assert_eq!(st.no_motion_frame_count, 0);
710 }
711}
712
713#[cfg(test)]
714mod vbv_tests {
715 #[test]
718 fn vbv_policy() {
719 use crate::encoders::vbv_bits;
720 let frame = 4_000_000f64 / 60.0;
721 assert_eq!(vbv_bits(4_000_000, 60.0, 0.0, 0.0), (frame * 1.5).round() as u32);
722 assert_eq!(vbv_bits(4_000_000, 60.0, 2.0, 0.0), (frame * 3.0).round() as u32);
723 assert_eq!(vbv_bits(4_000_000, 60.0, 2.0, 1.0), frame.round() as u32);
724 assert_eq!(vbv_bits(8_000_000, 60.0, 0.0, 1.0), (2.0 * frame).round() as u32);
725 }
726}