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

OnMutate Observers/on_mutate Hooks #16143

Closed
wants to merge 12 commits into from
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3857,6 +3857,10 @@ doc-scrape-examples = true
[package.metadata.example.testbed_3d]
hidden = true

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: remove later

[[example]]
name = "playground"
path = "examples/playground.rs"
doc-scrape-examples = false

[[example]]
name = "testbed_ui_layout_rounding"
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ bitflags::bitflags! {
const ON_INSERT_OBSERVER = (1 << 5);
const ON_REPLACE_OBSERVER = (1 << 6);
const ON_REMOVE_OBSERVER = (1 << 7);
const ON_MUTATE_OBSERVER = (1 << 8);
}
}

Expand Down Expand Up @@ -697,6 +698,11 @@ impl Archetype {
pub fn has_remove_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
}

#[inline]
pub fn has_mutate_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_MUTATE_OBSERVER)
}
}

/// The next [`ArchetypeId`] in an [`Archetypes`] collection.
Expand Down
23 changes: 22 additions & 1 deletion crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
prelude::World,
query::DebugCheckedUnwrap,
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE},
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_MUTATE, ON_REPLACE},
};
use bevy_ptr::{ConstNonNull, OwningPtr};
use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap};
Expand Down Expand Up @@ -1093,6 +1093,13 @@ impl<'w> BundleInserter<'w> {
add_bundle.iter_inserted(),
);
}
if new_archetype.has_mutate_observer() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: instead add these to the ComponentMutationBuffer

deferred_world.trigger_observers(
ON_MUTATE,
entity,
add_bundle.iter_inserted(),
);
}
}
InsertMode::Keep => {
// insert triggers only for new components if we're not replacing them (since
Expand All @@ -1109,6 +1116,13 @@ impl<'w> BundleInserter<'w> {
add_bundle.iter_added(),
);
}
if new_archetype.has_mutate_observer() {
deferred_world.trigger_observers(
ON_MUTATE,
entity,
add_bundle.iter_added(),
);
}
}
}
}
Expand Down Expand Up @@ -1249,6 +1263,13 @@ impl<'w> BundleSpawner<'w> {
bundle_info.iter_contributed_components(),
);
}
if archetype.has_mutate_observer() {
deferred_world.trigger_observers(
ON_MUTATE,
entity,
bundle_info.iter_contributed_components(),
);
}
};

location
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub mod prelude {
},
world::{
Command, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut,
FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World,
FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, OnMutate, World,
},
};

Expand Down
13 changes: 13 additions & 0 deletions crates/bevy_ecs/src/observer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ pub struct Observers {
on_insert: CachedObservers,
on_replace: CachedObservers,
on_remove: CachedObservers,
on_mutate: CachedObservers,
// Map from trigger type to set of observers
cache: HashMap<ComponentId, CachedObservers>,
}
Expand All @@ -258,6 +259,7 @@ impl Observers {
ON_INSERT => &mut self.on_insert,
ON_REPLACE => &mut self.on_replace,
ON_REMOVE => &mut self.on_remove,
ON_MUTATE => &mut self.on_mutate,
_ => self.cache.entry(event_type).or_default(),
}
}
Expand All @@ -268,6 +270,7 @@ impl Observers {
ON_INSERT => Some(&self.on_insert),
ON_REPLACE => Some(&self.on_replace),
ON_REMOVE => Some(&self.on_remove),
ON_MUTATE => Some(&self.on_mutate),
_ => self.cache.get(&event_type),
}
}
Expand Down Expand Up @@ -342,6 +345,7 @@ impl Observers {
ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),
ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER),
ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER),
ON_MUTATE => Some(ArchetypeFlags::ON_MUTATE_OBSERVER),
_ => None,
}
}
Expand Down Expand Up @@ -378,6 +382,14 @@ impl Observers {
{
flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER);
}

if self
.on_mutate
.component_observers
.contains_key(&component_id)
{
flags.insert(ArchetypeFlags::ON_MUTATE_OBSERVER);
}
}
}

Expand Down Expand Up @@ -565,6 +577,7 @@ impl World {
}
}

// TODO GRACE: write tests here
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: write tests

