From 26df1c1179a07fe3f28d84db33592bc771e5af2d Mon Sep 17 00:00:00 2001 From: Ricky Taylor Date: Tue, 21 May 2024 19:32:00 +0100 Subject: [PATCH] Add more load_direct implementations (#13415) # Objective - Introduce variants of `LoadContext::load_direct` which allow picking asset type & configuring settings. - Fixes #12963. ## Solution - Implements `ErasedLoadedAsset::downcast` and adds some accessors to `LoadedAsset`. - Changes `load_direct`/`load_direct_with_reader` to be typed, and introduces `load_direct_untyped`/`load_direct_untyped_with_reader`. - Introduces `load_direct_with_settings` and `load_direct_with_reader_and_settings`. ## Testing - I've run cargo test and played with the examples which use `load_direct`. - I also extended the `asset_processing` example to use the new typed version of `load_direct` and use `load_direct_with_settings`. --- ## Changelog - Introduced new `load_direct` methods in `LoadContext` to allow specifying type & settings ## Migration Guide - `LoadContext::load_direct` has been renamed to `LoadContext::load_direct_untyped`. You may find the new `load_direct` is more appropriate for your use case (and the migration may only be moving one type parameter). - `LoadContext::load_direct_with_reader` has been renamed to `LoadContext::load_direct_untyped_with_reader`. --- This might not be an obvious win as a solution because it introduces quite a few new `load_direct` alternatives - but it does follow the existing pattern pretty well. I'm very open to alternatives. :sweat_smile: --- crates/bevy_asset/src/lib.rs | 11 +- crates/bevy_asset/src/loader.rs | 302 ++++++++++++++---- crates/bevy_asset/src/meta.rs | 29 +- examples/asset/asset_decompression.rs | 2 +- examples/asset/processing/asset_processing.rs | 16 +- examples/asset/processing/assets/a.cool.ron | 3 +- examples/asset/processing/assets/d.cool.ron | 5 +- .../asset/processing/assets/foo/b.cool.ron | 3 +- .../asset/processing/assets/foo/c.cool.ron | 3 +- 9 files changed, 291 insertions(+), 83 deletions(-) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index ea8caf003a1dd..71bd60b94f81a 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -510,12 +510,13 @@ mod tests { let mut ron: CoolTextRon = ron::de::from_bytes(&bytes)?; let mut embedded = String::new(); for dep in ron.embedded_dependencies { - let loaded = load_context.load_direct(&dep).await.map_err(|_| { - Self::Error::CannotLoadDependency { + let loaded = load_context + .load_direct::(&dep) + .await + .map_err(|_| Self::Error::CannotLoadDependency { dependency: dep.into(), - } - })?; - let cool = loaded.get::().unwrap(); + })?; + let cool = loaded.get(); embedded.push_str(&cool.text); } Ok(CoolText { diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 8630ffea8f1e5..03530d191b50d 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,8 +1,8 @@ use crate::{ io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader}, meta::{ - loader_settings_meta_transform, AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, - Settings, + loader_settings_meta_transform, meta_transform_settings, AssetHash, AssetMeta, + AssetMetaDyn, ProcessedInfoMinimal, Settings, }, path::AssetPath, Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, LoadedUntypedAsset, @@ -165,6 +165,29 @@ impl LoadedAsset { meta, } } + + /// Cast (and take ownership) of the [`Asset`] value of the given type. + pub fn take(self) -> A { + self.value + } + + /// Retrieves a reference to the internal [`Asset`] type. + pub fn get(&self) -> &A { + &self.value + } + + /// Returns the [`ErasedLoadedAsset`] for the given label, if it exists. + pub fn get_labeled( + &self, + label: impl Into>, + ) -> Option<&ErasedLoadedAsset> { + self.labeled_assets.get(&label.into()).map(|a| &a.asset) + } + + /// Iterate over all labels for "labeled assets" in the loaded asset + pub fn iter_labels(&self) -> impl Iterator { + self.labeled_assets.keys().map(|s| &**s) + } } impl From for LoadedAsset { @@ -228,6 +251,25 @@ impl ErasedLoadedAsset { pub fn iter_labels(&self) -> impl Iterator { self.labeled_assets.keys().map(|s| &**s) } + + /// Cast this loaded asset as the given type. If the type does not match, + /// the original type-erased asset is returned. + #[allow(clippy::result_large_err)] + pub fn downcast(mut self) -> Result, ErasedLoadedAsset> { + match self.value.downcast::() { + Ok(value) => Ok(LoadedAsset { + value: *value, + dependencies: self.dependencies, + loader_dependencies: self.loader_dependencies, + labeled_assets: self.labeled_assets, + meta: self.meta, + }), + Err(value) => { + self.value = value; + Err(self) + } + } + } } /// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into a [`World`]'s [`Assets`] collection. @@ -464,7 +506,7 @@ impl<'a> LoadContext<'a> { /// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset. /// If the current context is a normal [`AssetServer::load`], an actual asset load will be kicked off immediately, which ensures the load happens /// as soon as possible. - /// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately. + /// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately. /// If the current context is configured to not load dependencies automatically (ex: [`AssetProcessor`](crate::processor::AssetProcessor)), /// a load will not be kicked off automatically. It is then the calling context's responsibility to begin a load if necessary. pub fn load<'b, A: Asset>(&mut self, path: impl Into>) -> Handle { @@ -495,7 +537,7 @@ impl<'a> LoadContext<'a> { /// Loads the [`Asset`] of type `A` at the given `path` with the given [`AssetLoader::Settings`] settings `S`. This is a "deferred" /// load. If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log - /// and the asset load will fail. + /// and the asset load will fail. pub fn load_with_settings<'b, A: Asset, S: Settings + Default>( &mut self, path: impl Into>, @@ -525,6 +567,95 @@ impl<'a> LoadContext<'a> { handle } + async fn load_direct_untyped_internal( + &mut self, + path: AssetPath<'static>, + meta: Box, + loader: &dyn ErasedAssetLoader, + reader: &mut Reader<'_>, + ) -> Result { + let loaded_asset = self + .asset_server + .load_with_meta_loader_and_reader( + &path, + meta, + loader, + reader, + false, + self.populate_hashes, + ) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error, + })?; + let info = loaded_asset + .meta + .as_ref() + .and_then(|m| m.processed_info().as_ref()); + let hash = info.map(|i| i.full_hash).unwrap_or(Default::default()); + self.loader_dependencies.insert(path, hash); + Ok(loaded_asset) + } + + async fn load_direct_internal( + &mut self, + path: AssetPath<'static>, + meta: Box, + loader: &dyn ErasedAssetLoader, + reader: &mut Reader<'_>, + ) -> Result, LoadDirectError> { + self.load_direct_untyped_internal(path.clone(), meta, loader, &mut *reader) + .await + .and_then(move |untyped_asset| { + untyped_asset.downcast::().map_err(|_| LoadDirectError { + dependency: path.clone(), + error: AssetLoadError::RequestedHandleTypeMismatch { + path, + requested: TypeId::of::(), + actual_asset_name: loader.asset_type_name(), + loader_name: loader.type_name(), + }, + }) + }) + } + + async fn load_direct_untyped_with_transform( + &mut self, + path: AssetPath<'static>, + meta_transform: impl FnOnce(&mut dyn AssetMetaDyn), + ) -> Result { + let (mut meta, loader, mut reader) = self + .asset_server + .get_meta_loader_and_reader(&path, None) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error, + })?; + meta_transform(&mut *meta); + self.load_direct_untyped_internal(path.clone(), meta, &*loader, &mut *reader) + .await + } + + async fn load_direct_with_transform( + &mut self, + path: AssetPath<'static>, + meta_transform: impl FnOnce(&mut dyn AssetMetaDyn), + ) -> Result, LoadDirectError> { + let (mut meta, loader, mut reader) = self + .asset_server + .get_meta_loader_and_reader(&path, Some(TypeId::of::())) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error, + })?; + meta_transform(&mut *meta); + self.load_direct_internal(path.clone(), meta, &*loader, &mut *reader) + .await + } + /// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before /// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are /// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a @@ -535,42 +666,54 @@ impl<'a> LoadContext<'a> { /// /// [`Process`]: crate::processor::Process /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave - pub async fn load_direct<'b>( + pub async fn load_direct<'b, A: Asset>( + &mut self, + path: impl Into>, + ) -> Result, LoadDirectError> { + self.load_direct_with_transform(path.into().into_owned(), |_| {}) + .await + } + + /// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before + /// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are + /// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a + /// "load dependency". + /// + /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor, + /// changing a "load dependency" will result in re-processing of the asset. + /// + /// If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log + /// and the asset load will fail. + /// + /// [`Process`]: crate::processor::Process + /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + pub async fn load_direct_with_settings<'b, A: Asset, S: Settings + Default>( + &mut self, + path: impl Into>, + settings: impl Fn(&mut S) + Send + Sync + 'static, + ) -> Result, LoadDirectError> { + self.load_direct_with_transform(path.into().into_owned(), move |meta| { + meta_transform_settings(meta, &settings); + }) + .await + } + + /// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before + /// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are + /// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a + /// "load dependency". + /// + /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor, + /// changing a "load dependency" will result in re-processing of the asset. + /// + /// [`Process`]: crate::processor::Process + /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + pub async fn load_direct_untyped<'b>( &mut self, path: impl Into>, ) -> Result { - let path = path.into().into_owned(); - let to_error = |e: AssetLoadError| -> LoadDirectError { - LoadDirectError { - dependency: path.clone(), - error: e, - } - }; - let loaded_asset = { - let (meta, loader, mut reader) = self - .asset_server - .get_meta_loader_and_reader(&path, None) - .await - .map_err(to_error)?; - self.asset_server - .load_with_meta_loader_and_reader( - &path, - meta, - &*loader, - &mut *reader, - false, - self.populate_hashes, - ) - .await - .map_err(to_error)? - }; - let info = loaded_asset - .meta - .as_ref() - .and_then(|m| m.processed_info().as_ref()); - let hash = info.map(|i| i.full_hash).unwrap_or(Default::default()); - self.loader_dependencies.insert(path, hash); - Ok(loaded_asset) + self.load_direct_untyped_with_transform(path.into().into_owned(), |_| {}) + .await } /// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before @@ -583,16 +726,16 @@ impl<'a> LoadContext<'a> { /// /// [`Process`]: crate::processor::Process /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave - pub async fn load_direct_with_reader<'b>( + pub async fn load_direct_with_reader<'b, A: Asset>( &mut self, reader: &mut Reader<'_>, path: impl Into>, - ) -> Result { + ) -> Result, LoadDirectError> { let path = path.into().into_owned(); let loader = self .asset_server - .get_path_asset_loader(&path) + .get_asset_loader_with_asset_type::() .await .map_err(|error| LoadDirectError { dependency: path.clone(), @@ -601,32 +744,77 @@ impl<'a> LoadContext<'a> { let meta = loader.default_meta(); - let loaded_asset = self + self.load_direct_internal(path, meta, &*loader, reader) + .await + } + + /// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before + /// returning. Use this if you need the _value_ of another asset in order to load the current asset, and that value comes from your [`Reader`]. + /// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a + /// "load dependency". + /// + /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor, + /// changing a "load dependency" will result in re-processing of the asset. + /// + /// If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log + /// and the asset load will fail. + /// + /// [`Process`]: crate::processor::Process + /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + pub async fn load_direct_with_reader_and_settings<'b, A: Asset, S: Settings + Default>( + &mut self, + reader: &mut Reader<'_>, + path: impl Into>, + settings: impl Fn(&mut S) + Send + Sync + 'static, + ) -> Result, LoadDirectError> { + let path = path.into().into_owned(); + + let loader = self .asset_server - .load_with_meta_loader_and_reader( - &path, - meta, - &*loader, - reader, - false, - self.populate_hashes, - ) + .get_asset_loader_with_asset_type::() .await .map_err(|error| LoadDirectError { dependency: path.clone(), - error, + error: error.into(), })?; - let info = loaded_asset - .meta - .as_ref() - .and_then(|m| m.processed_info().as_ref()); + let mut meta = loader.default_meta(); + meta_transform_settings(&mut *meta, &settings); - let hash = info.map(|i| i.full_hash).unwrap_or_default(); + self.load_direct_internal(path, meta, &*loader, reader) + .await + } - self.loader_dependencies.insert(path, hash); + /// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before + /// returning. Use this if you need the _value_ of another asset in order to load the current asset, and that value comes from your [`Reader`]. + /// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a + /// "load dependency". + /// + /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor, + /// changing a "load dependency" will result in re-processing of the asset. + /// + /// [`Process`]: crate::processor::Process + /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + pub async fn load_direct_untyped_with_reader<'b>( + &mut self, + reader: &mut Reader<'_>, + path: impl Into>, + ) -> Result { + let path = path.into().into_owned(); - Ok(loaded_asset) + let loader = self + .asset_server + .get_path_asset_loader(&path) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error: error.into(), + })?; + + let meta = loader.default_meta(); + + self.load_direct_untyped_internal(path, meta, &*loader, reader) + .await } } diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index dcfdd957a408a..1fcefbb00f8ab 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -208,21 +208,26 @@ impl AssetLoader for () { } } +pub(crate) fn meta_transform_settings( + meta: &mut dyn AssetMetaDyn, + settings: &(impl Fn(&mut S) + Send + Sync + 'static), +) { + if let Some(loader_settings) = meta.loader_settings_mut() { + if let Some(loader_settings) = loader_settings.downcast_mut::() { + settings(loader_settings); + } else { + error!( + "Configured settings type {} does not match AssetLoader settings type", + std::any::type_name::(), + ); + } + } +} + pub(crate) fn loader_settings_meta_transform( settings: impl Fn(&mut S) + Send + Sync + 'static, ) -> MetaTransform { - Box::new(move |meta| { - if let Some(loader_settings) = meta.loader_settings_mut() { - if let Some(loader_settings) = loader_settings.downcast_mut::() { - settings(loader_settings); - } else { - error!( - "Configured settings type {} does not match AssetLoader settings type", - std::any::type_name::(), - ); - } - } - }) + Box::new(move |meta| meta_transform_settings(meta, &settings)) } pub type AssetHash = [u8; 32]; diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index 13bbb0d05895c..b806e7d7423ca 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -72,7 +72,7 @@ impl AssetLoader for GzAssetLoader { let mut reader = VecReader::new(bytes_uncompressed); let uncompressed = load_context - .load_direct_with_reader(&mut reader, contained_path) + .load_direct_untyped_with_reader(&mut reader, contained_path) .await?; Ok(GzAsset { uncompressed }) diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index fb51aa4f4250a..ebe5f628980bb 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -72,7 +72,7 @@ struct Text(String); #[derive(Default)] struct TextLoader; -#[derive(Default, Serialize, Deserialize)] +#[derive(Clone, Default, Serialize, Deserialize)] struct TextSettings { text_override: Option, } @@ -107,6 +107,7 @@ struct CoolTextRon { text: String, dependencies: Vec, embedded_dependencies: Vec, + dependencies_with_settings: Vec<(String, TextSettings)>, } #[derive(Asset, TypePath, Debug)] @@ -145,9 +146,16 @@ impl AssetLoader for CoolTextLoader { let ron: CoolTextRon = ron::de::from_bytes(&bytes)?; let mut base_text = ron.text; for embedded in ron.embedded_dependencies { - let loaded = load_context.load_direct(&embedded).await?; - let text = loaded.get::().unwrap(); - base_text.push_str(&text.0); + let loaded = load_context.load_direct::(&embedded).await?; + base_text.push_str(&loaded.get().0); + } + for (path, settings_override) in ron.dependencies_with_settings { + let loaded = load_context + .load_direct_with_settings::(&path, move |settings| { + *settings = settings_override.clone(); + }) + .await?; + base_text.push_str(&loaded.get().0); } Ok(CoolText { text: base_text, diff --git a/examples/asset/processing/assets/a.cool.ron b/examples/asset/processing/assets/a.cool.ron index 6c6051f1e29de..486ce4f4eff04 100644 --- a/examples/asset/processing/assets/a.cool.ron +++ b/examples/asset/processing/assets/a.cool.ron @@ -5,4 +5,5 @@ "foo/c.cool.ron", ], embedded_dependencies: [], -) \ No newline at end of file + dependencies_with_settings: [], +) diff --git a/examples/asset/processing/assets/d.cool.ron b/examples/asset/processing/assets/d.cool.ron index 12b8254c51cf5..5a38723aeb710 100644 --- a/examples/asset/processing/assets/d.cool.ron +++ b/examples/asset/processing/assets/d.cool.ron @@ -6,4 +6,7 @@ "foo/c.cool.ron", "embedded://asset_processing/e.txt" ], -) \ No newline at end of file + dependencies_with_settings: [ + ("embedded://asset_processing/e.txt", (text_override: Some("E"))), + ], +) diff --git a/examples/asset/processing/assets/foo/b.cool.ron b/examples/asset/processing/assets/foo/b.cool.ron index f72581c1db245..22120498338ff 100644 --- a/examples/asset/processing/assets/foo/b.cool.ron +++ b/examples/asset/processing/assets/foo/b.cool.ron @@ -2,4 +2,5 @@ text: "b", dependencies: [], embedded_dependencies: [], -) \ No newline at end of file + dependencies_with_settings: [], +) diff --git a/examples/asset/processing/assets/foo/c.cool.ron b/examples/asset/processing/assets/foo/c.cool.ron index c145493f15109..efe4e97f42b11 100644 --- a/examples/asset/processing/assets/foo/c.cool.ron +++ b/examples/asset/processing/assets/foo/c.cool.ron @@ -2,4 +2,5 @@ text: "c", dependencies: [], embedded_dependencies: ["a.cool.ron", "foo/b.cool.ron"], -) \ No newline at end of file + dependencies_with_settings: [], +)