Implement single threaded task scheduler for WebAssembly (#496)

* Add hello_wasm example

* Implement single threaded task scheduler for WebAssembly
This commit is contained in:
Tomasz Sterna 2020-09-16 03:05:31 +02:00 committed by GitHub
parent 1bfc147e7b
commit 2b0ee24a5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 287 additions and 24 deletions

View file

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

View file

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

View file

@ -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<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();
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();
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);
}
}
}
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;
#[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 end_time = Instant::now();
let mut rc = Rc::new(app);
let f = Rc::new(RefCell::new(None));
let g = f.clone();
if let Some(wait) = wait {
let exe_time = end_time - start_time;
if exe_time < wait {
thread::sleep(wait - exe_time);
}
}
},
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);
};
}
}
});
}

View file

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

View file

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

View file

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

View file

@ -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
examples/wasm/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

BIN
examples/wasm/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

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

View file

@ -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
examples/wasm/index.html Normal file
View file

@ -0,0 +1,9 @@
<html>
<head>
<meta charset="UTF-8" />
</head>
<script type="module">
import init from './target/headless_wasm.js'
init()
</script>
</html>