From c6204279eb6da95564d4e3f6d87dc6187f091615 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Sat, 8 Mar 2025 08:22:28 +1100 Subject: [PATCH] Support for non-browser `wasm` (#17499) # Objective - Contributes to #15460 - Supersedes #8520 - Fixes #4906 ## Solution - Added a new `web` feature to `bevy`, and several of its crates. - Enabled new `web` feature automatically within crates without `no_std` support. ## Testing - `cargo build --no-default-features --target wasm32v1-none` --- ## Migration Guide When using Bevy crates which _don't_ automatically enable the `web` feature, please enable it when building for the browser. ## Notes - I added [`cfg_if`](https://crates.io/crates/cfg-if) to help manage some of the feature gate gore that this extra feature introduces. It's still pretty ugly, but I think much easier to read. - Certain `wasm` targets (e.g., [wasm32-wasip1](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-wasip1.html#wasm32-wasip1)) provide an incomplete implementation for `std`. I have not tested these platforms, but I suspect Bevy's liberal use of usually unsupported features (e.g., threading) will cause these targets to fail. As such, consider `wasm32-unknown-unknown` as the only `wasm` platform with support from Bevy for `std`. All others likely will need to be treated as `no_std` platforms. --- Cargo.toml | 3 + crates/bevy_animation/Cargo.toml | 1 + crates/bevy_app/Cargo.toml | 18 +- crates/bevy_app/src/app.rs | 2 +- crates/bevy_app/src/panic_handler.rs | 15 +- crates/bevy_app/src/schedule_runner.rs | 92 ++++----- crates/bevy_app/src/task_pool_plugin.rs | 60 +++--- crates/bevy_asset/Cargo.toml | 10 + crates/bevy_audio/Cargo.toml | 7 + crates/bevy_internal/Cargo.toml | 9 + crates/bevy_log/Cargo.toml | 4 + crates/bevy_picking/Cargo.toml | 1 + crates/bevy_platform_support/Cargo.toml | 12 +- crates/bevy_platform_support/src/time.rs | 194 ------------------ .../src/time/fallback.rs | 176 ++++++++++++++++ crates/bevy_platform_support/src/time/mod.rs | 15 ++ crates/bevy_reflect/Cargo.toml | 7 +- crates/bevy_reflect/derive/Cargo.toml | 1 + crates/bevy_render/Cargo.toml | 13 ++ crates/bevy_scene/Cargo.toml | 1 + crates/bevy_tasks/Cargo.toml | 40 +++- crates/bevy_tasks/examples/busy_behavior.rs | 3 +- crates/bevy_tasks/examples/idle_behavior.rs | 3 +- crates/bevy_tasks/src/executor.rs | 22 +- crates/bevy_tasks/src/lib.rs | 114 +++++----- .../src/single_threaded_task_pool.rs | 37 ++-- crates/bevy_tasks/src/usages.rs | 2 +- crates/bevy_winit/Cargo.toml | 13 ++ docs/cargo_features.md | 1 + 29 files changed, 491 insertions(+), 385 deletions(-) delete mode 100644 crates/bevy_platform_support/src/time.rs create mode 100644 crates/bevy_platform_support/src/time/fallback.rs create mode 100644 crates/bevy_platform_support/src/time/mod.rs diff --git a/Cargo.toml b/Cargo.toml index dab13437df..d80a0a85e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -513,6 +513,9 @@ critical-section = ["bevy_internal/critical-section"] # Uses the `libm` maths library instead of the one provided in `std` and `core`. libm = ["bevy_internal/libm"] +# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures. +web = ["bevy_internal/web"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 0dd91e85a9..e3c9f9beb5 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -44,6 +44,7 @@ smallvec = "1" tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. uuid = { version = "1.13.1", default-features = false, features = ["js"] } [lints] diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 5451e11baa..204b26958f 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -61,6 +61,17 @@ critical-section = [ "bevy_reflect?/critical-section", ] +## Enables use of browser APIs. +## Note this is currently only applicable on `wasm32` architectures. +web = [ + "bevy_platform_support/web", + "bevy_tasks/web", + "bevy_reflect?/web", + "dep:wasm-bindgen", + "dep:web-sys", + "dep:console_error_panic_hook", +] + [dependencies] # bevy bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } @@ -78,14 +89,15 @@ thiserror = { version = "2", default-features = false } variadics_please = "1.1" tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } +cfg-if = "1.0.0" [target.'cfg(any(unix, windows))'.dependencies] ctrlc = { version = "3.4.4", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = { version = "0.2" } -web-sys = { version = "0.3", features = ["Window"] } -console_error_panic_hook = "0.1.6" +wasm-bindgen = { version = "0.2", optional = true } +web-sys = { version = "0.3", features = ["Window"], optional = true } +console_error_panic_hook = { version = "0.1.6", optional = true } [dev-dependencies] crossbeam-channel = "0.5.0" diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 0625419e25..9c5669423f 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1353,7 +1353,7 @@ type RunnerFn = Box AppExit>; fn run_once(mut app: App) -> AppExit { while app.plugins_state() == PluginsState::Adding { - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(all(target_arch = "wasm32", feature = "web")))] bevy_tasks::tick_global_task_pools_on_main_thread(); } app.finish(); diff --git a/crates/bevy_app/src/panic_handler.rs b/crates/bevy_app/src/panic_handler.rs index e73c3d0098..1021a3dc2e 100644 --- a/crates/bevy_app/src/panic_handler.rs +++ b/crates/bevy_app/src/panic_handler.rs @@ -43,21 +43,16 @@ impl Plugin for PanicHandlerPlugin { { static SET_HOOK: std::sync::Once = std::sync::Once::new(); SET_HOOK.call_once(|| { - #[cfg(target_arch = "wasm32")] - { - // This provides better panic handling in JS engines (displays the panic message and improves the backtrace). - std::panic::set_hook(alloc::boxed::Box::new(console_error_panic_hook::hook)); - } - #[cfg(not(target_arch = "wasm32"))] - { - #[cfg(feature = "error_panic_hook")] - { + cfg_if::cfg_if! { + if #[cfg(all(target_arch = "wasm32", feature = "web"))] { + // This provides better panic handling in JS engines (displays the panic message and improves the backtrace). + std::panic::set_hook(alloc::boxed::Box::new(console_error_panic_hook::hook)); + } else if #[cfg(feature = "error_panic_hook")] { let current_hook = std::panic::take_hook(); std::panic::set_hook(alloc::boxed::Box::new( bevy_ecs::error::bevy_error_panic_hook(current_hook), )); } - // Otherwise use the default target panic hook - Do nothing. } }); diff --git a/crates/bevy_app/src/schedule_runner.rs b/crates/bevy_app/src/schedule_runner.rs index 952ccad072..6d10c92d76 100644 --- a/crates/bevy_app/src/schedule_runner.rs +++ b/crates/bevy_app/src/schedule_runner.rs @@ -6,7 +6,7 @@ use crate::{ use bevy_platform_support::time::Instant; use core::time::Duration; -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "web"))] use { alloc::{boxed::Box, rc::Rc}, core::cell::RefCell, @@ -77,7 +77,7 @@ impl Plugin for ScheduleRunnerPlugin { let plugins_state = app.plugins_state(); if plugins_state != PluginsState::Cleaned { while app.plugins_state() == PluginsState::Adding { - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(all(target_arch = "wasm32", feature = "web")))] bevy_tasks::tick_global_task_pools_on_main_thread(); } app.finish(); @@ -118,58 +118,56 @@ impl Plugin for ScheduleRunnerPlugin { Ok(None) }; - #[cfg(not(target_arch = "wasm32"))] - { - loop { - match tick(&mut app, wait) { - Ok(Some(_delay)) => { - #[cfg(feature = "std")] - std::thread::sleep(_delay); - } - Ok(None) => continue, - Err(exit) => return exit, + cfg_if::cfg_if! { + if #[cfg(all(target_arch = "wasm32", feature = "web"))] { + fn set_timeout(callback: &Closure, dur: Duration) { + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0( + callback.as_ref().unchecked_ref(), + dur.as_millis() as i32, + ) + .expect("Should register `setTimeout`."); } - } - } + let asap = Duration::from_millis(1); - #[cfg(target_arch = "wasm32")] - { - fn set_timeout(callback: &Closure, dur: Duration) { - web_sys::window() - .unwrap() - .set_timeout_with_callback_and_timeout_and_arguments_0( - callback.as_ref().unchecked_ref(), - dur.as_millis() as i32, - ) - .expect("Should register `setTimeout`."); - } - let asap = Duration::from_millis(1); + let exit = Rc::new(RefCell::new(AppExit::Success)); + let closure_exit = exit.clone(); - let exit = Rc::new(RefCell::new(AppExit::Success)); - let closure_exit = exit.clone(); + let mut app = Rc::new(app); + let moved_tick_closure = Rc::new(RefCell::new(None)); + let base_tick_closure = moved_tick_closure.clone(); - let mut app = Rc::new(app); - let moved_tick_closure = Rc::new(RefCell::new(None)); - let base_tick_closure = moved_tick_closure.clone(); + let tick_app = move || { + let app = Rc::get_mut(&mut app).unwrap(); + let delay = tick(app, wait); + match delay { + Ok(delay) => set_timeout( + moved_tick_closure.borrow().as_ref().unwrap(), + delay.unwrap_or(asap), + ), + Err(code) => { + closure_exit.replace(code); + } + } + }; + *base_tick_closure.borrow_mut() = + Some(Closure::wrap(Box::new(tick_app) as Box)); + set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap); - let tick_app = move || { - let app = Rc::get_mut(&mut app).unwrap(); - let delay = tick(app, wait); - match delay { - Ok(delay) => set_timeout( - moved_tick_closure.borrow().as_ref().unwrap(), - delay.unwrap_or(asap), - ), - Err(code) => { - closure_exit.replace(code); + exit.take() + } else { + loop { + match tick(&mut app, wait) { + Ok(Some(_delay)) => { + #[cfg(feature = "std")] + std::thread::sleep(_delay); + } + Ok(None) => continue, + Err(exit) => return exit, } } - }; - *base_tick_closure.borrow_mut() = - Some(Closure::wrap(Box::new(tick_app) as Box)); - set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap); - - exit.take() + } } } } diff --git a/crates/bevy_app/src/task_pool_plugin.rs b/crates/bevy_app/src/task_pool_plugin.rs index 174bca105b..aa1dd23fd5 100644 --- a/crates/bevy_app/src/task_pool_plugin.rs +++ b/crates/bevy_app/src/task_pool_plugin.rs @@ -6,11 +6,19 @@ use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuil use core::{fmt::Debug, marker::PhantomData}; use log::trace; -#[cfg(not(target_arch = "wasm32"))] -use {crate::Last, bevy_ecs::prelude::NonSend}; +cfg_if::cfg_if! { + if #[cfg(not(all(target_arch = "wasm32", feature = "web")))] { + use {crate::Last, bevy_ecs::prelude::NonSend, bevy_tasks::tick_global_task_pools_on_main_thread}; -#[cfg(not(target_arch = "wasm32"))] -use bevy_tasks::tick_global_task_pools_on_main_thread; + /// A system used to check and advanced our task pools. + /// + /// Calls [`tick_global_task_pools_on_main_thread`], + /// and uses [`NonSendMarker`] to ensure that this system runs on the main thread + fn tick_global_task_pools(_main_thread_marker: Option>) { + tick_global_task_pools_on_main_thread(); + } + } +} /// Setup of default task pools: [`AsyncComputeTaskPool`], [`ComputeTaskPool`], [`IoTaskPool`]. #[derive(Default)] @@ -24,22 +32,13 @@ impl Plugin for TaskPoolPlugin { // Setup the default bevy task pools self.task_pool_options.create_default_pools(); - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(all(target_arch = "wasm32", feature = "web")))] _app.add_systems(Last, tick_global_task_pools); } } /// A dummy type that is [`!Send`](Send), to force systems to run on the main thread. pub struct NonSendMarker(PhantomData<*mut ()>); -/// A system used to check and advanced our task pools. -/// -/// Calls [`tick_global_task_pools_on_main_thread`], -/// and uses [`NonSendMarker`] to ensure that this system runs on the main thread -#[cfg(not(target_arch = "wasm32"))] -fn tick_global_task_pools(_main_thread_marker: Option>) { - tick_global_task_pools_on_main_thread(); -} - /// Defines a simple way to determine how many threads to use given the number of remaining cores /// and number of total cores #[derive(Clone)] @@ -176,20 +175,21 @@ impl TaskPoolOptions { remaining_threads = remaining_threads.saturating_sub(io_threads); IoTaskPool::get_or_init(|| { - #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] - let mut builder = TaskPoolBuilder::default() + let builder = TaskPoolBuilder::default() .num_threads(io_threads) .thread_name("IO Task Pool".to_string()); - #[cfg(not(target_arch = "wasm32"))] - { + #[cfg(not(all(target_arch = "wasm32", feature = "web")))] + let builder = { + let mut builder = builder; if let Some(f) = self.io.on_thread_spawn.clone() { builder = builder.on_thread_spawn(move || f()); } if let Some(f) = self.io.on_thread_destroy.clone() { builder = builder.on_thread_destroy(move || f()); } - } + builder + }; builder.build() }); @@ -205,20 +205,21 @@ impl TaskPoolOptions { remaining_threads = remaining_threads.saturating_sub(async_compute_threads); AsyncComputeTaskPool::get_or_init(|| { - #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] - let mut builder = TaskPoolBuilder::default() + let builder = TaskPoolBuilder::default() .num_threads(async_compute_threads) .thread_name("Async Compute Task Pool".to_string()); - #[cfg(not(target_arch = "wasm32"))] - { + #[cfg(not(all(target_arch = "wasm32", feature = "web")))] + let builder = { + let mut builder = builder; if let Some(f) = self.async_compute.on_thread_spawn.clone() { builder = builder.on_thread_spawn(move || f()); } if let Some(f) = self.async_compute.on_thread_destroy.clone() { builder = builder.on_thread_destroy(move || f()); } - } + builder + }; builder.build() }); @@ -234,20 +235,21 @@ impl TaskPoolOptions { trace!("Compute Threads: {}", compute_threads); ComputeTaskPool::get_or_init(|| { - #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] - let mut builder = TaskPoolBuilder::default() + let builder = TaskPoolBuilder::default() .num_threads(compute_threads) .thread_name("Compute Task Pool".to_string()); - #[cfg(not(target_arch = "wasm32"))] - { + #[cfg(not(all(target_arch = "wasm32", feature = "web")))] + let builder = { + let mut builder = builder; if let Some(f) = self.compute.on_thread_spawn.clone() { builder = builder.on_thread_spawn(move || f()); } if let Some(f) = self.compute.on_thread_destroy.clone() { builder = builder.on_thread_destroy(move || f()); } - } + builder + }; builder.build() }); diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index f462cac503..0c1576561e 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -57,6 +57,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = [ "Window", @@ -66,6 +67,15 @@ web-sys = { version = "0.3", features = [ wasm-bindgen-futures = "0.4" js-sys = "0.3" uuid = { version = "1.13.1", default-features = false, features = ["js"] } +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] notify-debouncer-full = { version = "0.5.0", optional = true } diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index f15c5b1876..84060fe26b 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -26,9 +26,16 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } cpal = { version = "0.15", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. rodio = { version = "0.20", default-features = false, features = [ "wasm-bindgen", ] } +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } [features] mp3 = ["rodio/mp3"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index f1b7310283..e6d2375a98 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -333,6 +333,15 @@ libm = [ # This backend is incompatible with `no_std` targets. async_executor = ["std", "bevy_tasks/async_executor", "bevy_ecs/async_executor"] +# Enables use of browser APIs. +# Note this is currently only applicable on `wasm32` architectures. +web = [ + "bevy_app/web", + "bevy_platform_support/web", + "bevy_reflect/web", + "bevy_tasks/web", +] + [dependencies] # bevy (no_std) bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 1eca34f498..cc7c53e676 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -38,6 +38,10 @@ android_log-sys = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] tracing-wasm = "0.2.1" +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } [target.'cfg(target_os = "ios")'.dependencies] tracing-oslog = "0.2" diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index 55d8e02162..95db895ac0 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -36,6 +36,7 @@ uuid = { version = "1.13.1", features = ["v4"] } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. uuid = { version = "1.13.1", default-features = false, features = ["js"] } [lints] diff --git a/crates/bevy_platform_support/Cargo.toml b/crates/bevy_platform_support/Cargo.toml index 7b0300b821..75883a697c 100644 --- a/crates/bevy_platform_support/Cargo.toml +++ b/crates/bevy_platform_support/Cargo.toml @@ -30,13 +30,19 @@ std = [ "foldhash/std", ] +## Allows access to the `alloc` crate. alloc = ["portable-atomic-util/alloc", "dep:hashbrown"] ## `critical-section` provides the building blocks for synchronization primitives ## on all platforms, including `no_std`. critical-section = ["dep:critical-section", "portable-atomic/critical-section"] +## Enables use of browser APIs. +## Note this is currently only applicable on `wasm32` architectures. +web = ["dep:web-time", "dep:getrandom"] + [dependencies] +cfg-if = "1.0.0" critical-section = { version = "1.2.0", default-features = false, optional = true } spin = { version = "0.9.8", default-features = false, features = [ "mutex", @@ -53,8 +59,10 @@ hashbrown = { version = "0.15.1", features = [ ], optional = true, default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -web-time = { version = "1.1", default-features = false } -getrandom = { version = "0.2.0", default-features = false, features = ["js"] } +web-time = { version = "1.1", default-features = false, optional = true } +getrandom = { version = "0.2.0", default-features = false, optional = true, features = [ + "js", +] } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] portable-atomic = { version = "1", default-features = false, features = [ diff --git a/crates/bevy_platform_support/src/time.rs b/crates/bevy_platform_support/src/time.rs deleted file mode 100644 index 79fc81c7fe..0000000000 --- a/crates/bevy_platform_support/src/time.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Provides `Instant` for all platforms. - -pub use time::Instant; - -// TODO: Create a `web` feature to enable WASI compatibility. -// See https://github.com/bevyengine/bevy/issues/4906 -#[cfg(target_arch = "wasm32")] -use web_time as time; - -#[cfg(all(not(target_arch = "wasm32"), feature = "std"))] -use std::time; - -#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))] -use fallback as time; - -#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))] -mod fallback { - //! Provides a fallback implementation of `Instant` from the standard library. - - #![expect( - unsafe_code, - reason = "Instant fallback requires unsafe to allow users to update the internal value" - )] - - use crate::sync::atomic::{AtomicPtr, Ordering}; - - use core::{ - fmt, - ops::{Add, AddAssign, Sub, SubAssign}, - time::Duration, - }; - - static ELAPSED_GETTER: AtomicPtr<()> = AtomicPtr::new(unset_getter as *mut _); - - /// Fallback implementation of `Instant` suitable for a `no_std` environment. - /// - /// If you are on any of the following target architectures, this is a drop-in replacement: - /// - /// - `x86` - /// - `x86_64` - /// - `aarch64` - /// - /// On any other architecture, you must call [`Instant::set_elapsed`], providing a method - /// which when called supplies a monotonically increasing count of elapsed nanoseconds relative - /// to some arbitrary point in time. - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct Instant(Duration); - - impl Instant { - /// Returns an instant corresponding to "now". - #[must_use] - pub fn now() -> Instant { - let getter = ELAPSED_GETTER.load(Ordering::Acquire); - - // SAFETY: Function pointer is always valid - let getter = unsafe { core::mem::transmute::<_, fn() -> Duration>(getter) }; - - Self((getter)()) - } - - /// Provides a function returning the amount of time that has elapsed since execution began. - /// The getter provided to this method will be used by [`now`](Instant::now). - /// - /// # Safety - /// - /// - The function provided must accurately represent the elapsed time. - /// - The function must preserve all invariants of the [`Instant`] type. - /// - The pointer to the function must be valid whenever [`Instant::now`] is called. - pub unsafe fn set_elapsed(getter: fn() -> Duration) { - ELAPSED_GETTER.store(getter as *mut _, Ordering::Release); - } - - /// Returns the amount of time elapsed from another instant to this one, - /// or zero duration if that instant is later than this one. - #[must_use] - pub fn duration_since(&self, earlier: Instant) -> Duration { - self.saturating_duration_since(earlier) - } - - /// Returns the amount of time elapsed from another instant to this one, - /// or None if that instant is later than this one. - /// - /// Due to monotonicity bugs, even under correct logical ordering of the passed `Instant`s, - /// this method can return `None`. - #[must_use] - pub fn checked_duration_since(&self, earlier: Instant) -> Option { - self.0.checked_sub(earlier.0) - } - - /// Returns the amount of time elapsed from another instant to this one, - /// or zero duration if that instant is later than this one. - #[must_use] - pub fn saturating_duration_since(&self, earlier: Instant) -> Duration { - self.0.saturating_sub(earlier.0) - } - - /// Returns the amount of time elapsed since this instant. - #[must_use] - pub fn elapsed(&self) -> Duration { - self.saturating_duration_since(Instant::now()) - } - - /// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as - /// `Instant` (which means it's inside the bounds of the underlying data structure), `None` - /// otherwise. - pub fn checked_add(&self, duration: Duration) -> Option { - self.0.checked_add(duration).map(Instant) - } - - /// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as - /// `Instant` (which means it's inside the bounds of the underlying data structure), `None` - /// otherwise. - pub fn checked_sub(&self, duration: Duration) -> Option { - self.0.checked_sub(duration).map(Instant) - } - } - - impl Add for Instant { - type Output = Instant; - - /// # Panics - /// - /// This function may panic if the resulting point in time cannot be represented by the - /// underlying data structure. See [`Instant::checked_add`] for a version without panic. - fn add(self, other: Duration) -> Instant { - self.checked_add(other) - .expect("overflow when adding duration to instant") - } - } - - impl AddAssign for Instant { - fn add_assign(&mut self, other: Duration) { - *self = *self + other; - } - } - - impl Sub for Instant { - type Output = Instant; - - fn sub(self, other: Duration) -> Instant { - self.checked_sub(other) - .expect("overflow when subtracting duration from instant") - } - } - - impl SubAssign for Instant { - fn sub_assign(&mut self, other: Duration) { - *self = *self - other; - } - } - - impl Sub for Instant { - type Output = Duration; - - /// Returns the amount of time elapsed from another instant to this one, - /// or zero duration if that instant is later than this one. - fn sub(self, other: Instant) -> Duration { - self.duration_since(other) - } - } - - impl fmt::Debug for Instant { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } - } - - fn unset_getter() -> Duration { - let _nanos: u64; - - #[cfg(target_arch = "x86")] - unsafe { - _nanos = core::arch::x86::_rdtsc(); - } - - #[cfg(target_arch = "x86_64")] - unsafe { - _nanos = core::arch::x86_64::_rdtsc(); - } - - #[cfg(target_arch = "aarch64")] - unsafe { - let mut ticks: u64; - core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks); - _nanos = ticks; - } - - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] - panic!("An elapsed time getter has not been provided to `Instant`. Please use `Instant::set_elapsed(...)` before calling `Instant::now()`"); - - #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))] - return Duration::from_nanos(_nanos); - } -} diff --git a/crates/bevy_platform_support/src/time/fallback.rs b/crates/bevy_platform_support/src/time/fallback.rs new file mode 100644 index 0000000000..a0a9354902 --- /dev/null +++ b/crates/bevy_platform_support/src/time/fallback.rs @@ -0,0 +1,176 @@ +//! Provides a fallback implementation of `Instant` from the standard library. + +#![expect( + unsafe_code, + reason = "Instant fallback requires unsafe to allow users to update the internal value" +)] + +use crate::sync::atomic::{AtomicPtr, Ordering}; + +use core::{ + fmt, + ops::{Add, AddAssign, Sub, SubAssign}, + time::Duration, +}; + +static ELAPSED_GETTER: AtomicPtr<()> = AtomicPtr::new(unset_getter as *mut _); + +/// Fallback implementation of `Instant` suitable for a `no_std` environment. +/// +/// If you are on any of the following target architectures, this is a drop-in replacement: +/// +/// - `x86` +/// - `x86_64` +/// - `aarch64` +/// +/// On any other architecture, you must call [`Instant::set_elapsed`], providing a method +/// which when called supplies a monotonically increasing count of elapsed nanoseconds relative +/// to some arbitrary point in time. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Instant(Duration); + +impl Instant { + /// Returns an instant corresponding to "now". + #[must_use] + pub fn now() -> Instant { + let getter = ELAPSED_GETTER.load(Ordering::Acquire); + + // SAFETY: Function pointer is always valid + let getter = unsafe { core::mem::transmute::<_, fn() -> Duration>(getter) }; + + Self((getter)()) + } + + /// Provides a function returning the amount of time that has elapsed since execution began. + /// The getter provided to this method will be used by [`now`](Instant::now). + /// + /// # Safety + /// + /// - The function provided must accurately represent the elapsed time. + /// - The function must preserve all invariants of the [`Instant`] type. + /// - The pointer to the function must be valid whenever [`Instant::now`] is called. + pub unsafe fn set_elapsed(getter: fn() -> Duration) { + ELAPSED_GETTER.store(getter as *mut _, Ordering::Release); + } + + /// Returns the amount of time elapsed from another instant to this one, + /// or zero duration if that instant is later than this one. + #[must_use] + pub fn duration_since(&self, earlier: Instant) -> Duration { + self.saturating_duration_since(earlier) + } + + /// Returns the amount of time elapsed from another instant to this one, + /// or None if that instant is later than this one. + /// + /// Due to monotonicity bugs, even under correct logical ordering of the passed `Instant`s, + /// this method can return `None`. + #[must_use] + pub fn checked_duration_since(&self, earlier: Instant) -> Option { + self.0.checked_sub(earlier.0) + } + + /// Returns the amount of time elapsed from another instant to this one, + /// or zero duration if that instant is later than this one. + #[must_use] + pub fn saturating_duration_since(&self, earlier: Instant) -> Duration { + self.0.saturating_sub(earlier.0) + } + + /// Returns the amount of time elapsed since this instant. + #[must_use] + pub fn elapsed(&self) -> Duration { + self.saturating_duration_since(Instant::now()) + } + + /// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as + /// `Instant` (which means it's inside the bounds of the underlying data structure), `None` + /// otherwise. + pub fn checked_add(&self, duration: Duration) -> Option { + self.0.checked_add(duration).map(Instant) + } + + /// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as + /// `Instant` (which means it's inside the bounds of the underlying data structure), `None` + /// otherwise. + pub fn checked_sub(&self, duration: Duration) -> Option { + self.0.checked_sub(duration).map(Instant) + } +} + +impl Add for Instant { + type Output = Instant; + + /// # Panics + /// + /// This function may panic if the resulting point in time cannot be represented by the + /// underlying data structure. See [`Instant::checked_add`] for a version without panic. + fn add(self, other: Duration) -> Instant { + self.checked_add(other) + .expect("overflow when adding duration to instant") + } +} + +impl AddAssign for Instant { + fn add_assign(&mut self, other: Duration) { + *self = *self + other; + } +} + +impl Sub for Instant { + type Output = Instant; + + fn sub(self, other: Duration) -> Instant { + self.checked_sub(other) + .expect("overflow when subtracting duration from instant") + } +} + +impl SubAssign for Instant { + fn sub_assign(&mut self, other: Duration) { + *self = *self - other; + } +} + +impl Sub for Instant { + type Output = Duration; + + /// Returns the amount of time elapsed from another instant to this one, + /// or zero duration if that instant is later than this one. + fn sub(self, other: Instant) -> Duration { + self.duration_since(other) + } +} + +impl fmt::Debug for Instant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +fn unset_getter() -> Duration { + let _nanos: u64; + + #[cfg(target_arch = "x86")] + unsafe { + _nanos = core::arch::x86::_rdtsc(); + } + + #[cfg(target_arch = "x86_64")] + unsafe { + _nanos = core::arch::x86_64::_rdtsc(); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + let mut ticks: u64; + core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks); + _nanos = ticks; + } + + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] + panic!("An elapsed time getter has not been provided to `Instant`. Please use `Instant::set_elapsed(...)` before calling `Instant::now()`"); + + #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))] + return Duration::from_nanos(_nanos); +} diff --git a/crates/bevy_platform_support/src/time/mod.rs b/crates/bevy_platform_support/src/time/mod.rs new file mode 100644 index 0000000000..260d8e4aea --- /dev/null +++ b/crates/bevy_platform_support/src/time/mod.rs @@ -0,0 +1,15 @@ +//! Provides `Instant` for all platforms. + +pub use time::Instant; + +cfg_if::cfg_if! { + if #[cfg(all(target_arch = "wasm32", feature = "web"))] { + use web_time as time; + } else if #[cfg(feature = "std")] { + use std::time; + } else { + mod fallback; + + use fallback as time; + } +} diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 18de9488f5..9067cc4c0f 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -69,6 +69,10 @@ critical-section = [ "bevy_utils/critical-section", ] +## Enables use of browser APIs. +## Note this is currently only applicable on `wasm32` architectures. +web = ["bevy_platform_support/web", "uuid?/js"] + [dependencies] # bevy bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" } @@ -111,9 +115,6 @@ wgpu-types = { version = "24", features = [ "serde", ], optional = true, default-features = false } -[target.'cfg(target_arch = "wasm32")'.dependencies] -uuid = { version = "1.13.1", default-features = false, features = ["js"] } - [dev-dependencies] ron = "0.8.0" rmp-serde = "1.1" diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index 45899d52ff..ad6ec8cd2f 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -26,6 +26,7 @@ syn = { version = "2.0", features = ["full"] } uuid = { version = "1.13.1", features = ["v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. uuid = { version = "1.13.1", default-features = false, features = ["js"] } [lints] diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 5873b59e70..e91f8cc81f 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -123,6 +123,19 @@ web-sys = { version = "0.3.67", features = [ 'Window', ] } wasm-bindgen = "0.2" +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } [target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] send_wrapper = "0.6.0" diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 8e720de19e..d7fb04aaba 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -38,6 +38,7 @@ thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. uuid = { version = "1.13.1", default-features = false, features = ["js"] } [dev-dependencies] diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index b035e1abf8..a8b256f35e 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -10,16 +10,42 @@ keywords = ["bevy"] [features] default = ["std", "async_executor"] + +# Functionality + +## Enables multi-threading support. +## Without this feature, all tasks will be run on a single thread. +multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] + +## Uses `async-executor` as a task execution backend. +## This backend is incompatible with `no_std` targets. +async_executor = ["std", "dep:async-executor"] + +# Platform Compatibility + +## Allows access to the `std` crate. Enabling this feature will prevent compilation +## on `no_std` targets, but provides access to certain additional features on +## supported platforms. std = [ "futures-lite/std", "async-task/std", "bevy_platform_support/std", "once_cell/std", ] -multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] -async_executor = ["std", "dep:async-executor"] + +## `critical-section` provides the building blocks for synchronization primitives +## on all platforms, including `no_std`. critical-section = ["bevy_platform_support/critical-section"] +## Enables use of browser APIs. +## Note this is currently only applicable on `wasm32` architectures. +web = [ + "bevy_platform_support/web", + "dep:wasm-bindgen-futures", + "dep:pin-project", + "dep:futures-channel", +] + [dependencies] bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false, features = [ "alloc", @@ -33,6 +59,7 @@ derive_more = { version = "1", default-features = false, features = [ "deref", "deref_mut", ] } +cfg-if = "1.0.0" async-executor = { version = "1.11", optional = true } async-channel = { version = "2.3.0", optional = true } async-io = { version = "2.0.0", optional = true } @@ -46,9 +73,9 @@ crossbeam-queue = { version = "0.3", default-features = false, features = [ ] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-futures = "0.4" -pin-project = "1" -futures-channel = "0.3" +wasm-bindgen-futures = { version = "0.4", optional = true } +pin-project = { version = "1", optional = true } +futures-channel = { version = "0.3", optional = true } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] async-task = { version = "4.4.0", default-features = false, features = [ @@ -61,9 +88,6 @@ atomic-waker = { version = "1", default-features = false, features = [ "portable-atomic", ] } -[dev-dependencies] -web-time = { version = "1.1" } - [lints] workspace = true diff --git a/crates/bevy_tasks/examples/busy_behavior.rs b/crates/bevy_tasks/examples/busy_behavior.rs index ee92ec3593..808940ab9f 100644 --- a/crates/bevy_tasks/examples/busy_behavior.rs +++ b/crates/bevy_tasks/examples/busy_behavior.rs @@ -2,8 +2,9 @@ //! for 100ms. It's expected to take about a second to run (assuming the machine has >= 4 logical //! cores) +use bevy_platform_support::time::Instant; use bevy_tasks::TaskPoolBuilder; -use web_time::{Duration, Instant}; +use core::time::Duration; fn main() { let pool = TaskPoolBuilder::new() diff --git a/crates/bevy_tasks/examples/idle_behavior.rs b/crates/bevy_tasks/examples/idle_behavior.rs index 2887163170..3de9388fd2 100644 --- a/crates/bevy_tasks/examples/idle_behavior.rs +++ b/crates/bevy_tasks/examples/idle_behavior.rs @@ -2,8 +2,9 @@ //! spinning. Other than the one thread, the system should remain idle, demonstrating good behavior //! for small workloads. +use bevy_platform_support::time::Instant; use bevy_tasks::TaskPoolBuilder; -use web_time::{Duration, Instant}; +use core::time::Duration; fn main() { let pool = TaskPoolBuilder::new() diff --git a/crates/bevy_tasks/src/executor.rs b/crates/bevy_tasks/src/executor.rs index d6b7234e74..9a9f4f9dfa 100644 --- a/crates/bevy_tasks/src/executor.rs +++ b/crates/bevy_tasks/src/executor.rs @@ -14,21 +14,19 @@ use core::{ }; use derive_more::{Deref, DerefMut}; +cfg_if::cfg_if! { + if #[cfg(feature = "async_executor")] { + type ExecutorInner<'a> = async_executor::Executor<'a>; + type LocalExecutorInner<'a> = async_executor::LocalExecutor<'a>; + } else { + type ExecutorInner<'a> = crate::edge_executor::Executor<'a, 64>; + type LocalExecutorInner<'a> = crate::edge_executor::LocalExecutor<'a, 64>; + } +} + #[cfg(all(feature = "multi_threaded", not(target_arch = "wasm32")))] pub use async_task::FallibleTask; -#[cfg(feature = "async_executor")] -type ExecutorInner<'a> = async_executor::Executor<'a>; - -#[cfg(feature = "async_executor")] -type LocalExecutorInner<'a> = async_executor::LocalExecutor<'a>; - -#[cfg(not(feature = "async_executor"))] -type ExecutorInner<'a> = crate::edge_executor::Executor<'a, 64>; - -#[cfg(not(feature = "async_executor"))] -type LocalExecutorInner<'a> = crate::edge_executor::LocalExecutor<'a, 64>; - /// Wrapper around a multi-threading-aware async executor. /// Spawning will generally require tasks to be `Send` and `Sync` to allow multiple /// threads to send/receive/advance tasks. diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 094d8d5fd2..ae684a4eb5 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -11,19 +11,20 @@ extern crate std; extern crate alloc; -#[cfg(not(target_arch = "wasm32"))] mod conditional_send { - /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm), - /// futures aren't Send. - pub trait ConditionalSend: Send {} - impl ConditionalSend for T {} -} - -#[cfg(target_arch = "wasm32")] -#[expect(missing_docs, reason = "Not all docs are written yet (#3492).")] -mod conditional_send { - pub trait ConditionalSend {} - impl ConditionalSend for T {} + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm), + /// futures aren't Send. + pub trait ConditionalSend {} + impl ConditionalSend for T {} + } else { + /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm), + /// futures aren't Send. + pub trait ConditionalSend: Send {} + impl ConditionalSend for T {} + } + } } pub use conditional_send::*; @@ -48,38 +49,40 @@ mod executor; mod slice; pub use slice::{ParallelSlice, ParallelSliceMut}; -#[cfg_attr(target_arch = "wasm32", path = "wasm_task.rs")] +#[cfg_attr(all(target_arch = "wasm32", feature = "web"), path = "wasm_task.rs")] mod task; pub use task::Task; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] -mod task_pool; +cfg_if::cfg_if! { + if #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { + mod task_pool; + mod thread_executor; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] -pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; + pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; + pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker}; + } else if #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] { + mod single_threaded_task_pool; -#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] -mod single_threaded_task_pool; - -#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] -pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; + pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; + } +} mod usages; -#[cfg(not(target_arch = "wasm32"))] -pub use usages::tick_global_task_pools_on_main_thread; +pub use futures_lite::future::poll_once; pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool}; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] -mod thread_executor; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] -pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker}; +#[cfg(not(all(target_arch = "wasm32", feature = "web")))] +pub use usages::tick_global_task_pools_on_main_thread; -#[cfg(all(feature = "async-io", feature = "std"))] -pub use async_io::block_on; -#[cfg(all(not(feature = "async-io"), feature = "std"))] -pub use futures_lite::future::block_on; -pub use futures_lite::future::poll_once; +#[cfg(feature = "std")] +cfg_if::cfg_if! { + if #[cfg(feature = "async-io")] { + pub use async_io::block_on; + } else { + pub use futures_lite::future::block_on; + } +} mod iter; pub use iter::ParallelIterator; @@ -102,27 +105,28 @@ pub mod prelude { pub use crate::block_on; } -#[cfg(feature = "std")] -use core::num::NonZero; +cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + use core::num::NonZero; -/// Gets the logical CPU core count available to the current process. -/// -/// This is identical to [`std::thread::available_parallelism`], except -/// it will return a default value of 1 if it internally errors out. -/// -/// This will always return at least 1. -#[cfg(feature = "std")] -pub fn available_parallelism() -> usize { - std::thread::available_parallelism() - .map(NonZero::::get) - .unwrap_or(1) -} - -/// Gets the logical CPU core count available to the current process. -/// -/// This will always return at least 1. -#[cfg(not(feature = "std"))] -pub fn available_parallelism() -> usize { - // Without access to std, assume a single thread is available - 1 + /// Gets the logical CPU core count available to the current process. + /// + /// This is identical to [`std::thread::available_parallelism`], except + /// it will return a default value of 1 if it internally errors out. + /// + /// This will always return at least 1. + pub fn available_parallelism() -> usize { + std::thread::available_parallelism() + .map(NonZero::::get) + .unwrap_or(1) + } + } else { + /// Gets the logical CPU core count available to the current process. + /// + /// This will always return at least 1. + pub fn available_parallelism() -> usize { + // Without access to std, assume a single thread is available + 1 + } + } } diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 4b44952ced..7f067b3f06 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -199,26 +199,27 @@ impl TaskPool { where T: 'static + MaybeSend + MaybeSync, { - #[cfg(target_arch = "wasm32")] - return Task::wrap_future(future); + cfg_if::cfg_if! { + if #[cfg(all(target_arch = "wasm32", feature = "web"))] { + return Task::wrap_future(future); + } else if #[cfg(feature = "std")] { + return LOCAL_EXECUTOR.with(|executor| { + let task = executor.spawn(future); + // Loop until all tasks are done + while executor.try_tick() {} - #[cfg(all(not(target_arch = "wasm32"), feature = "std"))] - return LOCAL_EXECUTOR.with(|executor| { - let task = executor.spawn(future); - // Loop until all tasks are done - while executor.try_tick() {} + Task::new(task) + }); + } else { + return { + let task = LOCAL_EXECUTOR.spawn(future); + // Loop until all tasks are done + while LOCAL_EXECUTOR.try_tick() {} - Task::new(task) - }); - - #[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))] - return { - let task = LOCAL_EXECUTOR.spawn(future); - // Loop until all tasks are done - while LOCAL_EXECUTOR.try_tick() {} - - Task::new(task) - }; + Task::new(task) + }; + } + } } /// Spawns a static future on the JS event loop. This is exactly the same as [`TaskPool::spawn`]. diff --git a/crates/bevy_tasks/src/usages.rs b/crates/bevy_tasks/src/usages.rs index 82da333ef4..9563007b03 100644 --- a/crates/bevy_tasks/src/usages.rs +++ b/crates/bevy_tasks/src/usages.rs @@ -81,7 +81,7 @@ taskpool! { /// # Warning /// /// This function *must* be called on the main thread, or the task pools will not be updated appropriately. -#[cfg(not(target_arch = "wasm32"))] +#[cfg(not(all(target_arch = "wasm32", feature = "web")))] pub fn tick_global_task_pools_on_main_thread() { COMPUTE_TASK_POOL .get() diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index fa57f117fb..ff888332ab 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -66,6 +66,19 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } wasm-bindgen = { version = "0.2" } web-sys = "0.3" crossbeam-channel = "0.5" +# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ + "web", +] } [lints] workspace = true diff --git a/docs/cargo_features.md b/docs/cargo_features.md index a58c4f14f9..20a1d98600 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -119,6 +119,7 @@ The default feature set enables most of the expected features of a game engine, |track_location|Enables source location tracking for change detection and spawning/despawning, which can assist with debugging| |wav|WAV audio format support| |wayland|Wayland display server support| +|web|Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.| |webgpu|Enable support for WebGPU in Wasm. When enabled, this feature will override the `webgl2` feature and you won't be able to run Wasm builds with WebGL2, only with WebGPU.| |webp|WebP image format support| |zlib|For KTX2 supercompression|