aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--simloop/include/simloop.h21
-rw-r--r--simloop/src/simloop.c84
-rw-r--r--simloop/test/simloop_test.c69
3 files changed, 53 insertions, 121 deletions
diff --git a/simloop/include/simloop.h b/simloop/include/simloop.h
index 4e3ed20..6a83b23 100644
--- a/simloop/include/simloop.h
+++ b/simloop/include/simloop.h
@@ -2,7 +2,7 @@
2 2
3#include <stdint.h> 3#include <stdint.h>
4 4
5typedef uint64_t simloop_time_t; 5typedef uint64_t simloop_time_t; ///< Time delta in nanoseconds.
6 6
7typedef struct SimloopArgs { 7typedef struct SimloopArgs {
8 int update_fps; ///< Update frame rate. Must be >0. 8 int update_fps; ///< Update frame rate. Must be >0.
@@ -11,13 +11,12 @@ typedef struct SimloopArgs {
11 11
12typedef struct SimloopOut { 12typedef struct SimloopOut {
13 uint64_t frame; ///< Frame counter. 13 uint64_t frame; ///< Frame counter.
14 simloop_time_t render_elapsed; ///< Amount of time elapsed in the rendering.
15 simloop_time_t update_elapsed; ///< Amount of time elapsed in the simulation. 14 simloop_time_t update_elapsed; ///< Amount of time elapsed in the simulation.
16 simloop_time_t update_dt; ///< Delta time for simulation updates. 15 simloop_time_t update_dt; ///< Delta time for simulation updates.
17 double percent_frame; ///< Percent progress between this frame and 16 simloop_time_t throttle; ///< Render throttle if max render fps is given.
18 ///< the next. Used for smooth animation. 17 double percent_frame; ///< Percent progress between this frame and
19 bool should_update; ///< Whether the simulation should update. 18 ///< the next. Used for smooth animation.
20 bool should_render; ///< Whether the simulation should be rendered. 19 bool should_update; ///< Whether the simulation should update.
21} SimloopOut; 20} SimloopOut;
22 21
23typedef struct SimloopTimeline { 22typedef struct SimloopTimeline {
@@ -26,12 +25,10 @@ typedef struct SimloopTimeline {
26} SimloopTimeline; 25} SimloopTimeline;
27 26
28typedef struct Simloop { 27typedef struct Simloop {
29 simloop_time_t clock; ///< Tracks simulation time. 28 simloop_time_t clock; ///< Tracks simulation time.
30 uint64_t frame; ///< Frame counter, number of updates done. 29 uint64_t frame; ///< Frame counter, number of updates done.
31 SimloopTimeline update; ///< Update timeline. 30 SimloopTimeline update; ///< Update timeline.
32 SimloopTimeline render; ///< Render timeline. 31 simloop_time_t render_ddt; ///< Desired render delta time.
33 double percent_frame;
34 bool first_iter;
35} Simloop; 32} Simloop;
36 33
37/// Create a simulation loop. 34/// Create a simulation loop.
diff --git a/simloop/src/simloop.c b/simloop/src/simloop.c
index b8547fd..d231ab3 100644
--- a/simloop/src/simloop.c
+++ b/simloop/src/simloop.c
@@ -20,87 +20,51 @@ Simloop simloop_make(const SimloopArgs* args) {
20 .ddt = ddt_from_fps(args->update_fps), 20 .ddt = ddt_from_fps(args->update_fps),
21 .time = 0, 21 .time = 0,
22 }, 22 },
23 .render = 23 .render_ddt = ddt_from_fps(args->max_render_fps),
24 (SimloopTimeline){
25 .ddt = ddt_from_fps(args->max_render_fps),
26 .time = 0,
27 },
28 .percent_frame = 0.,
29 .first_iter = true,
30 }; 24 };
31} 25}
32 26
33static bool step_update(const Simloop* sim, SimloopTimeline* timeline) { 27void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) {
34 assert(sim); 28 assert(sim);
35 assert(timeline); 29 assert(out);
36 assert(timeline->ddt > 0); 30
31 sim->clock += dt;
37 32
33 // Simulation update.
38 // If the update falls behind the clock, we advance by a single ddt increment 34 // If the update falls behind the clock, we advance by a single ddt increment
39 // per loop iteration here and give it a chance to catch up over subsequent 35 // per loop iteration here and give it a chance to catch up over subsequent
40 // iterations. 36 // iterations.
41 // This has the implication that percent_frame can fall out of range (>1) if 37 // This has the implication that percent_frame can fall out of range (>1) if
42 // we are not careful with how it is defined. See the general update function 38 // we are not careful with how it is defined. See the general update function
43 // below. 39 // below.
44 const simloop_time_t dt = sim->clock - timeline->time; 40 const simloop_time_t delta = sim->clock - sim->update.time;
45 const bool should_step = dt >= timeline->ddt; 41 const bool update_this_tick = delta >= sim->update.ddt;
46 timeline->time += should_step ? timeline->ddt : 0; 42 sim->update.time += update_this_tick ? sim->update.ddt : 0;
47 return should_step;
48}
49
50static bool step_render(const Simloop* sim, SimloopTimeline* timeline) {
51 assert(sim);
52 assert(timeline);
53
54 bool render = false;
55 if (timeline->ddt > 0) {
56 render = step_update(sim, timeline);
57 } else {
58 render = timeline->time < sim->clock;
59 timeline->time = sim->clock;
60 }
61 return render;
62}
63
64void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) {
65 assert(sim);
66 assert(out);
67
68 sim->clock += dt;
69
70 // Simulation update.
71 const bool update_this_tick = step_update(sim, &sim->update);
72 43
73 // Simulation render. 44 // Loop-state update.
74 const bool render_this_tick = 45 sim->frame += (update_this_tick ? 1 : 0);
75 step_render(sim, &sim->render) ||
76 sim->first_iter; // Trigger an initial render on the first frame.
77 46
78 // Interpolator for smooth animation. 47 // Interpolator for smooth animation.
79 // If rendering is not frame-rate capped, then its timeline should always be
80 // at least as recent as the update's. Otherwise, it is possible for the
81 // rendering timeline to be behind.
82 // If the update falls behind the clock, then percent_frame can fall out of 48 // If the update falls behind the clock, then percent_frame can fall out of
83 // range (>1) if we are not careful. We impose that it is strictly never >1 49 // range (>1) if we are not careful. We impose that it is strictly never >1
84 // to account for this case. 50 // to account for this case.
85 assert(sim->update.ddt > 0); 51 assert(sim->update.ddt > 0);
86 assert( 52 assert(sim->update.time <= sim->clock);
87 (sim->render.ddt == 0) ? (sim->update.time <= sim->render.time) : true); 53 out->percent_frame = min(
88 sim->percent_frame = 54 1., (double)(sim->clock - sim->update.time) / (double)sim->update.ddt);
89 (sim->render.time >= sim->update.time) 55 assert((0. <= out->percent_frame) && (out->percent_frame <= 1.));
90 ? min(1., ((double)(sim->render.time - sim->update.time) / 56
91 (double)sim->update.ddt)) 57 // Render frame rate throttle.
92 : sim->percent_frame; 58 // Note that if no max render fps is given, then render_ddt is 0. The logic
93 assert((0. <= sim->percent_frame) && (sim->percent_frame <= 1.)); 59 // works for both render_ddt>0 and =0.
94 60 // Need to be careful with subtraction since the quantities are unsigned.
95 // Loop state update. 61 // Subtract an epsilon to account for delays in thread scheduling.
96 sim->frame += (update_this_tick ? 1 : 0); 62 static const simloop_time_t eps = 50'000; // 50us
97 sim->first_iter = false; 63 out->throttle =
64 (sim->render_ddt > (dt - eps)) ? (sim->render_ddt - eps - dt) : 0;
98 65
99 out->frame = sim->frame; 66 out->frame = sim->frame;
100 out->render_elapsed = sim->render.time;
101 out->update_elapsed = sim->update.time; 67 out->update_elapsed = sim->update.time;
102 out->update_dt = sim->update.ddt; 68 out->update_dt = sim->update.ddt;
103 out->percent_frame = sim->percent_frame;
104 out->should_update = update_this_tick; 69 out->should_update = update_this_tick;
105 out->should_render = render_this_tick;
106} 70}
diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c
index 790ad73..61e7dff 100644
--- a/simloop/test/simloop_test.c
+++ b/simloop/test/simloop_test.c
@@ -29,65 +29,38 @@ static uint64_t xorshift64(XorShift64State* state) {
29// ----------------------------------------------------------------------------- 29// -----------------------------------------------------------------------------
30// Tests. 30// Tests.
31 31
32/// At time/frame 0: 32/// At time/frame 0, no update is triggered (not enough time passed).
33/// 1. An initial render is always triggered.
34/// 2. No update is triggered (not enough time passed).
35TEST_CASE(simloop_initial_render) { 33TEST_CASE(simloop_initial_render) {
36 Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10}); 34 Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10});
37 SimloopOut simout; 35 SimloopOut simout;
38 36
39 simloop_update(&simloop, 0, &simout); 37 simloop_update(&simloop, 0, &simout);
40 38
41 TEST_TRUE(simout.should_render);
42 TEST_TRUE(!simout.should_update); 39 TEST_TRUE(!simout.should_update);
43 TEST_EQUAL(simout.frame, 0); 40 TEST_EQUAL(simout.frame, 0);
44} 41}
45 42
46/// A frame is not re-rendered if time does not advance. 43/// The simulation is not updated if time does not advance.
47/// This applies whether rendering is frame-rate capped or unlimited, and 44/// This applies generally to any time > 0.
48/// whether we are in the initial frame or a subsequent one. 45TEST_CASE(simloop_render_not_retriggered) {
49void simloop_render_not_retriggered( 46 Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10});
50 struct test_case_metadata* metadata, int max_render_fps,
51 bool initial_frame) {
52 Simloop simloop = simloop_make(
53 &(SimloopArgs){.update_fps = 10, .max_render_fps = max_render_fps});
54 SimloopOut simout; 47 SimloopOut simout;
55 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;
56 simloop_update(&simloop, 0, &simout); 54 simloop_update(&simloop, 0, &simout);
55 const uint64_t frame_after = simout.frame;
57 56
58 TEST_TRUE(simout.should_render);
59 TEST_TRUE(!simout.should_update); 57 TEST_TRUE(!simout.should_update);
60 TEST_EQUAL(simout.frame, 0); 58 TEST_EQUAL(frame_before, frame_after);
61
62 if (!initial_frame) {
63 // Advance time beyond the initial frame.
64 simloop_update(&simloop, 1, &simout);
65 }
66
67 for (int i = 0; i < 10; i++) {
68 // Note that time does not advance here.
69 simloop_update(&simloop, 0, &simout);
70 TEST_TRUE(!simout.should_render);
71 TEST_TRUE(!simout.should_update);
72 TEST_EQUAL(simout.frame, 0);
73 }
74}
75TEST_CASE(simloop_render_not_retriggered_capped_initial_frame) {
76 simloop_render_not_retriggered(metadata, 10, true);
77}
78TEST_CASE(simloop_render_not_retriggered_unlimited_initial_frame) {
79 simloop_render_not_retriggered(metadata, 0, true);
80}
81TEST_CASE(simloop_render_not_retriggered_capped_subsequent_frame) {
82 simloop_render_not_retriggered(metadata, 10, false);
83}
84TEST_CASE(simloop_render_not_retriggered_unlimited_subsequent_frame) {
85 simloop_render_not_retriggered(metadata, 0, false);
86} 59}
87 60
88/// A simulation loop with no render frame cap: 61/// A simulation loop with no render frame cap:
89/// 1. Updates based on the desired update frame rate. 62/// 1. Updates based on the desired update frame rate.
90/// 2. Renders at every step. 63/// 2. Does not throttle rendering.
91TEST_CASE(simloop_no_render_frame_cap) { 64TEST_CASE(simloop_no_render_frame_cap) {
92 constexpr int UPDATE_FPS = 10; // 100ms delta 65 constexpr int UPDATE_FPS = 10; // 100ms delta
93 const simloop_time_t UPDATE_DDT = 66 const simloop_time_t UPDATE_DDT =
@@ -104,19 +77,19 @@ TEST_CASE(simloop_no_render_frame_cap) {
104 77
105 simloop_update(&simloop, 0, &simout); 78 simloop_update(&simloop, 0, &simout);
106 TEST_TRUE(!simout.should_update); // Time has not advanced. 79 TEST_TRUE(!simout.should_update); // Time has not advanced.
107 TEST_TRUE(simout.should_render); // Initial render. 80 TEST_EQUAL(simout.throttle, 0); // No throttling with no render frame cap.
108 81
109 for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { 82 for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) {
110 simloop_update(&simloop, STEP, &simout); 83 simloop_update(&simloop, STEP, &simout);
111 const bool expect_update = (t % UPDATE_DDT) == 0; 84 const bool expect_update = (t % UPDATE_DDT) == 0;
112 TEST_EQUAL(simout.should_update, expect_update); 85 TEST_EQUAL(simout.should_update, expect_update);
113 TEST_TRUE(simout.should_render); // Always renders. 86 TEST_EQUAL(simout.throttle, 0);
114 } 87 }
115} 88}
116 89
117/// A simulation loop with a render frame cap: 90/// A simulation loop with a render frame cap:
118/// 1. Updates based on the desired update frame rate. 91/// 1. Updates based on the desired update frame rate.
119/// 2. Renders based on the desired render frame rate. 92/// 2. Throttles rendering based on the desired render frame rate.
120TEST_CASE(simloop_with_render_frame_cap) { 93TEST_CASE(simloop_with_render_frame_cap) {
121 constexpr int UPDATE_FPS = 10; // 100ms delta 94 constexpr int UPDATE_FPS = 10; // 100ms delta
122 constexpr int RENDER_FPS = 5; // 200ms delta 95 constexpr int RENDER_FPS = 5; // 200ms delta
@@ -130,7 +103,6 @@ TEST_CASE(simloop_with_render_frame_cap) {
130 // We need simulation time to be an exact multiple of the desired deltas for 103 // We need simulation time to be an exact multiple of the desired deltas for
131 // the modulo comparisons below. 104 // the modulo comparisons below.
132 TEST_TRUE((UPDATE_DDT % STEP) == 0); 105 TEST_TRUE((UPDATE_DDT % STEP) == 0);
133 TEST_TRUE((RENDER_DDT % STEP) == 0);
134 106
135 Simloop simloop = simloop_make( 107 Simloop simloop = simloop_make(
136 &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS}); 108 &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS});
@@ -138,13 +110,12 @@ TEST_CASE(simloop_with_render_frame_cap) {
138 110
139 simloop_update(&simloop, 0, &simout); 111 simloop_update(&simloop, 0, &simout);
140 TEST_TRUE(!simout.should_update); // Time has not advanced. 112 TEST_TRUE(!simout.should_update); // Time has not advanced.
141 TEST_TRUE(simout.should_render); // Initial render. 113 TEST_EQUAL(simout.throttle, 0); // No throttle since time has not advanced.
142 114
143 for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { 115 for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) {
144 simloop_update(&simloop, STEP, &simout); 116 simloop_update(&simloop, STEP, &simout);
145 // A render is still expected at time 0.
146 TEST_EQUAL(simout.should_render, (t % RENDER_DDT) == 0);
147 TEST_EQUAL(simout.should_update, (t % UPDATE_DDT) == 0); 117 TEST_EQUAL(simout.should_update, (t % UPDATE_DDT) == 0);
118 TEST_NOTEQUAL(simout.throttle, 0);
148 } 119 }
149} 120}
150 121
@@ -166,12 +137,12 @@ TEST_CASE(simloop_percent_frame_01_large_jump) {
166 137
167 simloop_update(&simloop, 0, &simout); 138 simloop_update(&simloop, 0, &simout);
168 TEST_TRUE(!simout.should_update); // Time has not advanced. 139 TEST_TRUE(!simout.should_update); // Time has not advanced.
169 TEST_TRUE(simout.should_render); // Initial render.
170 140
171 for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { 141 for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) {
172 simloop_update(&simloop, STEP, &simout); 142 simloop_update(&simloop, STEP, &simout);
173 TEST_TRUE(simout.should_update); // Tries to catch up to clock. 143 TEST_TRUE(simout.should_update); // Tries to catch up to clock.
174 TEST_TRUE(simout.should_render); 144 TEST_TRUE(0. <= simout.percent_frame);
145 TEST_TRUE(simout.percent_frame <= 1.);
175 } 146 }
176} 147}
177 148