Skip to content

Commit e1b0bbf

Browse files
committed
Stageless: add a method to scope to always run a task on the scope thread (#7415)
# Objective - Currently exclusive systems and applying buffers run outside of the multithreaded executor and just calls the funtions on the thread the schedule is running on. Stageless changes this to run these using tasks in a scope. Specifically It uses `spawn_on_scope` to run these. For the render thread this is incorrect as calling `spawn_on_scope` there runs tasks on the main thread. It should instead run these on the render thread and only run nonsend systems on the main thread. ## Solution - Add another executor to `Scope` for spawning tasks on the scope. `spawn_on_scope` now always runs the task on the thread the scope is running on. `spawn_on_external` spawns onto the external executor than is optionally passed in. If None is passed `spawn_on_external` will spawn onto the scope executor. - Eventually this new machinery will be able to be removed. This will happen once a fix for removing NonSend resources from the world lands. So this is a temporary fix to support stageless. --- ## Changelog - add a spawn_on_external method to allow spawning on the scope's thread or an external thread ## Migration Guide > No migration guide. The main thread executor was introduced in pipelined rendering which was merged for 0.10. spawn_on_scope now behaves the same way as on 0.9.
1 parent 2e53f3b commit e1b0bbf

File tree

4 files changed

+112
-36
lines changed

4 files changed

+112
-36
lines changed

crates/bevy_ecs/src/schedule/executor_parallel.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ impl ParallelExecutor {
246246
if system_data.is_send {
247247
scope.spawn(task);
248248
} else {
249-
scope.spawn_on_scope(task);
249+
scope.spawn_on_external(task);
250250
}
251251

252252
#[cfg(test)]
@@ -281,7 +281,7 @@ impl ParallelExecutor {
281281
if system_data.is_send {
282282
scope.spawn(task);
283283
} else {
284-
scope.spawn_on_scope(task);
284+
scope.spawn_on_external(task);
285285
}
286286
}
287287
}

crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ impl MultiThreadedExecutor {
459459
scope.spawn(task);
460460
} else {
461461
self.local_thread_running = true;
462-
scope.spawn_on_scope(task);
462+
scope.spawn_on_external(task);
463463
}
464464
}
465465

crates/bevy_tasks/src/single_threaded_task_pool.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,28 @@ pub struct Scope<'scope, 'env: 'scope, T> {
178178
}
179179

180180
impl<'scope, 'env, T: Send + 'env> Scope<'scope, 'env, T> {
181-
/// Spawns a scoped future onto the thread-local executor. The scope *must* outlive
181+
/// Spawns a scoped future onto the executor. The scope *must* outlive
182182
/// the provided future. The results of the future will be returned as a part of
183183
/// [`TaskPool::scope`]'s return value.
184184
///
185-
/// On the single threaded task pool, it just calls [`Scope::spawn_local`].
185+
/// On the single threaded task pool, it just calls [`Scope::spawn_on_scope`].
186186
///
187187
/// For more information, see [`TaskPool::scope`].
188188
pub fn spawn<Fut: Future<Output = T> + 'env>(&self, f: Fut) {
189189
self.spawn_on_scope(f);
190190
}
191191

192+
/// Spawns a scoped future onto the executor. The scope *must* outlive
193+
/// the provided future. The results of the future will be returned as a part of
194+
/// [`TaskPool::scope`]'s return value.
195+
///
196+
/// On the single threaded task pool, it just calls [`Scope::spawn_on_scope`].
197+
///
198+
/// For more information, see [`TaskPool::scope`].
199+
pub fn spawn_on_external<Fut: Future<Output = T> + 'env>(&self, f: Fut) {
200+
self.spawn_on_scope(f);
201+
}
202+
192203
/// Spawns a scoped future that runs on the thread the scope called from. The
193204
/// scope *must* outlive the provided future. The results of the future will be
194205
/// returned as a part of [`TaskPool::scope`]'s return value.

crates/bevy_tasks/src/task_pool.rs

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,9 @@ impl TaskPool {
275275
F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>),
276276
T: Send + 'static,
277277
{
278-
Self::THREAD_EXECUTOR
279-
.with(|thread_executor| self.scope_with_executor_inner(true, thread_executor, f))
278+
Self::THREAD_EXECUTOR.with(|scope_executor| {
279+
self.scope_with_executor_inner(true, scope_executor, scope_executor, f)
280+
})
280281
}
281282

