-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Combine Query
and QueryLens
using a type parameter for state
#18162
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?
Conversation
First of all, thank you for this PR! I've been wanting to do something like this for some time! Let's get into the details: I've mentioned this in the discord discussion before, but I'll properly write it out here again: I think we should only store pointers to The meaning of the Separately, |
where | ||
D: ReadOnlyQueryData, | ||
S: Copy, |
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 should be using Clone
bounds, not Copy
, it allows for stuff like Arc<QueryState>
in the future.
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 don't want to use Clone
here because it makes it too easy to accidentally clone the entire QueryState
. This is a fairly niche method, so I'm comfortable declaring that it can't be used with unusual iterators.
Note that iter()
and iter_mut()
will always return a QueryIter
with &QueryState
, since they borrow from the Query
, so this only prevents calls to remaining()
on the result of into_iter()
with an exotic state.
fetch: D::Fetch<'w>, | ||
filter: F::Fetch<'w>, | ||
query_state: &'s QueryState<D, F>, | ||
fetch: <S::Data as WorldQuery>::Fetch<'w>, |
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 wonder whether we could simplify some of these/recover the old names with type aliases
Yup, that's a good idea! I had written this before those discussions, and was trying for the smallest possible change to
Yup, that makes sense, too. I had been thinking that we could just roll that into the safety requirements of the existing |
Use Deref instead of Borrow.
…pl EntitySetIterator for QueryIter`.
/// Abstracts over an owned or borrowed [`QueryState`]. | ||
/// | ||
/// # Safety | ||
/// | ||
/// This must `deref` to a `QueryState` that does not change. | ||
pub unsafe trait QueryStateDeref: | ||
Deref<Target = QueryState<Self::Data, Self::Filter>> | ||
{ | ||
/// The [`QueryData`] for this `QueryState`. | ||
type Data: QueryData; | ||
|
||
/// The [`QueryFilter`] for this `QueryState`. | ||
type Filter: QueryFilter; | ||
|
||
/// The type returned by [`Self::storage_ids`]. | ||
type StorageIter: Iterator<Item = StorageId> + Clone + Default; | ||
|
||
/// A read-only version of the state. | ||
type ReadOnly: QueryStateDeref< | ||
Data = <Self::Data as QueryData>::ReadOnly, | ||
Filter = Self::Filter, | ||
>; | ||
|
||
/// Iterates the storage ids that this [`QueryState`] matches. | ||
fn storage_ids(&self) -> Self::StorageIter; | ||
|
||
/// Borrows the remainder of the [`Self::StorageIter`] as an iterator | ||
/// usable with `&QueryState<D, F>`. | ||
fn reborrow_storage_ids( | ||
storage_iter: &Self::StorageIter, | ||
) -> iter::Copied<slice::Iter<StorageId>>; | ||
|
||
/// Converts this state to a read-only version. | ||
fn into_readonly(self) -> Self::ReadOnly; | ||
} |
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.
Hmm, I said earlier I wasn't sure where the API should live, but I am now certain that it should be on &QueryState
.
By putting it on the trait, we would need to describe the correct implementation for each method in the safety contract, or we cannot use these methods ourselves.
This trait should instead have no API for now, and would then only have to describe its supertrait and QueryData
/QueryFilter
.
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 delegated everything I could, but these methods are here because the types vary between owned and borrowed versions. The S::StorageIter
is stored at the same time as the S
, so it can't be slice::Iter
for owned QueryData
. And into_readonly
needs to return an owned QueryState
, since there's nothing left to borrow from.
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.
Right, it seems QueryIterationCursor
code doesn't compile when attempting to borrow the storage iterator from a deref.
However, this means that we are introducing some storage iterator clones into hot iteration code.
We should try to avoid that if possible, these clones shouldn't be necessary!
That does mean changing up the code to allow for this, which I hope isn't too difficult...
The iteration code of QueryIterationCursor
is important enough that changing it involves benchmarks/proving it isn't being regressed.
With into_readonly
, we run into the problem of needing to cast the original S
instead of just the deref.
If we need to put the cast after the deref, then we can just wrap S
with a struct that retains the deref, but appends the cast in its own impl!
For that, we don't need a dedicated method on QueryStateDeref
, it can be done in Query::into_readonly
directly.
If we later want it on QueryStateDeref
, then we can put it there as a provided method! Though the more private solution is preferable at first I think.
This would change the returned S
type of Query::into_readonly
into ReadOnly<S>
.
This solution would also transfer to owned transmutes, which into_readonly
is essentially a simple case of.
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.
The technique of wrapping an S
in a ReadOnly<S>
/Transmuted<S>
might actually be a nice speedup for QueryState::transmute
/Query::transmute_lens
in general, because we can then avoid creating a new QueryState
like we do now!
We can just keep the "access is a subset" check, then cast S
this way!
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.
However, this means that we are introducing some storage iterator clones into hot iteration code.
We should try to avoid that if possible, these clones shouldn't be necessary!
That does mean changing up the code to allow for this, which I hope isn't too difficult...
The iteration code ofQueryIterationCursor
is important enough that changing it involves benchmarks/proving it isn't being regressed.
Yup, the clones are unfortunate! They only occur for QueryLens::into_iter()
, though. Calling into_iter()
on an ordinary Query
, or calling iter()
or iter_mut()
on anything, will use &QueryState
. That still uses a slice iterator, so this won't change the behavior of any existing code. And if you're consuming a QueryLens
then you're already spending allocations on the QueryState
, so one more shouldn't be too bad.
I really don't want to make any big changes to QueryIterationCursor
in this PR, exactly because it's important for performance! If performance of QueryLens::into_iter()
becomes a problem, we can do a more targeted follow-up to rework QueryIterationCursor
.
For that, we don't need a dedicated method on
QueryStateDeref
, it can be done inQuery::into_readonly
directly. ... This would change the returnedS
type ofQuery::into_readonly
intoReadOnly<S>
.
Oh, wrapping the type is a clever idea! ... Hmm, it would mean that Query<&mut T>::as_readonly()
is no longer the same type as Query<&T>
, though, since the state type would differ. And I really don't want to make a breaking change like that as part of this PR!
The technique of wrapping an
S
in aReadOnly<S>
/Transmuted<S>
might actually be a nice speedup forQueryState::transmute
/Query::transmute_lens
in general, because we can then avoid creating a newQueryState
like we do now!
Yeah, I think there are some good opportunities in that space! It's a little harder than that because you also need to store a new D::State
and F::State
for the new D
and F
. Maybe Transmuted<S>
could hold new versions of those and delegate the rest to a wrapped state? Although then you're definitely not a Deref<Target=QueryState>
.
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.
However, this means that we are introducing some storage iterator clones into hot iteration code.
We should try to avoid that if possible, these clones shouldn't be necessary!
That does mean changing up the code to allow for this, which I hope isn't too difficult...
The iteration code ofQueryIterationCursor
is important enough that changing it involves benchmarks/proving it isn't being regressed.Yup, the clones are unfortunate! They only occur for
QueryLens::into_iter()
, though. Callinginto_iter()
on an ordinaryQuery
, or callingiter()
oriter_mut()
on anything, will use&QueryState
. That still uses a slice iterator, so this won't change the behavior of any existing code. And if you're consuming aQueryLens
then you're already spending allocations on theQueryState
, so one more shouldn't be too bad.I really don't want to make any big changes to
QueryIterationCursor
in this PR, exactly because it's important for performance! If performance ofQueryLens::into_iter()
becomes a problem, we can do a more targeted follow-up to reworkQueryIterationCursor
.
True, let us leave it for a follow-up then.
For that, we don't need a dedicated method on
QueryStateDeref
, it can be done inQuery::into_readonly
directly. ... This would change the returnedS
type ofQuery::into_readonly
intoReadOnly<S>
.Oh, wrapping the type is a clever idea! ... Hmm, it would mean that
Query<&mut T>::as_readonly()
is no longer the same type asQuery<&T>
, though, since the state type would differ. And I really don't want to make a breaking change like that as part of this PR!
Hmm, I don't quite follow, how would those differ?
But for now, what could be done in this PR is add an unsafe cast
method to QueryStateDeref
(mirroring ptr::cast
), which would serve the purpose we'd need it for. The impl for Box<QueryState>
should then also be changed into a into_raw
-> cast
-> from_raw
roundtrip.
The technique of wrapping an
S
in aReadOnly<S>
/Transmuted<S>
might actually be a nice speedup forQueryState::transmute
/Query::transmute_lens
in general, because we can then avoid creating a newQueryState
like we do now!Yeah, I think there are some good opportunities in that space! It's a little harder than that because you also need to store a new
D::State
andF::State
for the newD
andF
. MaybeTransmuted<S>
could hold new versions of those and delegate the rest to a wrapped state? Although then you're definitely not aDeref<Target=QueryState>
.
Right! I forgot that ReadOnly
has the equal state restriction. Maybe something can be done to add a similar restriction here.
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.
Hmm, it would mean that
Query<&mut T>::as_readonly()
is no longer the same type asQuery<&T>
, though, since the state type would differ.Hmm, I don't quite follow, how would those differ?
If I understood your suggestion, then Query<&mut T, (), &QS>::as_readonly()
would return Query<&T, (), ReadOnly<&QS>>
. But Query<&T>
is Query<&T, (), &QS>
, and &QS
!= ReadOnly<&QS>
. So a user couldn't pass the result of as_readonly()
to a method that took Query<&T>
, which I believe is the main use case for that method today.
But for now, what could be done in this PR is add an unsafe
cast
method toQueryStateDeref
(mirroringptr::cast
), which would serve the purpose we'd need it for. The impl forBox<QueryState>
should then also be changed into ainto_raw
->cast
->from_raw
roundtrip.
I don't think I see how to implement that. Do you mean exposing an equivalent of QueryState::as_transmuted_state
instead of QueryState::as_readonly
? That would still wind up needing an associated type, but it would need to be generic over NewD, NewF
. Since the only uses are as_readonly
and as_nop
, and as_nop
never needs an owned state, it seems simpler to only expose as_readonly
.
I can change the Box<QueryState>
implementation to do a pointer cast. I like that the current impl doesn't need unsafe
, but the safety there isn't any worse than the &QueryState
cast, so we might as well avoid reallocating!
Quick note: |
Oh, sorry! I was just trying to give you proper credit :). I didn't actually know that would turn into a ping! Do you want me to force-push a new message, or is it okay here because nobody is cloning my personal fork and the commit will be squashed if it gets merged? |
I think its fine here because of the squash, just noting for the future; some repos don't do that! |
…ding to allocate a new box.
…tate # Conflicts: # crates/bevy_ecs/src/query/iter.rs # crates/bevy_ecs/src/query/par_iter.rs # crates/bevy_ecs/src/system/query.rs
`cargo fmt`.
…tate # Conflicts: # crates/bevy_ecs/src/query/par_iter.rs
It looks like your PR is a breaking change, but you didn't provide a migration guide. Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes. |
I added a migration guide for the deprecated |
Objective
Make
QueryLens
easier to use by allowing query methods to be called on it directly instead of needing to call thequery()
method first.Introduce owned query iterators, so that it's possible for methods to return iterators constructed from
Query::transmute
orQuery::join
.Solution
Make
Query
and the various iterator types generic, where normal queries use borrowed&QueryState<D, F>
andQueryLens
uses ownedQueryState<D, F>
. Introduce atrait QueryStateBorrow: Borrow<QueryState>
to abstract over the two types.Have
Query
use a default type for the state so that it continues to work without specifying it explicitly.Note that the
'state
lifetime onQuery
was only used in&'state QueryState
, so it is now only used in the default type parameter. It still needs to be part of theQuery
type in order to be referenced in the default type, so we need aPhantomData
so that it's actually used. Another option here would be to makeQuery
a type alias for some new named type, but I think the user experience will be better with a default type parameter than with a type alias.Testing
I used
cargo-show-asm
to verify that the assembly ofQuery::iter
did not change.This rust code:
Run with
Results in the same asm before and after the change