Skip to content

Conversation

@hymm
Copy link
Contributor

@hymm hymm commented Nov 7, 2025

Objective

  • Add a checked version of EntityMut::get_components_mut and EntityWorldMut::get_components_mut that does not allocate

Solution

  • Add a iterator over the access type to QueryData. This is then used to iterate over the pairs of access to check if they are compatible or not.

Testing

  • Added a unit test

Bench checked vs unchecked (50000 entities)

#components unchecked checked times slower
2 509 us 959 us 1.88x
5 903 us 3281 us 3.6x
10 1700 us 25369 us 14.9x

so at 10 components each call was taking about 1.06us vs 0.03 us


ToDo

  • add release note
  • add migration guide
  • add macro for more benches
  • add bench results to pr description
  • look into if this will help with uncached queries
  • see if we can optimize it a bit

@hymm hymm mentioned this pull request Nov 7, 2025
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Nov 9, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 9, 2025

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

github-merge-queue bot pushed a commit that referenced this pull request Nov 13, 2025
# Objective

- As part of #21780, I need a way to iterate over the component ids of a
bundle for `Entity*Except` conflict checking without allocating. Pulled
this out as it changes some unrelated code too.

## Solution

- Change `Bundle::component_ids` and `Bundle::get_component_ids` to
return an iterator instead of taking a closure. In theory I would expect
this to compile to the same asm. I would also argue that using an
iterator is a more natural api for this than the closure. It probably
took a closure before because expressing that the iterator doesn't
capture the `&mut ComponentRegistrator` lifetime wasn't possible without
the `use` syntax.
- Removed some #[allow(deprecated)] in the Bundle macro that was missed.

## Testing

- Checked the asm for `hook_on_add` in the observers example for to
confirm it was still the same. This is a pretty simple example though,
so not sure how good of a check this is.
- None of the code touched are in any hot paths, but ran the spawn and
insert benches. Any changes seem to be in the noise.
@hymm hymm force-pushed the get_components_mut branch from d0fda0c to caf7606 Compare November 15, 2025 01:38
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Nov 15, 2025
# Objective

- As part of bevyengine#21780, I need a way to iterate over the component ids of a
bundle for `Entity*Except` conflict checking without allocating. Pulled
this out as it changes some unrelated code too.

## Solution

- Change `Bundle::component_ids` and `Bundle::get_component_ids` to
return an iterator instead of taking a closure. In theory I would expect
this to compile to the same asm. I would also argue that using an
iterator is a more natural api for this than the closure. It probably
took a closure before because expressing that the iterator doesn't
capture the `&mut ComponentRegistrator` lifetime wasn't possible without
the `use` syntax.
- Removed some #[allow(deprecated)] in the Bundle macro that was missed.

## Testing

- Checked the asm for `hook_on_add` in the observers example for to
confirm it was still the same. This is a pretty simple example though,
so not sure how good of a check this is.
- None of the code touched are in any hot paths, but ran the spawn and
insert benches. Any changes seem to be in the noise.
@hymm hymm marked this pull request as ready for review November 15, 2025 19:53
Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

This is a very clever idea! I'm a little curious just how much faster it is than doing a naive check like in #20273.

/// Accesses [`Component`](crate::prelude::Component) data
Component(EcsAccessLevel),
/// Accesses [`Resource`](crate::prelude::Resource) data
Resource(ResourceAccessLevel),
Copy link
Contributor

Choose a reason for hiding this comment

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

