From ee4aab0be1bdefbbca8f481ce4463bc511389515 Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Fri, 21 Mar 2025 17:33:02 +0700 Subject: [PATCH 1/7] Update screencast_thread.rs for support fps limit --- src/screencast_thread.rs | 132 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 6 deletions(-) diff --git a/src/screencast_thread.rs b/src/screencast_thread.rs index eb6dd90..d9d34d8 100644 --- a/src/screencast_thread.rs +++ b/src/screencast_thread.rs @@ -15,18 +15,31 @@ use pipewire::{ stream::{StreamRef, StreamState}, sys::pw_buffer, }; -use std::{collections::HashMap, ffi::c_void, io, iter, os::fd::IntoRawFd, slice}; +use spa_sys::{spa_format_video_raw_parse, spa_video_info_raw}; + +use std::{ + collections::HashMap, + ffi::c_void, + io, iter, + os::fd::IntoRawFd, + slice, + time::{Duration, Instant}, +}; use tokio::sync::oneshot; use wayland_client::{ protocol::{wl_buffer, wl_output, wl_shm}, WEnum, }; +use wayland_protocols::wp::input_timestamps::zv1::client::zwp_input_timestamps_v1; use crate::{ buffer, wayland::{CaptureSource, DmabufHelper, Session, WaylandHelper}, }; +const TIMESPEC_NSEC_PER_SEC: u32 = 1_000_000_000; +const FPS_MEASURE_PERIOD_SEC: u64 = 5; + pub struct ScreencastThread { node_id: u32, thread_stop_tx: pipewire::channel::Sender<()>, @@ -40,6 +53,7 @@ impl ScreencastThread { ) -> anyhow::Result { let (tx, rx) = oneshot::channel(); let (thread_stop_tx, thread_stop_rx) = pipewire::channel::channel::<()>(); + std::thread::spawn(move || { match start_stream(wayland_helper, capture_source, overlay_cursor) { Ok((loop_, _stream, _listener, _context, node_id_rx)) => { @@ -53,6 +67,7 @@ impl ScreencastThread { Err(err) => tx.send(Err(err)).unwrap(), } }); + Ok(Self { // XXX can second unwrap fail? node_id: rx.await.unwrap()?.await.unwrap()?, @@ -68,6 +83,54 @@ impl ScreencastThread { let _ = self.thread_stop_tx.send(()); } } +struct FPSLimit { + frame_last_time: Instant, + fps_last_time: Instant, + fps_frame_count: u64, +} + +impl FPSLimit { + fn new() -> Self { + Self { + frame_last_time: Instant::now(), + fps_last_time: Instant::now(), + fps_frame_count: 0, + } + } + + fn fps_limit_measure_start(&mut self) { + self.frame_last_time = Instant::now(); + } + + fn measure_fps(&mut self) { + let now = Instant::now(); + self.fps_frame_count += 1; + let elapsed_sec = (now - self.fps_last_time).as_secs(); + if elapsed_sec < FPS_MEASURE_PERIOD_SEC { + return; + } + + let avg_frames_per_sec = self.fps_frame_count / elapsed_sec; + println!( + "fps_limit: average FPS in the last {} seconds: {}", + avg_frames_per_sec, elapsed_sec + ); + self.fps_frame_count = 0; + self.fps_last_time = now; + } + + fn wait_for_next_frame(&mut self, max_fps: u32) { + self.measure_fps(); + + let elapsed_ns = self.frame_last_time.elapsed().as_nanos(); + let target_ns = (TIMESPEC_NSEC_PER_SEC / max_fps) as u128; + if target_ns > elapsed_ns { + let delay_ns = (target_ns - elapsed_ns) as u64; + // println!("Time til next: {}ns", delay_ns); + std::thread::sleep(Duration::from_nanos(delay_ns)); + } + } +} struct StreamData { dmabuf_helper: Option, @@ -78,6 +141,11 @@ struct StreamData { height: u32, node_id_tx: Option>>, buffer_damage: HashMap>, + // fps limit + framerate: u32, + seq: u32, + fps_limit: FPSLimit, + fps_max: u32, } impl StreamData { @@ -146,6 +214,23 @@ impl StreamData { if let Some(pod) = pod { let value = PodDeserializer::deserialize_from::(pod.as_bytes()); if let Ok((_, pod::Value::Object(object))) = &value { + let pwr_format: spa_video_info_raw = unsafe { + let mut pwr_format = std::mem::MaybeUninit::::uninit(); + spa_format_video_raw_parse( + pod.as_raw_ptr() as *const _, + pwr_format.as_mut_ptr(), + ); + pwr_format.assume_init() + }; + if pwr_format.max_framerate.num != 0 && pwr_format.max_framerate.denom != 0 { + let framerate = pwr_format.max_framerate.num / pwr_format.max_framerate.denom; + self.framerate = if framerate > self.fps_max { + self.fps_max + } else { + framerate + }; + } + if let Some(modifier_prop) = object .properties .iter() @@ -177,6 +262,7 @@ impl StreamData { let params = params( self.width, self.height, + self.framerate, plane_count, self.dmabuf_helper.as_ref(), Some(modifier), @@ -273,6 +359,12 @@ impl StreamData { fn process(&mut self, stream: &StreamRef) { let buffer = unsafe { stream.dequeue_raw_buffer() }; if !buffer.is_null() { + // Only wait if it not the first frame + if self.seq > 0 { + // Maybe there's a better way, e.g., using an event loop to wait and get a new frame + self.fps_limit.wait_for_next_frame(self.framerate); + } + let wl_buffer = unsafe { &*((*buffer).user_data as *const wl_buffer::WlBuffer) }; let full_damage = &[Rect { x: 0, @@ -311,6 +403,8 @@ impl StreamData { } } unsafe { stream.queue_raw_buffer(buffer) }; + self.fps_limit.fps_limit_measure_start(); + self.seq += 1; } } } @@ -341,6 +435,8 @@ fn start_stream( None => return Err(anyhow::anyhow!("failed to use shm capture to get size")), }; + let framerate = 60; // Default framerate. XXX is there a better way? + let dmabuf_helper = wayland_helper.dmabuf(); let stream = pipewire::stream::Stream::new( @@ -352,7 +448,7 @@ fn start_stream( }, )?; - let initial_params = params(width, height, 1, dmabuf_helper.as_ref(), None); + let initial_params = params(width, height, framerate, 1, dmabuf_helper.as_ref(), None); let mut initial_params: Vec<_> = initial_params .iter() .map(|x| Pod::from_bytes(x.as_slice()).unwrap()) @@ -379,6 +475,10 @@ fn start_stream( height, node_id_tx: Some(node_id_tx), buffer_damage: HashMap::new(), + framerate, + fps_limit: FPSLimit::new(), + fps_max: 120, // XXX + seq: 0, }; let listener = stream @@ -443,16 +543,17 @@ fn meta() -> Vec { fn params( width: u32, height: u32, + framerate: u32, blocks: u32, dmabuf: Option<&DmabufHelper>, fixated_modifier: Option, ) -> Vec> { [ Some(buffers(width, height, blocks)), - fixated_modifier.map(|x| format(width, height, None, Some(x))), + fixated_modifier.map(|x| format(width, height, framerate, None, Some(x))), // Favor dmabuf over shm by listing it first - dmabuf.map(|x| format(width, height, Some(x), None)), - Some(format(width, height, None, None)), + dmabuf.map(|x| format(width, height, framerate, Some(x), None)), + Some(format(width, height, framerate, None, None)), Some(meta()), ] .into_iter() @@ -523,6 +624,7 @@ fn buffers(width: u32, height: u32, blocks: u32) -> Vec { fn format( width: u32, height: u32, + framerate: u32, dmabuf: Option<&DmabufHelper>, fixated_modifier: Option, ) -> Vec { @@ -550,7 +652,25 @@ fn format( pod::Property { key: spa_sys::SPA_FORMAT_VIDEO_framerate, flags: pod::PropertyFlags::empty(), - value: pod::Value::Fraction(spa::utils::Fraction { num: 60, denom: 1 }), + value: pod::Value::Fraction(spa::utils::Fraction { num: 0, denom: 1 }), + }, + pod::Property { + key: spa_sys::SPA_FORMAT_VIDEO_maxFramerate, + flags: pod::PropertyFlags::empty(), + value: pod::Value::Choice(pod::ChoiceValue::Fraction(spa::utils::Choice( + spa::utils::ChoiceFlags::empty(), + spa::utils::ChoiceEnum::Range { + default: spa::utils::Fraction { + num: framerate, + denom: 1, + }, + min: spa::utils::Fraction { num: 1, denom: 1 }, + max: spa::utils::Fraction { + num: framerate, + denom: 1, + }, + }, + ))), }, // TODO max framerate ]; From 0e95dd693a3ba916c73945ac235c4f40ab2bc3fe Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Sun, 23 Mar 2025 16:48:33 +0700 Subject: [PATCH 2/7] add output_info_toplevel method for WaylandHelper --- src/wayland/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index d326f7d..4c8561b 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -273,6 +273,20 @@ impl WaylandHelper { self.inner.toplevels.lock().unwrap().clone() } + pub fn output_info_toplevel( + &self, + foreign_toplevel: &ExtForeignToplevelHandleV1, + ) -> Option { + let output_toplevels = self.inner.output_toplevels.lock().unwrap(); + output_toplevels.iter().find_map(|(output, toplevel_list)| { + if toplevel_list.contains(foreign_toplevel) { + self.output_info(output) + } else { + None + } + }) + } + pub fn output_info(&self, output: &wl_output::WlOutput) -> Option { self.inner.output_infos.lock().unwrap().get(output).cloned() } From 407ee31b56cee95240108375d2366ad15d374c5b Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Sun, 23 Mar 2025 17:01:54 +0700 Subject: [PATCH 3/7] use screen's frame rates for StreamData.framerate - Read the refresh rate of the captured screen and assign it to the `framerate` of `StreamData`. - Optimize the waiting mechanism for the next frame based on @Drakulix's advice [advice](https://github.com/pop-os/xdg-desktop-portal-cosmic/pull/152#discussion_r2008246077) by prioritizing frame capture and sending it to PipeWire first. Then, calculate the time taken to capture that frame, and if we are ahead of the next frame's capture time, wait for approximately `delay_ns = target_ns - frame_took_ns`. --- src/screencast_thread.rs | 72 +++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/screencast_thread.rs b/src/screencast_thread.rs index d9d34d8..611189d 100644 --- a/src/screencast_thread.rs +++ b/src/screencast_thread.rs @@ -17,6 +17,10 @@ use pipewire::{ }; use spa_sys::{spa_format_video_raw_parse, spa_video_info_raw}; +use crate::{ + buffer, + wayland::{CaptureSource, DmabufHelper, Session, WaylandHelper}, +}; use std::{ collections::HashMap, ffi::c_void, @@ -30,15 +34,9 @@ use wayland_client::{ protocol::{wl_buffer, wl_output, wl_shm}, WEnum, }; -use wayland_protocols::wp::input_timestamps::zv1::client::zwp_input_timestamps_v1; - -use crate::{ - buffer, - wayland::{CaptureSource, DmabufHelper, Session, WaylandHelper}, -}; const TIMESPEC_NSEC_PER_SEC: u32 = 1_000_000_000; -const FPS_MEASURE_PERIOD_SEC: u64 = 5; +const FPS_MEASURE_PERIOD_SEC: f64 = 5.; pub struct ScreencastThread { node_id: u32, @@ -87,6 +85,7 @@ struct FPSLimit { frame_last_time: Instant, fps_last_time: Instant, fps_frame_count: u64, + delay_til_next_frame_ns: u64, } impl FPSLimit { @@ -95,40 +94,46 @@ impl FPSLimit { frame_last_time: Instant::now(), fps_last_time: Instant::now(), fps_frame_count: 0, + delay_til_next_frame_ns: 0, } } - fn fps_limit_measure_start(&mut self) { + fn fps_limit_measure_start(&mut self, max_fps: u32) { + if max_fps <= 0 { + return; + } + self.frame_last_time = Instant::now(); } fn measure_fps(&mut self) { let now = Instant::now(); self.fps_frame_count += 1; - let elapsed_sec = (now - self.fps_last_time).as_secs(); + let elapsed_sec = (now - self.fps_last_time).as_secs_f64(); if elapsed_sec < FPS_MEASURE_PERIOD_SEC { return; } - let avg_frames_per_sec = self.fps_frame_count / elapsed_sec; - println!( - "fps_limit: average FPS in the last {} seconds: {}", - avg_frames_per_sec, elapsed_sec + let avg_frames_per_sec = self.fps_frame_count as f64 / elapsed_sec; + log::info!( + "fps_limit: average FPS in the last {:.2} seconds: {:.2}", + avg_frames_per_sec, + elapsed_sec ); self.fps_frame_count = 0; self.fps_last_time = now; } - fn wait_for_next_frame(&mut self, max_fps: u32) { + fn fps_limit_measure_end(&mut self, max_fps: u32) { + if max_fps <= 0 { + self.delay_til_next_frame_ns = 0; + return; + } self.measure_fps(); let elapsed_ns = self.frame_last_time.elapsed().as_nanos(); let target_ns = (TIMESPEC_NSEC_PER_SEC / max_fps) as u128; - if target_ns > elapsed_ns { - let delay_ns = (target_ns - elapsed_ns) as u64; - // println!("Time til next: {}ns", delay_ns); - std::thread::sleep(Duration::from_nanos(delay_ns)); - } + self.delay_til_next_frame_ns = target_ns.saturating_sub(elapsed_ns) as u64; } } @@ -359,12 +364,14 @@ impl StreamData { fn process(&mut self, stream: &StreamRef) { let buffer = unsafe { stream.dequeue_raw_buffer() }; if !buffer.is_null() { - // Only wait if it not the first frame - if self.seq > 0 { - // Maybe there's a better way, e.g., using an event loop to wait and get a new frame - self.fps_limit.wait_for_next_frame(self.framerate); + if self.fps_limit.delay_til_next_frame_ns != 0 { + // log::info!( + // "fps_limit: wait {}ns til next frame", + // self.fps_limit.delay_til_next_frame_ns + // ); + std::thread::sleep(Duration::from_nanos(self.fps_limit.delay_til_next_frame_ns)); } - + self.fps_limit.fps_limit_measure_start(self.framerate); let wl_buffer = unsafe { &*((*buffer).user_data as *const wl_buffer::WlBuffer) }; let full_damage = &[Rect { x: 0, @@ -403,7 +410,7 @@ impl StreamData { } } unsafe { stream.queue_raw_buffer(buffer) }; - self.fps_limit.fps_limit_measure_start(); + self.fps_limit.fps_limit_measure_end(self.framerate); self.seq += 1; } } @@ -429,14 +436,25 @@ fn start_stream( let (node_id_tx, node_id_rx) = oneshot::channel(); + // Gets framerate from screen's frame rate, and default to 0 if not available + let framerate = match &capture_source { + CaptureSource::Output(output) => wayland_helper + .output_info(output) + .and_then(|info| info.modes.into_iter().find(|mode| mode.current)) + .map_or(0, |mode| mode.refresh_rate as u32 / 1000), + CaptureSource::Toplevel(foreign_toplevel) => wayland_helper + .output_info_toplevel(foreign_toplevel) + .and_then(|info| info.modes.into_iter().find(|mode| mode.current)) + .map_or(0, |mode| mode.refresh_rate as u32 / 1000), + _ => 0, + }; + let (width, height) = match block_on(wayland_helper.capture_source_shm(capture_source.clone(), overlay_cursor)) { Some(frame) => (frame.width, frame.height), None => return Err(anyhow::anyhow!("failed to use shm capture to get size")), }; - let framerate = 60; // Default framerate. XXX is there a better way? - let dmabuf_helper = wayland_helper.dmabuf(); let stream = pipewire::stream::Stream::new( From 492768dc5c9b48d1e14fa750ef68bede16b8abf9 Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Sun, 23 Mar 2025 17:07:25 +0700 Subject: [PATCH 4/7] remove unused --- src/screencast_thread.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/screencast_thread.rs b/src/screencast_thread.rs index 611189d..ddbdc8a 100644 --- a/src/screencast_thread.rs +++ b/src/screencast_thread.rs @@ -148,7 +148,6 @@ struct StreamData { buffer_damage: HashMap>, // fps limit framerate: u32, - seq: u32, fps_limit: FPSLimit, fps_max: u32, } @@ -411,7 +410,6 @@ impl StreamData { } unsafe { stream.queue_raw_buffer(buffer) }; self.fps_limit.fps_limit_measure_end(self.framerate); - self.seq += 1; } } } @@ -495,8 +493,7 @@ fn start_stream( buffer_damage: HashMap::new(), framerate, fps_limit: FPSLimit::new(), - fps_max: 120, // XXX - seq: 0, + fps_max: 120, // XXX can read from config? }; let listener = stream From 5db0d7254e76b82c09f3fa1c89fcdc7de4f1881e Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Wed, 26 Mar 2025 11:07:05 +0700 Subject: [PATCH 5/7] throttle if the frame rate is too fast --- src/screencast_thread.rs | 54 +++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/screencast_thread.rs b/src/screencast_thread.rs index ddbdc8a..6dcb964 100644 --- a/src/screencast_thread.rs +++ b/src/screencast_thread.rs @@ -36,7 +36,7 @@ use wayland_client::{ }; const TIMESPEC_NSEC_PER_SEC: u32 = 1_000_000_000; -const FPS_MEASURE_PERIOD_SEC: f64 = 5.; +const FPS_MEASURE_PERIOD_SEC: u64 = 5; pub struct ScreencastThread { node_id: u32, @@ -84,7 +84,7 @@ impl ScreencastThread { struct FPSLimit { frame_last_time: Instant, fps_last_time: Instant, - fps_frame_count: u64, + fps_frame_count: u32, delay_til_next_frame_ns: u64, } @@ -110,16 +110,14 @@ impl FPSLimit { let now = Instant::now(); self.fps_frame_count += 1; let elapsed_sec = (now - self.fps_last_time).as_secs_f64(); - if elapsed_sec < FPS_MEASURE_PERIOD_SEC { + if elapsed_sec < 1. { return; } - - let avg_frames_per_sec = self.fps_frame_count as f64 / elapsed_sec; - log::info!( - "fps_limit: average FPS in the last {:.2} seconds: {:.2}", - avg_frames_per_sec, - elapsed_sec - ); + // log::info!( + // "fps_limit: average FPS in the last {:.2} seconds: {:.2}", + // elapsed_sec, + // self.fps_frame_count + // ); self.fps_frame_count = 0; self.fps_last_time = now; } @@ -131,9 +129,16 @@ impl FPSLimit { } self.measure_fps(); + // Throttling will not be applied if the current FPS is unlikely to exceed the target frame rate. + if self.fps_frame_count < max_fps - 1 { + self.delay_til_next_frame_ns = 0; + return; + } + let elapsed_ns = self.frame_last_time.elapsed().as_nanos(); let target_ns = (TIMESPEC_NSEC_PER_SEC / max_fps) as u128; - self.delay_til_next_frame_ns = target_ns.saturating_sub(elapsed_ns) as u64; + self.delay_til_next_frame_ns = + target_ns.saturating_sub(elapsed_ns + 3_000_000 /* safety margin */) as u64; } } @@ -226,7 +231,7 @@ impl StreamData { ); pwr_format.assume_init() }; - if pwr_format.max_framerate.num != 0 && pwr_format.max_framerate.denom != 0 { + if pwr_format.max_framerate.denom != 0 { let framerate = pwr_format.max_framerate.num / pwr_format.max_framerate.denom; self.framerate = if framerate > self.fps_max { self.fps_max @@ -370,6 +375,7 @@ impl StreamData { // ); std::thread::sleep(Duration::from_nanos(self.fps_limit.delay_til_next_frame_ns)); } + self.fps_limit.fps_limit_measure_start(self.framerate); let wl_buffer = unsafe { &*((*buffer).user_data as *const wl_buffer::WlBuffer) }; let full_damage = &[Rect { @@ -383,6 +389,7 @@ impl StreamData { .get(wl_buffer) .map(Vec::as_slice) .unwrap_or(full_damage); + match block_on(self.session.capture_wl_buffer(wl_buffer, damage)) { Ok(frame) => { self.buffer_damage @@ -434,18 +441,7 @@ fn start_stream( let (node_id_tx, node_id_rx) = oneshot::channel(); - // Gets framerate from screen's frame rate, and default to 0 if not available - let framerate = match &capture_source { - CaptureSource::Output(output) => wayland_helper - .output_info(output) - .and_then(|info| info.modes.into_iter().find(|mode| mode.current)) - .map_or(0, |mode| mode.refresh_rate as u32 / 1000), - CaptureSource::Toplevel(foreign_toplevel) => wayland_helper - .output_info_toplevel(foreign_toplevel) - .and_then(|info| info.modes.into_iter().find(|mode| mode.current)) - .map_or(0, |mode| mode.refresh_rate as u32 / 1000), - _ => 0, - }; + let framerate = 0; // default not limit the frame rate. let (width, height) = match block_on(wayland_helper.capture_source_shm(capture_source.clone(), overlay_cursor)) { @@ -669,7 +665,10 @@ fn format( flags: pod::PropertyFlags::empty(), value: pod::Value::Fraction(spa::utils::Fraction { num: 0, denom: 1 }), }, - pod::Property { + // TODO max framerate + ]; + if framerate > 0 { + properties.push(pod::Property { key: spa_sys::SPA_FORMAT_VIDEO_maxFramerate, flags: pod::PropertyFlags::empty(), value: pod::Value::Choice(pod::ChoiceValue::Fraction(spa::utils::Choice( @@ -686,9 +685,8 @@ fn format( }, }, ))), - }, - // TODO max framerate - ]; + }); + } if let Some(modifier) = fixated_modifier { properties.push(pod::Property { key: spa_sys::SPA_FORMAT_VIDEO_modifier, From b3bea103c3d46b05e6fa14a22866aba736bf3880 Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Thu, 27 Mar 2025 01:19:44 +0700 Subject: [PATCH 6/7] Balancing frame pacing and frame throttling Balancing Frame Pacing and Frame Throttling Implement flexible frame throttling by: 1. Waiting for half of the target frame duration (`delay_til_next_frame_ns`). 2. Calculating the elapsed time for a frame capture: - If we are ahead of the next frame capture time, wait for `delay_til_next_frame_ns`. - If we are behind the next frame capture time, accumulate the delay into `accumulated_frame_debt_ns`. 3. Using `accumulated_frame_debt_ns` to reduce waiting time in `delay_before_capture_frame_ns` and `delay_til_next_frame_ns`, helping to reach the target frame rate. --- src/screencast_thread.rs | 78 +++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/src/screencast_thread.rs b/src/screencast_thread.rs index 6dcb964..951f935 100644 --- a/src/screencast_thread.rs +++ b/src/screencast_thread.rs @@ -36,7 +36,7 @@ use wayland_client::{ }; const TIMESPEC_NSEC_PER_SEC: u32 = 1_000_000_000; -const FPS_MEASURE_PERIOD_SEC: u64 = 5; +const FPS_MEASURE_PERIOD_SEC: f64 = 5.; pub struct ScreencastThread { node_id: u32, @@ -85,7 +85,9 @@ struct FPSLimit { frame_last_time: Instant, fps_last_time: Instant, fps_frame_count: u32, + delay_before_capture_frame_ns: u64, delay_til_next_frame_ns: u64, + accumulated_frame_debt_ns: u64, } impl FPSLimit { @@ -94,7 +96,9 @@ impl FPSLimit { frame_last_time: Instant::now(), fps_last_time: Instant::now(), fps_frame_count: 0, + delay_before_capture_frame_ns: 0, delay_til_next_frame_ns: 0, + accumulated_frame_debt_ns: 0, } } @@ -110,35 +114,60 @@ impl FPSLimit { let now = Instant::now(); self.fps_frame_count += 1; let elapsed_sec = (now - self.fps_last_time).as_secs_f64(); - if elapsed_sec < 1. { + + if elapsed_sec < FPS_MEASURE_PERIOD_SEC { return; } - // log::info!( - // "fps_limit: average FPS in the last {:.2} seconds: {:.2}", - // elapsed_sec, - // self.fps_frame_count - // ); + let avg_frames_per_sec = self.fps_frame_count as f64 / elapsed_sec; + + log::info!( + "fps_limit: average FPS in the last {:.2} seconds: {:.2}", + elapsed_sec, + avg_frames_per_sec + ); self.fps_frame_count = 0; self.fps_last_time = now; } fn fps_limit_measure_end(&mut self, max_fps: u32) { if max_fps <= 0 { + self.delay_before_capture_frame_ns = 0; self.delay_til_next_frame_ns = 0; + self.accumulated_frame_debt_ns = 0; return; } self.measure_fps(); - // Throttling will not be applied if the current FPS is unlikely to exceed the target frame rate. - if self.fps_frame_count < max_fps - 1 { + let elapsed_ns = self.frame_last_time.elapsed().as_nanos(); + let target_ns = (TIMESPEC_NSEC_PER_SEC / max_fps) as u128; + + // Wait for half of the target frame rate duration before requesting a frame capture. + self.delay_before_capture_frame_ns = (target_ns / 2) as u64; + + // Throttle after the current frame has been captured: + let total_elapsed_ns = elapsed_ns + self.accumulated_frame_debt_ns as u128; + if target_ns > total_elapsed_ns { + // If it is before the next frame capture time -> wait for the right time. + self.delay_til_next_frame_ns = (target_ns - total_elapsed_ns) as u64; + } else { + // If it is after the next frame capture time, Set value of `delay_til_next_frame_ns` to 0 and increase value of `accumulated_frame_debt_ns` by the amount of time it has been delayed. self.delay_til_next_frame_ns = 0; - return; + self.accumulated_frame_debt_ns = target_ns.abs_diff(total_elapsed_ns) as u64; } - let elapsed_ns = self.frame_last_time.elapsed().as_nanos(); - let target_ns = (TIMESPEC_NSEC_PER_SEC / max_fps) as u128; - self.delay_til_next_frame_ns = - target_ns.saturating_sub(elapsed_ns + 3_000_000 /* safety margin */) as u64; + // Set `delay_before_capture_frame_ns` to its current value minus the overrun time, if any. + if self.accumulated_frame_debt_ns > self.delay_before_capture_frame_ns { + self.accumulated_frame_debt_ns -= self.delay_before_capture_frame_ns; + self.delay_before_capture_frame_ns = 0; + } else { + self.delay_before_capture_frame_ns -= self.accumulated_frame_debt_ns; + self.accumulated_frame_debt_ns = 0; + } + + // Reset at the end of each capture cycle, this helps prevent `accumulated_frame_debt_ns` from increasing indefinitely. + if self.fps_frame_count % max_fps == 0 { + self.accumulated_frame_debt_ns = 0; + } } } @@ -368,15 +397,16 @@ impl StreamData { fn process(&mut self, stream: &StreamRef) { let buffer = unsafe { stream.dequeue_raw_buffer() }; if !buffer.is_null() { - if self.fps_limit.delay_til_next_frame_ns != 0 { + self.fps_limit.fps_limit_measure_start(self.framerate); + if self.fps_limit.delay_before_capture_frame_ns != 0 { // log::info!( - // "fps_limit: wait {}ns til next frame", - // self.fps_limit.delay_til_next_frame_ns + // "fps_limit: wait {}ns before capture frame", + // self.fps_limit.delay_before_capture_frame_ns // ); - std::thread::sleep(Duration::from_nanos(self.fps_limit.delay_til_next_frame_ns)); + std::thread::sleep(Duration::from_nanos( + self.fps_limit.delay_before_capture_frame_ns, + )); } - - self.fps_limit.fps_limit_measure_start(self.framerate); let wl_buffer = unsafe { &*((*buffer).user_data as *const wl_buffer::WlBuffer) }; let full_damage = &[Rect { x: 0, @@ -417,6 +447,14 @@ impl StreamData { } unsafe { stream.queue_raw_buffer(buffer) }; self.fps_limit.fps_limit_measure_end(self.framerate); + + if self.fps_limit.delay_til_next_frame_ns != 0 { + // log::info!( + // "fps_limit: wait {}ns til next frame", + // self.fps_limit.delay_til_next_frame_ns + // ); + std::thread::sleep(Duration::from_nanos(self.fps_limit.delay_til_next_frame_ns)); + } } } } From 19358d394a2ea6a68bbe7609fbc251dd8142ef45 Mon Sep 17 00:00:00 2001 From: Vinh Nguyen Date: Thu, 27 Mar 2025 01:20:22 +0700 Subject: [PATCH 7/7] remove unused code --- src/wayland/mod.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 4c8561b..d326f7d 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -273,20 +273,6 @@ impl WaylandHelper { self.inner.toplevels.lock().unwrap().clone() } - pub fn output_info_toplevel( - &self, - foreign_toplevel: &ExtForeignToplevelHandleV1, - ) -> Option { - let output_toplevels = self.inner.output_toplevels.lock().unwrap(); - output_toplevels.iter().find_map(|(output, toplevel_list)| { - if toplevel_list.contains(foreign_toplevel) { - self.output_info(output) - } else { - None - } - }) - } - pub fn output_info(&self, output: &wl_output::WlOutput) -> Option { self.inner.output_infos.lock().unwrap().get(output).cloned() }