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>();
    }
}
```
This commit is contained in:
François 2023-05-05 00:07:57 +02:00 committed by GitHub
parent 5da8af7d37
commit 71842c5ac9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 519 additions and 170 deletions

View file

@ -142,6 +142,8 @@ jobs:
target: wasm32-unknown-unknown target: wasm32-unknown-unknown
- name: Check wasm - name: Check wasm
run: cargo check --target wasm32-unknown-unknown run: cargo check --target wasm32-unknown-unknown
env:
RUSTFLAGS: --cfg=web_sys_unstable_apis
markdownlint: markdownlint:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -53,6 +53,7 @@ default = [
"android_shared_stdcxx", "android_shared_stdcxx",
"tonemapping_luts", "tonemapping_luts",
"default_font", "default_font",
"webgl2",
] ]
# Force dynamic linking, which improves iterative compile times # Force dynamic linking, which improves iterative compile times
@ -235,15 +236,13 @@ shader_format_glsl = ["bevy_internal/shader_format_glsl"]
# Enable support for shaders in SPIR-V # Enable support for shaders in SPIR-V
shader_format_spirv = ["bevy_internal/shader_format_spirv"] 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] [dependencies]
bevy_dylib = { path = "crates/bevy_dylib", version = "0.11.0-dev", default-features = false, optional = true } 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 } 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] [dev-dependencies]
anyhow = "1.0.4" anyhow = "1.0.4"
rand = "0.8.0" rand = "0.8.0"

View file

@ -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 { impl App {
/// Creates a new [`App`] with some default structure to enable core engine features. /// Creates a new [`App`] with some default structure to enable core engine features.
/// This is the preferred constructor for most use cases. /// This is the preferred constructor for most use cases.
@ -288,19 +294,40 @@ impl App {
panic!("App::run() was called from within Plugin::build(), which is not allowed."); 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)); let runner = std::mem::replace(&mut app.runner, Box::new(run_once));
(runner)(app); (runner)(app);
} }
/// Run [`Plugin::setup`] for each plugin. This is usually called by [`App::run`], but can /// Check that [`Plugin::ready`] of all plugins returns true. This is usually called by the
/// be useful for situations where you want to use [`App::update`]. /// event loop, but can be useful for situations where you want to use [`App::update`]
pub fn setup(&mut self) { 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. // temporarily remove the plugin registry to run each plugin's setup function on app.
let plugin_registry = std::mem::take(&mut self.plugin_registry); let plugin_registry = std::mem::take(&mut self.plugin_registry);
for plugin in &plugin_registry { for plugin in &plugin_registry {
plugin.setup(self); 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.cleanup(self);
} }
self.plugin_registry = plugin_registry; self.plugin_registry = plugin_registry;
} }
@ -685,13 +712,18 @@ impl App {
plugin_name: plugin.name().to_string(), 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; self.building_plugin_depth += 1;
let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self))); let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self)));
self.building_plugin_depth -= 1; self.building_plugin_depth -= 1;
if let Err(payload) = result { if let Err(payload) = result {
resume_unwind(payload); resume_unwind(payload);
} }
self.plugin_registry.push(plugin); self.plugin_registry[plugin_position_in_registry] = plugin;
Ok(self) Ok(self)
} }

View file

@ -13,14 +13,35 @@ use std::any::Any;
/// should be overridden to return `false`. Plugins are considered duplicate if they have the same /// 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 /// [`name()`](Self::name). The default `name()` implementation returns the type name, which means
/// generic plugins with different type parameters will not be considered duplicates. /// 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 { pub trait Plugin: Downcast + Any + Send + Sync {
/// Configures the [`App`] to which this plugin is added. /// Configures the [`App`] to which this plugin is added.
fn build(&self, app: &mut App); 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, /// 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. /// 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 // do nothing
} }

View file

@ -15,6 +15,9 @@ pub struct BlitPlugin;
impl Plugin for BlitPlugin { impl Plugin for BlitPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl); 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 { let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return return
}; };

View file

@ -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 // Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the
// downscaling and upscaling which would result in black boxes. // downscaling and upscaling which would result in black boxes.
// The upper bound is to prevent NaNs. // 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 #ifdef USE_THRESHOLD
sample = soft_threshold(sample); sample = soft_threshold(sample);

View file

@ -61,8 +61,6 @@ impl Plugin for BloomPlugin {
}; };
render_app render_app
.init_resource::<BloomDownsamplingPipeline>()
.init_resource::<BloomUpsamplingPipeline>()
.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>() .init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>() .init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()
.add_systems( .add_systems(
@ -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 { pub struct BloomNode {

View file

@ -116,7 +116,6 @@ impl Plugin for CASPlugin {
Err(_) => return, Err(_) => return,
}; };
render_app render_app
.init_resource::<CASPipeline>()
.init_resource::<SpecializedRenderPipelines<CASPipeline>>() .init_resource::<SpecializedRenderPipelines<CASPipeline>>()
.add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare)); .add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare));
@ -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)] #[derive(Resource)]

View file

@ -81,7 +81,7 @@ impl Node for MainPass2dNode {
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't // 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 // 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() { if camera.viewport.is_some() {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered(); let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered();

View file

@ -92,7 +92,7 @@ impl Node for MainTransparentPass3dNode {
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't // 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 // 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() { if camera.viewport.is_some() {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered(); let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered();

View file

@ -92,7 +92,6 @@ impl Plugin for FxaaPlugin {
Err(_) => return, Err(_) => return,
}; };
render_app render_app
.init_resource::<FxaaPipeline>()
.init_resource::<SpecializedRenderPipelines<FxaaPipeline>>() .init_resource::<SpecializedRenderPipelines<FxaaPipeline>>()
.add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare)) .add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare))
.add_render_graph_node::<FxaaNode>(CORE_3D, core_3d::graph::node::FXAA) .add_render_graph_node::<FxaaNode>(CORE_3D, core_3d::graph::node::FXAA)
@ -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)] #[derive(Resource, Deref)]

View file

@ -41,10 +41,7 @@ impl Plugin for SkyboxPlugin {
Err(_) => return, Err(_) => return,
}; };
let render_device = render_app.world.resource::<RenderDevice>().clone();
render_app render_app
.insert_resource(SkyboxPipeline::new(&render_device))
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>() .init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
.add_systems( .add_systems(
Render, Render,
@ -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. /// Adds a skybox to a 3D camera, based on a cubemap texture.

View file

@ -92,7 +92,6 @@ impl Plugin for TonemappingPlugin {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.init_resource::<TonemappingPipeline>()
.init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>() .init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
.add_systems( .add_systems(
Render, Render,
@ -100,6 +99,12 @@ impl Plugin for TonemappingPlugin {
); );
} }
} }
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<TonemappingPipeline>();
}
}
} }
#[derive(Resource)] #[derive(Resource)]

View file

@ -53,23 +53,23 @@ fn tonemap_curve3(v: vec3<f32>) -> vec3<f32> {
} }
fn somewhat_boring_display_transform(col: vec3<f32>) -> vec3<f32> { fn somewhat_boring_display_transform(col: vec3<f32>) -> vec3<f32> {
var col = col; var boring_color = col;
let ycbcr = rgb_to_ycbcr(col); let ycbcr = rgb_to_ycbcr(boring_color);
let bt = tonemap_curve(length(ycbcr.yz) * 2.4); let bt = tonemap_curve(length(ycbcr.yz) * 2.4);
var desat = max((bt - 0.7) * 0.8, 0.0); var desat = max((bt - 0.7) * 0.8, 0.0);
desat *= desat; desat *= desat;
let desat_col = mix(col.rgb, ycbcr.xxx, desat); let desat_col = mix(boring_color.rgb, ycbcr.xxx, desat);
let tm_luma = tonemap_curve(ycbcr.x); let tm_luma = tonemap_curve(ycbcr.x);
let tm0 = col.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(col.rgb))); let tm0 = boring_color.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(boring_color.rgb)));
let final_mult = 0.97; let final_mult = 0.97;
let tm1 = tonemap_curve3(desat_col); let tm1 = tonemap_curve3(desat_col);
col = mix(tm0, tm1, bt * bt); boring_color = mix(tm0, tm1, bt * bt);
return col * final_mult; return boring_color * final_mult;
} }
// ------------------------------------------ // ------------------------------------------
@ -110,7 +110,7 @@ fn RRTAndODTFit(v: vec3<f32>) -> vec3<f32> {
} }
fn ACESFitted(color: vec3<f32>) -> vec3<f32> { fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
var color = color; var fitted_color = color;
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
let rgb_to_rrt = mat3x3<f32>( let rgb_to_rrt = mat3x3<f32>(
@ -126,17 +126,17 @@ fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
vec3(-0.00327, -0.07276, 1.07602) vec3(-0.00327, -0.07276, 1.07602)
); );
color *= rgb_to_rrt; fitted_color *= rgb_to_rrt;
// Apply RRT and ODT // Apply RRT and ODT
color = RRTAndODTFit(color); fitted_color = RRTAndODTFit(fitted_color);
color *= odt_to_rgb; fitted_color *= odt_to_rgb;
// Clamp to [0, 1] // Clamp to [0, 1]
color = saturate(color); fitted_color = saturate(fitted_color);
return color; return fitted_color;
} }
// ------------------------------- // -------------------------------
@ -171,30 +171,30 @@ fn convertOpenDomainToNormalizedLog2(color: vec3<f32>, minimum_ev: f32, maximum_
let in_midgray = 0.18; let in_midgray = 0.18;
// remove negative before log transform // remove negative before log transform
var color = max(vec3(0.0), color); var normalized_color = max(vec3(0.0), color);
// avoid infinite issue with log -- ref[1] // avoid infinite issue with log -- ref[1]
color = select(color, 0.00001525878 + color, color < 0.00003051757); normalized_color = select(normalized_color, 0.00001525878 + normalized_color, normalized_color < vec3<f32>(0.00003051757));
color = clamp( normalized_color = clamp(
log2(color / in_midgray), log2(normalized_color / in_midgray),
vec3(minimum_ev), vec3(minimum_ev),
vec3(maximum_ev) vec3(maximum_ev)
); );
let total_exposure = maximum_ev - minimum_ev; let total_exposure = maximum_ev - minimum_ev;
return (color - minimum_ev) / total_exposure; return (normalized_color - minimum_ev) / total_exposure;
} }
// Inverse of above // Inverse of above
fn convertNormalizedLog2ToOpenDomain(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> { fn convertNormalizedLog2ToOpenDomain(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
var color = color; var open_color = color;
let in_midgray = 0.18; let in_midgray = 0.18;
let total_exposure = maximum_ev - minimum_ev; let total_exposure = maximum_ev - minimum_ev;
color = (color * total_exposure) + minimum_ev; open_color = (open_color * total_exposure) + minimum_ev;
color = pow(vec3(2.0), color); open_color = pow(vec3(2.0), open_color);
color = color * in_midgray; open_color = open_color * in_midgray;
return color; return open_color;
} }
@ -204,16 +204,16 @@ fn convertNormalizedLog2ToOpenDomain(color: vec3<f32>, minimum_ev: f32, maximum_
// Prepare the data for display encoding. Converted to log domain. // Prepare the data for display encoding. Converted to log domain.
fn applyAgXLog(Image: vec3<f32>) -> vec3<f32> { fn applyAgXLog(Image: vec3<f32>) -> vec3<f32> {
var Image = max(vec3(0.0), Image); // clamp negatives var prepared_image = max(vec3(0.0), Image); // clamp negatives
let r = dot(Image, vec3(0.84247906, 0.0784336, 0.07922375)); let r = dot(prepared_image, vec3(0.84247906, 0.0784336, 0.07922375));
let g = dot(Image, vec3(0.04232824, 0.87846864, 0.07916613)); let g = dot(prepared_image, vec3(0.04232824, 0.87846864, 0.07916613));
let b = dot(Image, vec3(0.04237565, 0.0784336, 0.87914297)); let b = dot(prepared_image, vec3(0.04237565, 0.0784336, 0.87914297));
Image = vec3(r, g, b); prepared_image = vec3(r, g, b);
Image = convertOpenDomainToNormalizedLog2(Image, -10.0, 6.5); prepared_image = convertOpenDomainToNormalizedLog2(prepared_image, -10.0, 6.5);
Image = clamp(Image, vec3(0.0), vec3(1.0)); prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
return Image; return prepared_image;
} }
fn applyLUT3D(Image: vec3<f32>, block_size: f32) -> vec3<f32> { fn applyLUT3D(Image: vec3<f32>, block_size: f32) -> vec3<f32> {

View file

@ -97,7 +97,6 @@ impl Plugin for GizmoPlugin {
render_app render_app
.add_render_command::<Transparent2d, DrawGizmoLines>() .add_render_command::<Transparent2d, DrawGizmoLines>()
.init_resource::<GizmoLinePipeline>()
.init_resource::<SpecializedMeshPipelines<GizmoLinePipeline>>() .init_resource::<SpecializedMeshPipelines<GizmoLinePipeline>>()
.add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue)); .add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue));
} }
@ -109,11 +108,28 @@ impl Plugin for GizmoPlugin {
render_app render_app
.add_render_command::<Opaque3d, DrawGizmoLines>() .add_render_command::<Opaque3d, DrawGizmoLines>()
.init_resource::<GizmoPipeline>()
.init_resource::<SpecializedMeshPipelines<GizmoPipeline>>() .init_resource::<SpecializedMeshPipelines<GizmoPipeline>>()
.add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue)); .add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue));
} }
} }
fn finish(&self, app: &mut bevy_app::App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
#[cfg(feature = "bevy_sprite")]
{
use pipeline_2d::*;
render_app.init_resource::<GizmoLinePipeline>();
}
#[cfg(feature = "bevy_pbr")]
{
use pipeline_3d::*;
render_app.init_resource::<GizmoPipeline>();
}
}
} }
/// A [`Resource`] that stores configuration for gizmos. /// A [`Resource`] that stores configuration for gizmos.

View file

@ -293,9 +293,7 @@ impl Plugin for PbrPlugin {
sort_phase_system::<Shadow>.in_set(RenderSet::PhaseSort), sort_phase_system::<Shadow>.in_set(RenderSet::PhaseSort),
), ),
) )
.init_resource::<ShadowSamplers>() .init_resource::<LightMeta>();
.init_resource::<LightMeta>()
.init_resource::<GlobalLightMeta>();
let shadow_pass_node = ShadowPassNode::new(&mut render_app.world); let shadow_pass_node = ShadowPassNode::new(&mut render_app.world);
let mut graph = render_app.world.resource_mut::<RenderGraph>(); let mut graph = render_app.world.resource_mut::<RenderGraph>();
@ -308,4 +306,16 @@ impl Plugin for PbrPlugin {
bevy_core_pipeline::core_3d::graph::node::START_MAIN_PASS, bevy_core_pipeline::core_3d::graph::node::START_MAIN_PASS,
); );
} }
fn finish(&self, app: &mut App) {
let render_app = match app.get_sub_app_mut(RenderApp) {
Ok(render_app) => render_app,
Err(_) => return,
};
// Extract the required data from the main world
render_app
.init_resource::<ShadowSamplers>()
.init_resource::<GlobalLightMeta>();
}
} }

View file

@ -353,7 +353,7 @@ impl CascadeShadowConfigBuilder {
impl Default for CascadeShadowConfigBuilder { impl Default for CascadeShadowConfigBuilder {
fn default() -> Self { fn default() -> Self {
if cfg!(feature = "webgl") { if cfg!(all(feature = "webgl", target_arch = "wasm32")) {
// Currently only support one cascade in webgl. // Currently only support one cascade in webgl.
Self { Self {
num_cascades: 1, num_cascades: 1,

View file

@ -196,7 +196,6 @@ where
.add_render_command::<Transparent3d, DrawMaterial<M>>() .add_render_command::<Transparent3d, DrawMaterial<M>>()
.add_render_command::<Opaque3d, DrawMaterial<M>>() .add_render_command::<Opaque3d, DrawMaterial<M>>()
.add_render_command::<AlphaMask3d, DrawMaterial<M>>() .add_render_command::<AlphaMask3d, DrawMaterial<M>>()
.init_resource::<MaterialPipeline<M>>()
.init_resource::<ExtractedMaterials<M>>() .init_resource::<ExtractedMaterials<M>>()
.init_resource::<RenderMaterials<M>>() .init_resource::<RenderMaterials<M>>()
.init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>() .init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>()
@ -220,6 +219,12 @@ where
app.add_plugin(PrepassPlugin::<M>::default()); app.add_plugin(PrepassPlugin::<M>::default());
} }
} }
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<MaterialPipeline<M>>();
}
}
} }
/// A key uniquely identifying a specialized [`MaterialPipeline`]. /// A key uniquely identifying a specialized [`MaterialPipeline`].

View file

@ -108,11 +108,18 @@ where
Render, Render,
queue_prepass_view_bind_group::<M>.in_set(RenderSet::Queue), queue_prepass_view_bind_group::<M>.in_set(RenderSet::Queue),
) )
.init_resource::<PrepassPipeline<M>>()
.init_resource::<PrepassViewBindGroup>() .init_resource::<PrepassViewBindGroup>()
.init_resource::<SpecializedMeshPipelines<PrepassPipeline<M>>>() .init_resource::<SpecializedMeshPipelines<PrepassPipeline<M>>>()
.init_resource::<PreviousViewProjectionUniforms>(); .init_resource::<PreviousViewProjectionUniforms>();
} }
fn finish(&self, app: &mut bevy_app::App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<PrepassPipeline<M>>();
}
} }
/// Sets up the prepasses for a [`Material`]. /// Sets up the prepasses for a [`Material`].

View file

@ -217,9 +217,9 @@ pub struct GpuLights {
// NOTE: this must be kept in sync with the same constants in pbr.frag // NOTE: this must be kept in sync with the same constants in pbr.frag
pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256; pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256;
pub const MAX_DIRECTIONAL_LIGHTS: usize = 10; pub const MAX_DIRECTIONAL_LIGHTS: usize = 10;
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
pub const MAX_CASCADES_PER_LIGHT: usize = 4; pub const MAX_CASCADES_PER_LIGHT: usize = 4;
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
pub const MAX_CASCADES_PER_LIGHT: usize = 1; pub const MAX_CASCADES_PER_LIGHT: usize = 1;
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
@ -683,13 +683,13 @@ pub fn prepare_lights(
let mut point_lights: Vec<_> = point_lights.iter().collect::<Vec<_>>(); let mut point_lights: Vec<_> = point_lights.iter().collect::<Vec<_>>();
let mut directional_lights: Vec<_> = directional_lights.iter().collect::<Vec<_>>(); let mut directional_lights: Vec<_> = directional_lights.iter().collect::<Vec<_>>();
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
let max_texture_array_layers = render_device.limits().max_texture_array_layers as usize; let max_texture_array_layers = render_device.limits().max_texture_array_layers as usize;
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
let max_texture_cubes = max_texture_array_layers / 6; let max_texture_cubes = max_texture_array_layers / 6;
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
let max_texture_array_layers = 1; let max_texture_array_layers = 1;
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
let max_texture_cubes = 1; let max_texture_cubes = 1;
if !*max_directional_lights_warning_emitted && directional_lights.len() > MAX_DIRECTIONAL_LIGHTS if !*max_directional_lights_warning_emitted && directional_lights.len() > MAX_DIRECTIONAL_LIGHTS
@ -1162,9 +1162,9 @@ pub fn prepare_lights(
.create_view(&TextureViewDescriptor { .create_view(&TextureViewDescriptor {
label: Some("point_light_shadow_map_array_texture_view"), label: Some("point_light_shadow_map_array_texture_view"),
format: None, format: None,
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
dimension: Some(TextureViewDimension::CubeArray), dimension: Some(TextureViewDimension::CubeArray),
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
dimension: Some(TextureViewDimension::Cube), dimension: Some(TextureViewDimension::Cube),
aspect: TextureAspect::All, aspect: TextureAspect::All,
base_mip_level: 0, base_mip_level: 0,
@ -1177,9 +1177,9 @@ pub fn prepare_lights(
.create_view(&TextureViewDescriptor { .create_view(&TextureViewDescriptor {
label: Some("directional_light_shadow_map_array_texture_view"), label: Some("directional_light_shadow_map_array_texture_view"),
format: None, format: None,
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
dimension: Some(TextureViewDimension::D2Array), dimension: Some(TextureViewDimension::D2Array),
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
dimension: Some(TextureViewDimension::D2), dimension: Some(TextureViewDimension::D2),
aspect: TextureAspect::All, aspect: TextureAspect::All,
base_mip_level: 0, base_mip_level: 0,

View file

@ -106,7 +106,6 @@ impl Plugin for MeshRenderPlugin {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.init_resource::<MeshPipeline>()
.init_resource::<SkinnedMeshUniform>() .init_resource::<SkinnedMeshUniform>()
.add_systems(ExtractSchedule, (extract_meshes, extract_skinned_meshes)) .add_systems(ExtractSchedule, (extract_meshes, extract_skinned_meshes))
.add_systems( .add_systems(
@ -119,6 +118,12 @@ impl Plugin for MeshRenderPlugin {
); );
} }
} }
fn finish(&self, app: &mut bevy_app::App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<MeshPipeline>();
}
}
} }
#[derive(Component, ShaderType, Clone)] #[derive(Component, ShaderType, Clone)]
@ -328,9 +333,9 @@ impl FromWorld for MeshPipeline {
ty: BindingType::Texture { ty: BindingType::Texture {
multisampled: false, multisampled: false,
sample_type: TextureSampleType::Depth, sample_type: TextureSampleType::Depth,
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
view_dimension: TextureViewDimension::CubeArray, view_dimension: TextureViewDimension::CubeArray,
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
view_dimension: TextureViewDimension::Cube, view_dimension: TextureViewDimension::Cube,
}, },
count: None, count: None,
@ -349,9 +354,9 @@ impl FromWorld for MeshPipeline {
ty: BindingType::Texture { ty: BindingType::Texture {
multisampled: false, multisampled: false,
sample_type: TextureSampleType::Depth, sample_type: TextureSampleType::Depth,
#[cfg(not(feature = "webgl"))] #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))]
view_dimension: TextureViewDimension::D2Array, view_dimension: TextureViewDimension::D2Array,
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
view_dimension: TextureViewDimension::D2, view_dimension: TextureViewDimension::D2,
}, },
count: None, count: None,
@ -439,7 +444,9 @@ impl FromWorld for MeshPipeline {
let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]); let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]);
entries.extend_from_slice(&tonemapping_lut_entries); entries.extend_from_slice(&tonemapping_lut_entries);
if cfg!(not(feature = "webgl")) || (cfg!(feature = "webgl") && !multisampled) { if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32")))
|| (cfg!(all(feature = "webgl", target_arch = "wasm32")) && !multisampled)
{
entries.extend_from_slice(&prepass::get_bind_group_layout_entries( entries.extend_from_slice(&prepass::get_bind_group_layout_entries(
[16, 17, 18], [16, 17, 18],
multisampled, multisampled,
@ -1060,7 +1067,9 @@ pub fn queue_mesh_view_bind_groups(
entries.extend_from_slice(&tonemapping_luts); entries.extend_from_slice(&tonemapping_luts);
// When using WebGL, we can't have a depth texture with multisampling // When using WebGL, we can't have a depth texture with multisampling
if cfg!(not(feature = "webgl")) || (cfg!(feature = "webgl") && msaa.samples() == 1) { if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32")))
|| (cfg!(all(feature = "webgl", target_arch = "wasm32")) && msaa.samples() == 1)
{
entries.extend_from_slice(&prepass::get_bindings( entries.extend_from_slice(&prepass::get_bindings(
prepass_textures, prepass_textures,
&mut fallback_images, &mut fallback_images,

View file

@ -21,14 +21,14 @@ fn parallaxed_uv(
max_layer_count: f32, max_layer_count: f32,
max_steps: u32, max_steps: u32,
// The original interpolated uv // The original interpolated uv
uv: vec2<f32>, original_uv: vec2<f32>,
// The vector from the camera to the fragment at the surface in tangent space // The vector from the camera to the fragment at the surface in tangent space
Vt: vec3<f32>, Vt: vec3<f32>,
) -> vec2<f32> { ) -> vec2<f32> {
if max_layer_count < 1.0 { if max_layer_count < 1.0 {
return uv; return original_uv;
} }
var uv = uv; var uv = original_uv;
// Steep Parallax Mapping // Steep Parallax Mapping
// ====================== // ======================

View file

@ -46,11 +46,16 @@ impl Plugin for WireframePlugin {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.add_render_command::<Opaque3d, DrawWireframes>() .add_render_command::<Opaque3d, DrawWireframes>()
.init_resource::<WireframePipeline>()
.init_resource::<SpecializedMeshPipelines<WireframePipeline>>() .init_resource::<SpecializedMeshPipelines<WireframePipeline>>()
.add_systems(Render, queue_wireframes.in_set(RenderSet::Queue)); .add_systems(Render, queue_wireframes.in_set(RenderSet::Queue));
} }
} }
fn finish(&self, app: &mut bevy_app::App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<WireframePipeline>();
}
}
} }
/// Controls whether an entity should rendered in wireframe-mode if the [`WireframePlugin`] is enabled /// Controls whether an entity should rendered in wireframe-mode if the [`WireframePlugin`] is enabled

View file

@ -54,7 +54,7 @@ pub struct GlobalsUniform {
/// It wraps to zero when it reaches the maximum value of a u32. /// It wraps to zero when it reaches the maximum value of a u32.
frame_count: u32, frame_count: u32,
/// WebGL2 structs must be 16 byte aligned. /// WebGL2 structs must be 16 byte aligned.
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
_wasm_padding: f32, _wasm_padding: f32,
} }

View file

@ -44,6 +44,8 @@ pub mod prelude {
use bevy_window::{PrimaryWindow, RawHandleWrapper}; use bevy_window::{PrimaryWindow, RawHandleWrapper};
use globals::GlobalsPlugin; use globals::GlobalsPlugin;
pub use once_cell; pub use once_cell;
use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue};
use wgpu::Instance;
use crate::{ use crate::{
camera::CameraPlugin, camera::CameraPlugin,
@ -57,7 +59,10 @@ use bevy_app::{App, AppLabel, Plugin, SubApp};
use bevy_asset::{AddAsset, AssetServer}; use bevy_asset::{AddAsset, AssetServer};
use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemState}; use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemState};
use bevy_utils::tracing::debug; use bevy_utils::tracing::debug;
use std::ops::{Deref, DerefMut}; use std::{
ops::{Deref, DerefMut},
sync::{Arc, Mutex},
};
/// Contains the default Bevy rendering backend based on wgpu. /// Contains the default Bevy rendering backend based on wgpu.
#[derive(Default)] #[derive(Default)]
@ -184,6 +189,21 @@ pub mod main_graph {
} }
} }
#[derive(Resource)]
struct FutureRendererResources(
Arc<
Mutex<
Option<(
RenderDevice,
RenderQueue,
RenderAdapterInfo,
RenderAdapter,
Instance,
)>,
>,
>,
);
/// A Label for the rendering sub-app. /// A Label for the rendering sub-app.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
pub struct RenderApp; pub struct RenderApp;
@ -196,41 +216,54 @@ impl Plugin for RenderPlugin {
.init_asset_loader::<ShaderLoader>() .init_asset_loader::<ShaderLoader>()
.init_debug_asset_loader::<ShaderLoader>(); .init_debug_asset_loader::<ShaderLoader>();
let mut system_state: SystemState<Query<&RawHandleWrapper, With<PrimaryWindow>>> =
SystemState::new(&mut app.world);
let primary_window = system_state.get(&app.world);
if let Some(backends) = self.wgpu_settings.backends { if let Some(backends) = self.wgpu_settings.backends {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { let future_renderer_resources_wrapper = Arc::new(Mutex::new(None));
backends, app.insert_resource(FutureRendererResources(
dx12_shader_compiler: self.wgpu_settings.dx12_shader_compiler.clone(), future_renderer_resources_wrapper.clone(),
}); ));
let surface = primary_window.get_single().ok().map(|wrapper| unsafe {
// SAFETY: Plugins should be set up on the main thread.
let handle = wrapper.get_handle();
instance
.create_surface(&handle)
.expect("Failed to create wgpu surface")
});
let request_adapter_options = wgpu::RequestAdapterOptions { let mut system_state: SystemState<Query<&RawHandleWrapper, With<PrimaryWindow>>> =
power_preference: self.wgpu_settings.power_preference, SystemState::new(&mut app.world);
compatible_surface: surface.as_ref(), let primary_window = system_state.get(&app.world).get_single().ok().cloned();
..Default::default()
}; let settings = self.wgpu_settings.clone();
let (device, queue, adapter_info, render_adapter) = bevy_tasks::IoTaskPool::get()
futures_lite::future::block_on(renderer::initialize_renderer( .spawn_local(async move {
&instance, let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
&self.wgpu_settings, backends,
&request_adapter_options, dx12_shader_compiler: settings.dx12_shader_compiler.clone(),
)); });
debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); let surface = primary_window.map(|wrapper| unsafe {
debug!("Configured wgpu adapter Features: {:#?}", device.features()); // SAFETY: Plugins should be set up on the main thread.
app.insert_resource(device.clone()) let handle = wrapper.get_handle();
.insert_resource(queue.clone()) instance
.insert_resource(adapter_info.clone()) .create_surface(&handle)
.insert_resource(render_adapter.clone()) .expect("Failed to create wgpu surface")
.init_resource::<ScratchMainWorld>(); });
let request_adapter_options = wgpu::RequestAdapterOptions {
power_preference: settings.power_preference,
compatible_surface: surface.as_ref(),
..Default::default()
};
let (device, queue, adapter_info, render_adapter) =
renderer::initialize_renderer(
&instance,
&settings,
&request_adapter_options,
)
.await;
debug!("Configured wgpu adapter Limits: {:#?}", device.limits());
debug!("Configured wgpu adapter Features: {:#?}", device.features());
let mut future_renderer_resources_inner =
future_renderer_resources_wrapper.lock().unwrap();
*future_renderer_resources_inner =
Some((device, queue, adapter_info, render_adapter, instance));
})
.detach();
app.init_resource::<ScratchMainWorld>();
let mut render_app = App::empty(); let mut render_app = App::empty();
render_app.main_schedule_label = Box::new(Render); render_app.main_schedule_label = Box::new(Render);
@ -242,12 +275,6 @@ impl Plugin for RenderPlugin {
.add_schedule(ExtractSchedule, extract_schedule) .add_schedule(ExtractSchedule, extract_schedule)
.add_schedule(Render, Render::base_schedule()) .add_schedule(Render, Render::base_schedule())
.init_resource::<render_graph::RenderGraph>() .init_resource::<render_graph::RenderGraph>()
.insert_resource(RenderInstance(instance))
.insert_resource(PipelineCache::new(device.clone()))
.insert_resource(device)
.insert_resource(queue)
.insert_resource(render_adapter)
.insert_resource(adapter_info)
.insert_resource(app.world.resource::<AssetServer>().clone()) .insert_resource(app.world.resource::<AssetServer>().clone())
.add_systems(ExtractSchedule, PipelineCache::extract_shaders) .add_systems(ExtractSchedule, PipelineCache::extract_shaders)
.add_systems( .add_systems(
@ -315,6 +342,37 @@ impl Plugin for RenderPlugin {
.register_type::<primitives::CubemapFrusta>() .register_type::<primitives::CubemapFrusta>()
.register_type::<primitives::Frustum>(); .register_type::<primitives::Frustum>();
} }
fn ready(&self, app: &App) -> bool {
app.world
.get_resource::<FutureRendererResources>()
.and_then(|frr| frr.0.try_lock().map(|locked| locked.is_some()).ok())
.unwrap_or(true)
}
fn finish(&self, app: &mut App) {
if let Some(future_renderer_resources) =
app.world.remove_resource::<FutureRendererResources>()
{
let (device, queue, adapter_info, render_adapter, instance) =
future_renderer_resources.0.lock().unwrap().take().unwrap();
app.insert_resource(device.clone())
.insert_resource(queue.clone())
.insert_resource(adapter_info.clone())
.insert_resource(render_adapter.clone());
let render_app = app.sub_app_mut(RenderApp);
render_app
.insert_resource(RenderInstance(instance))
.insert_resource(PipelineCache::new(device.clone()))
.insert_resource(device)
.insert_resource(queue)
.insert_resource(render_adapter)
.insert_resource(adapter_info);
}
}
} }
/// A "scratch" world used to avoid allocating new worlds every frame when /// A "scratch" world used to avoid allocating new worlds every frame when

View file

@ -77,7 +77,7 @@ impl Plugin for PipelinedRenderingPlugin {
} }
// Sets up the render thread and inserts resources into the main app used for controlling the render thread. // Sets up the render thread and inserts resources into the main app used for controlling the render thread.
fn setup(&self, app: &mut App) { fn cleanup(&self, app: &mut App) {
// skip setting up when headless // skip setting up when headless
if app.get_sub_app(RenderExtractApp).is_err() { if app.get_sub_app(RenderExtractApp).is_err() {
return; return;

View file

@ -10,7 +10,6 @@ use crate::{
use bevy_utils::{default, detailed_trace}; use bevy_utils::{default, detailed_trace};
use std::ops::Range; use std::ops::Range;
use wgpu::{IndexFormat, RenderPass}; use wgpu::{IndexFormat, RenderPass};
use wgpu_hal::{MAX_BIND_GROUPS, MAX_VERTEX_BUFFERS};
/// Tracks the state of a [`TrackedRenderPass`]. /// Tracks the state of a [`TrackedRenderPass`].
/// ///
@ -114,8 +113,8 @@ impl<'a> TrackedRenderPass<'a> {
let max_vertex_buffers = limits.max_vertex_buffers as usize; let max_vertex_buffers = limits.max_vertex_buffers as usize;
Self { Self {
state: DrawState { state: DrawState {
bind_groups: vec![(None, Vec::new()); max_bind_groups.min(MAX_BIND_GROUPS)], bind_groups: vec![(None, Vec::new()); max_bind_groups],
vertex_buffers: vec![None; max_vertex_buffers.min(MAX_VERTEX_BUFFERS)], vertex_buffers: vec![None; max_vertex_buffers],
..default() ..default()
}, },
pass, pass,

View file

@ -194,7 +194,7 @@ impl ShaderCache {
Entry::Occupied(entry) => entry.into_mut(), Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
let mut shader_defs = shader_defs.to_vec(); let mut shader_defs = shader_defs.to_vec();
#[cfg(feature = "webgl")] #[cfg(all(feature = "webgl", target_arch = "wasm32"))]
{ {
shader_defs.push("NO_ARRAY_TEXTURES_SUPPORT".into()); shader_defs.push("NO_ARRAY_TEXTURES_SUPPORT".into());
shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into());

View file

@ -45,7 +45,7 @@ pub struct WgpuSettings {
impl Default for WgpuSettings { impl Default for WgpuSettings {
fn default() -> Self { fn default() -> Self {
let default_backends = if cfg!(feature = "webgl") { let default_backends = if cfg!(all(feature = "webgl", target_arch = "wasm32")) {
Backends::GL Backends::GL
} else { } else {
Backends::all() Backends::all()
@ -55,7 +55,8 @@ impl Default for WgpuSettings {
let priority = settings_priority_from_env().unwrap_or(WgpuSettingsPriority::Functionality); let priority = settings_priority_from_env().unwrap_or(WgpuSettingsPriority::Functionality);
let limits = if cfg!(feature = "webgl") || matches!(priority, WgpuSettingsPriority::WebGL2) let limits = if cfg!(all(feature = "webgl", target_arch = "wasm32"))
|| matches!(priority, WgpuSettingsPriority::WebGL2)
{ {
wgpu::Limits::downlevel_webgl2_defaults() wgpu::Limits::downlevel_webgl2_defaults()
} else { } else {

View file

@ -103,6 +103,15 @@ impl Plugin for ImagePlugin {
.resource_mut::<Assets<Image>>() .resource_mut::<Assets<Image>>()
.set_untracked(DEFAULT_IMAGE_HANDLE, Image::default()); .set_untracked(DEFAULT_IMAGE_HANDLE, Image::default());
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<TextureCache>().add_systems(
Render,
update_texture_cache_system.in_set(RenderSet::Cleanup),
);
}
}
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
let default_sampler = { let default_sampler = {
let device = render_app.world.resource::<RenderDevice>(); let device = render_app.world.resource::<RenderDevice>();
@ -110,15 +119,10 @@ impl Plugin for ImagePlugin {
}; };
render_app render_app
.insert_resource(DefaultImageSampler(default_sampler)) .insert_resource(DefaultImageSampler(default_sampler))
.init_resource::<TextureCache>()
.init_resource::<FallbackImage>() .init_resource::<FallbackImage>()
.init_resource::<FallbackImageCubemap>() .init_resource::<FallbackImageCubemap>()
.init_resource::<FallbackImageMsaaCache>() .init_resource::<FallbackImageMsaaCache>()
.init_resource::<FallbackImageDepthCache>() .init_resource::<FallbackImageDepthCache>();
.add_systems(
Render,
update_texture_cache_system.in_set(RenderSet::Cleanup),
);
} }
} }
} }

View file

@ -406,8 +406,11 @@ fn prepare_view_targets(
format: main_texture_format, format: main_texture_format,
usage: TextureUsages::RENDER_ATTACHMENT usage: TextureUsages::RENDER_ATTACHMENT
| TextureUsages::TEXTURE_BINDING, | TextureUsages::TEXTURE_BINDING,
// TODO: Consider changing this if main_texture_format is not sRGB view_formats: match main_texture_format {
view_formats: &[], TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb],
TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb],
_ => &[],
},
}; };
MainTargetTextures { MainTargetTextures {
a: texture_cache a: texture_cache
@ -440,7 +443,7 @@ fn prepare_view_targets(
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
format: main_texture_format, format: main_texture_format,
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[], view_formats: descriptor.view_formats,
}, },
) )
.default_view .default_view
@ -454,7 +457,7 @@ fn prepare_view_targets(
main_texture_format, main_texture_format,
main_texture: main_textures.main_texture.clone(), main_texture: main_textures.main_texture.clone(),
out_texture: out_texture_view.clone(), out_texture: out_texture_view.clone(),
out_texture_format, out_texture_format: out_texture_format.add_srgb_suffix(),
}); });
} }
} }

View file

@ -6,12 +6,12 @@ use crate::{
}; };
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_utils::{tracing::debug, HashMap, HashSet}; use bevy_utils::{default, tracing::debug, HashMap, HashSet};
use bevy_window::{ use bevy_window::{
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed,
}; };
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use wgpu::{BufferUsages, TextureFormat, TextureUsages}; use wgpu::{BufferUsages, TextureFormat, TextureUsages, TextureViewDescriptor};
pub mod screenshot; pub mod screenshot;
@ -40,13 +40,18 @@ impl Plugin for WindowRenderPlugin {
render_app render_app
.init_resource::<ExtractedWindows>() .init_resource::<ExtractedWindows>()
.init_resource::<WindowSurfaces>() .init_resource::<WindowSurfaces>()
.init_resource::<ScreenshotToScreenPipeline>()
.init_non_send_resource::<NonSendMarker>() .init_non_send_resource::<NonSendMarker>()
.add_systems(ExtractSchedule, extract_windows) .add_systems(ExtractSchedule, extract_windows)
.configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::Prepare)) .configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::Prepare))
.add_systems(Render, prepare_windows.in_set(WindowSystem::Prepare)); .add_systems(Render, prepare_windows.in_set(WindowSystem::Prepare));
} }
} }
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<ScreenshotToScreenPipeline>();
}
}
} }
pub struct ExtractedWindow { pub struct ExtractedWindow {
@ -71,8 +76,12 @@ pub struct ExtractedWindow {
impl ExtractedWindow { impl ExtractedWindow {
fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) { fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) {
let texture_view_descriptor = TextureViewDescriptor {
format: Some(frame.texture.format().add_srgb_suffix()),
..default()
};
self.swap_chain_texture_view = Some(TextureView::from( self.swap_chain_texture_view = Some(TextureView::from(
frame.texture.create_view(&Default::default()), frame.texture.create_view(&texture_view_descriptor),
)); ));
self.swap_chain_texture = Some(SurfaceTexture::from(frame)); self.swap_chain_texture = Some(SurfaceTexture::from(frame));
} }
@ -269,8 +278,11 @@ pub fn prepare_windows(
CompositeAlphaMode::PostMultiplied => wgpu::CompositeAlphaMode::PostMultiplied, CompositeAlphaMode::PostMultiplied => wgpu::CompositeAlphaMode::PostMultiplied,
CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit, CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
}, },
// TODO: Use an sRGB view format here on platforms that don't support sRGB surfaces. (afaik only WebGPU) view_formats: if !surface_data.format.is_srgb() {
view_formats: vec![], vec![surface_data.format.add_srgb_suffix()]
} else {
vec![]
},
}; };
// This is an ugly hack to work around drivers that don't support MSAA. // This is an ugly hack to work around drivers that don't support MSAA.

View file

@ -135,7 +135,9 @@ impl Plugin for ScreenshotPlugin {
"screenshot.wgsl", "screenshot.wgsl",
Shader::from_wgsl Shader::from_wgsl
); );
}
fn finish(&self, app: &mut bevy_app::App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>(); render_app.init_resource::<SpecializedRenderPipelines<ScreenshotToScreenPipeline>>();
} }

View file

@ -75,7 +75,6 @@ impl Plugin for SpritePlugin {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.init_resource::<ImageBindGroups>() .init_resource::<ImageBindGroups>()
.init_resource::<SpritePipeline>()
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>() .init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
.init_resource::<SpriteMeta>() .init_resource::<SpriteMeta>()
.init_resource::<ExtractedSprites>() .init_resource::<ExtractedSprites>()
@ -96,6 +95,12 @@ impl Plugin for SpritePlugin {
); );
}; };
} }
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<SpritePipeline>();
}
}
} }
pub fn calculate_bounds_2d( pub fn calculate_bounds_2d(

View file

@ -157,7 +157,6 @@ where
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.add_render_command::<Transparent2d, DrawMaterial2d<M>>() .add_render_command::<Transparent2d, DrawMaterial2d<M>>()
.init_resource::<Material2dPipeline<M>>()
.init_resource::<ExtractedMaterials2d<M>>() .init_resource::<ExtractedMaterials2d<M>>()
.init_resource::<RenderMaterials2d<M>>() .init_resource::<RenderMaterials2d<M>>()
.init_resource::<SpecializedMeshPipelines<Material2dPipeline<M>>>() .init_resource::<SpecializedMeshPipelines<Material2dPipeline<M>>>()
@ -173,6 +172,12 @@ where
); );
} }
} }
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<Material2dPipeline<M>>();
}
}
} }
/// Render pipeline data for a given [`Material2d`] /// Render pipeline data for a given [`Material2d`]

View file

@ -101,7 +101,6 @@ impl Plugin for Mesh2dRenderPlugin {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
.init_resource::<Mesh2dPipeline>()
.init_resource::<SpecializedMeshPipelines<Mesh2dPipeline>>() .init_resource::<SpecializedMeshPipelines<Mesh2dPipeline>>()
.add_systems(ExtractSchedule, extract_mesh2d) .add_systems(ExtractSchedule, extract_mesh2d)
.add_systems( .add_systems(
@ -113,6 +112,12 @@ impl Plugin for Mesh2dRenderPlugin {
); );
} }
} }
fn finish(&self, app: &mut bevy_app::App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<Mesh2dPipeline>();
}
}
} }
#[derive(Component, ShaderType, Clone)] #[derive(Component, ShaderType, Clone)]

View file

@ -21,7 +21,7 @@ pub mod widget;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_render::camera::CameraUpdateSystem; use bevy_render::camera::CameraUpdateSystem;
use bevy_render::extract_component::ExtractComponentPlugin; use bevy_render::{extract_component::ExtractComponentPlugin, RenderApp};
pub use focus::*; pub use focus::*;
pub use geometry::*; pub use geometry::*;
pub use layout::*; pub use layout::*;
@ -169,4 +169,13 @@ impl Plugin for UiPlugin {
crate::render::build_ui_render(app); crate::render::build_ui_render(app);
} }
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::<UiPipeline>();
}
} }

View file

@ -66,7 +66,6 @@ pub fn build_ui_render(app: &mut App) {
}; };
render_app render_app
.init_resource::<UiPipeline>()
.init_resource::<SpecializedRenderPipelines<UiPipeline>>() .init_resource::<SpecializedRenderPipelines<UiPipeline>>()
.init_resource::<UiImageBindGroups>() .init_resource::<UiImageBindGroups>()
.init_resource::<UiMeta>() .init_resource::<UiMeta>()

View file

@ -25,6 +25,7 @@ bevy_input = { path = "../bevy_input", version = "0.11.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.11.0-dev" } bevy_math = { path = "../bevy_math", version = "0.11.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.11.0-dev" } bevy_window = { path = "../bevy_window", version = "0.11.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.11.0-dev" }
# other # other
winit = { version = "0.28", default-features = false } winit = { version = "0.28", default-features = false }

View file

@ -17,6 +17,8 @@ mod winit_windows;
use bevy_a11y::AccessibilityRequested; use bevy_a11y::AccessibilityRequested;
use bevy_ecs::system::{SystemParam, SystemState}; use bevy_ecs::system::{SystemParam, SystemState};
#[cfg(not(target_arch = "wasm32"))]
use bevy_tasks::tick_global_task_pools_on_main_thread;
use system::{changed_window, create_window, despawn_window, CachedWindow}; use system::{changed_window, create_window, despawn_window, CachedWindow};
pub use winit_config::*; pub use winit_config::*;
@ -327,12 +329,25 @@ pub fn winit_runner(mut app: App) {
ResMut<CanvasParentResizeEventChannel>, ResMut<CanvasParentResizeEventChannel>,
)> = SystemState::from_world(&mut app.world); )> = SystemState::from_world(&mut app.world);
let mut finished_and_setup_done = false;
let event_handler = move |event: Event<()>, let event_handler = move |event: Event<()>,
event_loop: &EventLoopWindowTarget<()>, event_loop: &EventLoopWindowTarget<()>,
control_flow: &mut ControlFlow| { control_flow: &mut ControlFlow| {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); let _span = bevy_utils::tracing::info_span!("winit event_handler").entered();
if !finished_and_setup_done {
if !app.ready() {
#[cfg(not(target_arch = "wasm32"))]
tick_global_task_pools_on_main_thread();
} else {
app.finish();
app.cleanup();
finished_and_setup_done = true;
}
}
if let Some(app_exit_events) = app.world.get_resource::<Events<AppExit>>() { if let Some(app_exit_events) = app.world.get_resource::<Events<AppExit>>() {
if app_exit_event_reader.iter(app_exit_events).last().is_some() { if app_exit_event_reader.iter(app_exit_events).last().is_some() {
*control_flow = ControlFlow::Exit; *control_flow = ControlFlow::Exit;
@ -648,7 +663,7 @@ pub fn winit_runner(mut app: App) {
false false
}; };
if update { if update && finished_and_setup_done {
winit_state.last_update = Instant::now(); winit_state.last_update = Instant::now();
app.update(); app.update();
} }

View file

@ -207,7 +207,7 @@ Following is an example for `lighting`. For other examples, change the `lighting
following commands. following commands.
```sh ```sh
cargo build --release --example lighting --target wasm32-unknown-unknown cargo build --release --example lighting --target wasm32-unknown-unknown --features webgl
wasm-bindgen --out-name wasm_example \ wasm-bindgen --out-name wasm_example \
--out-dir examples/wasm/target \ --out-dir examples/wasm/target \
--target web target/wasm32-unknown-unknown/release/examples/lighting.wasm --target web target/wasm32-unknown-unknown/release/examples/lighting.wasm
@ -231,6 +231,19 @@ python3 -m http.server --directory examples/wasm
ruby -run -ehttpd examples/wasm ruby -run -ehttpd examples/wasm
``` ```
#### WebGL2 and WebGPU
Bevy support for WebGPU is being worked on, but is currently experimental.
To build for WebGPU, you'll need to disable default features and add all those you need, making sure to omit the `webgl2` feature.
Bevy has an helper to build its examples:
- Build for WebGL2: `cargo run -p build-wasm-example -- --api webgl2 load_gltf`
- Build for WebGPU: `cargo run -p build-wasm-example -- --api webgpu load_gltf`
This helper will log the command used to build the examples.
### Optimizing ### Optimizing
On the web, it's useful to reduce the size of the files that are distributed. On the web, it's useful to reduce the size of the files that are distributed.

