Skip to content

Commit b30ff2a

Browse files
authored
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.
1 parent 43fe83b commit b30ff2a

File tree

6 files changed

+138
-26
lines changed

6 files changed

+138
-26
lines changed

crates/bevy_asset/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ downcast-rs = "1.2.0"
3232
fastrand = "1.7.0"
3333
notify = { version = "6.0.0", optional = true }
3434
parking_lot = "0.12.1"
35+
async-channel = "1.4.2"
3536

3637
[target.'cfg(target_os = "android")'.dependencies]
3738
bevy_winit = { path = "../bevy_winit", version = "0.12.0-dev" }

crates/bevy_asset/src/asset_server.rs

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ pub(crate) struct AssetRefCounter {
6262
pub(crate) mark_unused_assets: Arc<Mutex<Vec<HandleId>>>,
6363
}
6464

65+
#[derive(Clone)]
66+
enum MaybeAssetLoader {
67+
Ready(Arc<dyn AssetLoader>),
68+
Pending {
69+
sender: async_channel::Sender<()>,
70+
receiver: async_channel::Receiver<()>,
71+
},
72+
}
73+
6574
/// Internal data for the asset server.
6675
///
6776
/// [`AssetServer`] is the public API for interacting with the asset server.
@@ -70,7 +79,7 @@ pub struct AssetServerInternal {
7079
pub(crate) asset_ref_counter: AssetRefCounter,
7180
pub(crate) asset_sources: Arc<RwLock<HashMap<SourcePathId, SourceInfo>>>,
7281
pub(crate) asset_lifecycles: Arc<RwLock<HashMap<Uuid, Box<dyn AssetLifecycle>>>>,
73-
loaders: RwLock<Vec<Arc<dyn AssetLoader>>>,
82+
loaders: RwLock<Vec<MaybeAssetLoader>>,
7483
extension_to_loader_index: RwLock<HashMap<String, usize>>,
7584
handle_to_path: Arc<RwLock<HashMap<HandleId, AssetPath<'static>>>>,
7685
}
@@ -157,6 +166,28 @@ impl AssetServer {
157166
Assets::new(self.server.asset_ref_counter.channel.sender.clone())
158167
}
159168

