use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct FrameRateConfig {
pub target_fps: u32,
pub adaptive: bool,
pub min_fps: u32,
pub max_fps: u32,
pub collect_stats: bool,
}
impl Default for FrameRateConfig {
fn default() -> Self {
Self {
target_fps: 60,
adaptive: false,
min_fps: 10,
max_fps: 120,
collect_stats: false,
}
}
}
impl FrameRateConfig {
pub fn new(target_fps: u32) -> Self {
Self {
target_fps: target_fps.clamp(1, 120),
..Default::default()
}
}
pub fn adaptive(mut self, min_fps: u32, max_fps: u32) -> Self {
self.adaptive = true;
self.min_fps = min_fps.clamp(1, 120);
self.max_fps = max_fps.clamp(self.min_fps, 120);
self
}
pub fn with_stats(mut self) -> Self {
self.collect_stats = true;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct FrameRateStats {
pub current_fps: f64,
pub avg_frame_time_ms: f64,
pub dropped_frames: u64,
pub total_frames: u64,
pub min_frame_time_ms: f64,
pub max_frame_time_ms: f64,
}
#[derive(Debug, Default)]
pub struct SharedFrameRateStats {
current_fps: AtomicU64,
avg_frame_time_ms: AtomicU64,
dropped_frames: AtomicU64,
total_frames: AtomicU64,
min_frame_time_ms: AtomicU64,
max_frame_time_ms: AtomicU64,
}
impl SharedFrameRateStats {
pub fn new() -> Arc<Self> {
Arc::new(Self {
current_fps: AtomicU64::new(0),
avg_frame_time_ms: AtomicU64::new(0),
dropped_frames: AtomicU64::new(0),
total_frames: AtomicU64::new(0),
min_frame_time_ms: AtomicU64::new(f64::MAX.to_bits()),
max_frame_time_ms: AtomicU64::new(0),
})
}
pub fn snapshot(&self) -> FrameRateStats {
FrameRateStats {
current_fps: f64::from_bits(self.current_fps.load(Ordering::Relaxed)),
avg_frame_time_ms: f64::from_bits(self.avg_frame_time_ms.load(Ordering::Relaxed)),
dropped_frames: self.dropped_frames.load(Ordering::Relaxed),
total_frames: self.total_frames.load(Ordering::Relaxed),
min_frame_time_ms: f64::from_bits(self.min_frame_time_ms.load(Ordering::Relaxed)),
max_frame_time_ms: f64::from_bits(self.max_frame_time_ms.load(Ordering::Relaxed)),
}
}
fn update(&self, stats: &FrameRateStats) {
self.current_fps
.store(stats.current_fps.to_bits(), Ordering::Relaxed);
self.avg_frame_time_ms
.store(stats.avg_frame_time_ms.to_bits(), Ordering::Relaxed);
self.dropped_frames
.store(stats.dropped_frames, Ordering::Relaxed);
self.total_frames
.store(stats.total_frames, Ordering::Relaxed);
self.min_frame_time_ms
.store(stats.min_frame_time_ms.to_bits(), Ordering::Relaxed);
self.max_frame_time_ms
.store(stats.max_frame_time_ms.to_bits(), Ordering::Relaxed);
}
}
pub struct FrameRateController {
config: FrameRateConfig,
last_frame: Instant,
frame_times: VecDeque<Duration>,
current_target_fps: u32,
stats: FrameRateStats,
shared_stats: Option<Arc<SharedFrameRateStats>>,
}
impl FrameRateController {
pub fn new(config: FrameRateConfig) -> Self {
let current_target_fps = config.target_fps;
let shared_stats = if config.collect_stats {
Some(SharedFrameRateStats::new())
} else {
None
};
Self {
config,
last_frame: Instant::now(),
frame_times: VecDeque::with_capacity(60),
current_target_fps,
stats: FrameRateStats::default(),
shared_stats,
}
}
pub fn shared_stats(&self) -> Option<Arc<SharedFrameRateStats>> {
self.shared_stats.clone()
}
pub fn frame_duration(&self) -> Duration {
Duration::from_millis(1000 / self.current_target_fps as u64)
}
pub fn current_fps(&self) -> u32 {
self.current_target_fps
}
pub fn should_render(&self) -> bool {
self.last_frame.elapsed() >= self.frame_duration()
}
pub fn record_frame(&mut self, render_time: Duration) {
let now = Instant::now();
let frame_time = now.duration_since(self.last_frame);
self.last_frame = now;
self.frame_times.push_back(frame_time);
if self.frame_times.len() > 60 {
self.frame_times.pop_front();
}
self.stats.total_frames += 1;
let frame_time_ms = frame_time.as_secs_f64() * 1000.0;
let target_frame_time_ms = 1000.0 / self.current_target_fps as f64;
if frame_time_ms > target_frame_time_ms * 1.5 {
self.stats.dropped_frames += 1;
}
if frame_time_ms < self.stats.min_frame_time_ms || self.stats.total_frames == 1 {
self.stats.min_frame_time_ms = frame_time_ms;
}
if frame_time_ms > self.stats.max_frame_time_ms {
self.stats.max_frame_time_ms = frame_time_ms;
}
if !self.frame_times.is_empty() {
let total: Duration = self.frame_times.iter().sum();
let avg = total / self.frame_times.len() as u32;
self.stats.avg_frame_time_ms = avg.as_secs_f64() * 1000.0;
self.stats.current_fps = 1000.0 / self.stats.avg_frame_time_ms;
}
if self.config.adaptive {
self.adjust_frame_rate(render_time);
}
if let Some(ref shared) = self.shared_stats {
shared.update(&self.stats);
}
}
fn adjust_frame_rate(&mut self, render_time: Duration) {
let render_time_ms = render_time.as_secs_f64() * 1000.0;
let target_frame_time_ms = 1000.0 / self.current_target_fps as f64;
if render_time_ms > target_frame_time_ms * 0.8 {
let new_fps = (self.current_target_fps as f64 * 0.9) as u32;
self.current_target_fps = new_fps.clamp(self.config.min_fps, self.config.max_fps);
}
else if render_time_ms < target_frame_time_ms * 0.5
&& self.current_target_fps < self.config.target_fps
{
let new_fps = (self.current_target_fps as f64 * 1.1) as u32;
self.current_target_fps = new_fps.clamp(self.config.min_fps, self.config.max_fps);
}
}
pub fn stats(&self) -> &FrameRateStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = FrameRateStats::default();
self.frame_times.clear();
if let Some(ref shared) = self.shared_stats {
shared.update(&self.stats);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_rate_config_default() {
let config = FrameRateConfig::default();
assert_eq!(config.target_fps, 60);
assert!(!config.adaptive);
assert!(!config.collect_stats);
}
#[test]
fn test_frame_rate_config_new() {
let config = FrameRateConfig::new(30);
assert_eq!(config.target_fps, 30);
}
#[test]
fn test_frame_rate_config_clamp() {
let config = FrameRateConfig::new(200);
assert_eq!(config.target_fps, 120);
let config = FrameRateConfig::new(0);
assert_eq!(config.target_fps, 1);
}
#[test]
fn test_frame_rate_config_adaptive() {
let config = FrameRateConfig::new(60).adaptive(15, 90);
assert!(config.adaptive);
assert_eq!(config.min_fps, 15);
assert_eq!(config.max_fps, 90);
}
#[test]
fn test_frame_rate_config_with_stats() {
let config = FrameRateConfig::new(60).with_stats();
assert!(config.collect_stats);
}
#[test]
fn test_frame_rate_controller_creation() {
let controller = FrameRateController::new(FrameRateConfig::default());
assert_eq!(controller.current_fps(), 60);
assert!(controller.shared_stats().is_none());
}
#[test]
fn test_frame_rate_controller_with_stats() {
let config = FrameRateConfig::new(60).with_stats();
let controller = FrameRateController::new(config);
assert!(controller.shared_stats().is_some());
}
#[test]
fn test_frame_duration() {
let controller = FrameRateController::new(FrameRateConfig::new(60));
assert_eq!(controller.frame_duration(), Duration::from_millis(16));
let controller = FrameRateController::new(FrameRateConfig::new(30));
assert_eq!(controller.frame_duration(), Duration::from_millis(33));
}
#[test]
fn test_record_frame() {
let config = FrameRateConfig::new(60).with_stats();
let mut controller = FrameRateController::new(config);
for _ in 0..5 {
std::thread::sleep(Duration::from_millis(10));
controller.record_frame(Duration::from_millis(5));
}
assert_eq!(controller.stats().total_frames, 5);
assert!(controller.stats().current_fps > 0.0);
}
#[test]
fn test_shared_stats_snapshot() {
let config = FrameRateConfig::new(60).with_stats();
let mut controller = FrameRateController::new(config);
let shared = controller.shared_stats().unwrap();
controller.record_frame(Duration::from_millis(5));
let snapshot = shared.snapshot();
assert_eq!(snapshot.total_frames, 1);
}
#[test]
fn test_reset_stats() {
let config = FrameRateConfig::new(60).with_stats();
let mut controller = FrameRateController::new(config);
controller.record_frame(Duration::from_millis(5));
assert_eq!(controller.stats().total_frames, 1);
controller.reset_stats();
assert_eq!(controller.stats().total_frames, 0);
}
}