From 4f3ed196faa2cda8035cbe7f5313fe36e19b6b1a Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 3 Feb 2023 03:19:41 +0000 Subject: [PATCH 1/5] Stageless: move MainThreadExecutor to schedule_v3 (#7444) # Objective - Trying to move some of the fixes from https://github.com/bevyengine/bevy/pull/7267 to make that one easier to review - The MainThreadExecutor is how the render world runs nonsend systems on the main thread for pipelined rendering. - The multithread executor for stageless wasn't using the MainThreadExecutor. - MainThreadExecutor was declared in the old executor_parallel module that is getting deleted. - The way the MainThreadExecutor was getting passed to the scope was actually unsound as the resource could be dropped from the World while the schedule was running ## Solution - Move MainThreadExecutor to the new multithreaded_executor's file. - Make the multithreaded executor use the MainThreadExecutor - Clone the MainThreadExecutor onto the stack and pass that ref in ## Changelog - Move MainThreadExecutor for stageless migration. --- .../src/schedule/executor_parallel.rs | 22 ++--- .../bevy_ecs/src/schedule_v3/executor/mod.rs | 2 +- .../schedule_v3/executor/multi_threaded.rs | 83 ++++++++++++------- crates/bevy_render/src/pipelined_rendering.rs | 3 +- 4 files changed, 62 insertions(+), 48 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor_parallel.rs b/crates/bevy_ecs/src/schedule/executor_parallel.rs index 0db9627633ba1..dd966ac3cf036 100644 --- a/crates/bevy_ecs/src/schedule/executor_parallel.rs +++ b/crates/bevy_ecs/src/schedule/executor_parallel.rs @@ -1,15 +1,12 @@ -use std::sync::Arc; - -use crate as bevy_ecs; use crate::{ archetype::ArchetypeComponentId, query::Access, schedule::{ParallelSystemExecutor, SystemContainer}, - system::Resource, + schedule_v3::MainThreadExecutor, world::World, }; use async_channel::{Receiver, Sender}; -use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor}; +use bevy_tasks::{ComputeTaskPool, Scope, TaskPool}; #[cfg(feature = "trace")] use bevy_utils::tracing::Instrument; use event_listener::Event; @@ -18,16 +15,6 @@ use fixedbitset::FixedBitSet; #[cfg(test)] use scheduling_event::*; -/// New-typed [`ThreadExecutor`] [`Resource`] that is used to run systems on the main thread -#[derive(Resource, Default, Clone)] -pub struct MainThreadExecutor(pub Arc>); - -impl MainThreadExecutor { - pub fn new() -> Self { - MainThreadExecutor(Arc::new(ThreadExecutor::new())) - } -} - struct SystemSchedulingMetadata { /// Used to signal the system's task to start the system. start: Event, @@ -138,7 +125,10 @@ impl ParallelSystemExecutor for ParallelExecutor { } } - let thread_executor = world.get_resource::().map(|e| &*e.0); + let thread_executor = world + .get_resource::() + .map(|e| e.0.clone()); + let thread_executor = thread_executor.as_deref(); ComputeTaskPool::init(TaskPool::default).scope_with_executor( false, diff --git a/crates/bevy_ecs/src/schedule_v3/executor/mod.rs b/crates/bevy_ecs/src/schedule_v3/executor/mod.rs index bfc1eef14d609..8fc1788cb72b5 100644 --- a/crates/bevy_ecs/src/schedule_v3/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule_v3/executor/mod.rs @@ -2,7 +2,7 @@ mod multi_threaded; mod simple; mod single_threaded; -pub use self::multi_threaded::MultiThreadedExecutor; +pub use self::multi_threaded::{MainThreadExecutor, MultiThreadedExecutor}; pub use self::simple::SimpleExecutor; pub use self::single_threaded::SingleThreadedExecutor; diff --git a/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs index 21d6c5d4cdb7f..0796ce1963b62 100644 --- a/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs @@ -1,14 +1,16 @@ -use bevy_tasks::{ComputeTaskPool, Scope, TaskPool}; +use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor}; use bevy_utils::default; use bevy_utils::syncunsafecell::SyncUnsafeCell; #[cfg(feature = "trace")] use bevy_utils::tracing::{info_span, Instrument}; +use std::sync::Arc; use async_channel::{Receiver, Sender}; use fixedbitset::FixedBitSet; use crate::{ archetype::ArchetypeComponentId, + prelude::Resource, query::Access, schedule_v3::{ is_apply_system_buffers, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, @@ -17,6 +19,8 @@ use crate::{ world::World, }; +use crate as bevy_ecs; + /// A funky borrow split of [`SystemSchedule`] required by the [`MultiThreadedExecutor`]. struct SyncUnsafeSchedule<'a> { systems: &'a [SyncUnsafeCell], @@ -145,47 +149,56 @@ impl SystemExecutor for MultiThreadedExecutor { } } + let thread_executor = world + .get_resource::() + .map(|e| e.0.clone()); + let thread_executor = thread_executor.as_deref(); + let world = SyncUnsafeCell::from_mut(world); let SyncUnsafeSchedule { systems, mut conditions, } = SyncUnsafeSchedule::new(schedule); - ComputeTaskPool::init(TaskPool::default).scope(|scope| { - // the executor itself is a `Send` future so that it can run - // alongside systems that claim the local thread - let executor = async { - while self.num_completed_systems < num_systems { - // SAFETY: self.ready_systems does not contain running systems - unsafe { - self.spawn_system_tasks(scope, systems, &mut conditions, world); - } - - if self.num_running_systems > 0 { - // wait for systems to complete - let index = self - .receiver - .recv() - .await - .unwrap_or_else(|error| unreachable!("{}", error)); + ComputeTaskPool::init(TaskPool::default).scope_with_executor( + false, + thread_executor, + |scope| { + // the executor itself is a `Send` future so that it can run + // alongside systems that claim the local thread + let executor = async { + while self.num_completed_systems < num_systems { + // SAFETY: self.ready_systems does not contain running systems + unsafe { + self.spawn_system_tasks(scope, systems, &mut conditions, world); + } - self.finish_system_and_signal_dependents(index); + if self.num_running_systems > 0 { + // wait for systems to complete + let index = self + .receiver + .recv() + .await + .unwrap_or_else(|error| unreachable!("{}", error)); - while let Ok(index) = self.receiver.try_recv() { self.finish_system_and_signal_dependents(index); - } - self.rebuild_active_access(); + while let Ok(index) = self.receiver.try_recv() { + self.finish_system_and_signal_dependents(index); + } + + self.rebuild_active_access(); + } } - } - }; + }; - #[cfg(feature = "trace")] - let executor_span = info_span!("schedule_task"); - #[cfg(feature = "trace")] - let executor = executor.instrument(executor_span); - scope.spawn(executor); - }); + #[cfg(feature = "trace")] + let executor_span = info_span!("schedule_task"); + #[cfg(feature = "trace")] + let executor = executor.instrument(executor_span); + scope.spawn(executor); + }, + ); // Do one final apply buffers after all systems have completed // SAFETY: all systems have completed, and so no outstanding accesses remain @@ -574,3 +587,13 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World }) .fold(true, |acc, res| acc && res) } + +/// New-typed [`ThreadExecutor`] [`Resource`] that is used to run systems on the main thread +#[derive(Resource, Default, Clone)] +pub struct MainThreadExecutor(pub Arc>); + +impl MainThreadExecutor { + pub fn new() -> Self { + MainThreadExecutor(Arc::new(ThreadExecutor::new())) + } +} diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 43bbbf8409060..516911aa038ae 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -2,7 +2,8 @@ use async_channel::{Receiver, Sender}; use bevy_app::{App, AppLabel, Plugin, SubApp}; use bevy_ecs::{ - schedule::{MainThreadExecutor, StageLabel, SystemStage}, + schedule::{StageLabel, SystemStage}, + schedule_v3::MainThreadExecutor, system::Resource, world::{Mut, World}, }; From faf007ec24839806dfcff9ed78beac1b59e05864 Mon Sep 17 00:00:00 2001 From: Mike Hsu Date: Sat, 28 Jan 2023 20:01:56 -0800 Subject: [PATCH 2/5] add a method to be able to always run a task on the scope thread --- .../src/schedule/executor_parallel.rs | 2 +- .../src/single_threaded_task_pool.rs | 15 ++- crates/bevy_tasks/src/task_pool.rs | 126 +++++++++++++----- 3 files changed, 109 insertions(+), 34 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor_parallel.rs b/crates/bevy_ecs/src/schedule/executor_parallel.rs index dd966ac3cf036..330544c5d6748 100644 --- a/crates/bevy_ecs/src/schedule/executor_parallel.rs +++ b/crates/bevy_ecs/src/schedule/executor_parallel.rs @@ -246,7 +246,7 @@ impl ParallelExecutor { if system_data.is_send { scope.spawn(task); } else { - scope.spawn_on_scope(task); + scope.spawn_on_external(task); } #[cfg(test)] diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 9b77d8fd3bb2c..83e8084a6d3cd 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -178,17 +178,28 @@ pub struct Scope<'scope, 'env: 'scope, T> { } impl<'scope, 'env, T: Send + 'env> Scope<'scope, 'env, T> { - /// Spawns a scoped future onto the thread-local executor. The scope *must* outlive + /// Spawns a scoped future onto the executor. The scope *must* outlive /// the provided future. The results of the future will be returned as a part of /// [`TaskPool::scope`]'s return value. /// - /// On the single threaded task pool, it just calls [`Scope::spawn_local`]. + /// On the single threaded task pool, it just calls [`Scope::spawn_on_scope`]. /// /// For more information, see [`TaskPool::scope`]. pub fn spawn + 'env>(&self, f: Fut) { self.spawn_on_scope(f); } + /// Spawns a scoped future onto the executor. The scope *must* outlive + /// the provided future. The results of the future will be returned as a part of + /// [`TaskPool::scope`]'s return value. + /// + /// On the single threaded task pool, it just calls [`Scope::spawn_on_scope`]. + /// + /// For more information, see [`TaskPool::scope`]. + pub fn spawn_on_external + 'env>(&self, f: Fut) { + self.spawn_on_scope(f); + } + /// Spawns a scoped future that runs on the thread the scope called from. The /// scope *must* outlive the provided future. The results of the future will be /// returned as a part of [`TaskPool::scope`]'s return value. diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 5ded04818694f..bd921ea8ce0ea 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -275,8 +275,9 @@ impl TaskPool { F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>), T: Send + 'static, { - Self::THREAD_EXECUTOR - .with(|thread_executor| self.scope_with_executor_inner(true, thread_executor, f)) + Self::THREAD_EXECUTOR.with(|scope_executor| { + self.scope_with_executor_inner(true, scope_executor, scope_executor, f) + }) } /// This allows passing an external executor to spawn tasks on. When you pass an external executor @@ -291,28 +292,39 @@ impl TaskPool { pub fn scope_with_executor<'env, F, T>( &self, tick_task_pool_executor: bool, - thread_executor: Option<&ThreadExecutor>, + external_executor: Option<&ThreadExecutor>, f: F, ) -> Vec where F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>), T: Send + 'static, { - // If a `thread_executor` is passed use that. Otherwise get the `thread_executor` stored - // in the `THREAD_EXECUTOR` thread local. - if let Some(thread_executor) = thread_executor { - self.scope_with_executor_inner(tick_task_pool_executor, thread_executor, f) - } else { - Self::THREAD_EXECUTOR.with(|thread_executor| { - self.scope_with_executor_inner(tick_task_pool_executor, thread_executor, f) - }) - } + Self::THREAD_EXECUTOR.with(|scope_executor| { + // If a `thread_executor` is passed use that. Otherwise get the `thread_executor` stored + // in the `THREAD_EXECUTOR` thread local. + if let Some(external_executor) = external_executor { + self.scope_with_executor_inner( + tick_task_pool_executor, + external_executor, + scope_executor, + f, + ) + } else { + self.scope_with_executor_inner( + tick_task_pool_executor, + scope_executor, + scope_executor, + f, + ) + } + }) } fn scope_with_executor_inner<'env, F, T>( &self, tick_task_pool_executor: bool, - thread_executor: &ThreadExecutor, + external_executor: &ThreadExecutor, + scope_executor: &ThreadExecutor, f: F, ) -> Vec where @@ -326,15 +338,17 @@ impl TaskPool { // transmute the lifetimes to 'env here to appease the compiler as it is unable to validate safety. let executor: &async_executor::Executor = &self.executor; let executor: &'env async_executor::Executor = unsafe { mem::transmute(executor) }; - let thread_executor: &'env ThreadExecutor<'env> = - unsafe { mem::transmute(thread_executor) }; + let external_executor: &'env ThreadExecutor<'env> = + unsafe { mem::transmute(external_executor) }; + let scope_executor: &'env ThreadExecutor<'env> = unsafe { mem::transmute(scope_executor) }; let spawned: ConcurrentQueue> = ConcurrentQueue::unbounded(); let spawned_ref: &'env ConcurrentQueue> = unsafe { mem::transmute(&spawned) }; let scope = Scope { executor, - thread_executor, + external_executor, + scope_executor, spawned: spawned_ref, scope: PhantomData, env: PhantomData, @@ -357,24 +371,34 @@ impl TaskPool { }; let tick_task_pool_executor = tick_task_pool_executor || self.threads.is_empty(); - if let Some(thread_ticker) = thread_executor.ticker() { + + // we get this from a thread local so we should always be on the scope executors thread. + let scope_ticker = scope_executor.ticker().unwrap(); + if let Some(thread_ticker) = external_executor.ticker() { if tick_task_pool_executor { - Self::execute_local_global(thread_ticker, executor, get_results).await + Self::execute_external_scope_global( + thread_ticker, + scope_ticker, + executor, + get_results, + ) + .await } else { - Self::execute_local(thread_ticker, get_results).await + Self::execute_external_scope(thread_ticker, scope_ticker, get_results).await } } else if tick_task_pool_executor { - Self::execute_global(executor, get_results).await + Self::execute_scope_global(executor, scope_ticker, get_results).await } else { - get_results.await + Self::execute_scope(scope_ticker, get_results).await } }) } } #[inline] - async fn execute_local_global<'scope, 'ticker, T>( - thread_ticker: ThreadExecutorTicker<'scope, 'ticker>, + async fn execute_external_scope_global<'scope, 'ticker, T>( + external_ticker: ThreadExecutorTicker<'scope, 'ticker>, + scope_ticker: ThreadExecutorTicker<'scope, 'ticker>, executor: &'scope async_executor::Executor<'scope>, get_results: impl Future>, ) -> Vec { @@ -384,7 +408,7 @@ impl TaskPool { loop { let tick_forever = async { loop { - thread_ticker.tick().await; + external_ticker.tick().or(scope_ticker.tick()).await; } }; // we don't care if it errors. If a scoped task errors it will propagate @@ -399,15 +423,16 @@ impl TaskPool { } #[inline] - async fn execute_local<'scope, 'ticker, T>( - thread_ticker: ThreadExecutorTicker<'scope, 'ticker>, + async fn execute_external_scope<'scope, 'ticker, T>( + external_ticker: ThreadExecutorTicker<'scope, 'ticker>, + scope_ticker: ThreadExecutorTicker<'scope, 'ticker>, get_results: impl Future>, ) -> Vec { let execute_forever = async { loop { let tick_forever = async { loop { - thread_ticker.tick().await; + external_ticker.tick().or(scope_ticker.tick()).await; } }; let _result = AssertUnwindSafe(tick_forever).catch_unwind().await.is_ok(); @@ -417,13 +442,19 @@ impl TaskPool { } #[inline] - async fn execute_global<'scope, T>( + async fn execute_scope_global<'scope, 'ticker, T>( executor: &'scope async_executor::Executor<'scope>, + scope_ticker: ThreadExecutorTicker<'scope, 'ticker>, get_results: impl Future>, ) -> Vec { let execute_forever = async { loop { - let _result = AssertUnwindSafe(executor.run(std::future::pending::<()>())) + let tick_forever = async { + loop { + scope_ticker.tick().await; + } + }; + let _result = AssertUnwindSafe(executor.run(tick_forever)) .catch_unwind() .await .is_ok(); @@ -432,6 +463,24 @@ impl TaskPool { execute_forever.or(get_results).await } + #[inline] + async fn execute_scope<'scope, 'ticker, T>( + scope_ticker: ThreadExecutorTicker<'scope, 'ticker>, + get_results: impl Future>, + ) -> Vec { + let execute_forever = async { + loop { + let tick_forever = async { + loop { + scope_ticker.tick().await; + } + }; + let _result = AssertUnwindSafe(tick_forever).catch_unwind().await.is_ok(); + } + }; + execute_forever.or(get_results).await + } + /// Spawns a static future onto the thread pool. The returned Task is a future. It can also be /// cancelled and "detached" allowing it to continue running without having to be polled by the /// end-user. @@ -501,7 +550,8 @@ impl Drop for TaskPool { #[derive(Debug)] pub struct Scope<'scope, 'env: 'scope, T> { executor: &'scope async_executor::Executor<'scope>, - thread_executor: &'scope ThreadExecutor<'scope>, + external_executor: &'scope ThreadExecutor<'scope>, + scope_executor: &'scope ThreadExecutor<'scope>, spawned: &'scope ConcurrentQueue>, // make `Scope` invariant over 'scope and 'env scope: PhantomData<&'scope mut &'scope ()>, @@ -531,7 +581,21 @@ impl<'scope, 'env, T: Send + 'scope> Scope<'scope, 'env, T> { /// /// For more information, see [`TaskPool::scope`]. pub fn spawn_on_scope + 'scope + Send>(&self, f: Fut) { - let task = self.thread_executor.spawn(f).fallible(); + let task = self.scope_executor.spawn(f).fallible(); + // ConcurrentQueue only errors when closed or full, but we never + // close and use an unbounded queue, so it is safe to unwrap + self.spawned.push(task).unwrap(); + } + + /// Spawns a scoped future onto the thread of the external thread executor. + /// This is typically the main thread. The scope *must* outlive + /// the provided future. The results of the future will be returned as a part of + /// [`TaskPool::scope`]'s return value. Users should generally prefer to use + /// [`Scope::spawn`] instead, unless the provided future needs to run on the scope's thread. + /// + /// For more information, see [`TaskPool::scope`]. + pub fn spawn_on_external + 'scope + Send>(&self, f: Fut) { + let task = self.external_executor.spawn(f).fallible(); // ConcurrentQueue only errors when closed or full, but we never // close and use an unbounded queue, so it is safe to unwrap self.spawned.push(task).unwrap(); From 7c45a220d370af7ec325ad1d24ef5db880a5cc03 Mon Sep 17 00:00:00 2001 From: Mike Hsu Date: Sun, 29 Jan 2023 13:19:19 -0800 Subject: [PATCH 3/5] cleanup --- crates/bevy_tasks/src/task_pool.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index bd921ea8ce0ea..254e482da5ed6 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -300,7 +300,7 @@ impl TaskPool { T: Send + 'static, { Self::THREAD_EXECUTOR.with(|scope_executor| { - // If a `thread_executor` is passed use that. Otherwise get the `thread_executor` stored + // If a `external_executor` is passed use that. Otherwise get the executor stored // in the `THREAD_EXECUTOR` thread local. if let Some(external_executor) = external_executor { self.scope_with_executor_inner( @@ -374,20 +374,21 @@ impl TaskPool { // we get this from a thread local so we should always be on the scope executors thread. let scope_ticker = scope_executor.ticker().unwrap(); - if let Some(thread_ticker) = external_executor.ticker() { + if let Some(external_ticker) = external_executor.ticker() { if tick_task_pool_executor { - Self::execute_external_scope_global( - thread_ticker, - scope_ticker, + Self::execute_global_external_scope( executor, + external_ticker, + scope_ticker, get_results, ) .await } else { - Self::execute_external_scope(thread_ticker, scope_ticker, get_results).await + Self::execute_external_scope(external_ticker, scope_ticker, get_results) + .await } } else if tick_task_pool_executor { - Self::execute_scope_global(executor, scope_ticker, get_results).await + Self::execute_global_scope(executor, scope_ticker, get_results).await } else { Self::execute_scope(scope_ticker, get_results).await } @@ -396,10 +397,10 @@ impl TaskPool { } #[inline] - async fn execute_external_scope_global<'scope, 'ticker, T>( + async fn execute_global_external_scope<'scope, 'ticker, T>( + executor: &'scope async_executor::Executor<'scope>, external_ticker: ThreadExecutorTicker<'scope, 'ticker>, scope_ticker: ThreadExecutorTicker<'scope, 'ticker>, - executor: &'scope async_executor::Executor<'scope>, get_results: impl Future>, ) -> Vec { // we restart the executors if a task errors. if a scoped @@ -442,7 +443,7 @@ impl TaskPool { } #[inline] - async fn execute_scope_global<'scope, 'ticker, T>( + async fn execute_global_scope<'scope, 'ticker, T>( executor: &'scope async_executor::Executor<'scope>, scope_ticker: ThreadExecutorTicker<'scope, 'ticker>, get_results: impl Future>, @@ -591,7 +592,7 @@ impl<'scope, 'env, T: Send + 'scope> Scope<'scope, 'env, T> { /// This is typically the main thread. The scope *must* outlive /// the provided future. The results of the future will be returned as a part of /// [`TaskPool::scope`]'s return value. Users should generally prefer to use - /// [`Scope::spawn`] instead, unless the provided future needs to run on the scope's thread. + /// [`Scope::spawn`] instead, unless the provided future needs to run on the external thread. /// /// For more information, see [`TaskPool::scope`]. pub fn spawn_on_external + 'scope + Send>(&self, f: Fut) { From 099d57e8682e3777830371b686e150d1cdbe0cc6 Mon Sep 17 00:00:00 2001 From: Mike Hsu Date: Sun, 29 Jan 2023 14:04:28 -0800 Subject: [PATCH 4/5] show change on stageless --- crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs index 0796ce1963b62..7a7cdb5fd2e0f 100644 --- a/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs @@ -450,7 +450,7 @@ impl MultiThreadedExecutor { scope.spawn(task); } else { self.local_thread_running = true; - scope.spawn_on_scope(task); + scope.spawn_on_external(task); } } From 4e8737af5e8b2be4e119c872ce7b284638c94410 Mon Sep 17 00:00:00 2001 From: Mike Hsu Date: Sun, 29 Jan 2023 14:28:43 -0800 Subject: [PATCH 5/5] missed one --- crates/bevy_ecs/src/schedule/executor_parallel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/executor_parallel.rs b/crates/bevy_ecs/src/schedule/executor_parallel.rs index 330544c5d6748..9ecde8cca4f91 100644 --- a/crates/bevy_ecs/src/schedule/executor_parallel.rs +++ b/crates/bevy_ecs/src/schedule/executor_parallel.rs @@ -281,7 +281,7 @@ impl ParallelExecutor { if system_data.is_send { scope.spawn(task); } else { - scope.spawn_on_scope(task); + scope.spawn_on_external(task); } } }