Copy on Write AssetPaths (#9729)

# Objective

The `AssetServer` and `AssetProcessor` do a lot of `AssetPath` cloning
(across many threads). To store the path on the handle, to store paths
in dependency lists, to pass an owned path to the offloaded thread, to
pass a path to the LoadContext, etc , etc. Cloning multiple string
allocations multiple times like this will add up. It is worth optimizing
this.

Referenced in #9714 

## Solution

Added a new `CowArc<T>` type to `bevy_util`, which behaves a lot like
`Cow<T>`, but the Owned variant is an `Arc<T>`. Use this in place of
`Cow<str>` and `Cow<Path>` on `AssetPath`.

---

## Changelog

- `AssetPath` now internally uses `CowArc`, making clone operations much
cheaper
- `AssetPath` now serializes as `AssetPath("some_path.extension#Label")`
instead of as `AssetPath { path: "some_path.extension", label:
Some("Label) }`


## Migration Guide

```rust
// Old
AssetPath::new("logo.png", None);

// New
AssetPath::new("logo.png");

// Old
AssetPath::new("scene.gltf", Some("Mesh0");

// New
AssetPath::new("scene.gltf").with_label("Mesh0");
```

`AssetPath` now serializes as `AssetPath("some_path.extension#Label")`
instead of as `AssetPath { path: "some_path.extension", label:
Some("Label) }`

---------

Co-authored-by: Pascal Hertleif <killercup@gmail.com>
This commit is contained in:
Carter Anderson 2023-09-09 16:15:10 -07:00 committed by GitHub
parent 1980ac88f1
commit 17edf4f7c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 400 additions and 163 deletions

View file

@ -36,7 +36,7 @@ impl ProcessorGatedReader {
) -> Result<RwLockReadGuardArc<()>, AssetReaderError> {
let infos = self.processor_data.asset_infos.read().await;
let info = infos
.get(&AssetPath::new(path.to_owned(), None))
.get(&AssetPath::from_path(path.to_path_buf()))
.ok_or_else(|| AssetReaderError::NotFound(path.to_owned()))?;
Ok(info.file_transaction_lock.read_arc().await)
}

View file

@ -8,7 +8,7 @@ use crate::{
Asset, AssetLoadError, AssetServer, Assets, Handle, UntypedAssetId, UntypedHandle,
};
use bevy_ecs::world::World;
use bevy_utils::{BoxedFuture, HashMap, HashSet};
use bevy_utils::{BoxedFuture, CowArc, HashMap, HashSet};
use downcast_rs::{impl_downcast, Downcast};
use futures_lite::AsyncReadExt;
use ron::error::SpannedError;
@ -143,7 +143,7 @@ pub struct LoadedAsset<A: Asset> {
pub(crate) value: A,
pub(crate) dependencies: HashSet<UntypedAssetId>,
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
pub(crate) labeled_assets: HashMap<String, LabeledAsset>,
pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
pub(crate) meta: Option<Box<dyn AssetMetaDyn>>,
}
@ -175,7 +175,7 @@ pub struct ErasedLoadedAsset {
pub(crate) value: Box<dyn AssetContainer>,
pub(crate) dependencies: HashSet<UntypedAssetId>,
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
pub(crate) labeled_assets: HashMap<String, LabeledAsset>,
pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
pub(crate) meta: Option<Box<dyn AssetMetaDyn>>,
}
@ -214,13 +214,16 @@ impl ErasedLoadedAsset {
}
/// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
pub fn get_labeled(&self, label: &str) -> Option<&ErasedLoadedAsset> {
self.labeled_assets.get(label).map(|a| &a.asset)
pub fn get_labeled(
&self,
label: impl Into<CowArc<'static, str>>,
) -> Option<&ErasedLoadedAsset> {
self.labeled_assets.get(&label.into()).map(|a| &a.asset)
}
/// Iterate over all labels for "labeled assets" in the loaded asset
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| s.as_str())
self.labeled_assets.keys().map(|s| &**s)
}
}
@ -269,7 +272,7 @@ pub struct LoadContext<'a> {
dependencies: HashSet<UntypedAssetId>,
/// Direct dependencies used by this loader.
loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
labeled_assets: HashMap<String, LabeledAsset>,
labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
}
impl<'a> LoadContext<'a> {
@ -362,9 +365,10 @@ impl<'a> LoadContext<'a> {
/// See [`AssetPath`] for more on labeled assets.
pub fn add_loaded_labeled_asset<A: Asset>(
&mut self,
label: String,
label: impl Into<CowArc<'static, str>>,
loaded_asset: LoadedAsset<A>,
) -> Handle<A> {
let label = label.into();
let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
let labeled_path = self.asset_path.with_label(label.clone());
let handle = self
@ -383,9 +387,9 @@ impl<'a> LoadContext<'a> {
/// Returns `true` if an asset with the label `label` exists in this context.
///
/// See [`AssetPath`] for more on labeled assets.
pub fn has_labeled_asset(&self, label: &str) -> bool {
let path = self.asset_path.with_label(label);
self.asset_server.get_handle_untyped(path).is_some()
pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
let path = self.asset_path.with_label(label.into());
self.asset_server.get_handle_untyped(&path).is_some()
}
/// "Finishes" this context by populating the final [`Asset`] value (and the erased [`AssetMeta`] value, if it exists).
@ -406,7 +410,7 @@ impl<'a> LoadContext<'a> {
}
/// Gets the source asset path for this load context.
pub fn asset_path(&self) -> &AssetPath {
pub fn asset_path(&self) -> &AssetPath<'static> {
&self.asset_path
}
@ -432,7 +436,7 @@ impl<'a> LoadContext<'a> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
self.loader_dependencies
.insert(AssetPath::new(path.to_owned(), None), hash);
.insert(AssetPath::from_path(path.to_owned()), hash);
Ok(bytes)
}
@ -461,14 +465,12 @@ impl<'a> LoadContext<'a> {
path: impl Into<AssetPath<'b>>,
settings: impl Fn(&mut S) + Send + Sync + 'static,
) -> Handle<A> {
let path = path.into().to_owned();
let path = path.into();
let handle = if self.should_load_dependencies {
self.asset_server.load_with_settings(path.clone(), settings)
} else {
self.asset_server.get_or_create_path_handle(
path.clone(),
Some(loader_settings_meta_transform(settings)),
)
self.asset_server
.get_or_create_path_handle(path, Some(loader_settings_meta_transform(settings)))
};
self.dependencies.insert(handle.id().untyped());
handle
@ -477,8 +479,11 @@ impl<'a> LoadContext<'a> {
/// Returns a handle to an asset of type `A` with the label `label`. This [`LoadContext`] must produce an asset of the
/// given type and the given label or the dependencies of this asset will never be considered "fully loaded". However you
/// can call this method before _or_ after adding the labeled asset.
pub fn get_label_handle<A: Asset>(&mut self, label: &str) -> Handle<A> {
let path = self.asset_path.with_label(label).to_owned();
pub fn get_label_handle<'b, A: Asset>(
&mut self,
label: impl Into<CowArc<'b, str>>,
) -> Handle<A> {
let path = self.asset_path.with_label(label);
let handle = self.asset_server.get_or_create_path_handle::<A>(path, None);
self.dependencies.insert(handle.id().untyped());
handle
@ -498,36 +503,37 @@ impl<'a> LoadContext<'a> {
&mut self,
path: impl Into<AssetPath<'b>>,
) -> Result<ErasedLoadedAsset, LoadDirectError> {
let path = path.into();
let path = path.into().into_owned();
let to_error = |e: AssetLoadError| -> LoadDirectError {
LoadDirectError {
dependency: path.to_owned(),
dependency: path.clone(),
error: e,
}
};
let (meta, loader, mut reader) = self
.asset_server
.get_meta_loader_and_reader(&path)
.await
.map_err(to_error)?;
let loaded_asset = self
.asset_server
.load_with_meta_loader_and_reader(
&path,
meta,
&*loader,
&mut *reader,
false,
self.populate_hashes,
)
.await
.map_err(to_error)?;
let loaded_asset = {
let (meta, loader, mut reader) = self
.asset_server
.get_meta_loader_and_reader(&path)
.await
.map_err(to_error)?;
self.asset_server
.load_with_meta_loader_and_reader(
&path,
meta,
&*loader,
&mut *reader,
false,
self.populate_hashes,
)
.await
.map_err(to_error)?
};
let info = loaded_asset
.meta
.as_ref()
.and_then(|m| m.processed_info().as_ref());
let hash = info.map(|i| i.full_hash).unwrap_or(Default::default());
self.loader_dependencies.insert(path.to_owned(), hash);
self.loader_dependencies.insert(path, hash);
Ok(loaded_asset)
}
}

View file

@ -1,9 +1,10 @@
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde::{Deserialize, Serialize};
use bevy_utils::CowArc;
use serde::{de::Visitor, ser::SerializeTupleStruct, Deserialize, Serialize};
use std::{
borrow::Cow,
fmt::{Debug, Display},
hash::Hash,
ops::Deref,
path::{Path, PathBuf},
};
@ -33,11 +34,17 @@ use std::{
/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset.
/// let mesh: Handle<Mesh> = asset_server.load("my_scene.scn#PlayerMesh");
/// ```
#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, Reflect)]
///
/// [`AssetPath`] implements [`From`] for `&'static str`, `&'static Path`, and `&'a String`,
/// which allows us to optimize the static cases.
/// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and
/// clones internal owned [`AssetPaths`](AssetPath).
/// This also means that you should use [`AssetPath::new`] in cases where `&str` is the explicit type.
#[derive(Eq, PartialEq, Hash, Clone, Reflect)]
#[reflect(Debug, PartialEq, Hash, Serialize, Deserialize)]
pub struct AssetPath<'a> {
pub path: Cow<'a, Path>,
pub label: Option<Cow<'a, str>>,
path: CowArc<'a, Path>,
label: Option<CowArc<'a, str>>,
}
impl<'a> Debug for AssetPath<'a> {
@ -57,74 +64,95 @@ impl<'a> Display for AssetPath<'a> {
}
impl<'a> AssetPath<'a> {
/// Creates a new asset path using borrowed information.
#[inline]
pub fn new_ref(path: &'a Path, label: Option<&'a str>) -> AssetPath<'a> {
AssetPath {
path: Cow::Borrowed(path),
label: label.map(Cow::Borrowed),
/// Creates a new [`AssetPath`] from a string in the asset path format:
/// * An asset at the root: `"scene.gltf"`
/// * An asset nested in some folders: `"some/path/scene.gltf"`
/// * An asset with a "label": `"some/path/scene.gltf#Mesh0"`
///
/// Prefer [`From<'static str>`] for static strings, as this will prevent allocations
/// and reference counting for [`AssetPath::into_owned`].
pub fn new(asset_path: &'a str) -> AssetPath<'a> {
let (path, label) = Self::get_parts(asset_path);
Self {
path: CowArc::Borrowed(path),
label: label.map(CowArc::Borrowed),
}
}
/// Creates a new asset path.
fn get_parts(asset_path: &str) -> (&Path, Option<&str>) {
let mut parts = asset_path.splitn(2, '#');
let path = Path::new(parts.next().expect("Path must be set."));
let label = parts.next();
(path, label)
}
/// Creates a new [`AssetPath`] from a [`Path`].
#[inline]
pub fn new(path: PathBuf, label: Option<String>) -> AssetPath<'a> {
pub fn from_path(path: impl Into<CowArc<'a, Path>>) -> AssetPath<'a> {
AssetPath {
path: Cow::Owned(path),
label: label.map(Cow::Owned),
path: path.into(),
label: None,
}
}
/// Gets the "sub-asset label".
#[inline]
pub fn label(&self) -> Option<&str> {
self.label.as_ref().map(|label| label.as_ref())
self.label.as_deref()
}
/// Gets the path to the asset in the "virtual filesystem".
#[inline]
pub fn path(&self) -> &Path {
&self.path
self.path.deref()
}
/// Gets the path to the asset in the "virtual filesystem" without a label (if a label is currently set).
#[inline]
pub fn without_label(&self) -> AssetPath<'_> {
AssetPath::new_ref(&self.path, None)
Self {
path: self.path.clone(),
label: None,
}
}
/// Removes a "sub-asset label" from this [`AssetPath`] and returns it, if one was set.
/// Removes a "sub-asset label" from this [`AssetPath`], if one was set.
#[inline]
pub fn remove_label(&mut self) -> Option<Cow<'a, str>> {
pub fn remove_label(&mut self) {
self.label = None;
}
/// Takes the "sub-asset label" from this [`AssetPath`], if one was set.
#[inline]
pub fn take_label(&mut self) -> Option<CowArc<'a, str>> {
self.label.take()
}
/// Returns this asset path with the given label. This will replace the previous
/// label if it exists.
#[inline]
pub fn with_label(&self, label: impl Into<Cow<'a, str>>) -> AssetPath<'a> {
pub fn with_label(&self, label: impl Into<CowArc<'a, str>>) -> AssetPath<'a> {
AssetPath {
path: self.path.clone(),
label: Some(label.into()),
}
}
/// Converts the borrowed path data to owned.
#[inline]
pub fn to_owned(&self) -> AssetPath<'static> {
/// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]".
/// If it is already an "owned [`Arc`]", it will remain unchanged.
///
/// [`Arc`]: std::sync::Arc
pub fn into_owned(self) -> AssetPath<'static> {
AssetPath {
path: Cow::Owned(self.path.to_path_buf()),
label: self
.label
.as_ref()
.map(|value| Cow::Owned(value.to_string())),
path: self.path.into_owned(),
label: self.label.map(|l| l.into_owned()),
}
}
/// Returns the full extension (including multiple '.' values).
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
pub fn get_full_extension(&self) -> Option<String> {
let file_name = self.path.file_name()?.to_str()?;
let file_name = self.path().file_name()?.to_str()?;
let index = file_name.find('.')?;
let extension = file_name[index + 1..].to_lowercase();
Some(extension)
@ -141,47 +169,104 @@ impl<'a> AssetPath<'a> {
}
}
impl<'a> From<&'a str> for AssetPath<'a> {
fn from(asset_path: &'a str) -> Self {
let mut parts = asset_path.splitn(2, '#');
let path = Path::new(parts.next().expect("Path must be set."));
let label = parts.next();
impl From<&'static str> for AssetPath<'static> {
#[inline]
fn from(asset_path: &'static str) -> Self {
let (path, label) = Self::get_parts(asset_path);
AssetPath {
path: Cow::Borrowed(path),
label: label.map(Cow::Borrowed),
path: CowArc::Static(path),
label: label.map(CowArc::Static),
}
}
}
impl<'a> From<&'a String> for AssetPath<'a> {
#[inline]
fn from(asset_path: &'a String) -> Self {
asset_path.as_str().into()
AssetPath::new(asset_path.as_str())
}
}
impl<'a> From<&'a Path> for AssetPath<'a> {
fn from(path: &'a Path) -> Self {
AssetPath {
path: Cow::Borrowed(path),
impl From<String> for AssetPath<'static> {
#[inline]
fn from(asset_path: String) -> Self {
AssetPath::new(asset_path.as_str()).into_owned()
}
}
impl From<&'static Path> for AssetPath<'static> {
#[inline]
fn from(path: &'static Path) -> Self {
Self {
path: CowArc::Static(path),
label: None,
}
}
}
impl<'a> From<PathBuf> for AssetPath<'a> {
impl From<PathBuf> for AssetPath<'static> {
#[inline]
fn from(path: PathBuf) -> Self {
AssetPath {
path: Cow::Owned(path),
Self {
path: path.into(),
label: None,
}
}
}
impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
fn from(value: &'a AssetPath<'b>) -> Self {
value.clone()
}
}
impl<'a> From<AssetPath<'a>> for PathBuf {
fn from(path: AssetPath<'a>) -> Self {
match path.path {
Cow::Borrowed(borrowed) => borrowed.to_owned(),
Cow::Owned(owned) => owned,
}
fn from(value: AssetPath<'a>) -> Self {
value.path().to_path_buf()
}
}
impl<'a> Serialize for AssetPath<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_tuple_struct("AssetPath", 1)?;
let string = self.to_string();
state.serialize_field(&string)?;
state.end()
}
}
impl<'de> Deserialize<'de> for AssetPath<'static> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_tuple_struct("AssetPath", 1, AssetPathVisitor)
}
}
struct AssetPathVisitor;
impl<'de> Visitor<'de> for AssetPathVisitor {
type Value = AssetPath<'static>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string AssetPath")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(AssetPath::new(v).into_owned())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(AssetPath::from(v))
}
}

View file

@ -364,7 +364,7 @@ impl AssetProcessor {
/// asset have finished, thanks to the `file_transaction_lock`.
async fn handle_removed_asset(&self, path: PathBuf) {
debug!("Removing processed {:?} because source was removed", path);
let asset_path = AssetPath::new(path, None);
let asset_path = AssetPath::from_path(path);
let mut infos = self.data.asset_infos.write().await;
if let Some(info) = infos.get(&asset_path) {
// we must wait for uncontested write access to the asset source to ensure existing readers / writers
@ -380,19 +380,19 @@ impl AssetProcessor {
/// This will cause direct path dependencies to break.
async fn handle_renamed_asset(&self, old: PathBuf, new: PathBuf) {
let mut infos = self.data.asset_infos.write().await;
let old_asset_path = AssetPath::new(old, None);
let old_asset_path = AssetPath::from_path(old);
if let Some(info) = infos.get(&old_asset_path) {
// we must wait for uncontested write access to the asset source to ensure existing readers / writers
// can finish their operations
let _write_lock = info.file_transaction_lock.write();
let old = &old_asset_path.path;
let old = old_asset_path.path();
self.destination_writer().rename(old, &new).await.unwrap();
self.destination_writer()
.rename_meta(old, &new)
.await
.unwrap();
}
let new_asset_path = AssetPath::new(new.clone(), None);
let new_asset_path = AssetPath::from_path(new);
infos.rename(&old_asset_path, &new_asset_path).await;
}
@ -531,11 +531,11 @@ impl AssetProcessor {
.map_err(InitializeError::FailedToReadDestinationPaths)?;
for path in &source_paths {
asset_infos.get_or_insert(AssetPath::new(path.to_owned(), None));
asset_infos.get_or_insert(AssetPath::from_path(path.clone()));
}
for path in &destination_paths {
let asset_path = AssetPath::new(path.to_owned(), None);
let asset_path = AssetPath::from_path(path.clone());
let mut dependencies = Vec::new();
if let Some(info) = asset_infos.get_mut(&asset_path) {
match self.destination_reader().read_meta_bytes(path).await {
@ -551,7 +551,7 @@ impl AssetProcessor {
for process_dependency_info in
&processed_info.process_dependencies
{
dependencies.push(process_dependency_info.path.to_owned());
dependencies.push(process_dependency_info.path.clone());
}
}
info.processed_info = minimal.processed_info;
@ -573,7 +573,7 @@ impl AssetProcessor {
}
for dependency in dependencies {
asset_infos.add_dependant(&dependency, asset_path.to_owned());
asset_infos.add_dependant(&dependency, asset_path.clone());
}
}
@ -627,7 +627,7 @@ impl AssetProcessor {
async fn process_asset(&self, path: &Path) {
let result = self.process_asset_internal(path).await;
let mut infos = self.data.asset_infos.write().await;
let asset_path = AssetPath::new(path.to_owned(), None);
let asset_path = AssetPath::from_path(path.to_owned());
infos.finish_processing(asset_path, result).await;
}
@ -635,7 +635,7 @@ impl AssetProcessor {
if path.extension().is_none() {
return Err(ProcessError::ExtensionRequired);
}
let asset_path = AssetPath::new(path.to_owned(), None);
let asset_path = AssetPath::from_path(path.to_path_buf());
// TODO: check if already processing to protect against duplicate hot-reload events
debug!("Processing {:?}", path);
let server = &self.server;
@ -912,7 +912,7 @@ impl AssetProcessorData {
self.wait_until_initialized().await;
let mut receiver = {
let infos = self.asset_infos.write().await;
let info = infos.get(&AssetPath::new(path.to_owned(), None));
let info = infos.get(&AssetPath::from_path(path.to_path_buf()));
match info {
Some(info) => match info.status {
Some(result) => return result,
@ -1067,7 +1067,7 @@ impl ProcessorAssetInfos {
} else {
let dependants = self
.non_existent_dependants
.entry(asset_path.to_owned())
.entry(asset_path.clone())
.or_default();
dependants.insert(dependant);
}

View file

@ -1,6 +1,6 @@
use crate::{io::Writer, meta::Settings, Asset, ErasedLoadedAsset};
use crate::{AssetLoader, LabeledAsset};
use bevy_utils::{BoxedFuture, HashMap};
use bevy_utils::{BoxedFuture, CowArc, HashMap};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
@ -60,7 +60,7 @@ impl<S: AssetSaver> ErasedAssetSaver for S {
/// An [`Asset`] (and any labeled "sub assets") intended to be saved.
pub struct SavedAsset<'a, A: Asset> {
value: &'a A,
labeled_assets: &'a HashMap<String, LabeledAsset>,
labeled_assets: &'a HashMap<CowArc<'static, str>, LabeledAsset>,
}
impl<'a, A: Asset> Deref for SavedAsset<'a, A> {
@ -88,8 +88,11 @@ impl<'a, A: Asset> SavedAsset<'a, A> {
}
/// Returns the labeled asset, if it exists and matches this type.
pub fn get_labeled<B: Asset>(&self, label: &str) -> Option<SavedAsset<B>> {
let labeled = self.labeled_assets.get(label)?;
pub fn get_labeled<B: Asset>(
&self,
label: impl Into<CowArc<'static, str>>,
) -> Option<SavedAsset<B>> {
let labeled = self.labeled_assets.get(&label.into())?;
let value = labeled.asset.value.downcast_ref::<B>()?;
Some(SavedAsset {
value,
@ -98,13 +101,16 @@ impl<'a, A: Asset> SavedAsset<'a, A> {
}
/// Returns the type-erased labeled asset, if it exists and matches this type.
pub fn get_erased_labeled(&self, label: &str) -> Option<&ErasedLoadedAsset> {
let labeled = self.labeled_assets.get(label)?;
pub fn get_erased_labeled(
&self,
label: impl Into<CowArc<'static, str>>,
) -> Option<&ErasedLoadedAsset> {
let labeled = self.labeled_assets.get(&label.into())?;
Some(&labeled.asset)
}
/// Iterate over all labels for "labeled assets" in the loaded asset
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
self.labeled_assets.keys().map(|s| s.as_str())
self.labeled_assets.keys().map(|s| &**s)
}
}

View file

@ -257,8 +257,9 @@ impl AssetInfos {
}
/// Returns `true` if this path has
pub(crate) fn is_path_alive(&self, path: &AssetPath) -> bool {
if let Some(id) = self.path_to_id.get(path) {
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;
}

View file

@ -201,8 +201,9 @@ impl AssetServer {
/// Retrieves the default [`AssetLoader`] for the given path, if one can be found.
pub async fn get_path_asset_loader<'a>(
&self,
path: &AssetPath<'a>,
path: impl Into<AssetPath<'a>>,
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> {
let path = path.into();
let full_extension =
path.get_full_extension()
.ok_or(MissingAssetLoaderForExtensionError {
@ -252,27 +253,22 @@ impl AssetServer {
path: impl Into<AssetPath<'a>>,
meta_transform: Option<MetaTransform>,
) -> Handle<A> {
let path: AssetPath = path.into();
let mut path = path.into().into_owned();
let (handle, should_load) = self.data.infos.write().get_or_create_path_handle::<A>(
path.to_owned(),
path.clone(),
HandleLoadingMode::Request,
meta_transform,
);
if should_load {
let mut owned_handle = Some(handle.clone().untyped());
let mut owned_path = path.to_owned();
let server = self.clone();
IoTaskPool::get()
.spawn(async move {
if owned_path.label().is_some() {
owned_path.remove_label();
if path.take_label().is_some() {
owned_handle = None;
}
if let Err(err) = server
.load_internal(owned_handle, owned_path, false, None)
.await
{
if let Err(err) = server.load_internal(owned_handle, path, false, None).await {
error!("{}", err);
}
})
@ -287,19 +283,21 @@ impl AssetServer {
&self,
path: impl Into<AssetPath<'a>>,
) -> Result<UntypedHandle, AssetLoadError> {
self.load_internal(None, path.into(), false, None).await
let path: AssetPath = path.into();
self.load_internal(None, path, false, None).await
}
async fn load_internal<'a>(
&self,
input_handle: Option<UntypedHandle>,
mut path: AssetPath<'a>,
path: AssetPath<'a>,
force: bool,
meta_transform: Option<MetaTransform>,
) -> Result<UntypedHandle, AssetLoadError> {
let owned_path = path.to_owned();
let mut path = path.into_owned();
let path_clone = path.clone();
let (mut meta, loader, mut reader) = self
.get_meta_loader_and_reader(&owned_path)
.get_meta_loader_and_reader(&path_clone)
.await
.map_err(|e| {
// if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if
@ -316,7 +314,7 @@ impl AssetServer {
Some(handle) => {
if !has_label && handle.type_id() != loader.asset_type_id() {
return Err(AssetLoadError::RequestedHandleTypeMismatch {
path: path.to_owned(),
path: path.into_owned(),
requested: handle.type_id(),
actual_asset_name: loader.asset_type_name(),
loader_name: loader.type_name(),
@ -328,7 +326,7 @@ impl AssetServer {
None => {
let mut infos = self.data.infos.write();
infos.get_or_create_path_handle_untyped(
path.to_owned(),
path.clone(),
loader.asset_type_id(),
loader.asset_type_name(),
HandleLoadingMode::Request,
@ -346,7 +344,7 @@ impl AssetServer {
// We need to get the actual asset id
let mut infos = self.data.infos.write();
let (actual_handle, _) = infos.get_or_create_path_handle_untyped(
path.to_owned(),
path.clone(),
loader.asset_type_id(),
loader.asset_type_name(),
// ignore current load state ... we kicked off this sub asset load because it needed to be loaded but
@ -390,13 +388,12 @@ impl AssetServer {
/// Kicks off a reload of the asset stored at the given path. This will only reload the asset if it currently loaded.
pub fn reload<'a>(&self, path: impl Into<AssetPath<'a>>) {
let server = self.clone();
let path = path.into();
let owned_path = path.to_owned();
let path = path.into().into_owned();
IoTaskPool::get()
.spawn(async move {
if server.data.infos.read().is_path_alive(&owned_path) {
info!("Reloading {owned_path} because it has changed");
if let Err(err) = server.load_internal(None, owned_path, true, None).await {
if server.data.infos.read().is_path_alive(&path) {
info!("Reloading {path} because it has changed");
if let Err(err) = server.load_internal(None, path, true, None).await {
error!("{}", err);
}
}
@ -423,13 +420,13 @@ impl AssetServer {
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
pub(crate) fn load_asset_untyped(
&self,
path: Option<&AssetPath<'static>>,
path: Option<AssetPath<'static>>,
asset: impl Into<ErasedLoadedAsset>,
) -> UntypedHandle {
let loaded_asset = asset.into();
let handle = if let Some(path) = path {
let (handle, _) = self.data.infos.write().get_or_create_path_handle_untyped(
path.clone(),
path,
loaded_asset.asset_type_id(),
loaded_asset.asset_type_name(),
HandleLoadingMode::NotLoading,
@ -474,7 +471,7 @@ impl AssetServer {
load_folder(&child_path, server, handles).await?;
} else {
let path = child_path.to_str().expect("Path should be a valid string.");
match server.load_untyped_async(path).await {
match server.load_untyped_async(AssetPath::new(path)).await {
Ok(handle) => handles.push(handle),
// skip assets that cannot be loaded
Err(
@ -583,10 +580,10 @@ impl AssetServer {
}
/// Returns the path for the given `id`, if it has one.
pub fn get_path(&self, id: impl Into<UntypedAssetId>) -> Option<AssetPath<'static>> {
pub fn get_path(&self, id: impl Into<UntypedAssetId>) -> Option<AssetPath> {
let infos = self.data.infos.read();
let info = infos.get(id.into())?;
Some(info.path.as_ref()?.to_owned())
Some(info.path.as_ref()?.clone())
}
/// Pre-register a loader that will later be added.
@ -618,14 +615,18 @@ impl AssetServer {
}
/// Retrieve a handle for the given path. This will create a handle (and [`AssetInfo`]) if it does not exist
pub(crate) fn get_or_create_path_handle<A: Asset>(
pub(crate) fn get_or_create_path_handle<'a, A: Asset>(
&self,
path: AssetPath<'static>,
path: impl Into<AssetPath<'a>>,
meta_transform: Option<MetaTransform>,
) -> Handle<A> {
let mut infos = self.data.infos.write();
infos
.get_or_create_path_handle::<A>(path, HandleLoadingMode::NotLoading, meta_transform)
.get_or_create_path_handle::<A>(
path.into().into_owned(),
HandleLoadingMode::NotLoading,
meta_transform,
)
.0
}
@ -655,19 +656,19 @@ impl AssetServer {
AssetActionMinimal::Load { loader } => loader,
AssetActionMinimal::Process { .. } => {
return Err(AssetLoadError::CannotLoadProcessedAsset {
path: asset_path.to_owned(),
path: asset_path.clone().into_owned(),
})
}
AssetActionMinimal::Ignore => {
return Err(AssetLoadError::CannotLoadIgnoredAsset {
path: asset_path.to_owned(),
path: asset_path.clone().into_owned(),
})
}
};
let loader = self.get_asset_loader_with_type_name(&loader_name).await?;
let meta = loader.deserialize_meta(&meta_bytes).map_err(|e| {
AssetLoadError::AssetLoaderError {
path: asset_path.to_owned(),
path: asset_path.clone().into_owned(),
loader: loader.type_name(),
error: AssetLoaderError::DeserializeMeta(e),
}
@ -693,16 +694,14 @@ impl AssetServer {
load_dependencies: bool,
populate_hashes: bool,
) -> Result<ErasedLoadedAsset, AssetLoadError> {
let load_context = LoadContext::new(
self,
asset_path.to_owned(),
load_dependencies,
populate_hashes,
);
// TODO: experiment with this
let asset_path = asset_path.clone().into_owned();
let load_context =
LoadContext::new(self, asset_path.clone(), load_dependencies, populate_hashes);
loader.load(reader, meta, load_context).await.map_err(|e| {
AssetLoadError::AssetLoaderError {
loader: loader.type_name(),
path: asset_path.to_owned(),
path: asset_path,
error: e,
}
})
@ -753,12 +752,9 @@ pub fn handle_internal_asset_events(world: &mut World) {
// TODO: if the asset was processed and the processed file was changed, the first modified event
// should be skipped?
AssetSourceEvent::ModifiedAsset(path) | AssetSourceEvent::ModifiedMeta(path) => {
queue_ancestors(
&AssetPath::new_ref(&path, None),
&infos,
&mut paths_to_reload,
);
paths_to_reload.insert(path.into());
let path = AssetPath::from_path(path);
queue_ancestors(&path, &infos, &mut paths_to_reload);
paths_to_reload.insert(path);
}
_ => {}
}

View file

@ -0,0 +1,144 @@
use std::{
fmt::{Debug, Display},
hash::Hash,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
};
/// Much like a [`Cow`](std::borrow::Cow), but owned values are Arc-ed to make clones cheap. This should be used for values that
/// are cloned for use across threads and change rarely (if ever).
///
/// This also makes an opinionated tradeoff by adding a [`CowArc::Static`] and implementing [`From<&'static T>`] instead of
/// [`From<'a T>`]. This preserves the static context and prevents conversion to [`CowArc::Owned`] in cases where a reference
/// is known to be static. This is an optimization that prevents allocations and atomic ref-counting.
///
/// This means that static references should prefer [`From::from`] or [`CowArc::Static`] and non-static references must
/// use [`CowArc::Borrowed`].
pub enum CowArc<'a, T: ?Sized + 'static> {
/// A borrowed value
Borrowed(&'a T),
/// A static value reference. This exists to avoid conversion to [`CowArc::Owned`] in cases where a reference is
/// known to be static. This is an optimization that prevents allocations and atomic ref-counting.
Static(&'static T),
/// An owned [`Arc`]-ed value
Owned(Arc<T>),
}
impl<'a, T: ?Sized> Deref for CowArc<'a, T> {
type Target = T;
#[inline]
fn deref(&self) -> &Self::Target {
match self {
CowArc::Borrowed(v) | CowArc::Static(v) => v,
CowArc::Owned(v) => v,
}
}
}
impl<'a, T: ?Sized> CowArc<'a, T>
where
&'a T: Into<Arc<T>>,
{
/// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]".
/// If it is already an "owned [`Arc`]", it will remain unchanged.
#[inline]
pub fn into_owned(self) -> CowArc<'static, T> {
match self {
CowArc::Borrowed(value) => CowArc::Owned(value.into()),
CowArc::Static(value) => CowArc::Static(value),
CowArc::Owned(value) => CowArc::Owned(value),
}
}
}
impl<'a, T: ?Sized> Clone for CowArc<'a, T> {
#[inline]
fn clone(&self) -> Self {
match self {
Self::Borrowed(value) => Self::Borrowed(value),
Self::Static(value) => Self::Static(value),
Self::Owned(value) => Self::Owned(value.clone()),
}
}
}
impl<'a, T: PartialEq + ?Sized> PartialEq for CowArc<'a, T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.deref().eq(other.deref())
}
}
impl<'a, T: PartialEq + ?Sized> Eq for CowArc<'a, T> {}
impl<'a, T: Hash + ?Sized> Hash for CowArc<'a, T> {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.deref().hash(state);
}
}
impl<'a, T: Debug + ?Sized> Debug for CowArc<'a, T> {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self.deref(), f)
}
}
impl<'a, T: Display + ?Sized> Display for CowArc<'a, T> {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(self.deref(), f)
}
}
impl<'a, T: PartialOrd + ?Sized> PartialOrd for CowArc<'a, T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.deref().partial_cmp(other.deref())
}
}
impl<'a, T: Ord + ?Sized> Ord for CowArc<'a, T> {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.deref().cmp(other.deref())
}
}
impl From<PathBuf> for CowArc<'static, Path> {
#[inline]
fn from(value: PathBuf) -> Self {
CowArc::Owned(value.into())
}
}
impl From<&'static str> for CowArc<'static, Path> {
#[inline]
fn from(value: &'static str) -> Self {
CowArc::Static(Path::new(value))
}
}
impl From<String> for CowArc<'static, str> {
#[inline]
fn from(value: String) -> Self {
CowArc::Owned(value.into())
}
}
impl<'a> From<&'a String> for CowArc<'a, str> {
#[inline]
fn from(value: &'a String) -> Self {
CowArc::Borrowed(value)
}
}
impl<T: ?Sized> From<&'static T> for CowArc<'static, T> {
#[inline]
fn from(value: &'static T) -> Self {
CowArc::Static(value)
}
}

View file

@ -18,11 +18,13 @@ pub use short_names::get_short_name;
pub mod synccell;
pub mod syncunsafecell;
mod cow_arc;
mod default;
mod float_ord;
pub use ahash::{AHasher, RandomState};
pub use bevy_utils_proc_macros::*;
pub use cow_arc::*;
pub use default::default;
pub use float_ord::*;
pub use hashbrown;

View file

@ -72,10 +72,7 @@ fn setup(
// load 16 textures
let textures: Vec<_> = TILE_ID
.iter()
.map(|id| {
let path = format!("textures/rpg/tiles/generic-rpg-tile{id:0>2}.png");
asset_server.load(&path)
})
.map(|id| asset_server.load(format!("textures/rpg/tiles/generic-rpg-tile{id:0>2}.png")))
.collect();
// a cube with multiple textures

View file

@ -77,7 +77,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
info!("Loading {}", scene_path);
let (file_path, scene_index) = parse_scene(scene_path);
commands.insert_resource(SceneHandle::new(asset_server.load(&file_path), scene_index));
commands.insert_resource(SceneHandle::new(asset_server.load(file_path), scene_index));
}
fn setup_scene_after_load(