Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ mod tests {
sub_texts: ron
.sub_texts
.drain(..)
.map(|text| load_context.add_labeled_asset(text.clone(), SubText { text }))
.map(|text| load_context.add_subasset(text.clone(), SubText { text }))
.collect(),
})
}
Expand Down Expand Up @@ -2574,8 +2574,8 @@ mod tests {
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
load_context.add_labeled_asset("A".into(), TestAsset);
load_context.add_labeled_asset("B".into(), TestAsset);
load_context.add_subasset("A".into(), TestAsset);
load_context.add_subasset("B".into(), TestAsset);
Ok(TestAsset)
}

Expand Down Expand Up @@ -2852,7 +2852,7 @@ mod tests {
// Load the asset in the root context, but then put the handle in the subasset. So
// the subasset's (internal) load context never loaded `dep`.
let dep = load_context.load::<TestAsset>("abc.ron");
load_context.add_labeled_asset("subasset".into(), AssetWithDep { dep });
load_context.add_subasset("subasset".into(), AssetWithDep { dep });
Ok(TestAsset)
}

Expand Down
152 changes: 79 additions & 73 deletions crates/bevy_asset/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub trait AssetLoader: TypePath + Send + Sync + 'static {
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
/// The type of [error](`std::error::Error`) which could be encountered by this loader.
type Error: Into<BevyError>;
/// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`].
/// Asynchronously loads [`AssetLoader::Asset`] (and any other subassets) from the bytes provided by [`Reader`].
fn load(
&self,
reader: &mut dyn Reader,
Expand Down Expand Up @@ -132,20 +132,20 @@ where
}
}

pub(crate) struct LabeledAsset {
pub(crate) struct LoadedSubAsset {
pub(crate) asset: ErasedLoadedAsset,
pub(crate) handle: UntypedHandle,
}

/// The successful result of an [`AssetLoader::load`] call. This contains the loaded "root" asset and any other "labeled" assets produced
/// The successful result of an [`AssetLoader::load`] call. This contains the loaded "root" asset and any other "sub-assets" produced
/// by the loader. It also holds the input [`AssetMeta`] (if it exists) and tracks dependencies:
/// * normal dependencies: dependencies that must be loaded as part of this asset load (ex: assets a given asset has handles to).
/// * Loader dependencies: dependencies whose actual asset values are used during the load process
pub struct LoadedAsset<A: Asset> {
pub(crate) value: A,
pub(crate) dependencies: HashSet<ErasedAssetIndex>,
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
pub(crate) subassets: HashMap<CowArc<'static, str>, LoadedSubAsset>,
}

impl<A: Asset> LoadedAsset<A> {
Expand All @@ -162,7 +162,7 @@ impl<A: Asset> LoadedAsset<A> {
value,
dependencies,
loader_dependencies: HashMap::default(),
labeled_assets: HashMap::default(),
subassets: HashMap::default(),
}
}

Expand All @@ -176,17 +176,17 @@ impl<A: Asset> LoadedAsset<A> {
&self.value
}

/// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
pub fn get_labeled(
/// Returns the [`ErasedLoadedAsset`] for the given subasset name, if it exists.
pub fn get_subasset(
&self,
label: impl Into<CowArc<'static, str>>,
subasset_name: impl Into<CowArc<'static, str>>,
Copy link
Member

@cart cart Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Name" vs "label" is a bit of an interesting question. "Name" has a "canonical-ness" to it that "label" does not. Label was initially chosen because we thought we might eventually want to support multiple labels per asset.

In practice, that never ended up being very important, and label has served as a unique / canonical identifier. In that context, "name" makes more sense.

Additionally, with our current implementation and culture, I think subasset is clearly the better name than "labeled asset".

However we also have the upcoming "assets as entities" to consider. And as discussed above, Name is already a Bevy concept / component. Depending on how we choose to represent "asset entities" and "subasset entities", Name may or may not make sense in that context. I suspect it will (and the tooling benefits we get from that approach are extremely compelling), but that is uncharted territory.

Additionally, with "assets as entities" we might choose represent subassets as children. In that context, it might make more sense to call them "child assets" rather than subassets. Or we might choose to add a new SubassetOf Relationship (perhaps freeing up ChildOf for representing asset "scene hierarchies" directly). The "assets as entities" world might need a whole new set of entity / relationship APIs that replaces the current API.

The point is, we have big changes on the horizon that could very well affect the terminology here, and are likely to break these exact asset APIs. I'm not sure it makes sense to make these "superficial" breaking changes now. I'd rather direct this energy toward spec-ing out what the "assets as entities" data model / terminology looks like.

) -> Option<&ErasedLoadedAsset> {
self.labeled_assets.get(&label.into()).map(|a| &a.asset)
self.subassets.get(&subasset_name.into()).map(|a| &a.asset)
}

/// Iterate over all labels for "labeled assets" in the loaded asset
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| &**s)
/// Iterate over all subasset names for subassets in this loaded asset.
pub fn iter_subasset_names(&self) -> impl Iterator<Item = &str> {
self.subassets.keys().map(|s| &**s)
}
}

Expand All @@ -201,7 +201,7 @@ pub struct ErasedLoadedAsset {
pub(crate) value: Box<dyn AssetContainer>,
pub(crate) dependencies: HashSet<ErasedAssetIndex>,
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
pub(crate) subassets: HashMap<CowArc<'static, str>, LoadedSubAsset>,
}

impl<A: Asset> From<LoadedAsset<A>> for ErasedLoadedAsset {
Expand All @@ -210,7 +210,7 @@ impl<A: Asset> From<LoadedAsset<A>> for ErasedLoadedAsset {
value: Box::new(asset.value),
dependencies: asset.dependencies,
loader_dependencies: asset.loader_dependencies,
labeled_assets: asset.labeled_assets,
subassets: asset.subassets,
}
}
}
Expand All @@ -237,17 +237,17 @@ impl ErasedLoadedAsset {
self.value.asset_type_name()
}

/// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
pub fn get_labeled(
/// Returns the [`ErasedLoadedAsset`] for the given subasset name, if it exists.
pub fn get_subasset(
&self,
label: impl Into<CowArc<'static, str>>,
subasset_name: impl Into<CowArc<'static, str>>,
) -> Option<&ErasedLoadedAsset> {
self.labeled_assets.get(&label.into()).map(|a| &a.asset)
self.subassets.get(&subasset_name.into()).map(|a| &a.asset)
}

/// Iterate over all labels for "labeled assets" in the loaded asset
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| &**s)
/// Iterate over all subasset names for subassets in this loaded asset.
pub fn iter_subasset_names(&self) -> impl Iterator<Item = &str> {
self.subassets.keys().map(|s| &**s)
}

/// Cast this loaded asset as the given type. If the type does not match,
Expand All @@ -258,7 +258,7 @@ impl ErasedLoadedAsset {
value: *value,
dependencies: self.dependencies,
loader_dependencies: self.loader_dependencies,
labeled_assets: self.labeled_assets,
subassets: self.subassets,
}),
Err(value) => {
self.value = value;
Expand Down Expand Up @@ -330,7 +330,7 @@ pub struct LoadContext<'a> {
pub(crate) dependencies: HashSet<ErasedAssetIndex>,
/// Direct dependencies used by this loader.
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
pub(crate) subassets: HashMap<CowArc<'static, str>, LoadedSubAsset>,
}

impl<'a> LoadContext<'a> {
Expand All @@ -348,19 +348,19 @@ impl<'a> LoadContext<'a> {
should_load_dependencies,
dependencies: HashSet::default(),
loader_dependencies: HashMap::default(),
labeled_assets: HashMap::default(),
subassets: HashMap::default(),
}
}

