Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spatial Hashing #31

Merged
merged 52 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c847fcc
Add spatial hashing
aevyrie Jul 16, 2024
b59aec7
Merge branch 'main' into spatial-hashing
aevyrie Jul 16, 2024
ceca51d
Add neighbor queries
aevyrie Jul 17, 2024
8c13827
Add neighbor flood, and perf improvements.
aevyrie Jul 17, 2024
9f3f722
spatial hash map filtering
aevyrie Jul 18, 2024
c2c8943
Only run spatial hashes on filtered components
aevyrie Jul 20, 2024
f58f813
polish and perf
aevyrie Jul 20, 2024
ce93b56
spatial hash map
aevyrie Jul 28, 2024
5bfe0b9
Noise in example, timing diagnostics, refactor neighbors, parallelize…
aevyrie Aug 1, 2024
2d66b57
Optimize static scenes
aevyrie Aug 2, 2024
8f80274
Fix doc link
aevyrie Aug 2, 2024
7cd9386
Remove atomics in hot loop
aevyrie Aug 2, 2024
a1e986f
Add full font, improve perf scaling
aevyrie Aug 4, 2024
a807a20
better perf instrumentation
aevyrie Aug 10, 2024
f955407
Add root tagging time to example
aevyrie Aug 10, 2024
008839c
Add benchmark for global transform computation
aevyrie Aug 10, 2024
f169b58
Add some comments
aevyrie Aug 10, 2024
749c3ed
Minor refactors
aevyrie Aug 11, 2024
58b5c7b
perf optimizations
aevyrie Nov 7, 2024
314cc23
perf and hash collision handling
aevyrie Nov 14, 2024
1b9609a
Add prelude and clean up some naming
aevyrie Nov 16, 2024
739e0c7
Switch FstSpatialHash to use component hooks
aevyrie Nov 16, 2024
a813b71
Revert "Switch FstSpatialHash to use component hooks"
aevyrie Nov 16, 2024
eb4535c
Add spatial filter trait to clean up and document type bounds
aevyrie Nov 16, 2024
2e06a97
organize spatial hashing
aevyrie Nov 16, 2024
bc0f6d0
Fix docs
aevyrie Nov 16, 2024
8826b48
New and improved examples
aevyrie Nov 17, 2024
9d50fd5
Add note to small scale example
aevyrie Nov 17, 2024
63ab594
Update examples
aevyrie Nov 17, 2024
d9795c8
remove use bound
aevyrie Nov 17, 2024
8552c3b
update particle example
aevyrie Nov 18, 2024
92d8b29
refactors
aevyrie Nov 19, 2024
9a9a55d
Merge remote-tracking branch 'origin/main' into spatial-hashing
aevyrie Dec 5, 2024
f8204c0
Cell removal and insertion tracking in spatial map, plus bike shed di…
aevyrie Dec 8, 2024
1cadcc3
More refactoring nonsense
aevyrie Dec 9, 2024
6f1b42f
Initial spatial partitions
aevyrie Dec 11, 2024
cf75975
optimizations
aevyrie Dec 11, 2024
bf00f19
threads go brrr
aevyrie Dec 12, 2024
b74fdb2
partition debug visualization
aevyrie Dec 12, 2024
ae07e7a
better color hash
aevyrie Dec 12, 2024
48d65c5
Merge remote-tracking branch 'origin/main' into spatial-hashing
aevyrie Dec 23, 2024
733e6b4
Merge remote-tracking branch 'origin/main' into spatial-hashing
aevyrie Dec 23, 2024
0001d49
Merge remote-tracking branch 'origin/main' into spatial-hashing
aevyrie Dec 23, 2024
7671bbe
name and doc refactors
aevyrie Dec 23, 2024
e8cd72d
Update README.md
aevyrie Dec 23, 2024
591b71b
Update README.md
aevyrie Dec 23, 2024
0882dec
Update README.md
aevyrie Dec 23, 2024
170454d
Update README.md
aevyrie Dec 23, 2024
6f9da3b
Doc polish
aevyrie Dec 23, 2024
50c7d74
Changelog and more naming updates
aevyrie Dec 24, 2024
7870275
Fix example defaults, doc comment
aevyrie Dec 24, 2024
4401a4a
Formatting
aevyrie Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add neighbor flood, and perf improvements.
  • Loading branch information
aevyrie committed Jul 17, 2024
commit 8c1382790a24fc6ce94bad455f8d44ed448f6712
14 changes: 13 additions & 1 deletion benches/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn spatial_hashing(c: &mut Criterion) {

const WIDTH: i32 = 50;
const N_SPAWN: usize = 10_000;
const N_MOVE: usize = 10_000;
const N_MOVE: usize = 1_000;

let setup = |mut commands: Commands| {
commands.spawn_big_space(ReferenceFrame::<i32>::new(1.0, 0.0), |root| {
Expand Down Expand Up @@ -92,6 +92,8 @@ fn spatial_hashing(c: &mut Criterion) {
});
});

app.update();

let parent = app
.world_mut()
.query::<&Parent>()
Expand Down Expand Up @@ -119,4 +121,14 @@ fn spatial_hashing(c: &mut Criterion) {
);
});
});

