AssetServer LoadState API consistency (#15237)

# Objective

- implements consistently named AssertServer methods for asset,
dependency, and recursive dependency load states
- returns relevant LoadState when required, including error information
for failed loads
- resolves #15098

## Solution

- implement consistently named LoadState accessor methods:
- load_state, dependency_load_state, recursive_dependency_load_state
(return unwrapped load states)
- get_load_state, get_dependency_load_state,
get_recursive_dependency_load_state (return Option)
- is_loaded, is_loaded_with_dependencies,
is_loaded_with_recursive_dependencies (return bool)
- adds AssetLoadError to DependencyLoadState::Failed and
RecursiveDependencyLoadState::Failed

## Testing

- Added coverage to existing unit tests
This commit is contained in:
Cole Varner 2024-09-19 12:18:31 -07:00 committed by GitHub
parent 106db47f69
commit 612897becd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 245 additions and 42 deletions

View file

@ -1246,13 +1246,19 @@ mod tests {
assert!(d_text.is_none()); assert!(d_text.is_none());
assert!(matches!(d_load, LoadState::Failed(_))); assert!(matches!(d_load, LoadState::Failed(_)));
assert_eq!(d_deps, DependencyLoadState::Failed); assert!(matches!(d_deps, DependencyLoadState::Failed(_)));
assert_eq!(d_rec_deps, RecursiveDependencyLoadState::Failed); assert!(matches!(
d_rec_deps,
RecursiveDependencyLoadState::Failed(_)
));
assert_eq!(a_text.text, "a"); assert_eq!(a_text.text, "a");
assert_eq!(a_load, LoadState::Loaded); assert_eq!(a_load, LoadState::Loaded);
assert_eq!(a_deps, DependencyLoadState::Loaded); assert_eq!(a_deps, DependencyLoadState::Loaded);
assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Failed); assert!(matches!(
a_rec_deps,
RecursiveDependencyLoadState::Failed(_)
));
assert_eq!(b_text.text, "b"); assert_eq!(b_text.text, "b");
assert_eq!(b_load, LoadState::Loaded); assert_eq!(b_load, LoadState::Loaded);
@ -1261,9 +1267,127 @@ mod tests {
assert_eq!(c_text.text, "c"); assert_eq!(c_text.text, "c");
assert_eq!(c_load, LoadState::Loaded); assert_eq!(c_load, LoadState::Loaded);
assert_eq!(c_deps, DependencyLoadState::Failed); assert!(matches!(c_deps, DependencyLoadState::Failed(_)));
assert_eq!(c_rec_deps, RecursiveDependencyLoadState::Failed); assert!(matches!(
c_rec_deps,
RecursiveDependencyLoadState::Failed(_)
));
assert_eq!(asset_server.load_state(a_id), LoadState::Loaded);
assert_eq!(
asset_server.dependency_load_state(a_id),
DependencyLoadState::Loaded
);
assert!(matches!(
asset_server.recursive_dependency_load_state(a_id),
RecursiveDependencyLoadState::Failed(_)
));
assert!(asset_server.is_loaded(a_id));
assert!(asset_server.is_loaded_with_direct_dependencies(a_id));
assert!(!asset_server.is_loaded_with_dependencies(a_id));
Some(())
});
}
#[test]
fn dependency_load_states() {
// The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
#[cfg(not(feature = "multi_threaded"))]
panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
let a_path = "a.cool.ron";
let a_ron = r#"
(
text: "a",
dependencies: [
"b.cool.ron",
"c.cool.ron",
],
embedded_dependencies: [],
sub_texts: []
)"#;
let b_path = "b.cool.ron";
let b_ron = r#"
(
text: "b",
dependencies: [],
MALFORMED
embedded_dependencies: [],
sub_texts: []
)"#;
let c_path = "c.cool.ron";
let c_ron = r#"
(
text: "c",
dependencies: [],
embedded_dependencies: [],
sub_texts: []
)"#;
let dir = Dir::default();
dir.insert_asset_text(Path::new(a_path), a_ron);
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);
app.init_asset::<CoolText>()
.register_asset_loader(CoolTextLoader);
let asset_server = app.world().resource::<AssetServer>().clone();
let handle: Handle<CoolText> = asset_server.load(a_path);
let a_id = handle.id();
app.world_mut().spawn(handle);
gate_opener.open(a_path);
run_app_until(&mut app, |world| {
let _a_text = get::<CoolText>(world, a_id)?;
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert_eq!(a_load, LoadState::Loaded);
assert_eq!(a_deps, DependencyLoadState::Loading);
assert_eq!(a_rec_deps, RecursiveDependencyLoadState::Loading);
Some(())
});
gate_opener.open(b_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let b_id = a_text.dependencies[0].id();
let (b_load, _b_deps, _b_rec_deps) = asset_server.get_load_states(b_id).unwrap();
if !matches!(b_load, LoadState::Failed(_)) {
// wait until b fails
return None;
}
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert_eq!(a_load, LoadState::Loaded);
assert!(matches!(a_deps, DependencyLoadState::Failed(_)));
assert!(matches!(
a_rec_deps,
RecursiveDependencyLoadState::Failed(_)
));
Some(())
});
gate_opener.open(c_path);
run_app_until(&mut app, |world| {
let a_text = get::<CoolText>(world, a_id)?;
let c_id = a_text.dependencies[1].id();
// wait until c loads
let _c_text = get::<CoolText>(world, c_id)?;
let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap();
assert_eq!(a_load, LoadState::Loaded);
assert!(
matches!(a_deps, DependencyLoadState::Failed(_)),
"Successful dependency load should not overwrite a previous failure"
);
assert!(
matches!(a_rec_deps, RecursiveDependencyLoadState::Failed(_)),
"Successful dependency load should not overwrite a previous failure"
);
Some(()) Some(())
}); });
} }

