diff options
| -rw-r--r-- | simloop/include/simloop.h | 21 | ||||
| -rw-r--r-- | simloop/src/simloop.c | 84 | ||||
| -rw-r--r-- | simloop/test/simloop_test.c | 69 |
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 | ||
| 5 | typedef uint64_t simloop_time_t; | 5 | typedef uint64_t simloop_time_t; ///< Time delta in nanoseconds. |
| 6 | 6 | ||
| 7 | typedef struct SimloopArgs { | 7 | typedef 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 | ||
| 12 | typedef struct SimloopOut { | 12 | typedef 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 | ||
| 23 | typedef struct SimloopTimeline { | 22 | typedef struct SimloopTimeline { |
| @@ -26,12 +25,10 @@ typedef struct SimloopTimeline { | |||
| 26 | } SimloopTimeline; | 25 | } SimloopTimeline; |
| 27 | 26 | ||
| 28 | typedef struct Simloop { | 27 | typedef 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 | ||
| 33 | static bool step_update(const Simloop* sim, SimloopTimeline* timeline) { | 27 | void 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 | |||
| 50 | static 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 | |||
| 64 | void 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). | ||
| 35 | TEST_CASE(simloop_initial_render) { | 33 | TEST_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. | 45 | TEST_CASE(simloop_render_not_retriggered) { |
| 49 | void 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 | } | ||
| 75 | TEST_CASE(simloop_render_not_retriggered_capped_initial_frame) { | ||
| 76 | simloop_render_not_retriggered(metadata, 10, true); | ||
| 77 | } | ||
| 78 | TEST_CASE(simloop_render_not_retriggered_unlimited_initial_frame) { | ||
| 79 | simloop_render_not_retriggered(metadata, 0, true); | ||
| 80 | } | ||
| 81 | TEST_CASE(simloop_render_not_retriggered_capped_subsequent_frame) { | ||
| 82 | simloop_render_not_retriggered(metadata, 10, false); | ||
| 83 | } | ||
| 84 | TEST_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. |
| 91 | TEST_CASE(simloop_no_render_frame_cap) { | 64 | TEST_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. |
| 120 | TEST_CASE(simloop_with_render_frame_cap) { | 93 | TEST_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 | ||
