Skip to content
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

Query by Indexed Component Value #17608

Open
wants to merge 73 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
001b227
Initial Commit
bushrat011899 Jan 30, 2025
44408ba
Update Examples
bushrat011899 Jan 30, 2025
df42b47
Address CI
bushrat011899 Jan 30, 2025
8f6069a
Create system param and expose via `App`
bushrat011899 Jan 30, 2025
9c66cd2
CI
bushrat011899 Jan 30, 2025
5b10abb
Docs
bushrat011899 Jan 30, 2025
ef82a3d
Rename `IndexComponent`
bushrat011899 Jan 30, 2025
e372f5d
Update documentation for `IndexableComponent`
bushrat011899 Jan 30, 2025
908fd04
Added documentation to `Index`
bushrat011899 Jan 30, 2025
88c9b3b
Document manual `Default` implementation
bushrat011899 Jan 30, 2025
6fbc6c3
Just `unwrap`
bushrat011899 Jan 30, 2025
14e189e
Change from `TODO` to `PERF`
bushrat011899 Jan 30, 2025
44c2a3d
Add doc-test to `QueryByIndex`
bushrat011899 Jan 30, 2025
1778616
Add documentation and tests to the `index` module
bushrat011899 Jan 30, 2025
175c6dc
Update example description
bushrat011899 Jan 30, 2025
272c979
Switch to `i8` in example
bushrat011899 Jan 30, 2025
3f0c733
Improve documentation in `index` example
bushrat011899 Jan 30, 2025
6b2fe6e
Fix CI
bushrat011899 Jan 30, 2025
1d502e0
CI
bushrat011899 Jan 30, 2025
b18de48
Added benchmarks for iterating the index and updating it.
bushrat011899 Jan 30, 2025
beed9bb
CI
bushrat011899 Jan 30, 2025
a5e189b
Switch to cloning value in `on_replace`
bushrat011899 Jan 30, 2025
ec64913
Switch to internal liveness count instead of relying on querying
bushrat011899 Jan 30, 2025
9171430
Ensure initial state valid
bushrat011899 Jan 30, 2025
c5dac6f
CI
bushrat011899 Jan 30, 2025
776e0da
Refactor to separate concerns
bushrat011899 Jan 30, 2025
ea98ec8
Use component ID combinations instead of particular IDs
bushrat011899 Jan 31, 2025
4e5a6e0
Preallocate component IDs and increase the scale of the benchmark
bushrat011899 Jan 31, 2025
4e4e190
Refactor
bushrat011899 Jan 31, 2025
6b03687
Experiment: Maybe safe now?
bushrat011899 Jan 31, 2025
bdfd4ab
Formatting
bushrat011899 Jan 31, 2025
afa0441
Handle no-value-indexed
bushrat011899 Jan 31, 2025
4b158a0
Fix docs
bushrat011899 Jan 31, 2025
e758a62
Rewrite `index` example as Conway's Game of Life
bushrat011899 Jan 31, 2025
4d8f503
Tidy `QueryBuilder` usage
bushrat011899 Jan 31, 2025
69ab442
Change index marker name to be more descriptive
bushrat011899 Jan 31, 2025
b362d2d
Fix documentation and insure `mapping` is updated correctly
bushrat011899 Jan 31, 2025
32884e1
Switch to a list of spare slots for O(1) lookup
bushrat011899 Jan 31, 2025
2266934
Added automatic setup of `index` for the user and a `warn!` that they…
bushrat011899 Jan 31, 2025
5a1428b
Dumb import issue
bushrat011899 Jan 31, 2025
e2508dc
Remove redundant `.take(...)`
bushrat011899 Feb 1, 2025
64df47a
Fix CI
bushrat011899 Feb 1, 2025
5d67b38
For real this time
bushrat011899 Feb 1, 2025
7352492
Fix example
bushrat011899 Feb 1, 2025
94567fc
Refactor and mild performance improvement
bushrat011899 Feb 1, 2025
a5b1df2
Clippy
bushrat011899 Feb 1, 2025
de65088
Implement `Clone` for `QueryState`
bushrat011899 Feb 1, 2025
b1a322d
Clippy
bushrat011899 Feb 1, 2025
a024de0
Typo
bushrat011899 Feb 2, 2025
8a28afb
Better documentation in example
bushrat011899 Feb 2, 2025
400412f
Allow example on WASM
bushrat011899 Feb 2, 2025
d886a82
Refactor to expose index settings and custom storage backends.
bushrat011899 Feb 3, 2025
adf1132
Remove redundant transformations
bushrat011899 Feb 3, 2025
5d6606e
Remove redundant documentation
bushrat011899 Feb 3, 2025
afb3ad2
Merge remote-tracking branch 'upstream/main' into IndexComponents
bushrat011899 Feb 5, 2025
a003f40
Remove `add_index` and replace it with `add_index_with_options`
bushrat011899 Feb 5, 2025
7e6c1f9
Switch to `u8` for `address_space`
bushrat011899 Feb 5, 2025
beecc59
`fmt`
bushrat011899 Feb 5, 2025
8c2538e
Clarified `with(out)_states` documentation
bushrat011899 Feb 5, 2025
2f64169
Add some documentation to `QueryByIndexState`
bushrat011899 Feb 5, 2025
8f92f3b
Update `index` example documentation
bushrat011899 Feb 5, 2025
a5160a4
Remove redundant bounds
bushrat011899 Feb 5, 2025
4264e90
Better `address_space` documentation
bushrat011899 Feb 5, 2025
62f5fdb
Better explained when `SparseSet` may be more appropriate.
bushrat011899 Feb 5, 2025
733fdd3
Improved safety documentation for `QueryLens`
bushrat011899 Feb 5, 2025
514aadf
Document possibility of memory-leak-like behaviour when indexing freq…
bushrat011899 Feb 6, 2025
c54bec9
Docs
bushrat011899 Feb 6, 2025
70dd03a
Switch default `StorageType` to `SparseSet`
bushrat011899 Feb 7, 2025
21f6b40
Fix address-space-override
bushrat011899 Feb 7, 2025
cfcb2ae
Merge remote-tracking branch 'upstream/main' into IndexComponents
bushrat011899 Feb 7, 2025
d7fe564
Formatting
bushrat011899 Feb 7, 2025
37dfdaa
Fix from upstreaming
bushrat011899 Feb 7, 2025
6eed8b7
Fix default `address_space` value
bushrat011899 Feb 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,17 @@ description = "Demonstrates the creation and utility of immutable components"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "index"
path = "examples/ecs/index.rs"
doc-scrape-examples = true

