diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index d008c48b032fb..1104584902257 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -719,7 +719,7 @@ 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 + let (_, loaded) = load_context .loader() .immediate() .load::(&dep) @@ -727,7 +727,7 @@ mod tests { .map_err(|_| Self::Error::CannotLoadDependency { dependency: dep.into(), })?; - let cool = loaded.get_asset().get(); + let cool = loaded.get_asset(); embedded.push_str(&cool.text); } Ok(CoolText { @@ -1007,6 +1007,20 @@ mod tests { // Re-open a and b gates to allow c to load embedded deps (gates are closed after each load) gate_opener.open(a_path); gate_opener.open(b_path); + + // Wait for the C-load task to finish. If we don't do this, it's possible for nested asset A + // to be sent in one frame and for the nested asset B to be sent in the next frame. This + // causes an alternative order of events (but one that is just as valid), which would make + // this test flaky. + while !asset_server + .data + .infos + .read() + .pending_tasks + .iter() + .any(|(_, task)| task.is_finished()) + {} + run_app_until(&mut app, |world| { let a_text = get::(world, a_id)?; let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap(); @@ -1131,9 +1145,19 @@ mod tests { AssetEvent::Added { id: id_results.b_id, }, + // This extra LoadedWithDependencies happens because asset B got replaced when C was + // loaded (and we only see the modify after since events on `Assets` only merge in a + // system, so they look like they happen at the wrong time). + AssetEvent::LoadedWithDependencies { + id: id_results.b_id, + }, AssetEvent::Added { id: id_results.c_id, }, + AssetEvent::Modified { id: a_id }, + AssetEvent::Modified { + id: id_results.b_id, + }, AssetEvent::LoadedWithDependencies { id: id_results.d_id, }, diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 8b02e5b3dbebc..426903c388308 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -3,8 +3,8 @@ use crate::{ loader_builders::{Deferred, NestedLoader, StaticTyped}, meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings}, path::AssetPath, - Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId, - UntypedHandle, + Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, NestedAssets, + UntypedAssetId, UntypedHandle, }; use alloc::{ boxed::Box, @@ -15,8 +15,12 @@ use atomicow::CowArc; use bevy_ecs::world::World; use bevy_platform_support::collections::{HashMap, HashSet}; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; -use core::any::{Any, TypeId}; +use core::{ + any::{Any, TypeId}, + ops::Deref, +}; use downcast_rs::{impl_downcast, Downcast}; +use parking_lot::{RwLock, RwLockReadGuard}; use ron::error::SpannedError; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -383,6 +387,9 @@ pub enum DeserializeMetaError { /// Any asset state accessed by [`LoadContext`] will be tracked and stored for use in dependency events and asset preprocessing. pub struct LoadContext<'a> { pub(crate) asset_server: &'a AssetServer, + /// The nested assets that have been directly loaded across the entire loader. We use a + /// [`RwLock`] so that all [`LoadContext`]'s based on this one share these nested loaded assets. + pub(crate) nested_direct_loaded_assets: &'a RwLock, pub(crate) should_load_dependencies: bool, populate_hashes: bool, asset_path: AssetPath<'static>, @@ -396,12 +403,14 @@ impl<'a> LoadContext<'a> { /// Creates a new [`LoadContext`] instance. pub(crate) fn new( asset_server: &'a AssetServer, + nested_direct_loaded_assets: &'a RwLock, asset_path: AssetPath<'static>, should_load_dependencies: bool, populate_hashes: bool, ) -> Self { Self { asset_server, + nested_direct_loaded_assets, asset_path, populate_hashes, should_load_dependencies, @@ -443,6 +452,7 @@ impl<'a> LoadContext<'a> { pub fn begin_labeled_asset(&self) -> LoadContext { LoadContext::new( self.asset_server, + self.nested_direct_loaded_assets, self.asset_path.clone(), self.should_load_dependencies, self.populate_hashes, @@ -613,7 +623,7 @@ impl<'a> LoadContext<'a> { meta: &dyn AssetMetaDyn, loader: &dyn ErasedAssetLoader, reader: &mut dyn Reader, - ) -> Result { + ) -> Result, LoadDirectError> { let complete_asset = self .asset_server .load_with_meta_loader_and_reader( @@ -623,6 +633,7 @@ impl<'a> LoadContext<'a> { reader, false, self.populate_hashes, + self.nested_direct_loaded_assets, ) .await .map_err(|error| LoadDirectError::LoadError { @@ -631,8 +642,25 @@ impl<'a> LoadContext<'a> { })?; let info = meta.processed_info().as_ref(); let hash = info.map(|i| i.full_hash).unwrap_or_default(); - self.loader_dependencies.insert(path, hash); - Ok(complete_asset) + self.loader_dependencies.insert(path.clone(), hash); + self.nested_direct_loaded_assets + .write() + .insert(path.clone(), complete_asset); + Ok( + NestedErasedAssetRef::new(self.nested_direct_loaded_assets.read(), &path) + .expect("asset path is loaded, since we just inserted it"), + ) + } + + /// Gets a previously direct-loaded nested asset by its path. + /// + /// Returns None if the asset either hasn't been loaded yet or hasn't finished loading. See + /// [`Self::loader`] for starting nested asset loads. + pub fn get_direct_nested_asset<'p>( + &self, + path: impl Into>, + ) -> Option { + NestedErasedAssetRef::new(self.nested_direct_loaded_assets.read(), &path.into()) } /// Create a builder for loading nested assets in this context. @@ -654,6 +682,93 @@ impl<'a> LoadContext<'a> { } } +/// A reference to a nested, directly-loaded asset. This is similar to +/// [`CompleteErasedLoadedAsset`]. +pub struct NestedErasedAssetRef<'context> { + /// The lock that we hold to access the nested asset. + _lock: RwLockReadGuard<'context, NestedAssets>, + /// A pointer to the asset we are referencing. We store a pointer since otherwise, we'd need to + /// look up the asset every time we access the asset (and store the asset path here as well). + asset: *const CompleteErasedLoadedAsset, +} + +impl Deref for NestedErasedAssetRef<'_> { + type Target = CompleteErasedLoadedAsset; + + fn deref(&self) -> &Self::Target { + #[expect( + unsafe_code, + reason = "Not using raw pointers requires looking up the asset every time, and including the asset path in this struct" + )] + // SAFETY: We got this pointer from a reference, and we hold `self.lock`, so no mutable + // references alias this pointer. + unsafe { + &*self.asset + } + } +} + +impl<'context> NestedErasedAssetRef<'context> { + fn new( + lock: RwLockReadGuard<'context, NestedAssets>, + asset_path: &AssetPath<'_>, + ) -> Option { + lock.get(asset_path) + .map(|asset| &raw const *asset) + .map(|asset| Self { _lock: lock, asset }) + } + + pub fn downcast(self) -> Result, Self> { + let Some(asset) = self.get_asset().get().map(|asset| &raw const *asset) else { + return Err(self); + }; + Ok(NestedAssetRef { + erased_ref: self, + asset, + }) + } +} + +/// A reference to a nested, directly-loaded asset. This is similar to [`CompleteLoadedAsset`]. +pub struct NestedAssetRef<'context, A: Asset> { + /// The type-erased asset reference. + erased_ref: NestedErasedAssetRef<'context>, + /// A pointer to the typed asset we are referencing. We store a pointer since otherwise, we'd + /// need to do the cast every time (even though we've verified this is the correct type). + asset: *const A, +} + +impl NestedAssetRef<'_, A> { + /// Returns the stored asset. + pub fn get_asset(&self) -> &A { + #[expect( + unsafe_code, + reason = "Not using raw pointers requires casting the asset every time" + )] + // SAFETY: We got this pointer from a reference, and we hold `self.erased_ref`, so no + // mutable references alias this pointer. + unsafe { + &*self.asset + } + } + + /// Returns the [`ErasedLoadedAsset`] for the given label, if it exists. + pub fn get_labeled( + &self, + label: impl Into>, + ) -> Option<&ErasedLoadedAsset> { + self.erased_ref + .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.erased_ref.labeled_assets.keys().map(|s| &**s) + } +} + /// An error produced when calling [`LoadContext::read_asset_bytes`] #[derive(Error, Debug)] pub enum ReadAssetBytesError { diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 1867e5b03c4c9..6224941d6da19 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -4,8 +4,8 @@ use crate::{ io::Reader, meta::{meta_transform_settings, AssetMetaDyn, MetaTransform, Settings}, - Asset, AssetLoadError, AssetPath, CompleteErasedLoadedAsset, CompleteLoadedAsset, - ErasedAssetLoader, Handle, LoadContext, LoadDirectError, LoadedUntypedAsset, UntypedHandle, + Asset, AssetLoadError, AssetPath, ErasedAssetLoader, Handle, LoadContext, LoadDirectError, + LoadedUntypedAsset, NestedAssetRef, NestedErasedAssetRef, UntypedHandle, }; use alloc::{borrow::ToOwned, boxed::Box, sync::Arc}; use core::any::TypeId; @@ -115,6 +115,7 @@ impl ReaderRef<'_> { /// [`AssetProcessor`]: crate::processor::AssetProcessor /// [`Process`]: crate::processor::Process /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave +/// [`CompleteErasedLoadedAsset`]: crate::CompleteErasedLoadedAsset pub struct NestedLoader<'ctx, 'builder, T, M> { load_context: &'builder mut LoadContext<'ctx>, meta_transform: Option, @@ -374,7 +375,7 @@ impl NestedLoader<'_, '_, UnknownTyped, Deferred> { // immediate loading logic -impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> { +impl<'builder, 'reader, T> NestedLoader<'_, 'builder, 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 { @@ -386,14 +387,21 @@ impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> self, path: &AssetPath<'static>, asset_type_id: Option, - ) -> Result<(Arc, CompleteErasedLoadedAsset), LoadDirectError> { + ) -> Result< + ( + UntypedHandle, + Arc, + NestedErasedAssetRef<'builder>, + ), + LoadDirectError, + > { if path.label().is_some() { return Err(LoadDirectError::RequestedSubasset(path.clone())); } + let asset_server = self.load_context.asset_server.clone(); 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 + asset_server .get_asset_loader_with_asset_type_id(asset_type_id) .await .map_err(|error| LoadDirectError::LoadError { @@ -401,8 +409,7 @@ impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> error: error.into(), })? } else { - self.load_context - .asset_server + asset_server .get_path_asset_loader(path) .await .map_err(|error| LoadDirectError::LoadError { @@ -413,9 +420,7 @@ impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> let meta = loader.default_meta(); (meta, loader, ReaderRef::Borrowed(reader)) } else { - let (meta, loader, reader) = self - .load_context - .asset_server + let (meta, loader, reader) = asset_server .get_meta_loader_and_reader(path, asset_type_id) .await .map_err(|error| LoadDirectError::LoadError { @@ -433,11 +438,17 @@ impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> .load_context .load_direct_internal(path.clone(), meta.as_ref(), &*loader, reader.as_mut()) .await?; - Ok((loader, asset)) + + let handle = asset_server.get_or_create_path_handle_erased( + path, + asset.get_asset().asset_type_id(), + None, + ); + Ok((handle, loader, asset)) } } -impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { +impl<'builder> NestedLoader<'_, 'builder, StaticTyped, Immediate<'builder, '_>> { /// Attempts to load the asset at the given `path` immediately. /// /// This requires you to know the type of asset statically. @@ -451,11 +462,11 @@ impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { pub async fn load<'p, A: Asset>( self, path: impl Into>, - ) -> Result, LoadDirectError> { + ) -> Result<(Handle, NestedAssetRef<'builder, A>), LoadDirectError> { let path = path.into().into_owned(); self.load_internal(&path, Some(TypeId::of::())) .await - .and_then(move |(loader, untyped_asset)| { + .and_then(move |(handle, loader, untyped_asset)| { untyped_asset .downcast::() .map_err(|_| LoadDirectError::LoadError { @@ -467,40 +478,41 @@ impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { loader_name: loader.type_name(), }, }) + .map(|asset| (handle.typed(), asset)) }) } } -impl NestedLoader<'_, '_, DynamicTyped, Immediate<'_, '_>> { +impl<'builder> NestedLoader<'_, 'builder, DynamicTyped, Immediate<'builder, '_>> { /// 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>( + pub async fn load( self, - path: impl Into>, - ) -> Result { + path: impl Into>, + ) -> Result<(UntypedHandle, NestedErasedAssetRef<'builder>), LoadDirectError> { 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) + .map(|(handle, _, asset)| (handle, asset)) } } -impl NestedLoader<'_, '_, UnknownTyped, Immediate<'_, '_>> { +impl<'builder> NestedLoader<'_, 'builder, UnknownTyped, Immediate<'builder, '_>> { /// Attempts to load the asset at the given `path` immediately. /// /// This will infer the asset type from metadata. - pub async fn load<'p>( + pub async fn load( self, - path: impl Into>, - ) -> Result { + path: impl Into>, + ) -> Result<(UntypedHandle, NestedErasedAssetRef<'builder>), LoadDirectError> { let path = path.into().into_owned(); self.load_internal(&path, None) .await - .map(|(_, asset)| asset) + .map(|(handle, _, asset)| (handle, asset)) } } diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index d2202d2334d23..a048944300289 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -8,7 +8,7 @@ use crate::{ saver::{AssetSaver, SavedAsset}, transformer::{AssetTransformer, IdentityAssetTransformer, TransformedAsset}, AssetLoadError, AssetLoader, AssetPath, CompleteErasedLoadedAsset, DeserializeMetaError, - MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError, + MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError, NestedAssets, }; use alloc::{ borrow::ToOwned, @@ -17,6 +17,7 @@ use alloc::{ }; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::marker::PhantomData; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -310,8 +311,17 @@ impl<'a> ProcessContext<'a> { let loader_name = core::any::type_name::(); let loader = server.get_asset_loader_with_type_name(loader_name).await?; let mut reader = SliceReader::new(self.asset_bytes); + let nested_direct_loaded_assets = RwLock::new(NestedAssets::default()); let complete_asset = server - .load_with_meta_loader_and_reader(self.path, &meta, &*loader, &mut reader, false, true) + .load_with_meta_loader_and_reader( + self.path, + &meta, + &*loader, + &mut reader, + false, + true, + &nested_direct_loaded_assets, + ) .await?; for (path, full_hash) in &complete_asset.asset.loader_dependencies { self.new_processed_info diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index af042f5710198..ea2cdb19a08c5 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -26,10 +26,11 @@ use alloc::{ }; use atomicow::CowArc; use bevy_ecs::prelude::*; -use bevy_platform_support::collections::HashSet; +use bevy_platform_support::collections::{HashMap, HashSet}; use bevy_tasks::IoTaskPool; use core::{any::TypeId, future::Future, panic::AssertUnwindSafe, task::Poll}; use crossbeam_channel::{Receiver, Sender}; +use derive_more::derive::{Deref, DerefMut}; use either::Either; use futures_lite::{FutureExt, StreamExt}; use info::*; @@ -649,7 +650,8 @@ impl AssetServer { (handle.clone().unwrap(), path.clone()) }; - match self + let nested_direct_loaded_assets = RwLock::new(NestedAssets::default()); + let result = match self .load_with_meta_loader_and_reader( &base_path, meta.as_ref(), @@ -657,6 +659,7 @@ impl AssetServer { &mut *reader, true, false, + &nested_direct_loaded_assets, ) .await { @@ -683,7 +686,7 @@ impl AssetServer { handle.unwrap() }; - self.send_loaded_asset(base_handle.id(), loaded_asset); + self.send_loaded_asset(Some(base_handle.id()), loaded_asset); Ok(final_handle) } Err(err) => { @@ -694,12 +697,31 @@ impl AssetServer { }); Err(err) } + }; + + // Even if the asset failed to load, the nested assets still loaded correctly, so we might + // as well send those assets to "refresh" them. + for (path, asset) in nested_direct_loaded_assets.into_inner().0 { + // Even if the handle is None, one of its subassets may be loaded, so we should send the + // whole complete asset. + let handle = self + .data + .infos + .read() + .get_path_and_type_id_handle(&path, asset.asset.asset_type_id()); + self.send_loaded_asset(handle.map(|handle| handle.id()), asset); } + + result } /// Sends a load event for the given `loaded_asset` and does the same recursively for all /// labeled assets. - fn send_loaded_asset(&self, id: UntypedAssetId, mut complete_asset: CompleteErasedLoadedAsset) { + fn send_loaded_asset( + &self, + id: Option, + mut complete_asset: CompleteErasedLoadedAsset, + ) { for (_, labeled_asset) in complete_asset.labeled_assets.drain() { self.send_asset_event(InternalAssetEvent::Loaded { id: labeled_asset.handle.id(), @@ -707,10 +729,12 @@ impl AssetServer { }); } - self.send_asset_event(InternalAssetEvent::Loaded { - id, - loaded_asset: complete_asset.asset, - }); + if let Some(id) = id { + self.send_asset_event(InternalAssetEvent::Loaded { + id, + loaded_asset: complete_asset.asset, + }); + } } /// Kicks off a reload of the asset stored at the given path. This will only reload the asset if it currently loaded. @@ -1334,11 +1358,17 @@ impl AssetServer { reader: &mut dyn Reader, load_dependencies: bool, populate_hashes: bool, + nested_direct_loaded_assets: &RwLock, ) -> Result { // TODO: experiment with this let asset_path = asset_path.clone_owned(); - let load_context = - LoadContext::new(self, asset_path.clone(), load_dependencies, populate_hashes); + let load_context = LoadContext::new( + self, + nested_direct_loaded_assets, + asset_path.clone(), + load_dependencies, + populate_hashes, + ); AssertUnwindSafe(loader.load(reader, meta, load_context)) .catch_unwind() .await @@ -1781,6 +1811,10 @@ impl RecursiveDependencyLoadState { } } +/// A wrapper for storing nested directly-loaded assets. +#[derive(Deref, DerefMut, Default)] +pub(crate) struct NestedAssets(HashMap, CompleteErasedLoadedAsset>); + /// An error that occurs during an [`Asset`] load. #[derive(Error, Debug, Clone)] #[expect( diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index edc42ab853fb3..c8e1c11b7cd50 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -3,7 +3,7 @@ use bevy::{ asset::{ io::{Reader, VecReader}, - AssetLoader, CompleteErasedLoadedAsset, LoadContext, LoadDirectError, + AssetLoader, LoadContext, LoadDirectError, }, prelude::*, reflect::TypePath, @@ -14,7 +14,7 @@ use thiserror::Error; #[derive(Asset, TypePath)] struct GzAsset { - uncompressed: CompleteErasedLoadedAsset, + uncompressed: UntypedHandle, } #[derive(Default)] @@ -77,7 +77,8 @@ impl AssetLoader for GzAssetLoader { .immediate() .with_reader(&mut reader) .load(contained_path) - .await?; + .await? + .0; Ok(GzAsset { uncompressed }) } @@ -114,7 +115,6 @@ fn setup(mut commands: Commands, asset_server: Res) { fn decompress>, A: Asset>( mut commands: Commands, - asset_server: Res, mut compressed_assets: ResMut>, query: Query<(Entity, &Compressed)>, ) { @@ -123,11 +123,9 @@ fn decompress>, A: Asset>( continue; }; - let uncompressed = uncompressed.take::().unwrap(); - commands .entity(entity) .remove::>() - .insert(T::from(asset_server.add(uncompressed))); + .insert(T::from(uncompressed.typed())); } } diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index b24f7afb14bc1..0032ada36cb8b 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -153,8 +153,9 @@ impl AssetLoader for CoolTextLoader { .loader() .immediate() .load::(&embedded) - .await?; - base_text.push_str(&complete_loaded.get_asset().get().0); + .await? + .1; + base_text.push_str(&complete_loaded.get_asset().0); } for (path, settings_override) in ron.dependencies_with_settings { let complete_loaded = load_context @@ -164,8 +165,9 @@ impl AssetLoader for CoolTextLoader { }) .immediate() .load::(&path) - .await?; - base_text.push_str(&complete_loaded.get_asset().get().0); + .await? + .1; + base_text.push_str(&complete_loaded.get_asset().0); } Ok(CoolText { text: base_text,