-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Command execution order does not always match system execution order in the case of ambiguous systems #10122
Comments
Hi, you are right commands are run asynchronously. It is somewhat described in unofficial cheatbook https://bevy-cheatbook.github.io/programming/commands.html but there were changes in ECS in 0.10 (https://bevyengine.org/news/bevy-0-10/#ecs-schedule-v3) and AFAIK there are no stages anymore. Concurrency issues can be annoying and can bite literally anyone so IMO it is the important topic we should focus. Could you provide a minimal example to reproduce the bug (it is valuable even if it crashes only in 10%)? 🙏 |
I'll try to create a minimal example to reproduce the bug. |
Commands are definitely supposed to run in system order: if they do not, that is a bug in Bevy. |
I finally have a minimal example. I had to dig deeper into bevy to create the example and I think I found the bug: To summarize the order of events in the bug:
Finally, here is the example to reproduce this bug: use bevy::prelude::*;
#[derive(Resource)]
struct MainEntity(Option<Entity>);
#[derive(Resource)]
struct SharedResource;
fn main() {
App::new()
.insert_resource(MainEntity(None))
.insert_resource(SharedResource)
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (auxiliary, despawner, spawner))
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera3dBundle::default());
}
fn auxiliary(_shared: ResMut<SharedResource>) {}
fn despawner(
mut commands: Commands,
mut main_entity: ResMut<MainEntity>,
_shared: ResMut<SharedResource>,
) {
if let Some(entity) = main_entity.0 {
commands.entity(entity).despawn();
main_entity.0 = None;
}
}
fn spawner(mut commands: Commands, mut main_entity: ResMut<MainEntity>) {
if let None = main_entity.0 {
let entity = commands.spawn(TransformBundle::default()).id();
main_entity.0 = Some(entity);
}
} Due to the non-deterministic ordering of the systems in the schedule, you would probably have to run this example multiple times before it panics. I used bevy |
Thanks for digging! So far this looks like "working as intended" behavior, even if it is a pain. Does this still occur when your two systems that emit commands are ordered? |
When using: add_systems(Update, (auxiliary, (spawner, despawner).chain())) in the minimal example, there seems to be no crash. Same for my original application: it doesn't appear to crash when chaining the equivalent systems. Of course, its possible I just haven't checked enough times given the random nature of the crashes. Even if this is intended behavior, I think it should still be changed for 2 reasons:
I think that at the very least, there should be a warning in the documentation of |
That's pretty reasonable: do you have an idea of what your ideal behavior would look like here? |
I think the simplest solution is to have the executer record the order in which systems are spawned, and apply the command buffers in that order. For systems that are spawned together (in the same call to For systems that are spawned in different calls to For example, if the scheduled systems come in the following slice:
Then they should be applied in the order: This way, if a system has a dependency on the actions of another system (for example, when sharing state through resources), bevy will already make sure to spawn them one after the other (and most importantly, not in parallel), and then the commands will be applied in the same order that the shared state was modified in. |
What happens right now is that systems run at the first available opportunity—which can vary for pairs of ambiguous systems—while their command queues are applied in the pre-determined topological order. I'm not against "command queues should be applied in the same order that systems run", but some consequences are:
|
Its true that there will be an increase in the variability of the order in which commands are applied, but what we gain in return is the ability to observe that order from the systems. Currently the only way to know this order is by looking at the implementation of the executer and figuring out that you could know the order by inspecting the scheduler. In other words, the only way to know the order for ambiguous systems is by knowing the implementation details, which is probably not something the user should be aware of. As for determinism, I think it would be sensible to require a deterministic system ordering (meaning having zero ambiguities). Having ambiguous systems ran by the multithreaded executor would always be dependent on the time it takes each system to run: If a certain system would suddenly take longer to run, the executer can start another system at the same time, which would not happen if the system finished instantly instead. |
|
Bevy version
0.11.2
What you did
I have two systems that take a mutable reference to a resource that represents a document.
The document is essentially a list of spawnable mesh + material pairs.
The code looks something like this:
What went wrong
Sometimes, the
show_art_system
will panic with the following error:error[B0003]: Could not insert a bundle (of type (bevy_pbr::bundle::MaterialMeshBundle<bevy_pbr::pbr_material::StandardMaterial>, bevy_render::view::visibility::NoFrustumCulling)) for entity 19v0 because it doesn't exist in this World.
In my setup, the
show_art_system
always runs before thehide_art_system
,and the
hide_art_system
always checks if theArt
has an entity to despawn before calling despawn.When looking at my logs, this is also what I see: the art gets spawned first, and then despawned later.
This panic only happens when the art is spawned and despawned in the same frame.
After a lot of digging, what I discovered is that
show_art_system
andhide_art_system
receive differentCommand
buffers, and that even though these systems should be synchronized since they request mutable accessto the same resource, sometimes (somewhat rarely) their command buffers will be applied in reverse order relative to the
order in which the systems ran. This leads to the entity being spawned, then the
Despawn
command is applied to it,and then the
Insert
command is applied to it, leading to a panic since the entity was already despawned.I am not sure if this is a bevy bug or intended behavior.
If this is intended, I would appreciate some direction on how to structure my code to avoid this panic.
Additional information
Sadly, I don't have a simple, consistent way to reproduce this, so I don't have a minimal code example that reproduces the issue.
My actual code that causes this behavior only panics in about ~10% of runs, so it would probably be a bad example, since it also contains a lot of other noise.
The text was updated successfully, but these errors were encountered: