#include #include #include // ----------------------------------------------------------------------------- // Time. static simloop_time_t time_delta_from_sec(double seconds) { static constexpr double NANOS_PER_SEC = 1e9; return (simloop_time_t)(seconds * NANOS_PER_SEC); } // ----------------------------------------------------------------------------- // Randomness. typedef struct { uint64_t a; } XorShift64State; static 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) { Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10}); SimloopOut simout; simloop_update(&simloop, 0, &simout); TEST_TRUE(simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); } /// A frame is not re-rendered if time does not advance. /// This applies whether rendering is frame-rate capped or unlimited, and /// whether we are in the initial frame or a subsequent one. void simloop_render_not_retriggered( struct test_case_metadata* metadata, int max_render_fps, bool initial_frame) { Simloop simloop = simloop_make( &(SimloopArgs){.update_fps = 10, .max_render_fps = max_render_fps}); SimloopOut simout; simloop_update(&simloop, 0, &simout); TEST_TRUE(simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); if (!initial_frame) { // Advance time beyond the initial frame. simloop_update(&simloop, 1, &simout); } for (int i = 0; i < 10; i++) { // Note that time does not advance here. simloop_update(&simloop, 0, &simout); TEST_TRUE(!simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); } } TEST_CASE(simloop_render_not_retriggered_capped_initial_frame) { simloop_render_not_retriggered(metadata, 10, true); } TEST_CASE(simloop_render_not_retriggered_unlimited_initial_frame) { simloop_render_not_retriggered(metadata, 0, true); } TEST_CASE(simloop_render_not_retriggered_capped_subsequent_frame) { simloop_render_not_retriggered(metadata, 10, false); } TEST_CASE(simloop_render_not_retriggered_unlimited_subsequent_frame) { simloop_render_not_retriggered(metadata, 0, false); } /// A simulation loop with no render frame cap: /// 1. Updates based on the desired update frame rate. /// 2. Renders at every step. TEST_CASE(simloop_no_render_frame_cap) { constexpr int UPDATE_FPS = 10; // 100ms delta const simloop_time_t UPDATE_DDT = time_delta_from_sec(1.0 / (double)UPDATE_FPS); const simloop_time_t STEP = time_delta_from_sec(0.05); // 50ms const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(30); // We need simulation time to be an exact multiple of the desired deltas for // the modulo comparison below. TEST_TRUE((UPDATE_DDT % STEP) == 0); Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut simout; simloop_update(&simloop, 0, &simout); TEST_TRUE(!simout.should_update); // Time has not advanced. TEST_TRUE(simout.should_render); // Initial render. for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { simloop_update(&simloop, STEP, &simout); const bool expect_update = (t % UPDATE_DDT) == 0; TEST_EQUAL(simout.should_update, expect_update); TEST_TRUE(simout.should_render); // Always renders. } } /// 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 simloop_time_t UPDATE_DDT = time_delta_from_sec(1.0 / (double)UPDATE_FPS); const simloop_time_t RENDER_DDT = time_delta_from_sec(1.0 / (double)RENDER_FPS); const simloop_time_t STEP = time_delta_from_sec(0.1); // 100ms const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(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); Simloop simloop = simloop_make( &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS}); SimloopOut simout; simloop_update(&simloop, 0, &simout); TEST_TRUE(!simout.should_update); // Time has not advanced. TEST_TRUE(simout.should_render); // Initial render. for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { simloop_update(&simloop, STEP, &simout); // A render is still expected at time 0. TEST_EQUAL(simout.should_render, (t % RENDER_DDT) == 0); TEST_EQUAL(simout.should_update, (t % UPDATE_DDT) == 0); } } /// If the update falls behind the clock, then percent_frame can fall out of /// range (>1) if we are not careful. This tests for this condition. TEST_CASE(simloop_percent_frame_01_large_jump) { constexpr int UPDATE_FPS = 10; // 100ms delta const simloop_time_t UPDATE_DDT = time_delta_from_sec(1.0 / (double)UPDATE_FPS); const simloop_time_t STEP = time_delta_from_sec(1); const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(30); // We need simulation time to be an exact multiple of the desired deltas for // the modulo comparison below. TEST_TRUE((STEP % UPDATE_DDT) == 0); Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut simout; simloop_update(&simloop, 0, &simout); TEST_TRUE(!simout.should_update); // Time has not advanced. TEST_TRUE(simout.should_render); // Initial render. for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { simloop_update(&simloop, STEP, &simout); TEST_TRUE(simout.should_update); // Tries to catch up to clock. TEST_TRUE(simout.should_render); } } /// 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 simloop_time_t RANDOM_STEPS[] = { time_delta_from_sec(0.007), // 7ms time_delta_from_sec(0.005), // 5ms time_delta_from_sec(0.003), // 3ms }; constexpr uint64_t NUM_RANDOM_STEPS = sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]); const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(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){12069019817132197873ULL}; // Perform two simulations with random clock-time steps. for (int s = 0; s < 2; ++s) { simloop_time_t dt = 0; Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut simout; for (simloop_time_t t = 0; t <= SIM_DURATION_SEC;) { simloop_update(&simloop, dt, &simout); if (simout.should_update) { UPDATE_SIMULATION(sim[s]); } // Advance time with a random step. const simloop_time_t step = RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS]; t += step; dt = 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; }