Fix spawn tracking for spawn commands (#19351)

# Objective

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

## Solution

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

## Testing

See added test.
This commit is contained in:
SpecificProtagonist 2025-05-26 22:15:21 +02:00 committed by GitHub
parent 54c9f03021
commit 158d9aff0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 6 deletions

View File

@ -62,6 +62,31 @@ pub fn spawn_commands(criterion: &mut Criterion) {
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)]
struct Matrix([[f32; 4]; 4]);

View File

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

View File

@ -943,6 +943,15 @@ impl Entities {
meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at });
}
/// # 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 = MaybeUninit::new(SpawnedOrDespawned { by, at });
}
/// 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
/// value + 1.

View File

@ -16,7 +16,7 @@ use core::marker::PhantomData;
use crate::{
self as bevy_ecs,
bundle::{Bundle, InsertMode, NoBundleEffect},
change_detection::Mut,
change_detection::{MaybeLocation, Mut},
component::{Component, ComponentId, Mutable},
entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError},
error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError},
@ -317,12 +317,24 @@ impl<'w, 's> Commands<'w, 's> {
/// - [`spawn`](Self::spawn) to spawn an entity with components.
/// - [`spawn_batch`](Self::spawn_batch) to spawn many entities
/// with the same combination of components.
#[track_caller]
pub fn spawn_empty(&mut self) -> EntityCommands {
let entity = self.entities.reserve_entity();
EntityCommands {
let mut entity_commands = EntityCommands {
entity,
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
@ -369,9 +381,35 @@ impl<'w, 's> Commands<'w, 's> {
/// with the same combination of components.
#[track_caller]
pub fn spawn<T: Bundle>(&mut self, bundle: T) -> EntityCommands {
let mut entity = self.spawn_empty();
entity.insert(bundle);
entity
let entity = self.entities.reserve_entity();
let mut entity_commands = EntityCommands {
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`].
@ -2577,4 +2615,17 @@ mod tests {
assert!(world.contains_resource::<W<i32>>());
assert!(world.contains_resource::<W<f64>>());
}
#[test]
fn track_spawn_ticks() {
let mut world = World::default();
world.increment_change_tick();
let expected = world.change_tick();
let id = world.commands().spawn_empty().id();
world.flush();
assert_eq!(
Some(expected),
world.entities().entity_get_spawned_or_despawned_at(id)
);
}
}