-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Asset locking #15359
base: main
Are you sure you want to change the base?
Asset locking #15359
Conversation
b6c4ed7
to
6f72525
Compare
4638d29
to
c547158
Compare
Just rebased onto main. |
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.
Very clean code and easy to follow!
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.
The code is really well-made: commented, clearly engineered, great use of enums. Can you add an example demonstrating how (and especially why) a user might use this API?
Once that's in place, I'm on board and am willing to merge.
Left some thoughts in the issue: #15346 (comment) |
7228729
to
3f7c624
Compare
This currently buys us nothing, but it's a nice stepping stone to making `AssetPresence` have a locked variant.
This variant is still unused.
Now assets can be "locked" into an `Arc`.
This will make it more performant to try unlocking all the locked assets in the "happy case" that few assets are locked.
This way, if you lock an asset, you just have to drop its `Arc` and wait a frame for the asset to become mutable again (as opposed to requiring a system to figure out which assets can be unlocked and trying to unlock just those). The price is constant polling whenever an asset is locked. We have to repeatedly try unlocking an asset until it is actually ready to be unlocked.
This makes it easier to just unwrap a mutable borrow by just doing `.as_mut().unwrap()`.
…main world. For simplicity, we just clone the assets. In theory, we could have an enum to store the RenderAssets as either a locked Arc or an Unlocked asset, but I'd rather just avoid that branch.
This is actually a totally fine case, we just need a borrow to be able to clone the internal Arc's.
3f7c624
to
d333659
Compare
I rebased (#15281 had a big blast radius), and also included a new example to show off how you can use asset locking. I still need to figure out how to implement |
d333659
to
c65eb32
Compare
c65eb32
to
167aad4
Compare
This makes working with asset locking much more palatable, as users don't need to explicitly handle the locking case in most situations.
This is likely the common case for users, so we should have our examples use this function.
Ok, I've implemented One thing that's kinda rough here is that |
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 think this is a very interesting concept to add to Assets
and opens up further discussion around other systems. I've left some notes with possible future work, but I think as-is this is good to merge.
/// asset is cloned and replaces the existing asset (the previously locked asset is still | ||
/// readable by holders of its [`Arc`]). | ||
#[inline] | ||
pub fn get_mut_or_clone(&mut self, id: impl Into<AssetId<A>>) -> Option<&mut A> { |
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.
Future PR: a more fundamental version of the function could be
pub fn get_mut_or_replace_with(
&mut self,
id: impl Into<AssetId<A>>,
f: impl FnOnce(Option<&A>) -> A
) -> Option<&mut A>;
Which would allow the user to decide how to replace a locked asset, remove the A: Clone
requirement, and handling the Missing
case.
|
||
/// Locks the current asset, which prevents the asset from being modified | ||
/// until it is unlocked. Returns [`None`] only if [`AssetPresence::None`]. | ||
pub fn lock(&mut self) -> Option<Arc<A>> { |
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.
Future PR: since the only way to lock
an Asset
is via this method, using #[track_caller]
and std::panic::Location::caller()
, we could store a Vec<Location>
in the Locked
variant, allowing the try_unlock
method to return an error indicating where the Asset
has been locked from. I believe the #[track_caller]
attribute would need to be added to all the methods that forward to lock
as well (e.g., Assets::lock
)
I imagine we'd want such a thing in debug
builds only, since it adds more weight to the AssetPresence
type, but I imagine it would be quite helpful for diagnosing possibly frustrating deadlocks.
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 is an interesting idea, but it's not clear to me how big a win this is. For one, the list of lock locations can only grow (until an asset is finally unlocked). I suppose if we assume locking is infrequent and short-lived this isn't a big deal, but I can totally see an asset being locked for a long time and constantly getting more and more lock locations. For another, that only tracks the initial lock location, but you're free to clone the Arc as much as you want. I suspect the more dangerous situation is something storing the Arc without knowing it's a locked asset and blocking mutations that way (and there's no real way to solve this AFAICT).
Regardless, this definitely seems worthy of a discussion and I could be convinced!
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.
Yeah it's definitely not perfect, but it could help with at least giving the user a starting point for debugging how an asset deadlock started. Potentially, we could even wrap the returned Arc
to give it better debugging functionality. Storing a HashMap
of Location
's on the heap and counting down each location as the wrapped Arc
is dropped, and counting up as the Arc
is cloned.
Definitely half-baked and up for debate, and absolutely out of scope for this PR!
Going to wait on @cart to take another look at this design: he had opinions in the linked thread and I don't feel comfortable merging this without him :) |
Objective
Solution
Testing
bevy_asset
.Showcase
The primary new parts of the API are:
Assets::lock
- Locks the asset if it existsAssets::try_unlock
- Attempts to unlock the asset if there are no more outstanding handles (otherwise the asset remains locked).Migration Guide
Assets::get_or_insert_with
may now return an error if the asset is locked.Assets::get_mut
may now return an error if the asset is locked (in addition to if the asset is missing). Consider usingAssets::get_mut_or_clone
if your type isClone
to avoid handling locking.Assets::remove
andAssets::remove_untracked
now return anAssetPresence<A>
. This requires you to handle the case where an asset is locked.Assets::iter_mut
now returns an(AssetId<A>, AssetPresenceMut<'a, A>)
instead of(AssetId<A>, &'a mut A)
. This allows you to handle the locked case.ReflectAsset::get_mut
andReflectAsset::get_unchecked_mut
may now return an error if the asset is locked (in addition to if the asset is missing).ReflectAsset::remove
now returns aReflectedAssetPresence
to store both the locked and unlocked assets.