mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include "host_overlay.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -538,6 +539,144 @@ static void test_ingame_menu_txn() {
|
||||
"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). */
|
||||
@@ -636,6 +775,7 @@ int main() {
|
||||
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;
|
||||
|
||||
@@ -228,6 +228,8 @@ if(BUILD_TESTING)
|
||||
add_test(NAME menu_video_snapshot_test COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_menu_video_snapshot.py
|
||||
--cxx ${CMAKE_CXX_COMPILER})
|
||||
add_test(NAME menu_quality_callbacks_test COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_menu_quality_callbacks.py)
|
||||
add_test(NAME host_pause_first_present_test COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_host_pause_first_present.py
|
||||
--cxx ${CMAKE_CXX_COMPILER})
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace PSXRecompV4 { struct UserSettings; struct GameConfig; }
|
||||
namespace PSXRecompV4 { struct UserSettings; struct GameConfig; struct RuntimeConfig; }
|
||||
struct PsxHostVideoSettings;
|
||||
|
||||
namespace psxq {
|
||||
@@ -88,6 +88,63 @@ Edit launcher_custom_edit(const std::vector<std::string>& keys, int base, int cu
|
||||
const std::function<bool(const std::string&)>& row_changed,
|
||||
int* picked);
|
||||
|
||||
/* The settings-editor rows a preset can set, in the launcher ABI's encodings
|
||||
* (RecompLauncherCSettings fields of the same names). Launcher-neutral so the
|
||||
* host's preset logic is testable without a frontend header. */
|
||||
struct Rows {
|
||||
int texture_filter = 0, fmv_filter = 0, antialiasing = 0;
|
||||
int dynamic_resolution = 0, dynamic_resolution_min = 0;
|
||||
int frame_generation = 0, render_thread = 0, present_thread = 0;
|
||||
int geometry_correction = 0, perspective_texturing = 0;
|
||||
int frame_interp = 0, frame_interp_fps = 0;
|
||||
};
|
||||
/* A preset's rows from its parsed [video] block. Pure: reads nothing but its
|
||||
* arguments, so a frontend may call it from any thread (the in-game menu's
|
||||
* draw thread included). dynres_env / dynres_min_env are the startup env
|
||||
* overrides (< 0 / PSX_IR_UNSET-like `none` = not set; pass has_* false). */
|
||||
Rows rows_from_runtime(const PSXRecompV4::RuntimeConfig& r,
|
||||
bool has_dynres_env, int dynres_env,
|
||||
bool has_dynres_min_env, int dynres_min_env);
|
||||
/* Does the row a governed key owns differ? (preset-only keys have no row) */
|
||||
bool row_differs(const std::string& key, const Rows& a, const Rows& b);
|
||||
/* The host's classification of a frontend submission (Settings.quality_preset
|
||||
* and quality_base, 1-based; 5 = Custom). preset_rows(tier, out) gives a
|
||||
* tier's rows. current/current_base: the run's state before this submission.
|
||||
* - a tier other than the current one: PICK of that tier;
|
||||
* - Custom from a base: launcher_custom_edit against the base's rows (an
|
||||
* unrelated row is NONE, a new base with only unrelated rows is PICK);
|
||||
* set_base/base say the run's Custom base moves to it. */
|
||||
struct LauncherEdit {
|
||||
Edit edit = Edit::NONE;
|
||||
int picked = PSX_QUALITY_NONE;
|
||||
bool set_base = false;
|
||||
int base = PSX_QUALITY_NONE;
|
||||
bool base_changed = false; /* Custom moved to another base */
|
||||
};
|
||||
LauncherEdit classify_launcher(int preset1, int base1, const Rows& submitted,
|
||||
int current, int current_base,
|
||||
const std::vector<std::string>& keys,
|
||||
const std::function<bool(int, Rows*)>& preset_rows);
|
||||
/* The in-game menu's selected-versus-live rows. A provider is seeded with
|
||||
* what the player selected, not with live values that only catch up later:
|
||||
* a preset pick's rows that apply at the next launch, and rows a netplay
|
||||
* match defers, stay pending until the live value matches. Every other row
|
||||
* follows the live value (so a hotkey or mod change still shows). Owned by
|
||||
* the host's main thread; providers only see seed() results. */
|
||||
class MenuSelection {
|
||||
public:
|
||||
void reset() { valid_ = false; pending_ = 0; }
|
||||
/* After the host applied a submission: what the player selected, and the
|
||||
* live rows after the apply (deferred rows still differ). */
|
||||
void record(const Rows& selected, const Rows& live_after);
|
||||
/* The rows to seed a provider with. */
|
||||
Rows seed(const Rows& live) const;
|
||||
bool pending() const { return valid_ && pending_ != 0; }
|
||||
private:
|
||||
Rows sel_{};
|
||||
unsigned pending_ = 0;
|
||||
bool valid_ = false;
|
||||
};
|
||||
/* Copy one governed key's settings.toml field(s) (value and presence). */
|
||||
void copy_key(PSXRecompV4::UserSettings& dst, const PSXRecompV4::UserSettings& src,
|
||||
const std::string& key);
|
||||
|
||||
+186
-128
@@ -16096,88 +16096,80 @@ namespace {
|
||||
/* Graphics presets in the launcher (docs/QUALITY_PRESETS.md). */
|
||||
std::string g_quality_summary_s, g_quality_reason_s;
|
||||
|
||||
/* Fill the launcher rows a preset sets: run the preset's [video] values
|
||||
* through the same globals -> launcher encodings the seed uses, then put
|
||||
* the globals back (the launch path applies the pick). */
|
||||
/* The display height the internal-resolution rows are seeded for. Set on
|
||||
* the main thread (pre-game fill, in-game menu setup); the mapper below
|
||||
* only reads it. */
|
||||
std::atomic<int> g_quality_display_h{0};
|
||||
|
||||
/* A preset's rows, side-effect free: computed from its parsed [video]
|
||||
* block and startup constants (reference lines, env overrides), never
|
||||
* from or into the live globals. Safe on any thread -- the in-game menu
|
||||
* calls it from its draw thread while the game runs. */
|
||||
psxq::Rows quality_rows_for(const PSXRecompV4::RuntimeConfig& r) {
|
||||
return psxq::rows_from_runtime(r, g_video_dynres_env >= 0, g_video_dynres_env,
|
||||
g_video_dynres_min_env != PSX_IR_UNSET,
|
||||
g_video_dynres_min_env);
|
||||
}
|
||||
psxq::Rows quality_rows_of(const RecompLauncherCSettings& s) {
|
||||
psxq::Rows r;
|
||||
r.texture_filter = s.texture_filter;
|
||||
r.fmv_filter = s.fmv_filter;
|
||||
r.antialiasing = s.antialiasing;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_DYNAMIC_RESOLUTION)
|
||||
r.dynamic_resolution = s.dynamic_resolution;
|
||||
r.dynamic_resolution_min = s.dynamic_resolution_min;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE)
|
||||
r.render_thread = s.render_thread;
|
||||
r.present_thread = s.present_thread;
|
||||
r.frame_generation = s.frame_generation;
|
||||
#endif
|
||||
r.geometry_correction = s.geometry_correction;
|
||||
r.perspective_texturing = s.perspective_texturing;
|
||||
r.frame_interp = s.frame_interp;
|
||||
r.frame_interp_fps = s.frame_interp_fps;
|
||||
return r;
|
||||
}
|
||||
void quality_rows_to(const psxq::Rows& r, RecompLauncherCSettings* s) {
|
||||
s->texture_filter = r.texture_filter;
|
||||
s->fmv_filter = r.fmv_filter;
|
||||
s->antialiasing = r.antialiasing;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_DYNAMIC_RESOLUTION)
|
||||
s->dynamic_resolution = r.dynamic_resolution;
|
||||
s->dynamic_resolution_min = r.dynamic_resolution_min;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE)
|
||||
s->render_thread = r.render_thread;
|
||||
s->present_thread = r.present_thread;
|
||||
s->frame_generation = r.frame_generation;
|
||||
#endif
|
||||
s->geometry_correction = r.geometry_correction;
|
||||
s->perspective_texturing = r.perspective_texturing;
|
||||
s->frame_interp = r.frame_interp;
|
||||
s->frame_interp_fps = r.frame_interp_fps;
|
||||
}
|
||||
bool quality_preset_rows(int tier, psxq::Rows* out) {
|
||||
const auto* p = quality_preset_for(tier);
|
||||
if (!p) return false;
|
||||
*out = quality_rows_for(p->runtime);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Fill the launcher rows a preset sets (Settings encodings). Pure. */
|
||||
void quality_launcher_apply(int preset, RecompLauncherCSettings* s) {
|
||||
const auto* p = quality_preset_for(preset - 1);
|
||||
if (!p || !s) return;
|
||||
const int sv_scale = g_video_scale, sv_ir = g_video_internal_res,
|
||||
sv_dyn = g_video_dynres, sv_dynmin = g_video_dynres_min,
|
||||
sv_aa = g_video_aa, sv_paa = g_video_post_aa, sv_ss = g_video_ss_milli,
|
||||
sv_tf = g_video_texfilter, sv_fmv = g_video_fmv_filter,
|
||||
sv_gc = g_video_geometry_correction, sv_pt = g_video_perspective_texturing,
|
||||
sv_pfb = g_video_pgxp_position_fallback,
|
||||
sv_ppp = g_video_pgxp_preserve_projection,
|
||||
sv_pdb = g_video_pgxp_depth_buffer, sv_pcc = g_video_pgxp_color_correction,
|
||||
sv_seam = g_video_pgxp_seam, sv_rt = g_render_thread,
|
||||
sv_pth = g_present_thread, sv_fg = g_frame_generation,
|
||||
sv_fgm = g_frame_generation_method, sv_fi = g_frame_interpolation,
|
||||
sv_fif = g_frame_interpolation_fps;
|
||||
const float sv_tol = g_video_pgxp_tolerance, sv_dth = g_video_pgxp_depth_threshold,
|
||||
sv_bloom = g_video_bloom;
|
||||
const int sv_chroma = g_video_fmv_chroma, sv_lod = g_video_texture_lod,
|
||||
sv_aniso = g_video_aniso, sv_dith = g_video_dithering;
|
||||
quality_apply_video_globals(p->runtime);
|
||||
const PSXRecompV4::RuntimeConfig& r = p->runtime;
|
||||
quality_rows_to(quality_rows_for(r), s);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
s->internal_resolution = internal_resolution_for_launcher();
|
||||
s->internal_resolution = r.video_internal_resolution != PSX_IR_UNSET
|
||||
? r.video_internal_resolution
|
||||
: psx_ir_from_supersampling(r.video_supersampling < 1 ? 1 : r.video_supersampling,
|
||||
g_video_ref_lines);
|
||||
#endif
|
||||
s->supersampling = psx_ir_launcher_seed_supersampling(
|
||||
kLauncherHasInternalResolution, g_video_internal_res, g_video_scale,
|
||||
g_video_ref_lines, psx_sdl_display_pixel_height(nullptr));
|
||||
#if defined(RECOMP_LAUNCHER_HAS_DYNAMIC_RESOLUTION)
|
||||
s->dynamic_resolution = dynres_requested() != 0;
|
||||
s->dynamic_resolution_min = dynres_min_value();
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE)
|
||||
s->render_thread = g_render_thread ? 1 : 0;
|
||||
s->present_thread = g_present_thread ? 1 : 0;
|
||||
s->frame_generation = g_frame_generation ? 1 : 0;
|
||||
#endif
|
||||
s->antialiasing = g_video_aa ? 1 : 0;
|
||||
s->texture_filter = g_video_texfilter;
|
||||
s->fmv_filter = cfg_fmv_filter_to_launcher(g_video_fmv_filter);
|
||||
s->geometry_correction = g_video_geometry_correction ? 1 : 0;
|
||||
s->perspective_texturing = g_video_perspective_texturing ? 1 : 0;
|
||||
s->frame_interp = g_frame_interpolation ? 1 : 0;
|
||||
s->frame_interp_fps = g_frame_interpolation_fps;
|
||||
g_video_scale = sv_scale; g_video_internal_res = sv_ir; g_video_dynres = sv_dyn;
|
||||
g_video_dynres_min = sv_dynmin; g_video_aa = sv_aa; g_video_post_aa = sv_paa;
|
||||
g_video_ss_milli = sv_ss; g_video_texfilter = sv_tf; g_video_fmv_filter = sv_fmv;
|
||||
g_video_geometry_correction = sv_gc; g_video_perspective_texturing = sv_pt;
|
||||
g_video_pgxp_tolerance = sv_tol; g_video_pgxp_position_fallback = sv_pfb;
|
||||
g_video_pgxp_preserve_projection = sv_ppp; g_video_pgxp_depth_buffer = sv_pdb;
|
||||
g_video_pgxp_color_correction = sv_pcc; g_video_pgxp_seam = sv_seam;
|
||||
g_video_pgxp_depth_threshold = sv_dth; g_render_thread = sv_rt;
|
||||
g_present_thread = sv_pth; g_frame_generation = sv_fg;
|
||||
g_frame_generation_method = sv_fgm; g_frame_interpolation = sv_fi;
|
||||
g_frame_interpolation_fps = sv_fif;
|
||||
g_video_bloom = sv_bloom; g_video_fmv_chroma = sv_chroma;
|
||||
g_video_texture_lod = sv_lod; g_video_aniso = sv_aniso; g_video_dithering = sv_dith;
|
||||
}
|
||||
|
||||
/* Does a launcher row a governed, player-editable key owns differ between
|
||||
* two Settings? (Host-side Custom classification: only the keys this
|
||||
* title's presets govern count.) */
|
||||
static bool quality_row_differs(const std::string& k, const RecompLauncherCSettings& a,
|
||||
const RecompLauncherCSettings& b) {
|
||||
if (k == "texture_filtering") return a.texture_filter != b.texture_filter;
|
||||
if (k == "fmv_filter") return a.fmv_filter != b.fmv_filter;
|
||||
if (k == "antialiasing") return (a.antialiasing != 0) != (b.antialiasing != 0);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_DYNAMIC_RESOLUTION)
|
||||
if (k == "dynamic_resolution") return (a.dynamic_resolution != 0) != (b.dynamic_resolution != 0);
|
||||
if (k == "dynamic_resolution_min") return a.dynamic_resolution_min != b.dynamic_resolution_min;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE)
|
||||
if (k == "frame_generation") return (a.frame_generation != 0) != (b.frame_generation != 0);
|
||||
if (k == "render_thread") return (a.render_thread != 0) != (b.render_thread != 0);
|
||||
if (k == "present_thread") return (a.present_thread != 0) != (b.present_thread != 0);
|
||||
#endif
|
||||
if (k == "geometry_correction") return (a.geometry_correction != 0) != (b.geometry_correction != 0);
|
||||
if (k == "perspective_texturing") return (a.perspective_texturing != 0) != (b.perspective_texturing != 0);
|
||||
if (k == "frame_interpolation") return (a.frame_interp != 0) != (b.frame_interp != 0);
|
||||
if (k == "frame_interpolation_fps") return a.frame_interp_fps != b.frame_interp_fps;
|
||||
return false; /* preset-only keys have no row */
|
||||
kLauncherHasInternalResolution, r.video_internal_resolution, r.video_supersampling,
|
||||
g_video_ref_lines, g_quality_display_h.load());
|
||||
}
|
||||
|
||||
void quality_publish_strings(void) {
|
||||
@@ -16205,6 +16197,7 @@ namespace {
|
||||
void quality_fill_game_info(RecompLauncherCGameInfo* gi) {
|
||||
if (!g_quality_offered) return;
|
||||
quality_publish_strings();
|
||||
g_quality_display_h = psx_sdl_display_pixel_height(nullptr);
|
||||
gi->quality_offered_mask = (int)g_quality_offered;
|
||||
gi->quality_detected = (g_quality.detected >= 0 && g_quality.detected < PSX_QUALITY_COUNT)
|
||||
? g_quality.detected + 1 : 0;
|
||||
@@ -16227,50 +16220,31 @@ namespace {
|
||||
* settled when settings.toml is saved (quality_save_user_settings). */
|
||||
void quality_take_launcher(const RecompLauncherCSettings* ls,
|
||||
PSXRecompV4::UserSettings* /*us*/, bool apply_now) {
|
||||
if (!g_quality_offered || ls->quality_preset < 1 || ls->quality_preset > 5) return;
|
||||
const int preset = ls->quality_preset - 1;
|
||||
if (preset == PSX_QUALITY_CUSTOM) {
|
||||
/* A player may pick a different tier and edit one of its rows
|
||||
* before leaving the launcher. Custom belongs to that tier,
|
||||
* including its values which have no launcher row. */
|
||||
const int base = ls->quality_base - 1;
|
||||
const auto* p = quality_preset_for(base);
|
||||
/* The frontends flag Custom on any row their snapshot tracks;
|
||||
* only a row this title's presets govern makes it Custom. */
|
||||
if (p && g_quality.preset != PSX_QUALITY_CUSTOM) {
|
||||
RecompLauncherCSettings expect = *ls;
|
||||
quality_launcher_apply(base + 1, &expect);
|
||||
int picked = PSX_QUALITY_NONE;
|
||||
const psxq::Edit e = psxq::launcher_custom_edit(
|
||||
g_quality_keys, base, g_quality.preset,
|
||||
[&](const std::string& k) { return quality_row_differs(k, *ls, expect); },
|
||||
&picked);
|
||||
if (e == psxq::Edit::NONE) return; /* unrelated rows only */
|
||||
if (e == psxq::Edit::PICK) { /* a pick, then unrelated rows */
|
||||
g_quality_edit = e;
|
||||
g_quality_picked = picked;
|
||||
if (apply_now && !g_quality.env_override) quality_apply_video_globals(p->runtime);
|
||||
return;
|
||||
if (!g_quality_offered) return;
|
||||
const psxq::LauncherEdit le = psxq::classify_launcher(
|
||||
ls->quality_preset, ls->quality_base, quality_rows_of(*ls),
|
||||
g_quality.preset, g_quality.base, g_quality_keys, quality_preset_rows);
|
||||
if (le.edit == psxq::Edit::NONE && !le.set_base) return;
|
||||
if (le.edit != psxq::Edit::NONE) g_quality_edit = le.edit;
|
||||
if (le.edit == psxq::Edit::PICK) {
|
||||
g_quality_picked = le.picked;
|
||||
if (apply_now && !g_quality.env_override) {
|
||||
if (const auto* p = quality_preset_for(le.picked)) {
|
||||
/* The launcher wrote this preset's values into its rows,
|
||||
* which the launch path applies next; this covers the rest
|
||||
* (supersample, PGXP extras, ...). */
|
||||
quality_apply_video_globals(p->runtime);
|
||||
if (ls->quality_preset - 1 != PSX_QUALITY_CUSTOM)
|
||||
std::fprintf(stdout, "psxrecomp: graphics preset %s\n",
|
||||
psx_quality_name(le.picked));
|
||||
}
|
||||
}
|
||||
const bool changed_base = p && base != g_quality.base;
|
||||
if (g_quality.preset != PSX_QUALITY_CUSTOM || changed_base)
|
||||
g_quality_edit = psxq::Edit::GOVERNED_EDIT;
|
||||
if (p) g_quality.base = base;
|
||||
if (apply_now && changed_base) quality_apply_video_globals(p->runtime);
|
||||
return;
|
||||
}
|
||||
if (preset == g_quality.preset) return;
|
||||
g_quality_edit = psxq::Edit::PICK;
|
||||
g_quality_picked = preset;
|
||||
if (apply_now && !g_quality.env_override) {
|
||||
if (const auto* p = quality_preset_for(preset)) {
|
||||
/* The launcher wrote this preset's values into its rows,
|
||||
* which the launch path applies next; this covers the rest
|
||||
* (supersample, PGXP extras, ...). */
|
||||
quality_apply_video_globals(p->runtime);
|
||||
std::fprintf(stdout, "psxrecomp: graphics preset %s\n", psx_quality_name(preset));
|
||||
}
|
||||
if (le.set_base) {
|
||||
g_quality.base = le.base;
|
||||
if (apply_now && le.base_changed)
|
||||
if (const auto* p = quality_preset_for(le.base)) quality_apply_video_globals(p->runtime);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -16558,14 +16532,52 @@ bool g_ovl_netplay = false;
|
||||
|
||||
/* Only the main thread reads live runtime state. Provider draw callbacks
|
||||
* consume a coherent snapshot under the same mutex as their posted edits. */
|
||||
#if defined(RECOMP_LAUNCHER) && defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
/* Graphics presets in the menu: the run's preset state and the live rows a
|
||||
* preset governs, captured on the main thread; the player's selection
|
||||
* (psxq::MenuSelection) so a provider is re-seeded with what it chose, not
|
||||
* with live values a pick or a netplay match defers. */
|
||||
int g_ovl_q_preset = 0, g_ovl_q_base = 0;
|
||||
std::atomic<int> g_ovl_q_detected{0};
|
||||
psxq::Rows g_ovl_q_live{};
|
||||
psxq::MenuSelection g_ovl_q_sel;
|
||||
std::string g_ovl_q_summary, g_ovl_q_reason; /* fixed at menu setup */
|
||||
psxq::Rows mh_quality_live_rows(const PsxHostVideoSettings& v) {
|
||||
psxq::Rows r;
|
||||
r.texture_filter = v.texture_filter;
|
||||
r.fmv_filter = cfg_fmv_filter_to_launcher(v.fmv_filter);
|
||||
r.antialiasing = v.present_linear;
|
||||
r.dynamic_resolution = v.dynamic_resolution;
|
||||
r.dynamic_resolution_min = dynres_min_value();
|
||||
r.frame_generation = v.frame_generation;
|
||||
r.render_thread = v.render_thread;
|
||||
r.present_thread = v.present_thread;
|
||||
r.geometry_correction = g_video_geometry_correction ? 1 : 0;
|
||||
r.perspective_texturing = g_video_perspective_texturing ? 1 : 0;
|
||||
r.frame_interp = g_frame_interpolation ? 1 : 0;
|
||||
r.frame_interp_fps = g_frame_interpolation_fps;
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
|
||||
void mh_refresh_snapshot() {
|
||||
PsxHostVideoSettings current;
|
||||
psx_video_current(¤t);
|
||||
const bool netplay = psx_netplay_active() != 0;
|
||||
#if defined(RECOMP_LAUNCHER) && defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
const psxq::Rows qlive = mh_quality_live_rows(current);
|
||||
RecompLauncherCSettings q{};
|
||||
quality_seed_launcher(&q);
|
||||
#endif
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_video_current = current;
|
||||
g_ovl_video_boot = g_video_boot;
|
||||
g_ovl_netplay = netplay;
|
||||
#if defined(RECOMP_LAUNCHER) && defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
g_ovl_q_live = qlive;
|
||||
g_ovl_q_preset = q.quality_preset;
|
||||
g_ovl_q_base = q.quality_base;
|
||||
#endif
|
||||
}
|
||||
int mh_netplay_active(void) {
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
@@ -16704,16 +16716,16 @@ void mh_launcher_settings_get(RecompLauncherCSettings* out) {
|
||||
ls.render_thread = v.render_thread;
|
||||
ls.present_thread = v.present_thread;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
/* Rows the menu may not show but a preset governs: as in force, so the
|
||||
* preset classification (quality_take_launcher) compares like for like. */
|
||||
#if defined(RECOMP_LAUNCHER_HAS_DYNAMIC_RESOLUTION)
|
||||
ls.dynamic_resolution_min = dynres_min_value();
|
||||
#endif
|
||||
ls.geometry_correction = g_video_geometry_correction ? 1 : 0;
|
||||
ls.perspective_texturing = g_video_perspective_texturing ? 1 : 0;
|
||||
ls.frame_interp = g_frame_interpolation ? 1 : 0;
|
||||
ls.frame_interp_fps = g_frame_interpolation_fps;
|
||||
quality_seed_launcher(&ls);
|
||||
/* Every row a preset governs, the hidden ones included, as the player
|
||||
* selected them (main-thread snapshot): a pick's next-launch rows and
|
||||
* netplay-deferred rows stay selected, so the host's classification
|
||||
* compares like for like with the preset in quality_preset/base. */
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
quality_rows_to(g_ovl_q_sel.seed(g_ovl_q_live), &ls);
|
||||
ls.quality_preset = g_ovl_q_preset;
|
||||
ls.quality_base = g_ovl_q_base;
|
||||
}
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK) && \
|
||||
defined(PSX_HAS_JOGCON_FFB_PLAYER_SETTING)
|
||||
@@ -16734,6 +16746,9 @@ unsigned mh_launcher_settings_apply(const RecompLauncherCSettings* s) {
|
||||
}
|
||||
return mh_restart_bits(next);
|
||||
}
|
||||
#if defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
int mh_quality_redetect(void) { return g_ovl_q_detected.load(); }
|
||||
#endif
|
||||
void mh_launcher_game_info(RecompLauncherCGameInfo* out) {
|
||||
if (!out) return;
|
||||
RecompLauncherCGameInfo& gi = *out;
|
||||
@@ -16752,8 +16767,19 @@ void mh_launcher_game_info(RecompLauncherCGameInfo* out) {
|
||||
gi.has_rewind_depth = 0;
|
||||
gi.mods = PSXRecompV4::mod_runtime_launcher_provider();
|
||||
#if defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
/* The Graphics preset row, as in the pre-game launcher. */
|
||||
quality_fill_game_info(&gi);
|
||||
/* The Graphics preset contract, for providers that implement it. Only
|
||||
* thread-safe callbacks: quality_apply is a pure preset -> rows mapping,
|
||||
* and re-detect returns this session's detection (a fresh probe needs
|
||||
* the GL context and mutates the run's state: it stays in the pre-game
|
||||
* launcher). The strings were fixed on the main thread at menu setup. */
|
||||
if (g_quality_offered) {
|
||||
gi.quality_offered_mask = (int)g_quality_offered;
|
||||
gi.quality_detected = g_ovl_q_detected.load();
|
||||
gi.quality_summary = g_ovl_q_summary.c_str();
|
||||
gi.quality_reason = g_ovl_q_reason.c_str();
|
||||
gi.quality_apply = quality_launcher_apply;
|
||||
gi.quality_redetect = mh_quality_redetect;
|
||||
}
|
||||
#endif
|
||||
#if defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT)
|
||||
/* The same lobby the pre-game launcher drives, so the menu can host/join. */
|
||||
@@ -16791,6 +16817,23 @@ void ovl_draw(int w, int h, void*) { if (g_ovl && g_ovl->draw) g_ovl->draw(g_ovl
|
||||
#endif
|
||||
|
||||
static void recomp_overlay_setup(const char* argv0, const std::string& title) {
|
||||
#if defined(RECOMP_LAUNCHER) && defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
/* Main thread: the preset strings and detection the menu publishes, and
|
||||
* the display the preset rows are seeded for. Fixed before any provider
|
||||
* attaches, never written while one can read them. */
|
||||
if (g_quality_offered && !g_ovl_on) {
|
||||
g_ovl_q_summary = g_quality.summary;
|
||||
g_ovl_q_reason = g_quality.reason;
|
||||
g_ovl_q_detected = (g_quality.detected >= 0 && g_quality.detected < PSX_QUALITY_COUNT)
|
||||
? g_quality.detected + 1 : 0;
|
||||
g_quality_display_h = psx_sdl_display_pixel_height(nullptr);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_q_sel.reset();
|
||||
}
|
||||
mh_refresh_snapshot();
|
||||
}
|
||||
#endif
|
||||
const char* e = std::getenv("PSX_OVERLAY");
|
||||
const PsxMenuProvider* p = psx_menu_provider();
|
||||
g_menu_host.default_selection = 0;
|
||||
@@ -16884,6 +16927,21 @@ static void host_pause_service(void) {
|
||||
#endif
|
||||
const PsxHostVideoSettings v = ovl_video_from(post);
|
||||
(void)psx_video_apply_live(&v);
|
||||
#if defined(RECOMP_LAUNCHER_HAS_QUALITY_PRESETS)
|
||||
/* What the player selected versus what is live now: rows that wait
|
||||
* (a pick's next-launch rows, netplay-deferred rows) stay selected
|
||||
* for the next seed. Then refresh the preset state the save settled. */
|
||||
{
|
||||
PsxHostVideoSettings now;
|
||||
psx_video_current(&now);
|
||||
const psxq::Rows live = mh_quality_live_rows(now);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_ovl_mu);
|
||||
g_ovl_q_sel.record(quality_rows_of(post), live);
|
||||
}
|
||||
mh_refresh_snapshot();
|
||||
}
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK) && \
|
||||
defined(PSX_HAS_JOGCON_FFB_PLAYER_SETTING)
|
||||
/* Controls > Wheel force feedback (experimental): live, and saved to
|
||||
|
||||
@@ -223,6 +223,102 @@ Edit launcher_custom_edit(const std::vector<std::string>& keys, int base, int cu
|
||||
return Edit::NONE;
|
||||
}
|
||||
|
||||
Rows rows_from_runtime(const PSXRecompV4::RuntimeConfig& r,
|
||||
bool has_dynres_env, int dynres_env,
|
||||
bool has_dynres_min_env, int dynres_min_env) {
|
||||
Rows o;
|
||||
o.texture_filter = r.video_texture_filter;
|
||||
int fmv = r.video_fmv_filter; /* cfg -> launcher (cfg_fmv_filter_to_launcher) */
|
||||
if (fmv < 0 || fmv >= PSXRecompV4::VIDEO_FMV_FILTER_COUNT)
|
||||
fmv = PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT;
|
||||
o.fmv_filter = fmv + 1;
|
||||
o.antialiasing = r.video_antialiasing ? 1 : 0;
|
||||
o.dynamic_resolution = (has_dynres_env ? dynres_env : (r.video_dynamic_resolution ? 1 : 0)) ? 1 : 0;
|
||||
o.dynamic_resolution_min = has_dynres_min_env ? dynres_min_env : r.video_dynamic_resolution_min;
|
||||
o.render_thread = r.video_render_thread ? 1 : 0;
|
||||
o.present_thread = r.video_present_thread ? 1 : 0;
|
||||
o.frame_generation = r.video_frame_generation ? 1 : 0;
|
||||
o.geometry_correction = r.video_geometry_correction ? 1 : 0;
|
||||
o.perspective_texturing = r.video_perspective_texturing ? 1 : 0;
|
||||
o.frame_interp = r.video_frame_interpolation ? 1 : 0;
|
||||
o.frame_interp_fps = r.video_frame_interpolation_fps;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool row_differs(const std::string& k, const Rows& a, const Rows& b) {
|
||||
if (k == "texture_filtering") return a.texture_filter != b.texture_filter;
|
||||
if (k == "fmv_filter") return a.fmv_filter != b.fmv_filter;
|
||||
if (k == "antialiasing") return (a.antialiasing != 0) != (b.antialiasing != 0);
|
||||
if (k == "dynamic_resolution") return (a.dynamic_resolution != 0) != (b.dynamic_resolution != 0);
|
||||
if (k == "dynamic_resolution_min") return a.dynamic_resolution_min != b.dynamic_resolution_min;
|
||||
if (k == "frame_generation") return (a.frame_generation != 0) != (b.frame_generation != 0);
|
||||
if (k == "render_thread") return (a.render_thread != 0) != (b.render_thread != 0);
|
||||
if (k == "present_thread") return (a.present_thread != 0) != (b.present_thread != 0);
|
||||
if (k == "geometry_correction") return (a.geometry_correction != 0) != (b.geometry_correction != 0);
|
||||
if (k == "perspective_texturing") return (a.perspective_texturing != 0) != (b.perspective_texturing != 0);
|
||||
if (k == "frame_interpolation") return (a.frame_interp != 0) != (b.frame_interp != 0);
|
||||
if (k == "frame_interpolation_fps") return a.frame_interp_fps != b.frame_interp_fps;
|
||||
return false; /* preset-only keys have no row */
|
||||
}
|
||||
|
||||
LauncherEdit classify_launcher(int preset1, int base1, const Rows& submitted,
|
||||
int current, int current_base,
|
||||
const std::vector<std::string>& keys,
|
||||
const std::function<bool(int, Rows*)>& preset_rows) {
|
||||
LauncherEdit out;
|
||||
if (preset1 < 1 || preset1 > 5) return out;
|
||||
const int preset = preset1 - 1;
|
||||
if (preset != PSX_QUALITY_CUSTOM) {
|
||||
if (preset != current) { out.edit = Edit::PICK; out.picked = preset; }
|
||||
return out;
|
||||
}
|
||||
/* A player may pick a different tier and edit one of its rows before
|
||||
* leaving the editor: Custom belongs to that tier. The frontends flag
|
||||
* Custom on any row their snapshot tracks; only a row this title's
|
||||
* presets govern makes it Custom. */
|
||||
const int base = base1 - 1;
|
||||
Rows expect;
|
||||
const bool have = base >= 0 && base < PSX_QUALITY_COUNT && preset_rows && preset_rows(base, &expect);
|
||||
if (have && current != PSX_QUALITY_CUSTOM) {
|
||||
int picked = PSX_QUALITY_NONE;
|
||||
const Edit e = launcher_custom_edit(
|
||||
keys, base, current,
|
||||
[&](const std::string& k) { return row_differs(k, submitted, expect); }, &picked);
|
||||
if (e == Edit::NONE) return out; /* unrelated rows only */
|
||||
if (e == Edit::PICK) { out.edit = e; out.picked = picked; return out; }
|
||||
}
|
||||
out.base_changed = have && base != current_base;
|
||||
if (current != PSX_QUALITY_CUSTOM || out.base_changed) out.edit = Edit::GOVERNED_EDIT;
|
||||
if (have) { out.set_base = true; out.base = base; }
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
int Rows::* const kRowFields[] = {
|
||||
&Rows::texture_filter, &Rows::fmv_filter, &Rows::antialiasing,
|
||||
&Rows::dynamic_resolution, &Rows::dynamic_resolution_min,
|
||||
&Rows::frame_generation, &Rows::render_thread, &Rows::present_thread,
|
||||
&Rows::geometry_correction, &Rows::perspective_texturing,
|
||||
&Rows::frame_interp, &Rows::frame_interp_fps,
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void MenuSelection::record(const Rows& selected, const Rows& live_after) {
|
||||
sel_ = selected;
|
||||
pending_ = 0;
|
||||
for (unsigned i = 0; i < sizeof kRowFields / sizeof kRowFields[0]; ++i)
|
||||
if (selected.*kRowFields[i] != live_after.*kRowFields[i]) pending_ |= 1u << i;
|
||||
valid_ = true;
|
||||
}
|
||||
|
||||
Rows MenuSelection::seed(const Rows& live) const {
|
||||
Rows out = live;
|
||||
if (!valid_) return out;
|
||||
for (unsigned i = 0; i < sizeof kRowFields / sizeof kRowFields[0]; ++i)
|
||||
if (pending_ & (1u << i)) out.*kRowFields[i] = sel_.*kRowFields[i];
|
||||
return out;
|
||||
}
|
||||
|
||||
static void copy_meta(PSXRecompV4::UserSettings& d, const PSXRecompV4::UserSettings& s) {
|
||||
d.has_quality_preset = s.has_quality_preset; d.quality_preset = s.quality_preset;
|
||||
d.has_quality_base = s.has_quality_base; d.quality_base = s.quality_base;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""The in-game menu's preset callbacks run on a provider's draw thread while
|
||||
the game runs: they must not write runtime state (psxrecomp#674 review P1).
|
||||
|
||||
quality_apply is a pure preset -> rows mapping, re-detect returns this
|
||||
session's detection, settings_get reads only the main-thread snapshot, and
|
||||
the menu's game info never publishes the pre-game launcher's stateful
|
||||
callbacks (quality_fill_game_info: quality_launcher_redetect, string
|
||||
publication)."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
from test_netplay_load_apply_flow import function_body, require
|
||||
|
||||
source = (Path(__file__).resolve().parents[1] / "src/main.cpp").read_text(encoding="utf-8")
|
||||
WRITE = re.compile(r"\bg_\w+\s*(?:\.\w+\s*)?(?:=(?!=)|\+\+|--|\+=|-=)")
|
||||
STATEFUL = ("quality_apply_video_globals", "quality_publish_strings", "psxq::decide",
|
||||
"quality_launcher_redetect", "psx_video_current", "quality_fill_game_info",
|
||||
"psx_sdl_display_pixel_height")
|
||||
|
||||
for sig in ("psxq::Rows quality_rows_for(const PSXRecompV4::RuntimeConfig& r)",
|
||||
"void quality_rows_to(const psxq::Rows& r, RecompLauncherCSettings* s)",
|
||||
"bool quality_preset_rows(int tier, psxq::Rows* out)",
|
||||
"void quality_launcher_apply(int preset, RecompLauncherCSettings* s)",
|
||||
"int mh_quality_redetect(void)",
|
||||
"void mh_launcher_settings_get(RecompLauncherCSettings* out)",
|
||||
"void mh_launcher_game_info(RecompLauncherCGameInfo* out)"):
|
||||
body = function_body(source, sig)
|
||||
m = WRITE.search(body)
|
||||
require(m is None, f"{sig}: writes runtime state ({m.group(0) if m else ''})")
|
||||
for name in STATEFUL:
|
||||
require(name not in body, f"{sig}: calls {name}")
|
||||
|
||||
gi = function_body(source, "void mh_launcher_game_info(RecompLauncherCGameInfo* out)")
|
||||
require("gi.quality_apply = quality_launcher_apply" in gi and
|
||||
"gi.quality_redetect = mh_quality_redetect" in gi,
|
||||
"menu game info publishes the thread-safe preset callbacks")
|
||||
print("menu quality callbacks: draw-thread safe PASS")
|
||||
Reference in New Issue
Block a user