From 6ba5167b64efe787dd1275c3c3b095312184b975 Mon Sep 17 00:00:00 2001 From: Dylan Staatz Date: Fri, 18 Mar 2022 16:20:19 -0400 Subject: [PATCH 1/4] Add cell_multithreaded_v2 --- src/cell_event.rs | 1 - src/cells_multithreaded_v2.rs | 205 ++++++++++++++++++++++++++++++++++ src/main.rs | 22 ++-- 3 files changed, 218 insertions(+), 10 deletions(-) delete mode 100644 src/cell_event.rs create mode 100644 src/cells_multithreaded_v2.rs diff --git a/src/cell_event.rs b/src/cell_event.rs deleted file mode 100644 index b00d483..0000000 --- a/src/cell_event.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct CellStatesChangedEvent; diff --git a/src/cells_multithreaded_v2.rs b/src/cells_multithreaded_v2.rs new file mode 100644 index 0000000..efe74d0 --- /dev/null +++ b/src/cells_multithreaded_v2.rs @@ -0,0 +1,205 @@ +use std::collections::{HashSet}; +use std::hash::Hash; + +use bevy::tasks::ComputeTaskPool; +use bevy::{ + input::Input, + math::{ivec3, vec3, IVec3}, + prelude::*, +}; +use rand::Rng; + +use crate::rotating_camera::UpdateEvent; +use crate::{ + cell_renderer::{InstanceData, InstanceMaterialData}, + rule::Rule, + utils::{self, keep_in_bounds}, +}; + +/// The state of all cells last frame +#[derive(Default)] +struct PrevCells { + states: HashSet, +} + +impl PrevCells { + // Returns the number of neighbors at a given position + fn neighbor_count(&self, rule: &Rule, pos: &IVec3) -> u8 { + rule.neighbour_method + .get_neighbour_iter() + .iter() + .filter(|&dir| { + let mut neighbour_pos = *pos + *dir; + keep_in_bounds(rule.bounding_size, &mut neighbour_pos); + + self.states.contains(&neighbour_pos) + }) + .count() + .try_into() + .unwrap() + } +} + +/// A single cell +#[derive(Debug, Component)] +struct Cell { + /// The position index of the cell + pos: IVec3, + + /// The cell that might be at this position + /// + /// Stores (value, neighbors) + state: Option<(u8, u8)>, +} + +impl Cell { + fn new(pos: IVec3) -> Self { + Self { pos, state: None } + } +} + +impl From<(i32, i32, i32)> for Cell { + fn from(pos: (i32, i32, i32)) -> Self { + Cell::new(pos.into()) + } +} + +/// Spawn all the nessasary `Cell` entities and setup `PrevCells` +fn init_cells(rule: Res, mut commands: Commands) { + let bounds = rule.get_bounding_ranges(); + for i in bounds.0 { + for j in bounds.1.clone() { + for k in bounds.2.clone() { + commands.spawn().insert(Cell::from((i, j, k))); + } + } + } +} + +/// Spawn hunk of noise if the input was given +fn spawn_noise(keyboard_input: Res>, mut cells: ResMut) { + if keyboard_input.just_pressed(KeyCode::P) { + info!("Spawn noise"); + + // Different version of `utils::spawn_noise` + let mut rand = rand::thread_rng(); + let spawn_size = 6; + (0..12 * 12 * 12).for_each(|_i| { + let pos = ivec3( + rand.gen_range(-spawn_size..=spawn_size), + rand.gen_range(-spawn_size..=spawn_size), + rand.gen_range(-spawn_size..=spawn_size), + ); + cells.states.insert(pos); + }); + } +} + +/// Step all `Cell` entities forward based on `PrevCells` if the input was given +fn tick_cells( + task_pool: Res, + rule: Res, + keyboard_input: Res>, + prev_cells: Res, + mut cell: Query<&mut Cell>, + mut cell_event: EventWriter, +) { + if !keyboard_input.pressed(KeyCode::E) { + return; + } + + info!("Tick Cells"); + + // Modifiy all cells in parallel in batches of 32 + cell.par_for_each_mut(&task_pool, 32, |mut cell| { + let neighbor_count = prev_cells.neighbor_count(&rule, &cell.pos); + + if let Some(ref mut cell_state) = cell.state { + // Decrement value if survival rule isn't passed + if !(cell_state.0 == rule.states && rule.survival_rule.in_range(neighbor_count)) { + cell_state.0 -= 1; + cell_state.1 = neighbor_count; + + // Destroy if no value left + if cell_state.0 == 0 { + cell.state = None; + } + } + } else { + // Check for birth + if rule.birth_rule.in_range(neighbor_count) { + cell.state = Some((rule.states, neighbor_count)); + } + } + }); + + cell_event.send(UpdateEvent); +} + +/// Save all important `Cell` entities to `PrevCells` for the next frame +fn save(rule: Res, mut cells: ResMut, query: Query<&Cell>) { + cells.states.clear(); + + // TODO: parallelize + // TODO: could calculate neighbors for next step at this time + query.for_each(|cell| { + if let Some(cell_state) = cell.state { + if cell_state.0 == rule.states { + cells.states.insert(cell.pos.clone()); + } + } + }); +} + +/// Convert `Cell` entities to `InstanceMaterialData` +fn prepare_cell_data( + rule: Res, + cells: Query<&Cell>, + mut query: Query<&mut InstanceMaterialData>, +) { + // take the first + let mut instance_data = query.iter_mut().next().unwrap(); + instance_data.0.clear(); + for cell in cells.iter() { + let pos = cell.pos; + + if let Some(ref state) = cell.state { + let dist_to_center = utils::dist_to_center(pos, &rule); + + instance_data.0.push(InstanceData { + position: vec3(pos.x as f32, pos.y as f32, pos.z as f32), + scale: 1.0, + color: rule + .color_method + .color(rule.states, state.0, state.1, dist_to_center) + .as_rgba_f32(), + //color: Color::rgba(state.value as f32 / rule.states as f32, 0.0, 0.0, 1.0) + // .as_rgba_f32(), + }); + } + } +} + +/// The system stages to do in order +#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemLabel)] +pub enum Stages { + SpawnNoise, + TickCells, +} + +pub struct CellsMultithreadedV2Plugin; + +impl Plugin for CellsMultithreadedV2Plugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.init_resource::() + .add_startup_system(init_cells) + .add_system(spawn_noise.label(Stages::SpawnNoise)) + .add_system( + tick_cells + .label(Stages::TickCells) + .after(Stages::SpawnNoise), + ) + .add_system(save.after(Stages::TickCells)) + .add_system(prepare_cell_data.after(Stages::TickCells)); + } +} diff --git a/src/main.rs b/src/main.rs index 4f0439f..1798073 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,20 @@ -use bevy::{prelude::*, render::view::NoFrustumCulling}; -use cell_event::CellStatesChangedEvent; -pub mod cell_event; mod cell_renderer; mod cells_multithreaded; +mod cells_multithreaded_v2; mod cells_single_threaded; mod neighbours; mod rotating_camera; mod rule; mod utils; + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +use bevy::{prelude::*, render::view::NoFrustumCulling}; + use cell_renderer::*; -//use cells_multithreaded::*; -use cells_single_threaded::*; +use cells_multithreaded_v2::*; +// use cells_multithreaded::*; +// use cells_single_threaded::*; use neighbours::NeighbourMethod; use rotating_camera::{RotatingCamera, RotatingCameraPlugin}; use rule::*; @@ -98,7 +102,7 @@ fn main() { //color_method: ColorMethod::StateLerp(Color::RED, Color::BLUE), //neighbour_method: NeighbourMethod::Moore, - // LARGE LINES + // LARGE LINES` //survival_rule: Value::Singles(vec![5]), //birth_rule: Value::Singles(vec![4, 6, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 24]), //states: 35, @@ -113,13 +117,13 @@ fn main() { .insert_resource(task_pool_settings) .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::rgb(0.65f32, 0.9f32, 0.96f32))) - .add_event::() .add_plugin(RotatingCameraPlugin) .add_plugin(CellMaterialPlugin) .insert_resource(rule) // you can swap out the different implementations - //.add_plugin(CellsMultithreadedPlugin) - .add_plugin(CellsSinglethreadedPlugin) + .add_plugin(CellsMultithreadedV2Plugin) + // .add_plugin(CellsMultithreadedPlugin) + // .add_plugin(CellsSinglethreadedPlugin) .add_startup_system(setup) .run(); } From 05cfc6abaaac2a9cba273a17acaa9cb565c7519f Mon Sep 17 00:00:00 2001 From: Dylan Staatz Date: Fri, 18 Mar 2022 18:05:44 -0400 Subject: [PATCH 2/4] Integrated neighbor caching strategy to cells_multitreaded_v2 --- src/cells_multithreaded_v2.rs | 52 +++++++++++++++++++++++++++-------- src/main.rs | 6 ++-- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/cells_multithreaded_v2.rs b/src/cells_multithreaded_v2.rs index efe74d0..0904ea2 100644 --- a/src/cells_multithreaded_v2.rs +++ b/src/cells_multithreaded_v2.rs @@ -1,5 +1,7 @@ -use std::collections::{HashSet}; +use std::collections::{HashSet, HashMap}; use std::hash::Hash; +use std::sync::{Mutex, Arc}; +use std::time::Instant; use bevy::tasks::ComputeTaskPool; use bevy::{ @@ -23,8 +25,8 @@ struct PrevCells { } impl PrevCells { - // Returns the number of neighbors at a given position - fn neighbor_count(&self, rule: &Rule, pos: &IVec3) -> u8 { + // Returns the number of neighbours at a given position + fn neighbour_count(&self, rule: &Rule, pos: &IVec3) -> u8 { rule.neighbour_method .get_neighbour_iter() .iter() @@ -48,7 +50,7 @@ struct Cell { /// The cell that might be at this position /// - /// Stores (value, neighbors) + /// Stores (value, neighbours) state: Option<(u8, u8)>, } @@ -108,17 +110,35 @@ fn tick_cells( return; } - info!("Tick Cells"); + let timer = Instant::now(); + + let mut neighbours = HashMap::new(); + + for pos in prev_cells.states.iter() { + + for dir in rule.neighbour_method.get_neighbour_iter() { + let mut neighbour_pos = *pos + *dir; + keep_in_bounds(rule.bounding_size, &mut neighbour_pos); + + if !neighbours.contains_key(&neighbour_pos) { + neighbours.insert(neighbour_pos, 0); + } + let neighbour = neighbours.get_mut(&neighbour_pos).unwrap(); + *neighbour += 1; + } + } + info!("Tick neighbours: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); // Modifiy all cells in parallel in batches of 32 + let timer = Instant::now(); cell.par_for_each_mut(&task_pool, 32, |mut cell| { - let neighbor_count = prev_cells.neighbor_count(&rule, &cell.pos); + let neighbour_count = neighbours.get(&cell.pos).cloned().unwrap_or(0); if let Some(ref mut cell_state) = cell.state { // Decrement value if survival rule isn't passed - if !(cell_state.0 == rule.states && rule.survival_rule.in_range(neighbor_count)) { + if !(cell_state.0 == rule.states && rule.survival_rule.in_range(neighbour_count)) { cell_state.0 -= 1; - cell_state.1 = neighbor_count; + cell_state.1 = neighbour_count; // Destroy if no value left if cell_state.0 == 0 { @@ -127,21 +147,25 @@ fn tick_cells( } } else { // Check for birth - if rule.birth_rule.in_range(neighbor_count) { - cell.state = Some((rule.states, neighbor_count)); + if rule.birth_rule.in_range(neighbour_count) { + cell.state = Some((rule.states, neighbour_count)); } } }); cell_event.send(UpdateEvent); + + info!("Tick Modifiy: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); } /// Save all important `Cell` entities to `PrevCells` for the next frame fn save(rule: Res, mut cells: ResMut, query: Query<&Cell>) { + let timer = Instant::now(); + cells.states.clear(); // TODO: parallelize - // TODO: could calculate neighbors for next step at this time + // TODO: could calculate neighbours for next step at this time query.for_each(|cell| { if let Some(cell_state) = cell.state { if cell_state.0 == rule.states { @@ -149,6 +173,8 @@ fn save(rule: Res, mut cells: ResMut, query: Query<&Cell>) { } } }); + + info!("Save: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); } /// Convert `Cell` entities to `InstanceMaterialData` @@ -157,6 +183,8 @@ fn prepare_cell_data( cells: Query<&Cell>, mut query: Query<&mut InstanceMaterialData>, ) { + let timer = Instant::now(); + // take the first let mut instance_data = query.iter_mut().next().unwrap(); instance_data.0.clear(); @@ -178,6 +206,8 @@ fn prepare_cell_data( }); } } + + info!("Prepare: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); } /// The system stages to do in order diff --git a/src/main.rs b/src/main.rs index 1798073..95a2e59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,8 +13,8 @@ use bevy::{prelude::*, render::view::NoFrustumCulling}; use cell_renderer::*; use cells_multithreaded_v2::*; -// use cells_multithreaded::*; -// use cells_single_threaded::*; +use cells_multithreaded::*; +use cells_single_threaded::*; use neighbours::NeighbourMethod; use rotating_camera::{RotatingCamera, RotatingCameraPlugin}; use rule::*; @@ -38,7 +38,7 @@ impl CellState { fn main() { let rule = Rule { - bounding_size: 25, + bounding_size: 50, // builder survival_rule: Value::Singles(vec![2, 6, 9]), From 45ca28192a286ec90e7fdb3dd76cae99904728d8 Mon Sep 17 00:00:00 2001 From: Dylan Staatz Date: Fri, 18 Mar 2022 19:54:30 -0400 Subject: [PATCH 3/4] Used atomics for neighbors calculation --- src/cells_multithreaded_v2.rs | 143 ++++++++++++++++++++++++++-------- src/main.rs | 8 +- 2 files changed, 115 insertions(+), 36 deletions(-) diff --git a/src/cells_multithreaded_v2.rs b/src/cells_multithreaded_v2.rs index 0904ea2..d412968 100644 --- a/src/cells_multithreaded_v2.rs +++ b/src/cells_multithreaded_v2.rs @@ -1,9 +1,9 @@ -use std::collections::{HashSet, HashMap}; +use std::collections::HashSet; use std::hash::Hash; -use std::sync::{Mutex, Arc}; +use std::sync::atomic::{AtomicU8, Ordering}; use std::time::Instant; -use bevy::tasks::ComputeTaskPool; +use bevy::tasks::{ComputeTaskPool, TaskPool}; use bevy::{ input::Input, math::{ivec3, vec3, IVec3}, @@ -24,21 +24,74 @@ struct PrevCells { states: HashSet, } -impl PrevCells { - // Returns the number of neighbours at a given position - fn neighbour_count(&self, rule: &Rule, pos: &IVec3) -> u8 { - rule.neighbour_method - .get_neighbour_iter() - .iter() - .filter(|&dir| { - let mut neighbour_pos = *pos + *dir; - keep_in_bounds(rule.bounding_size, &mut neighbour_pos); +struct Neighbors { + /// Cache of [`Rule::bounding_size`] + bounding_size: i32, + + /// 3D arrary of all neighbor values + data: Vec>>, +} - self.states.contains(&neighbour_pos) +impl Neighbors { + fn zeros(rule: &Rule) -> Self { + let bounds = rule.get_bounding_ranges(); + + let vec = bounds + .0 + .into_iter() + .map(|_| { + bounds + .1 + .clone() + .into_iter() + .map(|_| { + bounds + .2 + .clone() + .into_iter() + .map(|_| AtomicU8::new(0)) + .collect::>() + }) + .collect::>() }) - .count() - .try_into() - .unwrap() + .collect::>(); + + Self { + bounding_size: rule.bounding_size, + data: vec, + } + } + + fn inc(&self, index: &IVec3) { + self.get(index).fetch_add(1, Ordering::Relaxed); + } + + fn get(&self, index: &IVec3) -> &AtomicU8 { + let (i, j, k) = self.convert_index(index); + &self.data[i][j][k] + } + + fn convert_index(&self, index: &IVec3) -> (usize, usize, usize) { + ( + (index.x + self.bounding_size).try_into().unwrap(), + (index.y + self.bounding_size).try_into().unwrap(), + (index.z + self.bounding_size).try_into().unwrap(), + ) + } + + fn clear(&self, _task_pool: &TaskPool) { + // TODO: parallelize + for val in self.iter() { + val.store(0, Ordering::Relaxed); + } + } + + fn iter<'a>(&'a self) -> impl Iterator { + self.data + .iter() + .map(|data| data.iter().map(|data| data.iter())) + .flatten() + .flatten() } } @@ -112,27 +165,49 @@ fn tick_cells( let timer = Instant::now(); - let mut neighbours = HashMap::new(); + let num_threads = 8; + info!("num_threads: {}", num_threads); + info!("prev_cells.states.len(): {}", prev_cells.states.len()); - for pos in prev_cells.states.iter() { + let neighbours = Neighbors::zeros(&rule); - for dir in rule.neighbour_method.get_neighbour_iter() { - let mut neighbour_pos = *pos + *dir; - keep_in_bounds(rule.bounding_size, &mut neighbour_pos); + task_pool.scope(|s| { + let neighbours = &neighbours; + let prev_cells = &prev_cells; + let rule = &rule; - if !neighbours.contains_key(&neighbour_pos) { - neighbours.insert(neighbour_pos, 0); - } - let neighbour = neighbours.get_mut(&neighbour_pos).unwrap(); - *neighbour += 1; + for thread_id in 0..num_threads { + s.spawn(async move { + let mut iter = prev_cells.states.iter(); + + // Advance to the threads starting point + for _ in 0..thread_id { + if iter.next().is_none() { + // We know the iterator is fused + break; + } + } + + for pos in iter.step_by(num_threads) { + for dir in rule.neighbour_method.get_neighbour_iter() { + let mut neighbour_pos = *pos + *dir; + keep_in_bounds(rule.bounding_size, &mut neighbour_pos); + neighbours.inc(&neighbour_pos); + } + } + }); } - } - info!("Tick neighbours: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); + }); + + info!( + "Tick neighbours: {:.3} ms", + timer.elapsed().as_secs_f64() * 1000.0 + ); // Modifiy all cells in parallel in batches of 32 let timer = Instant::now(); cell.par_for_each_mut(&task_pool, 32, |mut cell| { - let neighbour_count = neighbours.get(&cell.pos).cloned().unwrap_or(0); + let neighbour_count = neighbours.get(&cell.pos).load(Ordering::Relaxed); if let Some(ref mut cell_state) = cell.state { // Decrement value if survival rule isn't passed @@ -155,7 +230,10 @@ fn tick_cells( cell_event.send(UpdateEvent); - info!("Tick Modifiy: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); + info!( + "Tick Modifiy: {:.3} ms", + timer.elapsed().as_secs_f64() * 1000.0 + ); } /// Save all important `Cell` entities to `PrevCells` for the next frame @@ -165,7 +243,8 @@ fn save(rule: Res, mut cells: ResMut, query: Query<&Cell>) { cells.states.clear(); // TODO: parallelize - // TODO: could calculate neighbours for next step at this time + // TODO: could calculate neighbours for next step at this time, then neighbors could be + // calculated while rendering query.for_each(|cell| { if let Some(cell_state) = cell.state { if cell_state.0 == rule.states { @@ -184,7 +263,7 @@ fn prepare_cell_data( mut query: Query<&mut InstanceMaterialData>, ) { let timer = Instant::now(); - + // take the first let mut instance_data = query.iter_mut().next().unwrap(); instance_data.0.clear(); diff --git a/src/main.rs b/src/main.rs index 95a2e59..9b31086 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,8 @@ mod utils; use bevy::{prelude::*, render::view::NoFrustumCulling}; use cell_renderer::*; -use cells_multithreaded_v2::*; -use cells_multithreaded::*; +// use cells_multithreaded::*; +// use cells_multithreaded_v2::*; use cells_single_threaded::*; use neighbours::NeighbourMethod; use rotating_camera::{RotatingCamera, RotatingCameraPlugin}; @@ -121,9 +121,9 @@ fn main() { .add_plugin(CellMaterialPlugin) .insert_resource(rule) // you can swap out the different implementations - .add_plugin(CellsMultithreadedV2Plugin) + // .add_plugin(CellsMultithreadedV2Plugin) // .add_plugin(CellsMultithreadedPlugin) - // .add_plugin(CellsSinglethreadedPlugin) + .add_plugin(CellsSinglethreadedPlugin) .add_startup_system(setup) .run(); } From a1cecd61892a82db310ca23444db704ea0c5b631 Mon Sep 17 00:00:00 2001 From: Dylan Staatz Date: Sat, 19 Mar 2022 02:53:53 -0400 Subject: [PATCH 4/4] Restructured and cleaned up multithreaded_v2 --- src/cells_multithreaded_v2.rs | 375 +++++++++++++++++----------------- 1 file changed, 187 insertions(+), 188 deletions(-) diff --git a/src/cells_multithreaded_v2.rs b/src/cells_multithreaded_v2.rs index d412968..fd925b4 100644 --- a/src/cells_multithreaded_v2.rs +++ b/src/cells_multithreaded_v2.rs @@ -1,9 +1,9 @@ -use std::collections::HashSet; use std::hash::Hash; +use std::ops::{Index, IndexMut}; use std::sync::atomic::{AtomicU8, Ordering}; use std::time::Instant; -use bevy::tasks::{ComputeTaskPool, TaskPool}; +use bevy::tasks::{ComputeTaskPool, ParallelSlice, TaskPool, ParallelSliceMut}; use bevy::{ input::Input, math::{ivec3, vec3, IVec3}, @@ -18,124 +18,81 @@ use crate::{ utils::{self, keep_in_bounds}, }; -/// The state of all cells last frame -#[derive(Default)] -struct PrevCells { - states: HashSet, -} - -struct Neighbors { +struct Cells { /// Cache of [`Rule::bounding_size`] bounding_size: i32, - /// 3D arrary of all neighbor values - data: Vec>>, + /// length of one side of the 3d array + side_length: usize, + + /// 3D arrary of (value, neighbours) + data: Vec<(u8, AtomicU8)>, } -impl Neighbors { +impl Cells { fn zeros(rule: &Rule) -> Self { - let bounds = rule.get_bounding_ranges(); - - let vec = bounds - .0 - .into_iter() - .map(|_| { - bounds - .1 - .clone() - .into_iter() - .map(|_| { - bounds - .2 - .clone() - .into_iter() - .map(|_| AtomicU8::new(0)) - .collect::>() - }) - .collect::>() - }) - .collect::>(); + + let side_length: usize = (rule.bounding_size * 2 + 1).try_into().unwrap(); + + let vec = (0..side_length.pow(3)) + .map(|_| (0.into(), 0.into())) + .collect(); Self { bounding_size: rule.bounding_size, + side_length, data: vec, } } - fn inc(&self, index: &IVec3) { - self.get(index).fetch_add(1, Ordering::Relaxed); + fn len(&self) -> usize { + self.side_length.pow(3) } - fn get(&self, index: &IVec3) -> &AtomicU8 { - let (i, j, k) = self.convert_index(index); - &self.data[i][j][k] + fn get(&self, pos: &IVec3) -> &(u8, AtomicU8) { + let index = self.to_index(pos); + &self[index] } - fn convert_index(&self, index: &IVec3) -> (usize, usize, usize) { - ( - (index.x + self.bounding_size).try_into().unwrap(), - (index.y + self.bounding_size).try_into().unwrap(), - (index.z + self.bounding_size).try_into().unwrap(), - ) + fn get_mut(&mut self, pos: &IVec3) -> &mut (u8, AtomicU8) { + let index = self.to_index(pos); + &mut self[index] } - fn clear(&self, _task_pool: &TaskPool) { - // TODO: parallelize - for val in self.iter() { - val.store(0, Ordering::Relaxed); - } - } + fn to_index(&self, pos: &IVec3) -> usize { + let i: usize = (pos.x + self.bounding_size).try_into().unwrap(); + let j: usize = (pos.y + self.bounding_size).try_into().unwrap(); + let k: usize = (pos.z + self.bounding_size).try_into().unwrap(); - fn iter<'a>(&'a self) -> impl Iterator { - self.data - .iter() - .map(|data| data.iter().map(|data| data.iter())) - .flatten() - .flatten() + i * self.side_length * self.side_length + j * self.side_length + k } -} -/// A single cell -#[derive(Debug, Component)] -struct Cell { - /// The position index of the cell - pos: IVec3, + fn to_pos(&self, mut index: usize) -> IVec3 { - /// The cell that might be at this position - /// - /// Stores (value, neighbours) - state: Option<(u8, u8)>, -} + let i = index / (self.side_length * self.side_length); + index %= self.side_length * self.side_length; -impl Cell { - fn new(pos: IVec3) -> Self { - Self { pos, state: None } - } -} + let j = index / (self.side_length); + index %= self.side_length; -impl From<(i32, i32, i32)> for Cell { - fn from(pos: (i32, i32, i32)) -> Self { - Cell::new(pos.into()) - } -} + let k = index; -/// Spawn all the nessasary `Cell` entities and setup `PrevCells` -fn init_cells(rule: Res, mut commands: Commands) { - let bounds = rule.get_bounding_ranges(); - for i in bounds.0 { - for j in bounds.1.clone() { - for k in bounds.2.clone() { - commands.spawn().insert(Cell::from((i, j, k))); - } - } + + ( + i as i32 - self.bounding_size, + j as i32 - self.bounding_size, + k as i32 - self.bounding_size, + ) + .into() } -} - -/// Spawn hunk of noise if the input was given -fn spawn_noise(keyboard_input: Res>, mut cells: ResMut) { - if keyboard_input.just_pressed(KeyCode::P) { - info!("Spawn noise"); + fn spawn_noise( + &mut self, + // _task_pool: &TaskPool, // TODO: parallelize? + rule: &Rule + ) { + let timer = Instant::now(); + // Different version of `utils::spawn_noise` let mut rand = rand::thread_rng(); let spawn_size = 6; @@ -145,121 +102,159 @@ fn spawn_noise(keyboard_input: Res>, mut cells: ResMut rand.gen_range(-spawn_size..=spawn_size), rand.gen_range(-spawn_size..=spawn_size), ); - cells.states.insert(pos); + let (value, _) = self.get_mut(&pos); + *value = rule.states; }); + + info!( + "spawn_noise: {:.3} ms", + timer.elapsed().as_secs_f64() * 1000.0 + ); } -} -/// Step all `Cell` entities forward based on `PrevCells` if the input was given -fn tick_cells( - task_pool: Res, - rule: Res, - keyboard_input: Res>, - prev_cells: Res, - mut cell: Query<&mut Cell>, - mut cell_event: EventWriter, -) { - if !keyboard_input.pressed(KeyCode::E) { - return; + fn increment_neighbor(&self, pos: &IVec3) { + self.get(pos).1.fetch_add(1, Ordering::Relaxed); } - let timer = Instant::now(); + fn add_to_neighbours(&self, rule: &Rule, index: usize) { + let (ref value, ref _neighbours) = self[index]; - let num_threads = 8; - info!("num_threads: {}", num_threads); - info!("prev_cells.states.len(): {}", prev_cells.states.len()); - - let neighbours = Neighbors::zeros(&rule); - - task_pool.scope(|s| { - let neighbours = &neighbours; - let prev_cells = &prev_cells; - let rule = &rule; - - for thread_id in 0..num_threads { - s.spawn(async move { - let mut iter = prev_cells.states.iter(); - - // Advance to the threads starting point - for _ in 0..thread_id { - if iter.next().is_none() { - // We know the iterator is fused - break; - } - } - - for pos in iter.step_by(num_threads) { - for dir in rule.neighbour_method.get_neighbour_iter() { - let mut neighbour_pos = *pos + *dir; - keep_in_bounds(rule.bounding_size, &mut neighbour_pos); - neighbours.inc(&neighbour_pos); - } - } - }); + if *value == rule.states { + let pos = self.to_pos(index); + for dir in rule.neighbour_method.get_neighbour_iter() { + let mut neighbour_pos = pos + *dir; + keep_in_bounds(rule.bounding_size, &mut neighbour_pos); + self.increment_neighbor(&neighbour_pos); + } } - }); + } - info!( - "Tick neighbours: {:.3} ms", - timer.elapsed().as_secs_f64() * 1000.0 - ); + fn calculate_neighbors(&self, task_pool: &TaskPool, rule: &Rule) { + + self.clear_neighbors(task_pool); - // Modifiy all cells in parallel in batches of 32 - let timer = Instant::now(); - cell.par_for_each_mut(&task_pool, 32, |mut cell| { - let neighbour_count = neighbours.get(&cell.pos).load(Ordering::Relaxed); + let timer = Instant::now(); + + let indicies = (0..self.len()).collect::>(); + + indicies.par_chunk_map(task_pool, 1024, |indicies| { + for index in indicies { + self.add_to_neighbours(rule, *index); + } + }); + + info!( + "calculate_neighbors: {:.3} ms", + timer.elapsed().as_secs_f64() * 1000.0 + ); + } + + fn clear_neighbors(&self, task_pool: &TaskPool) { + let timer = Instant::now(); + + let slice = &self.data[..]; - if let Some(ref mut cell_state) = cell.state { + slice.par_chunk_map(task_pool, 2048, |states| { + for (_, ref neighbours) in states { + neighbours.store(0, Ordering::Relaxed); + } + }); + + info!( + "clear_neighbors: {:.3} ms", + timer.elapsed().as_secs_f64() * 1000.0 + ); + } + + fn update(rule: &Rule, value: &mut u8, neighbours: &u8) { + if *value != 0 { // Decrement value if survival rule isn't passed - if !(cell_state.0 == rule.states && rule.survival_rule.in_range(neighbour_count)) { - cell_state.0 -= 1; - cell_state.1 = neighbour_count; - - // Destroy if no value left - if cell_state.0 == 0 { - cell.state = None; - } + if !(*value == rule.states && rule.survival_rule.in_range(*neighbours)) { + *value -= 1; } } else { // Check for birth - if rule.birth_rule.in_range(neighbour_count) { - cell.state = Some((rule.states, neighbour_count)); + if rule.birth_rule.in_range(*neighbours) { + *value = rule.states; } } - }); + } - cell_event.send(UpdateEvent); + fn update_values(&mut self, task_pool: &TaskPool, rule: &Rule) { + + let timer = Instant::now(); + + let mut slice = &mut self.data[..]; + + slice.par_chunk_map_mut(task_pool, 1024, |states| { + for (ref mut value, ref mut neighbours) in states { + Self::update(rule, value, neighbours.get_mut()); + } + }); - info!( - "Tick Modifiy: {:.3} ms", - timer.elapsed().as_secs_f64() * 1000.0 - ); + info!( + "update_values: {:.3} ms", + timer.elapsed().as_secs_f64() * 1000.0 + ); + } } -/// Save all important `Cell` entities to `PrevCells` for the next frame -fn save(rule: Res, mut cells: ResMut, query: Query<&Cell>) { - let timer = Instant::now(); +impl Index for Cells { + type Output = (u8, AtomicU8); - cells.states.clear(); + fn index(&self, index: usize) -> &Self::Output { + &self.data[index] + } +} - // TODO: parallelize - // TODO: could calculate neighbours for next step at this time, then neighbors could be - // calculated while rendering - query.for_each(|cell| { - if let Some(cell_state) = cell.state { - if cell_state.0 == rule.states { - cells.states.insert(cell.pos.clone()); - } - } - }); +impl IndexMut for Cells { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.data[index] + } +} - info!("Save: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); +/// Spawn all the nessasary `Cell` entities and setup `Cells` +fn init_cells(rule: Res, mut commands: Commands) { + commands.insert_resource(Cells::zeros(&rule)) +} + +fn spawn_noise( + keyboard_input: Res>, + rule: Res, + mut cells: ResMut, + mut cell_event: EventWriter, +) { + if keyboard_input.just_pressed(KeyCode::P) { + + cells.spawn_noise(&rule); + + cell_event.send(UpdateEvent); + } +} + +fn tick_cells( + task_pool: Res, + keyboard_input: Res>, + rule: Res, + mut cells: ResMut, + mut cell_event: EventWriter, +) { + if !keyboard_input.pressed(KeyCode::E) { + return; + } + + // Calculate neighbor values in parallel + cells.calculate_neighbors(&task_pool, &rule); + + // Modifiy all cell values in parallel + cells.update_values(&task_pool, &rule); + + cell_event.send(UpdateEvent); } -/// Convert `Cell` entities to `InstanceMaterialData` fn prepare_cell_data( rule: Res, - cells: Query<&Cell>, + cells: Res, mut query: Query<&mut InstanceMaterialData>, ) { let timer = Instant::now(); @@ -267,10 +262,14 @@ fn prepare_cell_data( // take the first let mut instance_data = query.iter_mut().next().unwrap(); instance_data.0.clear(); - for cell in cells.iter() { - let pos = cell.pos; + + for index in 0..cells.len() { + let (value, ref neighbours) = cells[index]; + + if value != 0 { + let pos = cells.to_pos(index); + let neighbours = neighbours.load(Ordering::Relaxed); - if let Some(ref state) = cell.state { let dist_to_center = utils::dist_to_center(pos, &rule); instance_data.0.push(InstanceData { @@ -278,15 +277,16 @@ fn prepare_cell_data( scale: 1.0, color: rule .color_method - .color(rule.states, state.0, state.1, dist_to_center) + .color(rule.states, value, neighbours, dist_to_center) .as_rgba_f32(), - //color: Color::rgba(state.value as f32 / rule.states as f32, 0.0, 0.0, 1.0) + //color: Color::rgba(value as f32 / rule.states as f32, 0.0, 0.0, 1.0) // .as_rgba_f32(), }); + } } - info!("Prepare: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); + info!("prepare_cell_data: {:.3} ms", timer.elapsed().as_secs_f64() * 1000.0); } /// The system stages to do in order @@ -300,7 +300,7 @@ pub struct CellsMultithreadedV2Plugin; impl Plugin for CellsMultithreadedV2Plugin { fn build(&self, app: &mut bevy::prelude::App) { - app.init_resource::() + app .add_startup_system(init_cells) .add_system(spawn_noise.label(Stages::SpawnNoise)) .add_system( @@ -308,7 +308,6 @@ impl Plugin for CellsMultithreadedV2Plugin { .label(Stages::TickCells) .after(Stages::SpawnNoise), ) - .add_system(save.after(Stages::TickCells)) .add_system(prepare_cell_data.after(Stages::TickCells)); } }