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.
This commit is contained in:
Zachary Harrold 2025-03-08 08:22:28 +11:00 committed by GitHub
parent edba54adac
commit c6204279eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 491 additions and 385 deletions

View File

@ -513,6 +513,9 @@ critical-section = ["bevy_internal/critical-section"]
# Uses the `libm` maths library instead of the one provided in `std` and `core`. # Uses the `libm` maths library instead of the one provided in `std` and `core`.
libm = ["bevy_internal/libm"] libm = ["bevy_internal/libm"]
# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.
web = ["bevy_internal/web"]
[dependencies] [dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true } tracing = { version = "0.1", default-features = false, optional = true }

View File

@ -44,6 +44,7 @@ smallvec = "1"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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"] } uuid = { version = "1.13.1", default-features = false, features = ["js"] }
[lints] [lints]

View File

@ -61,6 +61,17 @@ critical-section = [
"bevy_reflect?/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] [dependencies]
# bevy # bevy
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
@ -78,14 +89,15 @@ thiserror = { version = "2", default-features = false }
variadics_please = "1.1" variadics_please = "1.1"
tracing = { version = "0.1", default-features = false, optional = true } tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false } log = { version = "0.4", default-features = false }
cfg-if = "1.0.0"
[target.'cfg(any(unix, windows))'.dependencies] [target.'cfg(any(unix, windows))'.dependencies]
ctrlc = { version = "3.4.4", optional = true } ctrlc = { version = "3.4.4", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2", optional = true }
web-sys = { version = "0.3", features = ["Window"] } web-sys = { version = "0.3", features = ["Window"], optional = true }
console_error_panic_hook = "0.1.6" console_error_panic_hook = { version = "0.1.6", optional = true }
[dev-dependencies] [dev-dependencies]
crossbeam-channel = "0.5.0" crossbeam-channel = "0.5.0"

View File

@ -1353,7 +1353,7 @@ type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
fn run_once(mut app: App) -> AppExit { fn run_once(mut app: App) -> AppExit {
while app.plugins_state() == PluginsState::Adding { 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(); bevy_tasks::tick_global_task_pools_on_main_thread();
} }
app.finish(); app.finish();

View File

@ -43,21 +43,16 @@ impl Plugin for PanicHandlerPlugin {
{ {
static SET_HOOK: std::sync::Once = std::sync::Once::new(); static SET_HOOK: std::sync::Once = std::sync::Once::new();
SET_HOOK.call_once(|| { SET_HOOK.call_once(|| {
#[cfg(target_arch = "wasm32")] 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). // 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)); std::panic::set_hook(alloc::boxed::Box::new(console_error_panic_hook::hook));
} } else if #[cfg(feature = "error_panic_hook")] {
#[cfg(not(target_arch = "wasm32"))]
{
#[cfg(feature = "error_panic_hook")]
{
let current_hook = std::panic::take_hook(); let current_hook = std::panic::take_hook();
std::panic::set_hook(alloc::boxed::Box::new( std::panic::set_hook(alloc::boxed::Box::new(
bevy_ecs::error::bevy_error_panic_hook(current_hook), bevy_ecs::error::bevy_error_panic_hook(current_hook),
)); ));
} }
// Otherwise use the default target panic hook - Do nothing. // Otherwise use the default target panic hook - Do nothing.
} }
}); });

View File

@ -6,7 +6,7 @@ use crate::{
use bevy_platform_support::time::Instant; use bevy_platform_support::time::Instant;
use core::time::Duration; use core::time::Duration;
#[cfg(target_arch = "wasm32")] #[cfg(all(target_arch = "wasm32", feature = "web"))]
use { use {
alloc::{boxed::Box, rc::Rc}, alloc::{boxed::Box, rc::Rc},
core::cell::RefCell, core::cell::RefCell,
@ -77,7 +77,7 @@ impl Plugin for ScheduleRunnerPlugin {
let plugins_state = app.plugins_state(); let plugins_state = app.plugins_state();
if plugins_state != PluginsState::Cleaned { if plugins_state != PluginsState::Cleaned {
while app.plugins_state() == PluginsState::Adding { 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(); bevy_tasks::tick_global_task_pools_on_main_thread();
} }
app.finish(); app.finish();
@ -118,58 +118,56 @@ impl Plugin for ScheduleRunnerPlugin {
Ok(None) Ok(None)
}; };
#[cfg(not(target_arch = "wasm32"))] cfg_if::cfg_if! {
{ if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
loop { fn set_timeout(callback: &Closure<dyn FnMut()>, dur: Duration) {
match tick(&mut app, wait) { web_sys::window()
Ok(Some(_delay)) => { .unwrap()
#[cfg(feature = "std")] .set_timeout_with_callback_and_timeout_and_arguments_0(
std::thread::sleep(_delay); callback.as_ref().unchecked_ref(),
} dur.as_millis() as i32,
Ok(None) => continue, )
Err(exit) => return exit, .expect("Should register `setTimeout`.");
} }
} let asap = Duration::from_millis(1);
}
#[cfg(target_arch = "wasm32")] let exit = Rc::new(RefCell::new(AppExit::Success));
{ let closure_exit = exit.clone();
fn set_timeout(callback: &Closure<dyn FnMut()>, 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 mut app = Rc::new(app);
let closure_exit = exit.clone(); let moved_tick_closure = Rc::new(RefCell::new(None));
let base_tick_closure = moved_tick_closure.clone();
let mut app = Rc::new(app); let tick_app = move || {
let moved_tick_closure = Rc::new(RefCell::new(None)); let app = Rc::get_mut(&mut app).unwrap();
let base_tick_closure = moved_tick_closure.clone(); 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<dyn FnMut()>));
set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap);
let tick_app = move || { exit.take()
let app = Rc::get_mut(&mut app).unwrap(); } else {
let delay = tick(app, wait); loop {
match delay { match tick(&mut app, wait) {
Ok(delay) => set_timeout( Ok(Some(_delay)) => {
moved_tick_closure.borrow().as_ref().unwrap(), #[cfg(feature = "std")]
delay.unwrap_or(asap), std::thread::sleep(_delay);
), }
Err(code) => { Ok(None) => continue,
closure_exit.replace(code); Err(exit) => return exit,
} }
} }
}; }
*base_tick_closure.borrow_mut() =
Some(Closure::wrap(Box::new(tick_app) as Box<dyn FnMut()>));
set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap);
exit.take()
} }
} }
} }

View File

@ -6,11 +6,19 @@ use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuil
use core::{fmt::Debug, marker::PhantomData}; use core::{fmt::Debug, marker::PhantomData};
use log::trace; use log::trace;
#[cfg(not(target_arch = "wasm32"))] cfg_if::cfg_if! {
use {crate::Last, bevy_ecs::prelude::NonSend}; 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"))] /// A system used to check and advanced our task pools.
use bevy_tasks::tick_global_task_pools_on_main_thread; ///
/// 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<NonSend<NonSendMarker>>) {
tick_global_task_pools_on_main_thread();
}
}
}
/// Setup of default task pools: [`AsyncComputeTaskPool`], [`ComputeTaskPool`], [`IoTaskPool`]. /// Setup of default task pools: [`AsyncComputeTaskPool`], [`ComputeTaskPool`], [`IoTaskPool`].
#[derive(Default)] #[derive(Default)]
@ -24,22 +32,13 @@ impl Plugin for TaskPoolPlugin {
// Setup the default bevy task pools // Setup the default bevy task pools
self.task_pool_options.create_default_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); _app.add_systems(Last, tick_global_task_pools);
} }
} }
/// A dummy type that is [`!Send`](Send), to force systems to run on the main thread. /// A dummy type that is [`!Send`](Send), to force systems to run on the main thread.
pub struct NonSendMarker(PhantomData<*mut ()>); 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<NonSend<NonSendMarker>>) {
tick_global_task_pools_on_main_thread();
}
/// Defines a simple way to determine how many threads to use given the number of remaining cores /// Defines a simple way to determine how many threads to use given the number of remaining cores
/// and number of total cores /// and number of total cores
#[derive(Clone)] #[derive(Clone)]
@ -176,20 +175,21 @@ impl TaskPoolOptions {
remaining_threads = remaining_threads.saturating_sub(io_threads); remaining_threads = remaining_threads.saturating_sub(io_threads);
IoTaskPool::get_or_init(|| { IoTaskPool::get_or_init(|| {
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))] let builder = TaskPoolBuilder::default()
let mut builder = TaskPoolBuilder::default()
.num_threads(io_threads) .num_threads(io_threads)
.thread_name("IO Task Pool".to_string()); .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() { if let Some(f) = self.io.on_thread_spawn.clone() {
builder = builder.on_thread_spawn(move || f()); builder = builder.on_thread_spawn(move || f());
} }
if let Some(f) = self.io.on_thread_destroy.clone() { if let Some(f) = self.io.on_thread_destroy.clone() {
builder = builder.on_thread_destroy(move || f()); builder = builder.on_thread_destroy(move || f());
} }
} builder
};
builder.build() builder.build()
}); });
@ -205,20 +205,21 @@ impl TaskPoolOptions {
remaining_threads = remaining_threads.saturating_sub(async_compute_threads); remaining_threads = remaining_threads.saturating_sub(async_compute_threads);
AsyncComputeTaskPool::get_or_init(|| { AsyncComputeTaskPool::get_or_init(|| {
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))] let builder = TaskPoolBuilder::default()
let mut builder = TaskPoolBuilder::default()
.num_threads(async_compute_threads) .num_threads(async_compute_threads)
.thread_name("Async Compute Task Pool".to_string()); .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() { if let Some(f) = self.async_compute.on_thread_spawn.clone() {
builder = builder.on_thread_spawn(move || f()); builder = builder.on_thread_spawn(move || f());
} }
if let Some(f) = self.async_compute.on_thread_destroy.clone() { if let Some(f) = self.async_compute.on_thread_destroy.clone() {
builder = builder.on_thread_destroy(move || f()); builder = builder.on_thread_destroy(move || f());
} }
} builder
};
builder.build() builder.build()
}); });
@ -234,20 +235,21 @@ impl TaskPoolOptions {
trace!("Compute Threads: {}", compute_threads); trace!("Compute Threads: {}", compute_threads);
ComputeTaskPool::get_or_init(|| { ComputeTaskPool::get_or_init(|| {
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))] let builder = TaskPoolBuilder::default()
let mut builder = TaskPoolBuilder::default()
.num_threads(compute_threads) .num_threads(compute_threads)
.thread_name("Compute Task Pool".to_string()); .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() { if let Some(f) = self.compute.on_thread_spawn.clone() {
builder = builder.on_thread_spawn(move || f()); builder = builder.on_thread_spawn(move || f());
} }
if let Some(f) = self.compute.on_thread_destroy.clone() { if let Some(f) = self.compute.on_thread_destroy.clone() {
builder = builder.on_thread_destroy(move || f()); builder = builder.on_thread_destroy(move || f());
} }
} builder
};
builder.build() builder.build()
}); });

