Skip to content
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

Introduce methods on QueryState to obtain a Query #15858

Merged
merged 7 commits into from
Feb 5, 2025

Conversation

chescock
Copy link
Contributor

Objective

Simplify and expand the API for QueryState.

QueryState has a lot of methods that mirror those on Query. These are then multiplied by variants that take &World, &mut World, and UnsafeWorldCell. In addition, many of them have _manual variants that take &QueryState and avoid calling update_archetypes(). Not all of the combinations exist, however, so some operations are not possible.

Solution

Introduce methods to get a Query from a QueryState. That will reduce duplication between the types, and ensure that the full Query API is always available for QueryState.

Introduce methods on Query that consume the query to return types with the full 'w lifetime. This avoids issues with borrowing where things like query_state.query(&world).get(entity) don't work because they borrow from the temporary Query.

Finally, implement Copy for read-only Querys. get_inner and iter_inner currently take &self, so changing them to consume self would be a breaking change. By making Query: Copy, they can consume a copy of self and continue to work.

The consuming methods also let us simplify the implementation of methods on Query, by doing fn foo(&self) { self.as_readonly().foo_inner() } and fn foo_mut(&mut self) { self.reborrow().foo_inner() }. That structure makes it more difficult to accidentally extend lifetimes, since the safe as_readonly() and reborrow() methods shrink them appropriately. The optimizer is able to see that they are both identity functions and inline them, so there should be no performance cost.

Note that this change would conflict with #15848. If QueryState is stored as a Cow, then the consuming methods cannot be implemented, and Copy cannot be implemented.

Future Work

The next step is to mark the methods on QueryState as #[deprecated], and move the implementations into Query.

Migration Guide

Query::to_readonly has been renamed to Query::as_readonly.

…ry to consume Self and return the original lifetime.
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 11, 2024
@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Oct 11, 2024
@alice-i-cecile alice-i-cecile added M-Needs-Release-Note Work that should be called out in the blog due to impact X-Contentious There are nontrivial implications that should be thought through C-Code-Quality A section of code that is hard to understand or change labels Oct 11, 2024
@Trashtalk217
Copy link
Contributor

I agree that there's a lot of duplication between QueryState's and Query's API and it's good that this is lessened, but just out of curiosity (just to offer a potentially dumb idea), would it perhaps be better if QueryState and Query were just one single struct instead of two?

@Trashtalk217 Trashtalk217 self-assigned this Oct 16, 2024
@chescock
Copy link
Contributor Author

I agree that there's a lot of duplication between QueryState's and Query's API and it's good that this is lessened, but just out of curiosity (just to offer a potentially dumb idea), would it perhaps be better if QueryState and Query were just one single struct instead of two?

I think the idea is that you need access to the world to actually do querying, but that we want to cache some state that lives longer than that borrow. So a Query is a &QueryState for the state plus UnsafeWorldCell<'w> and Ticks from the world.

I don't see a clean way to get rid of either. If you don't have Query then you'd need to pass a world to every method on QueryState, which is terrible ergonomics for even the simplest system. If you don't have QueryState then you need to look up ComponentIds and match archetypes every time you run a system, which is terrible performance.

@chescock
Copy link
Contributor Author

I realized I could split this into two independent PRs: One for the methods on QueryState to obtain a Query, and one for the consuming methods on Query. The consuming methods aren't all that useful by themselves, but the methods on QueryState should be, and those shouldn't be as contentious. Would anyone like me to split it up like that?

@BenjaminBrienen
Copy link
Contributor

BenjaminBrienen commented Oct 31, 2024

I say go big! Go ahead and remove the to-be-deprecated methods on QueryState! 😆

You can split it up if you think it would help, but I don't mind bigger PRs.

@@ -1542,24 +1542,6 @@ mod tests {
});
}

#[test]
#[should_panic = "Encountered a mismatched World."]
fn query_validates_world_id() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove this test? Is it no longer applicable?

