Skip to content

Commit 71842c5

Browse files
authored
Webgpu support (#8336)
# 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>(); } } ```
1 parent 5da8af7 commit 71842c5

File tree

46 files changed

+520
-171
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+520
-171
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ jobs:
142142
target: wasm32-unknown-unknown
143143
- name: Check wasm
144144
run: cargo check --target wasm32-unknown-unknown
145+
env:
146+
RUSTFLAGS: --cfg=web_sys_unstable_apis
145147

146148
markdownlint:
147149
runs-on: ubuntu-latest

Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ default = [
5353
"android_shared_stdcxx",
5454
"tonemapping_luts",
5555
"default_font",
56+
"webgl2",
5657
]
5758

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

239+
# Enable some limitations to be able to use WebGL2. If not enabled, it will default to WebGPU in Wasm
240+
webgl2 = ["bevy_internal/webgl"]
241+
238242
[dependencies]
239243
bevy_dylib = { path = "crates/bevy_dylib", version = "0.11.0-dev", default-features = false, optional = true }
240244
bevy_internal = { path = "crates/bevy_internal", version = "0.11.0-dev", default-features = false }
241245

242-
[target.'cfg(target_arch = "wasm32")'.dependencies]
243-
bevy_internal = { path = "crates/bevy_internal", version = "0.11.0-dev", default-features = false, features = [
244-
"webgl",
245-
] }
246-
247246
[dev-dependencies]
248247
anyhow = "1.0.4"
249248
rand = "0.8.0"

crates/bevy_app/src/app.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ impl Default for App {
200200
}
201201
}
202202

203+
// Dummy plugin used to temporary hold the place in the plugin registry
204+
struct PlaceholderPlugin;
205+
impl Plugin for PlaceholderPlugin {
206+
fn build(&self, _app: &mut App) {}
207+
}
208+
203209
impl App {
204210
/// Creates a new [`App`] with some default structure to enable core engine features.
205211
/// This is the preferred constructor for most use cases.
@@ -288,19 +294,40 @@ impl App {
288294
panic!("App::run() was called from within Plugin::build(), which is not allowed.");
289295
}
290296

291-
Self::setup(&mut app);
292-
293297
let runner = std::mem::replace(&mut app.runner, Box::new(run_once));
294298
(runner)(app);
295299
}
296300

297-
/// Run [`Plugin::setup`] for each plugin. This is usually called by [`App::run`], but can
298-
/// be useful for situations where you want to use [`App::update`].
299-
pub fn setup(&mut self) {
301+
/// Check that [`Plugin::ready`] of all plugins returns true. This is usually called by the
302+
/// event loop, but can be useful for situations where you want to use [`App::update`]
303+
pub fn ready(&self) -> bool {
304+
for plugin in &self.plugin_registry {
305+
if !plugin.ready(self) {
306+
return false;
307+
}
308+
}
309+
true
310+
}
311+
312+
/// Run [`Plugin::finish`] for each plugin. This is usually called by the event loop once all
313+
/// plugins are [`App::ready`], but can be useful for situations where you want to use
314+
/// [`App::update`].
315+
pub fn finish(&mut self) {
316+
// temporarily remove the plugin registry to run each plugin's setup function on app.
317+
let plugin_registry = std::mem::take(&mut self.plugin_registry);
318+
for plugin in &plugin_registry {
319+
plugin.finish(self);
320+
}
321+
self.plugin_registry = plugin_registry;
322+
}
323+
324+
/// Run [`Plugin::cleanup`] for each plugin. This is usually called by the event loop after
325+
/// [`App::finish`], but can be useful for situations where you want to use [`App::update`].
326+
pub fn cleanup(&mut self) {
300327
// temporarily remove the plugin registry to run each plugin's setup function on app.
301328
let plugin_registry = std::mem::take(&mut self.plugin_registry);
302329
for plugin in &plugin_registry {
303-
plugin.setup(self);
330+
plugin.cleanup(self);
304331
}
305332
self.plugin_registry = plugin_registry;
306333
}
@@ -685,13 +712,18 @@ impl App {
685712
plugin_name: plugin.name().to_string(),
686713
})?;
687714
}
715+
716+
// Reserve that position in the plugin registry. if a plugin adds plugins, they will be correctly ordered
717+
let plugin_position_in_registry = self.plugin_registry.len();
718+
self.plugin_registry.push(Box::new(PlaceholderPlugin));
719+
688720
self.building_plugin_depth += 1;
689721
let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self)));
690722
self.building_plugin_depth -= 1;
691723
if let Err(payload) = result {
692724
resume_unwind(payload);
693725
}
694-
self.plugin_registry.push(plugin);
726+
self.plugin_registry[plugin_position_in_registry] = plugin;
695727
Ok(self)
696728
}
697729

