fix: cap fixed-step catch-up
This commit is contained in:
@@ -105,6 +105,8 @@ pub struct FixedStepClock {
|
|||||||
tick: Tick,
|
tick: Tick,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
platform_event_collections: u64,
|
platform_event_collections: u64,
|
||||||
|
dropped_presentation_millis: u64,
|
||||||
|
dropped_presentation_frames: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fixed-step configuration.
|
/// Fixed-step configuration.
|
||||||
@@ -112,11 +114,16 @@ pub struct FixedStepClock {
|
|||||||
pub struct FixedStepConfig {
|
pub struct FixedStepConfig {
|
||||||
/// Milliseconds per simulation tick.
|
/// Milliseconds per simulation tick.
|
||||||
pub step_millis: u32,
|
pub step_millis: u32,
|
||||||
|
/// Maximum simulation ticks executed for a single presentation frame.
|
||||||
|
pub max_steps_per_frame: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FixedStepConfig {
|
impl Default for FixedStepConfig {
|
||||||
fn default() -> Self {
|
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) => {
|
Self::DuplicateOriginalObjectId(id) => {
|
||||||
write!(f, "original object id {} is already registered", id.0)
|
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
|
/// # 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> {
|
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);
|
return Err(WorldError::InvalidFixedStep);
|
||||||
}
|
}
|
||||||
Ok(FixedStepClock {
|
Ok(FixedStepClock {
|
||||||
@@ -443,6 +453,8 @@ pub fn fixed_step_clock(config: FixedStepConfig) -> Result<FixedStepClock, World
|
|||||||
tick: Tick(0),
|
tick: Tick(0),
|
||||||
paused: false,
|
paused: false,
|
||||||
platform_event_collections: 0,
|
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
|
/// # 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(
|
pub fn advance_fixed_step(
|
||||||
clock: &mut FixedStepClock,
|
clock: &mut FixedStepClock,
|
||||||
config: FixedStepConfig,
|
config: FixedStepConfig,
|
||||||
elapsed_millis: u64,
|
elapsed_millis: u64,
|
||||||
) -> Result<u32, WorldError> {
|
) -> Result<u32, WorldError> {
|
||||||
if config.step_millis == 0 {
|
if config.step_millis == 0 || config.max_steps_per_frame == 0 {
|
||||||
return Err(WorldError::InvalidFixedStep);
|
return Err(WorldError::InvalidFixedStep);
|
||||||
}
|
}
|
||||||
if clock.paused {
|
if clock.paused {
|
||||||
@@ -476,12 +489,20 @@ pub fn advance_fixed_step(
|
|||||||
}
|
}
|
||||||
clock.accumulated_millis = clock.accumulated_millis.saturating_add(elapsed_millis);
|
clock.accumulated_millis = clock.accumulated_millis.saturating_add(elapsed_millis);
|
||||||
let step = u64::from(config.step_millis);
|
let step = u64::from(config.step_millis);
|
||||||
let mut ticks = 0_u32;
|
let available_steps = clock.accumulated_millis / step;
|
||||||
while clock.accumulated_millis >= step {
|
let ticks_u64 = available_steps.min(u64::from(config.max_steps_per_frame));
|
||||||
clock.accumulated_millis -= step;
|
let consumed = ticks_u64.saturating_mul(step);
|
||||||
clock.tick.0 = clock.tick.0.saturating_add(1);
|
if available_steps > u64::from(config.max_steps_per_frame) {
|
||||||
ticks = ticks.saturating_add(1);
|
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)
|
Ok(ticks)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,6 +518,18 @@ pub fn platform_event_collections(clock: &FixedStepClock) -> u64 {
|
|||||||
clock.platform_event_collections
|
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.
|
/// Runs end-frame callbacks in stable sequence order.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn end_frame_callback_order(mut callbacks: Vec<WorldEvent>) -> Vec<u64> {
|
pub fn end_frame_callback_order(mut callbacks: Vec<WorldEvent>) -> Vec<u64> {
|
||||||
@@ -805,7 +838,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fixed_step_pause_and_long_determinism_are_stable() {
|
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");
|
let mut clock = fixed_step_clock(config).expect("clock");
|
||||||
collect_platform_events(&mut clock);
|
collect_platform_events(&mut clock);
|
||||||
set_paused(&mut clock, true);
|
set_paused(&mut clock, true);
|
||||||
@@ -829,6 +865,32 @@ mod tests {
|
|||||||
assert_eq!(first_hashes, second_hashes);
|
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]
|
#[test]
|
||||||
fn render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order() {
|
fn render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order() {
|
||||||
let callbacks = vec![
|
let callbacks = vec![
|
||||||
|
|||||||
Reference in New Issue
Block a user