Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ bevy_math = { version = "0.17.1", default-features = false }
bevy_reflect = { version = "0.17.1", default-features = false, features = [
"glam",
] }
bevy_image = { version = "0.17.1", default-features = false, features = ["zstd_rust"] }
bevy_tasks = { version = "0.17.1", default-features = false }
bevy_transform = { version = "0.17.1", default-features = false, features = [
"bevy-support",
Expand Down Expand Up @@ -86,7 +85,8 @@ bevy = { version = "0.17.1", default-features = false, features = [
"tonemapping_luts",
"multi_threaded",
"png",
"reflect_auto_register"
"reflect_auto_register",
"zstd_rust"
] }
noise = "0.9"
turborand = "0.10"
Expand Down
195 changes: 195 additions & 0 deletions benches/benchmarks.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! `big_space` benchmarks.
#![allow(clippy::type_complexity)]
#![allow(missing_docs)]
#![allow(clippy::unit_arg)]

use bevy::prelude::*;
use big_space::plugin::BigSpaceMinimalPlugins;
Expand All @@ -17,6 +18,7 @@ criterion_group!(
deep_hierarchy,
wide_hierarchy,
vs_bevy,
partition_change_tracking,
);
criterion_main!(benches);

Expand Down Expand Up @@ -447,3 +449,196 @@ fn vs_bevy(c: &mut Criterion) {
});
});
}

fn partition_change_tracking(c: &mut Criterion) {
use partitions::*;

let mut group = c.benchmark_group("partition_change_tracking");

// Ensure the benchmarked app has a floating origin in the same grid as the scenario
fn spawn_bench_floating_origin(mut commands: Commands, grids: Query<Entity, With<Grid>>) {
if let Ok(grid_entity) = grids.single() {
// Attach a floating origin entity under the scenario's grid
commands.entity(grid_entity).with_children(|b| {
b.spawn((FloatingOrigin, Transform::default(), CellCoord::default()));
});
}
}

fn build_app(config: ScenarioConfig) -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, BigSpaceMinimalPlugins))
.add_plugins((CellHashingPlugin::default(), PartitionPlugin::default()));
add_partition_perf(&mut app, config);
// Add floating origin for the benchmark's big space after setup has created the grid
app.add_systems(PostStartup, spawn_bench_floating_origin);
// Warm up the world and apply startup
app.update();
app
}

// Axis 1: scaling with static entities (no movement)
for &n in &[100usize, 10_000, 100_000] {
let mut app = build_app(ScenarioConfig {
n_entities: n,
percent_moving: 0.0,
density: Density::Dense,
});
group.bench_function(format!("static_n={}", n), |b| {
b.iter(|| black_box(app.update()));
});
}

// Axis 2: 10k entities, varying percent moving
for &(label, pct) in &[("25%", 0.25f32), ("50%", 0.5), ("100%", 1.0)] {
let mut app = build_app(ScenarioConfig {
n_entities: 10_000,
percent_moving: pct,
density: Density::Dense,
});
group.bench_function(format!("n=10000_moving={}", label), |b| {
b.iter(|| black_box(app.update()));
});
}

// Axis 3: 10k, 25% moving, Sparse vs Dense
for &(label, density) in &[("sparse", Density::Sparse), ("dense", Density::Dense)] {
let mut app = build_app(ScenarioConfig {
n_entities: 10_000,
percent_moving: 0.25,
density,
});
group.bench_function(format!("n=10000_25pct_{}", label), |b| {
b.iter(|| black_box(app.update()));
});
}
}

