From 292f19e9779ef57c0ee0b9f06a4a08a997baa1c3 Mon Sep 17 00:00:00 2001 From: tetrisgm <30949000+tetrisgm@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:19:05 -0700 Subject: [PATCH 1/3] launcher: raise the SSAA ladder to 16x and name the steps by resolution launcher_model_cycle_supersampling clamped to 1..4 and wrapped at 4x. The config loader has always accepted [video] supersampling 1..16, the runtime honours it, and the GL backend renders it -- so 1440p and above were unreachable from the launcher even though every layer underneath supported them. On a ~256-line PSX frame 4x is 1024 lines, short of 1080p. Cycle a ladder instead (1,2,3,4,5,6,8,9,12,16), whose steps are the smallest multipliers that REACH each standard monitor height on a 240-256 line frame: 3x>=720, 5x>=1080, 6x>=1440, 9x>=2160, 16x>=4096. Label them by the height they reach ("6x (1440p)"), since a player is picking a monitor resolution rather than an SSAA factor, and the multiplier that gets to 4K differs per console because the native frame does. That needs the native frame height, so SystemProfile gains native_fb_height, appended so existing positional profile rows zero-fill it; 0 keeps the bare "Nx" label. Only the PSX row declares it (256) for now. Co-Authored-By: Claude Opus 5 --- src/common/launcher_model.c | 47 ++++++++++++++++++++++++------ src/common/launcher_system_types.h | 8 +++++ src/consoles/psx/psx_profile.h | 1 + 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/common/launcher_model.c b/src/common/launcher_model.c index a0968a8..ec7fdc7 100644 --- a/src/common/launcher_model.c +++ b/src/common/launcher_model.c @@ -1120,20 +1120,49 @@ const char* launcher_model_renderer_label(const LauncherModel* m) { return m->s.renderer ? "OpenGL" : "Software"; } +/* Internal-resolution ladder. The steps are chosen so that, on a console whose + * native frame is ~240-256 scanlines, each one is the smallest multiplier that + * REACHES a standard monitor height: 3x>=720, 5x>=1080, 6x>=1440, 9x>=2160, + * 16x>=4096. The runtime and the config loader have always accepted 1..16 + * ([video] supersampling) and the GL backend renders it, but this cycle was + * capped at 4x -- so 1440p and above were unreachable from the launcher even + * though everything below it supported them. */ +static const int kSsaaLadder[] = { 1, 2, 3, 4, 5, 6, 8, 9, 12, 16 }; +#define LNG_SSAA_LADDER_COUNT ((int)(sizeof(kSsaaLadder) / sizeof(kSsaaLadder[0]))) + void launcher_model_cycle_supersampling(LauncherModel* m) { - int v = clampi(m->s.supersampling ? m->s.supersampling : 1, 1, 4); - m->s.supersampling = (v % 4) + 1; + const int cur = clampi(m->s.supersampling ? m->s.supersampling : 1, 1, 16); + for (int i = 0; i < LNG_SSAA_LADDER_COUNT; i++) { + if (kSsaaLadder[i] > cur) { m->s.supersampling = kSsaaLadder[i]; return; } + } + m->s.supersampling = kSsaaLadder[0]; /* wrap */ } const char* launcher_model_supersampling_label(const LauncherModel* m) { /* Settings SSAA: offline full SW/GL path; netplay dual-raster uses this - * for OpenGL present quality while SW authority stays 1×. */ - static char buf[24]; - int v = clampi(m->s.supersampling ? m->s.supersampling : 1, 1, 4); - if (v <= 1) - snprintf(buf, sizeof(buf), "1x"); - else - snprintf(buf, sizeof(buf), "%dx", v); + * for OpenGL present quality while SW authority stays 1×. + * + * Named after the output height it reaches, because that is what a player + * is actually choosing. Consoles that do not declare native_fb_height keep + * the bare multiplier. */ + static char buf[32]; + const int v = clampi(m->s.supersampling ? m->s.supersampling : 1, 1, 16); + const SystemProfile* prof = (const SystemProfile*)m->profile; + const int nh = prof ? prof->native_fb_height : 0; + if (nh > 0 && v > 1) { + const int lines = nh * v; + const char* name = NULL; + if (lines >= 4096) name = "8K"; + else if (lines >= 2160) name = "4K"; + else if (lines >= 1440) name = "1440p"; + else if (lines >= 1080) name = "1080p"; + else if (lines >= 720) name = "720p"; + if (name) { + snprintf(buf, sizeof(buf), "%dx (%s)", v, name); + return buf; + } + } + snprintf(buf, sizeof(buf), "%dx", v); return buf; } diff --git a/src/common/launcher_system_types.h b/src/common/launcher_system_types.h index 177b1a2..d61feeb 100644 --- a/src/common/launcher_system_types.h +++ b/src/common/launcher_system_types.h @@ -144,6 +144,14 @@ typedef struct SystemProfile { // wordmark may be a third-party trademark); an absent file falls back to // the text, so this is inert unless a host drops the asset in. const char* wordmark_image; + // Native framebuffer height in scanlines, used ONLY to name the + // supersampling steps after the output resolution they reach + // ("6x (1440p)") instead of a bare multiplier. A player picks a monitor + // resolution, not an SSAA factor, and the multiplier that reaches 4K + // differs per console because the native frame does. 0 => unknown, and the + // label stays a plain "Nx". Appended so existing positional profile rows + // zero-fill it. + int native_fb_height; } SystemProfile; // ONE ROW PER CONSOLE // ---- shared panel composition arrays (NULL-terminated) -------------------------- diff --git a/src/consoles/psx/psx_profile.h b/src/consoles/psx/psx_profile.h index c359c8b..066a384 100644 --- a/src/consoles/psx/psx_profile.h +++ b/src/consoles/psx/psx_profile.h @@ -125,6 +125,7 @@ static const SystemProfile kSystemProfilePsx = { /* hide_audio_freq */ 0, /* brand */ "brand_psx.tga", /* wordmark_image */ NULL, + /* native_fb_height */ 256, /* PAL 512x256; NTSC 240 rounds the same way */ }; // ---- name aliases + ABI capability defaults ------------------------------------- From d927728dc45c476b6b1625502737807b016c1d71 Mon Sep 17 00:00:00 2001 From: tetrisgm <30949000+tetrisgm@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:04:28 -0700 Subject: [PATCH 2/3] launcher: label opening sequence skip --- 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 57d2690..e8062d4 100644 --- a/src/common/backends/imgui/launcher_imgui.cpp +++ b/src/common/backends/imgui/launcher_imgui.cpp @@ -2737,7 +2737,7 @@ void draw_display_controls(LauncherModel* m, const LauncherTheme& th) { } if (m->has_skip_fmv) { - row_label("Skip FMVs", th); + row_label("Skip opening sequence", th); bool sk = m->s.auto_skip_fmv != 0; if (ImGui::Checkbox("##skipfmv", &sk)) launcher_model_toggle_skip_fmv(m); } From d33f0822fa6f5e3e56d618a96738b31d6e0404b3 Mon Sep 17 00:00:00 2001 From: tetrisgm <30949000+tetrisgm@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:34:47 -0700 Subject: [PATCH 3/3] Reject invalid disc paths in launcher --- src/common/launcher_model.c | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/common/launcher_model.c b/src/common/launcher_model.c index cd2a4cf..825bc15 100644 --- a/src/common/launcher_model.c +++ b/src/common/launcher_model.c @@ -217,6 +217,7 @@ static void lm_bind_disc_selection(LauncherModel* m) { static void run_verify(LauncherModel* m); // fwd; defined below, called from launcher_model_set_rom static void update_msu1_patch_available(LauncherModel* m); // fwd; called from launcher_model_set_rom +static void lm_normalize_bios_path(const char* path, char* out, size_t out_cap); static void lm_inspect_memcard(LauncherModel* m, int slot); // fwd; host memcard_inspect callback static void lm_inspect_tpak(LauncherModel* m, int slot); // fwd; host tpak_inspect callback @@ -1031,7 +1032,10 @@ int launcher_model_autofill_sibling_discs(LauncherModel* m) { void launcher_model_set_rom(LauncherModel* m, const char* path) { m->rom_present = path && path[0] != '\0'; - safe_copy(m->rom_full, sizeof(m->rom_full), m->rom_present ? path : ""); + if (m->rom_present) + lm_normalize_bios_path(path, m->rom_full, sizeof(m->rom_full)); + else + m->rom_full[0] = '\0'; m->rom_sha1_hex[0] = '\0'; m->rom_patch_prepared_path[0] = '\0'; m->rom_patch_prepared_sha1[0] = '\0'; @@ -1127,17 +1131,27 @@ void launcher_model_set_rom(LauncherModel* m, const char* path) { } // Disc-verdict (verify.mode==1 systems, e.g. PSX): run the SystemProfile's -// VerifyProbeFn against the current ROM/disc path, or synthesize a sensible -// placeholder verdict when the probe is NULL / declines (no host wired up -// yet) so the disc-verdict UI always renders a real verdict block instead of -// a "not recognized" dead end. No-op for verify.mode==0 systems (SNES) — the -// CRC/SHA line above already covers them and m->verify stays zeroed. +// VerifyProbeFn against the current ROM/disc path. A host verifier that +// declines a path is a real failure: do not turn a missing/bad CUE into a +// placeholder success, or the launcher will enable Play only for the game to +// fail later with "Disc Image Not Found". The placeholder remains only for +// profiles that have no verifier at all (legacy launcher behavior). static void run_verify(LauncherModel* m) { if (!m->profile || m->profile->verify.mode != 1) return; memset(&m->verify, 0, sizeof(m->verify)); + if (!m->rom_present) { + m->verify.verdict = 0; + return; + } + if (strcmp(m->rom_size, "--") == 0) { + m->verify.verdict = 3; + safe_copy(m->verify.netplay_detail, sizeof(m->verify.netplay_detail), + "The selected disc image could not be opened. Choose an existing .cue or .bin file."); + return; + } // Host disc-verify callback (REAL serial/region/ISO/verdict) takes // precedence — re-run here on every ROM/disc change. - if (m->disc_verify_cb && m->rom_present) { + if (m->disc_verify_cb) { RecompLauncherCDiscVerify dv; memset(&dv, 0, sizeof(dv)); if (m->disc_verify_cb(m->rom_full, &dv)) { safe_copy(m->verify.serial, sizeof(m->verify.serial), dv.serial); @@ -1151,6 +1165,10 @@ static void run_verify(LauncherModel* m) { dv.netplay_detail); return; } + m->verify.verdict = 3; + safe_copy(m->verify.netplay_detail, sizeof(m->verify.netplay_detail), + "The selected disc image could not be verified. Choose the matching .cue file."); + return; } VerifyProbeFn probe = m->profile->verify.probe; bool ok = probe && probe(m, &m->verify);