diff --git a/Cargo.toml b/Cargo.toml index de6d4d9113b97..3347bf7eaea9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,11 @@ bevy_winit = { path = "crates/bevy_winit", optional = true, version = "0.1" } [dev-dependencies] rand = "0.7.3" serde = { version = "1", features = ["derive"] } +log = "0.4" + +#wasm +console_error_panic_hook = "0.1.6" +console_log = { version = "0.2", features = ["color"] } [[example]] name = "hello_world" @@ -255,3 +260,13 @@ path = "examples/window/multiple_windows.rs" [[example]] name = "window_settings" path = "examples/window/window_settings.rs" + +[[example]] +name = "hello_wasm" +path = "examples/wasm/hello_wasm.rs" +required-features = [] + +[[example]] +name = "headless_wasm" +path = "examples/wasm/headless_wasm.rs" +required-features = [] diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 5b4506e5ad217..8be8fd53dde8a 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -26,3 +26,8 @@ bevy_math = { path = "../bevy_math", version = "0.1" } libloading = { version = "0.6", optional = true } log = { version = "0.4", features = ["release_max_level_info"] } serde = { version = "1.0", features = ["derive"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { version = "0.2" } +web-sys = { version = "0.3", features = [ "Window" ] } +wasm-timer = "0.2.5" diff --git a/crates/bevy_app/src/schedule_runner.rs b/crates/bevy_app/src/schedule_runner.rs index d80a174e0f890..ef373eddff66f 100644 --- a/crates/bevy_app/src/schedule_runner.rs +++ b/crates/bevy_app/src/schedule_runner.rs @@ -4,10 +4,17 @@ use crate::{ event::{EventReader, Events}, plugin::Plugin, }; -use std::{ - thread, - time::{Duration, Instant}, -}; +use std::time::Duration; + +#[cfg(not(target_arch = "wasm32"))] +use std::{thread, time::Instant}; +#[cfg(target_arch = "wasm32")] +use wasm_timer::Instant; + +#[cfg(target_arch = "wasm32")] +use std::{cell::RefCell, rc::Rc}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{prelude::*, JsCast}; /// Determines the method used to run an [App]'s `Schedule` #[derive(Copy, Clone, Debug)] @@ -53,32 +60,74 @@ impl Plugin for ScheduleRunnerPlugin { RunMode::Once => { app.update(); } - RunMode::Loop { wait } => loop { - let start_time = Instant::now(); + RunMode::Loop { wait } => { + let mut tick = move |app: &mut App, + wait: Option| + -> Option { + let start_time = Instant::now(); - if let Some(app_exit_events) = app.resources.get_mut::>() { - if app_exit_event_reader.latest(&app_exit_events).is_some() { - break; + if let Some(app_exit_events) = app.resources.get_mut::>() { + if app_exit_event_reader.latest(&app_exit_events).is_some() { + return None; + } } - } - app.update(); + app.update(); - if let Some(app_exit_events) = app.resources.get_mut::>() { - if app_exit_event_reader.latest(&app_exit_events).is_some() { - break; + if let Some(app_exit_events) = app.resources.get_mut::>() { + if app_exit_event_reader.latest(&app_exit_events).is_some() { + return None; + } } - } - let end_time = Instant::now(); + let end_time = Instant::now(); - if let Some(wait) = wait { - let exe_time = end_time - start_time; - if exe_time < wait { - thread::sleep(wait - exe_time); + if let Some(wait) = wait { + let exe_time = end_time - start_time; + if exe_time < wait { + return Some(wait - exe_time); + } + } + + None + }; + + #[cfg(not(target_arch = "wasm32"))] + { + loop { + if let Some(delay) = tick(&mut app, wait) { + thread::sleep(delay); + } } } - }, + + #[cfg(target_arch = "wasm32")] + { + fn set_timeout(f: &Closure, dur: Duration) { + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0( + f.as_ref().unchecked_ref(), + dur.as_millis() as i32, + ) + .expect("should register `setTimeout`"); + } + let asap = Duration::from_millis(1); + + let mut rc = Rc::new(app); + let f = Rc::new(RefCell::new(None)); + let g = f.clone(); + + let c = move || { + let mut app = Rc::get_mut(&mut rc).unwrap(); + let delay = tick(&mut app, wait).unwrap_or(asap); + set_timeout(f.borrow().as_ref().unwrap(), delay); + }; + + *g.borrow_mut() = Some(Closure::wrap(Box::new(c) as Box)); + set_timeout(g.borrow().as_ref().unwrap(), asap); + }; + } } }); } diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 28ffdda055fbc..33c379139ebcf 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -4,9 +4,16 @@ pub use slice::{ParallelSlice, ParallelSliceMut}; mod task; pub use task::Task; +#[cfg(not(target_arch = "wasm32"))] mod task_pool; +#[cfg(not(target_arch = "wasm32"))] pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; +#[cfg(target_arch = "wasm32")] +mod single_threaded_task_pool; +#[cfg(target_arch = "wasm32")] +pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder}; + mod usages; pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IOTaskPool}; diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs new file mode 100644 index 0000000000000..6076b2a120b58 --- /dev/null +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -0,0 +1,112 @@ +use std::{ + future::Future, + mem, + pin::Pin, + sync::{Arc, Mutex}, +}; + +/// Used to create a TaskPool +#[derive(Debug, Default, Clone)] +pub struct TaskPoolBuilder {} + +impl TaskPoolBuilder { + /// Creates a new TaskPoolBuilder instance + pub fn new() -> Self { + Self::default() + } + + pub fn num_threads(self, _num_threads: usize) -> Self { + self + } + + pub fn stack_size(self, _stack_size: usize) -> Self { + self + } + + pub fn thread_name(self, _thread_name: String) -> Self { + self + } + + pub fn build(self) -> TaskPool { + TaskPool::new_internal() + } +} + +/// A thread pool for executing tasks. Tasks are futures that are being automatically driven by +/// the pool on threads owned by the pool. In this case - main thread only. +#[derive(Debug, Default, Clone)] +pub struct TaskPool {} + +impl TaskPool { + /// Create a `TaskPool` with the default configuration. + pub fn new() -> Self { + TaskPoolBuilder::new().build() + } + + #[allow(unused_variables)] + fn new_internal() -> Self { + Self {} + } + + /// Return the number of threads owned by the task pool + pub fn thread_num(&self) -> usize { + 1 + } + + /// Allows spawning non-`static futures on the thread pool. The function takes a callback, + /// passing a scope object into it. The scope object provided to the callback can be used + /// to spawn tasks. This function will await the completion of all tasks before returning. + /// + /// This is similar to `rayon::scope` and `crossbeam::scope` + pub fn scope<'scope, F, T>(&self, f: F) -> Vec + where + F: FnOnce(&mut Scope<'scope, T>) + 'scope + Send, + T: Send + 'static, + { + let executor = async_executor::LocalExecutor::new(); + + let executor: &async_executor::LocalExecutor = &executor; + let executor: &'scope async_executor::LocalExecutor = unsafe { mem::transmute(executor) }; + + let mut scope = Scope { + executor, + results: Vec::new(), + }; + + f(&mut scope); + + // Loop until all tasks are done + while executor.try_tick() {} + + scope + .results + .iter() + .map(|result| result.lock().unwrap().take().unwrap()) + .collect() + } +} + +pub struct Scope<'scope, T> { + executor: &'scope async_executor::LocalExecutor, + // Vector to gather results of all futures spawned during scope run + results: Vec>>>, +} + +impl<'scope, T: Send + 'static> Scope<'scope, T> { + pub fn spawn + 'scope + Send>(&mut self, f: Fut) { + let result = Arc::new(Mutex::new(None)); + self.results.push(result.clone()); + let f = async move { + result.lock().unwrap().replace(f.await); + }; + + // SAFETY: This function blocks until all futures complete, so we do not read/write the + // data from futures outside of the 'scope lifetime. However, rust has no way of knowing + // this so we must convert to 'static here to appease the compiler as it is unable to + // validate safety. + let fut: Pin + 'scope>> = Box::pin(f); + let fut: Pin + 'static>> = unsafe { mem::transmute(fut) }; + + self.executor.spawn(fut).detach(); + } +} diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 059efc4d8ceed..f1ebd68a80ae3 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -21,3 +21,6 @@ bevy_utils = { path = "../bevy_utils", version = "0.1" } # other uuid = { version = "0.8", features = ["v4", "serde"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +uuid = { version = "0.8", features = ["wasm-bindgen"] } diff --git a/examples/README.md b/examples/README.md index 0362862937d1a..117e11d3b3f58 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,7 +24,7 @@ Example | Main | Description Example | File | Description --- | --- | --- -`load_model` | [`3d/load_model.rs`](./3d/load_model.rs) | Loads and renders a simple model +`load_model` | [`3d/load_model.rs`](./3d/load_model.rs) | Loads and renders a simple model `msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges `parenting` | [`3d/parenting.rs`](./3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations `3d_scene` | [`3d/3d_scene.rs`](./3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting @@ -116,3 +116,19 @@ Example | File | Description `clear_color` | [`window/clear_color.rs`](./window/clear_color.rs) | Creates a solid color window `multiple_windows` | [`window/multiple_windows.rs`](./window/multiple_windows.rs) | Creates two windows and cameras viewing the same mesh `window_settings` | [`window/window_settings.rs`](./window/window_settings.rs) | Demonstrates customizing default window settings + +## WASM + +#### pre-req + + $ rustup target add wasm32-unknown-unknown + $ cargo install wasm-bindgen-cli + +#### build & run + + $ cargo build --example headless_wasm --target wasm32-unknown-unknown --no-default-features + $ wasm-bindgen --out-dir examples/wasm/target --target web target/wasm32-unknown-unknown/debug/examples/headless_wasm.wasm + +Then serve `examples/wasm` dir to browser. i.e. + + $ basic-http-server examples/wasm diff --git a/examples/wasm/.gitignore b/examples/wasm/.gitignore new file mode 100644 index 0000000000000..2f7896d1d1365 --- /dev/null +++ b/examples/wasm/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/examples/wasm/favicon.ico b/examples/wasm/favicon.ico new file mode 100644 index 0000000000000..f2c5168a03585 Binary files /dev/null and b/examples/wasm/favicon.ico differ diff --git a/examples/wasm/headless_wasm.rs b/examples/wasm/headless_wasm.rs new file mode 100644 index 0000000000000..801ba5ed83585 --- /dev/null +++ b/examples/wasm/headless_wasm.rs @@ -0,0 +1,32 @@ +extern crate console_error_panic_hook; +use bevy::{app::ScheduleRunnerPlugin, prelude::*}; +use std::{panic, time::Duration}; + +fn main() { + panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Debug).expect("cannot initialize console_log"); + + App::build() + .add_plugin(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( + 1.0 / 60.0, + ))) + .add_startup_system(hello_world_system.system()) + .add_system(counter.system()) + .run(); +} + +fn hello_world_system() { + log::info!("hello wasm"); +} + +fn counter(mut state: Local) { + if state.count % 60 == 0 { + log::info!("counter system: {}", state.count); + } + state.count += 1; +} + +#[derive(Default)] +struct CounterState { + count: u32, +} diff --git a/examples/wasm/hello_wasm.rs b/examples/wasm/hello_wasm.rs new file mode 100644 index 0000000000000..4354dc1ae70ca --- /dev/null +++ b/examples/wasm/hello_wasm.rs @@ -0,0 +1,14 @@ +extern crate console_error_panic_hook; +use bevy::prelude::*; +use std::panic; + +fn main() { + panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Debug).expect("cannot initialize console_log"); + + App::build().add_system(hello_wasm_system.system()).run(); +} + +fn hello_wasm_system() { + log::info!("hello wasm"); +} diff --git a/examples/wasm/index.html b/examples/wasm/index.html new file mode 100644 index 0000000000000..929142536e8ad --- /dev/null +++ b/examples/wasm/index.html @@ -0,0 +1,9 @@ + + + + + +