diff options
| author | Kai Stevenson <kai@kaistevenson.com> | 2026-08-15 22:46:53 -0700 |
|---|---|---|
| committer | Kai Stevenson <kai@kaistevenson.com> | 2026-08-15 22:46:53 -0700 |
| commit | d7eccba2055cd7784a0db6d9ee8692a4f3510931 (patch) | |
| tree | 37f4c19781c788fa2af1ee4eeb7195f49a6d2d7a /src | |
| parent | 05135beb87bbf0edace544b80ba40e327d9a89ac (diff) | |
fire, optimize viewport rendering
Diffstat (limited to 'src')
| -rw-r--r-- | src/camera.rs | 8 | ||||
| -rw-r--r-- | src/main.rs | 6 | ||||
| -rw-r--r-- | src/renderer/mod.rs | 14 | ||||
| -rw-r--r-- | src/renderer/ui.rs | 35 | ||||
| -rw-r--r-- | src/sim/cell.rs | 21 | ||||
| -rw-r--r-- | src/sim/materials/fire.rs | 53 | ||||
| -rw-r--r-- | src/sim/materials/mod.rs | 14 | ||||
| -rw-r--r-- | src/sim/sim.rs | 65 | ||||
| -rw-r--r-- | src/sim/world.rs | 5 |
9 files changed, 169 insertions, 52 deletions
diff --git a/src/camera.rs b/src/camera.rs index 0b69833..d14adbd 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -20,6 +20,14 @@ impl Camera { (1.0 / half_w, -1.0 / half_h) } + // xl, xu, yl, yu + pub fn viewport_bounds_world(&self) -> (f32, f32, f32, f32) { + let (xl, yl) = self.screen_position_to_world(0.0, 0.0); + let (xu, yu) = + self.screen_position_to_world(self.screen_size.0 as f32, self.screen_size.1 as f32); + (xl, xu, yl, yu) + } + pub fn handle_camera_input(&mut self, input: &Input, delta_time: f32) { // wasd movement let x: f32 = if input.is_left_pressed { diff --git a/src/main.rs b/src/main.rs index 92fe01e..7cbf9d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -252,10 +252,10 @@ impl ApplicationHandler for App { && 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, // wake the chunk - false, + x, y, cell, false, // wake the chunk ); } } @@ -266,13 +266,13 @@ impl ApplicationHandler for App { 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; - self.last_sim_tick = now; 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 { diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index fcc5d36..4499688 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -212,8 +212,8 @@ impl RendererState { let mut renderer_chunks: Vec<RendererChunk> = Vec::new(); // match number of world chunks // TODO refactor so that this implicit - for _ in -10..10 { - for _ in -10..10 { + for _ in -10..1 { + for _ in -100..100 { let texture = device.create_texture(&wgpu::TextureDescriptor { label: None, mip_level_count: 1, @@ -336,7 +336,7 @@ impl RendererState { .resizable(false) // TODO collapse button .show_collapsible(ui, &mut true, |panel_ui| { - draw_egui(panel_ui, config, camera, diagnostics, input) + draw_egui(panel_ui, config, camera, diagnostics, input, world) }); }); @@ -449,9 +449,13 @@ impl RendererState { render_pass.set_bind_group(0, &self.camera_uniform_bind_group, &[]); + let (xl, xu, yl, yu) = camera.viewport_bounds_world(); + let ((cxl, cyl), _) = World::split_game_position(xl.floor() as i32, yl.floor() as i32); + let ((cxu, cyu), _) = World::split_game_position(xu.floor() as i32, yu.floor() as i32); + // TODO only visible chunks - for cx in -10..10 { - for cy in -10..10 { + for cx in cxl..=cxu { + for cy in cyl..=cyu { if let Some(idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) { let render_chunk = &self.renderer_chunks[*idx]; render_pass.set_bind_group(1, &render_chunk.bind_group, &[]); diff --git a/src/renderer/ui.rs b/src/renderer/ui.rs index 87ab878..b4e6f74 100644 --- a/src/renderer/ui.rs +++ b/src/renderer/ui.rs @@ -1,6 +1,9 @@ use egui::{Color32, Stroke, Ui, epaint::CircleShape}; -use crate::{Camera, Config, Diagnostics, Input, sim::materials::MaterialId}; +use crate::{ + Camera, Config, Diagnostics, Input, + sim::{materials::MaterialId, world::World}, +}; pub fn draw_egui<'a>( ui: &mut Ui, @@ -8,6 +11,7 @@ pub fn draw_egui<'a>( camera: &mut Camera, diagnostics: &Diagnostics, input: &Input, + world: &World, ) { puffin::profile_function!(); ui.heading("Config"); @@ -24,7 +28,7 @@ pub fn draw_egui<'a>( .icon(move |ui, rect, _, _| { ui.painter().add(egui::Shape::Circle(CircleShape { center: rect.center(), - radius: rect.width() / 2.5, + radius: rect.height() / 2.0, stroke: Stroke::NONE, fill: Color32::from_rgb(material.color.0, material.color.1, material.color.2), })); @@ -54,11 +58,24 @@ pub fn draw_egui<'a>( )) }); - input - .last_mouse_pos_on_board - .map(|p| ui.label(format!("Mouse (board): x,y=({x}, {y})", x = p.0, y = p.1,))); - - input - .last_mouse_pos_on_board - .map(|p| ui.label(format!("Mouse (chunk): x,y=({x}, {y})", x = p.0, y = p.1,))); + if let Some((x, y)) = input.last_mouse_pos_on_board { + ui.heading("Entity"); + ui.label(format!("x,y=({x}, {y})")); + if let Some(cell) = world.get_cell_from_game_position(x, y) { + let material = cell.material.def(); + let cell_label = ui.label( + egui::RichText::new(format!("Cell: {}", material.name)).color(Color32::LIGHT_BLUE), + ); + ui.painter().add(egui::Shape::Circle(CircleShape { + center: cell_label.rect.right_center() + egui::vec2(10.0, 0.0), + radius: cell_label.rect.height() / 2.5, + stroke: Stroke::NONE, + fill: Color32::from_rgb(material.color.0, material.color.1, material.color.2), + })); + ui.label( + egui::RichText::new(format!("Flags: {:b}", cell.flags)).color(Color32::YELLOW), + ); + ui.label(egui::RichText::new(format!("Data: {:b}", cell.data)).color(Color32::RED)); + } + } } diff --git a/src/sim/cell.rs b/src/sim/cell.rs index 2e26ddc..d3ca302 100644 --- a/src/sim/cell.rs +++ b/src/sim/cell.rs @@ -4,6 +4,20 @@ use crate::sim::materials::MaterialId; pub struct Cell { pub material: MaterialId, pub flags: u8, + pub data: u16, +} + +impl Cell { + const FLAG_PARITY: u8 = 0b0000_0001; + + #[inline] + pub fn parity(self) -> u8 { + self.flags & Self::FLAG_PARITY + } + #[inline] + pub fn flip_parity(&mut self) { + self.flags ^= Self::FLAG_PARITY; + } } impl Cell { @@ -11,9 +25,14 @@ impl Cell { Cell { material: MaterialId::Void, flags: 0, + data: 0, } } pub fn from_material(material: MaterialId) -> Cell { - Cell { material, flags: 0 } + Cell { + material, + flags: 0, + data: 0, + } } } diff --git a/src/sim/materials/fire.rs b/src/sim/materials/fire.rs new file mode 100644 index 0000000..b155986 --- /dev/null +++ b/src/sim/materials/fire.rs @@ -0,0 +1,53 @@ +use rand::RngExt; + +use crate::sim::{cell::Cell, materials::MaterialId, sim::UpdateCtx}; + +trait FireCellView { + fn get_ticks_lived(self) -> u16; + fn set_ticks_lived(&mut self, ticks: u16) -> (); + fn is_flammable(self) -> bool; +} + +impl FireCellView for Cell { + fn get_ticks_lived(self) -> u16 { + self.data + } + fn set_ticks_lived(&mut self, ticks: u16) -> () { + self.data = ticks; + } + // this could be a property of the material def, I think it's better here for now + fn is_flammable(self) -> bool { + [MaterialId::Wood].contains(&self.material) + } +} + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + let ticks_lived = ctx.cell.get_ticks_lived(); + // 2 seconds + if ticks_lived > 240 { + // kill ourselves, with a chance to turn into ash + if ctx.rng.random_range(0.0..1.0) > 0.8 { + // TODO ash material + ctx.set_cell(0, 0, Cell::from_material(MaterialId::Sand)); + } else { + ctx.set_cell(0, 0, Cell::void()); + } + return; + } + + // 4 times in our lifespan, try to spread in a random direction + // since it's random, fire may die out if there's nothing in that direction + // a 1-thick line of flammable cells has a 31% chance to die each iteration + if ticks_lived % 60 == 0 { + let (dx, dy) = (ctx.rng.random_range(-1..=1), ctx.rng.random_range(-1..=1)); + if let Some(target) = ctx.get_cell(dx, dy) + && target.is_flammable() + { + ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Fire)); + } + } + + ctx.cell.set_ticks_lived(ticks_lived + 1); + ctx.set_cell(0, 0, *ctx.cell); +} diff --git a/src/sim/materials/mod.rs b/src/sim/materials/mod.rs index d2d0e0e..764318a 100644 --- a/src/sim/materials/mod.rs +++ b/src/sim/materials/mod.rs @@ -1,5 +1,6 @@ use crate::sim::sim::UpdateCtx; +mod fire; mod gas; mod sand; mod water; @@ -12,6 +13,7 @@ pub enum MaterialId { Wood, Water, Gas, + Fire, } pub struct MaterialDef { @@ -21,7 +23,7 @@ pub struct MaterialDef { pub sim_update: Option<fn(ctx: &mut UpdateCtx) -> ()>, } -static MATERIALS: [MaterialDef; 5] = [ +static MATERIALS: [MaterialDef; 6] = [ MaterialDef { name: "Void", color: (0x00, 0x00, 0x00, 0x00), @@ -52,15 +54,23 @@ static MATERIALS: [MaterialDef; 5] = [ density: 10, sim_update: Some(gas::sim_update), }, + MaterialDef { + name: "Fire", + color: (0xFC, 0x66, 0x00, 0xAA), + // for now this matches wood + density: 50, + sim_update: Some(fire::sim_update), + }, ]; impl MaterialId { - pub const ALL: [MaterialId; 5] = [ + pub const ALL: [MaterialId; 6] = [ MaterialId::Void, MaterialId::Sand, MaterialId::Wood, MaterialId::Water, MaterialId::Gas, + MaterialId::Fire, ]; #[inline] pub fn def(self) -> &'static MaterialDef { diff --git a/src/sim/sim.rs b/src/sim/sim.rs index 4c6d908..84c3126 100644 --- a/src/sim/sim.rs +++ b/src/sim/sim.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; use fxhash::FxHashMap; +use rand::{Rng, SeedableRng, rngs::SmallRng}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ @@ -30,17 +31,6 @@ impl<'a> ChunkAccess<'a> { unsafe impl Sync for ChunkAccess<'_> {} -pub struct UpdateCtx<'a, 'b, 'c> { - pub chunks: &'a mut [Option<&'b mut Chunk>; 9], - pub seqno: u64, - pub seqno_parity: u8, - - pub x: i32, - pub y: i32, - pub cell: &'c mut Cell, - pub material: &'c MaterialDef, -} - fn get_cell(chunks: &[Option<&mut Chunk>; 9], x: i32, y: i32) -> Option<Cell> { let dcx = x.div_euclid(CHUNK_SIZE); let dcy = y.div_euclid(CHUNK_SIZE); @@ -74,17 +64,28 @@ pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell if let Some(chunk) = &mut chunks[(dcx + 1 + (dcy + 1) * 3) as usize] { chunk.set_cell_at_local_position(nc_x, nc_y, cell); chunk.needs_texture_update = true; - chunk.sleeping = false; } } else { if let Some(target) = &mut chunks[4] { target.set_cell_at_local_position(x as u8, y as u8, cell); target.needs_texture_update = true; - target.sleeping = false; } } } +pub struct UpdateCtx<'a, 'b, 'c> { + pub chunks: &'a mut [Option<&'b mut Chunk>; 9], + pub seqno: u64, + pub seqno_parity: u8, + + pub x: i32, + pub y: i32, + pub cell: &'c mut Cell, + pub material: &'c MaterialDef, + + pub rng: &'c mut dyn Rng, +} + impl UpdateCtx<'_, '_, '_> { pub fn get_cell(&self, dx: i32, dy: i32) -> Option<Cell> { let x = self.x + dx; @@ -92,7 +93,7 @@ impl UpdateCtx<'_, '_, '_> { get_cell(self.chunks, x, y) } - fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { + pub fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { // cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood // as this would wake a chunk outside of the neighbourhood debug_assert!(dx > -15 && dx < 15); @@ -122,8 +123,8 @@ impl UpdateCtx<'_, '_, '_> { pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) { puffin::profile_function!(); - // scan bottom to top to enable contiguous falling let seqno_parity = (seqno as u8) & 0b1; + let mut rng = SmallRng::seed_from_u64(seqno); if chunks[4].is_some() { for y in (0..CHUNK_SIZE).rev() { @@ -135,24 +136,28 @@ pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) { }; let mut cell = get_cell(chunks, x, y).unwrap(); - if cell.flags & 0b1 == seqno_parity { - // flip the parity bit - // TODO if the cell doesn't move this doesn't stay - cell.flags = cell.flags ^ 0b1; - let material = cell.material.def(); + let material = cell.material.def(); + + if let Some(update) = material.sim_update { + if cell.parity() == seqno_parity { + cell.flip_parity(); + // apply the flipped parity in case the sim target doesn't + set_cell(chunks, x, y, cell); + + let mut update_ctx = UpdateCtx { + chunks, + seqno, + seqno_parity, - let mut update_ctx = UpdateCtx { - chunks, - seqno, - seqno_parity, + x, + y, + cell: &mut cell, + material, - x, - y, - cell: &mut cell, - material, - }; + // TODO this is platform-dependent, will break for multiplayer + rng: &mut rng, + }; - if let Some(update) = material.sim_update { update(&mut update_ctx); } } diff --git a/src/sim/world.rs b/src/sim/world.rs index 6645d4e..5993b10 100644 --- a/src/sim/world.rs +++ b/src/sim/world.rs @@ -48,6 +48,7 @@ impl World { self.chunks[idx].set_cell_at_local_position(dx, dy, cell); // this is a temporary hack self.chunks[idx].sleeping = sleeping; + self.chunks[idx].needs_texture_update = true; } } @@ -63,8 +64,8 @@ impl World { chunk_position_to_chunk_idx: FxHashMap::default(), }; - for y in -10..10 { - for x in -10..10 { + for y in -10..1 { + for x in -100..100 { world.insert(x, y, Chunk::void()); } } |