We could probably ignore resource access for this. We don't allow mutable access (#17116), so it will never conflict. And it's never valid to access resources through EntityMut::get_components_mut anyway (#20315).

/// Returns an iterator over the access needed by [`QueryData::fetch`]. Access conflicts are usually
/// checked in [`WorldQuery::init_fetch`], but in certain cases this method can be useful to implement
/// a way of checking for access conflicts in a non-allocating way.
fn iter_access<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to have the caller call get_state first and pass in the &Self::State here?

They're almost certainly about to call it anyway, and that would let us avoid needing to do the HashMap lookup in components.component_id::<T>() twice. Or even more than twice, since has_conflicts calls this in a loop.

And that would let you handle FilteredEntityMut and EntityMutExcept by including a variant of EcsAccessType with a borrowed &Access. That could eliminate a lot of the complexity around except_index.

It would be a little awkward to hook in to get_components_mut, since get_state is called inside of UnsafeEntityCell::get_components but we want to check the conflicts outside of that. Although even if you wind up calling get_state twice, that's no more calls to component_id::<T>() than you're doing now!

Copy link
Contributor Author

@hymm hymm Nov 15, 2025

Choose a reason for hiding this comment

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

I have no idea what that would look like. How would you convert the get_state to an iterator?

edit: I have an idea of how this might look.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have no idea what that would look like. How would you convert the get_state to an iterator?

I meant that the State is where we store the ComponentId, so you could do

pub unsafe trait QueryData: WorldQuery {
    fn iter_access(state: &Self::State) -> impl Iterator<Item = EcsAccessType>;
}

unsafe impl<'a, T: Component> QueryData for &'a T {
    fn iter_access(state: &Self::State) -> impl Iterator<Item = EcsAccessType> {
        // Don't need to call `component_id::<T>`!
        iter::once(EcsAccessType::Component(EcsAccessLevel::Read(*state)))
    }
}

// And then call it like
let state = Q::get_state(components)?;
let iter = Q::iter_access(&state).enumerate();

And that structure would also let you handle FilteredEntityMut and EntityMutExcept by borrowing, like

pub enum EcsAccessLevel<'a> {
    Filtered(&'a Access),
    // ...
}

unsafe impl<'a, 'b, B: Bundle> QueryData for EntityMutExcept<'a, 'b, B> {
    fn iter_access<'s>(state: &'s Self::State) -> impl Iterator<Item = EcsAccessType<'s>> {
        iter::once(EcsAccessType::Component(EcsAccessLevel::Filtered(state)))
    }
}

That should be straightforward to handle in EcsAccessType::is_compatible(), and it avoids all the complexity of except_index.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good idea. This was a nice bump in perf. More than twice as fast. I updated the perf table above with the new numbers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh wait. get_state allocates in the case of Entity*Except so this won't work.

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum QueryAccessError {
/// Component was not registered on world
ComponentNotRegistered,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that since adding a component to an entity registers it, ComponentNotRegistered is just a special case of EntityDoesNotMatch. I don't think a caller can ever really do anything differently in those cases, so I'd be inclined to remove the ComponentNotRegistered variant and just return EntityDoesNotMatch in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can technically call this method before the component has been registered. I hit this while I was writing my tests. Is the average user likely to hit this? probably not. But the way you would fix this is by registering the component which is not what you would do for EntityDoesNotMatch.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can technically call this method before the component has been registered. I hit this while I was writing my tests. Is the average user likely to hit this? probably not. But the way you would fix this is by registering the component which is not what you would do for EntityDoesNotMatch.

But if the component hasn't been registered, then it hasn't been inserted onto any entity, which means it hasn't been inserted onto the current entity. So registering it will just change the error to EntityDoesNotMatch. And inserting the component onto that entity will fix either error.

... oh, except for Option<D> or Has<C>, which might fail instead of returning None or false. Blah.

/// Component was not registered on world
ComponentNotRegistered,
/// The [`EcsAccessType`]'s conflict with each other
Conflict,
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure we don't want to panic on conflicting access? It's based on the static type, so any given call will either always succeed or always fail. And callers will be tempted to ignore the error payload and treat Conflict the same as EntityDoesNotMatch, which would make it harder to notice that the type has a conflict.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I guess it's a safety issue like with normal systems. Hopefully with enough const rust features we'll be able to move a lot of these checks to compile time. We did get const type id's recently.

With this change I am tempted to change these apis back to being options. As you said above, it's not easy to hit the component not registered error.

///
/// Note that this does a O(n^2) check that the [`QueryData`](crate::query::QueryData) does not conflict. If performance is a
/// consideration you should use [`Self::get_components_mut_unchecked`] instead.
pub fn get_components_mut<Q: ReleaseStateQueryData>(
Copy link
Contributor

Choose a reason for hiding this comment

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

We may also want into_components_mut versions of these that consume self and return the full 'w lifetime.

You can share the implementations by implementing EntityMut::get_components_mut as self.reborrow().into_components_mut::<Q>() and EntityWorldMut::get_components_mut as self.as_mutable().into_components_mut::<Q>(). That also makes it harder to accidentally extend the lifetimes, since you're delegating that to the existing reborrow() and as_mutable() methods. (That's what I did in #20273.)

@hymm
Copy link
Contributor Author

hymm commented Nov 15, 2025

I'm a little curious just how much faster it is than doing a naive check like in #20273.

I didn't do exactly that, but I did try the algorithm in this pr with fixedbitsets and it was significantly slower at 2 and 5 components. iirc it was around 30us for both. Around the same at 10 components and faster at 16.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible M-Release-Note Work that should be called out in the blog due to impact 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.

3 participants