View File

@ -57,6 +57,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } bevy_window = { path = "../bevy_window", version = "0.16.0-dev" }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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" } wasm-bindgen = { version = "0.2" }
web-sys = { version = "0.3", features = [ web-sys = { version = "0.3", features = [
"Window", "Window",
@ -66,6 +67,15 @@ web-sys = { version = "0.3", features = [
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
js-sys = "0.3" js-sys = "0.3"
uuid = { version = "1.13.1", default-features = false, features = ["js"] } 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] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
notify-debouncer-full = { version = "0.5.0", optional = true } notify-debouncer-full = { version = "0.5.0", optional = true }

View File

@ -26,9 +26,16 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
cpal = { version = "0.15", optional = true } cpal = { version = "0.15", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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 = [ rodio = { version = "0.20", default-features = false, features = [
"wasm-bindgen", "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] [features]
mp3 = ["rodio/mp3"] mp3 = ["rodio/mp3"]

View File

@ -333,6 +333,15 @@ libm = [
# This backend is incompatible with `no_std` targets. # This backend is incompatible with `no_std` targets.
async_executor = ["std", "bevy_tasks/async_executor", "bevy_ecs/async_executor"] 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] [dependencies]
# bevy (no_std) # bevy (no_std)
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [

View File

@ -38,6 +38,10 @@ android_log-sys = "0.3.0"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
tracing-wasm = "0.2.1" 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] [target.'cfg(target_os = "ios")'.dependencies]
tracing-oslog = "0.2" tracing-oslog = "0.2"

View File

@ -36,6 +36,7 @@ uuid = { version = "1.13.1", features = ["v4"] }
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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"] } uuid = { version = "1.13.1", default-features = false, features = ["js"] }
[lints] [lints]

View File

@ -30,13 +30,19 @@ std = [
"foldhash/std", "foldhash/std",
] ]
## Allows access to the `alloc` crate.
alloc = ["portable-atomic-util/alloc", "dep:hashbrown"] alloc = ["portable-atomic-util/alloc", "dep:hashbrown"]
## `critical-section` provides the building blocks for synchronization primitives ## `critical-section` provides the building blocks for synchronization primitives
## on all platforms, including `no_std`. ## on all platforms, including `no_std`.
critical-section = ["dep:critical-section", "portable-atomic/critical-section"] 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] [dependencies]
cfg-if = "1.0.0"
critical-section = { version = "1.2.0", default-features = false, optional = true } critical-section = { version = "1.2.0", default-features = false, optional = true }
spin = { version = "0.9.8", default-features = false, features = [ spin = { version = "0.9.8", default-features = false, features = [
"mutex", "mutex",
@ -53,8 +59,10 @@ hashbrown = { version = "0.15.1", features = [
], optional = true, default-features = false } ], optional = true, default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
web-time = { version = "1.1", default-features = false } web-time = { version = "1.1", default-features = false, optional = true }
getrandom = { version = "0.2.0", default-features = false, features = ["js"] } 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] [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 = [ portable-atomic = { version = "1", default-features = false, features = [

View File

@ -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<Duration> {
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<Instant> {
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<Instant> {
self.0.checked_sub(duration).map(Instant)
}
}
impl Add<Duration> 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<Duration> for Instant {
fn add_assign(&mut self, other: Duration) {
*self = *self + other;
}
}
impl Sub<Duration> for Instant {
type Output = Instant;
fn sub(self, other: Duration) -> Instant {
self.checked_sub(other)
.expect("overflow when subtracting duration from instant")
}
}
impl SubAssign<Duration> for Instant {
fn sub_assign(&mut self, other: Duration) {
*self = *self - other;
}
}
impl Sub<Instant> 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);
}
}

View File

@ -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<Duration> {
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<Instant> {
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<Instant> {
self.0.checked_sub(duration).map(Instant)
}
}
impl Add<Duration> 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<Duration> for Instant {
fn add_assign(&mut self, other: Duration) {
*self = *self + other;
}
}
impl Sub<Duration> for Instant {
type Output = Instant;
fn sub(self, other: Duration) -> Instant {
self.checked_sub(other)
.expect("overflow when subtracting duration from instant")
}
}
impl SubAssign<Duration> for Instant {
fn sub_assign(&mut self, other: Duration) {
*self = *self - other;
}
}
impl Sub<Instant> 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);
}

View File

@ -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;
}
}

View File

@ -69,6 +69,10 @@ critical-section = [
"bevy_utils/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] [dependencies]
# bevy # bevy
bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" } bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" }
@ -111,9 +115,6 @@ wgpu-types = { version = "24", features = [
"serde", "serde",
], optional = true, default-features = false } ], optional = true, default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
uuid = { version = "1.13.1", default-features = false, features = ["js"] }
[dev-dependencies] [dev-dependencies]
ron = "0.8.0" ron = "0.8.0"
rmp-serde = "1.1" rmp-serde = "1.1"

View File

@ -26,6 +26,7 @@ syn = { version = "2.0", features = ["full"] }
uuid = { version = "1.13.1", features = ["v4"] } uuid = { version = "1.13.1", features = ["v4"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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"] } uuid = { version = "1.13.1", default-features = false, features = ["js"] }
[lints] [lints]

View File

@ -123,6 +123,19 @@ web-sys = { version = "0.3.67", features = [
'Window', 'Window',
] } ] }
wasm-bindgen = "0.2" 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] [target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies]
send_wrapper = "0.6.0" send_wrapper = "0.6.0"

View File

@ -38,6 +38,7 @@ thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = ["from"] } derive_more = { version = "1", default-features = false, features = ["from"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [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"] } uuid = { version = "1.13.1", default-features = false, features = ["js"] }
[dev-dependencies] [dev-dependencies]

View File

@ -10,16 +10,42 @@ keywords = ["bevy"]
[features] [features]
default = ["std", "async_executor"] 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 = [ std = [
"futures-lite/std", "futures-lite/std",
"async-task/std", "async-task/std",
"bevy_platform_support/std", "bevy_platform_support/std",
"once_cell/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"] 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] [dependencies]
bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false, features = [ bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false, features = [
"alloc", "alloc",
@ -33,6 +59,7 @@ derive_more = { version = "1", default-features = false, features = [
"deref", "deref",
"deref_mut", "deref_mut",
] } ] }
cfg-if = "1.0.0"
async-executor = { version = "1.11", optional = true } async-executor = { version = "1.11", optional = true }
async-channel = { version = "2.3.0", optional = true } async-channel = { version = "2.3.0", optional = true }
async-io = { version = "2.0.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] [target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = { version = "0.4", optional = true }
pin-project = "1" pin-project = { version = "1", optional = true }
futures-channel = "0.3" 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] [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 = [ async-task = { version = "4.4.0", default-features = false, features = [
@ -61,9 +88,6 @@ atomic-waker = { version = "1", default-features = false, features = [
"portable-atomic", "portable-atomic",
] } ] }
[dev-dependencies]
web-time = { version = "1.1" }
[lints] [lints]
workspace = true workspace = true

View File

@ -2,8 +2,9 @@
//! for 100ms. It's expected to take about a second to run (assuming the machine has >= 4 logical //! for 100ms. It's expected to take about a second to run (assuming the machine has >= 4 logical
//! cores) //! cores)
use bevy_platform_support::time::Instant;
use bevy_tasks::TaskPoolBuilder; use bevy_tasks::TaskPoolBuilder;
use web_time::{Duration, Instant}; use core::time::Duration;
fn main() { fn main() {
let pool = TaskPoolBuilder::new() let pool = TaskPoolBuilder::new()

View File

@ -2,8 +2,9 @@
//! spinning. Other than the one thread, the system should remain idle, demonstrating good behavior //! spinning. Other than the one thread, the system should remain idle, demonstrating good behavior
//! for small workloads. //! for small workloads.
use bevy_platform_support::time::Instant;
use bevy_tasks::TaskPoolBuilder; use bevy_tasks::TaskPoolBuilder;
use web_time::{Duration, Instant}; use core::time::Duration;
fn main() { fn main() {
let pool = TaskPoolBuilder::new() let pool = TaskPoolBuilder::new()

View File

@ -14,21 +14,19 @@ use core::{
}; };
use derive_more::{Deref, DerefMut}; 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")))] #[cfg(all(feature = "multi_threaded", not(target_arch = "wasm32")))]
pub use async_task::FallibleTask; 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. /// Wrapper around a multi-threading-aware async executor.
/// Spawning will generally require tasks to be `Send` and `Sync` to allow multiple /// Spawning will generally require tasks to be `Send` and `Sync` to allow multiple
/// threads to send/receive/advance tasks. /// threads to send/receive/advance tasks.

View File

@ -11,19 +11,20 @@ extern crate std;
extern crate alloc; extern crate alloc;
#[cfg(not(target_arch = "wasm32"))]
mod conditional_send { mod conditional_send {
/// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm), cfg_if::cfg_if! {
/// futures aren't Send. if #[cfg(target_arch = "wasm32")] {
pub trait ConditionalSend: Send {} /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm),
impl<T: Send> ConditionalSend for T {} /// futures aren't Send.
} pub trait ConditionalSend {}
impl<T> ConditionalSend for T {}
#[cfg(target_arch = "wasm32")] } else {
#[expect(missing_docs, reason = "Not all docs are written yet (#3492).")] /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm),
mod conditional_send { /// futures aren't Send.
pub trait ConditionalSend {} pub trait ConditionalSend: Send {}
impl<T> ConditionalSend for T {} impl<T: Send> ConditionalSend for T {}
}
}
} }
pub use conditional_send::*; pub use conditional_send::*;
@ -48,38 +49,40 @@ mod executor;
mod slice; mod slice;
pub use slice::{ParallelSlice, ParallelSliceMut}; 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; mod task;
pub use task::Task; pub use task::Task;
#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] cfg_if::cfg_if! {
mod task_pool; 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")))] pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder, ThreadExecutor};
mod single_threaded_task_pool; }
}
#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))]
pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder, ThreadExecutor};
mod usages; mod usages;
#[cfg(not(target_arch = "wasm32"))] pub use futures_lite::future::poll_once;
pub use usages::tick_global_task_pools_on_main_thread;
pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool}; pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool};
#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] #[cfg(not(all(target_arch = "wasm32", feature = "web")))]
mod thread_executor; pub use usages::tick_global_task_pools_on_main_thread;
#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))]
pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker};
#[cfg(all(feature = "async-io", feature = "std"))] #[cfg(feature = "std")]
pub use async_io::block_on; cfg_if::cfg_if! {
#[cfg(all(not(feature = "async-io"), feature = "std"))] if #[cfg(feature = "async-io")] {
pub use futures_lite::future::block_on; pub use async_io::block_on;
pub use futures_lite::future::poll_once; } else {
pub use futures_lite::future::block_on;
}
}
mod iter; mod iter;
pub use iter::ParallelIterator; pub use iter::ParallelIterator;
@ -102,27 +105,28 @@ pub mod prelude {
pub use crate::block_on; pub use crate::block_on;
} }
#[cfg(feature = "std")] cfg_if::cfg_if! {
use core::num::NonZero; if #[cfg(feature = "std")] {
use core::num::NonZero;
/// Gets the logical CPU core count available to the current process. /// Gets the logical CPU core count available to the current process.
/// ///
/// This is identical to [`std::thread::available_parallelism`], except /// This is identical to [`std::thread::available_parallelism`], except
/// it will return a default value of 1 if it internally errors out. /// it will return a default value of 1 if it internally errors out.
/// ///
/// This will always return at least 1. /// This will always return at least 1.
#[cfg(feature = "std")] pub fn available_parallelism() -> usize {
pub fn available_parallelism() -> usize { std::thread::available_parallelism()
std::thread::available_parallelism() .map(NonZero::<usize>::get)
.map(NonZero::<usize>::get) .unwrap_or(1)
.unwrap_or(1) }
} } else {
/// Gets the logical CPU core count available to the current process.
/// Gets the logical CPU core count available to the current process. ///
/// /// This will always return at least 1.
/// This will always return at least 1. pub fn available_parallelism() -> usize {
#[cfg(not(feature = "std"))] // Without access to std, assume a single thread is available
pub fn available_parallelism() -> usize { 1
// Without access to std, assume a single thread is available }
1 }
} }