/// Begins a new labeled asset load. Use the returned [`LoadContext`] to load
/// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load.
/// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent
/// Begins a new subasset load. Use the returned [`LoadContext`] to load dependencies for the
/// new asset and call [`LoadContext::finish`] to finalize the subasset load. When finished,
/// make sure you call [`LoadContext::add_loaded_subasset`] to add the results back to the parent
/// context.
/// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add
/// the labeled [`LoadContext`] back to the parent context.
/// [`LoadContext::begin_labeled_asset`] exists largely to enable parallel asset loading.
/// Prefer [`LoadContext::subasset_scope`] when possible, which will automatically add
/// the subasset [`LoadContext`] back to the parent context.
/// [`LoadContext::begin_subasset`] exists largely to enable parallel asset loading.
///
/// See [`AssetPath`] for more on labeled assets.
/// See [`AssetPath`] for more on subassets.
///
/// ```no_run
/// # use bevy_asset::{Asset, LoadContext};
Expand All @@ -370,18 +370,18 @@ impl<'a> LoadContext<'a> {
/// # let load_context: LoadContext = panic!();
/// let mut handles = Vec::new();
/// for i in 0..2 {
/// let labeled = load_context.begin_labeled_asset();
/// let subasset = load_context.begin_subasset();
/// handles.push(std::thread::spawn(move || {
/// (i.to_string(), labeled.finish(Image::default()))
/// (i.to_string(), subasset.finish(Image::default()))
/// }));
/// }
///
/// for handle in handles {
/// let (label, loaded_asset) = handle.join().unwrap();
/// load_context.add_loaded_labeled_asset(label, loaded_asset);
/// let (subasset_name, loaded_asset) = handle.join().unwrap();
/// load_context.add_loaded_subasset(subasset_name, loaded_asset);
/// }
/// ```
pub fn begin_labeled_asset(&self) -> LoadContext<'_> {
pub fn begin_subasset(&self) -> LoadContext<'_> {
LoadContext::new(
self.asset_server,
self.asset_path.clone(),
Expand All @@ -390,71 +390,77 @@ impl<'a> LoadContext<'a> {
)
}

/// Creates a new [`LoadContext`] for the given `label`. The `load` function is responsible for loading an [`Asset`] of
/// Creates a new [`LoadContext`] for the given `subasset_name`. The `load` function is responsible for loading an [`Asset`] of
/// type `A`. `load` will be called immediately and the result will be used to finalize the [`LoadContext`], resulting in a new
/// [`LoadedAsset`], which is registered under the `label` label.
/// [`LoadedAsset`], which is registered under the `subasset_name`.
///
/// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the
/// result with [`LoadContext::add_loaded_labeled_asset`].
/// This exists to remove the need to manually call [`LoadContext::begin_subasset`] and then manually register the
/// result with [`LoadContext::add_loaded_subasset`].
///
/// See [`AssetPath`] for more on labeled assets.
pub fn labeled_asset_scope<A: Asset, E>(
/// See [`AssetPath`] for more on subassets.
pub fn subasset_scope<A: Asset, E>(
&mut self,
label: String,
subasset_name: String,
load: impl FnOnce(&mut LoadContext) -> Result<A, E>,
) -> Result<Handle<A>, E> {
let mut context = self.begin_labeled_asset();
let mut context = self.begin_subasset();
let asset = load(&mut context)?;
let loaded_asset = context.finish(asset);
Ok(self.add_loaded_labeled_asset(label, loaded_asset))
Ok(self.add_loaded_subasset(subasset_name, loaded_asset))
}

/// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label.
/// This will add the given `asset` as a sub-[`Asset`] with the `subasset_name`.
///
/// # Warning
///
/// This will not assign dependencies to the given `asset`. If adding an asset
/// with dependencies generated from calls such as [`LoadContext::load`], use
/// [`LoadContext::labeled_asset_scope`] or [`LoadContext::begin_labeled_asset`] to generate a
/// new [`LoadContext`] to track the dependencies for the labeled asset.
/// [`LoadContext::subasset_scope`] or [`LoadContext::begin_subasset`] to generate a
/// new [`LoadContext`] to track the dependencies for the subasset.
///
/// See [`AssetPath`] for more on labeled assets.
pub fn add_labeled_asset<A: Asset>(&mut self, label: String, asset: A) -> Handle<A> {
self.labeled_asset_scope(label, |_| Ok::<_, ()>(asset))
/// See [`AssetPath`] for more on subassets.
pub fn add_subasset<A: Asset>(&mut self, subasset_name: String, asset: A) -> Handle<A> {
self.subasset_scope(subasset_name, |_| Ok::<_, ()>(asset))
.expect("the closure returns Ok")
}

/// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context.
/// This can be used in combination with [`LoadContext::begin_labeled_asset`] to parallelize
/// Add a [`LoadedAsset`] that is a "sub asset" of the root path of this load context.
/// This can be used in combination with [`LoadContext::begin_subasset`] to parallelize
/// sub asset loading.
///
/// See [`AssetPath`] for more on labeled assets.
pub fn add_loaded_labeled_asset<A: Asset>(
/// See [`AssetPath`] for more on subassets.
pub fn add_loaded_subasset<A: Asset>(
&mut self,
label: impl Into<CowArc<'static, str>>,
subasset_name: impl Into<CowArc<'static, str>>,
loaded_asset: LoadedAsset<A>,
) -> Handle<A> {
let label = label.into();
let subasset_name = subasset_name.into();
let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
let labeled_path = self.asset_path.clone().with_label(label.clone());
let subasset_path = self
.asset_path
.clone()
.with_subasset_name(subasset_name.clone());
let handle = self
.asset_server
.get_or_create_path_handle(labeled_path, None);
self.labeled_assets.insert(
label,
LabeledAsset {
.get_or_create_path_handle(subasset_path, None);
self.subassets.insert(
subasset_name,
LoadedSubAsset {
asset: loaded_asset,
handle: handle.clone().untyped(),
},
);
handle
}

/// Returns `true` if an asset with the label `label` exists in this context.
/// Returns `true` if an asset with the `subasset_name` exists in this context.
///
/// See [`AssetPath`] for more on labeled assets.
pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
let path = self.asset_path.clone().with_label(label.into());
/// See [`AssetPath`] for more on subassets.
pub fn has_subasset<'b>(&self, subasset_name: impl Into<CowArc<'b, str>>) -> bool {
let path = self
.asset_path
.clone()
.with_subasset_name(subasset_name.into());
!self.asset_server.get_handles_untyped(&path).is_empty()
}

Expand All @@ -479,7 +485,7 @@ impl<'a> LoadContext<'a> {
value,
dependencies: self.dependencies,
loader_dependencies: self.loader_dependencies,
labeled_assets: self.labeled_assets,
subassets: self.subassets,
}
}

Expand Down Expand Up @@ -525,14 +531,14 @@ impl<'a> LoadContext<'a> {
Ok(bytes)
}

/// Returns a handle to an asset of type `A` with the label `label`. This [`LoadContext`] must produce an asset of the
/// given type and the given label or the dependencies of this asset will never be considered "fully loaded". However you
/// can call this method before _or_ after adding the labeled asset.
pub fn get_label_handle<'b, A: Asset>(
/// Returns a handle to an asset of type `A` with the `subasset_name`. This [`LoadContext`] must produce an asset of the
/// given type and the given subasset name or the dependencies of this asset will never be considered "fully loaded". However you
/// can call this method before _or_ after adding the subasset.
pub fn get_subasset_handle<'b, A: Asset>(
&mut self,
label: impl Into<CowArc<'b, str>>,
subasset_name: impl Into<CowArc<'b, str>>,
) -> Handle<A> {
let path = self.asset_path.clone().with_label(label);
let path = self.asset_path.clone().with_subasset_name(subasset_name);
let handle = self.asset_server.get_or_create_path_handle::<A>(path, None);
// `get_or_create_path_handle` always returns a Strong variant, so we are safe to unwrap.
let index = (&handle).try_into().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_asset/src/loader_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>>
path: &AssetPath<'static>,
asset_type_id: Option<TypeId>,
) -> Result<(Arc<dyn ErasedAssetLoader>, ErasedLoadedAsset), LoadDirectError> {
if path.label().is_some() {
if path.subasset_name().is_some() {
return Err(LoadDirectError::RequestedSubasset(path.clone()));
}
self.load_context
Expand Down
Loading