Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement single threaded task scheduler for WebAssembly #496

Merged
merged 2 commits into from
Sep 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"] }
smokku marked this conversation as resolved.
Show resolved Hide resolved
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>