[package.metadata.example.index]
name = "Indexes"
description = "Demonstrates the creation and utility of indexes"
category = "ECS (Entity Component System)"
wasm = false
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

[[example]]
name = "iter_combinations"
path = "examples/ecs/iter_combinations.rs"
Expand Down
37 changes: 37 additions & 0 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
event::{event_update_system, EventCursor},
index::IndexComponent,
intern::Interned,
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
Expand Down Expand Up @@ -1324,6 +1325,42 @@ impl App {
self.world_mut().add_observer(observer);
self
}

/// Creates and maintains an index for the provided immutable [`Component`] `C` using observers.
///
/// This allows querying by component _value_ to be very performant, at the expense of a small
/// amount of overhead when modifying the indexed component.
///
/// See [`IndexComponent`] for further details.
///
/// # Examples
///
/// ```rust
/// # use bevy_app::prelude::*;
/// # use bevy_ecs::prelude::*;
/// #
/// # let mut app = App::new();
/// #[derive(Component, PartialEq, Eq, Hash, Clone)]
/// #[component(immutable)]
/// enum FavoriteColor {
/// Red,
/// Green,
/// Blue,
/// }
///
/// app.add_index::<FavoriteColor>();
///
/// fn find_red_fans(mut query: QueryByIndex<FavoriteColor, Entity>) {
/// for entity in query.at(&FavoriteColor::Red).iter() {
/// println!("{entity:?} likes the color Red!");
/// }
/// }
/// # app.add_systems(Update, find_red_fans);
/// ```
pub fn add_index<C: IndexComponent>(&mut self) -> &mut Self {
self.world_mut().add_index::<C>();
self
}
}

type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
Expand Down
286 changes: 286 additions & 0 deletions crates/bevy_ecs/src/index/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
//! Provides indexing support for the ECS.
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

use crate::{
self as bevy_ecs,
component::{Component, ComponentDescriptor, ComponentId, Immutable, StorageType, Tick},
prelude::Trigger,
query::{QueryBuilder, QueryData, QueryFilter, QueryState, With},
system::{Commands, Query, Res, SystemMeta, SystemParam},
world::{unsafe_world_cell::UnsafeWorldCell, OnInsert, OnReplace, World},
};
use alloc::{format, vec::Vec};
use bevy_ecs_macros::Resource;
use bevy_platform_support::collections::HashMap;
use bevy_ptr::OwningPtr;
use core::{alloc::Layout, hash::Hash, ptr::NonNull};

