bevy/crates/bevy_asset/src/assets.rs
Joona Aalto a795de30b4
Use impl Into<A> for Assets::add (#10878)
# Motivation

When spawning entities into a scene, it is very common to create assets
like meshes and materials and to add them via asset handles. A common
setup might look like this:

```rust
fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn(PbrBundle {
        mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
        material: materials.add(StandardMaterial::from(Color::RED)),
        ..default()
    });
}
```

Let's take a closer look at the part that adds the assets using `add`.

```rust
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(StandardMaterial::from(Color::RED)),
```

Here, "mesh" and "material" are both repeated three times. It's very
explicit, but I find it to be a bit verbose. In addition to being more
code to read and write, the extra characters can sometimes also lead to
the code being formatted to span multiple lines even though the core
task, adding e.g. a primitive mesh, is extremely simple.

A way to address this is by using `.into()`:

```rust
mesh: meshes.add(shape::Cube { size: 1.0 }.into()),
material: materials.add(Color::RED.into()),
```

This is fine, but from the names and the type of `meshes`, we already
know what the type should be. It's very clear that `Cube` should be
turned into a `Mesh` because of the context it's used in. `.into()` is
just seven characters, but it's so common that it quickly adds up and
gets annoying.

It would be nice if you could skip all of the conversion and let Bevy
handle it for you:

```rust
mesh: meshes.add(shape::Cube { size: 1.0 }),
material: materials.add(Color::RED),
```

# Objective

Make adding assets more ergonomic by making `Assets::add` take an `impl
Into<A>` instead of `A`.

## Solution

`Assets::add` now takes an `impl Into<A>` instead of `A`, so e.g. this
works:

```rust
    commands.spawn(PbrBundle {
        mesh: meshes.add(shape::Cube { size: 1.0 }),
        material: materials.add(Color::RED),
        ..default()
    });
```

I also changed all examples to use this API, which increases consistency
as well because `Mesh::from` and `into` were being used arbitrarily even
in the same file. This also gets rid of some lines of code because
formatting is nicer.

---

## Changelog

- `Assets::add` now takes an `impl Into<A>` instead of `A`
- Examples don't use `T::from(K)` or `K.into()` when adding assets

## Migration Guide

Some `into` calls that worked previously might now be broken because of
the new trait bounds. You need to either remove `into` or perform the
conversion explicitly with `from`:

```rust
// Doesn't compile
let mesh_handle = meshes.add(shape::Cube { size: 1.0 }.into()),

// These compile
let mesh_handle = meshes.add(shape::Cube { size: 1.0 }),
let mesh_handle = meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
```

## Concerns

I believe the primary concerns might be:

1. Is this too implicit?
2. Does this increase codegen bloat?

Previously, the two APIs were using `into` or `from`, and now it's
"nothing" or `from`. You could argue that `into` is slightly more
explicit than "nothing" in cases like the earlier examples where a
`Color` gets converted to e.g. a `StandardMaterial`, but I personally
don't think `into` adds much value even in this case, and you could
still see the actual type from the asset type.

As for codegen bloat, I doubt it adds that much, but I'm not very
familiar with the details of codegen. I personally value the user-facing
code reduction and ergonomics improvements that these changes would
provide, but it might be worth checking the other effects in more
detail.

Another slight concern is migration pain; apps might have a ton of
`into` calls that would need to be removed, and it did take me a while
to do so for Bevy itself (maybe around 20-40 minutes). However, I think
the fact that there *are* so many `into` calls just highlights that the
API could be made nicer, and I'd gladly migrate my own projects for it.
2024-01-08 22:14:43 +00:00

583 lines
21 KiB
Rust

use crate::{self as bevy_asset};
use crate::{
Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, LoadState, UntypedHandle,
};
use bevy_ecs::{
prelude::EventWriter,
system::{Res, ResMut, Resource},
};
use bevy_reflect::{Reflect, TypePath, Uuid};
use bevy_utils::HashMap;
use crossbeam_channel::{Receiver, Sender};
use serde::{Deserialize, Serialize};
use std::{
any::TypeId,
iter::Enumerate,
marker::PhantomData,
sync::{atomic::AtomicU32, Arc},
};
use thiserror::Error;
/// A generational runtime-only identifier for a specific [`Asset`] stored in [`Assets`]. This is optimized for efficient runtime
/// usage and is not suitable for identifying assets across app runs.
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Reflect, Serialize, Deserialize,
)]
pub struct AssetIndex {
pub(crate) generation: u32,
pub(crate) index: u32,
}
/// Allocates generational [`AssetIndex`] values and facilitates their reuse.
pub(crate) struct AssetIndexAllocator {
/// A monotonically increasing index.
next_index: AtomicU32,
recycled_queue_sender: Sender<AssetIndex>,
/// This receives every recycled AssetIndex. It serves as a buffer/queue to store indices ready for reuse.
recycled_queue_receiver: Receiver<AssetIndex>,
recycled_sender: Sender<AssetIndex>,
recycled_receiver: Receiver<AssetIndex>,
}
impl Default for AssetIndexAllocator {
fn default() -> Self {
let (recycled_queue_sender, recycled_queue_receiver) = crossbeam_channel::unbounded();
let (recycled_sender, recycled_receiver) = crossbeam_channel::unbounded();
Self {
recycled_queue_sender,
recycled_queue_receiver,
recycled_sender,
recycled_receiver,
next_index: Default::default(),
}
}
}
impl AssetIndexAllocator {
/// Reserves a new [`AssetIndex`], either by reusing a recycled index (with an incremented generation), or by creating a new index
/// by incrementing the index counter for a given asset type `A`.
pub fn reserve(&self) -> AssetIndex {
if let Ok(mut recycled) = self.recycled_queue_receiver.try_recv() {
recycled.generation += 1;
self.recycled_sender.send(recycled).unwrap();
recycled
} else {
AssetIndex {
index: self
.next_index
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
generation: 0,
}
}
}
/// Queues the given `index` for reuse. This should only be done if the `index` is no longer being used.
pub fn recycle(&self, index: AssetIndex) {
self.recycled_queue_sender.send(index).unwrap();
}
}
/// A "loaded asset" containing the untyped handle for an asset stored in a given [`AssetPath`].
///
/// [`AssetPath`]: crate::AssetPath
#[derive(Asset, TypePath)]
pub struct LoadedUntypedAsset {
#[dependency]
pub handle: UntypedHandle,
}
// PERF: do we actually need this to be an enum? Can we just use an "invalid" generation instead
#[derive(Default)]
enum Entry<A: Asset> {
/// None is an indicator that this entry does not have live handles.
#[default]
None,
/// Some is an indicator that there is a live handle active for the entry at this [`AssetIndex`]
Some { value: Option<A>, generation: u32 },
}
/// Stores [`Asset`] values in a Vec-like storage identified by [`AssetIndex`].
struct DenseAssetStorage<A: Asset> {
storage: Vec<Entry<A>>,
len: u32,
allocator: Arc<AssetIndexAllocator>,
}
impl<A: Asset> Default for DenseAssetStorage<A> {
fn default() -> Self {
Self {
len: 0,
storage: Default::default(),
allocator: Default::default(),
}
}
}
impl<A: Asset> DenseAssetStorage<A> {
// Returns the number of assets stored.
pub(crate) fn len(&self) -> usize {
self.len as usize
}
// Returns `true` if there are no assets stored.
pub(crate) fn is_empty(&self) -> bool {
self.len == 0
}
/// Insert the value at the given index. Returns true if a value already exists (and was replaced)
pub(crate) fn insert(
&mut self,
index: AssetIndex,
asset: A,
) -> Result<bool, InvalidGenerationError> {
self.flush();
let entry = &mut self.storage[index.index as usize];
if let Entry::Some { value, generation } = entry {
if *generation == index.generation {
let exists = value.is_some();
if !exists {
self.len += 1;
}
*value = Some(asset);
Ok(exists)
} else {
Err(InvalidGenerationError {
index,
current_generation: *generation,
})
}
} else {
unreachable!("entries should always be valid after a flush");
}
}
/// Removes the asset stored at the given `index` and returns it as [`Some`] (if the asset exists).
/// This will recycle the id and allow new entries to be inserted.
pub(crate) fn remove_dropped(&mut self, index: AssetIndex) -> Option<A> {
self.remove_internal(index, |dense_storage| {
dense_storage.storage[index.index as usize] = Entry::None;
dense_storage.allocator.recycle(index);
})
}
/// Removes the asset stored at the given `index` and returns it as [`Some`] (if the asset exists).
/// This will _not_ recycle the id. New values with the current ID can still be inserted. The ID will
/// not be reused until [`DenseAssetStorage::remove_dropped`] is called.
pub(crate) fn remove_still_alive(&mut self, index: AssetIndex) -> Option<A> {
self.remove_internal(index, |_| {})
}
fn remove_internal(
&mut self,
index: AssetIndex,
removed_action: impl FnOnce(&mut Self),
) -> Option<A> {
self.flush();
let value = match &mut self.storage[index.index as usize] {
Entry::None => return None,
Entry::Some { value, generation } => {
if *generation == index.generation {
value.take().map(|value| {
self.len -= 1;
value
})
} else {
return None;
}
}
};
removed_action(self);
value
}
pub(crate) fn get(&self, index: AssetIndex) -> Option<&A> {
let entry = self.storage.get(index.index as usize)?;
match entry {
Entry::None => None,
Entry::Some { value, generation } => {
if *generation == index.generation {
value.as_ref()
} else {
None
}
}
}
}
pub(crate) fn get_mut(&mut self, index: AssetIndex) -> Option<&mut A> {
let entry = self.storage.get_mut(index.index as usize)?;
match entry {
Entry::None => None,
Entry::Some { value, generation } => {
if *generation == index.generation {
value.as_mut()
} else {
None
}
}
}
}
pub(crate) fn flush(&mut self) {
// NOTE: this assumes the allocator index is monotonically increasing.
let new_len = self
.allocator
.next_index
.load(std::sync::atomic::Ordering::Relaxed);
self.storage.resize_with(new_len as usize, || Entry::Some {
value: None,
generation: 0,
});
while let Ok(recycled) = self.allocator.recycled_receiver.try_recv() {
let entry = &mut self.storage[recycled.index as usize];
*entry = Entry::Some {
value: None,
generation: recycled.generation,
};
}
}
pub(crate) fn get_index_allocator(&self) -> Arc<AssetIndexAllocator> {
self.allocator.clone()
}
pub(crate) fn ids(&self) -> impl Iterator<Item = AssetId<A>> + '_ {
self.storage
.iter()
.enumerate()
.filter_map(|(i, v)| match v {
Entry::None => None,
Entry::Some { value, generation } => {
if value.is_some() {
Some(AssetId::from(AssetIndex {
index: i as u32,
generation: *generation,
}))
} else {
None
}
}
})
}
}
/// Stores [`Asset`] values identified by their [`AssetId`].
///
/// Assets identified by [`AssetId::Index`] will be stored in a "dense" vec-like storage. This is more efficient, but it means that
/// the assets can only be identified at runtime. This is the default behavior.
///
/// Assets identified by [`AssetId::Uuid`] will be stored in a hashmap. This is less efficient, but it means that the assets can be referenced
/// at compile time.
///
/// This tracks (and queues) [`AssetEvent`] events whenever changes to the collection occur.
#[derive(Resource)]
pub struct Assets<A: Asset> {
dense_storage: DenseAssetStorage<A>,
hash_map: HashMap<Uuid, A>,
handle_provider: AssetHandleProvider,
queued_events: Vec<AssetEvent<A>>,
}
impl<A: Asset> Default for Assets<A> {
fn default() -> Self {
let dense_storage = DenseAssetStorage::default();
let handle_provider =
AssetHandleProvider::new(TypeId::of::<A>(), dense_storage.get_index_allocator());
Self {
dense_storage,
handle_provider,
hash_map: Default::default(),
queued_events: Default::default(),
}
}
}
impl<A: Asset> Assets<A> {
/// Retrieves an [`AssetHandleProvider`] capable of reserving new [`Handle`] values for assets that will be stored in this
/// collection.
pub fn get_handle_provider(&self) -> AssetHandleProvider {
self.handle_provider.clone()
}
/// Inserts the given `asset`, identified by the given `id`. If an asset already exists for `id`, it will be replaced.
pub fn insert(&mut self, id: impl Into<AssetId<A>>, asset: A) {
let id: AssetId<A> = id.into();
match id {
AssetId::Index { index, .. } => {
self.insert_with_index(index, asset).unwrap();
}
AssetId::Uuid { uuid } => {
self.insert_with_uuid(uuid, asset);
}
}
}
/// Retrieves an [`Asset`] stored for the given `id` if it exists. If it does not exist, it will be inserted using `insert_fn`.
// PERF: Optimize this or remove it
pub fn get_or_insert_with(
&mut self,
id: impl Into<AssetId<A>>,
insert_fn: impl FnOnce() -> A,
) -> &mut A {
let id: AssetId<A> = id.into();
if self.get(id).is_none() {
self.insert(id, insert_fn());
}
self.get_mut(id).unwrap()
}
/// Returns `true` if the `id` exists in this collection. Otherwise it returns `false`.
// PERF: Optimize this or remove it
pub fn contains(&self, id: impl Into<AssetId<A>>) -> bool {
self.get(id).is_some()
}
pub(crate) fn insert_with_uuid(&mut self, uuid: Uuid, asset: A) -> Option<A> {
let result = self.hash_map.insert(uuid, asset);
if result.is_some() {
self.queued_events
.push(AssetEvent::Modified { id: uuid.into() });
} else {
self.queued_events
.push(AssetEvent::Added { id: uuid.into() });
}
result
}
pub(crate) fn insert_with_index(
&mut self,
index: AssetIndex,
asset: A,
) -> Result<bool, InvalidGenerationError> {
let replaced = self.dense_storage.insert(index, asset)?;
if replaced {
self.queued_events
.push(AssetEvent::Modified { id: index.into() });
} else {
self.queued_events
.push(AssetEvent::Added { id: index.into() });
}
Ok(replaced)
}
/// Adds the given `asset` and allocates a new strong [`Handle`] for it.
#[inline]
pub fn add(&mut self, asset: impl Into<A>) -> Handle<A> {
let index = self.dense_storage.allocator.reserve();
self.insert_with_index(index, asset.into()).unwrap();
Handle::Strong(
self.handle_provider
.get_handle(index.into(), false, None, None),
)
}
/// Retrieves a reference to the [`Asset`] with the given `id`, if its exists.
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
#[inline]
pub fn get(&self, id: impl Into<AssetId<A>>) -> Option<&A> {
let id: AssetId<A> = id.into();
match id {
AssetId::Index { index, .. } => self.dense_storage.get(index),
AssetId::Uuid { uuid } => self.hash_map.get(&uuid),
}
}
/// Retrieves a mutable reference to the [`Asset`] with the given `id`, if its exists.
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
#[inline]
pub fn get_mut(&mut self, id: impl Into<AssetId<A>>) -> Option<&mut A> {
let id: AssetId<A> = id.into();
let result = match id {
AssetId::Index { index, .. } => self.dense_storage.get_mut(index),
AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid),
};
if result.is_some() {
self.queued_events.push(AssetEvent::Modified { id });
}
result
}
/// Removes (and returns) the [`Asset`] with the given `id`, if its exists.
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
pub fn remove(&mut self, id: impl Into<AssetId<A>>) -> Option<A> {
let id: AssetId<A> = id.into();
let result = self.remove_untracked(id);
if result.is_some() {
self.queued_events.push(AssetEvent::Removed { id });
}
result
}
/// Removes (and returns) the [`Asset`] with the given `id`, if its exists. This skips emitting [`AssetEvent::Removed`].
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
pub fn remove_untracked(&mut self, id: impl Into<AssetId<A>>) -> Option<A> {
let id: AssetId<A> = id.into();
match id {
AssetId::Index { index, .. } => self.dense_storage.remove_still_alive(index),
AssetId::Uuid { uuid } => self.hash_map.remove(&uuid),
}
}
/// Removes (and returns) the [`Asset`] with the given `id`, if its exists.
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
pub(crate) fn remove_dropped(&mut self, id: impl Into<AssetId<A>>) -> Option<A> {
let id: AssetId<A> = id.into();
let result = match id {
AssetId::Index { index, .. } => self.dense_storage.remove_dropped(index),
AssetId::Uuid { uuid } => self.hash_map.remove(&uuid),
};
if result.is_some() {
self.queued_events.push(AssetEvent::Removed { id });
}
result
}
/// Returns `true` if there are no assets in this collection.
pub fn is_empty(&self) -> bool {
self.dense_storage.is_empty() && self.hash_map.is_empty()
}
/// Returns the number of assets currently stored in the collection.
pub fn len(&self) -> usize {
self.dense_storage.len() + self.hash_map.len()
}
/// Returns an iterator over the [`AssetId`] of every [`Asset`] stored in this collection.
pub fn ids(&self) -> impl Iterator<Item = AssetId<A>> + '_ {
self.dense_storage
.ids()
.chain(self.hash_map.keys().map(|uuid| AssetId::from(*uuid)))
}
/// Returns an iterator over the [`AssetId`] and [`Asset`] ref of every asset in this collection.
// PERF: this could be accelerated if we implement a skip list. Consider the cost/benefits
pub fn iter(&self) -> impl Iterator<Item = (AssetId<A>, &A)> {
self.dense_storage
.storage
.iter()
.enumerate()
.filter_map(|(i, v)| match v {
Entry::None => None,
Entry::Some { value, generation } => value.as_ref().map(|v| {
let id = AssetId::Index {
index: AssetIndex {
generation: *generation,
index: i as u32,
},
marker: PhantomData,
};
(id, v)
}),
})
.chain(
self.hash_map
.iter()
.map(|(i, v)| (AssetId::Uuid { uuid: *i }, v)),
)
}
/// Returns an iterator over the [`AssetId`] and mutable [`Asset`] ref of every asset in this collection.
// PERF: this could be accelerated if we implement a skip list. Consider the cost/benefits
pub fn iter_mut(&mut self) -> AssetsMutIterator<'_, A> {
AssetsMutIterator {
dense_storage: self.dense_storage.storage.iter_mut().enumerate(),
hash_map: self.hash_map.iter_mut(),
queued_events: &mut self.queued_events,
}
}
/// A system that synchronizes the state of assets in this collection with the [`AssetServer`]. This manages
/// [`Handle`] drop events.
pub fn track_assets(mut assets: ResMut<Self>, asset_server: Res<AssetServer>) {
let assets = &mut *assets;
// note that we must hold this lock for the entire duration of this function to ensure
// that `asset_server.load` calls that occur during it block, which ensures that
// re-loads are kicked off appropriately. This function must be "transactional" relative
// to other asset info operations
let mut infos = asset_server.data.infos.write();
let mut not_ready = Vec::new();
while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() {
let id = drop_event.id.typed();
assets.queued_events.push(AssetEvent::Unused { id });
if drop_event.asset_server_managed {
let untyped_id = drop_event.id.untyped(TypeId::of::<A>());
if let Some(info) = infos.get(untyped_id) {
if info.load_state == LoadState::Loading
|| info.load_state == LoadState::NotLoaded
{
not_ready.push(drop_event);
continue;
}
}
if infos.process_handle_drop(untyped_id) {
assets.remove_dropped(id);
}
} else {
assets.remove_dropped(id);
}
}
// TODO: this is _extremely_ inefficient find a better fix
// This will also loop failed assets indefinitely. Is that ok?
for event in not_ready {
assets.handle_provider.drop_sender.send(event).unwrap();
}
}
/// A system that applies accumulated asset change events to the [`Events`] resource.
///
/// [`Events`]: bevy_ecs::event::Events
pub fn asset_events(mut assets: ResMut<Self>, mut events: EventWriter<AssetEvent<A>>) {
events.send_batch(assets.queued_events.drain(..));
}
}
/// A mutable iterator over [`Assets`].
pub struct AssetsMutIterator<'a, A: Asset> {
queued_events: &'a mut Vec<AssetEvent<A>>,
dense_storage: Enumerate<std::slice::IterMut<'a, Entry<A>>>,
hash_map: bevy_utils::hashbrown::hash_map::IterMut<'a, Uuid, A>,
}
impl<'a, A: Asset> Iterator for AssetsMutIterator<'a, A> {
type Item = (AssetId<A>, &'a mut A);
fn next(&mut self) -> Option<Self::Item> {
for (i, entry) in &mut self.dense_storage {
match entry {
Entry::None => {
continue;
}
Entry::Some { value, generation } => {
let id = AssetId::Index {
index: AssetIndex {
generation: *generation,
index: i as u32,
},
marker: PhantomData,
};
self.queued_events.push(AssetEvent::Modified { id });
if let Some(value) = value {
return Some((id, value));
}
}
}
}
if let Some((key, value)) = self.hash_map.next() {
let id = AssetId::Uuid { uuid: *key };
self.queued_events.push(AssetEvent::Modified { id });
Some((id, value))
} else {
None
}
}
}
#[derive(Error, Debug)]
#[error("AssetIndex {index:?} has an invalid generation. The current generation is: '{current_generation}'.")]
pub struct InvalidGenerationError {
index: AssetIndex,
current_generation: u32,
}