From d802612394c2db996b838ec9300f5f8d8f66c8aa Mon Sep 17 00:00:00 2001 From: Malek Date: Sat, 1 Nov 2025 17:39:38 -0400 Subject: [PATCH 01/32] awa --- Cargo.toml | 6 + crates/bevy_ecs/src/lib.rs | 4 +- .../bevy_ecs/src/schedule/executor/async.rs | 152 ++++++++++++++++++ crates/bevy_ecs/src/schedule/executor/mod.rs | 1 + .../src/schedule/executor/multi_threaded.rs | 3 + .../src/schedule/executor/single_threaded.rs | 3 + crates/bevy_ecs/src/schedule/mod.rs | 2 +- crates/bevy_ecs/src/world/mod.rs | 2 +- examples/ecs/async_ecs.rs | 128 +++++++++++++++ 9 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 crates/bevy_ecs/src/schedule/executor/async.rs create mode 100644 examples/ecs/async_ecs.rs diff --git a/Cargo.toml b/Cargo.toml index 9e1af044b7401..930f896840d2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2505,6 +2505,12 @@ path = "examples/ecs/system_stepping.rs" doc-scrape-examples = true required-features = ["bevy_debug_stepping"] +[[example]] +name = "async_ecs" +path = "examples/ecs/async_ecs.rs" +doc-scrape-examples = true +required-features = ["default"] + [package.metadata.example.system_stepping] name = "System Stepping" description = "Demonstrate stepping through systems in order of execution." diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index a281f4b7d5853..e13e052f7133a 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -94,7 +94,7 @@ pub mod prelude { resource::Resource, schedule::{ common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, - Schedules, SystemCondition, SystemSet, + Schedules, SystemCondition, SystemSet, executor::r#async::async_access }, spawn::{Spawn, SpawnIter, SpawnRelated, SpawnWith, WithOneRelated, WithRelated}, system::{ @@ -105,7 +105,7 @@ pub mod prelude { }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, World, + FromWorld, World, identifier::WorldId }, }; diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs new file mode 100644 index 0000000000000..2678d95d188c5 --- /dev/null +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -0,0 +1,152 @@ +use crate::world::unsafe_world_cell::UnsafeWorldCell; +use bevy_ecs::prelude::World; +use bevy_ecs::system::{IntoSystem, RunSystemError, RunSystemOnce, System, SystemIn, SystemInput, SystemParam, SystemState}; +use bevy_ecs::world::WorldId; +use bevy_platform::collections::HashMap; +use std::marker::PhantomData; +use std::pin::Pin; +use std::prelude::v1::Vec; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::task::{Context, Poll}; +use std::thread; + +pub(crate) static ASYNC_ECS_WORLD_ACCESS: LockWrapper = LockWrapper(OnceLock::new()); +pub(crate) static ASYNC_ECS_WAKER_LIST: EcsWakerList = EcsWakerList(OnceLock::new()); +#[derive(bevy_ecs_macros::Resource)] +pub(crate) struct AsyncBarrier(thread::Thread, AtomicI64); + +pub(crate) struct EcsWakerList(OnceLock>>>); + +impl EcsWakerList { + pub fn wait(&self, world: &mut World) -> Option<()> { + let world_id = world.id(); + // We intentionally do not hold this lock the whole time because we are emptying the vec, it's gonna be all new wakers next time. + let waker_list = self + .0 + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .ok()? + .remove(&world_id)?; + let waker_list_len = waker_list.len(); + world.insert_resource(AsyncBarrier( + thread::current(), + AtomicI64::new(waker_list_len as i64), + )); + let drop_wrapper = unsafe { ASYNC_ECS_WORLD_ACCESS.set(world) }; + for waker in waker_list { + waker.wake(); + } + if waker_list_len != 0 { + thread::park(); + } + drop(drop_wrapper); + Some(()) + } +} + +pub(crate) struct LockWrapper(OnceLock>>>>); + +// SAFETY: Because this lockwrapper removes the UnsafeWorldCell when it goes out of scope, we ensure the UnsafeWorlCell inside can't actually escape the lifetime. +impl Drop for LockWrapper { + fn drop(&mut self) { + if let Some(awa) = self.0.get() { + if let Ok(mut awa) = awa.lock() { + awa.take(); + } + } + } +} + +// SAFETY: Because this lockwrapper removes the UnsafeWorldCell when it goes out of scope, we ensure the UnsafeWorlCell inside can't actually escape the lifetime. +pub(crate) struct DropWrapper { + _unread: OnceLock>>>>, +} + +impl LockWrapper { + pub(crate) unsafe fn set(&self, world: &mut World) -> DropWrapper { + unsafe { + self.0 + .get_or_init(|| Arc::new(Mutex::new(None))) + .lock() + .unwrap() + // SAFETY: This mem transmute is safe only because we drop it after, and our ASYNC_ECS_WORLD_ACCESS is private, and we don't clone it + // where we do use it, so the lifetime doesn't get propagated anywhere. + .replace(std::mem::transmute(world.as_unsafe_world_cell())); + DropWrapper { + _unread: self.0.clone(), + } + } + } + pub(crate) unsafe fn get(&self, func: impl FnOnce(&mut World) -> T) -> Option { + let uwu = self.0.get()?.try_lock().ok()?.clone()?; + // SAFETY: this is safe because we ensure no one else has access to the world. + let out; + unsafe { + out = func(uwu.world_mut()); + } + Some(out) + } +} + +pub async fn async_access(world_id: WorldId, ecs_access: Func) -> Out +where + P: SystemParam + 'static, + for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, +{ + SystemParamThing::(PhantomData::

, PhantomData, Some(ecs_access), world_id) + .await +} + +struct SystemParamThing<'a, 'b, P: SystemParam + 'static, Func, Out>( + PhantomData

, + PhantomData<(Out, &'a (), &'b ())>, + Option, + WorldId, +); + +impl<'a, 'b, P: SystemParam + 'static, Func, Out> Unpin for SystemParamThing<'a, 'b, P, Func, Out> {} + +impl<'a, 'b, P, Func, Out> Future for SystemParamThing<'a, 'b, P, Func, Out> +where + P: SystemParam + 'static, + for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, +{ + type Output = Out; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match unsafe { + ASYNC_ECS_WORLD_ACCESS.get(|world: &mut World| { + let out; + // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. + let mut system_state: SystemState

= SystemState::new(world); + { + // Obtain params and immediately consume them with the closure, + // ensuring the borrow ends before `apply`. + let state = system_state.get_unchecked(world.as_unsafe_world_cell()); + out = self.as_mut().2.take().unwrap()(state); + system_state.apply(world); + } + let async_barrier = world.get_resource::().unwrap(); + if async_barrier.1.fetch_add(-1, Ordering::Relaxed) == 0 { + async_barrier.0.unpark(); + } + out + }) + } { + Some(awa) => Poll::Ready(awa), + None => { + let mut hashmap = ASYNC_ECS_WAKER_LIST + .0 + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .unwrap(); + if !hashmap.contains_key(&self.3) { + hashmap.insert(self.3.clone(), Vec::new()); + } + hashmap.get_mut(&self.3).unwrap().push(cx.waker().clone()); + Poll::Pending + } + } + } +} diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 197ba52be51c5..faff30895ac86 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -1,3 +1,4 @@ +pub mod r#async; #[cfg(feature = "std")] mod multi_threaded; mod single_threaded; diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 497b937c31f51..7b7974a2c2692 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -242,6 +242,9 @@ impl SystemExecutor for MultiThreadedExecutor { _skip_systems: Option<&FixedBitSet>, error_handler: ErrorHandler, ) { + // First thing we do is run async ecs accesses + crate::schedule::executor::r#async::ASYNC_ECS_WAKER_LIST.wait(world); + let state = self.state.get_mut().unwrap(); // reset counts if schedule.systems.is_empty() { diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index d1a519a5c1d49..1cce49a012937 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -60,6 +60,9 @@ impl SystemExecutor for SingleThreadedExecutor { _skip_systems: Option<&FixedBitSet>, error_handler: ErrorHandler, ) { + // We run our async ecs accesses first + crate::schedule::executor::r#async::ASYNC_ECS_WAKER_LIST.wait(world); + // If stepping is enabled, make sure we skip those systems that should // not be run. #[cfg(feature = "bevy_debug_stepping")] diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index f66549921bdae..e2cd16f3e0e6f 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -4,7 +4,7 @@ mod auto_insert_apply_deferred; mod condition; mod config; mod error; -mod executor; +pub mod executor; mod node; mod pass; mod schedule; diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 9d60968ffef47..ef803dc7fb5cf 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -5,7 +5,7 @@ mod deferred_world; mod entity_access; mod entity_fetch; mod filtered_resource; -mod identifier; +pub(crate) mod identifier; mod spawn_batch; pub mod error; diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs new file mode 100644 index 0000000000000..51e2bc962c274 --- /dev/null +++ b/examples/ecs/async_ecs.rs @@ -0,0 +1,128 @@ +//! A minimal example showing how to perform asynchronous work in Bevy +//! using [`AsyncComputeTaskPool`] for parallel task execution and a crossbeam channel +//! to communicate between async tasks and the main ECS thread. +//! +//! This example demonstrates how to spawn detached async tasks, send completion messages via channels, +//! and dynamically spawn ECS entities (cubes) as results from these tasks. The system processes +//! async task results in the main game loop, all without blocking or polling the main thread. + +use bevy::{ + math::ops::{cos, sin}, + prelude::*, + tasks::AsyncComputeTaskPool, +}; +use futures_timer::Delay; +use rand::Rng; +use std::time::Duration; + +const NUM_CUBES: i32 = 6; +const LIGHT_RADIUS: f32 = 8.0; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems( + Startup, + ( + setup_env, + setup_assets, + spawn_tasks.after(setup_assets), + ), + ) + .add_systems(Update, rotate_light) + .run(); +} + +/// Spawns async tasks on the compute task pool to simulate delayed cube creation. +/// +/// Each task is executed on a separate thread and sends the result (cube position) +/// back through the `CubeChannel` once completed. The tasks are detached to +/// run asynchronously without blocking the main thread. +/// +/// In this example, we don't implement task tracking or proper error handling. +fn spawn_tasks(world_id: WorldId) { + let pool = AsyncComputeTaskPool::get(); + + for x in -NUM_CUBES..NUM_CUBES { + for z in -NUM_CUBES..NUM_CUBES { + // Spawn a task on the async compute pool + pool.spawn(async move { + let delay = Duration::from_secs_f32(rand::rng().random_range(2.0..8.0)); + // Simulate a delay before task completion + Delay::new(delay).await; + async_access(world_id, |(mut commands, box_mesh, box_material): ( Commands, Res, Res)| { + commands.spawn(( + Mesh3d(box_mesh.clone()), + MeshMaterial3d(box_material.clone()), + Transform::from_xyz(x as f32, 0.5, z as f32), + )); + }).await; + }) + .detach(); + } + } +} + +/// Resource holding the mesh handle for the box (used for spawning cubes) +#[derive(Resource, Deref)] +struct BoxMeshHandle(Handle); + +/// Resource holding the material handle for the box (used for spawning cubes) +#[derive(Resource, Deref)] +struct BoxMaterialHandle(Handle); + +/// Sets up the shared mesh and material for the cubes. +fn setup_assets( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Create and store a cube mesh + let box_mesh_handle = meshes.add(Cuboid::new(0.4, 0.4, 0.4)); + commands.insert_resource(BoxMeshHandle(box_mesh_handle)); + + // Create and store a red material + let box_material_handle = materials.add(Color::srgb(1.0, 0.2, 0.3)); + commands.insert_resource(BoxMaterialHandle(box_material_handle)); +} + +/// Sets up the environment by spawning the ground, light, and camera. +fn setup_env( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Spawn a circular ground plane + commands.spawn(( + Mesh3d(meshes.add(Circle::new(1.618 * NUM_CUBES as f32))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + + // Spawn a point light with shadows enabled + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(0.0, LIGHT_RADIUS, 4.0), + )); + + // Spawn a camera looking at the origin + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-6.5, 5.5, 12.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +/// Rotates the point light around the origin (0, 0, 0) +fn rotate_light(mut query: Query<&mut Transform, With>, time: Res

, PhantomData, Some(ecs_access), world_id) - .await + SystemParamThing::( + PhantomData::

, + PhantomData, + Some(ecs_access), + (world_id, schedule.intern()), + TaskId::new().unwrap(), + ) + .await } struct SystemParamThing<'a, 'b, P: SystemParam + 'static, Func, Out>( PhantomData

, PhantomData<(Out, &'a (), &'b ())>, Option, - WorldId, + (WorldId, InternedScheduleLabel), + TaskId, ); impl<'a, 'b, P: SystemParam + 'static, Func, Out> Unpin for SystemParamThing<'a, 'b, P, Func, Out> {} @@ -112,40 +199,103 @@ where P: SystemParam + 'static, for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, { - type Output = Out; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match unsafe { - ASYNC_ECS_WORLD_ACCESS.get(|world: &mut World| { + unsafe { + match ASYNC_ECS_WORLD_ACCESS.get(|world: UnsafeWorldCell| { + let async_barrier = { world.get_resource::().unwrap().clone() }; + let our_thing = async_barrier.1.load(Ordering::SeqCst); + std::println!("A: {}", our_thing); + struct RaiiThing(AsyncBarrier); + impl Drop for RaiiThing { + fn drop(&mut self) { + std::println!("ready to drop uwu"); + let val = self.0.1.fetch_add(-1, Ordering::SeqCst); + std::println!("counter is: {val}"); + if val == 0 { + self.0.0.unpark(); + } + } + } + RaiiThing(async_barrier.clone()); let out; + std::println!("B: {}", our_thing); + let mut system_state = SystemState::

::new(world.world_mut()); // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. - let mut system_state: SystemState

= SystemState::new(world); - { + unsafe { // Obtain params and immediately consume them with the closure, // ensuring the borrow ends before `apply`. - let state = system_state.get_unchecked(world.as_unsafe_world_cell()); + if let Err(err) = SystemState::validate_param(&mut system_state, world) { + panic!(); + return Poll::Ready(Err(err.into())); + } + std::println!("C: {}", our_thing); + let state = system_state.get_unchecked(world); + std::println!("D: {}", our_thing); out = self.as_mut().2.take().unwrap()(state); - system_state.apply(world); + std::println!("E: {}", our_thing); } - let async_barrier = world.get_resource::().unwrap(); - if async_barrier.1.fetch_add(-1, Ordering::Relaxed) == 0 { - async_barrier.0.unpark(); + system_state.apply(world.world_mut()); + std::println!("F: {}", our_thing); + /*if let Err(err) = async_barrier.2.send(Box::new(move |world: &mut World| { + system_state.apply(world); + })) { + return Poll::Ready(Err(err.into())); + }*/ + Poll::Ready(Ok(out)) + }) { + Some(Poll::Pending) => { + let mut hashmap = ASYNC_ECS_WAKER_LIST + .0 + .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) + .lock() + .unwrap(); + if !hashmap.0.contains_key(&self.3) { + hashmap.0.insert(self.3.clone(), Vec::new()); + } + hashmap.0 + .get_mut(&self.3) + .unwrap() + .push((cx.waker().clone(), self.4)); + /*if !hashmap.1.contains_key(&self.4) { + hashmap.1 + .insert( + self.4, + MyThing::FnClosure(Box::new( + |world: &mut World| -> Box { + Box::new(SystemState::

::new(world)) + }, + )), + ); + }*/ + Poll::Pending } - out - }) - } { - Some(awa) => Poll::Ready(awa), - None => { - let mut hashmap = ASYNC_ECS_WAKER_LIST - .0 - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .unwrap(); - if !hashmap.contains_key(&self.3) { - hashmap.insert(self.3.clone(), Vec::new()); + None => { + let mut hashmap = ASYNC_ECS_WAKER_LIST + .0 + .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) + .lock() + .unwrap(); + if !hashmap.0.contains_key(&self.3) { + hashmap.0.insert(self.3.clone(), Vec::new()); + } + hashmap.0 + .get_mut(&self.3) + .unwrap() + .push((cx.waker().clone(), self.4)); + /*hashmap.1 + .insert( + self.4, + MyThing::FnClosure(Box::new( + |world: &mut World| -> Box { + Box::new(SystemState::

::new(world)) + }, + )), + );*/ + Poll::Pending } - hashmap.get_mut(&self.3).unwrap().push(cx.waker().clone()); - Poll::Pending + Some(awa) => awa, } } } diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 7b7974a2c2692..497b937c31f51 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -242,9 +242,6 @@ impl SystemExecutor for MultiThreadedExecutor { _skip_systems: Option<&FixedBitSet>, error_handler: ErrorHandler, ) { - // First thing we do is run async ecs accesses - crate::schedule::executor::r#async::ASYNC_ECS_WAKER_LIST.wait(world); - let state = self.state.get_mut().unwrap(); // reset counts if schedule.systems.is_empty() { diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 1cce49a012937..d1a519a5c1d49 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -60,9 +60,6 @@ impl SystemExecutor for SingleThreadedExecutor { _skip_systems: Option<&FixedBitSet>, error_handler: ErrorHandler, ) { - // We run our async ecs accesses first - crate::schedule::executor::r#async::ASYNC_ECS_WAKER_LIST.wait(world); - // If stepping is enabled, make sure we skip those systems that should // not be run. #[cfg(feature = "bevy_debug_stepping")] diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index b21ec09984b5d..683465cd515a8 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -535,7 +535,7 @@ impl Schedule { }); let error_handler = world.default_error_handler(); - + let _ = r#async::ASYNC_ECS_WAKER_LIST.wait(self.label, world); #[cfg(not(feature = "bevy_debug_stepping"))] self.executor .run(&mut self.executable, world, None, error_handler); diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 62b3597f89897..268f88462943d 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -216,9 +216,9 @@ impl SystemMeta { /// } /// ``` pub struct SystemState { - meta: SystemMeta, - param_state: Param::State, - world_id: WorldId, + pub(crate) meta: SystemMeta, + pub(crate) param_state: Param::State, + pub(crate) world_id: WorldId, } // Allow closure arguments to be inferred. diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index 51e2bc962c274..545ad504dc372 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -15,7 +15,7 @@ use futures_timer::Delay; use rand::Rng; use std::time::Duration; -const NUM_CUBES: i32 = 6; +const NUM_CUBES: i32 = 16; const LIGHT_RADIUS: f32 = 8.0; fn main() { @@ -23,11 +23,7 @@ fn main() { .add_plugins(DefaultPlugins) .add_systems( Startup, - ( - setup_env, - setup_assets, - spawn_tasks.after(setup_assets), - ), + (setup_env, setup_assets, spawn_tasks.after(setup_assets)), ) .add_systems(Update, rotate_light) .run(); @@ -49,16 +45,27 @@ fn spawn_tasks(world_id: WorldId) { pool.spawn(async move { let delay = Duration::from_secs_f32(rand::rng().random_range(2.0..8.0)); // Simulate a delay before task completion + println!("delaying for {:?}", delay); Delay::new(delay).await; - async_access(world_id, |(mut commands, box_mesh, box_material): ( Commands, Res, Res)| { - commands.spawn(( - Mesh3d(box_mesh.clone()), - MeshMaterial3d(box_material.clone()), - Transform::from_xyz(x as f32, 0.5, z as f32), - )); - }).await; + if let Err(e) = + async_access::<(Commands, Res, Res), _, _>( + world_id, + Update, + |(mut commands, box_mesh, box_material)| { + println!("spawning"); + commands.spawn(( + Mesh3d(box_mesh.clone()), + MeshMaterial3d(box_material.clone()), + Transform::from_xyz(x as f32, 0.5, z as f32), + )); + }, + ) + .await + { + println!("got error: {}", e); + } }) - .detach(); + .detach(); } } } From bf8ffb1a387cf8d557f35a673b185f3c3580a2e2 Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 14:36:41 -0500 Subject: [PATCH 03/32] uwu --- .../bevy_ecs/src/schedule/executor/async.rs | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 0181c3d140e85..dcd40c4918d23 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -87,13 +87,13 @@ impl EcsWakerList { Arc::new(AtomicI64::new(waker_list_len as i64 - 1)), tx, )); - /*for (_, task_id) in waker_list.iter() { + for (_, task_id) in waker_list.iter() { let mut uwu = this.lock() .unwrap(); let task = uwu.1.remove(task_id).unwrap(); uwu.1.insert(*task_id, task.into_state(world)); drop(uwu); - }*/ + } if let None = ASYNC_ECS_WORLD_ACCESS.set(world, || { for (waker, _) in waker_list { waker.wake(); @@ -221,7 +221,21 @@ where RaiiThing(async_barrier.clone()); let out; std::println!("B: {}", our_thing); - let mut system_state = SystemState::

::new(world.world_mut()); + let mut hashmap = ASYNC_ECS_WAKER_LIST + .0 + .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) + .lock() + .unwrap(); + let Some(awa) = hashmap.1.remove(&self.4) else { + return Poll::Pending + }; + drop(hashmap); + let mut uwu = match awa { + MyThing::FnClosure(_) => panic!(), + MyThing::SystemState(state) => *state.downcast::>().unwrap() + }; + let mut system_state = uwu; + //let mut system_state = SystemState::

