Skip to main content

bevy_ecs/change_detection/
mod.rs

1//! Types that detect when their internal data mutate.
2
3mod maybe_location;
4mod params;
5mod tick;
6mod traits;
7
8pub use maybe_location::MaybeLocation;
9pub use params::*;
10pub use tick::*;
11pub use traits::{DetectChanges, DetectChangesMut};
12
13/// The (arbitrarily chosen) minimum number of world tick increments between `check_tick` scans.
14///
15/// Change ticks can only be scanned when systems aren't running. Thus, if the threshold is `N`,
16/// the maximum is `2 * N - 1` (i.e. the world ticks `N - 1` times, then `N` times).
17///
18/// If no change is older than `u32::MAX - (2 * N - 1)` following a scan, none of their ages can
19/// overflow and cause false positives.
20// (518,400,000 = 1000 ticks per frame * 144 frames per second * 3600 seconds per hour)
21pub const CHECK_TICK_THRESHOLD: u32 = 518_400_000;
22
23/// The maximum change tick difference that won't overflow before the next `check_tick` scan.
24///
25/// Changes stop being detected once they become this old.
26pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1);
27
28#[cfg(test)]
29mod tests {
30    use bevy_ecs_macros::Resource;
31    use bevy_ptr::PtrMut;
32    use bevy_reflect::{CreateTypeData, ReflectFromPtr};
33    use core::ops::{Deref, DerefMut};
34
35    use crate::{
36        change_detection::{
37            AtomicTick, ComponentTicks, ComponentTicksMut, MaybeLocation, Mut, NonSendMut, Ref,
38            ResMut, Tick, CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE,
39        },
40        component::Component,
41        system::{IntoSystem, Single, System},
42        world::World,
43    };
44
45    use super::{DetectChanges, DetectChangesMut, MutUntyped};
46
47    #[derive(Component, PartialEq)]
48    struct C;
49
50    #[derive(Resource)]
51    struct R;
52
53    #[derive(Resource, PartialEq)]
54    struct R2(u8);
55
56    impl Deref for R2 {
57        type Target = u8;
58        fn deref(&self) -> &u8 {
59            &self.0
60        }
61    }
62
63    impl DerefMut for R2 {
64        fn deref_mut(&mut self) -> &mut u8 {
65            &mut self.0
66        }
67    }
68
69    #[test]
70    fn change_expiration() {
71        fn change_detected(query: Option<Single<Ref<C>>>) -> bool {
72            query.unwrap().is_changed()
73        }
74
75        fn change_expired(query: Option<Single<Ref<C>>>) -> bool {
76            query.unwrap().is_changed()
77        }
78
79        let mut world = World::new();
80
81        // component added: 1, changed: 1
82        world.spawn(C);
83
84        let mut change_detected_system = IntoSystem::into_system(change_detected);
85        let mut change_expired_system = IntoSystem::into_system(change_expired);
86        change_detected_system.initialize(&mut world);
87        change_expired_system.initialize(&mut world);
88
89        // world: 1, system last ran: 0, component changed: 1
90        // The spawn will be detected since it happened after the system "last ran".
91        assert!(change_detected_system.run((), &mut world).unwrap());
92
93        // world: 1 + MAX_CHANGE_AGE
94        let change_tick = world.change_tick.get_mut();
95        *change_tick = change_tick.wrapping_add(MAX_CHANGE_AGE);
96
97        // Both the system and component appeared `MAX_CHANGE_AGE` ticks ago.
98        // Since we clamp things to `MAX_CHANGE_AGE` for determinism,
99        // `ComponentTicks::is_changed` will now see `MAX_CHANGE_AGE > MAX_CHANGE_AGE`
100        // and return `false`.
101        assert!(!change_expired_system.run((), &mut world).unwrap());
102    }
103
104    #[test]
105    fn change_tick_wraparound() {
106        let mut world = World::new();
107        world.last_change_tick = Tick::new(u32::MAX);
108        *world.change_tick.get_mut() = 0;
109
110        // component added: 0, changed: 0
111        world.spawn(C);
112
113        world.increment_change_tick();
114
115        // Since the world is always ahead, as long as changes can't get older than `u32::MAX` (which we ensure),
116        // the wrapping difference will always be positive, so wraparound doesn't matter.
117        let mut query = world.query::<Ref<C>>();
118        assert!(query.single(&world).unwrap().is_changed());
119    }
120
121    #[test]
122    fn change_tick_scan() {
123        let mut world = World::new();
124
125        // component added: 1, changed: 1
126        world.spawn(C);
127
128        // a bunch of stuff happens, the component is now older than `MAX_CHANGE_AGE`
129        *world.change_tick.get_mut() += MAX_CHANGE_AGE + CHECK_TICK_THRESHOLD;
130        let change_tick = world.change_tick();
131
132        let mut query = world.query::<Ref<C>>();
133        for tracker in query.iter(&world) {
134            let ticks_since_insert = change_tick.relative_to(*tracker.ticks.added).get();
135            let ticks_since_change = change_tick.relative_to(*tracker.ticks.changed).get();
136            assert!(ticks_since_insert > MAX_CHANGE_AGE);
137            assert!(ticks_since_change > MAX_CHANGE_AGE);
138        }
139
140        // scan change ticks and clamp those at risk of overflow
141        world.check_change_ticks();
142
143        for tracker in query.iter(&world) {
144            let ticks_since_insert = change_tick.relative_to(*tracker.ticks.added).get();
145            let ticks_since_change = change_tick.relative_to(*tracker.ticks.changed).get();
146            assert_eq!(ticks_since_insert, MAX_CHANGE_AGE);
147            assert_eq!(ticks_since_change, MAX_CHANGE_AGE);
148        }
149    }
150
151    #[test]
152    fn mut_from_res_mut() {
153        let mut component_ticks = ComponentTicks {
154            added: Tick::new(1),
155            changed: Tick::new(2),
156        };
157        let mut caller = MaybeLocation::caller();
158        let summary_tick = AtomicTick::default();
159        let ticks = ComponentTicksMut {
160            added: &mut component_ticks.added,
161            changed: &mut component_ticks.changed,
162            changed_by: caller.as_mut(),
163            last_run: Tick::new(3),
164            this_run: Tick::new(4),
165            summary_tick: Some(&summary_tick),
166        };
167        let mut res = R {};
168
169        let res_mut = ResMut {
170            value: &mut res,
171            ticks,
172        };
173
174        let into_mut: Mut<R> = res_mut.into();
175        assert_eq!(1, into_mut.ticks.added.get());
176        assert_eq!(2, into_mut.ticks.changed.get());
177        assert_eq!(3, into_mut.ticks.last_run.get());
178        assert_eq!(4, into_mut.ticks.this_run.get());
179    }
180
181    #[test]
182    fn mut_new() {
183        let mut component_ticks = ComponentTicks {
184            added: Tick::new(1),
185            changed: Tick::new(3),
186        };
187        let mut res = R {};
188        let mut caller = MaybeLocation::caller();
189        let summary_tick = AtomicTick::default();
190
191        let val = Mut::new(
192            &mut res,
193            &mut component_ticks.added,
194            &mut component_ticks.changed,
195            Some(&summary_tick),
196            Tick::new(2), // last_run
197            Tick::new(4), // this_run
198            caller.as_mut(),
199        );
200
201        assert!(!val.is_added());
202        assert!(val.is_changed());
203    }
204
205    #[test]
206    fn mut_from_non_send_mut() {
207        let mut component_ticks = ComponentTicks {
208            added: Tick::new(1),
209            changed: Tick::new(2),
210        };
211        let mut caller = MaybeLocation::caller();
212        let summary_tick = AtomicTick::default();
213        let ticks = ComponentTicksMut {
214            added: &mut component_ticks.added,
215            changed: &mut component_ticks.changed,
216            changed_by: caller.as_mut(),
217            last_run: Tick::new(3),
218            this_run: Tick::new(4),
219            summary_tick: Some(&summary_tick),
220        };
221        let mut res = R {};
222
223        let non_send_mut = NonSendMut {
224            value: &mut res,
225            ticks,
226        };
227
228        let into_mut: Mut<R> = non_send_mut.into();
229        assert_eq!(1, into_mut.ticks.added.get());
230        assert_eq!(2, into_mut.ticks.changed.get());
231        assert_eq!(3, into_mut.ticks.last_run.get());
232        assert_eq!(4, into_mut.ticks.this_run.get());
233    }
234
235    #[test]
236    fn map_mut() {
237        use super::*;
238        struct Outer(i64);
239
240        let last_run = Tick::new(2);
241        let this_run = Tick::new(3);
242        let mut component_ticks = ComponentTicks {
243            added: Tick::new(1),
244            changed: Tick::new(2),
245        };
246        let mut caller = MaybeLocation::caller();
247        let summary_tick = AtomicTick::default();
248        let ticks = ComponentTicksMut {
249            added: &mut component_ticks.added,
250            changed: &mut component_ticks.changed,
251            changed_by: caller.as_mut(),
252            last_run,
253            this_run,
254            summary_tick: Some(&summary_tick),
255        };
256
257        let mut outer = Outer(0);
258
259        let ptr = Mut {
260            value: &mut outer,
261            ticks,
262        };
263        assert!(!ptr.is_changed());
264
265        // Perform a mapping operation.
266        let mut inner = ptr.map_unchanged(|x| &mut x.0);
267        assert!(!inner.is_changed());
268
269        // Mutate the inner value.
270        *inner = 64;
271        assert!(inner.is_changed());
272        // Modifying one field of a component should flag a change for the entire component.
273        assert!(component_ticks.is_changed(last_run, this_run));
274    }
275
276    #[test]
277    fn set_if_neq() {
278        let mut world = World::new();
279
280        world.insert_resource(R2(0));
281        // Resources are Changed when first added
282        world.increment_change_tick();
283        // This is required to update world::last_change_tick
284        world.clear_trackers();
285
286        let mut r = world.resource_mut::<R2>();
287        assert!(!r.is_changed(), "Resource must begin unchanged.");
288
289        r.set_if_neq(R2(0));
290        assert!(
291            !r.is_changed(),
292            "Resource must not be changed after setting to the same value."
293        );
294
295        r.set_if_neq(R2(3));
296        assert!(
297            r.is_changed(),
298            "Resource must be changed after setting to a different value."
299        );
300    }
301
302    #[test]
303    fn as_deref_mut() {
304        let mut world = World::new();
305
306        world.insert_resource(R2(0));
307        // Resources are Changed when first added
308        world.increment_change_tick();
309        // This is required to update world::last_change_tick
310        world.clear_trackers();
311
312        let mut r = world.resource_mut::<R2>();
313        assert!(!r.is_changed(), "Resource must begin unchanged.");
314
315        let mut r = r.as_deref_mut();
316        assert!(
317            !r.is_changed(),
318            "Dereferencing should not mark the item as changed yet"
319        );
320
321        r.set_if_neq(3);
322        assert!(
323            r.is_changed(),
324            "Resource must be changed after setting to a different value."
325        );
326    }
327
328    #[test]
329    fn mut_untyped_to_reflect() {
330        let last_run = Tick::new(2);
331        let this_run = Tick::new(3);
332        let mut component_ticks = ComponentTicks {
333            added: Tick::new(1),
334            changed: Tick::new(2),
335        };
336        let mut caller = MaybeLocation::caller();
337        let summary_tick = AtomicTick::default();
338        let ticks = ComponentTicksMut {
339            added: &mut component_ticks.added,
340            changed: &mut component_ticks.changed,
341            changed_by: caller.as_mut(),
342            last_run,
343            this_run,
344            summary_tick: Some(&summary_tick),
345        };
346
347        let mut value: i32 = 5;
348
349        let value = MutUntyped {
350            value: PtrMut::from(&mut value),
351            ticks,
352        };
353
354        let reflect_from_ptr = <ReflectFromPtr as CreateTypeData<i32>>::create_type_data(());
355
356        let mut new = value.map_unchanged(|ptr| {
357            // SAFETY: The underlying type of `ptr` matches `reflect_from_ptr`.
358            unsafe { reflect_from_ptr.ptr_as_reflect_mut(ptr) }
359        });
360
361        assert!(!new.is_changed());
362
363        new.reflect_mut();
364
365        assert!(new.is_changed());
366    }
367
368    #[test]
369    fn mut_untyped_from_mut() {
370        let mut component_ticks = ComponentTicks {
371            added: Tick::new(1),
372            changed: Tick::new(2),
373        };
374        let mut caller = MaybeLocation::caller();
375        let summary_tick = AtomicTick::default();
376        let ticks = ComponentTicksMut {
377            added: &mut component_ticks.added,
378            changed: &mut component_ticks.changed,
379            changed_by: caller.as_mut(),
380            last_run: Tick::new(3),
381            this_run: Tick::new(4),
382            summary_tick: Some(&summary_tick),
383        };
384        let mut c = C {};
385
386        let mut_typed = Mut {
387            value: &mut c,
388            ticks,
389        };
390
391        let into_mut: MutUntyped = mut_typed.into();
392        assert_eq!(1, into_mut.ticks.added.get());
393        assert_eq!(2, into_mut.ticks.changed.get());
394        assert_eq!(3, into_mut.ticks.last_run.get());
395        assert_eq!(4, into_mut.ticks.this_run.get());
396    }
397}