Parallelize spatial grid construction.

This solution may or may not actually work well.  An incremental
solution may be better.
This commit is contained in:
Joshua Yanovski 2022-09-22 17:18:13 -07:00
parent 46cd112928
commit cc6d904c75
5 changed files with 242 additions and 187 deletions

View File

@ -71,6 +71,7 @@ petgraph = { version = "0.6", optional = true }
kiddo = { version = "0.1", optional = true } kiddo = { version = "0.1", optional = true }
# Data structures # Data structures
dashmap = { version = "5.4.0", features = ["rayon"] }
hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] }
slotmap = { version = "1.0", features = ["serde"] } slotmap = { version = "1.0", features = ["serde"] }
indexmap = { version = "1.3.0", features = ["rayon"] } indexmap = { version = "1.3.0", features = ["rayon"] }

View File

@ -1,4 +1,4 @@
use crate::util::SpatialGrid; use crate::util::{SpatialGrid, SpatialGridRef};
/// Cached [`SpatialGrid`] for reuse within different ecs systems during a tick. /// Cached [`SpatialGrid`] for reuse within different ecs systems during a tick.
/// This is used to accelerate queries on entities within a specific area. /// This is used to accelerate queries on entities within a specific area.
@ -6,7 +6,7 @@ use crate::util::SpatialGrid;
/// positions are calculated for the tick. So any position modifications outside /// positions are calculated for the tick. So any position modifications outside
/// the physics system will not be reflected here until the next tick when the /// the physics system will not be reflected here until the next tick when the
/// physics system runs. /// physics system runs.
pub struct CachedSpatialGrid(pub SpatialGrid); pub struct CachedSpatialGrid(pub SpatialGridRef);
impl Default for CachedSpatialGrid { impl Default for CachedSpatialGrid {
fn default() -> Self { fn default() -> Self {
@ -14,7 +14,7 @@ impl Default for CachedSpatialGrid {
let lg2_large_cell_size = 6; // 64 let lg2_large_cell_size = 6; // 64
let radius_cutoff = 8; let radius_cutoff = 8;
let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff).into_read_only();
Self(spatial_grid) Self(spatial_grid)
} }

View File

@ -38,4 +38,4 @@ pub use dir::*;
pub use option::either_with; pub use option::either_with;
pub use plane::Plane; pub use plane::Plane;
pub use projection::Projection; pub use projection::Projection;
pub use spatial_grid::SpatialGrid; pub use spatial_grid::{SpatialGrid, SpatialGridRef};

View File

@ -1,29 +1,36 @@
use core::sync::atomic::{AtomicU32, Ordering};
use vek::*; use vek::*;
pub type MapMut = dashmap::DashMap<Vec2<i32>, Vec<specs::Entity>>;
pub type MapRef = dashmap::ReadOnlyView<Vec2<i32>, Vec<specs::Entity>>;
#[derive(Debug)] #[derive(Debug)]
pub struct SpatialGrid { pub struct SpatialGridInner<Map, Radius> {
// Uses two scales of grids so that we can have a hard limit on how far to search in the /// Uses two scales of grids so that we can have a hard limit on how far to search in the
// smaller grid /// smaller grid
grid: hashbrown::HashMap<Vec2<i32>, Vec<specs::Entity>>, grid: Map,
large_grid: hashbrown::HashMap<Vec2<i32>, Vec<specs::Entity>>, large_grid: Map,
// Log base 2 of the cell size of the spatial grid /// Log base 2 of the cell size of the spatial grid
lg2_cell_size: usize, lg2_cell_size: usize,
// Log base 2 of the cell size of the large spatial grid /// Log base 2 of the cell size of the large spatial grid
lg2_large_cell_size: usize, lg2_large_cell_size: usize,
// Entities with a radius over this value are store in the coarser large_grid /// Entities with a radius over this value are store in the coarser large_grid
// This is the amount of buffer space we need to add when finding the intersections with cells /// This is the amount of buffer space we need to add when finding the intersections with cells
// in the regular grid /// in the regular grid
radius_cutoff: u32, radius_cutoff: u32,
// Stores the largest radius of the entities in the large_grid /// Stores the largest radius of the entities in the large_grid
// This is the amount of buffer space we need to add when finding the intersections with cells /// This is the amount of buffer space we need to add when finding the intersections with cells
// in the larger grid /// in the larger grid
// note: could explore some distance field type thing for querying whether there are large /// note: could explore some distance field type thing for querying whether there are large
// entities nearby that necessitate expanding the cells searched for collision (and querying /// entities nearby that necessitate expanding the cells searched for collision (and querying
// how much it needs to be expanded) /// how much it needs to be expanded)
// TODO: log this to metrics? /// TODO: log this to metrics?
largest_large_radius: u32, largest_large_radius: Radius,
} }
pub type SpatialGrid = SpatialGridInner<MapMut, AtomicU32>;
pub type SpatialGridRef = SpatialGridInner<MapRef, u32>;
impl SpatialGrid { impl SpatialGrid {
pub fn new(lg2_cell_size: usize, lg2_large_cell_size: usize, radius_cutoff: u32) -> Self { pub fn new(lg2_cell_size: usize, lg2_large_cell_size: usize, radius_cutoff: u32) -> Self {
Self { Self {
@ -32,28 +39,55 @@ impl SpatialGrid {
lg2_cell_size, lg2_cell_size,
lg2_large_cell_size, lg2_large_cell_size,
radius_cutoff, radius_cutoff,
largest_large_radius: radius_cutoff, largest_large_radius: radius_cutoff.into(),
} }
} }
/// Add an entity at the provided 2d pos into the spatial grid /// Add an entity at the provided 2d pos into the spatial grid
pub fn insert(&mut self, pos: Vec2<i32>, radius: u32, entity: specs::Entity) { pub fn insert(&self, pos: Vec2<i32>, radius: u32, entity: specs::Entity) {
if radius <= self.radius_cutoff { if radius <= self.radius_cutoff {
let cell = pos.map(|e| e >> self.lg2_cell_size); let cell = pos.map(|e| e >> self.lg2_cell_size);
self.grid.entry(cell).or_default().push(entity); self.grid.entry(cell).or_default().push(entity);
} else { } else {
let cell = pos.map(|e| e >> self.lg2_large_cell_size); let cell = pos.map(|e| e >> self.lg2_large_cell_size);
self.large_grid.entry(cell).or_default().push(entity); self.large_grid.entry(cell).or_default().push(entity);
self.largest_large_radius = self.largest_large_radius.max(radius); // NOTE: Relaxed ordering is sufficient, because max is a monotonic function (for the
// duration of shared write access to the map; clear takes &mut so it's okay that it's
// not monotonic). We don't need to order this operation with anything else, either,
// because all interesting map operations require unique access to Self to perform,
// which axiomatically synchronizes with the end of all prior shared borrows.
//
// TODO: Verify that intrinsics lower intelligently to a priority update on CPUs (since
// the intrinsic seems targeted at GPUs).
self.largest_large_radius.fetch_max(radius, Ordering::Relaxed);
} }
} }
pub fn into_read_only(self) -> SpatialGridRef {
SpatialGridInner {
grid: self.grid.into_read_only(),
large_grid: self.large_grid.into_read_only(),
lg2_cell_size: self.lg2_cell_size,
lg2_large_cell_size: self.lg2_large_cell_size,
radius_cutoff: self.radius_cutoff,
largest_large_radius: self.largest_large_radius.into_inner(),
}
}
pub fn clear(&mut self) {
self.grid.clear();
self.large_grid.clear();
self.largest_large_radius = self.radius_cutoff.into();
}
}
impl SpatialGridRef {
/// Get an iterator over the entities overlapping the provided axis aligned /// Get an iterator over the entities overlapping the provided axis aligned
/// bounding region. /// bounding region.
/// NOTE: for best optimization of the iterator use /// NOTE: for best optimization of the iterator use
/// `for_each` rather than a for loop. /// `for_each` rather than a for loop.
pub fn in_aabr<'a>(&'a self, aabr: Aabr<i32>) -> impl Iterator<Item = specs::Entity> + 'a { pub fn in_aabr<'a>(&'a self, aabr: Aabr<i32>) -> impl Iterator<Item = specs::Entity> + 'a {
let iter = |max_entity_radius, grid: &'a hashbrown::HashMap<_, _>, lg2_cell_size| { let iter = |max_entity_radius, grid: &'a MapRef, lg2_cell_size| {
// Add buffer for other entity radius // Add buffer for other entity radius
let min = aabr.min - max_entity_radius as i32; let min = aabr.min - max_entity_radius as i32;
let max = aabr.max + max_entity_radius as i32; let max = aabr.max + max_entity_radius as i32;
@ -100,9 +134,14 @@ impl SpatialGrid {
self.in_aabr(aabr) self.in_aabr(aabr)
} }
pub fn clear(&mut self) { pub fn into_inner(self) -> SpatialGrid {
self.grid.clear(); SpatialGridInner {
self.large_grid.clear(); grid: self.grid.into_inner(),
self.largest_large_radius = self.radius_cutoff; large_grid: self.large_grid.into_inner(),
lg2_cell_size: self.lg2_cell_size,
lg2_large_cell_size: self.lg2_large_cell_size,
radius_cutoff: self.radius_cutoff,
largest_large_radius: self.largest_large_radius.into(),
}
} }
} }

View File

@ -15,7 +15,7 @@ use common::{
states, states,
terrain::{Block, BlockKind, TerrainGrid}, terrain::{Block, BlockKind, TerrainGrid},
uid::Uid, uid::Uid,
util::{Projection, SpatialGrid}, util::{Projection, SpatialGrid, SpatialGridRef},
vol::{BaseVol, ReadVol}, vol::{BaseVol, ReadVol},
}; };
use common_base::{prof_span, span}; use common_base::{prof_span, span};
@ -146,37 +146,38 @@ impl<'a> PhysicsData<'a> {
/// Add/reset physics state components /// Add/reset physics state components
fn reset(&mut self) { fn reset(&mut self) {
span!(_guard, "Add/reset physics state components"); span!(_guard, "Add/reset physics state components");
for (entity, _, _, _, _) in ( (
&self.read.entities, &self.read.entities,
&self.read.colliders, self.read.colliders.mask(),
&self.write.positions, self.write.positions.mask(),
&self.write.velocities, self.write.velocities.mask(),
&self.write.orientations, self.write.orientations.mask(),
) )
.join() .join()
{ .for_each(|(entity, _, _, _, _)| {
let _ = self let _ = self
.write .write
.physics_states .physics_states
.entry(entity) .entry(entity)
.map(|e| e.or_insert_with(Default::default)); .map(|e| e.or_insert_with(Default::default));
} });
} }
fn maintain_pushback_cache(&mut self) { fn maintain_pushback_cache(&mut self) {
span!(_guard, "Maintain pushback cache"); span!(_guard, "Maintain pushback cache");
// Add PreviousPhysCache for all relevant entities // Add PreviousPhysCache for all relevant entities
for entity in ( (
&self.read.entities, &self.read.entities,
&self.read.colliders, self.read.colliders.mask(),
&self.write.velocities, self.write.velocities.mask(),
&self.write.positions, self.write.positions.mask(),
!&self.write.previous_phys_cache, !self.write.previous_phys_cache.mask(),
) )
.join() .join()
.map(|(e, _, _, _, _)| e) .map(|(e, _, _, _, _)| e)
.collect::<Vec<_>>() .collect::<Vec<_>>()
{ .into_iter()
.for_each(|entity| {
let _ = self let _ = self
.write .write
.previous_phys_cache .previous_phys_cache
@ -191,11 +192,11 @@ impl<'a> PhysicsData<'a> {
pos: None, pos: None,
ori: Quaternion::identity(), ori: Quaternion::identity(),
}); });
} });
// Update PreviousPhysCache // Update PreviousPhysCache
for (_, vel, position, ori, mut phys_cache, collider, scale, cs) in ( (
&self.read.entities, /* &self.read.entities, */
&self.write.velocities, &self.write.velocities,
&self.write.positions, &self.write.positions,
&self.write.orientations, &self.write.orientations,
@ -204,8 +205,9 @@ impl<'a> PhysicsData<'a> {
self.read.scales.maybe(), self.read.scales.maybe(),
self.read.char_states.maybe(), self.read.char_states.maybe(),
) )
.join() .par_join()
{ .for_each(
|(/* _, */ vel, position, ori, mut phys_cache, collider, scale, cs)| {
let scale = scale.map(|s| s.0).unwrap_or(1.0); let scale = scale.map(|s| s.0).unwrap_or(1.0);
let z_limits = calc_z_limit(cs, collider); let z_limits = calc_z_limit(cs, collider);
let (z_min, z_max) = z_limits; let (z_min, z_max) = z_limits;
@ -220,13 +222,16 @@ impl<'a> PhysicsData<'a> {
// Move center to the middle between OLD and OLD+VEL_DT // Move center to the middle between OLD and OLD+VEL_DT
// so that we can reduce the collision_boundary. // so that we can reduce the collision_boundary.
phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0; phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0;
phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude(); phys_cache.collision_boundary =
radius + (phys_cache.velocity_dt / 2.0).magnitude();
phys_cache.scale = scale; phys_cache.scale = scale;
phys_cache.scaled_radius = flat_radius; phys_cache.scaled_radius = flat_radius;
let neighborhood_radius = match collider { let neighborhood_radius = match collider {
Collider::CapsulePrism { radius, .. } => radius * scale, Collider::CapsulePrism { radius, .. } => radius * scale,
Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => flat_radius, Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => {
flat_radius
},
}; };
phys_cache.neighborhood_radius = neighborhood_radius; phys_cache.neighborhood_radius = neighborhood_radius;
@ -270,7 +275,8 @@ impl<'a> PhysicsData<'a> {
}; };
phys_cache.origins = origins; phys_cache.origins = origins;
phys_cache.ori = ori; phys_cache.ori = ori;
} },
);
} }
fn construct_spatial_grid(&mut self) -> SpatialGrid { fn construct_spatial_grid(&mut self) -> SpatialGrid {
@ -292,27 +298,28 @@ impl<'a> PhysicsData<'a> {
let lg2_cell_size = 5; let lg2_cell_size = 5;
let lg2_large_cell_size = 6; let lg2_large_cell_size = 6;
let radius_cutoff = 8; let radius_cutoff = 8;
let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff);
for (entity, pos, phys_cache, _, _) in ( (
&read.entities, &read.entities,
&write.positions, &write.positions,
&write.previous_phys_cache, &write.previous_phys_cache,
write.velocities.mask(), write.velocities.mask(),
!&read.projectiles, // Not needed because they are skipped in the inner loop below !read.projectiles.mask(), /* Not needed because they are skipped in the inner loop
* below */
) )
.join() .par_join()
{ .for_each(|(entity, pos, phys_cache, _, _)| {
// Note: to not get too fine grained we use a 2D grid for now // Note: to not get too fine grained we use a 2D grid for now
let radius_2d = phys_cache.scaled_radius.ceil() as u32; let radius_2d = phys_cache.scaled_radius.ceil() as u32;
let pos_2d = pos.0.xy().map(|e| e as i32); let pos_2d = pos.0.xy().map(|e| e as i32);
const POS_TRUNCATION_ERROR: u32 = 1; const POS_TRUNCATION_ERROR: u32 = 1;
spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity); spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity);
} });
spatial_grid spatial_grid
} }
fn apply_pushback(&mut self, job: &mut Job<Sys>, spatial_grid: &SpatialGrid) { fn apply_pushback(&mut self, job: &mut Job<Sys>, spatial_grid: &SpatialGridRef) {
span!(_guard, "Apply pushback"); span!(_guard, "Apply pushback");
job.cpu_stats.measure(ParMode::Rayon); job.cpu_stats.measure(ParMode::Rayon);
let PhysicsData { let PhysicsData {
@ -529,16 +536,16 @@ impl<'a> PhysicsData<'a> {
let lg2_cell_size = 7; // 128 let lg2_cell_size = 7; // 128
let lg2_large_cell_size = 8; // 256 let lg2_large_cell_size = 8; // 256
let radius_cutoff = 64; let radius_cutoff = 64;
let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff);
// TODO: give voxel colliders their own component type // TODO: give voxel colliders their own component type
for (entity, pos, collider, ori) in ( (
&read.entities, &read.entities,
&write.positions, &write.positions,
&read.colliders, &read.colliders,
&write.orientations, &write.orientations,
) )
.join() .par_join()
{ .for_each(|(entity, pos, collider, ori)| {
let vol = match collider { let vol = match collider {
Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(id), Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(id),
Collider::Volume(vol) => Some(&**vol), Collider::Volume(vol) => Some(&**vol),
@ -552,7 +559,7 @@ impl<'a> PhysicsData<'a> {
const POS_TRUNCATION_ERROR: u32 = 1; const POS_TRUNCATION_ERROR: u32 = 1;
spatial_grid.insert(pos_2d, radius + POS_TRUNCATION_ERROR, entity); spatial_grid.insert(pos_2d, radius + POS_TRUNCATION_ERROR, entity);
} }
} });
spatial_grid spatial_grid
} }
@ -560,7 +567,7 @@ impl<'a> PhysicsData<'a> {
fn handle_movement_and_terrain( fn handle_movement_and_terrain(
&mut self, &mut self,
job: &mut Job<Sys>, job: &mut Job<Sys>,
voxel_collider_spatial_grid: &SpatialGrid, voxel_collider_spatial_grid: &SpatialGridRef,
) { ) {
let PhysicsData { let PhysicsData {
ref read, ref read,
@ -610,7 +617,7 @@ impl<'a> PhysicsData<'a> {
&write.physics_states, &write.physics_states,
&read.masses, &read.masses,
&read.densities, &read.densities,
!&read.is_ridings, !read.is_ridings.mask(),
) )
.par_join() .par_join()
.for_each_init( .for_each_init(
@ -694,19 +701,19 @@ impl<'a> PhysicsData<'a> {
// Update cached 'old' physics values to the current values ready for the next // Update cached 'old' physics values to the current values ready for the next
// tick // tick
prof_span!(guard, "record ori into phys_cache"); prof_span!(guard, "record ori into phys_cache");
for (pos, ori, previous_phys_cache, _) in ( (
&write.positions, &write.positions,
&write.orientations, &write.orientations,
&mut write.previous_phys_cache, &mut write.previous_phys_cache,
&read.colliders, read.colliders.mask(),
) )
.join() .par_join()
{ .for_each(|(pos, ori, previous_phys_cache, _)| {
// Note: updating ori with the rest of the cache values above was attempted but // Note: updating ori with the rest of the cache values above was attempted but
// it did not work (investigate root cause?) // it did not work (investigate root cause?)
previous_phys_cache.pos = Some(*pos); previous_phys_cache.pos = Some(*pos);
previous_phys_cache.ori = ori.to_quat(); previous_phys_cache.ori = ori.to_quat();
} });
drop(guard); drop(guard);
} }
@ -714,7 +721,7 @@ impl<'a> PhysicsData<'a> {
job: &mut Job<Sys>, job: &mut Job<Sys>,
read: &PhysicsRead, read: &PhysicsRead,
write: &mut PhysicsWrite, write: &mut PhysicsWrite,
voxel_collider_spatial_grid: &SpatialGrid, voxel_collider_spatial_grid: &SpatialGridRef,
terrain_like_entities: bool, terrain_like_entities: bool,
) { ) {
let (positions, velocities, previous_phys_cache, orientations) = ( let (positions, velocities, previous_phys_cache, orientations) = (
@ -1240,17 +1247,17 @@ impl<'a> PhysicsData<'a> {
write.outcomes.emitter().emit_many(outcomes); write.outcomes.emitter().emit_many(outcomes);
prof_span!(guard, "write deferred pos and vel"); prof_span!(guard, "write deferred pos and vel");
for (_, pos, vel, ori, pos_vel_ori_defer, _) in ( (
&read.entities, /* &read.entities, */
&mut write.positions, &mut write.positions,
&mut write.velocities, &mut write.velocities,
&mut write.orientations, &mut write.orientations,
&mut write.pos_vel_ori_defers, &mut write.pos_vel_ori_defers,
&read.colliders, &read.colliders,
) )
.join() .par_join()
.filter(|tuple| tuple.5.is_voxel() == terrain_like_entities) .filter(|tuple| tuple./*5*/4.is_voxel() == terrain_like_entities)
{ .for_each(|(/* _, */ pos, vel, ori, pos_vel_ori_defer, _)| {
if let Some(new_pos) = pos_vel_ori_defer.pos.take() { if let Some(new_pos) = pos_vel_ori_defer.pos.take() {
*pos = new_pos; *pos = new_pos;
} }
@ -1260,7 +1267,7 @@ impl<'a> PhysicsData<'a> {
if let Some(new_ori) = pos_vel_ori_defer.ori.take() { if let Some(new_ori) = pos_vel_ori_defer.ori.take() {
*ori = new_ori; *ori = new_ori;
} }
} });
drop(guard); drop(guard);
let mut event_emitter = read.event_bus.emitter(); let mut event_emitter = read.event_bus.emitter();
@ -1276,7 +1283,11 @@ impl<'a> PhysicsData<'a> {
ref mut write, ref mut write,
} = self; } = self;
let spatial_grid = &mut write.cached_spatial_grid.0; // Borrow checker dance, since transferring away from a read only view requires
// &mut self.
let mut spatial_grid = core::mem::take(&mut *write.cached_spatial_grid)
.0
.into_inner();
spatial_grid.clear(); spatial_grid.clear();
( (
&read.entities, &read.entities,
@ -1284,7 +1295,7 @@ impl<'a> PhysicsData<'a> {
read.scales.maybe(), read.scales.maybe(),
read.colliders.maybe(), read.colliders.maybe(),
) )
.join() .par_join()
.for_each(|(entity, pos, scale, collider)| { .for_each(|(entity, pos, scale, collider)| {
let scale = scale.map(|s| s.0).unwrap_or(1.0); let scale = scale.map(|s| s.0).unwrap_or(1.0);
let radius_2d = let radius_2d =
@ -1293,6 +1304,8 @@ impl<'a> PhysicsData<'a> {
const POS_TRUNCATION_ERROR: u32 = 1; const POS_TRUNCATION_ERROR: u32 = 1;
spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity); spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity);
}); });
write.cached_spatial_grid.0 = spatial_grid.into_read_only();
} }
} }
@ -1321,10 +1334,12 @@ impl<'a> System<'a> for Sys {
// entities. // entities.
physics_data.maintain_pushback_cache(); physics_data.maintain_pushback_cache();
let spatial_grid = physics_data.construct_spatial_grid(); let spatial_grid = physics_data.construct_spatial_grid().into_read_only();
physics_data.apply_pushback(job, &spatial_grid); physics_data.apply_pushback(job, &spatial_grid);
let voxel_collider_spatial_grid = physics_data.construct_voxel_collider_spatial_grid(); let voxel_collider_spatial_grid = physics_data
.construct_voxel_collider_spatial_grid()
.into_read_only();
physics_data.handle_movement_and_terrain(job, &voxel_collider_spatial_grid); physics_data.handle_movement_and_terrain(job, &voxel_collider_spatial_grid);
// Spatial grid used by other systems // Spatial grid used by other systems