crates/bevy_app/src/plugin.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,35 @@ use std::any::Any;
1313
/// should be overridden to return `false`. Plugins are considered duplicate if they have the same
1414
/// [`name()`](Self::name). The default `name()` implementation returns the type name, which means
1515
/// generic plugins with different type parameters will not be considered duplicates.
16+
///
17+
/// ## Lifecycle of a plugin
18+
///
19+
/// When adding a plugin to an [`App`]:
20+
/// * the app calls [`Plugin::build`] immediately, and register the plugin
21+
/// * once the app started, it will wait for all registered [`Plugin::ready`] to return `true`
22+
/// * it will then call all registered [`Plugin::finish`]
23+
/// * and call all registered [`Plugin::cleanup`]
1624
pub trait Plugin: Downcast + Any + Send + Sync {
1725
/// Configures the [`App`] to which this plugin is added.
1826
fn build(&self, app: &mut App);
1927

20-
/// Runs after all plugins are built, but before the app runner is called.
28+
/// Has the plugin finished it's setup? This can be useful for plugins that needs something
29+
/// asynchronous to happen before they can finish their setup, like renderer initialization.
30+
/// Once the plugin is ready, [`finish`](Plugin::finish) should be called.
31+
fn ready(&self, _app: &App) -> bool {
32+
true
33+
}
34+
35+
/// Finish adding this plugin to the [`App`], once all plugins registered are ready. This can
36+
/// be useful for plugins that depends on another plugin asynchronous setup, like the renderer.
37+
fn finish(&self, _app: &mut App) {
38+
// do nothing
39+
}
40+
41+
/// Runs after all plugins are built and finished, but before the app schedule is executed.
2142
/// This can be useful if you have some resource that other plugins need during their build step,
2243
/// but after build you want to remove it and send it to another thread.
23-
fn setup(&self, _app: &mut App) {
44+
fn cleanup(&self, _app: &mut App) {
2445
// do nothing
2546
}
2647

crates/bevy_core_pipeline/src/blit/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub struct BlitPlugin;
1515
impl Plugin for BlitPlugin {
1616
fn build(&self, app: &mut App) {
1717
load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl);
18+
}
19+
20+
fn finish(&self, app: &mut App) {
1821
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
1922
return
2023
};

crates/bevy_core_pipeline/src/bloom/bloom.wgsl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ fn downsample_first(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32>
130130
// Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the
131131
// downscaling and upscaling which would result in black boxes.
132132
// The upper bound is to prevent NaNs.
133-
sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+38));
133+
// with f32::MAX (E+38) Chrome fails with ":value 340282346999999984391321947108527833088.0 cannot be represented as 'f32'"
134+
sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+37));
134135

135136
#ifdef USE_THRESHOLD
136137
sample = soft_threshold(sample);

crates/bevy_core_pipeline/src/bloom/mod.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ impl Plugin for BloomPlugin {
6161
};
6262

6363
render_app
64-
.init_resource::<BloomDownsamplingPipeline>()
65-
.init_resource::<BloomUpsamplingPipeline>()
6664
.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
6765
.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()
6866
.add_systems(
@@ -95,6 +93,17 @@ impl Plugin for BloomPlugin {
9593
],
9694
);
9795
}
96+
97+
fn finish(&self, app: &mut App) {
98+
let render_app = match app.get_sub_app_mut(RenderApp) {
99+
Ok(render_app) => render_app,
100+
Err(_) => return,
101+
};
102+
103+
render_app
104+
.init_resource::<BloomDownsamplingPipeline>()
105+
.init_resource::<BloomUpsamplingPipeline>();
106+
}
98107
}
99108