169+
/// Pre-register a loader that will later be added.
170+
///
171+
/// Assets loaded with matching extensions will be blocked until the
172+
/// real loader is added.
173+
pub fn preregister_loader(&self, extensions: &[&str]) {
174+
let mut loaders = self.server.loaders.write();
175+
let loader_index = loaders.len();
176+
for extension in extensions {
177+
if self
178+
.server
179+
.extension_to_loader_index
180+
.write()
181+
.insert(extension.to_string(), loader_index)
182+
.is_some()
183+
{
184+
warn!("duplicate preregistration for `{extension}`, any assets loaded with the previous loader will never complete.");
185+
}
186+
}
187+
let (sender, receiver) = async_channel::bounded(1);
188+
loaders.push(MaybeAssetLoader::Pending { sender, receiver });
189+
}
190+
160191
/// Adds the provided asset loader to the server.
161192
///
162193
/// If `loader` has one or more supported extensions in conflict with loaders that came before
@@ -166,14 +197,50 @@ impl AssetServer {
166197
T: AssetLoader,
167198
{
168199
let mut loaders = self.server.loaders.write();
169-
let loader_index = loaders.len();
200+
let next_loader_index = loaders.len();
201+
let mut maybe_existing_loader_index = None;
202+
let mut loader_map = self.server.extension_to_loader_index.write();
203+
let mut maybe_sender = None;
204+
170205
for extension in loader.extensions() {
171-
self.server
172-
.extension_to_loader_index
173-
.write()
174-
.insert(extension.to_string(), loader_index);
206+
if let Some(&extension_index) = loader_map.get(*extension) {
207+
// replacing an existing entry
208+
match maybe_existing_loader_index {
209+
None => {
210+
match &loaders[extension_index] {
211+
MaybeAssetLoader::Ready(_) => {
212+
// replacing an existing loader, nothing special to do
213+
}
214+
MaybeAssetLoader::Pending { sender, .. } => {
215+
// the loader was pre-registered, store the channel to notify pending assets
216+
maybe_sender = Some(sender.clone());
217+
}
218+
}
219+
}
220+
Some(index) => {
221+
// ensure the loader extensions are consistent
222+
if index != extension_index {
223+
warn!("inconsistent extensions between loader preregister_loader and add_loader, \
224+
loading `{extension}` assets will never complete.");
225+
}
226+
}
227+
}
228+
229+
maybe_existing_loader_index = Some(extension_index);
230+
} else {
231+
loader_map.insert(extension.to_string(), next_loader_index);
232+
}
233+
}
234+
235+
if let Some(existing_index) = maybe_existing_loader_index {
236+
loaders[existing_index] = MaybeAssetLoader::Ready(Arc::new(loader));
237+
if let Some(sender) = maybe_sender {
238+
// notify after replacing the loader
239+
let _ = sender.send_blocking(());
240+
}
241+
} else {
242+
loaders.push(MaybeAssetLoader::Ready(Arc::new(loader)));
175243
}
176-
loaders.push(Arc::new(loader));
177244
}
178245

179246
/// Gets a strong handle for an asset with the provided id.
@@ -188,7 +255,7 @@ impl AssetServer {
188255
HandleUntyped::strong(id.into(), sender)
189256
}
190257

191-
fn get_asset_loader(&self, extension: &str) -> Result<Arc<dyn AssetLoader>, AssetServerError> {
258+
fn get_asset_loader(&self, extension: &str) -> Result<MaybeAssetLoader, AssetServerError> {
192259
let index = {
193260
// scope map to drop lock as soon as possible
194261
let map = self.server.extension_to_loader_index.read();
@@ -204,7 +271,8 @@ impl AssetServer {
204271
fn get_path_asset_loader<P: AsRef<Path>>(
205272
&self,
206273
path: P,
207-
) -> Result<Arc<dyn AssetLoader>, AssetServerError> {
274+
include_pending: bool,
275+
) -> Result<MaybeAssetLoader, AssetServerError> {
208276
let s = path
209277
.as_ref()
210278
.file_name()
@@ -223,7 +291,9 @@ impl AssetServer {
223291
ext = &ext[idx + 1..];
224292
exts.push(ext);
225293
if let Ok(loader) = self.get_asset_loader(ext) {
226-
return Ok(loader);
294+
if include_pending || matches!(loader, MaybeAssetLoader::Ready(_)) {
295+
return Ok(loader);
296+
}
227297
}
228298
}
229299
Err(AssetServerError::MissingAssetLoader {
@@ -354,12 +424,21 @@ impl AssetServer {
354424
};
355425

356426
// get the according asset loader
357-
let asset_loader = match self.get_path_asset_loader(asset_path.path()) {
358-
Ok(loader) => loader,
427+
let mut maybe_asset_loader = self.get_path_asset_loader(asset_path.path(), true);
428+
429+
// if it's still pending, block until notified and refetch the new asset loader
430+
if let Ok(MaybeAssetLoader::Pending { receiver, .. }) = maybe_asset_loader {
431+
let _ = receiver.recv().await;
432+
maybe_asset_loader = self.get_path_asset_loader(asset_path.path(), false);
433+
}
434+
435+
let asset_loader = match maybe_asset_loader {
436+
Ok(MaybeAssetLoader::Ready(loader)) => loader,
359437
Err(err) => {
360438
set_asset_failed();
361439
return Err(err);
362440
}
441+
Ok(MaybeAssetLoader::Pending { .. }) => unreachable!(),
363442
};
364443

365444
// load the asset bytes
@@ -492,7 +571,7 @@ impl AssetServer {
492571
if self.asset_io().is_dir(&child_path) {
493572
handles.extend(self.load_folder(&child_path)?);
494573
} else {
495-
if self.get_path_asset_loader(&child_path).is_err() {
574+
if self.get_path_asset_loader(&child_path, true).is_err() {
496575
continue;
497576
}
498577
let handle =
@@ -711,23 +790,28 @@ mod test {
711790
let asset_server = setup(".");
712791
asset_server.add_loader(FakePngLoader);
713792

714-
let t = asset_server.get_path_asset_loader("test.png");
715-
assert_eq!(t.unwrap().extensions()[0], "png");
793+
let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.png", true) else {
794+
panic!();
795+
};
796+
797+
assert_eq!(t.extensions()[0], "png");
716798
}
717799

718800
#[test]
719801
fn case_insensitive_extensions() {
720802
let asset_server = setup(".");
721803
asset_server.add_loader(FakePngLoader);
722804

723-
let t = asset_server.get_path_asset_loader("test.PNG");
724-
assert_eq!(t.unwrap().extensions()[0], "png");
805+
let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.PNG", true) else {
806+
panic!();
807+
};
808+
assert_eq!(t.extensions()[0], "png");
725809
}
726810

727811
#[test]
728812
fn no_loader() {
729813
let asset_server = setup(".");
730-
let t = asset_server.get_path_asset_loader("test.pong");
814+
let t = asset_server.get_path_asset_loader("test.pong", true);
731815
assert!(t.is_err());
732816
}
733817

@@ -736,7 +820,7 @@ mod test {
736820
let asset_server = setup(".");
737821

738822
assert!(
739-
match asset_server.get_path_asset_loader("test.v1.2.3.pong") {
823+
match asset_server.get_path_asset_loader("test.v1.2.3.pong", true) {
740824
Err(AssetServerError::MissingAssetLoader { extensions }) =>
741825
extensions == vec!["v1.2.3.pong", "2.3.pong", "3.pong", "pong"],
742826
_ => false,
@@ -771,17 +855,21 @@ mod test {
771855
let asset_server = setup(".");
772856
asset_server.add_loader(FakePngLoader);
773857

774-
let t = asset_server.get_path_asset_loader("test-v1.2.3.png");
775-
assert_eq!(t.unwrap().extensions()[0], "png");
858+
let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test-v1.2.3.png", true) else {
859+
panic!();
860+
};
861+
assert_eq!(t.extensions()[0], "png");
776862
}
777863

778864
#[test]
779865
fn multiple_extensions() {
780866
let asset_server = setup(".");
781867
asset_server.add_loader(FakeMultipleDotLoader);
782868

783-
let t = asset_server.get_path_asset_loader("test.test.png");
784-
assert_eq!(t.unwrap().extensions()[0], "test.png");
869+
let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.test.png", true) else {
870+
panic!();
871+
};
872+
assert_eq!(t.extensions()[0], "test.png");
785873
}
786874

787875
fn create_dir_and_file(file: impl AsRef<Path>) -> tempfile::TempDir {

crates/bevy_asset/src/assets.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ pub trait AddAsset {
317317
fn add_asset_loader<T>(&mut self, loader: T) -> &mut Self
318318
where
319319
T: AssetLoader;
320+
321+
/// Preregisters a loader for the given extensions, that will block asset loads until a real loader
322+
/// is registered.
323+
fn preregister_asset_loader(&mut self, extensions: &[&str]) -> &mut Self;
320324
}
321325

322326
impl AddAsset for App {
@@ -404,6 +408,13 @@ impl AddAsset for App {
404408
self.world.resource_mut::<AssetServer>().add_loader(loader);
405409
self
406410
}
411+
412+
fn preregister_asset_loader(&mut self, extensions: &[&str]) -> &mut Self {
413+
self.world
414+
.resource_mut::<AssetServer>()
415+
.preregister_loader(extensions);
416+
self
417+
}
407418
}
408419

409420
/// Loads an internal asset from a project source file.

crates/bevy_gltf/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ impl Plugin for GltfPlugin {
4444
.add_asset::<Gltf>()
4545
.add_asset::<GltfNode>()
4646
.add_asset::<GltfPrimitive>()
47-
.add_asset::<GltfMesh>();
47+
.add_asset::<GltfMesh>()
48+
.preregister_asset_loader(&["gltf", "glb"]);
4849
}
4950

5051
fn finish(&self, app: &mut App) {

crates/bevy_render/src/texture/image_texture_loader.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct ImageTextureLoader {
1717
supported_compressed_formats: CompressedImageFormats,
1818
}
1919

20-
const FILE_EXTENSIONS: &[&str] = &[
20+
pub(crate) const IMG_FILE_EXTENSIONS: &[&str] = &[
2121
#[cfg(feature = "basis-universal")]
2222
"basis",
2323
#[cfg(feature = "bmp")]
@@ -73,7 +73,7 @@ impl AssetLoader for ImageTextureLoader {
7373
}
7474

7575
fn extensions(&self) -> &[&str] {
76-
FILE_EXTENSIONS
76+
IMG_FILE_EXTENSIONS
7777
}
7878
}
7979

crates/bevy_render/src/texture/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ impl Plugin for ImagePlugin {
9696
update_texture_cache_system.in_set(RenderSet::Cleanup),
9797
);
9898
}
99+
100+
#[cfg(any(
101+
feature = "png",
102+
feature = "dds",
103+
feature = "tga",
104+
feature = "jpeg",
105+
feature = "bmp",
106+
feature = "basis-universal",
107+
feature = "ktx2",
108+
))]
109+
app.preregister_asset_loader(IMG_FILE_EXTENSIONS);
99110
}
100111

101112
fn finish(&self, app: &mut App) {

0 commit comments

Comments
 (0)