Skip to content

Conversation

@andriyDev
Copy link
Contributor

@andriyDev andriyDev commented Jan 26, 2026

Objective

  • A step towards Assets as Entities #11266.
  • Provide a generic way to hold a reference to an entity that gets cleaned up automatically.

Solution

  • Wrap Arc/Weak and bundle it with an entity.
  • When the Arc is dropped, send a message to a concurrent queue that the entity should be despawned.
  • Have a function to handle the messages in the concurrent queue and despawn the appropriate entity.

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

  • Added some basic tests of the ref counting.

Showcase

fn my_system(world: &mut World) {
    let my_rc_source = EntityRcSource::new();

    // Create a new EntityRc.
    let entity = world.spawn_empty().id();
    let handle = my_rc_source.create_rc(entity, ());

    // Dropping the handle (and letting the source handle it) despawns the entity.
    drop(handle);
    my_rc_source.handle_dropped_rcs(&mut world.commands());
    world.flush();

    assert!(world.get_entity(entity).is_none());
}

@andriyDev andriyDev added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events A-Assets Load files from disk to use for things like images, models, and sounds D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 26, 2026
@andriyDev andriyDev force-pushed the entity-rc branch 4 times, most recently from b67cd94 to a97709b Compare January 26, 2026 01:34
Copy link
Contributor

@CorvusPrudens CorvusPrudens left a 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.

@alice-i-cecile alice-i-cecile added D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes and removed D-Straightforward Simple bug fixes and API improvements, docs, test and examples labels Jan 26, 2026

use crate::{entity::Entity, system::Commands};

/// A reference count for an entity.
Copy link
Member

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
Copy link
Member

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`].
Copy link
Member

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?

Copy link
Member

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.
Copy link
Member

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>>);
Copy link
Member

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>>);
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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 = ()> {
Copy link
Member

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
Copy link
Member

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>> {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// 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 {
Copy link
Member

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.
Copy link
Member

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> {
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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).
Copy link
Member

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."

Copy link
Contributor

@ElliottjPierce ElliottjPierce left a 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.
Copy link
Contributor

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 {
Copy link
Contributor

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> {
Copy link
Contributor

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) {
Copy link
Contributor

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?

@andriyDev andriyDev added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Assets Load files from disk to use for things like images, models, and sounds A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants