aboutsummaryrefslogtreecommitdiff
path: root/simloop/test
diff options
context:
space:
mode:
Diffstat (limited to 'simloop/test')
-rw-r--r--simloop/test/simloop_test.c306
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
10static 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
18typedef struct {
19 uint64_t a;
20} XorShift64State;
21
22static 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).
33TEST_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.
45TEST_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.
64TEST_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.
93TEST_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.
124TEST_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.
159TEST_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.
233static 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.
273TEST_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.
279TEST_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.
287TEST_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
306int main() { return 0; }