282283
/// This allows passing an external executor to spawn tasks on. When you pass an external executor
@@ -291,28 +292,39 @@ impl TaskPool {
291292
pub fn scope_with_executor<'env, F, T>(
292293
&self,
293294
tick_task_pool_executor: bool,
294-
thread_executor: Option<&ThreadExecutor>,
295+
external_executor: Option<&ThreadExecutor>,
295296
f: F,
296297
) -> Vec<T>
297298
where
298299
F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>),
299300
T: Send + 'static,
300301
{
301-
// If a `thread_executor` is passed use that. Otherwise get the `thread_executor` stored
302-
// in the `THREAD_EXECUTOR` thread local.
303-
if let Some(thread_executor) = thread_executor {
304-
self.scope_with_executor_inner(tick_task_pool_executor, thread_executor, f)
305-
} else {
306-
Self::THREAD_EXECUTOR.with(|thread_executor| {
307-
self.scope_with_executor_inner(tick_task_pool_executor, thread_executor, f)
308-
})
309-
}
302+
Self::THREAD_EXECUTOR.with(|scope_executor| {
303+
// If a `external_executor` is passed use that. Otherwise get the executor stored
304+
// in the `THREAD_EXECUTOR` thread local.
305+
if let Some(external_executor) = external_executor {
306+
self.scope_with_executor_inner(
307+
tick_task_pool_executor,
308+
external_executor,
309+
scope_executor,
310+
f,
311+
)
312+
} else {
313+
self.scope_with_executor_inner(
314+
tick_task_pool_executor,
315+
scope_executor,
316+
scope_executor,
317+
f,
318+
)
319+
}
320+
})
310321
}
311322

312323
fn scope_with_executor_inner<'env, F, T>(
313324
&self,
314325
tick_task_pool_executor: bool,
315-
thread_executor: &ThreadExecutor,
326+
external_executor: &ThreadExecutor,
327+
scope_executor: &ThreadExecutor,
316328
f: F,
317329
) -> Vec<T>
318330
where
@@ -326,15 +338,17 @@ impl TaskPool {
326338
// transmute the lifetimes to 'env here to appease the compiler as it is unable to validate safety.
327339
let executor: &async_executor::Executor = &self.executor;
328340
let executor: &'env async_executor::Executor = unsafe { mem::transmute(executor) };
329-
let thread_executor: &'env ThreadExecutor<'env> =
330-
unsafe { mem::transmute(thread_executor) };
341+
let external_executor: &'env ThreadExecutor<'env> =
342+
unsafe { mem::transmute(external_executor) };
343+
let scope_executor: &'env ThreadExecutor<'env> = unsafe { mem::transmute(scope_executor) };
331344
let spawned: ConcurrentQueue<FallibleTask<T>> = ConcurrentQueue::unbounded();
332345
let spawned_ref: &'env ConcurrentQueue<FallibleTask<T>> =
333346
unsafe { mem::transmute(&spawned) };
334347

335348
let scope = Scope {
336349
executor,
337-
thread_executor,
350+
external_executor,
351+
scope_executor,
338352
spawned: spawned_ref,
339353
scope: PhantomData,
340354
env: PhantomData,
@@ -357,25 +371,36 @@ impl TaskPool {
357371
};
358372

359373
let tick_task_pool_executor = tick_task_pool_executor || self.threads.is_empty();
360-
if let Some(thread_ticker) = thread_executor.ticker() {
374+
375+
// we get this from a thread local so we should always be on the scope executors thread.
376+
let scope_ticker = scope_executor.ticker().unwrap();
377+
if let Some(external_ticker) = external_executor.ticker() {
361378
if tick_task_pool_executor {
362-
Self::execute_local_global(thread_ticker, executor, get_results).await
379+
Self::execute_global_external_scope(
380+
executor,
381+
external_ticker,
382+
scope_ticker,
383+
get_results,
384+
)
385+
.await
363386
} else {
364-
Self::execute_local(thread_ticker, get_results).await
387+
Self::execute_external_scope(external_ticker, scope_ticker, get_results)
388+
.await
365389
}
366390
} else if tick_task_pool_executor {
367-
Self::execute_global(executor, get_results).await
391+
Self::execute_global_scope(executor, scope_ticker, get_results).await
368392
} else {
369-
get_results.await
393+
Self::execute_scope(scope_ticker, get_results).await
370394
}
371395
})
372396
}
373397
}
374398

