fix: cap fixed-step catch-up

This commit is contained in:
2026-06-22 17:02:00 +04:00
parent ccd61c05b0
commit 42441082f0
+74 -12
View File
@@ -105,6 +105,8 @@ pub struct FixedStepClock {
tick: Tick,
paused: bool,
platform_event_collections: u64,
dropped_presentation_millis: u64,
dropped_presentation_frames: u64,
}
/// Fixed-step configuration.
@@ -112,11 +114,16 @@ pub struct FixedStepClock {
pub struct FixedStepConfig {
/// Milliseconds per simulation tick.
pub step_millis: u32,
/// Maximum simulation ticks executed for a single presentation frame.
pub max_steps_per_frame: u32,
}
impl Default for FixedStepConfig {
fn default() -> Self {
Self { step_millis: 16 }
Self {
step_millis: 16,
max_steps_per_frame: 8,
}
}
}
@@ -176,7 +183,9 @@ impl std::fmt::Display for WorldError {
Self::DuplicateOriginalObjectId(id) => {
write!(f, "original object id {} is already registered", id.0)
}
Self::InvalidFixedStep => write!(f, "fixed-step configuration must be non-zero"),
Self::InvalidFixedStep => {
write!(f, "fixed-step configuration values must be non-zero")
}
}
}
}
@@ -433,9 +442,10 @@ pub fn canonical_state_hash(world: &World) -> StateHash {
///
/// # Errors
///
/// Returns [`WorldError::InvalidFixedStep`] when the configured step is zero.
/// Returns [`WorldError::InvalidFixedStep`] when the configured step or
/// per-frame catch-up limit is zero.
pub fn fixed_step_clock(config: FixedStepConfig) -> Result<FixedStepClock, WorldError> {
if config.step_millis == 0 {
if config.step_millis == 0 || config.max_steps_per_frame == 0 {
return Err(WorldError::InvalidFixedStep);
}
Ok(FixedStepClock {
@@ -443,6 +453,8 @@ pub fn fixed_step_clock(config: FixedStepConfig) -> Result<FixedStepClock, World
tick: Tick(0),
paused: false,
platform_event_collections: 0,
dropped_presentation_millis: 0,
dropped_presentation_frames: 0,
})
}
@@ -462,13 +474,14 @@ pub fn set_paused(clock: &mut FixedStepClock, paused: bool) {
///
/// # Errors
///
/// Returns [`WorldError::InvalidFixedStep`] when the configured step is zero.
/// Returns [`WorldError::InvalidFixedStep`] when the configured step or
/// per-frame catch-up limit is zero.
pub fn advance_fixed_step(
clock: &mut FixedStepClock,
config: FixedStepConfig,
elapsed_millis: u64,
) -> Result<u32, WorldError> {
if config.step_millis == 0 {
if config.step_millis == 0 || config.max_steps_per_frame == 0 {
return Err(WorldError::InvalidFixedStep);
}
if clock.paused {
@@ -476,12 +489,20 @@ pub fn advance_fixed_step(
}
clock.accumulated_millis = clock.accumulated_millis.saturating_add(elapsed_millis);
let step = u64::from(config.step_millis);
let mut ticks = 0_u32;
while clock.accumulated_millis >= step {
clock.accumulated_millis -= step;
clock.tick.0 = clock.tick.0.saturating_add(1);
ticks = ticks.saturating_add(1);
let available_steps = clock.accumulated_millis / step;
let ticks_u64 = available_steps.min(u64::from(config.max_steps_per_frame));
let consumed = ticks_u64.saturating_mul(step);
if available_steps > u64::from(config.max_steps_per_frame) {
let dropped = clock.accumulated_millis.saturating_sub(consumed);
clock.dropped_presentation_millis =
clock.dropped_presentation_millis.saturating_add(dropped);
clock.dropped_presentation_frames = clock.dropped_presentation_frames.saturating_add(1);
clock.accumulated_millis = 0;
} else {
clock.accumulated_millis = clock.accumulated_millis.saturating_sub(consumed);
}
let ticks = u32::try_from(ticks_u64).unwrap_or(u32::MAX);
clock.tick.0 = clock.tick.0.saturating_add(ticks_u64);
Ok(ticks)
}
@@ -497,6 +518,18 @@ pub fn platform_event_collections(clock: &FixedStepClock) -> u64 {
clock.platform_event_collections
}
/// Returns total presentation time dropped by fixed-step catch-up limits.
#[must_use]
pub fn dropped_presentation_millis(clock: &FixedStepClock) -> u64 {
clock.dropped_presentation_millis
}
/// Returns how many presentation frames exceeded fixed-step catch-up limits.
#[must_use]
pub fn dropped_presentation_frames(clock: &FixedStepClock) -> u64 {
clock.dropped_presentation_frames
}
/// Runs end-frame callbacks in stable sequence order.
#[must_use]
pub fn end_frame_callback_order(mut callbacks: Vec<WorldEvent>) -> Vec<u64> {
@@ -805,7 +838,10 @@ mod tests {
#[test]
fn fixed_step_pause_and_long_determinism_are_stable() {
let config = FixedStepConfig { step_millis: 20 };
let config = FixedStepConfig {
step_millis: 20,
max_steps_per_frame: 8,
};
let mut clock = fixed_step_clock(config).expect("clock");
collect_platform_events(&mut clock);
set_paused(&mut clock, true);
@@ -829,6 +865,32 @@ mod tests {
assert_eq!(first_hashes, second_hashes);
}
#[test]
fn fixed_step_catch_up_is_capped_and_reports_dropped_time() {
let config = FixedStepConfig {
step_millis: 20,
max_steps_per_frame: 3,
};
let mut clock = fixed_step_clock(config).expect("clock");
assert_eq!(advance_fixed_step(&mut clock, config, 95), Ok(3));
assert_eq!(fixed_step_tick(&clock), Tick(3));
assert_eq!(dropped_presentation_millis(&clock), 35);
assert_eq!(dropped_presentation_frames(&clock), 1);
assert_eq!(advance_fixed_step(&mut clock, config, 10), Ok(0));
assert_eq!(advance_fixed_step(&mut clock, config, 10), Ok(1));
assert_eq!(fixed_step_tick(&clock), Tick(4));
assert_eq!(dropped_presentation_millis(&clock), 35);
assert_eq!(dropped_presentation_frames(&clock), 1);
assert_eq!(
advance_fixed_step(&mut clock, config, u64::MAX),
Ok(config.max_steps_per_frame)
);
assert_eq!(dropped_presentation_frames(&clock), 2);
}
#[test]
fn render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order() {
let callbacks = vec![