Skip to content

Conversation

chescock
Copy link
Contributor

Objective

For #17647, we want to create a QueryData that can follow a relation and query data from an entity's parent. If the parent does not have the queried data, the child entity should be skipped in the query. However, there is no way to tell from the child's archetype whether the parent will match! So, we need to support non-archetypal QueryData, just as we support non-archetypal QueryFilters for Added and Changed.

That is, if Query<Parent<&T>> yields &T, and we do:

let parent1 = world.spawn(T).id();
let child1 = world.spawn(ChildOf(parent1));
let parent2 = world.spawn(()).id();
let child2 = world.spawn(ChildOf(parent2));

let query = world.query::<Parent<&T>>();

then query must yield a row for child1 but not for child2, even though they have the same archetype.

Solution

Change QueryData::fetch to return Option so that entities can be filtered during fetching by returning None.

To support ExactSizeIterator, introduce an ArchetypeQueryData trait and an QueryData::IS_ARCHETYPAL associated constant, similar to ArchetypeFilter and QueryFilter::IS_ARCHETYPAL. Implement this trait on existing QueryData types. Modify ExactSizeIterator implementations to require D: ArchetypeQueryData, and the size_hint() methods to return a minimum size of 0 if !D::IS_ARCHETYPAL.

Alternatives

We could do nothing here, and have Query<Parent<&T>> yield Option<&T>. That makes the API less convenient, though. Note that if one wants to query for Option, they can use either Query<Option<Parent<&T>> or Query<Parent<Option<&T>>, depending on whether they want to include entities with no parent.

Another option is to re-use the ArchetypeFilter trait instead of introducing a new one. There are no places where we want to abstract over both, however, and it would require writing bounds like D: QueryData + ArchetypeFilter, F: QueryFilter + ArchetypeFilter instead of simply D: ArchetypeQueryData, F: ArchetypeFilter.

@chescock chescock added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events 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 17, 2025
entity: Entity,
table_row: TableRow,
) -> Self::Item<'w, 's>;
) -> Option<Self::Item<'w, 's>>;
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this adding branching into every fetch? I suppose in the common cases it might get optimized out. We should check the assembly and see.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

isn't this adding branching into every fetch? I suppose in the common cases it might get optimized out. We should check the assembly and see.

Yeah, I'm hoping and expecting that the optimizer will remove them after inlining a function that always returns Some, but I haven't actually tested it yet. I'll do some tests with cargo asm and report back!

