use std::thread; use std::time::{Duration, Instant}; #[derive(Clone, Debug)] pub struct Config { pub target_frame_hz: Option, 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, 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) } } }