#Async ECS ergonomics: better late than never
1 messages · Page 2 of 1
it's a pretty easy transpose
the SyncPoint derive can just be a layer ontop of the current design, that you can use for explicit scheduling control. It is even rather easy to do so.
maybe we'll wait to merge this in until your SystemSet stuff is in @warm mesa
because at that point system set will be close enough to system that i think it will be convincing? 🤔
Hmmmm, okay let me cook:
So, if a Sync Point, is to be an Exclusive System, that runs a Schedule/Other Systems, then that kinda checks out with the World Sync Point. (not impl wise but semantically-a-bit)
So, an Async Sync Point, is to be an Exclusive System, that runs an (Async)Schedule/Other (Async)Systems.
So then async_access<...>(world_id, Schedule, ...) is equivalent to app.add_systems(Schedule, ...), except it doesnt support passing back a System with ScheduleConfigs 
Where the confusion sets in, is that currently, Schedules are not Sync Points, in that they are not an Exclusive System that runs other Systems. Even with doots changes I am not sure that it will be changing Schedules specifically that far.
This also means if you expand that idea, and make the World Sync Point an Schedule, or Exclusive System that runs other Systems, you gain the ability to add regular systems to it, or perhaps.....reactive systems 🤨
my stuff shouldn't block this, it should only unlock some more parallelization opportunities
not from a practical perspective but a semantic one
I think it becomes a lot more obvious when the api surface can become this
world_id.run_systems(Update, (my_system.before(system3), system2).chain()).await;
world_id.run_fixed_systems(Update, (my_system.before(system3), system2).chain()).await;
world_id.run_startup_systems(Update, (my_system.before(system3), system2).chain()).await;
because here we're just adding our systems to a SystemSet instead of a schedule
that specific API (the ideal one) is quite a few versions away
focus on what you can do with today's stuff, but maybe some of the stuff I get merged in the nearer term will be helpful
Okay, i'm going to update this to use the new version of the crate, and fix all the lints, probably sometime sunday or saturday
Thx 😄 no pressure!
I've updated this branch to use your new bevy_malek_async crate, the new api is very good (minimal generics!), still hangs after a few rounds though
Can't say to much but i am stress testing the crap out of bevy_malek_async
I figured out an issue with my crates implementation, will be pushing a fix within the next few days
Should fix your hang @vocal vortex
When rerecast 👀
awawawawa
maybe later today, depends, that or this weekend i think
@vocal vortex if you wanna test with the newest version of bevy malek async
i think i have fixed the hangs
oop
it's still trying to get published on crates
1 sec
okay now published
also sorry i've been gone for so long, i've been doing fulltime bevy at work so have had no time or energy for any extra bevy out of work
this is still the case btw 😂 😭
so i probably won't be doing much until i run out of internal funding to do bevy stuff on
and i just moved!
awawawawa
okay wait i also have to publish a release for 0.18 lmao
this shouldn't be that difficult
okay 0.18 has been released
still hanging :(

updated my repo if you want to test yourself
tested it myself, didn't get a hang get a crash where it doesn't actually crash, just the window disappears lol
but i think it's the same thing
gonna try to figure out why it's happening
going to try to keep reducing the surface area of the code until i can distill the problem down into it's essential parts

@eager trout I've had a lot of fun playing with bevy_malek_async, you can find my fork here.
Exclusive Systems
I added async_exclusive_access as an alternative to async_access that allows for exclusive systems. There's probably a better way to do that, or maybe its already possible?
Ergonomics
I also added AsyncCommands and AsyncWorld to examples/utils. There's likely a long path to finding consensus on something we can upstream so good to keep out of lib.rs, but something like this may be useful for the examples to demonstrate how the crate can be used ergonomically.
#[derive(Event)]
struct Response(String);
App::new()
..
.add_systems(Startup, spawn_web_request)
.run();
}
// AsyncCommands just spawns an AsyncSystem
// if we implemented IntoSystem for async systems this step
// would not be nessecary.
fn spawn_web_request(commands: AsyncCommands) {
commands.run(fetch_example_com);
}
// an 'async system' is a FnOnce(AsyncWorld) -> impl Future<Output=Result>;
// AsyncWorld just contains async equivelents of `World` methods
async fn fetch_example_com(world: AsyncWorld) -> Result {
let body: String = send_request("http://example.com").await?;
world.trigger(Response(body)).await;
Ok(())
}
Questions
I notice you're using std::thread::spawn instead of IoTaskPool/ComputeTaskPool, was that just for the tokio shenanigans or is there something else? Would these also be valid ways of spawning the tasks?
fyi I've been working on a minimal deps examples/ureq_demo.rs for wasm/embedded support.
Yeah theres no limitations on how you spawn threads or tasks
Let me drop it here: #assets-dev message
We had this too by the way: https://discord.com/channels/691052431525675048/1475724749047857326
So i've been rewriting the async stuff to be even better , so its cancel safe, more performant, and also is a little bit less code
Going to continue working on that tonight, once thats done and pushed i'll go back to working on the navmesh editor with the async
Also going to explore modifying it to not work off of schedules, and instead systems as cart requires
aha it works!
well i need to test it on gingeh's branch, and in other spots, but it at least appears to work with the simple case.
i also made the code better ( i think at least ) though there is a dyn now, but i think it's way worth it given the reduction in unsafe compared to how i was doing it before
also dropped the crossbeam requirement, so now it's capable of being upstreamed as no-std into bevy, and without any new dependencies ( the only thing we need as a no-std equivalent is a condvar now )
before not as in public code, but new private code i was working on that i hadn't pushed, i added a new way to do things that was better, but had more unsafe
but i just reduced that unsafe by hiding it behind a dyn instead of, effectively building a vtable manually lmao
don't have this part yet either
but things are looking good
the whole implementation is now only 587 loc, there are 92 lines worth of comments tho 
i had the weirdest bug because i was using a const instead of a static for a OnceLock<RwLock<HashMap<>>>
SHIT i was about to say "gingeh i'm happy to report this fixes the crashing issue" but it doesn't, it delays it by a while i was able to play for a long time but it still happens
but i also noticed you're using some nightly async features 🤔
and now i'm wondering if those might be the cause of the issue
its late, so i save debugging that for tomorrow!
still sounds like progress!
Yeah the code is a definite improvement
Now its cancel safe, so there should be no way to mis-use it
And that opens up even more async patterns
I'll share the code tonight
But its still in progress cause I wanna figure out the ginghe issue
Okay I meant to share it last night
But I was at work till 8:30 so I didn't when I got home
Tonight I plan to share it
I'll just push my code to the repo
I'll wait to do a new release till I test it more and make other changes
Also I feel like I can get it more performant
@young sorrel I want you to update your thing that uses it tho in the branch because it has a ton of good test cases
I forgor where that code was
If you could relink
Pog thanks
it is done
the new simpler better cancel-safe code has been pushed to github
i still have some things to do to make this code a little smaller and simpler
i can get rid of the Arc<> inside the ecs task and return a future that is cloneable
cause it internally has an arc that manages it's SystemState
specifically it has a Arc<dyn Thingie>
which we need to use polymorphism here to hide the specific state details uwu
I wonder if the deadlock is related to this class of bugs: https://jacko.io/snooze.html
🤔
hmnn
mayhaps
i also realize i have a bug in my implementation of the code, which is the same exact class of bug i had in the prior implementation
this bug is a conceptually difficult one to squash lmao
but if i manage to squash it it may solve the issue!
okay
round 3 of re-designing this
i think what i'm going to do is some sort of completion round barrier based around the waker i send back to get awoken. This way they can never be de-synced. You can never wait for a future to advance a barrier unless you actually called wake on the future and told to executor to try running it 
IT IS FIXED
okay
so
i did have other issues
i think
but the issue actually wasn't any of those
it was i needed to tick the global task pool thread
before calling my await barrier
so that way if you were doing anything on bevy's task pools and not tokio ( which runs independently ) then it could ensure advancement
so suborbatil works now!
BUT i do like this new design and it is more resilient
i still have more work to do to make this better now though
@vocal vortex if you wanna try it now it should work with the latest git
you were right 
it indeed was
oh wait, having git issues
one sec and it will be pushed
oh yeah it's pushed!
down to 628 loc btw, just in terms of raw complexity the code is getting simpler while having more functionality 
okay got it down to 479 lines of actual rust code ( there's 71 lines of comments and 45 blank lines ) and i believe i have re-enabled the capability of reusing EcsTask to persist system parameters, i'm testing the code on dloms branch now
okay there's a deadlock heh
i am working on solving it!
Okay i understand the issue
just tested and had no issues on debug/release on desktop !!
@fringe ridge when it says
/// A function used by `bevy_app` to tick the global tasks pools on the main thread.
/// This will run a maximum of 100 local tasks per executor per call to this function.
///
/// # Warning
///
/// This function *must* be called on the main thread, or the task pools will not be updated appropriately.
pub fn tick_global_task_pools_on_main_thread() {
Does this take into account multiple bevy apps running simultaneously that were started on different threads?
but it fails to compile for wasm
yep
oh same function lol
to compile for wasm in your case you should just be able to comment out that line
and then it should work?
Honestly, I'm not sure - I'm not deep into the task stuff. But I would guess this doesn't consider that since the task pools are all static
hmm, lemme try to figure out via git history who invented the comment
I would very much love for our task pools to not be static personally lol
I've had to make tests resilient to our task pools being overloaded lol
It would be nice if each test could have its own task pools
yeah 🤔
i think there might be some fundemental issues here
actual correctness issues not considering the idea of tests or multiple bevy apps running in parallel
but i'm not sure, it really depends on what the comment means, because each bevy app calls this on not the main thread
hmm @fringe ridge git says you invented this function, where did you port it from?
deadlock :(
(wasm, debug, commented that line out)
My git blame says nth wrote it lol
#20369
@proven sandal could i pull you in here?
when it says
/// A function used by `bevy_app` to tick the global tasks pools on the main thread.
/// This will run a maximum of 100 local tasks per executor per call to this function.
///
/// # Warning
///
/// This function *must* be called on the main thread, or the task pools will not be updated appropriately.
pub fn tick_global_task_pools_on_main_thread() {
Does this take into account multiple bevy apps running simultaneously that were started on different threads?
or how does it end up interacting with that
and release wasm
I believe this may be used to satisfy nonsend resource requirements.
That said, oof, I think this really might need to be unsafe with a proper safety comment
I will look when I can
Ooof yeah
Thanks!
I think i might be able to be very sneaky and avoid having heap allocations at all which would be cray cray. Would mean you could easily create an incredible amount of these futures with minimal perf impact
Idk
Maybe not
How bad is one arc 🤔
I guess I should really benchmark things
it shouldn't be unsafe. The non send tasks are stored in the thread local executors.
so what happens if it’s called on the wrong thread?
The tasks on the main thread potentially don't run. This method is mostly needed when bevy is running single threaded. When bevy is multithreaded TaskPool::scope also runs tasks.
I guess it depends on what thread it's being called on. So if it's a task pool thread it doesn't really matter since there's other ways that tasks are being run. If it's not a task pool thread, then it needs to be called if some non send tasks have been queued on that thread.
So, unless I call the tick global tasks method before blocking bevy from continuing execution of stuff, for the executor, even when spawning non-local tasks, those futures will never actually get polled.
If we do some sort of condvar notification, or channel, anything, that waits for my future to polled, and in the meantime blocks bevy then it never executes on other available threads that the task pool has access to. Why?
And why would ticking the local executor execute tasks spawned on the non-local executor?
I will make a minimum reproduction to show you what I mean
Please hold
Wait maybe not, hmm, okay something else is wrong with my code
My minimum repro isn't hitting it
Forget everything i said
I think i was totally wrong
Which makes sense because it made no sense how local executors would impact non local tasks
Lmao
Okay wait, so.....
Uhh
Okay I still need the tick global task pool
But I fixed the deadlock
Lmao i'm so confused as to why I need tick global task pools
I think i still must not understand
Something fundamental here
I think i will try to create a minimal repro to see what triggers the nessecity of the tick global task pool thing
It should be very informative
Okay actually no
Its just spawn local vs not
Everything makes sense now!
I have two last things to do
First is make Gingeh's project work on wasm
Second is to switch from using schedules to system ids a-la carts instructions

Okay did the switch from schedules to interned systems, not system ids
Instead of schedules

Yeah this is better, and we can do a custom trait/derive to make it a unit struct that you schedule around
I wanna figure out how to get the condvar equivalent working on no-std so we can get this working on no-std as well
Going to have to make a pr for that to bevy first to add that
Wish we had bushrat 🥺
Okay
Have a smol pr i'm gonna put in
In order to make the amount of unsafe even smaller, and easier to reason about, by allowing
One to create an unsafeworldcell directly from a pointer
@lethal adder you willing to give a review?
this is helpful but not nessecary for the async ecs stuff, it just makes it a little neater and cleaner
i prefer to not forge static lifetimes
I can give a look this weekend, dont have time during the week though 🙏 Appreciate the grind though
okie
oof okay, so
I am here!
[
12.123µs,
12.203µs,
12.363µs,
12.383µs,
12.404µs,
12.464µs,
12.484µs,
12.504µs,
12.594µs,
12.633µs,
12.694µs,
12.785µs,
12.794µs,
12.854µs,
12.884µs,
12.885µs,
12.905µs,
12.914µs,
12.914µs,
12.924µs,
12.934µs,
12.995µs,
13.005µs,
13.085µs,
13.094µs,
13.104µs,
13.144µs,
13.165µs,
13.165µs,
13.195µs,
13.215µs,
13.215µs,
13.235µs,
13.285µs,
13.345µs,
13.475µs,
13.545µs,
13.576µs,
13.837µs,
13.967µs,
14.057µs,
14.117µs,
14.137µs,
14.267µs,
14.277µs,
14.638µs,
15.029µs,
16.05µs,
21.02µs,
1.27114ms,
]
This is the timings for
fn spawn_tasks(world_id: WorldId) {
let pool = AsyncComputeTaskPool::get();
let task = world_id.ecs_task::<(
Local<u32>,
Commands,
Res<BoxMeshHandle>,
Res<BoxMaterialHandle>,
)>();
pool.spawn(async move {
println!("I am here!");
let mut timings = vec![];
for _ in 0..50 {
let start = Instant::now();
world_id
.ecs_task::<()>()
.run_system(async_system, |()| {
})
.await;
let end = start.elapsed();
timings.push(end);
}
timings.sort_by(|a, b| a.cmp(b));
println!("{:#?}", timings);
}).detach();
}
I've ported over all my fixes and other things to the pr in the branch but haven't pushed it
that 1.2 ms is just the time it takes from the task getting spawned in startup to the actual update schedule running afaict
but the average 12-13us is not what i wanted to see
this means you can only call an async ecs task a few hundred times per frame before it starts to eat into your budget 😔
OH
OH WAIT
I am here!
[
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
60ns,
61ns,
70ns,
70ns,
70ns,
70ns,
70ns,
70ns,
70ns,
70ns,
71ns,
71ns,
71ns,
71ns,
80ns,
80ns,
90ns,
90ns,
90ns,
91ns,
100ns,
140ns,
251ns,
11.652µs,
913.072µs,
]
haha fuck yeah
i wasn't caching the EcsTask struct
pool.spawn(async move {
println!("I am here!");
let task = world_id.ecs_task::<()>();
let mut timings = vec![];
for _ in 0..50 {
let start = Instant::now();
task
.run_system(async_system, |()| {
})
.await;
let end = start.elapsed();
timings.push(end);
}
timings.sort_by(|a, b| a.cmp(b));
println!("{:#?}", timings);
}).detach();
WE ARE IN NANOSECONDS BABYYYYY
LETS FUCKING GOOOO
HAHAHAHHA
you seem happy
yeah, nanosecond range for ecs tasks is exactly where i wanted to be
async ecs seems really cool
is there like an example snippet anywhere? I wanna see how it would be used
you can do 277,000 async ecs tasks now, ( though it would eat your whole frame budget ) in a single frame
yeah! gimme a sec
i wrote one for loading screens
// This is the implementation
#[derive(Resource)]
struct LoadingScreen {
total: u32,
current: u32,
}
fn add_to_loading_screen<Out, T: Future<Out>(input: T) -> impl Future<Out> {
async {
async_ecs(|loading_screen: ResMut<LoadingScreen>| {
loading_screen.total += 1;
}).await;
let out = input.await;
async_ecs(|loading_screen: ResMut<LoadingScreen>, mut commands: Commands| {
loading_screen.current += 1;
if loading_screen.current == loading_screen.total {
commands.trigger(LoadingScreenEnd);
}
}).await;
}
}
// And this is what it lets us do
let (gltf_1, network_request, _) = (
asset_server.load("gltf1").add_to_loading_screen(),
network_request.add_to_loading_screen(),
material_pipeline.add_to_loading_screen()
).join().await;
oooh thats cool
yeah!!!! i think so too!
I wonder what the base time for a non-async system is, so you can provide an average ratio for cost.
I mean... i want to optimize it more
there are more optimizations to be had
individual arc clones to eliminate
etc
buttttt
it is more than fast enough now for merging and being very useful
in terms of just speed
For sure, was just curious what existing overhead there is in just system running in itself. Basically how much faster CAN it be.
Beware the dark path! Leave the cave!
No! 🥺 I want to optimize!
hehehe
i mean to be fair most of my effort has already gone into optimize the heck of this
including the loc
only 462 LOC for the actual file with stuff
and that includes quite a few comments
and docs

it should be nice and reviewable
speaking of which, i am going to push to the PR now!
also it does what cart wanted
which is using systems to schedule 'sync points' for it instead of schedules
so that blocker is out of the way
Good!
and now it's failing all the CI! yayyy
🥴
ready for reviews tho
if anyone wants to provide
Link it!
it's the same one, i rebased on main
the nice thing about your code not really touching the rest of bevy is that bevy can jump 2 whole versions and rebase still works
Now, even though i documented the code heavily, i did invent a whole new concept here, one which relies on a lot of custom low-ish level primitives, so if anyone wants to hop in a screenshareing voice call so i can explain how exactly everything works, i am super down
it's complicated stuff i have been banging my head against a wall to fix each and every last bug
but i think they are finally all squashed
OH WAIT
okay the ci is breaking because i am linking to a local crate instead of the published one lmao
truly awesome commits
hm this new api is quite confusing
yeah it's a little more confusing, it was a cart requirement
but in follow-up we'll add a macro that let's you do
maybe it could be paired with a bare-minimum example?
#[derive(SyncPoint)]
struct MySyncPoint;
app.add_systems(Update, MySyncPoint);
and then
task.run_system(MySyncPoint, || {})
because the current example is doing a lot more than just the bare minimum to run a system in a task
true!
and maybe async_system should be given a different name and a doc comment
because it aint async, and it's not what's being run in a task
could the macro be avoided by using a generic system instead? (like app.add_systems(Update, tick_async_ecs::<MySyncPoint>))
just spitballing, no clue if that's actually viable
like this
fn tick_async_ecs<Marker>(world: &mut World) {
run_async_ecs_system(world, tick_async_ecs::<Marker>);
}
I guess that does mean needing to write task.run_system(tick_async_ecs::<MySyncPoint>, || {}) :/
unless it's included in the implementation of run_system
or a wrapper
Incredible idea!
Let's do that 
@vocal vortex done
yay!
take a look and see what you think 🤔 i wanna try to come up with names that are preferably one word because it's going to show up a lot and be effectively 0-information
yeah it's unfortunate that you need to write out task.run_system(tick_async_ecs::<MySyncPoint>, || { every time you want to run a system
ideally it could be task.run_system::<MySyncPoint>(|| {
OH 🤔
it could be
yeah
or we could even have it just be task.run_system(MySyncPoint, || {})
with the .add_systems(Update, async_sync_point::<MySyncPoint);
Would it make more sense for the Sync Point to be provided to the ECS Task?
Since the Sync Point determines when the Task is polled, and not necesarrily the System, also the Task represent the Params for a System, so the Sync Points could make sense as well.
just ported suborbital to use the new api and the animation is noticably smoother 👀
btw it still doesn't compile on wasm
Oh yeah, I wonder if it will work if we just cfg it out on wasm
I didn't realize that was the issue
Also 🤔 If the Params or Access desired is paired with Label or Timing desired, then it could be an Trait or part of the Sync Point as an assoc type, so you dont have to provide it when calling, and also if you were to limit polling the async to only the async_sync_point::<SyncPoint>() system, then you can provide an system impl that uses the Params for parallel. (Where the default Params/Access is World)
Would change the API I think, because you would probably want to support any sub-set of Access/Params of the Task, so if you call try_run_system().unwrap().await it checks the Access of the provided System's Params and ensures it overlaps with the Access of the Task's Params, since we dont have a way to check sub-set/subtyping of Params at compile time. Maybe can still have run_system for T where T: SyncPoint<Params = World>
Mostly just yapping though
WAIT yes maybe we can get parallel schedule execution this way 

now the problem is, the first time you create the system params we need world access in order to initialize it
wait we also need world access in order to apply system state 🤔
i feel like this is possible tho
this would make it SO good because then everything could run in parallel
just re-using the bevy schedule infra
then though, your sync point would have to specify the system params it cares about
🤔 but if it does that then do we even need system init or apply system state, cause the sync point IS that system init and the system state application
inherently

so it's a different semantic setup, that being said the downsides are pretty big
one of the really nice things about the current setup is the sync point lets you do arbitrary system params at that sync point
and this setup wouldn't allow that
and that flexibility is super nice and ergonomic when writing code
just adding and removing system parameters as you please
I think actually, that the tradeoff of extremely ergonomic code-writing is worth the non-parallel execution 🤔
in any case you aren't going to likely be executing code every frame if it's async, so parallel execution matters a lot less
there are cases where you would
we could always offer both 🤔 but that might be confusing 
So, the two alternatives are effectively, we have this:
struct MySyncPoint;
app.add_systems(Update, async_sync::<MySyncPoint>.after(other_system).before(system_a));
async {
world_id.ecs_task::<Commands>().run_system(MySyncPoint, |commands| {
});
world_id.ecs_task::<(Commands, Local<u32>)>().run_system(MySyncPoint, |commands| {
});
}
vs
struct MySyncPoint;
app.add_systems(Update, async_sync::<MySyncPoint, Commands>.after(system_a).before(system_b));
app.add_systems(Update, async_sync::<MySyncPoint, (Commands, Local<u32>)>.after(system_a).before(system_b));
async {
world_id.ecs_task::<Commands>().run_system(MySyncPoint, |commands| {
});
world_id.ecs_task::<(Commands, Local<u32>)>().run_system(MySyncPoint, |commands| {
});
}
so the latter requires you to register an independent system sync point for each combination of parameters you want to use, but it runs everything in parallel with bevy
automagically
Thoughts?
( also the latter one Might actually in practice end up less performant because batching together everything combined with the waker and poll stuff given how i'm doing things, i think it may actually come out that it's less performant. it might be more tho! )
but in cases where you have long complicated code running in the async blocks it def would be faster
like super bigly
@vocal vortex pushed the even more ergonomic sync points btw
also should have hopefully fixed it not building on wasm?
i need to cleanup some doc comments and the PR description too, but then after that i think we will be in a good spot to get this reviewed and hopefully upstreamed 🥺
What if, you do the latter but keep SystemState on the Task instead of getting it from the Sync System, so then for the subtyping you only have to worry about Access checks matching, but the State init would require.....permanent Global UnsafeWorldCell 😈 /hj
Huh
i'm getting a CI failure in an unrelated crate 🥺
@glossy gyro halp, what should i do?
$ cargo fmt --all -- --check
Diff in /home/runner/work/bevy/bevy/crates/bevy_asset/src/io/embedded/mod.rs:419:
#[cfg(test)]
mod tests {
- use super::{_embedded_asset_path, EmbeddedAssetRegistry};
+ use super::{EmbeddedAssetRegistry, _embedded_asset_path};
use std::path::Path;
// Relative paths show up if this macro is being invoked by a local crate.
thread 'main' (3782) panicked at tools/ci/src/ci.rs:62:13:
One or more CI commands failed:
format: Please run 'cargo fmt --all' to format your code.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
New PR off of main to fix. Then merge in main
oh for some reason my local cargo fmt
is different than yours
Huhhhhhhhhhhhhhhhhhhhhhhhh
Nightly prolly
Damn CI got hands
@eager trout Talk me through the principles of what makes up the new API?
Yey! So we get access to a couple of things, it's rather complicated so if you want to head to bed and talk about it another day don't let me keep you up.
But i'll post it here so you can look at it later
So in systems you get access to WorldId as a ecs parameter, but also world.id() also gives you it, this is important, it's used for the following function:
world_id.ecs_task::<(SystemParams)>()
Where SystemParams are your normal system parameters, if you want multiple you stick them within the tuple and do the normal tuple-ish thing.
let ecs_task = world_id.ecs_task::<(Local, Commands, Res<Thing>, ResMut<OtherThing>, Query<Entity, With<Blue>));
This ecs_task can be taken by reference but also cloned and it persists system parameters like Changed, Added and Local.
You can pass it to multiple threads on different async tasks and have them interop and cooperate.
But i'm getting a little ahead of myself, so what you can do with ecs_task is call .run_system on it. Now, importantly, this isn't a real bevy system, it is a little more powerful.
async {
let mut thing = vec![];
ecs_task.run_system(|_| {
thing.push("h");
}).await.unwrap();
thing.push("i");
println!("{:?}", thing);
}
You can mutably capture stuff from your environment and use it inline within the closure, this means you can just use local variables and do stuff to the ecs with them. You can't do this with normal world.run_system you have to use the .run_system_with and such.
// This is the implementation
pub struct SyncPoint;
pub struct LoadingScreenPlugin;
#[derive(Resource, Default)]
struct LoadingScreen {
total: u32,
current: u32,
}
impl Plugin for LoadingScreenPlugin {
fn build(app: &mut App) {
app.init_resource::<LoadingScreen>();
app.add_systems(Update, async_sync_point::<SyncPoint>);
}
}
fn add_to_loading_screen<Out, T: Future<Out>(input: T) -> impl Future<Out> {
async {
async_ecs(SyncPoint, |loading_screen: ResMut<LoadingScreen>| {
loading_screen.total += 1;
}).await;
let out = input.await;
async_ecs(SyncPoint, |loading_screen: ResMut<LoadingScreen>, mut commands: Commands| {
loading_screen.current += 1;
if loading_screen.current == loading_screen.total {
commands.trigger(LoadingScreenEnd);
}
}).await;
}
}
// And this is what it lets us do
let network_request = //...
let material_pipeline = //...
let (gltf_1, network_request, _) = (
asset_server.load("gltf1").add_to_loading_screen(),
network_request.add_to_loading_screen(),
material_pipeline.add_to_loading_screen()
).join().await;
This is an example of a theoretical api one could build ontop of this primitive, which showcases how incredibly powerful and flexable it is.
ecs_task.run_system can be used on any arbitrary async runtime, whether that be tokio, bevy's task pools, or any third party or custom async runtime, it's async runtime agnostic
This is a new primitive, so while it can be used by itself, it's intended to be the low-level way that async access is done, and higher level abstractions can be built ontop of it.
For example, one can use this to build a 0-copy networking <-> bevy interaction. By passing through arbitrary parameters one can read a buffer that is rkyv, use the data arbitrarily in bevy, and then hand the buffer back
let mut vec = vec![];
let ecs_task = world_id.ecs_task::<(MessageWriter<AudioData>, Single<&PlayerPosition, With<MyPlayer>)>();
loop {
async {
network.read(&mut vec).await;
let data: &[(PlayerPosition, &[AudioData])] = vec.parse();
ecs_task.run_system(SyncPoint, |mut audio_data_writer, player_position| {
// can use data even though it's lifetime is only for this async block *because* of the async_ecs integration's power
for (other_player_position, audio_data_many) in data {
if other_player_position.distance(player_position) > 500.0 {
continue;
}
for packet in audio_data_many {
audio_data_writer.write(packet);
}
}
}).await.unwrap();
// reuse allocation
vec.clear();
}
}
this is instead of having to pass the audio_data through channels, and having to have some sort of way to store the player position in a mutex that you access and update from the bevy ecs side and read on the async side
Right, so the thing the PR introduces is this ability to call ecs_task on WorldId (a Copy type), which lets you use async queries within asynccomputepool tasks and the way this is tied into the invariants of bevy ecs is by introducing sync points in the schedule where polling happens reliably?
yes!
oh yes the sync points
so you do sync points by just creating an arbitrary struct, and by adding it to the system with .add_systems(Schedule, async_sync_point::<T>)
importantly, when you call ecs_task.run_system(T, || {}) you don't have a schedule along with T so if you add the system to multiple schedules it will try to do this .run_system on any one of the schedules
ALSO important to note is this functionality does not work on web ( yet ), it's coming in a follow up pr
not async queries per-say, it lets you use system parameters async-ly, not just queries but other system parameters like Local or Res or Commands
right yes :p either way it's "these computations are happening at these sync points" right?
yep!
If you don't register sync points, does it default to just kinda dangling?
yeah if you don't register the sync points then it doesn't end up ever running the futures they just sit there
🤔 i could probably add some sort of detection for that
where if a future is sitting on an un-registered system sync point then it logs an error
From an DX point of view that'd probably be greatly appreciated :')
hahaha yeah
i don't wanna make this pr anymore complicated than it is but i def want to add that in a follow up, thanks for the idea!
i will absolutely be doing that
So the hook to grab people with is that we get to do "system stuff" in AsyncComputeTaskPool contexts now. The sync points are a core part of the API way we get this working, so they should get mentioned more or less immediately afterwards. The kinds of apis and features this enables are a good thing to end a release note on, or focus a showcase example around but are secondary to explaining. The information above is more or less enough information to bring a skeleton of a release note together, I'm struggling to find a similar-scope release note to reference structure + length with also it's almost midnight for me. If you want to write a "shitty first draft" release note I'd be happy to look at it tomorrow morning if no-one else does first, but if this has helped rubber duck a way of framing this in release note form just go ahead :)
yeah, and i'll look through bevy's release notes to try to figure out how to structure this to sound, somewhat good
😂
it's been a long day for me too though, you should go to bed, i'll work on smth tmrw if i don't get to it tonight
the shitty rough first draft, but it is nice to just get everything down on paper here first
For sure ^^
@eager trout, poking at https://github.com/bevyengine/bevy/pull/21744 now. If you're going to be force-pushing to rebase can you at least collapse the git history 🥲
awawawa, like do a git squash?
Yeah
sure, and just to be clear you want me to squash all of the history here?
I think that's probably best. We don't need 50 comments of "ci plz"
(I suggest you use git rebase -i and pick which ones to squash)
But that depends on if there's history you want to preserve
@eager trout reviewed ❤️ I'm interested in seeing more, but there's quite a lot of cleanup needed before we can really understand what's going on here
I also don't at all understand why async_task is a method on WorldId, which is just a dumb ID
is this a, i should add a comment explaining why it's WorldId, or you want a description here?
So because rust's type system is incomplete, it can't actually infer type parameters in a closure when the closure is the kind it needs to be for the async primitive, so because of that you have to put the parameters somewhere explicitly
doing .ecs_task means that you only have to put the parameters there, having to put the parameters on the .run_system results in you having to do <(), _, _, _>
I want it to not be on WorldId 😅
Yes please
EcsTask::new::<(Commands, Query<&A>)>(world_id);
Much better!
We could also add a helper:
commands.spawn_async_task::<Commands, Query<&A>(||{})
oh no, so like, you will want to be passing in a WorldId around because it's just a u32
so it means you can pass that into async blocks
and stuff
this will be going onto different threads and such
But you can just infer the WorldId based on the commands that generated it
true, this would be a higher level thing though, i don't want to put any of those things in this pr 🤔
Okay, that's fine, we can start with a nice simple low-level API 🙂
@glossy gyro is the complexity in the example the extra stuff i added, i.e. should i just make it a 1-1 port of the other async example ( which is what it was based on ) but instead using async access, or do you want a whole different async access example that is actually just simpler
An example showing how to offload work to background async tasks using channels for communication.
also same question goes for the nits around box mesh handle being combined and such
Make an example that's actually good please 😅 And delete the old one probably
Thoughts on the queue_async / AsyncWorld pattern?
It obscures the raw primitives but maybe thats a good idea for a 'hello world'.
#[derive(Event)]
struct Response(String);
fn main(){
App::new()
.add_plugins((MinimalPlugins, AsyncPlugin))
.add_systems(Startup, start_request)
.add_observer(on_response)
.run();
}
fn start_request(commands: Commands) {
// queue_async spawns the async task and provide an `AsyncWorld`
commands.queue_async(async |world: AsyncWorld| {
let body: String = fetch("http://example.com").await?;
// `AsyncWorld` contains an async equivelent of each `World` method
world.trigger(Response(body)).await;
Ok(())
});
}
fn on_response(response: On<Response>, mut commands: Commands){
println!("response: {}", response);
commands.write_message(AppExit::Success);
}
Of course there would be a lot more design work to do to actually get a pattern like this upstreamed,
but if we kept these opinions in examples/utils for now could be enough to demonstrate a nice way of using async bevy.
More details in my bevy_malek_async fork
The idea of async systems is that, while they are computing, they do not read or write anything to the world? So they do not block the regular systems and frames
Its an interesting idea for a more user friendly api built on bevys task pools!
could also be nice to declare the runtime in the plugin
I think in this case, the engine should only provide the most useful basic primitives, and the community can develop abstractions as needed on top
@glossy gyro so i was looking at your comment on why the thing needs to be a global static, and this is something i've struggled with for a while, thinking of ways to get rid of that, but on my walk home i finally came up with the fix! I think i can get rid of ALL statics here, make it entirely driven off of arcs stored in the world
so i am going to try to do that
( as well as address all your other feedback )
but i am very excited 
down to 338 lines of actual source code in the main file where everything goes down now 
( not including comments and whitespace ) still need to properly comment ALSO now we need an AsyncEcsPlugin in MinimalPlugins and DefaultPlugins 
so we can be sure to init our resource in it 
i haven't actually tested it yet but i'm pretty sure it works, will get back to it later tonight and addressing the rest of alice's feedback as well
also another exciting correctness improvement, now we work off of a Weak and if we can't upgrade it into an Arc then the future returns a poll error specifying that the world has disappeared
which is an edge case that previously would have had you just, waiting forever
in that async task
so yay!
ugh this is SO much better
this is so GOOD

i wish i had more time... i will have to work on more cleanup and doing things tmrw
also
i want advice from people on renaming things
because i'm terrible at naming things
and i have had to name a few more things here, and just in general
name fixes for structs and such plz, rn i have structs like
#[derive(PartialEq)]
enum TickAsyncTasksResult {
MoreTasksToTick,
NoMoreTasksToTick,
}
and
struct AsyncSystemState {
system_state_handler: Arc<dyn SystemStateHandler>,
waker: Waker,
wake_signal: WakeSignal,
initialized: bool,
}
struct AwokenAsyncSystemState {
system_state_handler: Arc<dyn SystemStateHandler>,
wake_signal: WakeSignal,
}
struct NeedToApplyAsyncSystemState {
system_state_handler: Arc<dyn SystemStateHandler>,
}
try abbreviating Async System State 👍
with TickAsyncTasksResult, u could shape it like struct AnyTasksRemaining(bool)
I'd expect something with result in the name to be result shaped
Irl I had a bug with my code last night
I dreamt of the fix
At work so I can't implement it but i'm pretty certain it will work
I have a question for the people in this chat, where do you think the code belongs?
For your information now the code requires a plugin in order to function properly. The code also does not require any ecs internals in order to work. This code could go anywhere, but I think we want it in the MinimalPlugins. So should I move it from bevy ecs to bevy app? Or leave it in bevy ecs? Or put it somewhere else?
I'm hesitant to put it in bevy_task because its not reliant on any of bevy's actual async stuff, one could use it with tokio for example. But maybe bevy_task is the right place to put it
Thoughts?
Talked with ChrisBiscardi
Given the fact that future higher level apis would integrate with bevy's task pools
Putting it in bevy_task makes sense
If anyone else has thoughts or opinions speak up plz
It could also be its own crate, bevy_async
Bevy owns that
Idk what the bar is for splitting things into their own crate, but this is conceptually pretty different from what bevy_tasks currently has inside it
So maybe that would be for the best
for the theme it should have versioning that is entirely out of sync with the rest of bevy
Yeah I think i'm going to just throw it in bevy_tasks
if it needs Plugin it can't go in bevy_tasks as that would make bevy_ecs dependent on bevy_app
Ooo
True didn't think of that, ill put it in a new crate
not bevy_async though pls, bevy_async_ecs or something
call it bevasync_y
Why not bevy_async? 🤔
I own that crate name and I'm happy to give it up for this, if that's the name that sticks. My non-biased opinion is that bevy_async is a better fit
I'm going through and rewriting everything, renaming everything, modularizing it a bit, documenting everything as i go, i will want everyone's input on how things should be named and called, but i don't know if i will have the refactor done by tonight
i'm trying to get it done by tonight
FIXED UP HEAVILY DOCUMENTED PR INCOMINGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG
( just testing some things first and doing a final pass over all the documentation )
@glossy gyro if i could request a re-review, i did do a simpler example, but i still copied the other async_channel example
I also made sure this didn't have any straggling outdated documentation
and added doc-tests and better docs
Okay 🙂
I'll probably be until Sunday to have time to re-review
I've gone over the new crate extensively in an attempt to understand it, and here's the full writeup of my analysis:
The objective here is to coordinate three participants that want to share &mut World access:
- The main Bevy schedule
- Futures and async tasks running on other threads
- The bridge driver between these two (introduced in this crate)
Invariants of this crate:
- Normal rust safety invariants for
&mut World(aliasing) - At most one future has world access at a time
- Futures only access the world while the scoped pointer (managed by the bridge driver) is live
- SystemState is always initialized before use
- Deferred ops are only applied after every future finishes polling and releases world access
- The driver can't deadlock
- All futures that want world access can eventually complete (assuming fair scheduling by the async runtime)
- If the world is dropped, futures don't leak and eventually finish (in an error state)
The protocol:
Futures (tasks on worker threads)
| enqueue requests (create signal guard clones: one kept, one sent)
v
Driver (exclusive system, world-owning thread)
1. Drain request queue for this sync point
2. Initialize SystemStates
3. Publish World pointer (via scoped_static_storage). Future access scope begins
4. Wake all drained futures
-> Futures race for locks (non-blocking)
-> Success: acquire both locks, do work, complete
-> Failure: signal driver (Drop signal guard), re-enqueue later
-> Direct access: non-queued future polled during scope,
bypasses queue, acquires locks, completes (no signal)
5. Wait for all signal guards to drop + scope mutex released
6. Unpublish pointer, scope ends.
7. Apply any deferred ops from SystemState of polled futures
8. Loop (up to MaxAsyncTicksPerSyncPoint) or return
v
Schedule resumes (normal systems run)
Dual locking:
- The published World pointer lock is managed by the ScopedStatic primitive in scoped_static_storage (only one future can lock this at a time)
- SystemState locks are managed by the SystemStateStore primitive of this crate (futures using different SystemState types can work in parallel)
Preventing driver deadlocks when futures panic:
- If a future panics while holding locks, rust's panic unwinding drops destructors in reverse scope order
- First, the SystemState MutexGuard drops (releasing the lock)
- Second, the World pointer's scope MutexGuard drops (releasing the lock)
- Finally, the guard signal constructed by the future during
poll()drops, and the driver is notified
How futures can fail cleanly:
- If the async bridge cannot be reached (
Weak::upgrade()fails duringpoll()), the world has been dropped and the future cannot complete - If SystemStates are invalid, they can't be used and the future cannot complete
- Regardless, the future returns
Ready(Err)and completes permanently
As a short conclusion, I believe that the crate faithfully implements this protocol in a correct way, upholding all the invariants
@eager trout ping

I think that something like TLA+ could be used to formally prove that the protocol actually upholds all the invariants, but actually doing that is out of the scope of my knowledge
https://lamport.azurewebsites.net/video/intro.html 20 minute intro
I also plan to open a PR targeting your branch with some naming suggestions (and a few other minor tweaks)
Yes please!
this is a great writeup and summary, thank you so much
when i get home tmrw i'll include this in the PR description, i think it summarizes the complex logic well 
perhaps this should go somewhere in the code? As a comment?
🤔
big thinkies
talked with alice, i think tmrw i'm gonna just yoink your thing and put it in the lib.rs @young sorrel
or if you wanna make a pr targeting my branch with your suggestions, also include this description
in the lib.rs
or maybe as a comment over the plugin 🤔
i think maybe as a comment over the plugin is best, that way when someone hovers over the plugin they get to see how the whole system works 🤔
idk
crate docs please!
We should use crate docs much more
oh! i didn't know this was a thing 👀
For the viewers at home: this is evolving quite rapidly behind the scenes
web vs std semantics are quite complicated
Did you see the web_task pr and corresponding discussion in #windowing-dev ?
I have grand plans to make this entirely uniform. ConditionalSend needs to die as well.
Is there any way in which, for the single threaded non-send runtime, we could also expose some sort of .tick functionality? Like with bevy's normal non send task pools having that?
So we could have it tick also outside the micro queue?
why would we want that? I’m working on removing tick from the multithreaded executor and making the entire ECS event loop async
So, if the async stuff that is non-send doesn't have a tick functionality, it can only run in between frames, and this means that any async-ecs interop can't happen at pre defined sync points inside bevy frames
@proven sandal this describes the execution model
ok I think what we want is compatible. I want the main schedule runner to be a future you have to poll to completion (on all platforms). I want this because I think it will simplify windowing on the single-threaded web target. polling that future is basically “tick” right?
Yep!
great then we want the same thing
So how does this interop with the micro queue?
Or i guess I can wait until you write your code and see
See we do have a hacky workaround for web
Where we do a lil unsafe and leak world context into a static in-between frames, so that way we can access it during the browser micro queue stuff
But that eliminates the current sync points, in favor of the ecs async interop happening in-between the end of the current frame and the start of the next one
frames will be worked on across multiple micro-tasks
potentially each system will be queued separately, idk, depends on perf
hmm but they can't be queued during the current frame without explicit tick calls and waiting right? Otherwise it would violate non-send requirements
I don’t totally follow.
Hmm maybe I didn't understand and prematurely got on the same page haha
So, for non-local futures
In order to run them during the frame execution time ( not in-between frames ), becauss you only have one thread they can live on
They can't get sent to web-workers
So you have to manually have some way of ticking / polling futures
During frames
If u want to do async work during frames
If you want to see the justification of why this is incredibly useful, and you have the time to read 1,000 loc pr
more broadly I am trying to keep the async runtime running on the main ECS thread so that schedules can run concurrently with other work, on all platforms.
I think that will satisfy your needs
Okay!
If the schedule is a future then can await other futures, this would simplify a lot of things. Notably we could wait (async) in the driver with event-listener instead of CondVar (blocks thread)
Which works on all platforms and std configurations I believe
Then every closure could be non static and the static closure path could be removed
yep
having .await sync points would make your work here a lot easier
Sounds awesome, I think I understand enough about async now to help review it
When it's ready
might be a while, async schedules require systems that return futures (because schedules are systems)
still working out the best way to make that not terrible
What's built here already wraps a fn(SystemParam)->out inside a future, maybe there's some opportunity to share functionality here
Assuming that getting systems inside futures could already be a solved problem, how drastic is it to change the scheduler into an executor?
got it
I think I found a way to make the entire App always Send (and the RunnerFn too). The restriction moves into bevy_winit where the app must still be ran on the same thread it was built on
interesting
have you read my winit plans
making the ecs/app Send is super important because it's vital that we have the ability to move it out of the winit thread
if i can schedule systems that are futures, then this entire problem is solved basically.
maybe i need to review your code, you may have already done what i needed
where's the working tree for this
here's the core change, let me know if you think this is sane:
// Store EventLoop in TLS so the runner closure doesn't capture it (!Send).
PENDING_EVENT_LOOP.with(|el| *el.borrow_mut() = Some(event_loop));
app.set_runner(|app| {
let event_loop = PENDING_EVENT_LOOP
.with(|el| el.borrow_mut().take())
.expect("EventLoop not found in TLS, was the App moved to a different thread?");
winit_runner(app, event_loop)
});
static PENDING_EVENT_LOOP: RefCell<Option<EventLoop<WinitUserEvent>>> = const { RefCell::new(None) };
i mean, i want to just make App be actually send.
and I want set_runner to take a future.
so instead we will do
let app = ...
spawn(async {
loop {
app.redraw_requested().await
app.update().await
}
})
winit_runner(event_loop);
on native the winit runner takes over the main thread, the ecs app is sent to a different thread and polled by the executor (which owns the thread).
on web, the winit runner does some cursed shit using an exception, but the ecs updates concurrently with it by running in browser micro-tasks.
no idea if this breaks something wider, but running examples on native still works for me
yes I remember reading this a while ago
maybe i'm dumb, what does this actually buy us? why is a runtime-send restriction more useful?
right now? not much I think, apps can move between threads for headless
are nonsend resources not an issue?
World catches that at runtime if I understand correctly, right?
the idea is to eventually move the App to its own thread where it communicates with the main winit thread
thread::spawn(move || run_app(app, runtime_events_receiver));
from your doc
this commit is just a stepping stone towards this vision
Reading through this, I think there's some other small stepping stones that can be done now
hmm, can't do much more without implementing the whole thing. Gnarly
yeah, fortunately a lot of it is already complete. we're just waiting on some SMEs to have their time freed up
@proven sandal
This does what you want but significantly smaller
And is also compatible with a version of exec_local
If done on world
Where the future doesn't have to be send/sync
Also it doesn't box the future
this is not entirely the same
despawning the entity doesn’t cancel the task
not sure how important that is, but I thought it was a nice feature of my impl
otherwise yeah this looks nice, it makes sense that you could build this on top of async world access
Oh true!
I’d say either put a Task<()> in a component on the spawned entity, or don’t spawn an entity and make the observer global on output type.
well, I’m going to do some other stuff now, happy that this has given you ideas
No it is nice
It would be nice if we could have that
this will give you that
Btw, I really do like your design
Its very fun
There's lots of fun interesting ways these things can play on eachother, and I want to explore them to make everyone more productive!

@young sorrel doing it this way would require the static work jobs thing, so I am becoming more and more partial to a two paths system 🤔
Oh sorry nth I didn't mean to ping you
Please disregard 😅
Yes I think both methods can be useful
I also have the forbidden third method that lets web run non-static closures with some unsafety involved
Hmm further thinking on this, what if we could make it very easy to wrap any arbitrary future to send it's result via an observer, within any arbitrary future. So something like:
struct FutureToObserverPlugin;
impl FutureToObserverPlugin {
fn build(app: &mut App) {
app.add_systems(Startup, drive_async_bridge::<FutureToObserverSyncPoint>);
app.add_systems(Update, drive_async_bridge::<FutureToObserverSyncPoint>);
//.. etc
}
}
struct FutureToObserverSyncPoint;
trait AsyncBridgeExt {
async fn trigger<O: Send, F: Future<Out=O>>(&self, future: F) {
let out = future.await;
self.new::<Commands>().access(FutureToObserverSyncPoint, |mut commands| {
commands.trigger(Ready(out));
});
}
}
This would let you, inside any async block where you have piped through your async bridge ( which is easy cause it's trivially clone-able and sendable ), arbitrarily easily trigger observers on futures just inline whenever during your async code.
i.e.
struct HttpRequest(body: String);
async {
some_async_stuff.await;
other code
async_bridge.trigger(HttpRequest(reqwest::get("www.google.com"))).await;
other code
async_bridge.trigger(HttpRequest(reqwest::get("bevy.org"))).await;
}
And you have
App.add_observer(|http_request: On<HttpRequest>| {
});
or some such
i wonder if there is any utility to this
i mean it's a pretty simple wrapper
ofc i wanna go the other way and make it so that way you can .await observers
async {
async_bridge.on(entity, |click: On<Click>| {
}).await;
}
And then with things that implement clone you can easily just have
async {
let click = async_brige.wait_for::<On<Click>>(entity).await;
}
lots and lots of fun ideas to explore in 3rd party crates
hm honestly not loving this, it's easily just reduceable to a trigger method on AsyncBridge and then just awaiting the future inline
trait AsyncBridgeExt {
async fn trigger<E: Event + Send>(&self, event: E) {
self.new::<Commands>().access(FutureToObserverSyncPoint, |mut commands| {
commands.trigger(event);
}).unwrap();
}
}
#[derive(Event)]
struct HttpRequest(body: String);
async {
some_async_stuff.await;
other code
async_bridge.trigger(HttpRequest(reqwest::get("www.google.com").await)).await;
other code
async_bridge.trigger(HttpRequest(reqwest::get("bevy.org").await)).await;
}
Oh yeah! we can totally do this
just add an observer to it
that awaits the despawn
here's a version with triggering async observer and also await observers as async!
fn exec<...>(&mut self, sync_point: M, future: F) -> Self::Spawned<'_> {
let entity = self.spawn_empty();
self.queue(move |world: &mut World| {
let async_bridge = world.get_resource::<AsyncBridge>().unwrap().clone();
let despawn_event = async_bridge.on::<On<Despawn>>();
ComputeTaskPool::get().spawn(async move {
select! {
res = future => {
async_bridge.trigger_entity(entity, res).await;
},
_ = despawn_event.access_cloned() => {},
};
}).detach();
});
self.entity(entity)
}
this combines the thing i was talking about above with async observers
i guess it's async triggers vs async observers 🤔
that's a good way to name it
Useful, but out of scope for this primitive I think
Scope creep is hard
Oh yeah for sure
as i said, fun ideas to explore in 3rd party crates 
Moving into here from private messages, talking about potential naming:
I think async tick budget makes sense
I am not sold on ErasedSystemStateStore -> ErasedStateStore because it makes it very not obvious what it actually is, which is a a thing that manages and holds system state
not arbitrary state
going from queued tasks -> pending tasks i'm also less sold on.
But i'm not sure what is the right naming in general for that section.
One viewpoint is we are queing them for later, and then they become pending when we do the waker on them? 🤔
i do like polled_requests or polled_x whatever is the best x
for after we have tried to wake them and run them
I think generally getting rid of the drive naming and unifying it into tick makes sense
but maybe going from DriveStatus -> TickStatus. I don't love result because of the rust connotation of result being a specific result enum, but i don't feel that strongly on that, TickResult is a good name
I also think the enum variants make more sense, DidWork and NoWork
I think we need to really think about what the bridge we are talking about it 🤔
because there's a couple of ways of thinking about it which heavily affect the naming.
Is the async bridge the active connection between the ecs and the future, is it the thing that makes that connection directly ( and holds the system state ) or is it the thing that produces the thing that makes the connection
I think if we can figure out what is a logically consistent naming with the bridge, where it fits, we can derive a lot of naming from that scheme
So one thought is the bridge is brought up and torn-down everytime the tick_async_bridge happens. This means that tick_async_bridge wouldn't be a good name for it, because we aren't ticking existing bridges, we are establishing the connection between the async and ecs. 🤔
But QueuedBridgeRequest or QueuedBridgeConnection would be a good name
Okay hi dlom we can start with just talking about the system state cell stuff
and then we can talk about bridge stuff
Calling this a Cell fits standard rust terminology (SystemStateCell and ErasedStateCell)
I do like that
BUT i don't like ErasedStateCell because it doesn't have SystemState in it
I think pending makes sense because the future hasn't been woken at all yet
We could erase State and just call it ErasedSystemCell 🤔
The difference between a system and it's state isn't actually a thing here because we don't have real systems
ErasedSystemStateCell ?
It's not really an external type so users won't have to type it, and it's perfectly descriptive
Replacing drive with tick is good I think
Instead of returning a status, the ticker could just return the number of futures processed (0 for no work)
Then the caller can check if > 0 instead of matching on the enum
Thoughts?
DidWork would be any value greater than 0
I'm not sure how much you've changed on your side, but I think the entire purpose of the async bridge (as it's called now) is to produce AsyncParams (I think this is the proper name for this) objects for a given system param type
So like AsyncParamsManager or something like that could be appropriate for the bridge?
HMmm 🤔
It's really a singleton object wrapped in an arc owned by the world that we reference with weak pointers on the async side
okay okay let's first, what should a bridge be, should it be whatever persistent connection we have between that async task and the ecs
or should a bridge be brought up and torn down each 'request'
so we are creating a new bridge each time we try to 'cross' it so to speak
Whatever is held in the weak ref can't be torn down
It needs to stay there so we can upgrade and access
And the end user needs a way to get a strong handle to this inner object so they can create weak handled descendants
I think I called it "bridge state"?
yeah, you called the bridge state the thing that contains both the world, and the pending requests
So in this framework, a bridge is not any particular link between the ecs and the async world, but is the general link between the two
Another typical name could be BridgeInner or whatever noun you pick other than Bridge
There's only one link per world, no? It's THE link
Unless you think there could be several? Trying to imagine a use case
A bridge per sync point?
Working name bridge for now lol
i think it's really important to figure this out because it's a new name for a new concept in a way 🤔
similar to how there's watchers in tokio
AsyncWorld is also an option
i was thinking maybe what we have is the AsyncSystemHandle is the bridge
this is just an option i am not sold on anything yet i want to explore what makes sense
AsyncSystemHandle is that what I called AsyncParams?
It's what i called EcsAccess
The object that has the "run" method
I'll push for AsyncParams, I think it makes sense
That's really all it is, a reference to a system param implementor that you can invoke the async run fn
Or AsyncSystemParams
Again not a type that users would type out, it's returned
Let me move to a better spot first
We should make sure the Root object is the only strong handle, and when users want to clone it to move into tasks to get some form of ecs access they should be cloning it as weak
we should probably name the weak handles some form of handle to the root object that users clone
AsyncSystemState
.bridge
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(move || {
let spawned_id = world_handle.system_state::<Commands>().bridge(SyncPoint, |mut commands| async {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let spawned_id = world_handle.system_state::<Commands>().bridge(SyncPoint, |mut commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let my_system_state: AsyncSystemState<Commands> = world_handle.system_state::<Commands>();
my_system_state.bridge(SyncPoint, |mut commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle: AsyncWorldHandle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let my_system_state: AsyncSystemState<Commands> = world_handle.system_state::<Commands>();
my_system_state.bridge(SyncPoint, |mut commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle: AsyncWorldHandle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let my_system_state = AsyncSystemState::<Commands>::new(&world_handle);
my_system_state.bridge(SyncPoint, |mut commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle: AsyncWorldHandle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let my_system_state = AsyncSystemState::new::<Commands>(&world_handle);
my_system_state.bridge(SyncPoint, |mut commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle: AsyncWorldHandle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let my_system_state = AsyncSystemState::new(&world_handle);
my_system_state.bridge(SyncPoint, |mut commands: Commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let world_handle: AsyncWorldHandle = world.handle(); // cheaply clonable handle to pass to async context
spawn_task(async move {
let my_system_state: AsyncSystemState::<Commands> = AsyncSystemState::new(&world_handle);
my_system_state.bridge(SyncPoint, |mut commands: Commands| {
let id = commands.spawn_empty();
return id
}).await;
log!("{}", spawned_id);
);
}
Source of the Rust file src/system/function_system.rs.
this
Here's the final public facing API we decided on:
fn vanilla_bevy_system(world: Res<AsyncWorld>) {
let my_world_handle = world.clone(); // cheaply clonable handle to pass to async context
AsyncComputeTaskPool::get().spawn(async move {
let my_system_state: AsyncSystemState<Commands> = my_world_handle.system_state::<Commands>();
let mut spawned_entity_count: u32 = 0;
let spawned_id = my_system_state.bridge(MySyncPoint, |mut commands| {
// do anything...
let id = commands.spawn_empty().id();
spawned_entity_count += 1; // capture mutable reference
return id
}).await.unwrap();
println!("{}, {}", spawned_id, spawned_entity_count); // (entity id), 1
}).detach();
}
@young sorrel looking through your second PR to my PR, I don't think we should be catching panics
Also most of the documentation has been stripped describing what things do
I'm down with re-organization but not with stripping the documentation out, i know the docs have to be rewritten a little because the names for things are a little different but i think the detailed documentation was very useful
I do think the reorganization itself made sense though, so if you want i'll make changes to my own pr copying the re-org but keeping the documentation i had written and updating it
given that you came up with the re-organization i don't want to plagiarize your PR without permission though, if you would rather do the reorganization yourself, standalone, and make a PR to do it to my branch
Whichever you prefer, let me know
Also you removed the no_std implementation for the LatchGuard
😔
i do agree with the latchguard making a new pair
given how crossbeam channels and such work maybe it should be a function that's not associated with any struct, in the guarded_latch module
let (latch_waiter, latch_guard) = guarded_latch::new()
the no_std implementation should work on no_std implementations like the game boy because we have local threadpools that we manually tick
since there are no other threads, it combines with the no_std impl to just work, or in theory should
actually we could get @shy oxide to test for us before we end up merging the PR, that our async stuff works on the game boy
Yeah happy to check. I haven't tried doing game boy on main recently, so might need to update some stuff first
yeah, i mean i can whip up a simple small example
one that just uses bevy_async, bevy_ecs, and bevy_tasks
oh and bevy_app
but yeah, i'll let you know when we have a good example to test 
I'm like 95% sure it deadlocks on no_std even with the spin lock
Also I tried to make run take a ConditionalSend closure but it doesn't work because the closure has to be stored in a resource and resources have to be Send
Yes go ahead and push commits to my branch or incorporate it yourself
I'm bad at writing inline comments when it seems obvious to me, and I tried to simplify the code everywhere enough that it's self documenting and easily apparent
I did mean to add doc comments back to BridgeFut but I didn't get around to it last night
I was mostly curious how big the core was without comments and it's so tiny
So perfectly tiny and reviewable
It is pretty dang small without much comments

Okay!
The more I think about it, the reason it will work on no_std would be if the waiter was a no-op, we can just std config it out
Because we're ticking the local task pools manually
So those task pools run, prior to us 'waiting', and that waiting does nothing
this also means that bridging is deterministic with spawn_local, unlike with normal spawn tasks where it is dependent on the async runtime, with spawn local there is a guarentee that it will run because we are manually ticking it
but it wouldn't work with arbitrary async runtimes on no_std, maybe there is something to be said for having a resource with function pointers that tick async task pools manually, and setting up to default include the bevy task pools, so that way it could integrate with arbitrary local/single threaded async runtimes 🤔
Right now if a user closure panics it crashes the whole app
If you don't catch
but panics inside systems normally crash the application
Maybe they shouldn't 🤔
Async scheduler 2.0 can probably safely catch all of those and save the app
@shy oxide if you can use the current pr to test a simple async system that does this!
use bevy_app::prelude::*;
use bevy_async::prelude::*;
use bevy_ecs::prelude::*;
use bevy_tasks::AsyncComputeTaskPool;
use bevy_platform::sync::atomic::AtomicBool;
use bevy_platform::sync::atomic::Ordering;
use bevy_platform::sync::Arc;
use bevy_app::ScheduleRunnerPlugin;
struct MySyncPoint;
static ACCESS_RAN: AtomicBool = AtomicBool::new(false);
fn main() {
let mut app = App::new();
app.add_plugins((
AsyncPlugin::default(),
ScheduleRunnerPlugin::default(),
TaskPoolPlugin::default(),
));
app.add_systems(Update, async_world_sync_point::<MySyncPoint>);
app.add_systems(Startup, move |world: Res<AsyncWorld>| {
let world = world.clone();
AsyncComputeTaskPool::get()
.spawn(async move {
let system_state = world.system_state::<Commands>();
system_state
.bridge(MySyncPoint, |mut commands: Commands| {
commands.spawn_empty();
ACCESS_RAN.store(true, Ordering::Relaxed);
})
.await
.unwrap();
})
.detach();
});
app.update();
assert!(ACCESS_RAN.load(Ordering::Relaxed));
}
it should only take bevy_app bevy_ecs and bevy_async
on the game boy
OH
but instead of spawn use spawn_local
@shy oxide
@pulsar fjord i am ready for your review
@glossy gyro i'm ready for your review
( no_std support isn't a blocker to getting this merged, so i'm perfectly fine with going ahead )
( after we get this merged we can also work on getting dlom's alternative system in that works in a more standard but less flexible manner @young sorrel )
There's almost exactly the same number of lines of comments as there are lines of code
haha
comments are 339 loc vs code is 389 loc
okay so now i need to go back and work on that release note
🤔
we'll probably end up updating the release note because we'll add web support and the secondary system by that time but even then
ofc got ci issues but i'll fix it by force pushing to the same commit over and over again
( still want reviews though, they are nits, not impacting anything ) 🥺
oh this still shows the new commit, darn
Is this making it into 0.19?
haha
no
😔
hmm i'm running the cargo run -p build-templated-pages -- update examples but it isn't regenerating the page
oh hm
i might have it fixed
sadly have to have it wasm = false for now
Saw that alice wanted some unit tests to handle the error cases
so adding those now
oh this sorta sucks
system param validation is not longer seperate
so we can't bubble up errors easily if resources don't exist
or other param validation errors occur
🤔
WELL
okay it's only a runtime panic if it's inherently incompatable i think
yeah actually this is fine
OH dang it
bevy_platform's arc doesn't do unsized coercion
cause that's something inherent to rust's compiler
and std lib
mannnnn
error[E0308]: mismatched types
--> crates/bevy_async/src/bridge_future.rs:58:27
|
58 | system_state: Arc::new(SystemStateCell::<P>::default()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Arc<dyn ErasedSystemStateCell>`, found `Arc<SystemStateCell<P>>`
|
= note: expected struct `Arc<(dyn ErasedSystemStateCell + 'static)>`
found struct `Arc<SystemStateCell<P>>`
= help: `SystemStateCell<P>` implements `ErasedSystemStateCell` so you could box the found value and coerce it to the trait object `Box<dyn ErasedSystemStateCell>`, you will have to change the expected type as well
there's a nightly rustflag to enable the coercion lmao
i'll just get around it by boxing it i think
on no_std
oh yay i can do an Arc::from for a box
okay that's not bad
added a no_std test that passes @young sorrel @shy oxide 
if it works there it should work on no_std, because we're compiling with no_std so it's emulating at least the restrictions of no_std
still, a test on an actual no_std platform couldn't hurt!
All ci is green everything is Gucci
Approved 
@young sorrel I GOT A VERSION WORKING ON WEB WITHOUT ANY UNSAFE 
( this is follow up pr material )
so hopefully once the original pr gets merged we can get this one merged in real quick right after, it's only 116 lines of additional code ( and the code here isn't the best, it's likely going to be less code once we get to properly doing it 
+116 -39
I'm getting this panic after trying to port suborbital to the latest api
okay no panic if I enable default features
oh never mind it just took a bit longer this time
I've pushed my code to the async branch if you want to investigate
I will! I wonder what it could be 🤔
I got it to reproduce once while I didn't have my debugger open, but then not again when I did, but i'm waiting for it to
I think i know where the problem is tho
Cause I was thinking on my walk home this morning
Oh wait no, nvm that can't be it
Ig we'll see when my debugger catches the freeze
Okay, got a freeze but its really strange as i'm not seeing any of the async compute task pool threads
Okay wait I think i see the issue, we need to enable the feature bevy_tasks automatically if its used in the normal bevy app
So the issue i think is that, without default features, bevy_async doesn't have the bevy_task feature, which makes it work with single threaded bevy tasks
and without default features multithreading is disabled
Yes nailing down the features and all the different valid combinations is a good next step
If bridge ends up working everywhere on all platforms and feature combinations then run is probably unnecessary
It only existed to fill the gap
Yeah, now that we know we don't need unsafe for it to work on web
No objections, I haven't torn into the code in detail to see how it works, but it should not affect anything negatively here
And with this new web_task crate, building web with multithreading should allow us to make things respect sync points when building on the web
sure but it still panics with default features
weird
bridge behaving differently on native/web is kind of a sticking point for me, ideally it should behave the same on all platforms. Bypassing the sync point mechanism just to get it working on web is a big smell, either we should find a real solution (hopefully this new web tasks provides it) or we should take a step back and discuss the sync points more (I believe this was a cart ask)
Maybe web gets "inter-frame bridge"
🤔
Bundling it under the same API is slightly dishonest if it ignores the provided sync point
web is already dishonest about a lot of things, like the fact that you can't manually poll a task on web even though the api looks like you can
OH wait 🤔 i wonder.....
i am not sure it compiles with std if the std flag is set on bevy
i might have forgot that
which means without default features ( which is what bevy_async defaults to when included in bevy )
it just skips the waiter
and that is why it crashes, cause there's no system state to apply
🤔
Yes, that's one of my hotter bevy takes: web feels like a second class citizen even though it seems (to me) to be the most popular platform to target
@vocal vortex if you do bevy_async = { git = "" } with features including std and bevy_tasks does it not crash?
because that's what i did to get it to not crash on my end
to not freeze
it is! and it's sad
but idk how much can really be done about it
web is fundamentally limited
Moving the app into its own task separate from winit and making the schedule a future is probably a good start
Then the problem is entirely delegated to the executor
Users use web tasks and the web executor and don't worry about the details
That's why I'm keen on this thread that @proven sandal is pulling on
yeah if we were existing in a non-main thread then we could just wait on the tasks like all the other platforms
exactly
this is abouit the only sane way to fit bevy and winit into the web together
Stuff like this async primitive would fall out naturally
It would have just one impl that works everywhere (theoretically...)
Any movement on this recently? I really want this now lol
no crash 👀
Winit is just so complicated
yeah it's just some stuff i gotta fix with feature flags
yay
my bad
web-tasks was transferred to the org and the pr switching to it has merged. so a tiny bit.
the remaining work had two prongs, a big winit refactor and a big scheduling refactor
those are both going to take a while. windowing will land first, we need that goal to be officially started.
the scheduling changes will also need to be a goal, and that hasn’t even been written yet, so that’s probably going to land second
this is basically the take of the year for me on bevy. the web is by far our most important and least well supported target.
some potential future improvements on the lang side, like we could target wasm32-component-web instead of wasm32-unknown-unknown
interesting. i feel like that's going to take a looong time to get browser adoption.
if ever.
personally i'd love to see web move to wasm32-none-none and just go full no_std+alloc
interesting, how would we work with web-workers then though? 🤔
you use js-sys for everything
you already can't use std to start web-workers.
i see 🤔
is wasm32-none-none something we can already target?
can we target web as no_std
because std is lorge, and this makes an argument for no_std-ifying everything even more
yep, wasm32-none-none is already a valid web target
the problem is mostly the ecosystem, lots of web-facing stuff is built using std
we would need to finish the no-stdification of so much shit lol
this is super off topic for this group tho
yeah for bevy getting to nostd web is going to be a long process.
Time for a new WG?
we'd need nostd wgpu first
was just thinking that
i bet you could get nostd bevy ecs working on wasm32-none-none tho
with the async_bridge primitive we can get a lot closer to no_std for bevy_assets
since a big part of why it's STD rn ( though not the only part ) is all the async machinery it relies on has no no_std version
Wouldn't that be absolutely terrible for performance? 
yes this is not a serious suggestion
@glossy gyro i responded to a few of your comments with potentially sufficient reasoning or suggestions that i am asking if implemented would fix your concerns, if you could reply to those on if they are sufficient or not
Alright, next round of review is up for y'all: https://github.com/bevyengine/bevy/pull/21744.
Sorry for being such a hardass 😅 It's coming along well though! This is much much better than the last round of review!
Okay, I think I've got all of these. Please resolve comments aggressively for this PR: it's getting very hard to keep everything in my RAM 😅
you missed at least one!
Literally didn't!
For that one, make sure it's not an imperfect derive
we don
We may be missing a derive debug on the internal error type
don't own the crate
it's a third party external one bevy_ecs relies on
( one of the small few )
Yeah, but it may only impl <T: Debug> Debug for ThirdPartyError<T>
That's what derive(Debug) does
ohhhh
you are right!
oh wait 🤔
oh yeah yep
impl<T: fmt::Debug> fmt::Debug for PushError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PushError::Full(t) => f.debug_tuple("Full").field(t).finish(),
PushError::Closed(t) => f.debug_tuple("Closed").field(t).finish(),
}
}
}
😄
https://github.com/bevyengine/bevy/pull/21744#discussion_r3012589019
@glossy gyro should this be described in the example or somewhere else? Where do you want me to detail how this works
Oh also I slapped M-Needs-Release-Note on it. Add dlom as author 2, and me as author 3?
In the example
okay
i do not know how to do this, but dlom certainly counts as an author
gimme a sec i'll figure it out
I definitely think the web sync point dishonesty needs to be documented somewhere before this merges (if it isn't already)
true
but
it doesn't have web support yet
so there is no dishonesty yet
when we add web support we can add a note
@glossy gyro https://github.com/bevyengine/bevy/pull/21744#discussion_r3012546178
On the latter note of 'we should add some default sync points' maybe but that is directly related to https://github.com/bevyengine/bevy/pull/21744#discussion_r3012539465
There is no runtime effect ( other than the insertion of the resource at plugin build ) that this crate has unless you add a system to the schedule.
And cart actually took issue with the idea of us adding sync points inherently because of any potential overhead and messy-ness 🤔
What do you think of the proposal of adding a system with the sync point that is the schedule for every system to the plugin and then making that a feature flag.
I.e.
build(app: &mut App) {
#[cfg(feature = "standard_sync_points")]
{
app.add_system(First, async_world_sync_point::<First>);
app.add_system(FixedUpdate, async_world_sync_point::<FixedUpdate>);
... and so on
}
}
Ah interesting. I don't think that's worth it at all then. Let's stick to a strong warning in the plugin docs
okie
also i'm not sure it's entirely possible to warn, without false positives, that a sync_point has no associated system
🤔 @young sorrel what do you think
The sync point being a system that users can order as they wish seems the most powerful
oh yeah, no i agree on that, about warning without false positives, that a sync_point has no associated system when you are attempting to bridge into it
And it's simple, just add one fn to your schedule
Can you actually detect this? Maybe the user has the sync point system running only conditionally
yeah 🤔 exactly
Like within a state
or if you're adding stuff in a Startup and the user's sync point is in Update
i'm not sure it's viable to detect without false positives
I think a warning like "calling bridge with a non existent sync point will hang the async task forever" is good enough
User responsibility
Maybe expose the queue lengths somehow
So an inspector plugin could read all the queues and see if they're filling up
Or some kind of async monitor user plugin could watch it and report somehow
we could watch all the queues indefinetly
but then it's like, well do we want to wait 30 seconds before warning the developer?
that isn't useful for if they are putting it behind a state transition
or such
Honestly a very simple built-in async watchdog is probably within scope, like it's a resource spawned by default (configurable) that watches the async queues to see if they cross a length threshold (configurable #) and logs a warning (with simple guidance on how to configure the watchdog)
third config knob: duration between watchdog checks, so every 5 seconds the watchdog runs and checks queue lengths and warns if they're above a threshold
i guess we could add a watch dog with specific SyncPoints it ignores?
why would you want to ignore specific ones?
actually I guess queues won't grow unbounded because the task queueing the future is frozen waiting for the future anyways
so these queues wont grow very large unless there are multiple tasks using the same sync point
it'll only be unbounded if the number of tasks is also unbounded
https://notes.brooklynzelenka.com/Blog/Surelock slightly-relevant blogpost (multi-leveled locking)
woah thats cool
I read this yesterday it's really cool (though const generic arithmetic not being present means the impl is very macro heavy)
The branded lifetimes stuff is cute
found a bug, lmao
i am so so so certain it is the last one
the bug means sometimes you can end up with unapplied command state caused by suprious wakeups
i just had to add an Arc<AtomicBool> flag to the bridge future and the request and signal if it's actually been signaled or not
Very slick
This looks really cool, but is there even a slim chance of bsn! officially supporting this?
It depends, something i might be able to convince @pseudo vine of, is adding a generic ability to using async closures in bsn, and have them auto-spawn on bevy's task pools, if we could also add the ability to generically define as many parameters which are Resource + Clone, then that could be fully supported without it being something niche and specific to async reactivity/ui only.
@pseudo vine is this generic async addition on the table?
If we can somehow support closures (async and non-async) with #Name references more generally as field values rather than as some special general-purpose top level "scene type", I think thats my preferred approach
I don't think we can support them as field values period, because we have no where to store the Vec<ScopedEntities> per closure.
But we could support both async and non-async closures
( and observers ofc )
Because we can turn them into custom structs with fields dedicated to storing their scoped entities before resolution
Hmmm I'm not yet convinced of blockers here. The template type would store a list of bsn-macro-generated Vec<ScopedEntityIndices>, which then resolves to a final captured Vec<Entity> in the returned closure
Ohhh
I see
Modify every template type
That would work!
Oh hm, yeah we would need a vec of vec of scoped entities, but yeah, finer details
So to answer you now, yes, but not looking quite as slick ( wrap the closure in a struct like AsyncCompute() )
We would also have to store a vec of closures, one potentially per field ( probably more optimal to do an array of option closures ) right?
Unless we want to enforce that every field starts as a closure as a template
But I think thats suboptimal for performance, all that dynamic dispatch to set a few hundred fields?
( wrap the closure in a struct like
AsyncCompute())
That's not too bad IMO
this sounds like graphics, is this graphics
😔
It could be!?
Well at least with vulkan
Lemme find the link
The talk was presented at Vulkanised 2026 which took place on Feb 9-11 in San Diego, USA. Vulkanised is organized by the Khronos Group and is the largest event dedicated to 3D developers using the Vulkan API, and provides a unique opportunity to bring the Vulkan developer community together to exchange ideas, solve problems and help steer the ...
It 'could' be a feature for the 'yeeting RenderWorld', where async systems are used for extraction systems. So a little graphic kek
I have managed to make this work in my crate!
async |ui: Ctx| {
loop {
let value_change = ui.on_cloned::<ValueChange<bool>, ()>(#Checkbox).await;
ui.bridge(|mut commands: Commands| {
if value_change.value {
commands.entity(#Checkbox).insert(Checked);
} else {
commands.entity(#Checkbox).remove::<Checked>();
}
commands.entity(todo_list_root).trigger(RefreshList);
}).await;
}
}
This infers the system parameters, caches them, and runs the bridge on the AsyncUi schedule
fn demo_root() -> impl Scene {
bsn! {
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
}
ThemeBackgroundColor(tokens::WINDOW_BG)
Children[(
Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
}
Children [
(#Minus
button(ButtonProps::default())
Children[(Text::new("-1") ThemedText)] ),
(#Counter
Text::new("0")
ThemedText
Node { margin: UiRect::horizontal(px(10.0)) } ),
(#Plus
button(ButtonProps::default())
Children[(Text::new("+1") ThemedText)] )
]
)]
async |ui: Ctx| {
let mut number = 0;
loop {
futures::select! {
_ = ui.on::<Activate>(#Minus).fuse() => { number -= 1 }
_ = ui.on::<Activate>(#Plus).fuse() => { number += 1 }
}
ui.bridge(|mut query: Query<&mut Text>| {
query.get_mut(#Counter).unwrap().0 = format!("{}", number);
}).await.unwrap();
}
}
}
}
So this is what the new feathers button replacement example looks like
What is the status of this working group, is it still full on in the experimenting/design phase or something that could land in 0.20/0.21? Just curious about the status, no pressure. Nice work so far (judging from the posted snippets).
There's a lot of experimental and design work to be done in general, that work relies upon some sort of async ecs access primitive that bridges a connection between the async and ecs domains. that primitive has been worked on and polished a lot and is viable to be upstreamed 0.20.
That being said, I will be publishing a crate with lots of async toys to play with, such that you can do async ui and other async ecs things, async observers, etc. And some more opinionated wrappers around cached async accesses, and that will drop when bevy 0.19 drops
I have a branch on bevy main ready to drop the second 0.19 releases
The async primitive works as a standalone crate yeah? I think you could publish a new version of your old standalone crate and let people use it today