diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1db57174ae..da15d6511f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 0f6e97f8fe..f7b0eb6f39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ default = [ "android_shared_stdcxx", "tonemapping_luts", "default_font", + "webgl2", ] # 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 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" diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 780b1ada97..613c5063df 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -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. @@ -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.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; } @@ -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) } diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index 8120bc9be5..725ebbe195 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -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 } diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 3cb17d0864..2bc2f63ea9 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -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 }; diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl index 39f472ea84..12eaf84dda 100644 --- a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl +++ b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl @@ -130,7 +130,8 @@ fn downsample_first(@location(0) output_uv: vec2) -> @location(0) vec4 // 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(0.0001), vec3(3.40282347E+38)); + // with f32::MAX (E+38) Chrome fails with ":value 340282346999999984391321947108527833088.0 cannot be represented as 'f32'" + sample = clamp(sample, vec3(0.0001), vec3(3.40282347E+37)); #ifdef USE_THRESHOLD sample = soft_threshold(sample); diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 3e761fbf76..931df7943c 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -61,8 +61,6 @@ impl Plugin for BloomPlugin { }; render_app - .init_resource::() - .init_resource::() .init_resource::>() .init_resource::>() .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::() + .init_resource::(); + } } pub struct BloomNode { diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs index 3d9223d706..cdb57d03e6 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -116,7 +116,6 @@ impl Plugin for CASPlugin { Err(_) => return, }; render_app - .init_resource::() .init_resource::>() .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::(); + } } #[derive(Resource)] diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs index 5145217c30..708193524b 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs @@ -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(); diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs index 1b348a915e..e3937e59a2 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -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(); diff --git a/crates/bevy_core_pipeline/src/fxaa/mod.rs b/crates/bevy_core_pipeline/src/fxaa/mod.rs index 7b3631f185..49a5674771 100644 --- a/crates/bevy_core_pipeline/src/fxaa/mod.rs +++ b/crates/bevy_core_pipeline/src/fxaa/mod.rs @@ -92,7 +92,6 @@ impl Plugin for FxaaPlugin { Err(_) => return, }; render_app - .init_resource::() .init_resource::>() .add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare)) .add_render_graph_node::(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::(); + } } #[derive(Resource, Deref)] diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 6d805e0478..7917c9efb5 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -41,10 +41,7 @@ impl Plugin for SkyboxPlugin { Err(_) => return, }; - let render_device = render_app.world.resource::().clone(); - render_app - .insert_resource(SkyboxPipeline::new(&render_device)) .init_resource::>() .add_systems( 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::().clone(); + + render_app.insert_resource(SkyboxPipeline::new(&render_device)); + } } /// Adds a skybox to a 3D camera, based on a cubemap texture. diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 31ecd12177..1f080aacab 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -92,7 +92,6 @@ impl Plugin for TonemappingPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app - .init_resource::() .init_resource::>() .add_systems( 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::(); + } + } } #[derive(Resource)] diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index d0c8a0951a..a32beda4a3 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -53,23 +53,23 @@ fn tonemap_curve3(v: vec3) -> vec3 { } fn somewhat_boring_display_transform(col: vec3) -> vec3 { - var col = col; - let ycbcr = rgb_to_ycbcr(col); + var boring_color = col; + let ycbcr = rgb_to_ycbcr(boring_color); let bt = tonemap_curve(length(ycbcr.yz) * 2.4); var desat = max((bt - 0.7) * 0.8, 0.0); 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 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 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) -> vec3 { } fn ACESFitted(color: vec3) -> vec3 { - var color = color; + var fitted_color = color; // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT let rgb_to_rrt = mat3x3( @@ -126,17 +126,17 @@ fn ACESFitted(color: vec3) -> vec3 { vec3(-0.00327, -0.07276, 1.07602) ); - color *= rgb_to_rrt; + fitted_color *= rgb_to_rrt; // 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] - color = saturate(color); + fitted_color = saturate(fitted_color); - return color; + return fitted_color; } // ------------------------------- @@ -171,30 +171,30 @@ fn convertOpenDomainToNormalizedLog2(color: vec3, minimum_ev: f32, maximum_ let in_midgray = 0.18; // 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] - color = select(color, 0.00001525878 + color, color < 0.00003051757); - color = clamp( - log2(color / in_midgray), + normalized_color = select(normalized_color, 0.00001525878 + normalized_color, normalized_color < vec3(0.00003051757)); + normalized_color = clamp( + log2(normalized_color / in_midgray), vec3(minimum_ev), vec3(maximum_ev) ); let total_exposure = maximum_ev - minimum_ev; - return (color - minimum_ev) / total_exposure; + return (normalized_color - minimum_ev) / total_exposure; } // Inverse of above fn convertNormalizedLog2ToOpenDomain(color: vec3, minimum_ev: f32, maximum_ev: f32) -> vec3 { - var color = color; + var open_color = color; let in_midgray = 0.18; let total_exposure = maximum_ev - minimum_ev; - color = (color * total_exposure) + minimum_ev; - color = pow(vec3(2.0), color); - color = color * in_midgray; + open_color = (open_color * total_exposure) + minimum_ev; + open_color = pow(vec3(2.0), open_color); + open_color = open_color * in_midgray; - return color; + return open_color; } @@ -204,16 +204,16 @@ fn convertNormalizedLog2ToOpenDomain(color: vec3, minimum_ev: f32, maximum_ // Prepare the data for display encoding. Converted to log domain. fn applyAgXLog(Image: vec3) -> vec3 { - var Image = max(vec3(0.0), Image); // clamp negatives - let r = dot(Image, vec3(0.84247906, 0.0784336, 0.07922375)); - let g = dot(Image, vec3(0.04232824, 0.87846864, 0.07916613)); - let b = dot(Image, vec3(0.04237565, 0.0784336, 0.87914297)); - Image = vec3(r, g, b); + var prepared_image = max(vec3(0.0), Image); // clamp negatives + let r = dot(prepared_image, vec3(0.84247906, 0.0784336, 0.07922375)); + let g = dot(prepared_image, vec3(0.04232824, 0.87846864, 0.07916613)); + let b = dot(prepared_image, vec3(0.04237565, 0.0784336, 0.87914297)); + 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)); - return Image; + prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0)); + return prepared_image; } fn applyLUT3D(Image: vec3, block_size: f32) -> vec3 { diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index fe11eb2d88..482c62f24a 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -97,7 +97,6 @@ impl Plugin for GizmoPlugin { render_app .add_render_command::() - .init_resource::() .init_resource::>() .add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue)); } @@ -109,11 +108,28 @@ impl Plugin for GizmoPlugin { render_app .add_render_command::() - .init_resource::() .init_resource::>() .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::(); + } + + #[cfg(feature = "bevy_pbr")] + { + use pipeline_3d::*; + + render_app.init_resource::(); + } + } } /// A [`Resource`] that stores configuration for gizmos. diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index bf7af957df..140364787b 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -293,9 +293,7 @@ impl Plugin for PbrPlugin { sort_phase_system::.in_set(RenderSet::PhaseSort), ), ) - .init_resource::() - .init_resource::() - .init_resource::(); + .init_resource::(); let shadow_pass_node = ShadowPassNode::new(&mut render_app.world); let mut graph = render_app.world.resource_mut::(); @@ -308,4 +306,16 @@ impl Plugin for PbrPlugin { 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::() + .init_resource::(); + } } diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 4e73b7b24c..05bab22965 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -353,7 +353,7 @@ impl CascadeShadowConfigBuilder { impl Default for CascadeShadowConfigBuilder { fn default() -> Self { - if cfg!(feature = "webgl") { + if cfg!(all(feature = "webgl", target_arch = "wasm32")) { // Currently only support one cascade in webgl. Self { num_cascades: 1, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a0fa33252b..19c5349df3 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -196,7 +196,6 @@ where .add_render_command::>() .add_render_command::>() .add_render_command::>() - .init_resource::>() .init_resource::>() .init_resource::>() .init_resource::>>() @@ -220,6 +219,12 @@ where app.add_plugin(PrepassPlugin::::default()); } } + + fn finish(&self, app: &mut App) { + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::>(); + } + } } /// A key uniquely identifying a specialized [`MaterialPipeline`]. diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index c24eb14335..7fd87d4716 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -108,11 +108,18 @@ where Render, queue_prepass_view_bind_group::.in_set(RenderSet::Queue), ) - .init_resource::>() .init_resource::() .init_resource::>>() .init_resource::(); } + + fn finish(&self, app: &mut bevy_app::App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::>(); + } } /// Sets up the prepasses for a [`Material`]. diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1e73198a87..410ec24471 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -217,9 +217,9 @@ pub struct GpuLights { // 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_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; -#[cfg(feature = "webgl")] +#[cfg(all(feature = "webgl", target_arch = "wasm32"))] pub const MAX_CASCADES_PER_LIGHT: usize = 1; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; @@ -683,13 +683,13 @@ pub fn prepare_lights( let mut point_lights: Vec<_> = point_lights.iter().collect::>(); let mut directional_lights: Vec<_> = directional_lights.iter().collect::>(); - #[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; - #[cfg(not(feature = "webgl"))] + #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))] 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; - #[cfg(feature = "webgl")] + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] let max_texture_cubes = 1; if !*max_directional_lights_warning_emitted && directional_lights.len() > MAX_DIRECTIONAL_LIGHTS @@ -1162,9 +1162,9 @@ pub fn prepare_lights( .create_view(&TextureViewDescriptor { label: Some("point_light_shadow_map_array_texture_view"), format: None, - #[cfg(not(feature = "webgl"))] + #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))] dimension: Some(TextureViewDimension::CubeArray), - #[cfg(feature = "webgl")] + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] dimension: Some(TextureViewDimension::Cube), aspect: TextureAspect::All, base_mip_level: 0, @@ -1177,9 +1177,9 @@ pub fn prepare_lights( .create_view(&TextureViewDescriptor { label: Some("directional_light_shadow_map_array_texture_view"), format: None, - #[cfg(not(feature = "webgl"))] + #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))] dimension: Some(TextureViewDimension::D2Array), - #[cfg(feature = "webgl")] + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] dimension: Some(TextureViewDimension::D2), aspect: TextureAspect::All, base_mip_level: 0, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index fb5fed27bb..3224c1275e 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -106,7 +106,6 @@ impl Plugin for MeshRenderPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app - .init_resource::() .init_resource::() .add_systems(ExtractSchedule, (extract_meshes, extract_skinned_meshes)) .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::(); + } + } } #[derive(Component, ShaderType, Clone)] @@ -328,9 +333,9 @@ impl FromWorld for MeshPipeline { ty: BindingType::Texture { multisampled: false, sample_type: TextureSampleType::Depth, - #[cfg(not(feature = "webgl"))] + #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))] view_dimension: TextureViewDimension::CubeArray, - #[cfg(feature = "webgl")] + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] view_dimension: TextureViewDimension::Cube, }, count: None, @@ -349,9 +354,9 @@ impl FromWorld for MeshPipeline { ty: BindingType::Texture { multisampled: false, sample_type: TextureSampleType::Depth, - #[cfg(not(feature = "webgl"))] + #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))] view_dimension: TextureViewDimension::D2Array, - #[cfg(feature = "webgl")] + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] view_dimension: TextureViewDimension::D2, }, count: None, @@ -439,7 +444,9 @@ impl FromWorld for MeshPipeline { let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]); 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( [16, 17, 18], multisampled, @@ -1060,7 +1067,9 @@ pub fn queue_mesh_view_bind_groups( entries.extend_from_slice(&tonemapping_luts); // 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( prepass_textures, &mut fallback_images, diff --git a/crates/bevy_pbr/src/render/parallax_mapping.wgsl b/crates/bevy_pbr/src/render/parallax_mapping.wgsl index 884b5e23c6..6cdf491f7a 100644 --- a/crates/bevy_pbr/src/render/parallax_mapping.wgsl +++ b/crates/bevy_pbr/src/render/parallax_mapping.wgsl @@ -21,14 +21,14 @@ fn parallaxed_uv( max_layer_count: f32, max_steps: u32, // The original interpolated uv - uv: vec2, + original_uv: vec2, // The vector from the camera to the fragment at the surface in tangent space Vt: vec3, ) -> vec2 { if max_layer_count < 1.0 { - return uv; + return original_uv; } - var uv = uv; + var uv = original_uv; // Steep Parallax Mapping // ====================== diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 7e885f9231..dfdac19b9c 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -46,11 +46,16 @@ impl Plugin for WireframePlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::() - .init_resource::() .init_resource::>() .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::(); + } + } } /// Controls whether an entity should rendered in wireframe-mode if the [`WireframePlugin`] is enabled diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index ef585000b8..eb91d019f3 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -54,7 +54,7 @@ pub struct GlobalsUniform { /// It wraps to zero when it reaches the maximum value of a u32. frame_count: u32, /// WebGL2 structs must be 16 byte aligned. - #[cfg(feature = "webgl")] + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] _wasm_padding: f32, } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index ab4c8f1dce..f04e9eb230 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -44,6 +44,8 @@ pub mod prelude { use bevy_window::{PrimaryWindow, RawHandleWrapper}; use globals::GlobalsPlugin; pub use once_cell; +use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use wgpu::Instance; use crate::{ camera::CameraPlugin, @@ -57,7 +59,10 @@ use bevy_app::{App, AppLabel, Plugin, SubApp}; use bevy_asset::{AddAsset, AssetServer}; use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemState}; 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. #[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. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] pub struct RenderApp; @@ -196,41 +216,54 @@ impl Plugin for RenderPlugin { .init_asset_loader::() .init_debug_asset_loader::(); - let mut system_state: SystemState>> = - SystemState::new(&mut app.world); - let primary_window = system_state.get(&app.world); - if let Some(backends) = self.wgpu_settings.backends { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends, - dx12_shader_compiler: self.wgpu_settings.dx12_shader_compiler.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 future_renderer_resources_wrapper = Arc::new(Mutex::new(None)); + app.insert_resource(FutureRendererResources( + future_renderer_resources_wrapper.clone(), + )); - let request_adapter_options = wgpu::RequestAdapterOptions { - power_preference: self.wgpu_settings.power_preference, - compatible_surface: surface.as_ref(), - ..Default::default() - }; - let (device, queue, adapter_info, render_adapter) = - futures_lite::future::block_on(renderer::initialize_renderer( - &instance, - &self.wgpu_settings, - &request_adapter_options, - )); - debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); - debug!("Configured wgpu adapter Features: {:#?}", device.features()); - app.insert_resource(device.clone()) - .insert_resource(queue.clone()) - .insert_resource(adapter_info.clone()) - .insert_resource(render_adapter.clone()) - .init_resource::(); + let mut system_state: SystemState>> = + SystemState::new(&mut app.world); + let primary_window = system_state.get(&app.world).get_single().ok().cloned(); + + let settings = self.wgpu_settings.clone(); + bevy_tasks::IoTaskPool::get() + .spawn_local(async move { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + dx12_shader_compiler: settings.dx12_shader_compiler.clone(), + }); + let surface = primary_window.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 { + 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::(); let mut render_app = App::empty(); render_app.main_schedule_label = Box::new(Render); @@ -242,12 +275,6 @@ impl Plugin for RenderPlugin { .add_schedule(ExtractSchedule, extract_schedule) .add_schedule(Render, Render::base_schedule()) .init_resource::() - .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::().clone()) .add_systems(ExtractSchedule, PipelineCache::extract_shaders) .add_systems( @@ -315,6 +342,37 @@ impl Plugin for RenderPlugin { .register_type::() .register_type::(); } + + fn ready(&self, app: &App) -> bool { + app.world + .get_resource::() + .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::() + { + 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 diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index d297ae2adb..c96d936b85 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -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. - fn setup(&self, app: &mut App) { + fn cleanup(&self, app: &mut App) { // skip setting up when headless if app.get_sub_app(RenderExtractApp).is_err() { return; diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index c9fcc1e77b..9be2d7ed2b 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -10,7 +10,6 @@ use crate::{ use bevy_utils::{default, detailed_trace}; use std::ops::Range; use wgpu::{IndexFormat, RenderPass}; -use wgpu_hal::{MAX_BIND_GROUPS, MAX_VERTEX_BUFFERS}; /// Tracks the state of a [`TrackedRenderPass`]. /// @@ -114,8 +113,8 @@ impl<'a> TrackedRenderPass<'a> { let max_vertex_buffers = limits.max_vertex_buffers as usize; Self { state: DrawState { - bind_groups: vec![(None, Vec::new()); max_bind_groups.min(MAX_BIND_GROUPS)], - vertex_buffers: vec![None; max_vertex_buffers.min(MAX_VERTEX_BUFFERS)], + bind_groups: vec![(None, Vec::new()); max_bind_groups], + vertex_buffers: vec![None; max_vertex_buffers], ..default() }, pass, diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index f958ee443b..4cdc740c92 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -194,7 +194,7 @@ impl ShaderCache { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { 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("SIXTEEN_BYTE_ALIGNMENT".into()); diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index e9f859b0af..17aa63d447 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -45,7 +45,7 @@ pub struct WgpuSettings { impl Default for WgpuSettings { fn default() -> Self { - let default_backends = if cfg!(feature = "webgl") { + let default_backends = if cfg!(all(feature = "webgl", target_arch = "wasm32")) { Backends::GL } else { Backends::all() @@ -55,7 +55,8 @@ impl Default for WgpuSettings { 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() } else { diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 1f4f45a83b..131ca51988 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -103,6 +103,15 @@ impl Plugin for ImagePlugin { .resource_mut::>() .set_untracked(DEFAULT_IMAGE_HANDLE, Image::default()); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::().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) { let default_sampler = { let device = render_app.world.resource::(); @@ -110,15 +119,10 @@ impl Plugin for ImagePlugin { }; render_app .insert_resource(DefaultImageSampler(default_sampler)) - .init_resource::() .init_resource::() .init_resource::() .init_resource::() - .init_resource::() - .add_systems( - Render, - update_texture_cache_system.in_set(RenderSet::Cleanup), - ); + .init_resource::(); } } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index c602dd7628..0ab2dd1ed7 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -406,8 +406,11 @@ fn prepare_view_targets( format: main_texture_format, usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - // TODO: Consider changing this if main_texture_format is not sRGB - view_formats: &[], + view_formats: match main_texture_format { + TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], + TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], + _ => &[], + }, }; MainTargetTextures { a: texture_cache @@ -440,7 +443,7 @@ fn prepare_view_targets( dimension: TextureDimension::D2, format: main_texture_format, usage: TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], + view_formats: descriptor.view_formats, }, ) .default_view @@ -454,7 +457,7 @@ fn prepare_view_targets( main_texture_format, main_texture: main_textures.main_texture.clone(), out_texture: out_texture_view.clone(), - out_texture_format, + out_texture_format: out_texture_format.add_srgb_suffix(), }); } } diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index 0ca26adcf9..dda0e2512e 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -6,12 +6,12 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; -use bevy_utils::{tracing::debug, HashMap, HashSet}; +use bevy_utils::{default, tracing::debug, HashMap, HashSet}; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, }; use std::ops::{Deref, DerefMut}; -use wgpu::{BufferUsages, TextureFormat, TextureUsages}; +use wgpu::{BufferUsages, TextureFormat, TextureUsages, TextureViewDescriptor}; pub mod screenshot; @@ -40,13 +40,18 @@ impl Plugin for WindowRenderPlugin { render_app .init_resource::() .init_resource::() - .init_resource::() .init_non_send_resource::() .add_systems(ExtractSchedule, extract_windows) .configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::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::(); + } + } } pub struct ExtractedWindow { @@ -71,8 +76,12 @@ pub struct ExtractedWindow { impl ExtractedWindow { 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( - frame.texture.create_view(&Default::default()), + frame.texture.create_view(&texture_view_descriptor), )); self.swap_chain_texture = Some(SurfaceTexture::from(frame)); } @@ -269,8 +278,11 @@ pub fn prepare_windows( CompositeAlphaMode::PostMultiplied => wgpu::CompositeAlphaMode::PostMultiplied, 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: vec![], + view_formats: if !surface_data.format.is_srgb() { + vec![surface_data.format.add_srgb_suffix()] + } else { + vec![] + }, }; // This is an ugly hack to work around drivers that don't support MSAA. diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index b573c5269f..ab7d9e8818 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -135,7 +135,9 @@ impl Plugin for ScreenshotPlugin { "screenshot.wgsl", Shader::from_wgsl ); + } + fn finish(&self, app: &mut bevy_app::App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 0a08c95d33..403e00722e 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -75,7 +75,6 @@ impl Plugin for SpritePlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .init_resource::() .init_resource::>() .init_resource::() .init_resource::() @@ -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::(); + } + } } pub fn calculate_bounds_2d( diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index ed03db9a48..19c656b5f6 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -157,7 +157,6 @@ where if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() - .init_resource::>() .init_resource::>() .init_resource::>() .init_resource::>>() @@ -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::>(); + } + } } /// Render pipeline data for a given [`Material2d`] diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index be626ea08f..93c634f18a 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -101,7 +101,6 @@ impl Plugin for Mesh2dRenderPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app - .init_resource::() .init_resource::>() .add_systems(ExtractSchedule, extract_mesh2d) .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::(); + } + } } #[derive(Component, ShaderType, Clone)] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index a2bf0656b1..b7ec10fb29 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -21,7 +21,7 @@ pub mod widget; #[cfg(feature = "bevy_text")] use bevy_render::camera::CameraUpdateSystem; -use bevy_render::extract_component::ExtractComponentPlugin; +use bevy_render::{extract_component::ExtractComponentPlugin, RenderApp}; pub use focus::*; pub use geometry::*; pub use layout::*; @@ -169,4 +169,13 @@ impl Plugin for UiPlugin { 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::(); + } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index eb821a32f9..f203c4b227 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -66,7 +66,6 @@ pub fn build_ui_render(app: &mut App) { }; render_app - .init_resource::() .init_resource::>() .init_resource::() .init_resource::() diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 2cd383c8ef..c1919a8d62 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -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_window = { path = "../bevy_window", 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 winit = { version = "0.28", default-features = false } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 522737e227..17be50b290 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -17,6 +17,8 @@ mod winit_windows; use bevy_a11y::AccessibilityRequested; 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}; pub use winit_config::*; @@ -327,12 +329,25 @@ pub fn winit_runner(mut app: App) { ResMut, )> = SystemState::from_world(&mut app.world); + let mut finished_and_setup_done = false; + let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { #[cfg(feature = "trace")] 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::>() { if app_exit_event_reader.iter(app_exit_events).last().is_some() { *control_flow = ControlFlow::Exit; @@ -648,7 +663,7 @@ pub fn winit_runner(mut app: App) { false }; - if update { + if update && finished_and_setup_done { winit_state.last_update = Instant::now(); app.update(); } diff --git a/docs-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index 5b25037209..1a8d141372 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -207,7 +207,7 @@ Following is an example for `lighting`. For other examples, change the `lighting following commands. ```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 \ --out-dir examples/wasm/target \ --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 ``` +#### 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 On the web, it's useful to reduce the size of the files that are distributed. diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 28d4b205d9..6ae8ca9a07 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -34,6 +34,7 @@ The default feature set enables most of the expected features of a game engine, |png|PNG image format support| |tonemapping_luts|Include tonemapping Look Up Tables KTX2 files| |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| |zstd|For KTX2 supercompression| diff --git a/examples/README.md b/examples/README.md index fce3e507cf..b1cac442fa 100644 --- a/examples/README.md +++ b/examples/README.md @@ -496,7 +496,7 @@ Following is an example for `lighting`. For other examples, change the `lighting following commands. ```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 \ --out-dir examples/wasm/target \ --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 ``` +#### 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 On the web, it's useful to reduce the size of the files that are distributed. diff --git a/examples/shader/compute_shader_game_of_life.rs b/examples/shader/compute_shader_game_of_life.rs index 29a41b3e83..47d5f75933 100644 --- a/examples/shader/compute_shader_game_of_life.rs +++ b/examples/shader/compute_shader_game_of_life.rs @@ -72,9 +72,7 @@ impl Plugin for GameOfLifeComputePlugin { // for operation on by the compute shader and display on the sprite. app.add_plugin(ExtractResourcePlugin::::default()); let render_app = app.sub_app_mut(RenderApp); - render_app - .init_resource::() - .add_systems(Render, queue_bind_group.in_set(RenderSet::Queue)); + render_app.add_systems(Render, queue_bind_group.in_set(RenderSet::Queue)); let mut render_graph = render_app.world.resource_mut::(); render_graph.add_node("game_of_life", GameOfLifeNode::default()); @@ -83,6 +81,11 @@ impl Plugin for GameOfLifeComputePlugin { 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::(); + } } #[derive(Resource, Clone, Deref, ExtractResource)] diff --git a/examples/shader/shader_instancing.rs b/examples/shader/shader_instancing.rs index b9b53ea1ea..a470ae62f0 100644 --- a/examples/shader/shader_instancing.rs +++ b/examples/shader/shader_instancing.rs @@ -83,7 +83,6 @@ impl Plugin for CustomMaterialPlugin { app.add_plugin(ExtractComponentPlugin::::default()); app.sub_app_mut(RenderApp) .add_render_command::() - .init_resource::() .init_resource::>() .add_systems( Render, @@ -93,6 +92,10 @@ impl Plugin for CustomMaterialPlugin { ), ); } + + fn finish(&self, app: &mut App) { + app.sub_app_mut(RenderApp).init_resource::(); + } } #[derive(Clone, Copy, Pod, Zeroable)] diff --git a/tools/build-wasm-example/src/main.rs b/tools/build-wasm-example/src/main.rs index 63d3eadd60..8afa10e25b 100644 --- a/tools/build-wasm-example/src/main.rs +++ b/tools/build-wasm-example/src/main.rs @@ -1,8 +1,14 @@ use std::{fs::File, io::Write}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use xshell::{cmd, Shell}; +#[derive(Debug, Copy, Clone, ValueEnum)] +enum Api { + Webgl2, + Webgpu, +} + #[derive(Parser, Debug)] struct Args { /// Examples to build @@ -19,31 +25,77 @@ struct Args { #[arg(short, long)] /// Stop after this number of frames frames: Option, + + #[arg(value_enum, short, long, default_value_t = Api::Webgl2)] + /// Browser API to use for rendering + api: Api, } fn main() { let cli = Args::parse(); - eprintln!("{cli:?}"); 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 { let mut file = File::create("ci_testing_config.ron").unwrap(); file.write_fmt(format_args!("(exit_after: Some({frames}))")) .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 { let sh = Shell::new().unwrap(); - let bevy_ci_testing = bevy_ci_testing.clone(); - cmd!( + let features_string = features.join(","); + 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, - "cargo build {bevy_ci_testing...} --release --target wasm32-unknown-unknown --example {example}" - ) - .run() - .expect("Error building example"); + "cargo build {parameters...} --release --target wasm32-unknown-unknown --example {example}" + ); + if matches!(cli.api, Api::Webgpu) { + cmd = cmd.env("RUSTFLAGS", "--cfg=web_sys_unstable_apis"); + } + cmd.run().expect("Error building example"); + cmd!( sh, "wasm-bindgen --out-dir examples/wasm/target --out-name wasm_example --target web target/wasm32-unknown-unknown/release/examples/{example}.wasm"