::new(world.world_mut()); // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. unsafe { // Obtain params and immediately consume them with the closure, @@ -236,13 +250,13 @@ where out = self.as_mut().2.take().unwrap()(state); std::println!("E: {}", our_thing); } - system_state.apply(world.world_mut()); + //system_state.apply(world.world_mut()); std::println!("F: {}", our_thing); - /*if let Err(err) = async_barrier.2.send(Box::new(move |world: &mut World| { + if let Err(err) = async_barrier.2.send(Box::new(move |world: &mut World| { system_state.apply(world); })) { return Poll::Ready(Err(err.into())); - }*/ + } Poll::Ready(Ok(out)) }) { Some(Poll::Pending) => { @@ -258,7 +272,7 @@ where .get_mut(&self.3) .unwrap() .push((cx.waker().clone(), self.4)); - /*if !hashmap.1.contains_key(&self.4) { + if !hashmap.1.contains_key(&self.4) { hashmap.1 .insert( self.4, @@ -268,7 +282,7 @@ where }, )), ); - }*/ + } Poll::Pending } None => { @@ -284,7 +298,7 @@ where .get_mut(&self.3) .unwrap() .push((cx.waker().clone(), self.4)); - /*hashmap.1 + hashmap.1 .insert( self.4, MyThing::FnClosure(Box::new( @@ -292,7 +306,7 @@ where Box::new(SystemState::

::new(world)) }, )), - );*/ + ); Poll::Pending } Some(awa) => awa, From 53349864c832c034524e2d11f0238d3284c590a8 Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 17:23:23 -0500 Subject: [PATCH 04/32] uwuawaaw --- .../bevy_ecs/src/schedule/executor/async.rs | 339 ++++++++++++++---- examples/ecs/async_ecs.rs | 13 +- 2 files changed, 280 insertions(+), 72 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index dcd40c4918d23..dd999b64cd78c 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -5,16 +5,18 @@ use crate::{ system::{SystemParam, SystemState}, world::World, }; -use bevy_ecs::world::WorldId; +use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; -use std::any::Any; +use std::any::{Any, TypeId}; use std::marker::PhantomData; use std::pin::Pin; use std::prelude::v1::{Box, Vec}; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::{Arc, Mutex, OnceLock, RwLock}; use std::task::{Context, Poll}; use std::thread; +use concurrent_queue::{ConcurrentQueue, PopError, PushError}; +use crate::world::FromWorld; pub(crate) static ASYNC_ECS_WORLD_ACCESS: LockWrapper = LockWrapper(OnceLock::new()); pub(crate) static ASYNC_ECS_WAKER_LIST: EcsWakerList = EcsWakerList(OnceLock::new()); @@ -25,6 +27,48 @@ pub(crate) struct AsyncBarrier( std::sync::mpsc::Sender>, ); + +#[derive(bevy_ecs_macros::Resource)] +pub(crate) struct SystemParamQueue(RwLock>>>); + +#[derive(bevy_ecs_macros::Resource, Default)] +pub(crate) struct SystemParamApplications(HashMap>); +impl SystemParamApplications { + fn run(&mut self, world: &mut World) { + for closure in self.0.values_mut() { + closure(world); + } + } +} +impl FromWorld for SystemParamQueue { + fn from_world(world: &mut World) -> Self { + let this = Self(RwLock::new(HashMap::default())); + world.init_resource::(); + let mut system_param_applications = world.get_resource_mut::().unwrap(); + if !system_param_applications.0.contains_key(&TypeId::of::()) { + system_param_applications.0.insert(TypeId::of::(), Box::new(|world: &mut World| { + world.try_resource_scope(|world, system_param_queue: Mut>| { + for concurrent_queue in system_param_queue.0.read().unwrap().values() { + let mut system_state = match concurrent_queue.pop() { + Ok(val) => val, + Err(_) => panic!(), + }; + system_state.apply(world); + match concurrent_queue.push(system_state) { + Ok(_) => {} + Err(_) => panic!(), + } + } + }); + })); + } + this + } +} + +#[derive(bevy_ecs_macros::Resource, Clone)] +pub(crate) struct AsyncSystemChannel(Arc>); + enum MyThing { FnClosure(Box Box + Send + Sync>), SystemState(Box), @@ -66,20 +110,27 @@ impl TaskId { } pub(crate) struct EcsWakerList( - OnceLock>, HashMap)>>, + OnceLock< + Mutex<( + HashMap<(WorldId, InternedScheduleLabel), Vec<(std::task::Waker, TaskId)>>, + HashMap, + )>, + >, ); - impl EcsWakerList { pub fn wait(&self, schedule: InternedScheduleLabel, world: &mut World) -> Option<()> { - let this = self.0.get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))); + if !world.contains_resource::() { + world.insert_resource(AsyncSystemChannel(Arc::new(RwLock::new(TypeMap( + HashMap::new(), + ))))); + } + let this = self + .0 + .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))); let world_id = world.id(); // We intentionally do not hold this lock the whole time because we are emptying the vec, it's gonna be all new wakers next time. - let mut waker_list = this - .try_lock() - .ok()? - .0 - .remove(&(world_id, schedule))?; + let mut waker_list = this.try_lock().ok()?.0.remove(&(world_id, schedule))?; let waker_list_len = waker_list.len(); let (tx, rx) = std::sync::mpsc::channel(); world.insert_resource(AsyncBarrier( @@ -88,8 +139,7 @@ impl EcsWakerList { tx, )); for (_, task_id) in waker_list.iter() { - let mut uwu = this.lock() - .unwrap(); + let mut uwu = this.lock().unwrap(); let task = uwu.1.remove(task_id).unwrap(); uwu.1.insert(*task_id, task.into_state(world)); drop(uwu); @@ -99,18 +149,27 @@ impl EcsWakerList { waker.wake(); } if waker_list_len != 0 { - std::println!("thread is parking"); + //std::println!("thread is parking"); thread::park(); - std::println!("thread is unparked"); + //std::println!("thread is unparked"); } else { panic!("AWA"); } }) { - return None + return None; } - for thing in rx.try_iter() { + world.try_resource_scope(|world, mut system_param_applications: Mut| { + system_param_applications.run(world); + }); + /*let mut t = world + .get_resource::() + .unwrap() + .clone() + .0; + t.write().unwrap().run_all(world);*/ + /*for thing in rx.try_iter() { thing(world); - } + }*/ Some(()) } } @@ -118,7 +177,7 @@ impl EcsWakerList { pub(crate) struct LockWrapper(OnceLock>>>>); impl LockWrapper { - pub(crate) fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()>{ + pub(crate) fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()> { // local RAII type struct ClearOnDrop<'a> { slot: &'a Mutex>>, @@ -132,7 +191,8 @@ impl LockWrapper { } unsafe { - let mut awa = self.0 + let mut awa = self + .0 .get_or_init(|| Arc::new(Mutex::new(None))) .try_lock() .ok()?; @@ -140,8 +200,8 @@ impl LockWrapper { let _clear = ClearOnDrop { slot: self.0.get().unwrap(), }; - // SAFETY: This mem transmute is safe only because we drop it after, and our ASYNC_ECS_WORLD_ACCESS is private, and we don't clone it - // where we do use it, so the lifetime doesn't get propagated anywhere. + // SAFETY: This mem transmute is safe only because we drop it after, and our ASYNC_ECS_WORLD_ACCESS is private, and we don't clone it + // where we do use it, so the lifetime doesn't get propagated anywhere. awa.replace(std::mem::transmute(world.as_unsafe_world_cell())); drop(awa); func() @@ -165,7 +225,52 @@ impl LockWrapper { } } -pub async fn async_access( +struct TypeMap( + HashMap< + TypeId, + ( + Box, + Mutex>, + ), + >, +); +impl TypeMap { + pub fn set(&mut self, _t: &SystemState) { + let (tx, rx) = std::sync::mpsc::channel::>(); + + self.0.insert( + TypeId::of::(), + ( + Box::new(tx), + Mutex::new(Box::new(move |world: &mut World| { + for mut thing in rx.try_iter() { + thing.apply(world); + } + })), + ), + ); + } + pub fn has(&self, _t: &SystemState) -> bool { + self.0.contains_key(&TypeId::of::()) + } + pub fn send(&self, t: SystemState) -> Option<()> { + self.0 + .get(&TypeId::of::())? + .0 + .downcast_ref::>>() + .unwrap() + .send(t) + .ok()?; + Some(()) + } + pub fn run_all(&mut self, world: &mut World) { + for (_, closure) in self.0.values_mut() { + closure.lock().unwrap()(world); + } + } +} + +/*pub async fn async_access( world_id: WorldId, schedule: impl ScheduleLabel, ecs_access: Func, @@ -182,19 +287,74 @@ where TaskId::new().unwrap(), ) .await +}*/ + +pub fn async_access( + task_identifier: impl BecomeTaskIdentifier

, + schedule: impl ScheduleLabel, + ecs_access: Func, +) -> impl Future> +where + P: SystemParam + 'static, + for<'w, 's> Func: Clone + FnMut(P::Item<'w, 's>) -> Out, +{ + let task_identifier = task_identifier.become_task_identifier(); + SystemParamThing::( + PhantomData::

, + PhantomData, + Some(ecs_access), + (task_identifier.1, schedule.intern()), + task_identifier.0, + ) +} + +pub trait BecomeTaskIdentifier { + fn become_task_identifier(&self) -> TaskIdentifier; +} +impl BecomeTaskIdentifier for WorldId { + fn become_task_identifier(&self) -> TaskIdentifier { + TaskIdentifier::new(*self) + } +} + +impl BecomeTaskIdentifier for &TaskIdentifier { + fn become_task_identifier(&self) -> TaskIdentifier { + TaskIdentifier(self.0, self.1, PhantomData) + } } -struct SystemParamThing<'a, 'b, P: SystemParam + 'static, Func, Out>( +impl From for TaskIdentifier { + fn from(value: WorldId) -> Self { + TaskIdentifier::new(value) + } +} + +pub struct TaskIdentifier(TaskId, WorldId, PhantomData); + +impl Clone for TaskIdentifier { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for TaskIdentifier {} +impl TaskIdentifier { + pub fn new(world_id: WorldId) -> Self { + Self(TaskId::new().unwrap(), world_id, PhantomData) + } +} + +struct SystemParamThing( PhantomData

