Skip to content

Commit

Permalink
Allow removing and reloading assets with live handles (#10785)
Browse files Browse the repository at this point in the history
# Objective

Fixes #10444 

Currently manually removing an asset prevents it from being reloaded
while there are still active handles. Doing so will result in a panic,
because the storage entry has been marked as "empty / None" but the ID
is still assumed to be active by the asset server.

Patterns like `images.remove() -> asset_server.reload()` and
`images.remove() -> images.insert()` would fail if the handle was still
alive.

## Solution

Most of the groundwork for this was already laid in Bevy Asset V2. This
is largely just a matter of splitting out `remove` into two separate
operations:

* `remove_dropped`: remove the stored asset, invalidate the internal
Assets entry (preventing future insertions with the old id), and recycle
the id
* `remove_still_alive`: remove the stored asset, but leave the entry
otherwise untouched (and dont recycle the id).

`remove_still_alive` and `insert` can be called any number of times (in
any order) for an id until `remove_dropped` has been called, which will
invalidate the id.

From a user-facing perspective, there are no API changes and this is non
breaking. The public `Assets::remove` will internally call
`remove_still_alive`. `remove_dropped` can only be called by the
internal "handle management" system.

---

## Changelog

- Fix a bug preventing `Assets::remove` from blocking future inserts for
a specific `AssetIndex`.
  • Loading branch information
cart committed Nov 30, 2023
1 parent 960af2b commit 9ac0d9b
Showing 1 changed file with 52 additions and 16 deletions.
68 changes: 52 additions & 16 deletions crates/bevy_asset/src/assets.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate as bevy_asset;
use crate::{self as bevy_asset, LoadState};
use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle};
use bevy_ecs::{
prelude::EventWriter,
Expand Down Expand Up @@ -87,12 +87,11 @@ pub struct LoadedUntypedAsset {
// 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 {
value: Option<A>,
generation: u32,
},
/// 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`].
Expand Down Expand Up @@ -151,7 +150,26 @@ impl<A: Asset> DenseAssetStorage<A> {
}

/// Removes the asset stored at the given `index` and returns it as [`Some`] (if the asset exists).
pub(crate) fn remove(&mut self, index: AssetIndex) -> Option<A> {
/// 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,
Expand All @@ -166,8 +184,7 @@ impl<A: Asset> DenseAssetStorage<A> {
}
}
};
self.storage[index.index as usize] = Entry::None;
self.allocator.recycle(index);
removed_action(self);
value
}

Expand Down Expand Up @@ -393,9 +410,23 @@ impl<A: Asset> Assets<A> {
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(index),
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.
Expand Down Expand Up @@ -466,16 +497,21 @@ impl<A: Asset> Assets<A> {
let mut not_ready = Vec::new();
while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() {
let id = drop_event.id;
if !assets.contains(id.typed()) {
not_ready.push(drop_event);
continue;
}
if drop_event.asset_server_managed {
if infos.process_handle_drop(id.untyped(TypeId::of::<A>())) {
assets.remove(id.typed());
let untyped = id.untyped(TypeId::of::<A>());
if let Some(info) = infos.get(untyped) {
if info.load_state == LoadState::Loading
|| info.load_state == LoadState::NotLoaded
{
not_ready.push(drop_event);
continue;
}
}
if infos.process_handle_drop(untyped) {
assets.remove_dropped(id.typed());
}
} else {
assets.remove(id.typed());
assets.remove_dropped(id.typed());
}
}
// TODO: this is _extremely_ inefficient find a better fix
Expand Down

0 comments on commit 9ac0d9b

Please sign in to comment.