group.bench_function("Neighbors flood 1", |b| {
b.iter(|| {
black_box(
map.neighbors_flood(1, parent, GridCell::ZERO)
.iter()
.count(),
);
});
});
}
133 changes: 102 additions & 31 deletions src/spatial_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
};

use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::{prelude::*, query::QueryFilter};
use bevy_hierarchy::Parent;
use bevy_math::IVec3;
use bevy_reflect::Reflect;
Expand All @@ -19,25 +19,41 @@ use crate::{precision::GridPrecision, GridCell};

/// Add spatial hashing acceleration to `big_space`, accessible through the [`SpatialHashMap`]
/// resource, and [`SpatialHash`] components.
///
/// You can optionally add a filter to this plugin, to only run the spatial hashing on entities that
/// match the supplied query filter. This is useful if you only want to, say, compute hashes and
/// insert in the [`SpatialHashMap`] for `Player` entities. If you are adding multiple copies of
/// this plugin, take care not to overlap the queries to avoid duplicating work.
#[derive(Default)]
pub struct SpatialHashPlugin<P: GridPrecision>(PhantomData<P>);
pub struct SpatialHashPlugin<P: GridPrecision, F: QueryFilter = ()>(PhantomData<(P, F)>);

impl<P: GridPrecision> Plugin for SpatialHashPlugin<P> {
impl<P: GridPrecision, F: QueryFilter + Send + Sync + 'static> Plugin for SpatialHashPlugin<P, F> {
fn build(&self, app: &mut App) {
app.init_resource::<SpatialHashMap<P>>().add_systems(
app.init_resource::<SpatialHashMap<P, F>>().add_systems(
PostUpdate,
SpatialHashMap::<P>::update_spatial_hash
SpatialHashMap::<P, F>::update_spatial_hash
.after(crate::FloatingOriginSet::RecenterLargeTransforms)
.in_set(bevy_transform::TransformSystem::TransformPropagate),
);
}
}

/// A global spatial hash map for quickly finding entities in a grid cell.
#[derive(Resource, Default)]
pub struct SpatialHashMap<P: GridPrecision> {
#[derive(Resource)]
pub struct SpatialHashMap<P: GridPrecision, F: QueryFilter = ()> {
map: HashMap<SpatialHash<P>, HashSet<Entity, PassHash>, PassHash>,
reverse_map: HashMap<Entity, SpatialHash<P>, PassHash>,
spooky: PhantomData<F>,
}

impl<P: GridPrecision, F: QueryFilter> Default for SpatialHashMap<P, F> {
fn default() -> Self {
Self {
map: Default::default(),
reverse_map: Default::default(),
spooky: PhantomData,
}
}
}

/// An automatically updated `Component` that uniquely identifies an entity's cell.
Expand All @@ -60,7 +76,7 @@ pub struct SpatialHashMap<P: GridPrecision> {
/// This means you should only use spatial hashes to accelerate checks by filtering out entities
/// that could not possibly overlap; if the spatial hashes do not match, you can be certain they are
/// not in the same cell.
#[derive(Component, Clone, Copy, Eq, Debug, Reflect)]
#[derive(Component, Clone, Copy, Debug, Reflect)]
pub struct SpatialHash<P: GridPrecision>(u64, PhantomData<P>);

impl<P: GridPrecision> PartialEq for SpatialHash<P> {
Expand All @@ -69,6 +85,8 @@ impl<P: GridPrecision> PartialEq for SpatialHash<P> {
}
}

impl<P: GridPrecision> Eq for SpatialHash<P> {}

impl<P: GridPrecision> Hash for SpatialHash<P> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
Expand All @@ -84,7 +102,7 @@ impl<P: GridPrecision> SpatialHash<P> {
}
}

impl<P: GridPrecision> SpatialHashMap<P> {
impl<P: GridPrecision, F: QueryFilter> SpatialHashMap<P, F> {
fn insert(&mut self, entity: Entity, hash: SpatialHash<P>) {
// If this entity is already in the maps, we need to remove and update it.
if let Some(old_hash) = self.reverse_map.get_mut(&entity) {
Expand Down Expand Up @@ -128,42 +146,86 @@ impl<P: GridPrecision> SpatialHashMap<P> {
/// A radius of `1` will search all cells within a Chebyshev distance of `1`, or a total of 9
/// cells. You can also think of this as a cube centered on the specified cell, expanded in each
/// direction by `radius`.
///
/// Returns an iterator over all non-empty neighboring cells, including the cell, and the set of
/// entities in that cell.
pub fn neighbors<'a>(
&'a self,
cell_radius: u8,
parent: &'a Parent,
cell: GridCell<P>,
) -> impl Iterator<Item = &Entity> + 'a {
) -> impl Iterator<Item = (SpatialHash<P>, GridCell<P>, &HashSet<Entity, PassHash>)> + 'a {
let radius = cell_radius as i32;
let search_width = 1 + 2 * radius;
let search_volume = search_width.pow(3);
let center = -radius;
let hash = PartialSpatialHash::new(parent);
(0..search_volume)
.filter_map(move |i| {
let x = center + i; // % search_width.pow(0)
let y = center + i % search_width; // .pow(1)
let z = center + i % search_width.pow(2);
let offset = IVec3::new(x, y, z);
let hash = hash.generate(&(cell + offset));
self.get(&hash).map(|set| set.iter())
})
.flatten()
(0..search_volume).filter_map(move |i| {
let x = center + i; // % search_width.pow(0)
let y = center + i % search_width; // .pow(1)
let z = center + i % search_width.pow(2);
let offset = IVec3::new(x, y, z);
let neighbor_cell = cell + offset;
let neighbor_hash = hash.generate(&neighbor_cell);
self.get(&neighbor_hash)
.filter(|set| !set.is_empty())
.map(|set| (neighbor_hash, neighbor_cell, set))
})
}

/// Like [`Self::neighbors`], but flattens the result, giving you a flat list of entities in
/// neighboring cells.
pub fn neighbors_flattened<'a>(
&'a self,
cell_radius: u8,
parent: &'a Parent,
cell: GridCell<P>,
) -> impl Iterator<Item = &Entity> + 'a {
self.neighbors(cell_radius, parent, cell)
.flat_map(|(.., set)| set.iter())
}

/// Recursively searches for all connected neighboring cells within the given `cell_radius` at
/// every point. The result is a set of all grid cells connected by a cell distance of
/// `cell_radius` or less.
pub fn neighbors_flood<'a>(
&'a self,
cell_radius: u8,
parent: &'a Parent,
cell: GridCell<P>,
) -> HashMap<SpatialHash<P>, &'a HashSet<Entity, PassHash>, PassHash> {
let mut stack = vec![cell];
let mut result = HashMap::default();
while let Some(cell) = stack.pop() {
self.neighbors(cell_radius, parent, cell)
.for_each(|(hash, neighbor_cell, set)| {
if result.insert(hash, set).is_none() {
stack.push(neighbor_cell);
}
});
}
result
}

