Skip to content

Let FilteredEntity(Ref|Mut) receive access when nested. #18236

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

Merged

Conversation

chescock
Copy link
Contributor

Objective

Let FilteredEntityRef and FilteredEntityMut receive access when nested inside tuples or #[derive(QueryData)] types. Make sure to exclude any access that would conflict with other subqueries!

Fixes #14349

Solution

Replace WorldQuery::set_access(state, access) with a new method, QueryData::provide_extra_access(state, access, available_access), that passes both the total available access and the currently used access. This is called after WorldQuery::update_component_access(), so any access used by ordinary subqueries will be known. FilteredEntityRef and FilteredEntityMut can use the combination to determine how much access they can safely take, while tuples can safely pass those parameters directly to their subqueries.

This requires a new Access::remove_conflicting_access() method that can be used to remove any access that would conflict with existing access. Implementing this method was easier by first factoring some common set manipulation code out of Access::extend. I can extract that refactoring to a separate PR if desired.

Have FilteredEntity(Ref|Mut) store Access instead of FilteredAccess because they do not need to keep track of the filter. This was necessary in an early draft but no longer is. I left it in because it's small and I'm touching that code anyway, but I can extract it to a separate PR if desired.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Uncontroversial This work is generally agreed upon D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Mar 10, 2025
@alice-i-cecile alice-i-cecile added the C-Bug An unexpected or incorrect behavior label Mar 11, 2025
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

As far as I can tell this is correct, and mildly useful. I'd like to move forward with this, but this makes an already confusing bit of the code base more confusing. I've left suggestion for more docs, but I'd also like to see some unit tests for the invertable union / difference methods.

Those are pretty rough to mess up, and seem very testable!

@alice-i-cecile alice-i-cecile 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 Mar 11, 2025
@chescock
Copy link
Contributor Author

Added some more comments and tests!

While I'm adding docs to the set manipulation methods: A way to make this even more clear would be to create a new type that wraps a FixedBitSet and its inverted flag. I avoided that initially because it was a larger change than necessary, and because the layout change would make the Access struct a little bigger (from 128 bytes to 144). But it should be a lot easier to understand than the separate inverted flags.

Would it be worth doing that, either before, after, or as part of this PR?

@alice-i-cecile
Copy link
Member

I'd prefer to do that separately, but I do think it's a good idea. As long as we're not getting perf regressions I think some clarity there would be really helpful.

@chescock chescock added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Mar 14, 2025
state.clear_writes();
// Prevent any access that would conflict with other accesses in the current query
state.remove_conflicting_access(access);
// Finally, add the resulting access to the query access
Copy link
Contributor

Choose a reason for hiding this comment

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

This is interesting; because of how the macro is written, each term of a tuple inside a query like Query<(&A, &B)> sequentially updates the access by comparing with the access computed from the first n previous elements of the tuple?

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 think so? It does two passes. We first call update_component_access to determine the static access for things like &A and &mut B, and then call provide_extra_access to offer any leftover access. So you can do either (&mut A, FilteredEntityMut) or (FilteredEntityMut, &mut A), and the &mut A will claim the A access before the FilteredEntityMut sees it.

But within provide_extra_access, it offers access to each term in order and looks at the access that previous terms took. That causes the behavior you asked about below where (FilteredEntityRef, FilteredEntityMut) can be different from (FilteredEntityMut, FilteredEntityRef).

@@ -422,6 +423,89 @@ mod tests {
}
}

#[test]
fn builder_provide_access() {
Copy link
Contributor

@cBournhonesque cBournhonesque May 4, 2025

Choose a reason for hiding this comment

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

great tests!

let query =
QueryState::<(Entity, &A, &B)>::new(&mut world).transmute::<FilteredEntityRef>(&world);
let query = QueryState::<(Entity, &A, &B)>::new(&mut world)
.transmute::<(Entity, FilteredEntityRef)>(&world);
Copy link
Contributor

Choose a reason for hiding this comment

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

How come the tests needs an extra 'Entity' in the signature?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was just to make the tests start failing before I made the other changes :).

Copy link
Contributor

@cBournhonesque cBournhonesque 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 great; the current limitations of FilteredEntity(Ref|Mut) having to be the only QueryParam of a query was a bit artificial and confusing.

The unit tests are great

Copy link
Contributor

@cBournhonesque cBournhonesque 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 great; the current limitations of FilteredEntity(Ref|Mut) having to be the only QueryParam of a query was a bit artificial and confusing.

The unit tests are great

.data::<&B>()
.build();

// The `FilteredEntityRef` only has read access, so the `FilteredEntityMut` can have read access without conflicts
Copy link
Contributor

Choose a reason for hiding this comment

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

The access is provided from left to right, so if the tuple had been (FilteredEntityMut, FilteredEntityRef), the Ref wouldn't have read access to the mutable component?

It is not super obvious that the order matters, maybe this should be documented slightly more in the FilteredEntityRef/Mut docstrings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The access is provided from left to right, so if the tuple had been (FilteredEntityMut, FilteredEntityRef), the Ref wouldn't have read access to the mutable component?

Yes, that's right!

It is not super obvious that the order matters, maybe this should be documented slightly more in the FilteredEntityRef/Mut docstrings?

I wanted to ensure that it was sound to use more than one FilteredEntity thing in a query, but it's not really useful, and I don't expect anyone to actually want to do it. So I'd be inclined to leave the exact semantics out of the documentation so as not to confuse normal users with the details of unimportant edge cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's reasonable

@alice-i-cecile alice-i-cecile added this to the 0.17 milestone May 5, 2025
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Awesome, thank you!

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 5, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 5, 2025
Merged via the queue into bevyengine:main with commit bea0a0a May 5, 2025
34 checks passed
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-Bug An unexpected or incorrect behavior C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon
Projects
None yet
Development

Successfully merging this pull request may close these issues.

FilteredEntityRef/FilteredEntityMut's access is empty when used in composite queries
3 participants