Skip to content

Commit

Permalink
Spawn specific entities: spawn or insert operations, refactor spawn i…
Browse files Browse the repository at this point in the history
…nternals, world clearing (#2673)

This upstreams the code changes used by the new renderer to enable cross-app Entity reuse:

* Spawning at specific entities
* get_or_spawn: spawns an entity if it doesn't already exist and returns an EntityMut
* insert_or_spawn_batch: the batched equivalent to `world.get_or_spawn(entity).insert_bundle(bundle)`
* Clearing entities and storages
* Allocating Entities with "invalid" archetypes. These entities cannot be queried / are treated as "non existent". They serve as "reserved" entities that won't show up when calling `spawn()`. They must be "specifically spawned at" using apis like `get_or_spawn(entity)`.

In combination, these changes enable the "render world" to clear entities / storages each frame and reserve all "app world entities". These can then be spawned during the "render extract step".

This refactors "spawn" and "insert" code in a way that I think is a massive improvement to legibility and re-usability. It also yields marginal performance wins by reducing some duplicate lookups (less than a percentage point improvement on insertion benchmarks). There is also some potential for future unsafe reduction (by making BatchSpawner and BatchInserter generic). But for now I want to cut down generic usage to a minimum to encourage smaller binaries and faster compiles.

This is currently a draft because it needs more tests (although this code has already had some real-world testing on my custom-shaders branch). 

I also fixed the benchmarks (which currently don't compile!) / added new ones to illustrate batching wins.

After these changes, Bevy ECS is basically ready to accommodate the new renderer. I think the biggest missing piece at this point is "sub apps".
  • Loading branch information
cart committed Aug 25, 2021
1 parent c5717b5 commit b47217b
Show file tree
Hide file tree
Showing 12 changed files with 1,163 additions and 327 deletions.
97 changes: 95 additions & 2 deletions benches/benches/bevy_ecs/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bevy::ecs::{
entity::Entity,
system::{Command, CommandQueue, Commands},
world::World,
};
Expand All @@ -8,10 +9,12 @@ criterion_group!(
benches,
empty_commands,
spawn_commands,
insert_commands,
fake_commands,
zero_sized_commands,
medium_sized_commands,
large_sized_commands
get_or_spawn,
);
criterion_main!(benches);

Expand Down Expand Up @@ -76,6 +79,58 @@ fn spawn_commands(criterion: &mut Criterion) {
group.finish();
}

#[derive(Default)]
struct Matrix([[f32; 4]; 4]);

#[derive(Default)]
struct Vec3([f32; 3]);

fn insert_commands(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("insert_commands");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));

let entity_count = 10_000;
group.bench_function(format!("insert"), |bencher| {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let mut entities = Vec::new();
for i in 0..entity_count {
entities.push(world.spawn().id());
}

bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
for entity in entities.iter() {
commands.entity(*entity).insert_bundle((Matrix::default(), Vec3::default()));
}
drop(commands);
command_queue.apply(&mut world);
});
});
group.bench_function(format!("insert_batch"), |bencher| {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
let mut entities = Vec::new();
for i in 0..entity_count {
entities.push(world.spawn().id());
}

bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
let mut values = Vec::with_capacity(entity_count);
for entity in entities.iter() {
values.push((*entity, (Matrix::default(), Vec3::default())));
}
commands.insert_or_spawn_batch(values);
drop(commands);
command_queue.apply(&mut world);
});
});

group.finish();
}

struct FakeCommandA;
struct FakeCommandB(u64);

