#include #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, .timer = &timer}); SimloopOut simout; simloop_update(&simloop, &simout); TEST_TRUE(simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); } /// The initial render is not re-triggered if there is a render frame rate cap /// and time does not advance. TEST_CASE(simloop_initial_render_not_retriggered) { Timer timer = {}; Simloop simloop = simloop_make( &(SimloopArgs){.update_fps = 10, .max_render_fps = 10, .timer = &timer}); SimloopOut simout; simloop_update(&simloop, &simout); TEST_TRUE(simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); for (int i = 0; i < 10; i++) { // Note that time does not advance. simloop_update(&simloop, &simout); TEST_TRUE(!simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); } } /// A simulation loop with no render frame cap: /// 1. Updates based on the desired update frame rate. /// 2. Renders at every loop (provided there are updates). TEST_CASE(simloop_no_render_frame_cap) { constexpr int UPDATE_FPS = 10; // 100ms delta const time_delta UPDATE_DDT = sec_to_time_delta(1.0 / (double)UPDATE_FPS); const time_delta STEP = sec_to_time_delta(1); const time_delta SIM_TIME_SEC = sec_to_time_delta(30); // We need simulation time to be an exact multiple of the desired deltas for // the modulo comparisons below. TEST_TRUE((STEP % UPDATE_DDT) == 0); 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) { timer_advance(&timer, t); simloop_update(&simloop, &simout); const bool expect_update = (t > 0) && ((t % UPDATE_DDT) == 0); // A render is still expected at time 0. TEST_EQUAL(simout.should_render, (t == 0) || expect_update); TEST_EQUAL(simout.should_update, expect_update); } } /// A simulation loop with a render frame cap: /// 1. Updates based on the desired update frame rate. /// 2. Renders based on the desired render frame rate. TEST_CASE(simloop_with_render_frame_cap) { constexpr int UPDATE_FPS = 10; // 100ms delta constexpr int RENDER_FPS = 5; // 200ms delta const time_delta UPDATE_DDT = sec_to_time_delta(1.0 / (double)UPDATE_FPS); const time_delta RENDER_DDT = sec_to_time_delta(1.0 / (double)RENDER_FPS); const time_delta STEP = sec_to_time_delta(0.1); // 100ms const time_delta SIM_TIME_SEC = sec_to_time_delta(30); // We need simulation time to be an exact multiple of the desired deltas for // the modulo comparisons below. TEST_TRUE((UPDATE_DDT % STEP) == 0); TEST_TRUE((RENDER_DDT % STEP) == 0); Timer timer = {}; Simloop simloop = simloop_make(&(SimloopArgs){ .update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS, .timer = &timer}); SimloopOut simout; for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { timer_advance(&timer, t); simloop_update(&simloop, &simout); // A render is still expected at time 0. TEST_EQUAL(simout.should_render, (t % RENDER_DDT) == 0); TEST_EQUAL(simout.should_update, (t > 0) && ((t % UPDATE_DDT) == 0)); } } /// 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; }