#![doc = include_str!("../README.MD")]
use bevy::prelude::*;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
#[derive(Debug, Clone, Component)]
pub struct RatepacePlugin;
impl Plugin for RatepacePlugin {
fn build(&self, app: &mut App) {
app.register_type::<RatepaceSettings>();
let settings = RatepaceSettings::default();
let stats = RatepaceStats::default();
app.insert_resource(settings)
.insert_resource(FrameTimer::default())
.insert_resource(stats)
.add_systems(Main, framerate_limiter);
}
}
#[derive(Debug, Clone, Resource, Reflect)]
#[reflect(Resource)]
pub struct RatepaceSettings {
pub limiter: Limiter,
}
impl RatepaceSettings {
fn is_enabled(&self) -> bool {
self.limiter.is_enabled()
}
pub fn with_limiter(mut self, limiter: Limiter) -> Self {
self.limiter = limiter;
self
}
}
impl Default for RatepaceSettings {
fn default() -> RatepaceSettings {
RatepaceSettings {
limiter: Limiter::Off,
}
}
}
#[derive(Debug, Default, Clone, Reflect)]
pub enum Limiter {
Manual(Duration),
#[default]
Off,
}
impl Limiter {
pub fn is_enabled(&self) -> bool {
!matches!(self, Limiter::Off)
}
pub fn from_framerate(framerate: f64) -> Self {
Limiter::Manual(Duration::from_secs_f64(1.0 / framerate))
}
}
impl std::fmt::Display for Limiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Limiter::Manual(t) => write!(f, "{:.2} fps", 1.0 / t.as_secs_f32()),
Limiter::Off => write!(f, "Off"),
}
}
}
#[derive(Debug, Clone, Resource, Reflect)]
pub struct FrameTimer {
sleep_end: std::time::Instant,
}
impl Default for FrameTimer {
fn default() -> Self {
FrameTimer {
sleep_end: std::time::Instant::now(),
}
}
}
#[derive(Clone, Debug, Default, Resource)]
pub struct RatepaceStats {
frametime: Arc<Mutex<Duration>>,
oversleep: Arc<Mutex<Duration>>,
}
#[allow(unused_variables)]
fn framerate_limiter(
mut timer: ResMut<FrameTimer>,
stats: Res<RatepaceStats>,
settings: Res<RatepaceSettings>,
) {
let limit = match settings.limiter {
Limiter::Manual(frametime) => frametime,
Limiter::Off => Duration::new(0, 0),
};
let frame_time = timer.sleep_end.elapsed();
let oversleep = stats
.oversleep
.try_lock()
.as_deref()
.cloned()
.unwrap_or_default();
let sleep_time = limit.saturating_sub(frame_time + oversleep);
if settings.is_enabled() {
spin_sleep::sleep(sleep_time);
}
let frame_time_total = timer.sleep_end.elapsed();
timer.sleep_end = std::time::Instant::now();
if let Ok(mut frametime) = stats.frametime.try_lock() {
*frametime = frame_time;
}
if let Ok(mut oversleep) = stats.oversleep.try_lock() {
*oversleep = frame_time_total.saturating_sub(limit);
}
}