pub mod partitions {
pub use super::*;

/// Density of initial entity arrangement.
#[derive(Clone, Copy, Debug)]
pub enum Density {
/// Entities are placed with at least one empty cell between any two occupied cells,
/// which keeps all entities in independent partitions initially.
Sparse,
/// Entities are placed contiguously so that many share the same partition (often one big one).
Dense,
}

/// Configuration for a partition perf scenario.
#[derive(Resource, Clone, Copy, Debug)]
pub struct ScenarioConfig {
/// Total number of entities to spawn.
pub n_entities: usize,
/// Fraction of entities that move each frame in [0, 1].
pub percent_moving: f32,
/// Initial arrangement of entities.
pub density: Density,
}

impl Default for ScenarioConfig {
fn default() -> Self {
Self {
n_entities: 10_000,
percent_moving: 0.25,
density: Density::Dense,
}
}
}

/// Marker for movers.
#[derive(Component)]
pub struct Mover;

/// Resource that stores the root grid entity for the active scenario.
#[derive(Resource, Clone, Copy, Debug)]
pub struct ScenarioRoot(pub Entity);

/// Adds systems to the app that set up and then update a partition perf scenario.
///
/// You can use this from an example or a benchmark.
pub fn add_partition_perf(app: &mut App, config: ScenarioConfig) {
app.insert_resource(config)
.add_systems(Startup, setup_scenario)
.add_systems(Update, move_movers);
}

fn setup_scenario(mut commands: Commands, config: Res<ScenarioConfig>) {
// Grid with default settings is fine; large cell length to keep visuals consistent in example.
commands.spawn_big_space(Grid::new(10_000.0, 0.0), |root| {
// Spawn many entities as children of the root grid for better spawn throughput.
let grid_entity = root.id();
// Expose the scenario's root grid entity so examples can attach cameras to the same space.
root.commands().insert_resource(ScenarioRoot(grid_entity));
let n_movers =
((config.percent_moving.clamp(0.0, 1.0)) * config.n_entities as f32) as usize;

match config.density {
Density::Sparse => {
// Distribute sparsely in 3D with a gap of 1 cell between occupied cells
// along each axis to avoid initial merges (independent partitions).
let n = config.n_entities as i64;
let edge = (f64::cbrt(n as f64).ceil() as i64).max(1);
let mut i = 0usize;
'outer: for z in 0..edge {
for y in 0..edge {
for x in 0..edge {
if i >= config.n_entities {
break 'outer;
}
// Multiply by 2 to leave one empty cell between any two occupied cells
let cell = CellCoord::new(
(x * 2) as GridPrecision,
(y * 2) as GridPrecision,
(z * 2) as GridPrecision,
);
let mut ec = root.spawn_spatial(());
ec.insert(cell);
if i < n_movers {
ec.insert(Mover);
}
i += 1;
}
}
}
}
Density::Dense => {
let n = config.n_entities as i64;
let edge = (f64::cbrt(n as f64).ceil() as i64).max(1);
let mut i = 0usize;
'outer: for z in 0..edge {
for y in 0..edge {
for x in 0..edge {
if i >= config.n_entities {
break 'outer;
}
let cell = CellCoord::new(
x as GridPrecision,
y as GridPrecision,
z as GridPrecision,
);
// Spawn as a spatial child of the grid and only set CellCoord
let mut ec = root.spawn_spatial(());
ec.insert(cell);
if i < n_movers {
ec.insert(Mover);
}
i += 1;
}
}
}
}
}
});
}

fn move_movers(mut q: Query<&mut CellCoord, With<Mover>>, mut flip: Local<bool>) {
*flip = !*flip;
let dx = if *flip { 1 } else { -1 };
for mut cell in q.iter_mut() {
cell.x += dx as GridPrecision;
}
}
}
55 changes: 54 additions & 1 deletion examples/spatial_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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 = 1.0;
const PERCENT_STATIC: f32 = 0.9;

fn main() {
App::new()
Expand All @@ -25,6 +25,7 @@ fn main() {
BigSpaceDefaultPlugins,
CellHashingPlugin::default(),
PartitionPlugin::default(),
PartitionChangePlugin::default(),
))
.add_plugins(bevy::remote::RemotePlugin::default()) // Core remote protocol
.add_plugins(bevy::remote::http::RemoteHttpPlugin::default()) // Enable HTTP transport
Expand All @@ -34,6 +35,7 @@ fn main() {
(
move_player.after(TransformSystems::Propagate),
draw_partitions.after(SpatialHashSystems::UpdatePartitionLookup),
highlight_changed_entities.after(draw_partitions),
),
)
.add_systems(Update, (cursor_grab, spawn_spheres))
Expand All @@ -52,6 +54,7 @@ struct MaterialPresets {
default: Handle<StandardMaterial>,
highlight: Handle<StandardMaterial>,
flood: Handle<StandardMaterial>,
changed: Handle<StandardMaterial>,
sphere: Handle<Mesh>,
}

Expand All @@ -67,6 +70,7 @@ impl FromWorld for MaterialPresets {
});
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 changed = materials.add(Color::from(Srgba::new(10.0, 0.0, 0.0, 1.0)));

