runtime: fix game-owned display and controller mods

This commit is contained in:
Matthew Stanley
2026-07-28 00:04:20 -07:00
parent c1eb052ca3
commit 64097606fb
5 changed files with 110 additions and 3 deletions
+2
View File
@@ -384,7 +384,9 @@ if(BUILD_TESTING)
default_renderer_guards
fmv_quiet_guards
fps_telemetry_opt_in
frame_interpolation_display_rate
launcher_vulkan_option
mod_controller_override
overlay_candidate_capacity
overlay_decodable_fallback
overlay_init_guard
+15 -1
View File
@@ -73,11 +73,25 @@ int psx_mod_set_native_vblank_rate(uint32_t frames_per_second);
* Enable presentation-only frame interpolation while leaving guest VBlank,
* game logic, timers, and audio at their stock cadence. The OpenGL presenter
* blends between completed guest frames at the requested output rate.
* A value of zero selects an uncapped presentation loop.
* A value of zero follows the measured host-display refresh rate.
*/
int psx_mod_set_frame_interpolation(uint32_t frames_per_second);
int psx_mod_set_auto_skip_fmv(int enabled);
/*
* Override one player's resolved controller presentation mode for this launch.
* This is intentionally a trusted-plugin API, not a generic launcher setting:
* games may hide Hybrid from their normal selector while offering it as an
* explicit game-owned mod.
*/
enum {
PSX_MOD_CONTROLLER_HYBRID = 0,
PSX_MOD_CONTROLLER_ANALOG = 1,
PSX_MOD_CONTROLLER_DIGITAL = 2
};
int psx_mod_set_controller_mode_override(uint32_t player,
uint32_t controller_mode);
/*
* Register a C plugin before main() on the compilers supported by the runtime.
* The registry itself uses function-local initialization, so constructor order
+33 -2
View File
@@ -57,6 +57,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "freeze_heartbeat.h"
#include "config_loader.h"
#include "game_options.h"
#include "mod_plugins.h"
#include "mod_runtime.h"
#include "crc32.h"
#include "disc_identity.h"
@@ -537,6 +538,13 @@ static int g_low_latency_input = 1;
static int g_video_vsync = 1;
static int g_frame_interpolation = 0;
static int g_frame_interpolation_fps = 0;
static int g_mod_controller_mode_override[2] = { -1, -1 };
static_assert((int)PSX_MOD_CONTROLLER_HYBRID ==
(int)PSXRecompV4::PAD_MODE_HYBRID);
static_assert((int)PSX_MOD_CONTROLLER_ANALOG ==
(int)PSXRecompV4::PAD_MODE_ANALOG);
static_assert((int)PSX_MOD_CONTROLLER_DIGITAL ==
(int)PSXRecompV4::PAD_MODE_DIGITAL);
static double g_host_refresh_hz = 0.0;
static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94;
static double g_frame_period_ms = PSX_FRAME_PERIOD_MS;
@@ -677,7 +685,7 @@ extern "C" int psx_mod_set_frame_interpolation(
g_video_vsync = 0;
g_frame_interpolation = 1;
g_frame_interpolation_fps =
frames_per_second ? (int)frames_per_second : -1;
frames_per_second ? (int)frames_per_second : 0;
if (frames_per_second) {
std::fprintf(stdout,
@@ -686,7 +694,7 @@ extern "C" int psx_mod_set_frame_interpolation(
(unsigned)frames_per_second);
} else {
std::fprintf(stdout,
"psxrecomp: mod selected uncapped presentation-only "
"psxrecomp: mod selected display-refresh presentation-only "
"interpolation; guest timing remains stock\n");
}
return 1;
@@ -705,6 +713,20 @@ extern "C" int psx_mod_set_auto_skip_fmv(int enabled) {
return 1;
}
extern "C" int psx_mod_set_controller_mode_override(
uint32_t player, uint32_t controller_mode) {
if (player >= 2 ||
controller_mode > (uint32_t)PSXRecompV4::PAD_MODE_DIGITAL) {
std::fprintf(stderr,
"psxrecomp: mod rejected controller-mode override "
"(player %u, mode %u)\n",
(unsigned)player, (unsigned)controller_mode);
return 0;
}
g_mod_controller_mode_override[player] = (int)controller_mode;
return 1;
}
/* [widescreen] per-game hooks (see config_loader.h): anchor scratch addr for
* tagged sprite prims + HUD SPRT center-squash. Inert at 0/false. */
static uint32_t g_ws_anchor_addr = 0;
@@ -6123,7 +6145,16 @@ int main(int argc, char** argv) {
return 1;
}
}
/* Activation callbacks are re-run after every launcher session. Clear
* game-owned controller overrides first so disabling a package cannot
* leave its prior mode latched across a soft return to the launcher. */
g_mod_controller_mode_override[0] = -1;
g_mod_controller_mode_override[1] = -1;
mod_runtime_activate_plugins();
if (g_mod_controller_mode_override[0] >= 0)
p1_mode = g_mod_controller_mode_override[0];
if (g_mod_controller_mode_override[1] >= 0)
p2_mode = g_mod_controller_mode_override[1];
/* Re-apply the resolved language to the translation layer. text_xlate_init
* (at config load) only saw the game.toml default; this folds in the
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Guard the mod API's zero sentinel: follow the display, never busy-loop."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MAIN = (ROOT / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8")
HEADER = (ROOT / "runtime" / "include" / "mod_plugins.h").read_text(
encoding="utf-8"
)
EXPECTED_ASSIGNMENT = """g_frame_interpolation_fps =
frames_per_second ? (int)frames_per_second : 0;"""
assert EXPECTED_ASSIGNMENT in MAIN, (
"psx_mod_set_frame_interpolation(0) must preserve the 0 sentinel so "
"gl_renderer_set_interpolation resolves it to the measured host refresh"
)
assert "frames_per_second ? (int)frames_per_second : -1" not in MAIN, (
"the -1 sentinel selects the uncapped interpolation busy loop"
)
assert "zero follows the measured host-display refresh rate" in HEADER, (
"the public plugin contract must document the display-rate sentinel"
)
print("frame interpolation display-rate sentinel guard passed")
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Guard the game-owned controller-mode override lifecycle."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MAIN = (ROOT / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8")
HEADER = (ROOT / "runtime" / "include" / "mod_plugins.h").read_text(
encoding="utf-8"
)
for symbol in (
"PSX_MOD_CONTROLLER_HYBRID",
"PSX_MOD_CONTROLLER_ANALOG",
"PSX_MOD_CONTROLLER_DIGITAL",
"psx_mod_set_controller_mode_override",
):
assert symbol in HEADER, f"missing trusted-plugin controller API: {symbol}"
reset = """g_mod_controller_mode_override[0] = -1;
g_mod_controller_mode_override[1] = -1;
mod_runtime_activate_plugins();"""
apply = """if (g_mod_controller_mode_override[0] >= 0)
p1_mode = g_mod_controller_mode_override[0];
if (g_mod_controller_mode_override[1] >= 0)
p2_mode = g_mod_controller_mode_override[1];"""
assert reset in MAIN, "controller overrides must reset before every activation pass"
assert apply in MAIN, "controller overrides must replace the resolved launch modes"
assert MAIN.index(reset) < MAIN.index(apply), "reset/activate must precede application"
print("mod controller override lifecycle guard passed")