#Async ECS ergonomics: better late than never

1 messages · Page 2 of 1

eager trout
#

do you mean like this, in the example?

#

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? 🤔

lethal adder
#

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 thonk

#

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 🤨

warm mesa
eager trout
#

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

warm mesa
#

that specific API (the ideal one) is quite a few versions away

eager trout
#

hmm

#

😔

#

sad

warm mesa
#

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

eager trout
#

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

vocal vortex
#

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

eager trout
#

Can't say to much but i am stress testing the crap out of bevy_malek_async

eager trout
#

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

vocal vortex
#

ooh very exciting

#

let me know when it's ready to test again

eager trout
#

maybe later today, depends, that or this weekend i think

eager trout
#

@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

vocal vortex
eager trout
vocal vortex
#

updated my repo if you want to test yourself

eager trout
#

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

burnt loom
#

@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.

eager trout
south skiff
#

Let me drop it here: #assets-dev message

eager trout
#

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

eager trout
#

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 )

eager trout
#

but i just reduced that unsafe by hiding it behind a dyn instead of, effectively building a vtable manually lmao

eager trout
#

but things are looking good

#

the whole implementation is now only 587 loc, there are 92 lines worth of comments tho catnod

#

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!

eager trout
#

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

eager trout
#

I'll share the code tonight

#

But its still in progress cause I wanna figure out the ginghe issue

eager trout
#

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

eager trout
#

Pog thanks

eager trout
#

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

vocal vortex
eager trout
#

🤔

#

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!

eager trout
#

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 catnod

eager trout
#

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

eager trout
#

it indeed was

#

oh wait, having git issues

#

one sec and it will be pushed

#

oh yeah it's pushed!

eager trout
#

down to 628 loc btw, just in terms of raw complexity the code is getting simpler while having more functionality catnod

eager trout
#

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

eager trout
#

okay there's a deadlock heh

#

i am working on solving it!

#

Okay i understand the issue

vocal vortex
eager trout
#

@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?

vocal vortex
#

but it fails to compile for wasm

eager trout
#

yep

eager trout
#

to compile for wasm in your case you should just be able to comment out that line

#

and then it should work?

fringe ridge
eager trout
#

hmm, lemme try to figure out via git history who invented the comment

fringe ridge
#

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

eager trout
#

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?

vocal vortex
eager trout
#

oof

#

🤔 hng

#

that is a parallel issue to debug for me

fringe ridge
#

#20369

eager trout
#

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

vocal vortex
proven sandal
#

That said, oof, I think this really might need to be unsafe with a proper safety comment

#

I will look when I can

eager trout
eager trout
#

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

jolly moat
proven sandal
jolly moat
#

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.

eager trout
# jolly moat I guess it depends on what thread it's being called on. So if it's a task pool t...

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

eager trout
#

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

eager trout
#

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 🥺

eager trout
#

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

eager trout
#

@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

lethal adder
#

I can give a look this weekend, dont have time during the week though 🙏 Appreciate the grind though

eager trout
#

okie

eager trout
#

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

crisp gorge
#

you seem happy

eager trout
#

yeah, nanosecond range for ecs tasks is exactly where i wanted to be

crisp gorge
#

async ecs seems really cool

#

is there like an example snippet anywhere? I wanna see how it would be used

eager trout
#

you can do 277,000 async ecs tasks now, ( though it would eat your whole frame budget ) in a single frame

eager trout
#

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;
crisp gorge
#

oooh thats cool

eager trout
#

yeah!!!! i think so too!

lethal adder
#

I wonder what the base time for a non-async system is, so you can provide an average ratio for cost.

eager trout
#

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

lethal adder
#

For sure, was just curious what existing overhead there is in just system running in itself. Basically how much faster CAN it be.

glossy gyro
eager trout
#

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

eager trout
#

and now it's failing all the CI! yayyy

#

🥴

#

ready for reviews tho

#

if anyone wants to provide

glossy gyro
eager trout
#

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

eager trout
#

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

crisp gorge
#

truly awesome commits

vocal vortex
#

hm this new api is quite confusing

eager trout
#

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

vocal vortex
#

maybe it could be paired with a bare-minimum example?

eager trout
#
#[derive(SyncPoint)]
struct MySyncPoint;

app.add_systems(Update, MySyncPoint);

and then

task.run_system(MySyncPoint, || {})
vocal vortex
#

because the current example is doing a lot more than just the bare minimum to run a system in a task

eager trout
#

true!

vocal vortex
#

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

vocal vortex
#

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

