use crate::ecs::World;
use std::time::{Duration, Instant};
const MENU_FPS_CAP: u32 = 60;
fn pace_deadline(now: Instant, prev: Option<Instant>, target: Duration) -> (Instant, Instant) {
let wait_until = prev.filter(|d| *d > now).unwrap_or(now);
(wait_until, wait_until + target)
}
fn effective_cap(user_cap: u32, menu_active: bool) -> u32 {
if !menu_active {
return user_cap;
}
match user_cap {
0 => MENU_FPS_CAP,
cap => cap.min(MENU_FPS_CAP),
}
}
#[derive(Debug, Default)]
pub(crate) struct FramePacer {
deadline: Option<Instant>,
last_cap: u32,
}
impl FramePacer {
pub(crate) fn pace(&mut self, world: &World) {
let user_cap = world
.resource::<crate::ecs::FrameRateCap>()
.map(|c| c.0)
.unwrap_or(0);
if user_cap != self.last_cap {
self.deadline = None;
self.last_cap = user_cap;
}
let menu_active = world
.resource::<crate::ecs::MenuActive>()
.map(|m| m.0)
.unwrap_or(false);
let cap = effective_cap(user_cap, menu_active);
if cap == 0 {
self.deadline = None;
return;
}
let target = Duration::from_secs_f64(1.0 / cap as f64);
let (wait_until, next) = pace_deadline(Instant::now(), self.deadline, target);
while let Some(remaining) = wait_until.checked_duration_since(Instant::now()) {
if remaining > Duration::from_millis(1) {
std::thread::sleep(remaining - Duration::from_millis(1));
} else {
std::hint::spin_loop();
}
}
self.deadline = Some(next);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pace_deadline_first_frame_does_not_wait() {
let now = Instant::now();
let target = Duration::from_millis(16);
let (wait_until, next) = pace_deadline(now, None, target);
assert_eq!(wait_until, now);
assert_eq!(next, now + target);
}
#[test]
fn pace_deadline_accumulates_exactly_when_ahead() {
let now = Instant::now();
let target = Duration::from_millis(16);
let prev = now + Duration::from_millis(10);
let (wait_until, next) = pace_deadline(now, Some(prev), target);
assert_eq!(wait_until, prev);
assert_eq!(next, prev + target);
}
#[test]
fn pace_deadline_resets_after_overrun_without_burst() {
let now = Instant::now();
let target = Duration::from_millis(16);
let past = now - Duration::from_millis(5);
let (wait_until, next) = pace_deadline(now, Some(past), target);
assert_eq!(wait_until, now);
assert_eq!(next, now + target);
}
#[test]
fn menu_clamps_the_cap_down_only() {
assert_eq!(effective_cap(0, false), 0);
assert_eq!(effective_cap(144, false), 144);
assert_eq!(effective_cap(0, true), MENU_FPS_CAP);
assert_eq!(effective_cap(144, true), MENU_FPS_CAP);
assert_eq!(effective_cap(30, true), 30);
}
#[test]
fn cap_change_rebases_the_deadline() {
let mut world = World::new();
world.insert_resource(crate::ecs::FrameRateCap(1000));
let mut pacer = FramePacer::default();
pacer.pace(&world);
assert!(
pacer.deadline.is_some(),
"a positive cap schedules a deadline"
);
world.insert_resource(crate::ecs::FrameRateCap(0));
pacer.pace(&world);
assert!(pacer.deadline.is_none(), "cap 0 clears the schedule");
}
#[test]
fn unpublished_cap_is_unlimited() {
let world = World::new();
let mut pacer = FramePacer::default();
pacer.pace(&world);
assert!(pacer.deadline.is_none());
}
}