Skip to content

Commit

Permalink
Use higher precision timing in main loop (#2983)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mstoeckl authored Oct 31, 2024
1 parent 914c4de commit 119aed1
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 24 deletions.
7 changes: 3 additions & 4 deletions src/supertux/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@

#include <string>

// 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;
Expand Down
32 changes: 15 additions & 17 deletions src/supertux/screen_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint32>(1000.0f / LOGICAL_FPS)),
seconds_per_step(static_cast<float>(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(),
Expand Down Expand Up @@ -563,34 +562,34 @@ 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<std::chrono::nanoseconds>(now - last_time).count();
elapsed_time += 1e-9f * static_cast<float>(nsecs);
g_real_time += 1e-9f * static_cast<float>(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<Uint32>(1000.0f * (seconds_per_step - elapsed_time)));
return;
}

// Useful if screens edit their status without switching screens
Integration::update_status_all(m_screen_stack.back()->get_status());
Integration::update_all();

g_real_time = static_cast<float>(ticks) / 1000.0f;

float speed_multiplier = g_debug.get_game_speed_multiplier();
int steps = elapsed_ticks / ms_per_step;
int steps = static_cast<int>(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
Expand Down Expand Up @@ -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<float>(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) {
Expand Down
6 changes: 3 additions & 3 deletions src/supertux/screen_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#ifndef HEADER_SUPERTUX_SUPERTUX_SCREEN_MANAGER_HPP
#define HEADER_SUPERTUX_SUPERTUX_SCREEN_MANAGER_HPP

#include <chrono>
#include <memory>
#include <SDL.h>

Expand Down Expand Up @@ -80,9 +81,8 @@ class ScreenManager final : public Currenton<ScreenManager>
std::unique_ptr<ControllerHUD> 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<FPS_Stats> m_fps_statistics;

Expand Down

0 comments on commit 119aed1

Please sign in to comment.