aboutsummaryrefslogtreecommitdiff
path: root/simloop/test
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2026-04-11 17:27:18 -0700
committer3gg <3gg@shellblade.net>2026-04-11 17:27:18 -0700
commitecf662ccea7eac7d46c6cfd2fc413f1d7f821bc6 (patch)
treee849d7b66a26ded628ba191058461ea281d2d15a /simloop/test
parent896a6ef5959043db5463637d84ed524ae7bade1e (diff)
Add determinism test
Diffstat (limited to 'simloop/test')
-rw-r--r--simloop/test/simloop_test.c106
1 files changed, 100 insertions, 6 deletions
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 @@
3#include <test.h> 3#include <test.h>
4#include <timer.h> 4#include <timer.h>
5 5
6#include <stdint.h>
7
8// -----------------------------------------------------------------------------
9// Randomness.
10
11typedef struct {
12 uint64_t a;
13} XorShift64State;
14
15uint64_t xorshift64(XorShift64State* state) {
16 uint64_t x = state->a;
17 x ^= x << 7;
18 x ^= x >> 9;
19 return state->a = x;
20}
21
22// -----------------------------------------------------------------------------
23// Tests.
24
6/// At time/frame 0: 25/// At time/frame 0:
7/// 1. An initial render is always triggered. 26/// 1. An initial render is always triggered.
8/// 2. No update is triggered (not enough time passed). 27/// 2. No update is triggered (not enough time passed).
9TEST_CASE(simloop_initial_render) { 28TEST_CASE(simloop_initial_render) {
10 Timer timer = {}; 29 Timer timer = {};
11 Simloop simloop = simloop_make( 30 Simloop simloop =
12 &(SimloopArgs){.update_fps = 10, .max_render_fps = 0, .timer = &timer}); 31 simloop_make(&(SimloopArgs){.update_fps = 10, .timer = &timer});
13 SimloopOut simout; 32 SimloopOut simout;
14 33
15 simloop_update(&simloop, &simout); 34 simloop_update(&simloop, &simout);
@@ -51,9 +70,9 @@ TEST_CASE(simloop_no_render_frame_cap) {
51 const time_delta STEP = sec_to_time_delta(1); 70 const time_delta STEP = sec_to_time_delta(1);
52 const time_delta SIM_TIME_SEC = sec_to_time_delta(30); 71 const time_delta SIM_TIME_SEC = sec_to_time_delta(30);
53 72
54 Timer timer = {}; 73 Timer timer = {};
55 Simloop simloop = simloop_make(&(SimloopArgs){ 74 Simloop simloop =
56 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer}); 75 simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer});
57 SimloopOut simout; 76 SimloopOut simout;
58 77
59 for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { 78 for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) {
@@ -91,4 +110,79 @@ TEST_CASE(simloop_with_render_frame_cap) {
91 } 110 }
92} 111}
93 112
113/// One benefit of fixed over variable time deltas is determinism. Test for
114/// this by getting to t=10 by different clock time increments.
115///
116/// Note that the time increments must be able to keep up with the desired frame
117/// delta, otherwise determinism is not maintained. We can guarantee determinism
118/// at the expense of re-introducing divergence.
119/// TODO: Perhaps the API should return an update count instead of a boolean,
120/// advance simulation time per the number of updates, then leave it up to
121/// the client to decide whether to update just once or as many times as
122/// requested, depending on whether they want determinism or convergence.
123TEST_CASE(simloop_determinism) {
124 constexpr int UPDATE_FPS = 100; // 10ms delta
125 const time_delta RANDOM_STEPS[] = {
126 sec_to_time_delta(0.007), // 7ms
127 sec_to_time_delta(0.005), // 5ms
128 sec_to_time_delta(0.003), // 3ms
129 };
130 constexpr uint64_t NUM_RANDOM_STEPS =
131 sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]);
132 const time_delta SIM_TIME_SEC = sec_to_time_delta(10);
133 constexpr float ADD = 0.123f;
134
135 typedef struct Simulation {
136 int iter_count;
137 float sum;
138 } Simulation;
139
140#define UPDATE_SIMULATION(SIM) \
141 { \
142 SIM.sum += ADD; \
143 SIM.iter_count++; \
144 }
145
146 Simulation sim[2] = {0};
147 XorShift64State xss = (XorShift64State){12069019817132197873};
148
149 // Perform two simulations with random clock-time steps.
150 for (int s = 0; s < 2; ++s) {
151 Timer timer = {};
152 Simloop simloop =
153 simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer});
154 SimloopOut simout;
155
156 for (time_delta t = 0; t < SIM_TIME_SEC;) {
157 timer_advance(&timer, t);
158 simloop_update(&simloop, &simout);
159
160 if (simout.should_update) {
161 UPDATE_SIMULATION(sim[s]);
162 }
163
164 // Advance time with a random step.
165 const time_delta step = RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS];
166 t += step;
167 }
168 }
169
170 // Make sure the simulations have advanced by the same number of updates so
171 // that we can compare them. They may not have had the same update count
172 // depending on the clock-time steps.
173 while (sim[0].iter_count < sim[1].iter_count) {
174 UPDATE_SIMULATION(sim[0]);
175 }
176 while (sim[1].iter_count < sim[0].iter_count) {
177 UPDATE_SIMULATION(sim[1]);
178 }
179 TEST_EQUAL(sim[0].iter_count, sim[1].iter_count);
180
181 // The sums should be exactly equal if determinism holds.
182 // Check also that they are non-zero to make sure the simulation actually
183 // advanced.
184 TEST_TRUE(sim[0].sum > 0.f);
185 TEST_EQUAL(sim[0].sum, sim[1].sum);
186}
187
94int main() { return 0; } 188int main() { return 0; }