Skip to content

Commit 9cd3165

Browse files
hymmalice-i-cecile
andauthored
Query Joins (#11535)
# Objective - Add a way to combine 2 queries together in a similar way to `Query::transmute_lens` - Fixes #1658 ## Solution - Use a similar method to query transmute, but take the intersection of matched archetypes between the 2 queries and the union of the accesses to create the new underlying QueryState. --- ## Changelog - Add query joins --------- Co-authored-by: Alice Cecile <[email protected]>
1 parent bc1073e commit 9cd3165

File tree

3 files changed

+282
-3
lines changed

3 files changed

+282
-3
lines changed

crates/bevy_ecs/src/archetype.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ impl Archetype {
594594
///
595595
/// This is used in archetype update methods to limit archetype updates to the
596596
/// ones added since the last time the method ran.
597-
#[derive(Debug, Copy, Clone)]
597+
#[derive(Debug, Copy, Clone, PartialEq)]
598598
pub struct ArchetypeGeneration(ArchetypeId);
599599

600600
impl ArchetypeGeneration {

crates/bevy_ecs/src/query/state.rs

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
storage::{SparseSetIndex, TableId},
1111
world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId},
1212
};
13+
use bevy_utils::tracing::warn;
1314
#[cfg(feature = "trace")]
1415
use bevy_utils::tracing::Span;
1516
use fixedbitset::FixedBitSet;
@@ -374,7 +375,11 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
374375
NewF::update_component_access(&filter_state, &mut filter_component_access);
375376

376377
component_access.extend(&filter_component_access);
377-
assert!(component_access.is_subset(&self.component_access), "Transmuted state for {} attempts to access terms that are not allowed by original state {}.", std::any::type_name::<(NewD, NewF)>(), std::any::type_name::<(D, F)>() );
378+
assert!(
379+
component_access.is_subset(&self.component_access),
380+
"Transmuted state for {} attempts to access terms that are not allowed by original state {}.",
381+
std::any::type_name::<(NewD, NewF)>(), std::any::type_name::<(D, F)>()
382+
);
378383

379384
QueryState {
380385
world_id: self.world_id,
@@ -396,6 +401,114 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
396401
}
397402
}
398403

