//! Lightweight frame-time diagnostics. //! //! [`FrameTimeDiagnostics`] is a Bevy resource that maintains a rolling window //! of the last [`WINDOW_SIZE`] frame durations. Any system can read it to make //! performance-aware decisions — for example, disabling settle-bounce animations //! when the game is running below 30 FPS on a low-end device. //! //! # Reading diagnostics //! //! ```ignore //! fn my_system(diag: Res) { //! if diag.is_low_performance() { //! // Skip expensive visual effects. //! return; //! } //! println!("avg FPS: {:.1}", diag.fps()); //! } //! ``` //! //! # Update //! //! [`update_frame_time_diagnostics`] runs every frame via [`CardAnimationPlugin`] //! (or whichever plugin registers it). The window is circular so only the last //! `WINDOW_SIZE` frames influence the statistics. use bevy::prelude::*; /// Number of frames kept in the rolling statistics window. pub const WINDOW_SIZE: usize = 60; /// Rolling frame-time statistics over the last [`WINDOW_SIZE`] frames. /// /// All times are in seconds. Statistics are updated every frame by /// [`update_frame_time_diagnostics`]. #[derive(Resource, Debug)] pub struct FrameTimeDiagnostics { samples: [f32; WINDOW_SIZE], head: usize, count: usize, /// Smoothed average frame duration over the window (seconds). pub avg_secs: f32, /// Worst-case (slowest) frame duration in the window (seconds). pub max_secs: f32, /// Best-case (fastest) frame duration in the window (seconds). pub min_secs: f32, } impl Default for FrameTimeDiagnostics { fn default() -> Self { Self { samples: [0.0; WINDOW_SIZE], head: 0, count: 0, avg_secs: 0.0, max_secs: 0.0, min_secs: 0.0, } } } impl FrameTimeDiagnostics { /// Estimated frames per second based on the rolling average. /// /// Returns `0.0` until at least one frame has been recorded. pub fn fps(&self) -> f32 { if self.avg_secs > 0.0 { 1.0 / self.avg_secs } else { 0.0 } } /// Returns `true` when the rolling-average FPS is above `target`. /// /// Always returns `false` until the window is fully populated. pub fn is_above_target(&self, target_fps: f32) -> bool { self.count >= WINDOW_SIZE && self.fps() > target_fps } /// Returns `true` when the device appears to be running below 30 FPS. /// /// Only asserted after the window is fully populated so a single slow /// startup frame does not permanently suppress visual effects. pub fn is_low_performance(&self) -> bool { self.count >= WINDOW_SIZE && self.fps() < 30.0 } /// Appends `dt` to the ring buffer and recomputes statistics. /// /// O(WINDOW_SIZE) — acceptable because WINDOW_SIZE is small and constant. fn push(&mut self, dt: f32) { self.samples[self.head] = dt; self.head = (self.head + 1) % WINDOW_SIZE; if self.count < WINDOW_SIZE { self.count += 1; } let n = self.count; let mut sum = 0.0_f32; let mut max_val = 0.0_f32; let mut min_val = f32::MAX; for &s in &self.samples[..n] { sum += s; if s > max_val { max_val = s; } if s < min_val { min_val = s; } } self.avg_secs = sum / n as f32; self.max_secs = max_val; self.min_secs = if min_val == f32::MAX { 0.0 } else { min_val }; } } // --------------------------------------------------------------------------- // System // --------------------------------------------------------------------------- /// Records the current frame's delta time in [`FrameTimeDiagnostics`]. /// /// Registered by [`CardAnimationPlugin`]. Runs every frame in `Update`. pub(crate) fn update_frame_time_diagnostics( time: Res