100109
pub struct BloomNode {

crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ impl Plugin for CASPlugin {
116116
Err(_) => return,
117117
};
118118
render_app
119-
.init_resource::<CASPipeline>()
120119
.init_resource::<SpecializedRenderPipelines<CASPipeline>>()
121120
.add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare));
122121

@@ -149,6 +148,14 @@ impl Plugin for CASPlugin {
149148
);
150149
}
151150
}
151+
152+
fn finish(&self, app: &mut App) {
153+
let render_app = match app.get_sub_app_mut(RenderApp) {
154+
Ok(render_app) => render_app,
155+
Err(_) => return,
156+
};
157+
render_app.init_resource::<CASPipeline>();
158+
}
152159
}
153160

154161
#[derive(Resource)]

crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl Node for MainPass2dNode {
8181

8282
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
8383
// reset for the next render pass so add an empty render pass without a custom viewport
84-
#[cfg(feature = "webgl")]
84+
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
8585
if camera.viewport.is_some() {
8686
#[cfg(feature = "trace")]
8787
let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered();

crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ impl Node for MainTransparentPass3dNode {
9292

9393
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
9494
// reset for the next render pass so add an empty render pass without a custom viewport
95-
#[cfg(feature = "webgl")]
95+
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
9696
if camera.viewport.is_some() {
9797
#[cfg(feature = "trace")]
9898
let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered();

crates/bevy_core_pipeline/src/fxaa/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ impl Plugin for FxaaPlugin {
9292
Err(_) => return,
9393
};
9494
render_app
95-
.init_resource::<FxaaPipeline>()
9695
.init_resource::<SpecializedRenderPipelines<FxaaPipeline>>()
9796
.add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare))
9897
.add_render_graph_node::<FxaaNode>(CORE_3D, core_3d::graph::node::FXAA)
@@ -114,6 +113,14 @@ impl Plugin for FxaaPlugin {
114113
],
115114
);
116115
}
116+
117+
fn finish(&self, app: &mut App) {
118+
let render_app = match app.get_sub_app_mut(RenderApp) {
119+
Ok(render_app) => render_app,
120+
Err(_) => return,
121+
};
122+
render_app.init_resource::<FxaaPipeline>();
123+
}
117124
}
118125

119126
#[derive(Resource, Deref)]

crates/bevy_core_pipeline/src/skybox/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ impl Plugin for SkyboxPlugin {
4141
Err(_) => return,
4242
};
4343

44-
let render_device = render_app.world.resource::<RenderDevice>().clone();
45-
4644
render_app
47-
.insert_resource(SkyboxPipeline::new(&render_device))
4845
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
4946
.add_systems(
5047
Render,
@@ -54,6 +51,17 @@ impl Plugin for SkyboxPlugin {
5451
),
5552
);
5653
}
54+
55+
fn finish(&self, app: &mut App) {
56+
let render_app = match app.get_sub_app_mut(RenderApp) {
57+
Ok(render_app) => render_app,
58+
Err(_) => return,
59+
};
60+
61+
let render_device = render_app.world.resource::<RenderDevice>().clone();
62+
63+
render_app.insert_resource(SkyboxPipeline::new(&render_device));
64+
}
5765
}
5866

5967
/// Adds a skybox to a 3D camera, based on a cubemap texture.

crates/bevy_core_pipeline/src/tonemapping/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,19 @@ impl Plugin for TonemappingPlugin {
9292

9393
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
9494
render_app
95-
.init_resource::<TonemappingPipeline>()
9695
.init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
9796
.add_systems(
9897
Render,
9998
queue_view_tonemapping_pipelines.in_set(RenderSet::Queue),
10099
);
101100
}
102101
}
102+
103+
fn finish(&self, app: &mut App) {
104+
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
105+
render_app.init_resource::<TonemappingPipeline>();
106+
}
107+
}
103108
}
104109

105110
#[derive(Resource)]

0 commit comments

Comments
 (0)