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`.
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 }

View File

@ -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]

View File

@ -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"

View File

@ -1353,7 +1353,7 @@ type RunnerFn = Box<dyn FnOnce(App) -> 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();

View File

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

View File

@ -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<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);
#[cfg(target_arch = "wasm32")]
{
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 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<dyn FnMut()>));
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<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 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<NonSend<NonSendMarker>>) {
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<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
/// 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()
});

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" }
[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 }

View File

@ -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"]

View File

@ -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 = [

View File

@ -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"

View File

@ -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]

View File

@ -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 = [

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",
]
## 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"

View File

@ -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]

View File

@ -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"

View File

@ -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]

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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.

View File

@ -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<T: Send> 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<T> 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<T> 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<T: Send> 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::<usize>::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::<usize>::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
}
}
}

View File

@ -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`].

View File

@ -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()

View File

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

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|
|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|