Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGPU support #5027

Closed
wants to merge 9 commits into from
Closed

WebGPU support #5027

wants to merge 9 commits into from

Conversation

mockersf
Copy link
Member

@mockersf mockersf commented Jun 16, 2022

Objective

  • Enable building Bevy for WebGPU

Solution

  • Make building a plugin async. This allows us to initialize the renderer without blocking in wasm
  • Make #[bevy_main] handles async fn main

details per commit:

Those 3 commits are only partially related to webgpu, and I feel could be pushed in other PRs if needed to reduce this one

commit details
missing wasm flag just remove a warning when building with wasm - #5038
spirv as a feature disable default support of spirv. This doesn't build in wasm, and is probably not needed in most case - fixed by gfx-rs/wgpu#2355
fix skinning shader syntax the skinning shader uses a deprecated syntax to access a matrix, and it doesn't work anymore in webgpu - #5039

Those 4 commits are most of the changes:

commit details
async plugin v0 makes building a plugin async
webgpu feature and how to build update the wasm example builder to be able to build examples for webgpu. some manual editing is needed for now
async bevy_main v0 have #[bevy_main] handle async main function in the correct manner for each arch
automatically update shader syntax WGSL syntax has been updated but the latest version is not yet supported in most places. it is needed to run WGSL shaders in chrome

The last two commits are updating examples and tests

commit details
update many examples update examples
fix tests fix tests

to build an example for WebGPU:

  • edit root Cargo.toml and remove the automatic webgl feature added for wasm32
  • cargo run -p build-wasm-example -- lighting --webgpu

Migration Guide

TODO

@mockersf mockersf added O-Web Specific to web (WASM) builds X-Controversial There is active debate or serious implications around merging this PR labels Jun 16, 2022
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen C-Performance A change motivated by improving speed, memory usage or compile times labels Jun 16, 2022
@mockersf
Copy link
Member Author

  • I didn't test all examples, but at least all the stress tests work
  • I didn't test android and iOS

This adds some boilerplate:

  • plugins need to build async, but async trait are not possible in Rust yet so https://crates.io/crates/async-trait is needed
  • creating an app is now async too, and that adds additional dependencies (and only small code change if we want to push for #[bevy_main])

@alice-i-cecile
Copy link
Member

Can we test this in CI yet? It looks like WebGPU support still isn't general access, so that may be hard 🤔

@mockersf
Copy link
Member Author

You can check if your browser supports WebGPU here: https://wgpu.rs/examples-gpu/?example=hello-triangle. I was able to run Bevy examples in Chromium 105.0.5114.0, but not on Firefox Nightly 103.0a1. It seems Firefox doesn't support the new WGSL syntax yet.

As for running in CI, Bevy assumes for now that wasm32 means WebGL2. It requires manual editing of the Cargo.toml file to comment those lines to build for WebGPU. This PR doesn't make it easy to build for WebGPU but possible (you can depend on sub crates instead of Bevy if you don't want to keep up a fork with the webgl feature removed). I think for now keeping webgl as default is a good choice as WebGPU is very young and it's not possible to have both webgl and webgpu enabled at the same time.

@DJMcNab DJMcNab added A-App Bevy apps and plugins M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Jun 16, 2022
@@ -763,12 +763,12 @@ impl App {
/// #
/// App::new().add_plugin(bevy_log::LogPlugin::default());
/// ```
pub fn add_plugin<T>(&mut self, plugin: T) -> &mut Self
pub async fn add_plugin<T>(&mut self, plugin: T) -> &mut Self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think adding plugins should ever be async. IMO they should just register things, not do any actual work until the startup systems run.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this, but that ties into the broader discussion in #1255.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current add_plugin does all the work of building the plugin immediately.
We could (and probably should) switch to a register_plugin that would actually build the plugin just before running, then only the last step would need to be async. That would open a lot of solutions for plugin ordering and dependencies

renderer::initialize_renderer(&instance, &options, &request_adapter_options),
);
let (device, queue, adapter_info) =
renderer::initialize_renderer(&instance, &options, &request_adapter_options).await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4913 moves this into a closure called once winit says that the app has resumed rather than immediatly running it when building the plugin. This is necessary for android and would solve all the threading through of async plugin building.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not enough in this case. I tried using #4913 and it crashed here:

let (device, queue, adapter_info) = futures_lite::future::block_on(
renderer::initialize_renderer(&instance, &options, &request_adapter_options),
);

The issue with wasm/WebGPU is not to do it later, it's to be able to wait for it without blocking.

