From 119aed1386852c2cd3dc6689434a5a9690530faf Mon Sep 17 00:00:00 2001 From: mstoeckl Date: Thu, 31 Oct 2024 15:23:04 -0400 Subject: [PATCH] Use higher precision timing in main loop (#2983) Currently, because inverse frame rates are rounded to milliseconds in src/supertux/screen_manager.cpp, the only possible logical frame rates are 1/0.016 = 62.5fps, 1/0.015 = 66.66fps, 1/0.014 = 71.43fps, etc. (The current value LOGICAL_FPS=64.f gets rounded to 66.66.). This PR makes it possible to change the logical FPS to arbitrary values (like 120fps, if we want a multiple of the most common monitor refresh rate). It should also very slightly reduce jitter from time measurements on high frame rate displays. * Correct for rounding in LOGICAL_FPS The main loop for SuperTux used millisecond-precision timing, and as a result rounded the spacing between logical steps to the nearest millisecond. As a result, the actual logical fps did not match the LOGICAL_FPS constant. This commit updates LOGICAL_FPS to match. * Use higher precision timing in main loop This very slightly reduces jitter from timing quantization noise, and makes it possible to use arbitrary logical frame rates, instead of those corresponding to integer millisecond frame spacings. --- src/supertux/constants.hpp | 7 +++---- src/supertux/screen_manager.cpp | 32 +++++++++++++++----------------- src/supertux/screen_manager.hpp | 6 +++--- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/supertux/constants.hpp b/src/supertux/constants.hpp index cf22817f11f..f2624c06e12 100644 --- a/src/supertux/constants.hpp +++ b/src/supertux/constants.hpp @@ -19,10 +19,9 @@ #include -// the engine will be run with a logical framerate of 64fps. -// We chose 64fps here because it is a power of 2, so 1/64 gives an "even" -// binary fraction... -static const float LOGICAL_FPS = 64.0; +// the engine will be run with a logical framerate of 66.666fps, corresponding +// to a 15 msec gap between steps. Warning: changing this may affect physics +static const float LOGICAL_FPS = 1000.0f / 15.0f; // SHIFT_DELTA is used for sliding over 1-tile gaps and collision detection static const float SHIFT_DELTA = 7.0f; diff --git a/src/supertux/screen_manager.cpp b/src/supertux/screen_manager.cpp index 1343af44ace..e53cfcbfdc8 100644 --- a/src/supertux/screen_manager.cpp +++ b/src/supertux/screen_manager.cpp @@ -137,10 +137,9 @@ ScreenManager::ScreenManager(VideoSystem& video_system, InputManager& input_mana m_menu_manager(new MenuManager()), m_controller_hud(new ControllerHUD), m_mobile_controller(), - last_ticks(0), - elapsed_ticks(0), - ms_per_step(static_cast(1000.0f / LOGICAL_FPS)), - seconds_per_step(static_cast(ms_per_step) / 1000.0f), + last_time(std::chrono::steady_clock::now()), + elapsed_time(0.0f), + seconds_per_step(1.0f / LOGICAL_FPS), m_fps_statistics(new FPS_Stats()), m_speed(1.0), m_actions(), @@ -563,23 +562,25 @@ ScreenManager::handle_screen_switch() void ScreenManager::loop_iter() { - Uint32 ticks = SDL_GetTicks(); - elapsed_ticks += ticks - last_ticks; - last_ticks = ticks; + auto now = std::chrono::steady_clock::now(); + auto nsecs = std::chrono::duration_cast(now - last_time).count(); + elapsed_time += 1e-9f * static_cast(nsecs); + g_real_time += 1e-9f * static_cast(nsecs); + last_time = now; - if (elapsed_ticks > ms_per_step * 8) { + if (elapsed_time > seconds_per_step * 8) { // when the game loads up or levels are switched the // elapsed_ticks grows extremely large, so we just ignore those // large time jumps - elapsed_ticks = 0; + elapsed_time = 0; } bool always_draw = g_debug.draw_redundant_frames || g_config->frame_prediction; - if (elapsed_ticks < ms_per_step && !always_draw) { + if (elapsed_time < seconds_per_step && !always_draw) { // Sleep a bit because not enough time has passed since the previous // logical game step - SDL_Delay(ms_per_step - elapsed_ticks); + SDL_Delay(static_cast(1000.0f * (seconds_per_step - elapsed_time))); return; } @@ -587,10 +588,8 @@ void ScreenManager::loop_iter() Integration::update_status_all(m_screen_stack.back()->get_status()); Integration::update_all(); - g_real_time = static_cast(ticks) / 1000.0f; - float speed_multiplier = g_debug.get_game_speed_multiplier(); - int steps = elapsed_ticks / ms_per_step; + int steps = static_cast(std::floor(elapsed_time / seconds_per_step)); // Do not calculate more than a few steps at once // The maximum number of steps executed before drawing a frame is @@ -624,14 +623,13 @@ void ScreenManager::loop_iter() g_game_time += dtime; process_events(); update_gamelogic(dtime); - elapsed_ticks -= ms_per_step; + elapsed_time -= seconds_per_step; } // When the game is laggy, real time may be >1 step after the game time // To avoid predicting positions too far ahead, when using frame prediction, // limit the draw time offset to at most one step. - Uint32 tick_offset = std::min(elapsed_ticks, ms_per_step); - float time_offset = m_speed * speed_multiplier * static_cast(tick_offset) / 1000.0f; + float time_offset = m_speed * speed_multiplier * std::min(elapsed_time, seconds_per_step); if ((steps > 0 && !m_screen_stack.empty()) || always_draw) { diff --git a/src/supertux/screen_manager.hpp b/src/supertux/screen_manager.hpp index b2e7ab46fad..1b1de08c359 100644 --- a/src/supertux/screen_manager.hpp +++ b/src/supertux/screen_manager.hpp @@ -18,6 +18,7 @@ #ifndef HEADER_SUPERTUX_SUPERTUX_SCREEN_MANAGER_HPP #define HEADER_SUPERTUX_SUPERTUX_SCREEN_MANAGER_HPP +#include #include #include @@ -80,9 +81,8 @@ class ScreenManager final : public Currenton std::unique_ptr m_controller_hud; MobileController m_mobile_controller; - Uint32 last_ticks; - Uint32 elapsed_ticks; - const Uint32 ms_per_step; + std::chrono::steady_clock::time_point last_time; + float elapsed_time; const float seconds_per_step; std::unique_ptr m_fps_statistics;