fix(mods): reapply executable patches after state load

This commit is contained in:
Matthew Stanley
2026-08-06 21:00:58 -07:00
parent 9f05c25b3c
commit 577f6173b9
4 changed files with 163 additions and 20 deletions
+4
View File
@@ -35,6 +35,10 @@ extern "C" {
/* Called before a guest dispatch. Applies the complete main-EXE plan
* transactionally on the first dispatch to the configured entry point. */
void mod_runtime_on_dispatch(uint32_t target);
/* A full-machine savestate restores guest RAM after the initial entry-point
* application. Reapply the already-validated main-EXE plan so the current
* enabled mod selection remains authoritative after the restore. */
void mod_runtime_on_savestate_loaded(void);
/* Invokes activation callbacks for the committed plan. Call after the final
* launcher commit and before renderer/window initialization. */
void mod_runtime_activate_plugins(void);
+1
View File
@@ -409,6 +409,7 @@ static void present_session_reset(void) {
* longjmp). Clears present latches and forces the next vblank to show the
* restored VRAM — including a blank if display was disabled in the snapshot. */
extern "C" void psx_frontend_on_savestate_loaded(void) {
mod_runtime_on_savestate_loaded();
s_disabled_frame_presented = false;
s_force_present_after_load = true;
smooth_60_reset();
+90 -20
View File
@@ -153,6 +153,66 @@ void set_error(const std::string& error) {
state().error = error;
}
void apply_main_write(const ModResolution::Write& write) {
if (write.fields.empty()) {
for (size_t i = 0; i < write.replacement.size(); ++i)
psx_write_byte((uint32_t)write.location + (uint32_t)i,
write.replacement[i]);
dirty_ram_mark_executable_range(
(uint32_t)write.location & 0x1FFFFFFFu,
(uint32_t)write.replacement.size());
return;
}
for (const ModResolution::Write::Field& field : write.fields) {
for (size_t i = 0; i < field.replacement.size(); ++i)
psx_write_byte(
(uint32_t)write.location +
(uint32_t)field.offset + (uint32_t)i,
field.replacement[i]);
dirty_ram_mark_executable_range(
((uint32_t)write.location +
(uint32_t)field.offset) & 0x1FFFFFFFu,
(uint32_t)field.replacement.size());
}
}
bool restored_main_matches_plan(const RuntimeMods& s, uint32_t& failed_at) {
std::map<uint32_t, uint8_t> desired;
for (const ModResolution::Write& write : s.plan.writes) {
if (write.target != ModPatchTarget::MainExe) continue;
if (write.fields.empty()) {
for (size_t i = 0; i < write.replacement.size(); ++i)
desired[(uint32_t)write.location + (uint32_t)i] =
write.replacement[i];
} else {
for (const ModResolution::Write::Field& field : write.fields) {
for (size_t i = 0; i < field.replacement.size(); ++i)
desired[
(uint32_t)write.location +
(uint32_t)field.offset + (uint32_t)i] =
field.replacement[i];
}
}
}
for (const ModResolution::Write& write : s.plan.writes) {
if (write.target != ModPatchTarget::MainExe) continue;
for (size_t i = 0; i < write.expected.size(); ++i) {
const uint32_t address =
(uint32_t)write.location + (uint32_t)i;
const uint8_t observed = psx_read_byte(address);
if (observed == write.expected[i]) continue;
const auto replacement = desired.find(address);
if (replacement != desired.end() &&
observed == replacement->second)
continue;
failed_at = address;
return false;
}
}
return true;
}
bool sha256_file(const std::filesystem::path& path, std::string& out,
std::string* error) {
out.clear();
@@ -1066,26 +1126,7 @@ extern "C" void mod_runtime_on_dispatch(uint32_t target) {
}
for (const ModResolution::Write& write : s.plan.writes) {
if (write.target != ModPatchTarget::MainExe) continue;
if (write.fields.empty()) {
for (size_t i = 0; i < write.replacement.size(); ++i)
psx_write_byte((uint32_t)write.location + (uint32_t)i,
write.replacement[i]);
dirty_ram_mark_executable_range(
(uint32_t)write.location & 0x1FFFFFFFu,
(uint32_t)write.replacement.size());
} else {
for (const ModResolution::Write::Field& field : write.fields) {
for (size_t i = 0; i < field.replacement.size(); ++i)
psx_write_byte(
(uint32_t)write.location +
(uint32_t)field.offset + (uint32_t)i,
field.replacement[i]);
dirty_ram_mark_executable_range(
((uint32_t)write.location +
(uint32_t)field.offset) & 0x1FFFFFFFu,
(uint32_t)field.replacement.size());
}
}
apply_main_write(write);
}
s.main_applied = true;
if (!s.plan.writes.empty())
@@ -1093,6 +1134,35 @@ extern "C" void mod_runtime_on_dispatch(uint32_t target) {
s.plan.fingerprint.c_str());
}
extern "C" void mod_runtime_on_savestate_loaded(void) {
using namespace PSXRecompV4;
RuntimeMods& s = state();
if (!s.initialized || !s.plan.ok) return;
if (!s.main_applied) {
uint32_t failed_at = 0;
if (!restored_main_matches_plan(s, failed_at)) {
std::fprintf(stderr,
"psxrecomp: mod plan %s rejected after savestate restore at "
"0x%08X (expected-byte guard failed)\n",
s.plan.fingerprint.c_str(), (unsigned)failed_at);
return;
}
}
bool applied = false;
for (const ModResolution::Write& write : s.plan.writes) {
if (write.target != ModPatchTarget::MainExe) continue;
apply_main_write(write);
applied = true;
}
s.main_applied = true;
if (applied)
std::fprintf(stdout,
"psxrecomp: reapplied mod plan %s after savestate restore\n",
s.plan.fingerprint.c_str());
}
extern "C" void mod_runtime_enable_disc_patches(void) {
PSXRecompV4::state().disc_enabled = true;
}
+68
View File
@@ -25,6 +25,39 @@ extern "C" void psx_write_byte(uint32_t address, uint8_t value) {
ram[address & 0x1fffffu] = value;
}
extern "C" uint16_t psx_read_half(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint16_t)(ram[offset] | ((uint16_t)ram[offset + 1] << 8));
}
extern "C" void psx_write_half(uint32_t address, uint16_t value) {
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
}
extern "C" uint32_t psx_read_word(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint32_t)ram[offset] |
((uint32_t)ram[offset + 1] << 8) |
((uint32_t)ram[offset + 2] << 16) |
((uint32_t)ram[offset + 3] << 24);
}
extern "C" void psx_write_word(uint32_t address, uint32_t value) {
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
ram[offset + 2] = (uint8_t)(value >> 16);
ram[offset + 3] = (uint8_t)(value >> 24);
}
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
return 0;
}
extern "C" int psx_ws_x_margin(void) { return 0; }
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
extern "C" int fntrace_is_game_started(void) { return 1; }
@@ -292,6 +325,21 @@ int main() {
"adjacent sparse fields must compose while preserving guard-only "
"bytes");
/* A full-machine savestate replaces main RAM after the entry-point plan
* has already applied. Loading a stock checkpoint must not silently turn
* the currently enabled main-EXE features back off. */
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_savestate_loaded();
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
ram[0x1100] == 42 && ram[0x1102] == 43 &&
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"savestate restore must reapply the complete enabled main plan");
std::array<uint8_t, 2352> sector{};
sector[10] = 0xaa;
mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size());
@@ -374,6 +422,26 @@ int main() {
"a failed sparse guard-only byte must leave the complete main plan "
"untouched");
/* Loading can be requested while the boot executable is still running,
* before the configured game entry point has dispatched. The restored
* checkpoint itself supplies the bytes used to validate and apply the
* plan in that case. */
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_savestate_loaded();
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
ram[0x1100] == 42 && ram[0x1102] == 43 &&
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"pre-entry savestate restore must validate and apply the main plan");
mod_runtime_enable_disc_patches();
std::array<uint8_t, 2352> bad_sparse_disc{};
bad_sparse_disc[15] = 2;