aboutsummaryrefslogtreecommitdiff
path: root/simloop
diff options
context:
space:
mode:
Diffstat (limited to 'simloop')
-rw-r--r--simloop/CMakeLists.txt30
-rw-r--r--simloop/include/simloop.h44
-rw-r--r--simloop/src/simloop.c68
-rw-r--r--simloop/test/simloop_test.c60
4 files changed, 202 insertions, 0 deletions
diff --git a/simloop/CMakeLists.txt b/simloop/CMakeLists.txt
new file mode 100644
index 0000000..1997e2f
--- /dev/null
+++ b/simloop/CMakeLists.txt
@@ -0,0 +1,30 @@
1cmake_minimum_required(VERSION 3.5)
2
3project(simloop)
4
5set(CMAKE_C_STANDARD 23)
6set(CMAKE_C_STANDARD_REQUIRED On)
7set(CMAKE_C_EXTENSIONS Off)
8
9add_library(simloop
10 include/simloop.h
11 src/simloop.c)
12
13target_include_directories(simloop PUBLIC
14 include)
15
16target_link_libraries(simloop PUBLIC
17 timer)
18
19target_compile_options(simloop PRIVATE -Wall -Wextra -Wpedantic)
20
21# Test
22
23add_executable(simloop_test
24 test/simloop_test.c)
25
26target_link_libraries(simloop_test
27 simloop
28 test)
29
30target_compile_options(simloop_test PRIVATE -DUNIT_TEST -DNDEBUG -Wall -Wextra -Wpedantic)
diff --git a/simloop/include/simloop.h b/simloop/include/simloop.h
new file mode 100644
index 0000000..f267d40
--- /dev/null
+++ b/simloop/include/simloop.h
@@ -0,0 +1,44 @@
1/* Simulation loop module.
2 *
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
5 * simulation must be updated and/or the result rendered.
6 */
7#pragma once
8
9#include <timer.h>
10
11#include <stdint.h>
12
13typedef struct SimloopArgs {
14 int update_fps; ///< Update frame rate. Must be >0.
15 int max_render_fps; ///< Render frame rate cap. 0 to disable.
16 Timer* timer; ///< Timer that drives the simulation.
17} SimloopArgs;
18
19typedef struct SimloopOut {
20 uint64_t frame; ///< Frame counter.
21 time_delta render_elapsed; ///< Amount of time elapsed in the rendering.
22 time_delta update_elapsed; ///< Amount of time elapsed in the simulation.
23 time_delta update_dt; ///< Delta time for simulation updates.
24 int updates_pending; ///< Number of frames the simulation should produce.
25 bool should_render; ///< Whether the simulation should be rendered.
26} SimloopOut;
27
28typedef struct SimloopTimeline {
29 time_delta ddt; ///< Desired delta time.
30 time_point last_step; ///< Time of the last simulation step.
31} SimloopTimeline;
32
33typedef struct Simloop {
34 SimloopTimeline update; ///< Update timeline.
35 SimloopTimeline render; ///< Render timeline.
36 uint64_t frame; ///< Frame counter.
37 Timer* timer;
38} Simloop;
39
40/// Create a simulation loop.
41Simloop simloop_make(const SimloopArgs*);
42
43/// Step the simulation loop.
44void simloop_update(Simloop*, SimloopOut*);
diff --git a/simloop/src/simloop.c b/simloop/src/simloop.c
new file mode 100644
index 0000000..11f4d6d
--- /dev/null
+++ b/simloop/src/simloop.c
@@ -0,0 +1,68 @@
1#include <simloop.h>
2
3#include <assert.h>
4
5static uint64_t ddt_from_fps(int fps) {
6 static constexpr double NANOSECONDS = 1e9;
7 return (fps == 0) ? 0 : (uint64_t)(NANOSECONDS / (double)fps);
8}
9
10Simloop simloop_make(const SimloopArgs* args) {
11 assert(args);
12 assert(args->update_fps > 0);
13
14 return (Simloop){
15 .frame = 0,
16 .update = (SimloopTimeline){.ddt = ddt_from_fps(args->update_fps),
17 .last_step = args->timer->start_time},
18 .render = (SimloopTimeline){ .ddt = ddt_from_fps(args->max_render_fps),
19 .last_step = args->timer->start_time},
20 .timer = args->timer,
21 };
22}
23
24static time_delta time_elapsed(const Simloop* sim, time_point t) {
25 assert(sim);
26 return time_diff(sim->timer->start_time, t);
27}
28
29static int step_update(const Simloop* sim, SimloopTimeline* timeline) {
30 assert(sim);
31 assert(timeline);
32 assert(timeline->ddt > 0);
33
34 const time_delta dt = time_diff(timeline->last_step, sim->timer->last_tick);
35 const time_delta steps = dt / timeline->ddt;
36 timeline->last_step = time_add(timeline->last_step, dt);
37 return (int)steps;
38}
39
40static bool step_render(const Simloop* sim, SimloopTimeline* timeline) {
41 assert(sim);
42 assert(timeline);
43
44 bool render = false;
45 if (timeline->ddt > 0) {
46 render = step_update(sim, timeline) > 0;
47 } else {
48 timeline->last_step = sim->timer->last_tick;
49 render = true;
50 }
51 return render;
52}
53
54void simloop_update(Simloop* sim, SimloopOut* out) {
55 assert(sim);
56 assert(out);
57
58 const int new_frames = step_update(sim, &sim->update);
59 out->updates_pending = new_frames;
60 out->should_render =
61 step_render(sim, &sim->render) ||
62 (sim->frame == 0); // Trigger an initial render on the first frame.
63 sim->frame += new_frames;
64 out->frame = sim->frame;
65 out->render_elapsed = time_elapsed(sim, sim->render.last_step);
66 out->update_elapsed = time_elapsed(sim, sim->update.last_step);
67 out->update_dt = sim->update.ddt;
68}
diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c
new file mode 100644
index 0000000..3f2aa46
--- /dev/null
+++ b/simloop/test/simloop_test.c
@@ -0,0 +1,60 @@
1#include <simloop.h>
2
3#include <test.h>
4#include <timer.h>
5
6/// An initial render should always trigger on frame 0.
7TEST_CASE(simloop_initial_render) {
8 constexpr int UPDATE_FPS = 10;
9
10 Timer timer = {};
11 Simloop simloop = simloop_make(&(SimloopArgs){
12 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer});
13 SimloopOut simout;
14
15 simloop_update(&simloop, &simout);
16 TEST_TRUE(simout.should_render);
17}
18
19/// A simulation loop with no render frame cap.
20TEST_CASE(simloop_test_no_render_frame_cap) {
21 constexpr int UPDATE_FPS = 10;
22 const time_delta STEP = sec_to_time_delta(1);
23 const time_delta SIM_TIME_SEC = sec_to_time_delta(30);
24
25 Timer timer = {};
26 Simloop simloop = simloop_make(&(SimloopArgs){
27 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer});
28 SimloopOut simout;
29
30 for (time_delta t = STEP; t < SIM_TIME_SEC; t += STEP) {
31 timer_advance(&timer, t);
32 simloop_update(&simloop, &simout);
33 TEST_TRUE(simout.should_render);
34 TEST_EQUAL(simout.updates_pending, UPDATE_FPS);
35 }
36}
37
38/// A simulation loop with a render frame cap.
39TEST_CASE(simloop_test_with_render_frame_cap) {
40 constexpr int UPDATE_FPS = 10;
41 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);
45 const time_delta EXPECT_RENDER = sec_to_time_delta(1.0 / (double)RENDER_FPS);
46
47 Timer timer = {};
48 Simloop simloop = simloop_make(&(SimloopArgs){
49 .update_fps = UPDATE_FPS, .max_render_fps = 0, .timer = &timer});
50 SimloopOut simout;
51
52 for (time_delta t = STEP; t < SIM_TIME_SEC; t += STEP) {
53 timer_advance(&timer, t);
54 simloop_update(&simloop, &simout);
55 TEST_TRUE(((STEP % EXPECT_RENDER) == 0) ? simout.should_render : true);
56 TEST_TRUE(((STEP % EXPECT_UPDATE) == 0) ? simout.updates_pending : true);
57 }
58}
59
60int main() { return 0; }