Skip to content

column/table level change detection #11120

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ impl BundleInfo {
// NOTE: get_components calls this closure on each component in "bundle order".
// bundle_info.component_ids are also in "bundle order"
let mut bundle_component = 0;
table.update_last_mutable_access_tick(change_tick);
bundle.get_components(&mut |storage_type, component_ptr| {
let component_id = *self.component_ids.get_unchecked(bundle_component);
match storage_type {
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_ecs/src/query/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,11 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
) {
if Self::IS_DENSE {
Self::set_table(fetch, component_id, table);
} else {
fetch
.sparse_set
.debug_checked_unwrap()
.update_last_mutable_access_tick(fetch.this_run)
}
}

Expand All @@ -909,6 +914,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
table: &'w Table,
) {
let column = table.get_column(component_id).debug_checked_unwrap();
table.update_table_and_component_access_tick(component_id, fetch.this_run);
fetch.table_data = Some((
column.get_data_slice().into(),
column.get_added_ticks_slice().into(),
Expand Down
36 changes: 36 additions & 0 deletions crates/bevy_ecs/src/query/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ pub trait QueryFilter: WorldQuery {
entity: Entity,
table_row: TableRow,
) -> bool;

/// # Safety
///
/// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and
/// `table_row` must be in the range of the current table and archetype.
#[allow(unused_variables)]
unsafe fn archetype_filter_fetch(
fetch: &mut Self::Fetch<'_>,
state: &Self::State,
table: &Table,
) -> bool {
true
}
}

/// Filter that selects entities with a component `T`.
Expand Down Expand Up @@ -918,6 +931,29 @@ impl<T: Component> QueryFilter for Changed<T> {
) -> bool {
Self::fetch(fetch, entity, table_row)
}
unsafe fn archetype_filter_fetch(
fetch: &mut Self::Fetch<'_>,
&component_id: &ComponentId,
table: &Table,
) -> bool {
match T::Storage::STORAGE_TYPE {
StorageType::SparseSet => fetch
.sparse_set
.debug_checked_unwrap()
.read_last_mutable_access_tick()
.is_newer_than(fetch.last_run, fetch.this_run),
StorageType::Table => {
table
.read_last_mutable_access_tick()
.is_newer_than(fetch.last_run, fetch.this_run)
&& table
.get_column(component_id)
.debug_checked_unwrap()
.read_last_mutable_access_tick()
.is_newer_than(fetch.last_run, fetch.this_run)
}
}
}
}

/// A marker trait to indicate that the filter works at an archetype level.
Expand Down
33 changes: 33 additions & 0 deletions crates/bevy_ecs/src/query/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,15 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F>
for table_id in self.cursor.table_id_iter.clone() {
// SAFETY: Matched table IDs are guaranteed to still exist.
let table = unsafe { self.tables.get(*table_id).debug_checked_unwrap() };
unsafe {
if !F::archetype_filter_fetch(
&mut self.cursor.filter,
&self.query_state.filter_state,
table,
) {
continue;
}
};
accum =
// SAFETY:
// - The fetched table matches both D and F
Expand All @@ -246,6 +255,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F>
let archetype =
// SAFETY: Matched archetype IDs are guaranteed to still exist.
unsafe { self.archetypes.get(*archetype_id).debug_checked_unwrap() };
let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() };
unsafe {
if !F::archetype_filter_fetch(
&mut self.cursor.filter,
&self.query_state.filter_state,
table,
) {
continue;
}
};
accum =
// SAFETY:
// - The fetched archetype matches both D and F
Expand Down Expand Up @@ -762,6 +781,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
if self.current_row == self.current_len {
let table_id = self.table_id_iter.next()?;
let table = tables.get(*table_id).debug_checked_unwrap();
if !F::archetype_filter_fetch(
&mut self.filter,
&query_state.filter_state,
table,
) {
continue;
}
// SAFETY: `table` is from the world that `fetch/filter` were created for,
// `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with
D::set_table(&mut self.fetch, &query_state.fetch_state, table);
Expand Down Expand Up @@ -797,6 +823,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
let archetype_id = self.archetype_id_iter.next()?;
let archetype = archetypes.get(*archetype_id).debug_checked_unwrap();
let table = tables.get(archetype.table_id()).debug_checked_unwrap();
if !F::archetype_filter_fetch(
&mut self.filter,
&query_state.filter_state,
table,
) {
continue;
}
// SAFETY: `archetype` and `tables` are from the world that `fetch/filter` were created for,
// `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with
D::set_archetype(&mut self.fetch, &query_state.fetch_state, archetype, table);
Expand Down
8 changes: 8 additions & 0 deletions crates/bevy_ecs/src/storage/sparse_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ impl ComponentSparseSet {
pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) {
self.dense.check_change_ticks(change_tick);
}

pub fn read_last_mutable_access_tick(&self) -> Tick {
self.dense.read_last_mutable_access_tick()
}

pub fn update_last_mutable_access_tick(&self, tick: Tick) {
self.dense.update_last_mutable_access_tick(tick);
}
}

/// A data structure that blends dense and sparse storage
Expand Down
43 changes: 42 additions & 1 deletion crates/bevy_ecs/src/storage/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
};
use bevy_ptr::{OwningPtr, Ptr, PtrMut, UnsafeCellDeref};
use bevy_utils::HashMap;
use std::alloc::Layout;
use std::{alloc::Layout, sync::atomic::AtomicU32};
use std::{
cell::UnsafeCell,
ops::{Index, IndexMut},
Expand Down Expand Up @@ -152,6 +152,7 @@ pub struct Column {
data: BlobVec,
added_ticks: Vec<UnsafeCell<Tick>>,
changed_ticks: Vec<UnsafeCell<Tick>>,
last_mutable_access_tick: AtomicU32,
}

impl Column {
Expand All @@ -163,6 +164,7 @@ impl Column {
data: unsafe { BlobVec::new(component_info.layout(), component_info.drop(), capacity) },
added_ticks: Vec::with_capacity(capacity),
changed_ticks: Vec::with_capacity(capacity),
last_mutable_access_tick: AtomicU32::new(0),
}
}

Expand All @@ -181,6 +183,7 @@ impl Column {
#[inline]
pub(crate) unsafe fn initialize(&mut self, row: TableRow, data: OwningPtr<'_>, tick: Tick) {
debug_assert!(row.as_usize() < self.len());
self.update_last_mutable_access_tick(tick);
self.data.initialize_unchecked(row.as_usize(), data);
*self.added_ticks.get_unchecked_mut(row.as_usize()).get_mut() = tick;
*self
Expand All @@ -197,6 +200,7 @@ impl Column {
#[inline]
pub(crate) unsafe fn replace(&mut self, row: TableRow, data: OwningPtr<'_>, change_tick: Tick) {
debug_assert!(row.as_usize() < self.len());
self.update_last_mutable_access_tick(change_tick);
self.data.replace_unchecked(row.as_usize(), data);
*self
.changed_ticks
Expand Down Expand Up @@ -326,6 +330,7 @@ impl Column {
/// # Safety
/// `ptr` must point to valid data of this column's component type
pub(crate) unsafe fn push(&mut self, ptr: OwningPtr<'_>, ticks: ComponentTicks) {
self.update_last_mutable_access_tick(ticks.changed);
self.data.push(ptr);
self.added_ticks.push(UnsafeCell::new(ticks.added));
self.changed_ticks.push(UnsafeCell::new(ticks.changed));
Expand Down Expand Up @@ -539,6 +544,18 @@ impl Column {
component_ticks.get_mut().check_tick(change_tick);
}
}
pub fn read_last_mutable_access_tick(&self) -> Tick {
Tick::new(
self.last_mutable_access_tick
.load(std::sync::atomic::Ordering::Relaxed),
)
}
pub fn update_last_mutable_access_tick(&self, tick: Tick) {
// self.last_mutable_access_tick
// .store(tick.get(), std::sync::atomic::Ordering::Relaxed);
self.last_mutable_access_tick
.fetch_max(tick.get(), std::sync::atomic::Ordering::AcqRel);
}
Comment on lines +547 to +558
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain the choice for memory ordering? Naively, I would have expected the fetch_max to be Release and the load to be Acquire, so that we cannot miss changes. However I'm not confident in my understanding of atomic ordering.

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 haven't deeply considered this aspect yet(so please correct me if i am wrong). My thought was that there might not be any other data flow related to access-tick. Therefore, using a relaxed load operation would be acceptable. Bevy's scheduler may run systems with same table access in parallel, but we cannot guarantee the progress when system would load or store corresponding tick, when two systems write to the same tick concurrently. store operations must be observed. but for load operation ,even if changes are missed,it is not a big deal,our focus is on comparing the loaded value to the system's last_change_tick.

}

/// A builder type for constructing [`Table`]s.
Expand Down Expand Up @@ -579,6 +596,7 @@ impl TableBuilder {
Table {
columns: self.columns.into_immutable(),
entities: Vec::with_capacity(self.capacity),
last_mutable_access_tick: AtomicU32::new(0),
}
}
}
Expand All @@ -598,6 +616,7 @@ impl TableBuilder {
pub struct Table {
columns: ImmutableSparseSet<ComponentId, Column>,
entities: Vec<Entity>,
last_mutable_access_tick: AtomicU32,
}

impl Table {
Expand Down Expand Up @@ -827,6 +846,28 @@ impl Table {
column.clear();
}
}

pub fn read_last_mutable_access_tick(&self) -> Tick {
Tick::new(
self.last_mutable_access_tick
.load(std::sync::atomic::Ordering::Relaxed),
)
}

pub(crate) fn update_last_mutable_access_tick(&self, tick: Tick) {
self.last_mutable_access_tick
.fetch_max(tick.get(), std::sync::atomic::Ordering::AcqRel);
}

pub(crate) fn update_table_and_component_access_tick(
&self,
component_id: ComponentId,
tick: Tick,
) {
self.update_last_mutable_access_tick(tick);
self.get_column(component_id)
.map(|c| c.update_last_mutable_access_tick(tick));
}
}

/// A collection of [`Table`] storages, indexed by [`TableId`]
Expand Down
27 changes: 27 additions & 0 deletions crates/bevy_ecs/src/world/unsafe_world_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,12 @@ impl<'w> UnsafeEntityCell<'w> {
// - `location` is valid
// - aliasing rules are ensured by caller
unsafe {
self.world.update_fetch_mutable_access_tick(
self.location,
component_id,
T::Storage::STORAGE_TYPE,
change_tick,
);
get_component_and_ticks(
self.world,
component_id,
Expand Down Expand Up @@ -906,6 +912,27 @@ impl<'w> UnsafeWorldCell<'w> {
// of component/resource data
unsafe { self.storages() }.sparse_sets.get(component_id)
}

pub unsafe fn update_fetch_mutable_access_tick(
self,
location: EntityLocation,
component_id: ComponentId,
storage_type: StorageType,
change_tick: Tick,
) {
match storage_type {
StorageType::SparseSet => {
unsafe { self.storages() }
.sparse_sets
.get(component_id)
.map(|c| c.update_last_mutable_access_tick(change_tick));
}
StorageType::Table => {
unsafe { self.storages() }.tables[location.table_id]
.update_table_and_component_access_tick(component_id, change_tick);
}
}
}
}

/// Get an untyped pointer to a particular [`Component`] on a particular [`Entity`] in the provided [`World`].
Expand Down