fix: FPS limiter never passes frames when input > target rate

The old FpsLimit compared timestamps between CONSECUTIVE frames.
When PipeWire delivers at 60fps (16ms intervals) and target is 30fps
(33ms min_interval), the gap between consecutive frames is always
16ms < 33ms, so EVERY frame was rejected after the first.

Fix: track last_output_time and compare against that instead of the
previous frame's timestamp. Now frames pass when enough time has
elapsed since the last OUTPUT, not since the last INPUT.

Also adds PipeWire process callback counter logging and frame
diagnostic STATS in state_portal.rs for debugging.
This commit is contained in:
dailz
2026-05-29 22:09:35 +08:00
parent d80b34f44f
commit a83d146ed3
2 changed files with 50 additions and 22 deletions

View File

@@ -405,7 +405,14 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let format_info = format_info.clone(); let format_info = format_info.clone();
let frame_tx = frame_tx.clone(); let frame_tx = frame_tx.clone();
let dropped = dropped; let dropped = dropped;
let process_count = Rc::new(Cell::new(0u64));
let process_count_clone = process_count.clone();
move |stream, _| { move |stream, _| {
let count = process_count_clone.get() + 1;
process_count_clone.set(count);
if count <= 5 || count % 60 == 0 {
tracing::info!("PipeWire process callback #{count}");
}
// 从流中出队原始 buffer包含帧数据的元信息 // 从流中出队原始 buffer包含帧数据的元信息
let raw_buf = unsafe { stream.dequeue_raw_buffer() }; let raw_buf = unsafe { stream.dequeue_raw_buffer() };
if raw_buf.is_null() { if raw_buf.is_null() {

View File

@@ -1,7 +1,8 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
pub struct FpsLimit<T> { pub struct FpsLimit<T> {
on_deck: Option<(T, Instant)>, on_deck: Option<T>,
last_output_time: Option<Instant>,
min_interval: Duration, min_interval: Duration,
} }
@@ -9,30 +10,32 @@ impl<T> FpsLimit<T> {
pub fn new(fps: u32) -> Self { pub fn new(fps: u32) -> Self {
Self { Self {
on_deck: None, on_deck: None,
last_output_time: None,
min_interval: Duration::from_secs_f64(1.0 / fps as f64), min_interval: Duration::from_secs_f64(1.0 / fps as f64),
} }
} }
/// Feed a new frame. Returns: /// Feed a new frame. Returns:
/// - Some(previous_frame) if enough time elapsed since previous frame /// - Some(()) if enough time elapsed since the last output — proceed to encode current frame
/// - None if frame is buffered (first frame) or previous is dropped (too close) /// - None if too close to the last output — drop current frame
pub fn on_new_frame(&mut self, frame: T, timestamp: Instant) -> Option<T> { pub fn on_new_frame(&mut self, frame: T, timestamp: Instant) -> Option<T> {
let old = self.on_deck.replace((frame, timestamp)); let ready = match self.last_output_time {
match old { None => true,
None => None, // First frame — buffer it Some(last) => timestamp.duration_since(last) >= self.min_interval,
Some((old_frame, old_ts)) => { };
if timestamp.duration_since(old_ts) >= self.min_interval {
Some(old_frame) // Enough time — output previous if ready {
self.last_output_time = Some(timestamp);
self.on_deck = Some(frame);
self.on_deck.take()
} else { } else {
None // Too close — discard previous, keep new let _ = self.on_deck.replace(frame);
} None
}
} }
} }
/// Flush the last buffered frame at end of stream
pub fn flush(&mut self) -> Option<T> { pub fn flush(&mut self) -> Option<T> {
self.on_deck.take().map(|(frame, _ts)| frame) self.on_deck.take()
} }
} }
@@ -41,15 +44,15 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn first_frame_is_buffered() { fn first_frame_passes_immediately() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30); let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now(); let now = Instant::now();
let result = limiter.on_new_frame(1u32, now); let result = limiter.on_new_frame(1u32, now);
assert!(result.is_none()); assert_eq!(result, Some(1));
} }
#[test] #[test]
fn frames_too_close_drops_old() { fn frames_too_close_are_dropped() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30); let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now(); let now = Instant::now();
limiter.on_new_frame(1, now); limiter.on_new_frame(1, now);
@@ -58,12 +61,29 @@ mod tests {
} }
#[test] #[test]
fn frames_far_enough_output_old() { fn frames_far_enough_pass() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30); let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now(); let now = Instant::now();
limiter.on_new_frame(1, now); limiter.on_new_frame(1, now);
let result = limiter.on_new_frame(2, now + Duration::from_millis(40)); let result = limiter.on_new_frame(2, now + Duration::from_millis(34));
assert_eq!(result, Some(1)); assert_eq!(result, Some(2));
}
#[test]
fn high_fps_input_downsampled_correctly() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let base = Instant::now();
let mut outputs = Vec::new();
for i in 0..10u32 {
let t = base + Duration::from_millis(i as u64 * 16);
if let Some(f) = limiter.on_new_frame(i, t) {
outputs.push(f);
}
}
assert!(outputs.len() >= 3, "expected at least 3 outputs, got {} ({:?})", outputs.len(), outputs);
assert_eq!(outputs[0], 0);
} }
#[test] #[test]
@@ -71,7 +91,8 @@ mod tests {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30); let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now(); let now = Instant::now();
limiter.on_new_frame(1, now); limiter.on_new_frame(1, now);
assert_eq!(limiter.flush(), Some(1)); limiter.on_new_frame(2, now + Duration::from_millis(1));
assert_eq!(limiter.flush(), Some(2));
assert_eq!(limiter.flush(), None); assert_eq!(limiter.flush(), None);
} }
} }