From dadaf61c45d675f0e8b88fbc231748ad8247a736 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Mon, 20 Apr 2026 18:03:09 -0700 Subject: Percent frame interpolation factor for smooth animation --- simloop/test/simloop_test.c | 108 ++++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 29 deletions(-) (limited to 'simloop/test/simloop_test.c') diff --git a/simloop/test/simloop_test.c b/simloop/test/simloop_test.c index 603a38c..50e0852 100644 --- a/simloop/test/simloop_test.c +++ b/simloop/test/simloop_test.c @@ -43,11 +43,14 @@ TEST_CASE(simloop_initial_render) { TEST_EQUAL(simout.frame, 0); } -/// 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) { - Simloop simloop = - simloop_make(&(SimloopArgs){.update_fps = 10, .max_render_fps = 10}); +/// A frame is not re-rendered if time does not advance. +/// This applies whether rendering is frame-rate capped or unlimited, and +/// whether we are in the initial frame or a subsequent one. +void simloop_render_not_retriggered( + struct test_case_metadata* metadata, int max_render_fps, + bool initial_frame) { + Simloop simloop = simloop_make( + &(SimloopArgs){.update_fps = 10, .max_render_fps = max_render_fps}); SimloopOut simout; simloop_update(&simloop, 0, &simout); @@ -56,40 +59,58 @@ TEST_CASE(simloop_initial_render_not_retriggered) { TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); + if (!initial_frame) { + // Advance time beyond the initial frame. + simloop_update(&simloop, 1, &simout); + } + for (int i = 0; i < 10; i++) { - // Note that time does not advance. + // Note that time does not advance here. simloop_update(&simloop, 0, &simout); TEST_TRUE(!simout.should_render); TEST_TRUE(!simout.should_update); TEST_EQUAL(simout.frame, 0); } } +TEST_CASE(simloop_render_not_retriggered_capped_initial_frame) { + simloop_render_not_retriggered(metadata, 10, true); +} +TEST_CASE(simloop_render_not_retriggered_unlimited_initial_frame) { + simloop_render_not_retriggered(metadata, 0, true); +} +TEST_CASE(simloop_render_not_retriggered_capped_subsequent_frame) { + simloop_render_not_retriggered(metadata, 10, false); +} +TEST_CASE(simloop_render_not_retriggered_unlimited_subsequent_frame) { + simloop_render_not_retriggered(metadata, 0, false); +} /// A simulation loop with no render frame cap: /// 1. Updates based on the desired update frame rate. -/// 2. Renders at every loop (provided there are updates). +/// 2. Renders at every step. TEST_CASE(simloop_no_render_frame_cap) { 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); + const simloop_time_t STEP = time_delta_from_sec(0.05); // 50ms + const simloop_time_t SIM_DURATION_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); + // the modulo comparison below. + TEST_TRUE((UPDATE_DDT % STEP) == 0); - simloop_time_t dt = 0; Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut 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); + simloop_update(&simloop, 0, &simout); + TEST_TRUE(!simout.should_update); // Time has not advanced. + TEST_TRUE(simout.should_render); // Initial render. + + for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { + simloop_update(&simloop, STEP, &simout); + const bool expect_update = (t % UPDATE_DDT) == 0; TEST_EQUAL(simout.should_update, expect_update); - dt = STEP; + TEST_TRUE(simout.should_render); // Always renders. } } @@ -103,25 +124,54 @@ TEST_CASE(simloop_with_render_frame_cap) { 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); + const simloop_time_t STEP = time_delta_from_sec(0.1); // 100ms + const simloop_time_t SIM_DURATION_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); - simloop_time_t dt = 0; - Simloop simloop = simloop_make( + Simloop simloop = simloop_make( &(SimloopArgs){.update_fps = UPDATE_FPS, .max_render_fps = RENDER_FPS}); SimloopOut simout; - for (simloop_time_t t = 0; t <= SIM_TIME_SEC; t += STEP) { - simloop_update(&simloop, dt, &simout); + simloop_update(&simloop, 0, &simout); + TEST_TRUE(!simout.should_update); // Time has not advanced. + TEST_TRUE(simout.should_render); // Initial render. + + for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { + simloop_update(&simloop, STEP, &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; + TEST_EQUAL(simout.should_update, (t % UPDATE_DDT) == 0); + } +} + +/// If the update falls behind the clock, then percent_frame can fall out of +/// range (>1) if we are not careful. This tests for this condition. +TEST_CASE(simloop_percent_frame_01_large_jump) { + 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_DURATION_SEC = time_delta_from_sec(30); + + // We need simulation time to be an exact multiple of the desired deltas for + // the modulo comparison below. + TEST_TRUE((STEP % UPDATE_DDT) == 0); + + Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); + SimloopOut simout; + + simloop_update(&simloop, 0, &simout); + TEST_TRUE(!simout.should_update); // Time has not advanced. + TEST_TRUE(simout.should_render); // Initial render. + + for (simloop_time_t t = STEP; t <= SIM_DURATION_SEC; t += STEP) { + simloop_update(&simloop, STEP, &simout); + TEST_TRUE(simout.should_update); // Tries to catch up to clock. + TEST_TRUE(simout.should_render); } } @@ -144,8 +194,8 @@ TEST_CASE(simloop_determinism) { }; constexpr uint64_t NUM_RANDOM_STEPS = sizeof(RANDOM_STEPS) / sizeof(RANDOM_STEPS[0]); - const simloop_time_t SIM_TIME_SEC = time_delta_from_sec(10); - constexpr float ADD = 0.123f; + const simloop_time_t SIM_DURATION_SEC = time_delta_from_sec(10); + constexpr float ADD = 0.123f; typedef struct Simulation { int iter_count; @@ -167,7 +217,7 @@ TEST_CASE(simloop_determinism) { Simloop simloop = simloop_make(&(SimloopArgs){.update_fps = UPDATE_FPS}); SimloopOut simout; - for (simloop_time_t t = 0; t <= SIM_TIME_SEC;) { + for (simloop_time_t t = 0; t <= SIM_DURATION_SEC;) { simloop_update(&simloop, dt, &simout); if (simout.should_update) { -- cgit v1.2.3