I suppose the WebGPU context takes just long enough that it blocks. Some people reported a similar issue when using WebGL2 not in release mode, if initialising the renderer takes too long it doesn't work in wasm to block for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIth #4913 it would be possible to turn just the render initialize functions into async functions (hidden from the user) rather than forcing all plugin builders to be async (right in the face of every user).

I suppose the WebGPU context takes just long enough that it blocks.

Getting a value out of a javascript promise requires either calling .then() on it, or await on it. Using a mutex on the main thread like futures_lite does isn't possible in javascript. Only workers are allowed to block.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a mutex on the main thread like futures_lite does isn't possible in javascript

This is what Bevy is currently doing, and it works... if it's fast enough

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it works on systems where blocking is allowed (non-web platforms). Or when the future is already resolved (webgl on web as webgl isn't async).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that code can't work for WebGPU, even if its execution is delayed. If I understand correctly its asyncness needs to be kept, so at some point we need to expose something async to the user

Copy link
Contributor

@bjorn3 bjorn3 Jun 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to call the initialize_renderer function and then use wasm_bindgen_futures to run the rest of the render initialization after the promise resolves. This can be handled entirely inside bevy itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With #4913 the runner is essentially given responsibility to iterate over a vector of callbacks to handle initializing the render state.

One possibility is that those callbacks could be broken up relatively easily if there's some requirement to not block for too long with Web GPU for something. e.g. the runner could potentially run a single callback at a time and return to the browser mainloop between each callback if that would work to keep it happy.

Or, maybe as an alternative to making add_plugin() async then maybe the render_init callbacks could be made async for this. It would probably be fairly simple for the runner to continue running them as blocking functions via pollster::block_on in the general case but then there could be some extra care taken for webgpu to let them complete asynchronously.

Maybe the need to make the render_init functions async could be hidden for the most part from most plugins that don't need async render initialization by having separate APIs for .add_render_init and .add_async_render_init so only the few things dealing with webgpu would utilize async render init callbacks?

This was referenced Apr 6, 2023
@mockersf mockersf closed this Apr 29, 2023
cart pushed a commit that referenced this pull request May 4, 2023
# Objective

- Support WebGPU
- alternative to #5027 that doesn't need any async / await
- fixes #8315 
- Surprise fix #7318

## Solution

### For async renderer initialisation 

- Update the plugin lifecycle:
  - app builds the plugin
    - calls `plugin.build`
    - registers the plugin
  - app starts the event loop
- event loop waits for `ready` of all registered plugins in the same
order
    - returns `true` by default
- then call all `finish` then all `cleanup` in the same order as
registered
  - then execute the schedule

In the case of the renderer, to avoid anything async:
- building the renderer plugin creates a detached task that will send
back the initialised renderer through a mutex in a resource
- `ready` will wait for the renderer to be present in the resource
- `finish` will take that renderer and place it in the expected
resources by other plugins
- other plugins (that expect the renderer to be available) `finish` are
called and they are able to set up their pipelines
- `cleanup` is called, only custom one is still for pipeline rendering

### For WebGPU support

- update the `build-wasm-example` script to support passing `--api
webgpu` that will build the example with WebGPU support
- feature for webgl2 was always enabled when building for wasm. it's now
in the default feature list and enabled on all platforms, so check for
this feature must also check that the target_arch is `wasm32`

---

## Migration Guide

- `Plugin::setup` has been renamed `Plugin::cleanup`
- `Plugin::finish` has been added, and plugins adding pipelines should
do it in this function instead of `Plugin::build`
```rust
// Before
impl Plugin for MyPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource::<MyResource>
            .add_systems(Update, my_system);

        let render_app = match app.get_sub_app_mut(RenderApp) {
            Ok(render_app) => render_app,
            Err(_) => return,
        };

        render_app
            .init_resource::<RenderResourceNeedingDevice>()
            .init_resource::<OtherRenderResource>();
    }
}

// After
impl Plugin for MyPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource::<MyResource>
            .add_systems(Update, my_system);
    
        let render_app = match app.get_sub_app_mut(RenderApp) {
            Ok(render_app) => render_app,
            Err(_) => return,
        };
    
        render_app
            .init_resource::<OtherRenderResource>();
    }

    fn finish(&self, app: &mut App) {
        let render_app = match app.get_sub_app_mut(RenderApp) {
            Ok(render_app) => render_app,
            Err(_) => return,
        };
    
        render_app
            .init_resource::<RenderResourceNeedingDevice>();
    }
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-App Bevy apps and plugins A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible C-Performance A change motivated by improving speed, memory usage or compile times M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide O-Web Specific to web (WASM) builds X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

5 participants