, - PhantomData<(Out, &'a (), &'b ())>, + PhantomData, Option, (WorldId, InternedScheduleLabel), TaskId, ); -impl<'a, 'b, P: SystemParam + 'static, Func, Out> Unpin for SystemParamThing<'a, 'b, P, Func, Out> {} +impl Unpin for SystemParamThing {} -impl<'a, 'b, P, Func, Out> Future for SystemParamThing<'a, 'b, P, Func, Out> +impl Future for SystemParamThing where P: SystemParam + 'static, for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, @@ -202,61 +362,82 @@ where type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let task_id = self.4; unsafe { match ASYNC_ECS_WORLD_ACCESS.get(|world: UnsafeWorldCell| { - let async_barrier = { world.get_resource::().unwrap().clone() }; - let our_thing = async_barrier.1.load(Ordering::SeqCst); - std::println!("A: {}", our_thing); struct RaiiThing(AsyncBarrier); impl Drop for RaiiThing { fn drop(&mut self) { - std::println!("ready to drop uwu"); - let val = self.0.1.fetch_add(-1, Ordering::SeqCst); - std::println!("counter is: {val}"); + //std::println!("ready to drop uwu"); + let val = self.0 .1.fetch_add(-1, Ordering::SeqCst); + //std::println!("counter is: {val}"); if val == 0 { - self.0.0.unpark(); + self.0 .0.unpark(); } } } + let async_barrier = { world.get_resource::().unwrap().clone() }; RaiiThing(async_barrier.clone()); + + + let system_param_queue = match world.get_resource::>() { + None => { + return Poll::Pending + } + Some(system_param_queue) => system_param_queue, + }; + + let mut system_state = match system_param_queue.0.read().unwrap().get(&task_id) { + None => return Poll::Pending, + Some(cq) => { + cq.pop().unwrap() + } + }; let out; - std::println!("B: {}", our_thing); - let mut hashmap = ASYNC_ECS_WAKER_LIST + //std::println!("B: {}", our_thing); + /*let mut hashmap = ASYNC_ECS_WAKER_LIST .0 .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) .lock() .unwrap(); - let Some(awa) = hashmap.1.remove(&self.4) else { - return Poll::Pending + let Some(awa) = hashmap.1.remove(&task_id) else { + return Poll::Pending; }; drop(hashmap); let mut uwu = match awa { MyThing::FnClosure(_) => panic!(), - MyThing::SystemState(state) => *state.downcast::>().unwrap() + MyThing::SystemState(state) => *state.downcast::>().unwrap(), }; - let mut system_state = uwu; + let mut system_state = uwu;*/ //let mut system_state = SystemState::

::new(world.world_mut()); // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. unsafe { // Obtain params and immediately consume them with the closure, // ensuring the borrow ends before `apply`. if let Err(err) = SystemState::validate_param(&mut system_state, world) { - panic!(); return Poll::Ready(Err(err.into())); } - std::println!("C: {}", our_thing); + //std::println!("C: {}", our_thing); let state = system_state.get_unchecked(world); - std::println!("D: {}", our_thing); + //std::println!("D: {}", our_thing); out = self.as_mut().2.take().unwrap()(state); - std::println!("E: {}", our_thing); + //std::println!("E: {}", our_thing); } //system_state.apply(world.world_mut()); - std::println!("F: {}", our_thing); - if let Err(err) = async_barrier.2.send(Box::new(move |world: &mut World| { + //std::println!("F: {}", our_thing); + /*if !async_system_channel.0.read().unwrap().has(&system_state) { + async_system_channel.0.write().unwrap().set(&system_state); + } + async_system_channel.0.read().unwrap().send(system_state);*/ + match world.get_resource::>().unwrap().0.read().unwrap().get(&task_id).unwrap().push(system_state) { + Ok(_) => {} + Err(_) => panic!(), + } + /*if let Err(err) = async_barrier.2.send(Box::new(move |world: &mut World| { system_state.apply(world); })) { return Poll::Ready(Err(err.into())); - } + }*/ Poll::Ready(Ok(out)) }) { Some(Poll::Pending) => { @@ -268,20 +449,32 @@ where if !hashmap.0.contains_key(&self.3) { hashmap.0.insert(self.3.clone(), Vec::new()); } - hashmap.0 + hashmap + .0 .get_mut(&self.3) .unwrap() .push((cx.waker().clone(), self.4)); if !hashmap.1.contains_key(&self.4) { - hashmap.1 - .insert( - self.4, - MyThing::FnClosure(Box::new( - |world: &mut World| -> Box { - Box::new(SystemState::

::new(world)) - }, - )), - ); + hashmap.1.insert( + self.4, + MyThing::FnClosure(Box::new( + move |world: &mut World| -> Box { + world.init_resource::>(); + if !world.get_resource::>().unwrap().0.read().unwrap().contains_key(&task_id) { + let system_state = SystemState::

::new(world); + let cq = ConcurrentQueue::bounded(1); + match cq.push(system_state) { + Ok(_) => {} + Err(_) => { + panic!() + } + } + world.get_resource::>().unwrap().0.write().unwrap().insert(task_id, cq); + } + Box::new(SystemState::

::new(world)) + }, + )), + ); } Poll::Pending } @@ -294,19 +487,31 @@ where if !hashmap.0.contains_key(&self.3) { hashmap.0.insert(self.3.clone(), Vec::new()); } - hashmap.0 + hashmap + .0 .get_mut(&self.3) .unwrap() .push((cx.waker().clone(), self.4)); - hashmap.1 - .insert( - self.4, - MyThing::FnClosure(Box::new( - |world: &mut World| -> Box { - Box::new(SystemState::

::new(world)) - }, - )), - ); + hashmap.1.insert( + self.4, + MyThing::FnClosure(Box::new( + move |world: &mut World| -> Box { + world.init_resource::>(); + if !world.get_resource::>().unwrap().0.read().unwrap().contains_key(&task_id) { + let system_state = SystemState::

::new(world); + let cq = ConcurrentQueue::bounded(1); + match cq.push(system_state) { + Ok(_) => {} + Err(_) => { + panic!() + } + }; + world.get_resource::>().unwrap().0.write().unwrap().insert(task_id, cq); + } + Box::new(SystemState::

::new(world)) + }, + )), + ); Poll::Pending } Some(awa) => awa, diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index 545ad504dc372..608e7744ed195 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -14,6 +14,7 @@ use bevy::{ use futures_timer::Delay; use rand::Rng; use std::time::Duration; +use bevy_ecs::schedule::r#async::TaskIdentifier; const NUM_CUBES: i32 = 16; const LIGHT_RADIUS: f32 = 8.0; @@ -38,21 +39,23 @@ fn main() { /// In this example, we don't implement task tracking or proper error handling. fn spawn_tasks(world_id: WorldId) { let pool = AsyncComputeTaskPool::get(); - + let task_id = TaskIdentifier::new(world_id); for x in -NUM_CUBES..NUM_CUBES { for z in -NUM_CUBES..NUM_CUBES { // Spawn a task on the async compute pool + let task_id = task_id.clone(); pool.spawn(async move { let delay = Duration::from_secs_f32(rand::rng().random_range(2.0..8.0)); // Simulate a delay before task completion println!("delaying for {:?}", delay); Delay::new(delay).await; if let Err(e) = - async_access::<(Commands, Res, Res), _, _>( - world_id, + async_access::<(Local, Commands, Res, Res), _, _>( + &task_id, Update, - |(mut commands, box_mesh, box_material)| { - println!("spawning"); + |(mut local, mut commands, box_mesh, box_material)| { + *local += 1; + println!("spawning {}", *local); commands.spawn(( Mesh3d(box_mesh.clone()), MeshMaterial3d(box_material.clone()), From c112c9e28b83856a52f958efa46be24e76f6b3b4 Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 18:17:01 -0500 Subject: [PATCH 05/32] awa --- .../bevy_ecs/src/schedule/executor/async.rs | 390 ++++++++---------- examples/ecs/async_ecs.rs | 41 +- 2 files changed, 189 insertions(+), 242 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index dd999b64cd78c..47ac9347c121c 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -1,25 +1,78 @@ +use crate::schedule::r#async::keyed_queues::KeyedQueues; use crate::schedule::{InternedScheduleLabel, ScheduleLabel}; use crate::system::{RunSystemError, SystemMeta}; use crate::world::unsafe_world_cell::UnsafeWorldCell; +use crate::world::FromWorld; use crate::{ system::{SystemParam, SystemState}, world::World, }; use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; +use concurrent_queue::ConcurrentQueue; use std::any::{Any, TypeId}; use std::marker::PhantomData; use std::pin::Pin; use std::prelude::v1::{Box, Vec}; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, OnceLock, RwLock}; -use std::task::{Context, Poll}; +use std::task::{Context, Poll, Waker}; use std::thread; -use concurrent_queue::{ConcurrentQueue, PopError, PushError}; -use crate::world::FromWorld; + +mod keyed_queues { + use concurrent_queue::ConcurrentQueue; + use std::sync::Arc; + use std::{collections::HashMap, hash::Hash, sync::RwLock}; + + /// HashMap>> behind a single RwLock. + /// - Writers only contend when creating a new key or GC'ing. + /// - `push` is non-blocking (unbounded queue). + pub struct KeyedQueues { + inner: RwLock>>>, + } + + impl KeyedQueues + where + K: Eq + Hash + Clone, + V: Send + 'static, + { + pub fn new() -> Self { + Self { + inner: RwLock::new(HashMap::new()), + } + } + + #[inline] + pub fn get_or_create(&self, key: &K) -> Arc> { + // Fast path: try read lock first + if let Some(q) = self.inner.read().unwrap().get(key).cloned() { + return q; + } + // Slow path: create under write lock if still absent + let mut write = self.inner.write().unwrap(); + // We intentionally check a second time because of synchronization + if let Some(q) = write.get(key).cloned() { + return q; + } + let q = Arc::new(ConcurrentQueue::unbounded()); + write.insert(key.clone(), q.clone()); + q + } + + /// Potentially-blocking send but almost never blocking (unbounded queue => `push` never fails). + /// ( Only blocks when the (WorldId, Schedule) has never been used before + #[inline] + pub fn try_send(&self, key: &K, val: V) -> Result<(), concurrent_queue::PushError> { + let q = self.get_or_create(key); + q.push(val) + } + } +} pub(crate) static ASYNC_ECS_WORLD_ACCESS: LockWrapper = LockWrapper(OnceLock::new()); + pub(crate) static ASYNC_ECS_WAKER_LIST: EcsWakerList = EcsWakerList(OnceLock::new()); + #[derive(bevy_ecs_macros::Resource, Clone)] pub(crate) struct AsyncBarrier( thread::Thread, @@ -27,12 +80,15 @@ pub(crate) struct AsyncBarrier( std::sync::mpsc::Sender>, ); - #[derive(bevy_ecs_macros::Resource)] -pub(crate) struct SystemParamQueue(RwLock>>>); +pub(crate) struct SystemParamQueue( + RwLock>>>, +); #[derive(bevy_ecs_macros::Resource, Default)] -pub(crate) struct SystemParamApplications(HashMap>); +pub(crate) struct SystemParamApplications( + HashMap>, +); impl SystemParamApplications { fn run(&mut self, world: &mut World) { for closure in self.0.values_mut() { @@ -44,48 +100,34 @@ impl FromWorld for SystemParamQueue { fn from_world(world: &mut World) -> Self { let this = Self(RwLock::new(HashMap::default())); world.init_resource::(); - let mut system_param_applications = world.get_resource_mut::().unwrap(); + let mut system_param_applications = + world.get_resource_mut::().unwrap(); if !system_param_applications.0.contains_key(&TypeId::of::()) { - system_param_applications.0.insert(TypeId::of::(), Box::new(|world: &mut World| { - world.try_resource_scope(|world, system_param_queue: Mut>| { - for concurrent_queue in system_param_queue.0.read().unwrap().values() { - let mut system_state = match concurrent_queue.pop() { - Ok(val) => val, - Err(_) => panic!(), - }; - system_state.apply(world); - match concurrent_queue.push(system_state) { - Ok(_) => {} - Err(_) => panic!(), - } - } - }); - })); + system_param_applications.0.insert( + TypeId::of::(), + Box::new(|world: &mut World| { + world.try_resource_scope( + |world, system_param_queue: Mut>| { + for concurrent_queue in system_param_queue.0.read().unwrap().values() { + let mut system_state = match concurrent_queue.pop() { + Ok(val) => val, + Err(_) => panic!(), + }; + system_state.apply(world); + match concurrent_queue.push(system_state) { + Ok(_) => {} + Err(_) => panic!(), + } + } + }, + ); + }), + ); } this } } -#[derive(bevy_ecs_macros::Resource, Clone)] -pub(crate) struct AsyncSystemChannel(Arc>); - -enum MyThing { - FnClosure(Box Box + Send + Sync>), - SystemState(Box), -} - -impl MyThing { - pub fn into_state(mut self, world: &mut World) -> MyThing { - match self { - MyThing::FnClosure(mut f) => { - let state = f(world); - MyThing::SystemState(state) - } - MyThing::SystemState(_) => self, - } - } -} - #[derive(Clone, Copy, Hash, PartialOrd, PartialEq, Eq)] pub struct TaskId(usize); @@ -111,65 +153,47 @@ impl TaskId { pub(crate) struct EcsWakerList( OnceLock< - Mutex<( - HashMap<(WorldId, InternedScheduleLabel), Vec<(std::task::Waker, TaskId)>>, - HashMap, - )>, + KeyedQueues<(WorldId, InternedScheduleLabel), (Waker, fn(&mut World, TaskId), TaskId)>, >, ); impl EcsWakerList { pub fn wait(&self, schedule: InternedScheduleLabel, world: &mut World) -> Option<()> { - if !world.contains_resource::() { - world.insert_resource(AsyncSystemChannel(Arc::new(RwLock::new(TypeMap( - HashMap::new(), - ))))); - } - let this = self - .0 - .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))); let world_id = world.id(); - // We intentionally do not hold this lock the whole time because we are emptying the vec, it's gonna be all new wakers next time. - let mut waker_list = this.try_lock().ok()?.0.remove(&(world_id, schedule))?; + let mut waker_list = std::vec![]; + while let Ok((waker, system_init, task_id)) = ASYNC_ECS_WAKER_LIST + .0 + .get_or_init(|| KeyedQueues::new()) + .get_or_create(&(world_id, schedule)) + .pop() + { + // It's okay to call this every time, because it only *actually* inits the system if the task id is new + system_init(world, task_id); + waker_list.push(waker); + } let waker_list_len = waker_list.len(); + if waker_list_len == 0 { + return None; + } let (tx, rx) = std::sync::mpsc::channel(); world.insert_resource(AsyncBarrier( thread::current(), Arc::new(AtomicI64::new(waker_list_len as i64 - 1)), tx, )); - for (_, task_id) in waker_list.iter() { - let mut uwu = this.lock().unwrap(); - let task = uwu.1.remove(task_id).unwrap(); - uwu.1.insert(*task_id, task.into_state(world)); - drop(uwu); - } if let None = ASYNC_ECS_WORLD_ACCESS.set(world, || { - for (waker, _) in waker_list { + for waker in waker_list { waker.wake(); } - if waker_list_len != 0 { - //std::println!("thread is parking"); - thread::park(); - //std::println!("thread is unparked"); - } else { - panic!("AWA"); - } + thread::park(); }) { return None; } - world.try_resource_scope(|world, mut system_param_applications: Mut| { - system_param_applications.run(world); - }); - /*let mut t = world - .get_resource::() - .unwrap() - .clone() - .0; - t.write().unwrap().run_all(world);*/ - /*for thing in rx.try_iter() { - thing(world); - }*/ + world.try_resource_scope( + |world, mut system_param_applications: Mut| { + system_param_applications.run(world); + }, + ); Some(()) } } @@ -270,35 +294,16 @@ impl TypeMap { } } -/*pub async fn async_access( - world_id: WorldId, - schedule: impl ScheduleLabel, - ecs_access: Func, -) -> Result -where - P: SystemParam + 'static, - for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, -{ - SystemParamThing::( - PhantomData::

, - PhantomData, - Some(ecs_access), - (world_id, schedule.intern()), - TaskId::new().unwrap(), - ) - .await -}*/ - pub fn async_access( - task_identifier: impl BecomeTaskIdentifier

, + task_identifier: impl Into>, schedule: impl ScheduleLabel, ecs_access: Func, -) -> impl Future> +) -> impl Future> where P: SystemParam + 'static, for<'w, 's> Func: Clone + FnMut(P::Item<'w, 's>) -> Out, { - let task_identifier = task_identifier.become_task_identifier(); + let task_identifier = task_identifier.into(); SystemParamThing::( PhantomData::

