Skip to content

Commit

Permalink
Webgpu support (#8336)
Browse files Browse the repository at this point in the history
# 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>();
    }
}
```
  • Loading branch information
mockersf authored May 4, 2023
1 parent 5da8af7 commit 71842c5
Show file tree
Hide file tree
Showing 46 changed files with 520 additions and 171 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ jobs:
target: wasm32-unknown-unknown
- name: Check wasm
run: cargo check --target wasm32-unknown-unknown
env:
RUSTFLAGS: --cfg=web_sys_unstable_apis

markdownlint:
runs-on: ubuntu-latest
Expand Down
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ default = [
"android_shared_stdcxx",
"tonemapping_luts",
"default_font",
"webgl2",
]

# Force dynamic linking, which improves iterative compile times
Expand Down Expand Up @@ -235,15 +236,13 @@ shader_format_glsl = ["bevy_internal/shader_format_glsl"]
# Enable support for shaders in SPIR-V
shader_format_spirv = ["bevy_internal/shader_format_spirv"]

# Enable some limitations to be able to use WebGL2. If not enabled, it will default to WebGPU in Wasm
webgl2 = ["bevy_internal/webgl"]

[dependencies]
bevy_dylib = { path = "crates/bevy_dylib", version = "0.11.0-dev", default-features = false, optional = true }
bevy_internal = { path = "crates/bevy_internal", version = "0.11.0-dev", default-features = false }

[target.'cfg(target_arch = "wasm32")'.dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.11.0-dev", default-features = false, features = [
"webgl",
] }

[dev-dependencies]
anyhow = "1.0.4"
rand = "0.8.0"
Expand Down
46 changes: 39 additions & 7 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ impl Default for App {
}
}

// Dummy plugin used to temporary hold the place in the plugin registry
struct PlaceholderPlugin;
impl Plugin for PlaceholderPlugin {
fn build(&self, _app: &mut App) {}
}

impl App {
/// Creates a new [`App`] with some default structure to enable core engine features.
/// This is the preferred constructor for most use cases.
Expand Down Expand Up @@ -288,19 +294,40 @@ impl App {
panic!("App::run() was called from within Plugin::build(), which is not allowed.");
}

Self::setup(&mut app);

let runner = std::mem::replace(&mut app.runner, Box::new(run_once));
(runner)(app);
}

/// Run [`Plugin::setup`] for each plugin. This is usually called by [`App::run`], but can
/// be useful for situations where you want to use [`App::update`].
pub fn setup(&mut self) {
/// Check that [`Plugin::ready`] of all plugins returns true. This is usually called by the
/// event loop, but can be useful for situations where you want to use [`App::update`]
pub fn ready(&self) -> bool {
for plugin in &self.plugin_registry {
if !plugin.ready(self) {
return false;
}
}
true
}

/// Run [`Plugin::finish`] for each plugin. This is usually called by the event loop once all
/// plugins are [`App::ready`], but can be useful for situations where you want to use
/// [`App::update`].
pub fn finish(&mut self) {
// temporarily remove the plugin registry to run each plugin's setup function on app.
let plugin_registry = std::mem::take(&mut self.plugin_registry);
for plugin in &plugin_registry {
plugin.finish(self);
}
self.plugin_registry = plugin_registry;
}

/// Run [`Plugin::cleanup`] for each plugin. This is usually called by the event loop after
/// [`App::finish`], but can be useful for situations where you want to use [`App::update`].
pub fn cleanup(&mut self) {
// temporarily remove the plugin registry to run each plugin's setup function on app.
let plugin_registry = std::mem::take(&mut self.plugin_registry);
for plugin in &plugin_registry {
plugin.setup(self);
plugin.cleanup(self);
}
self.plugin_registry = plugin_registry;
}
Expand Down Expand Up @@ -685,13 +712,18 @@ impl App {
plugin_name: plugin.name().to_string(),
})?;
}

// Reserve that position in the plugin registry. if a plugin adds plugins, they will be correctly ordered
let plugin_position_in_registry = self.plugin_registry.len();
self.plugin_registry.push(Box::new(PlaceholderPlugin));

self.building_plugin_depth += 1;
let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self)));
self.building_plugin_depth -= 1;
if let Err(payload) = result {
resume_unwind(payload);
}
self.plugin_registry.push(plugin);
self.plugin_registry[plugin_position_in_registry] = plugin;
Ok(self)
}

Expand Down
25 changes: 23 additions & 2 deletions crates/bevy_app/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,35 @@ use std::any::Any;
/// should be overridden to return `false`. Plugins are considered duplicate if they have the same
/// [`name()`](Self::name). The default `name()` implementation returns the type name, which means
/// generic plugins with different type parameters will not be considered duplicates.
///
/// ## Lifecycle of a plugin
///
/// When adding a plugin to an [`App`]:
/// * the app calls [`Plugin::build`] immediately, and register the plugin
/// * once the app started, it will wait for all registered [`Plugin::ready`] to return `true`
/// * it will then call all registered [`Plugin::finish`]
/// * and call all registered [`Plugin::cleanup`]
pub trait Plugin: Downcast + Any + Send + Sync {
/// Configures the [`App`] to which this plugin is added.
fn build(&self, app: &mut App);

/// Runs after all plugins are built, but before the app runner is called.
/// Has the plugin finished it's setup? This can be useful for plugins that needs something
/// asynchronous to happen before they can finish their setup, like renderer initialization.
/// Once the plugin is ready, [`finish`](Plugin::finish) should be called.
fn ready(&self, _app: &App) -> bool {
true
}

/// Finish adding this plugin to the [`App`], once all plugins registered are ready. This can
/// be useful for plugins that depends on another plugin asynchronous setup, like the renderer.
fn finish(&self, _app: &mut App) {
// do nothing
}

/// Runs after all plugins are built and finished, but before the app schedule is executed.
/// This can be useful if you have some resource that other plugins need during their build step,
/// but after build you want to remove it and send it to another thread.
fn setup(&self, _app: &mut App) {
fn cleanup(&self, _app: &mut App) {
// do nothing
}

Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_core_pipeline/src/blit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub struct BlitPlugin;
impl Plugin for BlitPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl);
}

fn finish(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return
};
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_core_pipeline/src/bloom/bloom.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ fn downsample_first(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32>
// Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the
// downscaling and upscaling which would result in black boxes.
// The upper bound is to prevent NaNs.
sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+38));
// with f32::MAX (E+38) Chrome fails with ":value 340282346999999984391321947108527833088.0 cannot be represented as 'f32'"
sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+37));

#ifdef USE_THRESHOLD
sample = soft_threshold(sample);
Expand Down
13 changes: 11 additions & 2 deletions crates/bevy_core_pipeline/src/bloom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ impl Plugin for BloomPlugin {
};

render_app
.init_resource::<BloomDownsamplingPipeline>()
.init_resource::<BloomUpsamplingPipeline>()
.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()
.add_systems(
Expand Down Expand Up @@ -95,6 +93,17 @@ impl Plugin for BloomPlugin {
],
);
}

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::<BloomDownsamplingPipeline>()
.init_resource::<BloomUpsamplingPipeline>();
}
}

pub struct BloomNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ impl Plugin for CASPlugin {
Err(_) => return,
};
render_app
.init_resource::<CASPipeline>()
.init_resource::<SpecializedRenderPipelines<CASPipeline>>()
.add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare));

Expand Down Expand Up @@ -149,6 +148,14 @@ impl Plugin for CASPlugin {
);
}
}

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::<CASPipeline>();
}
}

#[derive(Resource)]
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ impl Node for MainPass2dNode {

// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
// reset for the next render pass so add an empty render pass without a custom viewport
#[cfg(feature = "webgl")]
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
if camera.viewport.is_some() {
#[cfg(feature = "trace")]
let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl Node for MainTransparentPass3dNode {

// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
// reset for the next render pass so add an empty render pass without a custom viewport
#[cfg(feature = "webgl")]
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
if camera.viewport.is_some() {
#[cfg(feature = "trace")]
let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered();
Expand Down
9 changes: 8 additions & 1 deletion crates/bevy_core_pipeline/src/fxaa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ impl Plugin for FxaaPlugin {
Err(_) => return,
};
render_app
.init_resource::<FxaaPipeline>()
.init_resource::<SpecializedRenderPipelines<FxaaPipeline>>()
.add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare))
.add_render_graph_node::<FxaaNode>(CORE_3D, core_3d::graph::node::FXAA)
Expand All @@ -114,6 +113,14 @@ impl Plugin for FxaaPlugin {
],
);
}

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::<FxaaPipeline>();
}
}

#[derive(Resource, Deref)]
Expand Down
14 changes: 11 additions & 3 deletions crates/bevy_core_pipeline/src/skybox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ impl Plugin for SkyboxPlugin {
Err(_) => return,
};

let render_device = render_app.world.resource::<RenderDevice>().clone();

render_app
.insert_resource(SkyboxPipeline::new(&render_device))
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
.add_systems(
Render,
Expand All @@ -54,6 +51,17 @@ impl Plugin for SkyboxPlugin {
),
);
}

fn finish(&self, app: &mut App) {
let render_app = match app.get_sub_app_mut(RenderApp) {
Ok(render_app) => render_app,
Err(_) => return,
};

let render_device = render_app.world.resource::<RenderDevice>().clone();

render_app.insert_resource(SkyboxPipeline::new(&render_device));
}
}

/// Adds a skybox to a 3D camera, based on a cubemap texture.
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_core_pipeline/src/tonemapping/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,19 @@ impl Plugin for TonemappingPlugin {

if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<TonemappingPipeline>()
.init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
.add_systems(
Render,
queue_view_tonemapping_pipelines.in_set(RenderSet::Queue),
);
}
}

fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<TonemappingPipeline>();
}
}
}

#[derive(Resource)]
Expand Down
Loading

0 comments on commit 71842c5

Please sign in to comment.