#[cfg(test)]
mod tests {
use alloc::vec;
Expand Down
60 changes: 55 additions & 5 deletions crates/bevy_ecs/src/system/system_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@ use crate::{
bundle::Bundles,
change_detection::{Ticks, TicksMut},
component::{ComponentId, ComponentTicks, Components, Tick},
entity::Entities,
entity::{Entities, Entity},
query::{
Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError,
QueryState, ReadOnlyQueryData,
},
storage::ResourceData,
system::{Query, Single, SystemMeta},
world::{
unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut,
FromWorld, World,
unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut, FromWorld, Mut, World, ON_MUTATE
},
};
use bevy_ecs_macros::impl_param_set;
pub use bevy_ecs_macros::{Resource, SystemParam};
use bevy_ptr::UnsafeCellDeref;
use bevy_utils::{all_tuples, synccell::SyncCell};
use bevy_utils::{all_tuples, synccell::SyncCell, Entry, HashMap, HashSet};
#[cfg(feature = "track_change_detection")]
use core::panic::Location;
use core::{
Expand Down Expand Up @@ -194,7 +193,7 @@ pub unsafe trait SystemParam: Sized {
/// and creates a new instance of this param's [`State`](SystemParam::State).
fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State;

/// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a
/// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).
///
/// # Safety
/// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`].
Expand Down Expand Up @@ -320,8 +319,59 @@ unsafe impl<D: QueryData + 'static, F: QueryFilter + 'static> SystemParam for Qu
// world data that the query needs.
unsafe { Query::new(world, state, system_meta.last_run, change_tick) }
}

fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any performance/code quality suggestions, especially here, are much appreciated.

A lot of what's here should probably be somewhere else, but I'm not sure where. It seems like a bad idea to iterate over every archetype a mutable query touches in Query::apply. An ideal solution would probably be the on_mutate_queue mentioned here, but I'm going to leave that for the future. For now, would it make more sense to instead iterate over every archetype, then if that archetype has the ON_MUTATE_OBSERVER flag iterate over its components' change information, then the components themselves? The downside of this is that this would need to be done potentially multiple times per command flush.

world.init_resource::<ComponentMutationBuffer>();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This probably should not be initialized here, but I'm not really sure where it should be. Should this be in World::bootstrap instead? Or does data like this make more sense to be somewhere on World?

world.resource_scope(|world, mut buffer: Mut<ComponentMutationBuffer>| {
let (writes, cant_write) = state.component_access.access.component_writes();
let mutable_components = if cant_write { Vec::new() } else { writes.collect() };
if mutable_components.is_empty() { return }

let buffer_started_empty = buffer.0.is_empty();

for archetype_id in state.matched_archetypes() {
let Some(archetype) = world.archetypes().get(archetype_id) else { continue; };
if !archetype.has_mutate_observer() { continue; }

for archetype_entity in archetype.entities() {
let entity = archetype_entity.id();
let entity_ref = world.entity(entity);
let mutated_observers = mutable_components
.iter()
.cloned()
.filter(|c| entity_ref
.get_change_ticks_by_id(*c)
.map(|ticks| ticks.changed.get() >= system_meta.last_run.get()) // FIXME GRACE: get this_run
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: don't compare change detection without this_run

.unwrap_or(false)
);
let values = buffer.0.entry(entity).or_default();
values.extend(mutated_observers);
}
}

if buffer_started_empty && !buffer.0.is_empty() {
world.commands().queue(flush_mutations);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a jank way that I'm deduplicating flush_mutations commands, where I'm only adding a flush_mutations command to the queue when ComponentMutationBuffer starts empty and has something added to it. Is there a better way of doing this?

}
});
}
}

fn flush_mutations(world: &mut World) {
world.resource_scope(|world, mut buffer: Mut<ComponentMutationBuffer>| {
let mut deferred_world = DeferredWorld::from(world);
for (entity, components) in buffer.0.drain() {
unsafe {
deferred_world.trigger_observers(ON_MUTATE, entity, components.iter().cloned())
}
}
});
}

#[derive(Default)]
pub(crate) struct ComponentMutationBuffer(HashMap<Entity, HashSet<ComponentId>>);

impl Resource for ComponentMutationBuffer {}

pub(crate) fn init_query_param<D: QueryData + 'static, F: QueryFilter + 'static>(
world: &mut World,
system_meta: &mut SystemMeta,
Expand Down
8 changes: 8 additions & 0 deletions crates/bevy_ecs/src/world/component_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub const ON_INSERT: ComponentId = ComponentId::new(1);
pub const ON_REPLACE: ComponentId = ComponentId::new(2);
/// [`ComponentId`] for [`OnRemove`]
pub const ON_REMOVE: ComponentId = ComponentId::new(3);
/// [`ComponentId`] for [`OnMutate`]
pub const ON_MUTATE: ComponentId = ComponentId::new(4);

/// Trigger emitted when a component is added to an entity. See [`crate::component::ComponentHooks::on_add`]
/// for more information.
Expand Down Expand Up @@ -41,3 +43,9 @@ pub struct OnReplace;
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnRemove;

/// Trigger emitted when a component is mutated.
#[derive(Event, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect(Debug))]
pub struct OnMutate;
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ impl World {
assert_eq!(ON_INSERT, self.register_component::<OnInsert>());
assert_eq!(ON_REPLACE, self.register_component::<OnReplace>());
assert_eq!(ON_REMOVE, self.register_component::<OnRemove>());
assert_eq!(ON_MUTATE, self.register_component::<OnMutate>());
}
/// Creates a new empty [`World`].
///
Expand Down
55 changes: 55 additions & 0 deletions examples/playground.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: remove

#![allow(missing_docs)]

use bevy::prelude::*;

// TODO GRACE: remove this file

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, on_space_press)
.add_observer(on_a_mutated)
.run();
}

#[derive(Component)]
struct A(i32);

fn setup(mut commands: Commands) {
commands.spawn(A(0));
}

fn on_space_press(input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut A>) {
if input.just_pressed(KeyCode::Space) {
for mut a in query.iter_mut() {
println!("asdfjkaskldfjlksadfljsdfk");
a.0 = 100;
}
}
}

fn on_a_mutated(trigger: Trigger<OnMutate, A>) {
println!("A mutation happened!");
}


// fn main() {
// App::new()
// .add_plugins(DefaultPlugins)
// .add_systems(Startup, setup)
// .add_observer(on_a_added)
// .run();
// }

// #[derive(Component)]
// struct A(i32);

// fn setup(mut commands: Commands) {
// commands.spawn_empty().insert(A(0)).insert(A(1));
// }

// fn on_a_added(trigger: Trigger<OnAdd, A>) {
// println!("Abafjgklasdfgjp;kl")
// }
Loading