Skip to content

Expose more data from FilteredAccess#23384

Merged
alice-i-cecile merged 7 commits intobevyengine:mainfrom
chescock:ComponentSet
Mar 19, 2026
Merged

Expose more data from FilteredAccess#23384
alice-i-cecile merged 7 commits intobevyengine:mainfrom
chescock:ComponentSet

Conversation

@chescock
Copy link
Copy Markdown
Contributor

Objective

Expose more data from FilteredAccess to allow third-party crates to inspect queries. In particular, expose required and filter_sets, which will allow third-party implementations of query observers (#20817) to determine what component observers they will need.

Solution

Add pub methods to expose required and filter_sets, make AccessFilters pub, and add methods to expose with and without.

Add a ComponentIdSet type that wraps FixedBitSet. This allows us to expose the efficient set operations like union_with that would not be available if we returned a simple Iterator<Item = ComponentId>. And not exposing the FixedBitSet directly avoids forcing users to manually convert between usize and ComponentId, and will let us change the implementation of the set (as in #18955) or representation of ComponentId without changing the public API. It also simplifies the internals a bit!

Showcase

Here is how to calculate the component observers required for a query observer in an implementation of #20817 using these APIs:

fn components_for_observers(
    access: &FilteredAccess,
) -> (&ComponentIdSet, ComponentIdSet, ComponentIdSet) {
    // We need `insert` observers for any component we read,
    // since the value has changed.
    // Unbounded access (`EntityRef`) is not supported,
    // since we cannot add observers to all possible components.
    let insert = access
        .access()
        .try_reads_and_writes()
        .expect("Query observers do not support unbounded access");

    // We need `add` observers for any component with archetypal access,
    // since `Has` may have changed,
    let mut add = access.access().archetypal().clone();
    // and any component with `With` filters,
    // since the query may start matching,
    for filter_set in access.filter_sets() {
        add.union_with(&filter_set.with());
    }
    // but not for any component already covered by `insert` observers
    add.difference_with(insert);

    // We need `remove` observers for any component we read or write,
    // since `Option<&T>` may have changed,
    let mut remove = insert.clone();
    // and for any component with archetypal access,
    // since `Has` may have changed,
    remove.union_with(access.access().archetypal());
    // and any component with `Without` filters,
    // since the query may start matching,
    for filter_set in access.filter_sets() {
        add.union_with(&filter_set.without());
    }
    // but not for any required component,
    // since it is guaranteed not to match after they are removed
    // (otherwise every `&T` would add a `remove` observer!)
    remove.difference_with(access.required());

    (insert, add, remove)
}

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change X-Uncontroversial This work is generally agreed upon D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Mar 16, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in ECS Mar 16, 2026
@alice-i-cecile alice-i-cecile modified the milestone: 0.19 Mar 16, 2026
Copy link
Copy Markdown
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

I much prefer the semantic newtype over ComponentIdSet: this would be worth doing on its own.

This information is fine to expose, and this is a good way to do so.

I would be happy if there were tests for ComponentIdSet, but I won't block on it :)

@chescock
Copy link
Copy Markdown
Contributor Author

I much prefer the semantic newtype over ComponentIdSet: this would be worth doing on its own.

Yeah, I wouldn't have thought to do it without the motivation of exposing it more publicly, but once I thought of it I liked it even internally!

I would be happy if there were tests for ComponentIdSet, but I won't block on it :)

Oh, fine, I guess if I'm going to claim that this will let us change the implementation, then I need to actually back it up by having tests :). I'll try to add some on Wednesday!

Copy link
Copy Markdown
Contributor

@Freyja-moth Freyja-moth left a comment

Choose a reason for hiding this comment

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

Can't see anything wrong and the newtype is appreciated

@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 Mar 19, 2026
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Mar 19, 2026
Merged via the queue into bevyengine:main with commit 9b91b56 Mar 19, 2026
40 checks passed
@github-project-automation github-project-automation bot moved this from Needs SME Triage to Done in ECS Mar 19, 2026
splo pushed a commit to splo/bevy that referenced this pull request Mar 31, 2026
# Objective

Expose more data from `FilteredAccess` to allow third-party crates to
inspect queries. In particular, expose `required` and `filter_sets`,
which will allow third-party implementations of query observers (bevyengine#20817)
to determine what component observers they will need.

## Solution

Add `pub` methods to expose `required` and `filter_sets`, make
`AccessFilters` `pub`, and add methods to expose `with` and `without`.

Add a `ComponentIdSet` type that wraps `FixedBitSet`. This allows us to
expose the efficient set operations like `union_with` that would not be
available if we returned a simple `Iterator<Item = ComponentId>`. And
not exposing the `FixedBitSet` directly avoids forcing users to manually
convert between `usize` and `ComponentId`, and will let us change the
implementation of the set (as in bevyengine#18955) or representation of
`ComponentId` without changing the public API. It also simplifies the
internals a bit!


## Showcase

Here is how to calculate the component observers required for a query
observer in an implementation of bevyengine#20817 using these APIs:

```rust
fn components_for_observers(
    access: &FilteredAccess,
) -> (&ComponentIdSet, ComponentIdSet, ComponentIdSet) {
    // We need `insert` observers for any component we read,
    // since the value has changed.
    // Unbounded access (`EntityRef`) is not supported,
    // since we cannot add observers to all possible components.
    let insert = access
        .access()
        .try_reads_and_writes()
        .expect("Query observers do not support unbounded access");

    // We need `add` observers for any component with archetypal access,
    // since `Has` may have changed,
    let mut add = access.access().archetypal().clone();
    // and any component with `With` filters,
    // since the query may start matching,
    for filter_set in access.filter_sets() {
        add.union_with(&filter_set.with());
    }
    // but not for any component already covered by `insert` observers
    add.difference_with(insert);

    // We need `remove` observers for any component we read or write,
    // since `Option<&T>` may have changed,
    let mut remove = insert.clone();
    // and for any component with archetypal access,
    // since `Has` may have changed,
    remove.union_with(access.access().archetypal());
    // and any component with `Without` filters,
    // since the query may start matching,
    for filter_set in access.filter_sets() {
        add.union_with(&filter_set.without());
    }
    // but not for any required component,
    // since it is guaranteed not to match after they are removed
    // (otherwise every `&T` would add a `remove` observer!)
    remove.difference_with(access.required());

    (insert, add, remove)
}
```

---------

Co-authored-by: Alice Cecile <[email protected]>
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-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants