Fix spawn tracking for spawn commands (#19351)

See also
https://discord.com/channels/691052431525675048/1374187654425481266/1375553989185372292.

Set spawn info in `Commands::spawn_empty`.
Also added a benchmark for `Commands::spawn`.

See added test.
This commit is contained in:
SpecificProtagonist 2025-05-26 22:15:21 +02:00 committed by François Mockers
parent 6fc2e919b8
commit 4562bb484f
4 changed files with 91 additions and 6 deletions

View File

@ -62,6 +62,31 @@ pub fn spawn_commands(criterion: &mut Criterion) {
group.finish(); group.finish();
} }
pub fn nonempty_spawn_commands(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("nonempty_spawn_commands");
group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in [100, 1_000, 10_000] {
group.bench_function(format!("{}_entities", entity_count), |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..entity_count {
if black_box(i % 2 == 0) {
commands.spawn(A);
}
}
command_queue.apply(&mut world);
});
});
}
group.finish();
}
#[derive(Default, Component)] #[derive(Default, Component)]
struct Matrix([[f32; 4]; 4]); struct Matrix([[f32; 4]; 4]);

View File

@ -17,6 +17,7 @@ criterion_group!(
benches, benches,
empty_commands, empty_commands,
spawn_commands, spawn_commands,
nonempty_spawn_commands,
insert_commands, insert_commands,
fake_commands, fake_commands,
zero_sized_commands, zero_sized_commands,

View File

@ -74,6 +74,7 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec};
use crate::{ use crate::{
archetype::{ArchetypeId, ArchetypeRow}, archetype::{ArchetypeId, ArchetypeRow},
change_detection::MaybeLocation, change_detection::MaybeLocation,
component::Tick,
identifier::{ identifier::{
error::IdentifierError, error::IdentifierError,
kinds::IdKind, kinds::IdKind,
@ -84,7 +85,13 @@ use crate::{
}; };
use alloc::vec::Vec; use alloc::vec::Vec;
use bevy_platform::sync::atomic::Ordering; use bevy_platform::sync::atomic::Ordering;
use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; use core::{
fmt,
hash::Hash,
mem::{self},
num::NonZero,
panic::Location,
};
use log::warn; use log::warn;
#[cfg(feature = "serialize")] #[cfg(feature = "serialize")]
@ -866,6 +873,20 @@ impl Entities {
meta.location = location; meta.location = location;
} }
/// # Safety
/// - `index` must be a valid entity index.
#[inline]
pub(crate) unsafe fn mark_spawn_despawn(&mut self, index: u32, by: MaybeLocation, _at: Tick) {
// // SAFETY: Caller guarantees that `index` a valid entity index
// let meta = unsafe { self.meta.get_unchecked_mut(index as usize) };
// meta.spawned_or_despawned_by = MaybeUninit::new(SpawnedOrDespawned { by, at });
by.map(|caller| {
// SAFETY: Caller guarantees that `index` a valid entity index
let meta = unsafe { self.meta.get_unchecked_mut(index as usize) };
meta.spawned_or_despawned_by = MaybeLocation::new(Some(caller));
});
}
/// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this
/// `index` will count `generation` starting from the prior `generation` + the specified /// `index` will count `generation` starting from the prior `generation` + the specified
/// value + 1. /// value + 1.

View File

@ -318,12 +318,24 @@ impl<'w, 's> Commands<'w, 's> {
/// - [`spawn`](Self::spawn) to spawn an entity with components. /// - [`spawn`](Self::spawn) to spawn an entity with components.
/// - [`spawn_batch`](Self::spawn_batch) to spawn many entities /// - [`spawn_batch`](Self::spawn_batch) to spawn many entities
/// with the same combination of components. /// with the same combination of components.
#[track_caller]
pub fn spawn_empty(&mut self) -> EntityCommands { pub fn spawn_empty(&mut self) -> EntityCommands {
let entity = self.entities.reserve_entity(); let entity = self.entities.reserve_entity();
EntityCommands { let mut entity_commands = EntityCommands {
entity, entity,
commands: self.reborrow(), commands: self.reborrow(),
};
let caller = MaybeLocation::caller();
entity_commands.queue(move |entity: EntityWorldMut| {
let index = entity.id().index();
let world = entity.into_world_mut();
let tick = world.change_tick();
// SAFETY: Entity has been flushed
unsafe {
world.entities_mut().mark_spawn_despawn(index, caller, tick);
} }
});
entity_commands
} }
/// Spawns a new [`Entity`] with the given components /// Spawns a new [`Entity`] with the given components
@ -370,9 +382,35 @@ impl<'w, 's> Commands<'w, 's> {
/// with the same combination of components. /// with the same combination of components.
#[track_caller] #[track_caller]
pub fn spawn<T: Bundle>(&mut self, bundle: T) -> EntityCommands { pub fn spawn<T: Bundle>(&mut self, bundle: T) -> EntityCommands {
let mut entity = self.spawn_empty(); let entity = self.entities.reserve_entity();
entity.insert(bundle); let mut entity_commands = EntityCommands {
entity entity,
commands: self.reborrow(),
};
let caller = MaybeLocation::caller();
entity_commands.queue(move |mut entity: EntityWorldMut| {
// Store metadata about the spawn operation.
// This is the same as in `spawn_empty`, but merged into
// the same command for better performance.
let index = entity.id().index();
entity.world_scope(|world| {
let tick = world.change_tick();
// SAFETY: Entity has been flushed
unsafe {
world.entities_mut().mark_spawn_despawn(index, caller, tick);
}
});
entity.insert_with_caller(
bundle,
InsertMode::Replace,
caller,
crate::relationship::RelationshipHookMode::Run,
);
});
// entity_command::insert(bundle, InsertMode::Replace)
entity_commands
} }
/// Returns the [`EntityCommands`] for the given [`Entity`]. /// Returns the [`EntityCommands`] for the given [`Entity`].