use alloc::{boxed::Box, vec::Vec};
use bevy_platform::cell::SyncUnsafeCell;
use bevy_platform::sync::Arc;
use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor};
use concurrent_queue::ConcurrentQueue;
use core::{any::Any, panic::AssertUnwindSafe};
use fixedbitset::FixedBitSet;
#[cfg(feature = "std")]
use std::eprintln;
use std::sync::{Mutex, MutexGuard};
#[cfg(feature = "trace")]
use tracing::{info_span, Span};
use crate::{
error::{ErrorContext, ErrorHandler, Result},
prelude::Resource,
schedule::{
is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule,
SystemWithAccess,
},
system::{RunSystemError, ScheduleSystem},
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
#[cfg(feature = "hotpatching")]
use crate::{prelude::DetectChanges, HotPatchChanges};
use super::__rust_begin_short_backtrace;
struct Environment<'env, 'sys> {
executor: &'env MultiThreadedExecutor,
systems: &'sys [SyncUnsafeCell<SystemWithAccess>],
conditions: SyncUnsafeCell<Conditions<'sys>>,
world_cell: UnsafeWorldCell<'env>,
}
struct Conditions<'a> {
system_conditions: &'a mut [Vec<ConditionWithAccess>],
set_conditions: &'a mut [Vec<ConditionWithAccess>],
sets_with_conditions_of_systems: &'a [FixedBitSet],
systems_in_sets_with_conditions: &'a [FixedBitSet],
}
impl<'env, 'sys> Environment<'env, 'sys> {
fn new(
executor: &'env MultiThreadedExecutor,
schedule: &'sys mut SystemSchedule,
world: &'env mut World,
) -> Self {
Environment {
executor,
systems: SyncUnsafeCell::from_mut(schedule.systems.as_mut_slice()).as_slice_of_cells(),
conditions: SyncUnsafeCell::new(Conditions {
system_conditions: &mut schedule.system_conditions,
set_conditions: &mut schedule.set_conditions,
sets_with_conditions_of_systems: &schedule.sets_with_conditions_of_systems,
systems_in_sets_with_conditions: &schedule.systems_in_sets_with_conditions,
}),
world_cell: world.as_unsafe_world_cell(),
}
}
}
struct SystemTaskMetadata {
conflicting_systems: FixedBitSet,
condition_conflicting_systems: FixedBitSet,
dependents: Vec<usize>,
is_send: bool,
is_exclusive: bool,
}
struct SystemResult {
system_index: usize,
}
pub struct MultiThreadedExecutor {
state: Mutex<ExecutorState>,
system_completion: ConcurrentQueue<SystemResult>,
apply_final_deferred: bool,
panic_payload: Mutex<Option<Box<dyn Any + Send>>>,
starting_systems: FixedBitSet,
#[cfg(feature = "trace")]
executor_span: Span,
}
pub struct ExecutorState {
system_task_metadata: Vec<SystemTaskMetadata>,
set_condition_conflicting_systems: Vec<FixedBitSet>,
local_thread_running: bool,
exclusive_running: bool,
num_running_systems: usize,
num_dependencies_remaining: Vec<usize>,
evaluated_sets: FixedBitSet,
ready_systems: FixedBitSet,
ready_systems_copy: FixedBitSet,
running_systems: FixedBitSet,
skipped_systems: FixedBitSet,
completed_systems: FixedBitSet,
unapplied_systems: FixedBitSet,
}
#[derive(Copy, Clone)]
struct Context<'scope, 'env, 'sys> {
environment: &'env Environment<'env, 'sys>,
scope: &'scope Scope<'scope, 'env, ()>,
error_handler: ErrorHandler,
}
impl Default for MultiThreadedExecutor {
fn default() -> Self {
Self::new()
}
}
impl SystemExecutor for MultiThreadedExecutor {
fn kind(&self) -> ExecutorKind {
ExecutorKind::MultiThreaded
}
fn init(&mut self, schedule: &SystemSchedule) {
let state = self.state.get_mut().unwrap();
let sys_count = schedule.system_ids.len();
let set_count = schedule.set_ids.len();
self.system_completion = ConcurrentQueue::bounded(sys_count.max(1));
self.starting_systems = FixedBitSet::with_capacity(sys_count);
state.evaluated_sets = FixedBitSet::with_capacity(set_count);
state.ready_systems = FixedBitSet::with_capacity(sys_count);
state.ready_systems_copy = FixedBitSet::with_capacity(sys_count);
state.running_systems = FixedBitSet::with_capacity(sys_count);
state.completed_systems = FixedBitSet::with_capacity(sys_count);
state.skipped_systems = FixedBitSet::with_capacity(sys_count);
state.unapplied_systems = FixedBitSet::with_capacity(sys_count);
state.system_task_metadata = Vec::with_capacity(sys_count);
for index in 0..sys_count {
state.system_task_metadata.push(SystemTaskMetadata {
conflicting_systems: FixedBitSet::with_capacity(sys_count),
condition_conflicting_systems: FixedBitSet::with_capacity(sys_count),
dependents: schedule.system_dependents[index].clone(),
is_send: schedule.systems[index].system.is_send(),
is_exclusive: schedule.systems[index].system.is_exclusive(),
});
if schedule.system_dependencies[index] == 0 {
self.starting_systems.insert(index);
}
}
{
#[cfg(feature = "trace")]
let _span = info_span!("calculate conflicting systems").entered();
for index1 in 0..sys_count {
let system1 = &schedule.systems[index1];
for index2 in 0..index1 {
let system2 = &schedule.systems[index2];
if !system2.access.is_compatible(&system1.access) {
state.system_task_metadata[index1]
.conflicting_systems
.insert(index2);
state.system_task_metadata[index2]
.conflicting_systems
.insert(index1);
}
}
for index2 in 0..sys_count {
let system2 = &schedule.systems[index2];
if schedule.system_conditions[index1]
.iter()
.any(|condition| !system2.access.is_compatible(&condition.access))
{
state.system_task_metadata[index1]
.condition_conflicting_systems
.insert(index2);
}
}
}
state.set_condition_conflicting_systems.clear();
state.set_condition_conflicting_systems.reserve(set_count);
for set_idx in 0..set_count {
let mut conflicting_systems = FixedBitSet::with_capacity(sys_count);
for sys_index in 0..sys_count {
let system = &schedule.systems[sys_index];
if schedule.set_conditions[set_idx]
.iter()
.any(|condition| !system.access.is_compatible(&condition.access))
{
conflicting_systems.insert(sys_index);
}
}
state
.set_condition_conflicting_systems
.push(conflicting_systems);
}
}
state.num_dependencies_remaining = Vec::with_capacity(sys_count);
}
fn run(
&mut self,
schedule: &mut SystemSchedule,
world: &mut World,
_skip_systems: Option<&FixedBitSet>,
error_handler: ErrorHandler,
) {
let state = self.state.get_mut().unwrap();
if schedule.systems.is_empty() {
return;
}
state.num_running_systems = 0;
state
.num_dependencies_remaining
.clone_from(&schedule.system_dependencies);
state.ready_systems.clone_from(&self.starting_systems);
#[cfg(feature = "bevy_debug_stepping")]
if let Some(skipped_systems) = _skip_systems {
debug_assert_eq!(skipped_systems.len(), state.completed_systems.len());
state.completed_systems |= skipped_systems;
for system_index in skipped_systems.ones() {
state.signal_dependents(system_index);
state.ready_systems.remove(system_index);
}
}
let thread_executor = world
.get_resource::<MainThreadExecutor>()
.map(|e| e.0.clone());
let thread_executor = thread_executor.as_deref();
let environment = &Environment::new(self, schedule, world);
ComputeTaskPool::get_or_init(TaskPool::default).scope_with_executor(
false,
thread_executor,
|scope| {
let context = Context {
environment,
scope,
error_handler,
};
context.tick_executor();
},
);
let systems = environment.systems;
let state = self.state.get_mut().unwrap();
if self.apply_final_deferred {
let res = apply_deferred(&state.unapplied_systems, systems, world);
if let Err(payload) = res {
let panic_payload = self.panic_payload.get_mut().unwrap();
*panic_payload = Some(payload);
}
state.unapplied_systems.clear();
}
let payload = self.panic_payload.get_mut().unwrap();
if let Some(payload) = payload.take() {
std::panic::resume_unwind(payload);
}
debug_assert!(state.ready_systems.is_clear());
debug_assert!(state.running_systems.is_clear());
state.evaluated_sets.clear();
state.skipped_systems.clear();
state.completed_systems.clear();
}
fn set_apply_final_deferred(&mut self, value: bool) {
self.apply_final_deferred = value;
}
}
impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> {
fn system_completed(
&self,
system_index: usize,
res: Result<(), Box<dyn Any + Send>>,
system: &ScheduleSystem,
) {
self.environment
.executor
.system_completion
.push(SystemResult { system_index })
.unwrap_or_else(|error| unreachable!("{}", error));
if let Err(payload) = res {
#[cfg(feature = "std")]
#[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")]
{
eprintln!("Encountered a panic in system `{}`!", system.name());
}
{
let mut panic_payload = self.environment.executor.panic_payload.lock().unwrap();
*panic_payload = Some(payload);
}
}
self.tick_executor();
}
#[expect(
clippy::mut_from_ref,
reason = "Field is only accessed here and is guarded by lock with a documented safety comment"
)]
fn try_lock<'a>(&'a self) -> Option<(&'a mut Conditions<'sys>, MutexGuard<'a, ExecutorState>)> {
let guard = self.environment.executor.state.try_lock().ok()?;
let conditions = unsafe { &mut *self.environment.conditions.get() };
Some((conditions, guard))
}
fn tick_executor(&self) {
loop {
let Some((conditions, mut guard)) = self.try_lock() else {
return;
};
guard.tick(self, conditions);
drop(guard);
if self.environment.executor.system_completion.is_empty() {
return;
}
}
}
}
impl MultiThreadedExecutor {
pub fn new() -> Self {
Self {
state: Mutex::new(ExecutorState::new()),
system_completion: ConcurrentQueue::unbounded(),
starting_systems: FixedBitSet::new(),
apply_final_deferred: true,
panic_payload: Mutex::new(None),
#[cfg(feature = "trace")]
executor_span: info_span!("multithreaded executor"),
}
}
}
impl ExecutorState {
fn new() -> Self {
Self {
system_task_metadata: Vec::new(),
set_condition_conflicting_systems: Vec::new(),
num_running_systems: 0,
num_dependencies_remaining: Vec::new(),
local_thread_running: false,
exclusive_running: false,
evaluated_sets: FixedBitSet::new(),
ready_systems: FixedBitSet::new(),
ready_systems_copy: FixedBitSet::new(),
running_systems: FixedBitSet::new(),
skipped_systems: FixedBitSet::new(),
completed_systems: FixedBitSet::new(),
unapplied_systems: FixedBitSet::new(),
}
}
fn tick(&mut self, context: &Context, conditions: &mut Conditions) {
#[cfg(feature = "trace")]
let _span = context.environment.executor.executor_span.enter();
for result in context.environment.executor.system_completion.try_iter() {
self.finish_system_and_handle_dependents(result);
}
unsafe {
self.spawn_system_tasks(context, conditions);
}
}
unsafe fn spawn_system_tasks(&mut self, context: &Context, conditions: &mut Conditions) {
if self.exclusive_running {
return;
}
#[cfg(feature = "hotpatching")]
let hotpatch_tick = context
.environment
.world_cell
.get_resource_ref::<HotPatchChanges>()
.map(|r| r.last_changed())
.unwrap_or_default();
let mut ready_systems = core::mem::take(&mut self.ready_systems_copy);
let mut check_for_new_ready_systems = true;
while check_for_new_ready_systems {
check_for_new_ready_systems = false;
ready_systems.clone_from(&self.ready_systems);
for system_index in ready_systems.ones() {
debug_assert!(!self.running_systems.contains(system_index));
let system =
&mut unsafe { &mut *context.environment.systems[system_index].get() }.system;
#[cfg(feature = "hotpatching")]
if hotpatch_tick.is_newer_than(
system.get_last_run(),
context.environment.world_cell.change_tick(),
) {
system.refresh_hotpatch();
}
if !self.can_run(system_index, conditions) {
continue;
}
self.ready_systems.remove(system_index);
if unsafe {
!self.should_run(
system_index,
system,
conditions,
context.environment.world_cell,
context.error_handler,
)
} {
self.skip_system_and_signal_dependents(system_index);
check_for_new_ready_systems = true;
continue;
}
self.running_systems.insert(system_index);
self.num_running_systems += 1;
if self.system_task_metadata[system_index].is_exclusive {
unsafe {
self.spawn_exclusive_system_task(context, system_index);
}
check_for_new_ready_systems = false;
break;
}
unsafe {
self.spawn_system_task(context, system_index);
}
}
}
self.ready_systems_copy = ready_systems;
}
fn can_run(&mut self, system_index: usize, conditions: &mut Conditions) -> bool {
let system_meta = &self.system_task_metadata[system_index];
if system_meta.is_exclusive && self.num_running_systems > 0 {
return false;
}
if !system_meta.is_send && self.local_thread_running {
return false;
}
for set_idx in conditions.sets_with_conditions_of_systems[system_index]
.difference(&self.evaluated_sets)
{
if !self.set_condition_conflicting_systems[set_idx].is_disjoint(&self.running_systems) {
return false;
}
}
if !system_meta
.condition_conflicting_systems
.is_disjoint(&self.running_systems)
{
return false;
}
if !self.skipped_systems.contains(system_index)
&& !system_meta
.conflicting_systems
.is_disjoint(&self.running_systems)
{
return false;
}
true
}
unsafe fn should_run(
&mut self,
system_index: usize,
system: &mut ScheduleSystem,
conditions: &mut Conditions,
world: UnsafeWorldCell,
error_handler: ErrorHandler,
) -> bool {
let mut should_run = !self.skipped_systems.contains(system_index);
for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() {
if self.evaluated_sets.contains(set_idx) {
continue;
}
let set_conditions_met = unsafe {
evaluate_and_fold_conditions(
&mut conditions.set_conditions[set_idx],
world,
error_handler,
system,
true,
)
};
if !set_conditions_met {
self.skipped_systems
.union_with(&conditions.systems_in_sets_with_conditions[set_idx]);
}
should_run &= set_conditions_met;
self.evaluated_sets.insert(set_idx);
}
let system_conditions_met = unsafe {
evaluate_and_fold_conditions(
&mut conditions.system_conditions[system_index],
world,
error_handler,
system,
false,
)
};
if !system_conditions_met {
self.skipped_systems.insert(system_index);
}
should_run &= system_conditions_met;
if should_run {
let valid_params = match unsafe { system.validate_param_unsafe(world) } {
Ok(()) => true,
Err(e) => {
if !e.skipped {
error_handler(
e.into(),
ErrorContext::System {
name: system.name(),
last_run: system.get_last_run(),
},
);
}
false
}
};
if !valid_params {
self.skipped_systems.insert(system_index);
}
should_run &= valid_params;
}
should_run
}
unsafe fn spawn_system_task(&mut self, context: &Context, system_index: usize) {
let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system;
let context = *context;
let system_meta = &self.system_task_metadata[system_index];
let task = async move {
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
unsafe {
if let Err(RunSystemError::Failed(err)) =
__rust_begin_short_backtrace::run_unsafe(
system,
context.environment.world_cell,
)
{
(context.error_handler)(
err,
ErrorContext::System {
name: system.name(),
last_run: system.get_last_run(),
},
);
}
};
}));
context.system_completed(system_index, res, system);
};
if system_meta.is_send {
context.scope.spawn(task);
} else {
self.local_thread_running = true;
context.scope.spawn_on_external(task);
}
}
unsafe fn spawn_exclusive_system_task(&mut self, context: &Context, system_index: usize) {
let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system;
let context = *context;
if is_apply_deferred(&**system) {
let unapplied_systems = self.unapplied_systems.clone();
self.unapplied_systems.clear();
let task = async move {
let world = unsafe { context.environment.world_cell.world_mut() };
let res = apply_deferred(&unapplied_systems, context.environment.systems, world);
context.system_completed(system_index, res, system);
};
context.scope.spawn_on_scope(task);
} else {
let task = async move {
let world = unsafe { context.environment.world_cell.world_mut() };
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
if let Err(RunSystemError::Failed(err)) =
__rust_begin_short_backtrace::run(system, world)
{
(context.error_handler)(
err,
ErrorContext::System {
name: system.name(),
last_run: system.get_last_run(),
},
);
}
}));
context.system_completed(system_index, res, system);
};
context.scope.spawn_on_scope(task);
}
self.exclusive_running = true;
self.local_thread_running = true;
}
fn finish_system_and_handle_dependents(&mut self, result: SystemResult) {
let SystemResult { system_index, .. } = result;
if self.system_task_metadata[system_index].is_exclusive {
self.exclusive_running = false;
}
if !self.system_task_metadata[system_index].is_send {
self.local_thread_running = false;
}
debug_assert!(self.num_running_systems >= 1);
self.num_running_systems -= 1;
self.running_systems.remove(system_index);
self.completed_systems.insert(system_index);
self.unapplied_systems.insert(system_index);
self.signal_dependents(system_index);
}
fn skip_system_and_signal_dependents(&mut self, system_index: usize) {
self.completed_systems.insert(system_index);
self.signal_dependents(system_index);
}
fn signal_dependents(&mut self, system_index: usize) {
for &dep_idx in &self.system_task_metadata[system_index].dependents {
let remaining = &mut self.num_dependencies_remaining[dep_idx];
debug_assert!(*remaining >= 1);
*remaining -= 1;
if *remaining == 0 && !self.completed_systems.contains(dep_idx) {
self.ready_systems.insert(dep_idx);
}
}
}
}
fn apply_deferred(
unapplied_systems: &FixedBitSet,
systems: &[SyncUnsafeCell<SystemWithAccess>],
world: &mut World,
) -> Result<(), Box<dyn Any + Send>> {
for system_index in unapplied_systems.ones() {
let system = &mut unsafe { &mut *systems[system_index].get() }.system;
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
system.apply_deferred(world);
}));
if let Err(payload) = res {
#[cfg(feature = "std")]
#[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")]
{
eprintln!(
"Encountered a panic when applying buffers for system `{}`!",
system.name()
);
}
return Err(payload);
}
}
Ok(())
}
unsafe fn evaluate_and_fold_conditions(
conditions: &mut [ConditionWithAccess],
world: UnsafeWorldCell,
error_handler: ErrorHandler,
for_system: &ScheduleSystem,
on_set: bool,
) -> bool {
#[expect(
clippy::unnecessary_fold,
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
)]
conditions
.iter_mut()
.map(|ConditionWithAccess { condition, .. }| {
unsafe { condition.validate_param_unsafe(world) }
.map_err(From::from)
.and_then(|()| {
unsafe {
__rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world)
}
})
.unwrap_or_else(|err| {
if let RunSystemError::Failed(err) = err {
error_handler(
err,
ErrorContext::RunCondition {
name: condition.name(),
last_run: condition.get_last_run(),
system: for_system.name(),
on_set,
},
);
};
false
})
})
.fold(true, |acc, res| acc && res)
}
#[derive(Resource, Clone)]
pub struct MainThreadExecutor(pub Arc<ThreadExecutor<'static>>);
impl Default for MainThreadExecutor {
fn default() -> Self {
Self::new()
}
}
impl MainThreadExecutor {
pub fn new() -> Self {
MainThreadExecutor(TaskPool::get_thread_executor())
}
}
#[cfg(test)]
mod tests {
use crate::{
prelude::Resource,
schedule::{ExecutorKind, IntoScheduleConfigs, Schedule},
system::Commands,
world::World,
};
#[derive(Resource)]
struct R;
#[test]
fn skipped_systems_notify_dependents() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.set_executor_kind(ExecutorKind::MultiThreaded);
schedule.add_systems(
(
(|| {}).run_if(|| false),
|mut commands: Commands| {
commands.insert_resource(R);
},
)
.chain(),
);
schedule.run(&mut world);
assert!(world.get_resource::<R>().is_some());
}
#[test]
fn check_spawn_exclusive_system_task_miri() {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.set_executor_kind(ExecutorKind::MultiThreaded);
schedule.add_systems(((|_: Commands| {}), |_: Commands| {}).chain());
schedule.run(&mut world);
}
}