Skip to content

Commit

Permalink
Added Support for Extension-less Assets (#10153)
Browse files Browse the repository at this point in the history
# Objective

- Addresses **Support processing and loading files without extensions**
from #9714
- Addresses **More runtime loading configuration** from #9714
- Fixes #367
- Fixes #10703

## Solution

`AssetServer::load::<A>` and `AssetServer::load_with_settings::<A>` can
now use the `Asset` type parameter `A` to select a registered
`AssetLoader` without inspecting the provided `AssetPath`. This change
cascades onto `LoadContext::load` and `LoadContext::load_with_settings`.
This allows the loading of assets which have incorrect or ambiguous file
extensions.

```rust
// Allow the type to be inferred by context
let handle = asset_server.load("data/asset_no_extension");

// Hint the type through the handle
let handle: Handle<CustomAsset> = asset_server.load("data/asset_no_extension");

// Explicit through turbofish
let handle = asset_server.load::<CustomAsset>("data/asset_no_extension");
```

Since a single `AssetPath` no longer maps 1:1 with an `Asset`, I've also
modified how assets are loaded to permit multiple asset types to be
loaded from a single path. This allows for two different `AssetLoaders`
(which return different types of assets) to both load a single path (if
requested).

```rust
// Uses GltfLoader
let model = asset_server.load::<Gltf>("cube.gltf");

// Hypothetical Blob loader for data transmission (for example)
let blob = asset_server.load::<Blob>("cube.gltf");
```

As these changes are reflected in the `LoadContext` as well as the
`AssetServer`, custom `AssetLoaders` can also take advantage of this
behaviour to create more complex assets.

---

## Change Log

- Updated `custom_asset` example to demonstrate extension-less assets.
- Added `AssetServer::get_handles_untyped` and Added
`AssetServer::get_path_ids`

## Notes

As a part of that refactor, I chose to store `AssetLoader`s (within
`AssetLoaders`) using a `HashMap<TypeId, ...>` instead of a `Vec<...>`.
My reasoning for this was I needed to add a relationship between `Asset`
`TypeId`s and the `AssetLoader`, so instead of having a `Vec` and a
`HashMap`, I combined the two, removing the `usize` index from the
adjacent maps.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
  • Loading branch information
bushrat011899 and alice-i-cecile authored Jan 31, 2024
1 parent 16d28cc commit afa7b5c
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 57 deletions.
3 changes: 3 additions & 0 deletions assets/data/asset_no_extension
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CustomAsset (
value: 13
)
5 changes: 1 addition & 4 deletions crates/bevy_asset/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,7 @@ impl AsyncRead for VecReader {
/// Appends `.meta` to the given path.
pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
let mut meta_path = path.to_path_buf();
let mut extension = path
.extension()
.unwrap_or_else(|| panic!("missing extension for asset path {path:?}"))
.to_os_string();
let mut extension = path.extension().unwrap_or_default().to_os_string();
extension.push(".meta");
meta_path.set_extension(extension);
meta_path
Expand Down
10 changes: 6 additions & 4 deletions crates/bevy_asset/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ pub trait AssetLoader: Send + Sync + 'static {
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>>;

/// Returns a list of extensions supported by this asset loader, without the preceding dot.
fn extensions(&self) -> &[&str];
/// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
fn extensions(&self) -> &[&str] {
&[]
}
}

/// Provides type-erased access to an [`AssetLoader`].
Expand Down Expand Up @@ -396,7 +398,7 @@ impl<'a> LoadContext<'a> {
/// See [`AssetPath`] for more on labeled assets.
pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
let path = self.asset_path.clone().with_label(label.into());
self.asset_server.get_handle_untyped(&path).is_some()
!self.asset_server.get_handles_untyped(&path).is_empty()
}

/// "Finishes" this context by populating the final [`Asset`] value (and the erased [`AssetMeta`] value, if it exists).
Expand Down Expand Up @@ -546,7 +548,7 @@ impl<'a> LoadContext<'a> {
let loaded_asset = {
let (meta, loader, mut reader) = self
.asset_server
.get_meta_loader_and_reader(&path)
.get_meta_loader_and_reader(&path, None)
.await
.map_err(to_error)?;
self.asset_server
Expand Down
98 changes: 78 additions & 20 deletions crates/bevy_asset/src/server/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl AssetInfo {

#[derive(Default)]
pub(crate) struct AssetInfos {
path_to_id: HashMap<AssetPath<'static>, UntypedAssetId>,
path_to_id: HashMap<AssetPath<'static>, HashMap<TypeId, UntypedAssetId>>,
infos: HashMap<UntypedAssetId, AssetInfo>,
/// If set to `true`, this informs [`AssetInfos`] to track data relevant to watching for changes (such as `load_dependants`)
/// This should only be set at startup.
Expand Down Expand Up @@ -191,7 +191,20 @@ impl AssetInfos {
loading_mode: HandleLoadingMode,
meta_transform: Option<MetaTransform>,
) -> Result<(UntypedHandle, bool), GetOrCreateHandleInternalError> {
match self.path_to_id.entry(path.clone()) {
let handles = self.path_to_id.entry(path.clone()).or_default();

let type_id = type_id
.or_else(|| {
// If a TypeId is not provided, we may be able to infer it if only a single entry exists
if handles.len() == 1 {
Some(*handles.keys().next().unwrap())
} else {
None
}
})
.ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?;

match handles.entry(type_id) {
Entry::Occupied(entry) => {
let id = *entry.get();
// if there is a path_to_id entry, info always exists
Expand Down Expand Up @@ -222,9 +235,6 @@ impl AssetInfos {
// We must create a new strong handle for the existing id and ensure that the drop of the old
// strong handle doesn't remove the asset from the Assets collection
info.handle_drops_to_skip += 1;
let type_id = type_id.ok_or(
GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified,
)?;
let provider = self
.handle_providers
.get(&type_id)
Expand All @@ -241,8 +251,6 @@ impl AssetInfos {
HandleLoadingMode::NotLoading => false,
HandleLoadingMode::Request | HandleLoadingMode::Force => true,
};
let type_id = type_id
.ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?;
let handle = Self::create_handle_internal(
&mut self.infos,
&self.handle_providers,
Expand Down Expand Up @@ -271,13 +279,52 @@ impl AssetInfos {
self.infos.get_mut(&id)
}

pub(crate) fn get_path_id(&self, path: &AssetPath) -> Option<UntypedAssetId> {
self.path_to_id.get(path).copied()
pub(crate) fn get_path_and_type_id_handle(
&self,
path: &AssetPath,
type_id: TypeId,
) -> Option<UntypedHandle> {
let id = self.path_to_id.get(path)?.get(&type_id)?;
self.get_id_handle(*id)
}

pub(crate) fn get_path_ids<'a>(
&'a self,
path: &'a AssetPath<'a>,
) -> impl Iterator<Item = UntypedAssetId> + 'a {
/// Concrete type to allow returning an `impl Iterator` even if `self.path_to_id.get(&path)` is `None`
enum HandlesByPathIterator<T> {
None,
Some(T),
}

impl<T> Iterator for HandlesByPathIterator<T>
where
T: Iterator<Item = UntypedAssetId>,
{
type Item = UntypedAssetId;

fn next(&mut self) -> Option<Self::Item> {
match self {
HandlesByPathIterator::None => None,
HandlesByPathIterator::Some(iter) => iter.next(),
}
}
}

if let Some(type_id_to_id) = self.path_to_id.get(path) {
HandlesByPathIterator::Some(type_id_to_id.values().copied())
} else {
HandlesByPathIterator::None
}
}

pub(crate) fn get_path_handle(&self, path: &AssetPath) -> Option<UntypedHandle> {
let id = *self.path_to_id.get(path)?;
self.get_id_handle(id)
pub(crate) fn get_path_handles<'a>(
&'a self,
path: &'a AssetPath<'a>,
) -> impl Iterator<Item = UntypedHandle> + 'a {
self.get_path_ids(path)
.filter_map(|id| self.get_id_handle(id))
}

pub(crate) fn get_id_handle(&self, id: UntypedAssetId) -> Option<UntypedHandle> {
Expand All @@ -289,12 +336,13 @@ impl AssetInfos {
/// Returns `true` if the asset this path points to is still alive
pub(crate) fn is_path_alive<'a>(&self, path: impl Into<AssetPath<'a>>) -> bool {
let path = path.into();
if let Some(id) = self.path_to_id.get(&path) {
if let Some(info) = self.infos.get(id) {
return info.weak_handle.strong_count() > 0;
}
}
false

let result = self
.get_path_ids(&path)
.filter_map(|id| self.infos.get(&id))
.any(|info| info.weak_handle.strong_count() > 0);

result
}

/// Returns `true` if the asset at this path should be reloaded
Expand Down Expand Up @@ -592,7 +640,7 @@ impl AssetInfos {

fn process_handle_drop_internal(
infos: &mut HashMap<UntypedAssetId, AssetInfo>,
path_to_id: &mut HashMap<AssetPath<'static>, UntypedAssetId>,
path_to_id: &mut HashMap<AssetPath<'static>, HashMap<TypeId, UntypedAssetId>>,
loader_dependants: &mut HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
living_labeled_assets: &mut HashMap<AssetPath<'static>, HashSet<String>>,
watching_for_changes: bool,
Expand All @@ -609,6 +657,8 @@ impl AssetInfos {
return false;
}

let type_id = entry.key().type_id();

let info = entry.remove();
let Some(path) = &info.path else {
return true;
Expand All @@ -622,7 +672,15 @@ impl AssetInfos {
living_labeled_assets,
);
}
path_to_id.remove(path);

if let Some(map) = path_to_id.get_mut(path) {
map.remove(&type_id);

if map.is_empty() {
path_to_id.remove(path);
}
};

true
}

Expand Down
Loading

0 comments on commit afa7b5c

Please sign in to comment.