Skip to main content

bevy_ecs/schedule/executor/
single_threaded.rs

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