404+
/// Use this to combine two queries. The data accessed will be the intersection
405+
/// of archetypes included in both queries. This can be useful for accessing a
406+
/// subset of the entities between two queries.
407+
///
408+
/// You should not call `update_archetypes` on the returned `QueryState` as the result
409+
/// could be unpredictable. You might end up with a mix of archetypes that only matched
410+
/// the original query + archetypes that only match the new `QueryState`. Most of the
411+
/// safe methods on `QueryState` call [`QueryState::update_archetypes`] internally, so
412+
/// this is best used through a `Query`.
413+
///
414+
/// ## Performance
415+
///
416+
/// This will have similar performance as constructing a new `QueryState` since much of internal state
417+
/// needs to be reconstructed. But it will be a little faster as it only needs to compare the intersection
418+
/// of matching archetypes rather than iterating over all archetypes.
419+
///
420+
/// ## Panics
421+
///
422+
/// Will panic if `NewD` contains accesses not in `Q` or `OtherQ`.
423+
pub fn join<OtherD: QueryData, NewD: QueryData>(
424+
&self,
425+
world: &World,
426+
other: &QueryState<OtherD>,
427+
) -> QueryState<NewD, ()> {
428+
self.join_filtered::<_, (), NewD, ()>(world, other)
429+
}
430+
431+
/// Use this to combine two queries. The data accessed will be the intersection
432+
/// of archetypes included in both queries.
433+
///
434+
/// ## Panics
435+
///
436+
/// Will panic if `NewD` or `NewF` requires accesses not in `Q` or `OtherQ`.
437+
pub fn join_filtered<
438+
OtherD: QueryData,
439+
OtherF: QueryFilter,
440+
NewD: QueryData,
441+
NewF: QueryFilter,
442+
>(
443+
&self,
444+
world: &World,
445+
other: &QueryState<OtherD, OtherF>,
446+
) -> QueryState<NewD, NewF> {
447+
if self.world_id != other.world_id {
448+
panic!("Joining queries initialized on different worlds is not allowed.");
449+
}
450+
451+
let mut component_access = FilteredAccess::default();
452+
let mut new_fetch_state = NewD::get_state(world)
453+
.expect("Could not create fetch_state, Please initialize all referenced components before transmuting.");
454+
let new_filter_state = NewF::get_state(world)
455+
.expect("Could not create filter_state, Please initialize all referenced components before transmuting.");
456+
457+
NewD::set_access(&mut new_fetch_state, &self.component_access);
458+
NewD::update_component_access(&new_fetch_state, &mut component_access);
459+
460+
let mut new_filter_component_access = FilteredAccess::default();
461+
NewF::update_component_access(&new_filter_state, &mut new_filter_component_access);
462+
463+
component_access.extend(&new_filter_component_access);
464+
465+
let mut joined_component_access = self.component_access.clone();
466+
joined_component_access.extend(&other.component_access);
467+
468+
assert!(
469+
component_access.is_subset(&joined_component_access),
470+
"Joined state for {} attempts to access terms that are not allowed by state {} joined with {}.",
471+
std::any::type_name::<(NewD, NewF)>(), std::any::type_name::<(D, F)>(), std::any::type_name::<(OtherD, OtherF)>()
472+
);
473+
474+
if self.archetype_generation != other.archetype_generation {
475+
warn!("You have tried to join queries with different archetype_generations. This could lead to unpredictable results.");
476+
}
477+
478+
// take the intersection of the matched ids
479+
let matched_tables: FixedBitSet = self
480+
.matched_tables
481+
.intersection(&other.matched_tables)
482+
.collect();
483+
let matched_table_ids: Vec<TableId> =
484+
matched_tables.ones().map(TableId::from_usize).collect();
485+
let matched_archetypes: FixedBitSet = self
486+
.matched_archetypes
487+
.intersection(&other.matched_archetypes)
488+
.collect();
489+
let matched_archetype_ids: Vec<ArchetypeId> =
490+
matched_archetypes.ones().map(ArchetypeId::new).collect();
491+
492+
QueryState {
493+
world_id: self.world_id,
494+
archetype_generation: self.archetype_generation,
495+
matched_table_ids,
496+
matched_archetype_ids,
497+
fetch_state: new_fetch_state,
498+
filter_state: new_filter_state,
499+
component_access: joined_component_access,
500+
matched_tables,
501+
matched_archetypes,
502+
archetype_component_access: self.archetype_component_access.clone(),
503+
#[cfg(feature = "trace")]
504+
par_iter_span: bevy_utils::tracing::info_span!(
505+
"par_for_each",
506+
query = std::any::type_name::<NewD>(),
507+
filter = std::any::type_name::<NewF>(),
508+
),
509+
}
510+
}
511+
399512
/// Gets the query result for the given [`World`] and [`Entity`].
400513
///
401514
/// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries.
@@ -1658,4 +1771,74 @@ mod tests {
16581771

16591772
assert_eq!(entity_a, detection_query.single(&world));
16601773
}
1774+
1775+
#[test]
1776+
#[should_panic(
1777+
expected = "Transmuted state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed<bevy_ecs::query::state::tests::B>) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())."
1778+
)]
1779+
fn cannot_transmute_changed_without_access() {
1780+
let mut world = World::new();
1781+
world.init_component::<A>();
1782+
world.init_component::<B>();
1783+
let query = QueryState::<&A>::new(&mut world);
1784+
let _new_query = query.transmute_filtered::<Entity, Changed<B>>(&world);
1785+
}
1786+
1787+
#[test]
1788+
fn join() {
1789+
let mut world = World::new();
1790+
world.spawn(A(0));
1791+
world.spawn(B(1));
1792+
let entity_ab = world.spawn((A(2), B(3))).id();
1793+
world.spawn((A(4), B(5), C(6)));
1794+
1795+
let query_1 = QueryState::<&A, Without<C>>::new(&mut world);
1796+
let query_2 = QueryState::<&B, Without<C>>::new(&mut world);
1797+
let mut new_query: QueryState<Entity, ()> = query_1.join_filtered(&world, &query_2);
1798+
1799+
assert_eq!(new_query.single(&world), entity_ab);
1800+
}
1801+
1802+
#[test]
1803+
fn join_with_get() {
1804+
let mut world = World::new();
1805+
world.spawn(A(0));
1806+
world.spawn(B(1));
1807+
let entity_ab = world.spawn((A(2), B(3))).id();
1808+
let entity_abc = world.spawn((A(4), B(5), C(6))).id();
1809+
1810+
let query_1 = QueryState::<&A>::new(&mut world);
1811+
let query_2 = QueryState::<&B, Without<C>>::new(&mut world);
1812+
let mut new_query: QueryState<Entity, ()> = query_1.join_filtered(&world, &query_2);
1813+
1814+
assert!(new_query.get(&world, entity_ab).is_ok());
1815+
// should not be able to get entity with c.
1816+
assert!(new_query.get(&world, entity_abc).is_err());
1817+
}
1818+
1819+
#[test]
1820+
#[should_panic(expected = "Joined state for (&bevy_ecs::query::state::tests::C, ()) \
1821+
attempts to access terms that are not allowed by state \
1822+
(&bevy_ecs::query::state::tests::A, ()) joined with (&bevy_ecs::query::state::tests::B, ()).")]
1823+
fn cannot_join_wrong_fetch() {
1824+
let mut world = World::new();
1825+
world.init_component::<C>();
1826+
let query_1 = QueryState::<&A>::new(&mut world);
1827+
let query_2 = QueryState::<&B>::new(&mut world);
1828+
let _query: QueryState<&C> = query_1.join(&world, &query_2);
1829+
}
1830+
1831+
#[test]
1832+
#[should_panic(
1833+
expected = "Joined state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed<bevy_ecs::query::state::tests::C>) \
1834+
attempts to access terms that are not allowed by state \
1835+
(&bevy_ecs::query::state::tests::A, bevy_ecs::query::filter::Without<bevy_ecs::query::state::tests::C>) \
1836+
joined with (&bevy_ecs::query::state::tests::B, bevy_ecs::query::filter::Without<bevy_ecs::query::state::tests::C>)."
1837+
)]
1838+
fn cannot_join_wrong_filter() {
1839+
let mut world = World::new();
1840+
let query_1 = QueryState::<&A, Without<C>>::new(&mut world);
1841+
let query_2 = QueryState::<&B, Without<C>>::new(&mut world);
1842+
let _: QueryState<Entity, Changed<C>> = query_1.join_filtered(&world, &query_2);
1843+
}
16611844
}

