diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 98b9c33bb07c8..286296d2acdf6 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -12,6 +12,11 @@ rust-version = "1.86.0" [features] default = ["std", "bevy_reflect", "async_executor", "backtrace"] +# Testing/internal toggles +query_uncached_default = [] + +# Experimental dynamic query support (builder and plan types) +dynamic_query = [] # Functionality diff --git a/crates/bevy_ecs/src/other/bevy/crates/bevy_ecs/src/query/cache.rs b/crates/bevy_ecs/src/other/bevy/crates/bevy_ecs/src/query/cache.rs new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index 8ba34a34f9ee7..6055eaa6d40ab 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -6,6 +6,18 @@ use crate::{ }; use super::{FilteredAccess, QueryData, QueryFilter}; +#[cfg(feature = "dynamic_query")] +use super::dynamic_plan::*; +#[cfg(feature = "dynamic_query")] +use alloc::vec::Vec; +#[cfg(feature = "dynamic_query")] +use smallvec::SmallVec; +#[cfg(feature = "dynamic_query")] +use crate::component::ComponentId; +#[cfg(feature = "dynamic_query")] +use crate::world::FilteredEntityRef; +#[cfg(feature = "dynamic_query")] +use crate::query::state::QueryState; /// Builder struct to create [`QueryState`] instances at runtime. /// @@ -41,6 +53,8 @@ pub struct QueryBuilder<'w, D: QueryData = (), F: QueryFilter = ()> { or: bool, first: bool, _marker: PhantomData<(D, F)>, + #[cfg(feature = "dynamic_query")] + plan: DynamicPlan, } impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { @@ -68,6 +82,8 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { or: false, first: false, _marker: PhantomData, + #[cfg(feature = "dynamic_query")] + plan: DynamicPlan::new(), } } @@ -269,6 +285,74 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { pub fn build(&mut self) -> QueryState { QueryState::::from_builder(self) } + + // ===== Dynamic extensions (behind feature) ===== + #[cfg(feature = "dynamic_query")] + /// Create a new variable/term and return its id. + pub fn var(&mut self) -> VarId { self.plan.var() } + + #[cfg(feature = "dynamic_query")] + /// Constrain that `var` has component `id` (read). + pub fn with_id_var(&mut self, id: ComponentId, var: TermVar) -> &mut Self { + // Update global filtered access like a normal With + read + let mut access = FilteredAccess::default(); + access.and_with(id); + access.add_component_read(id); + self.extend_access(access); + // Record constraint + self.plan.constraints.push(Constraint::With { var, component: id, write: false }); + self + } + + #[cfg(feature = "dynamic_query")] + /// Constrain that `var` has component `id` (write). + pub fn mut_id_var(&mut self, id: ComponentId, var: TermVar) -> &mut Self { + let mut access = FilteredAccess::default(); + access.and_with(id); + access.add_component_write(id); + self.extend_access(access); + self.plan.constraints.push(Constraint::With { var, component: id, write: true }); + self + } + + #[cfg(feature = "dynamic_query")] + /// Constrain that `var` does not have component `id`. + pub fn without_id_var(&mut self, id: ComponentId, var: TermVar) -> &mut Self { + let mut access = FilteredAccess::default(); + access.and_without(id); + self.extend_access(access); + self.plan.constraints.push(Constraint::Without { var, component: id }); + self + } + + #[cfg(feature = "dynamic_query")] + /// Constrain a relation between two variables. + pub fn relation_id(&mut self, rel: ComponentId, from: TermVar, to: TermVar) -> &mut Self { + // Relations may be stored sparsely; for now mark via a With on the `from` side so archetype pruning can work when possible. + let mut access = FilteredAccess::default(); + access.and_with(rel); + self.extend_access(access); + self.plan.constraints.push(Constraint::Relation { rel, from, to }); + self + } + + #[cfg(feature = "dynamic_query")] + /// Build a dynamic query that matches using the accumulated plan. + pub fn build_dynamic(&mut self) -> QueryState { + use crate::query::{DynamicData, DynamicState, QueryState}; + let world = self.world(); + // pessimistic dense + let is_dense = false; + let fetch_state = DynamicState { plan: self.plan.clone() }; + let filter_state = (); // no filter + QueryState::::from_states_uninitialized_with_access( + world, + fetch_state, + filter_state, + self.access.clone(), + is_dense, + ) + } } #[cfg(test)] diff --git a/crates/bevy_ecs/src/query/dynamic.rs b/crates/bevy_ecs/src/query/dynamic.rs new file mode 100644 index 0000000000000..2b3f8b9c26928 --- /dev/null +++ b/crates/bevy_ecs/src/query/dynamic.rs @@ -0,0 +1,225 @@ +#![cfg(feature = "dynamic_query")] +use crate::relationship::RelationshipAccessor; +use crate::world::unsafe_world_cell::UnsafeWorldCell; +use crate::{ + archetype::Archetype, + change_detection::Tick, + component::{ComponentId, Components}, + entity::Entity, + query::{FilteredAccess, WorldQuery}, + storage::Table, +}; +use super::dynamic_plan::{Constraint, DynamicPlan, TermVar, VarId}; +use alloc::vec::Vec; +use smallvec::SmallVec; + +#[derive(Clone, Debug)] +pub struct DynamicResult { + pub this: Entity, + pub vars: SmallVec<[(VarId, Entity); 3]>, +} + +#[derive(Clone)] +pub struct DynamicState { + pub plan: DynamicPlan, +} + +#[derive(Clone)] +pub struct DynamicFetch<'w> { + world: UnsafeWorldCell<'w>, + components: &'w Components, + last_run: Tick, + this_run: Tick, + current_archetype: Option<&'w Archetype>, + current_table: Option<&'w Table>, +} + +pub struct DynamicData; + +unsafe impl WorldQuery for DynamicData { + type Fetch<'w> = DynamicFetch<'w>; + type State = DynamicState; + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + DynamicFetch { + world: fetch.world, + components: fetch.components, + last_run: fetch.last_run, + this_run: fetch.this_run, + current_archetype: fetch.current_archetype, + current_table: fetch.current_table, + } + } + + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, _state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + DynamicFetch { + world, + components: world.components(), + last_run, + this_run, + current_archetype: None, + current_table: None, + } + } + + const IS_DENSE: bool = false; + + unsafe fn set_archetype<'w, 's>(fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, archetype: &'w Archetype, table: &'w Table) { + fetch.current_archetype = Some(archetype); + fetch.current_table = Some(table); + } + + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, table: &'w Table) { + fetch.current_archetype = None; + fetch.current_table = Some(table); + } + + fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { + access.extend(&state.plan.filtered_access); + } + + fn init_state(_world: &mut crate::world::World) -> Self::State { + panic!("DynamicData::init_state should not be called; build via builder"); + } + + fn get_state(_components: &Components) -> Option { None } + + fn matches_component_set(state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { + state + .plan + .filtered_access + .filter_sets + .iter() + .any(|set| { + set.with + .ones() + .all(|index| set_contains_id(ComponentId::get_sparse_set_index(index))) + && set + .without + .ones() + .all(|index| !set_contains_id(ComponentId::get_sparse_set_index(index))) + }) + } +} + +unsafe impl crate::query::QueryData for DynamicData { + const IS_READ_ONLY: bool = true; + type ReadOnly = Self; + type Item<'w, 's> = DynamicResult; + + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { item } + + unsafe fn fetch<'w, 's>(state: &'s ::State, fetch: &mut ::Fetch<'w>, entity: Entity, _table_row: crate::storage::TableRow) -> Self::Item<'w, 's> { + let result = solve_bindings(fetch.world, fetch.components, &state.plan, entity); + match result { + Some(vars) => DynamicResult { this: entity, vars }, + None => DynamicResult { this: entity, vars: SmallVec::new() }, + } + } +} + +unsafe impl crate::query::ReadOnlyQueryData for DynamicData {} + +fn solve_bindings(world: UnsafeWorldCell, components: &Components, plan: &DynamicPlan, this_entity: Entity) -> Option> { + let mut bindings: Vec> = vec![None; plan.vars.len()]; + bindings[0] = Some(this_entity); + + let mut changed = true; + let mut passes = 0; + while changed && passes < plan.constraints.len() + 2 { + changed = false; + passes += 1; + for c in &plan.constraints { + match *c { + Constraint::With { var, component, .. } => { + if let Some(e) = get_binding(&bindings, var) { + // require presence on bound var + let cell = world.get_entity(e).ok()?; + if !cell.contains_id(component) { return None; } + } + } + Constraint::Without { var, component } => { + if let Some(e) = get_binding(&bindings, var) { + let cell = world.get_entity(e).ok()?; + if cell.contains_id(component) { return None; } + } + } + Constraint::Relation { rel, from, to } => { + let info = components.get_info(rel)?; + let accessor = info.relationship_accessor()?; + match (get_binding(&bindings, from), get_binding(&bindings, to), accessor) { + (Some(from_e), None, RelationshipAccessor::Relationship { entity_field_offset, .. }) => { + // read Relationship on `from` + let cell = world.get_entity(from_e).ok()?; + // SAFETY: component id valid; offset used below + let ptr = unsafe { cell.get_by_id(rel)? }; + // SAFETY: offset points to Entity field per accessor contract + let target: Entity = unsafe { *ptr.byte_add(entity_field_offset).deref() }; + if set_binding(&mut bindings, to, target) { changed = true; } + } + (Some(from_e), Some(to_e), RelationshipAccessor::Relationship { entity_field_offset, .. }) => { + let cell = world.get_entity(from_e).ok()?; + let ptr = unsafe { cell.get_by_id(rel)? }; + let target: Entity = unsafe { *ptr.byte_add(entity_field_offset).deref() }; + if target != to_e { return None; } + } + (None, Some(to_e), RelationshipAccessor::RelationshipTarget { iter, .. }) => { + // iterate sources of `to` and bind `from` to first match + let cell = world.get_entity(to_e).ok()?; + let ptr = unsafe { cell.get_by_id(rel)? }; + // SAFETY: ptr is of correct component type by id; accessor promises safety + let mut it = unsafe { iter(ptr) }; + if let Some(src) = it.next() { + if set_binding(&mut bindings, from, src) { changed = true; } + } else { + return None; + } + } + (Some(from_e), Some(to_e), RelationshipAccessor::RelationshipTarget { iter, .. }) => { + let cell = world.get_entity(to_e).ok()?; + let ptr = unsafe { cell.get_by_id(rel)? }; + let mut it = unsafe { iter(ptr) }; + if !it.any(|src| src == from_e) { return None; } + } + // Other combinations (unbound vars) are deferred to later passes + _ => {} + } + } + } + } + } + + // Final validation: all With/Without on bound vars satisfied (already checked), ensure any still-unbound var mentioned in a With must fail + for c in &plan.constraints { + match *c { + Constraint::With { var, .. } | Constraint::Without { var, .. } => { + if get_binding(&bindings, var).is_none() { return None; } + } + _ => {} + } + } + + let mut out: SmallVec<[(VarId, Entity); 3]> = SmallVec::new(); + for (idx, b) in bindings.into_iter().enumerate() { + if let Some(e) = b { if idx != 0 { out.push((VarId(idx as u32), e)); } } + } + Some(out) +} + +fn get_binding(bindings: &Vec>, var: TermVar) -> Option { + match var { TermVar::This => bindings[0], TermVar::Var(VarId(i)) => bindings.get(i as usize).and_then(|b| *b) } +} + +fn set_binding(bindings: &mut Vec>, var: TermVar, value: Entity) -> bool { + match var { + TermVar::This => { + if let Some(prev) = bindings[0] { prev == value } else { bindings[0] = Some(value); true } + } + TermVar::Var(VarId(i)) => { + let slot = &mut bindings[i as usize]; + if let Some(prev) = *slot { prev == value } else { *slot = Some(value); true } + } + } +} + + diff --git a/crates/bevy_ecs/src/query/dynamic_plan.rs b/crates/bevy_ecs/src/query/dynamic_plan.rs new file mode 100644 index 0000000000000..62d7a3bec6e0f --- /dev/null +++ b/crates/bevy_ecs/src/query/dynamic_plan.rs @@ -0,0 +1,40 @@ +#![cfg(feature = "dynamic_query")] +use crate::component::ComponentId; +use alloc::vec::Vec; +use smallvec::SmallVec; +use super::FilteredAccess; + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub struct VarId(pub u32); + +#[derive(Copy, Clone, Debug)] +pub enum TermVar { This, Var(VarId) } + +#[derive(Clone, Debug)] +pub enum Constraint { + With { var: TermVar, component: ComponentId, write: bool }, + Without { var: TermVar, component: ComponentId }, + Relation { rel: ComponentId, from: TermVar, to: TermVar }, +} + +#[derive(Clone, Debug)] +pub struct DynamicPlan { + pub vars: SmallVec<[TermVar; 3]>, + pub constraints: Vec, + pub filtered_access: FilteredAccess, +} + +impl DynamicPlan { + pub fn new() -> Self { + let mut vars = SmallVec::new(); + vars.push(TermVar::This); + Self { vars, constraints: Vec::new(), filtered_access: FilteredAccess::default() } + } + pub fn var(&mut self) -> VarId { + let id = VarId(self.vars.len() as u32); + self.vars.push(TermVar::Var(id)); + id + } +} + + diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index fb8899fd5de87..4aa7063b4597f 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -9,6 +9,14 @@ mod iter; mod par_iter; mod state; mod world_query; +#[cfg(feature = "dynamic_query")] +mod dynamic_plan; +#[cfg(feature = "dynamic_query")] +pub use dynamic_plan::*; +#[cfg(feature = "dynamic_query")] +mod dynamic; +#[cfg(feature = "dynamic_query")] +pub use dynamic::*; pub use access::*; pub use bevy_ecs_macros::{QueryData, QueryFilter}; diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 91a979fa2e5e7..336a3190c5fb1 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -251,6 +251,34 @@ impl QueryState { } } + #[cfg(feature = "dynamic_query")] + /// Creates a new QueryState from explicit states and a precomputed FilteredAccess. + pub fn from_states_uninitialized_with_access( + world: &World, + fetch_state: ::State, + filter_state: ::State, + component_access: FilteredAccess, + is_dense: bool, + ) -> Self { + Self { + world_id: world.id(), + archetype_generation: ArchetypeGeneration::initial(), + matched_storage_ids: Vec::new(), + is_dense, + fetch_state, + filter_state, + component_access, + matched_tables: Default::default(), + matched_archetypes: Default::default(), + #[cfg(feature = "trace")] + par_iter_span: tracing::info_span!( + "par_for_each", + query = core::any::type_name::(), + filter = core::any::type_name::(), + ), + } + } + /// Creates a new [`QueryState`] from a given [`QueryBuilder`] and inherits its [`FilteredAccess`]. pub fn from_builder(builder: &mut QueryBuilder) -> Self { let mut fetch_state = D::init_state(builder.world_mut());