Settings: an older launcher's Supersampling pick wins over a preset

A host built against a recomp-ui without the Internal resolution row
(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION undefined) still seeded
internal_resolution on first boot and applied it after the launcher.
From a fresh start the seed was "native", so a Supersampling pick of 3x
was reset to 1x and "native" was saved; on the netplay soft-return the
sticky preset was re-applied at session_reboot. The legacy row stopped
working.

Both launcher exits now go through two pure helpers in
internal_resolution.h:
- psx_ir_launcher_seed_supersampling: with the row, the factor as before;
  without it, the factor itself when no preset is configured (the
  historical round trip), else the preset's scale capped at the row's 4x.
- psx_ir_adopt_launcher: with the row, the row's preset as before;
  without it, a pick in the Supersampling row drops the preset, and an
  untouched row keeps a preset the player cannot see (game.toml or a
  hand-edited settings.toml). It also returns what settings.toml saves:
  no internal_resolution key unless a preset is kept.

internal_resolution_test drives seed -> launcher -> adopt for both
launcher kinds, first boot and soft-return, against a model of the
legacy row (clamp to 1..4, cycle); keeping the old sticky preset fails
it. gl_internal_scale_guards pins both exits to the helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-09-26 03:24:11 -07:00
parent ecda594996
commit bb2864bc60
5 changed files with 267 additions and 35 deletions
+8
View File
@@ -478,6 +478,14 @@ same file still gets the nearest factor it supports. A legacy factor with no
matching preset appears in the launcher as its own entry, for example
"2x (480 lines)". `settings.toml` `window_width` accepts 640 to 7680.
A host built against a recomp-ui without the Internal resolution row keeps
the legacy Supersampling row (1x to 4x) and that row stays in charge: with no
preset configured the launcher round trip is exactly the historical one, and
no `internal_resolution` key is written. A preset from `game.toml` or a
hand-edited `settings.toml` starts the row on the nearest factor it can show
and survives if the row is left alone; picking a different factor there
drops the preset.
`PSX_INTERNAL_RESOLUTION=<preset or lines>` overrides every layer for one
run (validation, and two local netplay peers that share one `settings.toml`).
It is never written to `settings.toml`: the launcher shows and saves the
+67
View File
@@ -126,3 +126,70 @@ static inline int psx_ir_from_supersampling(int n, int ref_lines) {
long lines = (long)ref_lines * n;
return lines > PSX_IR_MAX_LINES ? PSX_IR_MAX_LINES : (int)lines;
}
/* ---- The launcher round trip -------------------------------------------------
* The host seeds the launcher with its video state, the player may change it,
* and the host adopts what comes back. A recomp-ui built with the Internal
* resolution row (RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) owns the preset.
* An older one only has the legacy Supersampling row, a 1x..4x cycle that
* clamps whatever it is seeded with. There a pick in that row must win over
* any preset, and a preset the player cannot see (game.toml, a hand-edited
* settings.toml) must survive an untouched row. With no preset the round trip
* is exactly the historical one: seed the factor, take the factor back. */
#define PSX_IR_LEGACY_SS_MAX 4 /* the legacy row's range, and settings.toml's
* supersampling for an older runtime */
/* The value seeded into the legacy Supersampling row. has_row: the launcher
* shows the Internal resolution row instead (the factor passes through). */
static inline int psx_ir_launcher_seed_supersampling(int has_row, int preset,
int supersampling,
int ref_lines,
int display_px_h) {
if (has_row || preset == PSX_IR_UNSET) return supersampling;
/* The preset's scale, as near as the row can show it. */
return psx_resolve_internal_scale(preset, ref_lines, display_px_h,
PSX_IR_LEGACY_SS_MAX);
}
typedef struct PsxIrAdopted {
int preset; /* the preset kept (PSX_IR_UNSET: the factor stands) */
int scale; /* the scale to request */
int save_ss; /* settings.toml supersampling */
int save_ir; /* settings.toml internal_resolution; PSX_IR_UNSET omits it */
} PsxIrAdopted;
/* What the host adopts when the launcher returns.
* has_row the launcher showed the Internal resolution row
* preset the host's preset when it seeded the launcher
* ss_seed what psx_ir_launcher_seed_supersampling returned
* ss_result the Supersampling row's value on return
* ir_result the Internal resolution row's value (has_row only)
* s_max the chosen renderer's ceiling */
static inline PsxIrAdopted psx_ir_adopt_launcher(int has_row, int preset,
int ss_seed, int ss_result,
int ir_result, int ref_lines,
int display_px_h, int s_max) {
PsxIrAdopted a;
if (has_row) {
if (psx_ir_value_valid(ir_result)) a.preset = ir_result;
else if (preset != PSX_IR_UNSET) a.preset = preset;
else a.preset = psx_ir_from_supersampling(ss_seed < 1 ? 1 : ss_seed, ref_lines);
} else {
int shown = ss_seed < 1 ? 1 : ss_seed > PSX_IR_LEGACY_SS_MAX
? PSX_IR_LEGACY_SS_MAX : ss_seed;
a.preset = (preset != PSX_IR_UNSET && ss_result == shown) ? preset
: PSX_IR_UNSET;
}
if (a.preset == PSX_IR_UNSET) {
a.scale = ss_result;
a.save_ss = ss_result;
a.save_ir = PSX_IR_UNSET;
} else {
a.scale = psx_resolve_internal_scale(a.preset, ref_lines, display_px_h, s_max);
/* An older runtime reading settings.toml only knows supersampling
* (1..4): leave it the nearest it can do. */
a.save_ss = a.scale < PSX_IR_LEGACY_SS_MAX ? a.scale : PSX_IR_LEGACY_SS_MAX;
a.save_ir = a.preset;
}
return a;
}
+53 -34
View File
@@ -96,6 +96,13 @@ extern "C" void psx_event_step_conservative_env_init(void);
#if defined(RECOMP_LAUNCHER)
#include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher */
/* A recomp-ui that shows the Internal resolution row; an older one only has
* the legacy Supersampling row (internal_resolution.h, launcher round trip). */
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
static constexpr int kLauncherHasInternalResolution = 1;
#else
static constexpr int kLauncherHasInternalResolution = 0;
#endif
#include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */
#include "launcher_boot_timing.h" /* PSX_LAUNCHER_BOOT_TIMING stamps */
#if defined(PSX_HAS_CODEGEN_SETUP_HOST)
@@ -1228,6 +1235,9 @@ static int g_video_requested_scale = 1;
static bool g_video_scale_applies = true;
static bool g_video_hidpi_window = false;
static_assert(PSX_IR_LEGACY_SS_MAX == SW_MAX_INTERNAL_SCALE,
"settings.toml supersampling for an older runtime is the software ceiling");
static int video_scale_ceiling(void) {
return g_video_renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE;
}
@@ -1251,7 +1261,7 @@ static void apply_internal_resolution(int display_px_h) {
/* The value the launcher row starts on: the preset, or the legacy factor shown
* as the preset it matches (3 -> 720p) or as its own "Nx" entry. */
static int internal_resolution_for_launcher(void) {
[[maybe_unused]] static int internal_resolution_for_launcher(void) {
if (g_video_internal_res != PSX_IR_UNSET) return g_video_internal_res;
return psx_ir_from_supersampling(g_video_scale < 1 ? 1 : g_video_scale, g_video_ref_lines);
}
@@ -13904,9 +13914,19 @@ int main(int argc, char** argv) {
std::filesystem::path match_session_bios_path;
bool match_session_bios_set = false;
seed.renderer = g_video_renderer; seed.has_renderer = true;
seed.supersampling = g_video_scale; seed.has_supersampling = true;
/* Internal resolution round trip (internal_resolution.h): only a
* launcher with the row seeds and returns the preset. An older one
* gets the legacy Supersampling row, and a pick there wins. */
const int ir_preset_seeded = g_video_internal_res;
const int ir_ss_seeded = psx_ir_launcher_seed_supersampling(
kLauncherHasInternalResolution, g_video_internal_res, g_video_scale,
g_video_ref_lines, psx_sdl_display_pixel_height(nullptr));
seed.supersampling = ir_ss_seeded; seed.has_supersampling = true;
int ir_row_result = PSX_IR_UNSET; /* the row's pick, when it has one */
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
seed.internal_resolution = internal_resolution_for_launcher();
seed.has_internal_resolution = true;
#endif
seed.antialiasing = g_video_aa; seed.has_antialiasing = true;
seed.texture_filter = g_video_texfilter; seed.has_texture_filter = true;
seed.fmv_filter = g_video_fmv_filter; seed.has_fmv_filter = true;
@@ -14407,10 +14427,7 @@ int main(int argc, char** argv) {
seed.renderer = ls.renderer; seed.has_renderer = true;
seed.supersampling = ls.supersampling; seed.has_supersampling = true;
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
if (psx_ir_value_valid(ls.internal_resolution)) {
seed.internal_resolution = ls.internal_resolution;
seed.has_internal_resolution = true;
}
ir_row_result = ls.internal_resolution;
#endif
seed.antialiasing = ls.antialiasing != 0; seed.has_antialiasing = true;
seed.geometry_correction = ls.geometry_correction != 0;
@@ -14645,15 +14662,16 @@ int main(int argc, char** argv) {
g_netplay_from_lobby = 0;
}
g_video_renderer = seed.renderer;
g_video_scale = seed.supersampling;
if (seed.has_internal_resolution &&
psx_ir_value_valid(seed.internal_resolution)) {
g_video_internal_res = seed.internal_resolution;
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
/* An older runtime reading this settings.toml only knows
* supersampling (1..4): leave it the nearest it can do. */
seed.supersampling = g_video_scale < SW_MAX_INTERNAL_SCALE
? g_video_scale : SW_MAX_INTERNAL_SCALE;
{
const PsxIrAdopted ir = psx_ir_adopt_launcher(
kLauncherHasInternalResolution, ir_preset_seeded, ir_ss_seeded,
seed.supersampling, ir_row_result, g_video_ref_lines,
psx_sdl_display_pixel_height(nullptr), video_scale_ceiling());
g_video_internal_res = ir.preset;
g_video_scale = ir.scale;
seed.supersampling = ir.save_ss;
seed.has_internal_resolution = ir.save_ir != PSX_IR_UNSET;
seed.internal_resolution = ir.save_ir;
}
g_video_aa = seed.antialiasing;
g_video_texfilter = seed.texture_filter;
@@ -16250,7 +16268,12 @@ soft_return_lobby:
ls.renderer = g_video_renderer;
if (ls.renderer < 0 || ls.renderer > (vulkan_offered ? 2 : 1))
ls.renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
ls.supersampling = g_video_scale;
/* Internal resolution round trip, as at first boot. */
const int ir_preset_seeded = g_video_internal_res;
const int ir_ss_seeded = psx_ir_launcher_seed_supersampling(
kLauncherHasInternalResolution, g_video_internal_res, g_video_scale,
g_video_ref_lines, psx_sdl_display_pixel_height(nullptr));
ls.supersampling = ir_ss_seeded;
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
ls.internal_resolution = internal_resolution_for_launcher();
#endif
@@ -16534,6 +16557,15 @@ soft_return_lobby:
player_deadzone[i] = ls.deadzone[i] * 32767 / 100;
}
}
int ir_row_result = PSX_IR_UNSET;
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
ir_row_result = ls.internal_resolution;
#endif
const PsxIrAdopted ir = psx_ir_adopt_launcher(
kLauncherHasInternalResolution, ir_preset_seeded, ir_ss_seeded,
ls.supersampling, ir_row_result, g_video_ref_lines,
psx_sdl_display_pixel_height(nullptr),
ls.renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE);
/* Persist controller (and rematch video) choices without wiping
* the rest of settings.toml — merge into the on-disk file. */
{
@@ -16597,19 +16629,10 @@ soft_return_lobby:
}
us.renderer = ls.renderer;
us.has_renderer = true;
us.supersampling = ls.supersampling;
us.supersampling = ir.save_ss;
us.has_supersampling = true;
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
if (psx_ir_value_valid(ls.internal_resolution)) {
us.internal_resolution = ls.internal_resolution;
us.has_internal_resolution = true;
int s = psx_resolve_internal_scale(ls.internal_resolution, g_video_ref_lines,
psx_sdl_display_pixel_height(nullptr),
ls.renderer == 1 ? GL_MAX_INTERNAL_SCALE
: SW_MAX_INTERNAL_SCALE);
us.supersampling = s < SW_MAX_INTERNAL_SCALE ? s : SW_MAX_INTERNAL_SCALE;
}
#endif
us.internal_resolution = ir.save_ir;
us.has_internal_resolution = ir.save_ir != PSX_IR_UNSET;
us.antialiasing = ls.antialiasing != 0;
us.has_antialiasing = true;
us.texture_filter = ls.texture_filter;
@@ -16685,12 +16708,8 @@ soft_return_lobby:
(void)PSXRecompV4::save_user_settings(settings_path, us);
}
g_video_renderer = ls.renderer;
g_video_scale = ls.supersampling;
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
if (psx_ir_value_valid(ls.internal_resolution))
g_video_internal_res = ls.internal_resolution;
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
#endif
g_video_internal_res = ir.preset;
g_video_scale = ir.scale;
g_video_aa = ls.antialiasing;
g_video_texfilter = ls.texture_filter;
g_video_fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter);
+14 -1
View File
@@ -81,7 +81,8 @@ class InternalResolutionGuards(unittest.TestCase):
def test_vocabulary_is_optional_abi(self):
# Builds against an older recomp-ui must still compile: every use of
# the new launcher fields sits behind the capability macro.
for field in ("gi->internal_resolution_labels", "ls.internal_resolution"):
for field in ("gi->internal_resolution_labels", "ls.internal_resolution",
"= internal_resolution_for_launcher();"):
for m in re.finditer(re.escape(field), MAIN):
before = MAIN[:m.start()]
opened = before.count("#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)")
@@ -94,6 +95,18 @@ class InternalResolutionGuards(unittest.TestCase):
self.assertIn("if (preset == PSX_IR_UNSET) return;",
body(MAIN, "static void apply_internal_resolution(int display_px_h)"))
def test_launcher_trips_use_the_round_trip_helpers(self):
# Both launcher exits (first boot, netplay soft-return) seed and adopt
# through internal_resolution.h, whose behaviour with and without the
# Internal resolution row is unit-tested (internal_resolution_test).
# A bare factor copy next to a sticky preset let the preset override
# a pick in an older launcher's Supersampling row.
self.assertEqual(MAIN.count("psx_ir_launcher_seed_supersampling("), 2)
self.assertEqual(MAIN.count("psx_ir_adopt_launcher("), 2)
self.assertEqual(MAIN.count("kLauncherHasInternalResolution, ir_preset_seeded, ir_ss_seeded,"), 2)
self.assertNotRegex(MAIN, r"g_video_scale\s*=\s*(seed|ls)\.supersampling;")
self.assertIn("g_video_internal_res = ir.preset;", MAIN)
def test_env_override_is_never_persisted(self):
# PSX_INTERNAL_RESOLUTION wins for the run only: the launcher shows and
# settings.toml saves the configured preset.
+125
View File
@@ -17,6 +17,129 @@ static void expect(const char* what, long long got, long long want) {
}
}
// ---- Launcher round trip: seed -> launcher -> adopt -------------------------
// A model of recomp-ui's legacy Supersampling row (before the Internal
// resolution row): launcher_model clamps the seeded value to 1..4 on load and
// each click cycles 1 -> 2 -> 3 -> 4 -> 1.
static int legacy_row(int seeded, int clicks) {
int v = seeded < 1 ? 1 : seeded > 4 ? 4 : seeded;
for (int i = 0; i < clicks; i++) v = (v % 4) + 1;
return v;
}
struct Host { // the host's video state between launcher trips
int preset; // g_video_internal_res
int scale; // g_video_scale
};
// One trip, the way main.cpp runs it at first boot and on the netplay
// soft-return. has_row = RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION.
// Legacy: ss_clicks cycles the Supersampling row. Row: row_pick > 0 picks a
// preset in the Internal resolution row (0 leaves it).
static PsxIrAdopted trip(Host& h, int has_row, int ss_clicks, int row_pick,
int s_max = 32, int display_px_h = 2160) {
const int seeded_preset = h.preset;
const int ss_seed = psx_ir_launcher_seed_supersampling(has_row, h.preset, h.scale,
240, display_px_h);
const int ss_result = has_row ? ss_seed : legacy_row(ss_seed, ss_clicks);
int ir_result = PSX_IR_UNSET;
if (has_row) {
const int shown = h.preset != PSX_IR_UNSET
? h.preset : psx_ir_from_supersampling(h.scale, 240);
ir_result = row_pick ? row_pick : shown;
}
PsxIrAdopted a = psx_ir_adopt_launcher(has_row, seeded_preset, ss_seed, ss_result,
ir_result, 240, display_px_h, s_max);
h.preset = a.preset;
h.scale = a.scale;
return a;
}
static void launcher_round_trips() {
// Older recomp-ui (no Internal resolution row), nothing configured: the
// historical round trip. The player's Supersampling pick is what runs and
// what is saved, and no internal_resolution key appears.
{
Host h{PSX_IR_UNSET, 1};
PsxIrAdopted a = trip(h, 0, 2, 0); // 1x -> 3x
expect("legacy pick scale", a.scale, 3);
expect("legacy pick preset", a.preset, PSX_IR_UNSET);
expect("legacy pick save ss", a.save_ss, 3);
expect("legacy pick no ir key", a.save_ir, PSX_IR_UNSET);
// Netplay soft-return: seeded with the running scale, picked again.
a = trip(h, 0, 1, 0); // 3x -> 4x
expect("legacy soft-return pick", a.scale, 4);
expect("legacy soft-return no ir key", a.save_ir, PSX_IR_UNSET);
a = trip(h, 0, 0, 0); // untouched
expect("legacy untouched", a.scale, 4);
}
{
Host h{PSX_IR_UNSET, 8}; // hand-edited 8
PsxIrAdopted a = trip(h, 0, 0, 0);
expect("legacy clamps like before", a.scale, 4); // the row returns 4
expect("legacy clamps save", a.save_ss, 4);
}
// A settings.toml "native" written by a build with the row, then an older
// launcher: the preset must not reset the player's pick (the reported bug).
{
Host h{PSX_IR_NATIVE, 1};
PsxIrAdopted a = trip(h, 0, 2, 0); // 1x -> 3x
expect("native preset yields to pick", a.scale, 3);
expect("native preset dropped", a.preset, PSX_IR_UNSET);
expect("native preset key dropped", a.save_ir, PSX_IR_UNSET);
expect("native preset save ss", a.save_ss, 3);
}
// A preset the older launcher cannot show (game.toml or hand-edited 4K):
// the row starts on the nearest it can show (4x) and an untouched row
// keeps the preset, first boot and soft-return alike.
{
Host h{2160, 1};
PsxIrAdopted a = trip(h, 0, 0, 0);
expect("hidden preset kept", a.preset, 2160);
expect("hidden preset scale", a.scale, 9);
expect("hidden preset save ss", a.save_ss, 4);
expect("hidden preset save ir", a.save_ir, 2160);
a = trip(h, 0, 0, 0); // soft-return, running 9x
expect("hidden preset soft-return kept", a.scale, 9);
a = trip(h, 0, 1, 0); // 4x -> 1x
expect("hidden preset replaced", a.scale, 1);
expect("hidden preset replaced preset", a.preset, PSX_IR_UNSET);
expect("hidden preset replaced key", a.save_ir, PSX_IR_UNSET);
}
{
Host h{PSX_IR_DISPLAY, 1}; // 2160-px display
PsxIrAdopted a = trip(h, 0, 0, 0);
expect("display kept", a.scale, 9);
a = trip(h, 0, 2, 0); // 4x -> 1x -> 2x
expect("display replaced", a.scale, 2);
}
// Launcher with the row: the row owns the preset, as before.
{
Host h{PSX_IR_UNSET, 3};
PsxIrAdopted a = trip(h, 1, 0, 0); // shows 720p, untouched
expect("row untouched preset", a.preset, 720);
expect("row untouched scale", a.scale, 3);
expect("row untouched save", a.save_ir, 720);
a = trip(h, 1, 0, 4320); // 8K
expect("row 8k scale", a.scale, 18);
expect("row 8k save ss", a.save_ss, 4);
expect("row 8k save ir", a.save_ir, 4320);
a = trip(h, 1, 0, 4320, 4); // software ceiling
expect("row 8k software", a.scale, 4);
a = trip(h, 1, 0, PSX_IR_NATIVE);
expect("row native", a.scale, 1);
expect("row native save", a.save_ir, PSX_IR_NATIVE);
}
// Invalid row values never reach the host: the seeded preset stands.
{
PsxIrAdopted a = psx_ir_adopt_launcher(1, 1080, 1, 1, 0, 240, 0, 32);
expect("row invalid keeps preset", a.preset, 1080);
a = psx_ir_adopt_launcher(1, PSX_IR_UNSET, 2, 2, -7, 240, 0, 32);
expect("row invalid legacy factor", a.preset, 480);
expect("row invalid legacy scale", a.scale, 2);
}
}
int main() {
// Presets at the NTSC 240-line reference (R4 races are 320x240).
struct { const char* id; int s240; int s480; } table[] = {
@@ -77,6 +200,8 @@ int main() {
expect("legacy round trip",
psx_resolve_internal_scale(psx_ir_from_supersampling(n, 240), 240, 0, 32), n);
launcher_round_trips();
if (g_failures) return 1;
std::printf("internal_resolution: all checks passed\n");
return 0;