375399
#[inline]
376-
async fn execute_local_global<'scope, 'ticker, T>(
377-
thread_ticker: ThreadExecutorTicker<'scope, 'ticker>,
400+
async fn execute_global_external_scope<'scope, 'ticker, T>(
378401
executor: &'scope async_executor::Executor<'scope>,
402+
external_ticker: ThreadExecutorTicker<'scope, 'ticker>,
403+
scope_ticker: ThreadExecutorTicker<'scope, 'ticker>,
379404
get_results: impl Future<Output = Vec<T>>,
380405
) -> Vec<T> {
381406
// we restart the executors if a task errors. if a scoped
@@ -384,7 +409,7 @@ impl TaskPool {
384409
loop {
385410
let tick_forever = async {
386411
loop {
387-
thread_ticker.tick().await;
412+
external_ticker.tick().or(scope_ticker.tick()).await;
388413
}
389414
};
390415
// we don't care if it errors. If a scoped task errors it will propagate
@@ -399,15 +424,16 @@ impl TaskPool {
399424
}
400425

401426
#[inline]
402-
async fn execute_local<'scope, 'ticker, T>(
403-
thread_ticker: ThreadExecutorTicker<'scope, 'ticker>,
427+
async fn execute_external_scope<'scope, 'ticker, T>(
428+
external_ticker: ThreadExecutorTicker<'scope, 'ticker>,
429+
scope_ticker: ThreadExecutorTicker<'scope, 'ticker>,
404430
get_results: impl Future<Output = Vec<T>>,
405431
) -> Vec<T> {
406432
let execute_forever = async {
407433
loop {
408434
let tick_forever = async {
409435
loop {
410-
thread_ticker.tick().await;
436+
external_ticker.tick().or(scope_ticker.tick()).await;
411437
}
412438
};
413439
let _result = AssertUnwindSafe(tick_forever).catch_unwind().await.is_ok();
@@ -417,13 +443,19 @@ impl TaskPool {
417443
}
418444

419445
#[inline]
420-
async fn execute_global<'scope, T>(
446+
async fn execute_global_scope<'scope, 'ticker, T>(
421447
executor: &'scope async_executor::Executor<'scope>,
448+
scope_ticker: ThreadExecutorTicker<'scope, 'ticker>,
422449
get_results: impl Future<Output = Vec<T>>,
423450
) -> Vec<T> {
424451
let execute_forever = async {
425452
loop {
426-
let _result = AssertUnwindSafe(executor.run(std::future::pending::<()>()))
453+
let tick_forever = async {
454+
loop {
455+
scope_ticker.tick().await;
456+
}
457+
};
458+
let _result = AssertUnwindSafe(executor.run(tick_forever))
427459
.catch_unwind()
428460
.await
429461
.is_ok();
@@ -432,6 +464,24 @@ impl TaskPool {
432464
execute_forever.or(get_results).await
433465
}
434466

467+
#[inline]
468+
async fn execute_scope<'scope, 'ticker, T>(
469+
scope_ticker: ThreadExecutorTicker<'scope, 'ticker>,
470+
get_results: impl Future<Output = Vec<T>>,
471+
) -> Vec<T> {
472+
let execute_forever = async {
473+
loop {
474+
let tick_forever = async {
475+
loop {
476+
scope_ticker.tick().await;
477+
}
478+
};
479+
let _result = AssertUnwindSafe(tick_forever).catch_unwind().await.is_ok();
480+
}
481+
};
482+
execute_forever.or(get_results).await
483+
}
484+
435485
/// Spawns a static future onto the thread pool. The returned Task is a future. It can also be
436486
/// cancelled and "detached" allowing it to continue running without having to be polled by the
437487
/// end-user.
@@ -501,7 +551,8 @@ impl Drop for TaskPool {
501551
#[derive(Debug)]
502552
pub struct Scope<'scope, 'env: 'scope, T> {
503553
executor: &'scope async_executor::Executor<'scope>,
504-
thread_executor: &'scope ThreadExecutor<'scope>,
554+
external_executor: &'scope ThreadExecutor<'scope>,
555+
scope_executor: &'scope ThreadExecutor<'scope>,
505556
spawned: &'scope ConcurrentQueue<FallibleTask<T>>,
506557
// make `Scope` invariant over 'scope and 'env
507558
scope: PhantomData<&'scope mut &'scope ()>,
@@ -531,7 +582,21 @@ impl<'scope, 'env, T: Send + 'scope> Scope<'scope, 'env, T> {
531582
///
532583
/// For more information, see [`TaskPool::scope`].
533584
pub fn spawn_on_scope<Fut: Future<Output = T> + 'scope + Send>(&self, f: Fut) {
534-
let task = self.thread_executor.spawn(f).fallible();
585+
let task = self.scope_executor.spawn(f).fallible();
586+
// ConcurrentQueue only errors when closed or full, but we never
587+
// close and use an unbounded queue, so it is safe to unwrap
588+
self.spawned.push(task).unwrap();
589+
}
590+
591+
/// Spawns a scoped future onto the thread of the external thread executor.
592+
/// This is typically the main thread. The scope *must* outlive
593+
/// the provided future. The results of the future will be returned as a part of
594+
/// [`TaskPool::scope`]'s return value. Users should generally prefer to use
595+
/// [`Scope::spawn`] instead, unless the provided future needs to run on the external thread.
596+
///
597+
/// For more information, see [`TaskPool::scope`].
598+
pub fn spawn_on_external<Fut: Future<Output = T> + 'scope + Send>(&self, f: Fut) {
599+
let task = self.external_executor.spawn(f).fallible();
535600
// ConcurrentQueue only errors when closed or full, but we never
536601
// close and use an unbounded queue, so it is safe to unwrap
537602
self.spawned.push(task).unwrap();

0 commit comments

Comments
 (0)