From 3d6b24880e11c66f2e047e58269b0776b103024c Mon Sep 17 00:00:00 2001 From: JaySpruce Date: Sun, 13 Oct 2024 13:14:16 -0500 Subject: [PATCH] Add `insert_batch` and variations (#15702) # Objective `insert_or_spawn_batch` exists, but a version for just inserting doesn't - Closes #2693 - Closes #8384 - Adopts/supersedes #8600 ## Solution Add `insert_batch`, along with the most common `insert` variations: - `World::insert_batch` - `World::insert_batch_if_new` - `World::try_insert_batch` - `World::try_insert_batch_if_new` - `Commands::insert_batch` - `Commands::insert_batch_if_new` - `Commands::try_insert_batch` - `Commands::try_insert_batch_if_new` ## Testing Added tests, and added a benchmark for `insert_batch`. Performance is slightly better than `insert_or_spawn_batch` when only inserting: ![Code_HPnUN0QeWe](https://github.com/user-attachments/assets/53091e4f-6518-43f4-a63f-ae57d5470c66)
old benchmark This was before reworking it to remove the `UnsafeWorldCell`: ![Code_QhXJb8sjlJ](https://github.com/user-attachments/assets/1061e2a7-a521-48e1-a799-1b6b8d1c0b93)
--- ## Showcase Usage is the same as `insert_or_spawn_batch`: ``` use bevy_ecs::{entity::Entity, world::World, component::Component}; #[derive(Component)] struct A(&'static str); #[derive(Component, PartialEq, Debug)] struct B(f32); let mut world = World::new(); let entity_a = world.spawn_empty().id(); let entity_b = world.spawn_empty().id(); world.insert_batch([ (entity_a, (A("a"), B(0.0))), (entity_b, (A("b"), B(1.0))), ]); assert_eq!(world.get::(entity_a), Some(&B(0.0))); ``` --- benches/benches/bevy_ecs/world/commands.rs | 20 +- crates/bevy_ecs/src/lib.rs | 128 +++++++++ crates/bevy_ecs/src/system/commands/mod.rs | 192 +++++++++++++ crates/bevy_ecs/src/world/mod.rs | 303 +++++++++++++++++++++ 4 files changed, 642 insertions(+), 1 deletion(-) diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index e7e0483bc8..ea220cf1ce 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -91,7 +91,7 @@ pub fn insert_commands(criterion: &mut Criterion) { command_queue.apply(&mut world); }); }); - group.bench_function("insert_batch", |bencher| { + group.bench_function("insert_or_spawn_batch", |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); let mut entities = Vec::new(); @@ -109,6 +109,24 @@ pub fn insert_commands(criterion: &mut Criterion) { command_queue.apply(&mut world); }); }); + group.bench_function("insert_batch", |bencher| { + let mut world = World::default(); + let mut command_queue = CommandQueue::default(); + let mut entities = Vec::new(); + for _ in 0..entity_count { + entities.push(world.spawn_empty().id()); + } + + bencher.iter(|| { + let mut commands = Commands::new(&mut command_queue, &world); + let mut values = Vec::with_capacity(entity_count); + for entity in &entities { + values.push((*entity, (Matrix::default(), Vec3::default()))); + } + commands.insert_batch(values); + command_queue.apply(&mut world); + }); + }); group.finish(); } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 3a15cabe3f..67254d6298 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1699,6 +1699,134 @@ mod tests { ); } + #[test] + fn insert_batch() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = world.spawn(B(0)).id(); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.insert_batch(values); + + assert_eq!( + world.get::(e0), + Some(&A(1)), + "first entity's A component should have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + assert_eq!( + world.get::(e1), + Some(&A(0)), + "second entity should have received A component" + ); + assert_eq!( + world.get::(e1), + Some(&B(1)), + "second entity's B component should have been replaced" + ); + } + + #[test] + fn insert_batch_same_archetype() { + let mut world = World::default(); + let e0 = world.spawn((A(0), B(0))).id(); + let e1 = world.spawn((A(0), B(0))).id(); + let e2 = world.spawn(B(0)).id(); + + let values = vec![(e0, (B(1), C)), (e1, (B(2), C)), (e2, (B(3), C))]; + + world.insert_batch(values); + let mut query = world.query::<(Option<&A>, &B, &C)>(); + let component_values = query.get_many(&world, [e0, e1, e2]).unwrap(); + + assert_eq!( + component_values, + [(Some(&A(0)), &B(1), &C), (Some(&A(0)), &B(2), &C), (None, &B(3), &C)], + "all entities should have had their B component replaced, received C component, and had their A component (or lack thereof) unchanged" + ); + } + + #[test] + fn insert_batch_if_new() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = world.spawn(B(0)).id(); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.insert_batch_if_new(values); + + assert_eq!( + world.get::(e0), + Some(&A(0)), + "first entity's A component should not have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + assert_eq!( + world.get::(e1), + Some(&A(0)), + "second entity should have received A component" + ); + assert_eq!( + world.get::(e1), + Some(&B(0)), + "second entity's B component should not have been replaced" + ); + } + + #[test] + fn try_insert_batch() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = Entity::from_raw(1); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.try_insert_batch(values); + + assert_eq!( + world.get::(e0), + Some(&A(1)), + "first entity's A component should have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + } + + #[test] + fn try_insert_batch_if_new() { + let mut world = World::default(); + let e0 = world.spawn(A(0)).id(); + let e1 = Entity::from_raw(1); + + let values = vec![(e0, (A(1), B(0))), (e1, (A(0), B(1)))]; + + world.try_insert_batch_if_new(values); + + assert_eq!( + world.get::(e0), + Some(&A(0)), + "first entity's A component should not have been replaced" + ); + assert_eq!( + world.get::(e0), + Some(&B(0)), + "first entity should have received B component" + ); + } + #[test] fn required_components() { #[derive(Component)] diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 4e10f88bb3..5e68742537 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -614,6 +614,110 @@ impl<'w, 's> Commands<'w, 's> { self.queue(insert_or_spawn_batch(bundles_iter)); } + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, overwriting any existing components shared by the `Bundle`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`entity`](Self::entity) for each pair, + /// and passing the bundle to [`insert`](EntityCommands::insert), + /// but it is faster due to memory pre-allocation. + /// + /// # Panics + /// + /// This command panics if any of the given entities do not exist. + /// + /// For the non-panicking version, see [`try_insert_batch`](Self::try_insert_batch). + #[track_caller] + pub fn insert_batch(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(insert_batch(batch)); + } + + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, except for any components already present on the `Entity`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`entity`](Self::entity) for each pair, + /// and passing the bundle to [`insert_if_new`](EntityCommands::insert_if_new), + /// but it is faster due to memory pre-allocation. + /// + /// # Panics + /// + /// This command panics if any of the given entities do not exist. + /// + /// For the non-panicking version, see [`try_insert_batch_if_new`](Self::try_insert_batch_if_new). + #[track_caller] + pub fn insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(insert_batch_if_new(batch)); + } + + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, overwriting any existing components shared by the `Bundle`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`get_entity`](Self::get_entity) for each pair, + /// and passing the bundle to [`insert`](EntityCommands::insert), + /// but it is faster due to memory pre-allocation. + /// + /// This command silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`insert_batch`](Self::insert_batch). + #[track_caller] + pub fn try_insert_batch(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(try_insert_batch(batch)); + } + + /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// When the command is applied, for each `(Entity, Bundle)` pair in the given batch, + /// the `Bundle` is added to the `Entity`, except for any components already present on the `Entity`. + /// + /// This method is equivalent to iterating the batch, + /// calling [`get_entity`](Self::get_entity) for each pair, + /// and passing the bundle to [`insert_if_new`](EntityCommands::insert_if_new), + /// but it is faster due to memory pre-allocation. + /// + /// This command silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`insert_batch_if_new`](Self::insert_batch_if_new). + #[track_caller] + pub fn try_insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, + { + self.queue(try_insert_batch_if_new(batch)); + } + /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with an inferred value. /// /// The inferred value is determined by the [`FromWorld`] trait of the resource. @@ -1734,6 +1838,94 @@ where } } +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will panic. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn insert_batch(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will panic. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn insert_batch_if_new(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will ignore them. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn try_insert_batch(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.try_insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will ignore them. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +fn try_insert_batch_if_new(batch: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |world: &mut World| { + world.try_insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + caller, + ); + } +} + /// A [`Command`] that despawns a specific entity. /// This will emit a warning if the entity does not exist. /// diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 00e175cd77..fe9752a334 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2466,6 +2466,309 @@ impl World { } } + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity`. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This will overwrite any previous values of components shared by the `Bundle`. + /// See [`World::insert_batch_if_new`] to keep the old values instead. + /// + /// # Panics + /// + /// This function will panic if any of the associated entities do not exist. + /// + /// For the non-panicking version, see [`World::try_insert_batch`]. + #[track_caller] + pub fn insert_batch(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity` without overwriting. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This is the same as [`World::insert_batch`], but in case of duplicate + /// components it will leave the old values instead of replacing them with new ones. + /// + /// # Panics + /// + /// This function will panic if any of the associated entities do not exist. + /// + /// For the non-panicking version, see [`World::try_insert_batch_if_new`]. + #[track_caller] + pub fn insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// Split into a new function so we can differentiate the calling location. + /// + /// This can be called by: + /// - [`World::insert_batch`] + /// - [`World::insert_batch_if_new`] + /// - [`Commands::insert_batch`] + /// - [`Commands::insert_batch_if_new`] + #[inline] + pub(crate) fn insert_batch_with_caller( + &mut self, + iter: I, + insert_mode: InsertMode, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.flush(); + + let change_tick = self.change_tick(); + + let bundle_id = self + .bundles + .register_info::(&mut self.components, &mut self.storages); + + struct InserterArchetypeCache<'w> { + inserter: BundleInserter<'w>, + archetype_id: ArchetypeId, + } + + let mut batch = iter.into_iter(); + + if let Some((first_entity, first_bundle)) = batch.next() { + if let Some(first_location) = self.entities().get(first_entity) { + let mut cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + first_location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: first_location.archetype_id, + }; + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + first_entity, + first_location, + first_bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + + for (entity, bundle) in batch { + if let Some(location) = cache.inserter.entities().get(entity) { + if location.archetype_id != cache.archetype_id { + cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: location.archetype_id, + } + } + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + entity, + location, + bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + } else { + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), entity); + } + } + } else { + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), first_entity); + } + } + } + + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity`. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This will overwrite any previous values of components shared by the `Bundle`. + /// See [`World::try_insert_batch_if_new`] to keep the old values instead. + /// + /// This function silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`World::insert_batch`]. + #[track_caller] + pub fn try_insert_batch(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.try_insert_batch_with_caller( + batch, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + /// For a given batch of ([`Entity`], [`Bundle`]) pairs, + /// adds the `Bundle` of components to each `Entity` without overwriting. + /// This is faster than doing equivalent operations one-by-one. + /// + /// A batch can be any type that implements [`IntoIterator`] containing `(Entity, Bundle)` tuples, + /// such as a [`Vec<(Entity, Bundle)>`] or an array `[(Entity, Bundle); N]`. + /// + /// This is the same as [`World::try_insert_batch`], but in case of duplicate + /// components it will leave the old values instead of replacing them with new ones. + /// + /// This function silently fails by ignoring any entities that do not exist. + /// + /// For the panicking version, see [`World::insert_batch_if_new`]. + #[track_caller] + pub fn try_insert_batch_if_new(&mut self, batch: I) + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.try_insert_batch_with_caller( + batch, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// Split into a new function so we can differentiate the calling location. + /// + /// This can be called by: + /// - [`World::try_insert_batch`] + /// - [`World::try_insert_batch_if_new`] + /// - [`Commands::try_insert_batch`] + /// - [`Commands::try_insert_batch_if_new`] + #[inline] + pub(crate) fn try_insert_batch_with_caller( + &mut self, + iter: I, + insert_mode: InsertMode, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.flush(); + + let change_tick = self.change_tick(); + + let bundle_id = self + .bundles + .register_info::(&mut self.components, &mut self.storages); + + struct InserterArchetypeCache<'w> { + inserter: BundleInserter<'w>, + archetype_id: ArchetypeId, + } + + let mut batch = iter.into_iter(); + + if let Some((first_entity, first_bundle)) = batch.next() { + if let Some(first_location) = self.entities().get(first_entity) { + let mut cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + first_location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: first_location.archetype_id, + }; + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + first_entity, + first_location, + first_bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + + for (entity, bundle) in batch { + if let Some(location) = cache.inserter.entities().get(entity) { + if location.archetype_id != cache.archetype_id { + cache = InserterArchetypeCache { + // SAFETY: we initialized this bundle_id in `register_info` + inserter: unsafe { + BundleInserter::new_with_id( + self, + location.archetype_id, + bundle_id, + change_tick, + ) + }, + archetype_id: location.archetype_id, + } + } + // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter + unsafe { + cache.inserter.insert( + entity, + location, + bundle, + insert_mode, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; + } + } + } + } + } + /// Temporarily removes the requested resource from this [`World`], runs custom user code, /// then re-adds the resource before returning. ///