#[inline]
pub(crate) unsafe fn new(
world: UnsafeWorldCell<'w>,
state: &'s QueryState<D, F>,
last_run: Tick,
this_run: Tick,
) -> Self {
state.validate_world(world.id());
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove this check?

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, right, that was subtle and I should have pointed it out!

The reason is that I changed all of the &self and &mut self methods on Query to call self.as_readonly().actual_method() and self.reborrow().actual_method(), and as_readonly() and reborrow() are implemented in terms of Query::new(). I wanted to make sure that change was zero-cost, but checking the assembly with cargo-show-asm showed that it was adding a call to validate_world to every existing method on Query! Since we always had a world that was already validated, I changed it to be a safety requirement, and now the assembly appears to be unchanged.

(Then I removed the test you asked about above because it's no longer true that it panics.)

@BenjaminBrienen BenjaminBrienen added D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way labels Oct 31, 2024
@BenjaminBrienen
Copy link
Contributor

BenjaminBrienen commented Oct 31, 2024

I think it would be good to add more examples/tests that might help miri find unsafe mistakes, but it seems pretty clean.

Copy link
Contributor

@13ros27 13ros27 left a comment

Choose a reason for hiding this comment

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

This looks good to me, mostly just nits although I do have one question about that IntoIterator for Query?

crates/bevy_ecs/src/query/state.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/query/state.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/query/state.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/system/query.rs Show resolved Hide resolved
crates/bevy_ecs/src/system/query.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/system/query.rs Outdated Show resolved Hide resolved
@@ -381,6 +381,14 @@ pub struct Query<'world, 'state, D: QueryData, F: QueryFilter = ()> {
this_run: Tick,
}

impl<D: ReadOnlyQueryData, F: QueryFilter> Clone for Query<'_, '_, D, F> {
fn clone(&self) -> Self {
*self
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry, noob question: how does this work?
The &QueryState inside the Query is simply copied? i.e. we create a new Query that points to the same QueryState?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup! Everything in Query is Copy, which means it can be copied by memcpy. That includes references like &QueryState, which just makes a new reference to the same underlying QueryState. So we can make Query be Copy, and then implement Clone in terms of it.

@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 Feb 5, 2025
/// Creates a [`Query`] from the given [`QueryState`] and [`World`].
///
/// This will create read-only queries, see [`Self::query_mut`] for mutable queries.
pub fn query<'w, 's>(&'s mut self, world: &'w World) -> Query<'w, 's, D::ReadOnly, F> {
Copy link
Member

Choose a reason for hiding this comment

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

Oh my gosh finally.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Feb 5, 2025
self.state
.iter_unchecked_manual(self.world, self.last_run, self.this_run)
}
self.reborrow().into_iter()
Copy link
Contributor

@cBournhonesque cBournhonesque Feb 5, 2025

Choose a reason for hiding this comment

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

Sorry noob question again could you please explain again why we need to re-borrow?

self.reborrow().into_iter() creates a new Query with reborrow() and then borrows from that with into_iter()
self.into_iter() would simply create a QueryIter that borrows from the initital &'a mut Query so the lifetime is constrained by `a; is that the issue?

The lifetimes are elided on reborrow so I don't get exactly what is happening

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reborrow() goes from &'a mut Query<'w> to Query<'a>, turning a borrowed Query into an owned one with a shorter lifetime.

into_iter() takes self instead of &mut self, so it actually consumes the Query.

We have an &mut Query, so we first reborrow() to get an owned Query with a shorter lifetime, and then use into_iter() to consume the owned query and create a QueryIter - still with the shorter lifetime.

It's more complex when looking at one method, but across the whole type it means we can re-use the safety proofs in reborrow() and as_readonly(). That lets us implement this method without unsafe code, and means the compiler would catch it if we tried to return QueryIter<'w, ...> instead of QueryIter<'_, ...>.

Merged via the queue into bevyengine:main with commit 6f39e44 Feb 5, 2025
29 checks passed
Copy link
Contributor

@Victoronz Victoronz left a comment

Choose a reason for hiding this comment

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

Sorry to block this (the merge queue got there first), but I don't think Query being Copy is sound?
IIUC you can go from a mutable Query to a read-only QueryLens, make a Query out of that lens again, then copy that Query, transmute_lens it again, and get a Query from it for a last time. Then you'd have a read-only query that is no longer bound to the &mut self of the original mutable Query.
Did I overlook something?

@alice-i-cecile
Copy link
Member

Discuss in #17693.

@Victoronz
Copy link
Contributor

Victoronz commented Feb 7, 2025

Something else I'd like to note is that this PR implements IntoIterator for Query, however that impl is not restricted to ReadOnlyQueryData. This means that a simple for loop over a mutable Query will consume it, even if that loop doesn't actually mutate anything!
I'd think this to be quite confusing for users. Especially since there is no "owned" way of iterating Query, we're still just borrowing the data in the end.
Implementing it only for ReadOnlyQueryData would be confusing as well, since that'd mean that writing mutable for loops now diverges from writing immutable ones!
Is that impl necessary?

@chescock
Copy link
Contributor Author

chescock commented Feb 7, 2025

Something else I'd like to note is that this PR implements IntoIterator for Query, however that impl is not restricted to ReadOnlyQueryData. This means that a simple for loop over a mutable Query will consume it, even if that loop doesn't actually mutate anything!

Yup! That's the generalization of iter_inner() to mutable data, and lets you write query_state.query_mut(world).into_iter() without needing to promote the temporary query to a variable, which may be impossible if you're returning data. That's necessary to completely replace query_state.iter_mut(world).

I'm hoping it will be a nicer experience for users: You can write the obvious for data in query, and in the common case where you only iterate once it will finally just work! If you need to iterate multiple times, you'll need to for data in &mut query, but that's true for other common collection types like Vec, so it should be familiar.

And because read-only queries also impl Copy, you can do multiple for data in query loops over them, and everything will work!

@chescock
Copy link
Contributor Author

chescock commented Feb 7, 2025

And while I'd been focusing on QueryState when writing this, the IntoIter impl should also make it nicer to work with QueryLens, since you won't always have to put the Query in a variable.

For example:

let mut lens: QueryLens<_> = ...;

// Fails with "error[E0716]: temporary value dropped while borrowed"
let first = lens.query().iter_mut().next();
assert!(first.is_some());

// Works!
let first = lens.query().into_iter().next();
assert!(first.is_some());

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

Restore the behavior of `Query::get_many` prior to #15858.  

When passed duplicate `Entity`s, `get_many` is supposed to return
results for all of them, since read-only queries don't alias. However,
#15858 merged the implementation with `get_many_mut` and caused it to
return `QueryEntityError::AliasedMutability`.

## Solution

Introduce a new `Query::get_many_readonly` method that consumes the
`Query` like `get_many_inner`, but that is constrained to `D:
ReadOnlyQueryData` so that it can skip the aliasing check. Implement
`Query::get_many` in terms of that new method. Add a test, and a comment
explaining why it doesn't match the pattern of the other `&self`
methods.
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-Code-Quality A section of code that is hard to understand or change C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes D-Unsafe Touches with unsafe code in some way M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants