From b03c941b8b0ab51227aa52d10616dbd47f75e5d9 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 2 May 2026 14:58:00 -0700 Subject: Handle large time spikes --- simloop/src/simloop.c | 24 ++++++++++++++++-------- simloop/test/simloop_test.c | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) (limited to 'simloop') diff --git a/simloop/src/simloop.c b/simloop/src/simloop.c index d231ab3..4fa5f62 100644 --- a/simloop/src/simloop.c +++ b/simloop/src/simloop.c @@ -31,15 +31,23 @@ void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) { sim->clock += dt; // Simulation update. - // If the update falls behind the clock, we advance by a single ddt increment - // per loop iteration here and give it a chance to catch up over subsequent - // iterations. + // If the simulation falls behind the clock, we advance by a single ddt + // increment per loop iteration here and give it a chance to catch up over + // subsequent iterations. // This has the implication that percent_frame can fall out of range (>1) if - // we are not careful with how it is defined. See the general update function - // below. - const simloop_time_t delta = sim->clock - sim->update.time; - const bool update_this_tick = delta >= sim->update.ddt; - sim->update.time += update_this_tick ? sim->update.ddt : 0; + // we are not careful with how it is defined. See the logic below. + // If the delta is too large, then we simply warp the simulation to the wall + // clock. This avoids the appearance of the simulation playing in fast-forward + // as it tries to catch up. Large time spikes can typically occur at the start + // of the simulation when the application loads assets, compiles shaders, etc. + static const uint64_t max_catchup_frames = 10; + const simloop_time_t delta = sim->clock - sim->update.time; + const uint64_t delta_frames = delta / sim->update.ddt; + const bool update_this_tick = delta >= sim->update.ddt; + const bool warp = delta_frames > max_catchup_frames; + sim->update.time = + warp ? sim->clock + : (sim->update.time + (update_this_tick ? sim->update.ddt : 0)); // Loop-state update. sim->frame += (update_this_tick ? 1 : 0); diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c index 61e7dff..bcf9d57 100644 --- a/simloop/test/simloop_test.c +++ b/simloop/test/simloop_test.c @@ -222,8 +222,14 @@ TEST_CASE(simloop_determinism) { } /// The simulation loop attempts to catch up with the clock in the event of a -/// time spike. This is possible only if the simulation loops with a frequency -/// higher than the requested update frequency given by the update delta time. +/// time spike. +/// +/// Catch-up is possible only if the simulation loops with a frequency higher +/// than the requested update frequency given by the update delta time. +/// +/// Catch-up is performed only for sufficiently small time spikes. For large +/// time spikes, the simulation clock is warped. This test is for the small +/// time spike case. static void simloop_catch_up( struct test_case_metadata* metadata, int update_ddt_ms, int loop_step_ms, bool expect_catchup) { @@ -241,9 +247,10 @@ static void simloop_catch_up( int frames = 0; // Simulate a time spike. - // Advance time to t=10s. That is a lag of 10,000ms / 100ms = 100 frames. - // The simulation now has 20s to catch up. - simloop_time_t dt = time_delta_from_sec(10); + // Advance time to t=1s. That is a lag of 1,000ms / 100ms = 10 frames. + // 10 frames is the maximum allowed catch-up. + // The simulation now has 29s to catch up. + simloop_time_t dt = time_delta_from_sec(1); for (simloop_time_t t = dt; t <= SIM_DURATION_SEC;) { simloop_update(&simloop, dt, &simout); @@ -275,4 +282,25 @@ TEST_CASE(simloop_catch_up_failure) { simloop_catch_up(metadata, UPDATE_DDT_MS, LOOP_DDT_MS, false); } +/// This tests the large time spike case, where the simulation clock is warped +/// to the wall clock. +TEST_CASE(simloop_warp) { + const int UPDATE_FPS = 50; + const simloop_time_t UPDATE_DDT = + time_delta_from_sec(1.0 / (double)UPDATE_FPS); + + Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); + SimloopOut simout; + + // The maximum allowed catch-up is 10 frames. Simulate a time spike larger + // than that. + const simloop_time_t TIME_SPIKE = UPDATE_DDT * 20; + simloop_update(&simloop, TIME_SPIKE, &simout); + TEST_TRUE(simout.should_update); // Warp should still request update. + + // Now "advance" by 0. + simloop_update(&simloop, 0, &simout); + TEST_TRUE(!simout.should_update); // No more updates after warp. +} + int main() { return 0; } -- cgit v1.2.3