#![deny(missing_docs)]
#[cfg(not(target_arch = "wasm32"))]
use bevy::winit::WinitWindows;
use bevy::{
prelude::*,
render::{pipelined_rendering::RenderExtractApp, RenderApp, RenderSet},
utils::Instant,
};
use std::{
sync::{Arc, Mutex},
time::Duration,
};
#[cfg(feature = "framepace_debug")]
pub mod debug;
#[derive(Debug, Clone, Component)]
pub struct FramepacePlugin;
impl Plugin for FramepacePlugin {
fn build(&self, app: &mut App) {
app.register_type::<FramepaceSettings>();
let limit = FrametimeLimit::default();
let settings = FramepaceSettings::default();
let settings_proxy = FramepaceSettingsProxy::default();
let stats = FramePaceStats::default();
app.insert_resource(settings)
.insert_resource(settings_proxy.clone())
.insert_resource(limit.clone())
.insert_resource(stats.clone())
.add_systems(Update, update_proxy_resources);
#[cfg(not(target_arch = "wasm32"))]
app.add_systems(Update, get_display_refresh_rate);
if let Ok(sub_app) = app.get_sub_app_mut(RenderExtractApp) {
sub_app
.insert_resource(FrameTimer::default())
.insert_resource(settings_proxy)
.insert_resource(limit)
.insert_resource(stats)
.add_systems(Main, framerate_limiter);
} else {
app.sub_app_mut(RenderApp)
.insert_resource(FrameTimer::default())
.insert_resource(settings_proxy)
.insert_resource(limit)
.insert_resource(stats)
.add_systems(
bevy::render::Render,
framerate_limiter
.in_set(RenderSet::Cleanup)
.after(World::clear_entities),
);
}
}
}
#[derive(Debug, Clone, Resource, Reflect)]
#[reflect(Resource)]
pub struct FramepaceSettings {
pub limiter: Limiter,
}
impl FramepaceSettings {
pub fn with_limiter(mut self, limiter: Limiter) -> Self {
self.limiter = limiter;
self
}
}
impl Default for FramepaceSettings {
fn default() -> FramepaceSettings {
FramepaceSettings {
limiter: Limiter::Auto,
}
}
}
#[derive(Default, Debug, Clone, Resource)]
struct FramepaceSettingsProxy {
limiter: Arc<Mutex<Limiter>>,
}
impl FramepaceSettingsProxy {
fn is_enabled(&self) -> bool {
self.limiter.try_lock().iter().any(|l| l.is_enabled())
}
}
fn update_proxy_resources(settings: Res<FramepaceSettings>, proxy: Res<FramepaceSettingsProxy>) {
if settings.is_changed() {
if let Ok(mut limiter) = proxy.limiter.try_lock() {
*limiter = settings.limiter.clone();
}
}
}
#[derive(Debug, Default, Clone, Reflect)]
pub enum Limiter {
#[default]
Auto,
Manual(Duration),
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 {
let output = match self {
Limiter::Auto => "Auto".into(),
Limiter::Manual(t) => format!("{:.2} fps", 1.0 / t.as_secs_f32()),
Limiter::Off => "Off".into(),
};
write!(f, "{}", output)
}
}
#[derive(Debug, Default, Clone, Resource)]
struct FrametimeLimit(Arc<Mutex<Duration>>);
#[derive(Debug, Clone, Resource, Reflect)]
pub struct FrameTimer {
sleep_end: Instant,
}
impl Default for FrameTimer {
fn default() -> Self {
FrameTimer {
sleep_end: Instant::now(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn get_display_refresh_rate(
settings: Res<FramepaceSettings>,
winit: NonSend<WinitWindows>,
windows: Query<Entity, With<Window>>,
frame_limit: Res<FrametimeLimit>,
) {
if !(settings.is_changed() || winit.is_changed()) {
return;
}
let new_frametime = match settings.limiter {
Limiter::Auto => match detect_frametime(winit, windows.iter()) {
Some(frametime) => frametime,
None => return,
},
Limiter::Manual(frametime) => frametime,
Limiter::Off => {
#[cfg(feature = "framepace_debug")]
info!("Frame limiter disabled");
return;
}
};
if let Ok(mut limit) = frame_limit.0.try_lock() {
if new_frametime != *limit {
#[cfg(feature = "framepace_debug")]
info!("Frametime limit changed to: {:?}", new_frametime);
*limit = new_frametime;
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn detect_frametime(
winit: NonSend<WinitWindows>,
windows: impl Iterator<Item = Entity>,
) -> Option<Duration> {
let best_framerate = {
let mhz = windows
.filter_map(|e| winit.get_window(e))
.filter_map(|w| w.current_monitor())
.map(|monitor| bevy::winit::get_best_videomode(&monitor).refresh_rate_millihertz())
.min()? as f64;
dbg!(mhz) / 1000.0
};
let best_frametime = Duration::from_secs_f64(1.0 / best_framerate);
Some(best_frametime)
}
#[derive(Clone, Debug, Default, Resource)]
pub struct FramePaceStats {
frametime: Arc<Mutex<Duration>>,
oversleep: Arc<Mutex<Duration>>,
}
fn framerate_limiter(
mut timer: ResMut<FrameTimer>,
target_frametime: Res<FrametimeLimit>,
stats: Res<FramePaceStats>,
settings: Res<FramepaceSettingsProxy>,
) {
if let Ok(limit) = target_frametime.0.try_lock() {
#[cfg(not(target_arch = "wasm32"))]
{
let oversleep = stats
.oversleep
.try_lock()
.as_deref()
.cloned()
.unwrap_or_default();
let sleep_time = limit.saturating_sub(timer.sleep_end.elapsed() + oversleep);
if settings.is_enabled() {
spin_sleep::sleep(sleep_time);
}
}
let frame_time_actual = timer.sleep_end.elapsed();
timer.sleep_end = Instant::now();
if let Ok(mut frametime) = stats.frametime.try_lock() {
*frametime = frame_time_actual;
}
if let Ok(mut oversleep) = stats.oversleep.try_lock() {
*oversleep = frame_time_actual.saturating_sub(*limit);
}
};
}