-
-
Notifications
You must be signed in to change notification settings - Fork 495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cap main loop elapsed time instead of resetting it #3178
base: master
Are you sure you want to change the base?
Conversation
This change adjusts the way the main loop handles the game logic updates falling significantly behind real time. There are three main ways this can occur: a single, very slow operation (like loading a level); consistently slow game logic updates; and consistently slow rendering. By limiting the maximum number of logic steps per frame to 4, the main loop couples rendering and game updates together, making it practical to treat them being slow in the same way. Previously, when the `elapsed_time` difference between real-ish time and game time grew too large (> 8 steps), it would be reset to zero. This works OK for transient slow operations, but when the game logic or rendering runs just a bit slower than real time, the `elapsed_time` difference would slowly grow until hitting the threshold, be reset to zero, and then repeat, leading to a sort of sawtooth pattern. The number of game steps run in each loop iteration is roughly proportional to `elapsed_time` (with an upper limit), so the number of game steps per drawn frame would also have a sawtooth pattern (like 1,2,3,3,4,4,1...), perceivable as periodically shaky motion of the rendered scene. By capping the `elapsed_time` to a fixed value, instead of resetting it, this change eliminates the sawtooth pattern, making the game run more evenly. (It also now runs faster, when logic or render is slow, because the main loop can consistently use 3 or 4 steps per frame.) The cost is that, on normal transient lag spikes (startup, level load, sector switch) there may be multiple steps for the frame instead of zero; however, such lag spikes are rare, concealed by loading screens or safe regions, and do not occur during critical moments of game play.
To test this, use an old computer + high resolution screen, render inefficiently (e.g. by setting Or modify the code to introduce lag. I tested this change using variations on following patch: diff --git a/src/supertux/screen_manager.cpp b/src/supertux/screen_manager.cpp
index 7f5f0082f..6f8a456b3 100644
--- a/src/supertux/screen_manager.cpp
+++ b/src/supertux/screen_manager.cpp
@@ -46,6 +46,7 @@
#include <stdio.h>
#include <chrono>
#include <iostream>
+#include <unistd.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
@@ -556,6 +557,7 @@ void ScreenManager::loop_iter()
g_real_time += 1e-9f * static_cast<float>(nsecs);
last_time = now;
+ std::cerr << "elapsed time: " << elapsed_time;
float max_elapsed_time = 4 * seconds_per_step;
if (elapsed_time > max_elapsed_time) {
// when the game loads up or levels are switched the elapsed_ticks grows
@@ -570,6 +572,7 @@ void ScreenManager::loop_iter()
// Sleep a bit because not enough time has passed since the previous
// logical game step
SDL_Delay(static_cast<Uint32>(1000.0f * (seconds_per_step - elapsed_time)));
+ std::cerr << std::endl;
return;
}
@@ -603,6 +606,7 @@ void ScreenManager::loop_iter()
steps = std::min<int>(steps, max_steps_per_frame);
}
+ std::cerr << " steps: " << steps << std::endl;
for (int i = 0; i < steps; ++i) {
// Perform a logical game step; seconds_per_step is set to a fixed value
// so that the game is deterministic.
@@ -611,6 +615,7 @@ void ScreenManager::loop_iter()
float dtime = seconds_per_step * m_speed * speed_multiplier;
g_game_time += dtime;
process_events();
+ usleep(0.8 * seconds_per_step * 1e6); // add 0.8 steps of lag per step
update_gamelogic(dtime);
elapsed_time -= seconds_per_step;
}
@@ -624,6 +629,7 @@ void ScreenManager::loop_iter()
|| always_draw) {
// Draw a frame
Compositor compositor(m_video_system, g_config->frame_prediction ? time_offset : 0.0f);
+ usleep(1.1 * seconds_per_step * 1e6); // add 1.1 steps of lag per frame
draw(compositor, *m_fps_statistics);
m_fps_statistics->report_frame();
} |
Could you test the frame prediction feature? EDIT: Taking context from the last PR I am confident you have probably tested it already. Code LGTM but I have not tested these. |
I did a quick test of the changes and did not see any problems with it. |
I also tested this and it seemed to be working fine, though I did not test frame prediction. |
Indeed, I have been testing this mainly with frame prediction on, but also sometimes with it off. Also: another way to test this change is to artificially limit the frame rate using |
This is a small tweak to the main game update loop that I've found to improve frame pacing, and increase logic steps/frame, in high lag scenarios where the game cannot maintain real time updates.