Skip to main content

bevy_ecs/schedule/executor/
single_threaded.rs

1use core::panic::AssertUnwindSafe;
2use fixedbitset::FixedBitSet;
3
4#[cfg(feature = "trace")]
5use alloc::string::ToString as _;
6#[cfg(feature = "trace")]
7use tracing::info_span;
8
9#[cfg(feature = "std")]
10use std::eprintln;
11
12use crate::{
13    error::{ErrorContext, ErrorHandler},
14    schedule::{is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule},
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 init(&mut self, schedule: &SystemSchedule) {
42        // pre-allocate space
43        let sys_count = schedule.system_ids.len();
44        let set_count = schedule.set_ids.len();
45        self.evaluated_sets = FixedBitSet::with_capacity(set_count);
46        self.completed_systems = FixedBitSet::with_capacity(sys_count);
47        self.unapplied_systems = FixedBitSet::with_capacity(sys_count);
48    }
49
50    fn run(
51        &mut self,
52        schedule: &mut SystemSchedule,
53        world: &mut World,
54        _skip_systems: Option<&FixedBitSet>,
55        error_handler: ErrorHandler,
56    ) {
57        // If stepping is enabled, make sure we skip those systems that should
58        // not be run.
59        #[cfg(feature = "bevy_debug_stepping")]
60        if let Some(skipped_systems) = _skip_systems {
61            // mark skipped systems as completed
62            self.completed_systems |= skipped_systems;
63        }
64
65        #[cfg(feature = "hotpatching")]
66        let hotpatch_tick = world
67            .get_resource_ref::<HotPatchChanges>()
68            .map(|r| r.last_changed())
69            .unwrap_or_default();
70
71        for system_index in 0..schedule.systems.len() {
72            let system = &mut schedule.systems[system_index].system;
73
74            #[cfg(feature = "trace")]
75            let name = system.name();
76            #[cfg(feature = "trace")]
77            let should_run_span = info_span!("check_conditions", name = name.to_string()).entered();
78
79            let mut should_run = !self.completed_systems.contains(system_index);
80            for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() {
81                if self.evaluated_sets.contains(set_idx) {
82                    continue;
83                }
84
85                // evaluate system set's conditions
86                let set_conditions_met = evaluate_and_fold_conditions(
87                    &mut schedule.set_conditions[set_idx],
88                    world,
89                    error_handler,
90                    system,
91                    true,
92                );
93
94                if !set_conditions_met {
95                    self.completed_systems
96                        .union_with(&schedule.systems_in_sets_with_conditions[set_idx]);
97                }
98
99                should_run &= set_conditions_met;
100                self.evaluated_sets.insert(set_idx);
101            }
102
103            // evaluate system's conditions
104            let system_conditions_met = evaluate_and_fold_conditions(
105                &mut schedule.system_conditions[system_index],
106                world,
107                error_handler,
108                system,
109                false,
110            );
111
112            should_run &= system_conditions_met;
113
114            #[cfg(feature = "trace")]
115            should_run_span.exit();
116
117            #[cfg(feature = "hotpatching")]
118            if hotpatch_tick.is_newer_than(system.get_last_run(), world.change_tick()) {
119                system.refresh_hotpatch();
120            }
121
122            // system has either been skipped or will run
123            self.completed_systems.insert(system_index);
124
125            if !should_run {
126                continue;
127            }
128
129            if is_apply_deferred(&**system) {
130                self.apply_deferred(schedule, world);
131                continue;
132            }
133
134            let f = AssertUnwindSafe(|| {
135                if let Err(RunSystemError::Failed(err)) =
136                    __rust_begin_short_backtrace::run_without_applying_deferred(system, world)
137                {
138                    error_handler(
139                        err,
140                        ErrorContext::System {
141                            name: system.name(),
142                            last_run: system.get_last_run(),
143                        },
144                    );
145                }
146            });
147
148            #[cfg(feature = "std")]
149            #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")]
150            {
151                if let Err(payload) = std::panic::catch_unwind(f) {
152                    eprintln!("Encountered a panic in system `{}`!", system.name());
153                    std::panic::resume_unwind(payload);
154                }
155            }
156
157            #[cfg(not(feature = "std"))]
158            {
159                (f)();
160            }
161
162            self.unapplied_systems.insert(system_index);
163        }
164
165        if self.apply_final_deferred {
166            self.apply_deferred(schedule, world);
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(&mut self, schedule: &mut SystemSchedule, world: &mut World) {
191        for system_index in self.unapplied_systems.ones() {
192            let system = &mut schedule.systems[system_index].system;
193            system.apply_deferred(world);
194        }
195
196        self.unapplied_systems.clear();
197    }
198}
199
200fn evaluate_and_fold_conditions(
201    conditions: &mut [ConditionWithAccess],
202    world: &mut World,
203    error_handler: ErrorHandler,
204    for_system: &ScheduleSystem,
205    on_set: bool,
206) -> bool {
207    #[cfg(feature = "hotpatching")]
208    let hotpatch_tick = world
209        .get_resource_ref::<HotPatchChanges>()
210        .map(|r| r.last_changed())
211        .unwrap_or_default();
212
213    #[expect(
214        clippy::unnecessary_fold,
215        reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
216    )]
217    conditions
218        .iter_mut()
219        .map(|ConditionWithAccess { condition, .. }| {
220            #[cfg(feature = "hotpatching")]
221            if hotpatch_tick.is_newer_than(condition.get_last_run(), world.change_tick()) {
222                condition.refresh_hotpatch();
223            }
224            __rust_begin_short_backtrace::readonly_run(&mut **condition, world).unwrap_or_else(
225                |err| {
226                    if let RunSystemError::Failed(err) = err {
227                        error_handler(
228                            err,
229                            ErrorContext::RunCondition {
230                                name: condition.name(),
231                                last_run: condition.get_last_run(),
232                                system: for_system.name(),
233                                on_set,
234                            },
235                        );
236                    };
237                    false
238                },
239            )
240        })
241        .fold(true, |acc, res| acc && res)
242}