From fda07feb734f0f4628435b4120ddfb2b988aee9d Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:52:36 -0400 Subject: [PATCH 1/6] launcher: Scanlines toggle + strength on the PSX Display card Adds a Scanlines checkbox and a strength slider to the Display settings for consoles that advertise has_scanlines (PSX). Settings.scanlines and Settings.scanline_strength_pct are appended additively to the CSettings ABI (RECOMP_LAUNCHER_HAS_SCANLINES gates source compatibility), and the row is gated by GameInfo.has_scanlines, set in the PSX profile. Mirrors the existing screen_kind wiring; the strength row only appears once scanlines are on. Co-Authored-By: Claude Opus 4.8 --- src/common/backends/imgui/launcher_imgui.cpp | 25 ++++++++++++++++++++ src/common/launcher_model.c | 21 ++++++++++++++++ src/common/launcher_model.h | 4 ++++ src/consoles/psx/psx_profile.h | 1 + src/recomp_launcher.h | 21 ++++++++++++++++ 5 files changed, 72 insertions(+) diff --git a/src/common/backends/imgui/launcher_imgui.cpp b/src/common/backends/imgui/launcher_imgui.cpp index 57d2690..26cfc5c 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -2376,6 +2376,7 @@ static const char* elide_left(const char* s, float max_w, char* out, size_t cap) bool any_deep_display(const LauncherModel* m) { return m->has_window_size || m->has_renderer || m->has_supersampling || m->has_antialiasing || m->has_texture_filter || m->has_screen_kind || + m->has_scanlines || m->has_fmv_filter || m->has_frame_interp || m->has_skip_fmv || m->has_geometry_precision || @@ -2705,6 +2706,30 @@ void draw_display_controls(LauncherModel* m, const LauncherTheme& th) { launcher_model_cycle_screen_kind(m); } + // Scanlines: darken every other display line for a CRT look. Independent of + // the Screen model colour filter above — the two compose. The strength row + // only appears once it is on. + if (m->has_scanlines) { + row_label("Scanlines", th); + bool sl = m->s.scanlines != 0; + if (ImGui::Checkbox("##scanlines", &sl)) + launcher_model_toggle_scanlines(m); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) + ImGui::SetTooltip("Darkens the gaps between display lines for a CRT " + "look.\nApplied at the console's own scanline " + "pitch; cleanest when the\nwindow is at least twice " + "the console's line count tall."); + if (m->s.scanlines) { + row_label("Scanline strength", th); + int p = launcher_model_scanline_strength_pct(m); + ImGui::PushID("scanline_strength"); + ImGui::SetNextItemWidth(px(150)); + if (ImGui::SliderInt("##scanline_strength", &p, 1, 100, "%d%%")) + launcher_model_set_scanline_strength_pct(m, p); + ImGui::PopID(); + } + } + // Frame interpolation is only meaningful under OpenGL (Software has no // interpolation pass); Presentation target only matters once frame // interpolation is actually on. diff --git a/src/common/launcher_model.c b/src/common/launcher_model.c index bffa759..cebeee2 100644 --- a/src/common/launcher_model.c +++ b/src/common/launcher_model.c @@ -268,6 +268,7 @@ void launcher_model_init(LauncherModel* m, m->has_texture_filter = game->has_texture_filter != 0; m->has_fmv_filter = game->has_fmv_filter != 0; m->has_screen_kind = game->has_screen_kind != 0; + m->has_scanlines = game->has_scanlines != 0; m->has_frame_interp = game->has_frame_interp != 0; m->has_spu_hq = game->has_spu_hq != 0; m->has_rewind_depth = game->has_rewind_depth != 0; @@ -1581,6 +1582,26 @@ const char* launcher_model_screen_kind_label(const LauncherModel* m) { return names[clampi(m->s.screen_kind, 0, n - 1)]; } +void launcher_model_toggle_scanlines(LauncherModel* m) { + if (!m || !m->has_scanlines) return; + m->s.scanlines = !m->s.scanlines; + /* First time on with no persisted strength: seed the default so the slider + * has a value to show (and the effect is visible). */ + if (m->s.scanlines && m->s.scanline_strength_pct == 0) + m->s.scanline_strength_pct = 50; +} + +void launcher_model_set_scanline_strength_pct(LauncherModel* m, int pct) { + if (!m || !m->has_scanlines) return; + m->s.scanline_strength_pct = clampi(pct, 1, 100); +} + +int launcher_model_scanline_strength_pct(const LauncherModel* m) { + if (!m) return 50; + int p = m->s.scanline_strength_pct; + return (p >= 1 && p <= 100) ? p : 50; /* 0 = unset -> default 50 */ +} + void launcher_model_toggle_frame_interp(LauncherModel* m) { m->s.frame_interp = !m->s.frame_interp; } diff --git a/src/common/launcher_model.h b/src/common/launcher_model.h index 475cedb..7afd571 100644 --- a/src/common/launcher_model.h +++ b/src/common/launcher_model.h @@ -369,6 +369,7 @@ typedef struct { bool has_texture_filter; bool has_fmv_filter; bool has_screen_kind; + bool has_scanlines; // present-time scanline post-process (PSX) bool has_frame_interp; bool has_spu_hq; bool has_rewind_depth; @@ -732,6 +733,9 @@ void launcher_model_toggle_perspective_texturing(LauncherModel* m); bool launcher_model_geometry_correction_inert(const LauncherModel* m); void launcher_model_cycle_screen_kind(LauncherModel* m); // Raw/CRT/Composite/Trinitron const char* launcher_model_screen_kind_label(const LauncherModel* m); +void launcher_model_toggle_scanlines(LauncherModel* m); +void launcher_model_set_scanline_strength_pct(LauncherModel* m, int pct); // clamps 1..100 +int launcher_model_scanline_strength_pct(const LauncherModel* m); // effective (seeds 50) void launcher_model_toggle_frame_interp(LauncherModel* m); void launcher_model_cycle_interp_fps(LauncherModel* m); // {0,90,120,144,165,240} wrap const char* launcher_model_interp_fps_label(const LauncherModel* m); // "Display refresh"/"90 fps" diff --git a/src/consoles/psx/psx_profile.h b/src/consoles/psx/psx_profile.h index c801d65..2171ea3 100644 --- a/src/consoles/psx/psx_profile.h +++ b/src/consoles/psx/psx_profile.h @@ -160,6 +160,7 @@ static inline void launcher_profile_apply_psx(RecompLauncherCGameInfo* gi) { gi->has_fmv_filter = 1; /* MDEC decodes video at native res; how it is * scaled to the window is a player choice. */ gi->has_frame_interp = 0; gi->has_spu_hq = 1; gi->has_skip_fmv = 0; + gi->has_scanlines = 1; /* present-time CRT scanline post-process */ gi->has_turbo_loads = 1; gi->has_bios = 1; gi->has_deadzone_pct = 1; gi->has_rewind_depth = RECOMP_UI_PSX_HAS_REWIND ? 1 : 0; diff --git a/src/recomp_launcher.h b/src/recomp_launcher.h index 7208b19..d24d107 100644 --- a/src/recomp_launcher.h +++ b/src/recomp_launcher.h @@ -777,6 +777,16 @@ struct RecompLauncherCSettings { * session. 0 = unset: the launcher seeds it by matching initial_rom * against the roster, falling back to disc 1. Appended additively. */ int disc_index; + + /* Scanline post-process on/off (GameInfo.has_scanlines consoles). 0 = off + * (also the unset default, like rewind_enabled — the host default is off, so + * a zero-initialized host predating this field gets the right answer), 1 = + * on. Appended additively. */ + int scanlines; + /* Scanline dark-gap depth as a percent. 0 = unset -> the model seeds 50; the + * effective range is 1..100. Stored as a percent (not 0..1) so the whole + * settings struct stays plain-int. Appended additively. */ + int scanline_strength_pct; }; /* Values for RecompLauncherCSettings.vsync (1-based; 0 = unset). */ @@ -794,6 +804,11 @@ struct RecompLauncherCSettings { /* Hosts can #ifdef on this to stay source-compatible with older recomp-ui. */ #define RECOMP_LAUNCHER_HAS_FMV_FILTER 1 +/* Scanline post-process (Settings.scanlines / scanline_strength_pct, + * GameInfo.has_scanlines). Hosts #ifdef on this to stay source-compatible with + * older recomp-ui that lacks the fields. */ +#define RECOMP_LAUNCHER_HAS_SCANLINES 1 + // ---- host verification/inspection results (filled by the callbacks below) ---- // Plain-C structs so a host can implement the callbacks with zero launcher // internal types. Mirror what the legacy launcher computed inline. @@ -1355,6 +1370,12 @@ typedef struct RecompLauncherCGameInfo { * Return 0 on success, like persist_setup. Uses persist_setup_ctx. */ int (*persist_setup_discs)(void* ctx, const char* const* disc_paths, int disc_count, const char* bios_path); + + /* Display row for Settings.scanlines / scanline_strength_pct: a present-time + * scanline post-process (a checkbox plus a strength slider). Only meaningful + * for a console whose runtime implements it (PSX); everything else leaves + * this 0 and the rows are absent. Appended for ABI stability. */ + int has_scanlines; } RecompLauncherCGameInfo; /* recomp_launcher_run_window return codes */ From 37e2d798266391d5488f8a738bf69ad2ed97a9d3 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 2/6] launcher: label implicit Select chords on PSX host shortcuts; TurboToggle hotkey The PSX runtime only matches a host shortcut bound to ONE button or ONE trigger direction while Select is also held (hotkey_pad_binding_down); the capture UI swallows a lone Select for the same reason, but the resulting label showed just "righttrigger+", so a player pressing R2 alone saw nothing happen. Prefix such bindings with "select + " on the PSX profile in both host-shortcut tables and add a one-line hint under the table whenever one is present. Explicit two-button chords are unchanged. Also surface the runtime's new [KeyMap] TurboToggle (press-to-latch fast-forward, default F9): LNG_HK_TURBO_TOGGLE, key/default/label tables, and the PSX hotkeys_mask. Co-Authored-By: Claude Fable 5.1 --- src/common/backends/imgui/launcher_imgui.cpp | 46 ++++++++++++++++++-- src/common/launcher_binds.c | 4 +- src/common/launcher_model.c | 2 +- src/common/launcher_model.h | 1 + src/consoles/psx/psx_profile.h | 3 +- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/common/backends/imgui/launcher_imgui.cpp b/src/common/backends/imgui/launcher_imgui.cpp index 048ac8a..2969c5e 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -3491,6 +3491,42 @@ void settings_pad_label(int binding, char* out, size_t capacity) { snprintf(out, capacity, "%s", text); } +/* PSX's runtime matches a host shortcut bound to ONE button or ONE trigger + * direction only while Select is also held (hotkey_pad_binding_down): the + * implicit chord keeps a lone face button from firing Rewind mid-game. A + * two-button capture is stored as an explicit chord and replaces that + * implicit Select. Show the requirement in the label so a player who binds + * "righttrigger+" is not left pressing R2 alone and seeing nothing. */ +static bool assist_pad_bind_implies_select(const LauncherModel* m, int binding) { + const SystemProfile* prof = m ? (const SystemProfile*)m->profile : nullptr; + const bool psx = prof && prof->id && std::strcmp(prof->id, "psx") == 0; + return psx && (RECOMP_LAUNCHER_PAD_IS_BUTTON(binding) || + RECOMP_LAUNCHER_PAD_IS_AXIS(binding)); +} + +static void assist_pad_label(const LauncherModel* m, int binding, + char* out, size_t capacity) { + char text[96]; + settings_pad_label(binding, text, sizeof text); + if (assist_pad_bind_implies_select(m, binding)) + snprintf(out, capacity, "select + %s", text); + else + snprintf(out, capacity, "%s", text); +} + +static void draw_assist_pad_chord_hint(const LauncherModel* m, + const LauncherTheme& th) { + if (!m) return; + for (int action = 0; action < m->assist_binding_count; ++action) { + if (assist_pad_bind_implies_select(m, m->s.assist_pad_bind[action])) { + ImGui::TextColored(col(th.text_muted), + "Single-button shortcuts fire with Select held; capture two" + " buttons together for a chord without Select."); + return; + } + } +} + void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, const char* table_id, int action_limit, bool show_reset) { @@ -3533,8 +3569,8 @@ void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, ImGui::TableSetColumnIndex(2); bool capture_pad = m->capturing && m->capture_assist && m->capture_pad && m->capture_btn == action; - char pad[48]; - settings_pad_label(m->s.assist_pad_bind[action], pad, sizeof pad); + char pad[112]; + assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); if (ImGui::Button( capture_pad ? "[ press a button... ]" : pad, ImVec2(px(170), 0))) @@ -3543,6 +3579,7 @@ void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, } ImGui::EndTable(); } + draw_assist_pad_chord_hint(m, th); if (show_reset && ImGui::Button(m->has_assist_tools ? "Reset Assist Controls" : "Reset Host Shortcuts")) @@ -3588,8 +3625,8 @@ void draw_controller_assist_shortcuts(LauncherModel* m, ImGui::TableSetColumnIndex(2); bool capture_pad = m->capturing && m->capture_assist && m->capture_pad && m->capture_btn == action; - char pad[48]; - settings_pad_label(m->s.assist_pad_bind[action], pad, sizeof pad); + char pad[112]; + assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); if (ImGui::Button(capture_pad ? "[ button... ]" : pad, ImVec2(-FLT_MIN, 0))) launcher_model_begin_assist_capture(m, action, true); @@ -3597,6 +3634,7 @@ void draw_controller_assist_shortcuts(LauncherModel* m, } ImGui::EndTable(); } + draw_assist_pad_chord_hint(m, th); if (m->capturing && m->capture_assist) ImGui::TextColored(col(th.warn), "Listening... (Esc cancels)"); } diff --git a/src/common/launcher_binds.c b/src/common/launcher_binds.c index bd1b7a6..a326f17 100644 --- a/src/common/launcher_binds.c +++ b/src/common/launcher_binds.c @@ -180,7 +180,7 @@ static const char* kHotkeyKey[LNG_HK_COUNT] = { "WindowBigger", "WindowSmaller", "VolumeUp", "VolumeDown", "DisplayPerf", "ToggleRenderer", "SolarBrighter", "SolarDimmer", "SolarLive", - "Rewind", "SaveStateMenu" + "Rewind", "SaveStateMenu", "TurboToggle" }; // Built-in defaults (shown when config.ini has no line; "" = unbound). static const char* kHotkeyDef[LNG_HK_COUNT] = { @@ -189,7 +189,7 @@ static const char* kHotkeyDef[LNG_HK_COUNT] = { * (psxrecomp host_keymap reads these from [KeyMap]). */ "", "", "Keypad +", "Keypad -", "F", "R", "", "", "", - "F8", "F7" + "F8", "F7", "F9" }; static void copy_str(char* d, size_t cap, const char* s) { diff --git a/src/common/launcher_model.c b/src/common/launcher_model.c index 92bad9c..b5b1804 100644 --- a/src/common/launcher_model.c +++ b/src/common/launcher_model.c @@ -51,7 +51,7 @@ static const char* kHotkeyNames[LNG_HK_COUNT] = { "Window bigger", "Window smaller", "Volume up", "Volume down", "FPS readout", "Toggle renderer", "Solar level up", "Solar level down", "Resume live solar", - "Rewind", "Save states menu" + "Rewind", "Save states menu", "Fast-forward toggle" }; static const char* kViewNames[7] = { "Dashboard", "Settings", "Controller", "Netplay", "Mods", diff --git a/src/common/launcher_model.h b/src/common/launcher_model.h index 0939e7e..c781c8c 100644 --- a/src/common/launcher_model.h +++ b/src/common/launcher_model.h @@ -98,6 +98,7 @@ typedef enum { LNG_HK_SOLAR_BRIGHTER, LNG_HK_SOLAR_DIMMER, LNG_HK_SOLAR_LIVE, LNG_HK_REWIND, /* PSX local rewind filmstrip → [KeyMap] Rewind */ LNG_HK_SAVE_STATE_MENU, /* PSX save-state slot menu → [KeyMap] SaveStateMenu */ + LNG_HK_TURBO_TOGGLE, /* PSX press-to-latch fast-forward → [KeyMap] TurboToggle */ LNG_HK_COUNT } LngHotkey; diff --git a/src/consoles/psx/psx_profile.h b/src/consoles/psx/psx_profile.h index c801d65..9eae781 100644 --- a/src/consoles/psx/psx_profile.h +++ b/src/consoles/psx/psx_profile.h @@ -118,7 +118,8 @@ static const SystemProfile kSystemProfilePsx = { (1u << LNG_HK_VOLUME_DOWN) | (1u << LNG_HK_DISPLAY_PERF) | RUI_PSX_REWIND_HOTKEY_MASK | - (1u << LNG_HK_SAVE_STATE_MENU)), + (1u << LNG_HK_SAVE_STATE_MENU) | + (1u << LNG_HK_TURBO_TOGGLE)), /* panels_dashboard */ kPanelsDashboardPsx, /* panels_settings */ kPanelsSettingsPsx, /* panels_controller */ kPanelsControllerCommon, From db126208c7e548872a238720055fdc75355cd8ff Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:44:03 -0400 Subject: [PATCH 3/6] launcher: unique IDs for host-shortcut buttons; keep assist capture off the pad grid Dear ImGui's ID-conflict check fired on the new "Fast-forward toggle" row: its keyboard and controller buttons both read "(unbound)" inside the same PushID(action) scope. Suffix them "##key" / "##pad" in both host-shortcut tables. Pre-existing: the PSX keyboard/pad binding grids and their "Map an input to ..." status lines tested capture_btn alone, so capturing host shortcut N lit up pad slot N as well (Fast-forward toggle = index 3 = Right). Gate every player-binding highlight and status line on !capture_assist. Co-Authored-By: Claude Fable 5.1 --- src/common/backends/imgui/launcher_imgui.cpp | 50 +++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/common/backends/imgui/launcher_imgui.cpp b/src/common/backends/imgui/launcher_imgui.cpp index 19d117b..14e4d87 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -3586,19 +3586,24 @@ void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, ImGui::TableSetColumnIndex(1); bool capture_key = m->capturing && m->capture_assist && !m->capture_pad && m->capture_btn == action; - if (ImGui::Button( - capture_key ? "[ press a key... ]" : - settings_key_label(m->s.assist_key_bind[action]), - ImVec2(px(170), 0))) + /* "##key" / "##pad": both columns can read "(unbound)" at once + * (a freshly added shortcut), which would give the two buttons + * the same ImGui ID inside this row's PushID scope. */ + char key_lbl[96]; + snprintf(key_lbl, sizeof key_lbl, "%s##key", + capture_key ? "[ press a key... ]" + : settings_key_label(m->s.assist_key_bind[action])); + if (ImGui::Button(key_lbl, ImVec2(px(170), 0))) launcher_model_begin_assist_capture(m, action, false); ImGui::TableSetColumnIndex(2); bool capture_pad = m->capturing && m->capture_assist && m->capture_pad && m->capture_btn == action; char pad[112]; assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); - if (ImGui::Button( - capture_pad ? "[ press a button... ]" : pad, - ImVec2(px(170), 0))) + char pad_lbl[128]; + snprintf(pad_lbl, sizeof pad_lbl, "%s##pad", + capture_pad ? "[ press a button... ]" : pad); + if (ImGui::Button(pad_lbl, ImVec2(px(170), 0))) launcher_model_begin_assist_capture(m, action, true); ImGui::PopID(); } @@ -3642,18 +3647,22 @@ void draw_controller_assist_shortcuts(LauncherModel* m, ImGui::TableSetColumnIndex(1); bool capture_key = m->capturing && m->capture_assist && !m->capture_pad && m->capture_btn == action; - if (ImGui::Button( - capture_key ? "[ key... ]" : - settings_key_label(m->s.assist_key_bind[action]), - ImVec2(-FLT_MIN, 0))) + /* "##key" / "##pad": see draw_assist_binding_editor. */ + char key_lbl[96]; + snprintf(key_lbl, sizeof key_lbl, "%s##key", + capture_key ? "[ key... ]" + : settings_key_label(m->s.assist_key_bind[action])); + if (ImGui::Button(key_lbl, ImVec2(-FLT_MIN, 0))) launcher_model_begin_assist_capture(m, action, false); ImGui::TableSetColumnIndex(2); bool capture_pad = m->capturing && m->capture_assist && m->capture_pad && m->capture_btn == action; char pad[112]; assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); - if (ImGui::Button(capture_pad ? "[ button... ]" : pad, - ImVec2(-FLT_MIN, 0))) + char pad_lbl[128]; + snprintf(pad_lbl, sizeof pad_lbl, "%s##pad", + capture_pad ? "[ button... ]" : pad); + if (ImGui::Button(pad_lbl, ImVec2(-FLT_MIN, 0))) launcher_model_begin_assist_capture(m, action, true); ImGui::PopID(); } @@ -4120,6 +4129,7 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { spec.buttons[b].label); ImGui::SameLine(label_col_w); const bool cap = m->capturing && !m->capture_pad && + !m->capture_assist && m->capture_btn == b; const bool cap_alt = cap && m->capture_slot == 1; const char* lbl = m->binds[p][b]; @@ -4193,7 +4203,7 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { s_kb_profile_saved_until = ImGui::GetTime() + 2.5; } } - if (m->capturing && !m->capture_pad) { + if (m->capturing && !m->capture_pad && !m->capture_assist) { const char* label = (m->capture_btn >= 0 && m->capture_btn < LNG_PSX_PAD_BUTTON_COUNT) @@ -4231,6 +4241,7 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { spec.buttons[b].label); ImGui::SameLine(label_col_w); const bool cap = m->capturing && m->capture_pad && + !m->capture_assist && m->capture_btn == b; const bool wait_rel = cap && m->map_all_wait_release; const char* pl = m->pad_binds[p][b][0] @@ -4278,7 +4289,7 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { s_profile_saved_until = ImGui::GetTime() + 2.5; } } - if (m->capturing && m->capture_pad) { + if (m->capturing && m->capture_pad && !m->capture_assist) { const char* label = (m->capture_btn >= 0 && m->capture_btn < LNG_PSX_PAD_BUTTON_COUNT) @@ -4379,7 +4390,8 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { for (int slot = 0; slot < bpi; ++slot) { if (slot) ImGui::SameLine(0, chip_gap); ImGui::PushID(slot); - const bool cap = m->capturing && m->capture_btn == b + const bool cap = m->capturing && !m->capture_assist && + m->capture_btn == b && m->capture_slot == slot; const char* txt = cap ? (pad_cap ? "[ press a key / pad... ]" : "[ press a key... ]") @@ -4392,7 +4404,8 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { } } else { // KEY chip - const bool cap_key = m->capturing && !m->capture_pad && m->capture_btn == b; + const bool cap_key = m->capturing && !m->capture_pad && + !m->capture_assist && m->capture_btn == b; if (cap_key) ImGui::PushStyleColor(ImGuiCol_Button, col(th.accent)); const char* key_text = settings_player_binds ? settings_key_label(m->s.player_key_bind[p][b]) @@ -4404,7 +4417,8 @@ void draw_controller_config_view(LauncherModel* m, const LauncherTheme& th) { if (has_pad) { ImGui::SameLine(0, chip_gap); ImGui::PushID("pad"); - const bool cap_pad = m->capturing && m->capture_pad && m->capture_btn == b; + const bool cap_pad = m->capturing && m->capture_pad && + !m->capture_assist && m->capture_btn == b; char settings_pad[48]; settings_pad_label(m->s.player_pad_bind[p][b], settings_pad, sizeof settings_pad); From af3e431d5f27c0f0c21a02fe710b95dad3d8c24b Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:01:02 -0400 Subject: [PATCH 4/6] launcher: Backspace clears a host shortcut or [KeyMap] hotkey while listening There was no way to remove a binding from the UI: capture commits any key or button, Esc keeps the old value, and Reset restores every default at once. - Host shortcuts (assist bindings): Backspace during a keyboard or controller capture stores 0 (unbound). Both tables say so while listening. - [KeyMap] hotkeys: Backspace writes the literal "None" instead of an empty value, and the display reads "(unbound)" for empty/None/(unbound) lines. The PSX runtime companion (psxrecomp feat/fast-forward-toggle) treats a present-but-empty/None line as an explicit unbind with no default fallback; a missing line still keeps the built-in default. Co-Authored-By: Claude Fable 5.1 --- src/common/backends/imgui/launcher_imgui.cpp | 22 +++++++++++++++++--- src/common/launcher_binds.c | 13 ++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/common/backends/imgui/launcher_imgui.cpp b/src/common/backends/imgui/launcher_imgui.cpp index 14e4d87..644cf1d 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -3610,6 +3610,8 @@ void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, ImGui::EndTable(); } draw_assist_pad_chord_hint(m, th); + if (m->capturing && m->capture_assist) + ImGui::TextColored(col(th.text_muted), "Backspace clears the binding."); if (show_reset && ImGui::Button(m->has_assist_tools ? "Reset Assist Controls" : "Reset Host Shortcuts")) @@ -3670,7 +3672,8 @@ void draw_controller_assist_shortcuts(LauncherModel* m, } draw_assist_pad_chord_hint(m, th); if (m->capturing && m->capture_assist) - ImGui::TextColored(col(th.warn), "Listening... (Esc cancels)"); + ImGui::TextColored(col(th.warn), + "Listening... (Esc cancels, Backspace clears)"); } void draw_assist_tools(LauncherModel* m, const LauncherTheme& th) { @@ -9382,6 +9385,13 @@ bool try_capture(LauncherModel* m, const SDL_Event& ev) { // the player's selected Input source device. if (m->capturing && m->capture_pad) { if (m->settings_bindings && m->capture_assist) { + // Backspace while listening clears the shortcut (0 = unbound); + // there is no other way to remove a host shortcut from the UI. + if (ev.type == SDL_EVENT_KEY_DOWN && LNG_EVKEY(ev) == SDLK_BACKSPACE) { + launcher_model_set_captured_pad(m, 0); + launcher_model_cancel_capture(m); + return true; + } if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { const int button = (int)LNG_EVGBTN(ev); uint32_t mask = launcher_input_gamepad_button_mask( @@ -9593,7 +9603,8 @@ bool try_capture(LauncherModel* m, const SDL_Event& ev) { // (capture_slot is always 0 for them). const SystemProfile* prof = (const SystemProfile*)m->profile; if (m->settings_bindings && m->capture_assist) - launcher_model_set_captured_key(m, (int)LNG_EVSCAN(ev)); + launcher_model_set_captured_key( + m, LNG_EVKEY(ev) == SDLK_BACKSPACE ? 0 : (int)LNG_EVSCAN(ev)); else if (prof && prof->controller.binds_per_input >= 2 && prof->id && !strcmp(prof->id, "psx")) launcher_binds_set_button_slot(m, m->cfg_player + 1, m->capture_btn, m->capture_slot, (int)LNG_EVSCAN(ev)); @@ -9608,7 +9619,12 @@ bool try_capture(LauncherModel* m, const SDL_Event& ev) { } // hotkey capture: wait past a bare modifier press for the real key if (is_modifier_scancode((SDL_Scancode)LNG_EVSCAN(ev))) return true; - launcher_binds_set_hotkey(m, m->capture_hk, (int)LNG_EVKEY(ev), (int)LNG_EVMOD(ev)); + // Backspace unbinds: launcher_binds_set_hotkey writes "None", which the + // PSX runtime honours as an explicit unbind (no default fallback). + if (LNG_EVKEY(ev) == SDLK_BACKSPACE) + launcher_binds_set_hotkey(m, m->capture_hk, 0, 0); + else + launcher_binds_set_hotkey(m, m->capture_hk, (int)LNG_EVKEY(ev), (int)LNG_EVMOD(ev)); launcher_model_cancel_hk_capture(m); return true; } diff --git a/src/common/launcher_binds.c b/src/common/launcher_binds.c index a326f17..955ddcc 100644 --- a/src/common/launcher_binds.c +++ b/src/common/launcher_binds.c @@ -384,7 +384,12 @@ static void reload_hotkey_display(LauncherModel* m) { char* hash = strchr(v, '#'); if (hash) *hash = 0; size_t vl = strlen(v); while (vl && (v[vl-1] == ' ' || v[vl-1] == '\t')) v[--vl] = 0; for (int h = 0; h < LNG_HK_COUNT; ++h) - if (ieq(p, klen, kHotkeyKey[h])) { copy_str(m->hotkeys[h], sizeof(m->hotkeys[h]), v); break; } + if (ieq(p, klen, kHotkeyKey[h])) { + const int unbound = !v[0] || ieq(v, strlen(v), "None") || + ieq(v, strlen(v), "(unbound)"); + copy_str(m->hotkeys[h], sizeof(m->hotkeys[h]), unbound ? "(unbound)" : v); + break; + } } free(text); } @@ -1175,6 +1180,10 @@ void launcher_binds_set_hotkey(LauncherModel* m, LngHotkey h, int keycode, int k if (h < 0 || h >= LNG_HK_COUNT) return; char val[64]; format_hotkey(keycode, kmod, val, sizeof(val)); - keymap_write(kHotkeyKey[h], val); + // keycode 0 = explicit unbind. Write the literal "None" rather than an + // empty value: the PSX runtime treats a present-but-empty/None line as + // "the user cleared this" and skips its built-in default, whereas a + // missing line keeps the default. + keymap_write(kHotkeyKey[h], val[0] ? val : "None"); copy_str(m->hotkeys[h], sizeof(m->hotkeys[h]), val[0] ? val : "(unbound)"); } From 2d213a72907cbd94b2548ebe7340f07d9138a9db Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:57:57 -0400 Subject: [PATCH 5/6] launcher: hide the Controller host-shortcut column when Player 1 has no connected gamepad The PSX runtime evaluates controller host shortcuts against Player 1's opened pad handle only. With Player 1 on Keyboard / None, or its saved pad unplugged, every controller shortcut is dead no matter what is bound -- yet the launcher still captured bindings from any connected pad, so a player rebinding shortcuts while the seat happened to be on Keyboard saw nothing work in-game. Both host-shortcut tables (Settings editor and Controller page) now drop the Controller column in that state and show a muted line saying why ("set Player 1's input source to a gamepad" / "Player 1's gamepad is not connected"). Other profiles keep the column unconditionally. Co-Authored-By: Claude Fable 5.1 --- src/common/backends/imgui/launcher_imgui.cpp | 95 ++++++++++++++------ 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/src/common/backends/imgui/launcher_imgui.cpp b/src/common/backends/imgui/launcher_imgui.cpp index 644cf1d..3bfc361 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -3539,6 +3539,34 @@ static void assist_pad_label(const LauncherModel* m, int binding, snprintf(out, capacity, "%s", text); } +/* The PSX runtime evaluates controller host shortcuts against PLAYER 1's + * opened pad handle only, so with Player 1 on Keyboard / None, or its pad + * unplugged, every controller shortcut is dead no matter what is bound. + * Hide the column in that state instead of offering bindings that cannot + * fire. Other profiles keep the column unconditionally. */ +static bool assist_pad_column_available(const LauncherModel* m) { + const SystemProfile* prof = m ? (const SystemProfile*)m->profile : nullptr; + const bool psx = prof && prof->id && std::strcmp(prof->id, "psx") == 0; + if (!psx) return true; + if (m->s.player_src[0] != 2) return false; + const char* guid = m->s.player_gamepad_guid[0]; + for (int j = 0; j < g_pad_count; ++j) { + if (g_pads[j].id && g_pads[j].id == m->player_pad_id[0]) return true; + if (guid[0] && g_pads[j].guid[0] && std::strcmp(g_pads[j].guid, guid) == 0) + return true; + } + return false; +} + +static void draw_assist_pad_unavailable_hint(const LauncherModel* m, + const LauncherTheme& th) { + if (!m) return; + ImGui::TextColored(col(th.text_muted), + m->s.player_src[0] == 2 + ? "Controller shortcuts hidden: Player 1's gamepad is not connected." + : "Controller shortcuts hidden: set Player 1's input source to a gamepad."); +} + static void draw_assist_pad_chord_hint(const LauncherModel* m, const LauncherTheme& th) { if (!m) return; @@ -3566,14 +3594,17 @@ void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, m->has_assist_tools ? "Global controls; they only operate while Assist Tools is enabled." : "Press a controller button or chord."); - if (ImGui::BeginTable(table_id, 3, ImGuiTableFlags_SizingFixedFit | - ImGuiTableFlags_RowBg)) { + const bool pad_col = assist_pad_column_available(m); + if (ImGui::BeginTable(table_id, pad_col ? 3 : 2, + ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, px(150)); ImGui::TableSetupColumn("Keyboard", ImGuiTableColumnFlags_WidthFixed, px(180)); - ImGui::TableSetupColumn("Controller", ImGuiTableColumnFlags_WidthFixed, - px(180)); + if (pad_col) + ImGui::TableSetupColumn("Controller", ImGuiTableColumnFlags_WidthFixed, + px(180)); ImGui::TableHeadersRow(); int count = m->assist_binding_count; if (action_limit > 0 && count > action_limit) count = action_limit; @@ -3595,21 +3626,24 @@ void draw_assist_binding_editor(LauncherModel* m, const LauncherTheme& th, : settings_key_label(m->s.assist_key_bind[action])); if (ImGui::Button(key_lbl, ImVec2(px(170), 0))) launcher_model_begin_assist_capture(m, action, false); - ImGui::TableSetColumnIndex(2); - bool capture_pad = m->capturing && m->capture_assist && - m->capture_pad && m->capture_btn == action; - char pad[112]; - assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); - char pad_lbl[128]; - snprintf(pad_lbl, sizeof pad_lbl, "%s##pad", - capture_pad ? "[ press a button... ]" : pad); - if (ImGui::Button(pad_lbl, ImVec2(px(170), 0))) - launcher_model_begin_assist_capture(m, action, true); + if (pad_col) { + ImGui::TableSetColumnIndex(2); + bool capture_pad = m->capturing && m->capture_assist && + m->capture_pad && m->capture_btn == action; + char pad[112]; + assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); + char pad_lbl[128]; + snprintf(pad_lbl, sizeof pad_lbl, "%s##pad", + capture_pad ? "[ press a button... ]" : pad); + if (ImGui::Button(pad_lbl, ImVec2(px(170), 0))) + launcher_model_begin_assist_capture(m, action, true); + } ImGui::PopID(); } ImGui::EndTable(); } - draw_assist_pad_chord_hint(m, th); + if (pad_col) draw_assist_pad_chord_hint(m, th); + else draw_assist_pad_unavailable_hint(m, th); if (m->capturing && m->capture_assist) ImGui::TextColored(col(th.text_muted), "Backspace clears the binding."); if (show_reset && ImGui::Button(m->has_assist_tools @@ -3630,15 +3664,17 @@ void draw_controller_assist_shortcuts(LauncherModel* m, m->has_assist_tools ? "(global; requires Assist Tools)" : "(keyboard and controller)"); - if (ImGui::BeginTable("controller_assist_binds", 3, + const bool pad_col = assist_pad_column_available(m); + if (ImGui::BeginTable("controller_assist_binds", pad_col ? 3 : 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch, 1.1f); ImGui::TableSetupColumn("Keyboard", ImGuiTableColumnFlags_WidthStretch, 1.0f); - ImGui::TableSetupColumn("Controller", ImGuiTableColumnFlags_WidthStretch, - 1.0f); + if (pad_col) + ImGui::TableSetupColumn("Controller", ImGuiTableColumnFlags_WidthStretch, + 1.0f); ImGui::TableHeadersRow(); for (int action = 0; action < m->assist_binding_count; ++action) { ImGui::PushID(action); @@ -3657,20 +3693,23 @@ void draw_controller_assist_shortcuts(LauncherModel* m, if (ImGui::Button(key_lbl, ImVec2(-FLT_MIN, 0))) launcher_model_begin_assist_capture(m, action, false); ImGui::TableSetColumnIndex(2); - bool capture_pad = m->capturing && m->capture_assist && - m->capture_pad && m->capture_btn == action; - char pad[112]; - assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); - char pad_lbl[128]; - snprintf(pad_lbl, sizeof pad_lbl, "%s##pad", - capture_pad ? "[ button... ]" : pad); - if (ImGui::Button(pad_lbl, ImVec2(-FLT_MIN, 0))) - launcher_model_begin_assist_capture(m, action, true); + if (pad_col) { + bool capture_pad = m->capturing && m->capture_assist && + m->capture_pad && m->capture_btn == action; + char pad[112]; + assist_pad_label(m, m->s.assist_pad_bind[action], pad, sizeof pad); + char pad_lbl[128]; + snprintf(pad_lbl, sizeof pad_lbl, "%s##pad", + capture_pad ? "[ button... ]" : pad); + if (ImGui::Button(pad_lbl, ImVec2(-FLT_MIN, 0))) + launcher_model_begin_assist_capture(m, action, true); + } ImGui::PopID(); } ImGui::EndTable(); } - draw_assist_pad_chord_hint(m, th); + if (pad_col) draw_assist_pad_chord_hint(m, th); + else draw_assist_pad_unavailable_hint(m, th); if (m->capturing && m->capture_assist) ImGui::TextColored(col(th.warn), "Listening... (Esc cancels, Backspace clears)"); From 4071e371c513144ab0ee52e61bb4013417c2fb3b Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Fri, 4 Sep 2026 08:00:57 -0400 Subject: [PATCH 6/6] launcher: fix ImGui assert when the Controller shortcut column is hidden draw_controller_assist_shortcuts still called TableSetColumnIndex(2) outside the pad_col guard, so with Player 1 on Keyboard the 2-column table hit IM_ASSERT(column_n < ColumnsCount) and the launcher aborted the moment the seat changed. Move the column select inside the guard (the Settings editor already had it there). Co-Authored-By: Claude Fable 5.1 --- src/common/backends/imgui/launcher_imgui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/backends/imgui/launcher_imgui.cpp b/src/common/backends/imgui/launcher_imgui.cpp index 3bfc361..9fd6a82 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -3692,8 +3692,8 @@ void draw_controller_assist_shortcuts(LauncherModel* m, : settings_key_label(m->s.assist_key_bind[action])); if (ImGui::Button(key_lbl, ImVec2(-FLT_MIN, 0))) launcher_model_begin_assist_capture(m, action, false); - ImGui::TableSetColumnIndex(2); if (pad_col) { + ImGui::TableSetColumnIndex(2); bool capture_pad = m->capturing && m->capture_assist && m->capture_pad && m->capture_btn == action; char pad[112];