From 68eb5df28ce7ead5974e24c17069534be277a0bc Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Mon, 6 Jul 2026 00:54:05 +0200 Subject: [PATCH 1/8] More accurate frame-time estimate for input_lag_turns == 0 --- src/main.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9b0da041f6..422210c2b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -226,7 +226,7 @@ TbBool TimerFreeze = false; /******************************************************************************/ static long double time_since_last_draw = 0; -static long double average_delta_time = 1; +static long double average_frame_draw_time = 1; static long double multiplayer_clock_adjust = 1; float interpolate_time = 0; /******************************************************************************/ @@ -3486,7 +3486,6 @@ static void gameplay_loop_draw() framerate_measurement_capture(Framerate_Draw); game.delta_time = time_since_last_draw; time_since_last_draw = 0; - average_delta_time += (game.delta_time - average_delta_time) * max(average_delta_time, .05L) / 20; interpolate_time = min(max(game.process_turn_time, 0.L), 1.L); keeper_screen_redraw(); } @@ -3505,6 +3504,12 @@ static void gameplay_loop_draw() } frametime_end_measurement(Frametime_Draw); last_draw_completed_time = get_time_tick_ns(); + + if ( do_draw ) { + update_gameplay_delta_time(); + const long double delta = time_since_last_draw - average_frame_draw_time; + average_frame_draw_time += delta * max(average_frame_draw_time, .05L) / 20; + } } static void gameplay_loop_logic() @@ -3538,7 +3543,8 @@ static void gameplay_loop_logic() // another frame could miss this deadline, skip it. // In a 3-4 player game, clients must be 2 frames early. const int frames = 1 + (netstate.my_id != SERVER_ID && game.active_players_count > 2); - if (game.process_turn_time + frames * average_delta_time < 1.0) + const long double offset = frames * average_frame_draw_time * multiplayer_clock_adjust * max(game.frame_skip, 1); + if (game.process_turn_time + offset < 1.0) return; } else From d0b15c89ef287b790df17e3a451d4a4e8a719c62 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Mon, 6 Jul 2026 01:05:50 +0200 Subject: [PATCH 2/8] Remove unused variable --- src/frontend.cpp | 2 -- src/main.cpp | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/frontend.cpp b/src/frontend.cpp index 5f82020d94..93aac85b02 100644 --- a/src/frontend.cpp +++ b/src/frontend.cpp @@ -96,7 +96,6 @@ extern "C" { #endif -extern long double last_draw_completed_time; /******************************************************************************/ TbClockMSec gui_message_timeout = 0; char gui_message_text[TEXT_BUFFER_LENGTH]; @@ -3375,7 +3374,6 @@ short frontend_draw(void) draw_debug_messages(); perform_any_screen_capturing(); LbScreenUnlock(); - last_draw_completed_time = get_time_tick_ns(); return result; } diff --git a/src/main.cpp b/src/main.cpp index 422210c2b9..4db4307a22 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -218,7 +218,6 @@ extern void startup_network_game(CoroutineLoop *context, TbBool local); /******************************************************************************/ TbClockMSec timerstarttime = 0; -long double last_draw_completed_time = 0; struct TimerTime Timer; TbBool TimerGame = false; TbBool TimerNoReset = false; @@ -3503,7 +3502,6 @@ static void gameplay_loop_draw() keeper_screen_swap(); } frametime_end_measurement(Frametime_Draw); - last_draw_completed_time = get_time_tick_ns(); if ( do_draw ) { update_gameplay_delta_time(); From 7bb4800d894a8eb7bdc5ff81e64ecb33983c1a98 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Sat, 4 Jul 2026 23:54:08 +0200 Subject: [PATCH 3/8] Rename turns_per_second_draw_* -> fps_limit_* --- src/console_cmd.c | 4 ++-- src/game_legacy.h | 6 +++--- src/main.cpp | 26 +++++++++++++------------- src/main_game.c | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/console_cmd.c b/src/console_cmd.c index 3d8314c44d..21887cd9de 100644 --- a/src/console_cmd.c +++ b/src/console_cmd.c @@ -480,7 +480,7 @@ void param_completion_for_magic_instance(PlayerNumber plyr_idx, char *args_str, TbBool cmd_stats(PlayerNumber plyr_idx, char * args) { - targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "turn fps is %d, draw fps is %d", turns_per_second, turns_per_second_draw_current); + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "turn fps is %d, draw fps is %d", turns_per_second, fps_limit_current); return true; } @@ -498,7 +498,7 @@ TbBool cmd_fps_turn(PlayerNumber plyr_idx, char * args) TbBool cmd_fps_draw(PlayerNumber plyr_idx, char * args) { - parse_draw_fps_config_val(args, &turns_per_second_draw_main, &turns_per_second_draw_secondary); + parse_draw_fps_config_val(args, &fps_limit_main, &fps_limit_secondary); redetect_screen_refresh_rate_for_draw(); return true; } diff --git a/src/game_legacy.h b/src/game_legacy.h index 42de8087d6..2c0a43af48 100644 --- a/src/game_legacy.h +++ b/src/game_legacy.h @@ -411,9 +411,9 @@ struct Game { extern struct Game game; extern int32_t turns_per_second; -extern int32_t turns_per_second_draw_current; -extern int32_t turns_per_second_draw_main; -extern int32_t turns_per_second_draw_secondary; +extern int32_t fps_limit_current; +extern int32_t fps_limit_main; +extern int32_t fps_limit_secondary; TbBool network_is_active(void); diff --git a/src/main.cpp b/src/main.cpp index 4db4307a22..1510233f1c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -151,9 +151,9 @@ char autostart_multiplayer_campaign[80] = ""; int autostart_multiplayer_level = 0; int32_t turns_per_second; -int32_t turns_per_second_draw_current = 0; -int32_t turns_per_second_draw_main = 0; // -1 if auto -int32_t turns_per_second_draw_secondary = 0; +int32_t fps_limit_current = 0; +int32_t fps_limit_main = 0; // -1 if auto +int32_t fps_limit_secondary = 0; unsigned char *blue_palette; @@ -3349,10 +3349,10 @@ TbBool keeper_wait_for_next_turn(void) TbBool keeper_wait_for_next_draw(void) { // fps.draw is currently unable to work properly with frame_skip - if (turns_per_second_draw_current > 0 && is_feature_on(Ft_DeltaTime) == true && game.frame_skip == 0) + if (fps_limit_current > 0 && is_feature_on(Ft_DeltaTime) == true && game.frame_skip == 0) { const long double tick_ns_one_sec = 1000000000.0; - const long double tick_ns_one_frame = tick_ns_one_sec/turns_per_second_draw_current; + const long double tick_ns_one_frame = tick_ns_one_sec/fps_limit_current; static long double tick_ns_last_draw = 0; long double tick_ns_cur = get_time_tick_ns(); @@ -3374,24 +3374,24 @@ TbBool keeper_wait_for_next_draw(void) void redetect_screen_refresh_rate_for_draw() { - turns_per_second_draw_current = 0; + fps_limit_current = 0; - if (turns_per_second_draw_main == -1) { - if (turns_per_second_draw_secondary > 0) - turns_per_second_draw_current = turns_per_second_draw_secondary; + if (fps_limit_main == -1) { + if (fps_limit_secondary > 0) + fps_limit_current = fps_limit_secondary; if (lbWindow != NULL) { int display_index = SDL_GetWindowDisplayIndex(lbWindow); if (display_index >= 0) { SDL_DisplayMode mode; if (SDL_GetCurrentDisplayMode(display_index, &mode) == 0 && mode.refresh_rate > 0) { - turns_per_second_draw_current = mode.refresh_rate; + fps_limit_current = mode.refresh_rate; } } } - } else if (turns_per_second_draw_main > 0) { - turns_per_second_draw_current = turns_per_second_draw_main; + } else if (fps_limit_main > 0) { + fps_limit_current = fps_limit_main; } } @@ -3464,7 +3464,7 @@ static void gameplay_loop_draw() if (is_feature_on(Ft_DeltaTime) && ! network_is_active()) { frametime_start_measurement(Frametime_Sleep); - if (turns_per_second_draw_current > 0) + if (fps_limit_current > 0) keeper_wait_for_next_draw(); frametime_end_measurement(Frametime_Sleep); } diff --git a/src/main_game.c b/src/main_game.c index 5788978b3f..d3f39e091a 100644 --- a/src/main_game.c +++ b/src/main_game.c @@ -455,9 +455,9 @@ void clear_complete_game(void) else set_selected_level_number(first_singleplayer_level()); turns_per_second = start_params.num_fps; - turns_per_second_draw_current = 0; - turns_per_second_draw_main = start_params.num_fps_draw_main; - turns_per_second_draw_secondary = start_params.num_fps_draw_secondary; + fps_limit_current = 0; + fps_limit_main = start_params.num_fps_draw_main; + fps_limit_secondary = start_params.num_fps_draw_secondary; game.mode_flags = start_params.mode_flags; game.easter_eggs_enabled = start_params.easter_egg; set_flag_value(game.system_flags, GSF_AllowOnePlayer, start_params.one_player); From 8f23fefb7fc15abf6363757a54ba1cbb585b5025 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Mon, 6 Jul 2026 02:02:16 +0200 Subject: [PATCH 4/8] Non-blocking fps limiter --- src/console_cmd.c | 1 + src/main.cpp | 81 ++++++++++++++++++++--------------------------- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/console_cmd.c b/src/console_cmd.c index 21887cd9de..50199f2e0e 100644 --- a/src/console_cmd.c +++ b/src/console_cmd.c @@ -493,6 +493,7 @@ TbBool cmd_fps_turn(PlayerNumber plyr_idx, char * args) } else { turns_per_second = atoi(pr1str); } + redetect_screen_refresh_rate_for_draw(); return true; } diff --git a/src/main.cpp b/src/main.cpp index 1510233f1c..5f4fc823be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -132,7 +132,7 @@ #include "net_input_lag.h" #include "moonphase.h" #include "frontmenu_ingame_map.h" -#include +#include #ifdef FUNCTESTING #include "ftests/ftest.h" @@ -150,12 +150,6 @@ struct StartupParameters start_params; char autostart_multiplayer_campaign[80] = ""; int autostart_multiplayer_level = 0; int32_t turns_per_second; - -int32_t fps_limit_current = 0; -int32_t fps_limit_main = 0; // -1 if auto -int32_t fps_limit_secondary = 0; - - unsigned char *blue_palette; unsigned char *red_palette; unsigned char *dog_palette; @@ -224,6 +218,11 @@ TbBool TimerNoReset = false; TbBool TimerFreeze = false; /******************************************************************************/ +int32_t fps_limit_current = 0; +int32_t fps_limit_main = 0; // -1 if auto +int32_t fps_limit_secondary = 0; +static long double frame_time_target = 0; +static long double process_frame_time = 0; static long double time_since_last_draw = 0; static long double average_frame_draw_time = 1; static long double multiplayer_clock_adjust = 1; @@ -3260,7 +3259,7 @@ short display_should_be_updated_this_turn(void) if ( (game.turns_fastforward == 0) && (!game.packet_loading_in_progress) ) { find_frame_rate(); - if ( is_feature_on(Ft_DeltaTime) || (game.frame_skip == 0) || ((get_gameturn() % game.frame_skip) == 0)) + if ( (game.frame_skip == 0) || ((get_gameturn() % game.frame_skip) == 0) ) return true; } else if ( ((get_gameturn() & 0x3F)==0) || @@ -3345,33 +3344,6 @@ TbBool keeper_wait_for_next_turn(void) return false; } - -TbBool keeper_wait_for_next_draw(void) -{ - // fps.draw is currently unable to work properly with frame_skip - if (fps_limit_current > 0 && is_feature_on(Ft_DeltaTime) == true && game.frame_skip == 0) - { - const long double tick_ns_one_sec = 1000000000.0; - const long double tick_ns_one_frame = tick_ns_one_sec/fps_limit_current; - - static long double tick_ns_last_draw = 0; - long double tick_ns_cur = get_time_tick_ns(); - long double tick_ns_used = tick_ns_cur - tick_ns_last_draw; - long double tick_ns_delay = tick_ns_one_frame - tick_ns_used; - - long double tick_ns_end = tick_ns_cur; - // tick_ns_used: every level, initialized_time_point will be reset, so tick_ns_used may be less than 0 when enter level for the non-first time, Skip it directly to solve the problem. - if (tick_ns_delay > 0 && tick_ns_used >= 0) { - tick_ns_end = tick_ns_cur + tick_ns_delay; - LbSleepUntilExt(tick_ns_end); - } - tick_ns_last_draw = tick_ns_end; - - return true; - } - return false; -} - void redetect_screen_refresh_rate_for_draw() { fps_limit_current = 0; @@ -3393,6 +3365,11 @@ void redetect_screen_refresh_rate_for_draw() } else if (fps_limit_main > 0) { fps_limit_current = fps_limit_main; } + + if (fps_limit_current > 0) + frame_time_target = 1.L / fps_limit_current * turns_per_second; + else + frame_time_target = 0; } TbBool keeper_wait_for_screen_focus(void) @@ -3438,38 +3415,51 @@ static void update_gameplay_delta_time() const int64_t ns = now - prev; prev = now; - const long double dt = min(max(ns / 1e9L * turns_per_second, 0.L), 1.L); + const long double seconds = max(ns / 1e9L, 0.L); + const long double turns = min(seconds * turns_per_second, 1.L); - game.process_turn_time += dt * multiplayer_clock_adjust * max(game.frame_skip, 1); + game.process_turn_time += turns * multiplayer_clock_adjust * max(game.frame_skip, 1); // This sets game.delta_time, which is used to pace locally-displayed // things (eg. tooltip scroll speed). It should not be affected by // multiplayer clock adjustment or frameskip. - time_since_last_draw += dt; + time_since_last_draw += turns; + + // Like process_turn_time, but for the video frame rate. This uses the + // same time unit (fractional turns) for convenience. + process_frame_time += turns; } else { // Set to 1 so that these variables don't affect anything. (if something is multiplied by 1 it doesn't change) time_since_last_draw = 1; game.delta_time = 1; game.process_turn_time = 1; + process_frame_time = 1; } } static void gameplay_loop_draw() { + if (use_delta_time()) + do_draw = true; + update_gameplay_delta_time(); if (game.process_turn_time > 1.0 && time_since_last_draw < 1.0) do_draw = false; - if (is_feature_on(Ft_DeltaTime) && ! network_is_active()) + // Frame rate limiter + frametime_start_measurement(Frametime_Sleep); + if (process_frame_time < frame_time_target) { - frametime_start_measurement(Frametime_Sleep); - if (fps_limit_current > 0) - keeper_wait_for_next_draw(); - frametime_end_measurement(Frametime_Sleep); + SDL_Delay(1); + do_draw = false; } - - update_gameplay_delta_time(); + else + { + process_frame_time = min(process_frame_time - frame_time_target, + 2 * frame_time_target); + } + frametime_end_measurement(Frametime_Sleep); // Floats are used a lot in the drawing related functions. But keep in mind integers are typically preferred for logic related functions. frametime_start_measurement(Frametime_Draw); @@ -3646,7 +3636,6 @@ extern "C" void network_yield_draw_gameplay() extern "C" void network_yield_waiting_gameplay_packets() { - do_draw = true; poll_inputs(); gameplay_loop_draw(); update_gameplay_delta_time(); From 33da9122c78da876c256ebd213f3b9e6c3936cc7 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Wed, 8 Jul 2026 18:47:03 +0200 Subject: [PATCH 5/8] Use "frames" as time unit for process_frame_time That makes way more sense. I don't know what I was thinking. --- src/console_cmd.c | 1 - src/main.cpp | 34 +++++++++++++++------------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/console_cmd.c b/src/console_cmd.c index 50199f2e0e..21887cd9de 100644 --- a/src/console_cmd.c +++ b/src/console_cmd.c @@ -493,7 +493,6 @@ TbBool cmd_fps_turn(PlayerNumber plyr_idx, char * args) } else { turns_per_second = atoi(pr1str); } - redetect_screen_refresh_rate_for_draw(); return true; } diff --git a/src/main.cpp b/src/main.cpp index 5f4fc823be..6adbabc109 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -221,7 +221,6 @@ TbBool TimerFreeze = false; int32_t fps_limit_current = 0; int32_t fps_limit_main = 0; // -1 if auto int32_t fps_limit_secondary = 0; -static long double frame_time_target = 0; static long double process_frame_time = 0; static long double time_since_last_draw = 0; static long double average_frame_draw_time = 1; @@ -3365,11 +3364,6 @@ void redetect_screen_refresh_rate_for_draw() } else if (fps_limit_main > 0) { fps_limit_current = fps_limit_main; } - - if (fps_limit_current > 0) - frame_time_target = 1.L / fps_limit_current * turns_per_second; - else - frame_time_target = 0; } TbBool keeper_wait_for_screen_focus(void) @@ -3417,6 +3411,7 @@ static void update_gameplay_delta_time() const long double seconds = max(ns / 1e9L, 0.L); const long double turns = min(seconds * turns_per_second, 1.L); + const long double frames = seconds * fps_limit_current; game.process_turn_time += turns * multiplayer_clock_adjust * max(game.frame_skip, 1); @@ -3425,9 +3420,8 @@ static void update_gameplay_delta_time() // multiplayer clock adjustment or frameskip. time_since_last_draw += turns; - // Like process_turn_time, but for the video frame rate. This uses the - // same time unit (fractional turns) for convenience. - process_frame_time += turns; + // Like process_turn_time, but for the video frame rate. + process_frame_time += frames; } else { // Set to 1 so that these variables don't affect anything. (if something is multiplied by 1 it doesn't change) time_since_last_draw = 1; @@ -3448,18 +3442,20 @@ static void gameplay_loop_draw() do_draw = false; // Frame rate limiter - frametime_start_measurement(Frametime_Sleep); - if (process_frame_time < frame_time_target) - { - SDL_Delay(1); - do_draw = false; - } - else + if (fps_limit_current > 0) { - process_frame_time = min(process_frame_time - frame_time_target, - 2 * frame_time_target); + frametime_start_measurement(Frametime_Sleep); + if (process_frame_time < 1.0) + { + SDL_Delay(1); + do_draw = false; + } + else + { + process_frame_time = min(1.L, process_frame_time - 1.L); + } + frametime_end_measurement(Frametime_Sleep); } - frametime_end_measurement(Frametime_Sleep); // Floats are used a lot in the drawing related functions. But keep in mind integers are typically preferred for logic related functions. frametime_start_measurement(Frametime_Draw); From 374fadbe5b802ef6c7bbfcf07e8f4d05e6e3fe59 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Wed, 8 Jul 2026 18:50:43 +0200 Subject: [PATCH 6/8] Clamp game.delta_time in gameplay_loop_draw() --- src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 6adbabc109..3615501478 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3410,7 +3410,7 @@ static void update_gameplay_delta_time() prev = now; const long double seconds = max(ns / 1e9L, 0.L); - const long double turns = min(seconds * turns_per_second, 1.L); + const long double turns = seconds * turns_per_second; const long double frames = seconds * fps_limit_current; game.process_turn_time += turns * multiplayer_clock_adjust * max(game.frame_skip, 1); @@ -3469,7 +3469,7 @@ static void gameplay_loop_draw() if ( do_draw ) { if (frametime_enabled()) framerate_measurement_capture(Framerate_Draw); - game.delta_time = time_since_last_draw; + game.delta_time = min(time_since_last_draw, 1.L); time_since_last_draw = 0; interpolate_time = min(max(game.process_turn_time, 0.L), 1.L); keeper_screen_redraw(); From 0045a92dc4615eaef2af259daae7d62f0c343573 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Wed, 8 Jul 2026 19:16:50 +0200 Subject: [PATCH 7/8] Skip SDL_Delay() when logic update is due This allows higher frameskip values. --- src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 3615501478..5ce5446765 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3447,7 +3447,8 @@ static void gameplay_loop_draw() frametime_start_measurement(Frametime_Sleep); if (process_frame_time < 1.0) { - SDL_Delay(1); + if (game.process_turn_time < 1.0) + SDL_Delay(1); do_draw = false; } else From bb060aaf2f496509f62d7dc0ad6664ea1036e3f2 Mon Sep 17 00:00:00 2001 From: "J.W. Jagersma" Date: Wed, 8 Jul 2026 20:04:01 +0200 Subject: [PATCH 8/8] Reset turn timer after losing window focus --- src/main.cpp | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5ce5446765..fe5f277495 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3366,25 +3366,6 @@ void redetect_screen_refresh_rate_for_draw() } } -TbBool keeper_wait_for_screen_focus(void) -{ - do { - if ( !poll_inputs() ) - { - force_application_close(); - break; - } - if (LbIsActive()) - return true; - if (network_is_active()) - return true; - if (!freeze_game_on_focus_lost()) - return true; - LbSleepFor(50); - } while ((!exit_keeper) && (!quit_game)); - return false; -} - static bool use_delta_time() { // Always enable interpolation in multiplayer games. @@ -3431,6 +3412,28 @@ static void update_gameplay_delta_time() } } +static bool keeper_wait_for_screen_focus() +{ + do { + if ( !poll_inputs() ) + { + force_application_close(); + break; + } + if (LbIsActive()) + return true; + if (network_is_active()) + return true; + if (!freeze_game_on_focus_lost()) + return true; + LbSleepFor(50); + update_gameplay_delta_time(); + game.process_turn_time = 1.0; + time_since_last_draw = 1.0; + } while ((!exit_keeper) && (!quit_game)); + return false; +} + static void gameplay_loop_draw() { if (use_delta_time())