-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Fix handles being split by asset_server_managed.
#22261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2c3a9f4
87e4d16
d6dbe33
82e43a6
d54f347
39f5924
1de7d65
fd65933
eae866c
c53fe87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,16 @@ | ||
| use crate::asset_changed::AssetChanges; | ||
| use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle}; | ||
| use crate::{ | ||
| Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, ErasedAssetIndex, Handle, | ||
| HandleEvent, UntypedHandle, | ||
| }; | ||
| use alloc::{sync::Arc, vec::Vec}; | ||
| use bevy_ecs::system::Local; | ||
| use bevy_ecs::{ | ||
| message::MessageWriter, | ||
| resource::Resource, | ||
| system::{Res, ResMut, SystemChangeTick}, | ||
| }; | ||
| use bevy_platform::collections::HashMap; | ||
| use bevy_platform::collections::{HashMap, HashSet}; | ||
| use bevy_reflect::{Reflect, TypePath}; | ||
| use core::{any::TypeId, iter::Enumerate, marker::PhantomData, sync::atomic::AtomicU32}; | ||
| use crossbeam_channel::{Receiver, Sender}; | ||
|
|
@@ -289,9 +293,8 @@ pub struct Assets<A: Asset> { | |
| hash_map: HashMap<Uuid, A>, | ||
| handle_provider: AssetHandleProvider, | ||
| queued_events: Vec<AssetEvent<A>>, | ||
| /// Assets managed by the `Assets` struct with live strong `Handle`s | ||
| /// originating from `get_strong_handle`. | ||
| duplicate_handles: HashMap<AssetIndex, u16>, | ||
| /// Maps each asset index to the number of currently living handles. | ||
| index_to_live_handles: HashMap<AssetIndex, u16>, | ||
| } | ||
|
|
||
| impl<A: Asset> Default for Assets<A> { | ||
|
|
@@ -304,7 +307,7 @@ impl<A: Asset> Default for Assets<A> { | |
| handle_provider, | ||
| hash_map: Default::default(), | ||
| queued_events: Default::default(), | ||
| duplicate_handles: Default::default(), | ||
| index_to_live_handles: Default::default(), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -400,7 +403,7 @@ impl<A: Asset> Assets<A> { | |
| 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, false, None, None)) | ||
| Handle::Strong(self.handle_provider.get_handle(index, None, None)) | ||
| } | ||
|
|
||
| /// Upgrade an `AssetId` into a strong `Handle` that will prevent asset drop. | ||
|
|
@@ -417,9 +420,8 @@ impl<A: Asset> Assets<A> { | |
| // We don't support strong handles for Uuid assets. | ||
| AssetId::Uuid { .. } => return None, | ||
| }; | ||
| *self.duplicate_handles.entry(index).or_insert(0) += 1; | ||
| Some(Handle::Strong( | ||
| self.handle_provider.get_handle(index, false, None, None), | ||
| self.handle_provider.get_handle(index, None, None), | ||
| )) | ||
| } | ||
|
|
||
|
|
@@ -479,23 +481,28 @@ impl<A: Asset> Assets<A> { | |
| let id: AssetId<A> = id.into(); | ||
| match id { | ||
| AssetId::Index { index, .. } => { | ||
| self.duplicate_handles.remove(&index); | ||
| self.index_to_live_handles.remove(&index); | ||
| self.dense_storage.remove_still_alive(index) | ||
| } | ||
| AssetId::Uuid { uuid } => self.hash_map.remove(&uuid), | ||
| } | ||
| } | ||
|
|
||
| /// Removes the [`Asset`] with the given `id`. | ||
| pub(crate) fn remove_dropped(&mut self, index: AssetIndex) { | ||
| match self.duplicate_handles.get_mut(&index) { | ||
| None => {} | ||
| Some(0) => { | ||
| self.duplicate_handles.remove(&index); | ||
| } | ||
| Some(value) => { | ||
| *value -= 1; | ||
| return; | ||
| /// Attempts to remove an [`Asset`] with the given `id`, if it has no more living handles. | ||
| /// | ||
| /// Returns true if the asset's entry was removed. This is not the same as removing the asset | ||
| /// itself - an asset which has not been inserted may still have its entry removed. | ||
| pub(crate) fn try_remove_dropped(&mut self, index: AssetIndex) -> bool { | ||
| use bevy_platform::collections::hash_map::Entry; | ||
| match self.index_to_live_handles.entry(index) { | ||
| Entry::Vacant(_) => unreachable!( | ||
| "we dropped a handle, so there must have been at least one live handle" | ||
| ), | ||
| Entry::Occupied(entry) => { | ||
| if *entry.get() > 0 { | ||
| return false; | ||
| } | ||
| entry.remove(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -507,6 +514,7 @@ impl<A: Asset> Assets<A> { | |
| self.queued_events | ||
| .push(AssetEvent::Removed { id: index.into() }); | ||
| } | ||
| true | ||
| } | ||
|
|
||
| /// Returns `true` if there are no assets in this collection. | ||
|
|
@@ -565,23 +573,74 @@ impl<A: Asset> Assets<A> { | |
|
|
||
| /// 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>) { | ||
| pub fn track_assets( | ||
| mut assets: ResMut<Self>, | ||
| asset_server: Res<AssetServer>, | ||
| mut maybe_drop_indices: Local<HashSet<AssetIndex>>, | ||
| ) { | ||
| 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.write_infos(); | ||
| while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() { | ||
| if drop_event.asset_server_managed { | ||
| // 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(drop_event.index) { | ||
| // a new handle has been created, or the asset doesn't exist | ||
| continue; | ||
|
|
||
| // Keep looping until we haven't dropped an asset entry. This way, if we also drop the | ||
| // asset, any handles it was holding may also result in dropping assets (so we clean up all | ||
| // these assets on the same frame). | ||
| let mut dropped_entry = true; | ||
| while dropped_entry { | ||
| dropped_entry = false; | ||
| assert!(maybe_drop_indices.is_empty()); | ||
|
|
||
| while let Ok(handle_event) = assets.handle_provider.event_receiver.try_recv() { | ||
| match handle_event { | ||
| HandleEvent::New(index) => { | ||
| *assets.index_to_live_handles.entry(index).or_default() += 1; | ||
| } | ||
| HandleEvent::Drop(index) => { | ||
| let live_handles = assets.index_to_live_handles.get_mut(&index).expect( | ||
| "we must have processed this handle's new event before the drop event", | ||
| ); | ||
| *live_handles -= 1; | ||
| if *live_handles == 0 { | ||
| // Defer the actual drop until later, so that if a `HandleEvent::New` for | ||
| // this asset comes later, we don't drop the asset too early. | ||
| maybe_drop_indices.insert(index); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| assets.remove_dropped(drop_event.index.index); | ||
| for index in maybe_drop_indices.drain() { | ||
| // Try to drop the asset entry (including the asset) after handling all the events. | ||
| // There is a *potential* race condition here if we get a `HandleEvent::New` between | ||
| // the previous loop, and this call. But this can't happen since there are only | ||
| // three ways to send a `HandleEvent::New`. | ||
| // | ||
| // 1. The AssetServer creates a handle for an asset being loaded. Since we have the | ||
| // asset server infos lock, this can't happen. | ||
| // 2. A user calls `Assets::get_strong_handle`. Since we have `ResMut<Self>`, this | ||
| // can't happen. | ||
| // 3. A new handle is reserved through `AssetHandleProvider::reserve_handle`. This | ||
| // can only be used to generate handles to new assets - it can't be used to | ||
| // generate new handles to existing assets. So we can't have dropped a handle to | ||
| // this asset yet, so we will never attempt to drop this asset too early. | ||
| // | ||
| // Therefore, there is no race condition here. If we ever add new ways to get strong | ||
| // handles to existing assets, we should reconsider this. Handles to new assets are | ||
| // always safe like above. | ||
| if !assets.try_remove_dropped(index) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be wrong here, but isn't this technically a race condition? There are a bunch of lock-shaped things here, the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup that's exactly correct, the only thing preventing us from a race condition here is the fact that we have exclusive access to Assets and are locking the asset server - there's no other way to get a new handle to an existing asset. The only other way to send a I |
||
| // We didn't actually drop the asset entry, so there's nothing more to do. | ||
| continue; | ||
| } | ||
| // Notify the AssetServer of the asset entry drop. | ||
| infos.process_asset_entry_drop(ErasedAssetIndex { | ||
| index, | ||
| type_id: TypeId::of::<A>(), | ||
| }); | ||
| dropped_entry = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why this import needs to be inside this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file defines a type
Entry, so the hashmapEntrycollides with it. Scoping it here sidesteps that issue.