View file

@ -34,6 +34,7 @@ The default feature set enables most of the expected features of a game engine,
|png|PNG image format support| |png|PNG image format support|
|tonemapping_luts|Include tonemapping Look Up Tables KTX2 files| |tonemapping_luts|Include tonemapping Look Up Tables KTX2 files|
|vorbis|OGG/VORBIS audio format support| |vorbis|OGG/VORBIS audio format support|
|webgl2|Enable some limitations to be able to use WebGL2. If not enabled, it will default to WebGPU in Wasm|
|x11|X11 display server support| |x11|X11 display server support|
|zstd|For KTX2 supercompression| |zstd|For KTX2 supercompression|

View file

@ -496,7 +496,7 @@ Following is an example for `lighting`. For other examples, change the `lighting
following commands. following commands.
```sh ```sh
cargo build --release --example lighting --target wasm32-unknown-unknown cargo build --release --example lighting --target wasm32-unknown-unknown --features webgl
wasm-bindgen --out-name wasm_example \ wasm-bindgen --out-name wasm_example \
--out-dir examples/wasm/target \ --out-dir examples/wasm/target \
--target web target/wasm32-unknown-unknown/release/examples/lighting.wasm --target web target/wasm32-unknown-unknown/release/examples/lighting.wasm
@ -520,6 +520,19 @@ python3 -m http.server --directory examples/wasm
ruby -run -ehttpd examples/wasm ruby -run -ehttpd examples/wasm
``` ```
#### WebGL2 and WebGPU
Bevy support for WebGPU is being worked on, but is currently experimental.
To build for WebGPU, you'll need to disable default features and add all those you need, making sure to omit the `webgl2` feature.
Bevy has an helper to build its examples:
- Build for WebGL2: `cargo run -p build-wasm-example -- --api webgl2 load_gltf`
- Build for WebGPU: `cargo run -p build-wasm-example -- --api webgpu load_gltf`
This helper will log the command used to build the examples.
### Optimizing ### Optimizing
On the web, it's useful to reduce the size of the files that are distributed. On the web, it's useful to reduce the size of the files that are distributed.

View file

@ -72,9 +72,7 @@ impl Plugin for GameOfLifeComputePlugin {
// for operation on by the compute shader and display on the sprite. // for operation on by the compute shader and display on the sprite.
app.add_plugin(ExtractResourcePlugin::<GameOfLifeImage>::default()); app.add_plugin(ExtractResourcePlugin::<GameOfLifeImage>::default());
let render_app = app.sub_app_mut(RenderApp); let render_app = app.sub_app_mut(RenderApp);
render_app render_app.add_systems(Render, queue_bind_group.in_set(RenderSet::Queue));
.init_resource::<GameOfLifePipeline>()
.add_systems(Render, queue_bind_group.in_set(RenderSet::Queue));
let mut render_graph = render_app.world.resource_mut::<RenderGraph>(); let mut render_graph = render_app.world.resource_mut::<RenderGraph>();
render_graph.add_node("game_of_life", GameOfLifeNode::default()); render_graph.add_node("game_of_life", GameOfLifeNode::default());
@ -83,6 +81,11 @@ impl Plugin for GameOfLifeComputePlugin {
bevy::render::main_graph::node::CAMERA_DRIVER, bevy::render::main_graph::node::CAMERA_DRIVER,
); );
} }
fn finish(&self, app: &mut App) {
let render_app = app.sub_app_mut(RenderApp);
render_app.init_resource::<GameOfLifePipeline>();
}
} }
#[derive(Resource, Clone, Deref, ExtractResource)] #[derive(Resource, Clone, Deref, ExtractResource)]

View file

@ -83,7 +83,6 @@ impl Plugin for CustomMaterialPlugin {
app.add_plugin(ExtractComponentPlugin::<InstanceMaterialData>::default()); app.add_plugin(ExtractComponentPlugin::<InstanceMaterialData>::default());
app.sub_app_mut(RenderApp) app.sub_app_mut(RenderApp)
.add_render_command::<Transparent3d, DrawCustom>() .add_render_command::<Transparent3d, DrawCustom>()
.init_resource::<CustomPipeline>()
.init_resource::<SpecializedMeshPipelines<CustomPipeline>>() .init_resource::<SpecializedMeshPipelines<CustomPipeline>>()
.add_systems( .add_systems(
Render, Render,
@ -93,6 +92,10 @@ impl Plugin for CustomMaterialPlugin {
), ),
); );
} }
fn finish(&self, app: &mut App) {
app.sub_app_mut(RenderApp).init_resource::<CustomPipeline>();
}
} }
#[derive(Clone, Copy, Pod, Zeroable)] #[derive(Clone, Copy, Pod, Zeroable)]

View file

@ -1,8 +1,14 @@
use std::{fs::File, io::Write}; use std::{fs::File, io::Write};
use clap::Parser; use clap::{Parser, ValueEnum};
use xshell::{cmd, Shell}; use xshell::{cmd, Shell};
#[derive(Debug, Copy, Clone, ValueEnum)]
enum Api {
Webgl2,
Webgpu,
}
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Args { struct Args {
/// Examples to build /// Examples to build
@ -19,31 +25,77 @@ struct Args {
#[arg(short, long)] #[arg(short, long)]
/// Stop after this number of frames /// Stop after this number of frames
frames: Option<usize>, frames: Option<usize>,
#[arg(value_enum, short, long, default_value_t = Api::Webgl2)]
/// Browser API to use for rendering
api: Api,
} }
fn main() { fn main() {
let cli = Args::parse(); let cli = Args::parse();
eprintln!("{cli:?}");
assert!(!cli.examples.is_empty(), "must have at least one example"); assert!(!cli.examples.is_empty(), "must have at least one example");
let mut bevy_ci_testing = vec![]; let mut default_features = true;
let mut features = vec![];
if let Some(frames) = cli.frames { if let Some(frames) = cli.frames {
let mut file = File::create("ci_testing_config.ron").unwrap(); let mut file = File::create("ci_testing_config.ron").unwrap();
file.write_fmt(format_args!("(exit_after: Some({frames}))")) file.write_fmt(format_args!("(exit_after: Some({frames}))"))
.unwrap(); .unwrap();
bevy_ci_testing = vec!["--features", "bevy_ci_testing"]; features.push("bevy_ci_testing");
}
match cli.api {
Api::Webgl2 => (),
Api::Webgpu => {
features.push("animation");
features.push("bevy_asset");
features.push("bevy_audio");
features.push("bevy_gilrs");
features.push("bevy_scene");
features.push("bevy_winit");
features.push("bevy_core_pipeline");
features.push("bevy_pbr");
features.push("bevy_gltf");
features.push("bevy_render");
features.push("bevy_sprite");
features.push("bevy_text");
features.push("bevy_ui");
features.push("png");
features.push("hdr");
features.push("ktx2");
features.push("zstd");
features.push("vorbis");
features.push("x11");
features.push("filesystem_watcher");
features.push("bevy_gizmos");
features.push("android_shared_stdcxx");
features.push("tonemapping_luts");
features.push("default_font");
default_features = false;
}
} }
for example in cli.examples { for example in cli.examples {
let sh = Shell::new().unwrap(); let sh = Shell::new().unwrap();
let bevy_ci_testing = bevy_ci_testing.clone(); let features_string = features.join(",");
cmd!( let mut parameters = vec![];
if !default_features {
parameters.push("--no-default-features");
}
if !features.is_empty() {
parameters.push("--features");
parameters.push(&features_string);
}
let mut cmd = cmd!(
sh, sh,
"cargo build {bevy_ci_testing...} --release --target wasm32-unknown-unknown --example {example}" "cargo build {parameters...} --release --target wasm32-unknown-unknown --example {example}"
) );
.run() if matches!(cli.api, Api::Webgpu) {
.expect("Error building example"); cmd = cmd.env("RUSTFLAGS", "--cfg=web_sys_unstable_apis");
}
cmd.run().expect("Error building example");
cmd!( cmd!(
sh, sh,
"wasm-bindgen --out-dir examples/wasm/target --out-name wasm_example --target web target/wasm32-unknown-unknown/release/examples/{example}.wasm" "wasm-bindgen --out-dir examples/wasm/target --out-name wasm_example --target web target/wasm32-unknown-unknown/release/examples/{example}.wasm"