, PhantomData, @@ -308,21 +313,6 @@ where ) } -pub trait BecomeTaskIdentifier { - fn become_task_identifier(&self) -> TaskIdentifier; -} -impl BecomeTaskIdentifier for WorldId { - fn become_task_identifier(&self) -> TaskIdentifier { - TaskIdentifier::new(*self) - } -} - -impl BecomeTaskIdentifier for &TaskIdentifier { - fn become_task_identifier(&self) -> TaskIdentifier { - TaskIdentifier(self.0, self.1, PhantomData) - } -} - impl From for TaskIdentifier { fn from(value: WorldId) -> Self { TaskIdentifier::new(value) @@ -362,15 +352,41 @@ where type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + fn system_state_init(world: &mut World, task_id: TaskId) { + world.init_resource::>(); + if !world + .get_resource::>() + .unwrap() + .0 + .read() + .unwrap() + .contains_key(&task_id) + { + let system_state = SystemState::

::new(world); + let cq = ConcurrentQueue::bounded(1); + match cq.push(system_state) { + Ok(_) => {} + Err(_) => { + panic!() + } + } + world + .get_resource::>() + .unwrap() + .0 + .write() + .unwrap() + .insert(task_id, cq); + } + } + let task_id = self.4; unsafe { match ASYNC_ECS_WORLD_ACCESS.get(|world: UnsafeWorldCell| { struct RaiiThing(AsyncBarrier); impl Drop for RaiiThing { fn drop(&mut self) { - //std::println!("ready to drop uwu"); let val = self.0 .1.fetch_add(-1, Ordering::SeqCst); - //std::println!("counter is: {val}"); if val == 0 { self.0 .0.unpark(); } @@ -379,37 +395,16 @@ where let async_barrier = { world.get_resource::().unwrap().clone() }; RaiiThing(async_barrier.clone()); - let system_param_queue = match world.get_resource::>() { - None => { - return Poll::Pending - } + None => return Poll::Pending, Some(system_param_queue) => system_param_queue, }; let mut system_state = match system_param_queue.0.read().unwrap().get(&task_id) { None => return Poll::Pending, - Some(cq) => { - cq.pop().unwrap() - } + Some(cq) => cq.pop().unwrap(), }; let out; - //std::println!("B: {}", our_thing); - /*let mut hashmap = ASYNC_ECS_WAKER_LIST - .0 - .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) - .lock() - .unwrap(); - let Some(awa) = hashmap.1.remove(&task_id) else { - return Poll::Pending; - }; - drop(hashmap); - let mut uwu = match awa { - MyThing::FnClosure(_) => panic!(), - MyThing::SystemState(state) => *state.downcast::>().unwrap(), - }; - let mut system_state = uwu;*/ - //let mut system_state = SystemState::

::new(world.world_mut()); // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. unsafe { // Obtain params and immediately consume them with the closure, @@ -417,101 +412,46 @@ where if let Err(err) = SystemState::validate_param(&mut system_state, world) { return Poll::Ready(Err(err.into())); } - //std::println!("C: {}", our_thing); let state = system_state.get_unchecked(world); - //std::println!("D: {}", our_thing); out = self.as_mut().2.take().unwrap()(state); - //std::println!("E: {}", our_thing); } - //system_state.apply(world.world_mut()); - //std::println!("F: {}", our_thing); - /*if !async_system_channel.0.read().unwrap().has(&system_state) { - async_system_channel.0.write().unwrap().set(&system_state); - } - async_system_channel.0.read().unwrap().send(system_state);*/ - match world.get_resource::>().unwrap().0.read().unwrap().get(&task_id).unwrap().push(system_state) { + match world + .get_resource::>() + .unwrap() + .0 + .read() + .unwrap() + .get(&task_id) + .unwrap() + .push(system_state) + { Ok(_) => {} Err(_) => panic!(), } - /*if let Err(err) = async_barrier.2.send(Box::new(move |world: &mut World| { - system_state.apply(world); - })) { - return Poll::Ready(Err(err.into())); - }*/ Poll::Ready(Ok(out)) }) { Some(Poll::Pending) => { - let mut hashmap = ASYNC_ECS_WAKER_LIST - .0 - .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) - .lock() - .unwrap(); - if !hashmap.0.contains_key(&self.3) { - hashmap.0.insert(self.3.clone(), Vec::new()); - } - hashmap - .0 - .get_mut(&self.3) - .unwrap() - .push((cx.waker().clone(), self.4)); - if !hashmap.1.contains_key(&self.4) { - hashmap.1.insert( - self.4, - MyThing::FnClosure(Box::new( - move |world: &mut World| -> Box { - world.init_resource::>(); - if !world.get_resource::>().unwrap().0.read().unwrap().contains_key(&task_id) { - let system_state = SystemState::

::new(world); - let cq = ConcurrentQueue::bounded(1); - match cq.push(system_state) { - Ok(_) => {} - Err(_) => { - panic!() - } - } - world.get_resource::>().unwrap().0.write().unwrap().insert(task_id, cq); - } - Box::new(SystemState::

::new(world)) - }, - )), - ); + match ASYNC_ECS_WAKER_LIST.0 + .get_or_init(|| keyed_queues::KeyedQueues::new()) + .try_send( + &self.3, + (cx.waker().clone(), system_state_init::

, task_id), + ) { + Ok(_) => {} + Err(_) => panic!(), } Poll::Pending } None => { - let mut hashmap = ASYNC_ECS_WAKER_LIST - .0 - .get_or_init(|| Mutex::new((HashMap::new(), HashMap::new()))) - .lock() - .unwrap(); - if !hashmap.0.contains_key(&self.3) { - hashmap.0.insert(self.3.clone(), Vec::new()); + match ASYNC_ECS_WAKER_LIST.0 + .get_or_init(|| keyed_queues::KeyedQueues::new()) + .try_send( + &self.3, + (cx.waker().clone(), system_state_init::

, task_id), + ) { + Ok(_) => {} + Err(_) => panic!(), } - hashmap - .0 - .get_mut(&self.3) - .unwrap() - .push((cx.waker().clone(), self.4)); - hashmap.1.insert( - self.4, - MyThing::FnClosure(Box::new( - move |world: &mut World| -> Box { - world.init_resource::>(); - if !world.get_resource::>().unwrap().0.read().unwrap().contains_key(&task_id) { - let system_state = SystemState::

::new(world); - let cq = ConcurrentQueue::bounded(1); - match cq.push(system_state) { - Ok(_) => {} - Err(_) => { - panic!() - } - }; - world.get_resource::>().unwrap().0.write().unwrap().insert(task_id, cq); - } - Box::new(SystemState::

::new(world)) - }, - )), - ); Poll::Pending } Some(awa) => awa, diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index 608e7744ed195..283779cecf7ea 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -11,10 +11,10 @@ use bevy::{ prelude::*, tasks::AsyncComputeTaskPool, }; +use bevy_ecs::schedule::r#async::TaskIdentifier; use futures_timer::Delay; use rand::Rng; use std::time::Duration; -use bevy_ecs::schedule::r#async::TaskIdentifier; const NUM_CUBES: i32 = 16; const LIGHT_RADIUS: f32 = 8.0; @@ -43,27 +43,34 @@ fn spawn_tasks(world_id: WorldId) { for x in -NUM_CUBES..NUM_CUBES { for z in -NUM_CUBES..NUM_CUBES { // Spawn a task on the async compute pool - let task_id = task_id.clone(); pool.spawn(async move { let delay = Duration::from_secs_f32(rand::rng().random_range(2.0..8.0)); // Simulate a delay before task completion println!("delaying for {:?}", delay); Delay::new(delay).await; - if let Err(e) = - async_access::<(Local, Commands, Res, Res), _, _>( - &task_id, - Update, - |(mut local, mut commands, box_mesh, box_material)| { - *local += 1; - println!("spawning {}", *local); - commands.spawn(( - Mesh3d(box_mesh.clone()), - MeshMaterial3d(box_material.clone()), - Transform::from_xyz(x as f32, 0.5, z as f32), - )); - }, - ) - .await + if let Err(e) = async_access::< + ( + Local, + Commands, + Res, + Res, + ), + _, + _, + >( + task_id, + Update, + |(mut local, mut commands, box_mesh, box_material)| { + *local += 1; + println!("spawning {}", *local); + commands.spawn(( + Mesh3d(box_mesh.clone()), + MeshMaterial3d(box_material.clone()), + Transform::from_xyz(x as f32, 0.5, z as f32), + )); + }, + ) + .await { println!("got error: {}", e); } From 19f4c848d5a4e402206b15e1f2726f7fa1bdb018 Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 18:20:09 -0500 Subject: [PATCH 06/32] uwu --- .../bevy_ecs/src/schedule/executor/async.rs | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 47ac9347c121c..c478a40f5c3b2 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -74,11 +74,7 @@ pub(crate) static ASYNC_ECS_WORLD_ACCESS: LockWrapper = LockWrapper(OnceLock::ne pub(crate) static ASYNC_ECS_WAKER_LIST: EcsWakerList = EcsWakerList(OnceLock::new()); #[derive(bevy_ecs_macros::Resource, Clone)] -pub(crate) struct AsyncBarrier( - thread::Thread, - Arc, - std::sync::mpsc::Sender>, -); +pub(crate) struct AsyncBarrier(thread::Thread, Arc); #[derive(bevy_ecs_macros::Resource)] pub(crate) struct SystemParamQueue( @@ -129,7 +125,7 @@ impl FromWorld for SystemParamQueue { } #[derive(Clone, Copy, Hash, PartialOrd, PartialEq, Eq)] -pub struct TaskId(usize); +struct TaskId(usize); /// The next [`TaskId`]. static MAX_TASK_ID: AtomicUsize = AtomicUsize::new(0); @@ -175,11 +171,9 @@ impl EcsWakerList { if waker_list_len == 0 { return None; } - let (tx, rx) = std::sync::mpsc::channel(); world.insert_resource(AsyncBarrier( thread::current(), Arc::new(AtomicI64::new(waker_list_len as i64 - 1)), - tx, )); if let None = ASYNC_ECS_WORLD_ACCESS.set(world, || { for waker in waker_list { @@ -430,21 +424,11 @@ where } Poll::Ready(Ok(out)) }) { - Some(Poll::Pending) => { - match ASYNC_ECS_WAKER_LIST.0 - .get_or_init(|| keyed_queues::KeyedQueues::new()) - .try_send( - &self.3, - (cx.waker().clone(), system_state_init::

, task_id), - ) { - Ok(_) => {} - Err(_) => panic!(), - } - Poll::Pending - } - None => { - match ASYNC_ECS_WAKER_LIST.0 - .get_or_init(|| keyed_queues::KeyedQueues::new()) + Some(awa) => awa, + _ => { + match ASYNC_ECS_WAKER_LIST + .0 + .get_or_init(|| KeyedQueues::new()) .try_send( &self.3, (cx.waker().clone(), system_state_init::

, task_id), @@ -454,7 +438,6 @@ where } Poll::Pending } - Some(awa) => awa, } } } From 4183331e8c820e75144ad29f59f4c3327be7ba77 Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 19:07:30 -0500 Subject: [PATCH 07/32] i have COOKED --- .../bevy_ecs/src/schedule/executor/async.rs | 149 ++++++++---------- 1 file changed, 66 insertions(+), 83 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index c478a40f5c3b2..4111499b9370d 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -69,7 +69,7 @@ mod keyed_queues { } } -pub(crate) static ASYNC_ECS_WORLD_ACCESS: LockWrapper = LockWrapper(OnceLock::new()); +pub(crate) static ASYNC_ECS_WORLD_ACCESS: AsyncWorldHolder = AsyncWorldHolder(OnceLock::new()); pub(crate) static ASYNC_ECS_WAKER_LIST: EcsWakerList = EcsWakerList(OnceLock::new()); @@ -192,99 +192,93 @@ impl EcsWakerList { } } -pub(crate) struct LockWrapper(OnceLock>>>>); +/// The PhantomData here is just there cause it's a cute way of showing that we have a mutex around our unsafe worldcell and that's what the mutex is 'locking' +/// +pub(crate) struct AsyncWorldHolder( + OnceLock< + RwLock< + HashMap< + WorldId, + RwLock< + Option<( + UnsafeWorldCell<'static>, + Mutex>>, + )>, + >, + >, + >, + >, +); -impl LockWrapper { +impl AsyncWorldHolder { pub(crate) fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()> { - // local RAII type - struct ClearOnDrop<'a> { - slot: &'a Mutex>>, + let this = self.0.get_or_init(|| RwLock::new(HashMap::new())); + let world_id = world.id(); + if !this.read().unwrap().contains_key(&world_id) { + // VERY rare only happens the first time we try to do anything async in a new World + let _ = this.write().unwrap().insert(world_id, RwLock::new(None)); } + struct ClearOnDrop<'a> { + slot: &'a RwLock< + Option<( + UnsafeWorldCell<'static>, + Mutex>>, + )>, + >, + } impl<'a> Drop for ClearOnDrop<'a> { fn drop(&mut self) { // clear it on the way out, even on panic - self.slot.lock().unwrap().take(); + self.slot.write().unwrap().take(); } } - unsafe { - let mut awa = self - .0 - .get_or_init(|| Arc::new(Mutex::new(None))) - .try_lock() - .ok()?; - // this guard lives until the end of the function + let binding = this.read().unwrap(); + let world_container = binding.get(&world_id).unwrap(); + // SAFETY this is required in order to make sure that even in the event of a panic, this can't get accessed let _clear = ClearOnDrop { - slot: self.0.get().unwrap(), + slot: world_container, }; // SAFETY: This mem transmute is safe only because we drop it after, and our ASYNC_ECS_WORLD_ACCESS is private, and we don't clone it // where we do use it, so the lifetime doesn't get propagated anywhere. - awa.replace(std::mem::transmute(world.as_unsafe_world_cell())); - drop(awa); + // Lifetimes are not used in any actual code optimization, so turning it into a static does not violate any of rust's rules + // As *LONG* as we keep it within it's lifetime, which we do here, manually, with our `ClearOnDrop` struct. + world_container.write().unwrap().replace(( + std::mem::transmute(world.as_unsafe_world_cell()), + Mutex::new(PhantomData), + )); func() } Some(()) } pub(crate) unsafe fn get( &self, + world_id: WorldId, func: impl FnOnce(UnsafeWorldCell) -> Poll, ) -> Option> { - let mut uwu = self.0.get()?.lock().ok()?; - if let Some(inner) = uwu.clone() { - // SAFETY: this is safe because we ensure no one else has access to the world. - let out; - unsafe { - out = func(inner); + // it's okay to *not* do the RaiiThing on these early returns, because that means we aren't in a state + // where a thread is parked because of our world. + let a = self.0.get()?.read().unwrap(); + let mut b = a.get(&world_id)?.read().unwrap(); + let Some(our_thing) = b.as_ref() else { + return None; + }; + struct RaiiThing(AsyncBarrier); + impl Drop for RaiiThing { + fn drop(&mut self) { + let val = self.0 .1.fetch_add(-1, Ordering::SeqCst); + if val == 0 { + self.0 .0.unpark(); + } } - return Some(out); - } - None - } -} - -struct TypeMap( - HashMap< - TypeId, - ( - Box, - Mutex>, - ), - >, -); -impl TypeMap { - pub fn set(&mut self, _t: &SystemState) { - let (tx, rx) = std::sync::mpsc::channel::>(); - - self.0.insert( - TypeId::of::(), - ( - Box::new(tx), - Mutex::new(Box::new(move |world: &mut World| { - for mut thing in rx.try_iter() { - thing.apply(world); - } - })), - ), - ); - } - pub fn has(&self, _t: &SystemState) -> bool { - self.0.contains_key(&TypeId::of::()) - } - pub fn send(&self, t: SystemState) -> Option<()> { - self.0 - .get(&TypeId::of::())? - .0 - .downcast_ref::>>() - .unwrap() - .send(t) - .ok()?; - Some(()) - } - pub fn run_all(&mut self, world: &mut World) { - for (_, closure) in self.0.values_mut() { - closure.lock().unwrap()(world); } + let async_barrier = { our_thing.0.get_resource::().unwrap().clone() }; + RaiiThing(async_barrier.clone()); + // this allows us to effectively yield as if pending if the world doesn't exist rn. + let _world = our_thing.1.try_lock().ok()?; + // SAFETY: this is safe because we ensure no one else has access to the world. + unsafe { Some(func(our_thing.0)) } } } @@ -375,20 +369,9 @@ where } let task_id = self.4; + let world_id = self.3 .0; unsafe { - match ASYNC_ECS_WORLD_ACCESS.get(|world: UnsafeWorldCell| { - struct RaiiThing(AsyncBarrier); - impl Drop for RaiiThing { - fn drop(&mut self) { - let val = self.0 .1.fetch_add(-1, Ordering::SeqCst); - if val == 0 { - self.0 .0.unpark(); - } - } - } - let async_barrier = { world.get_resource::().unwrap().clone() }; - RaiiThing(async_barrier.clone()); - + match ASYNC_ECS_WORLD_ACCESS.get(world_id, |world: UnsafeWorldCell| { let system_param_queue = match world.get_resource::>() { None => return Poll::Pending, Some(system_param_queue) => system_param_queue, From 21af8c68c0fb27b40d2e26c36bd440314a79bcce Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 19:25:25 -0500 Subject: [PATCH 08/32] i have COOKED --- .../bevy_ecs/src/schedule/executor/async.rs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 4111499b9370d..144c72eddaa14 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -1,6 +1,6 @@ use crate::schedule::r#async::keyed_queues::KeyedQueues; use crate::schedule::{InternedScheduleLabel, ScheduleLabel}; -use crate::system::{RunSystemError, SystemMeta}; +use crate::system::RunSystemError; use crate::world::unsafe_world_cell::UnsafeWorldCell; use crate::world::FromWorld; use crate::{ @@ -9,14 +9,13 @@ use crate::{ }; use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; +use bevy_platform::sync::{Arc, Mutex, OnceLock, RwLock}; use concurrent_queue::ConcurrentQueue; -use std::any::{Any, TypeId}; -use std::marker::PhantomData; -use std::pin::Pin; -use std::prelude::v1::{Box, Vec}; -use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, OnceLock, RwLock}; -use std::task::{Context, Poll, Waker}; +use core::any::{Any, TypeId}; +use core::marker::PhantomData; +use core::pin::Pin; +use core::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; +use core::task::{Context, Poll, Waker}; use std::thread; mod keyed_queues { @@ -82,9 +81,7 @@ pub(crate) struct SystemParamQueue( ); #[derive(bevy_ecs_macros::Resource, Default)] -pub(crate) struct SystemParamApplications( - HashMap>, -); +pub(crate) struct SystemParamApplications(HashMap); impl SystemParamApplications { fn run(&mut self, world: &mut World) { for closure in self.0.values_mut() { @@ -99,9 +96,9 @@ impl FromWorld for SystemParamQueue { let mut system_param_applications = world.get_resource_mut::().unwrap(); if !system_param_applications.0.contains_key(&TypeId::of::()) { - system_param_applications.0.insert( - TypeId::of::(), - Box::new(|world: &mut World| { + system_param_applications + .0 + .insert(TypeId::of::(), |world: &mut World| { world.try_resource_scope( |world, system_param_queue: Mut>| { for concurrent_queue in system_param_queue.0.read().unwrap().values() { @@ -117,8 +114,7 @@ impl FromWorld for SystemParamQueue { } }, ); - }), - ); + }); } this } @@ -278,7 +274,7 @@ impl AsyncWorldHolder { // this allows us to effectively yield as if pending if the world doesn't exist rn. let _world = our_thing.1.try_lock().ok()?; // SAFETY: this is safe because we ensure no one else has access to the world. - unsafe { Some(func(our_thing.0)) } + Some(func(our_thing.0)) } } From ba27d742abea0e43a84034414cb2025ba23bd4ff Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 19:27:43 -0500 Subject: [PATCH 09/32] i have COOKED --- crates/bevy_ecs/src/schedule/schedule.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 683465cd515a8..c402194b1eed0 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -535,7 +535,7 @@ impl Schedule { }); let error_handler = world.default_error_handler(); - let _ = r#async::ASYNC_ECS_WAKER_LIST.wait(self.label, world); + while let Some(()) = r#async::ASYNC_ECS_WAKER_LIST.wait(self.label, world) {} #[cfg(not(feature = "bevy_debug_stepping"))] self.executor .run(&mut self.executable, world, None, error_handler); From adaaf89b205fc87ccfa39ab4aff825dff1929a9a Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 19:32:27 -0500 Subject: [PATCH 10/32] fixed all the issues with the linter --- crates/bevy_ecs/src/schedule/executor/async.rs | 13 +++++++++++-- crates/bevy_ecs/src/schedule/executor/mod.rs | 1 + crates/bevy_ecs/src/schedule/mod.rs | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 144c72eddaa14..493e3e04534a1 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -11,7 +11,7 @@ use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; use bevy_platform::sync::{Arc, Mutex, OnceLock, RwLock}; use concurrent_queue::ConcurrentQueue; -use core::any::{Any, TypeId}; +use core::any::{TypeId}; use core::marker::PhantomData; use core::pin::Pin; use core::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; @@ -256,7 +256,7 @@ impl AsyncWorldHolder { // it's okay to *not* do the RaiiThing on these early returns, because that means we aren't in a state // where a thread is parked because of our world. let a = self.0.get()?.read().unwrap(); - let mut b = a.get(&world_id)?.read().unwrap(); + let b = a.get(&world_id)?.read().unwrap(); let Some(our_thing) = b.as_ref() else { return None; }; @@ -278,6 +278,10 @@ impl AsyncWorldHolder { } } +/// Allows you to access the ECS from any arbitrary async runtime. +/// Calls will never return immediately and will always start Pending at least once. +/// Call this with the same `TaskIdentifier` to persist SystemParams like Local or Changed +/// Just use `world_id` if you do not mind a new SystemParam being initialized every time. pub fn async_access( task_identifier: impl Into>, schedule: impl ScheduleLabel, @@ -303,6 +307,7 @@ impl From for TaskIdentifier { } } +/// A TaskIdentifier can be re-used in order to persist SystemParams like Local, Changed, or Added pub struct TaskIdentifier(TaskId, WorldId, PhantomData); impl Clone for TaskIdentifier { @@ -313,6 +318,9 @@ impl Clone for TaskIdentifier { impl Copy for TaskIdentifier {} impl TaskIdentifier { + + /// Generates a new unique TaskIdentifier that can be re-used in order to persist SystemParams + /// like Local, Changed, or Added pub fn new(world_id: WorldId) -> Self { Self(TaskId::new().unwrap(), world_id, PhantomData) } @@ -379,6 +387,7 @@ where }; let out; // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. + #[allow(unused_unsafe)] unsafe { // Obtain params and immediately consume them with the closure, // ensuring the borrow ends before `apply`. diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index faff30895ac86..b8955711b2c61 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -1,3 +1,4 @@ +/// Async ECS access, using the ECS from Async Tasks pub mod r#async; #[cfg(feature = "std")] mod multi_threaded; diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index e2cd16f3e0e6f..7016acb9e07e3 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -4,6 +4,8 @@ mod auto_insert_apply_deferred; mod condition; mod config; mod error; + +/// Bevy executor details pub mod executor; mod node; mod pass; From e88db61bac3b6609e351505a96678e2de88f059b Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 19:49:11 -0500 Subject: [PATCH 11/32] finally done and finished with the first draft of the PR --- crates/bevy_ecs/src/lib.rs | 6 +- .../bevy_ecs/src/schedule/executor/async.rs | 174 +++++++++--------- crates/bevy_ecs/src/schedule/schedule.rs | 2 +- examples/ecs/async_ecs.rs | 3 +- 4 files changed, 93 insertions(+), 92 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 3574a95671699..104ca8b284403 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -93,8 +93,10 @@ pub mod prelude { relationship::RelationshipTarget, resource::Resource, schedule::{ - common_conditions::*, executor::r#async::async_access, ApplyDeferred, - IntoScheduleConfigs, IntoSystemSet, Schedule, Schedules, SystemCondition, SystemSet, + common_conditions::*, + executor::r#async::{async_access, EcsTask}, + ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, Schedules, + SystemCondition, SystemSet, }, spawn::{Spawn, SpawnIter, SpawnRelated, SpawnWith, WithOneRelated, WithRelated}, system::{ diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 493e3e04534a1..dee0bacb57e0e 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -11,7 +11,7 @@ use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; use bevy_platform::sync::{Arc, Mutex, OnceLock, RwLock}; use concurrent_queue::ConcurrentQueue; -use core::any::{TypeId}; +use core::any::TypeId; use core::marker::PhantomData; use core::pin::Pin; use core::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; @@ -68,92 +68,90 @@ mod keyed_queues { } } -pub(crate) static ASYNC_ECS_WORLD_ACCESS: AsyncWorldHolder = AsyncWorldHolder(OnceLock::new()); +pub(crate) static GLOBAL_WORLD_ACCESS: WorldAccessRegistry = WorldAccessRegistry(OnceLock::new()); -pub(crate) static ASYNC_ECS_WAKER_LIST: EcsWakerList = EcsWakerList(OnceLock::new()); +pub(crate) static GLOBAL_WAKE_REGISTRY: WakeRegistry = WakeRegistry(OnceLock::new()); #[derive(bevy_ecs_macros::Resource, Clone)] -pub(crate) struct AsyncBarrier(thread::Thread, Arc); +pub(crate) struct WakeParkBarrier(thread::Thread, Arc); #[derive(bevy_ecs_macros::Resource)] -pub(crate) struct SystemParamQueue( - RwLock>>>, +pub(crate) struct SystemStatePool( + RwLock>>>, ); #[derive(bevy_ecs_macros::Resource, Default)] -pub(crate) struct SystemParamApplications(HashMap); -impl SystemParamApplications { +pub(crate) struct SystemParamAppliers(HashMap); +impl SystemParamAppliers { fn run(&mut self, world: &mut World) { for closure in self.0.values_mut() { closure(world); } } } -impl FromWorld for SystemParamQueue { +impl FromWorld for SystemStatePool { fn from_world(world: &mut World) -> Self { let this = Self(RwLock::new(HashMap::default())); - world.init_resource::(); - let mut system_param_applications = - world.get_resource_mut::().unwrap(); - if !system_param_applications.0.contains_key(&TypeId::of::()) { - system_param_applications - .0 - .insert(TypeId::of::(), |world: &mut World| { - world.try_resource_scope( - |world, system_param_queue: Mut>| { - for concurrent_queue in system_param_queue.0.read().unwrap().values() { - let mut system_state = match concurrent_queue.pop() { - Ok(val) => val, - Err(_) => panic!(), - }; - system_state.apply(world); - match concurrent_queue.push(system_state) { - Ok(_) => {} - Err(_) => panic!(), - } - } - }, - ); + world.init_resource::(); + let mut appliers = world.get_resource_mut::().unwrap(); + if !appliers.0.contains_key(&TypeId::of::()) { + appliers.0.insert(TypeId::of::(), |world: &mut World| { + world.try_resource_scope(|world, param_pool: Mut>| { + for concurrent_queue in param_pool.0.read().unwrap().values() { + let mut system_state = match concurrent_queue.pop() { + Ok(val) => val, + Err(_) => panic!(), + }; + system_state.apply(world); + match concurrent_queue.push(system_state) { + Ok(_) => {} + Err(_) => panic!(), + } + } }); + }); } this } } #[derive(Clone, Copy, Hash, PartialOrd, PartialEq, Eq)] -struct TaskId(usize); +struct AsyncTaskId(usize); -/// The next [`TaskId`]. +/// The next [`AsyncTaskId`]. static MAX_TASK_ID: AtomicUsize = AtomicUsize::new(0); -impl TaskId { - /// Create a new, unique [`TaskId`]. Returns [`None`] if the supply of unique - /// [`TaskId`]s has been exhausted +impl AsyncTaskId { + /// Create a new, unique [`AsyncTaskId`]. Returns [`None`] if the supply of unique + /// IDs has been exhausted. /// - /// Please note that the [`TaskId`]s created from this method are unique across - /// time - if a given [`TaskId`] is [`Drop`]ped its value still cannot be reused + /// Please note that the IDs created from this method are unique across + /// time - if a given ID is [`Drop`]ped its value still cannot be reused pub fn new() -> Option { MAX_TASK_ID // We use `Relaxed` here since this atomic only needs to be consistent with itself .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |val| { val.checked_add(1) }) - .map(TaskId) + .map(AsyncTaskId) .ok() } } -pub(crate) struct EcsWakerList( +pub(crate) struct WakeRegistry( OnceLock< - KeyedQueues<(WorldId, InternedScheduleLabel), (Waker, fn(&mut World, TaskId), TaskId)>, + KeyedQueues< + (WorldId, InternedScheduleLabel), + (Waker, fn(&mut World, AsyncTaskId), AsyncTaskId), + >, >, ); -impl EcsWakerList { +impl WakeRegistry { pub fn wait(&self, schedule: InternedScheduleLabel, world: &mut World) -> Option<()> { let world_id = world.id(); let mut waker_list = std::vec![]; - while let Ok((waker, system_init, task_id)) = ASYNC_ECS_WAKER_LIST + while let Ok((waker, system_init, task_id)) = GLOBAL_WAKE_REGISTRY .0 .get_or_init(|| KeyedQueues::new()) .get_or_create(&(world_id, schedule)) @@ -167,11 +165,11 @@ impl EcsWakerList { if waker_list_len == 0 { return None; } - world.insert_resource(AsyncBarrier( + world.insert_resource(WakeParkBarrier( thread::current(), Arc::new(AtomicI64::new(waker_list_len as i64 - 1)), )); - if let None = ASYNC_ECS_WORLD_ACCESS.set(world, || { + if let None = GLOBAL_WORLD_ACCESS.set(world, || { for waker in waker_list { waker.wake(); } @@ -179,18 +177,15 @@ impl EcsWakerList { }) { return None; } - world.try_resource_scope( - |world, mut system_param_applications: Mut| { - system_param_applications.run(world); - }, - ); + world.try_resource_scope(|world, mut appliers: Mut| { + appliers.run(world); + }); Some(()) } } /// The PhantomData here is just there cause it's a cute way of showing that we have a mutex around our unsafe worldcell and that's what the mutex is 'locking' -/// -pub(crate) struct AsyncWorldHolder( +pub(crate) struct WorldAccessRegistry( OnceLock< RwLock< HashMap< @@ -206,7 +201,7 @@ pub(crate) struct AsyncWorldHolder( >, ); -impl AsyncWorldHolder { +impl WorldAccessRegistry { pub(crate) fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()> { let this = self.0.get_or_init(|| RwLock::new(HashMap::new())); let world_id = world.id(); @@ -215,7 +210,7 @@ impl AsyncWorldHolder { let _ = this.write().unwrap().insert(world_id, RwLock::new(None)); } - struct ClearOnDrop<'a> { + struct ClearOnDropGuard<'a> { slot: &'a RwLock< Option<( UnsafeWorldCell<'static>, @@ -223,7 +218,7 @@ impl AsyncWorldHolder { )>, >, } - impl<'a> Drop for ClearOnDrop<'a> { + impl<'a> Drop for ClearOnDropGuard<'a> { fn drop(&mut self) { // clear it on the way out, even on panic self.slot.write().unwrap().take(); @@ -233,10 +228,10 @@ impl AsyncWorldHolder { let binding = this.read().unwrap(); let world_container = binding.get(&world_id).unwrap(); // SAFETY this is required in order to make sure that even in the event of a panic, this can't get accessed - let _clear = ClearOnDrop { + let _clear = ClearOnDropGuard { slot: world_container, }; - // SAFETY: This mem transmute is safe only because we drop it after, and our ASYNC_ECS_WORLD_ACCESS is private, and we don't clone it + // SAFETY: This mem transmute is safe only because we drop it after, and our GLOBAL_WORLD_ACCESS is private, and we don't clone it // where we do use it, so the lifetime doesn't get propagated anywhere. // Lifetimes are not used in any actual code optimization, so turning it into a static does not violate any of rust's rules // As *LONG* as we keep it within it's lifetime, which we do here, manually, with our `ClearOnDrop` struct. @@ -260,8 +255,8 @@ impl AsyncWorldHolder { let Some(our_thing) = b.as_ref() else { return None; }; - struct RaiiThing(AsyncBarrier); - impl Drop for RaiiThing { + struct UnparkOnDropGuard(WakeParkBarrier); + impl Drop for UnparkOnDropGuard { fn drop(&mut self) { let val = self.0 .1.fetch_add(-1, Ordering::SeqCst); if val == 0 { @@ -269,8 +264,14 @@ impl AsyncWorldHolder { } } } - let async_barrier = { our_thing.0.get_resource::().unwrap().clone() }; - RaiiThing(async_barrier.clone()); + let async_barrier = { + our_thing + .0 + .get_resource::() + .unwrap() + .clone() + }; + UnparkOnDropGuard(async_barrier.clone()); // this allows us to effectively yield as if pending if the world doesn't exist rn. let _world = our_thing.1.try_lock().ok()?; // SAFETY: this is safe because we ensure no one else has access to the world. @@ -280,10 +281,10 @@ impl AsyncWorldHolder { /// Allows you to access the ECS from any arbitrary async runtime. /// Calls will never return immediately and will always start Pending at least once. -/// Call this with the same `TaskIdentifier` to persist SystemParams like Local or Changed +/// Call this with the same `PersistentTask` to persist SystemParams like Local or Changed /// Just use `world_id` if you do not mind a new SystemParam being initialized every time. pub fn async_access( - task_identifier: impl Into>, + task_identifier: impl Into>, schedule: impl ScheduleLabel, ecs_access: Func, ) -> impl Future> @@ -292,7 +293,7 @@ where for<'w, 's> Func: Clone + FnMut(P::Item<'w, 's>) -> Out, { let task_identifier = task_identifier.into(); - SystemParamThing::( + PendingEcsCall::( PhantomData::

