use concinnity_core::ecs::FrameRateCap;
use concinnity_core::ecs::World;
use std::time::{Duration, Instant};
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)
}
#[derive(Debug, Default)]
pub(crate) struct FramePacer {
deadline: Option<Instant>,
last_cap: u32,
}
impl FramePacer {
pub(crate) fn pace(&mut self, world: &World) {
let cap = world.resource::<FrameRateCap>().map(|c| c.0).unwrap_or(0);
if cap != self.last_cap {
self.deadline = None;
self.last_cap = cap;
}
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 an_open_menu_follows_the_published_cap() {
let mut world = World::new();
world.insert_resource(concinnity_core::ecs::MenuActive(true));
world.insert_resource(FrameRateCap(0));
let mut pacer = FramePacer::default();
pacer.pace(&world);
assert!(pacer.deadline.is_none(), "an uncapped menu is not paced");
world.insert_resource(FrameRateCap(1000));
pacer.pace(&world);
assert_eq!(pacer.last_cap, 1000, "the menu paces at the cap itself");
}
#[test]
fn cap_change_rebases_the_deadline() {
let mut world = World::new();
world.insert_resource(FrameRateCap(1000));
let mut pacer = FramePacer::default();
pacer.pace(&world);
assert!(
pacer.deadline.is_some(),
"a positive cap schedules a deadline"
);
world.insert_resource(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());
}
}