Skip to content

Commit 3d27957

Browse files
authored
Try #3956:
2 parents cba9bcc + 9384cc9 commit 3d27957

File tree

7 files changed

+190
-118
lines changed

7 files changed

+190
-118
lines changed

crates/bevy_ecs/src/change_detection.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ use crate::{component::ComponentTicks, system::Resource};
55
use bevy_reflect::Reflect;
66
use std::ops::{Deref, DerefMut};
77

8+
/// The (arbitrarily chosen) minimum number of world tick increments between `check_tick` scans.
9+
///
10+
/// Change ticks can only be scanned when systems aren't running. Thus, if the threshold is `N`,
11+
/// the maximum is `2 * N - 1` (i.e. the world ticks `N - 1` times, then `N` times).
12+
///
13+
/// If no change is older than `u32::MAX - (2 * N - 1)` following a scan, none of their ages can
14+
/// overflow and cause false positives.
15+
// (518,400,000 = 1000 ticks per frame * 144 frames per second * 3600 seconds per hour)
16+
pub const CHECK_TICK_THRESHOLD: u32 = 518_400_000;
17+
18+
/// The maximum change tick difference that won't overflow before the next `check_tick` scan.
19+
///
20+
/// Changes stop being detected once they become this old.
21+
pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1);
22+
823
/// Types that implement reliable change detection.
924
///
1025
/// ## Example
@@ -199,3 +214,105 @@ pub struct ReflectMut<'a> {
199214
change_detection_impl!(ReflectMut<'a>, dyn Reflect,);
200215
#[cfg(feature = "bevy_reflect")]
201216
impl_into_inner!(ReflectMut<'a>, dyn Reflect,);
217+
218+
#[cfg(test)]
219+
mod tests {
220+
use crate::{
221+
self as bevy_ecs,
222+
change_detection::{CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE},
223+
component::Component,
224+
query::ChangeTrackers,
225+
system::{IntoSystem, Query, System},
226+
world::World,
227+
};
228+
229+
#[derive(Component)]
230+
struct C;
231+
232+
#[test]
233+
fn change_expiration() {
234+
fn change_detected(query: Query<ChangeTrackers<C>>) -> bool {
235+
query.single().is_changed()
236+
}
237+
238+
fn change_expired(query: Query<ChangeTrackers<C>>) -> bool {
239+
query.single().is_changed()
240+
}
241+
242+
let mut world = World::new();
243+
244+
// component added: 1, changed: 1
245+
world.spawn().insert(C);
246+
247+
let mut change_detected_system = IntoSystem::into_system(change_detected);
248+
let mut change_expired_system = IntoSystem::into_system(change_expired);
249+
change_detected_system.initialize(&mut world);
250+
change_expired_system.initialize(&mut world);
251+
252+
// world: 1, system last ran: 0, component changed: 1
253+
// The spawn will be detected since it happened after the system "last ran".
254+
assert!(change_detected_system.run((), &mut world));
255+
256+
// world: 1 + MAX_CHANGE_AGE
257+
let change_tick = world.change_tick.get_mut();
258+
*change_tick = change_tick.wrapping_add(MAX_CHANGE_AGE);
259+
260+
// Both the system and component appeared `MAX_CHANGE_AGE` ticks ago.
261+
// Since we clamp things to `MAX_CHANGE_AGE` for determinism,
262+
// `ComponentTicks::is_changed` will now see `MAX_CHANGE_AGE > MAX_CHANGE_AGE`
263+
// and return `false`.
264+
assert!(!change_expired_system.run((), &mut world));
265+
}
266+
267+
#[test]
268+
fn change_tick_wraparound() {
269+
fn change_detected(query: Query<ChangeTrackers<C>>) -> bool {
270+
query.single().is_changed()
271+
}
272+
273+
let mut world = World::new();
274+
world.last_change_tick = u32::MAX;
275+
*world.change_tick.get_mut() = 0;
276+
277+
// component added: 0, changed: 0
278+
world.spawn().insert(C);
279+
280+
// system last ran: u32::MAX
281+
let mut change_detected_system = IntoSystem::into_system(change_detected);
282+
change_detected_system.initialize(&mut world);
283+
284+
// Since the world is always ahead, as long as changes can't get older than `u32::MAX` (which we ensure),
285+
// the wrapping difference will always be positive, so wraparound doesn't matter.
286+
assert!(change_detected_system.run((), &mut world));
287+
}
288+
289+
#[test]
290+
fn change_tick_scan() {
291+
let mut world = World::new();
292+
293+
// component added: 1, changed: 1
294+
world.spawn().insert(C);
295+
296+
// a bunch of stuff happens, the component is now older than `MAX_CHANGE_AGE`
297+
*world.change_tick.get_mut() += MAX_CHANGE_AGE + CHECK_TICK_THRESHOLD;
298+
let change_tick = world.change_tick();
299+
300+
let mut query = world.query::<ChangeTrackers<C>>();
301+
for tracker in query.iter(&world) {
302+
let ticks_since_insert = change_tick.wrapping_sub(tracker.component_ticks.added);
303+
let ticks_since_change = change_tick.wrapping_sub(tracker.component_ticks.changed);
304+
assert!(ticks_since_insert > MAX_CHANGE_AGE);
305+
assert!(ticks_since_change > MAX_CHANGE_AGE);
306+
}
307+
308+
// scan change ticks and clamp those at risk of overflow
309+
world.check_change_ticks();
310+
311+
for tracker in query.iter(&world) {
312+
let ticks_since_insert = change_tick.wrapping_sub(tracker.component_ticks.added);
313+
let ticks_since_change = change_tick.wrapping_sub(tracker.component_ticks.changed);
314+
assert!(ticks_since_insert == MAX_CHANGE_AGE);
315+
assert!(ticks_since_change == MAX_CHANGE_AGE);
316+
}
317+
}
318+
}

crates/bevy_ecs/src/component.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Types for declaring and storing [`Component`]s.
22
33
use crate::{
4+
change_detection::MAX_CHANGE_AGE,
45
storage::{SparseSetIndex, Storages},
56
system::Resource,
67
};
@@ -338,6 +339,7 @@ impl Components {
338339
}
339340
}
340341

342+
/// Stores two [`World`](crate::world::World) "ticks" denoting when the component was added and when it was last changed (added or mutably-deferenced).
341343
#[derive(Clone, Debug)]
342344
pub struct ComponentTicks {
343345
pub(crate) added: u32,
@@ -346,22 +348,35 @@ pub struct ComponentTicks {
346348

347349
impl ComponentTicks {
348350
#[inline]
351+
/// Returns `true` if the component was added after the system last ran, `false` otherwise.
349352
pub fn is_added(&self, last_change_tick: u32, change_tick: u32) -> bool {
350-
// The comparison is relative to `change_tick` so that we can detect changes over the whole
351-
// `u32` range. Comparing directly the ticks would limit to half that due to overflow
352-
// handling.
353-
let component_delta = change_tick.wrapping_sub(self.added);
354-
let system_delta = change_tick.wrapping_sub(last_change_tick);
353+
// This works even with wraparound because the world tick (`change_tick`) is always "newer" than
354+
// `last_change_tick` and `self.added`, and we scan periodically to clamp `ComponentTicks` values
355+
// so they never get older than `u32::MAX` (the difference would overflow).
356+
//
357+
// The clamp here ensures determinism (since scans could differ between app runs).
358+
let ticks_since_insert = change_tick.wrapping_sub(self.added).min(MAX_CHANGE_AGE);
359+
let ticks_since_system = change_tick
360+
.wrapping_sub(last_change_tick)
361+
.min(MAX_CHANGE_AGE);
355362

356-
component_delta < system_delta
363+
ticks_since_system > ticks_since_insert
357364
}
358365

359366
#[inline]
367+
/// Returns `true` if the component was added or mutably-dereferenced after the system last ran, `false` otherwise.
360368
pub fn is_changed(&self, last_change_tick: u32, change_tick: u32) -> bool {
361-
let component_delta = change_tick.wrapping_sub(self.changed);
362-
let system_delta = change_tick.wrapping_sub(last_change_tick);
369+
// This works even with wraparound because the world tick (`change_tick`) is always "newer" than
370+
// `last_change_tick` and `self.changed`, and we scan periodically to clamp `ComponentTicks` values
371+
// so they never get older than `u32::MAX` (the difference would overflow).
372+
//
373+
// The clamp here ensures determinism (since scans could differ between app runs).
374+
let ticks_since_change = change_tick.wrapping_sub(self.changed).min(MAX_CHANGE_AGE);
375+
let ticks_since_system = change_tick
376+
.wrapping_sub(last_change_tick)
377+
.min(MAX_CHANGE_AGE);
363378

364-
component_delta < system_delta
379+
ticks_since_system > ticks_since_change
365380
}
366381

367382
pub(crate) fn new(change_tick: u32) -> Self {
@@ -377,8 +392,10 @@ impl ComponentTicks {
377392
}
378393

379394
/// Manually sets the change tick.
380-
/// Usually, this is done automatically via the [`DerefMut`](std::ops::DerefMut) implementation
381-
/// on [`Mut`](crate::world::Mut) or [`ResMut`](crate::system::ResMut) etc.
395+
///
396+
/// This is normally done automatically via the [`DerefMut`](std::ops::DerefMut) implementation
397+
/// on [`Mut<T>`](crate::world::Mut), [`ResMut<T>`](crate::system::ResMut), etc.
398+
/// However, components and resources that make use of interior mutability might require manual updates.
382399
///
383400
/// # Example
384401
/// ```rust,no_run
@@ -395,10 +412,10 @@ impl ComponentTicks {
395412
}
396413

397414
fn check_tick(last_change_tick: &mut u32, change_tick: u32) {
398-
let tick_delta = change_tick.wrapping_sub(*last_change_tick);
399-
const MAX_DELTA: u32 = (u32::MAX / 4) * 3;
400-
// Clamp to max delta
401-
if tick_delta > MAX_DELTA {
402-
*last_change_tick = change_tick.wrapping_sub(MAX_DELTA);
415+
let age = change_tick.wrapping_sub(*last_change_tick);
416+
// This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true
417+
// so long as this check always runs before that can happen.
418+
if age > MAX_CHANGE_AGE {
419+
*last_change_tick = change_tick.wrapping_sub(MAX_CHANGE_AGE);
403420
}
404421
}

crates/bevy_ecs/src/schedule/stage.rs

Lines changed: 16 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
change_detection::CHECK_TICK_THRESHOLD,
23
component::ComponentId,
34
prelude::IntoSystem,
45
schedule::{
@@ -424,15 +425,15 @@ impl SystemStage {
424425

425426
/// Rearranges all systems in topological orders. Systems must be initialized.
426427
fn rebuild_orders_and_dependencies(&mut self) {
427-
// This assertion is there to document that a maximum of `u32::MAX / 8` systems should be
428-
// added to a stage to guarantee that change detection has no false positive, but it
429-
// can be circumvented using exclusive or chained systems
428+
// This assertion exists to document that the number of systems in a stage is limited
429+
// to guarantee that change detection never yields false positives. However, it's possible
430+
// (but still unlikely) to circumvent this by abusing exclusive or chained systems.
430431
assert!(
431432
self.exclusive_at_start.len()
432433
+ self.exclusive_before_commands.len()
433434
+ self.exclusive_at_end.len()
434435
+ self.parallel.len()
435-
< (u32::MAX / 8) as usize
436+
< (CHECK_TICK_THRESHOLD as usize)
436437
);
437438
debug_assert!(
438439
self.uninitialized_run_criteria.is_empty()
@@ -562,17 +563,18 @@ impl SystemStage {
562563
}
563564
}
564565

565-
/// Checks for old component and system change ticks
566+
/// All system and component change ticks are scanned for risk of delta overflow once the world
567+
/// counter has incremented at least [`CHECK_TICK_THRESHOLD`](crate::change_detection::CHECK_TICK_THRESHOLD)
568+
/// times since the previous `check_tick` scan.
569+
///
570+
/// During each scan, any change ticks older than [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE)
571+
/// are clamped to that difference, preventing potential false positives due to overflow.
566572
fn check_change_ticks(&mut self, world: &mut World) {
567573
let change_tick = world.change_tick();
568-
let time_since_last_check = change_tick.wrapping_sub(self.last_tick_check);
569-
// Only check after at least `u32::MAX / 8` counts, and at most `u32::MAX / 4` counts
570-
// since the max number of [System] in a [SystemStage] is limited to `u32::MAX / 8`
571-
// and this function is called at the end of each [SystemStage] loop
572-
const MIN_TIME_SINCE_LAST_CHECK: u32 = u32::MAX / 8;
573-
574-
if time_since_last_check > MIN_TIME_SINCE_LAST_CHECK {
575-
// Check all system change ticks
574+
let ticks_since_last_check = change_tick.wrapping_sub(self.last_tick_check);
575+
576+
if ticks_since_last_check >= CHECK_TICK_THRESHOLD {
577+
// Check all system change ticks.
576578
for exclusive_system in &mut self.exclusive_at_start {
577579
exclusive_system.system_mut().check_change_tick(change_tick);
578580
}
@@ -586,9 +588,8 @@ impl SystemStage {
586588
parallel_system.system_mut().check_change_tick(change_tick);
587589
}
588590

589-
// Check component ticks
591+
// Check all component change ticks.
590592
world.check_change_ticks();
591-
592593
self.last_tick_check = change_tick;
593594
}
594595
}
@@ -947,8 +948,6 @@ impl Stage for SystemStage {
947948
#[cfg(test)]
948949
mod tests {
949950
use crate::{
950-
entity::Entity,
951-
query::{ChangeTrackers, Changed},
952951
schedule::{
953952
BoxedSystemLabel, ExclusiveSystemDescriptorCoercion, ParallelSystemDescriptorCoercion,
954953
RunCriteria, RunCriteriaDescriptorCoercion, RunCriteriaPiping, ShouldRun,
@@ -2011,75 +2010,6 @@ mod tests {
20112010
assert_eq!(*world.resource::<usize>(), 1);
20122011
}
20132012

2014-
#[test]
2015-
fn change_ticks_wrapover() {
2016-
const MIN_TIME_SINCE_LAST_CHECK: u32 = u32::MAX / 8;
2017-
const MAX_DELTA: u32 = (u32::MAX / 4) * 3;
2018-
2019-
let mut world = World::new();
2020-
world.spawn().insert(W(0usize));
2021-
*world.change_tick.get_mut() += MAX_DELTA + 1;
2022-
2023-
let mut stage = SystemStage::parallel();
2024-
fn work() {}
2025-
stage.add_system(work);
2026-
2027-
// Overflow twice
2028-
for _ in 0..10 {
2029-
stage.run(&mut world);
2030-
for tracker in world.query::<ChangeTrackers<W<usize>>>().iter(&world) {
2031-
let time_since_last_check = tracker
2032-
.change_tick
2033-
.wrapping_sub(tracker.component_ticks.added);
2034-
assert!(time_since_last_check <= MAX_DELTA);
2035-
let time_since_last_check = tracker
2036-
.change_tick
2037-
.wrapping_sub(tracker.component_ticks.changed);
2038-
assert!(time_since_last_check <= MAX_DELTA);
2039-
}
2040-
let change_tick = world.change_tick.get_mut();
2041-
*change_tick = change_tick.wrapping_add(MIN_TIME_SINCE_LAST_CHECK + 1);
2042-
}
2043-
}
2044-
2045-
#[test]
2046-
fn change_query_wrapover() {
2047-
use crate::{self as bevy_ecs, component::Component};
2048-
2049-
#[derive(Component)]
2050-
struct C;
2051-
let mut world = World::new();
2052-
2053-
// Spawn entities at various ticks
2054-
let component_ticks = [0, u32::MAX / 4, u32::MAX / 2, u32::MAX / 4 * 3, u32::MAX];
2055-
let ids = component_ticks
2056-
.iter()
2057-
.map(|tick| {
2058-
*world.change_tick.get_mut() = *tick;
2059-
world.spawn().insert(C).id()
2060-
})
2061-
.collect::<Vec<Entity>>();
2062-
2063-
let test_cases = [
2064-
// normal
2065-
(0, u32::MAX / 2, vec![ids[1], ids[2]]),
2066-
// just wrapped over
2067-
(u32::MAX / 2, 0, vec![ids[0], ids[3], ids[4]]),
2068-
];
2069-
for (last_change_tick, change_tick, changed_entities) in &test_cases {
2070-
*world.change_tick.get_mut() = *change_tick;
2071-
world.last_change_tick = *last_change_tick;
2072-
2073-
assert_eq!(
2074-
world
2075-
.query_filtered::<Entity, Changed<C>>()
2076-
.iter(&world)
2077-
.collect::<Vec<Entity>>(),
2078-
*changed_entities
2079-
);
2080-
}
2081-
}
2082-
20832013
#[test]
20842014
fn run_criteria_with_query() {
20852015
use crate::{self as bevy_ecs, component::Component};

crates/bevy_ecs/src/system/exclusive_system.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
22
archetype::ArchetypeGeneration,
3+
change_detection::MAX_CHANGE_AGE,
34
system::{check_system_change_tick, BoxedSystem, IntoSystem},
45
world::World,
56
};
@@ -44,7 +45,9 @@ where
4445
world.last_change_tick = saved_last_tick;
4546
}
4647

47-
fn initialize(&mut self, _: &mut World) {}
48+
fn initialize(&mut self, world: &mut World) {
49+
self.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE);
50+
}
4851

4952
fn check_change_tick(&mut self, change_tick: u32) {
5053
check_system_change_tick(&mut self.last_change_tick, change_tick, self.name.as_ref());

0 commit comments

Comments
 (0)