From 940e692d5b24f40147dc92c2dc23ce74c3d5ab7f Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Sun, 16 Aug 2026 10:53:45 -0700 Subject: small refactor to update loops and timing --- src/config.rs | 3 + src/main.rs | 317 ++++++++++++++++++++++++++++++++--------------------- src/renderer/ui.rs | 6 +- 3 files changed, 196 insertions(+), 130 deletions(-) (limited to 'src') diff --git a/src/config.rs b/src/config.rs index fc3e341..6a9d392 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,3 +6,6 @@ pub const CELLS_IN_CHUNK: usize = (CHUNK_SIZE * CHUNK_SIZE) as usize; pub const CAMERA_MOVEMENT_SPEED: f32 = 40.0; pub const SIM_FPS: u32 = 120; +pub const SIM_DELTA_TIME: f32 = 1.0 / SIM_FPS as f32; +pub const PHYSICS_FPS: u32 = 60; +pub const PHYSICS_DELTA_TIME: f32 = 1.0 / PHYSICS_FPS as f32; diff --git a/src/main.rs b/src/main.rs index 7cbf9d0..7cac1f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use winit::{ use crate::{ camera::Camera, - config::{SIM_FPS, WINDOW_TITLE}, + config::{PHYSICS_DELTA_TIME, PHYSICS_FPS, SIM_DELTA_TIME, SIM_FPS, WINDOW_TITLE}, renderer::RendererState, sim::{cell::Cell, materials::MaterialId, sim::sim_tick, world::World}, }; @@ -28,14 +28,14 @@ pub type Error = Box; pub type Result = std::result::Result; struct Config { - brush_radius: u8, + brush_radius: f32, brush_material: MaterialId, use_threading: bool, } struct Input { - last_mouse_pos_on_screen: Option<(f64, f64)>, - last_mouse_pos_on_board: Option<(i32, i32)>, + last_mouse_pos_on_screen: Option<(f32, f32)>, + last_mouse_world_pos: Option<(f32, f32)>, is_lmb_pressed: bool, // keybindings @@ -65,16 +65,82 @@ struct App { sim_seqno: u64, sim_paused: bool, ignore_pause_next_tick: bool, + last_sim_update: Instant, + sim_updates_due: f32, + + // physics state + last_physics_update: Instant, + physics_updates_due: f32, - // used to compute delta_time - last_sim_tick: Instant, - sim_ticks_due: f32, last_render: Instant, config: Config, diagnostics: Diagnostics, } +impl App { + // called as often as possible + // delta time is the real seconds elapsed since the last time this was called + fn update(&mut self, delta_time: f32) -> () { + // apply inputs + match &mut self.camera { + Some(camera) => camera.handle_camera_input(&self.input, delta_time), + _ => {} + } + + // // --TEST DRAWING-- + if self.input.is_lmb_pressed + && let Some(lm) = self.input.last_mouse_world_pos + { + // start with the bounding box of the drawing brush circle + some margin + // clamp the bounding box to the board sie + let bb_xl = (lm.0 - self.config.brush_radius).round() as i32; + let bb_xu = (lm.0 + self.config.brush_radius).round() as i32; + let bb_yl = (lm.1 - self.config.brush_radius).round() as i32; + let bb_yu = (lm.1 + self.config.brush_radius).round() as i32; + + // for each point, check if the distance is less than the brush size and write the pixel + for x in bb_xl..bb_xu { + for y in bb_yl..bb_yu { + let r = random_range(0.0..1.0); + if ((x - lm.0.round() as i32).pow(2) + (y - lm.1.round() as i32).pow(2)) + < (self.config.brush_radius as i32).pow(2) + && r > 0.9 + { + let mut cell = Cell::from_material(self.config.brush_material); + // ensure we simulate on the first tick + cell.flags = (self.sim_seqno as u8) & 0b1; + match &mut self.world { + Some(world) => world.set_cell_from_game_position( + x, y, cell, false, // wake the chunk + ), + _ => {} + } + } + } + } + } + } + // called SIM_FPS times per second + // will be called before the render and before the physics update(s) + // may be called multiple times if the sim time is behind + // sim_delta_time is statically 1/SIM_FPS + fn sim_update(&mut self, sim_delta_time: f32) -> () { + match &mut self.world { + Some(world) => { + sim_tick(world, self.sim_seqno, self.config.use_threading); + self.sim_seqno += 1; + } + _ => {} + } + } + // called PHYSICS_FPS times per second + // will be called before the render + // may be called multiple times if the physics time is behind + // physics_delta_time is statically 1/PHYSICS_FPS + fn physics_update(&mut self, physics_delta_time: f32) -> () {} +} + impl Default for App { fn default() -> Self { Self { @@ -83,7 +149,7 @@ impl Default for App { input: Input { last_mouse_pos_on_screen: None, - last_mouse_pos_on_board: None, + last_mouse_world_pos: None, is_lmb_pressed: false, is_up_pressed: false, @@ -99,14 +165,17 @@ impl Default for App { sim_seqno: 0, sim_paused: false, ignore_pause_next_tick: false, + last_sim_update: Instant::now(), + sim_updates_due: 0.0, + + last_physics_update: Instant::now(), + physics_updates_due: 0.0, - last_sim_tick: Instant::now(), - sim_ticks_due: 0., last_render: Instant::now(), config: Config { use_threading: true, - brush_radius: 10, + brush_radius: 10.0, brush_material: MaterialId::Sand, }, diagnostics: Diagnostics { @@ -144,145 +213,139 @@ impl ApplicationHandler for App { _: winit::window::WindowId, event: WindowEvent, ) { - if let Some(window) = &self.window - && let Some(renderer_state) = &mut self.renderer_state - && let Some(world) = &mut self.world - && let Some(camera) = &mut self.camera + if let Some(renderer_state) = &mut self.renderer_state + && let Some(window) = &mut self.window { - let egui_response = renderer_state.egui_state.on_window_event(window, &event); + let egui_response = renderer_state.egui_state.on_window_event(&window, &event); // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks if egui_response.consumed { return; } + } - match event { - WindowEvent::KeyboardInput { - event: - KeyEvent { - physical_key: PhysicalKey::Code(code), - state, - .. - }, - .. - } => { - let pressed = state.is_pressed(); - match code { - KeyCode::KeyW => self.input.is_up_pressed = pressed, - KeyCode::KeyA => self.input.is_left_pressed = pressed, - KeyCode::KeyS => self.input.is_down_pressed = pressed, - KeyCode::KeyD => self.input.is_right_pressed = pressed, - KeyCode::KeyC => self.world = Some(World::from_default_size()), - KeyCode::Space => { - if pressed { - self.sim_paused = !self.sim_paused - } + match event { + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: PhysicalKey::Code(code), + state, + .. + }, + .. + } => { + let pressed = state.is_pressed(); + match code { + KeyCode::KeyW => self.input.is_up_pressed = pressed, + KeyCode::KeyA => self.input.is_left_pressed = pressed, + KeyCode::KeyS => self.input.is_down_pressed = pressed, + KeyCode::KeyD => self.input.is_right_pressed = pressed, + KeyCode::KeyC => self.world = Some(World::from_default_size()), + KeyCode::Space => { + if pressed { + self.sim_paused = !self.sim_paused } - KeyCode::KeyX => { - if pressed { - self.ignore_pause_next_tick = true - } + } + KeyCode::KeyX => { + if pressed { + self.ignore_pause_next_tick = true } - _ => {} } + _ => {} } - WindowEvent::CursorMoved { position, .. } => { - self.input.last_mouse_pos_on_screen = Some((position.x, position.y)); - let world_pos = - camera.screen_position_to_world(position.x as f32, position.y as f32); - self.input.last_mouse_pos_on_board = - Some((world_pos.0 as i32, world_pos.1 as i32)) + } + WindowEvent::CursorMoved { position, .. } => { + self.input.last_mouse_pos_on_screen = Some((position.x as f32, position.y as f32)); + self.input.last_mouse_world_pos = if let Some(camera) = &mut self.camera { + Some(camera.screen_position_to_world(position.x as f32, position.y as f32)) + } else { + None } - WindowEvent::MouseInput { state, button, .. } => { - if button == MouseButton::Left { - self.input.is_lmb_pressed = state == ElementState::Pressed - } + } + WindowEvent::MouseInput { state, button, .. } => { + if button == MouseButton::Left { + self.input.is_lmb_pressed = state == ElementState::Pressed } - WindowEvent::Resized(size) => { + } + WindowEvent::Resized(size) => { + if let Some(renderer_state) = &mut self.renderer_state + && let Some(camera) = &mut self.camera + { renderer_state.resize(size.width, size.height); camera.resize((size.width as i32, size.height as i32)); } - WindowEvent::CloseRequested => { - event_loop.exit(); - } - WindowEvent::RedrawRequested => { - #[cfg(feature = "profiler")] - puffin::GlobalProfiler::lock().new_frame(); - - puffin::profile_scope!("redraw_requested"); - // compute FPS diagnostics - let now = Instant::now(); - let secs_since_last_frame = (now - self.last_render).as_secs_f32(); - self.last_render = now; - - let delta_time = secs_since_last_frame / (1.0 / 60.0); - - self.diagnostics - .frame_times - .push_back(secs_since_last_frame); + } + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::RedrawRequested => { + #[cfg(feature = "profiler")] + puffin::GlobalProfiler::lock().new_frame(); - if self.diagnostics.frame_times.len() > 30 { - self.diagnostics.frame_times.pop_front(); - } + puffin::profile_scope!("redraw_requested"); + // compute FPS diagnostics + let now = Instant::now(); + let secs_since_last_frame = (now - self.last_render).as_secs_f32(); + self.last_render = now; - let average_frame_time = self.diagnostics.frame_times.iter().sum::() - / self.diagnostics.frame_times.len() as f32; + let delta_time = secs_since_last_frame / (1.0 / 60.0); - self.diagnostics.fps = 1.0 / average_frame_time; + self.diagnostics + .frame_times + .push_back(secs_since_last_frame); - // apply inputs - camera.handle_camera_input(&self.input, delta_time); + if self.diagnostics.frame_times.len() > 30 { + self.diagnostics.frame_times.pop_front(); + } - // // --TEST DRAWING-- - if self.input.is_lmb_pressed - && let Some(lm) = self.input.last_mouse_pos_on_board - { - // start with the bounding box of the drawing brush circle + some margin - // clamp the bounding box to the board sie - let bb_xl = lm.0 - self.config.brush_radius as i32; - let bb_xu = lm.0 + self.config.brush_radius as i32; - let bb_yl = lm.1 - self.config.brush_radius as i32; - let bb_yu = lm.1 + self.config.brush_radius as i32; - - // for each point, check if the distance is less than the brush size and write the pixel - for x in bb_xl..bb_xu { - for y in bb_yl..bb_yu { - let r = random_range(0.0..1.0); - if ((x - lm.0).pow(2) + (y - lm.1).pow(2)) - < (self.config.brush_radius as i32).pow(2) - && r > 0.9 - { - let mut cell = Cell::from_material(self.config.brush_material); - // ensure we simulate on the first tick - cell.flags = (self.sim_seqno as u8) & 0b1; - world.set_cell_from_game_position( - x, y, cell, false, // wake the chunk - ); - } - } - } + let average_frame_time = self.diagnostics.frame_times.iter().sum::() + / self.diagnostics.frame_times.len() as f32; + + self.diagnostics.fps = 1.0 / average_frame_time; + + // UPDATE + self.update(delta_time); + + // SIM UPDATE + let secs_since_last_sim_update = (now - self.last_sim_update).as_secs_f32(); + let expected_secs_since_last_sim_update = 1.0 / SIM_FPS as f32; + + self.last_sim_update = now; + if self.sim_paused && self.ignore_pause_next_tick { + self.sim_update(SIM_DELTA_TIME); + self.ignore_pause_next_tick = false; + } else if !self.sim_paused { + self.sim_updates_due += + secs_since_last_sim_update / expected_secs_since_last_sim_update; + let mut updates_done = 0; + // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock + while self.sim_updates_due >= 1.0 && updates_done < 3 { + self.sim_update(SIM_DELTA_TIME); + updates_done += 1; } + self.sim_updates_due -= updates_done as f32; + } - // SIM logic - let secs_since_last_tick = (now - self.last_sim_tick).as_secs_f32(); - let expected_secs_since_last_tick = 1.0 / SIM_FPS as f32; - - self.last_sim_tick = now; - if self.sim_paused && self.ignore_pause_next_tick { - sim_tick(world, self.sim_seqno, self.config.use_threading); - self.sim_seqno += 1; - self.ignore_pause_next_tick = false; - } else if !self.sim_paused { - self.sim_ticks_due += secs_since_last_tick / expected_secs_since_last_tick; - let mut ticks_done = 0; - // don't ever tick more than 3 times per frame, or else we can get a pseudo deadlock - while self.sim_ticks_due >= 1.0 && ticks_done < 3 { - sim_tick(world, self.sim_seqno, self.config.use_threading); - self.sim_seqno += 1; - ticks_done += 1; - } - self.sim_ticks_due -= ticks_done as f32; + // PHYSICS UPDATE + let secs_since_last_physics_update = (now - self.last_physics_update).as_secs_f32(); + let expected_secs_since_last_physics_update = 1.0 / PHYSICS_FPS as f32; + + self.last_physics_update = now; + if !self.sim_paused { + self.physics_updates_due += + secs_since_last_physics_update / expected_secs_since_last_physics_update; + let mut updates_done = 0; + // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock + while self.physics_updates_due >= 1.0 && updates_done < 3 { + self.physics_update(PHYSICS_DELTA_TIME); + updates_done += 1; } + self.physics_updates_due -= updates_done as f32; + } + if let Some(renderer_state) = &mut self.renderer_state + && let Some(world) = &mut self.world + && let Some(camera) = &mut self.camera + { renderer_state.render( world, camera, @@ -291,8 +354,8 @@ impl ApplicationHandler for App { &mut self.input, ); } - _ => {} } + _ => {} } } diff --git a/src/renderer/ui.rs b/src/renderer/ui.rs index b4e6f74..2e4ebc4 100644 --- a/src/renderer/ui.rs +++ b/src/renderer/ui.rs @@ -15,7 +15,7 @@ pub fn draw_egui<'a>( ) { puffin::profile_function!(); ui.heading("Config"); - ui.add(egui::Slider::new(&mut config.brush_radius, 1..=100).text("Brush radius")); + ui.add(egui::Slider::new(&mut config.brush_radius, 1.0..=100.0).text("Brush radius")); let material = config.brush_material.def(); @@ -58,10 +58,10 @@ pub fn draw_egui<'a>( )) }); - if let Some((x, y)) = input.last_mouse_pos_on_board { + if let Some((x, y)) = input.last_mouse_world_pos { ui.heading("Entity"); ui.label(format!("x,y=({x}, {y})")); - if let Some(cell) = world.get_cell_from_game_position(x, y) { + if let Some(cell) = world.get_cell_from_game_position(x.round() as i32, y.round() as i32) { let material = cell.material.def(); let cell_label = ui.label( egui::RichText::new(format!("Cell: {}", material.name)).color(Color32::LIGHT_BLUE), -- cgit v1.3.1