@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Oct 17, 2025
let entity = self.entity_iter.next()?;
// SAFETY: `entity` is passed from `entity_iter` the first time.
unsafe { self.fetch_next(entity).into() }
while let Some(entity) = self.entity_iter.next() {
Copy link
Contributor

Choose a reason for hiding this comment

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

If i understand correctly, this while let Some() isn't strictly necessary since the only public way of getting QuerySortedIter is via the various QueryIter.sort() methods, which provide an entity_list containing only items where fetch returns Some(D::Item) (and not None).

However it's still good to have as a precaution for internal code if we manually create a QuerySortedIter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's actually necessary here. The sort() methods evaluate the QueryFilter, but they don't evaluate the QueryData. So now that the QueryData can perform filtering, it's possible for this to need to loop.

To run through an example: If you have Query<(&mut A, Parent<&B>), Changed<C>)> and do query.iter().sort::<&A>(), it will internally create a Query<(&A, Entity), Changed<C>>. That means it's already evaluated the Changed<C> filter while creating the entity_list. But it hasn't evaluated the data, so if Parent<&B> filters out an entity then we need a loop here. (And we can't just internally create a Query<(D, L), F>, since for this example that would try to query ((&mut A, Parent<&B>), &A), which has conflicting access.)

/// [`QueryIter`](crate::query::QueryIter) that contains archetype-level filters.
///
/// The trait must only be implemented for query data where its corresponding [`QueryData::IS_ARCHETYPAL`] is [`prim@true`].
pub trait ArchetypeQueryData: QueryData {}
Copy link
Contributor

Choose a reason for hiding this comment

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

This would not be needed if we could use nightly and const_generics, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would not be needed if we could use nightly and const_generics, right?

Oh, I don't know! ... Yup, it looks like #![feature(associated_const_equality)] would let us get rid of this, along with ArchetypeFilter and ReadOnlyQueryData!

I think there's also a proposal for #![feature(downcast_trait)] that would go the other direction and let us remove the associated constants in favor of the traits.

@cBournhonesque
Copy link
Contributor

cBournhonesque commented Oct 18, 2025

I ran a benchmark (on laptop):

group                                         after                                  before
-----                                         -----                                  ------
empty_archetypes/iter/10                      1.00      7.7±0.05µs        ? ?/sec    1.02      7.9±0.46µs        ? ?/sec
empty_archetypes/iter/100                     1.04      8.4±1.41µs        ? ?/sec    1.00      8.1±0.37µs        ? ?/sec
empty_archetypes/iter/1000                    1.00      8.2±0.39µs        ? ?/sec    1.02      8.4±0.18µs        ? ?/sec
empty_archetypes/iter/10000                   1.06     11.3±2.08µs        ? ?/sec    1.00     10.6±0.73µs        ? ?/sec
events_iter/size_16_events_100                1.08     60.3±3.70ns        ? ?/sec    1.00     55.9±1.13ns        ? ?/sec
events_iter/size_16_events_1000               1.06   543.4±27.97ns        ? ?/sec    1.00   513.4±22.81ns        ? ?/sec
events_iter/size_16_events_10000              1.03      5.3±0.19µs        ? ?/sec    1.00      5.2±0.27µs        ? ?/sec
events_iter/size_4_events_100                 1.05     63.3±7.21ns        ? ?/sec    1.00     60.3±8.39ns        ? ?/sec
events_iter/size_4_events_1000                1.01   538.8±21.85ns        ? ?/sec    1.00   534.2±10.39ns        ? ?/sec
events_iter/size_4_events_10000               1.14      5.7±0.47µs        ? ?/sec    1.00      5.0±0.08µs        ? ?/sec
events_iter/size_512_events_100               1.02     58.5±2.47ns        ? ?/sec    1.00     57.6±2.06ns        ? ?/sec
events_iter/size_512_events_1000              1.00   531.8±25.83ns        ? ?/sec    1.01   536.6±48.19ns        ? ?/sec
events_iter/size_512_events_10000             1.03      5.3±0.19µs        ? ?/sec    1.00      5.2±0.22µs        ? ?/sec
iter_fragmented(4096)_empty/foreach_sparse    1.02      8.2±0.52µs        ? ?/sec    1.00      8.1±0.12µs        ? ?/sec
iter_fragmented(4096)_empty/foreach_table     1.00      2.1±0.19µs        ? ?/sec    1.02      2.2±0.20µs        ? ?/sec
iter_fragmented/base                          1.11   266.8±26.12ns        ? ?/sec    1.00    240.9±4.87ns        ? ?/sec
iter_fragmented/foreach                       1.01     78.9±4.20ns        ? ?/sec    1.00     78.4±3.63ns        ? ?/sec
iter_fragmented/foreach_wide                  1.00      2.2±0.02µs        ? ?/sec    1.07      2.4±0.21µs        ? ?/sec
iter_fragmented/wide                          1.02      2.6±0.24µs        ? ?/sec    1.00      2.6±0.06µs        ? ?/sec
iter_fragmented_sparse/base                   1.07      5.1±0.06ns        ? ?/sec    1.00      4.7±0.03ns        ? ?/sec
iter_fragmented_sparse/foreach                1.00      3.8±0.11ns        ? ?/sec    1.06      4.0±0.04ns        ? ?/sec
iter_fragmented_sparse/foreach_wide           1.00     31.9±2.32ns        ? ?/sec    1.04     33.1±2.29ns        ? ?/sec
iter_fragmented_sparse/wide                   1.02     37.4±2.71ns        ? ?/sec    1.00     36.6±0.84ns        ? ?/sec
iter_simple/base                              1.03      6.0±0.37µs        ? ?/sec    1.00      5.9±0.16µs        ? ?/sec
iter_simple/foreach                           1.01      3.3±0.08µs        ? ?/sec    1.00      3.3±0.02µs        ? ?/sec
iter_simple/foreach_hybrid                    1.03     10.2±0.64µs        ? ?/sec    1.00     10.0±0.19µs        ? ?/sec
iter_simple/foreach_sparse_set                1.01     14.4±0.66µs        ? ?/sec    1.00     14.3±0.61µs        ? ?/sec
iter_simple/foreach_wide                      1.00     15.4±0.44µs        ? ?/sec    1.01     15.5±0.96µs        ? ?/sec
iter_simple/foreach_wide_sparse_set           1.29   354.4±69.46µs        ? ?/sec    1.00   274.0±56.14µs        ? ?/sec
iter_simple/sparse_set                        1.05     15.4±1.75µs        ? ?/sec    1.00     14.7±0.60µs        ? ?/sec
iter_simple/system                            1.01      5.9±0.45µs        ? ?/sec    1.00      5.8±0.03µs        ? ?/sec
iter_simple/wide                              1.00     52.6±4.16µs        ? ?/sec    1.00     52.5±3.76µs        ? ?/sec
iter_simple/wide_sparse_set                   1.05   418.7±54.28µs        ? ?/sec    1.00   399.2±65.53µs        ? ?/sec
par_iter_simple/hybrid                        1.00     55.8±3.46µs        ? ?/sec    1.01     56.2±1.57µs        ? ?/sec
par_iter_simple/with_0_fragment               1.00     29.2±1.41µs        ? ?/sec    1.00     29.2±0.22µs        ? ?/sec
par_iter_simple/with_1000_fragment            1.00     36.6±2.72µs        ? ?/sec    1.01     37.1±0.28µs        ? ?/sec
par_iter_simple/with_100_fragment             1.01     30.5±2.54µs        ? ?/sec    1.00     30.2±0.54µs        ? ?/sec
par_iter_simple/with_10_fragment              1.02     30.5±4.57µs        ? ?/sec    1.00     30.0±1.72µs        ? ?/sec
world_query_iter/50000_entities_sparse        1.00     22.7±0.74µs        ? ?/sec    1.44     32.6±0.44µs        ? ?/sec
world_query_iter/50000_entities_table         1.02     16.0±0.52µs        ? ?/sec    1.00     15.7±0.26µs        ? ?/sec

Seems to be within noise.
I tried to look at the asm for fetch and got

LBB518_2:
        ldr x8, [sp, #24]
                // src/query/mod.rs:49
                if let Some(inner) = self {
        ldur x10, [x29, #-32]
        str x10, [sp, #16]
        ldur x9, [x29, #-24]
        str x9, [sp, #8]
        stur x10, [x29, #-16]
        stur x9, [x29, #-8]
                // src/query/fetch.rs:1658
                let table = unsafe { table.debug_checked_unwrap() };
        str x10, [sp, #56]
        str x9, [sp, #64]
                // src/query/fetch.rs:1660
                let item = unsafe { table.get(table_row.index()) };
        ldr w0, [x8]
        bl bevy_ecs::storage::table::TableRow::index
        ldr x1, [sp, #8]
        mov x2, x0
        ldr x0, [sp, #16]
        bl bevy_ptr::ThinSlicePtr<T>::get
        mov x8, x0
        stur x8, [x29, #-40]
                // src/query/fetch.rs:1661
                item.deref()
        bl <&core::cell::UnsafeCell<T> as bevy_ptr::UnsafeCellDeref<T>>::deref
                // src/query/fetch.rs:1662
                },
        ldp x29, x30, [sp, #112]
        add sp, sp, #128
        ret

But i don't know how to interpret the results; an LLM said that the Option was indeed optimized away

@cBournhonesque
Copy link
Contributor

Previous approval was a mistake, but I don't know how to remove it..

let entity = self.entity_iter.next_back()?;
// SAFETY: `entity` is passed from `entity_iter` the first time.
unsafe { self.fetch_next(entity).into() }
while let Some(entity) = self.entity_iter.next_back() {
Copy link
Contributor

@Victoronz Victoronz Oct 19, 2025

Choose a reason for hiding this comment

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

I find these while let loops a bit worrying.
One of the big problems of mutable iter_many iteration is that we cannot produce an iterator out of a while let loop.
Here, I believe this works because the assumption is that Entity is Copy.
But if Item = Entity were to switch over to Item: EntityEquivalent, then we could run into the same lifetime problem here.
Sure, we could make the decision that fetching should only ever involve bare entities, but that feels like a decision we do not yet have the data to make.

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-Feature A new feature, making something new possible 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants