From 1ec7528dac4c8e3d40432785c0608a38b4b38079 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:35:07 -0700 Subject: [PATCH] gitadora: option to disable frame limiter (#848) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change There is a patch for GITADORA series that fixes frame pacing on modern Windows. Turn it into a signature patch and embed it into the game. ## Testing should work for all 64-bit gitadora versions, pre-GWD --- src/spice2x/games/gitadora/gitadora.cpp | 47 +++++++++++++++++++++++++ src/spice2x/games/gitadora/gitadora.h | 1 + src/spice2x/launcher/launcher.cpp | 3 ++ src/spice2x/launcher/options.cpp | 13 +++++++ src/spice2x/launcher/options.h | 1 + 5 files changed, 65 insertions(+) diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index 1758044..e7f8b8d 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -31,6 +31,7 @@ namespace games::gitadora { // settings bool TWOCHANNEL = false; + bool DISABLE_FRAME_LIMITER = false; std::optional CAB_TYPE = std::nullopt; bool P1_LEFTY = false; bool P2_LEFTY = false; @@ -65,6 +66,46 @@ namespace games::gitadora { return CreateDirectoryA(lpPathName, lpSecurityAttributes); } + + // libshare-pj paces mainloop with separate 12 ms and 16 ms waits. these waits + // interfere with the game's normal display synchronization on modern Windows + // and can hold a nominal 60 FPS game near 58 FPS. locate the instruction + // sequences at runtime so the fix does not depend on per-version file offsets. + static void disable_mainloop_frame_limiter(HMODULE sharepj_module) { + if (!sharepj_module) { + return; + } + + // both limiters have the same shape, only the millisecond target xx differs + // (0Ch for the 12 ms limiter, 10h for the 16 ms one): + // + // 48 83 F8 xx: cmp rax, xx; compare elapsed frame time with the target + // 73 10: jae +10h; skip the wait once the target has elapsed + // B9 xx 00 00 00: mov ecx, xx; load the target + // 48 2B C8: sub rcx, rax; calculate the remaining wait time + // 74 06: je +6h; skip the following six-byte Sleep call if no wait remains + // + // changing jae (73h) to jmp (EBh) makes each block always take its existing + // skip path, which bypasses only the associated Sleep call and leaves the 10h + // branch displacement and every other wait untouched. + const auto limiter_12ms_disabled = replace_pattern( + sharepj_module, + "4883F80C7310B90C000000482BC87406", + "????????EB??????????????????????", 0, 0); + const auto limiter_16ms_disabled = replace_pattern( + sharepj_module, + "4883F8107310B910000000482BC87406", + "????????EB??????????????????????", 0, 0); + + if (!limiter_12ms_disabled || !limiter_16ms_disabled) { + log_fatal( + "gitadora", + "failed to disable libshare-pj mainloop frame limiter (-gdnoframelimiter), ensure patch is not already applied"); + return; + } + + log_info("gitadora", "successfully disabled libshare-pj mainloop frame limiter (-gdnoframelimiter)"); + } #endif /* @@ -687,6 +728,12 @@ namespace games::gitadora { HMODULE system_module = libutils::try_module("libsystem.dll"); // patches +#ifdef SPICE64 + if (DISABLE_FRAME_LIMITER && !is_arena_model()) { + disable_mainloop_frame_limiter(sharepj_module); + } +#endif + detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc( sharepj_module, "eam_network_detected_ip_change")); detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc( diff --git a/src/spice2x/games/gitadora/gitadora.h b/src/spice2x/games/gitadora/gitadora.h index 8e4120a..7851949 100644 --- a/src/spice2x/games/gitadora/gitadora.h +++ b/src/spice2x/games/gitadora/gitadora.h @@ -13,6 +13,7 @@ namespace games::gitadora { // settings extern bool TWOCHANNEL; + extern bool DISABLE_FRAME_LIMITER; extern std::optional CAB_TYPE; extern bool P1_LEFTY; extern bool P2_LEFTY; diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 03981bf..40c221d 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -644,6 +644,9 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::LoadGitaDoraModule].value_bool()) { attach_gitadora = true; } + if (options[launcher::Options::GitaDoraDisableFrameLimiter].value_bool()) { + games::gitadora::DISABLE_FRAME_LIMITER = true; + } if (options[launcher::Options::GitaDoraCabinetType].is_active()) { games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32(); } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 6b155f6..c140793 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1202,6 +1202,19 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .quick_setting_category = "Game", }, + { + .title = "GitaDora Disable Frame Limiter (EXPERIMENTAL)", + .name = "gdnoframelimiter", + .desc = "Only for GITADORA to GALAXY WAVE; not for XG series or Arena Model.\n\n" + "Game engine's built-in frame limiter can result in unstable frame pacing " + "(around 56-58 FPS) on modern Windows. Turning this on disables that and " + "forces the game to rely on V-Sync instead, resulting in a stable FPS.\n\n" + "Ensure your monitors are set to 60Hz.", + .type = OptionType::Bool, + .game_name = "GitaDora", + .category = "Game Options", + .quick_setting_category = "Game", + }, { .title = "GitaDora Cabinet Type", .name = "gdcabtype", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index ef912a5..7737fec 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -114,6 +114,7 @@ namespace launcher { LoadHelloPopnMusicModule, LoadGitaDoraModule, GitaDoraTwoChannelAudio, + GitaDoraDisableFrameLimiter, GitaDoraCabinetType, GitaDoraLefty, GitaDoraWailHold,