aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--simloop/include/simloop.h38
-rw-r--r--simloop/src/simloop.c32
-rw-r--r--simloop/test/simloop_test.c72
3 files changed, 105 insertions, 37 deletions
diff --git a/simloop/include/simloop.h b/simloop/include/simloop.h
index f267d40..6ee3b98 100644
--- a/simloop/include/simloop.h
+++ b/simloop/include/simloop.h
@@ -3,6 +3,37 @@
3 * This implements a simulation loop but in a way that the client retains 3 * This implements a simulation loop but in a way that the client retains
4 * control flow. The client steps the loop and then checks whether the 4 * control flow. The client steps the loop and then checks whether the
5 * simulation must be updated and/or the result rendered. 5 * simulation must be updated and/or the result rendered.
6 *
7 * If the simulation's update cannot keep up with the desired frame rate, then
8 * the loop degrades to match the simulation's rate by requesting a single
9 * update.
10 *
11 * Under a variable time delta, the loop could simply update the simulation
12 * with a large delta that puts the simulation back into the current clock
13 * time. Under a fixed time delta this isn't possible, and we seem to have two
14 * choices instead:
15 *
16 * a) Queue as many updates as necessary to bring the simulation back to the
17 * current clock time (time_diff / fixed_delta).
18 *
19 * b) Queue a single update.
20 *
21 * The issue with (a) is that the number of requested updates diverges and
22 * eventually the simulation appears to freeze. At every loop, the number of
23 * queued updates increases with respect to the last iteration as the
24 * simulation fails to keep up with the desired frame rate. Example:
25 *
26 * desired delta = 10ms (100 fps)
27 * actual delta = 20ms ( 50 fps)
28 * ---------------------------
29 * iter, sim time, clock time
30 * ---------------------------
31 * 0, 0, 0, initial state
32 * 1, 0, 10, queue 1 update
33 * 2, 10, 30, queue (30-10)/10 = 2 updates
34 * 3, 30, 70, queue (70-30)/10 = 4 updates
35 * 4, 70, 150, queue (150-70)/10 = 8 updates
36 * ...
6 */ 37 */
7#pragma once 38#pragma once
8 39
@@ -21,8 +52,8 @@ typedef struct SimloopOut {
21 time_delta render_elapsed; ///< Amount of time elapsed in the rendering. 52 time_delta render_elapsed; ///< Amount of time elapsed in the rendering.
22 time_delta update_elapsed; ///< Amount of time elapsed in the simulation. 53 time_delta update_elapsed; ///< Amount of time elapsed in the simulation.
23 time_delta update_dt; ///< Delta time for simulation updates. 54 time_delta update_dt; ///< Delta time for simulation updates.
24 int updates_pending; ///< Number of frames the simulation should produce. 55 bool should_update; ///< Whether the simulation should update.
25 bool should_render; ///< Whether the simulation should be rendered. 56 bool should_render; ///< Whether the simulation should be rendered.
26} SimloopOut; 57} SimloopOut;
27 58
28typedef struct SimloopTimeline { 59typedef struct SimloopTimeline {
@@ -35,10 +66,13 @@ typedef struct Simloop {
35 SimloopTimeline render; ///< Render timeline. 66 SimloopTimeline render; ///< Render timeline.
36 uint64_t frame; ///< Frame counter. 67 uint64_t frame; ///< Frame counter.
37 Timer* timer; 68 Timer* timer;
69 bool first_iter;
38} Simloop; 70} Simloop;
39 71
40/// Create a simulation loop. 72/// Create a simulation loop.
41Simloop simloop_make(const SimloopArgs*); 73Simloop simloop_make(const SimloopArgs*);
42 74
43/// Step the simulation loop. 75/// Step the simulation loop.
76///
77/// The simulation always triggers a render of the initial state of simulation.
44void simloop_update(Simloop*, SimloopOut*); 78void simloop_update(Simloop*, SimloopOut*);
diff --git a/simloop/src/simloop.c b/simloop/src/simloop.c
index 11f4d6d..aa2b6b7 100644
--- a/simloop/src/simloop.c
+++ b/simloop/src/simloop.c
@@ -12,12 +12,13 @@ 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 = (SimloopTimeline){.ddt = ddt_from_fps(args->update_fps),
17 .last_step = args->timer->start_time}, 17 .last_step = args->timer->start_time},
18 .render = (SimloopTimeline){ .ddt = ddt_from_fps(args->max_render_fps), 18 .render = (SimloopTimeline){ .ddt = ddt_from_fps(args->max_render_fps),
19 .last_step = args->timer->start_time}, 19 .last_step = args->timer->start_time},
20 .timer = args->timer, 20 .timer = args->timer,
21 .first_iter = true,
21 }; 22 };
22} 23}
23 24
@@ -26,15 +27,16 @@ static time_delta time_elapsed(const Simloop* sim, time_point t) {
26 return time_diff(sim->timer->start_time, t); 27 return time_diff(sim->timer->start_time, t);
27} 28}
28 29
29static int step_update(const Simloop* sim, SimloopTimeline* timeline) { 30static bool step_update(const Simloop* sim, SimloopTimeline* timeline) {
30 assert(sim); 31 assert(sim);
31 assert(timeline); 32 assert(timeline);
32 assert(timeline->ddt > 0); 33 assert(timeline->ddt > 0);
33 34
34 const time_delta dt = time_diff(timeline->last_step, sim->timer->last_tick); 35 const time_delta dt = time_diff(timeline->last_step, sim->timer->last_tick);
35 const time_delta steps = dt / timeline->ddt; 36 const bool should_step = dt >= timeline->ddt;
36 timeline->last_step = time_add(timeline->last_step, dt); 37 timeline->last_step =
37 return (int)steps; 38 should_step ? time_add(timeline->last_step, dt) : timeline->last_step;
39 return should_step;
38} 40}
39 41
40static bool step_render(const Simloop* sim, SimloopTimeline* timeline) { 42static bool step_render(const Simloop* sim, SimloopTimeline* timeline) {
@@ -43,7 +45,7 @@ static bool step_render(const Simloop* sim, SimloopTimeline* timeline) {
43 45
44 bool render = false; 46 bool render = false;
45 if (timeline->ddt > 0) { 47 if (timeline->ddt > 0) {
46 render = step_update(sim, timeline) > 0; 48 render = step_update(sim, timeline);
47 } else { 49 } else {
48 timeline->last_step = sim->timer->last_tick; 50 timeline->last_step = sim->timer->last_tick;
49 render = true; 51 render = true;
@@ -55,12 +57,12 @@ void simloop_update(Simloop* sim, SimloopOut* out) {
55 assert(sim); 57 assert(sim);
56 assert(out); 58 assert(out);
57 59
58 const int new_frames = step_update(sim, &sim->update); 60 out->should_update = step_update(sim, &sim->update);
59 out->updates_pending = new_frames;
60 out->should_render = 61 out->should_render =
61 step_render(sim, &sim->render) || 62 step_render(sim, &sim->render) ||
62 (sim->frame == 0); // Trigger an initial render on the first frame. 63 (sim->first_iter); // Trigger an initial render on the first frame.
63 sim->frame += new_frames; 64 sim->frame += (out->should_update ? 1 : 0);
65 sim->first_iter = false;
64 out->frame = sim->frame; 66 out->frame = sim->frame;
65 out->render_elapsed = time_elapsed(sim, sim->render.last_step); 67 out->render_elapsed = time_elapsed(sim, sim->render.last_step);
66 out->update_elapsed = time_elapsed(sim, sim->update.last_step); 68 out->update_elapsed = time_elapsed(sim, sim->update.last_step);
diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c
index 3f2aa46..c79ee32 100644
--- a/simloop/test/simloop_test.c
+++ b/simloop/test/simloop_test.c
@@ -3,57 +3,89 @@
3#include <test.h> 3#include <test.h>
4#include <timer.h> 4#include <timer.h>
5 5
6/// An initial render should always trigger on frame 0. 6/// At time/frame 0:
7/// 1. An initial render is always triggered.
8/// 2. No update is triggered (not enough time passed).
7TEST_CASE(simloop_initial_render) { 9TEST_CASE(simloop_initial_render) {
8 constexpr int UPDATE_FPS = 10; 10 Timer timer = {};
11 Simloop simloop = simloop_make(
12 &(SimloopArgs){.update_fps = 10, .max_render_fps = 0, .timer = &timer});
13 SimloopOut simout;
9 14
10 Timer timer = {}; 15 simloop_update(&simloop, &simout);
11 Simloop simloop = simloop_make(&(SimloopArgs){ 16
12 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer}); 17 TEST_TRUE(simout.should_render);
18 TEST_TRUE(!simout.should_update);
19 TEST_EQUAL(simout.frame, 0);
20}
21
22/// The initial render is not re-triggered if there is a render frame rate cap
23/// and time does not advance.
24TEST_CASE(simloop_initial_render_not_retriggered) {
25 Timer timer = {};
26 Simloop simloop = simloop_make(
27 &(SimloopArgs){.update_fps = 10, .max_render_fps = 10, .timer = &timer});
13 SimloopOut simout; 28 SimloopOut simout;
14 29
15 simloop_update(&simloop, &simout); 30 simloop_update(&simloop, &simout);
31
16 TEST_TRUE(simout.should_render); 32 TEST_TRUE(simout.should_render);
33 TEST_TRUE(!simout.should_update);
34 TEST_EQUAL(simout.frame, 0);
35
36 for (int i = 0; i < 10; i++) {
37 // Note that time does not advance.
38 simloop_update(&simloop, &simout);
39 TEST_TRUE(!simout.should_render);
40 TEST_TRUE(!simout.should_update);
41 TEST_EQUAL(simout.frame, 0);
42 }
17} 43}
18 44
19/// A simulation loop with no render frame cap. 45/// A simulation loop with no render frame cap:
20TEST_CASE(simloop_test_no_render_frame_cap) { 46/// 1. Updates based on the desired update frame rate.
21 constexpr int UPDATE_FPS = 10; 47/// 2. Renders at every loop.
22 const time_delta STEP = sec_to_time_delta(1); 48TEST_CASE(simloop_no_render_frame_cap) {
23 const time_delta SIM_TIME_SEC = sec_to_time_delta(30); 49 constexpr int UPDATE_FPS = 10;
50 const time_delta EXPECT_UPDATE = sec_to_time_delta(1.0 / (double)UPDATE_FPS);
51 const time_delta STEP = sec_to_time_delta(1);
52 const time_delta SIM_TIME_SEC = sec_to_time_delta(30);
24 53
25 Timer timer = {}; 54 Timer timer = {};
26 Simloop simloop = simloop_make(&(SimloopArgs){ 55 Simloop simloop = simloop_make(&(SimloopArgs){
27 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer}); 56 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer});
28 SimloopOut simout; 57 SimloopOut simout;
29 58
30 for (time_delta t = STEP; t < SIM_TIME_SEC; t += STEP) { 59 for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) {
31 timer_advance(&timer, t); 60 timer_advance(&timer, t);
32 simloop_update(&simloop, &simout); 61 simloop_update(&simloop, &simout);
33 TEST_TRUE(simout.should_render); 62 TEST_TRUE(simout.should_render);
34 TEST_EQUAL(simout.updates_pending, UPDATE_FPS); 63 TEST_EQUAL((t > 0) && ((t % EXPECT_UPDATE) == 0), simout.should_update);
35 } 64 }
36} 65}
37 66
38/// A simulation loop with a render frame cap. 67/// A simulation loop with a render frame cap:
39TEST_CASE(simloop_test_with_render_frame_cap) { 68/// 1. Updates based on the desired update frame rate.
69/// 2. Renders based on the desired render frame rate.
70TEST_CASE(simloop_with_render_frame_cap) {
40 constexpr int UPDATE_FPS = 10; 71 constexpr int UPDATE_FPS = 10;
41 constexpr int RENDER_FPS = 5; 72 constexpr int RENDER_FPS = 5;
42 const time_delta STEP = sec_to_time_delta(0.1);
43 const time_delta SIM_TIME_SEC = sec_to_time_delta(30);
44 const time_delta EXPECT_UPDATE = sec_to_time_delta(1.0 / (double)UPDATE_FPS); 73 const time_delta EXPECT_UPDATE = sec_to_time_delta(1.0 / (double)UPDATE_FPS);
45 const time_delta EXPECT_RENDER = sec_to_time_delta(1.0 / (double)RENDER_FPS); 74 const time_delta EXPECT_RENDER = sec_to_time_delta(1.0 / (double)RENDER_FPS);
75 const time_delta STEP = sec_to_time_delta(0.1);
76 const time_delta SIM_TIME_SEC = sec_to_time_delta(30);
46 77
47 Timer timer = {}; 78 Timer timer = {};
48 Simloop simloop = simloop_make(&(SimloopArgs){ 79 Simloop simloop = simloop_make(&(SimloopArgs){
49 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer}); 80 .update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS, .timer = &timer});
50 SimloopOut simout; 81 SimloopOut simout;
51 82
52 for (time_delta t = STEP; t < SIM_TIME_SEC; t += STEP) { 83 for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) {
53 timer_advance(&timer, t); 84 timer_advance(&timer, t);
54 simloop_update(&simloop, &simout); 85 simloop_update(&simloop, &simout);
55 TEST_TRUE(((STEP % EXPECT_RENDER) == 0) ? simout.should_render : true); 86 // Also expecting initial render at t=0.
56 TEST_TRUE(((STEP % EXPECT_UPDATE) == 0) ? simout.updates_pending : true); 87 TEST_EQUAL((t % EXPECT_RENDER) == 0, simout.should_render);
88 TEST_EQUAL((t > 0) && ((t % EXPECT_UPDATE) == 0), simout.should_update);
57 } 89 }
58} 90}
59 91