bevy_ecs/schedule/executor/
single_threaded.rs

1use core::panic::AssertUnwindSafe;
2use fixedbitset::FixedBitSet;
3
4#[cfg(feature = "trace")]
5use tracing::info_span;
6
7#[cfg(feature = "std")]
8use std::eprintln;
9
10use crate::{
11    error::{ErrorContext, ErrorHandler},
12    schedule::{
13        is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule,
14    },
15    system::{RunSystemError, ScheduleSystem},
16    world::World,
17};
18
19#[cfg(feature = "hotpatching")]
20use crate::{change_detection::DetectChanges, HotPatchChanges};
21
22use super::__rust_begin_short_backtrace;
23
24/// Runs the schedule using a single thread.
25///
26/// Useful if you're dealing with a single-threaded environment, saving your threads for
27/// other things, or just trying minimize overhead.
28#[derive(Default)]
29pub struct SingleThreadedExecutor {
30    /// System sets whose conditions have been evaluated.
31    evaluated_sets: FixedBitSet,
32    /// Systems that have run or been skipped.
33    completed_systems: FixedBitSet,
34    /// Systems that have run but have not had their buffers applied.
35    unapplied_systems: FixedBitSet,
36    /// Setting when true applies deferred system buffers after all systems have run
37    apply_final_deferred: bool,
38}
39
40impl SystemExecutor for SingleThreadedExecutor {
41    fn kind(&self) -> ExecutorKind {
42        ExecutorKind::SingleThreaded
43    }
44
45    fn init(&mut self, schedule: &SystemSchedule) {
46        // pre-allocate space
47        let sys_count = schedule.system_ids.len();
48        let set_count = schedule.set_ids.len();
49        self.evaluated_sets = FixedBitSet::with_capacity(set_count);
50        self.completed_systems = FixedBitSet::with_capacity(sys_count);
51        self.unapplied_systems = FixedBitSet::with_capacity(sys_count);
52    }
53
54    fn run(
55        &mut self,
56        schedule: &mut SystemSchedule,
57        world: &mut World,
58        _skip_systems: Option<&FixedBitSet>,
59        error_handler: ErrorHandler,
60    ) {
61        // If stepping is enabled, make sure we skip those systems that should
62        // not be run.
63        #[cfg(feature = "bevy_debug_stepping")]
64        if let Some(skipped_systems) = _skip_systems {
65            // mark skipped systems as completed
66            self.completed_systems |= skipped_systems;
67        }
68
69        #[cfg(feature = "hotpatching")]
70        let hotpatch_tick = world
71            .get_resource_ref::<HotPatchChanges>()
72            .map(|r| r.last_changed())
73            .unwrap_or_default();
74
75        for system_index in 0..schedule.systems.len() {
76            let system = &mut schedule.systems[system_index].system;
77
78            #[cfg(feature = "trace")]
79            let name = system.name();
80            #[cfg(feature = "trace")]
81            let should_run_span = info_span!("check_conditions", name = name.as_string()).entered();
82
83            let mut should_run = !self.completed_systems.contains(system_index);
84            for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() {
85                if self.evaluated_sets.contains(set_idx) {
86                    continue;
87                }
88
89                // evaluate system set's conditions
90                let set_conditions_met = evaluate_and_fold_conditions(
91                    &mut schedule.set_conditions[set_idx],
92                    world,
93                    error_handler,
94                    system,
95                    true,
96                );
97
98                if !set_conditions_met {
99                    self.completed_systems
100                        .union_with(&schedule.systems_in_sets_with_conditions[set_idx]);
101                }
102
103                should_run &= set_conditions_met;
104                self.evaluated_sets.insert(set_idx);
105            }
106
107            // evaluate system's conditions
108            let system_conditions_met = evaluate_and_fold_conditions(
109                &mut schedule.system_conditions[system_index],
110                world,
111                error_handler,
112                system,
113                false,
114            );
115
116            should_run &= system_conditions_met;
117
118            #[cfg(feature = "trace")]
119            should_run_span.exit();
120
121            #[cfg(feature = "hotpatching")]
122            if hotpatch_tick.is_newer_than(system.get_last_run(), world.change_tick()) {
123                system.refresh_hotpatch();
124            }
125
126            // system has either been skipped or will run
127            self.completed_systems.insert(system_index);
128
129            if !should_run {
130                continue;
131            }
132
133            if is_apply_deferred(&**system) {
134                self.apply_deferred(schedule, world);
135                continue;
136            }
137
138            let f = AssertUnwindSafe(|| {
139                if let Err(RunSystemError::Failed(err)) =
140                    __rust_begin_short_backtrace::run_without_applying_deferred(system, world)
141                {
142                    error_handler(
143                        err,
144                        ErrorContext::System {
145                            name: system.name(),
146                            last_run: system.get_last_run(),
147                        },
148                    );
149                }
150            });
151
152            #[cfg(feature = "std")]
153            #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")]
154            {
155                if let Err(payload) = std::panic::catch_unwind(f) {
156                    eprintln!("Encountered a panic in system `{}`!", system.name());
157                    std::panic::resume_unwind(payload);
158                }
159            }
160
161            #[cfg(not(feature = "std"))]
162            {
163                (f)();
164            }
165
166            self.unapplied_systems.insert(system_index);
167        }
168
169        if self.apply_final_deferred {
170            self.apply_deferred(schedule, world);
171        }
172        self.evaluated_sets.clear();
173        self.completed_systems.clear();
174    }
175
176    fn set_apply_final_deferred(&mut self, apply_final_deferred: bool) {
177        self.apply_final_deferred = apply_final_deferred;
178    }
179}
180
181impl SingleThreadedExecutor {
182    /// Creates a new single-threaded executor for use in a [`Schedule`].
183    ///
184    /// [`Schedule`]: crate::schedule::Schedule
185    pub const fn new() -> Self {
186        Self {
187            evaluated_sets: FixedBitSet::new(),
188            completed_systems: FixedBitSet::new(),
189            unapplied_systems: FixedBitSet::new(),
190            apply_final_deferred: true,
191        }
192    }
193
194    fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) {
195        for system_index in self.unapplied_systems.ones() {
196            let system = &mut schedule.systems[system_index].system;
197            system.apply_deferred(world);
198        }
199
200        self.unapplied_systems.clear();
201    }
202}
203
204fn evaluate_and_fold_conditions(
205    conditions: &mut [ConditionWithAccess],
206    world: &mut World,
207    error_handler: ErrorHandler,
208    for_system: &ScheduleSystem,
209    on_set: bool,
210) -> bool {
211    #[cfg(feature = "hotpatching")]
212    let hotpatch_tick = world
213        .get_resource_ref::<HotPatchChanges>()
214        .map(|r| r.last_changed())
215        .unwrap_or_default();
216
217    #[expect(
218        clippy::unnecessary_fold,
219        reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
220    )]
221    conditions
222        .iter_mut()
223        .map(|ConditionWithAccess { condition, .. }| {
224            #[cfg(feature = "hotpatching")]
225            if hotpatch_tick.is_newer_than(condition.get_last_run(), world.change_tick()) {
226                condition.refresh_hotpatch();
227            }
228            __rust_begin_short_backtrace::readonly_run(&mut **condition, world).unwrap_or_else(
229                |err| {
230                    if let RunSystemError::Failed(err) = err {
231                        error_handler(
232                            err,
233                            ErrorContext::RunCondition {
234                                name: condition.name(),
235                                last_run: condition.get_last_run(),
236                                system: for_system.name(),
237                                on_set,
238                            },
239                        );
240                    };
241                    false
242                },
243            )
244        })
245        .fold(true, |acc, res| acc && res)
246}