From 3b8b98fa29a0fa54be451041b0bf9d59f08ed4a3 Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:06:16 -0400 Subject: [PATCH] runtime: fast-forward toggle shortcut + document the implicit Select chord Turbo (Tab / [hotkeys] fast_forward_pad) is hold-to-run only. Add a press-to-latch twin that feeds the same manual fast-forward block: - host_keymap: HOST_KEYMAP_TURBO_TOGGLE, [KeyMap] TurboToggle, default F9. - main.cpp: g_manual_turbo_latched flipped by the key edge or by a fourth host shortcut, PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE ("Fast-forward toggle", [hotkeys] fast_forward_toggle_pad, unbound by default), polled next to Rewind / Save states through hotkey_pad_binding_down(). OSD shows "Fast forward: Nx (locked)" / "Fast forward: off". Threaded through every settings hop (settings.toml -> globals -> launcher seed -> LauncherSettings -> back), mirroring fast_forward_pad. - config_loader: [hotkeys] fast_forward_toggle_pad read/write. - README: describe the toggle and state that a single-button / trigger host shortcut is an implicit chord with Select (hotkey_pad_binding_down), which the launcher now labels as "select + ..." (recomp-ui companion change). - test_rewind_toggle_combo.py guards the new sites. Co-Authored-By: Claude Fable 5.1 --- README.md | 7 ++- recompiler/src/config_loader.cpp | 12 +++- recompiler/src/config_loader.h | 1 + runtime/include/host_keymap.h | 1 + runtime/src/host_keymap.c | 3 + runtime/src/main.cpp | 68 ++++++++++++++++++++++- runtime/tests/test_rewind_toggle_combo.py | 23 +++++++- 7 files changed, 109 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 60c62e459..9ec71a2e0 100644 --- a/README.md +++ b/README.md @@ -716,7 +716,12 @@ acceleration is **opt-in**, per game, so the accurate path is never compromised: - **Turbo** — a hold-to-fast-forward key that compresses loads on demand (keyboard `[KeyMap] Turbo`, default Tab; controller `[hotkeys] fast_forward_pad`, default Select+L1, rebindable under the launcher's - Controller → Host Shortcuts alongside Rewind and Save states). + Controller → Host Shortcuts alongside Rewind and Save states). A + press-to-latch twin, **Turbo toggle** (`[KeyMap] TurboToggle`, default F9; + `[hotkeys] fast_forward_toggle_pad`, unbound by default), locks the same + speed until pressed again. Controller host shortcuts bound to a single + button or trigger are chords with Select — the launcher shows them as + `select + …`; only a two-button capture replaces the implicit Select. - **The "Fast Loading (host pacing)" and "CD Speed" mods** — automatic acceleration during load waits, shipped with every title and **off by default**, with `turbo_audio_sink` keeping the SPU timeline coherent through diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 669e6d4ea..6e7e21ec3 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -2436,6 +2436,13 @@ UserSettings load_user_settings(const fs::path& path) { s.has_hotkey_pad_fast_forward = true; } }); + if (h.contains("fast_forward_toggle_pad")) try_get([&]{ + const auto n = toml::find(h, "fast_forward_toggle_pad"); + if (pad_bind_value_ok(n)) { + s.hotkey_pad_fast_forward_toggle = (int)n; + s.has_hotkey_pad_fast_forward_toggle = true; + } + }); } if (doc.contains("launcher")) { const toml::value& l = toml::find(doc, "launcher"); @@ -2680,7 +2687,7 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { if (s.has_spu_hq) f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n"; if (s.has_hotkey_pad_rewind || s.has_hotkey_pad_save_state_menu || - s.has_hotkey_pad_fast_forward) { + s.has_hotkey_pad_fast_forward || s.has_hotkey_pad_fast_forward_toggle) { f << "\n[hotkeys]\n"; if (s.has_hotkey_pad_rewind) f << "rewind_pad = " << s.hotkey_pad_rewind << "\n"; @@ -2689,6 +2696,9 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { << s.hotkey_pad_save_state_menu << "\n"; if (s.has_hotkey_pad_fast_forward) f << "fast_forward_pad = " << s.hotkey_pad_fast_forward << "\n"; + if (s.has_hotkey_pad_fast_forward_toggle) + f << "fast_forward_toggle_pad = " + << s.hotkey_pad_fast_forward_toggle << "\n"; } if (s.has_skip_launcher) f << "\n[launcher]\nskip_launcher = " << (s.skip_launcher ? "true" : "false") << "\n"; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index dc0c4e3e4..766fcb202 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -1265,6 +1265,7 @@ struct UserSettings { // fast_forward_pad: hold-to-fast-forward, the controller twin of the // keyboard [KeyMap] Turbo (Tab). 0 = unbound. bool has_hotkey_pad_fast_forward = false; int hotkey_pad_fast_forward = 1528; /* select+l1 */ + bool has_hotkey_pad_fast_forward_toggle = false; int hotkey_pad_fast_forward_toggle = 0; /* unbound */ // [audio] bool has_spu_hq = false; bool spu_hq = false; bool has_audio_freq = false; int audio_freq = 44100; diff --git a/runtime/include/host_keymap.h b/runtime/include/host_keymap.h index c11862d2e..32aa0b3a2 100644 --- a/runtime/include/host_keymap.h +++ b/runtime/include/host_keymap.h @@ -22,6 +22,7 @@ typedef enum HostKeymapAction { HOST_KEYMAP_REWIND, /* default F8 */ HOST_KEYMAP_SAVE_STATE_MENU, /* default F7 */ HOST_KEYMAP_SCANLINES, /* default F6 */ + HOST_KEYMAP_TURBO_TOGGLE, /* default F9; latches Turbo until pressed again */ HOST_KEYMAP_ACTION_COUNT } HostKeymapAction; diff --git a/runtime/src/host_keymap.c b/runtime/src/host_keymap.c index 848309e20..36997456c 100644 --- a/runtime/src/host_keymap.c +++ b/runtime/src/host_keymap.c @@ -99,6 +99,8 @@ static void apply_defaults(void) { add_bind(HOST_KEYMAP_SAVE_STATE_MENU, (int)SDLK_F7, (int)SDL_SCANCODE_F7, 0); if (s_actions[HOST_KEYMAP_SCANLINES].count == 0) add_bind(HOST_KEYMAP_SCANLINES, (int)SDLK_F6, (int)SDL_SCANCODE_F6, 0); + if (s_actions[HOST_KEYMAP_TURBO_TOGGLE].count == 0) + add_bind(HOST_KEYMAP_TURBO_TOGGLE, (int)SDLK_F9, (int)SDL_SCANCODE_F9, 0); } /* Parse one "Ctrl+Alt+PageUp" token into key+mods. */ @@ -157,6 +159,7 @@ static HostKeymapAction action_for_key(const char *name) { if (ieq(name, "Rewind")) return HOST_KEYMAP_REWIND; if (ieq(name, "SaveStateMenu")) return HOST_KEYMAP_SAVE_STATE_MENU; if (ieq(name, "Scanlines")) return HOST_KEYMAP_SCANLINES; + if (ieq(name, "TurboToggle")) return HOST_KEYMAP_TURBO_TOGGLE; return HOST_KEYMAP_ACTION_COUNT; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 022b1baa7..687af2ec6 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1218,6 +1218,7 @@ static int g_rewind_interval = 15; /* frames between snaps (1/4/8/12/1 static int g_hotkey_pad_rewind = 1272; /* select + r3 */ static int g_hotkey_pad_save_state_menu = 2040;/* select + r1 */ static int g_hotkey_pad_fast_forward = 1528; /* select + l1 (hold) */ +static int g_hotkey_pad_fast_forward_toggle = 0; /* unbound: latch fast-forward */ static uint32_t g_savestate_input_guard_min_until = 0; static uint32_t g_savestate_input_guard_max_until = 0; static int g_headless = 0; /* debug/CI frontend: no SDL window/audio */ @@ -5970,6 +5971,7 @@ enum { PSX_ASSIST_BIND_REWIND = 0, PSX_ASSIST_BIND_SAVE_STATE_MENU, PSX_ASSIST_BIND_FAST_FORWARD, /* hold-to-fast-forward; pad twin of [KeyMap] Turbo */ + PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE, /* press-to-latch; pad twin of [KeyMap] TurboToggle */ PSX_ASSIST_BIND_COUNT }; @@ -6241,6 +6243,35 @@ static void savestate_menu_poll_toggle_buttons(void) { was_down = down; } +/* Latched fast-forward: [KeyMap] TurboToggle (default F9) or the [hotkeys] + * fast_forward_toggle_pad shortcut flips this; while set, the manual + * fast-forward block runs exactly as if Turbo were held. Cleared by the next + * press, so a hold-to-run Turbo release never cancels a latched run. */ +static int g_manual_turbo_latched = 0; + +static void fast_forward_toggle_flip(void) { + char msg[40]; + g_manual_turbo_latched = !g_manual_turbo_latched; + if (!g_manual_turbo_latched) { + host_osd_push("Fast forward: off", 900); + return; + } + const int mult = manual_fast_forward_multiplier(); + if (mult < 0) + snprintf(msg, sizeof(msg), "Fast forward: max (locked)"); + else + snprintf(msg, sizeof(msg), "Fast forward: %dx (locked)", mult); + host_osd_push(msg, 900); +} + +static void fast_forward_toggle_poll_buttons(void) { + static int was_down; + int down = hotkey_pad_binding_down(g_hotkey_pad_fast_forward_toggle); + if (down && !was_down) + fast_forward_toggle_flip(); + was_down = down; +} + static void rewind_poll_nav(uint32_t now_ms) { const Uint8 *keys = SDL_GetKeyboardState(NULL); int left = keys[SDL_SCANCODE_LEFT] ? 1 : 0; @@ -6609,6 +6640,12 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { debug_force_cd_reinsert(); host_osd_push("CD reinsert", 1500); } + else if (!key_repeat && + host_keymap_match_event(HOST_KEYMAP_TURBO_TOGGLE, + (int)key, (int)scancode, + (int)mod)) { + fast_forward_toggle_flip(); + } else if (!key_repeat && host_keymap_match_event(HOST_KEYMAP_DISPLAY_PERF, (int)key, (int)scancode, @@ -6668,6 +6705,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { } savestate_menu_poll_toggle_buttons(); rewind_poll_toggle_buttons(); + fast_forward_toggle_poll_buttons(); psx_rewind_note_frame(); psx_rewind_present_tick((uint32_t)SDL_GetTicks()); if (savestate_menu_open) @@ -6867,14 +6905,17 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { /* Keyboard ([KeyMap] Turbo, default Tab) or the controller host * shortcut ([hotkeys] fast_forward_pad, default select+L1). Both are * hold-to-run; the pad chord goes through the same combo matcher as - * Rewind / Save states so the launcher's binding editor covers it. */ + * Rewind / Save states so the launcher's binding editor covers it. + * g_manual_turbo_latched is the press-to-lock twin (TurboToggle / + * fast_forward_toggle_pad) and drives the same path. */ const bool kb_turbo = host_hotkey_input_focused() && host_keymap_down(HOST_KEYMAP_TURBO, keys, (int)SDL_GetModState()); - if (kb_turbo || hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) { + if (kb_turbo || g_manual_turbo_latched || + hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) { const int mult = manual_fast_forward_multiplier(); const int present_every = (mult < 0) ? 4 : (mult <= 4 ? 2 : 4); manual_turbo_active = true; - if (!turbo_was_down) { + if (!turbo_was_down && !g_manual_turbo_latched) { char msg[40]; if (mult < 0) snprintf(msg, sizeof(msg), "Fast forward: max"); @@ -10861,6 +10902,7 @@ namespace { "Rewind", "Save states", "Fast-forward", + "Fast-forward toggle", }; void ae_rui_set_sidecar_paths(const char* argv0) { @@ -11793,6 +11835,9 @@ int main(int argc, char** argv) { g_hotkey_pad_fast_forward = normalize_hotkey_pad_binding( us.hotkey_pad_fast_forward, PSX_HOTKEY_PAD_SELECT_L1); + if (us.has_hotkey_pad_fast_forward_toggle) + g_hotkey_pad_fast_forward_toggle = normalize_hotkey_pad_binding( + us.hotkey_pad_fast_forward_toggle, 0); if (us.has_bios_path && !bios_from_cli && !us.bios_path.empty()) { settings_bios_storage = us.bios_path.string(); bios_path = settings_bios_storage.c_str(); @@ -12309,6 +12354,8 @@ int main(int argc, char** argv) { seed.has_hotkey_pad_save_state_menu = true; seed.hotkey_pad_fast_forward = g_hotkey_pad_fast_forward; seed.has_hotkey_pad_fast_forward = true; + seed.hotkey_pad_fast_forward_toggle = g_hotkey_pad_fast_forward_toggle; + seed.has_hotkey_pad_fast_forward_toggle = true; seed.skip_launcher = skip_launcher_setting; seed.has_skip_launcher = true; if (has_netplay_player_name) { seed.netplay_player_name = netplay_player_name; @@ -12496,6 +12543,8 @@ int main(int argc, char** argv) { ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD] = normalize_hotkey_pad_binding(seed.hotkey_pad_fast_forward, PSX_HOTKEY_PAD_SELECT_L1); + ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE] = + normalize_hotkey_pad_binding(seed.hotkey_pad_fast_forward_toggle, 0); ls.auto_skip_fmv = seed.auto_skip_fmv ? 1 : 0; ls.turbo_loads = seed.turbo_loads ? 1 : 0; /* Localization: index of resolved_language within lang_menu_options @@ -12798,6 +12847,9 @@ int main(int argc, char** argv) { ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD], PSX_HOTKEY_PAD_SELECT_L1); seed.has_hotkey_pad_fast_forward = true; + seed.hotkey_pad_fast_forward_toggle = normalize_hotkey_pad_binding( + ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE], 0); + seed.has_hotkey_pad_fast_forward_toggle = true; seed.auto_skip_fmv = ls.auto_skip_fmv != 0; seed.has_auto_skip_fmv = skip_fmv_offered; seed.turbo_loads = ls.turbo_loads != 0; @@ -13018,6 +13070,9 @@ int main(int argc, char** argv) { g_hotkey_pad_fast_forward = seed.has_hotkey_pad_fast_forward ? seed.hotkey_pad_fast_forward : PSX_HOTKEY_PAD_SELECT_L1; + g_hotkey_pad_fast_forward_toggle = seed.has_hotkey_pad_fast_forward_toggle + ? seed.hotkey_pad_fast_forward_toggle + : 0; skip_launcher_setting = seed.skip_launcher; if (seed.has_bios_path) { settings_bios_storage = seed.bios_path.string(); @@ -14508,6 +14563,8 @@ int main(int argc, char** argv) { normalize_hotkey_pad_binding( g_hotkey_pad_fast_forward, PSX_HOTKEY_PAD_SELECT_L1); + ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE] = + normalize_hotkey_pad_binding(g_hotkey_pad_fast_forward_toggle, 0); ls.aspect_index = (g_video_aspect_num * 9 == g_video_aspect_den * 21) ? 2 : (g_video_aspect_num == 16 && g_video_aspect_den == 9) ? 1 : 0; ls.language_index = 0; @@ -14811,6 +14868,9 @@ int main(int argc, char** argv) { ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD], PSX_HOTKEY_PAD_SELECT_L1); us.has_hotkey_pad_fast_forward = true; + us.hotkey_pad_fast_forward_toggle = normalize_hotkey_pad_binding( + ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE], 0); + us.has_hotkey_pad_fast_forward_toggle = true; us.auto_skip_fmv = ls.auto_skip_fmv != 0; us.has_auto_skip_fmv = skip_fmv_offered; us.turbo_loads = ls.turbo_loads != 0; @@ -14894,6 +14954,8 @@ int main(int argc, char** argv) { g_hotkey_pad_fast_forward = normalize_hotkey_pad_binding( ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD], PSX_HOTKEY_PAD_SELECT_L1); + g_hotkey_pad_fast_forward_toggle = normalize_hotkey_pad_binding( + ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE], 0); switch (ls.aspect_index) { case 2: g_video_aspect_num = 21; g_video_aspect_den = 9; break; case 1: g_video_aspect_num = 16; g_video_aspect_den = 9; break; diff --git a/runtime/tests/test_rewind_toggle_combo.py b/runtime/tests/test_rewind_toggle_combo.py index 2399c5917..ac937f0b1 100644 --- a/runtime/tests/test_rewind_toggle_combo.py +++ b/runtime/tests/test_rewind_toggle_combo.py @@ -47,7 +47,7 @@ assert "static int g_hotkey_pad_fast_forward = 1528;" in MAIN assert "PSX_HOTKEY_PAD_SELECT_L1" in MAIN assert "PSX_ASSIST_BIND_FAST_FORWARD" in MAIN -assert "if (kb_turbo || hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) {" in MAIN +assert " hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) {" in MAIN assert MAIN.count("ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD]") == \ MAIN.count("ls.assist_pad_bind[PSX_ASSIST_BIND_SAVE_STATE_MENU]") assert '"Fast-forward",' in MAIN @@ -55,4 +55,25 @@ assert 'h.contains("fast_forward_pad")' in CFG # settings.toml read assert 'f << "fast_forward_pad = "' in CFG # settings.toml write +# Fast-forward toggle: a press-to-latch twin of the hold shortcut. Keyboard +# [KeyMap] TurboToggle (default F9) and pad [hotkeys] fast_forward_toggle_pad +# (unbound by default) flip one latch that feeds the same fast-forward block. +assert "static int g_hotkey_pad_fast_forward_toggle = 0;" in MAIN +assert "PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE" in MAIN +assert "static int g_manual_turbo_latched = 0;" in MAIN +assert "static void fast_forward_toggle_flip(void)" in MAIN +assert "fast_forward_toggle_poll_buttons();" in MAIN +assert "host_keymap_match_event(HOST_KEYMAP_TURBO_TOGGLE," in MAIN +assert "if (kb_turbo || g_manual_turbo_latched ||" in MAIN +assert MAIN.count("ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE]") == \ + MAIN.count("ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD]") +assert '"Fast-forward toggle",' in MAIN +assert 'h.contains("fast_forward_toggle_pad")' in CFG +assert 'f << "fast_forward_toggle_pad = "' in CFG +KEYMAP_H = (ROOT / "runtime" / "include" / "host_keymap.h").read_text(encoding="utf-8") +KEYMAP_C = (ROOT / "runtime" / "src" / "host_keymap.c").read_text(encoding="utf-8") +assert "HOST_KEYMAP_TURBO_TOGGLE," in KEYMAP_H +assert 'ieq(name, "TurboToggle")' in KEYMAP_C +assert "add_bind(HOST_KEYMAP_TURBO_TOGGLE, (int)SDLK_F9" in KEYMAP_C + print("host shortcut guard passed")