diff --git a/src/state_portal.rs b/src/state_portal.rs index 318a4b1..c2f5e3d 100644 --- a/src/state_portal.rs +++ b/src/state_portal.rs @@ -62,11 +62,7 @@ pub struct StatePortal { webrtc_thread: Option, webrtc_paused: Option>, last_capture_arrival: Option, // timestamp of last real frame arrival - stall_start: Option, // when current stall began - last_stall_log: Option, // rate-limiting for stall warnings - last_fillable_frame: Option, // cached last frame for filler duplication - next_filler_at: Option, // when to send next filler frame - filler_frames_sent: u64, + idle_log_start: Option, // when current idle period began (one-shot DEBUG log guard) shutdown_started: bool, // idempotency guard; plain bool because &mut self is exclusive (not AtomicBool) } @@ -109,11 +105,7 @@ impl StatePortal { webrtc_thread: None, webrtc_paused, last_capture_arrival: None, - stall_start: None, - last_stall_log: None, - last_fillable_frame: None, - next_filler_at: None, - filler_frames_sent: 0, + idle_log_start: None, shutdown_started: false, }) } @@ -355,14 +347,7 @@ impl StatePortal { } } let snap = self.stats.snapshot_and_reset(); - if self.filler_frames_sent > 0 { - tracing::info!( - "stats: {snap} filler_frames_sent={}", - self.filler_frames_sent - ); - } else { - tracing::info!("stats: {snap}"); - } + tracing::info!("stats: {snap}"); } Ok(true) @@ -374,89 +359,33 @@ impl StatePortal { }; let now = Instant::now(); - let frame_interval = Duration::from_secs_f64(1.0 / f64::from(self.args.fps.max(1))); - let stall_threshold = Duration::from_millis(100).max(frame_interval * 3); - if now.duration_since(last_capture_arrival) <= stall_threshold { + // Wayland damage-driven delivery: static content means no new frames. + // This is normal Wayland behavior, not a compositor hang. Only log DEBUG + // after a meaningful idle period, and only once per idle episode. + // See issues #15 and #18. + const CAPTURE_IDLE_LOG_THRESHOLD: Duration = Duration::from_secs(5); + if now.duration_since(last_capture_arrival) <= CAPTURE_IDLE_LOG_THRESHOLD { return; } - if self.stall_start.is_none() { - self.stall_start = Some(now); - self.last_stall_log = Some(now); - tracing::warn!("compositor frame delivery stalled"); - } else { - let should_log = self.last_stall_log.map_or(true, |last_log| { - now.duration_since(last_log) >= Duration::from_secs(1) - }); - if should_log { - self.last_stall_log = Some(now); - tracing::warn!("compositor frame delivery stalled"); - } - } - - self.maybe_send_filler_frame(); - } - - fn maybe_send_filler_frame(&mut self) { - if self.webrtc_thread.is_none() || self.stall_start.is_none() { - return; - } - let Some(cached) = &self.last_fillable_frame else { - return; - }; - - const MAX_FILLER_DURATION: Duration = Duration::from_secs(2); - if let Some(stall_start) = self.stall_start { - if stall_start.elapsed() > MAX_FILLER_DURATION { - return; - } - } - - let now = Instant::now(); - let frame_interval = Duration::from_secs_f64(1.0 / f64::from(self.args.fps.max(1))); - - let Some(next) = self.next_filler_at else { - self.next_filler_at = Some(now + frame_interval); - return; - }; - if now < next { - return; - } - - let filler = CpuNv12Frame { - y_data: cached.y_data.clone(), - uv_data: cached.uv_data.clone(), - y_stride: cached.y_stride, - uv_stride: cached.uv_stride, - pts: self.frames_encoded as i64, - }; - - if let Some(enc_thread) = &self.enc_thread { - match enc_thread.input_tx.try_send(filler) { - Ok(()) => { - self.frames_encoded += 1; - self.filler_frames_sent += 1; - self.next_filler_at = Some(next + frame_interval); - } - Err(crossbeam_channel::TrySendError::Full(_)) => {} - Err(crossbeam_channel::TrySendError::Disconnected(_)) => { - tracing::error!("Encode thread disconnected during filler"); - self.errored = true; - } - } + if self.idle_log_start.is_none() { + // Use last_capture_arrival as idle start for accurate elapsed duration. + self.idle_log_start = Some(last_capture_arrival); + tracing::debug!( + elapsed_ms = now.duration_since(last_capture_arrival).as_millis(), + "portal capture idle; no damage frames received (normal Wayland behavior)" + ); } } fn record_frame_arrival(&mut self) { - if let Some(stall_start) = self.stall_start.take() { - tracing::info!( - "compositor frame delivery resumed after {:.0}ms", - stall_start.elapsed().as_secs_f64() * 1000.0 + if let Some(idle_start) = self.idle_log_start.take() { + tracing::debug!( + idle_ms = idle_start.elapsed().as_millis(), + "portal capture resumed after idle period" ); - self.last_stall_log = None; } self.last_capture_arrival = Some(Instant::now()); - self.next_filler_at = None; } /// 为当前帧解析可用的 DRM 渲染设备 @@ -589,17 +518,9 @@ impl StatePortal { "internal invariant broken: encode thread missing while async import is active" ) })?; - let fillable_frame = CpuNv12Frame { - y_data: cpu_nv12.y_data.clone(), - uv_data: cpu_nv12.uv_data.clone(), - y_stride: cpu_nv12.y_stride, - uv_stride: cpu_nv12.uv_stride, - pts: 0, - }; match enc_thread.input_tx.try_send(cpu_nv12) { Ok(()) => { self.frames_encoded += 1; - self.last_fillable_frame = Some(fillable_frame); } Err(crossbeam_channel::TrySendError::Full(_)) => { tracing::debug!("Encode thread input full, dropping portal frame"); @@ -625,7 +546,6 @@ impl StatePortal { } self.shutdown_started = true; - self.last_fillable_frame = None; // 1. Stop encode thread (drops webrtc_tx → signals WebRTC thread to exit) if let Some(mut enc_thread) = self.enc_thread.take() { drop(enc_thread.input_tx);