Avoid a panic when loading labelled assets (#13506)

# Objective

- Fixes #10820.

## Solution

- Check that the asset ID to be inserted is still being managed.
- Since this route is only used by `AssetServer`-tracked handles, if the
`infos` map no longer contains the asset ID, all handles must have been
dropped. In this case, since nobody can be watching for the result,
we're safe to bail out. This avoids the panic when inserting the asset,
because when the handles are dropped, its slot in `Assets<A>` is
poisoned.
- Someone may be waiting for a labelled asset rather than the main
asset, these are handled with separate calls to `process_asset_load`, so
shouldn't cause any issues.
- Removed the workaround keeping asset info alive after the handle has
died, since we should no longer be trying to operate on any assets once
their handles have been dropped.

## Testing

- I added a `break` in `handle_internal_asset_events`
(`crates/bevy_asset/src/server/mod.rs` on line 1152). I don't believe
this should affect correctness, only efficiency, since it is effectively
only allowing one asset event to be handled per frame. This causes
examples like `animated_fox` to produce the issue fairly frequently.
- I wrote a small program which called `AssetServer::reload` and could
trigger it too.

---

## Changelog
- Fixed an issue which could cause a panic when loading an asset which
was no longer referenced.

---

## Remaining Work

~This needs more testing. I don't yet have a complete project that
reliably crashes without changes to bevy.~ We have at least one vote of
confidence so far from @Testare who had a project broken by this bug.

@cart, (sorry for the ping), I believe you added the code which delays
`remove_dropped`. Was there any other reason `track_assets` needed to
keep the dropped assets alive?
This commit is contained in:
Ricky Taylor 2024-06-06 00:04:52 +01:00 committed by GitHub
parent 2165f2218f
commit 9a123cd3a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 6 additions and 16 deletions

View file

@ -1,7 +1,5 @@
use crate::{self as bevy_asset}; use crate::{self as bevy_asset};
use crate::{ use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle};
Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, LoadState, UntypedHandle,
};
use bevy_ecs::{ use bevy_ecs::{
prelude::EventWriter, prelude::EventWriter,
system::{Res, ResMut, Resource}, system::{Res, ResMut, Resource},
@ -545,18 +543,11 @@ impl<A: Asset> Assets<A> {
// re-loads are kicked off appropriately. This function must be "transactional" relative // re-loads are kicked off appropriately. This function must be "transactional" relative
// to other asset info operations // to other asset info operations
let mut infos = asset_server.data.infos.write(); 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() { while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() {
let id = drop_event.id.typed(); let id = drop_event.id.typed();
if drop_event.asset_server_managed { if drop_event.asset_server_managed {
let untyped_id = id.untyped(); let untyped_id = id.untyped();
if let Some(info) = infos.get(untyped_id) {
if let LoadState::Loading | LoadState::NotLoaded = info.load_state {
not_ready.push(drop_event);
continue;
}
}
// the process_handle_drop call checks whether new handles have been created since the drop event was fired, before removing the asset // the process_handle_drop call checks whether new handles have been created since the drop event was fired, before removing the asset
if !infos.process_handle_drop(untyped_id) { if !infos.process_handle_drop(untyped_id) {
@ -568,12 +559,6 @@ impl<A: Asset> Assets<A> {
assets.queued_events.push(AssetEvent::Unused { id }); assets.queued_events.push(AssetEvent::Unused { id });
assets.remove_dropped(id); 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. /// A system that applies accumulated asset change events to the [`Events`] resource.

View file

@ -377,6 +377,11 @@ impl AssetInfos {
world: &mut World, world: &mut World,
sender: &Sender<InternalAssetEvent>, sender: &Sender<InternalAssetEvent>,
) { ) {
// Check whether the handle has been dropped since the asset was loaded.
if !self.infos.contains_key(&loaded_asset_id) {
return;
}
loaded_asset.value.insert(loaded_asset_id, world); loaded_asset.value.insert(loaded_asset_id, world);
let mut loading_deps = loaded_asset.dependencies; let mut loading_deps = loaded_asset.dependencies;
let mut failed_deps = HashSet::new(); let mut failed_deps = HashSet::new();