Path: blob/main/crates/bevy_ecs/src/change_detection/mod.rs
7219 views
//! Types that detect when their internal data mutate.12mod maybe_location;3mod params;4mod tick;5mod traits;67pub use maybe_location::MaybeLocation;8pub use params::*;9pub use tick::*;10pub use traits::{DetectChanges, DetectChangesMut};1112/// The (arbitrarily chosen) minimum number of world tick increments between `check_tick` scans.13///14/// Change ticks can only be scanned when systems aren't running. Thus, if the threshold is `N`,15/// the maximum is `2 * N - 1` (i.e. the world ticks `N - 1` times, then `N` times).16///17/// If no change is older than `u32::MAX - (2 * N - 1)` following a scan, none of their ages can18/// overflow and cause false positives.19// (518,400,000 = 1000 ticks per frame * 144 frames per second * 3600 seconds per hour)20pub const CHECK_TICK_THRESHOLD: u32 = 518_400_000;2122/// The maximum change tick difference that won't overflow before the next `check_tick` scan.23///24/// Changes stop being detected once they become this old.25pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1);2627#[cfg(test)]28mod tests {29use bevy_ecs_macros::Resource;30use bevy_ptr::PtrMut;31use bevy_reflect::{FromType, ReflectFromPtr};32use core::ops::{Deref, DerefMut};3334use crate::{35change_detection::{36ComponentTicks, ComponentTicksMut, MaybeLocation, Mut, NonSendMut, Ref, ResMut, Tick,37CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE,38},39component::Component,40system::{IntoSystem, Single, System},41world::World,42};4344use super::{DetectChanges, DetectChangesMut, MutUntyped};4546#[derive(Component, PartialEq)]47struct C;4849#[derive(Resource)]50struct R;5152#[derive(Resource, PartialEq)]53struct R2(u8);5455impl Deref for R2 {56type Target = u8;57fn deref(&self) -> &u8 {58&self.059}60}6162impl DerefMut for R2 {63fn deref_mut(&mut self) -> &mut u8 {64&mut self.065}66}6768#[test]69fn change_expiration() {70fn change_detected(query: Option<Single<Ref<C>>>) -> bool {71query.unwrap().is_changed()72}7374fn change_expired(query: Option<Single<Ref<C>>>) -> bool {75query.unwrap().is_changed()76}7778let mut world = World::new();7980// component added: 1, changed: 181world.spawn(C);8283let mut change_detected_system = IntoSystem::into_system(change_detected);84let mut change_expired_system = IntoSystem::into_system(change_expired);85change_detected_system.initialize(&mut world);86change_expired_system.initialize(&mut world);8788// world: 1, system last ran: 0, component changed: 189// The spawn will be detected since it happened after the system "last ran".90assert!(change_detected_system.run((), &mut world).unwrap());9192// world: 1 + MAX_CHANGE_AGE93let change_tick = world.change_tick.get_mut();94*change_tick = change_tick.wrapping_add(MAX_CHANGE_AGE);9596// Both the system and component appeared `MAX_CHANGE_AGE` ticks ago.97// Since we clamp things to `MAX_CHANGE_AGE` for determinism,98// `ComponentTicks::is_changed` will now see `MAX_CHANGE_AGE > MAX_CHANGE_AGE`99// and return `false`.100assert!(!change_expired_system.run((), &mut world).unwrap());101}102103#[test]104fn change_tick_wraparound() {105let mut world = World::new();106world.last_change_tick = Tick::new(u32::MAX);107*world.change_tick.get_mut() = 0;108109// component added: 0, changed: 0110world.spawn(C);111112world.increment_change_tick();113114// Since the world is always ahead, as long as changes can't get older than `u32::MAX` (which we ensure),115// the wrapping difference will always be positive, so wraparound doesn't matter.116let mut query = world.query::<Ref<C>>();117assert!(query.single(&world).unwrap().is_changed());118}119120#[test]121fn change_tick_scan() {122let mut world = World::new();123124// component added: 1, changed: 1125world.spawn(C);126127// a bunch of stuff happens, the component is now older than `MAX_CHANGE_AGE`128*world.change_tick.get_mut() += MAX_CHANGE_AGE + CHECK_TICK_THRESHOLD;129let change_tick = world.change_tick();130131let mut query = world.query::<Ref<C>>();132for tracker in query.iter(&world) {133let ticks_since_insert = change_tick.relative_to(*tracker.ticks.added).get();134let ticks_since_change = change_tick.relative_to(*tracker.ticks.changed).get();135assert!(ticks_since_insert > MAX_CHANGE_AGE);136assert!(ticks_since_change > MAX_CHANGE_AGE);137}138139// scan change ticks and clamp those at risk of overflow140world.check_change_ticks();141142for tracker in query.iter(&world) {143let ticks_since_insert = change_tick.relative_to(*tracker.ticks.added).get();144let ticks_since_change = change_tick.relative_to(*tracker.ticks.changed).get();145assert_eq!(ticks_since_insert, MAX_CHANGE_AGE);146assert_eq!(ticks_since_change, MAX_CHANGE_AGE);147}148}149150#[test]151fn mut_from_res_mut() {152let mut component_ticks = ComponentTicks {153added: Tick::new(1),154changed: Tick::new(2),155};156let mut caller = MaybeLocation::caller();157let ticks = ComponentTicksMut {158added: &mut component_ticks.added,159changed: &mut component_ticks.changed,160changed_by: caller.as_mut(),161last_run: Tick::new(3),162this_run: Tick::new(4),163};164let mut res = R {};165166let res_mut = ResMut {167value: &mut res,168ticks,169};170171let into_mut: Mut<R> = res_mut.into();172assert_eq!(1, into_mut.ticks.added.get());173assert_eq!(2, into_mut.ticks.changed.get());174assert_eq!(3, into_mut.ticks.last_run.get());175assert_eq!(4, into_mut.ticks.this_run.get());176}177178#[test]179fn mut_new() {180let mut component_ticks = ComponentTicks {181added: Tick::new(1),182changed: Tick::new(3),183};184let mut res = R {};185let mut caller = MaybeLocation::caller();186187let val = Mut::new(188&mut res,189&mut component_ticks.added,190&mut component_ticks.changed,191Tick::new(2), // last_run192Tick::new(4), // this_run193caller.as_mut(),194);195196assert!(!val.is_added());197assert!(val.is_changed());198}199200#[test]201fn mut_from_non_send_mut() {202let mut component_ticks = ComponentTicks {203added: Tick::new(1),204changed: Tick::new(2),205};206let mut caller = MaybeLocation::caller();207let ticks = ComponentTicksMut {208added: &mut component_ticks.added,209changed: &mut component_ticks.changed,210changed_by: caller.as_mut(),211last_run: Tick::new(3),212this_run: Tick::new(4),213};214let mut res = R {};215216let non_send_mut = NonSendMut {217value: &mut res,218ticks,219};220221let into_mut: Mut<R> = non_send_mut.into();222assert_eq!(1, into_mut.ticks.added.get());223assert_eq!(2, into_mut.ticks.changed.get());224assert_eq!(3, into_mut.ticks.last_run.get());225assert_eq!(4, into_mut.ticks.this_run.get());226}227228#[test]229fn map_mut() {230use super::*;231struct Outer(i64);232233let last_run = Tick::new(2);234let this_run = Tick::new(3);235let mut component_ticks = ComponentTicks {236added: Tick::new(1),237changed: Tick::new(2),238};239let mut caller = MaybeLocation::caller();240let ticks = ComponentTicksMut {241added: &mut component_ticks.added,242changed: &mut component_ticks.changed,243changed_by: caller.as_mut(),244last_run,245this_run,246};247248let mut outer = Outer(0);249250let ptr = Mut {251value: &mut outer,252ticks,253};254assert!(!ptr.is_changed());255256// Perform a mapping operation.257let mut inner = ptr.map_unchanged(|x| &mut x.0);258assert!(!inner.is_changed());259260// Mutate the inner value.261*inner = 64;262assert!(inner.is_changed());263// Modifying one field of a component should flag a change for the entire component.264assert!(component_ticks.is_changed(last_run, this_run));265}266267#[test]268fn set_if_neq() {269let mut world = World::new();270271world.insert_resource(R2(0));272// Resources are Changed when first added273world.increment_change_tick();274// This is required to update world::last_change_tick275world.clear_trackers();276277let mut r = world.resource_mut::<R2>();278assert!(!r.is_changed(), "Resource must begin unchanged.");279280r.set_if_neq(R2(0));281assert!(282!r.is_changed(),283"Resource must not be changed after setting to the same value."284);285286r.set_if_neq(R2(3));287assert!(288r.is_changed(),289"Resource must be changed after setting to a different value."290);291}292293#[test]294fn as_deref_mut() {295let mut world = World::new();296297world.insert_resource(R2(0));298// Resources are Changed when first added299world.increment_change_tick();300// This is required to update world::last_change_tick301world.clear_trackers();302303let mut r = world.resource_mut::<R2>();304assert!(!r.is_changed(), "Resource must begin unchanged.");305306let mut r = r.as_deref_mut();307assert!(308!r.is_changed(),309"Dereferencing should not mark the item as changed yet"310);311312r.set_if_neq(3);313assert!(314r.is_changed(),315"Resource must be changed after setting to a different value."316);317}318319#[test]320fn mut_untyped_to_reflect() {321let last_run = Tick::new(2);322let this_run = Tick::new(3);323let mut component_ticks = ComponentTicks {324added: Tick::new(1),325changed: Tick::new(2),326};327let mut caller = MaybeLocation::caller();328let ticks = ComponentTicksMut {329added: &mut component_ticks.added,330changed: &mut component_ticks.changed,331changed_by: caller.as_mut(),332last_run,333this_run,334};335336let mut value: i32 = 5;337338let value = MutUntyped {339value: PtrMut::from(&mut value),340ticks,341};342343let reflect_from_ptr = <ReflectFromPtr as FromType<i32>>::from_type();344345let mut new = value.map_unchanged(|ptr| {346// SAFETY: The underlying type of `ptr` matches `reflect_from_ptr`.347unsafe { reflect_from_ptr.as_reflect_mut(ptr) }348});349350assert!(!new.is_changed());351352new.reflect_mut();353354assert!(new.is_changed());355}356357#[test]358fn mut_untyped_from_mut() {359let mut component_ticks = ComponentTicks {360added: Tick::new(1),361changed: Tick::new(2),362};363let mut caller = MaybeLocation::caller();364let ticks = ComponentTicksMut {365added: &mut component_ticks.added,366changed: &mut component_ticks.changed,367changed_by: caller.as_mut(),368last_run: Tick::new(3),369this_run: Tick::new(4),370};371let mut c = C {};372373let mut_typed = Mut {374value: &mut c,375ticks,376};377378let into_mut: MutUntyped = mut_typed.into();379assert_eq!(1, into_mut.ticks.added.get());380assert_eq!(2, into_mut.ticks.changed.get());381assert_eq!(3, into_mut.ticks.last_run.get());382assert_eq!(4, into_mut.ticks.this_run.get());383}384}385386387