Skip to content

Commit

Permalink
Implement single threaded task scheduler for WebAssembly (bevyengine#496
Browse files Browse the repository at this point in the history
)

* Add hello_wasm example

* Implement single threaded task scheduler for WebAssembly
  • Loading branch information
smokku authored and mrk-its committed Oct 6, 2020
1 parent f9fd4ae commit e2e7a72
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 22 deletions.
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = []
5 changes: 5 additions & 0 deletions crates/bevy_app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
91 changes: 70 additions & 21 deletions crates/bevy_app/src/schedule_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<Duration>|
-> Option<Duration> {
let start_time = Instant::now();

if let Some(app_exit_events) = app.resources.get_mut::<Events<AppExit>>() {
if app_exit_event_reader.latest(&app_exit_events).is_some() {
break;
if let Some(app_exit_events) = app.resources.get_mut::<Events<AppExit>>() {
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::<Events<AppExit>>() {
if app_exit_event_reader.latest(&app_exit_events).is_some() {
break;
if let Some(app_exit_events) = app.resources.get_mut::<Events<AppExit>>() {
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<dyn FnMut()>, 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<dyn FnMut()>));
set_timeout(g.borrow().as_ref().unwrap(), asap);
};
}
}
});
}
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_tasks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
112 changes: 112 additions & 0 deletions crates/bevy_tasks/src/single_threaded_task_pool.rs
Original file line number Diff line number Diff line change
@@ -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<T>
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<Arc<Mutex<Option<T>>>>,
}

impl<'scope, T: Send + 'static> Scope<'scope, T> {
pub fn spawn<Fut: Future<Output = T> + '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<Box<dyn Future<Output = ()> + 'scope>> = Box::pin(f);
let fut: Pin<Box<dyn Future<Output = ()> + 'static>> = unsafe { mem::transmute(fut) };

self.executor.spawn(fut).detach();
}
}
3 changes: 3 additions & 0 deletions crates/bevy_window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
18 changes: 17 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions examples/wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
Binary file added examples/wasm/favicon.ico
Binary file not shown.
32 changes: 32 additions & 0 deletions examples/wasm/headless_wasm.rs
Original file line number Diff line number Diff line change
@@ -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<CounterState>) {
if state.count % 60 == 0 {
log::info!("counter system: {}", state.count);
}
state.count += 1;
}

#[derive(Default)]
struct CounterState {
count: u32,
}
14 changes: 14 additions & 0 deletions examples/wasm/hello_wasm.rs
Original file line number Diff line number Diff line change
@@ -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");
}
9 changes: 9 additions & 0 deletions examples/wasm/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<meta charset="UTF-8" />
</head>
<script type="module">
import init from './target/headless_wasm.js'
init()
</script>
</html>

0 comments on commit e2e7a72

Please sign in to comment.