allow asset loader pre-registration (#9429)

# Objective

- When loading gltf files during app creation (for example using a
FromWorld impl and adding that as a resource), no loader was found.
- As the gltf loader can load compressed formats, it needs to know what
the GPU supports so it's not available at app creation time.

## Solution

alternative to #9426

- add functionality to preregister the loader. loading assets with
matching extensions will block until a real loader is registered.
- preregister "gltf" and "glb".
- prereigster image formats.

the way this is set up, if a set of extensions are all registered with a
single preregistration call, then later a loader is added that matches
some of the extensions, assets using the remaining extensions will then
fail. i think that should work well for image formats that we don't know
are supported until later.
This commit is contained in:
robtfm 2023-08-14 22:27:51 +01:00 committed by Carter Anderson
parent 77507c3af1
commit bd6764113b
6 changed files with 138 additions and 26 deletions

View file

@ -32,6 +32,7 @@ downcast-rs = "1.2.0"
fastrand = "1.7.0" fastrand = "1.7.0"
notify = { version = "6.0.0", optional = true } notify = { version = "6.0.0", optional = true }
parking_lot = "0.12.1" parking_lot = "0.12.1"
async-channel = "1.4.2"
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
bevy_winit = { path = "../bevy_winit", version = "0.11.1" } bevy_winit = { path = "../bevy_winit", version = "0.11.1" }

View file

@ -62,6 +62,15 @@ pub(crate) struct AssetRefCounter {
pub(crate) mark_unused_assets: Arc<Mutex<Vec<HandleId>>>, pub(crate) mark_unused_assets: Arc<Mutex<Vec<HandleId>>>,
} }
#[derive(Clone)]
enum MaybeAssetLoader {
Ready(Arc<dyn AssetLoader>),
Pending {
sender: async_channel::Sender<()>,
receiver: async_channel::Receiver<()>,
},
}
/// Internal data for the asset server. /// Internal data for the asset server.
/// ///
/// [`AssetServer`] is the public API for interacting with the asset server. /// [`AssetServer`] is the public API for interacting with the asset server.
@ -70,7 +79,7 @@ pub struct AssetServerInternal {
pub(crate) asset_ref_counter: AssetRefCounter, pub(crate) asset_ref_counter: AssetRefCounter,
pub(crate) asset_sources: Arc<RwLock<HashMap<SourcePathId, SourceInfo>>>, pub(crate) asset_sources: Arc<RwLock<HashMap<SourcePathId, SourceInfo>>>,
pub(crate) asset_lifecycles: Arc<RwLock<HashMap<Uuid, Box<dyn AssetLifecycle>>>>, pub(crate) asset_lifecycles: Arc<RwLock<HashMap<Uuid, Box<dyn AssetLifecycle>>>>,
loaders: RwLock<Vec<Arc<dyn AssetLoader>>>, loaders: RwLock<Vec<MaybeAssetLoader>>,
extension_to_loader_index: RwLock<HashMap<String, usize>>, extension_to_loader_index: RwLock<HashMap<String, usize>>,
handle_to_path: Arc<RwLock<HashMap<HandleId, AssetPath<'static>>>>, handle_to_path: Arc<RwLock<HashMap<HandleId, AssetPath<'static>>>>,
} }
@ -157,6 +166,28 @@ impl AssetServer {
Assets::new(self.server.asset_ref_counter.channel.sender.clone()) Assets::new(self.server.asset_ref_counter.channel.sender.clone())
} }
/// Pre-register a loader that will later be added.
///
/// Assets loaded with matching extensions will be blocked until the
/// real loader is added.
pub fn preregister_loader(&self, extensions: &[&str]) {
let mut loaders = self.server.loaders.write();
let loader_index = loaders.len();
for extension in extensions {
if self
.server
.extension_to_loader_index
.write()
.insert(extension.to_string(), loader_index)
.is_some()
{
warn!("duplicate preregistration for `{extension}`, any assets loaded with the previous loader will never complete.");
}
}
let (sender, receiver) = async_channel::bounded(1);
loaders.push(MaybeAssetLoader::Pending { sender, receiver });
}
/// Adds the provided asset loader to the server. /// Adds the provided asset loader to the server.
/// ///
/// If `loader` has one or more supported extensions in conflict with loaders that came before /// If `loader` has one or more supported extensions in conflict with loaders that came before
@ -166,14 +197,50 @@ impl AssetServer {
T: AssetLoader, T: AssetLoader,
{ {
let mut loaders = self.server.loaders.write(); let mut loaders = self.server.loaders.write();
let loader_index = loaders.len(); let next_loader_index = loaders.len();
let mut maybe_existing_loader_index = None;
let mut loader_map = self.server.extension_to_loader_index.write();
let mut maybe_sender = None;
for extension in loader.extensions() { for extension in loader.extensions() {
self.server if let Some(&extension_index) = loader_map.get(*extension) {
.extension_to_loader_index // replacing an existing entry
.write() match maybe_existing_loader_index {
.insert(extension.to_string(), loader_index); None => {
match &loaders[extension_index] {
MaybeAssetLoader::Ready(_) => {
// replacing an existing loader, nothing special to do
}
MaybeAssetLoader::Pending { sender, .. } => {
// the loader was pre-registered, store the channel to notify pending assets
maybe_sender = Some(sender.clone());
}
}
}
Some(index) => {
// ensure the loader extensions are consistent
if index != extension_index {
warn!("inconsistent extensions between loader preregister_loader and add_loader, \
loading `{extension}` assets will never complete.");
}
}
}
maybe_existing_loader_index = Some(extension_index);
} else {
loader_map.insert(extension.to_string(), next_loader_index);
}
}
if let Some(existing_index) = maybe_existing_loader_index {
loaders[existing_index] = MaybeAssetLoader::Ready(Arc::new(loader));
if let Some(sender) = maybe_sender {
// notify after replacing the loader
let _ = sender.send_blocking(());
}
} else {
loaders.push(MaybeAssetLoader::Ready(Arc::new(loader)));
} }
loaders.push(Arc::new(loader));
} }
/// Gets a strong handle for an asset with the provided id. /// Gets a strong handle for an asset with the provided id.
@ -188,7 +255,7 @@ impl AssetServer {
HandleUntyped::strong(id.into(), sender) HandleUntyped::strong(id.into(), sender)
} }
fn get_asset_loader(&self, extension: &str) -> Result<Arc<dyn AssetLoader>, AssetServerError> { fn get_asset_loader(&self, extension: &str) -> Result<MaybeAssetLoader, AssetServerError> {
let index = { let index = {
// scope map to drop lock as soon as possible // scope map to drop lock as soon as possible
let map = self.server.extension_to_loader_index.read(); let map = self.server.extension_to_loader_index.read();
@ -204,7 +271,8 @@ impl AssetServer {
fn get_path_asset_loader<P: AsRef<Path>>( fn get_path_asset_loader<P: AsRef<Path>>(
&self, &self,
path: P, path: P,
) -> Result<Arc<dyn AssetLoader>, AssetServerError> { include_pending: bool,
) -> Result<MaybeAssetLoader, AssetServerError> {
let s = path let s = path
.as_ref() .as_ref()
.file_name() .file_name()
@ -223,7 +291,9 @@ impl AssetServer {
ext = &ext[idx + 1..]; ext = &ext[idx + 1..];
exts.push(ext); exts.push(ext);
if let Ok(loader) = self.get_asset_loader(ext) { if let Ok(loader) = self.get_asset_loader(ext) {
return Ok(loader); if include_pending || matches!(loader, MaybeAssetLoader::Ready(_)) {
return Ok(loader);
}
} }
} }
Err(AssetServerError::MissingAssetLoader { Err(AssetServerError::MissingAssetLoader {
@ -354,12 +424,21 @@ impl AssetServer {
}; };
// get the according asset loader // get the according asset loader
let asset_loader = match self.get_path_asset_loader(asset_path.path()) { let mut maybe_asset_loader = self.get_path_asset_loader(asset_path.path(), true);
Ok(loader) => loader,
// if it's still pending, block until notified and refetch the new asset loader
if let Ok(MaybeAssetLoader::Pending { receiver, .. }) = maybe_asset_loader {
let _ = receiver.recv().await;
maybe_asset_loader = self.get_path_asset_loader(asset_path.path(), false);
}
let asset_loader = match maybe_asset_loader {
Ok(MaybeAssetLoader::Ready(loader)) => loader,
Err(err) => { Err(err) => {
set_asset_failed(); set_asset_failed();
return Err(err); return Err(err);
} }
Ok(MaybeAssetLoader::Pending { .. }) => unreachable!(),
}; };
// load the asset bytes // load the asset bytes
@ -492,7 +571,7 @@ impl AssetServer {
if self.asset_io().is_dir(&child_path) { if self.asset_io().is_dir(&child_path) {
handles.extend(self.load_folder(&child_path)?); handles.extend(self.load_folder(&child_path)?);
} else { } else {
if self.get_path_asset_loader(&child_path).is_err() { if self.get_path_asset_loader(&child_path, true).is_err() {
continue; continue;
} }
let handle = let handle =
@ -711,8 +790,11 @@ mod test {
let asset_server = setup("."); let asset_server = setup(".");
asset_server.add_loader(FakePngLoader); asset_server.add_loader(FakePngLoader);
let t = asset_server.get_path_asset_loader("test.png"); let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.png", true) else {
assert_eq!(t.unwrap().extensions()[0], "png"); panic!();
};
assert_eq!(t.extensions()[0], "png");
} }
#[test] #[test]
@ -720,14 +802,16 @@ mod test {
let asset_server = setup("."); let asset_server = setup(".");
asset_server.add_loader(FakePngLoader); asset_server.add_loader(FakePngLoader);
let t = asset_server.get_path_asset_loader("test.PNG"); let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.PNG", true) else {
assert_eq!(t.unwrap().extensions()[0], "png"); panic!();
};
assert_eq!(t.extensions()[0], "png");
} }
#[test] #[test]
fn no_loader() { fn no_loader() {
let asset_server = setup("."); let asset_server = setup(".");
let t = asset_server.get_path_asset_loader("test.pong"); let t = asset_server.get_path_asset_loader("test.pong", true);
assert!(t.is_err()); assert!(t.is_err());
} }
@ -736,7 +820,7 @@ mod test {
let asset_server = setup("."); let asset_server = setup(".");
assert!( assert!(
match asset_server.get_path_asset_loader("test.v1.2.3.pong") { match asset_server.get_path_asset_loader("test.v1.2.3.pong", true) {
Err(AssetServerError::MissingAssetLoader { extensions }) => Err(AssetServerError::MissingAssetLoader { extensions }) =>
extensions == vec!["v1.2.3.pong", "2.3.pong", "3.pong", "pong"], extensions == vec!["v1.2.3.pong", "2.3.pong", "3.pong", "pong"],
_ => false, _ => false,
@ -771,8 +855,10 @@ mod test {
let asset_server = setup("."); let asset_server = setup(".");
asset_server.add_loader(FakePngLoader); asset_server.add_loader(FakePngLoader);
let t = asset_server.get_path_asset_loader("test-v1.2.3.png"); let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test-v1.2.3.png", true) else {
assert_eq!(t.unwrap().extensions()[0], "png"); panic!();
};
assert_eq!(t.extensions()[0], "png");
} }
#[test] #[test]
@ -780,8 +866,10 @@ mod test {
let asset_server = setup("."); let asset_server = setup(".");
asset_server.add_loader(FakeMultipleDotLoader); asset_server.add_loader(FakeMultipleDotLoader);
let t = asset_server.get_path_asset_loader("test.test.png"); let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.test.png", true) else {
assert_eq!(t.unwrap().extensions()[0], "test.png"); panic!();
};
assert_eq!(t.extensions()[0], "test.png");
} }
fn create_dir_and_file(file: impl AsRef<Path>) -> tempfile::TempDir { fn create_dir_and_file(file: impl AsRef<Path>) -> tempfile::TempDir {

View file

@ -317,6 +317,10 @@ pub trait AddAsset {
fn add_asset_loader<T>(&mut self, loader: T) -> &mut Self fn add_asset_loader<T>(&mut self, loader: T) -> &mut Self
where where
T: AssetLoader; T: AssetLoader;
/// Preregisters a loader for the given extensions, that will block asset loads until a real loader
/// is registered.
fn preregister_asset_loader(&mut self, extensions: &[&str]) -> &mut Self;
} }
impl AddAsset for App { impl AddAsset for App {
@ -404,6 +408,13 @@ impl AddAsset for App {
self.world.resource_mut::<AssetServer>().add_loader(loader); self.world.resource_mut::<AssetServer>().add_loader(loader);
self self
} }
fn preregister_asset_loader(&mut self, extensions: &[&str]) -> &mut Self {
self.world
.resource_mut::<AssetServer>()
.preregister_loader(extensions);
self
}
} }
/// Loads an internal asset from a project source file. /// Loads an internal asset from a project source file.

View file

@ -44,7 +44,8 @@ impl Plugin for GltfPlugin {
.add_asset::<Gltf>() .add_asset::<Gltf>()
.add_asset::<GltfNode>() .add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>() .add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>(); .add_asset::<GltfMesh>()
.preregister_asset_loader(&["gltf", "glb"]);
} }
fn finish(&self, app: &mut App) { fn finish(&self, app: &mut App) {

View file

@ -17,7 +17,7 @@ pub struct ImageTextureLoader {
supported_compressed_formats: CompressedImageFormats, supported_compressed_formats: CompressedImageFormats,
} }
const FILE_EXTENSIONS: &[&str] = &[ pub(crate) const IMG_FILE_EXTENSIONS: &[&str] = &[
#[cfg(feature = "basis-universal")] #[cfg(feature = "basis-universal")]
"basis", "basis",
#[cfg(feature = "bmp")] #[cfg(feature = "bmp")]
@ -73,7 +73,7 @@ impl AssetLoader for ImageTextureLoader {
} }
fn extensions(&self) -> &[&str] { fn extensions(&self) -> &[&str] {
FILE_EXTENSIONS IMG_FILE_EXTENSIONS
} }
} }

View file

@ -96,6 +96,17 @@ impl Plugin for ImagePlugin {
update_texture_cache_system.in_set(RenderSet::Cleanup), update_texture_cache_system.in_set(RenderSet::Cleanup),
); );
} }
#[cfg(any(
feature = "png",
feature = "dds",
feature = "tga",
feature = "jpeg",
feature = "bmp",
feature = "basis-universal",
feature = "ktx2",
))]
app.preregister_asset_loader(IMG_FILE_EXTENSIONS);
} }
fn finish(&self, app: &mut App) { fn finish(&self, app: &mut App) {