Skip to content

Commit

Permalink
wasm assets
Browse files Browse the repository at this point in the history
  • Loading branch information
mrk-its committed Sep 25, 2020
1 parent 028a22b commit 2943b68
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 37 deletions.
9 changes: 8 additions & 1 deletion Cargo.toml
Expand Up @@ -91,9 +91,11 @@ rand = "0.7.3"
serde = { version = "1", features = ["derive"] }
log = "0.4"

#wasm
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
#wasm examples
console_error_panic_hook = "0.1.6"
console_log = { version = "0.2", features = ["color"] }
anyhow = "1.0"

[[example]]
name = "hello_world"
Expand Down Expand Up @@ -281,3 +283,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"]
7 changes: 7 additions & 0 deletions crates/bevy_asset/Cargo.toml
Expand Up @@ -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"
31 changes: 19 additions & 12 deletions crates/bevy_asset/src/asset_server.rs
Expand Up @@ -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
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions 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<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
TAsset: 'static,
{
sender: Sender<AssetResult<TAsset>>,
loader: TLoader,
}
@@ -1,34 +1,18 @@
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

#[async_trait]
pub trait AssetLoadRequestHandler: Send + Sync + 'static {
fn handle_request(&self, load_request: &LoadRequest);
async 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,
}

impl<TLoader, TAsset> ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
Expand All @@ -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),
Expand Down
62 changes: 62 additions & 0 deletions 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<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()
}
}
2 changes: 2 additions & 0 deletions crates/bevy_tasks/Cargo.toml
Expand Up @@ -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"
26 changes: 26 additions & 0 deletions crates/bevy_tasks/src/single_threaded_task_pool.rs
Expand Up @@ -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 channed / 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> {
Expand Down
65 changes: 65 additions & 0 deletions 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::<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,
};
}
}

0 comments on commit 2943b68

Please sign in to comment.