View File

@ -199,26 +199,27 @@ impl TaskPool {
where where
T: 'static + MaybeSend + MaybeSync, T: 'static + MaybeSend + MaybeSync,
{ {
#[cfg(target_arch = "wasm32")] cfg_if::cfg_if! {
return Task::wrap_future(future); 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"))] Task::new(task)
return LOCAL_EXECUTOR.with(|executor| { });
let task = executor.spawn(future); } else {
// Loop until all tasks are done return {
while executor.try_tick() {} let task = LOCAL_EXECUTOR.spawn(future);
// Loop until all tasks are done
while LOCAL_EXECUTOR.try_tick() {}
Task::new(task) 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)
};
} }
/// Spawns a static future on the JS event loop. This is exactly the same as [`TaskPool::spawn`]. /// Spawns a static future on the JS event loop. This is exactly the same as [`TaskPool::spawn`].

View File

@ -81,7 +81,7 @@ taskpool! {
/// # Warning /// # Warning
/// ///
/// This function *must* be called on the main thread, or the task pools will not be updated appropriately. /// 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() { pub fn tick_global_task_pools_on_main_thread() {
COMPUTE_TASK_POOL COMPUTE_TASK_POOL
.get() .get()

View File

@ -66,6 +66,19 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2" }
web-sys = "0.3" web-sys = "0.3"
crossbeam-channel = "0.5" 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] [lints]
workspace = true workspace = true

View File

@ -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| |track_location|Enables source location tracking for change detection and spawning/despawning, which can assist with debugging|
|wav|WAV audio format support| |wav|WAV audio format support|
|wayland|Wayland display server 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.| |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| |webp|WebP image format support|
|zlib|For KTX2 supercompression| |zlib|For KTX2 supercompression|