eager trout
#

Let's do that catnod

eager trout
#

@vocal vortex done

vocal vortex
#

yay!

eager trout
#

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

vocal vortex
#

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>(|| {

eager trout
#

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);

vocal vortex
#

yeah that'd be perfect if possible

#

imo

lethal adder
#

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.

vocal vortex
#

just ported suborbital to use the new api and the animation is noticably smoother 👀

vocal vortex
#

btw it still doesn't compile on wasm

eager trout
#

I didn't realize that was the issue

lethal adder
#

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

eager trout
#

WAIT yes maybe we can get parallel schedule execution this way hyper_think

#

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 catnod

#

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

eager trout
#

@vocal vortex pushed the even more ergonomic sync points btw

#

also should have hopefully fixed it not building on wasm?

eager trout
#

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 🥺

lethal adder
#

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

eager trout
#

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
glossy gyro
eager trout
#

oh for some reason my local cargo fmt

#

is different than yours

#

Huhhhhhhhhhhhhhhhhhhhhhhhh

glossy gyro
eager trout
#

fr fr

#

so true

eager trout
glossy gyro
vague prairie
#

@eager trout Talk me through the principles of what makes up the new API?

eager trout
#

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

south skiff
eager trout
#

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

vague prairie
#

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?

eager trout
#

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

eager trout
vague prairie
#

right yes :p either way it's "these computations are happening at these sync points" right?

eager trout
#

yep!

vague prairie
#

If you don't register sync points, does it default to just kinda dangling?

eager trout
#

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

vague prairie
#

From an DX point of view that'd probably be greatly appreciated :')

eager trout
#

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

vague prairie
#

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 :)

eager trout
#

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

vague prairie
#

For sure ^^

glossy gyro
eager trout
glossy gyro
eager trout
#

sure, and just to be clear you want me to squash all of the history here?

glossy gyro
#

I think that's probably best. We don't need 50 comments of "ci plz"

fringe ridge
#

But that depends on if there's history you want to preserve

eager trout
#

yeah 🤔

#

i don't think i do i'll just compress it all down

glossy gyro
#

@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

eager trout
#

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 <(), _, _, _>

glossy gyro
eager trout
#

Ohhh

#

do you want a standalone function?

#

🤔

glossy gyro
#

Yes please

eager trout
#

EcsTask::new::<(Commands, Query<&A>)>(world_id);

glossy gyro
eager trout
#

EcsTask::new::<(Commands, Query<&A>)>(world_id); okay

#

i'll do that

glossy gyro
eager trout
#

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

glossy gyro
eager trout
#

true, this would be a higher level thing though, i don't want to put any of those things in this pr 🤔

glossy gyro
#

Okay, that's fine, we can start with a nice simple low-level API 🙂

eager trout
#

@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

#

also same question goes for the nits around box mesh handle being combined and such

glossy gyro
burnt loom
#

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

fierce oak
#

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

eager trout
burnt loom
#

could also be nice to declare the runtime in the plugin

young sorrel
#

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

eager trout
#

@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 catnod

eager trout
#

down to 338 lines of actual source code in the main file where everything goes down now vibecat
( not including comments and whitespace ) still need to properly comment ALSO now we need an AsyncEcsPlugin in MinimalPlugins and DefaultPlugins catnod

#

so we can be sure to init our resource in it catnod

#

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!

eager trout
#

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>,
}
errant moon
#

with TickAsyncTasksResult, u could shape it like struct AnyTasksRemaining(bool)

#

I'd expect something with result in the name to be result shaped

eager trout
#

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

eager trout
#

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

fierce oak
eager trout
#

Yeah I think i'm going to just throw it in bevy_tasks

jolly moat
proven sandal
#

Or in bevy_ecs

eager trout
#

Pog new crate time

eager trout
proven sandal
#

not bevy_async though pls, bevy_async_ecs or something

lyric adder
#

call it bevasync_y

eager trout
young sorrel
eager trout
#

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

eager trout
#

FIXED UP HEAVILY DOCUMENTED PR INCOMINGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG

#

( just testing some things first and doing a final pass over all the documentation )

eager trout
#

@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 catnod and added doc-tests and better docs

glossy gyro
#

I'll probably be until Sunday to have time to re-review

young sorrel
#

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 during poll()), 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

eager trout
young sorrel
#

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

young sorrel
#

I also plan to open a PR targeting your branch with some naming suggestions (and a few other minor tweaks)

eager trout
#

Yes please!

eager trout
#

when i get home tmrw i'll include this in the PR description, i think it summarizes the complex logic well catnod

