mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +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]
|
||||
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 = []
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
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
|
||||
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
|
||||
--- | --- | ---
|
||||
`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
|
||||
|
|
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