diff options
| author | 3gg <3gg@shellblade.net> | 2026-04-05 14:48:01 -0700 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2026-04-05 14:48:01 -0700 |
| commit | 5458f3db7b08bf06c1f5c99a5851f6b270126b92 (patch) | |
| tree | 764eff1369fffdd444e7e3cbd649ea5975065cc8 | |
| parent | e97bd18127fe019dd14256d807f9ea5d06cc36b1 (diff) | |
Initial simloop module
| -rw-r--r-- | simloop/CMakeLists.txt | 30 | ||||
| -rw-r--r-- | simloop/include/simloop.h | 44 | ||||
| -rw-r--r-- | simloop/src/simloop.c | 68 | ||||
| -rw-r--r-- | simloop/test/simloop_test.c | 60 |
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 @@ | |||
| 1 | cmake_minimum_required(VERSION 3.5) | ||
| 2 | |||
| 3 | project(simloop) | ||
| 4 | |||
| 5 | set(CMAKE_C_STANDARD 23) | ||
| 6 | set(CMAKE_C_STANDARD_REQUIRED On) | ||
| 7 | set(CMAKE_C_EXTENSIONS Off) | ||
| 8 | |||
| 9 | add_library(simloop | ||
| 10 | include/simloop.h | ||
| 11 | src/simloop.c) | ||
| 12 | |||
| 13 | target_include_directories(simloop PUBLIC | ||
| 14 | include) | ||
| 15 | |||
| 16 | target_link_libraries(simloop PUBLIC | ||
| 17 | timer) | ||
| 18 | |||
| 19 | target_compile_options(simloop PRIVATE -Wall -Wextra -Wpedantic) | ||
| 20 | |||
| 21 | # Test | ||
| 22 | |||
| 23 | add_executable(simloop_test | ||
| 24 | test/simloop_test.c) | ||
| 25 | |||
| 26 | target_link_libraries(simloop_test | ||
| 27 | simloop | ||
| 28 | test) | ||
| 29 | |||
| 30 | target_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 | |||
| 13 | typedef 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 | |||
| 19 | typedef 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 | |||
| 28 | typedef struct SimloopTimeline { | ||
| 29 | time_delta ddt; ///< Desired delta time. | ||
| 30 | time_point last_step; ///< Time of the last simulation step. | ||
| 31 | } SimloopTimeline; | ||
| 32 | |||
| 33 | typedef 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. | ||
| 41 | Simloop simloop_make(const SimloopArgs*); | ||
| 42 | |||
| 43 | /// Step the simulation loop. | ||
| 44 | void 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 | |||
| 5 | static 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 | |||
| 10 | Simloop 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 | |||
| 24 | static time_delta time_elapsed(const Simloop* sim, time_point t) { | ||
| 25 | assert(sim); | ||
| 26 | return time_diff(sim->timer->start_time, t); | ||
| 27 | } | ||
| 28 | |||
| 29 | static 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 | |||
| 40 | static 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 | |||
| 54 | void 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. | ||
| 7 | TEST_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. | ||
| 20 | TEST_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. | ||
| 39 | TEST_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 | |||
| 60 | int main() { return 0; } | ||
