From 88e8388a51f1d1764bb5b1a053693e87ba7678d6 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 9 Sep 2023 17:20:49 -0700 Subject: [PATCH 01/11] Multiple Asset Providers --- Cargo.toml | 8 +- crates/bevy_asset/Cargo.toml | 4 +- crates/bevy_asset/src/io/android.rs | 7 - crates/bevy_asset/src/io/file/file_watcher.rs | 356 ++++++---- crates/bevy_asset/src/io/file/mod.rs | 25 +- crates/bevy_asset/src/io/gated.rs | 7 - crates/bevy_asset/src/io/memory.rs | 99 ++- crates/bevy_asset/src/io/mod.rs | 15 +- crates/bevy_asset/src/io/processor_gated.rs | 62 +- crates/bevy_asset/src/io/provider.rs | 667 ++++++++++++++---- crates/bevy_asset/src/io/rust_src/mod.rs | 246 +++++++ .../src/io/rust_src/rust_src_watcher.rs | 83 +++ crates/bevy_asset/src/io/wasm.rs | 7 - crates/bevy_asset/src/lib.rs | 257 +++---- crates/bevy_asset/src/loader.rs | 34 +- crates/bevy_asset/src/path.rs | 218 +++++- crates/bevy_asset/src/processor/log.rs | 47 +- crates/bevy_asset/src/processor/mod.rs | 576 ++++++++------- crates/bevy_asset/src/processor/process.rs | 34 +- crates/bevy_asset/src/server/mod.rs | 154 ++-- crates/bevy_gltf/src/loader.rs | 2 +- crates/bevy_internal/Cargo.toml | 5 +- crates/bevy_utils/src/cow_arc.rs | 10 +- docs/cargo_features.md | 2 +- examples/asset/custom_asset_reader.rs | 28 +- examples/asset/hot_asset_reloading.rs | 5 +- examples/asset/processing/e.txt | 1 + examples/asset/processing/processing.rs | 31 +- examples/scene/scene.rs | 5 +- examples/shader/post_processing.rs | 5 +- examples/tools/scene_viewer/main.rs | 9 +- 31 files changed, 2044 insertions(+), 965 deletions(-) create mode 100644 crates/bevy_asset/src/io/rust_src/mod.rs create mode 100644 crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs create mode 100644 examples/asset/processing/e.txt diff --git a/Cargo.toml b/Cargo.toml index d06f251e497ee..0f1b483d0b57c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -242,7 +242,10 @@ shader_format_spirv = ["bevy_internal/shader_format_spirv"] webgl2 = ["bevy_internal/webgl"] # Enables watching the filesystem for Bevy Asset hot-reloading -filesystem_watcher = ["bevy_internal/filesystem_watcher"] +file_watcher = ["bevy_internal/file_watcher"] + +# Enables watching in memory asset providers for Bevy Asset hot-reloading +rust_src_watcher = ["bevy_internal/rust_src_watcher"] [dependencies] bevy_dylib = { path = "crates/bevy_dylib", version = "0.12.0-dev", default-features = false, optional = true } @@ -1053,6 +1056,7 @@ wasm = true name = "hot_asset_reloading" path = "examples/asset/hot_asset_reloading.rs" doc-scrape-examples = true +required-features = ["file_watcher"] [package.metadata.example.hot_asset_reloading] name = "Hot Reloading of Assets" @@ -1064,7 +1068,7 @@ wasm = true name = "asset_processing" path = "examples/asset/processing/processing.rs" doc-scrape-examples = true -required-features = ["filesystem_watcher"] +required-features = ["file_watcher"] [package.metadata.example.asset_processing] name = "Asset Processing" diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 05174c810cdf7..f6266a9f0d986 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -11,8 +11,10 @@ keywords = ["bevy"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -filesystem_watcher = ["notify-debouncer-full"] +file_watcher = ["notify-debouncer-full", "watch"] +rust_src_watcher = ["file_watcher"] multi-threaded = ["bevy_tasks/multi-threaded"] +watch = [] [dependencies] bevy_app = { path = "../bevy_app", version = "0.12.0-dev" } diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index 7ee1f5a1c927a..ac9916406f8fe 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -72,11 +72,4 @@ impl AssetReader for AndroidAssetReader { error!("Reading directories is not supported with the AndroidAssetReader"); Box::pin(async move { Ok(false) }) } - - fn watch_for_changes( - &self, - _event_sender: crossbeam_channel::Sender, - ) -> Option> { - None - } } diff --git a/crates/bevy_asset/src/io/file/file_watcher.rs b/crates/bevy_asset/src/io/file/file_watcher.rs index 03351278ea6fe..d7aedd4bae0e6 100644 --- a/crates/bevy_asset/src/io/file/file_watcher.rs +++ b/crates/bevy_asset/src/io/file/file_watcher.rs @@ -1,4 +1,4 @@ -use crate::io::{AssetSourceEvent, AssetWatcher}; +use crate::io::{AssetProviderEvent, AssetWatcher}; use anyhow::Result; use bevy_log::error; use bevy_utils::Duration; @@ -21,162 +21,260 @@ pub struct FileWatcher { impl FileWatcher { pub fn new( root: PathBuf, - sender: Sender, + sender: Sender, debounce_wait_time: Duration, ) -> Result { - let owned_root = root.clone(); - let mut debouncer = new_debouncer( + let root = super::get_base_path().join(root); + let watcher = new_asset_event_debouncer( + root.clone(), debounce_wait_time, - None, - move |result: DebounceEventResult| { - match result { - Ok(events) => { - for event in events.iter() { - match event.kind { - notify::EventKind::Create(CreateKind::File) => { - let (path, is_meta) = - get_asset_path(&owned_root, &event.paths[0]); + FileEventHandler { + root, + sender, + last_event: None, + }, + )?; + Ok(FileWatcher { _watcher: watcher }) + } +} + +impl AssetWatcher for FileWatcher {} + +pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) { + let relative_path = absolute_path.strip_prefix(root).unwrap(); + let is_meta = relative_path + .extension() + .map(|e| e == "meta") + .unwrap_or(false); + let asset_path = if is_meta { + relative_path.with_extension("") + } else { + relative_path.to_owned() + }; + (asset_path, is_meta) +} + +/// This is a bit more abstracted than it normally would be because we want to try _very hard_ not to duplicate this +/// event management logic across filesystem-driven [`AssetWatcher`] impls. Each operating system / platform behaves +/// a little differently and this is the result of a delicate balancing act that we should only perform once. +pub(crate) fn new_asset_event_debouncer( + root: PathBuf, + debounce_wait_time: Duration, + mut handler: impl FilesystemEventHandler, +) -> Result, notify::Error> { + let root = super::get_base_path().join(root); + let mut debouncer = new_debouncer( + debounce_wait_time, + None, + move |result: DebounceEventResult| { + match result { + Ok(events) => { + handler.begin(); + for event in events.iter() { + match event.kind { + notify::EventKind::Create(CreateKind::File) => { + if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) { if is_meta { - sender.send(AssetSourceEvent::AddedMeta(path)).unwrap(); + handler.handle( + &event.paths, + AssetProviderEvent::AddedMeta(path), + ); } else { - sender.send(AssetSourceEvent::AddedAsset(path)).unwrap(); + handler.handle( + &event.paths, + AssetProviderEvent::AddedAsset(path), + ); } } - notify::EventKind::Create(CreateKind::Folder) => { - let (path, _) = get_asset_path(&owned_root, &event.paths[0]); - sender.send(AssetSourceEvent::AddedFolder(path)).unwrap(); + } + notify::EventKind::Create(CreateKind::Folder) => { + if let Some((path, _)) = handler.get_path(&event.paths[0]) { + handler.handle( + &event.paths, + AssetProviderEvent::AddedFolder(path), + ); } - notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => { - let (path, is_meta) = - get_asset_path(&owned_root, &event.paths[0]); + } + notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => { + if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) { if is_meta { - sender.send(AssetSourceEvent::ModifiedMeta(path)).unwrap(); + handler.handle( + &event.paths, + AssetProviderEvent::ModifiedMeta(path), + ); } else { - sender.send(AssetSourceEvent::ModifiedAsset(path)).unwrap(); + handler.handle( + &event.paths, + AssetProviderEvent::ModifiedAsset(path), + ); } } - notify::EventKind::Remove(RemoveKind::Any) | - // Because this is debounced over a reasonable period of time, "From" events are assumed to be "dangling" without - // a follow up "To" event. Without debouncing, "From" -> "To" -> "Both" events are emitted for renames. - // If a From is dangling, it is assumed to be "removed" from the context of the asset system. - notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => { - let (path, is_meta) = - get_asset_path(&owned_root, &event.paths[0]); - sender - .send(AssetSourceEvent::RemovedUnknown { path, is_meta }) - .unwrap(); + } + // Because this is debounced over a reasonable period of time, Modify(ModifyKind::Name(RenameMode::From) + // events are assumed to be "dangling" without a follow up "To" event. Without debouncing, "From" -> "To" -> "Both" + // events are emitted for renames. If a From is dangling, it is assumed to be "removed" from the context of the asset + // system. + notify::EventKind::Remove(RemoveKind::Any) + | notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => { + if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) { + handler.handle( + &event.paths, + AssetProviderEvent::RemovedUnknown { path, is_meta }, + ); } - notify::EventKind::Create(CreateKind::Any) - | notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { - let (path, is_meta) = - get_asset_path(&owned_root, &event.paths[0]); - let event = if event.paths[0].is_dir() { - AssetSourceEvent::AddedFolder(path) + } + notify::EventKind::Create(CreateKind::Any) + | notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { + if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) { + let asset_event = if event.paths[0].is_dir() { + AssetProviderEvent::AddedFolder(path) } else if is_meta { - AssetSourceEvent::AddedMeta(path) + AssetProviderEvent::AddedMeta(path) } else { - AssetSourceEvent::AddedAsset(path) + AssetProviderEvent::AddedAsset(path) }; - sender.send(event).unwrap(); + handler.handle(&event.paths, asset_event); } - notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { - let (old_path, old_is_meta) = - get_asset_path(&owned_root, &event.paths[0]); - let (new_path, new_is_meta) = - get_asset_path(&owned_root, &event.paths[1]); - // only the new "real" path is considered a directory - if event.paths[1].is_dir() { - sender - .send(AssetSourceEvent::RenamedFolder { - old: old_path, - new: new_path, - }) - .unwrap(); - } else { - match (old_is_meta, new_is_meta) { - (true, true) => { - sender - .send(AssetSourceEvent::RenamedMeta { - old: old_path, - new: new_path, - }) - .unwrap(); - } - (false, false) => { - sender - .send(AssetSourceEvent::RenamedAsset { - old: old_path, - new: new_path, - }) - .unwrap(); - } - (true, false) => { - error!( - "Asset metafile {old_path:?} was changed to asset file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid" + } + notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { + let Some((old_path, old_is_meta)) = + handler.get_path(&event.paths[0]) + else { + continue; + }; + let Some((new_path, new_is_meta)) = + handler.get_path(&event.paths[1]) + else { + continue; + }; + // only the new "real" path is considered a directory + if event.paths[1].is_dir() { + handler.handle( + &event.paths, + AssetProviderEvent::RenamedFolder { + old: old_path, + new: new_path, + }, + ); + } else { + match (old_is_meta, new_is_meta) { + (true, true) => { + handler.handle( + &event.paths, + AssetProviderEvent::RenamedMeta { + old: old_path, + new: new_path, + }, ); - } - (false, true) => { - error!( - "Asset file {old_path:?} was changed to meta file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid" + } + (false, false) => { + handler.handle( + &event.paths, + AssetProviderEvent::RenamedAsset { + old: old_path, + new: new_path, + }, ); - } + } + (true, false) => { + error!( + "Asset metafile {old_path:?} was changed to asset file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid" + ); + } + (false, true) => { + error!( + "Asset file {old_path:?} was changed to meta file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid" + ); } } } - notify::EventKind::Modify(_) => { - let (path, is_meta) = - get_asset_path(&owned_root, &event.paths[0]); - if event.paths[0].is_dir() { - // modified folder means nothing in this case - } else if is_meta { - sender.send(AssetSourceEvent::ModifiedMeta(path)).unwrap(); - } else { - sender.send(AssetSourceEvent::ModifiedAsset(path)).unwrap(); - }; - } - notify::EventKind::Remove(RemoveKind::File) => { - let (path, is_meta) = - get_asset_path(&owned_root, &event.paths[0]); - if is_meta { - sender.send(AssetSourceEvent::RemovedMeta(path)).unwrap(); - } else { - sender.send(AssetSourceEvent::RemovedAsset(path)).unwrap(); - } - } - notify::EventKind::Remove(RemoveKind::Folder) => { - let (path, _) = get_asset_path(&owned_root, &event.paths[0]); - sender.send(AssetSourceEvent::RemovedFolder(path)).unwrap(); + } + notify::EventKind::Modify(_) => { + let Some((path, is_meta)) = handler.get_path(&event.paths[0]) + else { + continue; + }; + if event.paths[0].is_dir() { + // modified folder means nothing in this case + } else if is_meta { + handler.handle( + &event.paths, + AssetProviderEvent::ModifiedMeta(path), + ); + } else { + handler.handle( + &event.paths, + AssetProviderEvent::ModifiedAsset(path), + ); + }; + } + notify::EventKind::Remove(RemoveKind::File) => { + let Some((path, is_meta)) = handler.get_path(&event.paths[0]) + else { + continue; + }; + if is_meta { + handler.handle( + &event.paths, + AssetProviderEvent::RemovedMeta(path), + ); + } else { + handler.handle( + &event.paths, + AssetProviderEvent::RemovedAsset(path), + ); } - _ => {} } + notify::EventKind::Remove(RemoveKind::Folder) => { + let Some((path, _)) = handler.get_path(&event.paths[0]) else { + continue; + }; + handler + .handle(&event.paths, AssetProviderEvent::RemovedFolder(path)); + } + _ => {} } } - Err(errors) => errors.iter().for_each(|error| { - error!("Encountered a filesystem watcher error {error:?}"); - }), } - }, - )?; - debouncer.watcher().watch(&root, RecursiveMode::Recursive)?; - debouncer.cache().add_root(&root, RecursiveMode::Recursive); - Ok(Self { - _watcher: debouncer, - }) - } + Err(errors) => errors.iter().for_each(|error| { + error!("Encountered a filesystem watcher error {error:?}"); + }), + } + }, + )?; + debouncer.watcher().watch(&root, RecursiveMode::Recursive)?; + debouncer.cache().add_root(&root, RecursiveMode::Recursive); + Ok(debouncer) } -impl AssetWatcher for FileWatcher {} +pub(crate) struct FileEventHandler { + sender: crossbeam_channel::Sender, + root: PathBuf, + last_event: Option, +} -pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) { - let relative_path = absolute_path.strip_prefix(root).unwrap(); - let is_meta = relative_path - .extension() - .map(|e| e == "meta") - .unwrap_or(false); - let asset_path = if is_meta { - relative_path.with_extension("") - } else { - relative_path.to_owned() - }; - (asset_path, is_meta) +impl FilesystemEventHandler for FileEventHandler { + fn begin(&mut self) { + self.last_event = None; + } + fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)> { + Some(get_asset_path(&self.root, absolute_path)) + } + + fn handle(&mut self, _absolute_paths: &[PathBuf], event: AssetProviderEvent) { + if self.last_event.as_ref() != Some(&event) { + self.last_event = Some(event.clone()); + self.sender.send(event).unwrap(); + } + } +} + +pub(crate) trait FilesystemEventHandler: Send + Sync + 'static { + /// Called each time a set of debounced events is processed + fn begin(&mut self); + /// Returns an actual asset path (if one exists for the given `absolute_path`), as well as a [`bool`] that is + /// true if the `absolute_path` corresponds to a meta file. + fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)>; + /// Handle the given event + fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetProviderEvent); } diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 3e53981a643d2..63eabcc751a1b 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -1,9 +1,11 @@ -#[cfg(feature = "filesystem_watcher")] +#[cfg(feature = "file_watcher")] mod file_watcher; +#[cfg(feature = "file_watcher")] +pub use file_watcher::*; use crate::io::{ - get_meta_path, AssetReader, AssetReaderError, AssetWatcher, AssetWriter, AssetWriterError, - PathStream, Reader, Writer, + get_meta_path, AssetReader, AssetReaderError, AssetWriter, AssetWriterError, PathStream, + Reader, Writer, }; use anyhow::Result; use async_fs::{read_dir, File}; @@ -165,23 +167,6 @@ impl AssetReader for FileAssetReader { Ok(metadata.file_type().is_dir()) }) } - - fn watch_for_changes( - &self, - _event_sender: crossbeam_channel::Sender, - ) -> Option> { - #[cfg(feature = "filesystem_watcher")] - return Some(Box::new( - file_watcher::FileWatcher::new( - self.root_path.clone(), - _event_sender, - std::time::Duration::from_millis(300), - ) - .unwrap(), - )); - #[cfg(not(feature = "filesystem_watcher"))] - return None; - } } pub struct FileAssetWriter { diff --git a/crates/bevy_asset/src/io/gated.rs b/crates/bevy_asset/src/io/gated.rs index cc8bd9ab117ff..a493559888bf9 100644 --- a/crates/bevy_asset/src/io/gated.rs +++ b/crates/bevy_asset/src/io/gated.rs @@ -97,11 +97,4 @@ impl AssetReader for GatedReader { ) -> BoxedFuture<'a, std::result::Result> { self.reader.is_directory(path) } - - fn watch_for_changes( - &self, - event_sender: Sender, - ) -> Option> { - self.reader.watch_for_changes(event_sender) - } } diff --git a/crates/bevy_asset/src/io/memory.rs b/crates/bevy_asset/src/io/memory.rs index 9ca193e08d6ee..eb030fae4de71 100644 --- a/crates/bevy_asset/src/io/memory.rs +++ b/crates/bevy_asset/src/io/memory.rs @@ -41,25 +41,31 @@ impl Dir { self.insert_meta(path, asset.as_bytes().to_vec()); } - pub fn insert_asset(&self, path: &Path, asset: Vec) { + pub fn insert_asset(&self, path: &Path, value: impl Into) { let mut dir = self.clone(); if let Some(parent) = path.parent() { dir = self.get_or_insert_dir(parent); } dir.0.write().assets.insert( path.file_name().unwrap().to_string_lossy().to_string(), - Data(Arc::new((asset, path.to_owned()))), + Data { + value: value.into(), + path: path.to_owned(), + }, ); } - pub fn insert_meta(&self, path: &Path, asset: Vec) { + pub fn insert_meta(&self, path: &Path, value: impl Into) { let mut dir = self.clone(); if let Some(parent) = path.parent() { dir = self.get_or_insert_dir(parent); } dir.0.write().metadata.insert( path.file_name().unwrap().to_string_lossy().to_string(), - Data(Arc::new((asset, path.to_owned()))), + Data { + value: value.into(), + path: path.to_owned(), + }, ); } @@ -118,11 +124,16 @@ impl Dir { pub struct DirStream { dir: Dir, index: usize, + dir_index: usize, } impl DirStream { fn new(dir: Dir) -> Self { - Self { dir, index: 0 } + Self { + dir, + index: 0, + dir_index: 0, + } } } @@ -134,10 +145,17 @@ impl Stream for DirStream { _cx: &mut std::task::Context<'_>, ) -> Poll> { let this = self.get_mut(); - let index = this.index; - this.index += 1; let dir = this.dir.0.read(); - Poll::Ready(dir.assets.values().nth(index).map(|d| d.path().to_owned())) + + let dir_index = this.dir_index; + if let Some(dir_path) = dir.dirs.keys().nth(dir_index).map(|d| dir.path.join(d)) { + this.dir_index += 1; + Poll::Ready(Some(dir_path)) + } else { + let index = this.index; + this.index += 1; + Poll::Ready(dir.assets.values().nth(index).map(|d| d.path().to_owned())) + } } } @@ -150,14 +168,45 @@ pub struct MemoryAssetReader { /// Asset data stored in a [`Dir`]. #[derive(Clone, Debug)] -pub struct Data(Arc<(Vec, PathBuf)>); +pub struct Data { + path: PathBuf, + value: Value, +} + +/// Stores either an allocated vec of bytes or a static array of bytes. +#[derive(Clone, Debug)] +pub enum Value { + Vec(Arc>), + Static(&'static [u8]), +} impl Data { fn path(&self) -> &Path { - &self.0 .1 + &self.path } - fn data(&self) -> &[u8] { - &self.0 .0 + fn value(&self) -> &[u8] { + match &self.value { + Value::Vec(vec) => vec, + Value::Static(value) => value, + } + } +} + +impl From> for Value { + fn from(value: Vec) -> Self { + Self::Vec(Arc::new(value)) + } +} + +impl From<&'static [u8]> for Value { + fn from(value: &'static [u8]) -> Self { + Self::Static(value) + } +} + +impl From<&'static [u8; N]> for Value { + fn from(value: &'static [u8; N]) -> Self { + Self::Static(value) } } @@ -172,10 +221,11 @@ impl AsyncRead for DataReader { cx: &mut std::task::Context<'_>, buf: &mut [u8], ) -> std::task::Poll> { - if self.bytes_read >= self.data.data().len() { + if self.bytes_read >= self.data.value().len() { Poll::Ready(Ok(0)) } else { - let n = ready!(Pin::new(&mut &self.data.data()[self.bytes_read..]).poll_read(cx, buf))?; + let n = + ready!(Pin::new(&mut &self.data.value()[self.bytes_read..]).poll_read(cx, buf))?; self.bytes_read += n; Poll::Ready(Ok(n)) } @@ -197,7 +247,7 @@ impl AssetReader for MemoryAssetReader { }); reader }) - .ok_or(AssetReaderError::NotFound(PathBuf::new())) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) }) } @@ -215,7 +265,7 @@ impl AssetReader for MemoryAssetReader { }); reader }) - .ok_or(AssetReaderError::NotFound(PathBuf::new())) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) }) } @@ -230,7 +280,7 @@ impl AssetReader for MemoryAssetReader { let stream: Box = Box::new(DirStream::new(dir)); stream }) - .ok_or(AssetReaderError::NotFound(PathBuf::new())) + .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf())) }) } @@ -240,13 +290,6 @@ impl AssetReader for MemoryAssetReader { ) -> BoxedFuture<'a, std::result::Result> { Box::pin(async move { Ok(self.root.get_dir(path).is_some()) }) } - - fn watch_for_changes( - &self, - _event_sender: crossbeam_channel::Sender, - ) -> Option> { - None - } } #[cfg(test)] @@ -264,12 +307,12 @@ pub mod test { dir.insert_asset(a_path, a_data.clone()); let asset = dir.get_asset(a_path).unwrap(); assert_eq!(asset.path(), a_path); - assert_eq!(asset.data(), a_data); + assert_eq!(asset.value(), a_data); dir.insert_meta(a_path, a_meta.clone()); let meta = dir.get_metadata(a_path).unwrap(); assert_eq!(meta.path(), a_path); - assert_eq!(meta.data(), a_meta); + assert_eq!(meta.value(), a_meta); let b_path = Path::new("x/y/b.txt"); let b_data = "b".as_bytes().to_vec(); @@ -279,10 +322,10 @@ pub mod test { let asset = dir.get_asset(b_path).unwrap(); assert_eq!(asset.path(), b_path); - assert_eq!(asset.data(), b_data); + assert_eq!(asset.value(), b_data); let meta = dir.get_metadata(b_path).unwrap(); assert_eq!(meta.path(), b_path); - assert_eq!(meta.data(), b_meta); + assert_eq!(meta.value(), b_meta); } } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index a29902c5837b2..521f6b5734b38 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -5,6 +5,7 @@ pub mod file; pub mod gated; pub mod memory; pub mod processor_gated; +pub mod rust_src; #[cfg(target_arch = "wasm32")] pub mod wasm; @@ -14,7 +15,6 @@ pub use futures_lite::{AsyncReadExt, AsyncWriteExt}; pub use provider::*; use bevy_utils::BoxedFuture; -use crossbeam_channel::Sender; use futures_io::{AsyncRead, AsyncWrite}; use futures_lite::{ready, Stream}; use std::{ @@ -65,13 +65,6 @@ pub trait AssetReader: Send + Sync + 'static { path: &'a Path, ) -> BoxedFuture<'a, Result>; - /// Returns an Asset watcher that will send events on the given channel. - /// If this reader does not support watching for changes, this will return [`None`]. - fn watch_for_changes( - &self, - event_sender: Sender, - ) -> Option>; - /// Reads asset metadata bytes at the given `path` into a [`Vec`]. This is a convenience /// function that wraps [`AssetReader::read_meta`] by default. fn read_meta_bytes<'a>( @@ -179,8 +172,8 @@ pub trait AssetWriter: Send + Sync + 'static { } /// An "asset source change event" that occurs whenever asset (or asset metadata) is created/added/removed -#[derive(Clone, Debug)] -pub enum AssetSourceEvent { +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AssetProviderEvent { /// An asset at this path was added. AddedAsset(PathBuf), /// An asset at this path was modified. @@ -216,7 +209,7 @@ pub enum AssetSourceEvent { }, } -/// A handle to an "asset watcher" process, that will listen for and emit [`AssetSourceEvent`] values for as long as +/// A handle to an "asset watcher" process, that will listen for and emit [`AssetProviderEvent`] values for as long as /// [`AssetWatcher`] has not been dropped. /// /// See [`AssetReader::watch_for_changes`]. diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index ca37b47ea8029..ec7f65eda4ba5 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -1,5 +1,5 @@ use crate::{ - io::{AssetReader, AssetReaderError, PathStream, Reader}, + io::{AssetProviderId, AssetReader, AssetReaderError, PathStream, Reader}, processor::{AssetProcessorData, ProcessStatus}, AssetPath, }; @@ -16,13 +16,19 @@ use std::{path::Path, pin::Pin, sync::Arc}; /// [`AssetProcessor`]: crate::processor::AssetProcessor pub struct ProcessorGatedReader { reader: Box, + provider: AssetProviderId<'static>, processor_data: Arc, } impl ProcessorGatedReader { /// Creates a new [`ProcessorGatedReader`]. - pub fn new(reader: Box, processor_data: Arc) -> Self { + pub fn new( + provider: AssetProviderId<'static>, + reader: Box, + processor_data: Arc, + ) -> Self { Self { + provider, processor_data, reader, } @@ -32,12 +38,12 @@ impl ProcessorGatedReader { /// while it is held. async fn get_transaction_lock( &self, - path: &Path, + path: &AssetPath<'static>, ) -> Result, AssetReaderError> { let infos = self.processor_data.asset_infos.read().await; let info = infos - .get(&AssetPath::from_path(path.to_path_buf())) - .ok_or_else(|| AssetReaderError::NotFound(path.to_owned()))?; + .get(path) + .ok_or_else(|| AssetReaderError::NotFound(path.path().to_owned()))?; Ok(info.file_transaction_lock.read_arc().await) } } @@ -48,20 +54,21 @@ impl AssetReader for ProcessorGatedReader { path: &'a Path, ) -> BoxedFuture<'a, Result>, AssetReaderError>> { Box::pin(async move { - trace!("Waiting for processing to finish before reading {:?}", path); - let process_result = self.processor_data.wait_until_processed(path).await; + let asset_path = + AssetPath::from(path.to_path_buf()).with_provider(self.provider.clone()); + trace!("Waiting for processing to finish before reading {asset_path}"); + let process_result = self + .processor_data + .wait_until_processed(asset_path.clone()) + .await; match process_result { ProcessStatus::Processed => {} ProcessStatus::Failed | ProcessStatus::NonExistent => { - return Err(AssetReaderError::NotFound(path.to_owned())) + return Err(AssetReaderError::NotFound(path.to_owned())); } } - trace!( - "Processing finished with {:?}, reading {:?}", - process_result, - path - ); - let lock = self.get_transaction_lock(path).await?; + trace!("Processing finished with {asset_path}, reading {process_result:?}",); + let lock = self.get_transaction_lock(&asset_path).await?; let asset_reader = self.reader.read(path).await?; let reader: Box> = Box::new(TransactionLockedReader::new(asset_reader, lock)); @@ -74,23 +81,21 @@ impl AssetReader for ProcessorGatedReader { path: &'a Path, ) -> BoxedFuture<'a, Result>, AssetReaderError>> { Box::pin(async move { - trace!( - "Waiting for processing to finish before reading meta {:?}", - path - ); - let process_result = self.processor_data.wait_until_processed(path).await; + let asset_path = + AssetPath::from(path.to_path_buf()).with_provider(self.provider.clone()); + trace!("Waiting for processing to finish before reading meta for {asset_path}",); + let process_result = self + .processor_data + .wait_until_processed(asset_path.clone()) + .await; match process_result { ProcessStatus::Processed => {} ProcessStatus::Failed | ProcessStatus::NonExistent => { return Err(AssetReaderError::NotFound(path.to_owned())); } } - trace!( - "Processing finished with {:?}, reading meta {:?}", - process_result, - path - ); - let lock = self.get_transaction_lock(path).await?; + trace!("Processing finished with {process_result:?}, reading meta for {asset_path}",); + let lock = self.get_transaction_lock(&asset_path).await?; let meta_reader = self.reader.read_meta(path).await?; let reader: Box> = Box::new(TransactionLockedReader::new(meta_reader, lock)); Ok(reader) @@ -128,13 +133,6 @@ impl AssetReader for ProcessorGatedReader { Ok(result) }) } - - fn watch_for_changes( - &self, - event_sender: crossbeam_channel::Sender, - ) -> Option> { - self.reader.watch_for_changes(event_sender) - } } /// An [`AsyncRead`] impl that will hold its asset's transaction lock until [`TransactionLockedReader`] is dropped. diff --git a/crates/bevy_asset/src/io/provider.rs b/crates/bevy_asset/src/io/provider.rs index d41d8248ce042..9ed03cc674a31 100644 --- a/crates/bevy_asset/src/io/provider.rs +++ b/crates/bevy_asset/src/io/provider.rs @@ -1,190 +1,565 @@ -use bevy_ecs::system::Resource; -use bevy_utils::HashMap; - use crate::{ - io::{AssetReader, AssetWriter}, - AssetPlugin, + io::{ + processor_gated::ProcessorGatedReader, AssetProviderEvent, AssetReader, AssetWatcher, + AssetWriter, + }, + processor::AssetProcessorData, }; +use bevy_ecs::system::Resource; +use bevy_log::{error, warn}; +use bevy_utils::{CowArc, Duration, HashMap}; +use std::{fmt::Display, hash::Hash, sync::Arc}; +use thiserror::Error; /// A reference to an "asset provider", which maps to an [`AssetReader`] and/or [`AssetWriter`]. -#[derive(Default, Clone, Debug)] -pub enum AssetProvider { - /// The default asset provider +/// +/// * [`AssetProviderId::Default`] corresponds to "default asset paths" that don't specify a provider: `/path/to/asset.png` +/// * [`AssetProviderId::Name`] corresponds to asset paths that _do_ specify a provider: `remote://path/to/asset.png`, where `remote` is the name. +#[derive(Default, Clone, Debug, Eq)] +pub enum AssetProviderId<'a> { + /// The default asset provider. #[default] Default, - /// A custom / named asset provider - Custom(String), + /// A non-default named asset provider. + Name(CowArc<'a, str>), } -/// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`] and [`AssetWriter`] instances -/// for a given [`AssetProvider`]. -#[derive(Resource, Default)] -pub struct AssetProviders { - readers: HashMap Box + Send + Sync>>, - writers: HashMap Box + Send + Sync>>, - default_file_source: Option, - default_file_destination: Option, +impl<'a> Display for AssetProviderId<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.as_str() { + None => write!(f, "AssetProviderId::Default"), + Some(v) => write!(f, "AssetProviderId::Name({v})"), + } + } } -impl AssetProviders { - /// Inserts a new `get_reader` function with the given `provider` name. This function will be used to create new [`AssetReader`]s - /// when they are requested for the given `provider`. - pub fn insert_reader( +impl<'a> AssetProviderId<'a> { + /// Creates a new [`AssetProviderId`] + pub fn new(provider: Option>>) -> AssetProviderId<'a> { + match provider { + Some(provider) => AssetProviderId::Name(provider.into()), + None => AssetProviderId::Default, + } + } + + /// Returns [`None`] if this is [`AssetProviderId::Default`] and [`Some`] containing the + /// the name if this is [`AssetProviderId::Name`]. + pub fn as_str(&self) -> Option<&str> { + match self { + AssetProviderId::Default => None, + AssetProviderId::Name(v) => Some(v), + } + } + + /// If this is not already an owned / static id, create one. Otherwise, it will return itself (with a static lifetime). + pub fn into_owned(self) -> AssetProviderId<'static> { + match self { + AssetProviderId::Default => AssetProviderId::Default, + AssetProviderId::Name(v) => AssetProviderId::Name(v.into_owned()), + } + } + + /// Clones into an owned [`AssetProviderId<'static>`]. + /// This is equivalent to `.clone().into_owned()`. + #[inline] + pub fn clone_owned(&self) -> AssetProviderId<'static> { + self.clone().into_owned() + } +} + +impl From<&'static str> for AssetProviderId<'static> { + fn from(value: &'static str) -> Self { + AssetProviderId::Name(value.into()) + } +} + +impl<'a, 'b> From<&'a AssetProviderId<'b>> for AssetProviderId<'b> { + fn from(value: &'a AssetProviderId<'b>) -> Self { + value.clone() + } +} + +impl From> for AssetProviderId<'static> { + fn from(value: Option<&'static str>) -> Self { + match value { + Some(value) => AssetProviderId::Name(value.into()), + None => AssetProviderId::Default, + } + } +} + +impl From for AssetProviderId<'static> { + fn from(value: String) -> Self { + AssetProviderId::Name(value.into()) + } +} + +impl<'a> Hash for AssetProviderId<'a> { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} + +impl<'a> PartialEq for AssetProviderId<'a> { + fn eq(&self, other: &Self) -> bool { + self.as_str().eq(&other.as_str()) + } +} + +/// Metadata about an "asset provider", such as how to construct the [`AssetReader`] and [`AssetWriter`] for the provider, +/// and whether or not the provider is processed. +#[derive(Default)] +pub struct AssetProviderBuilder { + pub reader: Option Box + Send + Sync>>, + pub writer: Option Option> + Send + Sync>>, + pub watcher: Option< + Box< + dyn FnMut( + crossbeam_channel::Sender, + ) -> Option> + + Send + + Sync, + >, + >, + pub processed_reader: Option Box + Send + Sync>>, + pub processed_writer: Option Option> + Send + Sync>>, + pub processed_watcher: Option< + Box< + dyn FnMut( + crossbeam_channel::Sender, + ) -> Option> + + Send + + Sync, + >, + >, +} + +impl AssetProviderBuilder { + /// Builds a new [`AssetProvider`] with the given `id`. If `watch` is true, the unprocessed provider will watch for changes. + /// If `watch_processed` is true, the processed provider will watch for changes. + pub fn build( &mut self, - provider: &str, - get_reader: impl FnMut() -> Box + Send + Sync + 'static, - ) { - self.readers - .insert(provider.to_string(), Box::new(get_reader)); + id: AssetProviderId<'static>, + watch: bool, + watch_processed: bool, + ) -> Option { + let reader = (self.reader.as_mut()?)(); + let writer = self.writer.as_mut().map(|w| match (w)() { + Some(w) => w, + None => panic!("{} does not have an AssetWriter configured. Note that Web and Android do not currently support writing assets.", id), + }); + let processed_writer = self.processed_writer.as_mut().map(|w| match (w)() { + Some(w) => w, + None => panic!("{} does not have a processed AssetWriter configured. Note that Web and Android do not currently support writing assets.", id), + }); + let mut provider = AssetProvider { + id: id.clone(), + reader, + writer, + processed_reader: self.processed_reader.as_mut().map(|r| (r)()), + processed_writer, + event_receiver: None, + watcher: None, + processed_event_receiver: None, + processed_watcher: None, + }; + + if watch { + let (sender, receiver) = crossbeam_channel::unbounded(); + match self.watcher.as_mut().and_then(|w|(w)(sender)) { + Some(w) => { + provider.watcher = Some(w); + provider.event_receiver = Some(receiver); + }, + None => warn!("{id} does not have an AssetWatcher configured. Consider enabling the `file_watcher` feature. Note that Web and Android do not currently support watching assets."), + } + } + + if watch_processed { + let (sender, receiver) = crossbeam_channel::unbounded(); + match self.processed_watcher.as_mut().and_then(|w|(w)(sender)) { + Some(w) => { + provider.processed_watcher = Some(w); + provider.processed_event_receiver = Some(receiver); + }, + None => warn!("{id} does not have a processed AssetWatcher configured. Consider enabling the `file_watcher` feature. Note that Web and Android do not currently support watching assets."), + } + } + Some(provider) } - /// Inserts a new `get_reader` function with the given `provider` name. This function will be used to create new [`AssetReader`]s - /// when they are requested for the given `provider`. + + /// Will use the given `reader` function to construct unprocessed [`AssetReader`] instances. pub fn with_reader( mut self, - provider: &str, - get_reader: impl FnMut() -> Box + Send + Sync + 'static, + reader: impl FnMut() -> Box + Send + Sync + 'static, ) -> Self { - self.insert_reader(provider, get_reader); + self.reader = Some(Box::new(reader)); self } - /// Inserts a new `get_writer` function with the given `provider` name. This function will be used to create new [`AssetWriter`]s - /// when they are requested for the given `provider`. - pub fn insert_writer( - &mut self, - provider: &str, - get_writer: impl FnMut() -> Box + Send + Sync + 'static, - ) { - self.writers - .insert(provider.to_string(), Box::new(get_writer)); - } - /// Inserts a new `get_writer` function with the given `provider` name. This function will be used to create new [`AssetWriter`]s - /// when they are requested for the given `provider`. + + /// Will use the given `writer` function to construct unprocessed [`AssetWriter`] instances. pub fn with_writer( mut self, - provider: &str, - get_writer: impl FnMut() -> Box + Send + Sync + 'static, + writer: impl FnMut() -> Option> + Send + Sync + 'static, ) -> Self { - self.insert_writer(provider, get_writer); + self.writer = Some(Box::new(writer)); self } - /// Returns the default "asset source" path for the [`FileAssetReader`] and [`FileAssetWriter`]. - /// - /// [`FileAssetReader`]: crate::io::file::FileAssetReader - /// [`FileAssetWriter`]: crate::io::file::FileAssetWriter - pub fn default_file_source(&self) -> &str { - self.default_file_source - .as_deref() - .unwrap_or(AssetPlugin::DEFAULT_FILE_SOURCE) + + /// Will use the given `watcher` function to construct unprocessed [`AssetWatcher`] instances. + pub fn with_watcher( + mut self, + watcher: impl FnMut(crossbeam_channel::Sender) -> Option> + + Send + + Sync + + 'static, + ) -> Self { + self.watcher = Some(Box::new(watcher)); + self } - /// Sets the default "asset source" path for the [`FileAssetReader`] and [`FileAssetWriter`]. - /// - /// [`FileAssetReader`]: crate::io::file::FileAssetReader - /// [`FileAssetWriter`]: crate::io::file::FileAssetWriter - pub fn with_default_file_source(mut self, path: String) -> Self { - self.default_file_source = Some(path); + /// Will use the given `reader` function to construct processed [`AssetReader`] instances. + pub fn with_processed_reader( + mut self, + reader: impl FnMut() -> Box + Send + Sync + 'static, + ) -> Self { + self.processed_reader = Some(Box::new(reader)); self } - /// Sets the default "asset destination" path for the [`FileAssetReader`] and [`FileAssetWriter`]. - /// - /// [`FileAssetReader`]: crate::io::file::FileAssetReader - /// [`FileAssetWriter`]: crate::io::file::FileAssetWriter - pub fn with_default_file_destination(mut self, path: String) -> Self { - self.default_file_destination = Some(path); + /// Will use the given `writer` function to construct processed [`AssetWriter`] instances. + pub fn with_processed_writer( + mut self, + writer: impl FnMut() -> Option> + Send + Sync + 'static, + ) -> Self { + self.processed_writer = Some(Box::new(writer)); self } - /// Returns the default "asset destination" path for the [`FileAssetReader`] and [`FileAssetWriter`]. - /// - /// [`FileAssetReader`]: crate::io::file::FileAssetReader - /// [`FileAssetWriter`]: crate::io::file::FileAssetWriter - pub fn default_file_destination(&self) -> &str { - self.default_file_destination - .as_deref() - .unwrap_or(AssetPlugin::DEFAULT_FILE_DESTINATION) + /// Will use the given `watcher` function to construct processed [`AssetWatcher`] instances. + pub fn with_processed_watcher( + mut self, + watcher: impl FnMut(crossbeam_channel::Sender) -> Option> + + Send + + Sync + + 'static, + ) -> Self { + self.processed_watcher = Some(Box::new(watcher)); + self } +} - /// Returns a new "source" [`AssetReader`] for the given [`AssetProvider`]. - pub fn get_source_reader(&mut self, provider: &AssetProvider) -> Box { - match provider { - AssetProvider::Default => { - #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - let reader = super::file::FileAssetReader::new(self.default_file_source()); - #[cfg(target_arch = "wasm32")] - let reader = super::wasm::HttpWasmAssetReader::new(self.default_file_source()); - #[cfg(target_os = "android")] - let reader = super::android::AndroidAssetReader; - Box::new(reader) +/// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`] and [`AssetWriter`] instances +/// for a given asset provider. +#[derive(Resource, Default)] +pub struct AssetProviderBuilders { + providers: HashMap, AssetProviderBuilder>, + default: Option, +} + +impl AssetProviderBuilders { + /// Inserts a new builder with the given `id` + pub fn insert( + &mut self, + id: impl Into>, + provider: AssetProviderBuilder, + ) { + match id.into() { + AssetProviderId::Default => { + self.default = Some(provider); } - AssetProvider::Custom(provider) => { - let get_reader = self - .readers - .get_mut(provider) - .unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider)); - (get_reader)() + AssetProviderId::Name(name) => { + self.providers.insert(name, provider); } } } - /// Returns a new "destination" [`AssetReader`] for the given [`AssetProvider`]. - pub fn get_destination_reader(&mut self, provider: &AssetProvider) -> Box { - match provider { - AssetProvider::Default => { - #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - let reader = super::file::FileAssetReader::new(self.default_file_destination()); - #[cfg(target_arch = "wasm32")] - let reader = super::wasm::HttpWasmAssetReader::new(self.default_file_destination()); - #[cfg(target_os = "android")] - let reader = super::android::AndroidAssetReader; - Box::new(reader) - } - AssetProvider::Custom(provider) => { - let get_reader = self - .readers - .get_mut(provider) - .unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider)); - (get_reader)() - } + + /// Gets a mutable builder with the given `id`, if it exists. + pub fn get_mut<'a, 'b>( + &'a mut self, + id: impl Into>, + ) -> Option<&'a mut AssetProviderBuilder> { + match id.into() { + AssetProviderId::Default => self.default.as_mut(), + AssetProviderId::Name(name) => self.providers.get_mut(&name.into_owned()), } } - /// Returns a new "source" [`AssetWriter`] for the given [`AssetProvider`]. - pub fn get_source_writer(&mut self, provider: &AssetProvider) -> Box { - match provider { - AssetProvider::Default => { - #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - return Box::new(super::file::FileAssetWriter::new( - self.default_file_source(), - )); - #[cfg(any(target_arch = "wasm32", target_os = "android"))] - panic!("Writing assets isn't supported on this platform yet"); - } - AssetProvider::Custom(provider) => { - let get_writer = self - .writers - .get_mut(provider) - .unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider)); - (get_writer)() + + /// Builds an new [`AssetProviders`] collection. If `watch` is true, the unprocessed providers will watch for changes. + /// If `watch_processed` is true, the processed providers will watch for changes. + pub fn build_providers(&mut self, watch: bool, watch_processed: bool) -> AssetProviders { + let mut providers = HashMap::new(); + for (id, provider) in &mut self.providers { + if let Some(data) = provider.build( + AssetProviderId::Name(id.clone_owned()), + watch, + watch_processed, + ) { + providers.insert(id.clone_owned(), data); } } + + AssetProviders { + providers, + default: self + .default + .as_mut() + .and_then(|p| p.build(AssetProviderId::Default, watch, watch_processed)) + .expect(MISSING_DEFAULT_PROVIDER), + } } - /// Returns a new "destination" [`AssetWriter`] for the given [`AssetProvider`]. - pub fn get_destination_writer(&mut self, provider: &AssetProvider) -> Box { - match provider { - AssetProvider::Default => { - #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - return Box::new(super::file::FileAssetWriter::new( - self.default_file_destination(), - )); - #[cfg(any(target_arch = "wasm32", target_os = "android"))] - panic!("Writing assets isn't supported on this platform yet"); - } - AssetProvider::Custom(provider) => { - let get_writer = self - .writers - .get_mut(provider) - .unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider)); - (get_writer)() - } + + /// Initializes the default [`AssetProviderBuilder`] if it has not already been set. + pub fn init_default_providers(&mut self, path: &str, processed_path: &str) { + self.default.get_or_insert_with(|| { + AssetProviderBuilder::default() + .with_reader(AssetProvider::get_default_reader(path.to_string())) + .with_writer(AssetProvider::get_default_writer(path.to_string())) + .with_watcher(AssetProvider::get_default_watcher( + path.to_string(), + Duration::from_millis(300), + )) + .with_processed_reader(AssetProvider::get_default_reader( + processed_path.to_string(), + )) + .with_processed_writer(AssetProvider::get_default_writer( + processed_path.to_string(), + )) + .with_processed_watcher(AssetProvider::get_default_watcher( + processed_path.to_string(), + Duration::from_millis(300), + )) + }); + } +} + +/// A collection of unprocessed and processed [`AssetReader`], [`AssetWriter`], and [`AssetWatcher`] instances +/// for a specific asset provider, identified by an [`AssetProviderId`]. +pub struct AssetProvider { + id: AssetProviderId<'static>, + reader: Box, + writer: Option>, + processed_reader: Option>, + processed_writer: Option>, + watcher: Option>, + processed_watcher: Option>, + event_receiver: Option>, + processed_event_receiver: Option>, +} + +impl AssetProvider { + /// Starts building a new [`AssetProvider`]. + pub fn build() -> AssetProviderBuilder { + AssetProviderBuilder::default() + } + + /// Returns this provider's id. + #[inline] + pub fn id(&self) -> AssetProviderId<'static> { + self.id.clone() + } + + /// Return's this provider's unprocessed [`AssetReader`]. + #[inline] + pub fn reader(&self) -> &dyn AssetReader { + &*self.reader + } + + /// Return's this provider's unprocessed [`AssetWriter`], if it exists. + #[inline] + pub fn writer(&self) -> Result<&dyn AssetWriter, MissingAssetWriterError> { + self.writer + .as_deref() + .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned())) + } + + /// Return's this provider's processed [`AssetReader`], if it exists. + #[inline] + pub fn processed_reader(&self) -> Result<&dyn AssetReader, MissingProcessedAssetReaderError> { + self.processed_reader + .as_deref() + .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned())) + } + + /// Return's this provider's processed [`AssetWriter`], if it exists. + #[inline] + pub fn processed_writer(&self) -> Result<&dyn AssetWriter, MissingProcessedAssetWriterError> { + self.processed_writer + .as_deref() + .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned())) + } + + /// Return's this provider's unprocessed event receiver, if the provider is currently watching for changes. + #[inline] + pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver> { + self.event_receiver.as_ref() + } + + /// Return's this provider's processed event receiver, if the provider is currently watching for changes. + #[inline] + pub fn processed_event_receiver( + &self, + ) -> Option<&crossbeam_channel::Receiver> { + self.processed_event_receiver.as_ref() + } + + /// Returns true if the assets in this provider should be processed. + #[inline] + pub fn should_process(&self) -> bool { + self.processed_writer.is_some() + } + + /// Returns a builder function for this platform's default [`AssetReader`]. `path` is the relative path to + /// the asset root. + pub fn get_default_reader(path: String) -> impl FnMut() -> Box + Send + Sync { + move || { + #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] + return Box::new(super::file::FileAssetReader::new(&path)); + #[cfg(target_arch = "wasm32")] + return Box::new(super::wasm::HttpWasmAssetReader::new(&path)); + #[cfg(target_os = "android")] + return Box::new(super::android::AndroidAssetReader); + } + } + + /// Returns a builder function for this platform's default [`AssetWriter`]. `path` is the relative path to + /// the asset root. This will return [`None`] if this platform does not support writing assets by default. + pub fn get_default_writer( + path: String, + ) -> impl FnMut() -> Option> + Send + Sync { + move || { + #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] + return Some(Box::new(super::file::FileAssetWriter::new(&path))); + #[cfg(any(target_arch = "wasm32", target_os = "android"))] + return None; + } + } + + /// Returns a builder function for this platform's default [`AssetWatcher`]. `path` is the relative path to + /// the asset root. This will return [`None`] if this platform does not support watching assets by default. + /// `file_debounce_time` is the amount of time to wait (and debounce duplicate events) before returning an event. + /// Higher durations reduce duplicates but increase the amount of time before a change event is processed. If the + /// duration is set too low, some systems might surface events _before_ their filesystem has the changes. + #[allow(unused)] + pub fn get_default_watcher( + path: String, + file_debounce_wait_time: Duration, + ) -> impl FnMut(crossbeam_channel::Sender) -> Option> + + Send + + Sync { + move |sender: crossbeam_channel::Sender| { + #[cfg(all( + feature = "file_watcher", + not(target_arch = "wasm32"), + not(target_os = "android") + ))] + return Some(Box::new( + super::file::FileWatcher::new( + std::path::PathBuf::from(path.clone()), + sender, + file_debounce_wait_time, + ) + .unwrap(), + )); + #[cfg(any( + not(feature = "file_watcher"), + target_arch = "wasm32", + target_os = "android" + ))] + return None; + } + } + + /// This will cause processed [`AssetReader`] futures (such as [`AssetReader::read`]) to wait until + /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset. + pub fn gate_on_processor(&mut self, processor_data: Arc) { + if let Some(reader) = self.processed_reader.take() { + self.processed_reader = Some(Box::new(ProcessorGatedReader::new( + self.id(), + reader, + processor_data, + ))); } } } + +/// A collection of [`AssetProviders`]. +pub struct AssetProviders { + providers: HashMap, AssetProvider>, + default: AssetProvider, +} + +impl AssetProviders { + /// Gets the [`AssetProvider`] with the given `id`, if it exists. + pub fn get<'a, 'b>( + &'a self, + id: impl Into>, + ) -> Result<&'a AssetProvider, MissingAssetProviderError> { + match id.into().into_owned() { + AssetProviderId::Default => Ok(&self.default), + AssetProviderId::Name(name) => self + .providers + .get(&name) + .ok_or_else(|| MissingAssetProviderError(AssetProviderId::Name(name))), + } + } + + /// Iterates all asset providers in the collection (including the default provider). + pub fn iter(&self) -> impl Iterator { + self.providers.values().chain(Some(&self.default)) + } + + /// Mutably iterates all asset providers in the collection (including the default provider). + pub fn iter_mut(&mut self) -> impl Iterator { + self.providers.values_mut().chain(Some(&mut self.default)) + } + + /// Iterates all processed asset providers in the collection (including the default provider). + pub fn iter_processed(&self) -> impl Iterator { + self.iter().filter(|p| p.should_process()) + } + + /// Mutably iterates all processed asset providers in the collection (including the default provider). + pub fn iter_processed_mut(&mut self) -> impl Iterator { + self.iter_mut().filter(|p| p.should_process()) + } + + /// Iterates over the [`AssetProviderId`] of every [`AssetProvider`] in the collection (including the default provider). + pub fn provider_ids(&self) -> impl Iterator> + '_ { + self.providers + .keys() + .map(|k| AssetProviderId::Name(k.clone_owned())) + .chain(Some(AssetProviderId::Default)) + } + + /// This will cause processed [`AssetReader`] futures (such as [`AssetReader::read`]) to wait until + /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset. + pub fn gate_on_processor(&mut self, processor_data: Arc) { + for provider in self.iter_processed_mut() { + provider.gate_on_processor(processor_data.clone()); + } + } +} + +/// An error returned when an [`AssetProvider`] does not exist for a given id. +#[derive(Error, Debug)] +#[error("Asset Provider '{0}' does not exist")] +pub struct MissingAssetProviderError(AssetProviderId<'static>); + +/// An error returned when an [`AssetWriter`] does not exist for a given id. +#[derive(Error, Debug)] +#[error("Asset Provider '{0}' does not have an AssetWriter.")] +pub struct MissingAssetWriterError(AssetProviderId<'static>); + +/// An error returned when a processed [`AssetReader`] does not exist for a given id. +#[derive(Error, Debug)] +#[error("Asset Provider '{0}' does not have a processed AssetReader.")] +pub struct MissingProcessedAssetReaderError(AssetProviderId<'static>); + +/// An error returned when a processed [`AssetWriter`] does not exist for a given id. +#[derive(Error, Debug)] +#[error("Asset Provider '{0}' does not have a processed AssetWriter.")] +pub struct MissingProcessedAssetWriterError(AssetProviderId<'static>); + +const MISSING_DEFAULT_PROVIDER: &str = + "A default AssetProvider is required. Add one to `AssetProviderBuilders`"; diff --git a/crates/bevy_asset/src/io/rust_src/mod.rs b/crates/bevy_asset/src/io/rust_src/mod.rs new file mode 100644 index 0000000000000..c2271e44b7b7e --- /dev/null +++ b/crates/bevy_asset/src/io/rust_src/mod.rs @@ -0,0 +1,246 @@ +#[cfg(feature = "rust_src_watcher")] +mod rust_src_watcher; + +#[cfg(feature = "rust_src_watcher")] +pub use rust_src_watcher::*; + +use crate::io::{ + memory::{Dir, MemoryAssetReader, Value}, + AssetProvider, AssetProviderBuilders, +}; +use bevy_ecs::system::Resource; +use std::path::{Path, PathBuf}; + +pub const RUST_SRC: &str = "rust_src"; + +/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended +/// to be shared with a [`MemoryAssetReader`]. +/// Generally this should not be interacted with directly. The [`rust_src_asset`] will populate this. +#[derive(Resource, Default)] +pub struct RustSrcRegistry { + dir: Dir, + #[cfg(feature = "rust_src_watcher")] + root_paths: std::sync::Arc< + parking_lot::RwLock>, + >, +} + +impl RustSrcRegistry { + /// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of + /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `rust_src` + /// asset provider. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` + /// or a [`Vec`]. + #[allow(unused)] + pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into) { + #[cfg(feature = "rust_src_watcher")] + self.root_paths + .write() + .insert(full_path.to_owned(), asset_path.to_owned()); + self.dir.insert_asset(asset_path, value); + } + + /// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of + /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `rust_src` + /// asset provider. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` + /// or a [`Vec`]. + #[allow(unused)] + pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into) { + #[cfg(feature = "rust_src_watcher")] + self.root_paths + .write() + .insert(full_path.to_owned(), asset_path.to_owned()); + self.dir.insert_meta(asset_path, value); + } + + /// Registers a `rust_src` [`AssetProvider`] that uses this [`RustSrcRegistry`]. + // NOTE: unused_mut because rust_src_watcher feature is the only mutable consumer of `let mut provider` + #[allow(unused_mut)] + pub fn register_provider(&self, providers: &mut AssetProviderBuilders) { + let dir = self.dir.clone(); + let processed_dir = self.dir.clone(); + let mut provider = AssetProvider::build() + .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() })) + .with_processed_reader(move || { + Box::new(MemoryAssetReader { + root: processed_dir.clone(), + }) + }); + + #[cfg(feature = "rust_src_watcher")] + { + let root_paths = self.root_paths.clone(); + let dir = self.dir.clone(); + let processed_root_paths = self.root_paths.clone(); + let processd_dir = self.dir.clone(); + provider = provider + .with_watcher(move |sender| { + Some(Box::new(RustSrcWatcher::new( + dir.clone(), + root_paths.clone(), + sender, + std::time::Duration::from_millis(300), + ))) + }) + .with_processed_watcher(move |sender| { + Some(Box::new(RustSrcWatcher::new( + processd_dir.clone(), + processed_root_paths.clone(), + sender, + std::time::Duration::from_millis(300), + ))) + }); + } + providers.insert(RUST_SRC, provider); + } +} + +/// Returns the [`Path`] for a given `rust_src` asset. +/// This is used internally by [`rust_src_asset`] and can be used to get a [`Path`] +/// that matches the [`AssetPath`](crate::AssetPath) used by that asset. +#[macro_export] +macro_rules! rust_src_path { + ($path_str: expr) => {{ + rust_src_path!("/src/", $path_str) + }}; + + ($source_path: expr, $path_str: expr) => {{ + let crate_name = module_path!().split(':').next().unwrap(); + let after_src = file!().split($source_path).nth(1).unwrap(); + let file_path = std::path::Path::new(after_src) + .parent() + .unwrap() + .join($path_str); + std::path::Path::new(crate_name).join(file_path) + }}; +} + +/// Creates a new `rust_src` asset by embedding the bytes of the given path into the current binary +/// and registering those bytes with the `rust_src` [`AssetProvider`]. +/// +/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second. +/// +/// By default this will generate an [`AssetPath`] using the following rules: +/// +/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point. +/// 2. Re-add the current `$crate_name` to the front of the path +/// +/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin) +/// that renders fancy rocks for scenes. +/// +/// * `bevy_rock` +/// * `src` +/// * `render` +/// * `rock.wgsl` +/// * `mod.rs` +/// * `lib.rs` +/// * `Cargo.toml` +/// +/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following +/// in `bevy_rock/src/render/mod.rs`: +/// +/// `rust_src_asset!(app, "rock.wgsl")` +/// +/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path: +/// +/// ```no_run +/// # use bevy_asset::{Asset, AssetServer}; +/// # use bevy_reflect::TypePath; +/// # let asset_server: AssetServer = panic!(); +/// #[derive(Asset, TypePath)] +/// # struct Shader; +/// let shader = asset_server.load::("rust_src://bevy_rock/render/rock.wgsl"); +/// ``` +/// +/// Some things to note in the path: +/// 1. The non-default `rust_src:://` [`AssetProvider`] +/// 2. `src` is trimmed from the path +/// +/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in +/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`rust_src_asset`] searches for the +/// _first instance_ of `bevy_rock/src` in the path. +/// +/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples), +/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`rust_src_asset`]: +/// +/// `rust_src_asset!(app, "/examples/rock_stuff/", "rock.wgsl")` +/// +/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are +/// equivalent: +/// +/// `rust_src_asset!(app, "rock.wgsl")` +/// `rust_src_asset!(app, "/src/", "rock.wgsl")` +/// +/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes. +/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't +/// available for some reason, you can use the [`rust_src_path`] macro to debug. +/// +/// Hot-reloading `rust_src` assets is supported. Just enable the `rust_src_watcher` cargo feature. +/// +/// [`AssetPath`]: crate::AssetPath +#[macro_export] +macro_rules! rust_src_asset { + ($app: ident, $path: expr) => {{ + rust_src_asset!($app, "/src/", $path) + }}; + + ($app: ident, $source_path: expr, $path: expr) => {{ + let mut rust_src = $app + .world + .resource_mut::<$crate::io::rust_src::RustSrcRegistry>(); + let path = $crate::rust_src_path!($source_path, $path); + #[cfg(feature = "rust_src_watcher")] + let full_path = std::path::Path::new(file!()).parent().unwrap().join($path); + #[cfg(not(feature = "rust_src_watcher"))] + let full_path = std::path::PathBuf::new(); + rust_src.insert_asset(full_path, &path, include_bytes!($path)); + }}; +} + +/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle. +#[macro_export] +macro_rules! load_internal_asset { + ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ + let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); + assets.insert($handle, ($loader)( + include_str!($path_str), + std::path::Path::new(file!()) + .parent() + .unwrap() + .join($path_str) + .to_string_lossy() + )); + }}; + // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded + ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{ + let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); + assets.insert($handle, ($loader)( + include_str!($path_str), + std::path::Path::new(file!()) + .parent() + .unwrap() + .join($path_str) + .to_string_lossy(), + $($param),+ + )); + }}; +} + +/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle. +#[macro_export] +macro_rules! load_internal_binary_asset { + ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ + let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); + assets.insert( + $handle, + ($loader)( + include_bytes!($path_str).as_ref(), + std::path::Path::new(file!()) + .parent() + .unwrap() + .join($path_str) + .to_string_lossy() + .into(), + ), + ); + }}; +} diff --git a/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs b/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs new file mode 100644 index 0000000000000..a58e181e2631d --- /dev/null +++ b/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs @@ -0,0 +1,83 @@ +use crate::io::{ + file::{get_asset_path, get_base_path, new_asset_event_debouncer, FilesystemEventHandler}, + memory::Dir, + AssetProviderEvent, AssetWatcher, +}; +use bevy_log::warn; +use bevy_utils::{Duration, HashMap}; +use notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap}; +use parking_lot::RwLock; +use std::{ + fs::File, + io::{BufReader, Read}, + path::{Path, PathBuf}, + sync::Arc, +}; + +pub struct RustSrcWatcher { + _watcher: Debouncer, +} + +impl RustSrcWatcher { + pub fn new( + dir: Dir, + root_paths: Arc>>, + sender: crossbeam_channel::Sender, + debounce_wait_time: Duration, + ) -> Self { + let root = get_base_path(); + let handler = RustSrcEventHandler { + dir, + root: root.clone(), + sender, + root_paths, + last_event: None, + }; + let watcher = new_asset_event_debouncer(root, debounce_wait_time, handler).unwrap(); + Self { _watcher: watcher } + } +} + +impl AssetWatcher for RustSrcWatcher {} + +/// A [`FilesystemEventHandler`] that uses [`RustSrcRegistry`](crate::io::rust_src::RustSrcRegistry) to hot-reload +/// binary-embedded Rust source files. This will read the contents of changed files from the file system and overwrite +/// the initial static bytes from the file embedded in the binary. +pub(crate) struct RustSrcEventHandler { + sender: crossbeam_channel::Sender, + root_paths: Arc>>, + root: PathBuf, + dir: Dir, + last_event: Option, +} +impl FilesystemEventHandler for RustSrcEventHandler { + fn begin(&mut self) { + self.last_event = None; + } + fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)> { + let (local_path, is_meta) = get_asset_path(&self.root, absolute_path); + let final_path = self.root_paths.read().get(&local_path)?.clone(); + if is_meta { + warn!("Meta file asset hot-reloading is not supported yet: {final_path:?}"); + } + Some((final_path, false)) + } + + fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetProviderEvent) { + if self.last_event.as_ref() != Some(&event) { + if let AssetProviderEvent::ModifiedAsset(path) = &event { + if let Ok(file) = File::open(&absolute_paths[0]) { + let mut reader = BufReader::new(file); + let mut buffer = Vec::new(); + + // Read file into vector. + if reader.read_to_end(&mut buffer).is_ok() { + self.dir.insert_asset(path, buffer); + } + } + } + self.last_event = Some(event.clone()); + self.sender.send(event).unwrap(); + } + } +} diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index a90a5a0569379..d14c046c1dabb 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -100,11 +100,4 @@ impl AssetReader for HttpWasmAssetReader { error!("Reading directories is not supported with the HttpWasmAssetReader"); Box::pin(async move { Ok(false) }) } - - fn watch_for_changes( - &self, - _event_sender: crossbeam_channel::Sender, - ) -> Option> { - None - } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index fcf4b09f19ba3..b5534d4a2842e 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -6,7 +6,7 @@ pub mod saver; pub mod prelude { #[doc(hidden)] pub use crate::{ - Asset, AssetApp, AssetEvent, AssetId, AssetPlugin, AssetServer, Assets, Handle, + Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets, Handle, UntypedHandle, }; } @@ -37,7 +37,7 @@ pub use anyhow; pub use bevy_utils::BoxedFuture; use crate::{ - io::{processor_gated::ProcessorGatedReader, AssetProvider, AssetProviders}, + io::{rust_src::RustSrcRegistry, AssetProviderBuilder, AssetProviderBuilders, AssetProviderId}, processor::{AssetProcessor, Process}, }; use bevy_app::{App, First, MainScheduleOrder, Plugin, PostUpdate, Startup}; @@ -52,138 +52,112 @@ use std::{any::TypeId, sync::Arc}; /// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetProvider`], /// which can be something like a filesystem, a network, etc. /// -/// Supports flexible "modes", such as [`AssetPlugin::Processed`] and -/// [`AssetPlugin::Unprocessed`] that enable using the asset workflow that best suits your project. -pub enum AssetPlugin { +/// Supports flexible "modes", such as [`AssetMode::Processed`] and +/// [`AssetMode::Unprocessed`] that enable using the asset workflow that best suits your project. +pub struct AssetPlugin { + /// The default file path to use (relative to the project root) for unprocessed assets. + pub file_path: String, + /// The default file path to use (relative to the project root) for processed assets. + pub processed_file_path: String, + /// If set, will override the default "watch for changes" setting. By default "watch for changes" will be `false` unless + /// the `watch` cargo feature is set. `watch` can be enabled manually, or it will be automatically enabled if a specific watcher + /// like `file_watcher` is enabled. + /// + /// Most use cases should leave this set to [`None`] and enable a specific watcher feature such as `file_watcher` to enable + /// watching for dev-scenarios. + pub watch_for_changes_override: Option, + /// The [`AssetMode`] to use for this server. + pub mode: AssetMode, +} + +pub enum AssetMode { /// Loads assets without any "preprocessing" from the configured asset `source` (defaults to the `assets` folder). - Unprocessed { - source: AssetProvider, - watch_for_changes: bool, - }, + Unprocessed, /// Loads "processed" assets from a given `destination` source (defaults to the `imported_assets/Default` folder). This should /// generally only be used when distributing apps. Use [`AssetPlugin::ProcessedDev`] to develop apps that process assets, /// then switch to [`AssetPlugin::Processed`] when deploying the apps. - Processed { - destination: AssetProvider, - watch_for_changes: bool, - }, + Processed, /// Starts an [`AssetProcessor`] in the background that reads assets from the `source` provider (defaults to the `assets` folder), /// processes them according to their [`AssetMeta`], and writes them to the `destination` provider (defaults to the `imported_assets/Default` folder). /// /// By default this will hot reload changes to the `source` provider, resulting in reprocessing the asset and reloading it in the [`App`]. /// /// [`AssetMeta`]: crate::meta::AssetMeta - ProcessedDev { - source: AssetProvider, - destination: AssetProvider, - watch_for_changes: bool, - }, + ProcessedDev, } impl Default for AssetPlugin { fn default() -> Self { - Self::unprocessed() + Self { + mode: AssetMode::Unprocessed, + file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(), + processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(), + watch_for_changes_override: None, + } } } impl AssetPlugin { - const DEFAULT_FILE_SOURCE: &'static str = "assets"; + const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets"; /// NOTE: this is in the Default sub-folder to make this forward compatible with "import profiles" /// and to allow us to put the "processor transaction log" at `imported_assets/log` - const DEFAULT_FILE_DESTINATION: &'static str = "imported_assets/Default"; - - /// Returns the default [`AssetPlugin::Processed`] configuration - pub fn processed() -> Self { - Self::Processed { - destination: Default::default(), - watch_for_changes: false, - } - } - - /// Returns the default [`AssetPlugin::ProcessedDev`] configuration - pub fn processed_dev() -> Self { - Self::ProcessedDev { - source: Default::default(), - destination: Default::default(), - watch_for_changes: true, - } - } - - /// Returns the default [`AssetPlugin::Unprocessed`] configuration - pub fn unprocessed() -> Self { - Self::Unprocessed { - source: Default::default(), - watch_for_changes: false, - } - } - - /// Enables watching for changes, which will hot-reload assets when they change. - pub fn watch_for_changes(mut self) -> Self { - match &mut self { - AssetPlugin::Unprocessed { - watch_for_changes, .. - } - | AssetPlugin::Processed { - watch_for_changes, .. - } - | AssetPlugin::ProcessedDev { - watch_for_changes, .. - } => *watch_for_changes = true, - }; - self - } + const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default"; } impl Plugin for AssetPlugin { fn build(&self, app: &mut App) { - app.init_schedule(UpdateAssets) - .init_schedule(AssetEvents) - .init_resource::(); + app.init_schedule(UpdateAssets).init_schedule(AssetEvents); + let rust_src = RustSrcRegistry::default(); + { + let mut providers = app + .world + .get_resource_or_insert_with::(Default::default); + providers.init_default_providers(&self.file_path, &self.processed_file_path); + rust_src.register_provider(&mut providers); + } { - match self { - AssetPlugin::Unprocessed { - source, - watch_for_changes, - } => { - let source_reader = app - .world - .resource_mut::() - .get_source_reader(source); - app.insert_resource(AssetServer::new(source_reader, *watch_for_changes)); + let mut watch = cfg!(feature = "watch"); + if let Some(watch_override) = self.watch_for_changes_override { + watch = watch_override; + } + match self.mode { + AssetMode::Unprocessed => { + let mut providers = app.world.resource_mut::(); + let providers = providers.build_providers(watch, false); + app.insert_resource(AssetServer::new( + providers, + AssetServerMode::Unprocessed, + watch, + )); } - AssetPlugin::Processed { - destination, - watch_for_changes, - } => { - let destination_reader = app - .world - .resource_mut::() - .get_destination_reader(destination); - app.insert_resource(AssetServer::new(destination_reader, *watch_for_changes)); + AssetMode::Processed => { + let mut providers = app.world.resource_mut::(); + let providers = providers.build_providers(false, watch); + app.insert_resource(AssetServer::new( + providers, + AssetServerMode::Processed, + watch, + )); } - AssetPlugin::ProcessedDev { - source, - destination, - watch_for_changes, - } => { - let mut asset_providers = app.world.resource_mut::(); - let processor = AssetProcessor::new(&mut asset_providers, source, destination); - let destination_reader = asset_providers.get_destination_reader(source); - // the main asset server gates loads based on asset state - let gated_reader = - ProcessorGatedReader::new(destination_reader, processor.data.clone()); + AssetMode::ProcessedDev => { + let mut providers = app.world.resource_mut::(); + let processor = AssetProcessor::new(&mut providers); + let mut providers = providers.build_providers(false, watch); + providers.gate_on_processor(processor.data.clone()); // the main asset server shares loaders with the processor asset server app.insert_resource(AssetServer::new_with_loaders( - Box::new(gated_reader), + providers, processor.server().data.loaders.clone(), - *watch_for_changes, + AssetServerMode::Processed, + watch, )) .insert_resource(processor) .add_systems(Startup, AssetProcessor::start); } } } - app.init_asset::() + app.insert_resource(rust_src) + .init_asset::() .init_asset::<()>() .configure_sets( UpdateAssets, @@ -253,6 +227,12 @@ pub trait AssetApp { fn register_asset_loader(&mut self, loader: L) -> &mut Self; /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`]. fn register_asset_processor(&mut self, processor: P) -> &mut Self; + /// Registers the given [`AssetProviderBuilder`] with the given `id`. + fn register_asset_provider( + &mut self, + id: impl Into>, + provider: AssetProviderBuilder, + ) -> &mut Self; /// Sets the default asset processor for the given `extension`. fn set_default_asset_processor(&mut self, extension: &str) -> &mut Self; /// Initializes the given loader in the [`App`]'s [`AssetServer`]. @@ -345,6 +325,21 @@ impl AssetApp for App { } self } + + fn register_asset_provider( + &mut self, + id: impl Into>, + provider: AssetProviderBuilder, + ) -> &mut Self { + { + let mut providers = self + .world + .get_resource_or_insert_with(AssetProviderBuilders::default); + providers.insert(id, provider); + } + + self + } } /// A system set that holds all "track asset" operations. @@ -361,55 +356,6 @@ pub struct UpdateAssets; #[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)] pub struct AssetEvents; -/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle. -#[macro_export] -macro_rules! load_internal_asset { - ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ - let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); - assets.insert($handle, ($loader)( - include_str!($path_str), - std::path::Path::new(file!()) - .parent() - .unwrap() - .join($path_str) - .to_string_lossy() - )); - }}; - // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded - ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{ - let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); - assets.insert($handle, ($loader)( - include_str!($path_str), - std::path::Path::new(file!()) - .parent() - .unwrap() - .join($path_str) - .to_string_lossy(), - $($param),+ - )); - }}; -} - -/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle. -#[macro_export] -macro_rules! load_internal_binary_asset { - ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ - let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); - assets.insert( - $handle, - ($loader)( - include_bytes!($path_str).as_ref(), - std::path::Path::new(file!()) - .parent() - .unwrap() - .join($path_str) - .to_string_lossy() - .into(), - ), - ); - }}; -} - #[cfg(test)] mod tests { use crate::{ @@ -419,11 +365,11 @@ mod tests { io::{ gated::{GateOpener, GatedReader}, memory::{Dir, MemoryAssetReader}, - Reader, + AssetProvider, Reader, }, loader::{AssetLoader, LoadContext}, - Asset, AssetApp, AssetEvent, AssetId, AssetPlugin, AssetProvider, AssetProviders, - AssetServer, Assets, DependencyLoadState, LoadState, RecursiveDependencyLoadState, + Asset, AssetApp, AssetEvent, AssetId, AssetPlugin, AssetProviderId, AssetServer, Assets, + DependencyLoadState, LoadState, RecursiveDependencyLoadState, }; use bevy_app::{App, Update}; use bevy_core::TaskPoolPlugin; @@ -508,17 +454,14 @@ mod tests { fn test_app(dir: Dir) -> (App, GateOpener) { let mut app = App::new(); let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir }); - app.insert_resource( - AssetProviders::default() - .with_reader("Test", move || Box::new(gated_memory_reader.clone())), + app.register_asset_provider( + AssetProviderId::Default, + AssetProvider::build().with_reader(move || Box::new(gated_memory_reader.clone())), ) .add_plugins(( TaskPoolPlugin::default(), LogPlugin::default(), - AssetPlugin::Unprocessed { - source: AssetProvider::Custom("Test".to_string()), - watch_for_changes: false, - }, + AssetPlugin::default(), )); (app, gate_opener) } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 6652f7243bd46..ddcc7faad9139 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,11 +1,12 @@ use crate::{ - io::{AssetReaderError, Reader}, + io::{AssetReaderError, MissingAssetProviderError, MissingProcessedAssetReaderError, Reader}, meta::{ loader_settings_meta_transform, AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings, }, path::AssetPath, - Asset, AssetLoadError, AssetServer, Assets, Handle, UntypedAssetId, UntypedHandle, + Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId, + UntypedHandle, }; use bevy_ecs::world::World; use bevy_utils::{BoxedFuture, CowArc, HashMap, HashSet}; @@ -370,7 +371,7 @@ impl<'a> LoadContext<'a> { ) -> Handle { let label = label.into(); let loaded_asset: ErasedLoadedAsset = loaded_asset.into(); - let labeled_path = self.asset_path.with_label(label.clone()); + let labeled_path = self.asset_path.clone().with_label(label.clone()); let handle = self .asset_server .get_or_create_path_handle(labeled_path, None); @@ -388,7 +389,7 @@ impl<'a> LoadContext<'a> { /// /// See [`AssetPath`] for more on labeled assets. pub fn has_labeled_asset<'b>(&self, label: impl Into>) -> bool { - let path = self.asset_path.with_label(label.into()); + let path = self.asset_path.clone().with_label(label.into()); self.asset_server.get_handle_untyped(&path).is_some() } @@ -415,15 +416,21 @@ impl<'a> LoadContext<'a> { } /// Gets the source asset path for this load context. - pub async fn read_asset_bytes<'b>( - &mut self, - path: &'b Path, + pub async fn read_asset_bytes<'b, 'c>( + &'b mut self, + path: impl Into>, ) -> Result, ReadAssetBytesError> { - let mut reader = self.asset_server.reader().read(path).await?; + let path = path.into(); + let provider = self.asset_server.get_provider(path.provider())?; + let asset_reader = match self.asset_server.mode() { + AssetServerMode::Unprocessed { .. } => provider.reader(), + AssetServerMode::Processed { .. } => provider.processed_reader()?, + }; + let mut reader = asset_reader.read(path.path()).await?; let hash = if self.populate_hashes { // NOTE: ensure meta is read while the asset bytes reader is still active to ensure transactionality // See `ProcessorGatedReader` for more info - let meta_bytes = self.asset_server.reader().read_meta_bytes(path).await?; + let meta_bytes = asset_reader.read_meta_bytes(path.path()).await?; let minimal: ProcessedInfoMinimal = ron::de::from_bytes(&meta_bytes) .map_err(DeserializeMetaError::DeserializeMinimal)?; let processed_info = minimal @@ -435,8 +442,7 @@ impl<'a> LoadContext<'a> { }; let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - self.loader_dependencies - .insert(AssetPath::from_path(path.to_owned()), hash); + self.loader_dependencies.insert(path.clone_owned(), hash); Ok(bytes) } @@ -483,7 +489,7 @@ impl<'a> LoadContext<'a> { &mut self, label: impl Into>, ) -> Handle { - let path = self.asset_path.with_label(label); + let path = self.asset_path.clone().with_label(label); let handle = self.asset_server.get_or_create_path_handle::(path, None); self.dependencies.insert(handle.id().untyped()); handle @@ -545,6 +551,10 @@ pub enum ReadAssetBytesError { DeserializeMetaError(#[from] DeserializeMetaError), #[error(transparent)] AssetReaderError(#[from] AssetReaderError), + #[error(transparent)] + MissingAssetProviderError(#[from] MissingAssetProviderError), + #[error(transparent)] + MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError), /// Encountered an I/O error while loading an asset. #[error("Encountered an io error while loading asset: {0}")] Io(#[from] std::io::Error), diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index efd12041148e3..0bd13f4cee703 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -1,3 +1,4 @@ +use crate::io::AssetProviderId; use bevy_reflect::{ std_traits::ReflectDefault, utility::NonGenericTypeInfoCell, FromReflect, FromType, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, @@ -12,10 +13,13 @@ use std::{ ops::Deref, path::{Path, PathBuf}, }; +use thiserror::Error; /// Represents a path to an asset in a "virtual filesystem". /// -/// Asset paths consist of two main parts: +/// Asset paths consist of three main parts: +/// * [`AssetPath::provider`]: The name of the [`AssetProvider`](crate::io::AssetProvider) to load the asset from. +/// This is optional. If one is not set the default provider will be used (which is the `assets` folder by default). /// * [`AssetPath::path`]: The "virtual filesystem path" pointing to an asset source file. /// * [`AssetPath::label`]: An optional "named sub asset". When assets are loaded, they are /// allowed to load "sub assets" of any type, which are identified by a named "label". @@ -33,11 +37,14 @@ use std::{ /// # struct Scene; /// # /// # let asset_server: AssetServer = panic!(); -/// // This loads the `my_scene.scn` base asset. +/// // This loads the `my_scene.scn` base asset from the default asset provider. /// let scene: Handle = asset_server.load("my_scene.scn"); /// -/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset. +/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset in the default asset provider. /// let mesh: Handle = asset_server.load("my_scene.scn#PlayerMesh"); +/// +/// // This loads the `my_scene.scn` base asset from a custom 'remote' asset provider. +/// let scene: Handle = asset_server.load("remote://my_scene.scn"); /// ``` /// /// [`AssetPath`] implements [`From`] for `&'static str`, `&'static Path`, and `&'a String`, @@ -47,6 +54,7 @@ use std::{ /// This also means that you should use [`AssetPath::new`] in cases where `&str` is the explicit type. #[derive(Eq, PartialEq, Hash, Clone, Default)] pub struct AssetPath<'a> { + provider: AssetProviderId<'a>, path: CowArc<'a, Path>, label: Option>, } @@ -67,38 +75,128 @@ impl<'a> Display for AssetPath<'a> { } } +#[derive(Error, Debug, PartialEq, Eq)] +pub enum ParseAssetPathError { + #[error("Asset provider must be followed by '://'")] + InvalidProviderSyntax, + #[error("Asset provider must be at least one character. Either specify the provider before the '://' or remove the `://`")] + MissingProvider, + #[error("Asset label must be at least one character. Either specify the label after the '#' or remove the '#'")] + MissingLabel, +} + impl<'a> AssetPath<'a> { /// Creates a new [`AssetPath`] from a string in the asset path format: /// * An asset at the root: `"scene.gltf"` /// * An asset nested in some folders: `"some/path/scene.gltf"` /// * An asset with a "label": `"some/path/scene.gltf#Mesh0"` + /// * An asset with a custom "provider": `"custom://some/path/scene.gltf#Mesh0"` /// /// Prefer [`From<'static str>`] for static strings, as this will prevent allocations /// and reference counting for [`AssetPath::into_owned`]. - pub fn new(asset_path: &'a str) -> AssetPath<'a> { - let (path, label) = Self::get_parts(asset_path); - Self { + /// + /// # Panics + /// Panics if the asset path is in an invalid format. Use [`AssetPath::try_parse`] for a fallible variant + pub fn parse(asset_path: &'a str) -> AssetPath<'a> { + Self::try_parse(asset_path).unwrap() + } + + /// Creates a new [`AssetPath`] from a string in the asset path format: + /// * An asset at the root: `"scene.gltf"` + /// * An asset nested in some folders: `"some/path/scene.gltf"` + /// * An asset with a "label": `"some/path/scene.gltf#Mesh0"` + /// * An asset with a custom "provider": `"custom://some/path/scene.gltf#Mesh0"` + /// + /// Prefer [`From<'static str>`] for static strings, as this will prevent allocations + /// and reference counting for [`AssetPath::into_owned`]. + /// + /// This will return a [`ParseAssetPathError`] if `asset_path` is in an invalid format. + pub fn try_parse(asset_path: &'a str) -> Result, ParseAssetPathError> { + let (provider, path, label) = Self::parse_internal(asset_path).unwrap(); + Ok(Self { + provider: match provider { + Some(provider) => AssetProviderId::Name(CowArc::Borrowed(provider)), + None => AssetProviderId::Default, + }, path: CowArc::Borrowed(path), label: label.map(CowArc::Borrowed), - } + }) } - fn get_parts(asset_path: &str) -> (&Path, Option<&str>) { - let mut parts = asset_path.splitn(2, '#'); - let path = Path::new(parts.next().expect("Path must be set.")); - let label = parts.next(); - (path, label) + fn parse_internal( + asset_path: &str, + ) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> { + let mut chars = asset_path.char_indices(); + let mut provider_range = None; + let mut path_range = 0..asset_path.len(); + let mut label_range = None; + while let Some((index, char)) = chars.next() { + match char { + ':' => { + let (_, char) = chars + .next() + .ok_or(ParseAssetPathError::InvalidProviderSyntax)?; + if char != '/' { + return Err(ParseAssetPathError::InvalidProviderSyntax); + } + let (index, char) = chars + .next() + .ok_or(ParseAssetPathError::InvalidProviderSyntax)?; + if char != '/' { + return Err(ParseAssetPathError::InvalidProviderSyntax); + } + provider_range = Some(0..index - 2); + path_range.start = index + 1; + } + '#' => { + path_range.end = index; + label_range = Some(index + 1..asset_path.len()); + break; + } + _ => {} + } + } + + let provider = match provider_range { + Some(provider_range) => { + if provider_range.is_empty() { + return Err(ParseAssetPathError::MissingProvider); + } + Some(&asset_path[provider_range]) + } + None => None, + }; + let label = match label_range { + Some(label_range) => { + if label_range.is_empty() { + return Err(ParseAssetPathError::MissingLabel); + } + Some(&asset_path[label_range]) + } + None => None, + }; + + let path = Path::new(&asset_path[path_range]); + Ok((provider, path, label)) } /// Creates a new [`AssetPath`] from a [`Path`]. #[inline] - pub fn from_path(path: impl Into>) -> AssetPath<'a> { + pub fn from_path(path: &'a Path) -> AssetPath<'a> { AssetPath { - path: path.into(), + path: CowArc::Borrowed(path), + provider: AssetProviderId::Default, label: None, } } + /// Gets the "asset provider", if one was defined. If none was defined, the default provider + /// will be used. + #[inline] + pub fn provider(&self) -> &AssetProviderId { + &self.provider + } + /// Gets the "sub-asset label". #[inline] pub fn label(&self) -> Option<&str> { @@ -115,6 +213,7 @@ impl<'a> AssetPath<'a> { #[inline] pub fn without_label(&self) -> AssetPath<'_> { Self { + provider: self.provider.clone(), path: self.path.clone(), label: None, } @@ -135,24 +234,62 @@ impl<'a> AssetPath<'a> { /// Returns this asset path with the given label. This will replace the previous /// label if it exists. #[inline] - pub fn with_label(&self, label: impl Into>) -> AssetPath<'a> { + pub fn with_label(self, label: impl Into>) -> AssetPath<'a> { AssetPath { - path: self.path.clone(), + provider: self.provider, + path: self.path, label: Some(label.into()), } } + /// Returns this asset path with the given provider. This will replace the previous + /// provider if it exists. + #[inline] + pub fn with_provider(self, provider: impl Into>) -> AssetPath<'a> { + AssetPath { + provider: provider.into(), + path: self.path, + label: self.label, + } + } + + /// Returns an [`AssetPath`] for the parent folder of this path, if there is a parent folder in the path. + pub fn parent(&self) -> Option> { + let path = match &self.path { + CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?), + CowArc::Static(path) => CowArc::Static(path.parent()?), + CowArc::Owned(path) => path.parent()?.to_path_buf().into(), + }; + Some(AssetPath { + provider: self.provider.clone(), + label: None, + path, + }) + } + /// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". - /// If it is already an "owned [`Arc`]", it will remain unchanged. + /// If internally a value is a static reference, the static reference will be used unchanged. + /// If internally a value is an "owned [`Arc`]", it will remain unchanged. /// /// [`Arc`]: std::sync::Arc pub fn into_owned(self) -> AssetPath<'static> { AssetPath { + provider: self.provider.into_owned(), path: self.path.into_owned(), label: self.label.map(|l| l.into_owned()), } } + /// Clones this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". + /// If internally a value is a static reference, the static reference will be used unchanged. + /// If internally a value is an "owned [`Arc`]", the [`Arc`] will be cloned. + /// + /// [`Arc`]: std::sync::Arc + #[inline] + pub fn clone_owned(&self) -> AssetPath<'static> { + self.clone().into_owned() + } + /// Returns the full extension (including multiple '.' values). /// Ex: Returns `"config.ron"` for `"my_asset.config.ron"` pub fn get_full_extension(&self) -> Option { @@ -176,8 +313,9 @@ impl<'a> AssetPath<'a> { impl From<&'static str> for AssetPath<'static> { #[inline] fn from(asset_path: &'static str) -> Self { - let (path, label) = Self::get_parts(asset_path); + let (provider, path, label) = Self::parse_internal(asset_path).unwrap(); AssetPath { + provider: provider.into(), path: CowArc::Static(path), label: label.map(CowArc::Static), } @@ -187,14 +325,14 @@ impl From<&'static str> for AssetPath<'static> { impl<'a> From<&'a String> for AssetPath<'a> { #[inline] fn from(asset_path: &'a String) -> Self { - AssetPath::new(asset_path.as_str()) + AssetPath::parse(asset_path.as_str()) } } impl From for AssetPath<'static> { #[inline] fn from(asset_path: String) -> Self { - AssetPath::new(asset_path.as_str()).into_owned() + AssetPath::parse(asset_path.as_str()).into_owned() } } @@ -202,6 +340,7 @@ impl From<&'static Path> for AssetPath<'static> { #[inline] fn from(path: &'static Path) -> Self { Self { + provider: AssetProviderId::Default, path: CowArc::Static(path), label: None, } @@ -212,6 +351,7 @@ impl From for AssetPath<'static> { #[inline] fn from(path: PathBuf) -> Self { Self { + provider: AssetProviderId::Default, path: path.into(), label: None, } @@ -261,7 +401,7 @@ impl<'de> Visitor<'de> for AssetPathVisitor { where E: serde::de::Error, { - Ok(AssetPath::new(v).into_owned()) + Ok(AssetPath::parse(v).into_owned()) } fn visit_string(self, v: String) -> Result @@ -402,3 +542,39 @@ impl FromReflect for AssetPath<'static> { >(::as_any(reflect))?)) } } + +#[cfg(test)] +mod tests { + use crate::AssetPath; + use std::path::Path; + + #[test] + fn parse_asset_path() { + let result = AssetPath::parse_internal("a/b.test"); + assert_eq!(result, Ok((None, Path::new("a/b.test"), None))); + + let result = AssetPath::parse_internal("http://a/b.test"); + assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None))); + + let result = AssetPath::parse_internal("http://a/b.test#Foo"); + assert_eq!( + result, + Ok((Some("http"), Path::new("a/b.test"), Some("Foo"))) + ); + + let result = AssetPath::parse_internal("http://"); + assert_eq!(result, Ok((Some("http"), Path::new(""), None))); + + let result = AssetPath::parse_internal("://x"); + assert_eq!(result, Err(crate::ParseAssetPathError::MissingProvider)); + + let result = AssetPath::parse_internal("a/b.test#"); + assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel)); + + let result = AssetPath::parse_internal("http:/"); + assert_eq!( + result, + Err(crate::ParseAssetPathError::InvalidProviderSyntax) + ); + } +} diff --git a/crates/bevy_asset/src/processor/log.rs b/crates/bevy_asset/src/processor/log.rs index 0c1c3d93fbade..642de9b127142 100644 --- a/crates/bevy_asset/src/processor/log.rs +++ b/crates/bevy_asset/src/processor/log.rs @@ -1,15 +1,16 @@ +use crate::AssetPath; use async_fs::File; use bevy_log::error; use bevy_utils::HashSet; use futures_lite::{AsyncReadExt, AsyncWriteExt}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use thiserror::Error; /// An in-memory representation of a single [`ProcessorTransactionLog`] entry. #[derive(Debug)] pub(crate) enum LogEntry { - BeginProcessing(PathBuf), - EndProcessing(PathBuf), + BeginProcessing(AssetPath<'static>), + EndProcessing(AssetPath<'static>), UnrecoverableError, } @@ -55,12 +56,12 @@ pub enum ValidateLogError { /// An error that occurs when validating individual [`ProcessorTransactionLog`] entries. #[derive(Error, Debug)] pub enum LogEntryError { - #[error("Encountered a duplicate process asset transaction: {0:?}")] - DuplicateTransaction(PathBuf), - #[error("A transaction was ended that never started {0:?}")] - EndedMissingTransaction(PathBuf), - #[error("An asset started processing but never finished: {0:?}")] - UnfinishedTransaction(PathBuf), + #[error("Encountered a duplicate process asset transaction: {0}")] + DuplicateTransaction(AssetPath<'static>), + #[error("A transaction was ended that never started {0}")] + EndedMissingTransaction(AssetPath<'static>), + #[error("An asset started processing but never finished: {0}")] + UnfinishedTransaction(AssetPath<'static>), } const LOG_PATH: &str = "imported_assets/log"; @@ -114,9 +115,13 @@ impl ProcessorTransactionLog { file.read_to_string(&mut string).await?; for line in string.lines() { if let Some(path_str) = line.strip_prefix(ENTRY_BEGIN) { - log_lines.push(LogEntry::BeginProcessing(PathBuf::from(path_str))); + log_lines.push(LogEntry::BeginProcessing( + AssetPath::parse(path_str).into_owned(), + )); } else if let Some(path_str) = line.strip_prefix(ENTRY_END) { - log_lines.push(LogEntry::EndProcessing(PathBuf::from(path_str))); + log_lines.push(LogEntry::EndProcessing( + AssetPath::parse(path_str).into_owned(), + )); } else if line.is_empty() { continue; } else { @@ -127,7 +132,7 @@ impl ProcessorTransactionLog { } pub(crate) async fn validate() -> Result<(), ValidateLogError> { - let mut transactions: HashSet = Default::default(); + let mut transactions: HashSet> = Default::default(); let mut errors: Vec = Vec::new(); let entries = Self::read().await?; for entry in entries { @@ -160,21 +165,27 @@ impl ProcessorTransactionLog { /// Logs the start of an asset being processed. If this is not followed at some point in the log by a closing [`ProcessorTransactionLog::end_processing`], /// in the next run of the processor the asset processing will be considered "incomplete" and it will be reprocessed. - pub(crate) async fn begin_processing(&mut self, path: &Path) -> Result<(), WriteLogError> { - self.write(&format!("{ENTRY_BEGIN}{}\n", path.to_string_lossy())) + pub(crate) async fn begin_processing( + &mut self, + path: &AssetPath<'_>, + ) -> Result<(), WriteLogError> { + self.write(&format!("{ENTRY_BEGIN}{path}\n")) .await .map_err(|e| WriteLogError { - log_entry: LogEntry::BeginProcessing(path.to_owned()), + log_entry: LogEntry::BeginProcessing(path.clone_owned()), error: e, }) } /// Logs the end of an asset being successfully processed. See [`ProcessorTransactionLog::begin_processing`]. - pub(crate) async fn end_processing(&mut self, path: &Path) -> Result<(), WriteLogError> { - self.write(&format!("{ENTRY_END}{}\n", path.to_string_lossy())) + pub(crate) async fn end_processing( + &mut self, + path: &AssetPath<'_>, + ) -> Result<(), WriteLogError> { + self.write(&format!("{ENTRY_END}{path}\n")) .await .map_err(|e| WriteLogError { - log_entry: LogEntry::EndProcessing(path.to_owned()), + log_entry: LogEntry::EndProcessing(path.clone_owned()), error: e, }) } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 2e67be23deca5..1e1b809acae6a 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -6,15 +6,15 @@ pub use process::*; use crate::{ io::{ - processor_gated::ProcessorGatedReader, AssetProvider, AssetProviders, AssetReader, - AssetReaderError, AssetSourceEvent, AssetWatcher, AssetWriter, AssetWriterError, + AssetProvider, AssetProviderBuilders, AssetProviderEvent, AssetProviderId, AssetProviders, + AssetReader, AssetReaderError, AssetWriter, AssetWriterError, MissingAssetProviderError, }, meta::{ get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta, AssetMetaDyn, AssetMetaMinimal, ProcessedInfo, ProcessedInfoMinimal, }, - AssetLoadError, AssetLoaderError, AssetPath, AssetServer, DeserializeMetaError, - LoadDirectError, MissingAssetLoaderForExtensionError, CANNOT_WATCH_ERROR_MESSAGE, + AssetLoadError, AssetLoaderError, AssetPath, AssetServer, AssetServerMode, + DeserializeMetaError, LoadDirectError, MissingAssetLoaderForExtensionError, }; use bevy_ecs::prelude::*; use bevy_log::{debug, error, trace, warn}; @@ -58,37 +58,23 @@ pub struct AssetProcessorData { /// Default processors for file extensions default_processors: RwLock>, state: async_lock::RwLock, - source_reader: Box, - source_writer: Box, - destination_reader: Box, - destination_writer: Box, + providers: AssetProviders, initialized_sender: async_broadcast::Sender<()>, initialized_receiver: async_broadcast::Receiver<()>, finished_sender: async_broadcast::Sender<()>, finished_receiver: async_broadcast::Receiver<()>, - source_event_receiver: crossbeam_channel::Receiver, - _source_watcher: Option>, } impl AssetProcessor { /// Creates a new [`AssetProcessor`] instance. - pub fn new( - providers: &mut AssetProviders, - source: &AssetProvider, - destination: &AssetProvider, - ) -> Self { + pub fn new(providers: &mut AssetProviderBuilders) -> Self { let data = Arc::new(AssetProcessorData::new( - providers.get_source_reader(source), - providers.get_source_writer(source), - providers.get_destination_reader(destination), - providers.get_destination_writer(destination), + providers.build_providers(true, false), )); - let destination_reader = providers.get_destination_reader(destination); // The asset processor uses its own asset server with its own id space - let server = AssetServer::new( - Box::new(ProcessorGatedReader::new(destination_reader, data.clone())), - true, - ); + let mut providers = providers.build_providers(false, false); + providers.gate_on_processor(data.clone()); + let server = AssetServer::new(providers, AssetServerMode::Processed, false); Self { server, data } } @@ -114,24 +100,18 @@ impl AssetProcessor { *self.data.state.read().await } - /// Retrieves the "source" [`AssetReader`] (the place where user-provided unprocessed "asset sources" are stored) - pub fn source_reader(&self) -> &dyn AssetReader { - &*self.data.source_reader - } - - /// Retrieves the "source" [`AssetWriter`] (the place where user-provided unprocessed "asset sources" are stored) - pub fn source_writer(&self) -> &dyn AssetWriter { - &*self.data.source_writer - } - - /// Retrieves the "destination" [`AssetReader`] (the place where processed / [`AssetProcessor`]-managed assets are stored) - pub fn destination_reader(&self) -> &dyn AssetReader { - &*self.data.destination_reader + /// Retrieves the [`AssetReaders`] for this processor + #[inline] + pub fn get_provider<'a, 'b>( + &'a self, + provider: impl Into>, + ) -> Result<&'a AssetProvider, MissingAssetProviderError> { + self.data.providers.get(provider.into()) } - /// Retrieves the "destination" [`AssetWriter`] (the place where processed / [`AssetProcessor`]-managed assets are stored) - pub fn destination_writer(&self) -> &dyn AssetWriter { - &*self.data.destination_writer + #[inline] + pub fn providers(&self) -> &AssetProviders { + &self.data.providers } /// Logs an unrecoverable error. On the next run of the processor, all assets will be regenerated. This should only be used as a last resort. @@ -144,14 +124,14 @@ impl AssetProcessor { /// Logs the start of an asset being processed. If this is not followed at some point in the log by a closing [`AssetProcessor::log_end_processing`], /// in the next run of the processor the asset processing will be considered "incomplete" and it will be reprocessed. - async fn log_begin_processing(&self, path: &Path) { + async fn log_begin_processing(&self, path: &AssetPath<'_>) { let mut log = self.data.log.write().await; let log = log.as_mut().unwrap(); log.begin_processing(path).await.unwrap(); } /// Logs the end of an asset being successfully processed. See [`AssetProcessor::log_begin_processing`]. - async fn log_end_processing(&self, path: &Path) { + async fn log_end_processing(&self, path: &AssetPath<'_>) { let mut log = self.data.log.write().await; let log = log.as_mut().unwrap(); log.end_processing(path).await.unwrap(); @@ -184,8 +164,11 @@ impl AssetProcessor { IoTaskPool::get().scope(|scope| { scope.spawn(async move { self.initialize().await.unwrap(); - let path = PathBuf::from(""); - self.process_assets_internal(scope, path).await.unwrap(); + for provider in self.providers().iter_processed() { + self.process_assets_internal(scope, provider, PathBuf::from("")) + .await + .unwrap(); + } }); }); // This must happen _after_ the scope resolves or it will happen "too early" @@ -202,13 +185,17 @@ impl AssetProcessor { loop { let mut started_processing = false; - for event in self.data.source_event_receiver.try_iter() { - if !started_processing { - self.set_state(ProcessorState::Processing).await; - started_processing = true; - } + for provider in self.data.providers.iter_processed() { + if let Some(receiver) = provider.event_receiver() { + for event in receiver.try_iter() { + if !started_processing { + self.set_state(ProcessorState::Processing).await; + started_processing = true; + } - self.handle_asset_source_event(event).await; + self.handle_asset_provider_event(provider, event).await; + } + } } if started_processing { @@ -217,84 +204,95 @@ impl AssetProcessor { } } - async fn handle_asset_source_event(&self, event: AssetSourceEvent) { + async fn handle_asset_provider_event( + &self, + provider: &AssetProvider, + event: AssetProviderEvent, + ) { trace!("{event:?}"); match event { - AssetSourceEvent::AddedAsset(path) - | AssetSourceEvent::AddedMeta(path) - | AssetSourceEvent::ModifiedAsset(path) - | AssetSourceEvent::ModifiedMeta(path) => { - self.process_asset(&path).await; + AssetProviderEvent::AddedAsset(path) + | AssetProviderEvent::AddedMeta(path) + | AssetProviderEvent::ModifiedAsset(path) + | AssetProviderEvent::ModifiedMeta(path) => { + self.process_asset(provider, path).await; } - AssetSourceEvent::RemovedAsset(path) => { - self.handle_removed_asset(path).await; + AssetProviderEvent::RemovedAsset(path) => { + self.handle_removed_asset(provider, path).await; } - AssetSourceEvent::RemovedMeta(path) => { - self.handle_removed_meta(&path).await; + AssetProviderEvent::RemovedMeta(path) => { + self.handle_removed_meta(provider, path).await; } - AssetSourceEvent::AddedFolder(path) => { - self.handle_added_folder(path).await; + AssetProviderEvent::AddedFolder(path) => { + self.handle_added_folder(provider, path).await; } // NOTE: As a heads up for future devs: this event shouldn't be run in parallel with other events that might // touch this folder (ex: the folder might be re-created with new assets). Clean up the old state first. // Currently this event handler is not parallel, but it could be (and likely should be) in the future. - AssetSourceEvent::RemovedFolder(path) => { - self.handle_removed_folder(&path).await; + AssetProviderEvent::RemovedFolder(path) => { + self.handle_removed_folder(provider, &path).await; } - AssetSourceEvent::RenamedAsset { old, new } => { + AssetProviderEvent::RenamedAsset { old, new } => { // If there was a rename event, but the path hasn't changed, this asset might need reprocessing. // Sometimes this event is returned when an asset is moved "back" into the asset folder if old == new { - self.process_asset(&new).await; + self.process_asset(provider, new).await; } else { - self.handle_renamed_asset(old, new).await; + self.handle_renamed_asset(provider, old, new).await; } } - AssetSourceEvent::RenamedMeta { old, new } => { + AssetProviderEvent::RenamedMeta { old, new } => { // If there was a rename event, but the path hasn't changed, this asset meta might need reprocessing. // Sometimes this event is returned when an asset meta is moved "back" into the asset folder if old == new { - self.process_asset(&new).await; + self.process_asset(provider, new).await; } else { debug!("Meta renamed from {old:?} to {new:?}"); let mut infos = self.data.asset_infos.write().await; // Renaming meta should not assume that an asset has also been renamed. Check both old and new assets to see // if they should be re-imported (and/or have new meta generated) - infos.check_reprocess_queue.push_back(old); - infos.check_reprocess_queue.push_back(new); + let new_asset_path = AssetPath::from(new).with_provider(provider.id()); + let old_asset_path = AssetPath::from(old).with_provider(provider.id()); + infos.check_reprocess_queue.push_back(old_asset_path); + infos.check_reprocess_queue.push_back(new_asset_path); } } - AssetSourceEvent::RenamedFolder { old, new } => { + AssetProviderEvent::RenamedFolder { old, new } => { // If there was a rename event, but the path hasn't changed, this asset folder might need reprocessing. // Sometimes this event is returned when an asset meta is moved "back" into the asset folder if old == new { - self.handle_added_folder(new).await; + self.handle_added_folder(provider, new).await; } else { // PERF: this reprocesses everything in the moved folder. this is not necessary in most cases, but // requires some nuance when it comes to path handling. - self.handle_removed_folder(&old).await; - self.handle_added_folder(new).await; + self.handle_removed_folder(provider, &old).await; + self.handle_added_folder(provider, new).await; } } - AssetSourceEvent::RemovedUnknown { path, is_meta } => { - match self.destination_reader().is_directory(&path).await { + AssetProviderEvent::RemovedUnknown { path, is_meta } => { + let processed_reader = provider.processed_reader().unwrap(); + match processed_reader.is_directory(&path).await { Ok(is_directory) => { if is_directory { - self.handle_removed_folder(&path).await; + self.handle_removed_folder(provider, &path).await; } else if is_meta { - self.handle_removed_meta(&path).await; + self.handle_removed_meta(provider, path).await; } else { - self.handle_removed_asset(path).await; + self.handle_removed_asset(provider, path).await; } } Err(err) => { - if let AssetReaderError::NotFound(_) = err { - // if the path is not found, a processed version does not exist - } else { - error!( - "Path '{path:?}' as removed, but the destination reader could not determine if it \ - was a folder or a file due to the following error: {err}" - ); + match err { + AssetReaderError::NotFound(_) => { + // if the path is not found, a processed version does not exist + } + AssetReaderError::Io(err) => { + error!( + "Path '{}' was removed, but the destination reader could not determine if it \ + was a folder or a file due to the following error: {err}", + AssetPath::from_path(&path).with_provider(provider.id()) + ); + } } } } @@ -302,38 +300,44 @@ impl AssetProcessor { } } - async fn handle_added_folder(&self, path: PathBuf) { - debug!("Folder {:?} was added. Attempting to re-process", path); + async fn handle_added_folder(&self, provider: &AssetProvider, path: PathBuf) { + debug!( + "Folder {} was added. Attempting to re-process", + AssetPath::from_path(&path).with_provider(provider.id()) + ); #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] error!("AddFolder event cannot be handled in single threaded mode (or WASM) yet."); #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] IoTaskPool::get().scope(|scope| { scope.spawn(async move { - self.process_assets_internal(scope, path).await.unwrap(); + self.process_assets_internal(scope, provider, path) + .await + .unwrap(); }); }); } /// Responds to a removed meta event by reprocessing the asset at the given path. - async fn handle_removed_meta(&self, path: &Path) { + async fn handle_removed_meta(&self, provider: &AssetProvider, path: PathBuf) { // If meta was removed, we might need to regenerate it. // Likewise, the user might be manually re-adding the asset. // Therefore, we shouldn't automatically delete the asset ... that is a // user-initiated action. debug!( "Meta for asset {:?} was removed. Attempting to re-process", - path + AssetPath::from_path(&path).with_provider(provider.id()) ); - self.process_asset(path).await; + self.process_asset(provider, path).await; } /// Removes all processed assets stored at the given path (respecting transactionality), then removes the folder itself. - async fn handle_removed_folder(&self, path: &Path) { + async fn handle_removed_folder(&self, provider: &AssetProvider, path: &Path) { debug!("Removing folder {:?} because source was removed", path); - match self.destination_reader().read_directory(path).await { + let processed_reader = provider.processed_reader().unwrap(); + match processed_reader.read_directory(path).await { Ok(mut path_stream) => { while let Some(child_path) = path_stream.next().await { - self.handle_removed_asset(child_path).await; + self.handle_removed_asset(provider, child_path).await; } } Err(err) => match err { @@ -349,28 +353,32 @@ impl AssetProcessor { } }, } - if let Err(AssetWriterError::Io(err)) = - self.destination_writer().remove_directory(path).await - { - // we can ignore NotFound because if the "final" file in a folder was removed - // then we automatically clean up this folder - if err.kind() != ErrorKind::NotFound { - error!("Failed to remove destination folder that no longer exists in asset source {path:?}: {err}"); + let processed_writer = provider.processed_writer().unwrap(); + if let Err(err) = processed_writer.remove_directory(path).await { + match err { + AssetWriterError::Io(err) => { + // we can ignore NotFound because if the "final" file in a folder was removed + // then we automatically clean up this folder + if err.kind() != ErrorKind::NotFound { + let asset_path = AssetPath::from_path(path).with_provider(provider.id()); + error!("Failed to remove destination folder that no longer exists in {asset_path}: {err}"); + } + } } } } /// Removes the processed version of an asset and associated in-memory metadata. This will block until all existing reads/writes to the /// asset have finished, thanks to the `file_transaction_lock`. - async fn handle_removed_asset(&self, path: PathBuf) { - debug!("Removing processed {:?} because source was removed", path); - let asset_path = AssetPath::from_path(path); + async fn handle_removed_asset(&self, provider: &AssetProvider, path: PathBuf) { + let asset_path = AssetPath::from(path).with_provider(provider.id()); + debug!("Removing processed {asset_path} because source was removed"); let mut infos = self.data.asset_infos.write().await; if let Some(info) = infos.get(&asset_path) { // we must wait for uncontested write access to the asset source to ensure existing readers / writers // can finish their operations let _write_lock = info.file_transaction_lock.write(); - self.remove_processed_asset_and_meta(asset_path.path()) + self.remove_processed_asset_and_meta(provider, asset_path.path()) .await; } infos.remove(&asset_path).await; @@ -378,22 +386,25 @@ impl AssetProcessor { /// Handles a renamed source asset by moving it's processed results to the new location and updating in-memory paths + metadata. /// This will cause direct path dependencies to break. - async fn handle_renamed_asset(&self, old: PathBuf, new: PathBuf) { + async fn handle_renamed_asset(&self, provider: &AssetProvider, old: PathBuf, new: PathBuf) { let mut infos = self.data.asset_infos.write().await; - let old_asset_path = AssetPath::from_path(old); - if let Some(info) = infos.get(&old_asset_path) { + let old = AssetPath::from(old).with_provider(provider.id()); + let new = AssetPath::from(new).with_provider(provider.id()); + let processed_writer = provider.processed_writer().unwrap(); + if let Some(info) = infos.get(&old) { // we must wait for uncontested write access to the asset source to ensure existing readers / writers // can finish their operations let _write_lock = info.file_transaction_lock.write(); - let old = old_asset_path.path(); - self.destination_writer().rename(old, &new).await.unwrap(); - self.destination_writer() - .rename_meta(old, &new) + processed_writer + .rename(old.path(), new.path()) + .await + .unwrap(); + processed_writer + .rename_meta(old.path(), new.path()) .await .unwrap(); } - let new_asset_path = AssetPath::from_path(new); - infos.rename(&old_asset_path, &new_asset_path).await; + infos.rename(&old, &new).await; } async fn finish_processing_assets(&self) { @@ -408,19 +419,20 @@ impl AssetProcessor { fn process_assets_internal<'scope>( &'scope self, scope: &'scope bevy_tasks::Scope<'scope, '_, ()>, + provider: &'scope AssetProvider, path: PathBuf, ) -> bevy_utils::BoxedFuture<'scope, Result<(), AssetReaderError>> { Box::pin(async move { - if self.source_reader().is_directory(&path).await? { - let mut path_stream = self.source_reader().read_directory(&path).await?; + if provider.reader().is_directory(&path).await? { + let mut path_stream = provider.reader().read_directory(&path).await?; while let Some(path) = path_stream.next().await { - self.process_assets_internal(scope, path).await?; + self.process_assets_internal(scope, provider, path).await?; } } else { // Files without extensions are skipped let processor = self.clone(); scope.spawn(async move { - processor.process_asset(&path).await; + processor.process_asset(provider, path).await; }); } Ok(()) @@ -434,8 +446,9 @@ impl AssetProcessor { IoTaskPool::get().scope(|scope| { for path in check_reprocess_queue.drain(..) { let processor = self.clone(); + let provider = self.get_provider(path.provider()).unwrap(); scope.spawn(async move { - processor.process_asset(&path).await; + processor.process_asset(provider, path.into()).await; }); } }); @@ -512,68 +525,84 @@ impl AssetProcessor { }) } - let mut source_paths = Vec::new(); - let source_reader = self.source_reader(); - get_asset_paths(source_reader, None, PathBuf::from(""), &mut source_paths) + for provider in self.providers().iter_processed() { + let Ok(processed_reader) = provider.processed_reader() else { + continue; + }; + let Ok(processed_writer) = provider.processed_writer() else { + continue; + }; + let mut source_paths = Vec::new(); + get_asset_paths( + provider.reader(), + None, + PathBuf::from(""), + &mut source_paths, + ) .await .map_err(InitializeError::FailedToReadSourcePaths)?; - let mut destination_paths = Vec::new(); - let destination_reader = self.destination_reader(); - let destination_writer = self.destination_writer(); - get_asset_paths( - destination_reader, - Some(destination_writer), - PathBuf::from(""), - &mut destination_paths, - ) - .await - .map_err(InitializeError::FailedToReadDestinationPaths)?; - - for path in &source_paths { - asset_infos.get_or_insert(AssetPath::from_path(path.clone())); - } + let mut destination_paths = Vec::new(); + get_asset_paths( + processed_reader, + Some(processed_writer), + PathBuf::from(""), + &mut destination_paths, + ) + .await + .map_err(InitializeError::FailedToReadDestinationPaths)?; - for path in &destination_paths { - let asset_path = AssetPath::from_path(path.clone()); - let mut dependencies = Vec::new(); - if let Some(info) = asset_infos.get_mut(&asset_path) { - match self.destination_reader().read_meta_bytes(path).await { - Ok(meta_bytes) => { - match ron::de::from_bytes::(&meta_bytes) { - Ok(minimal) => { - trace!( - "Populated processed info for asset {path:?} {:?}", - minimal.processed_info - ); + for path in source_paths { + asset_infos.get_or_insert(AssetPath::from(path).with_provider(provider.id())); + } - if let Some(processed_info) = &minimal.processed_info { - for process_dependency_info in - &processed_info.process_dependencies - { - dependencies.push(process_dependency_info.path.clone()); + for path in destination_paths { + let mut dependencies = Vec::new(); + let asset_path = AssetPath::from(path).with_provider(provider.id()); + if let Some(info) = asset_infos.get_mut(&asset_path) { + match processed_reader.read_meta_bytes(asset_path.path()).await { + Ok(meta_bytes) => { + match ron::de::from_bytes::(&meta_bytes) { + Ok(minimal) => { + trace!( + "Populated processed info for asset {asset_path} {:?}", + minimal.processed_info + ); + + if let Some(processed_info) = &minimal.processed_info { + for process_dependency_info in + &processed_info.process_dependencies + { + dependencies.push(process_dependency_info.path.clone()); + } } + info.processed_info = minimal.processed_info; + } + Err(err) => { + trace!("Removing processed data for {asset_path} because meta could not be parsed: {err}"); + self.remove_processed_asset_and_meta( + provider, + asset_path.path(), + ) + .await; } - info.processed_info = minimal.processed_info; - } - Err(err) => { - trace!("Removing processed data for {path:?} because meta could not be parsed: {err}"); - self.remove_processed_asset_and_meta(path).await; } } + Err(err) => { + trace!("Removing processed data for {asset_path} because meta failed to load: {err}"); + self.remove_processed_asset_and_meta(provider, asset_path.path()) + .await; + } } - Err(err) => { - trace!("Removing processed data for {path:?} because meta failed to load: {err}"); - self.remove_processed_asset_and_meta(path).await; - } + } else { + trace!("Removing processed data for non-existent asset {asset_path}"); + self.remove_processed_asset_and_meta(provider, asset_path.path()) + .await; } - } else { - trace!("Removing processed data for non-existent asset {path:?}"); - self.remove_processed_asset_and_meta(path).await; - } - for dependency in dependencies { - asset_infos.add_dependant(&dependency, asset_path.clone()); + for dependency in dependencies { + asset_infos.add_dependant(&dependency, asset_path.clone()); + } } } @@ -584,19 +613,20 @@ impl AssetProcessor { /// Removes the processed version of an asset and its metadata, if it exists. This _is not_ transactional like `remove_processed_asset_transactional`, nor /// does it remove existing in-memory metadata. - async fn remove_processed_asset_and_meta(&self, path: &Path) { - if let Err(err) = self.destination_writer().remove(path).await { + async fn remove_processed_asset_and_meta(&self, provider: &AssetProvider, path: &Path) { + if let Err(err) = provider.processed_writer().unwrap().remove(path).await { warn!("Failed to remove non-existent asset {path:?}: {err}"); } - if let Err(err) = self.destination_writer().remove_meta(path).await { + if let Err(err) = provider.processed_writer().unwrap().remove_meta(path).await { warn!("Failed to remove non-existent meta {path:?}: {err}"); } - self.clean_empty_processed_ancestor_folders(path).await; + self.clean_empty_processed_ancestor_folders(provider, path) + .await; } - async fn clean_empty_processed_ancestor_folders(&self, path: &Path) { + async fn clean_empty_processed_ancestor_folders(&self, provider: &AssetProvider, path: &Path) { // As a safety precaution don't delete absolute paths to avoid deleting folders outside of the destination folder if path.is_absolute() { error!("Attempted to clean up ancestor folders of an absolute path. This is unsafe so the operation was skipped."); @@ -606,8 +636,9 @@ impl AssetProcessor { if parent == Path::new("") { break; } - if self - .destination_writer() + if provider + .processed_writer() + .unwrap() .remove_empty_directory(parent) .await .is_err() @@ -624,33 +655,38 @@ impl AssetProcessor { /// to block reads until the asset is processed). /// /// [`LoadContext`]: crate::loader::LoadContext - async fn process_asset(&self, path: &Path) { - let result = self.process_asset_internal(path).await; + async fn process_asset(&self, provider: &AssetProvider, path: PathBuf) { + let asset_path = AssetPath::from(path).with_provider(provider.id()); + let result = self.process_asset_internal(provider, &asset_path).await; let mut infos = self.data.asset_infos.write().await; - let asset_path = AssetPath::from_path(path.to_owned()); infos.finish_processing(asset_path, result).await; } - async fn process_asset_internal(&self, path: &Path) -> Result { - if path.extension().is_none() { - return Err(ProcessError::ExtensionRequired); - } - let asset_path = AssetPath::from_path(path.to_path_buf()); + async fn process_asset_internal( + &self, + provider: &AssetProvider, + asset_path: &AssetPath<'static>, + ) -> Result { + // TODO: The extension check was removed now tht AssetPath is the input. is that ok? // TODO: check if already processing to protect against duplicate hot-reload events - debug!("Processing {:?}", path); + debug!("Processing {:?}", asset_path); let server = &self.server; + let path = asset_path.path(); + let reader = provider.reader(); + + let reader_err = |err| ProcessError::AssetReaderError { + path: asset_path.clone(), + err, + }; + let writer_err = |err| ProcessError::AssetWriterError { + path: asset_path.clone(), + err, + }; // Note: we get the asset source reader first because we don't want to create meta files for assets that don't have source files - let mut reader = self.source_reader().read(path).await.map_err(|e| match e { - AssetReaderError::NotFound(_) => ProcessError::MissingAssetSource(path.to_owned()), - AssetReaderError::Io(err) => ProcessError::AssetSourceIoError(err), - })?; - - let (mut source_meta, meta_bytes, processor) = match self - .source_reader() - .read_meta_bytes(path) - .await - { + let mut byte_reader = reader.read(path).await.map_err(reader_err)?; + + let (mut source_meta, meta_bytes, processor) = match reader.read_meta_bytes(path).await { Ok(meta_bytes) => { let minimal: AssetMetaMinimal = ron::de::from_bytes(&meta_bytes).map_err(|e| { ProcessError::DeserializeMetaError(DeserializeMetaError::DeserializeMinimal(e)) @@ -684,7 +720,7 @@ impl AssetProcessor { let meta = processor.default_meta(); (meta, Some(processor)) } else { - match server.get_path_asset_loader(&asset_path).await { + match server.get_path_asset_loader(asset_path.clone()).await { Ok(loader) => (loader.default_meta(), None), Err(MissingAssetLoaderForExtensionError { .. }) => { let meta: Box = @@ -695,19 +731,31 @@ impl AssetProcessor { }; let meta_bytes = meta.serialize(); // write meta to source location if it doesn't already exist - self.source_writer() + provider + .writer()? .write_meta_bytes(path, &meta_bytes) - .await?; + .await + .map_err(writer_err)?; (meta, meta_bytes, processor) } - Err(err) => return Err(ProcessError::ReadAssetMetaError(err)), + Err(err) => { + return Err(ProcessError::ReadAssetMetaError { + path: asset_path.clone(), + err, + }) + } }; + let processed_writer = provider.processed_writer()?; + let mut asset_bytes = Vec::new(); - reader + byte_reader .read_to_end(&mut asset_bytes) .await - .map_err(ProcessError::AssetSourceIoError)?; + .map_err(|e| ProcessError::AssetReaderError { + path: asset_path.clone(), + err: AssetReaderError::Io(e), + })?; // PERF: in theory these hashes could be streamed if we want to avoid allocating the whole asset. // The downside is that reading assets would need to happen twice (once for the hash and once for the asset loader) @@ -722,7 +770,7 @@ impl AssetProcessor { { let infos = self.data.asset_infos.read().await; if let Some(current_processed_info) = infos - .get(&asset_path) + .get(asset_path) .and_then(|i| i.processed_info.as_ref()) { if current_processed_info.hash == new_hash { @@ -754,18 +802,24 @@ impl AssetProcessor { // NOTE: if processing the asset fails this will produce an "unfinished" log entry, forcing a rebuild on next run. // Directly writing to the asset destination in the processor necessitates this behavior // TODO: this class of failure can be recovered via re-processing + smarter log validation that allows for duplicate transactions in the event of failures - self.log_begin_processing(path).await; + self.log_begin_processing(asset_path).await; if let Some(processor) = processor { - let mut writer = self.destination_writer().write(path).await?; + let mut writer = processed_writer.write(path).await.map_err(writer_err)?; let mut processed_meta = { let mut context = - ProcessContext::new(self, &asset_path, &asset_bytes, &mut new_processed_info); + ProcessContext::new(self, asset_path, &asset_bytes, &mut new_processed_info); processor .process(&mut context, source_meta, &mut *writer) .await? }; - writer.flush().await.map_err(AssetWriterError::Io)?; + writer + .flush() + .await + .map_err(|e| ProcessError::AssetWriterError { + path: asset_path.clone(), + err: AssetWriterError::Io(e), + })?; let full_hash = get_full_asset_hash( new_hash, @@ -777,20 +831,23 @@ impl AssetProcessor { new_processed_info.full_hash = full_hash; *processed_meta.processed_info_mut() = Some(new_processed_info.clone()); let meta_bytes = processed_meta.serialize(); - self.destination_writer() + processed_writer .write_meta_bytes(path, &meta_bytes) - .await?; + .await + .map_err(writer_err)?; } else { - self.destination_writer() + processed_writer .write_bytes(path, &asset_bytes) - .await?; + .await + .map_err(writer_err)?; *source_meta.processed_info_mut() = Some(new_processed_info.clone()); let meta_bytes = source_meta.serialize(); - self.destination_writer() + processed_writer .write_meta_bytes(path, &meta_bytes) - .await?; + .await + .map_err(writer_err)?; } - self.log_end_processing(path).await; + self.log_end_processing(asset_path).await; Ok(ProcessResult::Processed(new_processed_info)) } @@ -818,27 +875,35 @@ impl AssetProcessor { } LogEntryError::UnfinishedTransaction(path) => { debug!("Asset {path:?} did not finish processing. Clearning state for that asset"); - if let Err(err) = self.destination_writer().remove(&path).await { + let mut unrecoverable_err = |message: &str| { + error!("Failed to remove asset {path:?} because {message}"); + state_is_valid = false; + }; + let Ok(provider) = self.get_provider(path.provider()) else { + (unrecoverable_err)("AssetProvider does not exist"); + continue; + }; + let Ok(processed_writer) = provider.processed_writer() else { + (unrecoverable_err)("AssetProvider does not have a processed AssetWriter registered"); + continue; + }; + + if let Err(err) = processed_writer.remove(path.path()).await { match err { AssetWriterError::Io(err) => { // any error but NotFound means we could be in a bad state if err.kind() != ErrorKind::NotFound { - error!("Failed to remove asset {path:?}: {err}"); - state_is_valid = false; + (unrecoverable_err)("Failed to remove asset"); } } } } - if let Err(err) = self.destination_writer().remove_meta(&path).await - { + if let Err(err) = processed_writer.remove_meta(path.path()).await { match err { AssetWriterError::Io(err) => { // any error but NotFound means we could be in a bad state if err.kind() != ErrorKind::NotFound { - error!( - "Failed to remove asset meta {path:?}: {err}" - ); - state_is_valid = false; + (unrecoverable_err)("Failed to remove asset meta"); } } } @@ -852,12 +917,16 @@ impl AssetProcessor { if !state_is_valid { error!("Processed asset transaction log state was invalid and unrecoverable for some reason (see previous logs). Removing processed assets and starting fresh."); - if let Err(err) = self - .destination_writer() - .remove_assets_in_directory(Path::new("")) - .await - { - panic!("Processed assets were in a bad state. To correct this, the asset processor attempted to remove all processed assets and start from scratch. This failed. There is no way to continue. Try restarting, or deleting imported asset folder manually. {err}"); + for provider in self.providers().iter_processed() { + let Ok(processed_writer) = provider.processed_writer() else { + continue; + }; + if let Err(err) = processed_writer + .remove_assets_in_directory(Path::new("")) + .await + { + panic!("Processed assets were in a bad state. To correct this, the asset processor attempted to remove all processed assets and start from scratch. This failed. There is no way to continue. Try restarting, or deleting imported asset folder manually. {err}"); + } } } } @@ -870,35 +939,20 @@ impl AssetProcessor { } impl AssetProcessorData { - pub fn new( - source_reader: Box, - source_writer: Box, - destination_reader: Box, - destination_writer: Box, - ) -> Self { + pub fn new(providers: AssetProviders) -> Self { let (mut finished_sender, finished_receiver) = async_broadcast::broadcast(1); let (mut initialized_sender, initialized_receiver) = async_broadcast::broadcast(1); // allow overflow on these "one slot" channels to allow receivers to retrieve the "latest" state, and to allow senders to // not block if there was older state present. finished_sender.set_overflow(true); initialized_sender.set_overflow(true); - let (source_event_sender, source_event_receiver) = crossbeam_channel::unbounded(); - // TODO: watching for changes could probably be entirely optional / we could just warn here - let source_watcher = source_reader.watch_for_changes(source_event_sender); - if source_watcher.is_none() { - error!("{}", CANNOT_WATCH_ERROR_MESSAGE); - } + AssetProcessorData { - source_reader, - source_writer, - destination_reader, - destination_writer, + providers, finished_sender, finished_receiver, initialized_sender, initialized_receiver, - source_event_receiver, - _source_watcher: source_watcher, state: async_lock::RwLock::new(ProcessorState::Initializing), log: Default::default(), processors: Default::default(), @@ -908,11 +962,11 @@ impl AssetProcessorData { } /// Returns a future that will not finish until the path has been processed. - pub async fn wait_until_processed(&self, path: &Path) -> ProcessStatus { + pub async fn wait_until_processed(&self, path: AssetPath<'static>) -> ProcessStatus { self.wait_until_initialized().await; let mut receiver = { let infos = self.asset_infos.write().await; - let info = infos.get(&AssetPath::from_path(path.to_path_buf())); + let info = infos.get(&path); match info { Some(info) => match info.status { Some(result) => return result, @@ -1038,7 +1092,7 @@ pub struct ProcessorAssetInfos { /// Therefore this _must_ always be consistent with the `infos` data. If a new asset is added to `infos`, it should /// check this maps for dependencies and add them. If an asset is removed, it should update the dependants here. non_existent_dependants: HashMap, HashSet>>, - check_reprocess_queue: VecDeque, + check_reprocess_queue: VecDeque>, } impl ProcessorAssetInfos { @@ -1100,7 +1154,7 @@ impl ProcessorAssetInfos { info.update_status(ProcessStatus::Processed).await; let dependants = info.dependants.iter().cloned().collect::>(); for path in dependants { - self.check_reprocess_queue.push_back(path.path().to_owned()); + self.check_reprocess_queue.push_back(path); } } Ok(ProcessResult::SkippedNotChanged) => { @@ -1118,17 +1172,17 @@ impl ProcessorAssetInfos { // Skip assets without extensions } Err(ProcessError::MissingAssetLoaderForExtension(_)) => { - trace!("No loader found for {:?}", asset_path); + trace!("No loader found for {asset_path}"); } - Err(ProcessError::MissingAssetSource(_)) => { + Err(ProcessError::AssetReaderError { + err: AssetReaderError::NotFound(_), + .. + }) => { // if there is no asset source, no processing can be done - trace!( - "No need to process asset {:?} because it does not exist", - asset_path - ); + trace!("No need to process asset {asset_path} because it does not exist"); } Err(err) => { - error!("Failed to process asset {:?}: {:?}", asset_path, err); + error!("Failed to process asset {asset_path}: {err}"); // if this failed because a dependency could not be loaded, make sure it is reprocessed if that dependency is reprocessed if let ProcessError::AssetLoadError(AssetLoadError::AssetLoaderError { error: AssetLoaderError::Load(loader_error), @@ -1223,10 +1277,10 @@ impl ProcessorAssetInfos { new_info.dependants.iter().cloned().collect() }; // Queue the asset for a reprocess check, in case it needs new meta. - self.check_reprocess_queue.push_back(new.path().to_owned()); + self.check_reprocess_queue.push_back(new.clone()); for dependant in dependants { // Queue dependants for reprocessing because they might have been waiting for this asset. - self.check_reprocess_queue.push_back(dependant.into()); + self.check_reprocess_queue.push_back(dependant); } } } diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index e63dae81ff8e4..e2afd5c4f8957 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -1,5 +1,8 @@ use crate::{ - io::{AssetReaderError, AssetWriterError, Writer}, + io::{ + AssetReaderError, AssetWriterError, MissingAssetWriterError, + MissingProcessedAssetReaderError, MissingProcessedAssetWriterError, Writer, + }, meta::{AssetAction, AssetMeta, AssetMetaDyn, ProcessDependencyInfo, ProcessedInfo, Settings}, processor::AssetProcessor, saver::{AssetSaver, SavedAsset}, @@ -8,7 +11,7 @@ use crate::{ }; use bevy_utils::BoxedFuture; use serde::{Deserialize, Serialize}; -use std::{marker::PhantomData, path::PathBuf}; +use std::marker::PhantomData; use thiserror::Error; /// Asset "processor" logic that reads input asset bytes (stored on [`ProcessContext`]), processes the value in some way, @@ -70,20 +73,33 @@ pub struct LoadAndSaveSettings { /// An error that is encountered during [`Process::process`]. #[derive(Error, Debug)] pub enum ProcessError { - #[error("The asset source file for '{0}' does not exist")] - MissingAssetSource(PathBuf), - #[error(transparent)] - AssetSourceIoError(std::io::Error), #[error(transparent)] MissingAssetLoaderForExtension(#[from] MissingAssetLoaderForExtensionError), #[error(transparent)] MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError), #[error("The processor '{0}' does not exist")] MissingProcessor(String), + #[error("Encountered an AssetReader error for '{path}': {err}")] + AssetReaderError { + path: AssetPath<'static>, + err: AssetReaderError, + }, + #[error("Encountered an AssetWriter error for '{path}': {err}")] + AssetWriterError { + path: AssetPath<'static>, + err: AssetWriterError, + }, + #[error(transparent)] + MissingAssetWriterError(#[from] MissingAssetWriterError), + #[error(transparent)] + MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError), #[error(transparent)] - AssetWriterError(#[from] AssetWriterError), - #[error("Failed to read asset metadata {0:?}")] - ReadAssetMetaError(AssetReaderError), + MissingProcessedAssetWriterError(#[from] MissingProcessedAssetWriterError), + #[error("Failed to read asset metadata for {path}: {err}")] + ReadAssetMetaError { + path: AssetPath<'static>, + err: AssetReaderError, + }, #[error(transparent)] DeserializeMetaError(#[from] DeserializeMetaError), #[error(transparent)] diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index b4e8470b13c2d..9f9a2dd35a07f 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -2,7 +2,10 @@ mod info; use crate::{ folder::LoadedFolder, - io::{AssetReader, AssetReaderError, AssetSourceEvent, AssetWatcher, Reader}, + io::{ + AssetProvider, AssetProviderEvent, AssetProviderId, AssetProviders, AssetReader, + AssetReaderError, MissingAssetProviderError, MissingProcessedAssetReaderError, Reader, + }, loader::{AssetLoader, AssetLoaderError, ErasedAssetLoader, LoadContext, LoadedAsset}, meta::{ loader_settings_meta_transform, AssetActionMinimal, AssetMetaDyn, AssetMetaMinimal, @@ -48,52 +51,57 @@ pub(crate) struct AssetServerData { pub(crate) loaders: Arc>, asset_event_sender: Sender, asset_event_receiver: Receiver, - source_event_receiver: Receiver, - reader: Box, - _watcher: Option>, + providers: AssetProviders, + mode: AssetServerMode, +} + +/// The "asset mode" the server is currently in. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AssetServerMode { + /// This server loads unprocessed assets. + Unprocessed, + /// This server loads processed assets. + Processed, } impl AssetServer { /// Create a new instance of [`AssetServer`]. If `watch_for_changes` is true, the [`AssetReader`] storage will watch for changes to /// asset sources and hot-reload them. - pub fn new(reader: Box, watch_for_changes: bool) -> Self { - Self::new_with_loaders(reader, Default::default(), watch_for_changes) + pub fn new( + providers: AssetProviders, + mode: AssetServerMode, + watching_for_changes: bool, + ) -> Self { + Self::new_with_loaders(providers, Default::default(), mode, watching_for_changes) } pub(crate) fn new_with_loaders( - reader: Box, + providers: AssetProviders, loaders: Arc>, - watch_for_changes: bool, + mode: AssetServerMode, + watching_for_changes: bool, ) -> Self { let (asset_event_sender, asset_event_receiver) = crossbeam_channel::unbounded(); - let (source_event_sender, source_event_receiver) = crossbeam_channel::unbounded(); let mut infos = AssetInfos::default(); - let watcher = if watch_for_changes { - infos.watching_for_changes = true; - let watcher = reader.watch_for_changes(source_event_sender); - if watcher.is_none() { - error!("{}", CANNOT_WATCH_ERROR_MESSAGE); - } - watcher - } else { - None - }; + infos.watching_for_changes = watching_for_changes; Self { data: Arc::new(AssetServerData { - reader, - _watcher: watcher, + providers, + mode, asset_event_sender, asset_event_receiver, - source_event_receiver, loaders, infos: RwLock::new(infos), }), } } - /// Returns the primary [`AssetReader`]. - pub fn reader(&self) -> &dyn AssetReader { - &*self.data.reader + /// Retrieves the [`AssetReader`] for the given `provider`. + pub fn get_provider<'a>( + &'a self, + provider: impl Into>, + ) -> Result<&'a AssetProvider, MissingAssetProviderError> { + self.data.providers.get(provider.into()) } /// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used. @@ -450,28 +458,30 @@ impl AssetServer { /// contain handles to all assets in the folder. You can wait for all assets to load by checking the [`LoadedFolder`]'s /// [`RecursiveDependencyLoadState`]. #[must_use = "not using the returned strong handle may result in the unexpected release of the assets"] - pub fn load_folder(&self, path: impl AsRef) -> Handle { + pub fn load_folder<'a>(&self, path: impl Into>) -> Handle { let handle = { let mut infos = self.data.infos.write(); infos.create_loading_handle::() }; let id = handle.id().untyped(); + let path = path.into().into_owned(); fn load_folder<'a>( path: &'a Path, + reader: &'a dyn AssetReader, server: &'a AssetServer, handles: &'a mut Vec, ) -> bevy_utils::BoxedFuture<'a, Result<(), AssetLoadError>> { Box::pin(async move { - let is_dir = server.reader().is_directory(path).await?; + let is_dir = reader.is_directory(path).await?; if is_dir { - let mut path_stream = server.reader().read_directory(path.as_ref()).await?; + let mut path_stream = reader.read_directory(path.as_ref()).await?; while let Some(child_path) = path_stream.next().await { - if server.reader().is_directory(&child_path).await? { - load_folder(&child_path, server, handles).await?; + if reader.is_directory(&child_path).await? { + load_folder(&child_path, reader, server, handles).await?; } else { let path = child_path.to_str().expect("Path should be a valid string."); - match server.load_untyped_async(AssetPath::new(path)).await { + match server.load_untyped_async(AssetPath::parse(path)).await { Ok(handle) => handles.push(handle), // skip assets that cannot be loaded Err( @@ -488,11 +498,32 @@ impl AssetServer { } let server = self.clone(); - let owned_path = path.as_ref().to_owned(); IoTaskPool::get() .spawn(async move { + let Ok(provider) = server.get_provider(path.provider()) else { + error!( + "Failed to load {path}. AssetProvider {:?} does not exist", + path.provider() + ); + return; + }; + + let asset_reader = match server.data.mode { + AssetServerMode::Unprocessed { .. } => provider.reader(), + AssetServerMode::Processed { .. } => match provider.processed_reader() { + Ok(reader) => reader, + Err(_) => { + error!( + "Failed to load {path}. AssetProvider {:?} does not have a processed AssetReader", + path.provider() + ); + return; + } + }, + }; + let mut handles = Vec::new(); - match load_folder(&owned_path, &server, &mut handles).await { + match load_folder(path.path(), asset_reader, &server, &mut handles).await { Ok(_) => server.send_asset_event(InternalAssetEvent::Loaded { id, loaded_asset: LoadedAsset::new_with_dependencies( @@ -586,6 +617,11 @@ impl AssetServer { Some(info.path.as_ref()?.clone()) } + /// Returns the [`AssetServerMode`] this server is currently in. + pub fn mode(&self) -> AssetServerMode { + self.data.mode + } + /// Pre-register a loader that will later be added. /// /// Assets loaded with matching extensions will be blocked until the @@ -641,12 +677,17 @@ impl AssetServer { ), AssetLoadError, > { + let provider = self.get_provider(asset_path.provider())?; // NOTE: We grab the asset byte reader first to ensure this is transactional for AssetReaders like ProcessorGatedReader // The asset byte reader will "lock" the processed asset, preventing writes for the duration of the lock. // Then the meta reader, if meta exists, will correspond to the meta for the current "version" of the asset. // See ProcessedAssetInfo::file_transaction_lock for more context - let reader = self.data.reader.read(asset_path.path()).await?; - match self.data.reader.read_meta_bytes(asset_path.path()).await { + let asset_reader = match self.data.mode { + AssetServerMode::Unprocessed { .. } => provider.reader(), + AssetServerMode::Processed { .. } => provider.processed_reader()?, + }; + let reader = asset_reader.read(asset_path.path()).await?; + match asset_reader.read_meta_bytes(asset_path.path()).await { Ok(meta_bytes) => { // TODO: this isn't fully minimal yet. we only need the loader let minimal: AssetMetaMinimal = ron::de::from_bytes(&meta_bytes).map_err(|e| { @@ -656,19 +697,19 @@ impl AssetServer { AssetActionMinimal::Load { loader } => loader, AssetActionMinimal::Process { .. } => { return Err(AssetLoadError::CannotLoadProcessedAsset { - path: asset_path.clone().into_owned(), + path: asset_path.clone_owned(), }) } AssetActionMinimal::Ignore => { return Err(AssetLoadError::CannotLoadIgnoredAsset { - path: asset_path.clone().into_owned(), + path: asset_path.clone_owned(), }) } }; let loader = self.get_asset_loader_with_type_name(&loader_name).await?; let meta = loader.deserialize_meta(&meta_bytes).map_err(|e| { AssetLoadError::AssetLoaderError { - path: asset_path.clone().into_owned(), + path: asset_path.clone_owned(), loader: loader.type_name(), error: AssetLoaderError::DeserializeMeta(e), } @@ -695,7 +736,7 @@ impl AssetServer { populate_hashes: bool, ) -> Result { // TODO: experiment with this - let asset_path = asset_path.clone().into_owned(); + let asset_path = asset_path.clone_owned(); let load_context = LoadContext::new(self, asset_path.clone(), load_dependencies, populate_hashes); loader.load(reader, meta, load_context).await.map_err(|e| { @@ -747,17 +788,37 @@ pub fn handle_internal_asset_events(world: &mut World) { } let mut paths_to_reload = HashSet::new(); - for event in server.data.source_event_receiver.try_iter() { + let mut handle_event = |provider: AssetProviderId<'static>, event: AssetProviderEvent| { match event { // TODO: if the asset was processed and the processed file was changed, the first modified event // should be skipped? - AssetSourceEvent::ModifiedAsset(path) | AssetSourceEvent::ModifiedMeta(path) => { - let path = AssetPath::from_path(path); + AssetProviderEvent::ModifiedAsset(path) + | AssetProviderEvent::ModifiedMeta(path) => { + let path = AssetPath::from(path).with_provider(provider); queue_ancestors(&path, &infos, &mut paths_to_reload); paths_to_reload.insert(path); } _ => {} } + }; + + for provider in server.data.providers.iter() { + match server.data.mode { + AssetServerMode::Unprocessed { .. } => { + if let Some(receiver) = provider.event_receiver() { + for event in receiver.try_iter() { + handle_event(provider.id(), event); + } + } + } + AssetServerMode::Processed { .. } => { + if let Some(receiver) = provider.processed_event_receiver() { + for event in receiver.try_iter() { + handle_event(provider.id(), event); + } + } + } + } } for path in paths_to_reload { @@ -853,6 +914,10 @@ pub enum AssetLoadError { MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError), #[error(transparent)] AssetReaderError(#[from] AssetReaderError), + #[error(transparent)] + MissingAssetProviderError(#[from] MissingAssetProviderError), + #[error(transparent)] + MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError), #[error("Encountered an error while reading asset metadata bytes")] AssetMetaReadError, #[error(transparent)] @@ -902,8 +967,3 @@ impl std::fmt::Debug for AssetServer { .finish() } } - -pub(crate) static CANNOT_WATCH_ERROR_MESSAGE: &str = - "Cannot watch for changes because the current `AssetReader` does not support it. If you are using \ - the FileAssetReader (the default on desktop platforms), enabling the filesystem_watcher feature will \ - add this functionality."; diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index c00edd77eabeb..987d85a91aa78 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -1159,7 +1159,7 @@ async fn load_buffers( Err(()) => { // TODO: Remove this and add dep let buffer_path = load_context.path().parent().unwrap().join(uri); - load_context.read_asset_bytes(&buffer_path).await? + load_context.read_asset_bytes(buffer_path).await? } }; buffer_data.push(buffer_bytes); diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1a71d43819078..f8c4f2075c4dc 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -102,7 +102,10 @@ glam_assert = ["bevy_math/glam_assert"] default_font = ["bevy_text?/default_font"] # Enables watching the filesystem for Bevy Asset hot-reloading -filesystem_watcher = ["bevy_asset?/filesystem_watcher"] +file_watcher = ["bevy_asset?/file_watcher"] + +# Enables watching rust src files for Bevy Asset hot-reloading +rust_src_watcher = ["bevy_asset?/rust_src_watcher"] [dependencies] # bevy diff --git a/crates/bevy_utils/src/cow_arc.rs b/crates/bevy_utils/src/cow_arc.rs index 31a204863d3d3..c78318323588b 100644 --- a/crates/bevy_utils/src/cow_arc.rs +++ b/crates/bevy_utils/src/cow_arc.rs @@ -42,7 +42,7 @@ where &'a T: Into>, { /// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". - /// If it is already an "owned [`Arc`]", it will remain unchanged. + /// If it is already a [`CowArc::Owned`] or a [`CowArc::Static`], it will remain unchanged. #[inline] pub fn into_owned(self) -> CowArc<'static, T> { match self { @@ -51,6 +51,14 @@ where CowArc::Owned(value) => CowArc::Owned(value), } } + + /// Clones into an owned [`CowArc<'static>`]. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". + /// If it is already a [`CowArc::Owned`] or [`CowArc::Static`], the value will be cloned. + /// This is equivalent to `.clone().into_owned()`. + #[inline] + pub fn clone_owned(&self) -> CowArc<'static, T> { + self.clone().into_owned() + } } impl<'a, T: ?Sized> Clone for CowArc<'a, T> { diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 9c7ee1701fa5d..90da7bd3ce7a6 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -51,7 +51,7 @@ The default feature set enables most of the expected features of a game engine, |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| |dynamic_linking|Force dynamic linking, which improves iterative compile times| |exr|EXR image format support| -|filesystem_watcher|Enables watching the filesystem for Bevy Asset hot-reloading| +|file_watcher|Enables watching the filesystem for Bevy Asset hot-reloading| |flac|FLAC audio format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| |jpeg|JPEG image format support| diff --git a/examples/asset/custom_asset_reader.rs b/examples/asset/custom_asset_reader.rs index 99ad8fe07e973..356839eafc3b0 100644 --- a/examples/asset/custom_asset_reader.rs +++ b/examples/asset/custom_asset_reader.rs @@ -4,7 +4,7 @@ use bevy::{ asset::io::{ - file::FileAssetReader, AssetProvider, AssetProviders, AssetReader, AssetReaderError, + file::FileAssetReader, AssetProvider, AssetProviderId, AssetReader, AssetReaderError, PathStream, Reader, }, prelude::*, @@ -43,13 +43,6 @@ impl AssetReader for CustomAssetReader { ) -> BoxedFuture<'a, Result> { self.0.is_directory(path) } - - fn watch_for_changes( - &self, - event_sender: crossbeam_channel::Sender, - ) -> Option> { - self.0.watch_for_changes(event_sender) - } } /// A plugins that registers our new asset reader @@ -57,24 +50,17 @@ struct CustomAssetReaderPlugin; impl Plugin for CustomAssetReaderPlugin { fn build(&self, app: &mut App) { - let mut asset_providers = app - .world - .get_resource_or_insert_with::(Default::default); - asset_providers.insert_reader("CustomAssetReader", || { - Box::new(CustomAssetReader(FileAssetReader::new("assets"))) - }); + app.register_asset_provider( + AssetProviderId::Default, + AssetProvider::build() + .with_reader(|| Box::new(CustomAssetReader(FileAssetReader::new("assets")))), + ); } } fn main() { App::new() - .add_plugins(( - CustomAssetReaderPlugin, - DefaultPlugins.set(AssetPlugin::Unprocessed { - source: AssetProvider::Custom("CustomAssetReader".to_string()), - watch_for_changes: false, - }), - )) + .add_plugins((CustomAssetReaderPlugin, DefaultPlugins)) .add_systems(Startup, setup) .run(); } diff --git a/examples/asset/hot_asset_reloading.rs b/examples/asset/hot_asset_reloading.rs index 559087b704c4f..11cb7d916b1b5 100644 --- a/examples/asset/hot_asset_reloading.rs +++ b/examples/asset/hot_asset_reloading.rs @@ -1,12 +1,15 @@ //! Hot reloading allows you to modify assets files to be immediately reloaded while your game is //! running. This lets you immediately see the results of your changes without restarting the game. //! This example illustrates hot reloading mesh changes. +//! +//! Note that hot asset reloading requires the [`AssetWatcher`](bevy::asset::io::AssetWatcher) to be enabled +//! for your current platform. For desktop platforms, enable the `file_watcher` cargo feature. use bevy::prelude::*; fn main() { App::new() - .add_plugins(DefaultPlugins.set(AssetPlugin::default().watch_for_changes())) + .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .run(); } diff --git a/examples/asset/processing/e.txt b/examples/asset/processing/e.txt new file mode 100644 index 0000000000000..9cbe6ea56f225 --- /dev/null +++ b/examples/asset/processing/e.txt @@ -0,0 +1 @@ +e \ No newline at end of file diff --git a/examples/asset/processing/processing.rs b/examples/asset/processing/processing.rs index 400508c02cfca..6fce1a9b081fe 100644 --- a/examples/asset/processing/processing.rs +++ b/examples/asset/processing/processing.rs @@ -2,8 +2,9 @@ use bevy::{ asset::{ - io::{AssetProviders, Reader, Writer}, + io::{Reader, Writer}, processor::LoadAndSave, + rust_src_asset, saver::{AssetSaver, SavedAsset}, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext, }, @@ -15,15 +16,6 @@ use serde::{Deserialize, Serialize}; fn main() { App::new() - .insert_resource( - // This is just overriding the default paths to scope this to the correct example folder - // You can generally skip this in your own projects - AssetProviders::default() - .with_default_file_source("examples/asset/processing/assets".to_string()) - .with_default_file_destination( - "examples/asset/processing/imported_assets".to_string(), - ), - ) // Enabling `processed_dev` will configure the AssetPlugin to use asset processing. // This will run the AssetProcessor in the background, which will listen for changes to // the `assets` folder, run them through configured asset processors, and write the results @@ -31,9 +23,18 @@ fn main() { // // The AssetProcessor will create `.meta` files automatically for assets in the `assets` folder, // which can then be used to configure how the asset will be processed. - .add_plugins((DefaultPlugins.set(AssetPlugin::processed_dev()), TextPlugin)) - // This is what a deployed app should use - // .add_plugins((DefaultPlugins.set(AssetPlugin::processed()), TextPlugin)) + .add_plugins(( + DefaultPlugins.set(AssetPlugin { + // This is just overriding the default paths to scope this to the correct example folder + // You can generally skip this in your own projects + mode: AssetMode::ProcessedDev, + file_path: "examples/asset/processing/assets".to_string(), + processed_file_path: "examples/asset/processing/imported_assets/Default" + .to_string(), + ..default() + }), + TextPlugin, + )) .add_systems(Startup, setup) .add_systems(Update, print_text) .run(); @@ -50,6 +51,7 @@ pub struct TextPlugin; impl Plugin for TextPlugin { fn build(&self, app: &mut App) { + rust_src_asset!(app, "examples/asset/processing/", "e.txt"); app.init_asset::() .init_asset::() .register_asset_loader(CoolTextLoader) @@ -184,6 +186,7 @@ struct TextAssets { b: Handle, c: Handle, d: Handle, + e: Handle, } fn setup(mut commands: Commands, assets: Res) { @@ -194,6 +197,7 @@ fn setup(mut commands: Commands, assets: Res) { b: assets.load("foo/b.cool.ron"), c: assets.load("foo/c.cool.ron"), d: assets.load("d.cool.ron"), + e: assets.load("rust_src://asset_processing/e.txt"), }); } @@ -205,6 +209,7 @@ fn print_text(handles: Res, texts: Res>) { println!(" b: {:?}", texts.get(&handles.b)); println!(" c: {:?}", texts.get(&handles.c)); println!(" d: {:?}", texts.get(&handles.d)); + println!(" e: {:?}", texts.get(&handles.e)); println!("(You can modify source assets and their .meta files to hot-reload changes!)"); println!(); } diff --git a/examples/scene/scene.rs b/examples/scene/scene.rs index da09648c244e3..7f3af48996323 100644 --- a/examples/scene/scene.rs +++ b/examples/scene/scene.rs @@ -4,7 +4,7 @@ use std::{fs::File, io::Write}; fn main() { App::new() - .add_plugins(DefaultPlugins.set(AssetPlugin::default().watch_for_changes())) + .add_plugins(DefaultPlugins) .register_type::() .register_type::() .register_type::() @@ -75,7 +75,8 @@ fn load_scene_system(mut commands: Commands, asset_server: Res) { } // This system logs all ComponentA components in our world. Try making a change to a ComponentA in -// load_scene_example.scn. You should immediately see the changes appear in the console. +// load_scene_example.scn. If you enable the `file_watcher` cargo feature you should immediately see +// the changes appear in the console whenever you make a change. fn log_system( query: Query<(Entity, &ComponentA), Changed>, res: Option>, diff --git a/examples/shader/post_processing.rs b/examples/shader/post_processing.rs index fd6dfc3422173..bc3009a80949a 100644 --- a/examples/shader/post_processing.rs +++ b/examples/shader/post_processing.rs @@ -36,10 +36,7 @@ use bevy::{ fn main() { App::new() - .add_plugins(( - DefaultPlugins.set(AssetPlugin::default().watch_for_changes()), - PostProcessPlugin, - )) + .add_plugins((DefaultPlugins, PostProcessPlugin)) .add_systems(Startup, setup) .add_systems(Update, (rotate, update_settings)) .run(); diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index 2dbfc97b1c66d..f400513de5772 100644 --- a/examples/tools/scene_viewer/main.rs +++ b/examples/tools/scene_viewer/main.rs @@ -6,7 +6,6 @@ //! With no arguments it will load the `FlightHelmet` glTF model from the repository assets subdirectory. use bevy::{ - asset::io::AssetProviders, math::Vec3A, prelude::*, render::primitives::{Aabb, Sphere}, @@ -29,9 +28,6 @@ fn main() { color: Color::WHITE, brightness: 1.0 / 5.0f32, }) - .insert_resource(AssetProviders::default().with_default_file_source( - std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()), - )) .add_plugins(( DefaultPlugins .set(WindowPlugin { @@ -41,7 +37,10 @@ fn main() { }), ..default() }) - .set(AssetPlugin::default().watch_for_changes()), + .set(AssetPlugin { + file_path: std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()), + ..default() + }), CameraControllerPlugin, SceneViewerPlugin, MorphViewerPlugin, From 9a3d667bab1a047b3fbd9615089b97b8b2ce3148 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 20 Sep 2023 20:04:38 -0700 Subject: [PATCH 02/11] Provider -> Source --- crates/bevy_asset/src/io/file/file_watcher.rs | 63 ++-- crates/bevy_asset/src/io/mod.rs | 8 +- crates/bevy_asset/src/io/processor_gated.rs | 14 +- crates/bevy_asset/src/io/rust_src/mod.rs | 22 +- .../src/io/rust_src/rust_src_watcher.rs | 12 +- .../src/io/{provider.rs => source.rs} | 314 +++++++++--------- crates/bevy_asset/src/lib.rs | 85 ++--- crates/bevy_asset/src/loader.rs | 10 +- crates/bevy_asset/src/path.rs | 97 +++--- crates/bevy_asset/src/processor/mod.rs | 252 +++++++------- crates/bevy_asset/src/server/mod.rs | 67 ++-- examples/asset/custom_asset_reader.rs | 8 +- 12 files changed, 460 insertions(+), 492 deletions(-) rename crates/bevy_asset/src/io/{provider.rs => source.rs} (56%) diff --git a/crates/bevy_asset/src/io/file/file_watcher.rs b/crates/bevy_asset/src/io/file/file_watcher.rs index d7aedd4bae0e6..25ee5ae735982 100644 --- a/crates/bevy_asset/src/io/file/file_watcher.rs +++ b/crates/bevy_asset/src/io/file/file_watcher.rs @@ -1,4 +1,4 @@ -use crate::io::{AssetProviderEvent, AssetWatcher}; +use crate::io::{AssetSourceEvent, AssetWatcher}; use anyhow::Result; use bevy_log::error; use bevy_utils::Duration; @@ -21,7 +21,7 @@ pub struct FileWatcher { impl FileWatcher { pub fn new( root: PathBuf, - sender: Sender, + sender: Sender, debounce_wait_time: Duration, ) -> Result { let root = super::get_base_path().join(root); @@ -77,22 +77,20 @@ pub(crate) fn new_asset_event_debouncer( if is_meta { handler.handle( &event.paths, - AssetProviderEvent::AddedMeta(path), + AssetSourceEvent::AddedMeta(path), ); } else { handler.handle( &event.paths, - AssetProviderEvent::AddedAsset(path), + AssetSourceEvent::AddedAsset(path), ); } } } notify::EventKind::Create(CreateKind::Folder) => { if let Some((path, _)) = handler.get_path(&event.paths[0]) { - handler.handle( - &event.paths, - AssetProviderEvent::AddedFolder(path), - ); + handler + .handle(&event.paths, AssetSourceEvent::AddedFolder(path)); } } notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => { @@ -100,12 +98,12 @@ pub(crate) fn new_asset_event_debouncer( if is_meta { handler.handle( &event.paths, - AssetProviderEvent::ModifiedMeta(path), + AssetSourceEvent::ModifiedMeta(path), ); } else { handler.handle( &event.paths, - AssetProviderEvent::ModifiedAsset(path), + AssetSourceEvent::ModifiedAsset(path), ); } } @@ -119,7 +117,7 @@ pub(crate) fn new_asset_event_debouncer( if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) { handler.handle( &event.paths, - AssetProviderEvent::RemovedUnknown { path, is_meta }, + AssetSourceEvent::RemovedUnknown { path, is_meta }, ); } } @@ -127,11 +125,11 @@ pub(crate) fn new_asset_event_debouncer( | notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { if let Some((path, is_meta)) = handler.get_path(&event.paths[0]) { let asset_event = if event.paths[0].is_dir() { - AssetProviderEvent::AddedFolder(path) + AssetSourceEvent::AddedFolder(path) } else if is_meta { - AssetProviderEvent::AddedMeta(path) + AssetSourceEvent::AddedMeta(path) } else { - AssetProviderEvent::AddedAsset(path) + AssetSourceEvent::AddedAsset(path) }; handler.handle(&event.paths, asset_event); } @@ -151,7 +149,7 @@ pub(crate) fn new_asset_event_debouncer( if event.paths[1].is_dir() { handler.handle( &event.paths, - AssetProviderEvent::RenamedFolder { + AssetSourceEvent::RenamedFolder { old: old_path, new: new_path, }, @@ -161,7 +159,7 @@ pub(crate) fn new_asset_event_debouncer( (true, true) => { handler.handle( &event.paths, - AssetProviderEvent::RenamedMeta { + AssetSourceEvent::RenamedMeta { old: old_path, new: new_path, }, @@ -170,7 +168,7 @@ pub(crate) fn new_asset_event_debouncer( (false, false) => { handler.handle( &event.paths, - AssetProviderEvent::RenamedAsset { + AssetSourceEvent::RenamedAsset { old: old_path, new: new_path, }, @@ -197,14 +195,12 @@ pub(crate) fn new_asset_event_debouncer( if event.paths[0].is_dir() { // modified folder means nothing in this case } else if is_meta { - handler.handle( - &event.paths, - AssetProviderEvent::ModifiedMeta(path), - ); + handler + .handle(&event.paths, AssetSourceEvent::ModifiedMeta(path)); } else { handler.handle( &event.paths, - AssetProviderEvent::ModifiedAsset(path), + AssetSourceEvent::ModifiedAsset(path), ); }; } @@ -214,23 +210,18 @@ pub(crate) fn new_asset_event_debouncer( continue; }; if is_meta { - handler.handle( - &event.paths, - AssetProviderEvent::RemovedMeta(path), - ); + handler + .handle(&event.paths, AssetSourceEvent::RemovedMeta(path)); } else { - handler.handle( - &event.paths, - AssetProviderEvent::RemovedAsset(path), - ); + handler + .handle(&event.paths, AssetSourceEvent::RemovedAsset(path)); } } notify::EventKind::Remove(RemoveKind::Folder) => { let Some((path, _)) = handler.get_path(&event.paths[0]) else { continue; }; - handler - .handle(&event.paths, AssetProviderEvent::RemovedFolder(path)); + handler.handle(&event.paths, AssetSourceEvent::RemovedFolder(path)); } _ => {} } @@ -248,9 +239,9 @@ pub(crate) fn new_asset_event_debouncer( } pub(crate) struct FileEventHandler { - sender: crossbeam_channel::Sender, + sender: crossbeam_channel::Sender, root: PathBuf, - last_event: Option, + last_event: Option, } impl FilesystemEventHandler for FileEventHandler { @@ -261,7 +252,7 @@ impl FilesystemEventHandler for FileEventHandler { Some(get_asset_path(&self.root, absolute_path)) } - fn handle(&mut self, _absolute_paths: &[PathBuf], event: AssetProviderEvent) { + fn handle(&mut self, _absolute_paths: &[PathBuf], event: AssetSourceEvent) { if self.last_event.as_ref() != Some(&event) { self.last_event = Some(event.clone()); self.sender.send(event).unwrap(); @@ -276,5 +267,5 @@ pub(crate) trait FilesystemEventHandler: Send + Sync + 'static { /// true if the `absolute_path` corresponds to a meta file. fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)>; /// Handle the given event - fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetProviderEvent); + fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetSourceEvent); } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 521f6b5734b38..75e9f24f44597 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -9,10 +9,10 @@ pub mod rust_src; #[cfg(target_arch = "wasm32")] pub mod wasm; -mod provider; +mod source; pub use futures_lite::{AsyncReadExt, AsyncWriteExt}; -pub use provider::*; +pub use source::*; use bevy_utils::BoxedFuture; use futures_io::{AsyncRead, AsyncWrite}; @@ -173,7 +173,7 @@ pub trait AssetWriter: Send + Sync + 'static { /// An "asset source change event" that occurs whenever asset (or asset metadata) is created/added/removed #[derive(Clone, Debug, PartialEq, Eq)] -pub enum AssetProviderEvent { +pub enum AssetSourceEvent { /// An asset at this path was added. AddedAsset(PathBuf), /// An asset at this path was modified. @@ -209,7 +209,7 @@ pub enum AssetProviderEvent { }, } -/// A handle to an "asset watcher" process, that will listen for and emit [`AssetProviderEvent`] values for as long as +/// A handle to an "asset watcher" process, that will listen for and emit [`AssetSourceEvent`] values for as long as /// [`AssetWatcher`] has not been dropped. /// /// See [`AssetReader::watch_for_changes`]. diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index ec7f65eda4ba5..6e238b97ff450 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -1,5 +1,5 @@ use crate::{ - io::{AssetProviderId, AssetReader, AssetReaderError, PathStream, Reader}, + io::{AssetReader, AssetReaderError, AssetSourceId, PathStream, Reader}, processor::{AssetProcessorData, ProcessStatus}, AssetPath, }; @@ -16,19 +16,19 @@ use std::{path::Path, pin::Pin, sync::Arc}; /// [`AssetProcessor`]: crate::processor::AssetProcessor pub struct ProcessorGatedReader { reader: Box, - provider: AssetProviderId<'static>, + source: AssetSourceId<'static>, processor_data: Arc, } impl ProcessorGatedReader { /// Creates a new [`ProcessorGatedReader`]. pub fn new( - provider: AssetProviderId<'static>, + source: AssetSourceId<'static>, reader: Box, processor_data: Arc, ) -> Self { Self { - provider, + source, processor_data, reader, } @@ -54,8 +54,7 @@ impl AssetReader for ProcessorGatedReader { path: &'a Path, ) -> BoxedFuture<'a, Result>, AssetReaderError>> { Box::pin(async move { - let asset_path = - AssetPath::from(path.to_path_buf()).with_provider(self.provider.clone()); + let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); trace!("Waiting for processing to finish before reading {asset_path}"); let process_result = self .processor_data @@ -81,8 +80,7 @@ impl AssetReader for ProcessorGatedReader { path: &'a Path, ) -> BoxedFuture<'a, Result>, AssetReaderError>> { Box::pin(async move { - let asset_path = - AssetPath::from(path.to_path_buf()).with_provider(self.provider.clone()); + let asset_path = AssetPath::from(path.to_path_buf()).with_source(self.source.clone()); trace!("Waiting for processing to finish before reading meta for {asset_path}",); let process_result = self .processor_data diff --git a/crates/bevy_asset/src/io/rust_src/mod.rs b/crates/bevy_asset/src/io/rust_src/mod.rs index c2271e44b7b7e..6a286851ca522 100644 --- a/crates/bevy_asset/src/io/rust_src/mod.rs +++ b/crates/bevy_asset/src/io/rust_src/mod.rs @@ -6,7 +6,7 @@ pub use rust_src_watcher::*; use crate::io::{ memory::{Dir, MemoryAssetReader, Value}, - AssetProvider, AssetProviderBuilders, + AssetSource, AssetSourceBuilders, }; use bevy_ecs::system::Resource; use std::path::{Path, PathBuf}; @@ -28,7 +28,7 @@ pub struct RustSrcRegistry { impl RustSrcRegistry { /// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `rust_src` - /// asset provider. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` + /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` /// or a [`Vec`]. #[allow(unused)] pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into) { @@ -41,7 +41,7 @@ impl RustSrcRegistry { /// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `rust_src` - /// asset provider. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` + /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` /// or a [`Vec`]. #[allow(unused)] pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into) { @@ -52,13 +52,13 @@ impl RustSrcRegistry { self.dir.insert_meta(asset_path, value); } - /// Registers a `rust_src` [`AssetProvider`] that uses this [`RustSrcRegistry`]. - // NOTE: unused_mut because rust_src_watcher feature is the only mutable consumer of `let mut provider` + /// Registers a `rust_src` [`AssetSource`] that uses this [`RustSrcRegistry`]. + // NOTE: unused_mut because rust_src_watcher feature is the only mutable consumer of `let mut source` #[allow(unused_mut)] - pub fn register_provider(&self, providers: &mut AssetProviderBuilders) { + pub fn register_source(&self, sources: &mut AssetSourceBuilders) { let dir = self.dir.clone(); let processed_dir = self.dir.clone(); - let mut provider = AssetProvider::build() + let mut source = AssetSource::build() .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() })) .with_processed_reader(move || { Box::new(MemoryAssetReader { @@ -72,7 +72,7 @@ impl RustSrcRegistry { let dir = self.dir.clone(); let processed_root_paths = self.root_paths.clone(); let processd_dir = self.dir.clone(); - provider = provider + source = source .with_watcher(move |sender| { Some(Box::new(RustSrcWatcher::new( dir.clone(), @@ -90,7 +90,7 @@ impl RustSrcRegistry { ))) }); } - providers.insert(RUST_SRC, provider); + sources.insert(RUST_SRC, source); } } @@ -115,7 +115,7 @@ macro_rules! rust_src_path { } /// Creates a new `rust_src` asset by embedding the bytes of the given path into the current binary -/// and registering those bytes with the `rust_src` [`AssetProvider`]. +/// and registering those bytes with the `rust_src` [`AssetSource`]. /// /// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second. /// @@ -152,7 +152,7 @@ macro_rules! rust_src_path { /// ``` /// /// Some things to note in the path: -/// 1. The non-default `rust_src:://` [`AssetProvider`] +/// 1. The non-default `rust_src:://` [`AssetSource`] /// 2. `src` is trimmed from the path /// /// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in diff --git a/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs b/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs index a58e181e2631d..b1d34a4e0b677 100644 --- a/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs +++ b/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs @@ -1,7 +1,7 @@ use crate::io::{ file::{get_asset_path, get_base_path, new_asset_event_debouncer, FilesystemEventHandler}, memory::Dir, - AssetProviderEvent, AssetWatcher, + AssetSourceEvent, AssetWatcher, }; use bevy_log::warn; use bevy_utils::{Duration, HashMap}; @@ -22,7 +22,7 @@ impl RustSrcWatcher { pub fn new( dir: Dir, root_paths: Arc>>, - sender: crossbeam_channel::Sender, + sender: crossbeam_channel::Sender, debounce_wait_time: Duration, ) -> Self { let root = get_base_path(); @@ -44,11 +44,11 @@ impl AssetWatcher for RustSrcWatcher {} /// binary-embedded Rust source files. This will read the contents of changed files from the file system and overwrite /// the initial static bytes from the file embedded in the binary. pub(crate) struct RustSrcEventHandler { - sender: crossbeam_channel::Sender, + sender: crossbeam_channel::Sender, root_paths: Arc>>, root: PathBuf, dir: Dir, - last_event: Option, + last_event: Option, } impl FilesystemEventHandler for RustSrcEventHandler { fn begin(&mut self) { @@ -63,9 +63,9 @@ impl FilesystemEventHandler for RustSrcEventHandler { Some((final_path, false)) } - fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetProviderEvent) { + fn handle(&mut self, absolute_paths: &[PathBuf], event: AssetSourceEvent) { if self.last_event.as_ref() != Some(&event) { - if let AssetProviderEvent::ModifiedAsset(path) = &event { + if let AssetSourceEvent::ModifiedAsset(path) = &event { if let Ok(file) = File::open(&absolute_paths[0]) { let mut reader = BufReader::new(file); let mut buffer = Vec::new(); diff --git a/crates/bevy_asset/src/io/provider.rs b/crates/bevy_asset/src/io/source.rs similarity index 56% rename from crates/bevy_asset/src/io/provider.rs rename to crates/bevy_asset/src/io/source.rs index 9ed03cc674a31..367e5980cb30d 100644 --- a/crates/bevy_asset/src/io/provider.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -1,6 +1,6 @@ use crate::{ io::{ - processor_gated::ProcessorGatedReader, AssetProviderEvent, AssetReader, AssetWatcher, + processor_gated::ProcessorGatedReader, AssetReader, AssetSourceEvent, AssetWatcher, AssetWriter, }, processor::AssetProcessorData, @@ -11,112 +11,110 @@ use bevy_utils::{CowArc, Duration, HashMap}; use std::{fmt::Display, hash::Hash, sync::Arc}; use thiserror::Error; -/// A reference to an "asset provider", which maps to an [`AssetReader`] and/or [`AssetWriter`]. +/// A reference to an "asset source", which maps to an [`AssetReader`] and/or [`AssetWriter`]. /// -/// * [`AssetProviderId::Default`] corresponds to "default asset paths" that don't specify a provider: `/path/to/asset.png` -/// * [`AssetProviderId::Name`] corresponds to asset paths that _do_ specify a provider: `remote://path/to/asset.png`, where `remote` is the name. +/// * [`AssetSourceId::Default`] corresponds to "default asset paths" that don't specify a source: `/path/to/asset.png` +/// * [`AssetSourceId::Name`] corresponds to asset paths that _do_ specify a source: `remote://path/to/asset.png`, where `remote` is the name. #[derive(Default, Clone, Debug, Eq)] -pub enum AssetProviderId<'a> { - /// The default asset provider. +pub enum AssetSourceId<'a> { + /// The default asset source. #[default] Default, - /// A non-default named asset provider. + /// A non-default named asset source. Name(CowArc<'a, str>), } -impl<'a> Display for AssetProviderId<'a> { +impl<'a> Display for AssetSourceId<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.as_str() { - None => write!(f, "AssetProviderId::Default"), - Some(v) => write!(f, "AssetProviderId::Name({v})"), + None => write!(f, "AssetSourceId::Default"), + Some(v) => write!(f, "AssetSourceId::Name({v})"), } } } -impl<'a> AssetProviderId<'a> { - /// Creates a new [`AssetProviderId`] - pub fn new(provider: Option>>) -> AssetProviderId<'a> { - match provider { - Some(provider) => AssetProviderId::Name(provider.into()), - None => AssetProviderId::Default, +impl<'a> AssetSourceId<'a> { + /// Creates a new [`AssetSourceId`] + pub fn new(source: Option>>) -> AssetSourceId<'a> { + match source { + Some(source) => AssetSourceId::Name(source.into()), + None => AssetSourceId::Default, } } - /// Returns [`None`] if this is [`AssetProviderId::Default`] and [`Some`] containing the - /// the name if this is [`AssetProviderId::Name`]. + /// Returns [`None`] if this is [`AssetSourceId::Default`] and [`Some`] containing the + /// the name if this is [`AssetSourceId::Name`]. pub fn as_str(&self) -> Option<&str> { match self { - AssetProviderId::Default => None, - AssetProviderId::Name(v) => Some(v), + AssetSourceId::Default => None, + AssetSourceId::Name(v) => Some(v), } } /// If this is not already an owned / static id, create one. Otherwise, it will return itself (with a static lifetime). - pub fn into_owned(self) -> AssetProviderId<'static> { + pub fn into_owned(self) -> AssetSourceId<'static> { match self { - AssetProviderId::Default => AssetProviderId::Default, - AssetProviderId::Name(v) => AssetProviderId::Name(v.into_owned()), + AssetSourceId::Default => AssetSourceId::Default, + AssetSourceId::Name(v) => AssetSourceId::Name(v.into_owned()), } } - /// Clones into an owned [`AssetProviderId<'static>`]. + /// Clones into an owned [`AssetSourceId<'static>`]. /// This is equivalent to `.clone().into_owned()`. #[inline] - pub fn clone_owned(&self) -> AssetProviderId<'static> { + pub fn clone_owned(&self) -> AssetSourceId<'static> { self.clone().into_owned() } } -impl From<&'static str> for AssetProviderId<'static> { +impl From<&'static str> for AssetSourceId<'static> { fn from(value: &'static str) -> Self { - AssetProviderId::Name(value.into()) + AssetSourceId::Name(value.into()) } } -impl<'a, 'b> From<&'a AssetProviderId<'b>> for AssetProviderId<'b> { - fn from(value: &'a AssetProviderId<'b>) -> Self { +impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> { + fn from(value: &'a AssetSourceId<'b>) -> Self { value.clone() } } -impl From> for AssetProviderId<'static> { +impl From> for AssetSourceId<'static> { fn from(value: Option<&'static str>) -> Self { match value { - Some(value) => AssetProviderId::Name(value.into()), - None => AssetProviderId::Default, + Some(value) => AssetSourceId::Name(value.into()), + None => AssetSourceId::Default, } } } -impl From for AssetProviderId<'static> { +impl From for AssetSourceId<'static> { fn from(value: String) -> Self { - AssetProviderId::Name(value.into()) + AssetSourceId::Name(value.into()) } } -impl<'a> Hash for AssetProviderId<'a> { +impl<'a> Hash for AssetSourceId<'a> { fn hash(&self, state: &mut H) { self.as_str().hash(state); } } -impl<'a> PartialEq for AssetProviderId<'a> { +impl<'a> PartialEq for AssetSourceId<'a> { fn eq(&self, other: &Self) -> bool { self.as_str().eq(&other.as_str()) } } -/// Metadata about an "asset provider", such as how to construct the [`AssetReader`] and [`AssetWriter`] for the provider, -/// and whether or not the provider is processed. +/// Metadata about an "asset source", such as how to construct the [`AssetReader`] and [`AssetWriter`] for the source, +/// and whether or not the source is processed. #[derive(Default)] -pub struct AssetProviderBuilder { +pub struct AssetSourceBuilder { pub reader: Option Box + Send + Sync>>, pub writer: Option Option> + Send + Sync>>, pub watcher: Option< Box< - dyn FnMut( - crossbeam_channel::Sender, - ) -> Option> + dyn FnMut(crossbeam_channel::Sender) -> Option> + Send + Sync, >, @@ -125,24 +123,22 @@ pub struct AssetProviderBuilder { pub processed_writer: Option Option> + Send + Sync>>, pub processed_watcher: Option< Box< - dyn FnMut( - crossbeam_channel::Sender, - ) -> Option> + dyn FnMut(crossbeam_channel::Sender) -> Option> + Send + Sync, >, >, } -impl AssetProviderBuilder { - /// Builds a new [`AssetProvider`] with the given `id`. If `watch` is true, the unprocessed provider will watch for changes. - /// If `watch_processed` is true, the processed provider will watch for changes. +impl AssetSourceBuilder { + /// Builds a new [`AssetSource`] with the given `id`. If `watch` is true, the unprocessed source will watch for changes. + /// If `watch_processed` is true, the processed source will watch for changes. pub fn build( &mut self, - id: AssetProviderId<'static>, + id: AssetSourceId<'static>, watch: bool, watch_processed: bool, - ) -> Option { + ) -> Option { let reader = (self.reader.as_mut()?)(); let writer = self.writer.as_mut().map(|w| match (w)() { Some(w) => w, @@ -152,7 +148,7 @@ impl AssetProviderBuilder { Some(w) => w, None => panic!("{} does not have a processed AssetWriter configured. Note that Web and Android do not currently support writing assets.", id), }); - let mut provider = AssetProvider { + let mut source = AssetSource { id: id.clone(), reader, writer, @@ -168,8 +164,8 @@ impl AssetProviderBuilder { let (sender, receiver) = crossbeam_channel::unbounded(); match self.watcher.as_mut().and_then(|w|(w)(sender)) { Some(w) => { - provider.watcher = Some(w); - provider.event_receiver = Some(receiver); + source.watcher = Some(w); + source.event_receiver = Some(receiver); }, None => warn!("{id} does not have an AssetWatcher configured. Consider enabling the `file_watcher` feature. Note that Web and Android do not currently support watching assets."), } @@ -179,13 +175,13 @@ impl AssetProviderBuilder { let (sender, receiver) = crossbeam_channel::unbounded(); match self.processed_watcher.as_mut().and_then(|w|(w)(sender)) { Some(w) => { - provider.processed_watcher = Some(w); - provider.processed_event_receiver = Some(receiver); + source.processed_watcher = Some(w); + source.processed_event_receiver = Some(receiver); }, None => warn!("{id} does not have a processed AssetWatcher configured. Consider enabling the `file_watcher` feature. Note that Web and Android do not currently support watching assets."), } } - Some(provider) + Some(source) } /// Will use the given `reader` function to construct unprocessed [`AssetReader`] instances. @@ -209,7 +205,7 @@ impl AssetProviderBuilder { /// Will use the given `watcher` function to construct unprocessed [`AssetWatcher`] instances. pub fn with_watcher( mut self, - watcher: impl FnMut(crossbeam_channel::Sender) -> Option> + watcher: impl FnMut(crossbeam_channel::Sender) -> Option> + Send + Sync + 'static, @@ -239,7 +235,7 @@ impl AssetProviderBuilder { /// Will use the given `watcher` function to construct processed [`AssetWatcher`] instances. pub fn with_processed_watcher( mut self, - watcher: impl FnMut(crossbeam_channel::Sender) -> Option> + watcher: impl FnMut(crossbeam_channel::Sender) -> Option> + Send + Sync + 'static, @@ -250,26 +246,22 @@ impl AssetProviderBuilder { } /// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`] and [`AssetWriter`] instances -/// for a given asset provider. +/// for a given asset source. #[derive(Resource, Default)] -pub struct AssetProviderBuilders { - providers: HashMap, AssetProviderBuilder>, - default: Option, +pub struct AssetSourceBuilders { + sources: HashMap, AssetSourceBuilder>, + default: Option, } -impl AssetProviderBuilders { +impl AssetSourceBuilders { /// Inserts a new builder with the given `id` - pub fn insert( - &mut self, - id: impl Into>, - provider: AssetProviderBuilder, - ) { + pub fn insert(&mut self, id: impl Into>, source: AssetSourceBuilder) { match id.into() { - AssetProviderId::Default => { - self.default = Some(provider); + AssetSourceId::Default => { + self.default = Some(source); } - AssetProviderId::Name(name) => { - self.providers.insert(name, provider); + AssetSourceId::Name(name) => { + self.sources.insert(name, source); } } } @@ -277,55 +269,51 @@ impl AssetProviderBuilders { /// Gets a mutable builder with the given `id`, if it exists. pub fn get_mut<'a, 'b>( &'a mut self, - id: impl Into>, - ) -> Option<&'a mut AssetProviderBuilder> { + id: impl Into>, + ) -> Option<&'a mut AssetSourceBuilder> { match id.into() { - AssetProviderId::Default => self.default.as_mut(), - AssetProviderId::Name(name) => self.providers.get_mut(&name.into_owned()), + AssetSourceId::Default => self.default.as_mut(), + AssetSourceId::Name(name) => self.sources.get_mut(&name.into_owned()), } } - /// Builds an new [`AssetProviders`] collection. If `watch` is true, the unprocessed providers will watch for changes. - /// If `watch_processed` is true, the processed providers will watch for changes. - pub fn build_providers(&mut self, watch: bool, watch_processed: bool) -> AssetProviders { - let mut providers = HashMap::new(); - for (id, provider) in &mut self.providers { - if let Some(data) = provider.build( - AssetProviderId::Name(id.clone_owned()), + /// Builds an new [`AssetSources`] collection. If `watch` is true, the unprocessed sources will watch for changes. + /// If `watch_processed` is true, the processed sources will watch for changes. + pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources { + let mut sources = HashMap::new(); + for (id, source) in &mut self.sources { + if let Some(data) = source.build( + AssetSourceId::Name(id.clone_owned()), watch, watch_processed, ) { - providers.insert(id.clone_owned(), data); + sources.insert(id.clone_owned(), data); } } - AssetProviders { - providers, + AssetSources { + sources, default: self .default .as_mut() - .and_then(|p| p.build(AssetProviderId::Default, watch, watch_processed)) - .expect(MISSING_DEFAULT_PROVIDER), + .and_then(|p| p.build(AssetSourceId::Default, watch, watch_processed)) + .expect(MISSING_DEFAULT_SOURCE), } } - /// Initializes the default [`AssetProviderBuilder`] if it has not already been set. - pub fn init_default_providers(&mut self, path: &str, processed_path: &str) { + /// Initializes the default [`AssetSourceBuilder`] if it has not already been set. + pub fn init_default_sources(&mut self, path: &str, processed_path: &str) { self.default.get_or_insert_with(|| { - AssetProviderBuilder::default() - .with_reader(AssetProvider::get_default_reader(path.to_string())) - .with_writer(AssetProvider::get_default_writer(path.to_string())) - .with_watcher(AssetProvider::get_default_watcher( + AssetSourceBuilder::default() + .with_reader(AssetSource::get_default_reader(path.to_string())) + .with_writer(AssetSource::get_default_writer(path.to_string())) + .with_watcher(AssetSource::get_default_watcher( path.to_string(), Duration::from_millis(300), )) - .with_processed_reader(AssetProvider::get_default_reader( - processed_path.to_string(), - )) - .with_processed_writer(AssetProvider::get_default_writer( - processed_path.to_string(), - )) - .with_processed_watcher(AssetProvider::get_default_watcher( + .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string())) + .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string())) + .with_processed_watcher(AssetSource::get_default_watcher( processed_path.to_string(), Duration::from_millis(300), )) @@ -334,38 +322,38 @@ impl AssetProviderBuilders { } /// A collection of unprocessed and processed [`AssetReader`], [`AssetWriter`], and [`AssetWatcher`] instances -/// for a specific asset provider, identified by an [`AssetProviderId`]. -pub struct AssetProvider { - id: AssetProviderId<'static>, +/// for a specific asset source, identified by an [`AssetSourceId`]. +pub struct AssetSource { + id: AssetSourceId<'static>, reader: Box, writer: Option>, processed_reader: Option>, processed_writer: Option>, watcher: Option>, processed_watcher: Option>, - event_receiver: Option>, - processed_event_receiver: Option>, + event_receiver: Option>, + processed_event_receiver: Option>, } -impl AssetProvider { - /// Starts building a new [`AssetProvider`]. - pub fn build() -> AssetProviderBuilder { - AssetProviderBuilder::default() +impl AssetSource { + /// Starts building a new [`AssetSource`]. + pub fn build() -> AssetSourceBuilder { + AssetSourceBuilder::default() } - /// Returns this provider's id. + /// Returns this source's id. #[inline] - pub fn id(&self) -> AssetProviderId<'static> { + pub fn id(&self) -> AssetSourceId<'static> { self.id.clone() } - /// Return's this provider's unprocessed [`AssetReader`]. + /// Return's this source's unprocessed [`AssetReader`]. #[inline] pub fn reader(&self) -> &dyn AssetReader { &*self.reader } - /// Return's this provider's unprocessed [`AssetWriter`], if it exists. + /// Return's this source's unprocessed [`AssetWriter`], if it exists. #[inline] pub fn writer(&self) -> Result<&dyn AssetWriter, MissingAssetWriterError> { self.writer @@ -373,7 +361,7 @@ impl AssetProvider { .ok_or_else(|| MissingAssetWriterError(self.id.clone_owned())) } - /// Return's this provider's processed [`AssetReader`], if it exists. + /// Return's this source's processed [`AssetReader`], if it exists. #[inline] pub fn processed_reader(&self) -> Result<&dyn AssetReader, MissingProcessedAssetReaderError> { self.processed_reader @@ -381,7 +369,7 @@ impl AssetProvider { .ok_or_else(|| MissingProcessedAssetReaderError(self.id.clone_owned())) } - /// Return's this provider's processed [`AssetWriter`], if it exists. + /// Return's this source's processed [`AssetWriter`], if it exists. #[inline] pub fn processed_writer(&self) -> Result<&dyn AssetWriter, MissingProcessedAssetWriterError> { self.processed_writer @@ -389,21 +377,21 @@ impl AssetProvider { .ok_or_else(|| MissingProcessedAssetWriterError(self.id.clone_owned())) } - /// Return's this provider's unprocessed event receiver, if the provider is currently watching for changes. + /// Return's this source's unprocessed event receiver, if the source is currently watching for changes. #[inline] - pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver> { + pub fn event_receiver(&self) -> Option<&crossbeam_channel::Receiver> { self.event_receiver.as_ref() } - /// Return's this provider's processed event receiver, if the provider is currently watching for changes. + /// Return's this source's processed event receiver, if the source is currently watching for changes. #[inline] pub fn processed_event_receiver( &self, - ) -> Option<&crossbeam_channel::Receiver> { + ) -> Option<&crossbeam_channel::Receiver> { self.processed_event_receiver.as_ref() } - /// Returns true if the assets in this provider should be processed. + /// Returns true if the assets in this source should be processed. #[inline] pub fn should_process(&self) -> bool { self.processed_writer.is_some() @@ -444,10 +432,10 @@ impl AssetProvider { pub fn get_default_watcher( path: String, file_debounce_wait_time: Duration, - ) -> impl FnMut(crossbeam_channel::Sender) -> Option> + ) -> impl FnMut(crossbeam_channel::Sender) -> Option> + Send + Sync { - move |sender: crossbeam_channel::Sender| { + move |sender: crossbeam_channel::Sender| { #[cfg(all( feature = "file_watcher", not(target_arch = "wasm32"), @@ -483,83 +471,83 @@ impl AssetProvider { } } -/// A collection of [`AssetProviders`]. -pub struct AssetProviders { - providers: HashMap, AssetProvider>, - default: AssetProvider, +/// A collection of [`AssetSources`]. +pub struct AssetSources { + sources: HashMap, AssetSource>, + default: AssetSource, } -impl AssetProviders { - /// Gets the [`AssetProvider`] with the given `id`, if it exists. +impl AssetSources { + /// Gets the [`AssetSource`] with the given `id`, if it exists. pub fn get<'a, 'b>( &'a self, - id: impl Into>, - ) -> Result<&'a AssetProvider, MissingAssetProviderError> { + id: impl Into>, + ) -> Result<&'a AssetSource, MissingAssetSourceError> { match id.into().into_owned() { - AssetProviderId::Default => Ok(&self.default), - AssetProviderId::Name(name) => self - .providers + AssetSourceId::Default => Ok(&self.default), + AssetSourceId::Name(name) => self + .sources .get(&name) - .ok_or_else(|| MissingAssetProviderError(AssetProviderId::Name(name))), + .ok_or_else(|| MissingAssetSourceError(AssetSourceId::Name(name))), } } - /// Iterates all asset providers in the collection (including the default provider). - pub fn iter(&self) -> impl Iterator { - self.providers.values().chain(Some(&self.default)) + /// Iterates all asset sources in the collection (including the default source). + pub fn iter(&self) -> impl Iterator { + self.sources.values().chain(Some(&self.default)) } - /// Mutably iterates all asset providers in the collection (including the default provider). - pub fn iter_mut(&mut self) -> impl Iterator { - self.providers.values_mut().chain(Some(&mut self.default)) + /// Mutably iterates all asset sources in the collection (including the default source). + pub fn iter_mut(&mut self) -> impl Iterator { + self.sources.values_mut().chain(Some(&mut self.default)) } - /// Iterates all processed asset providers in the collection (including the default provider). - pub fn iter_processed(&self) -> impl Iterator { + /// Iterates all processed asset sources in the collection (including the default source). + pub fn iter_processed(&self) -> impl Iterator { self.iter().filter(|p| p.should_process()) } - /// Mutably iterates all processed asset providers in the collection (including the default provider). - pub fn iter_processed_mut(&mut self) -> impl Iterator { + /// Mutably iterates all processed asset sources in the collection (including the default source). + pub fn iter_processed_mut(&mut self) -> impl Iterator { self.iter_mut().filter(|p| p.should_process()) } - /// Iterates over the [`AssetProviderId`] of every [`AssetProvider`] in the collection (including the default provider). - pub fn provider_ids(&self) -> impl Iterator> + '_ { - self.providers + /// Iterates over the [`AssetSourceId`] of every [`AssetSource`] in the collection (including the default source). + pub fn ids(&self) -> impl Iterator> + '_ { + self.sources .keys() - .map(|k| AssetProviderId::Name(k.clone_owned())) - .chain(Some(AssetProviderId::Default)) + .map(|k| AssetSourceId::Name(k.clone_owned())) + .chain(Some(AssetSourceId::Default)) } /// This will cause processed [`AssetReader`] futures (such as [`AssetReader::read`]) to wait until /// the [`AssetProcessor`](crate::AssetProcessor) has finished processing the requested asset. pub fn gate_on_processor(&mut self, processor_data: Arc) { - for provider in self.iter_processed_mut() { - provider.gate_on_processor(processor_data.clone()); + for source in self.iter_processed_mut() { + source.gate_on_processor(processor_data.clone()); } } } -/// An error returned when an [`AssetProvider`] does not exist for a given id. +/// An error returned when an [`AssetSource`] does not exist for a given id. #[derive(Error, Debug)] -#[error("Asset Provider '{0}' does not exist")] -pub struct MissingAssetProviderError(AssetProviderId<'static>); +#[error("Asset Source '{0}' does not exist")] +pub struct MissingAssetSourceError(AssetSourceId<'static>); /// An error returned when an [`AssetWriter`] does not exist for a given id. #[derive(Error, Debug)] -#[error("Asset Provider '{0}' does not have an AssetWriter.")] -pub struct MissingAssetWriterError(AssetProviderId<'static>); +#[error("Asset Source '{0}' does not have an AssetWriter.")] +pub struct MissingAssetWriterError(AssetSourceId<'static>); /// An error returned when a processed [`AssetReader`] does not exist for a given id. #[derive(Error, Debug)] -#[error("Asset Provider '{0}' does not have a processed AssetReader.")] -pub struct MissingProcessedAssetReaderError(AssetProviderId<'static>); +#[error("Asset Source '{0}' does not have a processed AssetReader.")] +pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>); /// An error returned when a processed [`AssetWriter`] does not exist for a given id. #[derive(Error, Debug)] -#[error("Asset Provider '{0}' does not have a processed AssetWriter.")] -pub struct MissingProcessedAssetWriterError(AssetProviderId<'static>); +#[error("Asset Source '{0}' does not have a processed AssetWriter.")] +pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>); -const MISSING_DEFAULT_PROVIDER: &str = - "A default AssetProvider is required. Add one to `AssetProviderBuilders`"; +const MISSING_DEFAULT_SOURCE: &str = + "A default AssetSource is required. Add one to `AssetSourceBuilders`"; diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index b5534d4a2842e..a2362304022cb 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -37,7 +37,7 @@ pub use anyhow; pub use bevy_utils::BoxedFuture; use crate::{ - io::{rust_src::RustSrcRegistry, AssetProviderBuilder, AssetProviderBuilders, AssetProviderId}, + io::{rust_src::RustSrcRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId}, processor::{AssetProcessor, Process}, }; use bevy_app::{App, First, MainScheduleOrder, Plugin, PostUpdate, Startup}; @@ -49,7 +49,7 @@ use bevy_ecs::{ use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; use std::{any::TypeId, sync::Arc}; -/// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetProvider`], +/// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`], /// which can be something like a filesystem, a network, etc. /// /// Supports flexible "modes", such as [`AssetMode::Processed`] and @@ -71,18 +71,25 @@ pub struct AssetPlugin { } pub enum AssetMode { - /// Loads assets without any "preprocessing" from the configured asset `source` (defaults to the `assets` folder). + /// Loads assets from their [`AssetSource`]'s default [`AssetReader`] without any "preprocessing". + /// + /// [`AssetReader`]: crate::io::AssetReader + /// [`AssetSource`]: crate::io::AssetSource Unprocessed, - /// Loads "processed" assets from a given `destination` source (defaults to the `imported_assets/Default` folder). This should - /// generally only be used when distributing apps. Use [`AssetPlugin::ProcessedDev`] to develop apps that process assets, - /// then switch to [`AssetPlugin::Processed`] when deploying the apps. + /// Loads assets from their final processed [`AssetReader`]. This should generally only be used when distributing apps. + /// Use [`AssetMode::ProcessedDev`] to develop apps that process assets, then switch to [`AssetMode::Processed`] when deploying the apps. + /// + /// [`AssetReader`]: crate::io::AssetReader Processed, - /// Starts an [`AssetProcessor`] in the background that reads assets from the `source` provider (defaults to the `assets` folder), - /// processes them according to their [`AssetMeta`], and writes them to the `destination` provider (defaults to the `imported_assets/Default` folder). + /// Starts an [`AssetProcessor`] in the background that reads assets from their unprocessed [`AssetSource`] (defaults to the `assets` folder), + /// processes them according to their [`AssetMeta`], and writes them to their processed [`AssetSource`] (defaults to the `imported_assets/Default` folder). + /// + /// Apps will load assets from the processed [`AssetSource`]. Asset loads will wait until the asset processor has finished processing the requested asset. /// - /// By default this will hot reload changes to the `source` provider, resulting in reprocessing the asset and reloading it in the [`App`]. + /// This should generally be used in combination with the `file_watcher` cargo feature to support hot-reloading and re-processing assets. /// /// [`AssetMeta`]: crate::meta::AssetMeta + /// [`AssetSource`]: crate::io::AssetSource ProcessedDev, } @@ -109,11 +116,11 @@ impl Plugin for AssetPlugin { app.init_schedule(UpdateAssets).init_schedule(AssetEvents); let rust_src = RustSrcRegistry::default(); { - let mut providers = app + let mut sources = app .world - .get_resource_or_insert_with::(Default::default); - providers.init_default_providers(&self.file_path, &self.processed_file_path); - rust_src.register_provider(&mut providers); + .get_resource_or_insert_with::(Default::default); + sources.init_default_sources(&self.file_path, &self.processed_file_path); + rust_src.register_source(&mut sources); } { let mut watch = cfg!(feature = "watch"); @@ -122,31 +129,31 @@ impl Plugin for AssetPlugin { } match self.mode { AssetMode::Unprocessed => { - let mut providers = app.world.resource_mut::(); - let providers = providers.build_providers(watch, false); + let mut builders = app.world.resource_mut::(); + let sources = builders.build_sources(watch, false); app.insert_resource(AssetServer::new( - providers, + sources, AssetServerMode::Unprocessed, watch, )); } AssetMode::Processed => { - let mut providers = app.world.resource_mut::(); - let providers = providers.build_providers(false, watch); + let mut builders = app.world.resource_mut::(); + let sources = builders.build_sources(false, watch); app.insert_resource(AssetServer::new( - providers, + sources, AssetServerMode::Processed, watch, )); } AssetMode::ProcessedDev => { - let mut providers = app.world.resource_mut::(); - let processor = AssetProcessor::new(&mut providers); - let mut providers = providers.build_providers(false, watch); - providers.gate_on_processor(processor.data.clone()); + let mut builders = app.world.resource_mut::(); + let processor = AssetProcessor::new(&mut builders); + let mut sources = builders.build_sources(false, watch); + sources.gate_on_processor(processor.data.clone()); // the main asset server shares loaders with the processor asset server app.insert_resource(AssetServer::new_with_loaders( - providers, + sources, processor.server().data.loaders.clone(), AssetServerMode::Processed, watch, @@ -227,11 +234,11 @@ pub trait AssetApp { fn register_asset_loader(&mut self, loader: L) -> &mut Self; /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`]. fn register_asset_processor(&mut self, processor: P) -> &mut Self; - /// Registers the given [`AssetProviderBuilder`] with the given `id`. - fn register_asset_provider( + /// Registers the given [`AssetSourceBuilder`] with the given `id`. + fn register_asset_source( &mut self, - id: impl Into>, - provider: AssetProviderBuilder, + id: impl Into>, + source: AssetSourceBuilder, ) -> &mut Self; /// Sets the default asset processor for the given `extension`. fn set_default_asset_processor(&mut self, extension: &str) -> &mut Self; @@ -326,16 +333,16 @@ impl AssetApp for App { self } - fn register_asset_provider( + fn register_asset_source( &mut self, - id: impl Into>, - provider: AssetProviderBuilder, + id: impl Into>, + source: AssetSourceBuilder, ) -> &mut Self { { - let mut providers = self + let mut sources = self .world - .get_resource_or_insert_with(AssetProviderBuilders::default); - providers.insert(id, provider); + .get_resource_or_insert_with(AssetSourceBuilders::default); + sources.insert(id, source); } self @@ -365,10 +372,10 @@ mod tests { io::{ gated::{GateOpener, GatedReader}, memory::{Dir, MemoryAssetReader}, - AssetProvider, Reader, + AssetSource, Reader, }, loader::{AssetLoader, LoadContext}, - Asset, AssetApp, AssetEvent, AssetId, AssetPlugin, AssetProviderId, AssetServer, Assets, + Asset, AssetApp, AssetEvent, AssetId, AssetPlugin, AssetServer, AssetSourceId, Assets, DependencyLoadState, LoadState, RecursiveDependencyLoadState, }; use bevy_app::{App, Update}; @@ -454,9 +461,9 @@ mod tests { fn test_app(dir: Dir) -> (App, GateOpener) { let mut app = App::new(); let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir }); - app.register_asset_provider( - AssetProviderId::Default, - AssetProvider::build().with_reader(move || Box::new(gated_memory_reader.clone())), + app.register_asset_source( + AssetSourceId::Default, + AssetSource::build().with_reader(move || Box::new(gated_memory_reader.clone())), ) .add_plugins(( TaskPoolPlugin::default(), diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index ddcc7faad9139..cf415062899a4 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,5 +1,5 @@ use crate::{ - io::{AssetReaderError, MissingAssetProviderError, MissingProcessedAssetReaderError, Reader}, + io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader}, meta::{ loader_settings_meta_transform, AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings, @@ -421,10 +421,10 @@ impl<'a> LoadContext<'a> { path: impl Into>, ) -> Result, ReadAssetBytesError> { let path = path.into(); - let provider = self.asset_server.get_provider(path.provider())?; + let source = self.asset_server.get_source(path.source())?; let asset_reader = match self.asset_server.mode() { - AssetServerMode::Unprocessed { .. } => provider.reader(), - AssetServerMode::Processed { .. } => provider.processed_reader()?, + AssetServerMode::Unprocessed { .. } => source.reader(), + AssetServerMode::Processed { .. } => source.processed_reader()?, }; let mut reader = asset_reader.read(path.path()).await?; let hash = if self.populate_hashes { @@ -552,7 +552,7 @@ pub enum ReadAssetBytesError { #[error(transparent)] AssetReaderError(#[from] AssetReaderError), #[error(transparent)] - MissingAssetProviderError(#[from] MissingAssetProviderError), + MissingAssetSourceError(#[from] MissingAssetSourceError), #[error(transparent)] MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError), /// Encountered an I/O error while loading an asset. diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index 0bd13f4cee703..f104f8ddea2cf 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -1,4 +1,4 @@ -use crate::io::AssetProviderId; +use crate::io::AssetSourceId; use bevy_reflect::{ std_traits::ReflectDefault, utility::NonGenericTypeInfoCell, FromReflect, FromType, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, @@ -18,8 +18,8 @@ use thiserror::Error; /// Represents a path to an asset in a "virtual filesystem". /// /// Asset paths consist of three main parts: -/// * [`AssetPath::provider`]: The name of the [`AssetProvider`](crate::io::AssetProvider) to load the asset from. -/// This is optional. If one is not set the default provider will be used (which is the `assets` folder by default). +/// * [`AssetPath::source`]: The name of the [`AssetSource`](crate::io::AssetSource) to load the asset from. +/// This is optional. If one is not set the default source will be used (which is the `assets` folder by default). /// * [`AssetPath::path`]: The "virtual filesystem path" pointing to an asset source file. /// * [`AssetPath::label`]: An optional "named sub asset". When assets are loaded, they are /// allowed to load "sub assets" of any type, which are identified by a named "label". @@ -37,13 +37,13 @@ use thiserror::Error; /// # struct Scene; /// # /// # let asset_server: AssetServer = panic!(); -/// // This loads the `my_scene.scn` base asset from the default asset provider. +/// // This loads the `my_scene.scn` base asset from the default asset source. /// let scene: Handle = asset_server.load("my_scene.scn"); /// -/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset in the default asset provider. +/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset in the default asset source. /// let mesh: Handle = asset_server.load("my_scene.scn#PlayerMesh"); /// -/// // This loads the `my_scene.scn` base asset from a custom 'remote' asset provider. +/// // This loads the `my_scene.scn` base asset from a custom 'remote' asset source. /// let scene: Handle = asset_server.load("remote://my_scene.scn"); /// ``` /// @@ -54,7 +54,7 @@ use thiserror::Error; /// This also means that you should use [`AssetPath::new`] in cases where `&str` is the explicit type. #[derive(Eq, PartialEq, Hash, Clone, Default)] pub struct AssetPath<'a> { - provider: AssetProviderId<'a>, + source: AssetSourceId<'a>, path: CowArc<'a, Path>, label: Option>, } @@ -77,10 +77,10 @@ impl<'a> Display for AssetPath<'a> { #[derive(Error, Debug, PartialEq, Eq)] pub enum ParseAssetPathError { - #[error("Asset provider must be followed by '://'")] - InvalidProviderSyntax, - #[error("Asset provider must be at least one character. Either specify the provider before the '://' or remove the `://`")] - MissingProvider, + #[error("Asset source must be followed by '://'")] + InvalidSourceSyntax, + #[error("Asset source must be at least one character. Either specify the source before the '://' or remove the `://`")] + MissingSource, #[error("Asset label must be at least one character. Either specify the label after the '#' or remove the '#'")] MissingLabel, } @@ -90,7 +90,7 @@ impl<'a> AssetPath<'a> { /// * An asset at the root: `"scene.gltf"` /// * An asset nested in some folders: `"some/path/scene.gltf"` /// * An asset with a "label": `"some/path/scene.gltf#Mesh0"` - /// * An asset with a custom "provider": `"custom://some/path/scene.gltf#Mesh0"` + /// * An asset with a custom "source": `"custom://some/path/scene.gltf#Mesh0"` /// /// Prefer [`From<'static str>`] for static strings, as this will prevent allocations /// and reference counting for [`AssetPath::into_owned`]. @@ -105,18 +105,18 @@ impl<'a> AssetPath<'a> { /// * An asset at the root: `"scene.gltf"` /// * An asset nested in some folders: `"some/path/scene.gltf"` /// * An asset with a "label": `"some/path/scene.gltf#Mesh0"` - /// * An asset with a custom "provider": `"custom://some/path/scene.gltf#Mesh0"` + /// * An asset with a custom "source": `"custom://some/path/scene.gltf#Mesh0"` /// /// Prefer [`From<'static str>`] for static strings, as this will prevent allocations /// and reference counting for [`AssetPath::into_owned`]. /// /// This will return a [`ParseAssetPathError`] if `asset_path` is in an invalid format. pub fn try_parse(asset_path: &'a str) -> Result, ParseAssetPathError> { - let (provider, path, label) = Self::parse_internal(asset_path).unwrap(); + let (source, path, label) = Self::parse_internal(asset_path).unwrap(); Ok(Self { - provider: match provider { - Some(provider) => AssetProviderId::Name(CowArc::Borrowed(provider)), - None => AssetProviderId::Default, + source: match source { + Some(source) => AssetSourceId::Name(CowArc::Borrowed(source)), + None => AssetSourceId::Default, }, path: CowArc::Borrowed(path), label: label.map(CowArc::Borrowed), @@ -127,7 +127,7 @@ impl<'a> AssetPath<'a> { asset_path: &str, ) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> { let mut chars = asset_path.char_indices(); - let mut provider_range = None; + let mut source_range = None; let mut path_range = 0..asset_path.len(); let mut label_range = None; while let Some((index, char)) = chars.next() { @@ -135,17 +135,17 @@ impl<'a> AssetPath<'a> { ':' => { let (_, char) = chars .next() - .ok_or(ParseAssetPathError::InvalidProviderSyntax)?; + .ok_or(ParseAssetPathError::InvalidSourceSyntax)?; if char != '/' { - return Err(ParseAssetPathError::InvalidProviderSyntax); + return Err(ParseAssetPathError::InvalidSourceSyntax); } let (index, char) = chars .next() - .ok_or(ParseAssetPathError::InvalidProviderSyntax)?; + .ok_or(ParseAssetPathError::InvalidSourceSyntax)?; if char != '/' { - return Err(ParseAssetPathError::InvalidProviderSyntax); + return Err(ParseAssetPathError::InvalidSourceSyntax); } - provider_range = Some(0..index - 2); + source_range = Some(0..index - 2); path_range.start = index + 1; } '#' => { @@ -157,12 +157,12 @@ impl<'a> AssetPath<'a> { } } - let provider = match provider_range { - Some(provider_range) => { - if provider_range.is_empty() { - return Err(ParseAssetPathError::MissingProvider); + let source = match source_range { + Some(source_range) => { + if source_range.is_empty() { + return Err(ParseAssetPathError::MissingSource); } - Some(&asset_path[provider_range]) + Some(&asset_path[source_range]) } None => None, }; @@ -177,7 +177,7 @@ impl<'a> AssetPath<'a> { }; let path = Path::new(&asset_path[path_range]); - Ok((provider, path, label)) + Ok((source, path, label)) } /// Creates a new [`AssetPath`] from a [`Path`]. @@ -185,16 +185,16 @@ impl<'a> AssetPath<'a> { pub fn from_path(path: &'a Path) -> AssetPath<'a> { AssetPath { path: CowArc::Borrowed(path), - provider: AssetProviderId::Default, + source: AssetSourceId::Default, label: None, } } - /// Gets the "asset provider", if one was defined. If none was defined, the default provider + /// Gets the "asset source", if one was defined. If none was defined, the default source /// will be used. #[inline] - pub fn provider(&self) -> &AssetProviderId { - &self.provider + pub fn source(&self) -> &AssetSourceId { + &self.source } /// Gets the "sub-asset label". @@ -213,7 +213,7 @@ impl<'a> AssetPath<'a> { #[inline] pub fn without_label(&self) -> AssetPath<'_> { Self { - provider: self.provider.clone(), + source: self.source.clone(), path: self.path.clone(), label: None, } @@ -236,18 +236,18 @@ impl<'a> AssetPath<'a> { #[inline] pub fn with_label(self, label: impl Into>) -> AssetPath<'a> { AssetPath { - provider: self.provider, + source: self.source, path: self.path, label: Some(label.into()), } } - /// Returns this asset path with the given provider. This will replace the previous - /// provider if it exists. + /// Returns this asset path with the given asset source. This will replace the previous asset + /// source if it exists. #[inline] - pub fn with_provider(self, provider: impl Into>) -> AssetPath<'a> { + pub fn with_source(self, source: impl Into>) -> AssetPath<'a> { AssetPath { - provider: provider.into(), + source: source.into(), path: self.path, label: self.label, } @@ -261,7 +261,7 @@ impl<'a> AssetPath<'a> { CowArc::Owned(path) => path.parent()?.to_path_buf().into(), }; Some(AssetPath { - provider: self.provider.clone(), + source: self.source.clone(), label: None, path, }) @@ -274,7 +274,7 @@ impl<'a> AssetPath<'a> { /// [`Arc`]: std::sync::Arc pub fn into_owned(self) -> AssetPath<'static> { AssetPath { - provider: self.provider.into_owned(), + source: self.source.into_owned(), path: self.path.into_owned(), label: self.label.map(|l| l.into_owned()), } @@ -313,9 +313,9 @@ impl<'a> AssetPath<'a> { impl From<&'static str> for AssetPath<'static> { #[inline] fn from(asset_path: &'static str) -> Self { - let (provider, path, label) = Self::parse_internal(asset_path).unwrap(); + let (source, path, label) = Self::parse_internal(asset_path).unwrap(); AssetPath { - provider: provider.into(), + source: source.into(), path: CowArc::Static(path), label: label.map(CowArc::Static), } @@ -340,7 +340,7 @@ impl From<&'static Path> for AssetPath<'static> { #[inline] fn from(path: &'static Path) -> Self { Self { - provider: AssetProviderId::Default, + source: AssetSourceId::Default, path: CowArc::Static(path), label: None, } @@ -351,7 +351,7 @@ impl From for AssetPath<'static> { #[inline] fn from(path: PathBuf) -> Self { Self { - provider: AssetProviderId::Default, + source: AssetSourceId::Default, path: path.into(), label: None, } @@ -566,15 +566,12 @@ mod tests { assert_eq!(result, Ok((Some("http"), Path::new(""), None))); let result = AssetPath::parse_internal("://x"); - assert_eq!(result, Err(crate::ParseAssetPathError::MissingProvider)); + assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource)); let result = AssetPath::parse_internal("a/b.test#"); assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel)); let result = AssetPath::parse_internal("http:/"); - assert_eq!( - result, - Err(crate::ParseAssetPathError::InvalidProviderSyntax) - ); + assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax)); } } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 1e1b809acae6a..9ed29b3e22e20 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -6,8 +6,8 @@ pub use process::*; use crate::{ io::{ - AssetProvider, AssetProviderBuilders, AssetProviderEvent, AssetProviderId, AssetProviders, - AssetReader, AssetReaderError, AssetWriter, AssetWriterError, MissingAssetProviderError, + AssetReader, AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, + AssetSourceId, AssetSources, AssetWriter, AssetWriterError, MissingAssetSourceError, }, meta::{ get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta, @@ -30,10 +30,10 @@ use std::{ }; use thiserror::Error; -/// A "background" asset processor that reads asset values from a source [`AssetProvider`] (which corresponds to an [`AssetReader`] / [`AssetWriter`] pair), -/// processes them in some way, and writes them to a destination [`AssetProvider`]. +/// A "background" asset processor that reads asset values from a source [`AssetSource`] (which corresponds to an [`AssetReader`] / [`AssetWriter`] pair), +/// processes them in some way, and writes them to a destination [`AssetSource`]. /// -/// This will create .meta files (a human-editable serialized form of [`AssetMeta`]) in the source [`AssetProvider`] for assets that +/// This will create .meta files (a human-editable serialized form of [`AssetMeta`]) in the source [`AssetSource`] for assets that /// that can be loaded and/or processed. This enables developers to configure how each asset should be loaded and/or processed. /// /// [`AssetProcessor`] can be run in the background while a Bevy App is running. Changes to assets will be automatically detected and hot-reloaded. @@ -58,7 +58,7 @@ pub struct AssetProcessorData { /// Default processors for file extensions default_processors: RwLock>, state: async_lock::RwLock, - providers: AssetProviders, + sources: AssetSources, initialized_sender: async_broadcast::Sender<()>, initialized_receiver: async_broadcast::Receiver<()>, finished_sender: async_broadcast::Sender<()>, @@ -67,14 +67,12 @@ pub struct AssetProcessorData { impl AssetProcessor { /// Creates a new [`AssetProcessor`] instance. - pub fn new(providers: &mut AssetProviderBuilders) -> Self { - let data = Arc::new(AssetProcessorData::new( - providers.build_providers(true, false), - )); + pub fn new(source: &mut AssetSourceBuilders) -> Self { + let data = Arc::new(AssetProcessorData::new(source.build_sources(true, false))); // The asset processor uses its own asset server with its own id space - let mut providers = providers.build_providers(false, false); - providers.gate_on_processor(data.clone()); - let server = AssetServer::new(providers, AssetServerMode::Processed, false); + let mut sources = source.build_sources(false, false); + sources.gate_on_processor(data.clone()); + let server = AssetServer::new(sources, AssetServerMode::Processed, false); Self { server, data } } @@ -100,18 +98,18 @@ impl AssetProcessor { *self.data.state.read().await } - /// Retrieves the [`AssetReaders`] for this processor + /// Retrieves the [`AssetSource`] for this processor #[inline] - pub fn get_provider<'a, 'b>( + pub fn get_source<'a, 'b>( &'a self, - provider: impl Into>, - ) -> Result<&'a AssetProvider, MissingAssetProviderError> { - self.data.providers.get(provider.into()) + id: impl Into>, + ) -> Result<&'a AssetSource, MissingAssetSourceError> { + self.data.sources.get(id.into()) } #[inline] - pub fn providers(&self) -> &AssetProviders { - &self.data.providers + pub fn sources(&self) -> &AssetSources { + &self.data.sources } /// Logs an unrecoverable error. On the next run of the processor, all assets will be regenerated. This should only be used as a last resort. @@ -152,10 +150,11 @@ impl AssetProcessor { } /// Processes all assets. This will: + /// * For each "processed [`AssetSource`]: /// * Scan the [`ProcessorTransactionLog`] and recover from any failures detected - /// * Scan the destination [`AssetProvider`] to build the current view of already processed assets. - /// * Scan the source [`AssetProvider`] and remove any processed "destination" assets that are invalid or no longer exist. - /// * For each asset in the `source` [`AssetProvider`], kick off a new "process job", which will process the asset + /// * Scan the processed [`AssetReader`] to build the current view of already processed assets. + /// * Scan the unprocessed [`AssetReader`] and remove any final processed assets that are invalid or no longer exist. + /// * For each asset in the unprocessed [`AssetReader`], kick off a new "process job", which will process the asset /// (if the latest version of the asset has not been processed). #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] pub fn process_assets(&self) { @@ -164,8 +163,8 @@ impl AssetProcessor { IoTaskPool::get().scope(|scope| { scope.spawn(async move { self.initialize().await.unwrap(); - for provider in self.providers().iter_processed() { - self.process_assets_internal(scope, provider, PathBuf::from("")) + for source in self.sources().iter_processed() { + self.process_assets_internal(scope, source, PathBuf::from("")) .await .unwrap(); } @@ -178,22 +177,22 @@ impl AssetProcessor { debug!("Processing finished in {:?}", end_time - start_time); } - /// Listens for changes to assets in the source [`AssetProvider`] and update state accordingly. + /// Listens for changes to assets in the source [`AssetSource`] and update state accordingly. // PERF: parallelize change event processing pub async fn listen_for_source_change_events(&self) { debug!("Listening for changes to source assets"); loop { let mut started_processing = false; - for provider in self.data.providers.iter_processed() { - if let Some(receiver) = provider.event_receiver() { + for source in self.data.sources.iter_processed() { + if let Some(receiver) = source.event_receiver() { for event in receiver.try_iter() { if !started_processing { self.set_state(ProcessorState::Processing).await; started_processing = true; } - self.handle_asset_provider_event(provider, event).await; + self.handle_asset_source_event(source, event).await; } } } @@ -204,81 +203,77 @@ impl AssetProcessor { } } - async fn handle_asset_provider_event( - &self, - provider: &AssetProvider, - event: AssetProviderEvent, - ) { + async fn handle_asset_source_event(&self, source: &AssetSource, event: AssetSourceEvent) { trace!("{event:?}"); match event { - AssetProviderEvent::AddedAsset(path) - | AssetProviderEvent::AddedMeta(path) - | AssetProviderEvent::ModifiedAsset(path) - | AssetProviderEvent::ModifiedMeta(path) => { - self.process_asset(provider, path).await; + AssetSourceEvent::AddedAsset(path) + | AssetSourceEvent::AddedMeta(path) + | AssetSourceEvent::ModifiedAsset(path) + | AssetSourceEvent::ModifiedMeta(path) => { + self.process_asset(source, path).await; } - AssetProviderEvent::RemovedAsset(path) => { - self.handle_removed_asset(provider, path).await; + AssetSourceEvent::RemovedAsset(path) => { + self.handle_removed_asset(source, path).await; } - AssetProviderEvent::RemovedMeta(path) => { - self.handle_removed_meta(provider, path).await; + AssetSourceEvent::RemovedMeta(path) => { + self.handle_removed_meta(source, path).await; } - AssetProviderEvent::AddedFolder(path) => { - self.handle_added_folder(provider, path).await; + AssetSourceEvent::AddedFolder(path) => { + self.handle_added_folder(source, path).await; } // NOTE: As a heads up for future devs: this event shouldn't be run in parallel with other events that might // touch this folder (ex: the folder might be re-created with new assets). Clean up the old state first. // Currently this event handler is not parallel, but it could be (and likely should be) in the future. - AssetProviderEvent::RemovedFolder(path) => { - self.handle_removed_folder(provider, &path).await; + AssetSourceEvent::RemovedFolder(path) => { + self.handle_removed_folder(source, &path).await; } - AssetProviderEvent::RenamedAsset { old, new } => { + AssetSourceEvent::RenamedAsset { old, new } => { // If there was a rename event, but the path hasn't changed, this asset might need reprocessing. // Sometimes this event is returned when an asset is moved "back" into the asset folder if old == new { - self.process_asset(provider, new).await; + self.process_asset(source, new).await; } else { - self.handle_renamed_asset(provider, old, new).await; + self.handle_renamed_asset(source, old, new).await; } } - AssetProviderEvent::RenamedMeta { old, new } => { + AssetSourceEvent::RenamedMeta { old, new } => { // If there was a rename event, but the path hasn't changed, this asset meta might need reprocessing. // Sometimes this event is returned when an asset meta is moved "back" into the asset folder if old == new { - self.process_asset(provider, new).await; + self.process_asset(source, new).await; } else { debug!("Meta renamed from {old:?} to {new:?}"); let mut infos = self.data.asset_infos.write().await; // Renaming meta should not assume that an asset has also been renamed. Check both old and new assets to see // if they should be re-imported (and/or have new meta generated) - let new_asset_path = AssetPath::from(new).with_provider(provider.id()); - let old_asset_path = AssetPath::from(old).with_provider(provider.id()); + let new_asset_path = AssetPath::from(new).with_source(source.id()); + let old_asset_path = AssetPath::from(old).with_source(source.id()); infos.check_reprocess_queue.push_back(old_asset_path); infos.check_reprocess_queue.push_back(new_asset_path); } } - AssetProviderEvent::RenamedFolder { old, new } => { + AssetSourceEvent::RenamedFolder { old, new } => { // If there was a rename event, but the path hasn't changed, this asset folder might need reprocessing. // Sometimes this event is returned when an asset meta is moved "back" into the asset folder if old == new { - self.handle_added_folder(provider, new).await; + self.handle_added_folder(source, new).await; } else { // PERF: this reprocesses everything in the moved folder. this is not necessary in most cases, but // requires some nuance when it comes to path handling. - self.handle_removed_folder(provider, &old).await; - self.handle_added_folder(provider, new).await; + self.handle_removed_folder(source, &old).await; + self.handle_added_folder(source, new).await; } } - AssetProviderEvent::RemovedUnknown { path, is_meta } => { - let processed_reader = provider.processed_reader().unwrap(); + AssetSourceEvent::RemovedUnknown { path, is_meta } => { + let processed_reader = source.processed_reader().unwrap(); match processed_reader.is_directory(&path).await { Ok(is_directory) => { if is_directory { - self.handle_removed_folder(provider, &path).await; + self.handle_removed_folder(source, &path).await; } else if is_meta { - self.handle_removed_meta(provider, path).await; + self.handle_removed_meta(source, path).await; } else { - self.handle_removed_asset(provider, path).await; + self.handle_removed_asset(source, path).await; } } Err(err) => { @@ -290,7 +285,7 @@ impl AssetProcessor { error!( "Path '{}' was removed, but the destination reader could not determine if it \ was a folder or a file due to the following error: {err}", - AssetPath::from_path(&path).with_provider(provider.id()) + AssetPath::from_path(&path).with_source(source.id()) ); } } @@ -300,17 +295,17 @@ impl AssetProcessor { } } - async fn handle_added_folder(&self, provider: &AssetProvider, path: PathBuf) { + async fn handle_added_folder(&self, source: &AssetSource, path: PathBuf) { debug!( "Folder {} was added. Attempting to re-process", - AssetPath::from_path(&path).with_provider(provider.id()) + AssetPath::from_path(&path).with_source(source.id()) ); #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] error!("AddFolder event cannot be handled in single threaded mode (or WASM) yet."); #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] IoTaskPool::get().scope(|scope| { scope.spawn(async move { - self.process_assets_internal(scope, provider, path) + self.process_assets_internal(scope, source, path) .await .unwrap(); }); @@ -318,26 +313,26 @@ impl AssetProcessor { } /// Responds to a removed meta event by reprocessing the asset at the given path. - async fn handle_removed_meta(&self, provider: &AssetProvider, path: PathBuf) { + async fn handle_removed_meta(&self, source: &AssetSource, path: PathBuf) { // If meta was removed, we might need to regenerate it. // Likewise, the user might be manually re-adding the asset. // Therefore, we shouldn't automatically delete the asset ... that is a // user-initiated action. debug!( "Meta for asset {:?} was removed. Attempting to re-process", - AssetPath::from_path(&path).with_provider(provider.id()) + AssetPath::from_path(&path).with_source(source.id()) ); - self.process_asset(provider, path).await; + self.process_asset(source, path).await; } /// Removes all processed assets stored at the given path (respecting transactionality), then removes the folder itself. - async fn handle_removed_folder(&self, provider: &AssetProvider, path: &Path) { + async fn handle_removed_folder(&self, source: &AssetSource, path: &Path) { debug!("Removing folder {:?} because source was removed", path); - let processed_reader = provider.processed_reader().unwrap(); + let processed_reader = source.processed_reader().unwrap(); match processed_reader.read_directory(path).await { Ok(mut path_stream) => { while let Some(child_path) = path_stream.next().await { - self.handle_removed_asset(provider, child_path).await; + self.handle_removed_asset(source, child_path).await; } } Err(err) => match err { @@ -353,14 +348,14 @@ impl AssetProcessor { } }, } - let processed_writer = provider.processed_writer().unwrap(); + let processed_writer = source.processed_writer().unwrap(); if let Err(err) = processed_writer.remove_directory(path).await { match err { AssetWriterError::Io(err) => { // we can ignore NotFound because if the "final" file in a folder was removed // then we automatically clean up this folder if err.kind() != ErrorKind::NotFound { - let asset_path = AssetPath::from_path(path).with_provider(provider.id()); + let asset_path = AssetPath::from_path(path).with_source(source.id()); error!("Failed to remove destination folder that no longer exists in {asset_path}: {err}"); } } @@ -370,15 +365,15 @@ impl AssetProcessor { /// Removes the processed version of an asset and associated in-memory metadata. This will block until all existing reads/writes to the /// asset have finished, thanks to the `file_transaction_lock`. - async fn handle_removed_asset(&self, provider: &AssetProvider, path: PathBuf) { - let asset_path = AssetPath::from(path).with_provider(provider.id()); + async fn handle_removed_asset(&self, source: &AssetSource, path: PathBuf) { + let asset_path = AssetPath::from(path).with_source(source.id()); debug!("Removing processed {asset_path} because source was removed"); let mut infos = self.data.asset_infos.write().await; if let Some(info) = infos.get(&asset_path) { // we must wait for uncontested write access to the asset source to ensure existing readers / writers // can finish their operations let _write_lock = info.file_transaction_lock.write(); - self.remove_processed_asset_and_meta(provider, asset_path.path()) + self.remove_processed_asset_and_meta(source, asset_path.path()) .await; } infos.remove(&asset_path).await; @@ -386,11 +381,11 @@ impl AssetProcessor { /// Handles a renamed source asset by moving it's processed results to the new location and updating in-memory paths + metadata. /// This will cause direct path dependencies to break. - async fn handle_renamed_asset(&self, provider: &AssetProvider, old: PathBuf, new: PathBuf) { + async fn handle_renamed_asset(&self, source: &AssetSource, old: PathBuf, new: PathBuf) { let mut infos = self.data.asset_infos.write().await; - let old = AssetPath::from(old).with_provider(provider.id()); - let new = AssetPath::from(new).with_provider(provider.id()); - let processed_writer = provider.processed_writer().unwrap(); + let old = AssetPath::from(old).with_source(source.id()); + let new = AssetPath::from(new).with_source(source.id()); + let processed_writer = source.processed_writer().unwrap(); if let Some(info) = infos.get(&old) { // we must wait for uncontested write access to the asset source to ensure existing readers / writers // can finish their operations @@ -419,20 +414,20 @@ impl AssetProcessor { fn process_assets_internal<'scope>( &'scope self, scope: &'scope bevy_tasks::Scope<'scope, '_, ()>, - provider: &'scope AssetProvider, + source: &'scope AssetSource, path: PathBuf, ) -> bevy_utils::BoxedFuture<'scope, Result<(), AssetReaderError>> { Box::pin(async move { - if provider.reader().is_directory(&path).await? { - let mut path_stream = provider.reader().read_directory(&path).await?; + if source.reader().is_directory(&path).await? { + let mut path_stream = source.reader().read_directory(&path).await?; while let Some(path) = path_stream.next().await { - self.process_assets_internal(scope, provider, path).await?; + self.process_assets_internal(scope, source, path).await?; } } else { // Files without extensions are skipped let processor = self.clone(); scope.spawn(async move { - processor.process_asset(provider, path).await; + processor.process_asset(source, path).await; }); } Ok(()) @@ -446,9 +441,9 @@ impl AssetProcessor { IoTaskPool::get().scope(|scope| { for path in check_reprocess_queue.drain(..) { let processor = self.clone(); - let provider = self.get_provider(path.provider()).unwrap(); + let source = self.get_source(path.source()).unwrap(); scope.spawn(async move { - processor.process_asset(provider, path.into()).await; + processor.process_asset(source, path.into()).await; }); } }); @@ -484,7 +479,7 @@ impl AssetProcessor { processors.get(processor_type_name).cloned() } - /// Populates the initial view of each asset by scanning the source and destination folders. + /// Populates the initial view of each asset by scanning the unprocessed and processed asset folders. /// This info will later be used to determine whether or not to re-process an asset /// /// This will validate transactions and recover failed transactions when necessary. @@ -525,40 +520,40 @@ impl AssetProcessor { }) } - for provider in self.providers().iter_processed() { - let Ok(processed_reader) = provider.processed_reader() else { + for source in self.sources().iter_processed() { + let Ok(processed_reader) = source.processed_reader() else { continue; }; - let Ok(processed_writer) = provider.processed_writer() else { + let Ok(processed_writer) = source.processed_writer() else { continue; }; - let mut source_paths = Vec::new(); + let mut unprocessed_paths = Vec::new(); get_asset_paths( - provider.reader(), + source.reader(), None, PathBuf::from(""), - &mut source_paths, + &mut unprocessed_paths, ) .await .map_err(InitializeError::FailedToReadSourcePaths)?; - let mut destination_paths = Vec::new(); + let mut processed_paths = Vec::new(); get_asset_paths( processed_reader, Some(processed_writer), PathBuf::from(""), - &mut destination_paths, + &mut processed_paths, ) .await .map_err(InitializeError::FailedToReadDestinationPaths)?; - for path in source_paths { - asset_infos.get_or_insert(AssetPath::from(path).with_provider(provider.id())); + for path in unprocessed_paths { + asset_infos.get_or_insert(AssetPath::from(path).with_source(source.id())); } - for path in destination_paths { + for path in processed_paths { let mut dependencies = Vec::new(); - let asset_path = AssetPath::from(path).with_provider(provider.id()); + let asset_path = AssetPath::from(path).with_source(source.id()); if let Some(info) = asset_infos.get_mut(&asset_path) { match processed_reader.read_meta_bytes(asset_path.path()).await { Ok(meta_bytes) => { @@ -580,23 +575,20 @@ impl AssetProcessor { } Err(err) => { trace!("Removing processed data for {asset_path} because meta could not be parsed: {err}"); - self.remove_processed_asset_and_meta( - provider, - asset_path.path(), - ) - .await; + self.remove_processed_asset_and_meta(source, asset_path.path()) + .await; } } } Err(err) => { trace!("Removing processed data for {asset_path} because meta failed to load: {err}"); - self.remove_processed_asset_and_meta(provider, asset_path.path()) + self.remove_processed_asset_and_meta(source, asset_path.path()) .await; } } } else { trace!("Removing processed data for non-existent asset {asset_path}"); - self.remove_processed_asset_and_meta(provider, asset_path.path()) + self.remove_processed_asset_and_meta(source, asset_path.path()) .await; } @@ -613,20 +605,20 @@ impl AssetProcessor { /// Removes the processed version of an asset and its metadata, if it exists. This _is not_ transactional like `remove_processed_asset_transactional`, nor /// does it remove existing in-memory metadata. - async fn remove_processed_asset_and_meta(&self, provider: &AssetProvider, path: &Path) { - if let Err(err) = provider.processed_writer().unwrap().remove(path).await { + async fn remove_processed_asset_and_meta(&self, source: &AssetSource, path: &Path) { + if let Err(err) = source.processed_writer().unwrap().remove(path).await { warn!("Failed to remove non-existent asset {path:?}: {err}"); } - if let Err(err) = provider.processed_writer().unwrap().remove_meta(path).await { + if let Err(err) = source.processed_writer().unwrap().remove_meta(path).await { warn!("Failed to remove non-existent meta {path:?}: {err}"); } - self.clean_empty_processed_ancestor_folders(provider, path) + self.clean_empty_processed_ancestor_folders(source, path) .await; } - async fn clean_empty_processed_ancestor_folders(&self, provider: &AssetProvider, path: &Path) { + async fn clean_empty_processed_ancestor_folders(&self, source: &AssetSource, path: &Path) { // As a safety precaution don't delete absolute paths to avoid deleting folders outside of the destination folder if path.is_absolute() { error!("Attempted to clean up ancestor folders of an absolute path. This is unsafe so the operation was skipped."); @@ -636,7 +628,7 @@ impl AssetProcessor { if parent == Path::new("") { break; } - if provider + if source .processed_writer() .unwrap() .remove_empty_directory(parent) @@ -655,16 +647,16 @@ impl AssetProcessor { /// to block reads until the asset is processed). /// /// [`LoadContext`]: crate::loader::LoadContext - async fn process_asset(&self, provider: &AssetProvider, path: PathBuf) { - let asset_path = AssetPath::from(path).with_provider(provider.id()); - let result = self.process_asset_internal(provider, &asset_path).await; + async fn process_asset(&self, source: &AssetSource, path: PathBuf) { + let asset_path = AssetPath::from(path).with_source(source.id()); + let result = self.process_asset_internal(source, &asset_path).await; let mut infos = self.data.asset_infos.write().await; infos.finish_processing(asset_path, result).await; } async fn process_asset_internal( &self, - provider: &AssetProvider, + source: &AssetSource, asset_path: &AssetPath<'static>, ) -> Result { // TODO: The extension check was removed now tht AssetPath is the input. is that ok? @@ -672,7 +664,7 @@ impl AssetProcessor { debug!("Processing {:?}", asset_path); let server = &self.server; let path = asset_path.path(); - let reader = provider.reader(); + let reader = source.reader(); let reader_err = |err| ProcessError::AssetReaderError { path: asset_path.clone(), @@ -731,7 +723,7 @@ impl AssetProcessor { }; let meta_bytes = meta.serialize(); // write meta to source location if it doesn't already exist - provider + source .writer()? .write_meta_bytes(path, &meta_bytes) .await @@ -746,7 +738,7 @@ impl AssetProcessor { } }; - let processed_writer = provider.processed_writer()?; + let processed_writer = source.processed_writer()?; let mut asset_bytes = Vec::new(); byte_reader @@ -879,12 +871,12 @@ impl AssetProcessor { error!("Failed to remove asset {path:?} because {message}"); state_is_valid = false; }; - let Ok(provider) = self.get_provider(path.provider()) else { - (unrecoverable_err)("AssetProvider does not exist"); + let Ok(source) = self.get_source(path.source()) else { + (unrecoverable_err)("AssetSource does not exist"); continue; }; - let Ok(processed_writer) = provider.processed_writer() else { - (unrecoverable_err)("AssetProvider does not have a processed AssetWriter registered"); + let Ok(processed_writer) = source.processed_writer() else { + (unrecoverable_err)("AssetSource does not have a processed AssetWriter registered"); continue; }; @@ -917,8 +909,8 @@ impl AssetProcessor { if !state_is_valid { error!("Processed asset transaction log state was invalid and unrecoverable for some reason (see previous logs). Removing processed assets and starting fresh."); - for provider in self.providers().iter_processed() { - let Ok(processed_writer) = provider.processed_writer() else { + for source in self.sources().iter_processed() { + let Ok(processed_writer) = source.processed_writer() else { continue; }; if let Err(err) = processed_writer @@ -939,7 +931,7 @@ impl AssetProcessor { } impl AssetProcessorData { - pub fn new(providers: AssetProviders) -> Self { + pub fn new(source: AssetSources) -> Self { let (mut finished_sender, finished_receiver) = async_broadcast::broadcast(1); let (mut initialized_sender, initialized_receiver) = async_broadcast::broadcast(1); // allow overflow on these "one slot" channels to allow receivers to retrieve the "latest" state, and to allow senders to @@ -948,7 +940,7 @@ impl AssetProcessorData { initialized_sender.set_overflow(true); AssetProcessorData { - providers, + sources: source, finished_sender, finished_receiver, initialized_sender, diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 9f9a2dd35a07f..ee141518fff23 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -3,8 +3,8 @@ mod info; use crate::{ folder::LoadedFolder, io::{ - AssetProvider, AssetProviderEvent, AssetProviderId, AssetProviders, AssetReader, - AssetReaderError, MissingAssetProviderError, MissingProcessedAssetReaderError, Reader, + AssetReader, AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, + MissingAssetSourceError, MissingProcessedAssetReaderError, Reader, }, loader::{AssetLoader, AssetLoaderError, ErasedAssetLoader, LoadContext, LoadedAsset}, meta::{ @@ -51,7 +51,7 @@ pub(crate) struct AssetServerData { pub(crate) loaders: Arc>, asset_event_sender: Sender, asset_event_receiver: Receiver, - providers: AssetProviders, + sources: AssetSources, mode: AssetServerMode, } @@ -67,16 +67,12 @@ pub enum AssetServerMode { impl AssetServer { /// Create a new instance of [`AssetServer`]. If `watch_for_changes` is true, the [`AssetReader`] storage will watch for changes to /// asset sources and hot-reload them. - pub fn new( - providers: AssetProviders, - mode: AssetServerMode, - watching_for_changes: bool, - ) -> Self { - Self::new_with_loaders(providers, Default::default(), mode, watching_for_changes) + pub fn new(sources: AssetSources, mode: AssetServerMode, watching_for_changes: bool) -> Self { + Self::new_with_loaders(sources, Default::default(), mode, watching_for_changes) } pub(crate) fn new_with_loaders( - providers: AssetProviders, + sources: AssetSources, loaders: Arc>, mode: AssetServerMode, watching_for_changes: bool, @@ -86,7 +82,7 @@ impl AssetServer { infos.watching_for_changes = watching_for_changes; Self { data: Arc::new(AssetServerData { - providers, + sources, mode, asset_event_sender, asset_event_receiver, @@ -96,12 +92,12 @@ impl AssetServer { } } - /// Retrieves the [`AssetReader`] for the given `provider`. - pub fn get_provider<'a>( + /// Retrieves the [`AssetReader`] for the given `source`. + pub fn get_source<'a>( &'a self, - provider: impl Into>, - ) -> Result<&'a AssetProvider, MissingAssetProviderError> { - self.data.providers.get(provider.into()) + source: impl Into>, + ) -> Result<&'a AssetSource, MissingAssetSourceError> { + self.data.sources.get(source.into()) } /// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used. @@ -500,22 +496,22 @@ impl AssetServer { let server = self.clone(); IoTaskPool::get() .spawn(async move { - let Ok(provider) = server.get_provider(path.provider()) else { + let Ok(source) = server.get_source(path.source()) else { error!( - "Failed to load {path}. AssetProvider {:?} does not exist", - path.provider() + "Failed to load {path}. AssetSource {:?} does not exist", + path.source() ); return; }; let asset_reader = match server.data.mode { - AssetServerMode::Unprocessed { .. } => provider.reader(), - AssetServerMode::Processed { .. } => match provider.processed_reader() { + AssetServerMode::Unprocessed { .. } => source.reader(), + AssetServerMode::Processed { .. } => match source.processed_reader() { Ok(reader) => reader, Err(_) => { error!( - "Failed to load {path}. AssetProvider {:?} does not have a processed AssetReader", - path.provider() + "Failed to load {path}. AssetSource {:?} does not have a processed AssetReader", + path.source() ); return; } @@ -677,14 +673,14 @@ impl AssetServer { ), AssetLoadError, > { - let provider = self.get_provider(asset_path.provider())?; + let source = self.get_source(asset_path.source())?; // NOTE: We grab the asset byte reader first to ensure this is transactional for AssetReaders like ProcessorGatedReader // The asset byte reader will "lock" the processed asset, preventing writes for the duration of the lock. // Then the meta reader, if meta exists, will correspond to the meta for the current "version" of the asset. // See ProcessedAssetInfo::file_transaction_lock for more context let asset_reader = match self.data.mode { - AssetServerMode::Unprocessed { .. } => provider.reader(), - AssetServerMode::Processed { .. } => provider.processed_reader()?, + AssetServerMode::Unprocessed { .. } => source.reader(), + AssetServerMode::Processed { .. } => source.processed_reader()?, }; let reader = asset_reader.read(asset_path.path()).await?; match asset_reader.read_meta_bytes(asset_path.path()).await { @@ -788,13 +784,12 @@ pub fn handle_internal_asset_events(world: &mut World) { } let mut paths_to_reload = HashSet::new(); - let mut handle_event = |provider: AssetProviderId<'static>, event: AssetProviderEvent| { + let mut handle_event = |source: AssetSourceId<'static>, event: AssetSourceEvent| { match event { // TODO: if the asset was processed and the processed file was changed, the first modified event // should be skipped? - AssetProviderEvent::ModifiedAsset(path) - | AssetProviderEvent::ModifiedMeta(path) => { - let path = AssetPath::from(path).with_provider(provider); + AssetSourceEvent::ModifiedAsset(path) | AssetSourceEvent::ModifiedMeta(path) => { + let path = AssetPath::from(path).with_source(source); queue_ancestors(&path, &infos, &mut paths_to_reload); paths_to_reload.insert(path); } @@ -802,19 +797,19 @@ pub fn handle_internal_asset_events(world: &mut World) { } }; - for provider in server.data.providers.iter() { + for source in server.data.sources.iter() { match server.data.mode { AssetServerMode::Unprocessed { .. } => { - if let Some(receiver) = provider.event_receiver() { + if let Some(receiver) = source.event_receiver() { for event in receiver.try_iter() { - handle_event(provider.id(), event); + handle_event(source.id(), event); } } } AssetServerMode::Processed { .. } => { - if let Some(receiver) = provider.processed_event_receiver() { + if let Some(receiver) = source.processed_event_receiver() { for event in receiver.try_iter() { - handle_event(provider.id(), event); + handle_event(source.id(), event); } } } @@ -915,7 +910,7 @@ pub enum AssetLoadError { #[error(transparent)] AssetReaderError(#[from] AssetReaderError), #[error(transparent)] - MissingAssetProviderError(#[from] MissingAssetProviderError), + MissingAssetSourceError(#[from] MissingAssetSourceError), #[error(transparent)] MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError), #[error("Encountered an error while reading asset metadata bytes")] diff --git a/examples/asset/custom_asset_reader.rs b/examples/asset/custom_asset_reader.rs index 356839eafc3b0..3064cd12593c6 100644 --- a/examples/asset/custom_asset_reader.rs +++ b/examples/asset/custom_asset_reader.rs @@ -4,7 +4,7 @@ use bevy::{ asset::io::{ - file::FileAssetReader, AssetProvider, AssetProviderId, AssetReader, AssetReaderError, + file::FileAssetReader, AssetReader, AssetReaderError, AssetSource, AssetSourceId, PathStream, Reader, }, prelude::*, @@ -50,9 +50,9 @@ struct CustomAssetReaderPlugin; impl Plugin for CustomAssetReaderPlugin { fn build(&self, app: &mut App) { - app.register_asset_provider( - AssetProviderId::Default, - AssetProvider::build() + app.register_asset_source( + AssetSourceId::Default, + AssetSource::build() .with_reader(|| Box::new(CustomAssetReader(FileAssetReader::new("assets")))), ); } From 4d08ee10b71a31236de0ae5718027967fccff6f1 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 23 Sep 2023 11:44:04 -0700 Subject: [PATCH 03/11] Make it easier to define a "platform default" asset source --- crates/bevy_asset/src/io/source.rs | 33 ++++++++++++++++-------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index 367e5980cb30d..e25cebf8b9e9f 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -243,6 +243,22 @@ impl AssetSourceBuilder { self.processed_watcher = Some(Box::new(watcher)); self } + + pub fn platform_default(path: &str, processed_path: &str) -> Self { + Self::default() + .with_reader(AssetSource::get_default_reader(path.to_string())) + .with_writer(AssetSource::get_default_writer(path.to_string())) + .with_watcher(AssetSource::get_default_watcher( + path.to_string(), + Duration::from_millis(300), + )) + .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string())) + .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string())) + .with_processed_watcher(AssetSource::get_default_watcher( + processed_path.to_string(), + Duration::from_millis(300), + )) + } } /// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`] and [`AssetWriter`] instances @@ -303,21 +319,8 @@ impl AssetSourceBuilders { /// Initializes the default [`AssetSourceBuilder`] if it has not already been set. pub fn init_default_sources(&mut self, path: &str, processed_path: &str) { - self.default.get_or_insert_with(|| { - AssetSourceBuilder::default() - .with_reader(AssetSource::get_default_reader(path.to_string())) - .with_writer(AssetSource::get_default_writer(path.to_string())) - .with_watcher(AssetSource::get_default_watcher( - path.to_string(), - Duration::from_millis(300), - )) - .with_processed_reader(AssetSource::get_default_reader(processed_path.to_string())) - .with_processed_writer(AssetSource::get_default_writer(processed_path.to_string())) - .with_processed_watcher(AssetSource::get_default_watcher( - processed_path.to_string(), - Duration::from_millis(300), - )) - }); + self.default + .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path)); } } From f530ec0c53e51c149b54e5ffdb173e26471173ac Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 23 Sep 2023 12:00:46 -0700 Subject: [PATCH 04/11] rust_src -> embedded --- Cargo.toml | 2 +- crates/bevy_asset/Cargo.toml | 2 +- .../embedded_watcher.rs} | 14 +-- .../src/io/{rust_src => embedded}/mod.rs | 86 +++++++++---------- crates/bevy_asset/src/io/mod.rs | 2 +- crates/bevy_asset/src/lib.rs | 8 +- crates/bevy_internal/Cargo.toml | 4 +- examples/asset/processing/processing.rs | 6 +- 8 files changed, 62 insertions(+), 62 deletions(-) rename crates/bevy_asset/src/io/{rust_src/rust_src_watcher.rs => embedded/embedded_watcher.rs} (87%) rename crates/bevy_asset/src/io/{rust_src => embedded}/mod.rs (78%) diff --git a/Cargo.toml b/Cargo.toml index 0f1b483d0b57c..53bf6e4069a46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -245,7 +245,7 @@ webgl2 = ["bevy_internal/webgl"] file_watcher = ["bevy_internal/file_watcher"] # Enables watching in memory asset providers for Bevy Asset hot-reloading -rust_src_watcher = ["bevy_internal/rust_src_watcher"] +embedded_watcher = ["bevy_internal/embedded_watcher"] [dependencies] bevy_dylib = { path = "crates/bevy_dylib", version = "0.12.0-dev", default-features = false, optional = true } diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index f6266a9f0d986..c341f3e0fa1c8 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["bevy"] [features] file_watcher = ["notify-debouncer-full", "watch"] -rust_src_watcher = ["file_watcher"] +embedded_watcher = ["file_watcher"] multi-threaded = ["bevy_tasks/multi-threaded"] watch = [] diff --git a/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs similarity index 87% rename from crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs rename to crates/bevy_asset/src/io/embedded/embedded_watcher.rs index b1d34a4e0b677..8247c6cdc4124 100644 --- a/crates/bevy_asset/src/io/rust_src/rust_src_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -14,11 +14,11 @@ use std::{ sync::Arc, }; -pub struct RustSrcWatcher { +pub struct EmbeddedWatcher { _watcher: Debouncer, } -impl RustSrcWatcher { +impl EmbeddedWatcher { pub fn new( dir: Dir, root_paths: Arc>>, @@ -26,7 +26,7 @@ impl RustSrcWatcher { debounce_wait_time: Duration, ) -> Self { let root = get_base_path(); - let handler = RustSrcEventHandler { + let handler = EmbeddedEventHandler { dir, root: root.clone(), sender, @@ -38,19 +38,19 @@ impl RustSrcWatcher { } } -impl AssetWatcher for RustSrcWatcher {} +impl AssetWatcher for EmbeddedWatcher {} -/// A [`FilesystemEventHandler`] that uses [`RustSrcRegistry`](crate::io::rust_src::RustSrcRegistry) to hot-reload +/// A [`FilesystemEventHandler`] that uses [`EmbeddedAssetRegistry`](crate::io::embedded::EmbeddedAssetRegistry) to hot-reload /// binary-embedded Rust source files. This will read the contents of changed files from the file system and overwrite /// the initial static bytes from the file embedded in the binary. -pub(crate) struct RustSrcEventHandler { +pub(crate) struct EmbeddedEventHandler { sender: crossbeam_channel::Sender, root_paths: Arc>>, root: PathBuf, dir: Dir, last_event: Option, } -impl FilesystemEventHandler for RustSrcEventHandler { +impl FilesystemEventHandler for EmbeddedEventHandler { fn begin(&mut self) { self.last_event = None; } diff --git a/crates/bevy_asset/src/io/rust_src/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs similarity index 78% rename from crates/bevy_asset/src/io/rust_src/mod.rs rename to crates/bevy_asset/src/io/embedded/mod.rs index 6a286851ca522..1cd0cf43ec227 100644 --- a/crates/bevy_asset/src/io/rust_src/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "rust_src_watcher")] -mod rust_src_watcher; +#[cfg(feature = "embedded_watcher")] +mod embedded_watcher; -#[cfg(feature = "rust_src_watcher")] -pub use rust_src_watcher::*; +#[cfg(feature = "embedded_watcher")] +pub use embedded_watcher::*; use crate::io::{ memory::{Dir, MemoryAssetReader, Value}, @@ -11,28 +11,28 @@ use crate::io::{ use bevy_ecs::system::Resource; use std::path::{Path, PathBuf}; -pub const RUST_SRC: &str = "rust_src"; +pub const EMBEDDED: &str = "embedded"; /// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended /// to be shared with a [`MemoryAssetReader`]. -/// Generally this should not be interacted with directly. The [`rust_src_asset`] will populate this. +/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this. #[derive(Resource, Default)] -pub struct RustSrcRegistry { +pub struct EmbeddedAssetRegistry { dir: Dir, - #[cfg(feature = "rust_src_watcher")] + #[cfg(feature = "embedded_watcher")] root_paths: std::sync::Arc< parking_lot::RwLock>, >, } -impl RustSrcRegistry { +impl EmbeddedAssetRegistry { /// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of - /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `rust_src` + /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded` /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` /// or a [`Vec`]. #[allow(unused)] pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into) { - #[cfg(feature = "rust_src_watcher")] + #[cfg(feature = "embedded_watcher")] self.root_paths .write() .insert(full_path.to_owned(), asset_path.to_owned()); @@ -40,20 +40,20 @@ impl RustSrcRegistry { } /// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of - /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `rust_src` + /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded` /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]` /// or a [`Vec`]. #[allow(unused)] pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into) { - #[cfg(feature = "rust_src_watcher")] + #[cfg(feature = "embedded_watcher")] self.root_paths .write() .insert(full_path.to_owned(), asset_path.to_owned()); self.dir.insert_meta(asset_path, value); } - /// Registers a `rust_src` [`AssetSource`] that uses this [`RustSrcRegistry`]. - // NOTE: unused_mut because rust_src_watcher feature is the only mutable consumer of `let mut source` + /// Registers a `embedded` [`AssetSource`] that uses this [`EmbeddedAssetRegistry`]. + // NOTE: unused_mut because embedded_watcher feature is the only mutable consumer of `let mut source` #[allow(unused_mut)] pub fn register_source(&self, sources: &mut AssetSourceBuilders) { let dir = self.dir.clone(); @@ -66,7 +66,7 @@ impl RustSrcRegistry { }) }); - #[cfg(feature = "rust_src_watcher")] + #[cfg(feature = "embedded_watcher")] { let root_paths = self.root_paths.clone(); let dir = self.dir.clone(); @@ -74,7 +74,7 @@ impl RustSrcRegistry { let processd_dir = self.dir.clone(); source = source .with_watcher(move |sender| { - Some(Box::new(RustSrcWatcher::new( + Some(Box::new(EmbeddedWatcher::new( dir.clone(), root_paths.clone(), sender, @@ -82,7 +82,7 @@ impl RustSrcRegistry { ))) }) .with_processed_watcher(move |sender| { - Some(Box::new(RustSrcWatcher::new( + Some(Box::new(EmbeddedWatcher::new( processd_dir.clone(), processed_root_paths.clone(), sender, @@ -90,17 +90,17 @@ impl RustSrcRegistry { ))) }); } - sources.insert(RUST_SRC, source); + sources.insert(EMBEDDED, source); } } -/// Returns the [`Path`] for a given `rust_src` asset. -/// This is used internally by [`rust_src_asset`] and can be used to get a [`Path`] +/// Returns the [`Path`] for a given `embedded` asset. +/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`] /// that matches the [`AssetPath`](crate::AssetPath) used by that asset. #[macro_export] -macro_rules! rust_src_path { +macro_rules! embedded_path { ($path_str: expr) => {{ - rust_src_path!("/src/", $path_str) + embedded_path!("/src/", $path_str) }}; ($source_path: expr, $path_str: expr) => {{ @@ -114,8 +114,8 @@ macro_rules! rust_src_path { }}; } -/// Creates a new `rust_src` asset by embedding the bytes of the given path into the current binary -/// and registering those bytes with the `rust_src` [`AssetSource`]. +/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary +/// and registering those bytes with the `embedded` [`AssetSource`]. /// /// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second. /// @@ -138,7 +138,7 @@ macro_rules! rust_src_path { /// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following /// in `bevy_rock/src/render/mod.rs`: /// -/// `rust_src_asset!(app, "rock.wgsl")` +/// `embedded_asset!(app, "rock.wgsl")` /// /// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path: /// @@ -148,51 +148,51 @@ macro_rules! rust_src_path { /// # let asset_server: AssetServer = panic!(); /// #[derive(Asset, TypePath)] /// # struct Shader; -/// let shader = asset_server.load::("rust_src://bevy_rock/render/rock.wgsl"); +/// let shader = asset_server.load::("embedded://bevy_rock/render/rock.wgsl"); /// ``` /// /// Some things to note in the path: -/// 1. The non-default `rust_src:://` [`AssetSource`] +/// 1. The non-default `embedded:://` [`AssetSource`] /// 2. `src` is trimmed from the path /// /// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in -/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`rust_src_asset`] searches for the +/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the /// _first instance_ of `bevy_rock/src` in the path. /// /// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples), -/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`rust_src_asset`]: +/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]: /// -/// `rust_src_asset!(app, "/examples/rock_stuff/", "rock.wgsl")` +/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")` /// /// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are /// equivalent: /// -/// `rust_src_asset!(app, "rock.wgsl")` -/// `rust_src_asset!(app, "/src/", "rock.wgsl")` +/// `embedded_asset!(app, "rock.wgsl")` +/// `embedded_asset!(app, "/src/", "rock.wgsl")` /// /// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes. /// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't -/// available for some reason, you can use the [`rust_src_path`] macro to debug. +/// available for some reason, you can use the [`embedded_path`] macro to debug. /// -/// Hot-reloading `rust_src` assets is supported. Just enable the `rust_src_watcher` cargo feature. +/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature. /// /// [`AssetPath`]: crate::AssetPath #[macro_export] -macro_rules! rust_src_asset { +macro_rules! embedded_asset { ($app: ident, $path: expr) => {{ - rust_src_asset!($app, "/src/", $path) + embedded_asset!($app, "/src/", $path) }}; ($app: ident, $source_path: expr, $path: expr) => {{ - let mut rust_src = $app + let mut embedded = $app .world - .resource_mut::<$crate::io::rust_src::RustSrcRegistry>(); - let path = $crate::rust_src_path!($source_path, $path); - #[cfg(feature = "rust_src_watcher")] + .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>(); + let path = $crate::embedded_path!($source_path, $path); + #[cfg(feature = "embedded_watcher")] let full_path = std::path::Path::new(file!()).parent().unwrap().join($path); - #[cfg(not(feature = "rust_src_watcher"))] + #[cfg(not(feature = "embedded_watcher"))] let full_path = std::path::PathBuf::new(); - rust_src.insert_asset(full_path, &path, include_bytes!($path)); + embedded.insert_asset(full_path, &path, include_bytes!($path)); }}; } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 75e9f24f44597..680928d7aef9b 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -1,11 +1,11 @@ #[cfg(target_os = "android")] pub mod android; +pub mod embedded; #[cfg(not(target_arch = "wasm32"))] pub mod file; pub mod gated; pub mod memory; pub mod processor_gated; -pub mod rust_src; #[cfg(target_arch = "wasm32")] pub mod wasm; diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index a2362304022cb..b97c777a1e1ec 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -37,7 +37,7 @@ pub use anyhow; pub use bevy_utils::BoxedFuture; use crate::{ - io::{rust_src::RustSrcRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId}, + io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId}, processor::{AssetProcessor, Process}, }; use bevy_app::{App, First, MainScheduleOrder, Plugin, PostUpdate, Startup}; @@ -114,13 +114,13 @@ impl AssetPlugin { impl Plugin for AssetPlugin { fn build(&self, app: &mut App) { app.init_schedule(UpdateAssets).init_schedule(AssetEvents); - let rust_src = RustSrcRegistry::default(); + let embedded = EmbeddedAssetRegistry::default(); { let mut sources = app .world .get_resource_or_insert_with::(Default::default); sources.init_default_sources(&self.file_path, &self.processed_file_path); - rust_src.register_source(&mut sources); + embedded.register_source(&mut sources); } { let mut watch = cfg!(feature = "watch"); @@ -163,7 +163,7 @@ impl Plugin for AssetPlugin { } } } - app.insert_resource(rust_src) + app.insert_resource(embedded) .init_asset::() .init_asset::<()>() .configure_sets( diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index f8c4f2075c4dc..deeae433b3992 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -104,8 +104,8 @@ default_font = ["bevy_text?/default_font"] # Enables watching the filesystem for Bevy Asset hot-reloading file_watcher = ["bevy_asset?/file_watcher"] -# Enables watching rust src files for Bevy Asset hot-reloading -rust_src_watcher = ["bevy_asset?/rust_src_watcher"] +# Enables watching embedded files for Bevy Asset hot-reloading +embedded_watcher = ["bevy_asset?/embedded_watcher"] [dependencies] # bevy diff --git a/examples/asset/processing/processing.rs b/examples/asset/processing/processing.rs index 6fce1a9b081fe..b58cc329e43e4 100644 --- a/examples/asset/processing/processing.rs +++ b/examples/asset/processing/processing.rs @@ -2,9 +2,9 @@ use bevy::{ asset::{ + embedded_asset, io::{Reader, Writer}, processor::LoadAndSave, - rust_src_asset, saver::{AssetSaver, SavedAsset}, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext, }, @@ -51,7 +51,7 @@ pub struct TextPlugin; impl Plugin for TextPlugin { fn build(&self, app: &mut App) { - rust_src_asset!(app, "examples/asset/processing/", "e.txt"); + embedded_asset!(app, "examples/asset/processing/", "e.txt"); app.init_asset::() .init_asset::() .register_asset_loader(CoolTextLoader) @@ -197,7 +197,7 @@ fn setup(mut commands: Commands, assets: Res) { b: assets.load("foo/b.cool.ron"), c: assets.load("foo/c.cool.ron"), d: assets.load("d.cool.ron"), - e: assets.load("rust_src://asset_processing/e.txt"), + e: assets.load("embedded://asset_processing/e.txt"), }); } From e00ce7204c50930e2278103cc49b07cf734cf903 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 23 Sep 2023 12:24:29 -0700 Subject: [PATCH 05/11] Update cargo_features.md --- docs/cargo_features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 90da7bd3ce7a6..aaba4b6ac5824 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -50,6 +50,7 @@ The default feature set enables most of the expected features of a game engine, |dds|DDS compressed texture support| |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| |dynamic_linking|Force dynamic linking, which improves iterative compile times| +|embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading| |exr|EXR image format support| |file_watcher|Enables watching the filesystem for Bevy Asset hot-reloading| |flac|FLAC audio format support| From 6f3ca4b4abb6a46bcbaf53fda66fd7fa91acee62 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 11 Oct 2023 18:20:30 -0700 Subject: [PATCH 06/11] Resolve comments --- .../bevy_asset/src/io/embedded/embedded_watcher.rs | 5 +++++ crates/bevy_asset/src/io/file/file_watcher.rs | 5 +++++ crates/bevy_asset/src/io/source.rs | 7 +++++-- crates/bevy_asset/src/lib.rs | 2 +- crates/bevy_asset/src/processor/mod.rs | 12 ++++++------ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs index 8247c6cdc4124..5bc2536278e3f 100644 --- a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -14,6 +14,10 @@ use std::{ sync::Arc, }; +/// A watcher for assets stored in the `embedded` asset source. Embedded assets are assets whose +/// bytes have been embedded into the Rust binary using the [`embedded_asset`](crate::io::embedded::embedded_asset) macro. +/// This watcher will watch for changes to the "source files", read the contents of changed files from the file system +/// and overwrite the initial static bytes of the file embedded in the binary with the new dynamically loaded bytes. pub struct EmbeddedWatcher { _watcher: Debouncer, } @@ -54,6 +58,7 @@ impl FilesystemEventHandler for EmbeddedEventHandler { fn begin(&mut self) { self.last_event = None; } + fn get_path(&self, absolute_path: &Path) -> Option<(PathBuf, bool)> { let (local_path, is_meta) = get_asset_path(&self.root, absolute_path); let final_path = self.root_paths.read().get(&local_path)?.clone(); diff --git a/crates/bevy_asset/src/io/file/file_watcher.rs b/crates/bevy_asset/src/io/file/file_watcher.rs index 25ee5ae735982..0d2c0416113c8 100644 --- a/crates/bevy_asset/src/io/file/file_watcher.rs +++ b/crates/bevy_asset/src/io/file/file_watcher.rs @@ -14,6 +14,11 @@ use notify_debouncer_full::{ }; use std::path::{Path, PathBuf}; +/// An [`AssetWatcher`] that watches the filesystem for changes to asset files in a given root folder and emits [`AssetSourceEvent`] +/// for each relevant change. This uses [`notify_debouncer_full`] to retrieve "debounced" filesystem events. +/// "Debouncing" defines a time window to hold on to events and then removes duplicate events that fall into this window. +/// This introduces a small delay in processing events, but it helps reduce event duplicates. A small delay is also necessary +/// on some systems to avoid processing a change event before it has actually been applied. pub struct FileWatcher { _watcher: Debouncer, } diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index e25cebf8b9e9f..e516f3017e3f8 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -244,6 +244,9 @@ impl AssetSourceBuilder { self } + /// Returns a builder containing the "platform default source" for the given `path` and `processed_path`. + /// For most platforms, this will use [`FileAssetReader`](crate::io::file::FileAssetReader) / [`FileAssetWriter`](crate::io::file::FileAssetWriter), + /// but some platforms (such as Android) have their own default readers / writers / watchers. pub fn platform_default(path: &str, processed_path: &str) -> Self { Self::default() .with_reader(AssetSource::get_default_reader(path.to_string())) @@ -293,7 +296,7 @@ impl AssetSourceBuilders { } } - /// Builds an new [`AssetSources`] collection. If `watch` is true, the unprocessed sources will watch for changes. + /// Builds a new [`AssetSources`] collection. If `watch` is true, the unprocessed sources will watch for changes. /// If `watch_processed` is true, the processed sources will watch for changes. pub fn build_sources(&mut self, watch: bool, watch_processed: bool) -> AssetSources { let mut sources = HashMap::new(); @@ -318,7 +321,7 @@ impl AssetSourceBuilders { } /// Initializes the default [`AssetSourceBuilder`] if it has not already been set. - pub fn init_default_sources(&mut self, path: &str, processed_path: &str) { + pub fn init_default_source(&mut self, path: &str, processed_path: &str) { self.default .get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path)); } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index b97c777a1e1ec..367d304bb987b 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -119,7 +119,7 @@ impl Plugin for AssetPlugin { let mut sources = app .world .get_resource_or_insert_with::(Default::default); - sources.init_default_sources(&self.file_path, &self.processed_file_path); + sources.init_default_source(&self.file_path, &self.processed_file_path); embedded.register_source(&mut sources); } { diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 9ed29b3e22e20..9d0f588393af7 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -867,16 +867,16 @@ impl AssetProcessor { } LogEntryError::UnfinishedTransaction(path) => { debug!("Asset {path:?} did not finish processing. Clearning state for that asset"); - let mut unrecoverable_err = |message: &str| { - error!("Failed to remove asset {path:?} because {message}"); + let mut unrecoverable_err = |message: &dyn std::fmt::Display| { + error!("Failed to remove asset {path:?}: {message}"); state_is_valid = false; }; let Ok(source) = self.get_source(path.source()) else { - (unrecoverable_err)("AssetSource does not exist"); + (unrecoverable_err)(&"AssetSource does not exist"); continue; }; let Ok(processed_writer) = source.processed_writer() else { - (unrecoverable_err)("AssetSource does not have a processed AssetWriter registered"); + (unrecoverable_err)(&"AssetSource does not have a processed AssetWriter registered"); continue; }; @@ -885,7 +885,7 @@ impl AssetProcessor { AssetWriterError::Io(err) => { // any error but NotFound means we could be in a bad state if err.kind() != ErrorKind::NotFound { - (unrecoverable_err)("Failed to remove asset"); + (unrecoverable_err)(&err); } } } @@ -895,7 +895,7 @@ impl AssetProcessor { AssetWriterError::Io(err) => { // any error but NotFound means we could be in a bad state if err.kind() != ErrorKind::NotFound { - (unrecoverable_err)("Failed to remove asset meta"); + (unrecoverable_err)(&err); } } } From 2767872c673b0d90b79e0b4e49b5689a7c075dac Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 11 Oct 2023 18:22:54 -0700 Subject: [PATCH 07/11] Hot reload doc in scene viewer --- examples/tools/scene_viewer/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index f400513de5772..1f53b5f174fa7 100644 --- a/examples/tools/scene_viewer/main.rs +++ b/examples/tools/scene_viewer/main.rs @@ -4,6 +4,8 @@ //! replacing the path as appropriate. //! In case of multiple scenes, you can select which to display by adapting the file path: `/path/to/model.gltf#Scene1`. //! With no arguments it will load the `FlightHelmet` glTF model from the repository assets subdirectory. +//! +//! If you want to hot reload asset changes, enable the `file_watcher` cargo feature. use bevy::{ math::Vec3A, From 2e728fc0c25082c32d8c439beb9a7b210f5eca8d Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Fri, 13 Oct 2023 13:22:34 -0700 Subject: [PATCH 08/11] Box DeserializeMetaError --- crates/bevy_asset/src/server/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index ca915259c3d8e..16d087331b3c2 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -689,7 +689,7 @@ impl AssetServer { let minimal: AssetMetaMinimal = ron::de::from_bytes(&meta_bytes).map_err(|e| { AssetLoadError::DeserializeMeta { path: asset_path.clone_owned(), - error: DeserializeMetaError::DeserializeMinimal(e), + error: Box::new(DeserializeMetaError::DeserializeMinimal(e)), } })?; let loader_name = match minimal.asset { @@ -709,7 +709,7 @@ impl AssetServer { let meta = loader.deserialize_meta(&meta_bytes).map_err(|e| { AssetLoadError::DeserializeMeta { path: asset_path.clone_owned(), - error: e, + error: Box::new(e), } })?; @@ -920,7 +920,7 @@ pub enum AssetLoadError { #[error("Failed to deserialize meta for asset {path}: {error}")] DeserializeMeta { path: AssetPath<'static>, - error: DeserializeMetaError, + error: Box, }, #[error("Asset '{path}' is configured to be processed. It cannot be loaded directly.")] CannotLoadProcessedAsset { path: AssetPath<'static> }, From 219b0a4e81fc2baf7403269f02a22116c946804e Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Fri, 13 Oct 2023 14:49:22 -0700 Subject: [PATCH 09/11] Fix doc links --- crates/bevy_asset/src/io/embedded/mod.rs | 6 ++++++ crates/bevy_asset/src/io/mod.rs | 2 -- crates/bevy_asset/src/lib.rs | 2 ++ crates/bevy_asset/src/path.rs | 2 +- crates/bevy_tasks/src/single_threaded_task_pool.rs | 4 ++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index 1cd0cf43ec227..e5470cd3d5c3f 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -16,6 +16,8 @@ pub const EMBEDDED: &str = "embedded"; /// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended /// to be shared with a [`MemoryAssetReader`]. /// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this. +/// +/// [`embedded_asset`]: crate::embedded_asset #[derive(Resource, Default)] pub struct EmbeddedAssetRegistry { dir: Dir, @@ -97,6 +99,8 @@ impl EmbeddedAssetRegistry { /// Returns the [`Path`] for a given `embedded` asset. /// This is used internally by [`embedded_asset`] and can be used to get a [`Path`] /// that matches the [`AssetPath`](crate::AssetPath) used by that asset. +/// +/// [`embedded_asset`]: crate::embedded_asset #[macro_export] macro_rules! embedded_path { ($path_str: expr) => {{ @@ -177,6 +181,8 @@ macro_rules! embedded_path { /// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature. /// /// [`AssetPath`]: crate::AssetPath +/// [`embedded_asset`]: crate::embedded_asset +/// [`embedded_path`]: crate::embedded_path #[macro_export] macro_rules! embedded_asset { ($app: ident, $path: expr) => {{ diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 680928d7aef9b..14e52cddcb597 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -211,8 +211,6 @@ pub enum AssetSourceEvent { /// A handle to an "asset watcher" process, that will listen for and emit [`AssetSourceEvent`] values for as long as /// [`AssetWatcher`] has not been dropped. -/// -/// See [`AssetReader::watch_for_changes`]. pub trait AssetWatcher: Send + Sync + 'static {} /// An [`AsyncRead`] implementation capable of reading a [`Vec`]. diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index ff502de759ae9..148dc40e56d01 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -55,6 +55,8 @@ use std::{any::TypeId, sync::Arc}; /// /// Supports flexible "modes", such as [`AssetMode::Processed`] and /// [`AssetMode::Unprocessed`] that enable using the asset workflow that best suits your project. +/// +/// [`AssetSource`]: crate::io::AssetSource pub struct AssetPlugin { /// The default file path to use (relative to the project root) for unprocessed assets. pub file_path: String, diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index f104f8ddea2cf..11168ca245b50 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -51,7 +51,7 @@ use thiserror::Error; /// which allows us to optimize the static cases. /// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and /// clones internal owned [`AssetPaths`](AssetPath). -/// This also means that you should use [`AssetPath::new`] in cases where `&str` is the explicit type. +/// This also means that you should use [`AssetPath::parse`] in cases where `&str` is the explicit type. #[derive(Eq, PartialEq, Hash, Clone, Default)] pub struct AssetPath<'a> { source: AssetSourceId<'a>, diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 36e38df5a7970..9555a6a470f7c 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -12,7 +12,7 @@ pub struct TaskPoolBuilder {} /// This is a dummy struct for wasm support to provide the same api as with the multithreaded /// task pool. In the case of the multithreaded task pool this struct is used to spawn /// tasks on a specific thread. But the wasm task pool just calls -/// [`wasm_bindgen_futures::spawn_local`] for spawning which just runs tasks on the main thread +/// `wasm_bindgen_futures::spawn_local` for spawning which just runs tasks on the main thread /// and so the [`ThreadExecutor`] does nothing. #[derive(Default)] pub struct ThreadExecutor<'a>(PhantomData<&'a ()>); @@ -159,7 +159,7 @@ impl TaskPool { FakeTask } - /// Spawns a static future on the JS event loop. This is exactly the same as [`TaskSpool::spawn`]. + /// Spawns a static future on the JS event loop. This is exactly the same as [`TaskPool::spawn`]. pub fn spawn_local(&self, future: impl Future + 'static) -> FakeTask where T: 'static, From b048e4b7c5ee4fcf208be81315c9544c7bf1acf9 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Fri, 13 Oct 2023 14:58:58 -0700 Subject: [PATCH 10/11] Fix more broken links --- crates/bevy_asset/src/io/embedded/embedded_watcher.rs | 2 +- crates/bevy_asset/src/processor/mod.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs index 5bc2536278e3f..6e92caa5d3bb3 100644 --- a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -15,7 +15,7 @@ use std::{ }; /// A watcher for assets stored in the `embedded` asset source. Embedded assets are assets whose -/// bytes have been embedded into the Rust binary using the [`embedded_asset`](crate::io::embedded::embedded_asset) macro. +/// bytes have been embedded into the Rust binary using the [`embedded_asset`](crate::embedded_asset) macro. /// This watcher will watch for changes to the "source files", read the contents of changed files from the file system /// and overwrite the initial static bytes of the file embedded in the binary with the new dynamically loaded bytes. pub struct EmbeddedWatcher { diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index b2c5f7ecd2441..4e5b2a878ab00 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -647,6 +647,7 @@ impl AssetProcessor { /// to block reads until the asset is processed). /// /// [`LoadContext`]: crate::loader::LoadContext + /// [`ProcessorGatedReader`]: crate::io::processor_gated::ProcessorGatedReader async fn process_asset(&self, source: &AssetSource, path: PathBuf) { let asset_path = AssetPath::from(path).with_source(source.id()); let result = self.process_asset_internal(source, &asset_path).await; From f8bd1b2f51709bef45fc71ab3f040f6d37a65b9b Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Fri, 13 Oct 2023 16:00:31 -0700 Subject: [PATCH 11/11] Don't panic when constructing AssetSource with missing writers --- crates/bevy_asset/src/io/source.rs | 20 +++++++------------- crates/bevy_asset/src/io/wasm.rs | 3 +-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index e516f3017e3f8..ea07f8d39a4f3 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -140,14 +140,8 @@ impl AssetSourceBuilder { watch_processed: bool, ) -> Option { let reader = (self.reader.as_mut()?)(); - let writer = self.writer.as_mut().map(|w| match (w)() { - Some(w) => w, - None => panic!("{} does not have an AssetWriter configured. Note that Web and Android do not currently support writing assets.", id), - }); - let processed_writer = self.processed_writer.as_mut().map(|w| match (w)() { - Some(w) => w, - None => panic!("{} does not have a processed AssetWriter configured. Note that Web and Android do not currently support writing assets.", id), - }); + let writer = self.writer.as_mut().and_then(|w| (w)()); + let processed_writer = self.processed_writer.as_mut().and_then(|w| (w)()); let mut source = AssetSource { id: id.clone(), reader, @@ -405,12 +399,12 @@ impl AssetSource { /// Returns a builder function for this platform's default [`AssetReader`]. `path` is the relative path to /// the asset root. - pub fn get_default_reader(path: String) -> impl FnMut() -> Box + Send + Sync { + pub fn get_default_reader(_path: String) -> impl FnMut() -> Box + Send + Sync { move || { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - return Box::new(super::file::FileAssetReader::new(&path)); + return Box::new(super::file::FileAssetReader::new(&_path)); #[cfg(target_arch = "wasm32")] - return Box::new(super::wasm::HttpWasmAssetReader::new(&path)); + return Box::new(super::wasm::HttpWasmAssetReader::new(&_path)); #[cfg(target_os = "android")] return Box::new(super::android::AndroidAssetReader); } @@ -419,11 +413,11 @@ impl AssetSource { /// Returns a builder function for this platform's default [`AssetWriter`]. `path` is the relative path to /// the asset root. This will return [`None`] if this platform does not support writing assets by default. pub fn get_default_writer( - path: String, + _path: String, ) -> impl FnMut() -> Option> + Send + Sync { move || { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - return Some(Box::new(super::file::FileAssetWriter::new(&path))); + return Some(Box::new(super::file::FileAssetWriter::new(&_path))); #[cfg(any(target_arch = "wasm32", target_os = "android"))] return None; } diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 395b2d49b4459..99ff39799b087 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -1,6 +1,5 @@ use crate::io::{ - get_meta_path, AssetReader, AssetReaderError, AssetWatcher, EmptyPathStream, PathStream, - Reader, VecReader, + get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, }; use bevy_log::error; use bevy_utils::BoxedFuture;