, PhantomData, Some(ecs_access), @@ -301,42 +302,41 @@ where ) } -impl From for TaskIdentifier { +impl From for EcsTask { fn from(value: WorldId) -> Self { - TaskIdentifier::new(value) + EcsTask::new(value) } } -/// A TaskIdentifier can be re-used in order to persist SystemParams like Local, Changed, or Added -pub struct TaskIdentifier(TaskId, WorldId, PhantomData); +/// An EcsTask can be re-used in order to persist SystemParams like Local, Changed, or Added +pub struct EcsTask(AsyncTaskId, WorldId, PhantomData); -impl Clone for TaskIdentifier { +impl Clone for EcsTask { fn clone(&self) -> Self { *self } } -impl Copy for TaskIdentifier {} -impl TaskIdentifier { - - /// Generates a new unique TaskIdentifier that can be re-used in order to persist SystemParams +impl Copy for EcsTask {} +impl EcsTask { + /// Generates a new unique PersistentTask that can be re-used in order to persist SystemParams /// like Local, Changed, or Added pub fn new(world_id: WorldId) -> Self { - Self(TaskId::new().unwrap(), world_id, PhantomData) + Self(AsyncTaskId::new().unwrap(), world_id, PhantomData) } } -struct SystemParamThing( +struct PendingEcsCall( PhantomData

, PhantomData, Option, (WorldId, InternedScheduleLabel), - TaskId, + AsyncTaskId, ); -impl Unpin for SystemParamThing {} +impl Unpin for PendingEcsCall {} -impl Future for SystemParamThing +impl Future for PendingEcsCall where P: SystemParam + 'static, for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, @@ -344,10 +344,10 @@ where type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - fn system_state_init(world: &mut World, task_id: TaskId) { - world.init_resource::>(); + fn system_state_init(world: &mut World, task_id: AsyncTaskId) { + world.init_resource::>(); if !world - .get_resource::>() + .get_resource::>() .unwrap() .0 .read() @@ -363,7 +363,7 @@ where } } world - .get_resource::>() + .get_resource::>() .unwrap() .0 .write() @@ -375,8 +375,8 @@ where let task_id = self.4; let world_id = self.3 .0; unsafe { - match ASYNC_ECS_WORLD_ACCESS.get(world_id, |world: UnsafeWorldCell| { - let system_param_queue = match world.get_resource::>() { + match GLOBAL_WORLD_ACCESS.get(world_id, |world: UnsafeWorldCell| { + let system_param_queue = match world.get_resource::>() { None => return Poll::Pending, Some(system_param_queue) => system_param_queue, }; @@ -387,7 +387,7 @@ where }; let out; // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. - #[allow(unused_unsafe)] + #[expect(unused_unsafe)] unsafe { // Obtain params and immediately consume them with the closure, // ensuring the borrow ends before `apply`. @@ -398,7 +398,7 @@ where out = self.as_mut().2.take().unwrap()(state); } match world - .get_resource::>() + .get_resource::>() .unwrap() .0 .read() @@ -414,7 +414,7 @@ where }) { Some(awa) => awa, _ => { - match ASYNC_ECS_WAKER_LIST + match GLOBAL_WAKE_REGISTRY .0 .get_or_init(|| KeyedQueues::new()) .try_send( diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index c402194b1eed0..9a487e512e2b0 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -535,7 +535,7 @@ impl Schedule { }); let error_handler = world.default_error_handler(); - while let Some(()) = r#async::ASYNC_ECS_WAKER_LIST.wait(self.label, world) {} + while let Some(()) = r#async::GLOBAL_WAKE_REGISTRY.wait(self.label, world) {} #[cfg(not(feature = "bevy_debug_stepping"))] self.executor .run(&mut self.executable, world, None, error_handler); diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index 283779cecf7ea..655233a19f9d0 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -11,7 +11,6 @@ use bevy::{ prelude::*, tasks::AsyncComputeTaskPool, }; -use bevy_ecs::schedule::r#async::TaskIdentifier; use futures_timer::Delay; use rand::Rng; use std::time::Duration; @@ -39,7 +38,7 @@ fn main() { /// In this example, we don't implement task tracking or proper error handling. fn spawn_tasks(world_id: WorldId) { let pool = AsyncComputeTaskPool::get(); - let task_id = TaskIdentifier::new(world_id); + let task_id = EcsTask::new(world_id); for x in -NUM_CUBES..NUM_CUBES { for z in -NUM_CUBES..NUM_CUBES { // Spawn a task on the async compute pool From 3c937b1d4326331dd16f9af4ab6f31fc98409a0d Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 20:09:40 -0500 Subject: [PATCH 12/32] fixed lack of NonSend checking --- crates/bevy_ecs/src/schedule/executor/async.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index dee0bacb57e0e..1d536c1881ba6 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -1,12 +1,13 @@ use crate::schedule::r#async::keyed_queues::KeyedQueues; use crate::schedule::{InternedScheduleLabel, ScheduleLabel}; -use crate::system::RunSystemError; +use crate::system::{RunSystemError, SystemParamValidationError}; use crate::world::unsafe_world_cell::UnsafeWorldCell; use crate::world::FromWorld; use crate::{ system::{SystemParam, SystemState}, world::World, }; +use bevy_ecs::prelude::NonSend; use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; use bevy_platform::sync::{Arc, Mutex, OnceLock, RwLock}; @@ -394,6 +395,14 @@ where if let Err(err) = SystemState::validate_param(&mut system_state, world) { return Poll::Ready(Err(err.into())); } + if !system_state.meta().is_send() { + return Poll::Ready(Err( + SystemParamValidationError::invalid::>( + "Cannot have your system be non-send / exclusive", + ) + .into(), + )); + } let state = system_state.get_unchecked(world); out = self.as_mut().2.take().unwrap()(state); } From dc4497639aaf8b5c8f62e702a4b00b4749206a62 Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 20:46:05 -0500 Subject: [PATCH 13/32] added directly using WorldId example --- examples/ecs/async_ecs.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index 655233a19f9d0..b7983436f0423 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -73,6 +73,13 @@ fn spawn_tasks(world_id: WorldId) { { println!("got error: {}", e); } + if let Err(e) = async_access::<(), _, _>(world_id, PreUpdate, |()| { + println!("In PreUpdate"); + }) + .await + { + println!("{}", e); + } }) .detach(); } From 552777243910738b6716ada76c289c7086bc6b9e Mon Sep 17 00:00:00 2001 From: Malek Date: Tue, 4 Nov 2025 20:53:02 -0500 Subject: [PATCH 14/32] change from atomicusize to atomicu64 --- crates/bevy_ecs/src/schedule/executor/async.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 1d536c1881ba6..83e72b842decd 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -15,7 +15,7 @@ use concurrent_queue::ConcurrentQueue; use core::any::TypeId; use core::marker::PhantomData; use core::pin::Pin; -use core::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; +use core::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use core::task::{Context, Poll, Waker}; use std::thread; @@ -120,7 +120,7 @@ impl FromWorld for SystemStatePool { struct AsyncTaskId(usize); /// The next [`AsyncTaskId`]. -static MAX_TASK_ID: AtomicUsize = AtomicUsize::new(0); +static MAX_TASK_ID: AtomicU64 = AtomicU64::new(0); impl AsyncTaskId { /// Create a new, unique [`AsyncTaskId`]. Returns [`None`] if the supply of unique From 3650286cda76a27da9e4e42b8fed347e62dd8265 Mon Sep 17 00:00:00 2001 From: Malek Date: Thu, 6 Nov 2025 17:20:06 -0500 Subject: [PATCH 15/32] added docs and made some tweaks --- .../bevy_ecs/src/schedule/executor/async.rs | 165 ++++++++++++------ examples/ecs/async_ecs.rs | 5 + 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async.rs index 83e72b842decd..31b70c0c7f27b 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async.rs @@ -19,6 +19,12 @@ use core::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use core::task::{Context, Poll, Waker}; use std::thread; +/// Keyed queues is a combination of a hashmap and a concurrent queue which is useful because it +/// allows for non-blocking keyed queues. +/// We want every World's async machinery to be as independent as possible, and this allows us +/// to key our Queues on `(WorldId, Schedule)` so that there is 0 contention on the fast path and +/// arbitrary N number of worlds running in parallel on the same process do not interfere at all +/// except the very first time a new world initializes it's key. mod keyed_queues { use concurrent_queue::ConcurrentQueue; use std::sync::Arc; @@ -69,18 +75,29 @@ mod keyed_queues { } } -pub(crate) static GLOBAL_WORLD_ACCESS: WorldAccessRegistry = WorldAccessRegistry(OnceLock::new()); +/// This is an abstraction that temporarily and soundly stores the UnsafeWorldCell in a static so we can access +/// it from any async task, runtime, and thread. +static GLOBAL_WORLD_ACCESS: WorldAccessRegistry = WorldAccessRegistry(OnceLock::new()); + +/// The entrypoint, stores Wakers from async_access's that wish to be polled with world access +/// also stores the generic function pointer to the concrete function that initializes the +/// system state for any set of SystemParams pub(crate) static GLOBAL_WAKE_REGISTRY: WakeRegistry = WakeRegistry(OnceLock::new()); +/// Acts as a barrier that is waited on in the `wait` call, and once the AtomicI64 reaches 0 the +/// thread that `wait` was called on gets woken up and resumes. #[derive(bevy_ecs_macros::Resource, Clone)] pub(crate) struct WakeParkBarrier(thread::Thread, Arc); +/// Stores the previous system state per task id which allows `Local`, `Changed` and other filters +/// that depend on persistent state to work. #[derive(bevy_ecs_macros::Resource)] pub(crate) struct SystemStatePool( RwLock>>>, ); +/// Function pointer to a concrete version of a genericized system state being applied to the world. #[derive(bevy_ecs_macros::Resource, Default)] pub(crate) struct SystemParamAppliers(HashMap); impl SystemParamAppliers { @@ -116,8 +133,10 @@ impl FromWorld for SystemStatePool { } } +/// A monotonically increasing global identifier for any particular async task. +/// Is an internal implementation detail and thus not generally accessible #[derive(Clone, Copy, Hash, PartialOrd, PartialEq, Eq)] -struct AsyncTaskId(usize); +struct AsyncTaskId(u64); /// The next [`AsyncTaskId`]. static MAX_TASK_ID: AtomicU64 = AtomicU64::new(0); @@ -139,6 +158,7 @@ impl AsyncTaskId { } } +/// Is the GLOBAL_WAKE_REGISTRY pub(crate) struct WakeRegistry( OnceLock< KeyedQueues< @@ -149,9 +169,17 @@ pub(crate) struct WakeRegistry( ); impl WakeRegistry { + /// This function finds all pending `async_access` calls for a particular `Schedule` and a particular + /// `WorldId`. It wakes all of them, temporarily and soundly stores a `UnsafeWorldCell` in the + /// `GLOBAL_WORLD_ACCESS` and parks until the tasks it has awoken either complete their `async_access` + /// or have returned `Poll::Pending` for a variety of reasons. + /// The performance implications of this call are entirely dependent on the async runtime + /// you are using it with, certain poor implementations *could* cause this to take longer + /// than expect to resolve. + /// Returns `Some` as long as the last call processed any number of waiting `async_access` calls. pub fn wait(&self, schedule: InternedScheduleLabel, world: &mut World) -> Option<()> { let world_id = world.id(); - let mut waker_list = std::vec![]; + let mut waker_list = bevy_platform::prelude::vec![]; while let Ok((waker, system_init, task_id)) = GLOBAL_WAKE_REGISTRY .0 .get_or_init(|| KeyedQueues::new()) @@ -178,6 +206,7 @@ impl WakeRegistry { }) { return None; } + // Applies all the commands stored up to the world world.try_resource_scope(|world, mut appliers: Mut| { appliers.run(world); }); @@ -185,7 +214,10 @@ impl WakeRegistry { } } -/// The PhantomData here is just there cause it's a cute way of showing that we have a mutex around our unsafe worldcell and that's what the mutex is 'locking' +/// This is a very low contention, no contention in the normal execution path, way of storing and +/// using a UnsafeWorldCell from any thread/async task/async runtime. +/// The `Mutex>` is used to return `Poll::Pending` early from an `async_access` if +/// another `async_access` is currently using it. pub(crate) struct WorldAccessRegistry( OnceLock< RwLock< @@ -203,7 +235,8 @@ pub(crate) struct WorldAccessRegistry( ); impl WorldAccessRegistry { - pub(crate) fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()> { + /// During this `func: FnOnce()` call, calling `get` will access the stored UnsafeWorldCell + fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()> { let this = self.0.get_or_init(|| RwLock::new(HashMap::new())); let world_id = world.id(); if !this.read().unwrap().contains_key(&world_id) { @@ -221,8 +254,17 @@ impl WorldAccessRegistry { } impl<'a> Drop for ClearOnDropGuard<'a> { fn drop(&mut self) { - // clear it on the way out, even on panic - self.slot.write().unwrap().take(); + // clear it on the way out + // we can't actually panic here because panicking in a drop is bad + match self.slot.write() { + Ok(mut slot) => { + let _ = slot.take(); + } + Err(_) => { + // This is okay because the mutex is poisoned so nothing can access the + // UnsafeWorldCell now. + } + } } } unsafe { @@ -244,7 +286,7 @@ impl WorldAccessRegistry { } Some(()) } - pub(crate) unsafe fn get( + fn get( &self, world_id: WorldId, func: impl FnOnce(UnsafeWorldCell) -> Poll, @@ -265,7 +307,12 @@ impl WorldAccessRegistry { } } } - let async_barrier = { + // SAFETY: WakeParkBarrier is only *read* during this section per world, so reading it + // without an associated mutex is okay. + // Furthermore the WakeParkBarrier cannot be queried by `async_access` because it's type + // is not public, `async_access` cannot access `&mut World` to do a dynamic resource + // modification. + let async_barrier = unsafe { our_thing .0 .get_resource::() @@ -291,7 +338,7 @@ pub fn async_access( ) -> impl Future> where P: SystemParam + 'static, - for<'w, 's> Func: Clone + FnMut(P::Item<'w, 's>) -> Out, + for<'w, 's> Func: FnMut(P::Item<'w, 's>) -> Out, { let task_identifier = task_identifier.into(); PendingEcsCall::( @@ -375,37 +422,39 @@ where let task_id = self.4; let world_id = self.3 .0; - unsafe { - match GLOBAL_WORLD_ACCESS.get(world_id, |world: UnsafeWorldCell| { - let system_param_queue = match world.get_resource::>() { - None => return Poll::Pending, - Some(system_param_queue) => system_param_queue, - }; - let mut system_state = match system_param_queue.0.read().unwrap().get(&task_id) { - None => return Poll::Pending, - Some(cq) => cq.pop().unwrap(), - }; - let out; - // SAFETY: This is safe because we have a mutex around our world cell, so only one thing can have access to it at a time. - #[expect(unused_unsafe)] - unsafe { - // Obtain params and immediately consume them with the closure, - // ensuring the borrow ends before `apply`. - if let Err(err) = SystemState::validate_param(&mut system_state, world) { - return Poll::Ready(Err(err.into())); - } - if !system_state.meta().is_send() { - return Poll::Ready(Err( - SystemParamValidationError::invalid::>( - "Cannot have your system be non-send / exclusive", - ) - .into(), - )); - } - let state = system_state.get_unchecked(world); - out = self.as_mut().2.take().unwrap()(state); + match GLOBAL_WORLD_ACCESS.get(world_id, |world: UnsafeWorldCell| { + // SAFETY: We have a fake-mutex around our world, so no one else can do mutable access to it. + let system_param_queue = match unsafe { world.get_resource::>() } { + None => return Poll::Pending, + Some(system_param_queue) => system_param_queue, + }; + + let mut system_state = match system_param_queue.0.read().unwrap().get(&task_id) { + None => return Poll::Pending, + Some(cq) => cq.pop().unwrap(), + }; + let out; + // SAFETY: This is safe because we have a fake-mutex around our world cell, so only one thing can have access to it at a time. + unsafe { + // Obtain params and immediately consume them with the closure, + // ensuring the borrow ends before `apply`. + if let Err(err) = SystemState::validate_param(&mut system_state, world) { + return Poll::Ready(Err(err.into())); + } + if !system_state.meta().is_send() { + return Poll::Ready(Err( + SystemParamValidationError::invalid::>( + "Cannot have your system be non-send / exclusive", + ) + .into(), + )); } + let state = system_state.get_unchecked(world); + out = self.as_mut().2.take().unwrap()(state); + } + // SAFETY: We have a fake-mutex around our world, so no one else can do mutable access to it. + unsafe { match world .get_resource::>() .unwrap() @@ -417,24 +466,30 @@ where .push(system_state) { Ok(_) => {} - Err(_) => panic!(), + Err(_) => unreachable!("SystemStatePool should not be able to be removed if it previously existed, otherwise an invariant was violated"), } - Poll::Ready(Ok(out)) - }) { - Some(awa) => awa, - _ => { - match GLOBAL_WAKE_REGISTRY - .0 - .get_or_init(|| KeyedQueues::new()) - .try_send( - &self.3, - (cx.waker().clone(), system_state_init::

, task_id), - ) { - Ok(_) => {} - Err(_) => panic!(), - } - Poll::Pending + } + Poll::Ready(Ok(out)) + }) { + Some(awa) => awa, + _ => { + // This must be a static, sadly, because we must always make sure that we can store + // our pending wakers no matter what. Everything else that we care about can be + // stored on the world itself, but this must always be accessible, even if another + // `async_access` is currently running. + match GLOBAL_WAKE_REGISTRY + .0 + .get_or_init(|| KeyedQueues::new()) + .try_send( + &self.3, + (cx.waker().clone(), system_state_init::

, task_id), + ) { + Ok(_) => {} + // This should never panic because we never `close` our concurrent queues and + // the concurrent queue here is unbounded. + Err(_) => unreachable!(), } + Poll::Pending } } } diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index b7983436f0423..5e983f1d4adac 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -73,13 +73,18 @@ fn spawn_tasks(world_id: WorldId) { { println!("got error: {}", e); } + + let mut my_thing = String::new(); + if let Err(e) = async_access::<(), _, _>(world_id, PreUpdate, |()| { + my_thing.push('h'); println!("In PreUpdate"); }) .await { println!("{}", e); } + my_thing.push('h'); }) .detach(); } From 75b981f3f88d0f7e1536bcd8708b596d90a63bb6 Mon Sep 17 00:00:00 2001 From: Malek Date: Thu, 6 Nov 2025 17:29:01 -0500 Subject: [PATCH 16/32] changed the file name from `async` to `async_ecs` --- crates/bevy_ecs/src/lib.rs | 2 +- .../bevy_ecs/src/schedule/executor/{async.rs => async_ecs.rs} | 2 +- crates/bevy_ecs/src/schedule/executor/mod.rs | 2 +- crates/bevy_ecs/src/schedule/schedule.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename crates/bevy_ecs/src/schedule/executor/{async.rs => async_ecs.rs} (99%) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 104ca8b284403..82b99dddbd6ae 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -94,7 +94,7 @@ pub mod prelude { resource::Resource, schedule::{ common_conditions::*, - executor::r#async::{async_access, EcsTask}, + executor::async_ecs::{async_access, EcsTask}, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, Schedules, SystemCondition, SystemSet, }, diff --git a/crates/bevy_ecs/src/schedule/executor/async.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs similarity index 99% rename from crates/bevy_ecs/src/schedule/executor/async.rs rename to crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 31b70c0c7f27b..6f915eb97cda7 100644 --- a/crates/bevy_ecs/src/schedule/executor/async.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -1,4 +1,4 @@ -use crate::schedule::r#async::keyed_queues::KeyedQueues; +use crate::schedule::async_ecs::keyed_queues::KeyedQueues; use crate::schedule::{InternedScheduleLabel, ScheduleLabel}; use crate::system::{RunSystemError, SystemParamValidationError}; use crate::world::unsafe_world_cell::UnsafeWorldCell; diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index b8955711b2c61..4fd26c9f5448b 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -1,5 +1,5 @@ /// Async ECS access, using the ECS from Async Tasks -pub mod r#async; +pub mod async_ecs; #[cfg(feature = "std")] mod multi_threaded; mod single_threaded; diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 9a487e512e2b0..d85013fd06290 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -535,7 +535,7 @@ impl Schedule { }); let error_handler = world.default_error_handler(); - while let Some(()) = r#async::GLOBAL_WAKE_REGISTRY.wait(self.label, world) {} + while let Some(()) = async_ecs::GLOBAL_WAKE_REGISTRY.wait(self.label, world) {} #[cfg(not(feature = "bevy_debug_stepping"))] self.executor .run(&mut self.executable, world, None, error_handler); From 2ec0cc1b121111856b153ade2b90a806255f87db Mon Sep 17 00:00:00 2001 From: Malek Date: Thu, 6 Nov 2025 18:02:38 -0500 Subject: [PATCH 17/32] added the ability to cleanup async system params and also made it use the default error handler --- crates/bevy_ecs/src/lib.rs | 2 +- .../src/schedule/executor/async_ecs.rs | 92 +++++++++++++++---- examples/ecs/async_ecs.rs | 19 ++-- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 82b99dddbd6ae..8f335616ecf66 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -94,7 +94,7 @@ pub mod prelude { resource::Resource, schedule::{ common_conditions::*, - executor::async_ecs::{async_access, EcsTask}, + executor::async_ecs::{async_access, cleanup_ecs_task, EcsTask}, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, Schedules, SystemCondition, SystemSet, }, diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 6f915eb97cda7..707632bd775b5 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -1,12 +1,13 @@ use crate::schedule::async_ecs::keyed_queues::KeyedQueues; use crate::schedule::{InternedScheduleLabel, ScheduleLabel}; -use crate::system::{RunSystemError, SystemParamValidationError}; +use crate::system::SystemParamValidationError; use crate::world::unsafe_world_cell::UnsafeWorldCell; use crate::world::FromWorld; use crate::{ system::{SystemParam, SystemState}, world::World, }; +use bevy_ecs::error::ErrorContext; use bevy_ecs::prelude::NonSend; use bevy_ecs::world::{Mut, WorldId}; use bevy_platform::collections::HashMap; @@ -179,6 +180,14 @@ impl WakeRegistry { /// Returns `Some` as long as the last call processed any number of waiting `async_access` calls. pub fn wait(&self, schedule: InternedScheduleLabel, world: &mut World) -> Option<()> { let world_id = world.id(); + // Cleanups the garbage first. + for (cleanup_function, task_to_cleanup) in TASKS_TO_CLEANUP + .get_or_init(KeyedQueues::new) + .get_or_create(&world_id) + .try_iter() + { + cleanup_function(world, task_to_cleanup); + } let mut waker_list = bevy_platform::prelude::vec![]; while let Ok((waker, system_init, task_id)) = GLOBAL_WAKE_REGISTRY .0 @@ -331,33 +340,75 @@ impl WorldAccessRegistry { /// Calls will never return immediately and will always start Pending at least once. /// Call this with the same `PersistentTask` to persist SystemParams like Local or Changed /// Just use `world_id` if you do not mind a new SystemParam being initialized every time. -pub fn async_access( +pub async fn async_access( task_identifier: impl Into>, schedule: impl ScheduleLabel, ecs_access: Func, -) -> impl Future> +) -> Out where P: SystemParam + 'static, for<'w, 's> Func: FnMut(P::Item<'w, 's>) -> Out, { let task_identifier = task_identifier.into(); - PendingEcsCall::( + let out = PendingEcsCall::( PhantomData::

, PhantomData, Some(ecs_access), (task_identifier.1, schedule.intern()), task_identifier.0, ) + .await; + if task_identifier.3 == Cleanup::Auto { + cleanup_ecs_task(task_identifier); + } + out +} + +static TASKS_TO_CLEANUP: OnceLock< + KeyedQueues, +> = OnceLock::new(); + +/// Pass the `EcsTask` into here after you're done using it +/// This function will mark the `SystemState` for that task for cleanup. +pub fn cleanup_ecs_task(task: EcsTask

) { + fn cleanup_task(world: &mut World, task_id: AsyncTaskId) { + world.try_resource_scope(|_world, param_pool: Mut>| { + let mut pool = param_pool.0.write().unwrap(); + pool.remove(&task_id); + if pool.len() * 2 < pool.capacity() { + pool.shrink_to_fit(); + } + }); + } + // Should never panic cause this is an unbounded queue + match TASKS_TO_CLEANUP + .get_or_init(KeyedQueues::new) + .try_send(&task.1, (cleanup_task::

, task.0)) + { + Ok(_) => {} + Err(_) => unreachable!(), + } } impl From for EcsTask { fn from(value: WorldId) -> Self { - EcsTask::new(value) + EcsTask( + AsyncTaskId::new().unwrap(), + value, + PhantomData, + Cleanup::Auto, + ) } } /// An EcsTask can be re-used in order to persist SystemParams like Local, Changed, or Added -pub struct EcsTask(AsyncTaskId, WorldId, PhantomData); +pub struct EcsTask(AsyncTaskId, WorldId, PhantomData, Cleanup); + +#[derive(PartialEq, Copy, Clone)] +enum Cleanup { + Auto, + Manual, +} impl Clone for EcsTask { fn clone(&self) -> Self { @@ -370,7 +421,12 @@ impl EcsTask { /// Generates a new unique PersistentTask that can be re-used in order to persist SystemParams /// like Local, Changed, or Added pub fn new(world_id: WorldId) -> Self { - Self(AsyncTaskId::new().unwrap(), world_id, PhantomData) + Self( + AsyncTaskId::new().unwrap(), + world_id, + PhantomData, + Cleanup::Manual, + ) } } @@ -389,7 +445,7 @@ where P: SystemParam + 'static, for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, { - type Output = Result; + type Output = Out; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { fn system_state_init(world: &mut World, task_id: AsyncTaskId) { @@ -437,18 +493,22 @@ where let out; // SAFETY: This is safe because we have a fake-mutex around our world cell, so only one thing can have access to it at a time. unsafe { + let default_error_handler = world.default_error_handler(); // Obtain params and immediately consume them with the closure, // ensuring the borrow ends before `apply`. if let Err(err) = SystemState::validate_param(&mut system_state, world) { - return Poll::Ready(Err(err.into())); + default_error_handler(err.into(), ErrorContext::System { + name: system_state.meta.name.clone(), + last_run: system_state.meta.last_run, + }); } if !system_state.meta().is_send() { - return Poll::Ready(Err( - SystemParamValidationError::invalid::>( - "Cannot have your system be non-send / exclusive", - ) - .into(), - )); + default_error_handler(SystemParamValidationError::invalid::>( + "Cannot have your system be non-send / exclusive", + ).into(), ErrorContext::System { + name: system_state.meta.name.clone(), + last_run: system_state.meta.last_run, + }); } let state = system_state.get_unchecked(world); out = self.as_mut().2.take().unwrap()(state); @@ -469,7 +529,7 @@ where Err(_) => unreachable!("SystemStatePool should not be able to be removed if it previously existed, otherwise an invariant was violated"), } } - Poll::Ready(Ok(out)) + Poll::Ready(out) }) { Some(awa) => awa, _ => { diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index 5e983f1d4adac..a82af3e5f6e30 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -47,7 +47,7 @@ fn spawn_tasks(world_id: WorldId) { // Simulate a delay before task completion println!("delaying for {:?}", delay); Delay::new(delay).await; - if let Err(e) = async_access::< + let value = async_access::< ( Local, Commands, @@ -67,23 +67,22 @@ fn spawn_tasks(world_id: WorldId) { MeshMaterial3d(box_material.clone()), Transform::from_xyz(x as f32, 0.5, z as f32), )); + *local }, ) - .await - { - println!("got error: {}", e); + .await; + if value as i32 == (NUM_CUBES * 2) * (NUM_CUBES * 2) { + cleanup_ecs_task(task_id); } + println!("spawned {}", value); let mut my_thing = String::new(); - if let Err(e) = async_access::<(), _, _>(world_id, PreUpdate, |()| { + async_access::<(), _, _>(world_id, PreUpdate, |()| { my_thing.push('h'); - println!("In PreUpdate"); + //println!("In PreUpdate"); }) - .await - { - println!("{}", e); - } + .await; my_thing.push('h'); }) .detach(); From 0e40aa3e0d0c7f13f50ebf32811400fe7ffe03aa Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 14:50:16 -0500 Subject: [PATCH 18/32] made it less spurious and also hopefully fixed CI --- Cargo.toml | 6 ++ crates/bevy_ecs/src/lib.rs | 11 ++-- .../src/schedule/executor/async_ecs.rs | 62 ++++++++++--------- crates/bevy_ecs/src/schedule/executor/mod.rs | 1 + examples/ecs/async_ecs.rs | 31 +++++----- 5 files changed, 60 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 930f896840d2c..e043f8d7cc8a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2505,6 +2505,12 @@ path = "examples/ecs/system_stepping.rs" doc-scrape-examples = true required-features = ["bevy_debug_stepping"] +[package.metadata.example.async_ecs] +name = "async_ecs" +path = "examples/ecs/async_ecs.rs" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "async_ecs" path = "examples/ecs/async_ecs.rs" diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8f335616ecf66..89949e81fa39d 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -93,10 +93,8 @@ pub mod prelude { relationship::RelationshipTarget, resource::Resource, schedule::{ - common_conditions::*, - executor::async_ecs::{async_access, cleanup_ecs_task, EcsTask}, - ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, Schedules, - SystemCondition, SystemSet, + common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, + Schedules, SystemCondition, SystemSet, }, spawn::{Spawn, SpawnIter, SpawnRelated, SpawnWith, WithOneRelated, WithRelated}, system::{ @@ -113,7 +111,10 @@ pub mod prelude { #[doc(hidden)] #[cfg(feature = "std")] - pub use crate::system::ParallelCommands; + pub use crate::{ + schedule::executor::async_ecs::{async_access, EcsTask}, + system::ParallelCommands, + }; #[doc(hidden)] #[cfg(feature = "bevy_reflect")] diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 707632bd775b5..3f563e7e2193b 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -136,7 +136,7 @@ impl FromWorld for SystemStatePool { /// A monotonically increasing global identifier for any particular async task. /// Is an internal implementation detail and thus not generally accessible -#[derive(Clone, Copy, Hash, PartialOrd, PartialEq, Eq)] +#[derive(Clone, Copy, Hash, PartialOrd, PartialEq, Eq, Debug)] struct AsyncTaskId(u64); /// The next [`AsyncTaskId`]. @@ -203,15 +203,20 @@ impl WakeRegistry { if waker_list_len == 0 { return None; } - world.insert_resource(WakeParkBarrier( + let wake_park_barrier = WakeParkBarrier( thread::current(), - Arc::new(AtomicI64::new(waker_list_len as i64 - 1)), - )); + Arc::new(AtomicI64::new(waker_list_len as i64)), + ); + world.insert_resource(wake_park_barrier.clone()); if let None = GLOBAL_WORLD_ACCESS.set(world, || { for waker in waker_list { waker.wake(); } - thread::park(); + // We do this because we can get spurious wakes, but we wanna ensure that + // we stay parked until we have at least given every poll a chance to happen. + while wake_park_barrier.1.load(Ordering::SeqCst) > 0 { + thread::park(); + } }) { return None; } @@ -310,7 +315,9 @@ impl WorldAccessRegistry { struct UnparkOnDropGuard(WakeParkBarrier); impl Drop for UnparkOnDropGuard { fn drop(&mut self) { - let val = self.0 .1.fetch_add(-1, Ordering::SeqCst); + let val = self.0 .1.fetch_sub(1, Ordering::SeqCst) - 1; + // The runtime can poll us *more* often than when we call wake, + // this is why we use a AtomicI64 instead if val == 0 { self.0 .0.unpark(); } @@ -328,7 +335,7 @@ impl WorldAccessRegistry { .unwrap() .clone() }; - UnparkOnDropGuard(async_barrier.clone()); + let _guard = UnparkOnDropGuard(async_barrier.clone()); // this allows us to effectively yield as if pending if the world doesn't exist rn. let _world = our_thing.1.try_lock().ok()?; // SAFETY: this is safe because we ensure no one else has access to the world. @@ -354,13 +361,10 @@ where PhantomData::