#

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

#

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

glossy gyro
#

We should use crate docs much more

eager trout
#

oh! i didn't know this was a thing 👀

young sorrel
#

For the viewers at home: this is evolving quite rapidly behind the scenes

#

web vs std semantics are quite complicated

glossy gyro
proven sandal
eager trout
#

So we could have it tick also outside the micro queue?

proven sandal
eager trout
eager trout
proven sandal
eager trout
#

Yep!

proven sandal
#

great then we want the same thing

eager trout
#

Awesome

#

Yayyy

#

@young sorrel I think we won't have to do the hacky thing

eager trout
#

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

proven sandal
#

potentially each system will be queued separately, idk, depends on perf

eager trout
#

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

proven sandal
#

I don’t totally follow.

eager trout
#

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

proven sandal
#

I think that will satisfy your needs

eager trout
#

Okay!

young sorrel
#

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

proven sandal
#

having .await sync points would make your work here a lot easier

young sorrel
#

Sounds awesome, I think I understand enough about async now to help review it

#

When it's ready

proven sandal
#

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

young sorrel
#

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?

young sorrel
proven sandal
#

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

proven sandal
#

maybe i need to review your code, you may have already done what i needed

#

where's the working tree for this

young sorrel
#

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) };
proven sandal
#

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.

young sorrel
#

no idea if this breaks something wider, but running examples on native still works for me

young sorrel
proven sandal
young sorrel
#

right now? not much I think, apps can move between threads for headless

proven sandal
young sorrel
#

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

young sorrel
#

hmm, can't do much more without implementing the whole thing. Gnarly

proven sandal
eager trout
#

@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

proven sandal
#

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

eager trout
proven sandal
#

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

eager trout
#

It would be nice if we could have that

eager trout
#

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!

eager trout
#

Oh sorry nth I didn't mean to ping you

#

Please disregard 😅

young sorrel
#

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

eager trout
#

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

eager trout
#
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;
}
eager trout
#

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

young sorrel
#

Scope creep is hard

eager trout
eager trout
eager trout
#

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

young sorrel
eager trout
#

I do like that

#

BUT i don't like ErasedStateCell because it doesn't have SystemState in it

young sorrel
eager trout
#

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

young sorrel
#

ErasedSystemStateCell ?

#

It's not really an external type so users won't have to type it, and it's perfectly descriptive

eager trout
#

true!

#

yeah i am totally onboard with ErasedSystemStateCell

young sorrel
#

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?

eager trout
#

HMmm 🤔

young sorrel
#

It's really a singleton object wrapped in an arc owned by the world that we reference with weak pointers on the async side

eager trout
#

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

young sorrel
#

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"?

eager trout
#

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

young sorrel
#

Another typical name could be BridgeInner or whatever noun you pick other than Bridge

young sorrel
#

Unless you think there could be several? Trying to imagine a use case

#

A bridge per sync point?

#

Working name bridge for now lol

eager trout
#

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

young sorrel
#

AsyncWorld is also an option

eager trout
#

this is just an option i am not sold on anything yet i want to explore what makes sense

young sorrel
#

AsyncSystemHandle is that what I called AsyncParams?

eager trout
#

It's what i called EcsAccess

young sorrel
#

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

eager trout
#

🤔 i think, okay actually can we hop in a vc

#

i'm in #819673630131748945

young sorrel
#

Let me move to a better spot first

eager trout
#

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

young sorrel
#
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);
  );
}
eager trout
#
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);
  );
}
young sorrel
#
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);
  );
}
young sorrel
#
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);
  );
}
eager trout
#
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);
  );
}
young sorrel
#
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);
  );
}
eager trout
#
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);
  );
}
eager trout
young sorrel
young sorrel
#

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();
}
eager trout
#

@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

shy oxide
eager trout
#

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 catnod

young sorrel
#

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

young sorrel
#

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

eager trout
eager trout
#

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 🤔

young sorrel
#

If you don't catch

eager trout
#

but panics inside systems normally crash the application

young sorrel
#

Maybe they shouldn't 🤔

#

Async scheduler 2.0 can probably safely catch all of those and save the app

eager trout
#

@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 ) 🥺

eager trout
pulsar fjord
#

Is this making it into 0.19?

eager trout
#

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

eager trout
#

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 vibecat

#

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!

eager trout
#

All ci is green everything is Gucci

lethal adder
#

Approved bevy

eager trout
#

@young sorrel I GOT A VERSION WORKING ON WEB WITHOUT ANY UNSAFE vibecat

