-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Create a generic entity reference counting solution. #22710
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?
Conversation
b67cd94 to
a97709b
Compare
CorvusPrudens
left a comment
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 would probably avoid appending thingy in the release notes example (it's unnecessary and reduces clarity), but the technical implementation looks good to me.
|
|
||
| use crate::{entity::Entity, system::Commands}; | ||
|
|
||
| /// A reference count for an entity. |
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 a bad definition, since it doesn't explain what this does to someone who isn't familiar with reference counting.
|
|
||
| /// A reference count for an entity. | ||
| /// | ||
| /// This "handle" also stores some optional data, allowing users to customize any shared data |
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.
Of type T, right? This should be explicitly clarified.
| /// The reverse is also true: a held [`EntityRc`] does not guarantee that the entity still exists. | ||
| /// It can still be explicitly despawned, so users should try to be resilient to this. | ||
| /// | ||
| /// This type has similar semantics to [`Arc`]. |
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 note needs a bit more explanation: what semantics or properties are held in common?
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 analogy might be a helpful jumping off point to explain how this is intended to be used more broadly. You should not assume that your audience is familiar with Arc, especially at a level beyond "IDK it can be used to get around the borrow checker in a thread-safe way".
|
|
||
| use crate::{entity::Entity, system::Commands}; | ||
|
|
||
| /// A reference count for an entity. |
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 first line of docs also need to capture that it can store data of type T.
| /// | ||
| /// This type has similar semantics to [`Arc`]. | ||
| #[derive(Debug)] | ||
| pub struct EntityRc<T: Send + Sync + 'static = ()>(Arc<EntityRcInner<T>>); |
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.
We should mention that T defaults to (), and that this means that no data is shared.
| /// | ||
| /// This type has similar semantics to [`Arc`]. | ||
| #[derive(Debug)] | ||
| pub struct EntityRc<T: Send + Sync + 'static = ()>(Arc<EntityRcInner<T>>); |
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 think that this terminology is particularly clear. The fact that it's an entity is somewhat incidental: the point is that this is a component which holds shared, reference-counted data.
Something like RcData<T> or SharedData<T> makes much more sense to me.
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 disagree - the point is that the entity is the thing that should be considered "reference-counted". The fact that there is data is what I consider "incidental". We could just as easily have thrown out the T, but it just makes it more convenient to include some data so all handles can access it.
If the entity part weren't important, we would just tell people to use Arc, since that has exactly the same semantics.
|
|
||
| /// A reference count for an entity. | ||
| /// | ||
| /// This "handle" also stores some optional data, allowing users to customize any shared data |
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 model needs to be laid out more explicitly. Which entity owns the actual data? How is the shared data stored? How do you generate handles? Is there a distinction between the entity which "owns" the data (currently Assets) and entities which "reference" the data (currently handles)?
Doc tests may be helpful to explain usage :)
| /// | ||
| /// This type has similar semantics to [`Weak`]. | ||
| #[derive(Debug)] | ||
| pub struct EntityWeak<T: Send + Sync + 'static = ()> { |
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.
See this comment complaining about the names.
|
|
||
| /// A "non-owning" reference to a reference-counted entity. | ||
| /// | ||
| /// Holding this handle does not guarantee that the entity will not be cleaned up. This handle |
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 double negative here makes it very hard for me to parse this sentence.
| /// | ||
| /// Returns [`None`] if all [`EntityRc`]s were previously dropped. This does not necessarily | ||
| /// mean that the entity has been despawned yet. | ||
| pub fn upgrade(&self) -> Option<EntityRc<T>> { |
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.
Is returning an Option traditional here? The semantics are fairly non-intuitive just reading the signature to me; I would definitely prefer a Result.
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.
Yup - this is exactly the same as https://doc.rust-lang.org/stable/std/sync/struct.Weak.html#method.upgrade.
| /// Note: this can produce [`EntityRc`] containing any "payload", since the payload is not | ||
| /// accessible during despawn time. This is because it's possible for the entity to be despawned | ||
| /// explicitly even though an [`EntityRc`] is still held - callers should be resilient to this. | ||
| pub struct EntityRcSource { |
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.
Remember to update this name too if the naming convention is changed.
| } | ||
| } | ||
|
|
||
| /// Allows creating [`EntityRc`] and handles syncing them with the world. |
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 needs another paragraph of explanation below explaining how this actually works in practice, and why you might want to do this.
|
|
||
| impl<T: Send + Sync + 'static> EntityRc<T> { | ||
| /// Creates a new [`EntityWeak`] referring to the same entity (and reference count). | ||
| pub fn downgrade(this: &Self) -> EntityWeak<T> { |
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.
It's surprising to me that a method called downgrade takes &self rather than self. Doesn't this leave the original strong EntityRc alive?
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 surprised me as well! This is exactly what https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.downgrade does. This does indeed leave the original EntityRc alive, but I guess in a way that makes sense. Otherwise it also implicitly turns it into a drop, which could just immediately return you a Weak that is already invalid.
Also Weak has their own reference counting, so I'm guessing it's no faster to take an owned Arc.
| reference that data in multiple places. Most importantly, it drops the data once all references have | ||
| been removed. | ||
|
|
||
| We've recreated this tool for entities! With `EntityRc`, users can now reference an entity and have |
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 needs more context about why you might want to do this :)
| We've recreated this tool for entities! With `EntityRc`, users can now reference an entity and have | ||
| it automatically despawned when all `EntityRc`s have been dropped. | ||
|
|
||
| To do this, first create an `EntityRcSource`, and store it somewhere (like a resource). |
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.
These usage examples are quite good. I would be happy to see this duplicated to the module level docs, and high-level explanation of how this all fits together moved there.
Then, on each of the items in the module, drop breadcrumbs to the module docs, like "For more information about how this is used, see the module docs."
ElliottjPierce
left a comment
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 great step in the right direction. I have a few minor suggestions elsewhere but I also have a few bigger questions:
Understanding the use case
Could you help me understand the use case here? (I know assets as entities, but beyond that.)
What type do we plan on using for T in EntityRc<T>. If it's (), we can simplify this a lot.
Do we need super tight control over when we handle_dropped_rcs? If not, can we automate this in World::flush or something similar?
Would it be helpful to be able to, given a EntityRef or something, ask if this entity is reference counted and if so, get the corresponding EntityRc?
Technical questions
Why do we need EntityWeak or is there another way of doing this? If I understand correctly:
| Entity Type | Entity |
EntityRc |
EntityWeak |
|---|---|---|---|
| Prevents the entity from being despawned | No | No | No |
| Despawns the entity when all are dropped | No | Yes | No |
So Entity and EntityWeak look identical to me, except that EntityWeak can upgrade. Maybe we can make a struct ReferenceCounted(Weak<EntityRcInner>) component? And then you could get the EntityRc from a query or something. (I'm not opposed to EntityWeak at all; I just don't want to add more than we need.)
I would guess that if you call EntityRcSource::create_rcs twice on the same entity, one EntityRc would eventually despawn the entity before the other. Is that intentional or does it have a use case? I can't think of one unless you have multiple T's at play here.
Sorry; I know that's a lot of questions. I actually think this is a good implementation if we need all these features. It just depends on how advanced we need for assets as entities.
Like, if we just need entity reference counting, and that's all we need (which I really have no idea if that's all we need), we could do much simpler.
We could have something like:
#[derive(Component)]
struct ReferenceCounted {
strong_count: Arc<()>,
}
struct EntityRc {
e: Entity,
strong_count: Arc<()>,
}and have a system that just checks if the strong count is exactly 1 and despawns it.
Anyway, I like this implementation. I'm just not sure if this is minimal to what we need.
| } | ||
| } | ||
|
|
||
| /// Returns the entity this reference count refers to. |
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.
Probably worth inlining these just in case.
| /// Note: this can produce [`EntityRc`] containing any "payload", since the payload is not | ||
| /// accessible during despawn time. This is because it's possible for the entity to be despawned | ||
| /// explicitly even though an [`EntityRc`] is still held - callers should be resilient to this. | ||
| pub struct EntityRcSource { |
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.
Is there any reason this isn't Clone?
Is there any reason someone would want to have two different EntityRcSources? Maybe to have two different clean up cycles? But that would only be useful if the program isn't resilient to the entities being despawned. If there's not a strong reason to have multiple of these for one world, can we just put this in World and handle_dropped_rcs directly in World::flush or something similar?
| /// ([`EntityAllocator`](crate::entity::EntityAllocator)), remote entity allocation | ||
| /// ([`RemoteAllocator`](crate::entity::RemoteAllocator)), or even taking an existing entity and | ||
| /// making it reference counted. | ||
| pub fn create_rc<T: Send + Sync + 'static>(&self, entity: Entity, payload: T) -> EntityRc<T> { |
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.
If we expect T to almost always be (), might be worth making a convenience method here.
| /// | ||
| /// Note: if you have exclusive world access (`&mut World`), you can use | ||
| /// [`World::commands`](crate::world::World::commands) to get an instance of [`Commands`]. | ||
| pub fn handle_dropped_rcs(&self, commands: &mut Commands) { |
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 like the commands indirection here, but I'm not terribly opposed to it either. But it would be faster (fewer commands, tighter loop, etc) if this took &mut World. Maybe then have a single command that could call that &mut World function?
Objective
Solution
Arc/Weakand bundle it with an entity.Arcis dropped, send a message to a concurrent queue that the entity should be despawned.This will eventually replace the current
bevy_asset::Handle. One big distinction here is there's no way to stop an entity from being despawned once its RC is gone. Based on the discussion here, that appears to be something we are willing to give up.Testing
Showcase