mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Implement single threaded task scheduler for WebAssembly (#496)
* Add hello_wasm example * Implement single threaded task scheduler for WebAssembly
This commit is contained in:
parent
1bfc147e7b
commit
2b0ee24a5d
12 changed files with 287 additions and 24 deletions
15
Cargo.toml
15
Cargo.toml
|
@ -87,6 +87,11 @@ bevy_winit = { path = "crates/bevy_winit", optional = true, version = "0.1" }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
#wasm
|
||||||
|
console_error_panic_hook = "0.1.6"
|
||||||
|
console_log = { version = "0.2", features = ["color"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "hello_world"
|
name = "hello_world"
|
||||||
|
@ -255,3 +260,13 @@ path = "examples/window/multiple_windows.rs"
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "window_settings"
|
name = "window_settings"
|
||||||
path = "examples/window/window_settings.rs"
|
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 = []
|
||||||
|
|
|
@ -26,3 +26,8 @@ bevy_math = { path = "../bevy_math", version = "0.1" }
|
||||||
libloading = { version = "0.6", optional = true }
|
libloading = { version = "0.6", optional = true }
|
||||||
log = { version = "0.4", features = ["release_max_level_info"] }
|
log = { version = "0.4", features = ["release_max_level_info"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
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"
|
||||||
|
|
|
@ -4,10 +4,17 @@ use crate::{
|
||||||
event::{EventReader, Events},
|
event::{EventReader, Events},
|
||||||
plugin::Plugin,
|
plugin::Plugin,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::time::Duration;
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
#[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`
|
/// Determines the method used to run an [App]'s `Schedule`
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
@ -53,32 +60,74 @@ impl Plugin for ScheduleRunnerPlugin {
|
||||||
RunMode::Once => {
|
RunMode::Once => {
|
||||||
app.update();
|
app.update();
|
||||||
}
|
}
|
||||||
RunMode::Loop { wait } => loop {
|
RunMode::Loop { wait } => {
|
||||||
let start_time = Instant::now();
|
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 let Some(app_exit_events) = app.resources.get_mut::<Events<AppExit>>() {
|
||||||
if app_exit_event_reader.latest(&app_exit_events).is_some() {
|
if app_exit_event_reader.latest(&app_exit_events).is_some() {
|
||||||
break;
|
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();
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
if let Some(app_exit_events) = app.resources.get_mut::<Events<AppExit>>() {
|
fn set_timeout(f: &Closure<dyn FnMut()>, dur: Duration) {
|
||||||
if app_exit_event_reader.latest(&app_exit_events).is_some() {
|
web_sys::window()
|
||||||
break;
|
.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 c = move || {
|
||||||
let exe_time = end_time - start_time;
|
let mut app = Rc::get_mut(&mut rc).unwrap();
|
||||||
if exe_time < wait {
|
let delay = tick(&mut app, wait).unwrap_or(asap);
|
||||||
thread::sleep(wait - exe_time);
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,16 @@ pub use slice::{ParallelSlice, ParallelSliceMut};
|
||||||
mod task;
|
mod task;
|
||||||
pub use task::Task;
|
pub use task::Task;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod task_pool;
|
mod task_pool;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use task_pool::{Scope, TaskPool, TaskPoolBuilder};
|
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;
|
mod usages;
|
||||||
pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IOTaskPool};
|
pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IOTaskPool};
|
||||||
|
|
||||||
|
|
112
crates/bevy_tasks/src/single_threaded_task_pool.rs
Normal file
112
crates/bevy_tasks/src/single_threaded_task_pool.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,3 +21,6 @@ bevy_utils = { path = "../bevy_utils", version = "0.1" }
|
||||||
|
|
||||||
# other
|
# other
|
||||||
uuid = { version = "0.8", features = ["v4", "serde"] }
|
uuid = { version = "0.8", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
uuid = { version = "0.8", features = ["wasm-bindgen"] }
|
||||||
|
|
|
@ -24,7 +24,7 @@ Example | Main | Description
|
||||||
|
|
||||||
Example | File | 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
|
`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
|
`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
|
`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
|
`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
|
`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
|
`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
1
examples/wasm/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
BIN
examples/wasm/favicon.ico
Normal file
BIN
examples/wasm/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
32
examples/wasm/headless_wasm.rs
Normal file
32
examples/wasm/headless_wasm.rs
Normal 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,
|
||||||
|
}
|
14
examples/wasm/hello_wasm.rs
Normal file
14
examples/wasm/hello_wasm.rs
Normal 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
9
examples/wasm/index.html
Normal 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>
|
Loading…
Reference in a new issue