bevy_ratepace 0.17.0

`bevy_ratepace` is a crate to configure the update frequency of headless bevy.
Documentation
#![doc = include_str!("../README.MD")]

// Ported for headless and simplified from https://github.com/aevyrie/bevy_framepace

use bevy::prelude::*;

use std::{
    sync::{Arc, Mutex},
    time::Duration,
};

/// Adds framepacing and framelimiting functionality to your [`App`].
#[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);
    }
}

/// Framepacing plugin configuration.
#[derive(Debug, Clone, Resource, Reflect)]
#[reflect(Resource)]
pub struct RatepaceSettings {
    /// Configures the framerate limiting strategy.
    pub limiter: Limiter,
}
impl RatepaceSettings {
    fn is_enabled(&self) -> bool {
        self.limiter.is_enabled()
    }
    /// Builds plugin settings with the specified [`Limiter`] configuration.
    pub fn with_limiter(mut self, limiter: Limiter) -> Self {
        self.limiter = limiter;
        self
    }
}
impl Default for RatepaceSettings {
    fn default() -> RatepaceSettings {
        RatepaceSettings {
            limiter: Limiter::Off,
        }
    }
}

/// Configures the framelimiting technique for the app.
#[derive(Debug, Default, Clone, Reflect)]
pub enum Limiter {
    /// Set a fixed manual frametime limit. This should be greater than the monitors frametime
    /// (`1.0 / monitor frequency`).
    Manual(Duration),
    /// Disables frame limiting
    #[default]
    Off,
}

impl Limiter {
    /// Returns `true` if the [`Limiter`] is enabled.
    pub fn is_enabled(&self) -> bool {
        !matches!(self, Limiter::Off)
    }

    /// Constructs a new [`Limiter`] from the provided `framerate`.
    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"),
        }
    }
}

/// Tracks the instant of the end of the previous frame.
#[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(),
        }
    }
}

/// Holds frame time measurements for framepacing diagnostics
#[derive(Clone, Debug, Default, Resource)]
pub struct RatepaceStats {
    frametime: Arc<Mutex<Duration>>,
    oversleep: Arc<Mutex<Duration>>,
}

/// Accurately sleeps until it's time to start the next frame.
///
/// The `spin_sleep` dependency makes it possible to get extremely accurate sleep times across
/// platforms. Using `std::thread::sleep()` will not be precise enough, especially windows. Using a
/// spin lock, even with `std::hint::spin_loop()`, will result in significant power usage.
///
/// `spin_sleep` sleeps as long as possible given the platform's sleep accuracy, and spins for the
/// remainder. The dependency is however not WASM compatible, which is fine, because frame limiting
/// should not be used in a browser; this would compete with the browser's frame limiter.
#[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);
    }
}