#

( 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 catnod

#

+116 -39

vocal vortex
#

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

eager trout
eager trout
#

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

eager trout
#

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

young sorrel
#

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

eager trout
#

Yeah, now that we know we don't need unsafe for it to work on web

glossy gyro
eager trout
#

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

vocal vortex
eager trout
#

👀 i couldn't get it to panic

#

i was just getting freeze's at the waiter

vocal vortex
#

weird

young sorrel
#

Maybe web gets "inter-frame bridge"

eager trout
young sorrel
#

Bundling it under the same API is slightly dishonest if it ignores the provided sync point

eager trout
#

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

eager trout
#

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

#

🤔

young sorrel
eager trout
#

@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

eager trout
eager trout
#

but idk how much can really be done about it

#

web is fundamentally limited

young sorrel
#

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

eager trout
#

yeah if we were existing in a non-main thread then we could just wait on the tasks like all the other platforms

proven sandal
#

this is abouit the only sane way to fit bevy and winit into the web together

young sorrel
#

Stuff like this async primitive would fall out naturally

#

It would have just one impl that works everywhere (theoretically...)

young sorrel
young sorrel
#

Winit is just so complicated

eager trout
#

yay

#

my bad

proven sandal
#

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

proven sandal
young sorrel
#

some potential future improvements on the lang side, like we could target wasm32-component-web instead of wasm32-unknown-unknown

proven sandal
#

if ever.

#

personally i'd love to see web move to wasm32-none-none and just go full no_std+alloc

eager trout
proven sandal
#

you already can't use std to start web-workers.

eager trout
#

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

proven sandal
#

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

lyric adder
#

we would need to finish the no-stdification of so much shit lol

proven sandal
#

this is super off topic for this group tho

proven sandal
young sorrel
#

Time for a new WG?

proven sandal
lyric adder
#

was just thinking that

proven sandal
#

i bet you could get nostd bevy ecs working on wasm32-none-none tho

eager trout
proven sandal
#

just make all the wgpu calls through js-sys

#

easy lol

eager trout
#

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

lethal adder
proven sandal
eager trout
#

@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

glossy gyro
glossy gyro
eager trout
#

Didn't show up on my end 😭

#

github

glossy gyro
#

For that one, make sure it's not an imperfect derive

eager trout
#

we don

glossy gyro
#

We may be missing a derive debug on the internal error type

eager trout
#

don't own the crate

#

it's a third party external one bevy_ecs relies on

#

( one of the small few )

glossy gyro
#

Yeah, but it may only impl <T: Debug> Debug for ThirdPartyError<T>

#

That's what derive(Debug) does

eager trout
#

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(),
        }
    }
}
glossy gyro
#

😄

eager trout
glossy gyro
#

Oh also I slapped M-Needs-Release-Note on it. Add dlom as author 2, and me as author 3?

eager trout
#

okay

eager trout
#

gimme a sec i'll figure it out

young sorrel
#

I definitely think the web sync point dishonesty needs to be documented somewhere before this merges (if it isn't already)

eager trout
#

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
  }
}
glossy gyro
eager trout
#

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

young sorrel
#

The sync point being a system that users can order as they wish seems the most powerful

eager trout
#

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

young sorrel
#

And it's simple, just add one fn to your schedule

young sorrel
eager trout
#

yeah 🤔 exactly

young sorrel
#

Like within a state

eager trout
#

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

young sorrel
#

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

eager trout
#

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

young sorrel
#

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

eager trout
#

i guess we could add a watch dog with specific SyncPoints it ignores?

young sorrel
#

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

young sorrel
crisp gorge
#

woah thats cool

vague prairie
#

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

eager trout
#

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

eager trout
pulsar fjord
#

Very slick

tiny gulch
# eager trout

This looks really cool, but is there even a slim chance of bsn! officially supporting this?

eager trout
#

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?

pseudo vine
eager trout
#

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

pseudo vine
eager trout
#

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

eager trout
eager trout
#

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?

tiny gulch
eager trout
#

ye

#

catnod i agree

lyric adder
#

this sounds like graphics, is this graphics

pulsar fjord
#

No

#

Async is about running async functions

eager trout
#

It could be!?

#

Well at least with vulkan

eager trout
#

Lemme find the link

lethal adder
#

It 'could' be a feature for the 'yeeting RenderWorld', where async systems are used for extraction systems. So a little graphic kek

eager trout
#

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

dense frost
#

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).

eager trout
eager trout
#

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

young sorrel
#

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