Expand Down Expand Up @@ -106,7 +161,7 @@ fn fake_commands(criterion: &mut Criterion) {
bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
for i in 0..command_count {
if black_box(i % 2 == 0)
if black_box(i % 2 == 0) {
commands.add(FakeCommandA);
} else {
commands.add(FakeCommandB(0));
Expand All @@ -125,7 +180,7 @@ fn fake_commands(criterion: &mut Criterion) {
struct SizedCommand<T: Default + Send + Sync + 'static>(T);

impl<T: Default + Send + Sync + 'static> Command for SizedCommand<T> {
fn write(self: Box<Self>, world: &mut World) {
fn write(self, world: &mut World) {
black_box(self);
black_box(world);
}
Expand Down Expand Up @@ -175,3 +230,41 @@ fn medium_sized_commands(criterion: &mut Criterion) {
fn large_sized_commands(criterion: &mut Criterion) {
sized_commands_impl::<SizedCommand<LargeStruct>>(criterion);
}

fn get_or_spawn(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("get_or_spawn");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));

group.bench_function("individual", |bencher| {
let mut world = World::default();
let mut command_queue = CommandQueue::default();

bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
for i in 0..10_000 {
commands
.get_or_spawn(Entity::new(i))
.insert_bundle((Matrix::default(), Vec3::default()));
}
command_queue.apply(&mut world);
});
});

group.bench_function("batched", |bencher| {
let mut world = World::default();
let mut command_queue = CommandQueue::default();

bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
let mut values = Vec::with_capacity(10_000);
for i in 0..10_000 {
values.push((Entity::new(i), (Matrix::default(), Vec3::default())));
}
commands.insert_or_spawn_batch(values);
command_queue.apply(&mut world);
});
});

group.finish();
}
44 changes: 23 additions & 21 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,15 @@ use std::{
pub struct ArchetypeId(usize);

impl ArchetypeId {
pub const EMPTY: ArchetypeId = ArchetypeId(0);
pub const RESOURCE: ArchetypeId = ArchetypeId(1);
pub const INVALID: ArchetypeId = ArchetypeId(usize::MAX);

#[inline]
pub const fn new(index: usize) -> Self {
ArchetypeId(index)
}

#[inline]
pub const fn empty() -> ArchetypeId {
ArchetypeId(0)
}

#[inline]
pub const fn resource() -> ArchetypeId {
ArchetypeId(1)
}

#[inline]
pub fn index(self) -> usize {
self.0
Expand Down Expand Up @@ -60,7 +54,7 @@ impl Edges {
}

#[inline]
pub fn set_add_bundle(
pub fn insert_add_bundle(
&mut self,
bundle_id: BundleId,
archetype_id: ArchetypeId,
Expand All @@ -81,7 +75,7 @@ impl Edges {
}

#[inline]
pub fn set_remove_bundle(&mut self, bundle_id: BundleId, archetype_id: Option<ArchetypeId>) {
pub fn insert_remove_bundle(&mut self, bundle_id: BundleId, archetype_id: Option<ArchetypeId>) {
self.remove_bundle.insert(bundle_id, archetype_id);
}

Expand All @@ -94,7 +88,7 @@ impl Edges {
}

#[inline]
pub fn set_remove_bundle_intersection(
pub fn insert_remove_bundle_intersection(
&mut self,
bundle_id: BundleId,
archetype_id: Option<ArchetypeId>,
Expand Down Expand Up @@ -309,6 +303,11 @@ impl Archetype {
.get(component_id)
.map(|info| info.archetype_component_id)
}

pub(crate) fn clear_entities(&mut self) {
self.entities.clear();
self.table_info.entity_rows.clear();
}
}

/// A generational id that changes every time the set of archetypes changes
Expand Down Expand Up @@ -377,7 +376,7 @@ impl Default for Archetypes {
// adds the resource archetype. it is "special" in that it is inaccessible via a "hash",
// which prevents entities from being added to it
archetypes.archetypes.push(Archetype::new(
ArchetypeId::resource(),
ArchetypeId::RESOURCE,
TableId::empty(),
Cow::Owned(Vec::new()),
Cow::Owned(Vec::new()),
Expand All @@ -402,33 +401,30 @@ impl Archetypes {
#[inline]
pub fn empty(&self) -> &Archetype {
// SAFE: empty archetype always exists
unsafe { self.archetypes.get_unchecked(ArchetypeId::empty().index()) }
unsafe { self.archetypes.get_unchecked(ArchetypeId::EMPTY.index()) }
}

#[inline]
pub fn empty_mut(&mut self) -> &mut Archetype {
// SAFE: empty archetype always exists
unsafe {
self.archetypes
.get_unchecked_mut(ArchetypeId::empty().index())
.get_unchecked_mut(ArchetypeId::EMPTY.index())
}
}

#[inline]
pub fn resource(&self) -> &Archetype {
// SAFE: resource archetype always exists
unsafe {
self.archetypes
.get_unchecked(ArchetypeId::resource().index())
}
unsafe { self.archetypes.get_unchecked(ArchetypeId::RESOURCE.index()) }
}

#[inline]
pub fn resource_mut(&mut self) -> &mut Archetype {
// SAFE: resource archetype always exists
unsafe {
self.archetypes
.get_unchecked_mut(ArchetypeId::resource().index())
.get_unchecked_mut(ArchetypeId::RESOURCE.index())
}
}

Expand Down Expand Up @@ -519,6 +515,12 @@ impl Archetypes {
pub fn archetype_components_len(&self) -> usize {
self.archetype_component_count
}

pub fn clear_entities(&mut self) {
for archetype in self.archetypes.iter_mut() {
archetype.clear_entities();
}
}
}

impl Index<ArchetypeId> for Archetypes {
Expand Down
Loading

0 comments on commit b47217b

Please sign in to comment.