From feefeecec6c6050635b2c016452dfa1529575987 Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Thu, 13 Aug 2026 01:56:09 -0700 Subject: big refactor for board --- src/camera.rs | 110 ++++++++++++++++------- src/config.rs | 5 ++ src/main.rs | 85 +++++++++--------- src/sim/board.rs | 72 --------------- src/sim/cell.rs | 19 ++++ src/sim/chunk.rs | 25 ++++++ src/sim/materials/mod.rs | 10 +-- src/sim/materials/sand.rs | 6 +- src/sim/materials/water.rs | 46 ++++------ src/sim/mod.rs | 4 +- src/sim/overlay.rs | 67 +++++++------- src/sim/sim.rs | 212 ++++++++++++++++++++++++++++++++++++--------- src/sim/world.rs | 66 ++++++++++++++ src/ui.rs | 7 +- 14 files changed, 471 insertions(+), 263 deletions(-) delete mode 100644 src/sim/board.rs create mode 100644 src/sim/cell.rs create mode 100644 src/sim/chunk.rs create mode 100644 src/sim/world.rs (limited to 'src') diff --git a/src/camera.rs b/src/camera.rs index 604cb0d..9310cac 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,7 +1,7 @@ use crate::{ Input, - config::{CAMERA_MOVEMENT_SPEED, PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH}, - sim::board::Board, + config::{CAMERA_MOVEMENT_SPEED, CHUNK_SIZE, PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH}, + sim::{chunk::Chunk, world::World}, }; pub struct Camera { @@ -42,14 +42,9 @@ impl Camera { self.y += adjusted_y as f64; } - pub fn screen_position_to_board( - &self, - board: &Board, - screen_x: f64, - screen_y: f64, - ) -> (f64, f64) { - let camera_width = self.zoom * f64::from(board.size_x); - let camera_height = self.zoom * f64::from(board.size_y); + pub fn screen_position_to_world(&self, screen_x: f64, screen_y: f64) -> (f64, f64) { + let camera_width = self.zoom * f64::from(PIXEL_BUFFER_WIDTH); + let camera_height = self.zoom * f64::from(PIXEL_BUFFER_HEIGHT); let camera_start_x = self.x - camera_width / 2.0; let camera_start_y = self.y - camera_height / 2.0; ( @@ -58,32 +53,81 @@ impl Camera { ) } - pub fn write_frame_view( + pub fn write_frame_view(&self, frame: &mut [u8], world: &World) { + puffin::profile_function!(); + + const BG: [u8; 4] = [0x00, 0x00, 0x00, 0xFF]; + for px in frame.chunks_exact_mut(4) { + px.copy_from_slice(&BG); + } + + // world-space rect covered by the screen + let (xl, yl) = self.screen_position_to_world(0.0, 0.0); + let (xu, yu) = + self.screen_position_to_world(PIXEL_BUFFER_WIDTH as f64, PIXEL_BUFFER_HEIGHT as f64); + + let c = CHUNK_SIZE as i32; + let cx0 = (xl.floor() as i32).div_euclid(c); + let cx1 = (xu.ceil() as i32).div_euclid(c); + let cy0 = (yl.floor() as i32).div_euclid(c); + let cy1 = (yu.ceil() as i32).div_euclid(c); + + let scale = 1.0 / self.zoom; // pixels per cell + + for cy in cy0..=cy1 { + for cx in cx0..=cx1 { + let Some(&idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) else { + continue; + }; + self.write_chunk(frame, &world.chunks[idx], cx, cy, xl, yl, scale); + } + } + } + + fn write_chunk( &self, frame: &mut [u8], - board: &Board, - get_overlay: impl Fn(i32, i32) -> (u8, u8, u8, u8), + chunk: &Chunk, + cx: i32, + cy: i32, + xl: f64, + yl: f64, + scale: f64, ) { - for frame_y in 0..PIXEL_BUFFER_HEIGHT { - for frame_x in 0..PIXEL_BUFFER_WIDTH { - let (x_coord, y_coord) = - self.screen_position_to_board(board, frame_x as f64, frame_y as f64); - - let cell = board.cell_at_position(x_coord as i32, y_coord as i32); - let cell_color: Option<(u8, u8, u8, u8)> = cell.map(|c| { - let m = c.material.def(); - (m.color[0], m.color[1], m.color[2], 0xFF) - }); - let off_grid_color: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0xFF); - - let target_color: (u8, u8, u8, u8) = cell_color.unwrap_or(off_grid_color); - let overlay = get_overlay(x_coord as i32, y_coord as i32); - - let frame_idx = ((frame_y * PIXEL_BUFFER_WIDTH + frame_x) * 4) as usize; - frame[frame_idx] = target_color.0.saturating_add(overlay.0); - frame[frame_idx + 1] = target_color.1.saturating_add(overlay.1); - frame[frame_idx + 2] = target_color.2.saturating_add(overlay.2); - frame[frame_idx + 3] = target_color.3.saturating_add(overlay.3); + let c = CHUNK_SIZE as i32; + let w = PIXEL_BUFFER_WIDTH as i32; + let h = PIXEL_BUFFER_HEIGHT as i32; + + for ly in 0..c { + let wy = (cy * c + ly) as f64; + let sy0 = (((wy - yl) * scale).floor() as i32).max(0); + let sy1 = (((wy + 1.0 - yl) * scale).floor() as i32).min(h); + if sy0 >= sy1 { + continue; + } + + for lx in 0..c { + let wx = (cx * c + lx) as f64; + let sx0 = (((wx - xl) * scale).floor() as i32).max(0); + let sx1 = (((wx + 1.0 - xl) * scale).floor() as i32).min(w); + if sx0 >= sx1 { + continue; + } + + let color = chunk + .get_cell_at_local_position(lx as u8, ly as u8) + .material + .def() + .color; + let rgba = [color.0, color.1, color.2, color.3]; + + for sy in sy0..sy1 { + let start = (sy * w + sx0) as usize * 4; + let end = (sy * w + sx1) as usize * 4; + for px in frame[start..end].chunks_exact_mut(4) { + px.copy_from_slice(&rgba); + } + } } } } diff --git a/src/config.rs b/src/config.rs index 74a98e4..6e367eb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,9 @@ pub const WINDOW_TITLE: &str = "pxs"; + pub const PIXEL_BUFFER_WIDTH: u32 = 320; pub const PIXEL_BUFFER_HEIGHT: u32 = 240; + +pub const CHUNK_SIZE: u32 = 32; +pub const CELLS_IN_CHUNK: usize = (CHUNK_SIZE * CHUNK_SIZE) as usize; + pub const CAMERA_MOVEMENT_SPEED: f32 = 10.0; diff --git a/src/main.rs b/src/main.rs index 02920d6..b048122 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,7 @@ use egui::Id; use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; use pixels::{Pixels, ScalingMode, SurfaceTexture}; -use std::{ - cmp::{max, min}, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, event::{ @@ -25,10 +22,7 @@ use winit::{ use crate::{ camera::Camera, config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE}, - sim::{ - board::Board, materials::MaterialId, overlay::create_compute_combined_overlay_offset, - sim::sim_tick, - }, + sim::{cell::Cell, materials::MaterialId, sim::sim_tick, world::World}, ui::draw_egui, }; @@ -70,7 +64,7 @@ struct App { camera: Option, - board: Option, + world: Option, // sim state // the last/current (not yet completed) seqno @@ -110,10 +104,10 @@ impl Default for App { camera: Some(Camera { x: 0.0, y: 0.0, - zoom: 0.5, + zoom: 1.0, }), - board: Some(Board::empty()), + world: Some(World::from_default_size()), sim_seqno: 0, sim_paused: false, @@ -182,7 +176,7 @@ impl ApplicationHandler for App { && let Some(egui_context) = &self.egui_context && let Some(egui_state) = &mut self.egui_state && let Some(egui_renderer) = &mut self.egui_renderer - && let Some(board) = &mut self.board + && let Some(world) = &mut self.world && let Some(camera) = &mut self.camera { let egui_response = egui_state.on_window_event(window, &event); @@ -207,7 +201,7 @@ impl ApplicationHandler for App { 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.board = Some(Board::empty()), + KeyCode::KeyC => self.world = Some(World::from_default_size()), KeyCode::Space => { if pressed { self.sim_paused = !self.sim_paused @@ -225,9 +219,9 @@ impl ApplicationHandler for App { self.input.last_mouse_pos_on_screen = Some((position.x, position.y)); self.input.last_mouse_pos_on_board = pixels .window_pos_to_pixel((position.x as f32, position.y as f32)) - .map(|v| camera.screen_position_to_board(board, v.0 as f64, v.1 as f64)) + .map(|v| camera.screen_position_to_world(v.0 as f64, v.1 as f64)) .map(|v| (v.0 as i32, v.1 as i32)) - .ok() + .ok(); } WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left { @@ -242,6 +236,9 @@ impl ApplicationHandler for App { event_loop.exit(); } WindowEvent::RedrawRequested => { + #[cfg(feature = "profiler")] + puffin::GlobalProfiler::lock().new_frame(); + puffin::profile_scope!("redraw_requested"); // compute frame delta let now = Instant::now(); let secs_since_last_frame = (now - self.last_frame_real).as_secs_f32(); @@ -259,43 +256,28 @@ impl ApplicationHandler for App { frame.fill(0); // --TEST DRAWING-- - if self.input.is_lmb_pressed && self.input.last_mouse_pos_on_board.is_some() { + 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 // TODO better way to do this without unwrap? - let bb_xl = max( - self.input.last_mouse_pos_on_board.unwrap().0 - - self.config.brush_radius as i32, - -((board.size_x / 2) as i32), - ); - let bb_xu = min( - self.input.last_mouse_pos_on_board.unwrap().0 - + self.config.brush_radius as i32, - (board.size_x / 2) as i32, - ); - let bb_yl = max( - self.input.last_mouse_pos_on_board.unwrap().1 - - self.config.brush_radius as i32, - -((board.size_y / 2) as i32), - ); - let bb_yu = min( - self.input.last_mouse_pos_on_board.unwrap().1 - + self.config.brush_radius as i32, - (board.size_y / 2) as i32, - ); + 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 { // brush/selection - if ((x - self.input.last_mouse_pos_on_board.unwrap().0).pow(2) - + (y - self.input.last_mouse_pos_on_board.unwrap().1).pow(2)) + if ((x - lm.0).pow(2) + (y - lm.1).pow(2)) < (self.config.brush_radius as i32).pow(2) { - board.set_cell_at_position( + world.set_cell_from_game_position( x, y, - sim::board::Cell::from_material(self.config.brush_material), + Cell::from_material(self.config.brush_material), ); } } @@ -305,14 +287,14 @@ impl ApplicationHandler for App { // TODO check if we need to run another sim tick given the sim speed // SIM logic if !self.sim_paused || self.ignore_pause_next_tick { - sim_tick(board, self.sim_seqno, delta_time); + sim_tick(world, self.sim_seqno); self.sim_seqno += 1; self.ignore_pause_next_tick = false; } - let get_overlay = - create_compute_combined_overlay_offset(board, &self.config, &self.input); - camera.write_frame_view(frame, board, get_overlay); + // let get_overlay = + // create_compute_combined_overlay_offset(&world, &self.config, &self.input); + camera.write_frame_view(frame, &world); // egui logic let raw_input = egui_state.take_egui_input(window); @@ -410,8 +392,21 @@ impl ApplicationHandler for App { } } +#[cfg(feature = "profiler")] +fn start_profiler() { + let _server = puffin_http::Server::new("127.0.0.1:8585").unwrap(); + puffin::set_scopes_on(true); + std::mem::forget(_server); // keep serving for the process lifetime + + std::process::Command::new("puffin_viewer") + .args(["--url", "127.0.0.1:8585"]) + .spawn() + .ok(); // don't die if it isn't installed +} + fn main() -> Result<()> { - env_logger::init(); + #[cfg(feature = "profiler")] + start_profiler(); let event_loop = EventLoop::new()?; event_loop.set_control_flow(ControlFlow::Poll); diff --git a/src/sim/board.rs b/src/sim/board.rs deleted file mode 100644 index 7af19be..0000000 --- a/src/sim/board.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::{ - config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH}, - sim::materials::MaterialId, -}; - -#[derive(Clone, Copy)] -pub struct Cell { - pub material: MaterialId, - pub flags: u8, -} - -impl Cell { - pub fn void() -> Cell { - Cell { - material: MaterialId::Void, - flags: 0, - } - } - pub fn from_material(material: MaterialId) -> Cell { - Cell { material, flags: 0 } - } -} - -pub struct Board { - pub size_x: u32, - pub size_y: u32, - pub cells: Vec, -} - -impl Board { - pub fn index_to_position(&self, idx: usize) -> (i32, i32) { - let y = idx / self.size_x as usize; - let x = idx % self.size_x as usize; - let board_x = x as i32 - self.size_x as i32 / 2; - let board_y = y as i32 - self.size_x as i32 / 2; - return (board_x, board_y); - } - pub fn set_cell_at_position(&mut self, x: i32, y: i32, c: Cell) { - // TODO: option? - let idx = self.position_to_index(x, y).unwrap(); - self.cells[idx] = c; - } - pub fn cell_at_position(&self, x: i32, y: i32) -> Option { - Some(self.cells[self.position_to_index(x, y)?]) - } - pub fn position_to_index(&self, x: i32, y: i32) -> Option { - let board_x = x + self.size_x as i32 / 2; - let board_y = y + self.size_y as i32 / 2; - - let on_board = board_x >= 0 - && board_x < self.size_x as i32 - && board_y >= 0 - && board_y < self.size_y as i32; - - if !on_board { - return None; - } - - return Some((board_y * self.size_x as i32 + board_x) as usize); - } - pub fn empty() -> Board { - let size_x = PIXEL_BUFFER_WIDTH * 2; - let size_y = PIXEL_BUFFER_HEIGHT * 2; - let cells: Vec = vec![Cell::void(); (size_x * size_y) as usize]; - - Board { - size_x, - size_y, - cells, - } - } -} diff --git a/src/sim/cell.rs b/src/sim/cell.rs new file mode 100644 index 0000000..2e26ddc --- /dev/null +++ b/src/sim/cell.rs @@ -0,0 +1,19 @@ +use crate::sim::materials::MaterialId; + +#[derive(Clone, Copy)] +pub struct Cell { + pub material: MaterialId, + pub flags: u8, +} + +impl Cell { + pub fn void() -> Cell { + Cell { + material: MaterialId::Void, + flags: 0, + } + } + pub fn from_material(material: MaterialId) -> Cell { + Cell { material, flags: 0 } + } +} diff --git a/src/sim/chunk.rs b/src/sim/chunk.rs new file mode 100644 index 0000000..154cf82 --- /dev/null +++ b/src/sim/chunk.rs @@ -0,0 +1,25 @@ +use crate::{ + config::{CELLS_IN_CHUNK, CHUNK_SIZE}, + sim::cell::Cell, +}; + +pub struct Chunk { + pub cells: Box<[Cell; CELLS_IN_CHUNK as usize]>, +} + +impl Chunk { + #[inline] + pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] + } + #[inline] + pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] = cell; + } + + pub fn void() -> Self { + Chunk { + cells: Box::new([Cell::void(); CELLS_IN_CHUNK]), + } + } +} diff --git a/src/sim/materials/mod.rs b/src/sim/materials/mod.rs index 410236d..86ae7d5 100644 --- a/src/sim/materials/mod.rs +++ b/src/sim/materials/mod.rs @@ -14,7 +14,7 @@ pub enum MaterialId { pub struct MaterialDef { pub name: &'static str, - pub color: [u8; 4], + pub color: (u8, u8, u8, u8), pub density: u8, pub sim_update: Option ()>, } @@ -22,25 +22,25 @@ pub struct MaterialDef { static MATERIALS: [MaterialDef; 4] = [ MaterialDef { name: "Void", - color: [0x00, 0x00, 0x00, 0xFF], + color: (0x00, 0x00, 0x00, 0xFF), density: 0, sim_update: None, }, MaterialDef { name: "Sand", - color: [0xDE, 0xCB, 0x85, 0xFF], + color: (0xDE, 0xCB, 0x85, 0xFF), density: 50, sim_update: Some(sand::sim_update), }, MaterialDef { name: "Wood", - color: [0x85, 0x56, 0x1D, 0xFF], + color: (0x85, 0x56, 0x1D, 0xFF), density: 50, sim_update: None, }, MaterialDef { name: "Water", - color: [0x38, 0xA9, 0xFF, 0xFF], + color: (0x38, 0xA9, 0xFF, 0xFF), density: 40, sim_update: Some(water::sim_update), }, diff --git a/src/sim/materials/sand.rs b/src/sim/materials/sand.rs index 6a780f3..ac8889f 100644 --- a/src/sim/materials/sand.rs +++ b/src/sim/materials/sand.rs @@ -3,8 +3,8 @@ use crate::sim::sim::UpdateCtx; #[inline] pub fn sim_update(ctx: &mut UpdateCtx) { ctx.candidates_swap(&[ - (ctx.self_x, ctx.self_y + 1), - (ctx.self_x - 1 + 2 * ctx.seqno_parity as i32, ctx.self_y + 1), - (ctx.self_x + 1 - 2 * ctx.seqno_parity as i32, ctx.self_y + 1), + (0, 1), + (-1 + 2 * ctx.seqno_parity as i32, 1), + (1 - 2 * ctx.seqno_parity as i32, 1), ]); } diff --git a/src/sim/materials/water.rs b/src/sim/materials/water.rs index d875aee..65782dd 100644 --- a/src/sim/materials/water.rs +++ b/src/sim/materials/water.rs @@ -4,21 +4,21 @@ use crate::sim::sim::UpdateCtx; pub fn sim_update(ctx: &mut UpdateCtx) { // if the water can fall, do so if ctx.candidates_swap(&[ - (ctx.self_x, ctx.self_y + 1), - (ctx.self_x - 1 + 2 * ctx.seqno_parity as i32, ctx.self_y + 1), - (ctx.self_x + 1 - 2 * ctx.seqno_parity as i32, ctx.self_y + 1), + (0, 1), + (-1 + 2 * ctx.seqno_parity as i32, 1), + (1 - 2 * ctx.seqno_parity as i32, 1), ]) { return; } // if the water can't fall, check if we can move left or right // these are inverted on parity so that we don't preference a direction - let left_target = ctx.board.cell_at_position(ctx.self_x - 1, ctx.self_y); - let can_move_left = left_target - .is_some_and(|c| c.material.def().density < ctx.self_cell.material.def().density); - let right_target = ctx.board.cell_at_position(ctx.self_x + 1, ctx.self_y); - let can_move_right = right_target - .is_some_and(|c| c.material.def().density < ctx.self_cell.material.def().density); + let left_target = ctx.get_cell(-1, 0); + let can_move_left = + left_target.is_some_and(|c| c.material.def().density < ctx.material.density); + let right_target = ctx.get_cell(1, 0); + let can_move_right = + right_target.is_some_and(|c| c.material.def().density < ctx.material.density); // we can't move down or to other side, so we're stuck if !can_move_left && !can_move_right { @@ -40,39 +40,29 @@ pub fn sim_update(ctx: &mut UpdateCtx) { } let offset = side * (1 + i / 2); - let hole_target = ctx - .board - .cell_at_position(ctx.self_x + offset, ctx.self_y + 1); + let hole_target = ctx.get_cell(offset, 1); if let Some(target) = hole_target - && target.material.def().density < ctx.self_cell.material.def().density + && target.material.def().density < ctx.material.density { // we identified a hole and we know that the space on this side is open // move toward the hole - let move_target = if side == 1 { right_target } else { left_target }.clone(); // new_target.flags = new_target.flags ^ 0b1; - // safe to unwrap - ctx.board - .set_cell_at_position(ctx.self_x, ctx.self_y, move_target.unwrap()); - ctx.board - .set_cell_at_position(ctx.self_x + side, ctx.self_y, ctx.self_cell); + ctx.candidates_swap(&[(side, 0)]); return; } } // we didn't find a hole, so just move "randomly" on the same surface // TODO when to settle? - let (target, target_x) = if !can_move_left { - (right_target, 1) + let target_x = if !can_move_left { + 1 } else if !can_move_right { - (left_target, -1) + -1 } else if ctx.seqno_parity % 2 == 1 { - (right_target, 1) + 1 } else { - (left_target, -1) + -1 }; - ctx.board - .set_cell_at_position(ctx.self_x, ctx.self_y, target.unwrap()); - ctx.board - .set_cell_at_position(ctx.self_x + target_x, ctx.self_y, ctx.self_cell); + ctx.candidates_swap(&[(target_x, 0)]); } diff --git a/src/sim/mod.rs b/src/sim/mod.rs index 2b29628..7553f89 100644 --- a/src/sim/mod.rs +++ b/src/sim/mod.rs @@ -1,4 +1,6 @@ -pub mod board; +pub mod cell; +pub mod chunk; pub mod materials; pub mod overlay; pub mod sim; +pub mod world; diff --git a/src/sim/overlay.rs b/src/sim/overlay.rs index e0560b5..c9fdf17 100644 --- a/src/sim/overlay.rs +++ b/src/sim/overlay.rs @@ -1,48 +1,49 @@ -use crate::{Config, Input, sim::board::Board}; +use crate::{Config, Input, sim::world::World}; pub fn create_compute_combined_overlay_offset( - board: &Board, + world: &World, config: &Config, input: &Input, ) -> impl Fn(i32, i32) -> (u8, u8, u8, u8) { + puffin::profile_function!(); // TODO fix this move? - move |x: i32, y: i32| { + move |pixel_x: i32, pixel_y: i32| { // could allow negative offsets too let mut offset: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0x00); - // bounds - // left - let xl = -((board.size_x / 2 + 1) as i32); - // right - let xu = (board.size_x / 2 + 1) as i32; - // bottom - let yl = -((board.size_y / 2 + 1) as i32); - // top - let yu = (board.size_y / 2 + 1) as i32; + // // bounds + // // left + // let xl = -((board.get_game_width() / 2 + 1) as i32); + // // right + // let xu = (board.get_game_width() / 2 + 1) as i32; + // // bottom + // let yl = -((board.get_game_height() / 2 + 1) as i32); + // // top + // let yu = (board.get_game_height() / 2 + 1) as i32; - if ((x == xl || x == xu) && (y <= yu && y >= yl)) - || (y == yl || y == yu) && (x <= xu && x >= xl) - { - offset.0 = offset.0.saturating_add(0xFF); - offset.1 = offset.1.saturating_add(0xFF); - offset.2 = offset.2.saturating_add(0xFF); - } + // if ((pixel_x == xl || pixel_x == xu) && (pixel_y <= yu && pixel_y >= yl)) + // || (pixel_y == yl || pixel_y == yu) && (pixel_x <= xu && pixel_x >= xl) + // { + // offset.0 = offset.0.saturating_add(0xFF); + // offset.1 = offset.1.saturating_add(0xFF); + // offset.2 = offset.2.saturating_add(0xFF); + // } - // grid - if x % 30 == 0 || y % 30 == 0 { - offset.0 = offset.0.saturating_add(0x10); - offset.1 = offset.1.saturating_add(0x10); - offset.2 = offset.2.saturating_add(0x10); - } + // // grid + // if pixel_x % 30 == 0 || pixel_y % 30 == 0 { + // offset.0 = offset.0.saturating_add(0x10); + // offset.1 = offset.1.saturating_add(0x10); + // offset.2 = offset.2.saturating_add(0x10); + // } - // brush/selection - if input.last_mouse_pos_on_board.is_some_and(|p| { - ((x - p.0).pow(2) + (y - p.1).pow(2)) < (config.brush_radius as i32).pow(2) - }) { - offset.0 = offset.0.saturating_add(0x82); - offset.1 = offset.1.saturating_add(0xA1); - offset.2 = offset.2.saturating_add(0xAD); - } + // // brush/selection + // if input.last_mouse_pos_on_board.is_some_and(|p| { + // ((pixel_x - p.0).pow(2) + (pixel_y - p.1).pow(2)) < (config.brush_radius as i32).pow(2) + // }) { + // offset.0 = offset.0.saturating_add(0x82); + // offset.1 = offset.1.saturating_add(0xA1); + // offset.2 = offset.2.saturating_add(0xAD); + // } return offset; } diff --git a/src/sim/sim.rs b/src/sim/sim.rs index 350b823..5cd065e 100644 --- a/src/sim/sim.rs +++ b/src/sim/sim.rs @@ -1,26 +1,100 @@ -use crate::{Board, sim::board::Cell}; +use std::marker::PhantomData; -pub struct UpdateCtx<'a> { - pub self_x: i32, - pub self_y: i32, - pub self_cell: Cell, - pub delta_time: f32, +use crate::{ + config::CHUNK_SIZE, + sim::{cell::Cell, chunk::Chunk, materials::MaterialDef, world::World}, +}; + +struct ChunkAccess<'a> { + ptr: *mut Chunk, + len: usize, + _marker: PhantomData<&'a mut [Chunk]>, +} + +impl<'a> ChunkAccess<'a> { + pub fn new(chunks: &'a mut [Chunk]) -> Self { + Self { + ptr: chunks.as_mut_ptr(), + len: chunks.len(), + _marker: PhantomData, + } + } + unsafe fn get(&self, i: usize) -> &'a mut Chunk { + debug_assert!(i < self.len); + unsafe { &mut *self.ptr.add(i) } + } +} + +pub struct UpdateCtx<'a, 'b, 'c> { + pub chunks: &'a mut [Option<&'b mut Chunk>; 9], + pub seqno: u64, pub seqno_parity: u8, - pub board: &'a mut Board, + + 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 { + let dcx = x.div_euclid(CHUNK_SIZE as i32); + let dcy = y.div_euclid(CHUNK_SIZE as i32); + if dcx != 0 || dcy != 0 { + // in a different chunk + let nc_x = x.rem_euclid(CHUNK_SIZE as i32) as u8; + let nc_y = y.rem_euclid(CHUNK_SIZE as i32) as u8; + + return if let Some(chunk) = &chunks[(dcx + 1 + (dcy + 1) * 3) as usize] { + Some(chunk.get_cell_at_local_position(nc_x, nc_y)) + } else { + None + }; + } else { + if let Some(target) = &chunks[4] { + Some(target.get_cell_at_local_position(x as u8, y as u8)) + } else { + None + } + } +} + +pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell) { + let dcx = x.div_euclid(CHUNK_SIZE as i32); + let dcy = y.div_euclid(CHUNK_SIZE as i32); + if dcx != 0 || dcy != 0 { + // in a different chunk + let nc_x = x.rem_euclid(CHUNK_SIZE as i32) as u8; + let nc_y = y.rem_euclid(CHUNK_SIZE as i32) as u8; + + if let Some(chunk) = &mut chunks[(dcx + 1 + (dcy + 1) * 3) as usize] { + chunk.set_cell_at_local_position(nc_x, nc_y, cell); + } + } else { + if let Some(target) = &mut chunks[4] { + target.set_cell_at_local_position(x as u8, y as u8, cell); + } + } } -impl UpdateCtx<'_> { +impl UpdateCtx<'_, '_, '_> { + pub fn get_cell(&self, dx: i32, dy: i32) -> Option { + let x = self.x + dx; + let y = self.y + dy; + get_cell(self.chunks, x, y) + } + + fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { + let x = self.x + dx; + let y = self.y + dy; + set_cell(self.chunks, x, y, cell); + } + pub fn candidates_swap(&mut self, candidates: &[(i32, i32)]) -> bool { - for candidate in candidates { - let target = self.board.cell_at_position(candidate.0, candidate.1); - if let Some(target) = target - && target.material.def().density < self.self_cell.material.def().density - { - // swap the cells - self.board - .set_cell_at_position(self.self_x, self.self_y, target); - self.board - .set_cell_at_position(candidate.0, candidate.1, self.self_cell); + for &(dx, dy) in candidates { + let candidate_cell = self.get_cell(dx, dy); + if candidate_cell.is_some_and(|c| c.material.def().density < self.material.density) { + self.set_cell(0, 0, candidate_cell.unwrap()); + self.set_cell(dx, dy, *self.cell); return true; } } @@ -28,36 +102,90 @@ impl UpdateCtx<'_> { } } -// TODO: chunks -pub fn sim_tick(board: &mut Board, seqno: u64, delta_time: f32) { +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 bx = (board.size_x / 2) as i32; - let by = (board.size_y / 2) as i32; - for y in (-by..by + 1).rev() { - // invert scan order on every other frame - for col in -bx..bx + 1 { - let x = if seqno_parity == 0 { col } else { -col }; - - let cell = board.cell_at_position(x, y); - if let Some(mut cur) = cell - && cur.flags & 0b1 == seqno_parity - { - // flip the parity bit - cur.flags = cur.flags ^ 0b1; - - if let Some(update) = cur.material.def().sim_update { - update(&mut UpdateCtx { - self_x: x, - self_y: y, - self_cell: cur, - board, - delta_time, + if chunks[4].is_some() { + for y in (0..CHUNK_SIZE as i32).rev() { + for i in 0..CHUNK_SIZE as i32 { + let x = if seqno_parity == 0 { + i + } else { + (CHUNK_SIZE as i32) - i - 1 + }; + + 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 mut update_ctx = UpdateCtx { + chunks, + seqno, seqno_parity, - }); + + x, + y, + cell: &mut cell, + material, + }; + + if let Some(update) = material.sim_update { + update(&mut update_ctx); + } } } } } } + +const NEIGHBORHOOD_OFFSETS: [(i32, i32); 9] = [ + (-1, -1), + (0, -1), + (1, -1), + (-1, 0), + (0, 0), + (1, 0), + (-1, 1), + (0, 1), + (1, 1), +]; + +pub fn sim_tick(world: &mut World, seqno: u64) { + puffin::profile_function!(); + let mut update_groups: [Vec<(i32, i32)>; 9] = Default::default(); + + // assign a color to each chunk s.t. every chunk is surrounded by <= 8 chunks of different colors + // -------------------- + // | 0, 1, 2, 0, 1, 2 | + // | 3, 4, 5, 3, 4, 5 | + // | 6, 7, 8, 6, 7, 8 | + // | 0, 1, 2, 0, 1, 2 | + // | 3, 4, 5, 3, 4, 5 | + // | 6, 7, 8, 6, 7, 8 | + // -------------------- + + for (&(cx, cy), _) in &world.chunk_position_to_chunk_idx { + let color = (cx.rem_euclid(3) * 3 + cy.rem_euclid(3)) as usize; + update_groups[color].push((cx, cy)); + } + + for group in &update_groups { + let access = ChunkAccess::new(&mut world.chunks); + // TODO this can be parallelized since they will never share neighbours + for &(cx, cy) in group { + let mut chunks: [Option<&mut Chunk>; 9] = NEIGHBORHOOD_OFFSETS.map(|(dx, dy)| { + world + .chunk_position_to_chunk_idx + .get(&(cx + dx, cy + dy)) + .map(|&idx| unsafe { access.get(idx) }) + }); + + sim_tick_chunk(&mut chunks, seqno); + } + } +} diff --git a/src/sim/world.rs b/src/sim/world.rs new file mode 100644 index 0000000..3c7b05b --- /dev/null +++ b/src/sim/world.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use crate::{ + config::CHUNK_SIZE, + sim::{cell::Cell, chunk::Chunk}, +}; + +pub struct World { + pub chunks: Vec, + // TODO FxHashMap? + pub chunk_position_to_chunk_idx: HashMap<(i32, i32), usize>, +} + +impl World { + #[inline] + pub fn split_game_position(x: i32, y: i32) -> ((i32, i32), (u8, u8)) { + ( + ( + // TODO is this cast expensive? + x.div_euclid(CHUNK_SIZE as i32), + y.div_euclid(CHUNK_SIZE as i32), + ), + ( + x.rem_euclid(CHUNK_SIZE as i32) as u8, + y.rem_euclid(CHUNK_SIZE as i32) as u8, + ), + ) + } + + // VERY EXPENSIVE + pub fn get_cell_from_game_position(&self, x: i32, y: i32) -> Option { + let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); + self.chunk_position_to_chunk_idx + .get(&(cx, cy)) + .map(|&idx| self.chunks[idx].get_cell_at_local_position(dx, dy)) + } + + // VERY EXPENSIVE + pub fn set_cell_from_game_position(&mut self, x: i32, y: i32, cell: Cell) -> () { + let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); + if let Some(&idx) = self.chunk_position_to_chunk_idx.get(&(cx, cy)) { + self.chunks[idx].set_cell_at_local_position(dx, dy, cell); + } + } + + pub fn insert(&mut self, x: i32, y: i32, chunk: Chunk) -> () { + self.chunk_position_to_chunk_idx + .insert((x, y), self.chunks.len()); + self.chunks.push(chunk); + } + + pub fn from_default_size() -> Self { + let mut world = World { + chunks: Vec::new(), + chunk_position_to_chunk_idx: HashMap::new(), + }; + + for y in -10..10 { + for x in -10..10 { + world.insert(x, y, Chunk::void()); + } + } + + world + } +} diff --git a/src/ui.rs b/src/ui.rs index e55aff1..9c8f6c3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,6 +9,7 @@ pub fn draw_egui<'a>( diagnostics: &Diagnostics, input: &Input, ) { + puffin::profile_function!(); ui.heading("Config"); ui.add(egui::Slider::new(&mut config.brush_radius, 1..=100).text("Brush radius")); @@ -25,7 +26,7 @@ pub fn draw_egui<'a>( center: rect.center(), radius: rect.width() / 2.5, stroke: Stroke::NONE, - fill: Color32::from_rgb(material.color[0], material.color[1], material.color[2]), + fill: Color32::from_rgb(material.color.0, material.color.1, material.color.2), })); }) .show_ui(ui, |ui| { @@ -59,4 +60,8 @@ 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,))); } -- cgit v1.3.1