WASM asset loading (#559)

wasm assets
This commit is contained in:
Mariusz Kryński 2020-09-26 00:26:23 +02:00 committed by GitHub
parent afc656701d
commit a3012d94bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 37 deletions

View file

@ -94,6 +94,7 @@ log = "0.4"
#wasm #wasm
console_error_panic_hook = "0.1.6" console_error_panic_hook = "0.1.6"
console_log = { version = "0.2", features = ["color"] } console_log = { version = "0.2", features = ["color"] }
anyhow = "1.0"
[[example]] [[example]]
name = "hello_world" name = "hello_world"
@ -281,3 +282,8 @@ required-features = []
name = "winit_wasm" name = "winit_wasm"
path = "examples/wasm/winit_wasm.rs" path = "examples/wasm/winit_wasm.rs"
required-features = ["bevy_winit"] required-features = ["bevy_winit"]
[[example]]
name = "assets_wasm"
path = "examples/wasm/assets_wasm.rs"
required-features = ["bevy_winit"]

View file

@ -34,3 +34,10 @@ thiserror = "1.0"
log = { version = "0.4", features = ["release_max_level_info"] } log = { version = "0.4", features = ["release_max_level_info"] }
notify = { version = "5.0.0-pre.2", optional = true } notify = { version = "5.0.0-pre.2", optional = true }
parking_lot = "0.11.0" 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"

View file

@ -9,11 +9,10 @@ use bevy_utils::{HashMap, HashSet};
use crossbeam_channel::TryRecvError; use crossbeam_channel::TryRecvError;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::{ use std::{
env, fs, io, fs, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use thiserror::Error; use thiserror::Error;
/// The type used for asset versioning /// The type used for asset versioning
@ -67,8 +66,7 @@ impl LoadState {
/// Loads assets from the filesystem on background threads /// Loads assets from the filesystem on background threads
pub struct AssetServer { pub struct AssetServer {
asset_folders: RwLock<Vec<PathBuf>>, asset_folders: RwLock<Vec<PathBuf>>,
asset_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>, asset_handlers: RwLock<Vec<Arc<dyn AssetLoadRequestHandler>>>,
// TODO: this is a hack to enable retrieving generic AssetLoader<T>s. there must be a better way!
loaders: Vec<Resources>, loaders: Vec<Resources>,
task_pool: TaskPool, task_pool: TaskPool,
extension_to_handler_index: HashMap<String, usize>, extension_to_handler_index: HashMap<String, usize>,
@ -106,7 +104,7 @@ impl AssetServer {
.insert(extension.to_string(), handler_index); .insert(extension.to_string(), handler_index);
} }
asset_handlers.push(Box::new(asset_handler)); asset_handlers.push(Arc::new(asset_handler));
} }
pub fn add_loader<TLoader, TAsset>(&mut self, loader: TLoader) pub fn add_loader<TLoader, TAsset>(&mut self, loader: TLoader)
@ -173,11 +171,12 @@ impl AssetServer {
Ok(()) Ok(())
} }
#[cfg(not(target_arch = "wasm32"))]
fn get_root_path(&self) -> Result<PathBuf, AssetServerError> { fn get_root_path(&self) -> Result<PathBuf, AssetServerError> {
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)) Ok(PathBuf::from(manifest_dir))
} else { } else {
match env::current_exe() { match std::env::current_exe() {
Ok(exe_path) => exe_path Ok(exe_path) => exe_path
.parent() .parent()
.ok_or(AssetServerError::InvalidRootPath) .ok_or(AssetServerError::InvalidRootPath)
@ -187,6 +186,11 @@ impl AssetServer {
} }
} }
#[cfg(target_arch = "wasm32")]
fn get_root_path(&self) -> Result<PathBuf, AssetServerError> {
Ok(PathBuf::from("/"))
}
// TODO: add type checking here. people shouldn't be able to request a Handle<Texture> for a Mesh asset // TODO: add type checking here. people shouldn't be able to request a Handle<Texture> for a Mesh asset
pub fn load<T, P: AsRef<Path>>(&self, path: P) -> Result<Handle<T>, AssetServerError> { pub fn load<T, P: AsRef<Path>>(&self, path: P) -> Result<Handle<T>, AssetServerError> {
self.load_untyped(self.get_root_path()?.join(path)) self.load_untyped(self.get_root_path()?.join(path))
@ -272,19 +276,22 @@ impl AssetServer {
version: new_version, 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 self.task_pool
.spawn(async move { .spawn(async move {
let handlers = asset_handlers.read(); request_handler.handle_request(&load_request).await;
let request_handler = &handlers[load_request.handler_index];
request_handler.handle_request(&load_request);
}) })
.detach(); .detach();
// TODO: watching each asset explicitly is a simpler implementation, its possible it would be more efficient to watch // TODO: watching each asset explicitly is a simpler implementation, its possible it would be more efficient to watch
// folders instead (when possible) // folders instead (when possible)
#[cfg(feature = "filesystem_watcher")] #[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) Ok(handle_id)
} else { } else {
Err(AssetServerError::MissingAssetHandler) Err(AssetServerError::MissingAssetHandler)

View file

@ -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<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
TAsset: 'static,
{
sender: Sender<AssetResult<TAsset>>,
loader: TLoader,
}

View file

@ -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 anyhow::Result;
use async_trait::async_trait;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use fs::File; use std::{fs::File, io::Read};
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,
}
/// Handles load requests from an AssetServer /// 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<TLoader, TAsset> #[async_trait]
where pub trait AssetLoadRequestHandler: Send + Sync + 'static {
TLoader: AssetLoader<TAsset>, async fn handle_request(&self, load_request: &LoadRequest);
TAsset: 'static, fn extensions(&self) -> &[&str];
{
sender: Sender<AssetResult<TAsset>>,
loader: TLoader,
} }
impl<TLoader, TAsset> ChannelAssetHandler<TLoader, TAsset> impl<TLoader, TAsset> ChannelAssetHandler<TLoader, TAsset>
@ -53,12 +37,13 @@ where
} }
} }
#[async_trait]
impl<TLoader, TAsset> AssetLoadRequestHandler for ChannelAssetHandler<TLoader, TAsset> impl<TLoader, TAsset> AssetLoadRequestHandler for ChannelAssetHandler<TLoader, TAsset>
where where
TLoader: AssetLoader<TAsset> + 'static, TLoader: AssetLoader<TAsset> + 'static,
TAsset: Send + '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 result = self.load_asset(load_request);
let asset_result = AssetResult { let asset_result = AssetResult {
handle: Handle::from(load_request.handle_id), handle: Handle::from(load_request.handle_id),

View file

@ -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<TLoader, TAsset> ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
{
pub fn new(loader: TLoader, sender: Sender<AssetResult<TAsset>>) -> Self {
ChannelAssetHandler { sender, loader }
}
async fn load_asset(&self, load_request: &LoadRequest) -> Result<TAsset, AssetLoadError> {
// 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<TLoader, TAsset> AssetLoadRequestHandler for ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset> + '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()
}
}

View file

@ -16,3 +16,5 @@ event-listener = "2.4.0"
async-executor = "1.3.0" async-executor = "1.3.0"
async-channel = "1.4.2" async-channel = "1.4.2"
num_cpus = "1" num_cpus = "1"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"

View file

@ -82,6 +82,32 @@ impl TaskPool {
.map(|result| result.lock().unwrap().take().unwrap()) .map(|result| result.lock().unwrap().take().unwrap())
.collect() .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<T>(&self, future: impl Future<Output = T> + '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> { pub struct Scope<'scope, T> {

View file

@ -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::<RustSourceCode>()
.add_asset_loader::<RustSourceCode, RustSourceCodeLoader>()
.add_startup_system(asset_system.system())
.add_system(asset_events.system())
.run();
}
fn asset_system(asset_server: Res<AssetServer>) {
asset_server
.load::<Handle<RustSourceCode>, _>(PathBuf::from("assets_wasm.rs"))
.unwrap();
log::info!("hello wasm");
}
#[derive(Debug)]
pub struct RustSourceCode(pub String);
#[derive(Default)]
pub struct RustSourceCodeLoader;
impl AssetLoader<RustSourceCode> for RustSourceCodeLoader {
fn from_bytes(
&self,
_asset_path: &std::path::Path,
bytes: Vec<u8>,
) -> Result<RustSourceCode, anyhow::Error> {
Ok(RustSourceCode(String::from_utf8(bytes)?))
}
fn extensions(&self) -> &[&str] {
static EXT: &[&str] = &["rs"];
EXT
}
}
#[derive(Default)]
pub struct AssetEventsState {
reader: EventReader<AssetEvent<RustSourceCode>>,
}
pub fn asset_events(
mut state: Local<AssetEventsState>,
rust_sources: Res<Assets<RustSourceCode>>,
events: Res<Events<AssetEvent<RustSourceCode>>>,
) {
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,
};
}
}