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_ecs::entity::EntityHasher;
use bevy_math::DVec3; use bevy_math::DVec3;
use big_space::prelude::*; 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() { fn main() {
App::new() App::new()
@ -26,19 +35,11 @@ fn main() {
draw_partitions.after(GridHashMapSystem::UpdatePartition), draw_partitions.after(GridHashMapSystem::UpdatePartition),
), ),
) )
.add_systems(Update, cursor_grab) .add_systems(Update, (cursor_grab, spawn_spheres))
.init_resource::<MaterialPresets>() .init_resource::<MaterialPresets>()
.run(); .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)] #[derive(Component)]
struct Player; struct Player;
@ -50,25 +51,35 @@ struct MaterialPresets {
default: Handle<StandardMaterial>, default: Handle<StandardMaterial>,
highlight: Handle<StandardMaterial>, highlight: Handle<StandardMaterial>,
flood: Handle<StandardMaterial>, flood: Handle<StandardMaterial>,
sphere: Handle<Mesh>,
} }
impl FromWorld for MaterialPresets { impl FromWorld for MaterialPresets {
fn from_world(world: &mut World) -> Self { fn from_world(world: &mut World) -> Self {
let mut materials = world.resource_mut::<Assets<StandardMaterial>>(); 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)), base_color: Color::from(Srgba::new(0.5, 0.5, 0.5, 1.0)),
perceptual_roughness: 0.2, perceptual_roughness: 0.2,
metallic: 0.0, metallic: 0.0,
..Default::default() ..Default::default()
}; });
let h: StandardMaterial = Color::from(Srgba::new(2.0, 0.0, 8.0, 1.0)).into(); let highlight = materials.add(Color::from(Srgba::new(2.0, 0.0, 8.0, 1.0)));
let f: StandardMaterial = Color::from(Srgba::new(1.1, 0.1, 1.0, 1.0)).into(); 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 { Self {
default: materials.add(d), default,
highlight: materials.add(h), highlight,
flood: materials.add(f), flood,
sphere,
} }
} }
} }
@ -79,7 +90,7 @@ fn draw_partitions(
grids: Query<(&GlobalTransform, &Grid<i32>)>, grids: Query<(&GlobalTransform, &Grid<i32>)>,
camera: Query<&GridHash<i32>, With<Camera>>, 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 { let Ok((transform, grid)) = grids.get(p.grid()) else {
return; return;
}; };
@ -92,35 +103,19 @@ fn draw_partitions(
p.iter() p.iter()
.filter(|hash| *hash != camera.single()) .filter(|hash| *hash != camera.single())
.take(1_000)
.for_each(|h| { .for_each(|h| {
let center = [h.cell().x, h.cell().y, h.cell().z]; let center = [h.cell().x, h.cell().y, h.cell().z];
let local_trans = Transform::from_translation(IVec3::from(center).as_vec3() * l) let local_trans = Transform::from_translation(IVec3::from(center).as_vec3() * l)
.with_scale(Vec3::splat(l)); .with_scale(Vec3::splat(l));
gizmos.cuboid( gizmos.cuboid(
transform.mul_transform(local_trans), 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 let min = IVec3::from([p.min().x, p.min().y, p.min().z]).as_vec3() * l;
.iter() let max = IVec3::from([p.max().x, p.max().y, p.max().z]).as_vec3() * l;
.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 size = max - min; let size = max - min;
let center = min + (size) * 0.5; let center = min + (size) * 0.5;
@ -152,6 +147,7 @@ fn move_player(
hash_stats: Res<big_space::timing::SmoothedStat<big_space::timing::GridHashStats>>, hash_stats: Res<big_space::timing::SmoothedStat<big_space::timing::GridHashStats>>,
prop_stats: Res<big_space::timing::SmoothedStat<big_space::timing::PropagationStats>>, prop_stats: Res<big_space::timing::SmoothedStat<big_space::timing::PropagationStats>>,
) { ) {
let n_entities = non_player.iter().len();
for neighbor in neighbors.iter() { for neighbor in neighbors.iter() {
if let Ok(mut material) = materials.get_mut(*neighbor) { if let Ok(mut material) = materials.get_mut(*neighbor) {
**material = material_presets.default.clone_weak(); **material = material_presets.default.clone_weak();
@ -163,10 +159,12 @@ fn move_player(
if scale.abs() > 0.0 { if scale.abs() > 0.0 {
// Avoid change detection // Avoid change detection
for (i, (mut transform, _, _)) in non_player.iter_mut().enumerate() { 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.x += t.sin() * scale;
transform.translation.y += t.cos() * scale; transform.translation.y += t.cos() * scale;
transform.translation.z += (t * 2.3).sin() * scale; transform.translation.z += (t * 2.3).sin() * scale;
} else {
break;
} }
} }
} }
@ -213,6 +211,10 @@ fn move_player(
let mut text = text.single_mut(); let mut text = text.single_mut();
text.0 = format!( text.0 = format!(
"\ "\
Controls:
WASD to move, QE to roll
F to spawn 1,000, G to double
Population: {: >8} Entities Population: {: >8} Entities
Transform Propagation Transform Propagation
@ -229,7 +231,15 @@ Update Maps: {: >16.1?}
Update Partitions: {: >10.1?} Update Partitions: {: >10.1?}
Total: {: >22.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().grid_recentering(),
prop_stats.avg().low_precision_root_tagging(), prop_stats.avg().low_precision_root_tagging(),
@ -246,36 +256,7 @@ Total: {: >22.1?}",
); );
} }
fn spawn( fn spawn(mut commands: Commands) {
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(),
);
commands.spawn_big_space::<i32>(Grid::new(CELL_WIDTH, 0.0), |root| { commands.spawn_big_space::<i32>(Grid::new(CELL_WIDTH, 0.0), |root| {
root.spawn_spatial(( root.spawn_spatial((
FloatingOrigin, FloatingOrigin,
@ -298,32 +279,84 @@ fn spawn(
b.spawn(DirectionalLight::default()); b.spawn(DirectionalLight::default());
}); });
for (i, value) in values.iter().enumerate() { root.spawn_spatial(Player);
let mut sphere_builder = root.spawn((BigSpatialBundle::<i32> { });
transform: Transform::from_xyz(value.x, value.y, value.z), }
..default()
},)); fn spawn_spheres(
if i == 0 { mut commands: Commands,
sphere_builder.insert(( input: Res<ButtonInput<KeyCode>>,
Player, material_presets: Res<MaterialPresets>,
Mesh3d(meshes.add(Sphere::new(1.0))), mut grid: Query<(Entity, &Grid<i32>, &mut Children)>,
MeshMaterial3d(materials.add(Color::from(Srgba::new(20.0, 20.0, 0.0, 1.0)))), non_players: Query<(), With<NonPlayer>>,
Transform::from_scale(Vec3::splat(2.0)), ) {
)); let n_entities = non_players.iter().len().max(1);
} else { let n_spawn = if input.pressed(KeyCode::KeyG) {
sphere_builder.insert(( 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, NonPlayer,
Mesh3d(sphere_mesh_lq.clone()), Parent::from_reflect(dyn_parent).unwrap(),
Mesh3d(material_presets.sphere.clone_weak()),
MeshMaterial3d(material_presets.default.clone_weak()), MeshMaterial3d(material_presets.default.clone_weak()),
bevy_render::view::VisibilityRange { bevy_render::view::VisibilityRange {
start_margin: 1.0..5.0, start_margin: 1.0..5.0,
end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8, end_margin: HALF_WIDTH * CELL_WIDTH * 0.5..HALF_WIDTH * CELL_WIDTH * 0.8,
use_aabb: false, 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>) { fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {

View File

@ -21,6 +21,10 @@ pub trait BigSpaceCommands {
&mut self, &mut self,
child_builder: impl FnOnce(&mut GridCommands<P>), 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<'_, '_> { impl BigSpaceCommands for Commands<'_, '_> {
@ -45,6 +49,15 @@ impl BigSpaceCommands for Commands<'_, '_> {
) { ) {
self.spawn_big_space(Grid::default(), child_builder); 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. /// 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. /// Spawn an entity in this grid.
#[inline]
pub fn spawn(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> { pub fn spawn(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
let entity = self.commands.spawn(bundle).id(); let entity = self.commands.spawn(bundle).id();
self.children.push(entity); 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 /// Add a high-precision spatial entity ([`GridCell`]) to this grid, and insert the provided
/// bundle. /// bundle.
#[inline]
pub fn spawn_spatial(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> { pub fn spawn_spatial(&mut self, bundle: impl Bundle) -> SpatialEntityCommands<P> {
let entity = self let entity = self
.commands
.spawn(( .spawn((
#[cfg(feature = "bevy_render")] #[cfg(feature = "bevy_render")]
bevy_render::view::Visibility::default(), bevy_render::view::Visibility::default(),
@ -92,8 +106,6 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
.insert(bundle) .insert(bundle)
.id(); .id();
self.children.push(entity);
SpatialEntityCommands { SpatialEntityCommands {
entity, entity,
commands: self.commands.reborrow(), 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 { pub fn id(&self) -> Entity {
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 /// 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 /// to it via the closure. This allows you to insert bundles on this new spatial entities, and
/// add more children to it. /// add more children to it.
#[inline]
pub fn with_spatial( pub fn with_spatial(
&mut self, &mut self,
spatial: impl FnOnce(&mut SpatialEntityCommands<P>), 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 /// 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 /// to it via the closure. This allows you to insert bundles on this new spatial entities, and
/// add more children to it. /// add more children to it.
#[inline]
pub fn with_grid( pub fn with_grid(
&mut self, &mut self,
new_grid: Grid<P>, 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. /// 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 { pub fn with_grid_default(&mut self, builder: impl FnOnce(&mut GridCommands<P>)) -> &mut Self {
self.with_grid(Grid::default(), builder) self.with_grid(Grid::default(), builder)
} }
/// Spawn a grid as a child of the current grid. /// 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> { 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 entity = self
let mut commands = entity_commands.commands();
let entity = commands
.spawn(( .spawn((
#[cfg(feature = "bevy_render")] #[cfg(feature = "bevy_render")]
bevy_render::view::Visibility::default(), bevy_render::view::Visibility::default(),
@ -150,8 +164,6 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
.insert(bundle) .insert(bundle)
.id(); .id();
self.children.push(entity);
GridCommands { GridCommands {
entity, entity,
commands: self.commands.reborrow(), commands: self.commands.reborrow(),
@ -166,11 +178,13 @@ impl<'a, P: GridPrecision> GridCommands<'a, P> {
} }
/// Access the underlying commands. /// Access the underlying commands.
#[inline]
pub fn commands(&mut self) -> &mut Commands<'a, 'a> { pub fn commands(&mut self) -> &mut Commands<'a, 'a> {
&mut self.commands &mut self.commands
} }
/// Spawns the passed bundle which provides this grid, and adds it to this entity as a child. /// 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 { pub fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
self.commands.entity(self.entity).with_child(bundle); self.commands.entity(self.entity).with_child(bundle);
self 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 /// [`BigSpace`]s are only allowed to have a single type of `GridCell`, you cannot mix
/// [`GridPrecision`]s. /// [`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)] #[reflect(Component, Default, PartialEq)]
#[require(Transform, GlobalTransform)] #[require(Transform, GlobalTransform)]
#[component(storage = "Table", on_add = Self::on_add, on_remove = Self::on_remove)] #[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 /// 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. /// [`Grid`], it will be relocated to the nearest grid cell to reduce the size of the transform.
pub fn recenter_large_transforms( 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`]. /// 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 /// 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>( pub(super) fn update<F: GridHashMapFilter>(
mut commands: Commands, mut commands: Commands,
mut changed_hashes: ResMut<ChangedGridHashes<P, F>>, mut changed_hashes: ResMut<ChangedGridHashes<P, F>>,
mut spatial_entities: ParamSet<( mut spatial_entities: Query<
Query< (
( Entity,
Entity, &Parent,
&Parent, &GridCell<P>,
&GridCell<P>, &mut GridHash<P>,
&mut GridHash<P>, &mut FastGridHash,
&mut FastGridHash, ),
), (F, Or<(Changed<Parent>, Changed<GridCell<P>>)>),
(F, Or<(Changed<Parent>, Changed<GridCell<P>>)>), >,
>, added_entities: Query<(Entity, &Parent, &GridCell<P>), (F, Without<GridHash<P>>)>,
Query<(Entity, &Parent, &GridCell<P>), (F, Without<GridHash<P>>)>,
)>,
mut stats: Option<ResMut<crate::timing::GridHashStats>>, 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)>>>, mut thread_commands: Local<Parallel<Vec<(Entity, GridHash<P>, FastGridHash)>>>,
) { ) {
let start = Instant::now(); let start = Instant::now();
// Create new // Create new
spatial_entities added_entities
.p1()
.par_iter() .par_iter()
.for_each(|(entity, parent, cell)| { .for_each(|(entity, parent, cell)| {
let spatial_hash = GridHash::new(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_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() { for (entity, spatial_hash, fast_hash) in thread_commands.drain() {
commands.entity(entity).insert((spatial_hash, fast_hash)); commands.entity(entity).insert((spatial_hash, fast_hash));
} }
// Update existing // 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)| { |(entity, parent, cell, mut hash, mut fast_hash)| {
let new_hash = GridHash::new(parent, cell); let new_hash = GridHash::new(parent, cell);
let new_fast_hash = new_hash.pre_hash; let new_fast_hash = new_hash.pre_hash;
if hash.replace_if_neq(new_hash).is_some() { 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; fast_hash.0 = new_fast_hash;
}, },
); );
thread_updated_hashes.drain_into(&mut changed_hashes.updated);
changed_hashes.list.extend(thread_changed_hashes.drain());
if let Some(ref mut stats) = stats { if let Some(ref mut stats) = stats {
stats.hash_update_duration += start.elapsed(); 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 /// Get information about all entities located at this [`GridHash`], as well as its
/// neighbors. /// neighbors.
#[inline] #[inline]
@ -243,7 +247,11 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
} }
/// Private Systems /// 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 /// Update the [`GridHashMap`] with entities that have changed [`GridHash`]es, and meet the
/// optional [`GridHashMapFilter`]. /// optional [`GridHashMapFilter`].
pub(super) fn update( pub(super) fn update(
@ -263,12 +271,12 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
} }
if let Some(ref mut stats) = stats { 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. // See the docs on ChangedGridHash understand why we don't use query change detection.
for (entity, spatial_hash) in changed_hashes for (entity, spatial_hash) in changed_hashes
.list .updated
.drain(..) .drain(..)
.filter_map(|entity| all_hashes.get(entity).ok()) .filter_map(|entity| all_hashes.get(entity).ok())
{ {
@ -282,7 +290,11 @@ impl<P: GridPrecision, F: GridHashMapFilter> GridHashMap<P, F> {
} }
/// Private Methods /// 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. /// Insert an entity into the [`GridHashMap`], updating any existing entries.
#[inline] #[inline]
fn insert(&mut self, entity: Entity, hash: GridHash<P>) { fn insert(&mut self, entity: Entity, hash: GridHash<P>) {
@ -361,31 +373,35 @@ impl<P: GridPrecision> InnerGridHashMap<P> {
} else { } else {
let mut entities = self.hash_set_pool.pop().unwrap_or_default(); let mut entities = self.hash_set_pool.pop().unwrap_or_default();
entities.insert(entity); entities.insert(entity);
self.insert_entry(hash, entities);
}
}
let mut occupied_neighbors = self.neighbor_pool.pop().unwrap_or_default(); #[inline]
occupied_neighbors.extend(hash.adjacent(1).filter(|neighbor| { fn insert_entry(&mut self, hash: GridHash<P>, entities: HashSet<Entity, EntityHash>) {
self.inner let mut occupied_neighbors = self.neighbor_pool.pop().unwrap_or_default();
.get_mut(neighbor) occupied_neighbors.extend(hash.adjacent(1).filter(|neighbor| {
.map(|entry| { self.inner
entry.occupied_neighbors.push(hash); .get_mut(neighbor)
true .map(|entry| {
}) entry.occupied_neighbors.push(hash);
.unwrap_or_default() true
})); })
.unwrap_or_default()
}));
self.inner.insert( self.inner.insert(
hash, hash,
GridHashEntry { GridHashEntry {
entities, entities,
occupied_neighbors, occupied_neighbors,
}, },
); );
if !self.just_removed.remove(&hash) { if !self.just_removed.remove(&hash) {
// If a cell is removed then added within the same update, it can't be considered // 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. // "just added" because it *already existed* at the start of the update.
self.just_inserted.insert(hash); 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. /// react to a component being mutated. For now, this performs well enough.
#[derive(Resource)] #[derive(Resource)]
struct ChangedGridHashes<P: GridPrecision, F: GridHashMapFilter> { struct ChangedGridHashes<P: GridPrecision, F: GridHashMapFilter> {
list: Vec<Entity>, updated: Vec<Entity>,
spooky: PhantomData<(P, F)>, spooky: PhantomData<(P, F)>,
} }
impl<P: GridPrecision, F: GridHashMapFilter> Default for ChangedGridHashes<P, F> { impl<P: GridPrecision, F: GridHashMapFilter> Default for ChangedGridHashes<P, F> {
fn default() -> Self { fn default() -> Self {
Self { Self {
list: Vec::new(), updated: Vec::new(),
spooky: PhantomData, spooky: PhantomData,
} }
} }

View File

@ -1,16 +1,18 @@
//! Detect and update groups of nearby occupied cells. //! 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_app::prelude::*;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_tasks::{ComputeTaskPool, ParallelSliceMut}; use bevy_tasks::{ComputeTaskPool, ParallelSliceMut};
use bevy_utils::{ use bevy_utils::{
hashbrown::{HashMap, HashSet}, 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). /// Adds support for spatial partitioning. Requires [`GridHashPlugin`](super::GridHashPlugin).
pub struct GridPartitionPlugin<P, F = ()>(PhantomData<(P, F)>) 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. /// Partitions divide space into independent groups of cells.
/// ///
@ -135,15 +137,16 @@ where
let Some(hash) = set.iter().next() else { let Some(hash) = set.iter().next() else {
return; return;
}; };
let mut min = hash.cell();
let mut max = hash.cell();
for hash in set.iter() { for hash in set.iter() {
self.reverse_map.insert(*hash, partition); self.reverse_map.insert(*hash, partition);
min = min.min(hash.cell());
max = max.max(hash.cell());
} }
self.partitions.insert( self.partitions.insert(
partition, partition,
GridPartition { GridPartition::new(hash.grid(), vec![set], min, max),
grid: hash.grid(),
tables: vec![set],
},
); );
} }
@ -162,8 +165,14 @@ where
let Some(old_id) = self.reverse_map.remove(hash) else { let Some(old_id) = self.reverse_map.remove(hash) else {
return; return;
}; };
let mut empty = false;
if let Some(partition) = self.partitions.get_mut(&old_id) { 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); 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() { for removed_cell in hash_grid.just_removed().iter() {
// Group occupied neighbor cells by partition, so we can check if they are still // Group occupied neighbor cells by partition, so we can check if they are still
// connected to each other after this removal. // connected to each other after this removal.
@ -293,7 +296,7 @@ where
let _task_span = tracing::info_span!("parallel partition split").entered(); let _task_span = tracing::info_span!("parallel partition split").entered();
affected_cells affected_cells
.iter_mut() .iter_mut()
.filter_map(|(original_partition, adjacent_hashes)| { .filter_map(|(id, adjacent_hashes)| {
let mut new_partitions = Vec::with_capacity(0); let mut new_partitions = Vec::with_capacity(0);
let mut counter = 0; let mut counter = 0;
while let Some(this_cell) = adjacent_hashes.iter().next().copied() { while let Some(this_cell) = adjacent_hashes.iter().next().copied() {
@ -309,7 +312,7 @@ where
if adjacent_hashes.is_empty() && counter == 0 { if adjacent_hashes.is_empty() && counter == 0 {
// If it only took a single iteration to connect all affected cells, // 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 // it means the partition has not been split, and we can continue to
// the next // partition. // the next partition.
return None; return None;
} else { } else {
new_partitions new_partitions
@ -319,7 +322,7 @@ where
} }
Some(SplitResult { Some(SplitResult {
original_partition: *original_partition, original_partition_id: *id,
new_partitions, new_partitions,
}) })
}) })
@ -328,27 +331,15 @@ where
); );
for SplitResult { for SplitResult {
original_partition, original_partition_id,
ref mut new_partitions, ref mut new_partitions,
} in split_results.iter_mut().flatten() } in split_results.iter_mut().flatten()
{ {
// We want the original partition to retain the most cells to ensure that the smaller // 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. // sets are the ones that are assigned a new partition ID.
new_partitions.sort_unstable_by_key(|v| v.len()); new_partitions.sort_unstable_by_key(|set| set.len());
if let Some(partition) = new_partitions.pop() { if let Some(largest_partition) = new_partitions.pop() {
if let Some(tables) = partition_map partition_map.insert(*original_partition_id, largest_partition);
.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);
}
}
} }
// At this point the reverse map will be out of date. However, `partitions.insert()` // At this point the reverse map will be out of date. However, `partitions.insert()`
@ -363,86 +354,193 @@ where
} }
struct SplitResult<P: GridPrecision> { struct SplitResult<P: GridPrecision> {
original_partition: GridPartitionId, original_partition_id: GridPartitionId,
new_partitions: Vec<HashSet<GridHash<P>, PassHash>>, new_partitions: Vec<HashSet<GridHash<P>, PassHash>>,
} }
/// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other /// A private module to ensure the internal fields of the partition are not accessed directly.
/// [`GridCell`](crate::GridCell)s. /// Needed to ensure invariants are upheld.
#[derive(Debug)] mod private {
pub struct GridPartition<P: GridPrecision> { use super::{GridCell, GridHash, GridPrecision};
grid: Entity, use bevy_ecs::prelude::*;
tables: Vec<HashSet<GridHash<P>, PassHash>>, use bevy_utils::{hashbrown::HashSet, PassHash};
} /// A group of nearby [`GridCell`](crate::GridCell)s in an island disconnected from all other
impl<P: GridPrecision> GridPartition<P> { /// [`GridCell`](crate::GridCell)s.
/// Tables smaller than this will be drained into other tables when merging. Tables larger than #[derive(Debug)]
/// this limit will instead be added to a list of tables. This prevents partitions ending up pub struct GridPartition<P: GridPrecision> {
/// with many tables containing a few entries. grid: Entity,
/// tables: Vec<HashSet<GridHash<P>, PassHash>>,
/// Draining and extending a hash set is much slower than moving the entire hash set into a min: GridCell<P>,
/// list. The tradeoff is that the more tables added, the more there are that need to be max: GridCell<P>,
/// 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))
} }
/// Iterates over all [`GridHash`]s in this partition. impl<P: GridPrecision> GridPartition<P> {
#[inline] /// Returns `true` if the `hash` is in this partition.
pub fn iter(&self) -> impl Iterator<Item = &GridHash<P>> { #[inline]
self.tables.iter().flat_map(|table| table.iter()) pub fn contains(&self, hash: &GridHash<P>) -> bool {
} self.tables.iter().any(|table| table.contains(hash))
/// 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;
} }
if let Some(i) = self.smallest_table() {
self.tables[i].insert(cell); /// Iterates over all [`GridHash`]s in this partition.
} else { #[inline]
let mut table = HashSet::default(); pub fn iter(&self) -> impl Iterator<Item = &GridHash<P>> {
table.insert(cell); self.tables.iter().flat_map(|table| table.iter())
self.tables.push(table); }
/// 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] /// Private internal methods
fn smallest_table(&self) -> Option<usize> { impl<P: GridPrecision> GridPartition<P> {
self.tables pub(crate) fn new(
.iter() grid: Entity,
.enumerate() tables: Vec<HashSet<GridHash<P>, PassHash>>,
.map(|(i, t)| (i, t.len())) min: GridCell<P>,
.min_by_key(|(_, len)| *len) max: GridCell<P>,
.map(|(i, _len)| i) ) -> Self {
} Self {
grid,
tables,
min,
max,
}
}
#[inline] /// Tables smaller than this will be drained into other tables when merging. Tables larger than
fn extend(&mut self, mut partition: GridPartition<P>) { /// this limit will instead be added to a list of tables. This prevents partitions ending up
for mut table in partition.tables.drain(..) { /// with many tables containing a few entries.
if table.len() < Self::MIN_TABLE_SIZE { ///
if let Some(i) = self.smallest_table() { /// Draining and extending a hash set is much slower than moving the entire hash set into a
self.tables[i].extend(table.drain()); /// 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 { } else {
self.tables.push(table); 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 { } 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
}
} }