bevy/release-content/release-notes/entity-spawn-ticks.md
SpecificProtagonist 13e89a1678
Fix EntityMeta.spawned_or_despawned unsoundness (#19350)
# Objective

#19047 added an `MaybeUninit` field to `EntityMeta`, but did not
guarantee that it will be initialized before access:

```rust
let mut world = World::new();
let id = world.entities().reserve_entity();
world.flush();
world.entity(id);
```

<details>
<summary>Miri Error</summary>

```
error: Undefined Behavior: using uninitialized data, but this operation requires initialized memory
    --> /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1121:26
     |
1121 |                 unsafe { meta.spawned_or_despawned.assume_init() }
     |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ using uninitialized data, but this operation requires initialized memory
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
     = note: BACKTRACE:
     = note: inside closure at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1121:26: 1121:65
     = note: inside `std::option::Option::<&bevy_ecs::entity::EntityMeta>::map::<bevy_ecs::entity::SpawnedOrDespawned, {closure@bevy_ecs::entity::Entities::entity_get_spawned_or_despawned::{closure#1}}>` at /home/vj/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:1144:29: 1144:33
     = note: inside `bevy_ecs::entity::Entities::entity_get_spawned_or_despawned` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1112:9: 1122:15
     = note: inside closure at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1094:13: 1094:57
     = note: inside `bevy_ecs::change_detection::MaybeLocation::<std::option::Option<&std::panic::Location<'_>>>::new_with_flattened::<{closure@bevy_ecs::entity::Entities::entity_get_spawned_or_despawned_by::{closure#0}}>` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/change_detection.rs:1371:20: 1371:24
     = note: inside `bevy_ecs::entity::Entities::entity_get_spawned_or_despawned_by` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1093:9: 1096:11
     = note: inside `bevy_ecs::entity::Entities::entity_does_not_exist_error_details` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1163:23: 1163:70
     = note: inside `bevy_ecs::entity::EntityDoesNotExistError::new` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/entity/mod.rs:1182:22: 1182:74
     = note: inside `bevy_ecs::world::unsafe_world_cell::UnsafeWorldCell::<'_>::get_entity` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/world/unsafe_world_cell.rs:368:20: 368:73
     = note: inside `<bevy_ecs::entity::Entity as bevy_ecs::world::WorldEntityFetch>::fetch_ref` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/world/entity_fetch.rs:207:21: 207:42
     = note: inside `bevy_ecs::world::World::get_entity::<bevy_ecs::entity::Entity>` at /home/vj/workspace/rust/bevy/crates/bevy_ecs/src/world/mod.rs:911:18: 911:42
note: inside `main`
    --> src/main.rs:12:15
     |
12   |     world.entity(id);
     |
```

</details>

## Solution

- remove the existing `MaybeUninit` in `EntityMeta.spawned_or_despawned`
- initialize during flush. This is not needed for soundness, but not
doing this means we can't return a sensible location/tick for flushed
entities.

## Testing

Test via the snippet above (also added equivalent test).

---------

Co-authored-by: urben1680 <55257931+urben1680@users.noreply.github.com>
2025-05-27 22:45:07 +00:00

3.5 KiB

title authors pull_requests
Entity Spawn Ticks
@urben1680
@specificprotagonist
19047
19350

Keeping track which entities have been spawned since the last time a system ran could only be done indirectly by inserting marker components and do your logic on entities that match an Added<MyMarker> filter or in MyMarker's on_add hook.

This has the issue however that not every add reacts on a spawn but also on insertions at existing entities. Sometimes you cannot even add your marker because the spawn call is hidden in some non-public API.

The new SpawnDetails query data and Spawned query filter enable you to find recently spawned entities without any marker components.

SpawnDetails

Use this in your query when you want to get information about the entity's spawn. You might want to do that for debug purposes, using the struct's Debug implementation.

You can also get specific information via methods. The following example prints the entity id (prefixed with "new" if it showed up for the first time), the Tick it spawned at and, if the track_location feature is activated, the source code location where it was spawned. Said feature is not enabled by default because it comes with a runtime cost.

fn print_spawn_details(query: Query<(Entity, SpawnDetails)>) {
    for (entity, spawn_details) in &query {
        if spawn_details.is_spawned() {
            print!("new ");
        }
        print!(
            "entity {entity:?} spawned at {:?}",
            spawn_details.spawned_at()
        );
        match spawn_details.spawned_by().into_option() {
            Some(location) => println!(" by {location:?}"),
            None => println!()
        }    
    }
}

Spawned

Use this filter in your query if you are only interested in entities that were spawned after the last time your system ran.

Note that this, like Added<T> and Changed<T>, is a non-archetypal filter. This means that your query could still go through millions of entities without yielding any recently spawned ones. Unlike filters like With<T> which can easily skip all entities that do not have T without checking them one-by-one.

Because of this, these systems have roughly the same performance:

fn system1(query: Query<Entity, Spawned>) {
    for entity in &query { /* entity spawned */ }
}

fn system2(query: Query<(Entity, SpawnDetails)>) {
    for (entity, spawned) in &query {
        if spawned.is_spawned() { /* entity spawned */ }
    }
}

Getter methods

Getting around this weakness of non-archetypal filters can be to check only specific entities for their spawn tick: The method spawned_at was added to all entity pointer structs, such as EntityRef, EntityMut and EntityWorldMut.

In this example we want to filter for entities that were spawned after a certain tick:

fn filter_spawned_after(
    entities: impl IntoIterator<Item = Entity>,
    world: &World,
    tick: Tick,
) -> impl Iterator<Item = Entity> {
    let now = world.last_change_tick();
    entities.into_iter().filter(move |entity| world
        .entity(*entity)
        .spawned_at()
        .is_newer_than(tick, now)
    )
}

The tick is stored in Entities. It's method entity_get_spawned_or_despawned_at not only returns when a living entity spawned at, it also returns when a despawned entity found it's bitter end.

Note however that despawned entities can be replaced by Bevy at any following spawn. Then this method returns None for the despawned entity. The same is true if the entity is not even spawned yet, only allocated.