diff options
Diffstat (limited to 'simloop/test')
| -rw-r--r-- | simloop/test/simloop_test.c | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c new file mode 100644 index 0000000..bcf9d57 --- /dev/null +++ b/simloop/test/simloop_test.c | |||
| @@ -0,0 +1,306 @@ | |||
| 1 | #include <simloop.h> | ||
| 2 | |||
| 3 | #include <test.h> | ||
| 4 | |||
| 5 | #include <stdint.h> | ||
| 6 | |||
| 7 | // ----------------------------------------------------------------------------- | ||
| 8 | // Time. | ||
| 9 | |||
| 10 | static simloop_time_t time_delta_from_sec(double seconds) { | ||
| 11 | static constexpr double NANOS_PER_SEC = 1e9; | ||
| 12 | return (simloop_time_t)(seconds * NANOS_PER_SEC); | ||
| 13 | } | ||
| 14 | |||
| 15 | // ----------------------------------------------------------------------------- | ||
| 16 | // Randomness. | ||
| 17 | |||
| 18 | typedef struct { | ||
| 19 | uint64_t a; | ||
| 20 | } XorShift64State; | ||
| 21 | |||
| 22 | static uint64_t xorshift64(XorShift64State* state) { | ||
| 23 | uint64_t x = state->a; | ||
| 24 | x ^= x << 7; | ||
| 25 | x ^= x >> 9; | ||
| 26 | return state->a = x; | ||
| 27 | } | ||
| 28 | |||
| 29 | // ----------------------------------------------------------------------------- | ||
| 30 | // Tests. | ||
| 31 | |||
| 32 | /// At time/frame 0, no update is triggered (not enough time passed). | ||
| 33 | TEST_CASE(simloop_initial_render) { | ||
| 34 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10}); | ||
| 35 | SimloopOut simout; | ||
| 36 | |||
| 37 | simloop_update(&simloop, 0, &simout); | ||
| 38 | |||
| 39 | TEST_TRUE(!simout.should_update); | ||
| 40 | TEST_EQUAL(simout.frame, 0); | ||
| 41 | } | ||
| 42 | |||
| 43 | /// The simulation is not updated if time does not advance. | ||
| 44 | /// This applies generally to any time > 0. | ||
| 45 | TEST_CASE(simloop_render_not_retriggered) { | ||
| 46 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10}); | ||
| 47 | SimloopOut simout; | ||
| 48 | |||
| 49 | // Advance time by some amount to get past t=0. | ||
| 50 | simloop_update(&simloop, 1, &simout); | ||
| 51 | |||
| 52 | // Now "advance" by 0. | ||
| 53 | const uint64_t frame_before = simout.frame; | ||
| 54 | simloop_update(&simloop, 0, &simout); | ||
| 55 | const uint64_t frame_after = simout.frame; | ||
| 56 | |||
| 57 | TEST_TRUE(!simout.should_update); | ||
| 58 | TEST_EQUAL(frame_before, frame_after); | ||
| 59 | } | ||
| 60 | |||
| 61 | /// A simulation loop with no render frame cap: | ||
| 62 | /// 1. Updates based on the desired update frame rate. | ||
| 63 | /// 2. Does not throttle rendering. | ||
| 64 | TEST_CASE(simloop_no_render_frame_cap) { | ||
| 65 | constexpr int UPDATE_FPS = 10; // 100ms delta | ||
| 66 | const simloop_time_t UPDATE_DDT = | ||
| 67 | time_delta_from_sec(1.0 / (double)UPDATE_FPS); | ||
| 68 | const simloop_time_t STEP = time_delta_from_sec(0.05); // 50ms | ||
| 69 | const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(30); | ||
| 70 | |||
| 71 | // We need simulation time to be an exact multiple of the desired deltas for | ||
| 72 | // the modulo comparison below. | ||
| 73 | TEST_TRUE((UPDATE_DDT % STEP) == 0); | ||
| 74 | |||
| 75 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); | ||
| 76 | SimloopOut simout; | ||
| 77 | |||
| 78 | simloop_update(&simloop, 0, &simout); | ||
| 79 | TEST_TRUE(!simout.should_update); // Time has not advanced. | ||
| 80 | TEST_EQUAL(simout.throttle, 0); // No throttling with no render frame cap. | ||
| 81 | |||
| 82 | for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { | ||
| 83 | simloop_update(&simloop, STEP, &simout); | ||
| 84 | const bool expect_update = (t % UPDATE_DDT) == 0; | ||
| 85 | TEST_EQUAL(simout.should_update, expect_update); | ||
| 86 | TEST_EQUAL(simout.throttle, 0); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | /// A simulation loop with a render frame cap: | ||
| 91 | /// 1. Updates based on the desired update frame rate. | ||
| 92 | /// 2. Throttles rendering based on the desired render frame rate. | ||
| 93 | TEST_CASE(simloop_with_render_frame_cap) { | ||
| 94 | constexpr int UPDATE_FPS = 10; // 100ms delta | ||
| 95 | constexpr int RENDER_FPS = 5; // 200ms delta | ||
| 96 | const simloop_time_t UPDATE_DDT = | ||
| 97 | time_delta_from_sec(1.0 / (double)UPDATE_FPS); | ||
| 98 | const simloop_time_t RENDER_DDT = | ||
| 99 | time_delta_from_sec(1.0 / (double)RENDER_FPS); | ||
| 100 | const simloop_time_t STEP = time_delta_from_sec(0.1); // 100ms | ||
| 101 | const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(30); | ||
| 102 | |||
| 103 | // We need simulation time to be an exact multiple of the desired deltas for | ||
| 104 | // the modulo comparisons below. | ||
| 105 | TEST_TRUE((UPDATE_DDT % STEP) == 0); | ||
| 106 | |||
| 107 | Simloop simloop = simloop_make( | ||
| 108 | &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS}); | ||
| 109 | SimloopOut simout; | ||
| 110 | |||
| 111 | simloop_update(&simloop, 0, &simout); | ||
| 112 | TEST_TRUE(!simout.should_update); // Time has not advanced. | ||
| 113 | TEST_EQUAL(simout.throttle, 0); // No throttle since time has not advanced. | ||
| 114 | |||
| 115 | for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { | ||
| 116 | simloop_update(&simloop, STEP, &simout); | ||
| 117 | TEST_EQUAL(simout.should_update, (t % UPDATE_DDT) == 0); | ||
| 118 | TEST_NOTEQUAL(simout.throttle, 0); | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | /// If the update falls behind the clock, then percent_frame can fall out of | ||
| 123 | /// range (>1) if we are not careful. This tests for this condition. | ||
| 124 | TEST_CASE(simloop_percent_frame_01_large_jump) { | ||
| 125 | constexpr int UPDATE_FPS = 10; // 100ms delta | ||
| 126 | const simloop_time_t UPDATE_DDT = | ||
| 127 | time_delta_from_sec(1.0 / (double)UPDATE_FPS); | ||
| 128 | const simloop_time_t STEP = time_delta_from_sec(1); | ||
| 129 | const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(30); | ||
| 130 | |||
| 131 | // We need simulation time to be an exact multiple of the desired deltas for | ||
| 132 | // the modulo comparison below. | ||
| 133 | TEST_TRUE((STEP % UPDATE_DDT) == 0); | ||
| 134 | |||
| 135 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); | ||
| 136 | SimloopOut simout; | ||
| 137 | |||
| 138 | simloop_update(&simloop, 0, &simout); | ||
| 139 | TEST_TRUE(!simout.should_update); // Time has not advanced. | ||
| 140 | |||
| 141 | for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { | ||
| 142 | simloop_update(&simloop, STEP, &simout); | ||
| 143 | TEST_TRUE(simout.should_update); // Tries to catch up to clock. | ||
| 144 | TEST_TRUE(0. <= simout.percent_frame); | ||
| 145 | TEST_TRUE(simout.percent_frame <= 1.); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | /// One benefit of fixed over variable time deltas is determinism. Test for | ||
| 150 | /// this by getting to t=10 by different clock time increments. | ||
| 151 | /// | ||
| 152 | /// Note that the time increments must be able to keep up with the desired frame | ||
| 153 | /// delta, otherwise determinism is not maintained. We can guarantee determinism | ||
| 154 | /// at the expense of re-introducing divergence. | ||
| 155 | /// TODO: Perhaps the API should return an update count instead of a boolean, | ||
| 156 | /// advance simulation time per the number of updates, then leave it up to | ||
| 157 | /// the client to decide whether to update just once or as many times as | ||
| 158 | /// requested, depending on whether they want determinism or convergence. | ||
| 159 | TEST_CASE(simloop_determinism) { | ||
| 160 | constexpr int UPDATE_FPS = 100; // 10ms delta | ||
| 161 | const simloop_time_t RANDOM_STEPS[] = { | ||
| 162 | time_delta_from_sec(0.007), // 7ms | ||
| 163 | time_delta_from_sec(0.005), // 5ms | ||
| 164 | time_delta_from_sec(0.003), // 3ms | ||
| 165 | }; | ||
| 166 | constexpr uint64_t NUM_RANDOM_STEPS = | ||
| 167 | sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]); | ||
| 168 | const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(10); | ||
| 169 | constexpr float ADD = 0.123f; | ||
| 170 | |||
| 171 | typedef struct Simulation { | ||
| 172 | int iter_count; | ||
| 173 | float sum; | ||
| 174 | } Simulation; | ||
| 175 | |||
| 176 | #define UPDATE_SIMULATION(SIM) \ | ||
| 177 | { \ | ||
| 178 | SIM.sum += ADD; \ | ||
| 179 | SIM.iter_count++; \ | ||
| 180 | } | ||
| 181 | |||
| 182 | Simulation sim[2] = {0}; | ||
| 183 | XorShift64State xss = (XorShift64State){12069019817132197873ULL}; | ||
| 184 | |||
| 185 | // Perform two simulations with random clock-time steps. | ||
| 186 | for (int s = 0; s < 2; ++s) { | ||
| 187 | simloop_time_t dt = 0; | ||
| 188 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); | ||
| 189 | SimloopOut simout; | ||
| 190 | |||
| 191 | for (simloop_time_t t = 0; t <= SIM_DURATION_SEC;) { | ||
| 192 | simloop_update(&simloop, dt, &simout); | ||
| 193 | |||
| 194 | if (simout.should_update) { | ||
| 195 | UPDATE_SIMULATION(sim[s]); | ||
| 196 | } | ||
| 197 | |||
| 198 | // Advance time with a random step. | ||
| 199 | const simloop_time_t step = | ||
| 200 | RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS]; | ||
| 201 | t += step; | ||
| 202 | dt = step; | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | // Make sure the simulations have advanced by the same number of updates so | ||
| 207 | // that we can compare them. They may not have had the same update count | ||
| 208 | // depending on the clock-time steps. | ||
| 209 | while (sim[0].iter_count < sim[1].iter_count) { | ||
| 210 | UPDATE_SIMULATION(sim[0]); | ||
| 211 | } | ||
| 212 | while (sim[1].iter_count < sim[0].iter_count) { | ||
| 213 | UPDATE_SIMULATION(sim[1]); | ||
| 214 | } | ||
| 215 | TEST_EQUAL(sim[0].iter_count, sim[1].iter_count); | ||
| 216 | |||
| 217 | // The sums should be exactly equal if determinism holds. | ||
| 218 | // Check also that they are non-zero to make sure the simulation actually | ||
| 219 | // advanced. | ||
| 220 | TEST_TRUE(sim[0].sum > 0.f); | ||
| 221 | TEST_EQUAL(sim[0].sum, sim[1].sum); | ||
| 222 | } | ||
| 223 | |||
| 224 | /// The simulation loop attempts to catch up with the clock in the event of a | ||
| 225 | /// time spike. | ||
| 226 | /// | ||
| 227 | /// Catch-up is possible only if the simulation loops with a frequency higher | ||
| 228 | /// than the requested update frequency given by the update delta time. | ||
| 229 | /// | ||
| 230 | /// Catch-up is performed only for sufficiently small time spikes. For large | ||
| 231 | /// time spikes, the simulation clock is warped. This test is for the small | ||
| 232 | /// time spike case. | ||
| 233 | static void simloop_catch_up( | ||
| 234 | struct test_case_metadata* metadata, int update_ddt_ms, int loop_step_ms, | ||
| 235 | bool expect_catchup) { | ||
| 236 | const int UPDATE_FPS = 1000 / update_ddt_ms; | ||
| 237 | const simloop_time_t UPDATE_DDT = | ||
| 238 | time_delta_from_sec(1.0 / (double)UPDATE_FPS); | ||
| 239 | const simloop_time_t STEP = | ||
| 240 | time_delta_from_sec((double)loop_step_ms / 1000.0); | ||
| 241 | const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(30); | ||
| 242 | const int EXPECTED_TOTAL_FRAMES_WITH_CATCHUP = | ||
| 243 | (int)(SIM_DURATION_SEC / UPDATE_DDT); | ||
| 244 | |||
| 245 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); | ||
| 246 | SimloopOut simout; | ||
| 247 | int frames = 0; | ||
| 248 | |||
| 249 | // Simulate a time spike. | ||
| 250 | // Advance time to t=1s. That is a lag of 1,000ms / 100ms = 10 frames. | ||
| 251 | // 10 frames is the maximum allowed catch-up. | ||
| 252 | // The simulation now has 29s to catch up. | ||
| 253 | simloop_time_t dt = time_delta_from_sec(1); | ||
| 254 | for (simloop_time_t t = dt; t <= SIM_DURATION_SEC;) { | ||
| 255 | simloop_update(&simloop, dt, &simout); | ||
| 256 | |||
| 257 | if (simout.should_update) { | ||
| 258 | frames++; | ||
| 259 | } | ||
| 260 | |||
| 261 | // New delta is as usual. | ||
| 262 | dt = STEP; | ||
| 263 | t += dt; | ||
| 264 | } | ||
| 265 | |||
| 266 | if (expect_catchup) { | ||
| 267 | TEST_EQUAL(frames, EXPECTED_TOTAL_FRAMES_WITH_CATCHUP); | ||
| 268 | } else { | ||
| 269 | TEST_TRUE(frames < EXPECTED_TOTAL_FRAMES_WITH_CATCHUP); | ||
| 270 | } | ||
| 271 | } | ||
| 272 | /// (Loop frequency > update frequency) => successful catch-up. | ||
| 273 | TEST_CASE(simloop_catch_up_success) { | ||
| 274 | constexpr int UPDATE_DDT_MS = 100; | ||
| 275 | constexpr int LOOP_DDT_MS = 10; | ||
| 276 | simloop_catch_up(metadata, UPDATE_DDT_MS, LOOP_DDT_MS, true); | ||
| 277 | } | ||
| 278 | /// (Loop frequency < update frequency) => failed catch-up. | ||
| 279 | TEST_CASE(simloop_catch_up_failure) { | ||
| 280 | constexpr int UPDATE_DDT_MS = 10; | ||
| 281 | constexpr int LOOP_DDT_MS = 100; | ||
| 282 | simloop_catch_up(metadata, UPDATE_DDT_MS, LOOP_DDT_MS, false); | ||
| 283 | } | ||
| 284 | |||
| 285 | /// This tests the large time spike case, where the simulation clock is warped | ||
| 286 | /// to the wall clock. | ||
| 287 | TEST_CASE(simloop_warp) { | ||
| 288 | const int UPDATE_FPS = 50; | ||
| 289 | const simloop_time_t UPDATE_DDT = | ||
| 290 | time_delta_from_sec(1.0 / (double)UPDATE_FPS); | ||
| 291 | |||
| 292 | Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); | ||
| 293 | SimloopOut simout; | ||
| 294 | |||
| 295 | // The maximum allowed catch-up is 10 frames. Simulate a time spike larger | ||
| 296 | // than that. | ||
| 297 | const simloop_time_t TIME_SPIKE = UPDATE_DDT * 20; | ||
| 298 | simloop_update(&simloop, TIME_SPIKE, &simout); | ||
| 299 | TEST_TRUE(simout.should_update); // Warp should still request update. | ||
| 300 | |||
| 301 | // Now "advance" by 0. | ||
| 302 | simloop_update(&simloop, 0, &simout); | ||
| 303 | TEST_TRUE(!simout.should_update); // No more updates after warp. | ||
| 304 | } | ||
| 305 | |||
| 306 | int main() { return 0; } | ||
