From a3012d94bb4d24a1fb788896f42afb7178ca7de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Kry=C5=84ski?= Date: Sat, 26 Sep 2020 00:26:23 +0200 Subject: [PATCH] WASM asset loading (#559) wasm assets --- Cargo.toml | 6 ++ crates/bevy_asset/Cargo.toml | 7 ++ crates/bevy_asset/src/asset_server.rs | 31 +++++---- crates/bevy_asset/src/load_request/mod.rs | 31 +++++++++ .../platform_default.rs} | 35 +++------- .../src/load_request/platform_wasm.rs | 62 ++++++++++++++++++ crates/bevy_tasks/Cargo.toml | 2 + .../src/single_threaded_task_pool.rs | 26 ++++++++ examples/wasm/assets_wasm.rs | 65 +++++++++++++++++++ 9 files changed, 228 insertions(+), 37 deletions(-) create mode 100644 crates/bevy_asset/src/load_request/mod.rs rename crates/bevy_asset/src/{load_request.rs => load_request/platform_default.rs} (70%) create mode 100644 crates/bevy_asset/src/load_request/platform_wasm.rs create mode 100644 examples/wasm/assets_wasm.rs diff --git a/Cargo.toml b/Cargo.toml index 78fd7e4265..89acfcf086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ log = "0.4" #wasm console_error_panic_hook = "0.1.6" console_log = { version = "0.2", features = ["color"] } +anyhow = "1.0" [[example]] name = "hello_world" @@ -281,3 +282,8 @@ required-features = [] name = "winit_wasm" path = "examples/wasm/winit_wasm.rs" required-features = ["bevy_winit"] + +[[example]] +name = "assets_wasm" +path = "examples/wasm/assets_wasm.rs" +required-features = ["bevy_winit"] diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 1a183273b3..cc44ece932 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -34,3 +34,10 @@ thiserror = "1.0" log = { version = "0.4", features = ["release_max_level_info"] } notify = { version = "5.0.0-pre.2", optional = true } parking_lot = "0.11.0" +async-trait = "0.1.40" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { version = "0.2" } +web-sys = { version = "0.3", features = ["Request", "Window", "Response"]} +wasm-bindgen-futures = "0.4" +js-sys = "0.3" diff --git a/crates/bevy_asset/src/asset_server.rs b/crates/bevy_asset/src/asset_server.rs index 210343a2fd..4b19846d0b 100644 --- a/crates/bevy_asset/src/asset_server.rs +++ b/crates/bevy_asset/src/asset_server.rs @@ -9,11 +9,10 @@ use bevy_utils::{HashMap, HashSet}; use crossbeam_channel::TryRecvError; use parking_lot::RwLock; use std::{ - env, fs, io, + fs, io, path::{Path, PathBuf}, sync::Arc, }; - use thiserror::Error; /// The type used for asset versioning @@ -67,8 +66,7 @@ impl LoadState { /// Loads assets from the filesystem on background threads pub struct AssetServer { asset_folders: RwLock>, - asset_handlers: Arc>>>, - // TODO: this is a hack to enable retrieving generic AssetLoaders. there must be a better way! + asset_handlers: RwLock>>, loaders: Vec, task_pool: TaskPool, extension_to_handler_index: HashMap, @@ -106,7 +104,7 @@ impl AssetServer { .insert(extension.to_string(), handler_index); } - asset_handlers.push(Box::new(asset_handler)); + asset_handlers.push(Arc::new(asset_handler)); } pub fn add_loader(&mut self, loader: TLoader) @@ -173,11 +171,12 @@ impl AssetServer { Ok(()) } + #[cfg(not(target_arch = "wasm32"))] fn get_root_path(&self) -> Result { - if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { Ok(PathBuf::from(manifest_dir)) } else { - match env::current_exe() { + match std::env::current_exe() { Ok(exe_path) => exe_path .parent() .ok_or(AssetServerError::InvalidRootPath) @@ -187,6 +186,11 @@ impl AssetServer { } } + #[cfg(target_arch = "wasm32")] + fn get_root_path(&self) -> Result { + Ok(PathBuf::from("/")) + } + // TODO: add type checking here. people shouldn't be able to request a Handle for a Mesh asset pub fn load>(&self, path: P) -> Result, AssetServerError> { self.load_untyped(self.get_root_path()?.join(path)) @@ -272,19 +276,22 @@ impl AssetServer { version: new_version, }; - let asset_handlers = self.asset_handlers.clone(); + let handlers = self.asset_handlers.read(); + let request_handler = handlers[load_request.handler_index].clone(); + self.task_pool .spawn(async move { - let handlers = asset_handlers.read(); - let request_handler = &handlers[load_request.handler_index]; - request_handler.handle_request(&load_request); + request_handler.handle_request(&load_request).await; }) .detach(); // TODO: watching each asset explicitly is a simpler implementation, its possible it would be more efficient to watch // folders instead (when possible) #[cfg(feature = "filesystem_watcher")] - Self::watch_path_for_changes(&mut self.filesystem_watcher.write(), path)?; + Self::watch_path_for_changes( + &mut self.filesystem_watcher.write(), + path.to_owned(), + )?; Ok(handle_id) } else { Err(AssetServerError::MissingAssetHandler) diff --git a/crates/bevy_asset/src/load_request/mod.rs b/crates/bevy_asset/src/load_request/mod.rs new file mode 100644 index 0000000000..55a64c3825 --- /dev/null +++ b/crates/bevy_asset/src/load_request/mod.rs @@ -0,0 +1,31 @@ +use crate::{AssetLoader, AssetResult, AssetVersion, HandleId}; +use crossbeam_channel::Sender; +use std::path::PathBuf; + +#[cfg(not(target_arch = "wasm32"))] +#[path = "platform_default.rs"] +mod platform_specific; + +#[cfg(target_arch = "wasm32")] +#[path = "platform_wasm.rs"] +mod platform_specific; + +pub use platform_specific::*; + +/// A request from an [AssetServer](crate::AssetServer) to load an asset. +#[derive(Debug)] +pub struct LoadRequest { + pub path: PathBuf, + pub handle_id: HandleId, + pub handler_index: usize, + pub version: AssetVersion, +} + +pub(crate) struct ChannelAssetHandler +where + TLoader: AssetLoader, + TAsset: 'static, +{ + sender: Sender>, + loader: TLoader, +} diff --git a/crates/bevy_asset/src/load_request.rs b/crates/bevy_asset/src/load_request/platform_default.rs similarity index 70% rename from crates/bevy_asset/src/load_request.rs rename to crates/bevy_asset/src/load_request/platform_default.rs index 462181bf60..4aa84a0faf 100644 --- a/crates/bevy_asset/src/load_request.rs +++ b/crates/bevy_asset/src/load_request/platform_default.rs @@ -1,32 +1,16 @@ -use crate::{AssetLoadError, AssetLoader, AssetResult, AssetVersion, Handle, HandleId}; +use super::{ChannelAssetHandler, LoadRequest}; +use crate::{AssetLoadError, AssetLoader, AssetResult, Handle}; use anyhow::Result; +use async_trait::async_trait; use crossbeam_channel::Sender; -use fs::File; -use io::Read; -use std::{fs, io, path::PathBuf}; - -/// A request from an [AssetServer](crate::AssetServer) to load an asset. -#[derive(Debug)] -pub struct LoadRequest { - pub path: PathBuf, - pub handle_id: HandleId, - pub handler_index: usize, - pub version: AssetVersion, -} +use std::{fs::File, io::Read}; /// Handles load requests from an AssetServer -pub trait AssetLoadRequestHandler: Send + Sync + 'static { - fn handle_request(&self, load_request: &LoadRequest); - fn extensions(&self) -> &[&str]; -} -pub(crate) struct ChannelAssetHandler -where - TLoader: AssetLoader, - TAsset: 'static, -{ - sender: Sender>, - loader: TLoader, +#[async_trait] +pub trait AssetLoadRequestHandler: Send + Sync + 'static { + async fn handle_request(&self, load_request: &LoadRequest); + fn extensions(&self) -> &[&str]; } impl ChannelAssetHandler @@ -53,12 +37,13 @@ where } } +#[async_trait] impl AssetLoadRequestHandler for ChannelAssetHandler where TLoader: AssetLoader + 'static, TAsset: Send + 'static, { - fn handle_request(&self, load_request: &LoadRequest) { + async fn handle_request(&self, load_request: &LoadRequest) { let result = self.load_asset(load_request); let asset_result = AssetResult { handle: Handle::from(load_request.handle_id), diff --git a/crates/bevy_asset/src/load_request/platform_wasm.rs b/crates/bevy_asset/src/load_request/platform_wasm.rs new file mode 100644 index 0000000000..4a240bf3ea --- /dev/null +++ b/crates/bevy_asset/src/load_request/platform_wasm.rs @@ -0,0 +1,62 @@ +use super::{ChannelAssetHandler, LoadRequest}; +use crate::{AssetLoadError, AssetLoader, AssetResult, Handle}; +use anyhow::Result; +use async_trait::async_trait; +use crossbeam_channel::Sender; + +use js_sys::Uint8Array; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::Response; + +#[async_trait(?Send)] +pub trait AssetLoadRequestHandler: Send + Sync + 'static { + async fn handle_request(&self, load_request: &LoadRequest); + fn extensions(&self) -> &[&str]; +} + +impl ChannelAssetHandler +where + TLoader: AssetLoader, +{ + pub fn new(loader: TLoader, sender: Sender>) -> Self { + ChannelAssetHandler { sender, loader } + } + + async fn load_asset(&self, load_request: &LoadRequest) -> Result { + // TODO - get rid of some unwraps below (do some retrying maybe?) + let window = web_sys::window().unwrap(); + let resp_value = JsFuture::from(window.fetch_with_str(load_request.path.to_str().unwrap())) + .await + .unwrap(); + let resp: Response = resp_value.dyn_into().unwrap(); + let data = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap(); + let bytes = Uint8Array::new(&data).to_vec(); + let asset = self.loader.from_bytes(&load_request.path, bytes).unwrap(); + Ok(asset) + } +} + +#[async_trait(?Send)] +impl AssetLoadRequestHandler for ChannelAssetHandler +where + TLoader: AssetLoader + 'static, + TAsset: Send + 'static, +{ + async fn handle_request(&self, load_request: &LoadRequest) { + let asset = self.load_asset(load_request).await; + let asset_result = AssetResult { + handle: Handle::from(load_request.handle_id), + result: asset, + path: load_request.path.clone(), + version: load_request.version, + }; + self.sender + .send(asset_result) + .expect("loaded asset should have been sent"); + } + + fn extensions(&self) -> &[&str] { + self.loader.extensions() + } +} diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index b4a067d539..d596ea1bbc 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -16,3 +16,5 @@ event-listener = "2.4.0" async-executor = "1.3.0" async-channel = "1.4.2" num_cpus = "1" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 8899feb9ed..69048c9140 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -82,6 +82,32 @@ impl TaskPool { .map(|result| result.lock().unwrap().take().unwrap()) .collect() } + + // Spawns a static future onto the JS event loop. For now it is returning FakeTask + // instance with no-op detach method. Returning real Task is possible here, but tricky: + // future is running on JS event loop, Task is running on async_executor::LocalExecutor + // so some proxy future is needed. Moreover currently we don't have long-living + // LocalExecutor here (above `spawn` implementation creates temporary one) + // But for typical use cases it seems that current implementation should be sufficient: + // caller can spawn long-running future writing results to some channel / event queue + // and simply call detach on returned Task (like AssetServer does) - spawned future + // can write results to some channel / event queue. + + pub fn spawn(&self, future: impl Future + 'static) -> FakeTask + where + T: 'static, + { + wasm_bindgen_futures::spawn_local(async move { + future.await; + }); + FakeTask + } +} + +pub struct FakeTask; + +impl FakeTask { + pub fn detach(self) {} } pub struct Scope<'scope, T> { diff --git a/examples/wasm/assets_wasm.rs b/examples/wasm/assets_wasm.rs new file mode 100644 index 0000000000..59d36fe027 --- /dev/null +++ b/examples/wasm/assets_wasm.rs @@ -0,0 +1,65 @@ +extern crate console_error_panic_hook; +use bevy::{asset::AssetLoader, prelude::*}; +use std::{panic, path::PathBuf}; + +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_default_plugins() + .add_asset::() + .add_asset_loader::() + .add_startup_system(asset_system.system()) + .add_system(asset_events.system()) + .run(); +} + +fn asset_system(asset_server: Res) { + asset_server + .load::, _>(PathBuf::from("assets_wasm.rs")) + .unwrap(); + log::info!("hello wasm"); +} + +#[derive(Debug)] +pub struct RustSourceCode(pub String); + +#[derive(Default)] +pub struct RustSourceCodeLoader; +impl AssetLoader for RustSourceCodeLoader { + fn from_bytes( + &self, + _asset_path: &std::path::Path, + bytes: Vec, + ) -> Result { + Ok(RustSourceCode(String::from_utf8(bytes)?)) + } + + fn extensions(&self) -> &[&str] { + static EXT: &[&str] = &["rs"]; + EXT + } +} + +#[derive(Default)] +pub struct AssetEventsState { + reader: EventReader>, +} + +pub fn asset_events( + mut state: Local, + rust_sources: Res>, + events: Res>>, +) { + for event in state.reader.iter(&events) { + match event { + AssetEvent::Created { handle } => { + if let Some(code) = rust_sources.get(handle) { + log::info!("code: {}", code.0); + } + } + _ => continue, + }; + } +}