let mut meshes = world.resource_mut::<Assets<Mesh>>();
let sphere = meshes.add(
Expand All @@ -80,6 +84,7 @@ impl FromWorld for MaterialPresets {
default,
highlight,
flood,
changed,
sphere,
}
}
Expand Down Expand Up @@ -383,3 +388,51 @@ fn cursor_grab(
}
Ok(())
}

// Highlight entities that changed partitions by setting their material to bright red
// and keep the highlight for 10 frames.
fn highlight_changed_entities(
mut materials: Query<&mut MeshMaterial3d<StandardMaterial>>,
material_presets: Res<MaterialPresets>,
entity_partitions: Res<PartitionChange>,
// Track highlighted entities with a countdown of remaining frames
mut active: Local<Vec<(Entity, u8)>>,
) {
// We'll rebuild the active list each frame, carrying forward countdowns
let mut next_active: Vec<(Entity, u8)> =
Vec::with_capacity(active.len() + entity_partitions.changed.len());

// 1) Apply new changes: set material to changed and (re)start countdown at 10
for entity in entity_partitions.changed.keys().copied() {
if let Ok(mut mat) = materials.get_mut(entity) {
mat.set_if_neq(material_presets.changed.clone().into());
}
next_active.push((entity, 10));
}

// 2) Carry over previous active highlights that weren't refreshed this frame
for (entity, mut frames_left) in active.drain(..) {
// If the entity also changed this frame, it's already added with 10 above
if entity_partitions.changed.contains_key(&entity) {
continue;
}
if frames_left > 0 {
frames_left -= 1;
if frames_left > 0 {
// Keep highlighted
if let Ok(mut mat) = materials.get_mut(entity) {
mat.set_if_neq(material_presets.changed.clone().into());
}
next_active.push((entity, frames_left));
} else {
// Countdown expired: reset to default
if let Ok(mut mat) = materials.get_mut(entity) {
mat.set_if_neq(material_presets.default.clone().into());
}
}
}
}

// 3) Replace the active list with the updated one
*active = next_active;
}
7 changes: 6 additions & 1 deletion src/hash/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ impl CellId {

/// Update or insert the [`CellId`] of all changed entities that match the optional
/// [`SpatialHashFilter`].
#[allow(clippy::too_many_arguments)]
pub fn update<F: SpatialHashFilter>(
mut commands: Commands,
mut changed_cells: ResMut<ChangedCells<F>>,
Expand All @@ -183,6 +184,7 @@ impl CellId {
(F, Or<(Changed<ChildOf>, Changed<CellCoord>)>),
>,
added_entities: Query<(Entity, &ChildOf, &CellCoord), (F, Without<CellId>)>,
mut removed_cells: RemovedComponents<CellCoord>,
mut stats: Option<ResMut<crate::timing::GridHashStats>>,
mut thread_updated_hashes: Local<PortableParallel<Vec<Entity>>>,
mut thread_commands: Local<PortableParallel<Vec<(Entity, CellId, CellHash)>>>,
Expand Down Expand Up @@ -214,7 +216,10 @@ impl CellId {
fast_hash.0 = new_fast_hash;
},
);
changed_cells.updated.extend(thread_updated_hashes.drain());

changed_cells
.updated
.extend(thread_updated_hashes.drain().chain(removed_cells.read()));

if let Some(ref mut stats) = stats {
stats.hash_update_duration += start.elapsed();
Expand Down
5 changes: 3 additions & 2 deletions src/hash/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ use bevy_ecs::{prelude::*, query::QueryFilter};

pub mod component;
pub mod map;
pub mod partition;

/// Add spatial hashing acceleration to `big_space`, accessible through the [`CellLookup`] resource,
/// and [`CellId`] components.
///
/// You can optionally add a [`SpatialHashFilter`] to this plugin, to only run the spatial hashing on
/// You can optionally add a [`SpatialHashFilter`] to this plugin to only run the spatial hashing on
/// entities that match the query filter. This is useful if you only want to, say, compute hashes
/// and insert in the [`CellLookup`] for `Player` entities.
///
Expand Down Expand Up @@ -72,6 +71,8 @@ pub enum SpatialHashSystems {
UpdateCellLookup,
/// [`PartitionLookup`] updated.
UpdatePartitionLookup,
/// [`PartitionChange`] updated.
UpdatePartitionChange,
}

/// Used as a [`QueryFilter`] to include or exclude certain types of entities from spatial
Expand Down
Loading
Loading