From 4a7a8d54acfa0009bc3a5c2a7585c5aecd69a96a Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 17 Oct 2026 13:13:02 -0700 Subject: [PATCH] Keep diagnostic display independent and stage setup replay helper --- docs/PLAYTEST_DIAGNOSTICS.md | 5 +- runtime/include/mod_packages.h | 2 + runtime/include/mod_plugins.h | 5 ++ runtime/src/mod_packages.cpp | 24 ++++++++- runtime/src/mod_runtime.cpp | 2 + runtime/src/playtest_diagnostics.cpp | 43 +++++++++++----- runtime/tests/test_mod_packages.cpp | 11 ++++ runtime/tests/test_playtest_diagnostics.cpp | 57 ++++++++++++++++++--- tools/package_setup_host.sh | 3 ++ 9 files changed, 128 insertions(+), 24 deletions(-) diff --git a/docs/PLAYTEST_DIAGNOSTICS.md b/docs/PLAYTEST_DIAGNOSTICS.md index 95ad0aa8..6a5e1caa 100644 --- a/docs/PLAYTEST_DIAGNOSTICS.md +++ b/docs/PLAYTEST_DIAGNOSTICS.md @@ -29,7 +29,10 @@ Create a title mod package with a recording feature whose plugin is `game.diagnostics.osd`. The package controls their defaults. The helper also covers an existing `psx-runtime-pgxp` sibling and stages `replay-diagnostics.py` beside both executables. The bundled-release packager includes this script when -present; AppImage wrapping preserves it in the payload. No texture feature, +present, as does the setup-host kit packager; AppImage wrapping preserves it in +the payload. Input Display also works with Recording disabled, with no RAM +digest or file capture. Session changes and netplay suspension clear the display. +No texture feature, texture package or other enhancement is enabled or exposed by this helper. The implementation reads no title-specific addresses. `telemetry.csv` stores diff --git a/runtime/include/mod_packages.h b/runtime/include/mod_packages.h index 19e4cdbc..3d18e25d 100644 --- a/runtime/include/mod_packages.h +++ b/runtime/include/mod_packages.h @@ -595,6 +595,8 @@ private: bool mod_register_builtin_resolver(const std::string& id, ModBuiltinResolver resolver); void mod_clear_builtin_resolvers_for_tests(); bool mod_register_activation_plugin(const std::string& id, void (*callback)(void)); +bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void)); +void mod_reset_plugin_sessions(); bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)); bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void)); void mod_invoke_savestate_plugin(const std::string& id); diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index d9fc3954..083668c9 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -25,6 +25,11 @@ typedef int (*PSXModFunctionFilterCallback)(struct CPUState* cpu, */ int psx_mod_register_activation_plugin(const char* id, PSXModActivationCallback callback); +/* Host-only cleanup before each plan activates, including plans that omit this + * plugin, and when netplay clears the offline plan. Runs with emulation stopped + * and without a current feature/resource context. Never mutate guest state. */ +int psx_mod_register_session_reset_plugin(const char* id, + PSXModActivationCallback callback); int psx_mod_register_vblank_plugin(const char* id, PSXModVBlankCallback callback); /* Runs after successful guest-state restore, before the restored PC resumes. diff --git a/runtime/src/mod_packages.cpp b/runtime/src/mod_packages.cpp index 7f8b57c0..d27e8aac 100644 --- a/runtime/src/mod_packages.cpp +++ b/runtime/src/mod_packages.cpp @@ -43,6 +43,7 @@ std::map& media_preparers() { struct RegisteredPlugin { void (*savestate)(void) = nullptr; PSXModActivationCallback activation = nullptr; + PSXModActivationCallback session_reset = nullptr; PSXModVBlankCallback vblank = nullptr; /* Generated-function entry hooks, keyed by guest address. One id may * observe several functions. */ @@ -1491,6 +1492,19 @@ bool mod_register_activation_plugin(const std::string& id, void (*callback)(void return true; } +bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void)) { + if (!valid_id(id) || !callback) return false; + RegisteredPlugin& plugin = registered_plugins()[id]; + if (plugin.session_reset) return false; + plugin.session_reset = callback; + return true; +} + +void mod_reset_plugin_sessions() { + for (const auto& entry : registered_plugins()) + if (entry.second.session_reset) entry.second.session_reset(); +} + bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) { if (!valid_id(id) || !callback) return false; RegisteredPlugin& plugin = registered_plugins()[id]; @@ -1523,7 +1537,7 @@ bool mod_register_function_filter_plugin(const std::string& id, uint32_t address bool mod_plugin_registered(const std::string& id) { const auto found = registered_plugins().find(id); return found != registered_plugins().end() && - (found->second.activation || found->second.vblank || found->second.savestate || + (found->second.activation || found->second.session_reset || found->second.vblank || found->second.savestate || !found->second.function_entries.empty() || !found->second.guest_functions.empty() || !found->second.instructions.empty()); } @@ -1605,7 +1619,7 @@ void mod_clear_plugins_for_tests() { std::vector mod_registered_plugin_ids() { std::vector ids; for (const auto& entry : registered_plugins()) - if (entry.second.activation || entry.second.vblank || + if (entry.second.activation || entry.second.session_reset || entry.second.vblank || !entry.second.function_entries.empty() || !entry.second.guest_functions.empty()) ids.push_back(entry.first); return ids; /* std::map keeps them sorted */ @@ -4259,6 +4273,12 @@ extern "C" int psx_mod_register_activation_plugin( PSXRecompV4::mod_register_activation_plugin(id, callback) ? 1 : 0; } +extern "C" int psx_mod_register_session_reset_plugin( + const char* id, PSXModActivationCallback callback) { + return id && + PSXRecompV4::mod_register_session_reset_plugin(id, callback) ? 1 : 0; +} + extern "C" int psx_mod_register_savestate_plugin( const char* id, PSXModActivationCallback callback) { return id && diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index d7c0cdb7..7e96c7ae 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1599,6 +1599,7 @@ bool mod_runtime_initialize(const std::filesystem::path& root, } bool mod_runtime_clear_for_netplay(std::string* error) { + mod_reset_plugin_sessions(); psx_mod_netplay_set_active(0); (void)psx_mod_netplay_set_options_identity(""); state().ticket_ready = false; @@ -2844,6 +2845,7 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by extern "C" void mod_runtime_activate_plugins(void) { using namespace PSXRecompV4; HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION); + mod_reset_plugin_sessions(); RuntimeMods& s = state(); s.ticket_ready = false; psx_projection_reset_session(); diff --git a/runtime/src/playtest_diagnostics.cpp b/runtime/src/playtest_diagnostics.cpp index a03e278a..f4b5b545 100644 --- a/runtime/src/playtest_diagnostics.cpp +++ b/runtime/src/playtest_diagnostics.cpp @@ -65,6 +65,7 @@ std::ifstream replay; Row pending{}; // replay row the next VBlank verifies bool pending_valid; uint32_t sequence; +uint32_t osd_sequence; bool started, replaying, replay_failed, recording_failed, owns_sources, osd_enabled; bool baseline_loaded; bool input_only, legacy_route; @@ -453,7 +454,7 @@ void restored() { psx_mod_counter_add("psxrecomp.diagnostics.state-segments", 1); } -std::string input_line(unsigned seat, uint16_t buttons) { +std::string input_line(unsigned seat, uint16_t buttons, uint32_t frame) { static const char *names[] = {"Se", "L3", "R3", "St", "U", "R", "D", "L", "L2", "R2", "L1", "R1", "T", "O", "X", "[]"}; std::string text = "P" + std::to_string(seat + 1) + ":"; bool held = false; @@ -461,17 +462,24 @@ std::string input_line(unsigned seat, uint16_t buttons) { text += ' '; text += names[bit]; held = true; } if (!held) text += " -"; - text += " F" + std::to_string(sequence); + text += " F" + std::to_string(frame); if (replay_failed || recording_failed) text += " ERROR"; else if (replaying) text += input_only ? " ROUTE" : " PLAY"; else if (recording.is_open()) text += " REC"; return text; } -void show(const Row& row) { - if (!osd_enabled) return; - auto first = input_line(0, (uint16_t)row[ColButtons]), second = input_line(1, (uint16_t)row[ColButtons + 1]); +void tick_osd() { + if (!osd_enabled || psx_mod_netplay_is_active()) { + host_osd_set_diagnostics(nullptr, nullptr); + return; + } + // Input Display is independent of Recording. Only sample the accepted + // button words; do not hash RAM, enqueue rows or create session files. + auto first = input_line(0, sio_get_pad_buttons_slot(0), osd_sequence); + auto second = input_line(1, sio_get_pad_buttons_slot(1), osd_sequence); host_osd_set_diagnostics(first.c_str(), second.c_str()); + ++osd_sequence; } const char *column_name(unsigned column) { @@ -513,7 +521,6 @@ void tick() { verify(row); if (replaying) { psx_mod_counter_add(input_only ? "psxrecomp.diagnostics.route-inputs" : "psxrecomp.diagnostics.replayed-inputs", 1); - show(row); const uint32_t verified = sequence + 1; if (!next_row()) { replay.close(); replaying = false; pending_valid = false; @@ -533,7 +540,6 @@ void tick() { recording_location(std::string("Recording FAILED: ") + recording.error_text()); fprintf(stderr, "psxrecomp.diagnostics: %s\n", recording.error_text()); } - show(row); } ++sequence; const uint64_t ns = (uint64_t)std::chrono::duration_cast( @@ -543,13 +549,21 @@ void tick() { if (ns > 1000000) ++stats.slow; } -void activate() { +void reset_session() { release_sources(); close_recording(); replay.close(); recording.clear(); replay.clear(); started = replaying = replay_failed = recording_failed = pending_valid = false; baseline_loaded = pending_baseline_unavailable = false; input_only = legacy_route = false; legacy_connected = 4; + osd_enabled = false; + sequence = osd_sequence = 0; last_generation = 0; + pending_baseline.clear(); expected_baseline.clear(); + replay_path.clear(); baseline_path.clear(); + host_osd_set_diagnostics(nullptr, nullptr); +} + +void activate() { #ifndef PSX_NO_DEBUG_TOOLS const char* route = getenv("PSX_DIAGNOSTIC_INPUT_ONLY"); input_only = route && !strcmp(route,"1"); @@ -564,9 +578,6 @@ void activate() { started = true; fail("input-only routes require a developer build"); return; } #endif - osd_enabled = false; - sequence = 0; last_generation = 0; - pending_baseline.clear(); expected_baseline.clear(); const char *path = getenv("PSX_DIAGNOSTIC_REPLAY"); replay_path = path ? path : ""; const char *baseline = getenv("PSX_DIAGNOSTIC_BASELINE"); @@ -605,14 +616,18 @@ void activate() { host_osd_set_diagnostics(nullptr, nullptr); } -/* The input display is its own feature so it can be switched off while - * recording stays on. Activation order follows the manifest. */ -void activate_osd() { osd_enabled = true; } +void activate_osd() { + osd_enabled = true; + osd_sequence = 0; + host_osd_set_diagnostics(nullptr, nullptr); +} } PSX_MOD_CONSTRUCTOR(psx_register_playtest_diagnostics) { + psx_mod_register_session_reset_plugin(PSX_DIAGNOSTIC_MOD_ID, reset_session); psx_mod_register_activation_plugin(PSX_DIAGNOSTIC_MOD_ID, activate); psx_mod_register_vblank_plugin(PSX_DIAGNOSTIC_MOD_ID, tick); psx_mod_register_savestate_plugin(PSX_DIAGNOSTIC_MOD_ID, restored); psx_mod_register_activation_plugin(PSX_DIAGNOSTIC_MOD_ID ".osd", activate_osd); + psx_mod_register_vblank_plugin(PSX_DIAGNOSTIC_MOD_ID ".osd", tick_osd); } diff --git a/runtime/tests/test_mod_packages.cpp b/runtime/tests/test_mod_packages.cpp index cd232153..f0bd5e71 100644 --- a/runtime/tests/test_mod_packages.cpp +++ b/runtime/tests/test_mod_packages.cpp @@ -14,6 +14,7 @@ namespace fs = std::filesystem; using namespace PSXRecompV4; static int failures; +static int session_resets; static void check(bool value, const char* message) { if (!value) { @@ -321,6 +322,16 @@ int main() { check(mod_register_vblank_plugin( "test.vblank", +[]() {}), "test plugin must register"); + check(mod_register_session_reset_plugin( + "test.vblank", +[]() { ++session_resets; }), + "session cleanup must register independently of active features"); + check(!mod_register_session_reset_plugin("test.vblank", +[]() {}), + "duplicate session cleanup must be rejected"); + // No manifest or active plan exists yet: deselected plugins still clean + // up the host state left by the previous session. + mod_reset_plugin_sessions(); + mod_reset_plugin_sessions(); + check(session_resets == 2, "each session boundary must invoke registered cleanup"); write_text(root / "packages/plugin.mod/1.0.0/manifest.toml", "format_version = 5\n" "id = \"plugin.mod\"\n" diff --git a/runtime/tests/test_playtest_diagnostics.cpp b/runtime/tests/test_playtest_diagnostics.cpp index c6e3fafa..03e42ce8 100644 --- a/runtime/tests/test_playtest_diagnostics.cpp +++ b/runtime/tests/test_playtest_diagnostics.cpp @@ -37,17 +37,27 @@ static int status_ok=1,status_slot=-1; static const char *status_op="save"; static std::vector slot_image; static std::string plan_text="fixture-plan"; -static void (*activation)(),(*vblank)(),(*savestate)(),(*osd_activation)(); +static void (*activation)(),(*vblank)(),(*savestate)(),(*osd_activation)(),(*osd_vblank)(),(*session_reset)(); +static void (*recording_activation)(); +static uint64_t ram_reads; + +static void activate_recording_plan() {session_reset();recording_activation();} uint32_t psx_mod_read_word(uint32_t a) { + ++ram_reads; uint32_t v;memcpy(&v,&memory[a&0x1FFFFC],4);return v; } int psx_mod_register_activation_plugin(const char *id,PSXModActivationCallback cb) { - if(!strcmp(id,"psxrecomp.diagnostics"))activation=cb; + if(!strcmp(id,"psxrecomp.diagnostics")) {recording_activation=cb;activation=activate_recording_plan;} else if(!strcmp(id,"psxrecomp.diagnostics.osd"))osd_activation=cb; return 1; } -int psx_mod_register_vblank_plugin(const char*,PSXModVBlankCallback cb) {vblank=cb;return 1;} +int psx_mod_register_session_reset_plugin(const char*,PSXModActivationCallback cb) {session_reset=cb;return 1;} +int psx_mod_register_vblank_plugin(const char *id,PSXModVBlankCallback cb) { + if(!strcmp(id,"psxrecomp.diagnostics"))vblank=cb; + else if(!strcmp(id,"psxrecomp.diagnostics.osd"))osd_vblank=cb; + return 1; +} int psx_mod_register_savestate_plugin(const char*,PSXModActivationCallback cb) {savestate=cb;return 1;} int psx_mod_netplay_is_active(void) {return online;} int psx_mod_set_controller_source(uint32_t seat,PSXModControllerSource source) {sources.at(seat)=source;source_connected.at(seat)=1;return 1;} @@ -144,6 +154,7 @@ static void guest_frame(unsigned t) { static void run_frames(unsigned from,unsigned to,int tamper=-1) { for(unsigned t=from;t