-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
get_components_mut #21780
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?
get_components_mut #21780
Conversation
|
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. |
# 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.
d0fda0c to
caf7606
Compare
# 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.
chescock
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.
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), |
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.
| /// 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>( |
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.
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!
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 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.
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 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.
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, good idea. This was a nice bump in perf. More than twice as fast. I updated the perf table above with the new numbers.
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.
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, |
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.
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.
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.
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.
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.
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, |
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.
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.
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, 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>( |
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 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.)
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. |
Objective
EntityMut::get_components_mutandEntityWorldMut::get_components_mutthat does not allocateSolution
QueryData. This is then used to iterate over the pairs of access to check if they are compatible or not.Testing
Bench checked vs unchecked (50000 entities)
so at 10 components each call was taking about 1.06us vs 0.03 us
ToDo