- Implement a new timing module - Utilize the new timing module in glium platform implementation for frame limiting and fixed engine updates timing.
144 lines
4.6 KiB
Rust
144 lines
4.6 KiB
Rust
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Config {
|
|
pub target_frame_hz: Option<f64>,
|
|
pub target_update_hz: f64,
|
|
pub max_updates_per_frame: u32,
|
|
pub max_accumulated_steps: u32,
|
|
pub sleep_tolerance: Duration,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
target_frame_hz: Some(144.0),
|
|
target_update_hz: 60.0,
|
|
max_updates_per_frame: 5,
|
|
max_accumulated_steps: 8,
|
|
sleep_tolerance: Duration::from_micros(500),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Time {
|
|
cfg: Config,
|
|
last_instant: Instant,
|
|
next_frame_due: Instant,
|
|
frame_interval: Option<Duration>,
|
|
fixed_dt: Duration,
|
|
|
|
// tracking
|
|
frame_dt: Duration,
|
|
accumulator: Duration,
|
|
|
|
// counters
|
|
pub frame_count: u64,
|
|
pub update_count: u64,
|
|
}
|
|
|
|
pub struct TickPlan {
|
|
/// How many fixed updates to run this frame
|
|
pub updates: u32,
|
|
/// Interpolation factor for rendering between previous/next sim states
|
|
pub alpha: f32,
|
|
/// Measured last frame delta (seconds)
|
|
pub frame_dt: f32,
|
|
/// Fixed timestep (seconds)
|
|
pub fixed_dt: f32,
|
|
}
|
|
|
|
impl Time {
|
|
pub fn new(cfg: Config) -> Self {
|
|
let now = Instant::now();
|
|
let frame_interval = cfg.target_frame_hz.map(|hz| Duration::from_secs_f64(1.0 / hz));
|
|
let fixed_dt = Duration::from_secs_f64(1.0 / cfg.target_update_hz);
|
|
Self {
|
|
cfg,
|
|
last_instant: now,
|
|
next_frame_due: now,
|
|
frame_interval,
|
|
fixed_dt,
|
|
frame_dt: Duration::ZERO,
|
|
accumulator: Duration::ZERO,
|
|
frame_count: 0,
|
|
update_count: 0,
|
|
}
|
|
}
|
|
|
|
pub fn reconfigure(&mut self, cfg: Config) {
|
|
self.cfg = cfg.clone();
|
|
self.frame_interval = cfg.target_frame_hz.map(|hz| Duration::from_secs_f64(1.0 / hz));
|
|
self.fixed_dt = Duration::from_secs_f64(1.0 / cfg.target_update_hz);
|
|
}
|
|
|
|
pub fn begin_frame_blocking(&mut self) -> TickPlan {
|
|
// 1) If there's a frame cap, block until next frame deadline
|
|
if let Some(interval) = self.frame_interval {
|
|
let mut now = Instant::now();
|
|
if now < self.next_frame_due {
|
|
// Sleep most of the remainder, then spin the last tiny bit for precision
|
|
let total_remaining = self.next_frame_due - now;
|
|
if total_remaining > self.cfg.sleep_tolerance {
|
|
let sleep_for = total_remaining - self.cfg.sleep_tolerance;
|
|
thread::sleep(sleep_for);
|
|
}
|
|
// Short spin-wait for precision
|
|
while Instant::now() < self.next_frame_due {
|
|
std::hint::spin_loop();
|
|
}
|
|
now = self.next_frame_due;
|
|
}
|
|
self.next_frame_due = self.next_frame_due + interval;
|
|
// In case we fell far behind (e.g., debugger pause), resync.
|
|
if self.next_frame_due < now {
|
|
self.next_frame_due = now + interval;
|
|
}
|
|
}
|
|
|
|
// 2) Measure frame dt
|
|
let now = Instant::now();
|
|
self.frame_dt = now.saturating_duration_since(self.last_instant);
|
|
self.last_instant = now;
|
|
self.frame_count += 1;
|
|
|
|
// 3) Accumulate for fixed updates
|
|
self.accumulator += self.frame_dt;
|
|
|
|
// Clamp accumulator to avoid doing a huge number of updates after a stall
|
|
let max_accumulated = self.fixed_dt * self.cfg.max_accumulated_steps;
|
|
if self.accumulator > max_accumulated {
|
|
self.accumulator = max_accumulated;
|
|
}
|
|
|
|
// 4) Determine how many updates to run this frame
|
|
let mut updates = 0u32;
|
|
while self.accumulator >= self.fixed_dt && updates < self.cfg.max_updates_per_frame {
|
|
self.accumulator -= self.fixed_dt;
|
|
updates += 1;
|
|
self.update_count += 1;
|
|
}
|
|
|
|
// 5) Compute interpolation factor for rendering (0..1)
|
|
let alpha = if self.fixed_dt.is_zero() {
|
|
1.0
|
|
} else {
|
|
(self.accumulator.as_secs_f32() / self.fixed_dt.as_secs_f32()).clamp(0.0, 1.0)
|
|
};
|
|
|
|
TickPlan {
|
|
updates,
|
|
alpha,
|
|
frame_dt: self.frame_dt.as_secs_f32(),
|
|
fixed_dt: self.fixed_dt.as_secs_f32(),
|
|
}
|
|
}
|
|
|
|
pub fn frame_dt_seconds(&self) -> f32 { self.frame_dt.as_secs_f32() }
|
|
pub fn fixed_dt_seconds(&self) -> f32 { self.fixed_dt.as_secs_f32() }
|
|
pub fn alpha(&self) -> f32 {
|
|
if self.fixed_dt.is_zero() { 1.0 } else { (self.accumulator.as_secs_f32() / self.fixed_dt.as_secs_f32()).clamp(0.0, 1.0) }
|
|
}
|
|
}
|