Merge remote-tracking branch 'origin/main'

This commit is contained in:
Aevyrie Roessler 2025-03-02 00:19:04 -08:00
commit 2a1cb54e63
7 changed files with 434 additions and 249 deletions

View File

@ -7,7 +7,16 @@ use bevy::{
use bevy_ecs::entity::EntityHasher;
use bevy_math::DVec3;
use big_space::prelude::*;
use noise::{NoiseFn, Perlin};
use noise::{NoiseFn, Simplex};
use smallvec::SmallVec;
use turborand::prelude::*;
// Try bumping this up to really stress test. I'm able to push a million entities with an M3 Max.
const HALF_WIDTH: f32 = 50.0;
const CELL_WIDTH: f32 = 10.0;
// How fast the entities should move, causing them to move into neighboring cells.
const MOVEMENT_SPEED: f32 = 5.0;
const PERCENT_STATIC: f32 = 0.99;
fn main() {
App::new()
@ -26,19 +35,11 @@ fn main() {
draw_partitions.after(GridHashMapSystem::UpdatePartition),
),
)
.add_systems(Update, cursor_grab)
.add_systems(Update, (cursor_grab, spawn_spheres))
.init_resource::<MaterialPresets>()
.run();
}
// Try bumping this up to really stress test. I'm able to push a million entities with an M3 Max.
const N_ENTITIES: usize = 100_000;
const HALF_WIDTH: f32 = 40.0;
const CELL_WIDTH: f32 = 10.0;
// How fast the entities should move, causing them to move into neighboring cells.
const MOVEMENT_SPEED: f32 = 5.0;
const PERCENT_STATIC: f32 = 0.9;
#[derive(Component)]
struct Player;
@ -50,25 +51,35 @@ struct MaterialPresets {
default: Handle<StandardMaterial>,
highlight: Handle<StandardMaterial>,
flood: Handle<StandardMaterial>,
sphere: Handle<Mesh>,
}
impl FromWorld for MaterialPresets {
fn from_world(world: &mut World) -> Self {
let mut materials = world.resource_mut::<Assets<StandardMaterial>>();
let d: StandardMaterial = StandardMaterial {
let default = materials.add(StandardMaterial {
base_color: Color::from(Srgba::new(0.5, 0.5, 0.5, 1.0)),
perceptual_roughness: 0.2,
metallic: 0.0,
..Default::default()
};
let h: StandardMaterial = Color::from(Srgba::new(2.0, 0.0, 8.0, 1.0)).into();
let f: StandardMaterial = Color::from(Srgba::new(1.1, 0.1, 1.0, 1.0)).into();
});
let highlight = materials.add(Color::from(Srgba::new(2.0, 0.0, 8.0, 1.0)));
let flood = materials.add(Color::from(Srgba::new(1.1, 0.1, 1.0, 1.0)));
let mut meshes = world.resource_mut::<Assets<Mesh>>();
let sphere = meshes.add(
Sphere::new(HALF_WIDTH / (1_000_000_f32).powf(0.33) * 0.5)
.mesh()
.ico(0)
.unwrap(),
);
Self {
default: materials.add(d),
highlight: materials.add(h),
flood: materials.add(f),
default,
highlight,
flood,
sphere,
}
}
}
@ -79,7 +90,7 @@ fn draw_partitions(
grids: Query<(&GlobalTransform, &Grid<i32>)>,
camera: Query<&GridHash<i32>, With<Camera>>,
) {
for (id, p) in partitions.iter() {
for (id, p) in partitions.iter().take(10_000) {
let Ok((transform, grid)) = grids.get(p.grid()) else {
return;
};
@ -92,35 +103,19 @@ fn draw_partitions(
p.iter()
.filter(|hash| *hash != camera.single())
.take(1_000)
.for_each(|h| {
let center = [h.cell().x, h.cell().y, h.cell().z];
let local_trans = Transform::from_translation(IVec3::from(center).as_vec3() * l)
.with_scale(Vec3::splat(l));
gizmos.cuboid(
transform.mul_transform(local_trans),
Hsla::new(hue, 1.0, 0.5, 0.2),
Hsla::new(hue, 1.0, 0.5, 0.05),
);
});
let Some(min) = p
.iter()
.filter(|hash| *hash != camera.single())
.map(|h| [h.cell().x, h.cell().y, h.cell().z])
.reduce(|[ax, ay, az], [ix, iy, iz]| [ax.min(ix), ay.min(iy), az.min(iz)])
.map(|v| IVec3::from(v).as_vec3() * l)
else {
continue;
};
let Some(max) = p
.iter()
.filter(|hash| *hash != camera.single())
.map(|h| [h.cell().x, h.cell().y, h.cell().z])
.reduce(|[ax, ay, az], [ix, iy, iz]| [ax.max(ix), ay.max(iy), az.max(iz)])
.map(|v| IVec3::from(v).as_vec3() * l)
else {
continue;
};
let min = IVec3::from([p.min().x, p.min().y, p.min().z]).as_vec3() * l;
let max = IVec3::from([p.max().x, p.max().y, p.max().z]).as_vec3() * l;
let size = max - min;
let center = min + (size) * 0.5;
@ -152,6 +147,7 @@ fn move_player(
hash_stats: Res<big_space::timing::SmoothedStat<big_space::timing::GridHashStats>>,
prop_stats: Res<big_space::timing::SmoothedStat<big_space::timing::PropagationStats>>,
) {
let n_entities = non_player.iter().len();
for neighbor in neighbors.iter() {
if let Ok(mut material) = materials.get_mut(*neighbor) {
**material = material_presets.default.clone_weak();
@ -163,10 +159,12 @@ fn move_player(
if scale.abs() > 0.0 {
// Avoid change detection
for (i, (mut transform, _, _)) in non_player.iter_mut().enumerate() {
if i > (PERCENT_STATIC * N_ENTITIES as f32) as usize {
if i < ((1.0 - PERCENT_STATIC) * n_entities as f32) as usize {
transform.translation.x += t.sin() * scale;
transform.translation.y += t.cos() * scale;
transform.translation.z += (t * 2.3).sin() * scale;
} else {
break;
}
}
}
@ -213,6 +211,10 @@ fn move_player(
let mut text = text.single_mut();
text.0 = format!(
"\
Controls:
WASD to move, QE to roll
F to spawn 1,000, G to double
Population: {: >8} Entities
Transform Propagation
@ -229,7 +231,15 @@ Update Maps: {: >16.1?}
Update Partitions: {: >10.1?}
Total: {: >22.1?}",
N_ENTITIES,
n_entities
.to_string()
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(","),
//
prop_stats.avg().grid_recentering(),
prop_stats.avg().low_precision_root_tagging(),
@ -246,36 +256,7 @@ Total: {: >22.1?}",
);
}
fn spawn(
mut commands: Commands,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
material_presets: Res<MaterialPresets>,
) {
use turborand::prelude::*;
let rng = Rng::with_seed(342525);
let noise = Perlin::new(345612);
let rng = || loop {
let noise_scale = 5.0;
let threshold = 0.70;
let rng_val = || rng.f64_normalized() * noise_scale;
let coord = [rng_val(), rng_val(), rng_val()];
if noise.get(coord) > threshold {
return DVec3::from_array(coord).as_vec3() * HALF_WIDTH * CELL_WIDTH
/ noise_scale as f32;
}
};
let values: Vec<_> = std::iter::repeat_with(rng).take(N_ENTITIES).collect();
let sphere_mesh_lq = meshes.add(
Sphere::new(HALF_WIDTH / (N_ENTITIES as f32).powf(0.33) * 0.2)
.mesh()
.ico(0)
.unwrap(),
);
fn spawn(mut commands: Commands) {
commands.spawn_big_space::<i32>(Grid::new(CELL_WIDTH, 0.0), |root| {
root.spawn_spatial((
FloatingOrigin,
@ -298,32 +279,84 @@ fn spawn(
b.spawn(DirectionalLight::default());
});
for (i, value) in values.iter().enumerate() {
let mut sphere_builder = root.spawn((BigSpatialBundle::<i32> {
transform: Transform::from_xyz(value.x, value.y, value.z),
..default()
},));
if i == 0 {
sphere_builder.insert((
Player,
Mesh3d(meshes.add(Sphere::new(1.0))),
MeshMaterial3d(materials.add(Color::from(Srgba::new(20.0, 20.0, 0.0, 1.0)))),
Transform::from_scale(Vec3::splat(2.0)),
));
} else {
sphere_builder.insert((
root.spawn_spatial(Player);
});
}
fn spawn_spheres(
mut commands: Commands,
input: Res<ButtonInput<KeyCode>>,
material_presets: Res<MaterialPresets>,
mut grid: Query<(Entity, &Grid<i32>, &mut Children)>,
non_players: Query<(), With<NonPlayer>>,
) {
let n_entities = non_players.iter().len().max(1);
let n_spawn = if input.pressed(KeyCode::KeyG) {
n_entities
} else if input.pressed(KeyCode::KeyF) {
1_000
} else {
return;
};
let (entity, _grid, mut children) = grid.single_mut();
let mut dyn_parent = bevy_reflect::DynamicTupleStruct::default();
dyn_parent.insert(entity);
let dyn_parent = dyn_parent.as_partial_reflect();
let new_children = sample_noise(n_spawn, &Simplex::new(345612), &Rng::new())
.map(|value| {
let hash = GridHash::<i32>::__new_manual(entity, &GridCell::default());
commands
.spawn((
Transform::from_xyz(value.x, value.y, value.z),
GlobalTransform::default(),
GridCell::<i32>::default(),
FastGridHash::from(hash),
hash,
NonPlayer,
Mesh3d(sphere_mesh_lq.clone()),
Parent::from_reflect(dyn_parent).unwrap(),
Mesh3d(material_presets.sphere.clone_weak()),
MeshMaterial3d(material_presets.default.clone_weak()),
bevy_render::view::VisibilityRange {
start_margin: 1.0..5.0,
end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8,
use_aabb: false,
},
));
bevy_render::view::NoFrustumCulling,
))
.id()
})
.chain(children.iter().copied())
.collect::<SmallVec<[Entity; 8]>>();
let mut dyn_children = bevy_reflect::DynamicTupleStruct::default();
dyn_children.insert(new_children);
let dyn_children = dyn_children.as_partial_reflect();
*children = Children::from_reflect(dyn_children).unwrap();
}
#[inline]
fn sample_noise<'a, T: NoiseFn<f64, 3>>(
n_entities: usize,
noise: &'a T,
rng: &'a Rng,
) -> impl Iterator<Item = Vec3> + use<'a, T> {
std::iter::repeat_with(
|| loop {
let noise_scale = 0.05 * HALF_WIDTH as f64;
let threshold = 0.50;
let rng_val = || rng.f64_normalized() * noise_scale;
let coord = [rng_val(), rng_val(), rng_val()];
if noise.get(coord) > threshold {
return DVec3::from_array(coord).as_vec3() * HALF_WIDTH * CELL_WIDTH
/ noise_scale as f32;
}
}
});
},
// Vec3::ONE
)
.take(n_entities)
}
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {

View File

@ -21,6 +21,10 @@ pub trait BigSpaceCommands {
&mut self,
child_builder: impl FnOnce(&mut GridCommands<P>),
);
/// Access the [`GridCommands`] of an entity by passing in the [`Entity`] and [`Grid`]. Note
/// that the value of `grid` will be inserted in this entity when the command is applied.
fn grid<P: GridPrecision>(&mut self, entity: Entity, grid: Grid<P>) -> GridCommands<P>;
}
impl BigSpaceCommands for Commands<'_, '_> {
@ -45,6 +49,15 @@ impl BigSpaceCommands for Commands<'_, '_> {
) {
self.spawn_big_space(Grid::default(), child_builder);
}
fn grid<P: GridPrecision>(&mut self, entity: Entity, grid: Grid<P>) -> GridCommands<P> {
GridCommands {
entity,
commands: self.reborrow(),
grid,
children: Default::default(),
}
}
}
/// Build [`big_space`](crate) hierarchies more easily, with access to grids.
@ -68,6 +81,7 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
}
/// Spawn an entity in this grid.
#[inline]
pub fn spawn(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
let entity = self.commands.spawn(bundle).id();
self.children.push(entity);
@ -80,9 +94,9 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and insert the provided
/// bundle.
#[inline]
pub fn spawn_spatial(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
let entity = self
.commands
.spawn((
#[cfg(feature = "bevy_render")]
bevy_render::view::Visibility::default(),
@ -92,8 +106,6 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
.insert(bundle)
.id();
self.children.push(entity);
SpatialEntityCommands {
entity,
commands: self.commands.reborrow(),
@ -101,7 +113,8 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
}
}
/// Returns the [`Entity``] id of the entity.
/// Returns the [`Entity`] id of the entity.
#[inline]
pub fn id(&self) -> Entity {
self.entity
}
@ -109,6 +122,7 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and apply entity commands
/// to it via the closure. This allows you to insert bundles on this new spatial entities, and
/// add more children to it.
#[inline]
pub fn with_spatial(
&mut self,
spatial: impl FnOnce(&mut SpatialEntityCommands<P>),
@ -120,6 +134,7 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
/// Add a high-precision spatial entity ([`GridCell`]) to this grid, and apply entity commands
/// to it via the closure. This allows you to insert bundles on this new spatial entities, and
/// add more children to it.
#[inline]
pub fn with_grid(
&mut self,
new_grid: Grid<P>,
@ -130,16 +145,15 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
}
/// Same as [`Self::with_grid`], but using the default [`Grid`] value.
#[inline]
pub fn with_grid_default(&mut self, builder: impl FnOnce(&mut GridCommands<P>)) -> &mut Self {
self.with_grid(Grid::default(), builder)
}
/// Spawn a grid as a child of the current grid.
#[inline]
pub fn spawn_grid(&mut self, new_grid: Grid<P>, bundle: impl Bundle) -> GridCommands<P> {
let mut entity_commands = self.commands.entity(self.entity);
let mut commands = entity_commands.commands();
let entity = commands
let entity = self
.spawn((
#[cfg(feature = "bevy_render")]
bevy_render::view::Visibility::default(),
@ -150,8 +164,6 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
.insert(bundle)
.id();
self.children.push(entity);
GridCommands {
entity,
commands: self.commands.reborrow(),
@ -166,11 +178,13 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
}
/// Access the underlying commands.
#[inline]
pub fn commands(&mut self) -> &mut Commands<'a, 'a> {
&mut self.commands
}
/// Spawns the passed bundle which provides this grid, and adds it to this entity as a child.
#[inline]
pub fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
self.commands.entity(self.entity).with_child(bundle);
self

View File

@ -36,7 +36,7 @@ pub struct GridCellAny;
///
/// [`BigSpace`]s are only allowed to have a single type of `GridCell`, you cannot mix
/// [`GridPrecision`]s.
#[derive(Component, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Reflect)]
#[derive(Component, Default, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)]
#[reflect(Component, Default, PartialEq)]
#[require(Transform, GlobalTransform)]
#[component(storage = "Table", on_add = Self::on_add, on_remove = Self::on_remove)]
@ -87,6 +87,28 @@ impl<P: GridPrecision> GridCell<P> {
}
}
/// Returns a cell containing the minimum values for each element of self and rhs.
///
/// In other words this computes [self.x.min(rhs.x), self.y.min(rhs.y), ..].
pub fn min(&self, rhs: Self) -> Self {
Self {
x: self.x.min(rhs.x),
y: self.y.min(rhs.y),
z: self.z.min(rhs.z),
}
}
/// Returns a cell containing the maximum values for each element of self and rhs.
///
/// In other words this computes [self.x.max(rhs.x), self.y.max(rhs.y), ..].
pub fn max(&self, rhs: Self) -> Self {
Self {
x: self.x.max(rhs.x),
y: self.y.max(rhs.y),
z: self.z.max(rhs.z),
}
}
/// If an entity's transform translation becomes larger than the limit specified in its
/// [`Grid`], it will be relocated to the nearest grid cell to reduce the size of the transform.
pub fn recenter_large_transforms(

View File

@ -34,6 +34,12 @@ impl<P: GridPrecision> PartialEq<GridHash<P>> for FastGridHash {
}
}
impl<P: GridPrecision> From<GridHash<P>> for FastGridHash {
fn from(value: GridHash<P>) -> Self {
Self(value.pre_hash)
}
}
/// A unique spatial hash shared by all entities in the same [`GridCell`] within the same [`Grid`].
///
/// Once computed, a spatial hash can be used to rapidly check if any two entities are in the same
@ -153,52 +159,48 @@ impl<P: GridPrecision> GridHash<P> {
pub(super) fn update<F: GridHashMapFilter>(
mut commands: Commands,
mut changed_hashes: ResMut<ChangedGridHashes<P, F>>,
mut spatial_entities: ParamSet<(
Query<
(
Entity,
&Parent,
&GridCell<P>,
&mut GridHash<P>,
&mut FastGridHash,
),
(F, Or<(Changed<Parent>, Changed<GridCell<P>>)>),
>,
Query<(Entity, &Parent, &GridCell<P>), (F, Without<GridHash<P>>)>,
)>,
mut spatial_entities: Query<
(
Entity,
&Parent,
&GridCell<P>,
&mut GridHash<P>,
&mut FastGridHash,
),
(F, Or<(Changed<Parent>, Changed<GridCell<P>>)>),
>,
added_entities: Query<(Entity, &Parent, &GridCell<P>), (F, Without<GridHash<P>>)>,
mut stats: Option<ResMut<crate::timing::GridHashStats>>,
mut thread_changed_hashes: Local<Parallel<Vec<Entity>>>,
mut thread_updated_hashes: Local<Parallel<Vec<Entity>>>,
mut thread_commands: Local<Parallel<Vec<(Entity, GridHash<P>, FastGridHash)>>>,
) {
let start = Instant::now();
// Create new
spatial_entities
.p1()
added_entities
.par_iter()
.for_each(|(entity, parent, cell)| {
let spatial_hash = GridHash::new(parent, cell);
let fast_hash = FastGridHash(spatial_hash.pre_hash);
let fast_hash = spatial_hash.into();
thread_commands.scope(|tl| tl.push((entity, spatial_hash, fast_hash)));
thread_changed_hashes.scope(|tl| tl.push(entity));
thread_updated_hashes.scope(|tl| tl.push(entity));
});
for (entity, spatial_hash, fast_hash) in thread_commands.drain() {
commands.entity(entity).insert((spatial_hash, fast_hash));
}
// Update existing
spatial_entities.p0().par_iter_mut().for_each(
spatial_entities.par_iter_mut().for_each(
|(entity, parent, cell, mut hash, mut fast_hash)| {
let new_hash = GridHash::new(parent, cell);
let new_fast_hash = new_hash.pre_hash;
if hash.replace_if_neq(new_hash).is_some() {
thread_changed_hashes.scope(|tl| tl.push(entity));
thread_updated_hashes.scope(|tl| tl.push(entity));
}
fast_hash.0 = new_fast_hash;
},
);
changed_hashes.list.extend(thread_changed_hashes.drain());
thread_updated_hashes.drain_into(&mut changed_hashes.updated);
if let Some(ref mut stats) = stats {
stats.hash_update_duration += start.elapsed();

View File

@ -116,7 +116,11 @@ where
}
}
impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
impl<P, F> GridHashMap<P, F>
where
P: GridPrecision,
F: GridHashMapFilter,
{
/// Get information about all entities located at this [`GridHash`], as well as its
/// neighbors.
#[inline]
@ -243,7 +247,11 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
}
/// Private Systems
impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
impl<P, F> GridHashMap<P, F>
where
P: GridPrecision,
F: GridHashMapFilter,
{
/// Update the [`GridHashMap`] with entities that have changed [`GridHash`]es, and meet the
/// optional [`GridHashMapFilter`].
pub(super) fn update(
@ -263,12 +271,12 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
}
if let Some(ref mut stats) = stats {
stats.moved_entities = changed_hashes.list.len();
stats.moved_entities = changed_hashes.updated.len();
}
// See the docs on ChangedGridHash understand why we don't use query change detection.
for (entity, spatial_hash) in changed_hashes
.list
.updated
.drain(..)
.filter_map(|entity| all_hashes.get(entity).ok())
{
@ -282,7 +290,11 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
}
/// Private Methods
impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
impl<P, F> GridHashMap<P, F>
where
P: GridPrecision,
F: GridHashMapFilter,
{
/// Insert an entity into the [`GridHashMap`], updating any existing entries.
#[inline]
fn insert(&mut self, entity: Entity, hash: GridHash<P>) {
@ -361,31 +373,35 @@ impl<P: GridPrecision> InnerGridHashMap<P> {
} else {
let mut entities = self.hash_set_pool.pop().unwrap_or_default();
entities.insert(entity);
self.insert_entry(hash, entities);
}
}
let mut occupied_neighbors = self.neighbor_pool.pop().unwrap_or_default();
occupied_neighbors.extend(hash.adjacent(1).filter(|neighbor| {
self.inner
.get_mut(neighbor)
.map(|entry| {
entry.occupied_neighbors.push(hash);
true
})
.unwrap_or_default()
}));
#[inline]
fn insert_entry(&mut self, hash: GridHash<P>, entities: HashSet<Entity, EntityHash>) {
let mut occupied_neighbors = self.neighbor_pool.pop().unwrap_or_default();
occupied_neighbors.extend(hash.adjacent(1).filter(|neighbor| {
self.inner
.get_mut(neighbor)
.map(|entry| {
entry.occupied_neighbors.push(hash);
true
})
.unwrap_or_default()
}));
self.inner.insert(
hash,
GridHashEntry {
entities,
occupied_neighbors,
},
);
self.inner.insert(
hash,
GridHashEntry {
entities,
occupied_neighbors,
},
);
if !self.just_removed.remove(&hash) {
// If a cell is removed then added within the same update, it can't be considered
// "just added" because it *already existed* at the start of the update.
self.just_inserted.insert(hash);
}
if !self.just_removed.remove(&hash) {
// If a cell is removed then added within the same update, it can't be considered
// "just added" because it *already existed* at the start of the update.
self.just_inserted.insert(hash);
}
}

View File

@ -92,14 +92,14 @@ impl<T: QueryFilter + Send + Sync + 'static> GridHashMapFilter for T {}
/// react to a component being mutated. For now, this performs well enough.
#[derive(Resource)]
struct ChangedGridHashes<P: GridPrecision, F: GridHashMapFilter> {
list: Vec<Entity>,
updated: Vec<Entity>,
spooky: PhantomData<(P, F)>,
}
impl<P: GridPrecision, F: GridHashMapFilter> Default for ChangedGridHashes<P, F> {
fn default() -> Self {
Self {
list: Vec::new(),
updated: Vec::new(),
spooky: PhantomData,
}
}

View File

@ -1,16 +1,18 @@
//! Detect and update groups of nearby occupied cells.
use std::{hash::Hash, marker::PhantomData, ops::Deref, time::Instant};
use std::{hash::Hash, marker::PhantomData, ops::Deref};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::{ComputeTaskPool, ParallelSliceMut};
use bevy_utils::{
hashbrown::{HashMap, HashSet},
PassHash,
Instant, PassHash,
};
use super::{GridHash, GridHashMap, GridHashMapFilter, GridHashMapSystem, GridPrecision};
use super::{GridCell, GridHash, GridHashMap, GridHashMapFilter, GridHashMapSystem, GridPrecision};
pub use private::GridPartition;
/// Adds support for spatial partitioning. Requires [`GridHashPlugin`](super::GridHashPlugin).
pub struct GridPartitionPlugin<P, F = ()>(PhantomData<(P, F)>)
@ -61,7 +63,7 @@ impl Hash for GridPartitionId {
}
}
/// Groups connected [`GridCell`](crate::GridCell)s into [`GridPartition`]s.
/// Groups connected [`GridCell`]s into [`GridPartition`]s.
///
/// Partitions divide space into independent groups of cells.
///
@ -135,15 +137,16 @@ where
let Some(hash) = set.iter().next() else {
return;
};
let mut min = hash.cell();
let mut max = hash.cell();
for hash in set.iter() {
self.reverse_map.insert(*hash, partition);
min = min.min(hash.cell());
max = max.max(hash.cell());
}
self.partitions.insert(
partition,
GridPartition {
grid: hash.grid(),
tables: vec![set],
},
GridPartition::new(hash.grid(), vec![set], min, max),
);
}
@ -162,8 +165,14 @@ where
let Some(old_id) = self.reverse_map.remove(hash) else {
return;
};
let mut empty = false;
if let Some(partition) = self.partitions.get_mut(&old_id) {
partition.tables.iter_mut().any(|table| table.remove(hash));
if partition.remove(hash) && partition.is_empty() {
empty = true;
}
}
if empty {
self.partitions.remove(&old_id);
}
}
@ -252,12 +261,6 @@ where
partition_map.remove(removed_cell);
}
// Clean up empty tables and partitions
partition_map.partitions.retain(|_id, partition| {
partition.tables.retain(|table| !table.is_empty());
!partition.tables.is_empty()
});
for removed_cell in hash_grid.just_removed().iter() {
// Group occupied neighbor cells by partition, so we can check if they are still
// connected to each other after this removal.
@ -293,7 +296,7 @@ where
let _task_span = tracing::info_span!("parallel partition split").entered();
affected_cells
.iter_mut()
.filter_map(|(original_partition, adjacent_hashes)| {
.filter_map(|(id, adjacent_hashes)| {
let mut new_partitions = Vec::with_capacity(0);
let mut counter = 0;
while let Some(this_cell) = adjacent_hashes.iter().next().copied() {
@ -309,7 +312,7 @@ where
if adjacent_hashes.is_empty() && counter == 0 {
// If it only took a single iteration to connect all affected cells,
// it means the partition has not been split, and we can continue to
// the next // partition.
// the next partition.
return None;
} else {
new_partitions
@ -319,7 +322,7 @@ where
}
Some(SplitResult {
original_partition: *original_partition,
original_partition_id: *id,
new_partitions,
})
})
@ -328,27 +331,15 @@ where
);
for SplitResult {
original_partition,
original_partition_id,
ref mut new_partitions,
} in split_results.iter_mut().flatten()
{
// We want the original partition to retain the most cells to ensure that the smaller
// sets are the ones that are assigned a new partition ID.
new_partitions.sort_unstable_by_key(|v| v.len());
if let Some(partition) = new_partitions.pop() {
if let Some(tables) = partition_map
.partitions
.get_mut(original_partition)
.map(|p| &mut p.tables)
{
// TODO: keep these in an object pool to reuse allocs
tables.drain(1..);
if let Some(table) = tables.get_mut(0) {
*table = partition;
} else {
tables.push(partition);
}
}
new_partitions.sort_unstable_by_key(|set| set.len());
if let Some(largest_partition) = new_partitions.pop() {
partition_map.insert(*original_partition_id, largest_partition);
}
// At this point the reverse map will be out of date. However, `partitions.insert()`
@ -363,86 +354,193 @@ where
}
struct SplitResult<P: GridPrecision> {
original_partition: GridPartitionId,
original_partition_id: GridPartitionId,
new_partitions: Vec<HashSet<GridHash<P>, PassHash>>,
}
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
/// [`GridCell`](crate::GridCell)s.
#[derive(Debug)]
pub struct GridPartition<P: GridPrecision> {
grid: Entity,
tables: Vec<HashSet<GridHash<P>, PassHash>>,
}
impl<P: GridPrecision> GridPartition<P> {
/// Tables smaller than this will be drained into other tables when merging. Tables larger than
/// this limit will instead be added to a list of tables. This prevents partitions ending up
/// with many tables containing a few entries.
///
/// Draining and extending a hash set is much slower than moving the entire hash set into a
/// list. The tradeoff is that the more tables added, the more there are that need to be
/// iterated over when searching for a cell.
const MIN_TABLE_SIZE: usize = 128;
/// Returns `true` if the `hash` is in this partition.
#[inline]
pub fn contains(&self, hash: &GridHash<P>) -> bool {
self.tables.iter().any(|table| table.contains(hash))
/// A private module to ensure the internal fields of the partition are not accessed directly.
/// Needed to ensure invariants are upheld.
mod private {
use super::{GridCell, GridHash, GridPrecision};
use bevy_ecs::prelude::*;
use bevy_utils::{hashbrown::HashSet, PassHash};
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
/// [`GridCell`](crate::GridCell)s.
#[derive(Debug)]
pub struct GridPartition<P: GridPrecision> {
grid: Entity,
tables: Vec<HashSet<GridHash<P>, PassHash>>,
min: GridCell<P>,
max: GridCell<P>,
}
/// Iterates over all [`GridHash`]s in this partition.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &GridHash<P>> {
self.tables.iter().flat_map(|table| table.iter())
}
/// Returns the total number of cells in this partition.
#[inline]
pub fn num_cells(&self) -> usize {
self.tables.iter().map(|t| t.len()).sum()
}
#[inline]
fn insert(&mut self, cell: GridHash<P>) {
if self.contains(&cell) {
return;
impl<P: GridPrecision> GridPartition<P> {
/// Returns `true` if the `hash` is in this partition.
#[inline]
pub fn contains(&self, hash: &GridHash<P>) -> bool {
self.tables.iter().any(|table| table.contains(hash))
}
if let Some(i) = self.smallest_table() {
self.tables[i].insert(cell);
} else {
let mut table = HashSet::default();
table.insert(cell);
self.tables.push(table);
/// Iterates over all [`GridHash`]s in this partition.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &GridHash<P>> {
self.tables.iter().flat_map(|table| table.iter())
}
/// Returns the total number of cells in this partition.
#[inline]
pub fn num_cells(&self) -> usize {
self.tables.iter().map(|t| t.len()).sum()
}
/// The grid this partition resides in.
#[inline]
pub fn grid(&self) -> Entity {
self.grid
}
/// The maximum grid cell extent of the partition.
pub fn max(&self) -> GridCell<P> {
self.max
}
/// The minimum grid cell extent of the partition.
pub fn min(&self) -> GridCell<P> {
self.min
}
/// Frees up any unused memory. Returns `false` if the partition is completely empty.
pub fn is_empty(&self) -> bool {
self.tables.is_empty()
}
}
#[inline]
fn smallest_table(&self) -> Option<usize> {
self.tables
.iter()
.enumerate()
.map(|(i, t)| (i, t.len()))
.min_by_key(|(_, len)| *len)
.map(|(i, _len)| i)
}
/// Private internal methods
impl<P: GridPrecision> GridPartition<P> {
pub(crate) fn new(
grid: Entity,
tables: Vec<HashSet<GridHash<P>, PassHash>>,
min: GridCell<P>,
max: GridCell<P>,
) -> Self {
Self {
grid,
tables,
min,
max,
}
}
#[inline]
fn extend(&mut self, mut partition: GridPartition<P>) {
for mut table in partition.tables.drain(..) {
if table.len() < Self::MIN_TABLE_SIZE {
if let Some(i) = self.smallest_table() {
self.tables[i].extend(table.drain());
/// Tables smaller than this will be drained into other tables when merging. Tables larger than
/// this limit will instead be added to a list of tables. This prevents partitions ending up
/// with many tables containing a few entries.
///
/// Draining and extending a hash set is much slower than moving the entire hash set into a
/// list. The tradeoff is that the more tables added, the more there are that need to be
/// iterated over when searching for a cell.
const MIN_TABLE_SIZE: usize = 20_000;
#[inline]
pub(crate) fn insert(&mut self, cell: GridHash<P>) {
if self.contains(&cell) {
return;
}
if let Some(i) = self.smallest_table() {
self.tables[i].insert(cell);
} else {
let mut table = HashSet::default();
table.insert(cell);
self.tables.push(table);
}
self.min = self.min.min(cell.cell());
self.max = self.max.max(cell.cell());
}
#[inline]
fn smallest_table(&self) -> Option<usize> {
self.tables
.iter()
.enumerate()
.map(|(i, t)| (i, t.len()))
.min_by_key(|(_, len)| *len)
.map(|(i, _len)| i)
}
#[inline]
pub(crate) fn extend(&mut self, mut partition: GridPartition<P>) {
for mut table in partition.tables.drain(..) {
if table.len() < Self::MIN_TABLE_SIZE {
if let Some(i) = self.smallest_table() {
for hash in table.drain() {
self.tables[i].insert_unique_unchecked(hash);
}
} else {
self.tables.push(table);
}
} else {
self.tables.push(table);
}
}
self.min = self.min.min(partition.min);
self.max = self.max.max(partition.max);
}
/// Removes a grid hash from the partition. Returns whether the value was present.
#[inline]
pub(crate) fn remove(&mut self, hash: &GridHash<P>) -> bool {
let Some(i_table) = self
.tables
.iter_mut()
.enumerate()
.find_map(|(i, table)| table.remove(hash).then_some(i))
else {
return false;
};
if self.tables[i_table].is_empty() {
self.tables.swap_remove(i_table);
}
let (cell, min, max) = (hash.cell(), self.min, self.max);
// Only need to recompute the bounds if the removed cell was touching the boundary.
if min.x == cell.x || min.y == cell.y || min.z == cell.z {
self.compute_min();
}
// Note this is not an `else if`. The cell might be on the max bound in one axis, and the
// min bound in another.
if max.x == cell.x || max.y == cell.y || max.z == cell.z {
self.compute_max();
}
true
}
/// Computes the minimum bounding coordinate. Requires linearly scanning over entries in the
/// partition.
#[inline]
fn compute_min(&mut self) {
if let Some(min) = self
.iter()
.map(|hash| hash.cell())
.reduce(|acc, e| acc.min(e))
{
self.min = min
} else {
self.tables.push(table);
self.min = GridCell::ONE * P::from_f64(1e10);
}
}
/// Computes the maximum bounding coordinate. Requires linearly scanning over entries in the
/// partition.
#[inline]
fn compute_max(&mut self) {
if let Some(max) = self
.iter()
.map(|hash| hash.cell())
.reduce(|acc, e| acc.max(e))
{
self.max = max
} else {
self.min = GridCell::ONE * P::from_f64(-1e10);
}
}
}
/// The grid this partition resides in.
pub fn grid(&self) -> Entity {
self.grid
}
}