crates/bevy_ecs/src/system/query.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1344,7 +1344,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
13441344
pub fn transmute_lens_filtered<NewD: QueryData, NewF: QueryFilter>(
13451345
&mut self,
13461346
) -> QueryLens<'_, NewD, NewF> {
1347-
// SAFETY: There are no other active borrows of data from world
1347+
// SAFETY:
1348+
// - We have exclusive access to the query
1349+
// - `self` has correctly captured it's access
1350+
// - Access is checked to be a subset of the query's access when the state is created.
13481351
let world = unsafe { self.world.world() };
13491352
let state = self.state.transmute_filtered::<NewD, NewF>(world);
13501353
QueryLens {
@@ -1359,6 +1362,99 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
13591362
pub fn as_query_lens(&mut self) -> QueryLens<'_, D> {
13601363
self.transmute_lens()
13611364
}
1365+
1366+
/// Returns a [`QueryLens`] that can be used to get a query with the combined fetch.
1367+
///
1368+
/// For example, this can take a `Query<&A>` and a `Queryy<&B>` and return a `Query<&A, &B>`.
1369+
/// The returned query will only return items with both `A` and `B`. Note that since filter
1370+
/// are dropped, non-archetypal filters like `Added` and `Changed` will no be respected.
1371+
/// To maintain or change filter terms see `Self::join_filtered`.
1372+
///
1373+
/// ## Example
1374+
///
1375+
/// ```rust
1376+
/// # use bevy_ecs::prelude::*;
1377+
/// # use bevy_ecs::system::QueryLens;
1378+
/// #
1379+
/// # #[derive(Component)]
1380+
/// # struct Transform;
1381+
/// #
1382+
/// # #[derive(Component)]
1383+
/// # struct Player;
1384+
/// #
1385+
/// # #[derive(Component)]
1386+
/// # struct Enemy;
1387+
/// #
1388+
/// # let mut world = World::default();
1389+
/// # world.spawn((Transform, Player));
1390+
/// # world.spawn((Transform, Enemy));
1391+
///
1392+
/// fn system(
1393+
/// mut transforms: Query<&Transform>,
1394+
/// mut players: Query<&Player>,
1395+
/// mut enemies: Query<&Enemy>
1396+
/// ) {
1397+
/// let mut players_transforms: QueryLens<(&Transform, &Player)> = transforms.join(&mut players);
1398+
/// for (transform, player) in &players_transforms.query() {
1399+
/// // do something with a and b
1400+
/// }
1401+
///
1402+
/// let mut enemies_transforms: QueryLens<(&Transform, &Enemy)> = transforms.join(&mut enemies);
1403+
/// for (transform, enemy) in &enemies_transforms.query() {
1404+
/// // do something with a and b
1405+
/// }
1406+
/// }
1407+
///
1408+
/// # let mut schedule = Schedule::default();
1409+
/// # schedule.add_systems(system);
1410+
/// # schedule.run(&mut world);
1411+
/// ```
1412+
/// ## Panics
1413+
///
1414+
/// This will panic if `NewD` is not a subset of the union of the original fetch `Q` and `OtherD`.
1415+
///
1416+
/// ## Allowed Transmutes
1417+
///
1418+
/// Like `transmute_lens` the query terms can be changed with some restrictions.
1419+
/// See [`Self::transmute_lens`] for more details.
1420+
pub fn join<OtherD: QueryData, NewD: QueryData>(
1421+
&mut self,
1422+
other: &mut Query<OtherD>,
1423+
) -> QueryLens<'_, NewD> {
1424+
self.join_filtered(other)
1425+
}
1426+
1427+
/// Equivalent to [`Self::join`] but also includes a [`QueryFilter`] type.
1428+
///
1429+
/// Note that the lens with iterate a subset of the original queries tables
1430+
/// and archetypes. This means that additional archetypal query terms like
1431+
/// `With` and `Without` will not necessarily be respected and non-archetypal
1432+
/// terms like `Added` and `Changed` will only be respected if they are in
1433+
/// the type signature.
1434+
pub fn join_filtered<
1435+
OtherD: QueryData,
1436+
OtherF: QueryFilter,
1437+
NewD: QueryData,
1438+
NewF: QueryFilter,
1439+
>(
1440+
&mut self,
1441+
other: &mut Query<OtherD, OtherF>,
1442+
) -> QueryLens<'_, NewD, NewF> {
1443+
// SAFETY:
1444+
// - The queries have correctly captured their access.
1445+
// - We have exclusive access to both queries.
1446+
// - Access for QueryLens is checked when state is created.
1447+
let world = unsafe { self.world.world() };
1448+
let state = self
1449+
.state
1450+
.join_filtered::<OtherD, OtherF, NewD, NewF>(world, other.state);
1451+
QueryLens {
1452+
world: self.world,
1453+
state,
1454+
last_run: self.last_run,
1455+
this_run: self.this_run,
1456+
}
1457+
}
13621458
}
13631459

13641460
impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, F> {

0 commit comments

Comments
 (0)