From e5fe20ad83e01b3c8262ac90af984da47d8a16a1 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sun, 12 Apr 2026 10:48:04 -0700 Subject: Work in terms of time deltas; remove dependency on timer module --- simloop/CMakeLists.txt | 3 -- simloop/include/simloop.h | 31 +++++++------- simloop/src/simloop.c | 56 +++++++++++++----------- simloop/test/simloop_test.c | 102 +++++++++++++++++++++++--------------------- 4 files changed, 99 insertions(+), 93 deletions(-) (limited to 'simloop') 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 target_include_directories(simloop PUBLIC include) -target_link_libraries(simloop PUBLIC - timer) - target_compile_options(simloop PRIVATE -Wall -Wextra -Wpedantic) # 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 @@ */ #pragma once -#include - #include +typedef uint64_t simloop_time_t; + typedef struct SimloopArgs { - int update_fps; ///< Update frame rate. Must be >0. - int max_render_fps; ///< Render frame rate cap. 0 to disable. - Timer* timer; ///< Timer that drives the simulation. + int update_fps; ///< Update frame rate. Must be >0. + int max_render_fps; ///< Render frame rate cap. 0 to disable. } SimloopArgs; typedef struct SimloopOut { - uint64_t frame; ///< Frame counter. - time_delta render_elapsed; ///< Amount of time elapsed in the rendering. - time_delta update_elapsed; ///< Amount of time elapsed in the simulation. - time_delta update_dt; ///< Delta time for simulation updates. - bool should_update; ///< Whether the simulation should update. - bool should_render; ///< Whether the simulation should be rendered. + uint64_t frame; ///< Frame counter. + simloop_time_t render_elapsed; ///< Amount of time elapsed in the rendering. + simloop_time_t update_elapsed; ///< Amount of time elapsed in the simulation. + simloop_time_t update_dt; ///< Delta time for simulation updates. + bool should_update; ///< Whether the simulation should update. + bool should_render; ///< Whether the simulation should be rendered. } SimloopOut; typedef struct SimloopTimeline { - time_delta ddt; ///< Desired delta time. - time_point last_step; ///< Time of the last simulation step. + simloop_time_t ddt; ///< Desired delta time. + simloop_time_t time; ///< Time point of the last simulation step. } SimloopTimeline; typedef struct Simloop { + simloop_time_t clock; ///< Tracks simulation time. + uint64_t frame; ///< Frame counter, number of updates done. SimloopTimeline update; ///< Update timeline. SimloopTimeline render; ///< Render timeline. - uint64_t frame; ///< Frame counter. - Timer* timer; bool first_iter; bool updates_since_last_render; } Simloop; @@ -76,4 +75,4 @@ Simloop simloop_make(const SimloopArgs*); /// Step the simulation loop. /// /// The simulation always triggers a render of the initial state of simulation. -void simloop_update(Simloop*, SimloopOut*); +void 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 @@ #include -static uint64_t ddt_from_fps(int fps) { +static simloop_time_t ddt_from_fps(int fps) { static constexpr double NANOSECONDS = 1e9; - return (fps == 0) ? 0 : (uint64_t)(NANOSECONDS / (double)fps); + return (fps == 0) ? 0 : (simloop_time_t)(NANOSECONDS / (double)fps); } Simloop simloop_make(const SimloopArgs* args) { @@ -12,31 +12,30 @@ Simloop simloop_make(const SimloopArgs* args) { assert(args->update_fps > 0); return (Simloop){ - .frame = 0, - .update = (SimloopTimeline){.ddt = ddt_from_fps(args->update_fps), - .last_step = args->timer->start_time}, - .render = (SimloopTimeline){ .ddt = ddt_from_fps(args->max_render_fps), - .last_step = args->timer->start_time}, - .timer = args->timer, - .first_iter = true, + .frame = 0, + .update = + (SimloopTimeline){ + .ddt = ddt_from_fps(args->update_fps), + .time = 0, + }, + .render = + (SimloopTimeline){ + .ddt = ddt_from_fps(args->max_render_fps), + .time = 0, + }, + .first_iter = true, .updates_since_last_render = false, }; } -static time_delta time_elapsed(const Simloop* sim, time_point t) { - assert(sim); - return time_diff(sim->timer->start_time, t); -} - static bool step_update(const Simloop* sim, SimloopTimeline* timeline) { assert(sim); assert(timeline); assert(timeline->ddt > 0); - const time_delta dt = time_diff(timeline->last_step, sim->timer->last_tick); - const bool should_step = dt >= timeline->ddt; - timeline->last_step = - should_step ? time_add(timeline->last_step, dt) : timeline->last_step; + const simloop_time_t dt = sim->clock - timeline->time; + const bool should_step = dt >= timeline->ddt; + timeline->time += should_step ? timeline->ddt : 0; return should_step; } @@ -48,32 +47,37 @@ static bool step_render(const Simloop* sim, SimloopTimeline* timeline) { if (timeline->ddt > 0) { render = step_update(sim, timeline); } else { - timeline->last_step = sim->timer->last_tick; - render = true; + timeline->time = sim->clock; + render = true; } return render; } -void simloop_update(Simloop* sim, SimloopOut* out) { +void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) { assert(sim); assert(out); + sim->clock += dt; + // Simulation update. const bool updated = step_update(sim, &sim->update); - out->should_update = updated; sim->updates_since_last_render = sim->updates_since_last_render || updated; + // Simulation render. const bool rendered = (sim->updates_since_last_render && step_render(sim, &sim->render)) || (sim->first_iter); // Trigger an initial render on the first frame. - out->should_render = rendered; sim->updates_since_last_render = sim->updates_since_last_render && !out->should_render; + // Loop state update. sim->frame += (updated ? 1 : 0); - sim->first_iter = false; + sim->first_iter = false; + out->frame = sim->frame; - out->render_elapsed = time_elapsed(sim, sim->render.last_step); - out->update_elapsed = time_elapsed(sim, sim->update.last_step); + out->render_elapsed = sim->render.time; + out->update_elapsed = sim->update.time; out->update_dt = sim->update.ddt; + out->should_update = updated; + out->should_render = rendered; } 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,10 +1,17 @@ #include #include -#include #include +// ----------------------------------------------------------------------------- +// Time. + +static simloop_time_t time_delta_from_sec(double seconds) { + static constexpr double NANOS_PER_SEC = 1e9; + return (simloop_time_t)(seconds * NANOS_PER_SEC); +} + // ----------------------------------------------------------------------------- // Randomness. @@ -12,7 +19,7 @@ typedef struct { uint64_t a; } XorShift64State; -uint64_t xorshift64(XorShift64State* state) { +static uint64_t xorshift64(XorShift64State* state) { uint64_t x = state->a; x ^= x << 7; x ^= x >> 9; @@ -26,12 +33,10 @@ uint64_t xorshift64(XorShift64State* state) { /// 1. An initial render is always triggered. /// 2. No update is triggered (not enough time passed). TEST_CASE(simloop_initial_render) { - Timer timer = {}; - Simloop simloop = - simloop_make(&(SimloopArgs){.update_fps = 10, .timer = &timer}); + Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = 10}); SimloopOut simout; - simloop_update(&simloop, &simout); + simloop_update(&simloop, 0, &simout); TEST_TRUE(simout.should_render); TEST_TRUE(!simout.should_update); @@ -41,12 +46,11 @@ TEST_CASE(simloop_initial_render) { /// The initial render is not re-triggered if there is a render frame rate cap /// and time does not advance. TEST_CASE(simloop_initial_render_not_retriggered) { - Timer timer = {}; - Simloop simloop = simloop_make( - &(SimloopArgs){.update_fps = 10, .max_render_fps = 10, .timer = &timer}); + Simloop simloop = + simloop_make(&(SimloopArgs){.update_fps = 10, .max_render_fps = 10}); SimloopOut simout; - simloop_update(&simloop, &simout); + simloop_update(&simloop, 0, &simout); TEST_TRUE(simout.should_render); TEST_TRUE(!simout.should_update); @@ -54,7 +58,7 @@ TEST_CASE(simloop_initial_render_not_retriggered) { for (int i = 0; i < 10; i++) { // Note that time does not advance. - simloop_update(&simloop, &simout); + simloop_update(&simloop, 0, &simout); TEST_TRUE(!simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); @@ -65,27 +69,27 @@ TEST_CASE(simloop_initial_render_not_retriggered) { /// 1. Updates based on the desired update frame rate. /// 2. Renders at every loop (provided there are updates). TEST_CASE(simloop_no_render_frame_cap) { - constexpr int UPDATE_FPS = 10; // 100ms delta - const time_delta UPDATE_DDT = sec_to_time_delta(1.0 / (double)UPDATE_FPS); - const time_delta STEP = sec_to_time_delta(1); - const time_delta SIM_TIME_SEC = sec_to_time_delta(30); + constexpr int UPDATE_FPS = 10; // 100ms delta + const simloop_time_t UPDATE_DDT = + time_delta_from_sec(1.0 / (double)UPDATE_FPS); + const simloop_time_t STEP = time_delta_from_sec(1); + const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(30); // We need simulation time to be an exact multiple of the desired deltas for // the modulo comparisons below. TEST_TRUE((STEP % UPDATE_DDT) == 0); - Timer timer = {}; - Simloop simloop = - simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer}); + simloop_time_t dt = 0; + Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut simout; - for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { - timer_advance(&timer, t); - simloop_update(&simloop, &simout); + for (simloop_time_t t = 0; t <= SIM_TIME_SEC; t += STEP) { + simloop_update(&simloop, dt, &simout); const bool expect_update = (t > 0) && ((t % UPDATE_DDT) == 0); // A render is still expected at time 0. TEST_EQUAL(simout.should_render, (t == 0) || expect_update); TEST_EQUAL(simout.should_update, expect_update); + dt = STEP; } } @@ -93,29 +97,31 @@ TEST_CASE(simloop_no_render_frame_cap) { /// 1. Updates based on the desired update frame rate. /// 2. Renders based on the desired render frame rate. TEST_CASE(simloop_with_render_frame_cap) { - constexpr int UPDATE_FPS = 10; // 100ms delta - constexpr int RENDER_FPS = 5; // 200ms delta - const time_delta UPDATE_DDT = sec_to_time_delta(1.0 / (double)UPDATE_FPS); - const time_delta RENDER_DDT = sec_to_time_delta(1.0 / (double)RENDER_FPS); - const time_delta STEP = sec_to_time_delta(0.1); // 100ms - const time_delta SIM_TIME_SEC = sec_to_time_delta(30); + constexpr int UPDATE_FPS = 10; // 100ms delta + constexpr int RENDER_FPS = 5; // 200ms delta + const simloop_time_t UPDATE_DDT = + time_delta_from_sec(1.0 / (double)UPDATE_FPS); + const simloop_time_t RENDER_DDT = + time_delta_from_sec(1.0 / (double)RENDER_FPS); + const simloop_time_t STEP = time_delta_from_sec(0.1); // 100ms + const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(30); // We need simulation time to be an exact multiple of the desired deltas for // the modulo comparisons below. TEST_TRUE((UPDATE_DDT % STEP) == 0); TEST_TRUE((RENDER_DDT % STEP) == 0); - Timer timer = {}; - Simloop simloop = simloop_make(&(SimloopArgs){ - .update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS, .timer = &timer}); + simloop_time_t dt = 0; + Simloop simloop = simloop_make( + &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS}); SimloopOut simout; - for (time_delta t = 0; t < SIM_TIME_SEC; t += STEP) { - timer_advance(&timer, t); - simloop_update(&simloop, &simout); + for (simloop_time_t t = 0; t <= SIM_TIME_SEC; t += STEP) { + simloop_update(&simloop, dt, &simout); // A render is still expected at time 0. TEST_EQUAL(simout.should_render, (t % RENDER_DDT) == 0); TEST_EQUAL(simout.should_update, (t > 0) && ((t % UPDATE_DDT) == 0)); + dt = STEP; } } @@ -130,16 +136,16 @@ TEST_CASE(simloop_with_render_frame_cap) { /// the client to decide whether to update just once or as many times as /// requested, depending on whether they want determinism or convergence. TEST_CASE(simloop_determinism) { - constexpr int UPDATE_FPS = 100; // 10ms delta - const time_delta RANDOM_STEPS[] = { - sec_to_time_delta(0.007), // 7ms - sec_to_time_delta(0.005), // 5ms - sec_to_time_delta(0.003), // 3ms + constexpr int UPDATE_FPS = 100; // 10ms delta + const simloop_time_t RANDOM_STEPS[] = { + time_delta_from_sec(0.007), // 7ms + time_delta_from_sec(0.005), // 5ms + time_delta_from_sec(0.003), // 3ms }; constexpr uint64_t NUM_RANDOM_STEPS = sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]); - const time_delta SIM_TIME_SEC = sec_to_time_delta(10); - constexpr float ADD = 0.123f; + const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(10); + constexpr float ADD = 0.123f; typedef struct Simulation { int iter_count; @@ -153,26 +159,26 @@ TEST_CASE(simloop_determinism) { } Simulation sim[2] = {0}; - XorShift64State xss = (XorShift64State){12069019817132197873}; + XorShift64State xss = (XorShift64State){12069019817132197873ULL}; // Perform two simulations with random clock-time steps. for (int s = 0; s < 2; ++s) { - Timer timer = {}; - Simloop simloop = - simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS, .timer = &timer}); + simloop_time_t dt = 0; + Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut simout; - for (time_delta t = 0; t < SIM_TIME_SEC;) { - timer_advance(&timer, t); - simloop_update(&simloop, &simout); + for (simloop_time_t t = 0; t <= SIM_TIME_SEC;) { + simloop_update(&simloop, dt, &simout); if (simout.should_update) { UPDATE_SIMULATION(sim[s]); } // Advance time with a random step. - const time_delta step = RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS]; + const simloop_time_t step = + RANDOM_STEPS[xorshift64(&xss) % NUM_RANDOM_STEPS]; t += step; + dt = step; } } -- cgit v1.2.3