-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Nested Queries #21557
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?
Nested Queries #21557
Conversation
This should be possible, but the access checks are tricky.
…rom nested queries.
I'm not certain it's the best way of going about it but couldn't we implement this by storing the entity of the parent instead of the data and then resolving the data from the entity when it is needed? |
pub fn iter_mut(&mut self) -> QueryIter<'_, 's, D, F> { | ||
pub fn iter_mut(&mut self) -> QueryIter<'_, 's, D, F> | ||
where | ||
D: IterQueryData, |
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 PR looks good, but I don't fully understand IterQueryData
.
The trait says
A [`QueryData`] for which instances may be alive for different entities concurrently.
But when doing iter_mut
, we never have two 2 items concurrently right? since we would iterate through them one by one. So even if we have a nested query which accesses multiple entities, there would still not be a collision?
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.
But when doing
iter_mut
, we never have two 2 items concurrently right? since we would iterate through them one by one.
Nope, you can keep the old items around! The lifetime in fn next(&mut self)
isn't connected to the Item
type, so later calls don't invalidate earlier items. That's how things like collect()
work.
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.
Nope, you can keep the old items around! The lifetime in fn next(&mut self) isn't connected to the Item type, so later calls don't invalidate earlier items. That's how things like collect() work.
This is a really helpful explanation. Can you add something like this to the IterQueryData
docs?
/// ``` | ||
#[track_caller] | ||
pub fn transmute_lens<NewD: QueryData>(&mut self) -> QueryLens<'_, NewD> { | ||
pub fn transmute_lens<NewD: SingleEntityQueryData>(&mut self) -> QueryLens<'_, NewD> { |
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.
How come we can only transmute to a SingleEntityQueryData
?
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.
Ah i see in the PR description that this could be relaxed in the future
I'm not sure what you mean. Like, instead of yielding a let mut items = query.iter_mut().collect::<Vec<_>>();
let mapped = items.iter_mut().map(|item| item.get_mut()).collect::<Vec<_>>(); |
crates/bevy_ecs/src/query/state.rs
Outdated
} | ||
|
||
/// Collects the access from this query and any nested queries | ||
/// and panics |
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.
Was this doc cut short? This seems to be doing way more than simply collecting queries; it basically replaces the old job of <Query as SystemParam>::init_access
where we would check if the Query conflicts with other SystemParams of the System.
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.
Was this doc cut short?
Oh, yup, I'll fix that! I must have accidentally a
This seems to be doing way more than simply collecting queries; it basically replaces the old job of
<Query as SystemParam>::init_access
where we would check if the Query conflicts with other SystemParams of the System.
Yup, it's the same as init_access
; it collects the FilteredAccess
es into a FilteredAccessSet
and panics if there are conflicts.
Hmm, I should really rename it. I think in an earlier draft it just handled the access from nested queries, but now it also handles the access from the current query, so "external" is a misleading name. I'll try QueryState::init_access
to make it more clear that it's the same as SystemParam::init_access
, and then rename WorldQuery::update_external_component_access
to WorldQuery::init_nested_access
.
crates/bevy_ecs/src/query/state.rs
Outdated
} | ||
|
||
component_access_set.add(self.component_access.clone()); | ||
D::update_external_component_access( |
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.
Wouldn't we want to collect the access from nested queries before checking if they cause conflicts with other SystemParams? Or would the conflict become apparent later?
It would be nice to have at least one implementation of a QueryData that implements update_external_component_access
so we can add unit tests, but I understand that the PR is already big
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.
Ah is the answer that D::update_external_component_access
must itself check for conflicts and possibly panic?
But in that case couldn't we just update self.component_access
with the nested component accesses?
Where is the distinction between "current entity component access" and "nested entity component access" relevant?
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.
Wouldn't we want to collect the access from nested queries before checking if they cause conflicts with other SystemParams? Or would the conflict become apparent later?
I think either a preorder or postorder traversal would work. This is essentially visiting all of the Query
instances and calling init_access
on each one, and it doesn't matter whether we process the outer query before or after the nested ones, since the conflict check is symmetric.
It would be nice to have at least one implementation of a QueryData that implements
update_external_component_access
so we can add unit tests, but I understand that the PR is already big
Yeah, sorry. I wrote out an example impl for the PR description, but there's a little more prep work before we can commit a complete implementation, and creating a temporary one just for unit tests didn't seem worthwhile.
But in that case couldn't we just update
self.component_access
with the nested component accesses? Where is the distinction between "current entity component access" and "nested entity component access" relevant?
That's what I tried at first! But the query infrastructure also uses the filters in component_access
to determine which archetypes match the query, so you need to keep track of the "current entity" access for that purpose.
Then for a while I had both a FilteredAccess
for the main query and a FilteredAccessSet
for the nested queries. But that wound up storing multiple copies of each access, and it was easy enough to walk through the tree to collect them when needed.
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.
It would be nice to have at least one implementation of a QueryData that implements
update_external_component_access
so we can add unit tests
Thinking about this further, I think I should add a generic NestedQuery
implementation. It's not useful in application code, since you could just add another Query
system parameter, but I think it will be useful to delegate to, either in manual QueryData
impls or with #[derive(QueryData)]
. And that would give us an actual implementation to write unit tests with!
I'll try to put that together on Monday.
I think these changes are a great idea; I guess I would like to know how this contrasts to how flecs queries for relation data. Maybe @james-j-obrien knows? |
That's a quite involved question to answer (although a very interesting one). The big difference between the bevy and flecs queries are tied to one being defined in the type system and the other being defined dynamically. Due to this bevy queries are fundamentally based on nesting, you have tuples of query terms that each store their own state and generate their own code for managing that state. In flecs all the query terms are just stored in a flat array. For example in this PR we express querying our parent as creating a query term that traverses the relationship and then nested in that is the set of components we want to access on the target, whereas in flecs you would have a set of instructions that said: "get me any entity A with relationship of the form (ChildOf, B) and store B as a variable", "get me component Y on entity B", "get me component Z on entity B". This structure allows flecs to optimize/batch/reorder terms since they can be evaluated in the full context of the rest of the query, but for simple queries it's mostly a different path to the same goal. Since flecs also has fragmenting relations they can do stuff like cache the tables for B since you know that entities in A's table will always have parent B. All that being said, with bevy's queries as they exist today this PR seems like the shortest path to querying on multiple entities so seems like a positive step. |
…` and fix missing documentation.
…:init_nested_access`.
Thanks for the answer! I guess we could do something similar: for tuple queries, build such a node graph and use it to match entities. I guess we do already create a data structure that helps us find matching entities; that data structure is the QueryState.
And the main difference is that the flecs "QueryState" is more elaborate since it can contain sources, relationships, etc. |
@eugineerd, I liked your work over in #21601; can I get your review here in turn? |
/// or that only perform read access on other entities. | ||
/// Queries may perform mutable access on other entities if they | ||
/// can prove that the other entities are all distinct, | ||
/// such as if they are on the appropriate side of a one-to-many or one-to-one relation. |
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.
Do we have the corresponding safety constraints on the relationship traits in place already? I think I agree that banning self-targeted / non-unique relations is correct, but we should at least have safety requirements there.
Do we need to swap our relations implementations to ban e.g. Vec
in favor of the unique entity equivalents?
/// # schedule.run(&mut world); | ||
/// ``` | ||
pub fn sort<L: ReadOnlyQueryData + 'w>( | ||
pub fn sort<L: ReadOnlyQueryData + SingleEntityQueryData + '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.
Doc comments about why SingleEntityQueryData
is required for these methods would be helpful for users. It's not immediately clear why this is required.
|
||
/// Adds any component accesses to other entities used by this [`WorldQuery`]. | ||
/// | ||
/// This method should panic if the access would conflict with any existing access in the [`FilteredAccessSet`]. |
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.
Is this a "should panic" or a "must panic"?
/// This method should panic if the access would conflict with any existing access in the [`FilteredAccessSet`]. | ||
/// | ||
/// This is used for queries to request access to entities other than the current one, | ||
/// such as to read resources or to follow relations. |
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.
/// such as to read resources or to follow relations. | |
/// such as to follow relations. |
Resources as entities isn't merged, so we shouldn't add confusing doc comments.
Objective
Support queries that soundly access multiple entities.
This can be used to create queries that follow relations, as in #17647.
This can also be used to create queries that perform resource access. This has been supported since #16843, although that approach may become unsound if we do resources-as-components #19731, such as #21346.
Fixes #20315
Solution
Allow a
QueryData
that wants to access other entities to store aQueryState<D, F>
in itsWorldQuery::State
, so that it can create a nestedQuery<D, F>
during the outerfetch
.New
WorldQuery
methodsFor it to be sound to create the
Query
duringfetch
, we need to register theFilteredAccess
of the nested query and check for conflicts with other parameters. Create aWorldQuery::update_external_component_access
method for that purpose. ForQuery as SystemParam
, call this duringinit_access
so the access can be combined with the rest of the system access. For looseQueryState
s, call it duringQueryState::new
.In order to keep the query cache up-to-date, create a
WorldQuery::update_archetypes
method where it can callQueryState::update_archetypes_unsafe_world_cell
, and call it from there.New
QueryData
subtraitsSome operations would not be sound with nested queries! In particular, we want a
Parent<D>
query that reads data from the parent entity by following theChildOf
relation. But many entities may share a parent, so it's not sound to iterate aQuery<Parent<&mut C>>
.It is sound to
get_mut
, though, so we want the query type to exist, just not be iterable. And following the relation in the other direction for aQuery<Children<&mut C>>
is sound to iterate, since children are unique to a given parent.So, introduce two new
QueryData
subtraits:IterQueryData
- For anything it's sound to iterate. This is used to bounditer_mut
and related methods.SingleEntityQueryData
- For anything that only accesses data from one entity. This is used to boundEntityRef::get_components
(see Unsound to callEntityRef::get_components
with aQueryData
that performs resource access #20315). It's also used to boundtransmute
at the moment, although we may be able to relax that later (see below, under Future Work).Note that
SingleEntityQueryData: IterQueryData
, since single-entity queries never alias data across entities, andReadOnlyQueryData: IterQueryData
, since it's always sound to alias read-only data.Here is a summary of the traits implemented by some representative
QueryData
:&T
&mut T
Parent<&T>
Parent<&mut T>
(&mut T, Parent<&U>)
Children<&mut T>
Alternatives
We could avoid the need for the
IterQueryData
trait by making it a requirement for allQueryData
. That would reduce the number of traits required, at the cost of making it impossible to supportQuery<Parent<&mut C>>
.Showcase
Here is an implementation of a
Related<R, D, F>
query using this PR that almost works:That has a few flaws, notably that it fails with
because
QueryData
requires thatState = ReadOnly::State
, butQueryState<D, F> != QueryState<D::ReadOnly, F>
.It's also impossible to implement
get_state
, because constructing aQueryState
requires reading theDefaultQueryFilters
resource, butget_state
can be called fromtransmute
with no access.I believe it's possible to resolve those issues, but I don't think those solutions belong in this PR.
Future Work
There is more to do here, but this PR is already pretty big. Future work includes:
WorldQuery
types for working with relationships #17647AssetChanged
to use nested queries for resource access, and stop tracking resource access separately inAccess
SingleEntityQueryData
bound on transmutes and joins. This will require checking that the nested query access is also a subset of the original access. Although unless we also solve the problem ofget_state
being impossible to implement, transmuting to a query with nested queries won't work anyway.QueryIter
by offering afn fetch_next(&self) -> D::Item<'_>
method and relaxing theIterQueryData
bound onQuery::into_iter
andQuery::iter_mut
. This would work similar toiter_many_mut
anditer_many_inner
.IterQueryData
bound onQuery::single_inner
,Query::single_mut
, andSingle<D, F>
. This seems like it should be straightforward, because the method only returns a single item. But the way it checks that there is only one item is by fetching the second one!