mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Keep diagnostic display independent and stage setup replay helper
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -43,6 +43,7 @@ std::map<std::string, MediaPreparer>& 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<std::string> mod_registered_plugin_ids() {
|
||||
std::vector<std::string> 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 &&
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<std::chrono::nanoseconds>(
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -37,17 +37,27 @@ static int status_ok=1,status_slot=-1;
|
||||
static const char *status_op="save";
|
||||
static std::vector<uint8_t> 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<to;++t) {
|
||||
vblank();
|
||||
if(osd_enabled)osd_vblank();
|
||||
if((int)t==tamper)memory[0x80000]^=1;
|
||||
guest_frame(t);boundary_sample(script(t));
|
||||
}
|
||||
@@ -172,13 +183,37 @@ int main(int argc,char **argv) {
|
||||
std::filesystem::create_directories(PSXRecompV4::mod_runtime_root());
|
||||
{std::ofstream state(PSXRecompV4::mod_runtime_root()/"state.toml");state<<"format_version = 2\n";}
|
||||
{std::ofstream settings(std::filesystem::path(application_root)/"settings.toml");settings<<"[video]\n";}
|
||||
CHECK(activation && vblank && savestate && osd_activation);
|
||||
CHECK(activation && vblank && savestate && osd_activation && osd_vblank && session_reset);
|
||||
|
||||
// Display-only plans invoke their own callback: accepted input changes
|
||||
// are visible without creating files, hashing RAM or taking pad ownership.
|
||||
reset_machine();session_reset();osd_activation();
|
||||
const auto display_reads=ram_reads;
|
||||
sio_buttons[0]=0xBFFF;osd_vblank();
|
||||
CHECK(shown[0].find(" X ")!=std::string::npos && shown[0].find("F0")!=std::string::npos);
|
||||
CHECK(shown[0].find("REC")==std::string::npos && !recording.is_open() && !telemetry.is_open());
|
||||
sio_buttons[0]=0xFFFF;sio_buttons[1]=0x7FFF;osd_vblank();
|
||||
CHECK(shown[0].find(" X ")==std::string::npos && shown[1].find(" [] ")!=std::string::npos);
|
||||
CHECK(shown[1].find("F1")!=std::string::npos && ram_reads==display_reads);
|
||||
CHECK(!sources[0] && !sources[1] && sequence==0);
|
||||
CHECK(!std::filesystem::exists(std::filesystem::path(application_root)/"saves/diagnostics"));
|
||||
// Reactivation starts at F0; online suspension clears stale text and
|
||||
// offline resumption does not count the suspended network frames.
|
||||
osd_activation();CHECK(shown[0].empty());osd_vblank();
|
||||
CHECK(shown[0].find("F0")!=std::string::npos);
|
||||
online=true;osd_vblank();CHECK(shown[0].empty() && shown[1].empty());
|
||||
online=false;osd_vblank();CHECK(shown[0].find("F1")!=std::string::npos);
|
||||
session_reset();CHECK(shown[0].empty() && shown[1].empty() && !osd_enabled);
|
||||
|
||||
// ---- Record 200 frames of scripted two-seat input --------------------------
|
||||
env("PSX_DIAGNOSTIC_REPLAY","");env("PSX_DIAGNOSTIC_BASELINE","");
|
||||
reset_machine();activation();osd_activation();
|
||||
// The two features may activate in either order after the engine reset.
|
||||
reset_machine();session_reset();osd_activation();recording_activation();
|
||||
CHECK(osd_enabled);
|
||||
CHECK(!sources[0] && !sources[1]);
|
||||
const auto recording_reads=ram_reads;
|
||||
run_frames(0,200);
|
||||
CHECK(ram_reads-recording_reads==200*DigestWords); // display never adds RAM sampling
|
||||
CHECK(shown[0].find("REC")!=std::string::npos && shown[0].find("F199")!=std::string::npos);
|
||||
recording.flush();
|
||||
telemetry.flush();
|
||||
@@ -297,7 +332,7 @@ int main(int argc,char **argv) {
|
||||
{auto before=std::distance(std::filesystem::directory_iterator(std::filesystem::path(application_root)/"saves/diagnostics"),
|
||||
std::filesystem::directory_iterator{});
|
||||
online=true;reset_machine();activation();osd_activation();
|
||||
for(unsigned t=0;t<5;++t)vblank();
|
||||
for(unsigned t=0;t<5;++t) {vblank();osd_vblank();}
|
||||
CHECK(!recording.is_open() && shown[0].empty() && sequence==0);
|
||||
CHECK(std::distance(std::filesystem::directory_iterator(std::filesystem::path(application_root)/"saves/diagnostics"),
|
||||
std::filesystem::directory_iterator{})==before);
|
||||
@@ -359,6 +394,14 @@ int main(int argc,char **argv) {
|
||||
auto location=slurp(std::filesystem::path(application_root)/"diagnostics-location.txt");
|
||||
CHECK(location.find("Recording: ")!=std::string::npos);
|
||||
CHECK(location.find("psxrecomp-diagnostics")!=std::string::npos);
|
||||
close_recording();
|
||||
// Play Again with Recording disabled must release the previous capture
|
||||
// and clear its REC label before a new display-only plan starts.
|
||||
session_reset();
|
||||
CHECK(!recording.is_open() && !telemetry.is_open() && shown[0].empty());
|
||||
const auto restarted_reads=ram_reads;
|
||||
osd_activation();osd_vblank();
|
||||
CHECK(shown[0].find("F0")!=std::string::npos && shown[0].find("REC")==std::string::npos);
|
||||
CHECK(ram_reads==restarted_reads && !recording.is_open() && !sources[0]);
|
||||
session_reset();
|
||||
printf("diagnostic recording/replay boundary checks passed\n");return 0;
|
||||
}
|
||||
|
||||
@@ -280,6 +280,9 @@ else
|
||||
fi
|
||||
EXE_BASENAME="$(basename "${EXE}")"
|
||||
EXE_DIR="$(dirname "${EXE}")"
|
||||
if [[ -f "${EXE_DIR}/replay-diagnostics.py" ]]; then
|
||||
cp -a "${EXE_DIR}/replay-diagnostics.py" "${STAGE}/replay-diagnostics.py"
|
||||
fi
|
||||
|
||||
# Windows: CMake may already have placed imported runtime DLLs (zlib1.dll)
|
||||
# next to the host. Copy siblings into the stage before MinGW bundling —
|
||||
|
||||
Reference in New Issue
Block a user