From 84ab3a26b16bad457d787cc57385cb95a78b5219 Mon Sep 17 00:00:00 2001 From: reo Date: Wed, 24 Sep 2025 23:20:51 +0300 Subject: [PATCH] Timing Module Update - Implement a new timing module - Utilize the new timing module in glium platform implementation for frame limiting and fixed engine updates timing. --- core/src/engine.rs | 3 +- core/src/lib.rs | 2 + core/src/time.rs | 144 +++++++++++++++++++++++++++++++++ engine/src/engine.rs | 16 +++- engine/src/system.rs | 3 +- game/src/main.rs | 17 +++- glium_platform/src/platform.rs | 37 ++++++++- platform/src/context.rs | 8 ++ platform/src/lib.rs | 2 +- 9 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 core/src/time.rs diff --git a/core/src/engine.rs b/core/src/engine.rs index 9aafdf5..80d2a69 100644 --- a/core/src/engine.rs +++ b/core/src/engine.rs @@ -7,7 +7,8 @@ pub trait EngineTrait { type PlatformCtx: Clone; fn new() -> Self; fn initialize(&mut self, platform_context: Self::PlatformCtx); - fn update(&mut self, platform_context: Self::PlatformCtx); + fn frame_update(&mut self, platform_context: Self::PlatformCtx); + fn fixed_update(&mut self, platform_context: Self::PlatformCtx); fn handle_event(&mut self, platform_context: Self::PlatformCtx); fn current_scene_mut(&mut self) -> &mut Scene; fn get_debug_ui_buffer(&self) -> Rc>; diff --git a/core/src/lib.rs b/core/src/lib.rs index f4b8d0c..4b7d531 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,6 @@ pub mod engine; pub mod scene; pub mod debug_ui; +pub mod time; + pub use debug_ui::*; diff --git a/core/src/time.rs b/core/src/time.rs new file mode 100644 index 0000000..c47466d --- /dev/null +++ b/core/src/time.rs @@ -0,0 +1,144 @@ +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) } + } +} diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 5d6af36..82b1f91 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -44,7 +44,7 @@ impl EngineTrait for Engine { } /// Update the engine - fn update(&mut self, platform_context: PlatformContext) { + fn frame_update(&mut self, platform_context: PlatformContext) { self.debug_ui_buffer.borrow_mut().reset_buffer(); let mut ctx = SystemContext { scene: self.scene_manager.current_mut(), @@ -53,7 +53,19 @@ impl EngineTrait for Engine { }; for system in self.system_manager.systems.values_mut() { - system.update(&mut ctx); + system.frame_update(&mut ctx); + } + } + + fn fixed_update(&mut self, platform_context: PlatformContext) { + let mut ctx = SystemContext { + scene: self.scene_manager.current_mut(), + platform_context, + debug_ui_buffer: self.debug_ui_buffer.clone(), + }; + + for system in self.system_manager.systems.values_mut() { + system.fixed_update(&mut ctx); } } diff --git a/engine/src/system.rs b/engine/src/system.rs index 31708b3..1ab49bf 100644 --- a/engine/src/system.rs +++ b/engine/src/system.rs @@ -19,7 +19,8 @@ pub trait System { /// Spawn the first entities of the world. fn load_world(&mut self, _ctx: &mut SystemContext) {} fn handle_event(&mut self, _ctx: &mut SystemContext) {} - fn update(&mut self, _ctx: &mut SystemContext) {} + fn fixed_update(&mut self, _ctx: &mut SystemContext) {} + fn frame_update(&mut self, _ctx: &mut SystemContext) {} } pub struct SystemManager { diff --git a/game/src/main.rs b/game/src/main.rs index 126c2e4..b1b5c2c 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -1,3 +1,4 @@ +use std::fmt::format; use glam::{Quat, Vec3}; use raidillon_engine::{Engine, system::System}; use raidillon_engine::system::SystemContext; @@ -34,8 +35,8 @@ impl System for UpdateAspectRatioSystem { #[derive(Default)] struct RenderingTestSystem; + impl System for RenderingTestSystem { - fn initialize(&mut self) {} fn load_world(&mut self, ctx: &mut SystemContext) { ctx.scene.world.spawn((Camera { eye: Vec3::new(0.0, 0.0, 2.0), @@ -61,9 +62,19 @@ impl System for RenderingTestSystem { )); } - fn update(&mut self, ctx: &mut SystemContext) { - ctx.debug_ui_buffer.borrow_mut().text("Hello World!".to_owned()); + fn frame_update(&mut self, ctx: &mut SystemContext) { + let mut dbg_ui = ctx.debug_ui_buffer.borrow_mut(); + dbg_ui.text("Hello World!".to_owned()); + dbg_ui.text(format!("Frame Delta: {}", ctx.platform_context.time_ctx.frame_dt)); + dbg_ui.text(format!("Fixed Delta: {}", ctx.platform_context.time_ctx.fixed_dt)); } + + fn fixed_update(&mut self, ctx: &mut SystemContext) { + ctx.scene.world.query_mut::<(&mut Transform, &ModelHandle)>().into_iter().for_each(|(_, (t, _))| { + t.rotation *= Quat::from_rotation_y(10.0 * ctx.platform_context.time_ctx.fixed_dt); + }); + } + } fn main() { diff --git a/glium_platform/src/platform.rs b/glium_platform/src/platform.rs index 97c1535..16df321 100644 --- a/glium_platform/src/platform.rs +++ b/glium_platform/src/platform.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; use std::rc::Rc; -use raidillon_platform::{Platform, PlatformContext}; +use raidillon_platform::{Platform, PlatformContext, TimeContext}; use glium::backend::glutin::Display; use glium::backend::glutin::SimpleWindowBuilder; use glium::glutin::surface::WindowSurface; @@ -11,6 +11,8 @@ use crate::system::{RenderingContext, RenderingSystemManager}; use winit::event::{Event, WindowEvent}; use raidillon_assets::ModelManagerRef; use raidillon_core::engine::EngineTrait; +use raidillon_core::time; +use raidillon_core::time::Time; use crate::render::debug_ui::ImguiBridge; use crate::render::BasicMeshRenderingSystem; use crate::GliumAssetManager; @@ -22,6 +24,7 @@ pub struct GliumPlatform> { rendering_system_manager: RenderingSystemManager, asset_manager: ModelManagerRef, engine: E, + time: time::Time, } impl> Platform for GliumPlatform { @@ -38,6 +41,9 @@ impl> Platform for GliumPlatfor let asset_manager: ModelManagerRef = Rc::new(RefCell::new(Box::new(GliumAssetManager::new(Box::new(display.clone()))))); let mut rendering_system_manager = RenderingSystemManager::new(); + let time_cfg = time::Config::default(); + let time = time::Time::new(time_cfg); + // Install rendering systems rendering_system_manager.add::(&display, &window); rendering_system_manager.add::(&display, &window); @@ -49,6 +55,7 @@ impl> Platform for GliumPlatfor rendering_system_manager, asset_manager, engine, + time, } } @@ -59,6 +66,7 @@ impl> Platform for GliumPlatfor asset_manager: self.asset_manager.clone(), frame_width: w as f32, frame_height: h as f32, + time_ctx: self.construct_time_ctx(), }; self.engine.initialize(ctx.clone()); let _ = &self.event_loop.run(move |event, el| { @@ -98,9 +106,22 @@ impl> Platform for GliumPlatfor _ => {}, }, Event::AboutToWait => { + let plan = self.time.begin_frame_blocking(); + let mut ctx2 = ctx.clone(); - ctx2.current_event = event.clone(); - self.engine.update(ctx2); + ctx2.time_ctx = TimeContext { + frame_dt: self.time.frame_dt_seconds(), + fixed_dt: self.time.fixed_dt_seconds(), + alpha: self.time.alpha(), + }; + ctx2.current_event = Event::AboutToWait; + + for _ in 0..plan.updates { + self.engine.fixed_update(ctx2.clone()); + } + + self.engine.frame_update(ctx2.clone()); + self.rendering_system_manager .systems .values_mut() @@ -112,3 +133,13 @@ impl> Platform for GliumPlatfor }); } } + +impl> GliumPlatform { + fn construct_time_ctx(&self) -> TimeContext { + TimeContext { + frame_dt: self.time.frame_dt_seconds(), + fixed_dt: self.time.fixed_dt_seconds(), + alpha: self.time.alpha(), + } + } +} diff --git a/platform/src/context.rs b/platform/src/context.rs index 45e8fa4..bea1cb1 100644 --- a/platform/src/context.rs +++ b/platform/src/context.rs @@ -7,4 +7,12 @@ pub struct PlatformContext { pub asset_manager: ModelManagerRef, pub frame_width: f32, pub frame_height: f32, + pub time_ctx: TimeContext, +} + +#[derive(Clone)] +pub struct TimeContext { + pub frame_dt: f32, + pub fixed_dt: f32, + pub alpha: f32, } diff --git a/platform/src/lib.rs b/platform/src/lib.rs index 962c05f..8875d35 100644 --- a/platform/src/lib.rs +++ b/platform/src/lib.rs @@ -5,4 +5,4 @@ pub mod context; pub use platform::Platform; pub use camera::Camera; -pub use context::PlatformContext; +pub use context::{PlatformContext, TimeContext};