aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2026-04-12 10:48:04 -0700
committer3gg <3gg@shellblade.net>2026-04-12 10:48:04 -0700
commite5fe20ad83e01b3c8262ac90af984da47d8a16a1 (patch)
treeffb55eabccc2018cb0359dc054fdadebfc815d65
parent9dceaee478545ed8df89722f5c90bb171de100e8 (diff)
Work in terms of time deltas; remove dependency on timer module
-rw-r--r--simloop/CMakeLists.txt3
-rw-r--r--simloop/include/simloop.h31
-rw-r--r--simloop/src/simloop.c56
-rw-r--r--simloop/test/simloop_test.c102
4 files changed, 99 insertions, 93 deletions
diff --git a/simloop/CMakeLists.txt b/simloop/CMakeLists.txt
index 1997e2f..fe7f954 100644
--- a/simloop/CMakeLists.txt
+++ b/simloop/CMakeLists.txt
@@ -13,9 +13,6 @@ add_library(simloop
13target_include_directories(simloop PUBLIC 13target_include_directories(simloop PUBLIC
14 include) 14 include)
15 15
16target_link_libraries(simloop PUBLIC
17 timer)
18
19target_compile_options(simloop PRIVATE -Wall -Wextra -Wpedantic) 16target_compile_options(simloop PRIVATE -Wall -Wextra -Wpedantic)
20 17
21# Test 18# Test
diff --git a/simloop/include/simloop.h b/simloop/include/simloop.h
index 1e5b4e0..b08dcac 100644
--- a/simloop/include/simloop.h
+++ b/simloop/include/simloop.h
@@ -37,35 +37,34 @@
37 */ 37 */
38#pragma once 38#pragma once
39 39
40#include <timer.h>
41
42#include <stdint.h> 40#include <stdint.h>
43 41
42typedef uint64_t simloop_time_t;
43
44typedef struct SimloopArgs { 44typedef struct SimloopArgs {
45 int update_fps; ///< Update frame rate. Must be >0. 45 int update_fps; ///< Update frame rate. Must be >0.
46 int max_render_fps; ///< Render frame rate cap. 0 to disable. 46 int max_render_fps; ///< Render frame rate cap. 0 to disable.
47 Timer* timer; ///< Timer that drives the simulation.
48} SimloopArgs; 47} SimloopArgs;
49 48
50typedef struct SimloopOut { 49typedef struct SimloopOut {
51 uint64_t frame; ///< Frame counter. 50 uint64_t frame; ///< Frame counter.
52 time_delta render_elapsed; ///< Amount of time elapsed in the rendering. 51 simloop_time_t render_elapsed; ///< Amount of time elapsed in the rendering.
53 time_delta update_elapsed; ///< Amount of time elapsed in the simulation. 52 simloop_time_t update_elapsed; ///< Amount of time elapsed in the simulation.
54 time_delta update_dt; ///< Delta time for simulation updates. 53 simloop_time_t update_dt; ///< Delta time for simulation updates.
55 bool should_update; ///< Whether the simulation should update. 54 bool should_update; ///< Whether the simulation should update.
56 bool should_render; ///< Whether the simulation should be rendered. 55 bool should_render; ///< Whether the simulation should be rendered.
57} SimloopOut; 56} SimloopOut;
58 57
59typedef struct SimloopTimeline { 58typedef struct SimloopTimeline {
60 time_delta ddt; ///< Desired delta time. 59 simloop_time_t ddt; ///< Desired delta time.
61 time_point last_step; ///< Time of the last simulation step. 60 simloop_time_t time; ///< Time point of the last simulation step.
62} SimloopTimeline; 61} SimloopTimeline;
63 62
64typedef struct Simloop { 63typedef struct Simloop {
64 simloop_time_t clock; ///< Tracks simulation time.
65 uint64_t frame; ///< Frame counter, number of updates done.
65 SimloopTimeline update; ///< Update timeline. 66 SimloopTimeline update; ///< Update timeline.
66 SimloopTimeline render; ///< Render timeline. 67 SimloopTimeline render; ///< Render timeline.
67 uint64_t frame; ///< Frame counter.
68 Timer* timer;
69 bool first_iter; 68 bool first_iter;
70 bool updates_since_last_render; 69 bool updates_since_last_render;
71} Simloop; 70} Simloop;
@@ -76,4 +75,4 @@ Simloop simloop_make(const SimloopArgs*);
76/// Step the simulation loop. 75/// Step the simulation loop.
77/// 76///
78/// The simulation always triggers a render of the initial state of simulation. 77/// The simulation always triggers a render of the initial state of simulation.
79void simloop_update(Simloop*, SimloopOut*); 78void simloop_update(Simloop*, simloop_time_t dt, SimloopOut*);
diff --git a/simloop/src/simloop.c b/simloop/src/simloop.c
index 606f5ed..bd5a72d 100644
--- a/simloop/src/simloop.c
+++ b/simloop/src/simloop.c
@@ -2,9 +2,9 @@
2 2
3#include <assert.h> 3#include <assert.h>
4 4
5static uint64_t ddt_from_fps(int fps) { 5static simloop_time_t ddt_from_fps(int fps) {
6 static constexpr double NANOSECONDS = 1e9; 6 static constexpr double NANOSECONDS = 1e9;
7 return (fps == 0) ? 0 : (uint64_t)(NANOSECONDS / (double)fps); 7 return (fps == 0) ? 0 : (simloop_time_t)(NANOSECONDS / (double)fps);
8} 8}
9 9
10Simloop simloop_make(const SimloopArgs* args) { 10Simloop simloop_make(const SimloopArgs* args) {
@@ -12,31 +12,30 @@ Simloop simloop_make(const SimloopArgs* args) {
12 assert(args->update_fps > 0); 12 assert(args->update_fps > 0);
13 13
14 return (Simloop){ 14 return (Simloop){
15 .frame = 0, 15 .frame = 0,
16 .update = (SimloopTimeline){.ddt = ddt_from_fps(args->update_fps), 16 .update =
17 .last_step = args->timer->start_time}, 17 (SimloopTimeline){
18 .render = (SimloopTimeline){ .ddt = ddt_from_fps(args->max_render_fps), 18 .ddt = ddt_from_fps(args->update_fps),
19 .last_step = args->timer->start_time}, 19 .time = 0,
20 .timer = args->timer, 20 },
21 .first_iter = true, 21 .render =
22 (SimloopTimeline){
23 .ddt = ddt_from_fps(args->max_render_fps),
24 .time = 0,
25 },
26 .first_iter = true,
22 .updates_since_last_render = false, 27 .updates_since_last_render = false,
23 }; 28 };
24} 29}
25 30
26static time_delta time_elapsed(const Simloop* sim, time_point t) {
27 assert(sim);
28 return time_diff(sim->timer->start_time, t);
29}
30
31static bool step_update(const Simloop* sim, SimloopTimeline* timeline) { 31static bool step_update(const Simloop* sim, SimloopTimeline* timeline) {
32 assert(sim); 32 assert(sim);
33 assert(timeline); 33 assert(timeline);
34 assert(timeline->ddt > 0); 34 assert(timeline->ddt > 0);
35 35
36 const time_delta dt = time_diff(timeline->last_step, sim->timer->last_tick); 36 const simloop_time_t dt = sim->clock - timeline->time;
37 const bool should_step = dt >= timeline->ddt; 37 const bool should_step = dt >= timeline->ddt;
38 timeline->last_step = 38 timeline->time += should_step ? timeline->ddt : 0;
39 should_step ? time_add(timeline->last_step, dt) : timeline->last_step;
40 return should_step; 39 return should_step;
41} 40}
42 41
@@ -48,32 +47,37 @@ static bool step_render(const Simloop* sim, SimloopTimeline* timeline) {
48 if (timeline->ddt > 0) { 47 if (timeline->ddt > 0) {
49 render = step_update(sim, timeline); 48 render = step_update(sim, timeline);
50 } else { 49 } else {
51 timeline->last_step = sim->timer->last_tick; 50 timeline->time = sim->clock;
52 render = true; 51 render = true;
53 } 52 }
54 return render; 53 return render;
55} 54}
56 55
57void simloop_update(Simloop* sim, SimloopOut* out) { 56void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) {
58 assert(sim); 57 assert(sim);
59 assert(out); 58 assert(out);
60 59
60 sim->clock += dt;
61
61 // Simulation update. 62 // Simulation update.
62 const bool updated = step_update(sim, &sim->update); 63 const bool updated = step_update(sim, &sim->update);
63 out->should_update = updated;
64 sim->updates_since_last_render = sim->updates_since_last_render || updated; 64 sim->updates_since_last_render = sim->updates_since_last_render || updated;
65
65 // Simulation render. 66 // Simulation render.
66 const bool rendered = 67 const bool rendered =
67 (sim->updates_since_last_render && step_render(sim, &sim->render)) || 68 (sim->updates_since_last_render && step_render(sim, &sim->render)) ||
68 (sim->first_iter); // Trigger an initial render on the first frame. 69 (sim->first_iter); // Trigger an initial render on the first frame.
69 out->should_render = rendered;
70 sim->updates_since_last_render = 70 sim->updates_since_last_render =
71 sim->updates_since_last_render && !out->should_render; 71 sim->updates_since_last_render && !out->should_render;
72
72 // Loop state update. 73 // Loop state update.
73 sim->frame += (updated ? 1 : 0); 74 sim->frame += (updated ? 1 : 0);
74 sim->first_iter = false; 75 sim->first_iter = false;
76
75 out->frame = sim->frame; 77 out->frame = sim->frame;
76 out->render_elapsed = time_elapsed(sim, sim->render.last_step); 78 out->render_elapsed = sim->render.time;
77 out->update_elapsed = time_elapsed(sim, sim->update.last_step); 79 out->update_elapsed = sim->update.time;
78 out->update_dt = sim->update.ddt; 80 out->update_dt = sim->update.ddt;
81 out->should_update = updated;
82 out->should_render = rendered;
79} 83}
diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c
index e8efd94..603a38c 100644
--- a/simloop/test/simloop_test.c
+++ b/simloop/test/simloop_test.c
@@ -1,18 +1,25 @@
1#include <simloop.h> 1#include <simloop.h>
2 2
3#include <test.h> 3#include <test.h>
4#include <timer.h>
5 4
6#include <stdint.h> 5#include <stdint.h>
7 6
8// ----------------------------------------------------------------------------- 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// -----------------------------------------------------------------------------
9// Randomness. 16// Randomness.
10 17
11typedef struct { 18typedef struct {
12 uint64_t a; 19 uint64_t a;
13} XorShift64State; 20} XorShift64State;
14 21
15uint64_t xorshift64(XorShift64State* state) { 22static uint64_t xorshift64(XorShift64State* state) {
16 uint64_t x = state->a; 23 uint64_t x = state->a;
17 x ^= x << 7; 24 x ^= x << 7;
18 x ^= x >> 9; 25 x ^= x >> 9;
@@ -26,12 +33,10 @@ uint64_t xorshift64(XorShift64State* state) {
26/// 1. An initial render is always triggered. 33/// 1. An initial render is always triggered.
27/// 2. No update is triggered (not enough time passed). 34/// 2. No update is triggered (not enough time passed).
28TEST_CASE(simloop_initial_render) { 35TEST_CASE(simloop_initial_render) {
29 Timer timer = {}; 36 Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10});
30 Simloop simloop =
31 simloop_make(&(SimloopArgs){.update_fps = 10, .timer = &timer});
32 SimloopOut simout; 37 SimloopOut simout;
33 38
34 simloop_update(&simloop, &simout); 39 simloop_update(&simloop, 0, &simout);
35 40
36 TEST_TRUE(simout.should_render); 41 TEST_TRUE(simout.should_render);
37 TEST_TRUE(!simout.should_update); 42 TEST_TRUE(!simout.should_update);
@@ -41,12 +46,11 @@ TEST_CASE(simloop_initial_render) {
41/// The initial render is not re-triggered if there is a render frame rate cap 46/// The initial render is not re-triggered if there is a render frame rate cap
42/// and time does not advance. 47/// and time does not advance.
43TEST_CASE(simloop_initial_render_not_retriggered) { 48TEST_CASE(simloop_initial_render_not_retriggered) {
44 Timer timer = {}; 49 Simloop simloop =
45 Simloop simloop = simloop_make( 50 simloop_make(&(SimloopArgs){.update_fps = 10, .max_render_fps = 10});
46 &(SimloopArgs){.update_fps = 10, .max_render_fps = 10, .timer = &timer});
47 SimloopOut simout; 51 SimloopOut simout;
48 52
49 simloop_update(&simloop, &simout); 53 simloop_update(&simloop, 0, &simout);
50 54
51 TEST_TRUE(simout.should_render); 55 TEST_TRUE(simout.should_render);
52 TEST_TRUE(!simout.should_update); 56 TEST_TRUE(!simout.should_update);
@@ -54,7 +58,7 @@ TEST_CASE(simloop_initial_render_not_retriggered) {
54 58
55 for (int i = 0; i < 10; i++) { 59 for (int i = 0; i < 10; i++) {
56 // Note that time does not advance. 60 // Note that time does not advance.
57 simloop_update(&simloop, &simout); 61 simloop_update(&simloop, 0, &simout);
58 TEST_TRUE(!simout.should_render); 62 TEST_TRUE(!simout.should_render);
59 TEST_TRUE(!simout.should_update); 63 TEST_TRUE(!simout.should_update);
60 TEST_EQUAL(simout.frame, 0); 64 TEST_EQUAL(simout.frame, 0);
@@ -65,27 +69,27 @@ TEST_CASE(simloop_initial_render_not_retriggered) {
65/// 1. Updates based on the desired update frame rate. 69/// 1. Updates based on the desired update frame rate.
66/// 2. Renders at every loop (provided there are updates). 70/// 2. Renders at every loop (provided there are updates).
67TEST_CASE(simloop_no_render_frame_cap) { 71TEST_CASE(simloop_no_render_frame_cap) {
68 constexpr int UPDATE_FPS = 10; // 100ms delta 72 constexpr int UPDATE_FPS = 10; // 100ms delta
69 const time_delta UPDATE_DDT = sec_to_time_delta(1.0 / (double)UPDATE_FPS); 73 const simloop_time_t UPDATE_DDT =
70 const time_delta STEP = sec_to_time_delta(1); 74 time_delta_from_sec(1.0 / (double)UPDATE_FPS);
71 const time_delta SIM_TIME_SEC = sec_to_time_delta(30); 75 const simloop_time_t STEP = time_delta_from_sec(1);
76 const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(30);
72 77
73 // We need simulation time to be an exact multiple of the desired deltas for 78 // We need simulation time to be an exact multiple of the desired deltas for
74 // the modulo comparisons below. 79 // the modulo comparisons below.
75 TEST_TRUE((STEP % UPDATE_DDT) == 0); 80 TEST_TRUE((STEP % UPDATE_DDT) == 0);
76 81
77 Timer timer = {}; 82 simloop_time_t dt = 0;
78 Simloop simloop = 83 Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS});
79 simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer});
80 SimloopOut simout; 84 SimloopOut simout;
81 85
82 for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { 86 for (simloop_time_t t = 0; t <= SIM_TIME_SEC; t += STEP) {
83 timer_advance(&timer, t); 87 simloop_update(&simloop, dt, &simout);
84 simloop_update(&simloop, &simout);
85 const bool expect_update = (t > 0) && ((t % UPDATE_DDT) == 0); 88 const bool expect_update = (t > 0) && ((t % UPDATE_DDT) == 0);
86 // A render is still expected at time 0. 89 // A render is still expected at time 0.
87 TEST_EQUAL(simout.should_render, (t == 0) || expect_update); 90 TEST_EQUAL(simout.should_render, (t == 0) || expect_update);
88 TEST_EQUAL(simout.should_update, expect_update); 91 TEST_EQUAL(simout.should_update, expect_update);
92 dt = STEP;
89 } 93 }
90} 94}
91 95
@@ -93,29 +97,31 @@ TEST_CASE(simloop_no_render_frame_cap) {
93/// 1. Updates based on the desired update frame rate. 97/// 1. Updates based on the desired update frame rate.
94/// 2. Renders based on the desired render frame rate. 98/// 2. Renders based on the desired render frame rate.
95TEST_CASE(simloop_with_render_frame_cap) { 99TEST_CASE(simloop_with_render_frame_cap) {
96 constexpr int UPDATE_FPS = 10; // 100ms delta 100 constexpr int UPDATE_FPS = 10; // 100ms delta
97 constexpr int RENDER_FPS = 5; // 200ms delta 101 constexpr int RENDER_FPS = 5; // 200ms delta
98 const time_delta UPDATE_DDT = sec_to_time_delta(1.0 / (double)UPDATE_FPS); 102 const simloop_time_t UPDATE_DDT =
99 const time_delta RENDER_DDT = sec_to_time_delta(1.0 / (double)RENDER_FPS); 103 time_delta_from_sec(1.0 / (double)UPDATE_FPS);
100 const time_delta STEP = sec_to_time_delta(0.1); // 100ms 104 const simloop_time_t RENDER_DDT =
101 const time_delta SIM_TIME_SEC = sec_to_time_delta(30); 105 time_delta_from_sec(1.0 / (double)RENDER_FPS);
106 const simloop_time_t STEP = time_delta_from_sec(0.1); // 100ms
107 const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(30);
102 108
103 // We need simulation time to be an exact multiple of the desired deltas for 109 // We need simulation time to be an exact multiple of the desired deltas for
104 // the modulo comparisons below. 110 // the modulo comparisons below.
105 TEST_TRUE((UPDATE_DDT % STEP) == 0); 111 TEST_TRUE((UPDATE_DDT % STEP) == 0);
106 TEST_TRUE((RENDER_DDT % STEP) == 0); 112 TEST_TRUE((RENDER_DDT % STEP) == 0);
107 113
108 Timer timer = {}; 114 simloop_time_t dt = 0;
109 Simloop simloop = simloop_make(&(SimloopArgs){ 115 Simloop simloop = simloop_make(
110 .update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS, .timer = &timer}); 116 &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS});
111 SimloopOut simout; 117 SimloopOut simout;
112 118
113 for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { 119 for (simloop_time_t t = 0; t <= SIM_TIME_SEC; t += STEP) {
114 timer_advance(&timer, t); 120 simloop_update(&simloop, dt, &simout);
115 simloop_update(&simloop, &simout);
116 // A render is still expected at time 0. 121 // A render is still expected at time 0.
117 TEST_EQUAL(simout.should_render, (t % RENDER_DDT) == 0); 122 TEST_EQUAL(simout.should_render, (t % RENDER_DDT) == 0);
118 TEST_EQUAL(simout.should_update, (t > 0) && ((t % UPDATE_DDT) == 0)); 123 TEST_EQUAL(simout.should_update, (t > 0) && ((t % UPDATE_DDT) == 0));
124 dt = STEP;
119 } 125 }
120} 126}
121 127
@@ -130,16 +136,16 @@ TEST_CASE(simloop_with_render_frame_cap) {
130/// the client to decide whether to update just once or as many times as 136/// the client to decide whether to update just once or as many times as
131/// requested, depending on whether they want determinism or convergence. 137/// requested, depending on whether they want determinism or convergence.
132TEST_CASE(simloop_determinism) { 138TEST_CASE(simloop_determinism) {
133 constexpr int UPDATE_FPS = 100; // 10ms delta 139 constexpr int UPDATE_FPS = 100; // 10ms delta
134 const time_delta RANDOM_STEPS[] = { 140 const simloop_time_t RANDOM_STEPS[] = {
135 sec_to_time_delta(0.007), // 7ms 141 time_delta_from_sec(0.007), // 7ms
136 sec_to_time_delta(0.005), // 5ms 142 time_delta_from_sec(0.005), // 5ms
137 sec_to_time_delta(0.003), // 3ms 143 time_delta_from_sec(0.003), // 3ms
138 }; 144 };
139 constexpr uint64_t NUM_RANDOM_STEPS = 145 constexpr uint64_t NUM_RANDOM_STEPS =
140 sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]); 146 sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]);
141 const time_delta SIM_TIME_SEC = sec_to_time_delta(10); 147 const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(10);
142 constexpr float ADD = 0.123f; 148 constexpr float ADD = 0.123f;
143 149
144 typedef struct Simulation { 150 typedef struct Simulation {
145 int iter_count; 151 int iter_count;
@@ -153,26 +159,26 @@ TEST_CASE(simloop_determinism) {
153 } 159 }
154 160
155 Simulation sim[2] = {0}; 161 Simulation sim[2] = {0};
156 XorShift64State xss = (XorShift64State){12069019817132197873}; 162 XorShift64State xss = (XorShift64State){12069019817132197873ULL};
157 163
158 // Perform two simulations with random clock-time steps. 164 // Perform two simulations with random clock-time steps.
159 for (int s = 0; s < 2; ++s) { 165 for (int s = 0; s < 2; ++s) {
160 Timer timer = {}; 166 simloop_time_t dt = 0;
161 Simloop simloop = 167 Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS});
162 simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer});
163 SimloopOut simout; 168 SimloopOut simout;
164 169
165 for (time_delta t = 0; t < SIM_TIME_SEC;) { 170 for (simloop_time_t t = 0; t <= SIM_TIME_SEC;) {
166 timer_advance(&timer, t); 171 simloop_update(&simloop, dt, &simout);
167 simloop_update(&simloop, &simout);
168 172
169 if (simout.should_update) { 173 if (simout.should_update) {
170 UPDATE_SIMULATION(sim[s]); 174 UPDATE_SIMULATION(sim[s]);
171 } 175 }
172 176
173 // Advance time with a random step. 177 // Advance time with a random step.
174 const time_delta step = RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS]; 178 const simloop_time_t step =
179 RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS];
175 t += step; 180 t += step;
181 dt = step;
176 } 182 }
177 } 183 }
178 184