Files
psxrecomp/recompiler/tests/quality_presets_test.cpp
Shokunin b279afdc4a In-game presets: draw-thread-safe callbacks and a coherent menu seed
P1: quality_launcher_apply is now a pure preset -> rows mapping
(psxq::rows_from_runtime plus startup constants); it never writes the live
globals. The in-game menu publishes only thread-safe callbacks: that
mapper, and a re-detect that returns this session's detection (a fresh
probe needs the GL context and mutates the run's state, so it stays in the
pre-game launcher). Summary/reason strings and the detection are fixed on
the main thread at menu setup; settings_get reads only the main-thread
snapshot.

P2: psxq::MenuSelection keeps what the player selected versus what is live.
A pick's next-launch rows and netplay-deferred rows stay selected for the
next seed until the live value matches; other rows follow live. The host
classification moves to psxq::classify_launcher/row_differs (same rules).

Tests: quality_presets_test test_ingame_menu_host_path runs the review's
fixture through the host get/pick/apply/save path (Ultra -> High, then
Antialiasing = NONE; a governed edit is still Custom from High; a netplay-
deferred row stays selected). menu_quality_callbacks_test guards the
draw-thread callbacks against runtime writes and stateful calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:09:49 -07:00

786 lines
40 KiB
C++

/* quality_presets_test -- graphics presets (docs/QUALITY_PRESETS.md).
*
* 1. [quality.*] tables parse as [video] laid over [video], with validation;
* an unknown preset name or a bad value is an error;
* 2. detection rules: Steam Deck, Apple M1 (8 GB), Intel UHD and software
* GL are Low; an M4 or a discrete RTX is Ultra; cores and memory cap;
* 3. the decision: first launch detects and persists; same hardware keeps
* the saved preset; new hardware re-detects a preset but never touches
* Custom; PSX_QUALITY overrides one run without persisting;
* 4. a preset in force masks the player's saved values for its keys;
* 5. settings.toml round-trips the quality_* keys.
*/
#include "config_loader.h"
#include "quality_presets.h"
#include "host_overlay.h"
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
namespace fs = std::filesystem;
static int failures = 0;
static void check(bool cond, const char* what) {
if (!cond) {
std::fprintf(stderr, "FAIL: %s\n", what);
++failures;
}
}
static fs::path write_temp(const std::string& name, const std::string& body) {
fs::path p = fs::temp_directory_path() / name;
std::ofstream f(p, std::ios::binary | std::ios::trunc);
f << body;
return p;
}
static fs::path write_game_toml(const std::string& name, const std::string& tail) {
return write_temp(name,
"[game]\n"
"name = \"probe\"\n"
"exe = \"probe.exe\"\n"
"load_address = \"0x80010000\"\n"
"entry_pc = \"0x80010000\"\n"
"text_size = \"0x1000\"\n"
"[recompiler]\n"
"seeds = \"seeds.json\"\n"
"[runtime]\n"
"[video]\n"
"supersample = 1.5\n"
"frame_generation = true\n"
"render_thread = true\n"
"dynamic_resolution = true\n"
"dynamic_resolution_min = \"display\"\n"
"pgxp_depth_buffer = true\n"
"pgxp_color_correction = true\n"
"pgxp_seam = \"fine\"\n"
+ tail);
}
static const char* kPresets =
"[quality.ultra]\n"
"[quality.medium]\n"
"supersample = 1.0\n"
"dynamic_resolution_min = \"720p\"\n"
"[quality.low]\n"
"supersample = 1.0\n"
"frame_generation = false\n"
"dynamic_resolution_min = \"native\"\n"
"pgxp_depth_buffer = false\n"
"pgxp_color_correction = false\n"
"pgxp_seam = \"off\"\n";
static void test_parse() {
fs::path p = write_game_toml("psxrecomp_quality_parse.toml", kPresets);
auto gc = PSXRecompV4::load_game_config(p);
check(gc.quality_presets.size() == 3, "three presets parsed");
check(gc.quality_presets[0].name == "low" && gc.quality_presets[2].name == "ultra",
"presets ordered lowest first");
const auto& low = gc.quality_presets[0];
check(low.runtime.video_supersample_milli == 1000, "low: supersample 1.0");
check(!low.runtime.video_frame_generation, "low: frame_generation off");
check(low.runtime.video_render_thread, "low inherits [video] render_thread");
check(!low.runtime.video_pgxp_depth_buffer && !low.runtime.video_pgxp_color_correction &&
low.runtime.video_pgxp_seam == 0, "low: PGXP extras off");
check(gc.quality_presets[1].runtime.video_pgxp_depth_buffer &&
gc.quality_presets[1].runtime.video_pgxp_seam == 1,
"medium inherits the [video] PGXP extras");
check(gc.quality_presets[2].runtime.video_supersample_milli == 1500,
"ultra (empty) is the [video] block");
check(gc.runtime.video_supersample_milli == 1500, "gc.runtime stays the [video] block");
check(psxq::offered_mask(gc) == 0xBu, "offered mask low|medium|ultra");
auto keys = psxq::governed_keys(gc);
check(keys.size() == 6, "governed keys are the union of preset keys");
fs::remove(p);
fs::path bad = write_game_toml("psxrecomp_quality_badname.toml", "[quality.epic]\n");
bool threw = false;
try { (void)PSXRecompV4::load_game_config(bad); } catch (const std::exception&) { threw = true; }
check(threw, "unknown preset name is rejected");
fs::remove(bad);
fs::path badv = write_game_toml("psxrecomp_quality_badvalue.toml",
"[quality.low]\ndynamic_resolution_min = \"huge\"\n");
threw = false;
try { (void)PSXRecompV4::load_game_config(badv); } catch (const std::exception&) { threw = true; }
check(threw, "a preset value gets [video] validation");
fs::remove(badv);
fs::path none = write_game_toml("psxrecomp_quality_none.toml", "");
auto gn = PSXRecompV4::load_game_config(none);
check(gn.quality_presets.empty() && psxq::offered_mask(gn) == 0,
"no [quality] tables: no presets");
fs::remove(none);
}
static PsxHostInfo host(const char* cpu, const char* gpu, int cores, unsigned gb, int deck = 0) {
PsxHostInfo h;
std::memset(&h, 0, sizeof h);
std::snprintf(h.cpu, sizeof h.cpu, "%s", cpu);
std::snprintf(h.gpu, sizeof h.gpu, "%s", gpu);
h.logical_cores = cores;
h.ram_mb = (uint64_t)gb * 1024u;
h.steam_deck = deck;
return h;
}
static int tier(const PsxHostInfo& h) { return psx_quality_classify(&h, nullptr, 0); }
static void test_classify() {
check(tier(host("Apple M1", "Apple M1", 8, 8)) == PSX_QUALITY_LOW, "M1: Low");
check(tier(host("Apple M4", "Apple M4", 10, 16)) == PSX_QUALITY_ULTRA, "M4: Ultra");
check(tier(host("Apple M1 Max", "Apple M1 Max", 10, 32)) == PSX_QUALITY_ULTRA, "M1 Max: Ultra");
check(tier(host("Apple M2", "Apple M2", 8, 16)) == PSX_QUALITY_LOW, "M2 base: Low");
check(tier(host("Apple M3", "Apple M3", 8, 16)) == PSX_QUALITY_ULTRA, "M3: Ultra");
check(tier(host("AMD Custom APU 0405", "", 8, 16)) == PSX_QUALITY_LOW, "Deck by CPU: Low");
check(tier(host("AMD Custom APU 0932",
"AMD Custom GPU 0932 (radeonsi, vangogh, LLVM 15)", 8, 16)) == PSX_QUALITY_LOW,
"Deck OLED: Low");
check(tier(host("x", "", 8, 16, 1)) == PSX_QUALITY_LOW, "Deck by DMI: Low");
check(tier(host("Intel(R) Core(TM) i5-8250U", "Intel(R) UHD Graphics 620", 8, 16)) ==
PSX_QUALITY_LOW, "Intel UHD: Low");
check(tier(host("i7", "Intel(R) Iris(R) Xe Graphics", 8, 16)) == PSX_QUALITY_LOW,
"Iris Xe: Low");
check(tier(host("AMD Ryzen 9 7950X", "NVIDIA GeForce RTX 4080 SUPER/PCIe/SSE2", 32, 64)) ==
PSX_QUALITY_ULTRA, "RTX 4080: Ultra");
check(tier(host("x", "llvmpipe (LLVM 17.0.6, 256 bits)", 16, 32)) == PSX_QUALITY_LOW,
"software GL: Low");
check(tier(host("x", "Some Future GPU", 16, 32)) == PSX_QUALITY_MEDIUM, "unknown GPU: Medium (conservative)");
check(tier(host("AMD Ryzen 7 9800X3D 8-Core Processor", "AMD Radeon(TM) Graphics", 16, 48)) ==
PSX_QUALITY_LOW, "2-CU Radeon iGPU: Low");
check(tier(host("x", "NVIDIA GeForce RTX 3060", 4, 16)) == PSX_QUALITY_LOW,
"4 threads: Low");
check(tier(host("x", "NVIDIA GeForce RTX 3060", 2, 16)) == PSX_QUALITY_LOW,
"2 threads cap at Low");
check(tier(host("x", "NVIDIA GeForce RTX 3060", 12, 8)) == PSX_QUALITY_ULTRA,
"8 GB does not cap");
check(tier(host("x", "NVIDIA GeForce RTX 3060", 12, 4)) == PSX_QUALITY_LOW,
"4 GB caps at Low");
check(psx_quality_pick_offered(PSX_QUALITY_HIGH, 0xBu) == PSX_QUALITY_MEDIUM,
"High not offered: next lower");
check(psx_quality_pick_offered(PSX_QUALITY_LOW, 0xCu) == PSX_QUALITY_HIGH,
"nothing lower: next higher");
check(psx_quality_from_name("Ultra") == PSX_QUALITY_ULTRA, "names are case-insensitive");
check(psx_quality_from_name("custom") == PSX_QUALITY_CUSTOM, "custom name");
}
/* Real GL_RENDERER strings: Windows drivers, Mesa (Linux / SteamOS) and
* macOS. 16 threads and 32 GB, so only the GPU class decides. */
static void test_renderer_table() {
struct Row { const char* renderer; int want; };
static const Row rows[] = {
/* Windows (AMD Adrenalin, NVIDIA, Intel) */
{"AMD Radeon(TM) Graphics", PSX_QUALITY_LOW},
{"AMD Radeon RX Vega 11 Graphics", PSX_QUALITY_LOW},
{"AMD Radeon(TM) Vega 8 Graphics", PSX_QUALITY_LOW},
{"AMD Radeon(TM) R7 Graphics", PSX_QUALITY_LOW},
{"AMD Radeon 780M Graphics", PSX_QUALITY_LOW},
{"AMD Radeon(TM) 890M Graphics", PSX_QUALITY_LOW},
{"AMD Radeon RX 6800 XT", PSX_QUALITY_ULTRA},
{"AMD Radeon RX 7600M XT", PSX_QUALITY_ULTRA},
{"AMD Radeon(TM) RX 580 Series", PSX_QUALITY_ULTRA},
{"Radeon RX Vega 64", PSX_QUALITY_ULTRA},
{"AMD Radeon Pro W6800", PSX_QUALITY_ULTRA},
{"AMD Radeon(TM) 8060S Graphics", PSX_QUALITY_ULTRA},
{"NVIDIA GeForce RTX 4080 SUPER/PCIe/SSE2", PSX_QUALITY_ULTRA},
{"NVIDIA GeForce GTX 1060 6GB/PCIe/SSE2", PSX_QUALITY_ULTRA},
{"NVIDIA GeForce MX450/PCIe/SSE2", PSX_QUALITY_LOW},
{"NVIDIA GeForce GT 1030/PCIe/SSE2", PSX_QUALITY_LOW},
{"Intel(R) UHD Graphics 620", PSX_QUALITY_LOW},
{"Intel(R) Iris(R) Xe Graphics", PSX_QUALITY_LOW},
{"Intel(R) Arc(TM) Graphics", PSX_QUALITY_LOW},
{"Intel(R) Arc(TM) A770 Graphics", PSX_QUALITY_ULTRA},
{"GDI Generic", PSX_QUALITY_LOW},
{"Microsoft Basic Render Driver", PSX_QUALITY_LOW},
/* Mesa */
{"AMD Radeon Graphics (radeonsi, renoir, LLVM 15.0.7, DRM 3.54, 6.5.0)", PSX_QUALITY_LOW},
{"AMD Radeon Vega 8 Graphics (radeonsi, raven, LLVM 15.0.7, DRM 3.42)", PSX_QUALITY_LOW},
{"AMD Radeon 780M (radeonsi, gfx1103_r1, LLVM 17.0.6, DRM 3.57)", PSX_QUALITY_LOW},
{"AMD Custom GPU 0405 (radeonsi, vangogh, LLVM 15.0.7, DRM 3.49, 6.1.52-valve16-1-neptune-61)",
PSX_QUALITY_LOW},
{"AMD Custom GPU 0932 (radeonsi, vangogh, ACO, DRM 3.57)", PSX_QUALITY_LOW},
{"AMD Radeon RX 6700 XT (radeonsi, navi22, LLVM 17.0.6, DRM 3.57)", PSX_QUALITY_ULTRA},
{"Mesa Intel(R) UHD Graphics 620 (KBL GT2)", PSX_QUALITY_LOW},
{"Mesa Intel(R) Xe Graphics (TGL GT2)", PSX_QUALITY_LOW},
{"Mesa Intel(R) Arc(tm) Graphics (MTL)", PSX_QUALITY_LOW},
{"Mesa Intel(R) Arc(tm) A750 Graphics (DG2)", PSX_QUALITY_ULTRA},
{"NVIDIA GeForce RTX 3060/PCIe/SSE2", PSX_QUALITY_ULTRA},
{"NV167 (nouveau)", PSX_QUALITY_MEDIUM}, /* unnamed chip: unknown */
{"Virtio-GPU Venus (Some Future GPU)", PSX_QUALITY_MEDIUM},
{"llvmpipe (LLVM 17.0.6, 256 bits)", PSX_QUALITY_LOW},
{"zink Vulkan 1.3(AMD Radeon Graphics (RADV RENOIR) (MESA_RADV))", PSX_QUALITY_LOW},
/* macOS */
{"Apple M1", PSX_QUALITY_LOW},
{"Apple M2", PSX_QUALITY_LOW},
{"Apple M1 Pro", PSX_QUALITY_ULTRA},
{"Apple M2 Max", PSX_QUALITY_ULTRA},
{"Apple M3", PSX_QUALITY_ULTRA},
{"Apple M4", PSX_QUALITY_ULTRA},
{"AMD Radeon Pro 5500M OpenGL Engine", PSX_QUALITY_ULTRA},
{"Intel(R) Iris(TM) Plus Graphics OpenGL Engine", PSX_QUALITY_LOW},
};
for (const Row& r : rows) {
PsxHostInfo h = host("x", r.renderer, 16, 32);
const int got = psx_quality_classify(&h, nullptr, 0);
if (got != r.want) {
std::fprintf(stderr, " renderer \"%s\": got %s, want %s\n", r.renderer,
psx_quality_name(got), psx_quality_name(r.want));
check(false, "renderer table");
}
}
/* Linux without a GL probe: the PCI id. */
PsxHostInfo deck = host("x", "", 8, 16);
std::snprintf(deck.gpu_id, sizeof deck.gpu_id, "0x1002:0x163f");
check(psx_quality_classify(&deck, nullptr, 0) == PSX_QUALITY_LOW, "PCI id: Steam Deck -> Low");
PsxHostInfo uhd = host("x", "", 8, 16);
std::snprintf(uhd.gpu_id, sizeof uhd.gpu_id, "0x8086:0x5917");
check(psx_quality_classify(&uhd, nullptr, 0) == PSX_QUALITY_LOW, "PCI id: Intel iGPU -> Low");
PsxHostInfo arc = host("x", "", 8, 16);
std::snprintf(arc.gpu_id, sizeof arc.gpu_id, "0x8086:0x56a0");
check(psx_quality_classify(&arc, nullptr, 0) == PSX_QUALITY_ULTRA, "PCI id: Intel Arc -> Ultra");
}
static int probe_calls = 0;
static const char* fake_gl = nullptr; /* nullptr: the GL probe fails */
static int fake_probe(char* out, size_t cap) {
++probe_calls;
if (!fake_gl) return 0;
std::snprintf(out, cap, "%s", fake_gl);
return 1;
}
static void test_decide() {
const unsigned all = 0xFu;
PSXRecompV4::UserSettings fresh;
PsxHostInfo m1 = host("Intel(R) Core(TM) i5-8250U", "Intel(R) UHD Graphics 620", 8, 8);
{
/* The GL probe fails: classified from the OS name for this run, not
* saved, so the next launch probes again. */
psxq::Decision np = psxq::decide(all, fresh, m1, nullptr, false, fake_probe);
check(np.detected_now && !np.persist && !np.gl_probed && np.preset == PSX_QUALITY_LOW,
"probe failure: OS-name result used, not persisted");
probe_calls = 0;
/* Headless / hidden window: no detection, nothing saved. */
psxq::Decision hd = psxq::decide(all, fresh, m1, nullptr, false, fake_probe, true);
check(hd.detection_skipped && !hd.persist && hd.preset == PSX_QUALITY_NONE &&
probe_calls == 0, "headless: no detection, no probe, nothing saved");
PSXRecompV4::UserSettings saved_low;
saved_low.has_quality_preset = true; saved_low.quality_preset = "low";
psxq::Decision hs = psxq::decide(all, saved_low, m1, nullptr, false, fake_probe, true);
check(hs.preset == PSX_QUALITY_LOW && !hs.persist,
"headless with a saved preset: the saved preset applies");
}
fake_gl = "Intel(R) UHD Graphics 620";
psxq::Decision d = psxq::decide(all, fresh, m1, nullptr, false, fake_probe);
check(d.detected_now && d.persist, "first launch detects and persists");
check(d.preset == PSX_QUALITY_LOW && d.base == PSX_QUALITY_LOW, "UHD 620 first launch: Low");
check(probe_calls == 1, "a due detection asks GL");
{
PsxHostInfo igpu = host("AMD Ryzen 7 9800X3D 8-Core Processor", "NVIDIA GeForce RTX 4080 SUPER", 16, 48);
fake_gl = "AMD Radeon(TM) Graphics";
psxq::Decision g = psxq::decide(all, fresh, igpu, nullptr, false, fake_probe);
check(g.preset == PSX_QUALITY_LOW,
"GL's renderer wins over the OS adapter name (display on the iGPU)");
fake_gl = nullptr;
psxq::Decision o = psxq::decide(all, fresh, igpu, nullptr, false, fake_probe);
check(o.preset == PSX_QUALITY_ULTRA, "probe failure falls back to the OS name");
}
probe_calls = 0;
PSXRecompV4::UserSettings us;
psxq::record(us, d);
check(us.quality_preset == "low" && us.quality_hardware == d.fingerprint,
"record stores preset and fingerprint");
psxq::Decision again = psxq::decide(all, us, m1, nullptr, false, fake_probe);
check(!again.detected_now && !again.persist && again.preset == PSX_QUALITY_LOW,
"same hardware: saved preset, no detection");
PSXRecompV4::UserSettings chosen = us;
chosen.quality_preset = "high";
psxq::Decision kept = psxq::decide(all, chosen, m1, nullptr, false, fake_probe);
check(kept.preset == PSX_QUALITY_HIGH, "a player's preset pick is kept on the same hardware");
PsxHostInfo m4 = host("Apple M4", "Apple M4", 10, 16);
psxq::Decision moved = psxq::decide(all, us, m4, nullptr, false, fake_probe);
check(moved.detected_now && moved.preset == PSX_QUALITY_ULTRA,
"new hardware re-detects a preset");
PSXRecompV4::UserSettings custom = us;
custom.quality_preset = "custom";
custom.quality_base = "low";
psxq::Decision c = psxq::decide(all, custom, m4, nullptr, false, fake_probe);
check(c.preset == PSX_QUALITY_CUSTOM && c.base == PSX_QUALITY_LOW && !c.persist,
"Custom is never overridden, even on new hardware");
check(c.hardware_changed_custom, "new hardware under Custom is reported");
psxq::Decision re = psxq::decide(all, custom, m4, nullptr, true, fake_probe);
check(re.detected_now && re.preset == PSX_QUALITY_ULTRA, "Re-detect replaces Custom");
psxq::Decision env = psxq::decide(all, us, m1, "medium", false, fake_probe);
check(env.preset == PSX_QUALITY_MEDIUM && env.env_override && !env.persist,
"PSX_QUALITY overrides one run, not saved");
fake_gl = "Intel(R) UHD Graphics 620";
PsxHostInfo linux_box = host("Intel(R) Core(TM) i5-8250U", "", 8, 16);
std::snprintf(linux_box.gpu_id, sizeof linux_box.gpu_id, "0x8086:0x5917");
psxq::Decision gl = psxq::decide(all, fresh, linux_box, nullptr, false, fake_probe);
check(gl.preset == PSX_QUALITY_LOW,
"unnamed GPU: GL probe names it (UHD 620 -> Low)");
PSXRecompV4::UserSettings lus;
psxq::record(lus, gl);
psxq::Decision gl2 = psxq::decide(all, lus, linux_box, nullptr, false, fake_probe);
const int calls = probe_calls;
psxq::Decision gl3 = psxq::decide(all, lus, linux_box, nullptr, false, fake_probe);
check(!gl2.detected_now && !gl3.detected_now && probe_calls == calls,
"fingerprint is OS-only: the probe does not make the next launch re-detect");
psxq::Decision none = psxq::decide(0u, fresh, m1, nullptr, false, fake_probe);
check(none.preset == PSX_QUALITY_NONE && !none.persist, "no presets: no decision");
}
static void test_mask() {
PSXRecompV4::UserSettings us;
us.has_frame_generation = true; us.frame_generation = true;
us.has_dynamic_resolution_min = true;
us.has_internal_resolution = true;
us.has_render_thread = true;
psxq::mask_user_settings(us, {"frame_generation", "dynamic_resolution_min", "supersample"});
check(!us.has_frame_generation && !us.has_dynamic_resolution_min,
"a preset owns its keys");
check(us.has_render_thread && us.has_internal_resolution,
"keys the presets do not set stay the player's");
}
static void test_settings_round_trip() {
PSXRecompV4::UserSettings s;
s.has_quality_preset = true; s.quality_preset = "custom";
s.has_quality_base = true; s.quality_base = "medium";
s.has_quality_hardware = true; s.quality_hardware = "0123456789abcdef";
s.has_quality_detected = true; s.quality_detected = "low";
fs::path p = fs::temp_directory_path() / "psxrecomp_quality_settings.toml";
check(PSXRecompV4::save_user_settings(p, s), "save settings");
auto r = PSXRecompV4::load_user_settings(p);
check(r.has_quality_preset && r.quality_preset == "custom", "quality_preset round-trips");
check(r.has_quality_base && r.quality_base == "medium", "quality_base round-trips");
check(r.has_quality_hardware && r.quality_hardware == "0123456789abcdef",
"quality_hardware round-trips");
check(r.has_quality_detected && r.quality_detected == "low", "quality_detected round-trips");
fs::remove(p);
}
/* The preset/key-ownership transaction (psxq::settle_save), on the two cases
* from the #591 review, through a real settings.toml round trip. */
static PSXRecompV4::UserSettings save_load(const PSXRecompV4::UserSettings& s) {
fs::path p = fs::temp_directory_path() / "psxrecomp_quality_txn.toml";
check(PSXRecompV4::save_user_settings(p, s), "txn: save");
PSXRecompV4::UserSettings r = PSXRecompV4::load_user_settings(p);
fs::remove(p);
return r;
}
/* What the next launch does with the file: decide, then mask while a preset
* (not Custom) is in force (main.cpp quality_preset_in_force). */
static PSXRecompV4::UserSettings next_launch(const PSXRecompV4::UserSettings& disk,
const PsxHostInfo& h,
const std::vector<std::string>& keys,
psxq::Decision* out) {
psxq::Decision d = psxq::decide(0xFu, disk, h, nullptr, false, nullptr);
PSXRecompV4::UserSettings us = disk;
if (d.preset >= 0 && d.preset < PSX_QUALITY_COUNT) psxq::mask_user_settings(us, keys);
if (out) *out = d;
return us;
}
static void test_ownership_txn() {
const std::vector<std::string> keys = {"texture_filtering", "frame_generation",
"dynamic_resolution_min"};
PsxHostInfo m4 = host("Apple M4", "Apple M4", 10, 16);
char fp[17];
psx_quality_fingerprint(&m4, fp);
/* Saved Ultra on this machine. */
PSXRecompV4::UserSettings ultra;
ultra.has_quality_preset = true; ultra.quality_preset = "ultra";
ultra.has_quality_base = true; ultra.quality_base = "ultra";
ultra.has_quality_hardware = true; ultra.quality_hardware = fp;
ultra.has_quality_detected = true; ultra.quality_detected = "ultra";
ultra.has_scanlines = true; ultra.scanlines = false;
/* Review case 1: PSX_QUALITY=low for one run, Settings opened and quit
* with nothing changed. The boot launcher's seed is built empty and
* filled with this run's (Low) values. */
{
psxq::Decision run = psxq::decide(0xFu, ultra, m4, "low", false, nullptr);
check(run.env_override && run.preset == PSX_QUALITY_LOW, "txn1: override in force");
PSXRecompV4::UserSettings seed; /* empty boot seed */
seed.has_texture_filter = true; seed.texture_filter = 1; /* Low: bilinear */
seed.has_frame_generation = true; seed.frame_generation = false;
seed.has_scanlines = true; seed.scanlines = false;
psxq::settle_save(seed, ultra, run, psxq::Edit::NONE, PSX_QUALITY_NONE, keys);
PSXRecompV4::UserSettings disk = save_load(seed);
check(disk.quality_preset == "ultra" && disk.quality_base == "ultra",
"txn1: a one-run override never persists the preset");
check(!disk.has_texture_filter && !disk.has_frame_generation,
"txn1: nor the override's governed values (empty seed)");
psxq::Decision nd;
next_launch(disk, m4, keys, &nd);
check(nd.preset == PSX_QUALITY_ULTRA, "txn1: next launch is Ultra again");
/* Even a governed edit under the override applies this run only. */
PSXRecompV4::UserSettings edited = seed;
edited.texture_filter = 0;
psxq::Decision run2 = psxq::decide(0xFu, ultra, m4, "low", false, nullptr);
psxq::settle_save(edited, ultra, run2, psxq::Edit::GOVERNED_EDIT, PSX_QUALITY_NONE, keys);
check(edited.quality_preset == "ultra" && !edited.has_texture_filter,
"txn1: a governed edit under the override is not saved");
}
/* The override keeps a previous Custom profile and its values. */
{
PSXRecompV4::UserSettings custom = ultra;
custom.quality_preset = "custom";
custom.has_texture_filter = true; custom.texture_filter = 1;
psxq::Decision run = psxq::decide(0xFu, custom, m4, "low", false, nullptr);
PSXRecompV4::UserSettings seed;
seed.has_texture_filter = true; seed.texture_filter = 0;
psxq::settle_save(seed, custom, run, psxq::Edit::NONE, PSX_QUALITY_NONE, keys);
PSXRecompV4::UserSettings disk = save_load(seed);
check(disk.quality_preset == "custom" && disk.quality_base == "ultra" &&
disk.has_texture_filter && disk.texture_filter == 1,
"txn1: Custom and its values survive a one-run override");
}
/* Review case 2: a live in-game change to a governed value (texture
* filter) while Ultra is in force becomes Custom and survives restart. */
{
psxq::Decision run = psxq::decide(0xFu, ultra, m4, nullptr, false, nullptr);
PsxHostVideoSettings cur{}, next{};
cur.texture_filter = 0; next = cur; next.texture_filter = 1;
const psxq::Edit e = psxq::live_edit(cur, next, keys);
check(e == psxq::Edit::GOVERNED_EDIT, "txn2: texture filter is a governed live edit");
PSXRecompV4::UserSettings us = ultra; /* the live path loads disk */
us.has_texture_filter = true; us.texture_filter = 1;
check(psxq::settle_save(us, ultra, run, e, PSX_QUALITY_NONE, keys),
"txn2: the state changes");
PSXRecompV4::UserSettings disk = save_load(us);
check(disk.quality_preset == "custom" && disk.quality_base == "ultra",
"txn2: a governed live edit becomes Custom from Ultra");
psxq::Decision nd;
PSXRecompV4::UserSettings eff = next_launch(disk, m4, keys, &nd);
check(nd.preset == PSX_QUALITY_CUSTOM && eff.has_texture_filter &&
eff.texture_filter == 1,
"txn2: the next launch keeps the manual texture filter");
}
/* An unrelated live edit (scanlines) keeps the preset. */
{
psxq::Decision run = psxq::decide(0xFu, ultra, m4, nullptr, false, nullptr);
PsxHostVideoSettings cur{}, next{};
next = cur; next.scanlines = 1;
const psxq::Edit e = psxq::live_edit(cur, next, keys);
check(e == psxq::Edit::NONE, "txn3: scanlines are not governed");
PSXRecompV4::UserSettings us = ultra;
us.scanlines = true;
us.has_texture_filter = true; us.texture_filter = 0; /* the live path writes every row */
psxq::settle_save(us, ultra, run, e, PSX_QUALITY_NONE, keys);
check(us.quality_preset == "ultra" && us.scanlines && !us.has_texture_filter,
"txn3: unrelated edits keep the preset; its keys stay the preset's");
}
/* A launcher pick is recorded; the picked preset owns its keys. */
{
psxq::Decision run = psxq::decide(0xFu, ultra, m4, nullptr, false, nullptr);
PSXRecompV4::UserSettings seed;
seed.has_texture_filter = true; seed.texture_filter = 1;
psxq::settle_save(seed, ultra, run, psxq::Edit::PICK, PSX_QUALITY_MEDIUM, keys);
check(seed.quality_preset == "medium" && seed.quality_base == "medium" &&
seed.quality_hardware == fp && !seed.has_texture_filter,
"txn4: a picked preset is saved with the hardware it was picked on");
}
}
/* The in-game menu (#674): a hidden/headless run never detects but keeps a
* saved preset; a preset pick in the menu, then a governed live edit, are
* settled by the same save transaction as the launchers. */
static void test_ingame_menu_txn() {
const std::vector<std::string> keys = {"texture_filtering", "frame_generation",
"supersample"};
PsxHostInfo m4 = host("Apple M4", "Apple M4", 10, 16);
PSXRecompV4::UserSettings ultra;
ultra.has_quality_preset = true; ultra.quality_preset = "ultra";
psxq::Decision run = psxq::decide(0xFu, ultra, m4, nullptr, false, nullptr, /*no_detect=*/true);
check(run.detection_skipped && run.preset == PSX_QUALITY_ULTRA,
"menu: a hidden run keeps the saved preset without detecting");
/* Menu: pick High (the launcher contract: Settings.quality_preset). */
PSXRecompV4::UserSettings us = ultra;
us.has_frame_generation = true; us.frame_generation = true; /* the live path writes every row */
check(psxq::settle_save(us, ultra, run, psxq::Edit::PICK, PSX_QUALITY_HIGH, keys),
"menu: a pick changes the preset");
PSXRecompV4::UserSettings disk = save_load(us);
check(disk.quality_preset == "high" && !disk.has_frame_generation,
"menu: High is saved and owns its keys");
/* Menu: Smooth motion off (governed) while High is in force. */
PsxHostVideoSettings cur{}, next{};
cur.frame_generation = 1; next = cur; next.frame_generation = 0;
const psxq::Edit e = psxq::live_edit(cur, next, keys);
check(e == psxq::Edit::GOVERNED_EDIT, "menu: Smooth motion is a governed live edit");
PSXRecompV4::UserSettings us2 = disk;
us2.has_frame_generation = true; us2.frame_generation = false;
psxq::settle_save(us2, disk, run, e, PSX_QUALITY_NONE, keys);
PSXRecompV4::UserSettings disk2 = save_load(us2);
check(disk2.quality_preset == "custom" && disk2.quality_base == "high" &&
disk2.has_frame_generation && !disk2.frame_generation,
"menu: the governed edit is Custom from High and keeps the value");
}
/* The in-game menu host path (main.cpp mh_launcher_settings_get ->
* provider -> mh_launcher_settings_apply -> host_pause_service ->
* quality_take_launcher + psx_video_apply_live's settle_save), on the same
* psxq functions, with a provider that behaves like recomp-launcher's
* QualityTracker (a pick writes the preset's rows; any changed preset row
* reports Custom from the preset). The review fixture: Ultra has
* geometry_correction on, High has it off; neither governs antialiasing.
* Start Ultra, pick High in the menu, then toggle Antialiasing: NONE. */
struct MenuHostFixture {
PSXRecompV4::GameConfig gc;
std::vector<std::string> keys;
psxq::Decision run;
psxq::MenuSelection sel;
psxq::Rows live; /* what is in force now */
PSXRecompV4::UserSettings disk;
struct Seed { psxq::Rows rows; int preset1 = 0, base1 = 0; };
bool rows_for(int tier, psxq::Rows* out) const {
for (const auto& p : gc.quality_presets)
if (psx_quality_from_name(p.name.c_str()) == tier) {
*out = psxq::rows_from_runtime(p.runtime, false, 0, false, 0);
return true;
}
return false;
}
Seed get() const { /* mh_launcher_settings_get */
Seed s;
s.rows = sel.seed(live);
s.preset1 = run.preset >= 0 ? run.preset + 1 : 0;
s.base1 = run.base >= 0 ? run.base + 1 : 0;
return s;
}
psxq::Edit apply(const Seed& post) { /* host_pause_service */
const psxq::LauncherEdit le = psxq::classify_launcher(
post.preset1, post.base1, post.rows, run.preset, run.base, keys,
[this](int t, psxq::Rows* o) { return rows_for(t, o); });
if (le.set_base) run.base = le.base;
/* psx_video_apply_live: the rows the menu shows apply now (here:
* antialiasing, texture filter); geometry waits for the next launch. */
live.antialiasing = post.rows.antialiasing;
live.texture_filter = post.rows.texture_filter;
PSXRecompV4::UserSettings us = disk;
us.has_antialiasing = true; us.antialiasing = post.rows.antialiasing != 0;
us.has_geometry_correction = true; us.geometry_correction = post.rows.geometry_correction != 0;
psxq::settle_save(us, disk, run, le.edit, le.picked, keys);
disk = save_load(us);
sel.record(post.rows, live);
return le.edit;
}
/* recomp-launcher's QualityTracker, on rows. */
Seed tracker_pick(Seed s, int tier) const {
rows_for(tier, &s.rows);
s.preset1 = s.base1 = tier + 1;
return s;
}
Seed tracker_observe(Seed s) const {
psxq::Rows expect;
if (s.preset1 >= 1 && s.preset1 <= 4 && rows_for(s.preset1 - 1, &expect) &&
std::memcmp(&expect, &s.rows, sizeof expect) != 0) {
s.base1 = s.preset1;
s.preset1 = PSX_QUALITY_CUSTOM + 1;
}
return s;
}
};
static void test_ingame_menu_host_path() {
fs::path p = write_game_toml("psxrecomp_quality_menu.toml",
"geometry_correction = true\n"
"[quality.ultra]\n"
"[quality.high]\n"
"geometry_correction = false\n");
MenuHostFixture h;
h.gc = PSXRecompV4::load_game_config(p);
fs::remove(p);
h.keys = psxq::governed_keys(h.gc);
check(std::find(h.keys.begin(), h.keys.end(), "antialiasing") == h.keys.end(),
"fixture: antialiasing is not governed");
PsxHostInfo m4 = host("Apple M4", "Apple M4", 10, 16);
h.disk.has_quality_preset = true; h.disk.quality_preset = "ultra";
h.run = psxq::decide(psxq::offered_mask(h.gc), h.disk, m4, nullptr, false, nullptr, true);
check(h.run.preset == PSX_QUALITY_ULTRA, "fixture: Ultra in force");
h.rows_for(PSX_QUALITY_ULTRA, &h.live);
check(h.live.geometry_correction == 1, "fixture: Ultra has geometry correction");
/* Pick High in the menu. */
MenuHostFixture::Seed s = h.tracker_pick(h.get(), PSX_QUALITY_HIGH);
check(h.apply(s) == psxq::Edit::PICK, "menu host: picking High is a PICK");
check(h.disk.quality_preset == "high", "menu host: High saved");
check(h.live.geometry_correction == 1, "menu host: geometry waits for the next launch");
/* Re-seed (the provider reopens), adopt, toggle Antialiasing only. */
MenuHostFixture::Seed again = h.get();
check(again.preset1 == PSX_QUALITY_HIGH + 1, "menu host: re-seed says High");
check(again.rows.geometry_correction == 0,
"menu host: re-seed carries High's pending geometry, not the live Ultra value");
check(h.tracker_observe(again).preset1 == PSX_QUALITY_HIGH + 1,
"menu host: the re-seed is High as the tracker sees it (no spurious Custom)");
again.rows.antialiasing ^= 1;
again = h.tracker_observe(again); /* the tracker reports Custom from High */
check(h.apply(again) == psxq::Edit::NONE,
"menu host: Ultra -> High, then Antialiasing is NONE (not GOVERNED_EDIT)");
check(h.disk.quality_preset == "high", "menu host: High stays saved");
/* A governed edit after the pick still makes Custom from High. */
MenuHostFixture::Seed g = h.get();
g.rows.geometry_correction = 1;
g = h.tracker_observe(g);
check(h.apply(g) == psxq::Edit::GOVERNED_EDIT, "menu host: a governed edit is Custom");
check(h.disk.quality_preset == "custom" && h.disk.quality_base == "high",
"menu host: Custom from High");
check(h.get().rows.geometry_correction == 1, "menu host: the Custom override is kept");
/* A netplay match defers Smooth motion: the seed keeps the selection
* until the live value catches up, and other rows follow live. */
{
psxq::MenuSelection ms;
psxq::Rows live_now; live_now.frame_generation = 1; live_now.texture_filter = 0;
psxq::Rows chosen = live_now; chosen.frame_generation = 0;
ms.record(chosen, live_now);
check(ms.seed(live_now).frame_generation == 0, "netplay: a deferred row stays selected");
live_now.texture_filter = 3; /* a hotkey/mod change after the apply */
check(ms.seed(live_now).texture_filter == 3, "netplay: other rows follow live");
}
/* Without the selection (the bug): the re-seed mixes High with live Ultra
* geometry and the same Antialiasing toggle would classify GOVERNED_EDIT. */
psxq::Rows ultra_live; h.rows_for(PSX_QUALITY_ULTRA, &ultra_live);
psxq::Rows mixed; h.rows_for(PSX_QUALITY_HIGH, &mixed);
mixed.geometry_correction = ultra_live.geometry_correction;
mixed.antialiasing ^= 1;
const psxq::LauncherEdit bad = psxq::classify_launcher(
PSX_QUALITY_CUSTOM + 1, PSX_QUALITY_HIGH + 1, mixed, PSX_QUALITY_HIGH, PSX_QUALITY_HIGH,
h.keys, [&](int t, psxq::Rows* o) { return h.rows_for(t, o); });
check(bad.edit == psxq::Edit::GOVERNED_EDIT,
"menu host: a live-mixed seed is what misclassified (guards the fixture)");
}
/* The supported quality-key set (docs/QUALITY_PRESETS.md): anything else in
* a preset is a load error, and every player-editable key is masked and
* copied (ownership enforced). */
static void test_supported_keys() {
bool threw = false;
fs::path bad = write_game_toml("psxrecomp_quality_vsync.toml",
"[quality.ultra]\nvsync = \"on\"\n");
try { (void)PSXRecompV4::load_game_config(bad); } catch (const std::exception&) { threw = true; }
check(threw, "vsync in a preset is rejected (not a supported quality key)");
fs::remove(bad);
threw = false;
bad = write_game_toml("psxrecomp_quality_renderer.toml", "[quality.low]\nrenderer = \"opengl\"\n");
try { (void)PSXRecompV4::load_game_config(bad); } catch (const std::exception&) { threw = true; }
check(threw, "renderer in a preset is rejected");
fs::remove(bad);
/* R4's preset keys (#23) are all supported. */
for (const char* k : {"supersample", "dynamic_resolution_min", "frame_generation",
"pgxp_depth_buffer", "pgxp_color_correction", "pgxp_seam",
"texture_lod", "anisotropic_filtering", "fmv_chroma_smoothing",
"bloom", "dithering"})
check(PSXRecompV4::quality_key_supported(k), k);
/* Every player-editable key: masked while a preset is in force, and a
* one-run save restores its disk value. */
const std::vector<std::string> player = {
"texture_filtering", "fmv_filter", "antialiasing", "dynamic_resolution",
"dynamic_resolution_min", "frame_generation", "render_thread", "present_thread",
"geometry_correction", "perspective_texturing", "frame_interpolation",
"frame_interpolation_fps"};
PSXRecompV4::UserSettings on;
on.has_texture_filter = on.has_fmv_filter = on.has_antialiasing = true;
on.has_dynamic_resolution = on.has_dynamic_resolution_min = on.has_frame_generation = true;
on.has_render_thread = on.has_present_thread = on.has_geometry_correction = true;
on.has_perspective_texturing = on.has_frame_interpolation = true;
on.has_frame_interpolation_fps = true;
for (const std::string& k : player) {
check(PSXRecompV4::quality_key_supported(k) && PSXRecompV4::quality_key_player_editable(k),
("player-editable: " + k).c_str());
PSXRecompV4::UserSettings m = on;
psxq::mask_user_settings(m, {k});
PSXRecompV4::UserSettings c = on;
psxq::copy_key(c, PSXRecompV4::UserSettings{}, k);
const bool masked =
(k == "texture_filtering" && !m.has_texture_filter && !c.has_texture_filter) ||
(k == "fmv_filter" && !m.has_fmv_filter && !c.has_fmv_filter) ||
(k == "antialiasing" && !m.has_antialiasing && !c.has_antialiasing) ||
(k == "dynamic_resolution" && !m.has_dynamic_resolution && !c.has_dynamic_resolution) ||
(k == "dynamic_resolution_min" && !m.has_dynamic_resolution_min && !c.has_dynamic_resolution_min) ||
(k == "frame_generation" && !m.has_frame_generation && !c.has_frame_generation) ||
(k == "render_thread" && !m.has_render_thread && !c.has_render_thread) ||
(k == "present_thread" && !m.has_present_thread && !c.has_present_thread) ||
(k == "geometry_correction" && !m.has_geometry_correction && !c.has_geometry_correction) ||
(k == "perspective_texturing" && !m.has_perspective_texturing && !c.has_perspective_texturing) ||
(k == "frame_interpolation" && !m.has_frame_interpolation && !c.has_frame_interpolation) ||
(k == "frame_interpolation_fps" && !m.has_frame_interpolation_fps && !c.has_frame_interpolation_fps);
check(masked, ("masked and copied: " + k).c_str());
}
}
/* Host-side launcher classification: the frontend flags Custom on any
* tracked row; only rows of keys the title governs count. */
static void test_launcher_classification() {
const std::vector<std::string> keys = {"texture_filtering", "supersample",
"dynamic_resolution_min", "frame_generation"};
auto only = [](const char* changed) {
return [changed](const std::string& k) { return k == changed; };
};
int picked = PSX_QUALITY_NONE;
check(psxq::launcher_custom_edit(keys, PSX_QUALITY_ULTRA, PSX_QUALITY_ULTRA,
only("antialiasing"), &picked) == psxq::Edit::NONE,
"Antialiasing (not governed) leaves Ultra in force");
check(psxq::launcher_custom_edit(keys, PSX_QUALITY_ULTRA, PSX_QUALITY_ULTRA,
only("texture_filtering"), &picked) ==
psxq::Edit::GOVERNED_EDIT,
"a governed row (texture filter) makes Custom");
picked = PSX_QUALITY_NONE;
check(psxq::launcher_custom_edit(keys, PSX_QUALITY_LOW, PSX_QUALITY_ULTRA,
only("antialiasing"), &picked) == psxq::Edit::PICK &&
picked == PSX_QUALITY_LOW,
"pick Low then change Antialiasing: a Low pick, not Custom");
check(psxq::launcher_custom_edit(keys, PSX_QUALITY_ULTRA, PSX_QUALITY_CUSTOM,
only("antialiasing"), &picked) ==
psxq::Edit::GOVERNED_EDIT,
"already Custom stays Custom");
}
int main() {
test_parse();
test_classify();
test_renderer_table();
test_decide();
test_mask();
test_settings_round_trip();
test_ownership_txn();
test_supported_keys();
test_launcher_classification();
test_ingame_menu_txn();
test_ingame_menu_host_path();
if (failures) {
std::fprintf(stderr, "quality_presets_test: %d failure(s)\n", failures);
return 1;
}
std::printf("quality_presets_test: ok\n");
return 0;
}