/// Extension methods for [`World`] to assist with indexing components.
pub trait WorldIndexExtension {
/// Create and track an index for `C`.
/// This is required to use the [`QueryByIndex`] system parameter.
fn add_index<C: IndexComponent>(&mut self) -> &mut Self;
}

impl WorldIndexExtension for World {
fn add_index<C: IndexComponent>(&mut self) -> &mut Self {
if self.get_resource::<Index<C>>().is_none() {
self.add_observer(Index::<C>::on_insert);
self.add_observer(Index::<C>::on_replace);
}

self.init_resource::<Index<C>>();

self
}
}

/// Marker describing the requirements for a [`Component`] to be suitable for indexing.
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
pub trait IndexComponent: Component<Mutability = Immutable> + Eq + Hash + Clone {}
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

impl<C: Component<Mutability = Immutable> + Eq + Hash + Clone> IndexComponent for C {}

/// This system parameter allows querying by an indexed component value.
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
pub struct QueryByIndex<'world, C: IndexComponent, D: QueryData, F: QueryFilter = ()> {
world: UnsafeWorldCell<'world>,
state: Option<QueryState<D, (F, With<C>)>>,
last_run: Tick,
this_run: Tick,
index: Res<'world, Index<C>>,
}

impl<C: IndexComponent, D: QueryData, F: QueryFilter> QueryByIndex<'_, C, D, F> {
/// Return a [`Query`] only returning entities with a component `C` of the provided value.
///
/// # Examples
///
/// ```rust
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::new();
/// #[derive(Component, PartialEq, Eq, Hash, Clone)]
/// #[component(immutable)]
/// enum FavoriteColor {
/// Red,
/// Green,
/// Blue,
/// }
///
/// world.add_index::<FavoriteColor>();
///
/// fn find_red_fans(mut query: QueryByIndex<FavoriteColor, Entity>) {
/// for entity in query.at(&FavoriteColor::Red).iter() {
/// println!("{entity:?} likes the color Red!");
/// }
/// }
/// ```
pub fn at(&mut self, value: &C) -> Query<'_, '_, D, (F, With<C>)> {
Copy link
Member

Choose a reason for hiding this comment

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

Follow-up: this implies the ability to do between, which would be super sweet. Another argument for an Ord backend.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely! Generalised storage backends could allow some really nice methods here for sure.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd call this where personally but maybe at has a more ecs-y flavor that I'm not familiar with.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like at personally because it's like we're getting the query at a particular value. But I'd be happy to change it

Copy link
Contributor

@cBournhonesque cBournhonesque Jan 30, 2025

Choose a reason for hiding this comment

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

I prefer at to where personally. I was otherwise thinking of for, but I think at might be better

self.state = {
// SAFETY: Mutable references do not alias and will be dropped after this block
let mut builder = unsafe { QueryBuilder::new(self.world.world_mut()) };
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

match self.index.mapping.get(value).copied() {
// If there is a marker, restrict to it
Some(component_id) => builder.with_id(component_id),
// Otherwise, create a no-op query by including With<C> and Without<C>
None => builder.without::<C>(),
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
};

Some(builder.build())
};

// SAFETY: We have registered all of the query's world accesses,
// so the caller ensures that `world` has permission to access any
// world data that the query needs.
unsafe {
Query::new(
self.world,
self.state.as_mut().unwrap(),
self.last_run,
self.this_run,
)
}
}
}

// SAFETY: We rely on the known-safe implementations of `SystemParam` for `Res` and `Query`.
unsafe impl<C: IndexComponent, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam
for QueryByIndex<'_, C, D, F>
{
type State = (QueryState<D, (F, With<C>)>, ComponentId);
type Item<'w, 's> = QueryByIndex<'w, C, D, F>;

fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
let query_state = <Query<D, (F, With<C>)> as SystemParam>::init_state(world, system_meta);
let res_state = <Res<Index<C>> as SystemParam>::init_state(world, system_meta);

(query_state, res_state)
}

#[inline]
unsafe fn validate_param(
(query_state, res_state): &Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> bool {
let query_valid = <Query<D, (F, With<C>)> as SystemParam>::validate_param(
query_state,
system_meta,
world,
);
let res_valid =
<Res<Index<C>> as SystemParam>::validate_param(res_state, system_meta, world);

query_valid && res_valid
}

unsafe fn get_param<'world, 'state>(
(query_state, res_state): &'state mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'world>,
change_tick: Tick,
) -> Self::Item<'world, 'state> {
query_state.validate_world(world.id());

let index =
<Res<Index<C>> as SystemParam>::get_param(res_state, system_meta, world, change_tick);

QueryByIndex {
world,
state: None,
last_run: system_meta.last_run,
this_run: change_tick,
index,
}
}
}