View file

@ -388,8 +388,10 @@ impl AssetInfos {
loaded_asset.value.insert(loaded_asset_id, world); loaded_asset.value.insert(loaded_asset_id, world);
let mut loading_deps = loaded_asset.dependencies; let mut loading_deps = loaded_asset.dependencies;
let mut failed_deps = HashSet::new(); let mut failed_deps = HashSet::new();
let mut dep_error = None;
let mut loading_rec_deps = loading_deps.clone(); let mut loading_rec_deps = loading_deps.clone();
let mut failed_rec_deps = HashSet::new(); let mut failed_rec_deps = HashSet::new();
let mut rec_dep_error = None;
loading_deps.retain(|dep_id| { loading_deps.retain(|dep_id| {
if let Some(dep_info) = self.get_mut(*dep_id) { if let Some(dep_info) = self.get_mut(*dep_id) {
match dep_info.rec_dep_load_state { match dep_info.rec_dep_load_state {
@ -404,7 +406,10 @@ impl AssetInfos {
// If dependency is loaded, reduce our count by one // If dependency is loaded, reduce our count by one
loading_rec_deps.remove(dep_id); loading_rec_deps.remove(dep_id);
} }
RecursiveDependencyLoadState::Failed => { RecursiveDependencyLoadState::Failed(ref error) => {
if rec_dep_error.is_none() {
rec_dep_error = Some(error.clone());
}
failed_rec_deps.insert(*dep_id); failed_rec_deps.insert(*dep_id);
loading_rec_deps.remove(dep_id); loading_rec_deps.remove(dep_id);
} }
@ -419,7 +424,10 @@ impl AssetInfos {
// If dependency is loaded, reduce our count by one // If dependency is loaded, reduce our count by one
false false
} }
LoadState::Failed(_) => { LoadState::Failed(ref error) => {
if dep_error.is_none() {
dep_error = Some(error.clone());
}
failed_deps.insert(*dep_id); failed_deps.insert(*dep_id);
false false
} }
@ -437,7 +445,7 @@ impl AssetInfos {
let dep_load_state = match (loading_deps.len(), failed_deps.len()) { let dep_load_state = match (loading_deps.len(), failed_deps.len()) {
(0, 0) => DependencyLoadState::Loaded, (0, 0) => DependencyLoadState::Loaded,
(_loading, 0) => DependencyLoadState::Loading, (_loading, 0) => DependencyLoadState::Loading,
(_loading, _failed) => DependencyLoadState::Failed, (_loading, _failed) => DependencyLoadState::Failed(dep_error.unwrap()),
}; };
let rec_dep_load_state = match (loading_rec_deps.len(), failed_rec_deps.len()) { let rec_dep_load_state = match (loading_rec_deps.len(), failed_rec_deps.len()) {
@ -450,7 +458,7 @@ impl AssetInfos {
RecursiveDependencyLoadState::Loaded RecursiveDependencyLoadState::Loaded
} }
(_loading, 0) => RecursiveDependencyLoadState::Loading, (_loading, 0) => RecursiveDependencyLoadState::Loading,
(_loading, _failed) => RecursiveDependencyLoadState::Failed, (_loading, _failed) => RecursiveDependencyLoadState::Failed(rec_dep_error.unwrap()),
}; };
let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = { let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = {
@ -480,14 +488,14 @@ impl AssetInfos {
info.failed_rec_dependencies = failed_rec_deps; info.failed_rec_dependencies = failed_rec_deps;
info.load_state = LoadState::Loaded; info.load_state = LoadState::Loaded;
info.dep_load_state = dep_load_state; info.dep_load_state = dep_load_state;
info.rec_dep_load_state = rec_dep_load_state; info.rec_dep_load_state = rec_dep_load_state.clone();
if watching_for_changes { if watching_for_changes {
info.loader_dependencies = loaded_asset.loader_dependencies; info.loader_dependencies = loaded_asset.loader_dependencies;
} }
let dependants_waiting_on_rec_load = if matches!( let dependants_waiting_on_rec_load = if matches!(
rec_dep_load_state, rec_dep_load_state,
RecursiveDependencyLoadState::Loaded | RecursiveDependencyLoadState::Failed RecursiveDependencyLoadState::Loaded | RecursiveDependencyLoadState::Failed(_)
) { ) {
Some(std::mem::take( Some(std::mem::take(
&mut info.dependants_waiting_on_recursive_dep_load, &mut info.dependants_waiting_on_recursive_dep_load,
@ -505,7 +513,9 @@ impl AssetInfos {
for id in dependants_waiting_on_load { for id in dependants_waiting_on_load {
if let Some(info) = self.get_mut(id) { if let Some(info) = self.get_mut(id) {
info.loading_dependencies.remove(&loaded_asset_id); info.loading_dependencies.remove(&loaded_asset_id);
if info.loading_dependencies.is_empty() { if info.loading_dependencies.is_empty()
&& !matches!(info.dep_load_state, DependencyLoadState::Failed(_))
{
// send dependencies loaded event // send dependencies loaded event
info.dep_load_state = DependencyLoadState::Loaded; info.dep_load_state = DependencyLoadState::Loaded;
} }
@ -519,9 +529,9 @@ impl AssetInfos {
Self::propagate_loaded_state(self, loaded_asset_id, dep_id, sender); Self::propagate_loaded_state(self, loaded_asset_id, dep_id, sender);
} }
} }
RecursiveDependencyLoadState::Failed => { RecursiveDependencyLoadState::Failed(ref error) => {
for dep_id in dependants_waiting_on_rec_load { for dep_id in dependants_waiting_on_rec_load {
Self::propagate_failed_state(self, loaded_asset_id, dep_id); Self::propagate_failed_state(self, loaded_asset_id, dep_id, error);
} }
} }
RecursiveDependencyLoadState::Loading | RecursiveDependencyLoadState::NotLoaded => { RecursiveDependencyLoadState::Loading | RecursiveDependencyLoadState::NotLoaded => {
@ -570,11 +580,12 @@ impl AssetInfos {
infos: &mut AssetInfos, infos: &mut AssetInfos,
failed_id: UntypedAssetId, failed_id: UntypedAssetId,
waiting_id: UntypedAssetId, waiting_id: UntypedAssetId,
error: &Arc<AssetLoadError>,
) { ) {
let dependants_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_id) { let dependants_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_id) {
info.loading_rec_dependencies.remove(&failed_id); info.loading_rec_dependencies.remove(&failed_id);
info.failed_rec_dependencies.insert(failed_id); info.failed_rec_dependencies.insert(failed_id);
info.rec_dep_load_state = RecursiveDependencyLoadState::Failed; info.rec_dep_load_state = RecursiveDependencyLoadState::Failed(error.clone());
Some(std::mem::take( Some(std::mem::take(
&mut info.dependants_waiting_on_recursive_dep_load, &mut info.dependants_waiting_on_recursive_dep_load,
)) ))
@ -584,7 +595,7 @@ impl AssetInfos {
if let Some(dependants_waiting_on_rec_load) = dependants_waiting_on_rec_load { if let Some(dependants_waiting_on_rec_load) = dependants_waiting_on_rec_load {
for dep_id in dependants_waiting_on_rec_load { for dep_id in dependants_waiting_on_rec_load {
Self::propagate_failed_state(infos, waiting_id, dep_id); Self::propagate_failed_state(infos, waiting_id, dep_id, error);
} }
} }
} }
@ -595,14 +606,15 @@ impl AssetInfos {
return; return;
} }
let error = Arc::new(error);
let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = { let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = {
let Some(info) = self.get_mut(failed_id) else { let Some(info) = self.get_mut(failed_id) else {
// The asset was already dropped. // The asset was already dropped.
return; return;
}; };
info.load_state = LoadState::Failed(Box::new(error)); info.load_state = LoadState::Failed(error.clone());
info.dep_load_state = DependencyLoadState::Failed; info.dep_load_state = DependencyLoadState::Failed(error.clone());
info.rec_dep_load_state = RecursiveDependencyLoadState::Failed; info.rec_dep_load_state = RecursiveDependencyLoadState::Failed(error.clone());
( (
std::mem::take(&mut info.dependants_waiting_on_load), std::mem::take(&mut info.dependants_waiting_on_load),
std::mem::take(&mut info.dependants_waiting_on_recursive_dep_load), std::mem::take(&mut info.dependants_waiting_on_recursive_dep_load),
@ -613,12 +625,15 @@ impl AssetInfos {
if let Some(info) = self.get_mut(waiting_id) { if let Some(info) = self.get_mut(waiting_id) {
info.loading_dependencies.remove(&failed_id); info.loading_dependencies.remove(&failed_id);
info.failed_dependencies.insert(failed_id); info.failed_dependencies.insert(failed_id);
info.dep_load_state = DependencyLoadState::Failed; // don't overwrite DependencyLoadState if already failed to preserve first error
if !(matches!(info.dep_load_state, DependencyLoadState::Failed(_))) {
info.dep_load_state = DependencyLoadState::Failed(error.clone());
}
} }
} }
for waiting_id in dependants_waiting_on_rec_load { for waiting_id in dependants_waiting_on_rec_load {
Self::propagate_failed_state(self, failed_id, waiting_id); Self::propagate_failed_state(self, failed_id, waiting_id, &error);
} }
} }

View file

@ -898,17 +898,20 @@ impl AssetServer {
&self, &self,
id: impl Into<UntypedAssetId>, id: impl Into<UntypedAssetId>,
) -> Option<(LoadState, DependencyLoadState, RecursiveDependencyLoadState)> { ) -> Option<(LoadState, DependencyLoadState, RecursiveDependencyLoadState)> {
self.data self.data.infos.read().get(id.into()).map(|i| {
.infos (
.read() i.load_state.clone(),
.get(id.into()) i.dep_load_state.clone(),
.map(|i| (i.load_state.clone(), i.dep_load_state, i.rec_dep_load_state)) i.rec_dep_load_state.clone(),
)
})
} }
/// Retrieves the main [`LoadState`] of a given asset `id`. /// Retrieves the main [`LoadState`] of a given asset `id`.
/// ///
/// Note that this is "just" the root asset load state. To check if an asset _and_ its recursive /// Note that this is "just" the root asset load state. To get the load state of
/// dependencies have loaded, see [`AssetServer::is_loaded_with_dependencies`]. /// its dependencies or recursive dependencies, see [`AssetServer::get_dependency_load_state`]
/// and [`AssetServer::get_recursive_dependency_load_state`] respectively.
pub fn get_load_state(&self, id: impl Into<UntypedAssetId>) -> Option<LoadState> { pub fn get_load_state(&self, id: impl Into<UntypedAssetId>) -> Option<LoadState> {
self.data self.data
.infos .infos
@ -917,7 +920,27 @@ impl AssetServer {
.map(|i| i.load_state.clone()) .map(|i| i.load_state.clone())
} }
/// Retrieves the [`RecursiveDependencyLoadState`] of a given asset `id`. /// Retrieves the [`DependencyLoadState`] of a given asset `id`'s dependencies.
///
/// Note that this is only the load state of direct dependencies of the root asset. To get
/// the load state of the root asset itself or its recursive dependencies, see
/// [`AssetServer::get_load_state`] and [`AssetServer::get_recursive_dependency_load_state`] respectively.
pub fn get_dependency_load_state(
&self,
id: impl Into<UntypedAssetId>,
) -> Option<DependencyLoadState> {
self.data
.infos
.read()
.get(id.into())
.map(|i| i.dep_load_state.clone())
}
/// Retrieves the main [`RecursiveDependencyLoadState`] of a given asset `id`'s recursive dependencies.
///
/// Note that this is only the load state of recursive dependencies of the root asset. To get
/// the load state of the root asset itself or its direct dependencies only, see
/// [`AssetServer::get_load_state`] and [`AssetServer::get_dependency_load_state`] respectively.
pub fn get_recursive_dependency_load_state( pub fn get_recursive_dependency_load_state(
&self, &self,
id: impl Into<UntypedAssetId>, id: impl Into<UntypedAssetId>,
@ -926,15 +949,30 @@ impl AssetServer {
.infos .infos
.read() .read()
.get(id.into()) .get(id.into())
.map(|i| i.rec_dep_load_state) .map(|i| i.rec_dep_load_state.clone())
} }
/// Retrieves the main [`LoadState`] of a given asset `id`. /// Retrieves the main [`LoadState`] of a given asset `id`.
///
/// This is the same as [`AssetServer::get_load_state`] except the result is unwrapped. If
/// the result is None, [`LoadState::NotLoaded`] is returned.
pub fn load_state(&self, id: impl Into<UntypedAssetId>) -> LoadState { pub fn load_state(&self, id: impl Into<UntypedAssetId>) -> LoadState {
self.get_load_state(id).unwrap_or(LoadState::NotLoaded) self.get_load_state(id).unwrap_or(LoadState::NotLoaded)
} }
/// Retrieves the [`DependencyLoadState`] of a given asset `id`.
///
/// This is the same as [`AssetServer::get_dependency_load_state`] except the result is unwrapped. If
/// the result is None, [`DependencyLoadState::NotLoaded`] is returned.
pub fn dependency_load_state(&self, id: impl Into<UntypedAssetId>) -> DependencyLoadState {
self.get_dependency_load_state(id)
.unwrap_or(DependencyLoadState::NotLoaded)
}
/// Retrieves the [`RecursiveDependencyLoadState`] of a given asset `id`. /// Retrieves the [`RecursiveDependencyLoadState`] of a given asset `id`.
///
/// This is the same as [`AssetServer::get_recursive_dependency_load_state`] except the result is unwrapped. If
/// the result is None, [`RecursiveDependencyLoadState::NotLoaded`] is returned.
pub fn recursive_dependency_load_state( pub fn recursive_dependency_load_state(
&self, &self,
id: impl Into<UntypedAssetId>, id: impl Into<UntypedAssetId>,
@ -943,11 +981,30 @@ impl AssetServer {
.unwrap_or(RecursiveDependencyLoadState::NotLoaded) .unwrap_or(RecursiveDependencyLoadState::NotLoaded)
} }
/// Returns true if the asset and all of its dependencies (recursive) have been loaded. /// Convenience method that returns true if the asset has been loaded.
pub fn is_loaded(&self, id: impl Into<UntypedAssetId>) -> bool {
matches!(self.load_state(id), LoadState::Loaded)
}
/// Convenience method that returns true if the asset and all of its direct dependencies have been loaded.
pub fn is_loaded_with_direct_dependencies(&self, id: impl Into<UntypedAssetId>) -> bool {
matches!(
self.get_load_states(id),
Some((LoadState::Loaded, DependencyLoadState::Loaded, _))
)
}
/// Convenience method that returns true if the asset, all of its dependencies, and all of its recursive
/// dependencies have been loaded.
pub fn is_loaded_with_dependencies(&self, id: impl Into<UntypedAssetId>) -> bool { pub fn is_loaded_with_dependencies(&self, id: impl Into<UntypedAssetId>) -> bool {
let id = id.into(); matches!(
self.load_state(id) == LoadState::Loaded self.get_load_states(id),
&& self.recursive_dependency_load_state(id) == RecursiveDependencyLoadState::Loaded Some((
LoadState::Loaded,
DependencyLoadState::Loaded,
RecursiveDependencyLoadState::Loaded
))
)
} }
/// Returns an active handle for the given path, if the asset at the given path has already started loading, /// Returns an active handle for the given path, if the asset at the given path has already started loading,
@ -1363,12 +1420,14 @@ pub enum LoadState {
Loading, Loading,
/// The asset has been loaded and has been added to the [`World`] /// The asset has been loaded and has been added to the [`World`]
Loaded, Loaded,
/// The asset failed to load. /// The asset failed to load. The underlying [`AssetLoadError`] is
Failed(Box<AssetLoadError>), /// referenced by [`Arc`] clones in all related [`DependencyLoadState`]s
/// and [`RecursiveDependencyLoadState`]s in the asset's dependency tree.
Failed(Arc<AssetLoadError>),
} }
/// The load state of an asset's dependencies. /// The load state of an asset's dependencies.
#[derive(Component, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Component, Clone, Debug, Eq, PartialEq)]
pub enum DependencyLoadState { pub enum DependencyLoadState {
/// The asset has not started loading yet /// The asset has not started loading yet
NotLoaded, NotLoaded,
@ -1376,12 +1435,14 @@ pub enum DependencyLoadState {
Loading, Loading,
/// Dependencies have all loaded /// Dependencies have all loaded
Loaded, Loaded,
/// One or more dependencies have failed to load /// One or more dependencies have failed to load. The underlying [`AssetLoadError`]
Failed, /// is referenced by [`Arc`] clones in all related [`LoadState`] and
/// [`RecursiveDependencyLoadState`]s in the asset's dependency tree.
Failed(Arc<AssetLoadError>),
} }
/// The recursive load state of an asset's dependencies. /// The recursive load state of an asset's dependencies.
#[derive(Component, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Component, Clone, Debug, Eq, PartialEq)]
pub enum RecursiveDependencyLoadState { pub enum RecursiveDependencyLoadState {
/// The asset has not started loading yet /// The asset has not started loading yet
NotLoaded, NotLoaded,
@ -1389,8 +1450,11 @@ pub enum RecursiveDependencyLoadState {
Loading, Loading,
/// Dependencies in this asset's dependency tree have all loaded /// Dependencies in this asset's dependency tree have all loaded
Loaded, Loaded,
/// One or more dependencies have failed to load in this asset's dependency tree /// One or more dependencies have failed to load in this asset's dependency
Failed, /// tree. The underlying [`AssetLoadError`] is referenced by [`Arc`] clones
/// in all related [`LoadState`]s and [`DependencyLoadState`]s in the asset's
/// dependency tree.
Failed(Arc<AssetLoadError>),
} }
/// An error that occurs during an [`Asset`] load. /// An error that occurs during an [`Asset`] load.