-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Let query items borrow from query state to avoid needing to clone #15396
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
Conversation
That takes a mutable borrow of the QueryState, which will conflict once query items may borrow the state.
This allows them to borrow from the query state, which can avoid expensive clones in some cases.
IIUC, the lack of |
It might be. The issue is that the For what it's worth, I don't think |
crates/bevy_core/src/name.rs
Outdated
@@ -229,9 +229,9 @@ mod tests { | |||
let e2 = world.spawn(name.clone()).id(); | |||
let mut query = world.query::<NameOrEntity>(); | |||
let d1 = query.get(&world, e1).unwrap(); | |||
let d2 = query.get(&world, e2).unwrap(); |
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 needing to be moved is suspicious. Can you not call get multiple times on an immutable borrow of the world anymore?
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.
Not in this case, but I promise it's not as bad as it looks!
QueryState::get()
takes &mut self
so that it can call update_archetypes()
. And NameOrEntity
uses #[derive(QueryData)]
, which now generates an item struct that includes the 's
lifetime. So the item is treated as having a mutable borrow of the QueryState
.
This doesn't affect Query::get()
, which always takes &self
.
And this doesn't affect concrete types where QueryData::Item
doesn't include 's
. That is, it only affects generic parameters, #[derive(QueryData)]
types, and (in the future) FilteredEntityRef|Mut
. If we use let mut query = world.query::<(Option<&Name>, Entity)>();
here, it will compile.
And there is a workaround in this case, which is to call query.update_archetypes()
manually and then use query.get_manual()
, which takes &self
and so will allow overlapping borrows.
But, yeah, it is a breaking change 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.
After digesting this for a bit, maybe it isn't such a bad change. get
takes an &mut self
so hopefully it won't be that surprising for new users if you can't get multiple items out of it. And the workaround is more performant, so leading users to that is probably better.
But we should document the workaround on QueryState::get
and in the migration guide.
I haven't done a full review yet, but hopefully will get to that soon.
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.
Does this not also affect query iteration? As in, for the affected types, any item would have to be dropped before another can be retrieved?
(Like what happens in QueryManyIter::fetch_next()
, where item lifetimes are shrunk like this intentionally)
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.
Does this not also affect query iteration?
No, it doesn't. Query
has a &'s QueryState
, so we can copy that reference and hand it out with the full 's
lifetime. This change only affects methods that take &mut QueryState
.
(The issue with things like QueryManyIter::fetch_next()
and Query::iter_mut()
are that the UnsafeWorldCell<'w>
is acting like a &'w mut
, so we can't copy it and instead have to reborrow it with a shorter 'w
lifetime.)
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.
Okay, so this does not present an issue within individual iterators.
However, this will be a breaking change to more methods of QueryState
than what you've currently listed:
get
get_single
(no checked manual version)get_many
(no manual version)single
(no manual version)iter
iter_many
iter_combinations
(no manual version)par_iter
(no manual version)- and naturally, any method that takes at least
&'a QueryState
while the query items returned by one of the above is live.
The lack of manual versions can be considered API holes for some of the above, however not all of them (f.e. single
)
Do I understand correctly?
If so, I think the ability to mix these methods of QueryState
contains important use cases, and I would see those as more valuable than this change in its current form :/
Yeah, an additional collect would regress sort performance. That would make this change a perf trade-off, not a pure gain. By how much, would have to be benchmarked.
The tuple behavior is a bug: #14349. |
… they are necessary to work around borrowing QueryState mutably.
Yeah, there are definitely tradeoffs here! Given that (It feels like there must be some way to get the best of both options by dropping the
Sorry, I was wrong; it works fine. I confused the local |
To note, I think |
…yitem # Conflicts: # crates/bevy_core_pipeline/src/deferred/node.rs # crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs # crates/bevy_core_pipeline/src/prepass/node.rs # crates/bevy_ecs/src/query/iter.rs # crates/bevy_ecs/src/query/state.rs # crates/bevy_ecs/src/system/query.rs # crates/bevy_ecs/src/world/entity_ref.rs # crates/bevy_input_focus/src/lib.rs # crates/bevy_picking/src/events.rs
Add `const IS_READ_ONLY` to `NonReleaseQueryData`.
…yitem # Conflicts: # crates/bevy_ecs/src/query/state.rs # crates/bevy_ecs/src/system/query.rs
…yitem # Conflicts: # crates/bevy_ecs/src/name.rs # crates/bevy_ecs/src/query/fetch.rs # crates/bevy_ecs/src/query/state.rs # crates/bevy_ecs/src/system/query.rs # crates/bevy_ecs/src/system/system_param.rs # crates/bevy_ecs/src/world/entity_ref.rs
…yitem # Conflicts: # crates/bevy_ecs/src/system/query.rs # crates/bevy_ecs/src/world/unsafe_world_cell.rs # crates/bevy_input_focus/src/lib.rs
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.
Looks good! I'm no ecs expert so I can't comment on the architectural concerns, but it seems like most of that was addressed, and the migration isn't too bad. With #19602 on the horizon I think this has a lot more upsides than downsides
for #read_only_struct_name #user_ty_generics #user_where_clauses | ||
// Make these HRTBs with an unused lifetime parameter to allow trivial constraints | ||
// See https://github.com/rust-lang/rust/issues/48214 | ||
where #(for<'__a> #field_types: #path::query::QueryData<ReadOnly: #path::query::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.
Afaik blanket impls like this tend to give unhelpful errors when they go unfulfilled. In the future maybe we should add a #[query_data(release_state)]
and remove the per-field bounds, so any errors happen at the macro call site instead of at use sites?
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, I hadn't considered what the errors would look like! I'd still lean towards implementing this automatically for now, though. Very few APIs actually require it, so I'd expect it to be forgotten if it isn't automatic, but most QueryData
will implement it successfully.
IIUC, this change will mean that the consuming methods will not work for what is not Example of what fails: pub fn foo(query: Query<FilteredEntityRef>) {
for entity_ref in query {
// ...
}
} Note: I say "default" as in, what we intend for users to reach for first. Using |
That will still work fine! The issue is if you create a fn foo(query: Query<&Transform>) -> FilteredEntityRef {
let lens = query.transmute_lens_inner::<FilteredEntityRef>();
let query = lens.query_inner();
query.single_inner().unwrap()
} If we also do #18162, then the new consuming methods on |
You're right, I was missing that context.
Even if its not a breaking change, it is still inconsistent! |
Really excellent work. This is extremely complex, but you've done a great job navigating that in your code, docs, review comments and PR description. Thanks! |
# Objective Unblock #18162. #15396 added the `'s` lifetime to `QueryData::Item` to make it possible for query items to borrow from the state. The state isn't passed directly to `QueryData::fetch()`, so it also added the `'s` lifetime to `WorldQuery::Fetch` so that we can pass the borrows through there. Unfortunately, having `WorldQuery::Fetch` borrow from the state makes it impossible to have owned state, because we store the state and the `Fetch` in the same `struct` during iteration. ## Solution Undo the change to add the `'s` lifetime to `WorldQuery::Fetch`. Instead, add a `&'s Self::State` parameter to `QueryData::fetch()` and `QueryFilter::filter_fetch()` so that borrows from the state can be passed directly to query items. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Emerson Coskey <emerson@coskey.dev>
# Objective Unblock bevyengine#18162. bevyengine#15396 added the `'s` lifetime to `QueryData::Item` to make it possible for query items to borrow from the state. The state isn't passed directly to `QueryData::fetch()`, so it also added the `'s` lifetime to `WorldQuery::Fetch` so that we can pass the borrows through there. Unfortunately, having `WorldQuery::Fetch` borrow from the state makes it impossible to have owned state, because we store the state and the `Fetch` in the same `struct` during iteration. ## Solution Undo the change to add the `'s` lifetime to `WorldQuery::Fetch`. Instead, add a `&'s Self::State` parameter to `QueryData::fetch()` and `QueryFilter::filter_fetch()` so that borrows from the state can be passed directly to query items. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Emerson Coskey <emerson@coskey.dev>
…wing `Access` (#20111) # Objective Improve the performance of queries using `FilteredEntityRef`, `FilteredEntityMut`, `EntityRefExcept`, and `EntityMutExcept`. In particular, this appears to speed up `bevy_animation::animate_targets` by 10% in many-foxes. `FilteredEntity(Ref|Mut)` needs to store an `Access` to determine which components may be accessed. Prior to #15396, this required cloning the `Access` for each instance. Now, we can borrow the `Access` from the query state and make cheap pointer copies. `Entity(Ref|Mut)Except` avoided needing to clone an `Access` by calling functions on the `Bundle` trait. Unfortunately, that meant we needed to convert from a type to a `ComponentId` for every component in the bundle on every check. Now, we can do those conversions up front and pass references to an `Access`. Finally, fix a bug where `Entity(Ref|Mut)Except` would not initialize their components during `init_state`. I noticed this while updating `init_state` and fixed it while I was there. That was normally harmless because the components would be registered elsewhere, but a system like `fn system(_q1: Query<EntityMutExcept<C>>, _q2: Query<&mut C>) {}` would fail to find the `ComponentId` for `C` and not exclude it from the access for `q1`, and then panic with conflicting access from `q2`. ## Solution Change `FilteredEntityRef` and `FilteredEntityMut` to store `&'s Access` instead of `Access`, and change `EntityRefExcept` and `EntityMutExcept` to store an extra `&'s Access`. This adds the `'s` lifetime to those four types, and most changes are adding lifetimes as appropriate. Change the `WorldQuery::State` for `Entity(Ref|Mut)Except` to store an `Access` that can be borrowed from, replacing the `SmallVec<[ComponentId; 4]>` that was used only to set the query access. To support the conversions from `EntityRef` and `EntityMut`, we need to be able to create a `&'static Access` for read-all or write-all. I could not change `fn read_all_components()` to be `const` because it called the non-`const` `FixedBitSet::clear()`, so I created separate constructor functions. ## Testing Ran `cargo run --example many_foxes --features bevy/trace_tracy --release` before and after, and compared the results of `animate_targets`, since that is the only in-engine use of `EntityMutExcept` and was the motivation for creating it. Yellow is this PR, red is main: <img width="695" height="690" alt="image" src="https://github.com/user-attachments/assets/24531a3f-65bf-46d0-baa5-29ea9e56b16a" />
Objective
Improve the performance of
FilteredEntity(Ref|Mut)
andEntity(Ref|Mut)Except
.FilteredEntityRef
needs anAccess<ComponentId>
to determine what components it can access. There is one stored in the query state, but query items cannot borrow from the state, so it has toclone()
the access for each row. Cloning the access involves memory allocations and can be expensive.Solution
Let query items borrow from their query state.
Add an
's
lifetime toWorldQuery::Item
andWorldQuery::Fetch
, similar to the one inSystemParam
, and provide&'s Self::State
to the fetch so that it can borrow from the state.Unfortunately, there are a few cases where we currently return query items from temporary query states: the sorted iteration methods create a temporary state to query the sort keys, and the
EntityRef::components<Q>()
methods create a temporary state for their query.To allow these to continue to work with most
QueryData
implementations, introduce a new subtraitReleaseStateQueryData
that converts aQueryItem<'w, 's>
toQueryItem<'w, 'static>
, and is implemented for everything exceptFilteredEntity(Ref|Mut)
andEntity(Ref|Mut)Except
.#[derive(QueryData)]
will generateReleaseStateQueryData
implementations that apply when all of the subqueries implementReleaseStateQueryData
.This PR does not actually change the implementation of
FilteredEntity(Ref|Mut)
orEntity(Ref|Mut)Except
! That will be done as a follow-up PR so that the changes are easier to review. I have pushed the changes as chescock#5.Testing
I ran performance traces of many_foxes, both against main and against chescock#5, both including #15282. These changes do appear to make generalized animation a bit faster:
(Red is main, yellow is chescock#5)

Migration Guide
The
WorldQuery::Item
andWorldQuery::Fetch
associated types and theQueryItem
andROQueryItem
type aliases now have an additional lifetime parameter corresponding to the's
lifetime inQuery
. Manual implementations ofWorldQuery
will need to update the method signatures to include the new lifetimes. Other uses of the types will need to be updated to include a lifetime parameter, although it can usually be passed as'_
. In particular,ROQueryItem
is used when implementingRenderCommand
.Before:
After:
Methods on
QueryState
that take&mut self
may now result in conflicting borrows if the query items capture the lifetime of the mutable reference. This affectsget()
,iter()
, and others. To fix the errors, first callQueryState::update_archetypes()
, and then replace a callstate.foo(world, param)
withstate.query_manual(world).foo_inner(param)
. Alternately, you may be able to restructure the code to callstate.query(world)
once and then make multiple calls using theQuery
.Before:
After: