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/2] 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/2] 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,