From 63c7d3cb741b178ee8deb59e45a99e88b81cc2f6 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Tue, 10 Nov 2026 11:48:24 -0500 Subject: [PATCH] Make the frame limit count the frames the game draws, with frame generation on top. The limit counted the frames on screen, so the game was paced at the limit over frame generation's multiplier: 60 at 4x drew 15 frames a second, and each step up of the multiplier took drawn frames away. Now a limit of 60 at 4x is 60 drawn and 240 shown. Only the display choice still counts the screen's frames, since a frame past the monitor's refresh is never seen, and with no limit set the default is 60 drawn rather than the monitor's rate. Co-Authored-By: Claude Fable 5.2 --- README.md | 4 +-- docs/README-BuildKit.md | 3 +- nx2navi.ini.example | 15 +++++---- runtime/frame_rate.c | 67 +++++++++++++++++----------------------- runtime/host.h | 3 +- runtime/ui/menu.cpp | 6 ++-- runtime/ui_text.c | 13 ++++---- tests/frame_step_check.c | 20 ++++++++++-- 8 files changed, 66 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 39abd0f3..9c417385 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md). GPUs (one, two or three generated frames for every frame the game draws), or AMD FSR 3.1 (any GPU), a generated frame between every two the game draws, with the HUD kept out of it, for about twice the frame rate on a fast display; in a window of another shape than the picture, frames are generated inside the picture only, so the black - bars stay clean. With frame generation on, the frame limit counts the frames on screen, and with no limit chosen - it is the display's refresh rate. The frame limit can also be + bars stay clean. The frame limit counts the frames the game draws, and frame generation adds its + frames on top of it. The frame limit can also be handed to RivaTuner Statistics Server - HDR on HDR displays: the game's own native HDR, or its SDR picture tone mapped to the display's peak, with exposure, highlights, shadows, contrast, saturation and gamma in the menu - The game's known shaders prepared when the game is built, and the pipelines met in play built again in the diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index ad573506..5f29d7c6 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -153,8 +153,7 @@ beside the game. cards; Automatic picks by the card), Upscaling (DLSS on NVIDIA RTX cards, FSR 3.1 on any card where `amd_fidelityfx_vk.dll` loads), Frame generation (NVIDIA DLSS on RTX 40 and 50 cards, with a DLSS multiplier of 2x, or 3x and 4x on RTX 50 cards; FSR 3.1 on any card where it loads; it needs DLSS or FSR 3.1 - upscaling on, and the frame limit then counts the frames on screen; with no limit chosen it is the display's refresh - rate), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the + upscaling on; the frame limit counts the frames the game draws and the generated ones come on top), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the resolution. A setting that takes effect only when the game starts again says "after restart" once you change it, until you set it back, and the Restart button at the bottom then counts the settings waiting for it. - **Controls** in the menu has Button prompts: the game's own, or Xbox, PlayStation, Steam Deck or keyboard and mouse ones when diff --git a/nx2navi.ini.example b/nx2navi.ini.example index f0b1fc0b..2ab095cc 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -15,9 +15,9 @@ # 36 do, while 60 does not. vk_present = immediate shows a frame as soon as it is presented, without this quantisation, # at the cost of tearing. A monitor running with variable refresh (FreeSync, G-Sync) follows the frames instead, so # there any limit inside its range is even. -# With frame generation on (framegen, [display]) the limit counts the frames on screen, generated ones included: the -# game draws the limit divided by the multiplier. Unset, the limit is then the monitor's refresh rate instead of 60, so -# at 4x on a 165 Hz monitor the game draws about 41 fps and the screen shows 165; a limit set here is kept as it is. +# With frame generation on (framegen, [display]) the limit still counts the frames the game draws, and the generated +# frames come on top: 60 at 4x is 60 drawn and 240 on screen. Only display counts the frames on screen, so there the +# game draws the monitor's rate divided by the multiplier. # fps_limit = 60 # Where RivaTuner Statistics Server is installed and running, the limit goes into its profile for the game's # executable and RTSS does the limiting instead of the game; closing the game puts back what the profile held. @@ -406,20 +406,19 @@ # frame between every two the game draws, the HUD kept out of it) or dlss (NVIDIA DLSS Frame Generation on GeForce RTX # 40 and 50 GPUs, paced with Reflex; it needs nx2navi_dlssg.dll and NVIDIA Streamline's files next to the executable, # which the setup puts there). Both need DLSS or FSR 3.1 upscaling (dlaa = 1). While frame generation runs, fps_limit -# counts the frames on screen, generated ones included: the game draws the limit divided by the multiplier (2 for fsr3). -# With fps_limit unset it is the display's rate. +# counts the frames the game draws and the screen shows that times the multiplier (2 for fsr3); fps_limit = display +# alone holds the screen to the monitor's rate and the game to that rate divided by the multiplier. # fsr3 takes effect at once, dlss (on or off) at the next start. # framegen = off # On Linux dlss runs through NVIDIA's NGX itself, without Streamline: it needs dlaa = 1 with DLSS when the game starts # and NVIDIA's libnvidia-ngx-dlssg.so. (from NVIDIA's DLSS SDK repository) beside the game, then switches at # once, and its multiplier is fg_mult: 2 to 6 (above 2 needs an RTX 50 GPU) or dynamic, which generates only the frames -# needed to reach the display's refresh rate at the rate the game draws. There fps_limit counts the frames the game -# draws, and framegen_multiplier is not used. +# needed to reach the display's refresh rate at the rate the game draws. framegen_multiplier is not used there. # fg_mult = 2 # DLSS frame generation's multiplier: the frames on screen for each frame the game draws, 2, 3 or 4 (one, two or three # generated). 2 needs an RTX 40 GPU or newer, 3 and 4 an RTX 50 and a current driver. A value above what the GPU and # driver allow (Streamline reports it) is lowered to that, with a line in the log; the menu offers only what they allow. -# Each step adds a little latency, and on a 60 Hz display 4 leaves the game drawing 15 fps. Changes while the game runs +# Each step adds a little latency, and a display shows no more than its refresh rate. Changes while the game runs # take effect at once. # framegen_multiplier = 2 diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 25838b33..e7430c9d 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -115,8 +115,7 @@ int frame_step_realtime(void) { return step_word != NULL; } // The frame limit (menu, NX2NAVI_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the // refresh rate of the window's monitor, at most 240). A higher number gives 240, the rate of the shortest step the -// game survives (STEP_MIN_S). Unset, the older NX2NAVI_FPS60=0 still means 30, and frame generation in use means display -// (frame_limit_follows_display). A limit of 30 switches the set +// game survives (STEP_MIN_S). Unset, the older NX2NAVI_FPS60=0 still means 30. A limit of 30 switches the set // fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum // interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60 // only the pacer changes, as the step's floor is the limit's ceiling. Without it the game steps whole 60 Hz vsyncs and the pacer @@ -144,11 +143,9 @@ int frame_limit_adjustable(void) { return 1; } static unsigned limit_seen; static int limit_read; static int limit_hz = -1; // 0: display; -2: unlimited (free-running builds); -1 before the first read -static int limit_unset; // neither NX2NAVI_FPS_LIMIT nor the older NX2NAVI_FPS60=0 is set: the default 60 static int limit_parse(void) { const char *e = cfg_get("NX2NAVI_FPS_LIMIT"), *old = cfg_get("NX2NAVI_FPS60"); - limit_unset = (!e || !*e) && !(old && !strcmp(old, "0")); if (!e || !*e) return old && !strcmp(old, "0") ? 30 : 60; if (!strcmp(e, "display")) return 0; int free_run = frame_free_running(); @@ -215,22 +212,25 @@ static int movie_cap_hz(void) { // multiplier with NVIDIA's (vk_dlssg.c, one to three); 1 while neither generates (off, or paused for the menu). static int generated_factor(void) { return vk_fg_generating() ? 2 : vk_dlssg_multiplier(); } -// The limit in effect, as limit_hz counts it (0 display, -2 unlimited, else the rate). With frame generation's -// swapchain in use and no limit chosen it is the display's rate: the pacer divides the limit by generated_factor, so -// the default 60 held the game to 30, 20 or 15 rendered frames a second on any monitor, 15 at DLSS 4x on a 165 Hz one, -// where the display rate gives 41 rendered and 165 shown. "In use" rather than generating keeps the limit still while -// the menu pauses generation. A limit the player set is kept as it is. -int frame_limit_follows_display(void) { return limit_unset && limit_hz > 0 && (vk_fg_active() || vk_dlssg_active()); } -static int limit_effective(void) { return frame_limit_follows_display() ? 0 : limit_hz; } - -// The rate the game renders at under the limit: frame_step_limit_hz over the frames shown per game frame (mouse_direct.c, -// whose camera input follows the game's own frame time); 0 unknown. -double frame_step_game_hz(void) { - double hz = frame_step_limit_hz(); - int gen = generated_factor(); +// The limit counts the frames the game draws, and frame generation adds its frames on top: a limit of 60 at 4x is 60 +// drawn and 240 shown. Counted on the screen instead, the game was paced at the limit over the factor, so each step up +// of the multiplier took drawn frames away (60 at 4x drew 15) and the generated ones had less to work from and more +// delay. Only "display" is a rate on the screen, the monitor's: there the game draws that rate over the factor, since a +// frame past the monitor's refresh is never seen. The result is game frames a second, before the ceiling of the step. +static double limit_game_hz(int limit, int gen) { + if (limit) return shown_hz(limit); + double hz = shown_hz(gfx_window_refresh_hz()); return gen > 1 ? hz / gen : hz; } +// The rate the game renders at under the limit (mouse_direct.c, whose camera input follows the game's own frame time); +// 0 unknown or unlimited. +double frame_step_game_hz(void) { + if (frame_step_limit_hz() <= 0) return 0; + double hz = limit_game_hz(limit_hz, generated_factor()); + return !frame_free_running() && !step_word && hz > 60 ? 60 : hz > LIMIT_MAX_HZ ? LIMIT_MAX_HZ : hz; +} + // RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds no // frames, so the two never stack. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it out. static double paced_hz; // the game frames a second the newest frame_step_cap_ns paces at (ocarina_step); 0: none @@ -257,26 +257,17 @@ uint64_t frame_step_cap_ns(void) { paced_hz = 0; return 0; } - // unlimited in a build that steps by the clock: LIMIT_FREE_MAX_HZ game frames; during a movie, the movie's rate. - // Both times frame generation's factor for the division below. - int follow = frame_limit_follows_display(), limit = follow ? 0 : limit_hz; - static int follow_was, follow_logs; - if (follow != follow_was) { - follow_was = follow; - if (follow_logs++ < 20) { - if (follow) rt_log("frame rate: frame generation with no limit set: the limit is the monitor's refresh rate"); - else rt_log("frame rate: limit %d fps again", limit_hz); - } - } - double hz = limit == -2 ? (double)(movie_hz ? movie_hz : LIMIT_FREE_MAX_HZ) * gen - : shown_hz(limit ? limit : gfx_window_refresh_hz()); + // unlimited in a build that steps by the clock: LIMIT_FREE_MAX_HZ game frames; during a movie, the movie's rate + double hz = limit_hz == -2 ? LIMIT_FREE_MAX_HZ : limit_game_hz(limit_hz, gen); double max = frame_free_running() ? LIMIT_FREE_MAX_HZ : LIMIT_MAX_HZ; - if (limit == -2) max *= gen; // the cap is on the game's frames there, not the ones shown if (hz > max) hz = max; - // Frame generation presents one to three generated frames between each two of the game's: the game is paced at the - // limit over that factor (half, a third, a quarter), so the screen shows the limit. With VSync and the limit at the - // display's rate, the generated frames then fill the display's refreshes and none is dropped. - if (gen > 1) hz /= gen; + static double logged_hz; + static int logged_gen, gen_logs; + if (gen > 1 && (hz != logged_hz || gen != logged_gen) && gen_logs < 40) { + gen_logs++, logged_hz = hz, logged_gen = gen; + rt_log("frame rate: the game draws up to %.0f frames a second and frame generation shows %d for each (%.0f on " + "screen)", hz, gen, hz * gen); + } if (movie_hz) hz = movie_hz; paced_hz = hz; rtss_limit_want(rtss ? (int64_t)(hz * 1000 + 0.5) : -1); @@ -284,17 +275,17 @@ uint64_t frame_step_cap_ns(void) { return (uint64_t)(1e9 / hz + 0.5); } -// The frame rate the limit gives, for the performance overlay: the setting, the monitor's rate for "display", 60 at +// The limit as it is set, for the performance overlay: the setting, the monitor's rate for "display", 60 at // most without the real-time step; the game's own rate (60 Hz over its present interval) in a build without the patch // sets; 0 before the first frame read the setting. double frame_step_limit_hz(void) { if (frame_free_running()) { // 0 for unlimited, as before the first read frame_limit_live(); - int limit = limit_effective(); + int limit = limit_hz; if (limit == -2 || limit == -1) return 0; return shown_hz(limit ? limit : gfx_window_refresh_hz()); } - int hz = limit_effective(); + int hz = limit_hz; if (hz < 0) return 0; double r = shown_hz(hz ? hz : gfx_window_refresh_hz()); return !step_word && r > 60 ? 60 : r > LIMIT_MAX_HZ ? LIMIT_MAX_HZ : r; diff --git a/runtime/host.h b/runtime/host.h index 9f9137cb..e77c8a6f 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -336,8 +336,7 @@ int ov_movie_live(void); // ov_movie2.c: a mov int frame_limit_adjustable(void); // the frame limit setting changes the rate (always) int frame_free_running(void); // no fps60/realtime patch set: the game steps by the clock double frame_step_limit_hz(void); // the frame rate the limit gives (overlay); 0 unknown -double frame_step_game_hz(void); // ... over frame generation's factor: the game's rate -int frame_limit_follows_display(void); // no limit set and frame generation in use: the display's rate +double frame_step_game_hz(void); // the rate the game draws at under the limit; 0 unknown // rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none, -1 keep RTSS out int rtss_limit_active(void); // RTSS limits to the wanted rate: the pacer holds no frames diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 65c25399..ff048e40 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -73,7 +73,6 @@ int vk_ffx_available(void); // vk_ffx.cpp: FSR 3.1 upscaling can run int vk_ffx_framegen_available(void); // ... and FSR 3.1 frame generation int vk_texture_detail_auto(void); // vk_texture.c: the texture detail auto picks on this card, 0 full to 2 low double frame_step_limit_hz(void); -int frame_limit_follows_display(void); int frame_limit_adjustable(void); int rtss_limit_state(void); int frame_free_running(void); @@ -756,8 +755,7 @@ static void row_game_lang(void) { opt_choice_list("NX2NAVI_LANG", UI_MENU_GAME_LANG, vals, names, n + 1, UI_MENU_GAME_LANG_DESC, cur); } -// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30, and frame generation in use means the -// monitor's rate (frame_limit_follows_display), which the row then shows. The monitor choice shows the rate it gives. +// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives. // The note says when RivaTuner Statistics Server does the limiting (rtss_limit.c), cannot change its profile, or let the // game run past its limit. static void row_fps_limit() { @@ -778,7 +776,7 @@ static void row_fps_limit() { names[9] = display; } const char *set = cfg_get("NX2NAVI_FPS_LIMIT"), *old = cfg_get("NX2NAVI_FPS60"); - int cur = (!set || !*set) && old && !strcmp(old, "0") ? 0 : frame_limit_follows_display() ? 9 : -1; + int cur = (!set || !*set) && old && !strcmp(old, "0") ? 0 : -1; int rtss = rtss_limit_state(); const char *note = rtss == 1 ? menu_text(UI_MENU_FPS_RTSS) : rtss == 2 ? menu_text(UI_MENU_FPS_RTSS_READONLY) diff --git a/runtime/ui_text.c b/runtime/ui_text.c index d68515f2..faaa02d3 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -179,7 +179,7 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_FXAA_DESC] = "The game's own FXAA edge smoothing. It blurs fine detail a little; with DLAA on it is not needed.", [UI_MENU_FPS_LIMIT] = "Frame rate limit", - [UI_MENU_FPS_LIMIT_DESC] = "The highest frame rate the game runs at.", + [UI_MENU_FPS_LIMIT_DESC] = "The highest frame rate the game draws at. Frame generation adds its frames on top.", [UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS", // the monitor's refresh rate; printf format with the rate in Hz when it is known @@ -575,9 +575,8 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_FG_MULT_DYNAMIC] = "Dynamic", [UI_MENU_FRAMEGEN_MULT_DESC] = "Frames on screen for each frame the game draws: 2x adds one generated frame, 3x " "two, 4x three. Higher is smoother but adds a little delay. The frame limit counts " - "the frames on screen, so the game draws the limit divided by this: set the limit " - "to your display's rate. 2x needs a GeForce RTX 40 GPU or newer, 3x and 4x an RTX " - "50.", + "the frames the game draws, so the screen shows the limit times this. 2x needs a " + "GeForce RTX 40 GPU or newer, 3x and 4x an RTX 50.", [UI_MENU_FRAMEGEN_MULT_RTX50] = "3x and 4x need an RTX 50 GPU", [UI_MENU_FRAMEGEN_MULT_UPTO] = "up to %dx on this GPU and driver", [UI_MENU_FRAMEGEN_MULT_OLD_DLL] = "2x only: update the DLSS frame generation files", @@ -744,7 +743,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = { [UI_MENU_FXAA_DESC] = "Собственное сглаживание FXAA. Слегка размывает мелкие детали; с DLAA оно не нужно.", [UI_MENU_FPS_LIMIT] = "Ограничение частоты кадров", - [UI_MENU_FPS_LIMIT_DESC] = "Максимальная частота кадров игры.", + [UI_MENU_FPS_LIMIT_DESC] = "Максимальная частота кадров, которые рисует игра. Генерация кадров добавляет свои сверх неё.", [UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS", [UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)", @@ -1202,7 +1201,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = { [UI_MENU_SHARPNESS_NEEDS_FSR] = "só com AMD FSR 1", [UI_MENU_FPS_LIMIT] = "Limite de quadros", - [UI_MENU_FPS_LIMIT_DESC] = "A taxa de quadros máxima do jogo.", + [UI_MENU_FPS_LIMIT_DESC] = "A taxa máxima de quadros que o jogo desenha. A geração de quadros soma os dela por cima.", [UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS", [UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS", [UI_MENU_ASSUME_FOCUS] = "Continuar rodando fora de foco", @@ -1781,7 +1780,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = { [UI_MENU_COLOR_GAMMA] = "伽马", [UI_MENU_COLOR_GAMMA_DESC] = "小于 1 提亮暗部与中间调,大于 1 压暗;1 表示不调整。", [UI_MENU_FPS_LIMIT] = "帧率上限", - [UI_MENU_FPS_LIMIT_DESC] = "游戏的最高帧率。", + [UI_MENU_FPS_LIMIT_DESC] = "游戏绘制的最高帧率。帧生成的帧在此之上另算。", [UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS", diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index fa6e503e..7628ec1a 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -32,8 +32,9 @@ int ov_movie_live(void) { return 0; } int vk_fg_generating(void) { return 0; } int vk_fg_active(void) { return 0; } int vk_fg_wanted(void) { return 0; } -int vk_dlssg_multiplier(void) { return 1; } -int vk_dlssg_active(void) { return 0; } +static int fg_mult = 1; // frames on screen per game frame +int vk_dlssg_multiplier(void) { return fg_mult; } +int vk_dlssg_active(void) { return fg_mult > 1; } void rtss_limit_want(int64_t millihz) { (void)millihz; } int rtss_limit_active(void) { return 0; } int plat_mem_read(uint64_t a, void *out, size_t n) { (void)a, (void)out, (void)n; return 0; } @@ -135,6 +136,21 @@ int main(int argc, char **argv) { float g = present(0.0011); // 900 fps, as a quick menu without the pacer would run CHECK(g >= 0.125f, "short frames: %.4f frames", g); } + // The limit counts the frames the game draws: frame generation's multiplier leaves the pace of a numeric limit and + // of unlimited alone, and divides only the monitor's rate (144 Hz here), which is a rate on the screen. + static const struct { const char *l; int gen; double hz, game_hz; } caps[] = { + {"60", 1, 60, 60}, {"60", 4, 60, 60}, {"120", 2, 120, 120}, {"240", 4, 240, 240}, + {"display", 1, 144, 144}, {"display", 4, 36, 36}, {"unlimited", 1, 240, 0}, {"unlimited", 4, 240, 0}}; + for (unsigned i = 0; i < sizeof caps / sizeof *caps; i++) { + set_limit(caps[i].l); + fg_mult = caps[i].gen; + double hz = 1e9 / (double)frame_step_cap_ns(), game = frame_step_game_hz(); + CHECK(fabs(hz - caps[i].hz) < 0.01, "limit %s at %dx paces the game at %.3f fps, not %.0f", caps[i].l, caps[i].gen, + hz, caps[i].hz); + CHECK(fabs(game - caps[i].game_hz) < 0.01, "limit %s at %dx reports %.3f game fps, not %.0f", caps[i].l, + caps[i].gen, game, caps[i].game_hz); + } + fg_mult = 1; printf("frame_step_check: %s\n", fails ? "FAILED" : "ok"); return fails != 0; }