From 8c8a0f71e1be1d8c99e891d8e0cf9a56c799e434 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Nov 2025 11:45:53 -0800 Subject: [PATCH 1/5] Refactor some repeated test App setup into a reusable function. --- crates/bevy_asset/src/lib.rs | 117 +++++++++++++---------------------- 1 file changed, 44 insertions(+), 73 deletions(-) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index add1b42c35553..990c8831cafd2 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -901,7 +901,28 @@ mod tests { } } - fn test_app(dir: Dir) -> (App, GateOpener) { + /// Creates a basic asset app and an in-memory file system. + fn create_app() -> (App, Dir) { + let mut app = App::new(); + let dir = Dir::default(); + let dir_clone = dir.clone(); + app.register_asset_source( + AssetSourceId::Default, + AssetSourceBuilder::new(move || { + Box::new(MemoryAssetReader { + root: dir_clone.clone(), + }) + }), + ) + .add_plugins(( + TaskPoolPlugin::default(), + AssetPlugin::default(), + DiagnosticsPlugin, + )); + (app, dir) + } + + fn create_app_with_gate(dir: Dir) -> (App, GateOpener) { let mut app = App::new(); let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir }); app.register_asset_source( @@ -1007,7 +1028,7 @@ mod tests { d_id: AssetId, } - let (mut app, gate_opener) = test_app(dir); + let (mut app, gate_opener) = create_app_with_gate(dir); app.init_asset::() .init_asset::() .init_resource::() @@ -1312,7 +1333,7 @@ mod tests { dir.insert_asset_text(Path::new(c_path), c_ron); dir.insert_asset_text(Path::new(d_path), d_ron); - let (mut app, gate_opener) = test_app(dir); + let (mut app, gate_opener) = create_app_with_gate(dir); app.init_asset::() .register_asset_loader(CoolTextLoader); let asset_server = app.world().resource::().clone(); @@ -1437,7 +1458,7 @@ mod tests { dir.insert_asset_text(Path::new(b_path), b_ron); dir.insert_asset_text(Path::new(c_path), c_ron); - let (mut app, gate_opener) = test_app(dir); + let (mut app, gate_opener) = create_app_with_gate(dir); app.init_asset::() .register_asset_loader(CoolTextLoader); let asset_server = app.world().resource::().clone(); @@ -1514,7 +1535,7 @@ mod tests { let dir = Dir::default(); dir.insert_asset_text(Path::new("dep.cool.ron"), SIMPLE_TEXT); - let (mut app, _) = test_app(dir); + let (mut app, _) = create_app_with_gate(dir); app.init_asset::() .init_asset::() .init_resource::() @@ -1551,7 +1572,7 @@ mod tests { dir.insert_asset_text(Path::new(dep_path), SIMPLE_TEXT); - let (mut app, gate_opener) = test_app(dir); + let (mut app, gate_opener) = create_app_with_gate(dir); app.init_asset::() .init_asset::() .init_resource::() @@ -1692,7 +1713,7 @@ mod tests { dir.insert_asset_text(Path::new(b_path), b_ron); dir.insert_asset_text(Path::new(c_path), c_ron); - let (mut app, gate_opener) = test_app(dir); + let (mut app, gate_opener) = create_app_with_gate(dir); app.init_asset::() .init_asset::() .register_asset_loader(CoolTextLoader); @@ -1878,9 +1899,8 @@ mod tests { #[test] fn ignore_system_ambiguities_on_assets() { - let mut app = App::new(); - app.add_plugins(AssetPlugin::default()) - .init_asset::(); + let mut app = create_app().0; + app.init_asset::(); fn uses_assets(_asset: ResMut>) {} app.add_systems(Update, (uses_assets, uses_assets)); @@ -1899,9 +1919,7 @@ mod tests { // not capable of loading subassets when doing nested immediate loads. #[test] fn error_on_nested_immediate_load_of_subasset() { - let mut app = App::new(); - - let dir = Dir::default(); + let (mut app, dir) = create_app(); dir.insert_asset_text( Path::new("a.cool.ron"), r#"( @@ -1913,12 +1931,6 @@ mod tests { ); dir.insert_asset_text(Path::new("empty.txt"), ""); - app.register_asset_source( - AssetSourceId::Default, - AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })), - ) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())); - app.init_asset::() .init_asset::() .register_asset_loader(CoolTextLoader); @@ -2110,10 +2122,9 @@ mod tests { #[test] fn insert_dropped_handle_returns_error() { - let mut app = App::new(); + let mut app = create_app().0; - app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) - .init_asset::(); + app.init_asset::(); let handle = app.world().resource::>().reserve_handle(); // We still have the asset ID, but we've dropped the handle so the asset is no longer live. @@ -2171,14 +2182,7 @@ mod tests { #[test] fn dropping_handle_while_loading_cancels_load() { - let dir = Dir::default(); - let mut app = App::new(); - let reader = MemoryAssetReader { root: dir.clone() }; - app.register_asset_source( - AssetSourceId::Default, - AssetSourceBuilder::new(move || Box::new(reader.clone())), - ) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())); + let (mut app, dir) = create_app(); let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1); let (gate_sender, gate_receiver) = async_channel::bounded(1); @@ -2227,14 +2231,7 @@ mod tests { #[test] fn dropping_subasset_handle_while_loading_cancels_load() { - let dir = Dir::default(); - let mut app = App::new(); - let reader = MemoryAssetReader { root: dir.clone() }; - app.register_asset_source( - AssetSourceId::Default, - AssetSourceBuilder::new(move || Box::new(reader.clone())), - ) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())); + let (mut app, dir) = create_app(); let (in_loader_sender, in_loader_receiver) = async_channel::bounded(1); let (gate_sender, gate_receiver) = async_channel::bounded(1); @@ -2481,22 +2478,12 @@ mod tests { } } - // Create a test asset. + // Create a test asset and setup the app. - let dir = Dir::default(); + let (mut app, dir) = create_app(); dir.insert_asset(Path::new("test.u8"), &[]); - let asset_source = - AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })); - - // Set up the app. - - let mut app = App::new(); - - app.register_asset_source(AssetSourceId::Default, asset_source) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) - .init_asset::() - .register_asset_loader(U8Loader); + app.init_asset::().register_asset_loader(U8Loader); let asset_server = app.world().resource::(); @@ -2545,18 +2532,9 @@ mod tests { #[test] fn loading_two_subassets_does_not_start_two_loads() { - let mut app = App::new(); - - let dir = Dir::default(); + let (mut app, dir) = create_app(); dir.insert_asset(Path::new("test.txt"), &[]); - let asset_source = - AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })); - - app.register_asset_source(AssetSourceId::Default, asset_source) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) - .init_asset::(); - struct TwoSubassetLoader; impl AssetLoader for TwoSubassetLoader { @@ -2580,7 +2558,8 @@ mod tests { } } - app.register_asset_loader(TwoSubassetLoader); + app.init_asset::() + .register_asset_loader(TwoSubassetLoader); let asset_server = app.world().resource::().clone(); let _subasset_1: Handle = asset_server.load("test.txt#A"); @@ -2597,18 +2576,9 @@ mod tests { #[test] fn get_strong_handle_prevents_reload_when_asset_still_alive() { - let mut app = App::new(); - - let dir = Dir::default(); + let (mut app, dir) = create_app(); dir.insert_asset(Path::new("test.txt"), &[]); - let asset_source = - AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })); - - app.register_asset_source(AssetSourceId::Default, asset_source) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) - .init_asset::(); - struct TrivialLoader; impl AssetLoader for TrivialLoader { @@ -2630,7 +2600,8 @@ mod tests { } } - app.register_asset_loader(TrivialLoader); + app.init_asset::() + .register_asset_loader(TrivialLoader); let asset_server = app.world().resource::().clone(); let original_handle: Handle = asset_server.load("test.txt"); From c3bbb8dfca10ca98f2be415c4dad24a2b99409f8 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Nov 2025 11:45:53 -0800 Subject: [PATCH 2/5] Move TrivialLoader out of a test so it can be reused in other tests. --- crates/bevy_asset/src/lib.rs | 43 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 990c8831cafd2..b29b270167daa 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -2574,31 +2574,32 @@ mod tests { assert_eq!(get_started_load_count(app.world()), 2); } - #[test] - fn get_strong_handle_prevents_reload_when_asset_still_alive() { - let (mut app, dir) = create_app(); - dir.insert_asset(Path::new("test.txt"), &[]); - - struct TrivialLoader; + /// A loader that immediately returns a [`TestAsset`]. + struct TrivialLoader; - impl AssetLoader for TrivialLoader { - type Asset = TestAsset; - type Settings = (); - type Error = std::io::Error; + impl AssetLoader for TrivialLoader { + type Asset = TestAsset; + type Settings = (); + type Error = std::io::Error; - async fn load( - &self, - _reader: &mut dyn Reader, - _settings: &Self::Settings, - _load_context: &mut LoadContext<'_>, - ) -> Result { - Ok(TestAsset) - } + async fn load( + &self, + _reader: &mut dyn Reader, + _settings: &Self::Settings, + _load_context: &mut LoadContext<'_>, + ) -> Result { + Ok(TestAsset) + } - fn extensions(&self) -> &[&str] { - &["txt"] - } + fn extensions(&self) -> &[&str] { + &["txt"] } + } + + #[test] + fn get_strong_handle_prevents_reload_when_asset_still_alive() { + let (mut app, dir) = create_app(); + dir.insert_asset(Path::new("test.txt"), &[]); app.init_asset::() .register_asset_loader(TrivialLoader); From 1730e4dddbf76613ba8f9eda57cf9a35ac7c98dd Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Nov 2025 11:47:15 -0800 Subject: [PATCH 3/5] Write a test to show that transitive dependencies of immediate loads are loaded. --- crates/bevy_asset/src/lib.rs | 100 +++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index b29b270167daa..f381a74e507a9 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -718,8 +718,8 @@ mod tests { }, loader::{AssetLoader, LoadContext}, Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, - AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, UnapprovedPathMode, - UntypedHandle, + AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, LoadedAsset, + UnapprovedPathMode, UntypedHandle, }; use alloc::{ boxed::Box, @@ -743,6 +743,7 @@ mod tests { }; use bevy_reflect::TypePath; use core::time::Duration; + use futures_lite::AsyncReadExt; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -762,7 +763,7 @@ mod tests { pub text: String, } - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, Default)] pub struct CoolTextRon { pub text: String, pub dependencies: Vec, @@ -2644,4 +2645,97 @@ mod tests { // assert_eq!(get_started_load_count(app.world()), 1); assert_eq!(get_started_load_count(app.world()), 2); } + + #[test] + fn immediate_nested_asset_loads_dependency() { + let (mut app, dir) = create_app(); + + /// This asset holds a handle to its dependency. + #[derive(Asset, TypePath)] + struct DeferredNested(Handle); + + struct DeferredNestedLoader; + + impl AssetLoader for DeferredNestedLoader { + type Asset = DeferredNested; + type Settings = (); + type Error = std::io::Error; + + async fn load( + &self, + reader: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let mut nested_path = String::new(); + reader.read_to_string(&mut nested_path).await?; + Ok(DeferredNested(load_context.load(nested_path))) + } + + fn extensions(&self) -> &[&str] { + &["defer"] + } + } + + /// This asset holds a handle a dependency of one of its dependencies. + #[derive(Asset, TypePath)] + struct ImmediateNested(Handle); + + struct ImmediateNestedLoader; + + impl AssetLoader for ImmediateNestedLoader { + type Asset = ImmediateNested; + type Settings = (); + type Error = std::io::Error; + + async fn load( + &self, + reader: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let mut nested_path = String::new(); + reader.read_to_string(&mut nested_path).await?; + let deferred_nested: LoadedAsset = load_context + .loader() + .immediate() + .load(nested_path) + .await + .unwrap(); + Ok(ImmediateNested(deferred_nested.get().0.clone())) + } + + fn extensions(&self) -> &[&str] { + &["immediate"] + } + } + + app.init_asset::() + .init_asset::() + .init_asset::() + .register_asset_loader(TrivialLoader) + .register_asset_loader(DeferredNestedLoader) + .register_asset_loader(ImmediateNestedLoader); + + dir.insert_asset_text(Path::new("a.immediate"), "b.defer"); + dir.insert_asset_text(Path::new("b.defer"), "c.txt"); + dir.insert_asset_text(Path::new("c.txt"), "hiya"); + + let server = app.world().resource::().clone(); + let immediate_handle: Handle = server.load("a.immediate"); + + run_app_until(&mut app, |world| { + let immediate_assets = world.resource::>(); + let immediate = immediate_assets.get(&immediate_handle)?; + + let test_asset_handle = immediate.0.clone(); + world + .resource::>() + .get(&test_asset_handle)?; + + // The immediate asset is loaded, and the asset it got from its immediate load is also + // loaded. + Some(()) + }); + } } From 54e76912fe084a2c34796be486fca177f0a88c6f Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Nov 2025 11:26:30 -0800 Subject: [PATCH 4/5] Propagate should_load_dependencies to nested immediate asset loads. --- crates/bevy_asset/src/loader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index a56aab4a05740..e287370ce7438 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -536,7 +536,7 @@ impl<'a> LoadContext<'a> { meta, loader, reader, - false, + self.should_load_dependencies, self.populate_hashes, ) .await From 5ad736bbf027473affd40c74c443c9303da0cea9 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 16 Nov 2025 11:29:59 -0800 Subject: [PATCH 5/5] Add a comment explaining what should_load_dependencies is for. --- crates/bevy_asset/src/loader.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index e287370ce7438..cf40e8840ad50 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -321,6 +321,10 @@ 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, + /// Specifies whether dependencies that are loaded deferred should be loaded. + /// + /// This allows us to skip loads for cases where we're never going to use the asset and we just + /// need the dependency information, for example during asset processing. pub(crate) should_load_dependencies: bool, populate_hashes: bool, asset_path: AssetPath<'static>,