docs/bevy/examples/ecs/nondeterministic_system_order.rs 4.1 K · 100 lines · raw · history

1 //! By default, Bevy systems run in parallel with each other.
2 //! Unless the order is explicitly specified, their relative order is nondeterministic.
3 //!
4 //! In many cases, this doesn't matter and is in fact desirable!
5 //! Consider two systems, one which writes to resource A, and the other which writes to resource B.
6 //! By allowing their order to be arbitrary, we can evaluate them greedily, based on the data that is free.
7 //! Because their data accesses are **compatible**, there is no **observable** difference created based on the order they are run.
8 //!
9 //! But if instead we have two systems mutating the same data, or one reading it and the other mutating,
10 //! then the actual observed value will vary based on the nondeterministic order of evaluation.
11 //! These observable conflicts are called **system execution order ambiguities**.
12 //!
13 //! This example demonstrates how you might detect and resolve (or silence) these ambiguities.
14
15 use bevy::{
16 ecs::schedule::{LogLevel, ScheduleBuildSettings},
17 prelude::*,
18 };
19
20 fn main() {
21 App::new()
22 // We can modify the reporting strategy for system execution order ambiguities on a per-schedule basis.
23 // You must do this for each schedule you want to inspect; child schedules executed within an inspected
24 // schedule do not inherit this modification.
25 .edit_schedule(Update, |schedule| {
26 schedule.set_build_settings(ScheduleBuildSettings {
27 ambiguity_detection: LogLevel::Warn,
28 ..default()
29 });
30 })
31 .init_resource::<A>()
32 .init_resource::<B>()
33 .add_systems(
34 Update,
35 (
36 // This pair of systems has an ambiguous order,
37 // as their data access conflicts, and there's no order between them.
38 reads_a,
39 writes_a,
40 // This pair of systems has conflicting data access,
41 // but it's resolved with an explicit ordering:
42 // the .after relationship here means that we will always double after adding.
43 adds_one_to_b,
44 doubles_b.after(adds_one_to_b),
45 // This system isn't ambiguous with adds_one_to_b,
46 // due to the transitive ordering created by our constraints:
47 // if A is before B is before C, then A must be before C as well.
48 reads_b.after(doubles_b),
49 // This system will conflict with all of our writing systems
50 // but we've silenced its ambiguity with adds_one_to_b.
51 // This should only be done in the case of clear false positives:
52 // leave a comment in your code justifying the decision!
53 reads_a_and_b.ambiguous_with(adds_one_to_b),
54 ),
55 )
56 // Be mindful, internal ambiguities are reported too!
57 // If there are any ambiguities due solely to DefaultPlugins,
58 // or between DefaultPlugins and any of your third party plugins,
59 // please file a bug with the repo responsible!
60 // Only *you* can prevent nondeterministic bugs due to greedy parallelism.
61 .add_plugins(DefaultPlugins)
62 .run();
63 }
64
65 #[derive(Resource, Debug, Default)]
66 struct A(usize);
67
68 #[derive(Resource, Debug, Default)]
69 struct B(usize);
70
71 // Data access is determined solely on the basis of the types of the system's parameters
72 // Every implementation of the `SystemParam` and `WorldQuery` traits must declare which data is used
73 // and whether or not it is mutably accessed.
74 fn reads_a(_a: Res<A>) {}
75
76 fn writes_a(mut a: ResMut<A>) {
77 a.0 += 1;
78 }
79
80 fn adds_one_to_b(mut b: ResMut<B>) {
81 b.0 = b.0.saturating_add(1);
82 }
83
84 fn doubles_b(mut b: ResMut<B>) {
85 // This will overflow pretty rapidly otherwise
86 b.0 = b.0.saturating_mul(2);
87 }
88
89 fn reads_b(b: Res<B>) {
90 // This invariant is always true,
91 // because we've fixed the system order so doubling always occurs after adding.
92 assert!((b.0.is_multiple_of(2)) || (b.0 == usize::MAX));
93 }
94
95 fn reads_a_and_b(a: Res<A>, b: Res<B>) {
96 // Only display the first few steps to avoid burying the ambiguities in the console
97 if b.0 < 10 {
98 info!("{}, {}", a.0, b.0);
99 }
100 }