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
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"]

View file

@ -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"

View file

@ -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<Vec<PathBuf>>,
asset_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>,
// TODO: this is a hack to enable retrieving generic AssetLoader<T>s. there must be a better way!
asset_handlers: RwLock<Vec<Arc<dyn AssetLoadRequestHandler>>>,
loaders: Vec<Resources>,
task_pool: TaskPool,
extension_to_handler_index: HashMap<String, usize>,
@ -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<TLoader, TAsset>(&mut self, loader: TLoader)
@ -173,11 +171,12 @@ impl AssetServer {
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
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))
} 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<PathBuf, AssetServerError> {
Ok(PathBuf::from("/"))
}
// 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> {
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)

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 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<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
TAsset: 'static,
{
sender: Sender<AssetResult<TAsset>>,
loader: TLoader,
#[async_trait]
pub trait AssetLoadRequestHandler: Send + Sync + 'static {
async fn handle_request(&self, load_request: &LoadRequest);
fn extensions(&self) -> &[&str];
}
impl<TLoader, TAsset> ChannelAssetHandler<TLoader, TAsset>
@ -53,12 +37,13 @@ where
}
}
#[async_trait]
impl<TLoader, TAsset> AssetLoadRequestHandler for ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset> + '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),

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-channel = "1.4.2"
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())
.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> {

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,
};
}
}