From ecf662ccea7eac7d46c6cfd2fc413f1d7f821bc6 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 11 Apr 2026 17:27:18 -0700 Subject: Add determinism test --- simloop/test/simloop_test.c | 106 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 6 deletions(-) (limited to 'simloop') diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c index 9f11e86..41f9ac2 100644 --- a/simloop/test/simloop_test.c +++ b/simloop/test/simloop_test.c @@ -3,13 +3,32 @@ #include #include +#include + +// ----------------------------------------------------------------------------- +// Randomness. + +typedef struct { + uint64_t a; +} XorShift64State; + +uint64_t xorshift64(XorShift64State* state) { + uint64_t x = state->a; + x ^= x << 7; + x ^= x >> 9; + return state->a = x; +} + +// ----------------------------------------------------------------------------- +// Tests. + /// At time/frame 0: /// 1. An initial render is always triggered. /// 2. No update is triggered (not enough time passed). TEST_CASE(simloop_initial_render) { - Timer timer = {}; - Simloop simloop = simloop_make( - &(SimloopArgs){.update_fps = 10, .max_render_fps = 0, .timer = &timer}); + Timer timer = {}; + Simloop simloop = + simloop_make(&(SimloopArgs){.update_fps = 10, .timer = &timer}); SimloopOut simout; simloop_update(&simloop, &simout); @@ -51,9 +70,9 @@ TEST_CASE(simloop_no_render_frame_cap) { const time_delta STEP = sec_to_time_delta(1); const time_delta SIM_TIME_SEC = sec_to_time_delta(30); - Timer timer = {}; - Simloop simloop = simloop_make(&(SimloopArgs){ - .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer}); + Timer timer = {}; + Simloop simloop = + simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer}); SimloopOut simout; for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { @@ -91,4 +110,79 @@ TEST_CASE(simloop_with_render_frame_cap) { } } +/// One benefit of fixed over variable time deltas is determinism. Test for +/// this by getting to t=10 by different clock time increments. +/// +/// Note that the time increments must be able to keep up with the desired frame +/// delta, otherwise determinism is not maintained. We can guarantee determinism +/// at the expense of re-introducing divergence. +/// TODO: Perhaps the API should return an update count instead of a boolean, +/// advance simulation time per the number of updates, then leave it up to +/// the client to decide whether to update just once or as many times as +/// requested, depending on whether they want determinism or convergence. +TEST_CASE(simloop_determinism) { + constexpr int UPDATE_FPS = 100; // 10ms delta + const time_delta RANDOM_STEPS[] = { + sec_to_time_delta(0.007), // 7ms + sec_to_time_delta(0.005), // 5ms + sec_to_time_delta(0.003), // 3ms + }; + constexpr uint64_t NUM_RANDOM_STEPS = + sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]); + const time_delta SIM_TIME_SEC = sec_to_time_delta(10); + constexpr float ADD = 0.123f; + + typedef struct Simulation { + int iter_count; + float sum; + } Simulation; + +#define UPDATE_SIMULATION(SIM) \ + { \ + SIM.sum += ADD; \ + SIM.iter_count++; \ + } + + Simulation sim[2] = {0}; + XorShift64State xss = (XorShift64State){12069019817132197873}; + + // Perform two simulations with random clock-time steps. + for (int s = 0; s < 2; ++s) { + Timer timer = {}; + Simloop simloop = + simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer}); + SimloopOut simout; + + for (time_delta t = 0; t < SIM_TIME_SEC;) { + timer_advance(&timer, t); + simloop_update(&simloop, &simout); + + if (simout.should_update) { + UPDATE_SIMULATION(sim[s]); + } + + // Advance time with a random step. + const time_delta step = RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS]; + t += step; + } + } + + // Make sure the simulations have advanced by the same number of updates so + // that we can compare them. They may not have had the same update count + // depending on the clock-time steps. + while (sim[0].iter_count < sim[1].iter_count) { + UPDATE_SIMULATION(sim[0]); + } + while (sim[1].iter_count < sim[0].iter_count) { + UPDATE_SIMULATION(sim[1]); + } + TEST_EQUAL(sim[0].iter_count, sim[1].iter_count); + + // The sums should be exactly equal if determinism holds. + // Check also that they are non-zero to make sure the simulation actually + // advanced. + TEST_TRUE(sim[0].sum > 0.f); + TEST_EQUAL(sim[0].sum, sim[1].sum); +} + int main() { return 0; } -- cgit v1.2.3