fn update_spatial_hash(
mut commands: Commands,
mut spatial: ResMut<SpatialHashMap<P>>,
changed_entities: Query<
(Entity, &Parent, &GridCell<P>),
Or<(Changed<Parent>, Changed<GridCell<P>>)>,
(Entity, &Parent, &GridCell<P>, Option<&SpatialHash<P>>),
(Or<(Changed<Parent>, Changed<GridCell<P>>)>, F),
>,
) {
// This simple sequential impl is faster than the parallel versions I've tried.
for (entity, parent, cell) in &changed_entities {
for (entity, parent, cell, old_hash) in &changed_entities {
let spatial_hash = SpatialHash::new(parent, cell);
commands.entity(entity).insert(spatial_hash);
spatial.insert(entity, spatial_hash);
// Although spatial.insert checks for equality as well, this check has a 40% savings in
// cases where the grid cell is mutated (change detection triggered), but it has not
// actually changed, this also helps if multiple plugins are updating the spatial hash,
// and it is already correct.
if old_hash.ne(&Some(&spatial_hash)) {
commands.entity(entity).insert(spatial_hash);
spatial.insert(entity, spatial_hash);
}
}
}
}
Expand All @@ -187,7 +249,7 @@ impl<P: GridPrecision> PartialSpatialHash<P> {
}
}

/// Generate a mew, fully complete [`SpatialHash`] by providing the other required half of the
/// Generate a new, fully complete [`SpatialHash`] by providing the other required half of the
/// hash - the grid cell. This function can be called many times.
#[inline]
pub fn generate(&self, cell: &GridCell<P>) -> SpatialHash<P> {
Expand Down Expand Up @@ -311,7 +373,7 @@ mod tests {
commands.spawn_big_space(ReferenceFrame::<i32>::default(), |root| {
let a = root.spawn_spatial(GridCell::new(0, 0, 0)).id();
let b = root.spawn_spatial(GridCell::new(1, 1, 1)).id();
let c = root.spawn_spatial(GridCell::new(-2, -2, -2)).id();
let c = root.spawn_spatial(GridCell::new(2, 2, 2)).id();

root.commands().insert_resource(Entities { a, b, c });
});
Expand All @@ -330,15 +392,24 @@ mod tests {
.get(app.world(), entities.a)
.unwrap();

let neighbors: HashSet<Entity> = app
.world()
.resource::<SpatialHashMap<i32>>()
.neighbors(1, parent, GridCell::new(0, 0, 0))
let map = app.world().resource::<SpatialHashMap<i32>>();
let neighbors: HashSet<Entity> = map
.neighbors_flattened(1, parent, GridCell::ZERO)
.copied()
.collect();

assert!(neighbors.contains(&entities.a));
assert!(neighbors.contains(&entities.b));
assert!(!neighbors.contains(&entities.c));

let flooded: HashSet<Entity> = map
.neighbors_flood(1, parent, GridCell::ZERO)
.iter()
.flat_map(|(_hash, set)| set.iter().copied())
.collect();

assert!(flooded.contains(&entities.a));
assert!(flooded.contains(&entities.b));
assert!(flooded.contains(&entities.c));
}
}
Loading