summaryrefslogtreecommitdiff
path: root/src/sim/cell_sim
diff options
context:
space:
mode:
Diffstat (limited to 'src/sim/cell_sim')
-rw-r--r--src/sim/cell_sim/chunk.rs29
-rw-r--r--src/sim/cell_sim/mod.rs4
-rw-r--r--src/sim/cell_sim/overlay.rs50
-rw-r--r--src/sim/cell_sim/sim.rs234
-rw-r--r--src/sim/cell_sim/world.rs69
5 files changed, 386 insertions, 0 deletions
diff --git a/src/sim/cell_sim/chunk.rs b/src/sim/cell_sim/chunk.rs
new file mode 100644
index 0000000..116a38a
--- /dev/null
+++ b/src/sim/cell_sim/chunk.rs
@@ -0,0 +1,29 @@
+use crate::{
+ config::{CELLS_IN_CHUNK, CHUNK_SIZE},
+ sim::cell::cell::Cell,
+};
+
+pub struct Chunk {
+ pub cells: Box<[Cell; CELLS_IN_CHUNK]>,
+ pub sleeping: bool,
+ pub needs_texture_update: bool,
+}
+
+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]),
+ sleeping: true,
+ needs_texture_update: true,
+ }
+ }
+}
diff --git a/src/sim/cell_sim/mod.rs b/src/sim/cell_sim/mod.rs
new file mode 100644
index 0000000..a1db2a1
--- /dev/null
+++ b/src/sim/cell_sim/mod.rs
@@ -0,0 +1,4 @@
+pub mod chunk;
+pub mod overlay;
+pub mod sim;
+pub mod world;
diff --git a/src/sim/cell_sim/overlay.rs b/src/sim/cell_sim/overlay.rs
new file mode 100644
index 0000000..ee494ae
--- /dev/null
+++ b/src/sim/cell_sim/overlay.rs
@@ -0,0 +1,50 @@
+use crate::{Config, Input, sim::cell_sim::world::World};
+
+pub fn create_compute_combined_overlay_offset(
+ world: &World,
+ config: &Config,
+ input: &Input,
+) -> impl Fn(i32, i32) -> (u8, u8, u8, u8) {
+ puffin::profile_function!();
+ // TODO fix this move?
+ 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.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 ((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 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| {
+ // ((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/cell_sim/sim.rs b/src/sim/cell_sim/sim.rs
new file mode 100644
index 0000000..fd9b6c9
--- /dev/null
+++ b/src/sim/cell_sim/sim.rs
@@ -0,0 +1,234 @@
+use std::marker::PhantomData;
+
+use fxhash::FxHashMap;
+use rand::{Rng, SeedableRng, rngs::SmallRng};
+use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
+
+use crate::{
+ config::CHUNK_SIZE,
+ sim::{
+ cell::{cell::Cell, materials::MaterialDef},
+ cell_sim::{chunk::Chunk, 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) }
+ }
+}
+
+unsafe impl Sync for ChunkAccess<'_> {}
+
+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);
+ if dcx != 0 || dcy != 0 {
+ // in a different chunk
+ let nc_x = x.rem_euclid(CHUNK_SIZE) as u8;
+ let nc_y = y.rem_euclid(CHUNK_SIZE) as u8;
+
+ chunks[(dcx + 1 + (dcy + 1) * 3) as usize]
+ .as_ref()
+ .map(|chunk| chunk.get_cell_at_local_position(nc_x, nc_y))
+ } else {
+ chunks[4]
+ .as_ref()
+ .map(|target| target.get_cell_at_local_position(x as u8, y as u8))
+ }
+}
+
+pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell) {
+ let dcx = x.div_euclid(CHUNK_SIZE);
+ let dcy = y.div_euclid(CHUNK_SIZE);
+ if dcx != 0 || dcy != 0 {
+ // in a different chunk
+ let nc_x = x.rem_euclid(CHUNK_SIZE) as u8;
+ let nc_y = y.rem_euclid(CHUNK_SIZE) 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);
+ chunk.needs_texture_update = true;
+ }
+ } 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;
+ }
+ }
+}
+
+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;
+ let y = self.y + dy;
+ get_cell(self.chunks, x, y)
+ }
+
+ 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);
+ debug_assert!(dy > -15 && dy < 15);
+ let x = self.x + dx;
+ let y = self.y + dy;
+ set_cell(self.chunks, x, y, cell);
+ self.chunks.iter_mut().for_each(|c| {
+ if let Some(chunk) = c {
+ chunk.sleeping = false;
+ }
+ })
+ }
+
+ pub fn candidates_swap(&mut self, candidates: &[(i32, i32)]) -> bool {
+ 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;
+ }
+ }
+ false
+ }
+}
+
+pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) {
+ puffin::profile_function!();
+ 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() {
+ for i in 0..CHUNK_SIZE {
+ let x = if seqno_parity == 0 {
+ i
+ } else {
+ (CHUNK_SIZE) - i - 1
+ };
+
+ let mut cell = get_cell(chunks, x, y).unwrap();
+ let material = cell.material.def();
+
+ if let Some(update) = material.sim_update
+ && 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,
+
+ x,
+ y,
+ cell: &mut cell,
+ material,
+
+ // TODO this is platform-dependent, will break for multiplayer
+ rng: &mut rng,
+ };
+
+ 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, use_threading: bool) {
+ puffin::profile_function!();
+
+ let mut columns: FxHashMap<i32, Vec<i32>> = FxHashMap::default();
+ for &(cx, cy) in world.chunk_position_to_chunk_idx.keys() {
+ columns.entry(cx).or_default().push(cy);
+ }
+
+ // color columns s.t. columns of same color are separated by two columns
+ // and sort the column bottom-to-top
+ // --------------------
+ // | 0, 1, 2, 0, 1, 2 |
+ // | 0, 1, 2, 0, 1, 2 |
+ // | 0, 1, 2, 0, 1, 2 |
+ // --------------------
+ let mut columns_by_color: [Vec<(i32, Vec<i32>)>; 3] = Default::default();
+ for (cx, mut cys) in columns {
+ cys.sort_unstable_by(|a, b| b.cmp(a));
+ columns_by_color[cx.rem_euclid(3) as usize].push((cx, cys));
+ }
+
+ let access = ChunkAccess::new(&mut world.chunks);
+
+ for color in &columns_by_color {
+ puffin::profile_scope!("chunk_color");
+
+ let chunk_closure = |(cx, cys): &(i32, Vec<i32>)| {
+ let cx = *cx;
+ for &cy in cys {
+ 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) })
+ });
+
+ if let Some(target) = &mut chunks[4] {
+ if target.sleeping {
+ continue;
+ }
+ target.sleeping = true;
+ }
+
+ sim_tick_chunk(&mut chunks, seqno);
+ }
+ };
+
+ if use_threading {
+ // TODO use forte
+ color.par_iter().for_each(chunk_closure);
+ } else {
+ color.iter().for_each(chunk_closure);
+ };
+ }
+}
diff --git a/src/sim/cell_sim/world.rs b/src/sim/cell_sim/world.rs
new file mode 100644
index 0000000..924639a
--- /dev/null
+++ b/src/sim/cell_sim/world.rs
@@ -0,0 +1,69 @@
+use fxhash::FxHashMap;
+
+use crate::{
+ config::CHUNK_SIZE,
+ sim::{cell::cell::Cell, cell_sim::chunk::Chunk},
+};
+
+pub struct World {
+ pub chunks: Vec<Chunk>,
+ // TODO FxFxHashMap?
+ pub chunk_position_to_chunk_idx: FxHashMap<(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),
+ y.div_euclid(CHUNK_SIZE),
+ ),
+ (
+ x.rem_euclid(CHUNK_SIZE) as u8,
+ y.rem_euclid(CHUNK_SIZE) as u8,
+ ),
+ )
+ }
+
+ // VERY EXPENSIVE
+ pub fn get_cell_from_game_position(&self, x: i32, y: i32) -> Option<Cell> {
+ 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, sleeping: bool) {
+ 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);
+ // this is a temporary hack
+ self.chunks[idx].sleeping = sleeping;
+ self.chunks[idx].needs_texture_update = true;
+ }
+ }
+
+ 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: FxHashMap::default(),
+ };
+
+ for y in -10..1 {
+ for x in -100..100 {
+ world.insert(x, y, Chunk::void());
+ }
+ }
+
+ world
+ }
+}