, PhantomData, Some(ecs_access), - (task_identifier.1, schedule.intern()), - task_identifier.0, + (task_identifier.0 .1, schedule.intern()), + task_identifier.0 .0, ) .await; - if task_identifier.3 == Cleanup::Auto { - cleanup_ecs_task(task_identifier); - } out } @@ -370,7 +374,7 @@ static TASKS_TO_CLEANUP: OnceLock< /// Pass the `EcsTask` into here after you're done using it /// This function will mark the `SystemState` for that task for cleanup. -pub fn cleanup_ecs_task(task: EcsTask

) { +fn cleanup_ecs_task(task: &InternalEcsTask

) { fn cleanup_task(world: &mut World, task_id: AsyncTaskId) { world.try_resource_scope(|_world, param_pool: Mut>| { let mut pool = param_pool.0.write().unwrap(); @@ -390,43 +394,41 @@ pub fn cleanup_ecs_task(task: EcsTask

) { } } -impl From for EcsTask { +impl From for EcsTask

{ fn from(value: WorldId) -> Self { - EcsTask( + EcsTask(Arc::new(InternalEcsTask( AsyncTaskId::new().unwrap(), value, PhantomData, - Cleanup::Auto, - ) + ))) } } /// An EcsTask can be re-used in order to persist SystemParams like Local, Changed, or Added -pub struct EcsTask(AsyncTaskId, WorldId, PhantomData, Cleanup); +pub struct EcsTask(Arc>); + +struct InternalEcsTask(AsyncTaskId, WorldId, PhantomData

); -#[derive(PartialEq, Copy, Clone)] -enum Cleanup { - Auto, - Manual, +impl Drop for InternalEcsTask { + fn drop(&mut self) { + cleanup_ecs_task(self) + } } -impl Clone for EcsTask { +impl Clone for EcsTask

{ fn clone(&self) -> Self { - *self + EcsTask(self.0.clone()) } } - -impl Copy for EcsTask {} -impl EcsTask { +impl EcsTask

{ /// Generates a new unique PersistentTask that can be re-used in order to persist SystemParams /// like Local, Changed, or Added pub fn new(world_id: WorldId) -> Self { - Self( + Self(Arc::new(InternalEcsTask( AsyncTaskId::new().unwrap(), world_id, PhantomData, - Cleanup::Manual, - ) + ))) } } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 4fd26c9f5448b..d8d94548d3632 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "std")] /// Async ECS access, using the ECS from Async Tasks pub mod async_ecs; #[cfg(feature = "std")] diff --git a/examples/ecs/async_ecs.rs b/examples/ecs/async_ecs.rs index a82af3e5f6e30..f22da66bed61a 100644 --- a/examples/ecs/async_ecs.rs +++ b/examples/ecs/async_ecs.rs @@ -1,10 +1,10 @@ //! A minimal example showing how to perform asynchronous work in Bevy -//! using [`AsyncComputeTaskPool`] for parallel task execution and a crossbeam channel -//! to communicate between async tasks and the main ECS thread. +//! using [`AsyncComputeTaskPool`] to run detached tasks, combined with +//! `async_access` to safely access ECS data from async contexts. //! -//! This example demonstrates how to spawn detached async tasks, send completion messages via channels, -//! and dynamically spawn ECS entities (cubes) as results from these tasks. The system processes -//! async task results in the main game loop, all without blocking or polling the main thread. +//! Instead of using channels to send results back to the main thread, +//! this example performs ECS world mutations directly *inside* async tasks +//! by scheduling closures to run on a chosen schedule (e.g., `Update`). use bevy::{ math::ops::{cos, sin}, @@ -29,18 +29,20 @@ fn main() { .run(); } -/// Spawns async tasks on the compute task pool to simulate delayed cube creation. +/// Spawns a grid of async tasks to simulate delayed cube creation. /// -/// Each task is executed on a separate thread and sends the result (cube position) -/// back through the `CubeChannel` once completed. The tasks are detached to -/// run asynchronously without blocking the main thread. +/// Each task sleeps for a random duration, then uses `async_access` +/// to enqueue a closure that runs on the ECS main thread, allowing +/// mutation of ECS data (e.g., spawning entities and modifying `Local` state). /// -/// In this example, we don't implement task tracking or proper error handling. +/// No polling, task handles, or channels are needed — async work is detached, +/// and ECS access happens only inside scheduled closures. fn spawn_tasks(world_id: WorldId) { let pool = AsyncComputeTaskPool::get(); let task_id = EcsTask::new(world_id); for x in -NUM_CUBES..NUM_CUBES { for z in -NUM_CUBES..NUM_CUBES { + let task_id = task_id.clone(); // Spawn a task on the async compute pool pool.spawn(async move { let delay = Duration::from_secs_f32(rand::rng().random_range(2.0..8.0)); @@ -72,18 +74,15 @@ fn spawn_tasks(world_id: WorldId) { ) .await; if value as i32 == (NUM_CUBES * 2) * (NUM_CUBES * 2) { - cleanup_ecs_task(task_id); + println!("DONE"); } - println!("spawned {}", value); - + // Showcasing how you can mutably access variables from outside the closure let mut my_thing = String::new(); - async_access::<(), _, _>(world_id, PreUpdate, |()| { my_thing.push('h'); - //println!("In PreUpdate"); }) .await; - my_thing.push('h'); + my_thing.push('i'); }) .detach(); } From e92d602bc90d2ce0223534935f7ed52295c3bc79 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 15:02:39 -0500 Subject: [PATCH 19/32] undid unessecary publicking of SystemState fields --- crates/bevy_ecs/src/system/function_system.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 268f88462943d..154fd199acda1 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -216,9 +216,9 @@ impl SystemMeta { /// } /// ``` pub struct SystemState { - pub(crate) meta: SystemMeta, - pub(crate) param_state: Param::State, - pub(crate) world_id: WorldId, + pub meta: SystemMeta, + pub param_state: Param::State, + pub world_id: WorldId, } // Allow closure arguments to be inferred. From 31a0ef262b5fccb13f88b3595ee59850288577c4 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 15:03:15 -0500 Subject: [PATCH 20/32] actually undid unessecary publicking of SystemState fields --- crates/bevy_ecs/src/system/function_system.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 154fd199acda1..62b3597f89897 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -216,9 +216,9 @@ impl SystemMeta { /// } /// ``` pub struct SystemState { - pub meta: SystemMeta, - pub param_state: Param::State, - pub world_id: WorldId, + meta: SystemMeta, + param_state: Param::State, + world_id: WorldId, } // Allow closure arguments to be inferred. From 18cbae96452224a12184a9063c6c4d02ad0a5bd1 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 15:07:35 -0500 Subject: [PATCH 21/32] okay some last minute surface area changes --- crates/bevy_ecs/src/lib.rs | 4 ++-- crates/bevy_ecs/src/system/function_system.rs | 2 +- crates/bevy_ecs/src/world/mod.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 89949e81fa39d..d4b2b0c5270f0 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -104,8 +104,8 @@ pub mod prelude { SystemParamFunction, }, world::{ - identifier::WorldId, EntityMut, EntityRef, EntityWorldMut, FilteredResources, - FilteredResourcesMut, FromWorld, World, + EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, + FromWorld, World, WorldId, }, }; diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 62b3597f89897..d5ed1b55233f5 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -216,7 +216,7 @@ impl SystemMeta { /// } /// ``` pub struct SystemState { - meta: SystemMeta, + pub(crate) meta: SystemMeta, param_state: Param::State, world_id: WorldId, } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index ef803dc7fb5cf..9d60968ffef47 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -5,7 +5,7 @@ mod deferred_world; mod entity_access; mod entity_fetch; mod filtered_resource; -pub(crate) mod identifier; +mod identifier; mod spawn_batch; pub mod error; From 035c176df15e8517791c05f9d1cfc2aa2ed8dd8f Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 16:24:24 -0500 Subject: [PATCH 22/32] try to fix ci --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e043f8d7cc8a6..5ebb0843a0344 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2507,7 +2507,7 @@ required-features = ["bevy_debug_stepping"] [package.metadata.example.async_ecs] name = "async_ecs" -path = "examples/ecs/async_ecs.rs" +description = "Showcases using the `async_access` primitive for async <-> ecs interaction" category = "ECS (Entity Component System)" wasm = false From db1177ba33ecf6b136968c183e28c73050930a1a Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 16:25:30 -0500 Subject: [PATCH 23/32] try to fix ci --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index c83abdb3dd1de..fbf7ffe7995ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -340,6 +340,7 @@ Example | Description [System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully [System Stepping](../examples/ecs/system_stepping.rs) | Demonstrate stepping through systems in order of execution. +[async_ecs](../examples/ecs/async_ecs.rs) | Showcases using the `async_access` primitive for async <-> ecs interaction ### Embedded From fc3b3535d948aa9eceed501851f1aa2d47aa2690 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 16:31:08 -0500 Subject: [PATCH 24/32] fix ci? --- crates/bevy_ecs/src/schedule/executor/async_ecs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 3f563e7e2193b..9e5fe3be44d44 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -27,10 +27,10 @@ use std::thread; /// arbitrary N number of worlds running in parallel on the same process do not interfere at all /// except the very first time a new world initializes it's key. mod keyed_queues { + use bevy_platform::collections::HashMap; + use bevy_platform::sync::{Arc, RwLock}; use concurrent_queue::ConcurrentQueue; - use std::sync::Arc; - use std::{collections::HashMap, hash::Hash, sync::RwLock}; - + use core::hash::Hash; /// HashMap>> behind a single RwLock. /// - Writers only contend when creating a new key or GC'ing. /// - `push` is non-blocking (unbounded queue). @@ -293,7 +293,7 @@ impl WorldAccessRegistry { // Lifetimes are not used in any actual code optimization, so turning it into a static does not violate any of rust's rules // As *LONG* as we keep it within it's lifetime, which we do here, manually, with our `ClearOnDrop` struct. world_container.write().unwrap().replace(( - std::mem::transmute(world.as_unsafe_world_cell()), + core::mem::transmute(world.as_unsafe_world_cell()), Mutex::new(PhantomData), )); func() From 15b79b63ab5ffb615daf5e775edcb8b8223bf974 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 16:49:06 -0500 Subject: [PATCH 25/32] fix ci? --- .../src/schedule/executor/async_ecs.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 9e5fe3be44d44..45333a2ef2267 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -31,7 +31,7 @@ mod keyed_queues { use bevy_platform::sync::{Arc, RwLock}; use concurrent_queue::ConcurrentQueue; use core::hash::Hash; - /// HashMap>> behind a single RwLock. + /// `HashMap>>` behind a single RwLock. /// - Writers only contend when creating a new key or GC'ing. /// - `push` is non-blocking (unbounded queue). pub struct KeyedQueues { @@ -76,17 +76,17 @@ mod keyed_queues { } } -/// This is an abstraction that temporarily and soundly stores the UnsafeWorldCell in a static so we can access +/// This is an abstraction that temporarily and soundly stores the `UnsafeWorldCell` in a static so we can access /// it from any async task, runtime, and thread. static GLOBAL_WORLD_ACCESS: WorldAccessRegistry = WorldAccessRegistry(OnceLock::new()); -/// The entrypoint, stores Wakers from async_access's that wish to be polled with world access +/// The entrypoint, stores `Waker`s from `async_access`'s that wish to be polled with world access /// also stores the generic function pointer to the concrete function that initializes the /// system state for any set of SystemParams pub(crate) static GLOBAL_WAKE_REGISTRY: WakeRegistry = WakeRegistry(OnceLock::new()); -/// Acts as a barrier that is waited on in the `wait` call, and once the AtomicI64 reaches 0 the +/// Acts as a barrier that is waited on in the `wait` call, and once the `AtomicI64` reaches 0 the /// thread that `wait` was called on gets woken up and resumes. #[derive(bevy_ecs_macros::Resource, Clone)] pub(crate) struct WakeParkBarrier(thread::Thread, Arc); @@ -229,7 +229,7 @@ impl WakeRegistry { } /// This is a very low contention, no contention in the normal execution path, way of storing and -/// using a UnsafeWorldCell from any thread/async task/async runtime. +/// using a `UnsafeWorldCell` from any thread/async task/async runtime. /// The `Mutex>` is used to return `Poll::Pending` early from an `async_access` if /// another `async_access` is currently using it. pub(crate) struct WorldAccessRegistry( @@ -249,7 +249,7 @@ pub(crate) struct WorldAccessRegistry( ); impl WorldAccessRegistry { - /// During this `func: FnOnce()` call, calling `get` will access the stored UnsafeWorldCell + /// During this `func: FnOnce()` call, calling `get` will access the stored `UnsafeWorldCell` fn set(&self, world: &mut World, func: impl FnOnce()) -> Option<()> { let this = self.0.get_or_init(|| RwLock::new(HashMap::new())); let world_id = world.id(); @@ -345,8 +345,8 @@ impl WorldAccessRegistry { /// Allows you to access the ECS from any arbitrary async runtime. /// Calls will never return immediately and will always start Pending at least once. -/// Call this with the same `PersistentTask` to persist SystemParams like Local or Changed -/// Just use `world_id` if you do not mind a new SystemParam being initialized every time. +/// Call this with the same `EcsTask` to persist `SystemParams` like `Local` or `Changed` +/// Just use `world_id` if you do not mind a new `SystemParam` being initialized every time. pub async fn async_access( task_identifier: impl Into>, schedule: impl ScheduleLabel, @@ -404,7 +404,7 @@ impl From for EcsTask

{ } } -/// An EcsTask can be re-used in order to persist SystemParams like Local, Changed, or Added +/// An `EcsTask` can be re-used in order to persist `SystemParams` like `Local`, `Changed`, or `Added` pub struct EcsTask(Arc>); struct InternalEcsTask(AsyncTaskId, WorldId, PhantomData

); @@ -421,8 +421,8 @@ impl Clone for EcsTask

{ } } impl EcsTask

{ - /// Generates a new unique PersistentTask that can be re-used in order to persist SystemParams - /// like Local, Changed, or Added + /// Generates a new unique `EcsTask` that can be re-used in order to persist `SystemParams` + /// like `Local`, `Changed`, or `Added` pub fn new(world_id: WorldId) -> Self { Self(Arc::new(InternalEcsTask( AsyncTaskId::new().unwrap(), From 4d426c2cdd6e267d97a5e0fef05a19629a77f146 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 16:58:23 -0500 Subject: [PATCH 26/32] fix ci? --- .../src/schedule/executor/async_ecs.rs | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 45333a2ef2267..7db810029311a 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -31,9 +31,9 @@ mod keyed_queues { use bevy_platform::sync::{Arc, RwLock}; use concurrent_queue::ConcurrentQueue; use core::hash::Hash; - /// `HashMap>>` behind a single RwLock. - /// - Writers only contend when creating a new key or GC'ing. - /// - `push` is non-blocking (unbounded queue). + /// `HashMap>>` behind a single `RwLock`. + /// - Writers only contend when creating a new key. + /// - `push` is almost always non-blocking (unbounded queue). pub struct KeyedQueues { inner: RwLock>>>, } @@ -67,7 +67,7 @@ mod keyed_queues { } /// Potentially-blocking send but almost never blocking (unbounded queue => `push` never fails). - /// ( Only blocks when the (WorldId, Schedule) has never been used before + /// ( Only blocks when the `(WorldId, Schedule)` has never been used before #[inline] pub fn try_send(&self, key: &K, val: V) -> Result<(), concurrent_queue::PushError> { let q = self.get_or_create(key); @@ -82,7 +82,7 @@ static GLOBAL_WORLD_ACCESS: WorldAccessRegistry = WorldAccessRegistry(OnceLock:: /// The entrypoint, stores `Waker`s from `async_access`'s that wish to be polled with world access /// also stores the generic function pointer to the concrete function that initializes the -/// system state for any set of SystemParams +/// system state for any set of `SystemParams` pub(crate) static GLOBAL_WAKE_REGISTRY: WakeRegistry = WakeRegistry(OnceLock::new()); @@ -117,9 +117,8 @@ impl FromWorld for SystemStatePool { appliers.0.insert(TypeId::of::(), |world: &mut World| { world.try_resource_scope(|world, param_pool: Mut>| { for concurrent_queue in param_pool.0.read().unwrap().values() { - let mut system_state = match concurrent_queue.pop() { - Ok(val) => val, - Err(_) => panic!(), + let Ok(mut system_state) = concurrent_queue.pop() else { + unreachable!() }; system_state.apply(world); match concurrent_queue.push(system_state) { @@ -159,7 +158,7 @@ impl AsyncTaskId { } } -/// Is the GLOBAL_WAKE_REGISTRY +/// Is the `GLOBAL_WAKE_REGISTRY` pub(crate) struct WakeRegistry( OnceLock< KeyedQueues< @@ -191,7 +190,7 @@ impl WakeRegistry { let mut waker_list = bevy_platform::prelude::vec![]; while let Ok((waker, system_init, task_id)) = GLOBAL_WAKE_REGISTRY .0 - .get_or_init(|| KeyedQueues::new()) + .get_or_init(KeyedQueues::new) .get_or_create(&(world_id, schedule)) .pop() { @@ -208,16 +207,19 @@ impl WakeRegistry { Arc::new(AtomicI64::new(waker_list_len as i64)), ); world.insert_resource(wake_park_barrier.clone()); - if let None = GLOBAL_WORLD_ACCESS.set(world, || { - for waker in waker_list { - waker.wake(); - } - // We do this because we can get spurious wakes, but we wanna ensure that - // we stay parked until we have at least given every poll a chance to happen. - while wake_park_barrier.1.load(Ordering::SeqCst) > 0 { - thread::park(); - } - }) { + if GLOBAL_WORLD_ACCESS + .set(world, || { + for waker in waker_list { + waker.wake(); + } + // We do this because we can get spurious wakes, but we wanna ensure that + // we stay parked until we have at least given every poll a chance to happen. + while wake_park_barrier.1.load(Ordering::SeqCst) > 0 { + thread::park(); + } + }) + .is_none() + { return None; } // Applies all the commands stored up to the world @@ -281,6 +283,10 @@ impl WorldAccessRegistry { } } } + // SAFETY: This mem transmute is safe only because we drop it after, and our GLOBAL_WORLD_ACCESS is private, and we don't clone it + // where we do use it, so the lifetime doesn't get propagated anywhere. + // Lifetimes are not used in any actual code optimization, so turning it into a static does not violate any of rust's rules + // As *LONG* as we keep it within it's lifetime, which we do here, manually, with our `ClearOnDrop` struct. unsafe { let binding = this.read().unwrap(); let world_container = binding.get(&world_id).unwrap(); @@ -293,10 +299,12 @@ impl WorldAccessRegistry { // Lifetimes are not used in any actual code optimization, so turning it into a static does not violate any of rust's rules // As *LONG* as we keep it within it's lifetime, which we do here, manually, with our `ClearOnDrop` struct. world_container.write().unwrap().replace(( - core::mem::transmute(world.as_unsafe_world_cell()), + core::mem::transmute::>( + world.as_unsafe_world_cell(), + ), Mutex::new(PhantomData), )); - func() + func(); } Some(()) } @@ -309,9 +317,7 @@ impl WorldAccessRegistry { // where a thread is parked because of our world. let a = self.0.get()?.read().unwrap(); let b = a.get(&world_id)?.read().unwrap(); - let Some(our_thing) = b.as_ref() else { - return None; - }; + let our_thing = b.as_ref()?; struct UnparkOnDropGuard(WakeParkBarrier); impl Drop for UnparkOnDropGuard { fn drop(&mut self) { @@ -357,15 +363,14 @@ where for<'w, 's> Func: FnMut(P::Item<'w, 's>) -> Out, { let task_identifier = task_identifier.into(); - let out = PendingEcsCall::( + PendingEcsCall::( PhantomData::

, PhantomData, Some(ecs_access), (task_identifier.0 .1, schedule.intern()), task_identifier.0 .0, ) - .await; - out + .await } static TASKS_TO_CLEANUP: OnceLock< @@ -411,7 +416,7 @@ struct InternalEcsTask(AsyncTaskId, WorldId, PhantomDa impl Drop for InternalEcsTask { fn drop(&mut self) { - cleanup_ecs_task(self) + cleanup_ecs_task(self); } } @@ -541,7 +546,7 @@ where // `async_access` is currently running. match GLOBAL_WAKE_REGISTRY .0 - .get_or_init(|| KeyedQueues::new()) + .get_or_init(KeyedQueues::new) .try_send( &self.3, (cx.waker().clone(), system_state_init::

, task_id), From 93e709df0123c6594ddd8bb1e1e85eb02f1f00cc Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 17:05:52 -0500 Subject: [PATCH 27/32] fix ci? --- .../src/schedule/executor/async_ecs.rs | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 7db810029311a..989e691f68857 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -207,21 +207,16 @@ impl WakeRegistry { Arc::new(AtomicI64::new(waker_list_len as i64)), ); world.insert_resource(wake_park_barrier.clone()); - if GLOBAL_WORLD_ACCESS - .set(world, || { - for waker in waker_list { - waker.wake(); - } - // We do this because we can get spurious wakes, but we wanna ensure that - // we stay parked until we have at least given every poll a chance to happen. - while wake_park_barrier.1.load(Ordering::SeqCst) > 0 { - thread::park(); - } - }) - .is_none() - { - return None; - } + GLOBAL_WORLD_ACCESS.set(world, || { + for waker in waker_list { + waker.wake(); + } + // We do this because we can get spurious wakes, but we wanna ensure that + // we stay parked until we have at least given every poll a chance to happen. + while wake_park_barrier.1.load(Ordering::SeqCst) > 0 { + thread::park(); + } + })?; // Applies all the commands stored up to the world world.try_resource_scope(|world, mut appliers: Mut| { appliers.run(world); @@ -488,11 +483,7 @@ where match GLOBAL_WORLD_ACCESS.get(world_id, |world: UnsafeWorldCell| { // SAFETY: We have a fake-mutex around our world, so no one else can do mutable access to it. - let system_param_queue = match unsafe { world.get_resource::>() } { - None => return Poll::Pending, - Some(system_param_queue) => system_param_queue, - }; - + let Some(system_param_queue) = (unsafe { world.get_resource::>() }) else { return Poll::Pending }; let mut system_state = match system_param_queue.0.read().unwrap().get(&task_id) { None => return Poll::Pending, Some(cq) => cq.pop().unwrap(), From 4dcf35a5db8de6abeae6dc6560c18a93fa207b72 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 17:09:16 -0500 Subject: [PATCH 28/32] fix ci? --- crates/bevy_ecs/src/schedule/executor/async_ecs.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 989e691f68857..ce3abb6e322ef 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -83,7 +83,6 @@ static GLOBAL_WORLD_ACCESS: WorldAccessRegistry = WorldAccessRegistry(OnceLock:: /// The entrypoint, stores `Waker`s from `async_access`'s that wish to be polled with world access /// also stores the generic function pointer to the concrete function that initializes the /// system state for any set of `SystemParams` - pub(crate) static GLOBAL_WAKE_REGISTRY: WakeRegistry = WakeRegistry(OnceLock::new()); /// Acts as a barrier that is waited on in the `wait` call, and once the `AtomicI64` reaches 0 the From 4d7595adb8b8c3240660d328b2a9a1ee53f29b9a Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 17:29:34 -0500 Subject: [PATCH 29/32] fix ci? --- crates/bevy_ecs/src/schedule/schedule.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index d85013fd06290..381dcd6fe680d 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -535,7 +535,11 @@ impl Schedule { }); let error_handler = world.default_error_handler(); - while let Some(()) = async_ecs::GLOBAL_WAKE_REGISTRY.wait(self.label, world) {} + #[cfg(feature = "std")] + while async_ecs::GLOBAL_WAKE_REGISTRY + .wait(self.label, world) + .is_some() + {} #[cfg(not(feature = "bevy_debug_stepping"))] self.executor .run(&mut self.executable, world, None, error_handler); From 7eb307be39eb9157b7cd8d380d0a908f5cdfa5e9 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 19:08:35 -0500 Subject: [PATCH 30/32] reduce potential performance impact by checking length at the beginning and early-out-ing. --- crates/bevy_ecs/src/schedule/executor/async_ecs.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index ce3abb6e322ef..046e29dd2013f 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -178,6 +178,15 @@ impl WakeRegistry { /// Returns `Some` as long as the last call processed any number of waiting `async_access` calls. pub fn wait(&self, schedule: InternedScheduleLabel, world: &mut World) -> Option<()> { let world_id = world.id(); + if GLOBAL_WAKE_REGISTRY + .0 + .get_or_init(KeyedQueues::new) + .get_or_create(&(world_id, schedule)) + .len() + == 0 + { + return None; + } // Cleanups the garbage first. for (cleanup_function, task_to_cleanup) in TASKS_TO_CLEANUP .get_or_init(KeyedQueues::new) @@ -198,9 +207,6 @@ impl WakeRegistry { waker_list.push(waker); } let waker_list_len = waker_list.len(); - if waker_list_len == 0 { - return None; - } let wake_park_barrier = WakeParkBarrier( thread::current(), Arc::new(AtomicI64::new(waker_list_len as i64)), From 03debc9f3b990178db4165850e1849f4002bdb14 Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 19:15:11 -0500 Subject: [PATCH 31/32] fix ci? --- crates/bevy_ecs/src/schedule/executor/async_ecs.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 046e29dd2013f..7885429bd5992 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -182,8 +182,7 @@ impl WakeRegistry { .0 .get_or_init(KeyedQueues::new) .get_or_create(&(world_id, schedule)) - .len() - == 0 + .is_empty() { return None; } From 2409cde5a3a6def096c545e980e6faaaf6944fda Mon Sep 17 00:00:00 2001 From: Malek Date: Sun, 9 Nov 2025 21:58:57 -0500 Subject: [PATCH 32/32] make it FnOnce instead of FnMut --- crates/bevy_ecs/src/schedule/executor/async_ecs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs index 7885429bd5992..ac73d4627756c 100644 --- a/crates/bevy_ecs/src/schedule/executor/async_ecs.rs +++ b/crates/bevy_ecs/src/schedule/executor/async_ecs.rs @@ -359,7 +359,7 @@ pub async fn async_access( ) -> Out where P: SystemParam + 'static, - for<'w, 's> Func: FnMut(P::Item<'w, 's>) -> Out, + for<'w, 's> Func: FnOnce(P::Item<'w, 's>) -> Out, { let task_identifier = task_identifier.into(); PendingEcsCall::(