#[derive(Resource)]
struct Index<C: IndexComponent> {
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
mapping: HashMap<C, ComponentId>,
spare_markers: Vec<ComponentId>,
}

impl<C: IndexComponent> Default for Index<C> {
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
fn default() -> Self {
Self {
mapping: Default::default(),
spare_markers: Default::default(),
}
}
}

impl<C: IndexComponent> Index<C> {
fn on_insert(trigger: Trigger<OnInsert, C>, mut commands: Commands) {
let entity = trigger.target();

commands.queue(move |world: &mut World| {
world.resource_scope::<Self, _>(|world, mut index| {
let Some(value) = world.get::<C>(entity) else {
return;
};

// Need a marker component for this entity
let component_id = match index.mapping.get(value).copied() {
// This particular value already has an assigned marker, reuse it.
Some(component_id) => component_id,
None => {
// Need to clone the index value for later lookups
let value = value.clone();

// Attempt to recycle an old marker.
// Otherwise, allocate a new one.
let component_id = index
.spare_markers
.pop()
.unwrap_or_else(|| Self::alloc_new_marker(world));

index.mapping.insert(value, component_id);
component_id
}
};

// SAFETY:
// - component_id is from the same world
// - NonNull::dangling() is appropriate for a ZST component
unsafe {
world
.entity_mut(entity)
.insert_by_id(component_id, OwningPtr::new(NonNull::dangling()));
}
});
});
}

fn on_replace(
trigger: Trigger<OnReplace, C>,
query: Query<&C>,
index: Res<Index<C>>,
mut commands: Commands,
) {
let entity = trigger.target();

let value = query
.get(entity)
.expect("observer should see a component in on_replace");
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

let component_id = index
.mapping
.get(value)
.copied()
.expect("somehow didn't track this value");
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

commands.queue(move |world: &mut World| {
world.resource_scope::<Self, _>(|world, mut index| {
// The old marker is no longer applicable since the value has changed/been removed.
world.entity_mut(entity).remove_by_id(component_id);

// On removal, we check if this was the last usage of this marker.
// If so, we can recycle it for a different value
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
let Self {
ref mut mapping,
ref mut spare_markers,
..
} = index.as_mut();

// TODO: It may be more performant to make a clone of the old value and lookup directly
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
// rather than iterating the whole map.
mapping.retain(|_key, &mut component_id_other| {
if component_id_other != component_id {
return true;
}

let mut builder = QueryBuilder::<(), With<C>>::new(world);
builder.with_id(component_id);
let mut query = builder.build();

// is_empty
if query.iter(world).next().is_none() {
spare_markers.push(component_id);
false
} else {
true
}
});
});
});
}

/// Creates a new marker component for this index.
/// It represents a ZST and is not tied to a particular value.
/// This allows moving entities into new archetypes based on the indexed value.
fn alloc_new_marker(world: &mut World) -> ComponentId {
// SAFETY:
// - ZST is Send + Sync
// - No drop function provided or required
let descriptor = unsafe {
ComponentDescriptor::new_with_layout(
format!("Index Marker ({})", core::any::type_name::<Self>()),
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
StorageType::Table,
Layout::new::<()>(),
None,
false,
)
};

world.register_component_with_descriptor(descriptor)
}
}
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub mod entity_disabling;
pub mod event;
pub mod hierarchy;
pub mod identifier;
pub mod index;
pub mod intern;
pub mod label;
pub mod name;
Expand Down Expand Up @@ -78,6 +79,7 @@ pub mod prelude {
entity::{Entity, EntityBorrow, EntityMapper},
event::{Event, EventMutator, EventReader, EventWriter, Events},
hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children},
index::{QueryByIndex, WorldIndexExtension},
name::{Name, NameOrEntity},
observer::{CloneEntityWithObserversExt, Observer, Trigger},
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ Example | Description
[Generic System](../examples/ecs/generic_system.rs) | Shows how to create systems that can be reused with different types
[Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities
[Immutable Components](../examples/ecs/immutable_components.rs) | Demonstrates the creation and utility of immutable components
[Indexes](../examples/ecs/index.rs) | Demonstrates the creation and utility of indexes
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
[Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results
[Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this.
[Observer Propagation](../examples/ecs/observer_propagation.rs) | Demonstrates event propagation with observers
Expand Down
Loading