From 1df8238e8d8fe968d682468301ad4140c95b7604 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:14:04 +0100 Subject: [PATCH] bevy_asset: Improve `NestedLoader` API (#15509) # Objective The `NestedLoader` API as it stands right now is somewhat lacking: - It consists of several types `NestedLoader`, `UntypedNestedLoader`, `DirectNestedLoader`, and `UntypedDirectNestedLoader`, where a typestate pattern on `NestedLoader` would be make it more obvious what it does, and allow centralising the documentation - The term "untyped" in the asset loader code is overloaded. It can mean either: - we have literally no idea what the type of this asset will be when we load it (I dub this "unknown type") - we know what type of asset it will be, but we don't know it statically - we only have a TypeId (I dub this "dynamic type" / "erased") - There is no way to get an `UntypedHandle` (erased) given a `TypeId` ## Solution Changes `NestedLoader` into a type-state pattern, adding two type params: - `T` determines the typing - `StaticTyped`, the default, where you pass in `A` statically into `fn load() -> ..` - `DynamicTyped`, where you give a `TypeId`, giving you a `UntypedHandle` - `UnknownTyped`, where you have literally no idea what type of asset you're loading, giving you a `Handle` - `M` determines the "mode" (bikeshedding TBD, I couldn't come up with a better name) - `Deferred`, the default, won't load the asset when you call `load`, but it does give you a `Handle` to it (this is nice since it can be a sync fn) - `Immediate` will load the asset as soon as you call it, and give you access to it, but you must be in an async context to call it Changes some naming of internals in `AssetServer` to fit the new definitions of "dynamic type" and "unknown type". Note that I didn't do a full pass over this code to keep the diff small. That can probably be done in a new PR - I think the definiton I laid out of unknown type vs. erased makes it pretty clear where each one applies.
Old issue The only real problem I have with this PR is the requirement to pass in `type_name` (from `core::any::type_name`) into Erased. Users might not have that type name, only the ID, and it just seems sort of weird to *have* to give an asset type name. However, the reason we need it is because of this: ```rs pub(crate) fn get_or_create_path_handle_erased( &mut self, path: AssetPath<'static>, type_id: TypeId, type_name: &str, loading_mode: HandleLoadingMode, meta_transform: Option, ) -> (UntypedHandle, bool) { let result = self.get_or_create_path_handle_internal( path, Some(type_id), loading_mode, meta_transform, ); // it is ok to unwrap because TypeId was specified above unwrap_with_context(result, type_name).unwrap() } pub(crate) fn unwrap_with_context( result: Result, type_name: &str, ) -> Option { match result { Ok(value) => Some(value), Err(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified) => None, Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => { panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ Make sure you have called app.init_asset::<{type_name}>()") } } } ``` This `unwrap_with_context` is literally the only reason we need the `type_name`. Potentially, this can be turned into an `impl Into>`, and output a different error message if the type name is missing. Since if we are loading an asset where we only know the type ID, by definition we can't output that error message, since we don't have the type name. I'm open to suggestions on this.
## Testing Not sure how to test this, since I kept most of the actual NestedLoader logic the same. The only new API is loading an `UntypedHandle` when in the `DynamicTyped, Immediate` state. ## Migration Guide Code which uses `bevy_asset`'s `LoadContext::loader` / `NestedLoader` will see some naming changes: - `untyped` is replaced by `with_unknown_type` - `with_asset_type` is replaced by `with_static_type` - `with_asset_type_id` is replaced by `with_dynamic_type` - `direct` is replaced by `immediate` (the opposite of "immediate" is "deferred") --- crates/bevy_asset/Cargo.toml | 1 + crates/bevy_asset/src/lib.rs | 4 +- crates/bevy_asset/src/loader.rs | 10 +- crates/bevy_asset/src/loader_builders.rs | 427 +++++++++++++----- crates/bevy_asset/src/server/info.rs | 33 +- crates/bevy_asset/src/server/mod.rs | 98 +++- examples/asset/asset_decompression.rs | 4 +- examples/asset/processing/asset_processing.rs | 4 +- 8 files changed, 425 insertions(+), 156 deletions(-) diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index d05bb498888eb..7883985e51e5e 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -36,6 +36,7 @@ async-lock = "3.0" crossbeam-channel = "0.5" downcast-rs = "1.2" disqualified = "1.0" +either = "1.13" futures-io = "0.3" futures-lite = "2.0.1" blake3 = "1.5" diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index af957b45454f4..08d08c4b1c2df 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -188,7 +188,7 @@ pub use handle::*; pub use id::*; pub use loader::*; pub use loader_builders::{ - DirectNestedLoader, NestedLoader, UntypedDirectNestedLoader, UntypedNestedLoader, + Deferred, DynamicTyped, Immediate, NestedLoader, StaticTyped, UnknownTyped, }; pub use path::*; pub use reflect::*; @@ -689,7 +689,7 @@ mod tests { for dep in ron.embedded_dependencies { let loaded = load_context .loader() - .direct() + .immediate() .load::(&dep) .await .map_err(|_| Self::Error::CannotLoadDependency { diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index aa66985631253..e1315a96e26b2 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,6 +1,6 @@ use crate::{ io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader}, - loader_builders::NestedLoader, + loader_builders::{Deferred, NestedLoader, StaticTyped}, meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings}, path::AssetPath, Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId, @@ -290,7 +290,11 @@ impl AssetContainer for A { } } -/// An error that occurs when attempting to call [`DirectNestedLoader::load`](crate::DirectNestedLoader::load) +/// An error that occurs when attempting to call [`NestedLoader::load`] which +/// is configured to work [immediately]. +/// +/// [`NestedLoader::load`]: crate::NestedLoader::load +/// [immediately]: crate::Immediate #[derive(Error, Debug)] #[error("Failed to load dependency {dependency:?} {error}")] pub struct LoadDirectError { @@ -550,7 +554,7 @@ impl<'a> LoadContext<'a> { /// Create a builder for loading nested assets in this context. #[must_use] - pub fn loader(&mut self) -> NestedLoader<'a, '_> { + pub fn loader(&mut self) -> NestedLoader<'a, '_, StaticTyped, Deferred> { NestedLoader::new(self) } diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 23cf722803546..681bd16cbc94d 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -5,7 +5,7 @@ use crate::{ io::Reader, meta::{meta_transform_settings, AssetMetaDyn, MetaTransform, Settings}, Asset, AssetLoadError, AssetPath, ErasedAssetLoader, ErasedLoadedAsset, Handle, LoadContext, - LoadDirectError, LoadedAsset, LoadedUntypedAsset, + LoadDirectError, LoadedAsset, LoadedUntypedAsset, UntypedHandle, }; use alloc::sync::Arc; use core::any::TypeId; @@ -25,28 +25,157 @@ impl ReaderRef<'_> { } } -/// A builder for loading nested assets inside a `LoadContext`. +/// A builder for loading nested assets inside a [`LoadContext`]. +/// +/// # Loader state +/// +/// The type parameters `T` and `M` determine how this will load assets: +/// - `T`: the typing of this loader. How do we know what type of asset to load? +/// +/// See [`StaticTyped`] (the default), [`DynamicTyped`], and [`UnknownTyped`]. +/// +/// - `M`: the load mode. Do we want to load this asset right now (in which case +/// you will have to `await` the operation), or do we just want a [`Handle`], +/// and leave the actual asset loading to later? +/// +/// See [`Deferred`] (the default) and [`Immediate`]. +/// +/// When configuring this builder, you can freely switch between these modes +/// via functions like [`deferred`] and [`immediate`]. +/// +/// ## Typing +/// +/// To inform the loader of what type of asset to load: +/// - in [`StaticTyped`]: statically providing a type parameter `A: Asset` to +/// [`load`]. +/// +/// This is the simplest way to get a [`Handle
`] to the loaded asset, as +/// long as you know the type of `A` at compile time. +/// +/// - in [`DynamicTyped`]: providing the [`TypeId`] of the asset at runtime. +/// +/// If you know the type ID of the asset at runtime, but not at compile time, +/// use [`with_dynamic_type`] followed by [`load`] to start loading an asset +/// of that type. This lets you get an [`UntypedHandle`] (via [`Deferred`]), +/// or a [`ErasedLoadedAsset`] (via [`Immediate`]). +/// +/// - in [`UnknownTyped`]: loading either a type-erased version of the asset +/// ([`ErasedLoadedAsset`]), or a handle *to a handle* of the actual asset +/// ([`LoadedUntypedAsset`]). +/// +/// If you have no idea what type of asset you will be loading (not even at +/// runtime with a [`TypeId`]), use this. +/// +/// ## Load mode +/// +/// To inform the loader how you want to load the asset: +/// - in [`Deferred`]: when you request to load the asset, you get a [`Handle`] +/// for it, but the actual loading won't be completed until later. +/// +/// Use this if you only need a [`Handle`] or [`UntypedHandle`]. +/// +/// - in [`Immediate`]: the load request will load the asset right then and +/// there, waiting until the asset is fully loaded and giving you access to +/// it. +/// +/// Note that this requires you to `await` a future, so you must be in an +/// async context to use direct loading. In an asset loader, you will be in +/// an async context. +/// +/// 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 of the asset 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. +/// +/// # Load kickoff +/// +/// 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. +/// +/// If the current context is configured to not load dependencies automatically +/// (ex: [`AssetProcessor`]), a load will not be kicked off automatically. It is +/// then the calling context's responsibility to begin a load if necessary. /// /// # Lifetimes +/// /// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference /// - `builder`: the lifetime of the temporary builder structs -pub struct NestedLoader<'ctx, 'builder> { +/// +/// [`deferred`]: Self::deferred +/// [`immediate`]: Self::immediate +/// [`load`]: Self::load +/// [`with_dynamic_type`]: Self::with_dynamic_type +/// [`AssetServer::load`]: crate::AssetServer::load +/// [`AssetProcessor`]: crate::processor::AssetProcessor +/// [`Process`]: crate::processor::Process +/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave +pub struct NestedLoader<'ctx, 'builder, T, M> { load_context: &'builder mut LoadContext<'ctx>, meta_transform: Option, - asset_type_id: Option, + typing: T, + mode: M, +} + +mod sealed { + pub trait Typing {} + + pub trait Mode {} } -impl<'ctx, 'builder> NestedLoader<'ctx, 'builder> { - pub(crate) fn new( - load_context: &'builder mut LoadContext<'ctx>, - ) -> NestedLoader<'ctx, 'builder> { +/// [`NestedLoader`] will be provided the type of asset as a type parameter on +/// [`load`]. +/// +/// [`load`]: NestedLoader::load +pub struct StaticTyped(()); + +impl sealed::Typing for StaticTyped {} + +/// [`NestedLoader`] has been configured with info on what type of asset to load +/// at runtime. +pub struct DynamicTyped { + asset_type_id: TypeId, +} + +impl sealed::Typing for DynamicTyped {} + +/// [`NestedLoader`] does not know what type of asset it will be loading. +pub struct UnknownTyped(()); + +impl sealed::Typing for UnknownTyped {} + +/// [`NestedLoader`] will create and return asset handles immediately, but only +/// actually load the asset later. +pub struct Deferred(()); + +impl sealed::Mode for Deferred {} + +/// [`NestedLoader`] will immediately load an asset when requested. +pub struct Immediate<'builder, 'reader> { + reader: Option<&'builder mut (dyn Reader + 'reader)>, +} + +impl sealed::Mode for Immediate<'_, '_> {} + +// common to all states + +impl<'ctx, 'builder> NestedLoader<'ctx, 'builder, StaticTyped, Deferred> { + pub(crate) fn new(load_context: &'builder mut LoadContext<'ctx>) -> Self { NestedLoader { load_context, meta_transform: None, - asset_type_id: None, + typing: StaticTyped(()), + mode: Deferred(()), } } +} +impl<'ctx, 'builder, T: sealed::Typing, M: sealed::Mode> NestedLoader<'ctx, 'builder, T, M> { fn with_transform( mut self, transform: impl Fn(&mut dyn AssetMetaDyn) + Send + Sync + 'static, @@ -74,47 +203,105 @@ impl<'ctx, 'builder> NestedLoader<'ctx, 'builder> { self.with_transform(move |meta| meta_transform_settings(meta, &settings)) } - /// Specify the output asset type. + // convert between `T`s + + /// When [`load`]ing, you must pass in the asset type as a type parameter + /// statically. + /// + /// If you don't know the type statically (at compile time), consider + /// [`with_dynamic_type`] or [`with_unknown_type`]. + /// + /// [`load`]: Self::load + /// [`with_dynamic_type`]: Self::with_dynamic_type + /// [`with_unknown_type`]: Self::with_unknown_type #[must_use] - pub fn with_asset_type(mut self) -> Self { - self.asset_type_id = Some(TypeId::of::()); - self + pub fn with_static_type(self) -> NestedLoader<'ctx, 'builder, StaticTyped, M> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: StaticTyped(()), + mode: self.mode, + } } - /// Specify the output asset type. + /// When [`load`]ing, the loader will attempt to load an asset with the + /// given [`TypeId`]. + /// + /// [`load`]: Self::load #[must_use] - pub fn with_asset_type_id(mut self, asset_type_id: TypeId) -> Self { - self.asset_type_id = Some(asset_type_id); - self + pub fn with_dynamic_type( + self, + asset_type_id: TypeId, + ) -> NestedLoader<'ctx, 'builder, DynamicTyped, M> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: DynamicTyped { asset_type_id }, + mode: self.mode, + } } - /// Load assets directly, rather than creating handles. + /// When [`load`]ing, we will infer what type of asset to load from + /// metadata. + /// + /// [`load`]: Self::load #[must_use] - pub fn direct<'c>(self) -> DirectNestedLoader<'ctx, 'builder, 'c> { - DirectNestedLoader { - base: self, - reader: None, + pub fn with_unknown_type(self) -> NestedLoader<'ctx, 'builder, UnknownTyped, M> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: UnknownTyped(()), + mode: self.mode, } } - /// Load assets without static type information. + // convert between `M`s + + /// When [`load`]ing, create only asset handles, rather than returning the + /// actual asset. /// - /// If you need to specify the type of asset, but cannot do it statically, - /// use `.with_asset_type_id()`. + /// [`load`]: Self::load + pub fn deferred(self) -> NestedLoader<'ctx, 'builder, T, Deferred> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: self.typing, + mode: Deferred(()), + } + } + + /// The [`load`] call itself will load an asset, rather than scheduling the + /// loading to happen later. + /// + /// This gives you access to the loaded asset, but requires you to be in an + /// async context, and be able to `await` the resulting future. + /// + /// [`load`]: Self::load #[must_use] - pub fn untyped(self) -> UntypedNestedLoader<'ctx, 'builder> { - UntypedNestedLoader { base: self } + pub fn immediate<'c>(self) -> NestedLoader<'ctx, 'builder, T, Immediate<'builder, 'c>> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: self.typing, + mode: Immediate { reader: None }, + } } +} - /// 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`](crate::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. - /// 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. +// deferred loading logic + +impl NestedLoader<'_, '_, StaticTyped, Deferred> { + /// Retrieves a handle for the asset at the given path and adds that path as + /// a dependency of this asset. + /// + /// This requires you to know the type of asset statically. + /// - If you have runtime info for what type of asset you're loading (e.g. a + /// [`TypeId`]), use [`with_dynamic_type`]. + /// - If you do not know at all what type of asset you're loading, use + /// [`with_unknown_type`]. + /// + /// [`with_dynamic_type`]: Self::with_dynamic_type + /// [`with_unknown_type`]: Self::with_unknown_type pub fn load<'c, A: Asset>(self, path: impl Into>) -> Handle { let path = path.into().to_owned(); let handle = if self.load_context.should_load_dependencies { @@ -131,74 +318,78 @@ impl<'ctx, 'builder> NestedLoader<'ctx, 'builder> { } } -/// A builder for loading untyped nested assets inside a [`LoadContext`]. -/// -/// # Lifetimes -/// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference -/// - `builder`: the lifetime of the temporary builder structs -pub struct UntypedNestedLoader<'ctx, 'builder> { - base: NestedLoader<'ctx, 'builder>, +impl NestedLoader<'_, '_, DynamicTyped, Deferred> { + /// Retrieves a handle for the asset at the given path and adds that path as + /// a dependency of this asset. + /// + /// This requires you to pass in the asset type ID into + /// [`with_dynamic_type`]. + /// + /// [`with_dynamic_type`]: Self::with_dynamic_type + pub fn load<'p>(self, path: impl Into>) -> UntypedHandle { + let path = path.into().to_owned(); + let handle = if self.load_context.should_load_dependencies { + self.load_context + .asset_server + .load_erased_with_meta_transform( + path, + self.typing.asset_type_id, + self.meta_transform, + (), + ) + } else { + self.load_context + .asset_server + .get_or_create_path_handle_erased( + path, + self.typing.asset_type_id, + self.meta_transform, + ) + }; + self.load_context.dependencies.insert(handle.id()); + handle + } } -impl<'ctx, 'builder> UntypedNestedLoader<'ctx, 'builder> { - /// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset without knowing its type. +impl NestedLoader<'_, '_, UnknownTyped, Deferred> { + /// Retrieves a handle for the asset at the given path and adds that path as + /// a dependency of this asset. + /// + /// This will infer the asset type from metadata. pub fn load<'p>(self, path: impl Into>) -> Handle { let path = path.into().to_owned(); - let handle = if self.base.load_context.should_load_dependencies { - self.base - .load_context + let handle = if self.load_context.should_load_dependencies { + self.load_context .asset_server - .load_untyped_with_meta_transform(path, self.base.meta_transform) + .load_unknown_type_with_meta_transform(path, self.meta_transform) } else { - self.base - .load_context + self.load_context .asset_server - .get_or_create_path_handle(path, self.base.meta_transform) + .get_or_create_path_handle(path, self.meta_transform) }; - self.base - .load_context - .dependencies - .insert(handle.id().untyped()); + self.load_context.dependencies.insert(handle.id().untyped()); handle } } -/// A builder for directly loading nested assets inside a `LoadContext`. -/// -/// # Lifetimes -/// - `ctx`: the lifetime of the associated [`AssetServer`][crate::AssetServer] reference -/// - `builder`: the lifetime of the temporary builder structs -/// - `reader`: the lifetime of the [`Reader`] reference used to read the asset data -pub struct DirectNestedLoader<'ctx, 'builder, 'reader> { - base: NestedLoader<'ctx, 'builder>, - reader: Option<&'builder mut (dyn Reader + 'reader)>, -} +// immediate loading logic -impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reader> { +impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> { /// Specify the reader to use to read the asset data. #[must_use] pub fn with_reader(mut self, reader: &'builder mut (dyn Reader + 'reader)) -> Self { - self.reader = Some(reader); + self.mode.reader = Some(reader); self } - /// Load the asset without providing static type information. - /// - /// If you need to specify the type of asset, but cannot do it statically, - /// use `.with_asset_type_id()`. - #[must_use] - pub fn untyped(self) -> UntypedDirectNestedLoader<'ctx, 'builder, 'reader> { - UntypedDirectNestedLoader { base: self } - } - async fn load_internal( self, path: &AssetPath<'static>, + asset_type_id: Option, ) -> Result<(Arc, ErasedLoadedAsset), LoadDirectError> { - let (mut meta, loader, mut reader) = if let Some(reader) = self.reader { - let loader = if let Some(asset_type_id) = self.base.asset_type_id { - self.base - .load_context + let (mut meta, loader, mut reader) = if let Some(reader) = self.mode.reader { + let loader = if let Some(asset_type_id) = asset_type_id { + self.load_context .asset_server .get_asset_loader_with_asset_type_id(asset_type_id) .await @@ -207,8 +398,7 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade error: error.into(), })? } else { - self.base - .load_context + self.load_context .asset_server .get_path_asset_loader(path) .await @@ -221,10 +411,9 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade (meta, loader, ReaderRef::Borrowed(reader)) } else { let (meta, loader, reader) = self - .base .load_context .asset_server - .get_meta_loader_and_reader(path, self.base.asset_type_id) + .get_meta_loader_and_reader(path, asset_type_id) .await .map_err(|error| LoadDirectError { dependency: path.clone(), @@ -233,35 +422,35 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade (meta, loader, ReaderRef::Boxed(reader)) }; - if let Some(meta_transform) = self.base.meta_transform { + if let Some(meta_transform) = self.meta_transform { meta_transform(&mut *meta); } let asset = self - .base .load_context .load_direct_internal(path.clone(), meta, &*loader, reader.as_mut()) .await?; Ok((loader, asset)) } +} - /// 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". +impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { + /// Attempts to load the asset at the given `path` immediately. /// - /// 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. + /// This requires you to know the type of asset statically. + /// - If you have runtime info for what type of asset you're loading (e.g. a + /// [`TypeId`]), use [`with_dynamic_type`]. + /// - If you do not know at all what type of asset you're loading, use + /// [`with_unknown_type`]. /// - /// [`Process`]: crate::processor::Process - /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + /// [`with_dynamic_type`]: Self::with_dynamic_type + /// [`with_unknown_type`]: Self::with_unknown_type pub async fn load<'p, A: Asset>( - mut self, + self, path: impl Into>, ) -> Result, LoadDirectError> { - self.base.asset_type_id = Some(TypeId::of::()); let path = path.into().into_owned(); - self.load_internal(&path) + self.load_internal(&path, Some(TypeId::of::())) .await .and_then(move |(loader, untyped_asset)| { untyped_asset.downcast::().map_err(|_| LoadDirectError { @@ -277,32 +466,36 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade } } -/// A builder for directly loading untyped nested assets inside a `LoadContext`. -/// -/// # Lifetimes -/// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference -/// - `builder`: the lifetime of the temporary builder structs -/// - `reader`: the lifetime of the [`Reader`] reference used to read the asset data -pub struct UntypedDirectNestedLoader<'ctx, 'builder, 'reader> { - base: DirectNestedLoader<'ctx, 'builder, 'reader>, +impl NestedLoader<'_, '_, DynamicTyped, Immediate<'_, '_>> { + /// Attempts to load the asset at the given `path` immediately. + /// + /// This requires you to pass in the asset type ID into + /// [`with_dynamic_type`]. + /// + /// [`with_dynamic_type`]: Self::with_dynamic_type + pub async fn load<'p>( + self, + path: impl Into>, + ) -> Result { + let path = path.into().into_owned(); + let asset_type_id = Some(self.typing.asset_type_id); + self.load_internal(&path, asset_type_id) + .await + .map(|(_, asset)| asset) + } } -impl<'ctx: 'reader, 'builder, 'reader> UntypedDirectNestedLoader<'ctx, 'builder, 'reader> { - /// 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. +impl NestedLoader<'_, '_, UnknownTyped, Immediate<'_, '_>> { + /// Attempts to load the asset at the given `path` immediately. /// - /// [`Process`]: crate::processor::Process - /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + /// This will infer the asset type from metadata. pub async fn load<'p>( self, path: impl Into>, ) -> Result { let path = path.into().into_owned(); - self.base.load_internal(&path).await.map(|(_, asset)| asset) + self.load_internal(&path, None) + .await + .map(|(_, asset)| asset) } } diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index fdeaaff089604..85b3d02fa584a 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -10,6 +10,7 @@ use bevy_tasks::Task; use bevy_utils::{tracing::warn, Entry, HashMap, HashSet, TypeIdMap}; use core::any::TypeId; use crossbeam_channel::Sender; +use either::Either; use thiserror::Error; #[derive(Debug)] @@ -103,7 +104,7 @@ impl AssetInfos { None, true, ), - type_name, + Either::Left(type_name), ) .unwrap() } @@ -162,15 +163,15 @@ impl AssetInfos { ); // it is ok to unwrap because TypeId was specified above let (handle, should_load) = - unwrap_with_context(result, core::any::type_name::()).unwrap(); + unwrap_with_context(result, Either::Left(core::any::type_name::())).unwrap(); (handle.typed_unchecked(), should_load) } - pub(crate) fn get_or_create_path_handle_untyped( + pub(crate) fn get_or_create_path_handle_erased( &mut self, path: AssetPath<'static>, type_id: TypeId, - type_name: &'static str, + type_name: Option<&str>, loading_mode: HandleLoadingMode, meta_transform: Option, ) -> (UntypedHandle, bool) { @@ -180,8 +181,12 @@ impl AssetInfos { loading_mode, meta_transform, ); - // it is ok to unwrap because TypeId was specified above - unwrap_with_context(result, type_name).unwrap() + let type_info = match type_name { + Some(type_name) => Either::Left(type_name), + None => Either::Right(type_id), + }; + unwrap_with_context(result, type_info) + .expect("type should be correct since the `TypeId` is specified above") } /// Retrieves asset tracking data, or creates it if it doesn't exist. @@ -765,14 +770,20 @@ pub(crate) enum GetOrCreateHandleInternalError { pub(crate) fn unwrap_with_context( result: Result, - type_name: &'static str, + type_info: Either<&str, TypeId>, ) -> Option { match result { Ok(value) => Some(value), Err(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified) => None, - Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => { - panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ - Make sure you have called app.init_asset::<{type_name}>()") - } + Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => match type_info { + Either::Left(type_name) => { + panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ + Make sure you have called `app.init_asset::<{type_name}>()`"); + } + Either::Right(type_id) => { + panic!("Cannot allocate an AssetHandle of type '{type_id:?}' because the asset type has not been initialized. \ + Make sure you have called `app.init_asset::<(actual asset type)>()`") + } + }, } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 9d57808ed0bac..316642a45a260 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -31,6 +31,7 @@ use core::{ panic::AssertUnwindSafe, }; use crossbeam_channel::{Receiver, Sender}; +use either::Either; use futures_lite::{FutureExt, StreamExt}; use info::*; use loaders::*; @@ -382,25 +383,62 @@ impl AssetServer { ); if should_load { - let owned_handle = Some(handle.clone().untyped()); - let server = self.clone(); - let task = IoTaskPool::get().spawn(async move { - if let Err(err) = server.load_internal(owned_handle, path, false, None).await { - error!("{}", err); - } - drop(guard); - }); + self.spawn_load_task(handle.clone().untyped(), path, &mut infos, guard); + } - #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] - infos.pending_tasks.insert(handle.id().untyped(), task); + handle + } - #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] - task.detach(); + pub(crate) fn load_erased_with_meta_transform<'a, G: Send + Sync + 'static>( + &self, + path: impl Into>, + type_id: TypeId, + meta_transform: Option, + guard: G, + ) -> UntypedHandle { + let path = path.into().into_owned(); + let mut infos = self.data.infos.write(); + let (handle, should_load) = infos.get_or_create_path_handle_erased( + path.clone(), + type_id, + None, + HandleLoadingMode::Request, + meta_transform, + ); + + if should_load { + self.spawn_load_task(handle.clone(), path, &mut infos, guard); } handle } + pub(crate) fn spawn_load_task( + &self, + handle: UntypedHandle, + path: AssetPath<'static>, + infos: &mut AssetInfos, + guard: G, + ) { + let owned_handle = handle.clone(); + let server = self.clone(); + let task = IoTaskPool::get().spawn(async move { + if let Err(err) = server + .load_internal(Some(owned_handle), path, false, None) + .await + { + error!("{}", err); + } + drop(guard); + }); + + #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] + infos.pending_tasks.insert(handle.id(), task); + + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + task.detach(); + } + /// Asynchronously load an asset that you do not know the type of statically. If you _do_ know the type of the asset, /// you should use [`AssetServer::load`]. If you don't know the type of the asset, but you can't use an async method, /// consider using [`AssetServer::load_untyped`]. @@ -413,7 +451,7 @@ impl AssetServer { self.load_internal(None, path, false, None).await } - pub(crate) fn load_untyped_with_meta_transform<'a>( + pub(crate) fn load_unknown_type_with_meta_transform<'a>( &self, path: impl Into>, meta_transform: Option, @@ -492,7 +530,7 @@ impl AssetServer { /// required to figure out the asset type before a handle can be created. #[must_use = "not using the returned strong handle may result in the unexpected release of the assets"] pub fn load_untyped<'a>(&self, path: impl Into>) -> Handle { - self.load_untyped_with_meta_transform(path, None) + self.load_unknown_type_with_meta_transform(path, None) } /// Performs an async asset load. @@ -558,7 +596,7 @@ impl AssetServer { HandleLoadingMode::Request, meta_transform, ); - unwrap_with_context(result, loader.asset_type_name()) + unwrap_with_context(result, Either::Left(loader.asset_type_name())) } }; @@ -588,10 +626,10 @@ impl AssetServer { let (base_handle, base_path) = if path.label().is_some() { let mut infos = self.data.infos.write(); let base_path = path.without_label().into_owned(); - let (base_handle, _) = infos.get_or_create_path_handle_untyped( + let (base_handle, _) = infos.get_or_create_path_handle_erased( base_path.clone(), loader.asset_type_id(), - loader.asset_type_name(), + Some(loader.asset_type_name()), HandleLoadingMode::Force, None, ); @@ -707,10 +745,10 @@ impl AssetServer { ) -> UntypedHandle { let loaded_asset = asset.into(); let handle = if let Some(path) = path { - let (handle, _) = self.data.infos.write().get_or_create_path_handle_untyped( + let (handle, _) = self.data.infos.write().get_or_create_path_handle_erased( path, loaded_asset.asset_type_id(), - loaded_asset.asset_type_name(), + Some(loaded_asset.asset_type_name()), HandleLoadingMode::NotLoading, None, ); @@ -1131,6 +1169,28 @@ impl AssetServer { .0 } + /// Retrieve a handle for the given path, where the asset type ID and name + /// are not known statically. + /// + /// This will create a handle (and [`AssetInfo`]) if it does not exist. + pub(crate) fn get_or_create_path_handle_erased<'a>( + &self, + path: impl Into>, + type_id: TypeId, + meta_transform: Option, + ) -> UntypedHandle { + let mut infos = self.data.infos.write(); + infos + .get_or_create_path_handle_erased( + path.into().into_owned(), + type_id, + None, + HandleLoadingMode::NotLoading, + meta_transform, + ) + .0 + } + pub(crate) async fn get_meta_loader_and_reader<'a>( &'a self, asset_path: &'a AssetPath<'_>, diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index e961bc976cdaa..924943349c53b 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -73,9 +73,9 @@ impl AssetLoader for GzAssetLoader { let uncompressed = load_context .loader() - .direct() + .with_unknown_type() + .immediate() .with_reader(&mut reader) - .untyped() .load(contained_path) .await?; diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index 1367f26f2440f..e7eefd3aafba0 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -148,7 +148,7 @@ impl AssetLoader for CoolTextLoader { for embedded in ron.embedded_dependencies { let loaded = load_context .loader() - .direct() + .immediate() .load::(&embedded) .await?; base_text.push_str(&loaded.get().0); @@ -159,7 +159,7 @@ impl AssetLoader for CoolTextLoader { .with_settings(move |settings| { *settings = settings_override.clone(); }) - .direct() + .immediate() .load::(&path) .await?; base_text.push_str(&loaded.get().0);