aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 16:02:00 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 16:02:00 +0300
commit42441082f016ffb204eb47f6ff4e97f0ba2e94f4 (patch)
treef8eb3437eeefc8afb25313dee79072ea076a1cf8
parentccd61c05b032cedb113d7f2cc9cf16d0b5721d26 (diff)
downloadfparkan-42441082f016ffb204eb47f6ff4e97f0ba2e94f4.tar.xz
fparkan-42441082f016ffb204eb47f6ff4e97f0ba2e94f4.zip
fix: cap fixed-step catch-up
-rw-r--r--crates/fparkan-world/src/lib.rs86
1 files changed, 74 insertions, 12 deletions
diff --git a/crates/fparkan-world/src/lib.rs b/crates/fparkan-world/src/lib.rs
index ec988ff..9059a23 100644
--- a/crates/fparkan-world/src/lib.rs
+++ b/crates/fparkan-world/src/lib.rs
@@ -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);
@@ -830,6 +866,32 @@ mod tests {
}
#[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![
WorldEvent {