mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge pull request #271 from Alexbeav/pr/stale-static-guards
codegen+runtime: stop executing stale static code when a game overlays its own text
This commit is contained in:
@@ -101,6 +101,33 @@ CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& conf
|
||||
{ const char* e = std::getenv("PSX_CPS"); cps_enabled_ = (e == nullptr || e[0] != '0'); }
|
||||
}
|
||||
|
||||
/* Inter-piece host transfers (fallthrough into a split piece / the next
|
||||
* function, and the legacy non-CPS split-target transfers) hand execution to
|
||||
* another compiled dispatch entry WITHOUT passing the dispatcher's
|
||||
* stale-static validation. A game that overlays its own text at runtime
|
||||
* (CMR2 streams transform-loop variants over its boot EXE) keeps executing
|
||||
* the stale static translation of the target piece through such an edge.
|
||||
* The guard revalidates the target entry's emitted ranges; on mismatch it
|
||||
* publishes the PC and unwinds to the trampoline, whose dispatch takes the
|
||||
* sanctioned dirty-RAM-interpreter fallback over the live bytes. */
|
||||
static std::string emit_stale_static_guard(uint32_t target, const std::string& indent) {
|
||||
return fmt::format(
|
||||
"{0}if (!psx_game_text_native_ok(0x{1:08X}u)) {{ cpu->pc = 0x{1:08X}u; return; }} /* stale-static guard */\n",
|
||||
indent, target);
|
||||
}
|
||||
|
||||
/* Same guard when only the emitted function NAME is at hand (the
|
||||
* fallthrough-to-next-function edges). Game functions are named func_%08X;
|
||||
* anything else gets no guard (identical to the pre-guard emission). */
|
||||
static std::string emit_stale_static_guard_named(const std::string& name,
|
||||
const std::string& indent) {
|
||||
if (name.rfind("func_", 0) != 0) return "";
|
||||
char* end = nullptr;
|
||||
unsigned long v = strtoul(name.c_str() + 5, &end, 16);
|
||||
if (!end || *end != '\0' || v == 0) return "";
|
||||
return emit_stale_static_guard((uint32_t)v, indent);
|
||||
}
|
||||
|
||||
uint32_t CodeGenerator::partial_block_cycle_count(uint32_t addr,
|
||||
const ControlFlowGraph& cfg) const {
|
||||
if (cfg.blocks.count(addr)) {
|
||||
@@ -2154,6 +2181,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS taken: split */\n", branch_target);
|
||||
} else if (known_functions_.count(branch_target)) {
|
||||
ss << emit_interrupt_check(branch_target, config_.indent + config_.indent);
|
||||
ss << emit_stale_static_guard(branch_target, config_.indent + config_.indent);
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* taken: split piece */\n", branch_target);
|
||||
} else {
|
||||
@@ -2172,6 +2200,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS not taken: split */\n", fall_through_addr);
|
||||
} else if (known_functions_.count(fall_through_addr)) {
|
||||
ss << emit_interrupt_check(fall_through_addr, config_.indent + config_.indent);
|
||||
ss << emit_stale_static_guard(fall_through_addr, config_.indent + config_.indent);
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* not taken: split piece */\n", fall_through_addr);
|
||||
} else {
|
||||
@@ -2197,6 +2226,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
} else if (block.exit_instr.target != 0 && known_functions_.count(block.exit_instr.target)) {
|
||||
// Jump target is out-of-function and is a known function start
|
||||
ss << emit_interrupt_check(block.exit_instr.target, config_.indent);
|
||||
ss << emit_stale_static_guard(block.exit_instr.target, config_.indent);
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* j to split piece */\n",
|
||||
block.exit_instr.target);
|
||||
@@ -2368,8 +2398,13 @@ std::string CodeGenerator::translate_basic_block(
|
||||
ss << config_.indent << "{ uint32_t _csp = cpu->gpr[29];\n";
|
||||
ss << emit_interrupt_check(target, config_.indent);
|
||||
if (known_functions_.count(target) > 0) {
|
||||
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
|
||||
ss << config_.indent << fmt::format("if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n", addr + 8);
|
||||
ss << config_.indent << fmt::format(
|
||||
"if (psx_game_text_native_ok(0x{0:08X}u)) {{ func_{0:08X}(cpu); /* jal */\n", target);
|
||||
ss << config_.indent << fmt::format(
|
||||
"if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return;\n", addr + 8);
|
||||
ss << config_.indent << fmt::format(
|
||||
"}} else {{ call_by_address(cpu, 0x{:08X}u); /* jal: stale-static guard */\n", target);
|
||||
ss << config_.indent << "if (g_psx_call_bail) return; (void)_csp; } }\n";
|
||||
} else {
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
|
||||
/* psx_dispatch_call validated the (ra, sp) contract;
|
||||
@@ -2383,6 +2418,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
// Split-function: JAL continuation is outside this function piece.
|
||||
// Tail-call to the continuation piece (at exit_addr + 8, past delay slot).
|
||||
if (known_functions_.count(cont_addr)) {
|
||||
ss << emit_stale_static_guard(cont_addr, config_.indent);
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* jal cont: split piece */\n", cont_addr);
|
||||
} else {
|
||||
@@ -2424,6 +2460,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
} else {
|
||||
// Split-function: JALR continuation is outside this function piece.
|
||||
if (known_functions_.count(cont_addr)) {
|
||||
ss << emit_stale_static_guard(cont_addr, config_.indent);
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* jalr cont: split piece */\n", cont_addr);
|
||||
} else {
|
||||
@@ -2449,6 +2486,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
} else if (block.exit_instr.type == ControlFlowType::None) {
|
||||
uint32_t next_addr = block.end_addr + 4;
|
||||
if (known_functions_.count(next_addr) > 0) {
|
||||
ss << emit_stale_static_guard(next_addr, config_.indent);
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* fallthrough to split piece */\n",
|
||||
next_addr);
|
||||
@@ -2787,6 +2825,7 @@ GeneratedFunction CodeGenerator::generate_function(
|
||||
const BasicBlock& last = cfg.blocks.at(cfg.block_order.back());
|
||||
bool is_reachable = last.is_entry || !last.predecessors.empty();
|
||||
if (is_reachable) {
|
||||
body_ss << emit_stale_static_guard_named(fallthrough_name, " ");
|
||||
body_ss << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
|
||||
fallthrough_name);
|
||||
}
|
||||
@@ -3035,6 +3074,7 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
|
||||
last_block.exit_instr.type == ControlFlowType::Jump) &&
|
||||
last_block.successors.empty());
|
||||
if (needs_fallthrough) {
|
||||
body << emit_stale_static_guard_named(fallthrough_name, " ");
|
||||
body << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
|
||||
fallthrough_name);
|
||||
}
|
||||
@@ -3224,6 +3264,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern void cosim_block(uint32_t block_leader_phys);\n";
|
||||
ss << "extern void cosim_instr(uint32_t pc);\n";
|
||||
ss << "#endif\n";
|
||||
ss << "extern int psx_game_text_native_ok(uint32_t addr); /* stale-static guard (dispatch shard) */\n";
|
||||
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
|
||||
ss << "extern void psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
|
||||
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
|
||||
|
||||
+28
-10
@@ -482,15 +482,19 @@ int dirty_ram_text_native_ok(uint32_t phys) {
|
||||
* a mismatch never poisons an unrelated 4 KB page forever: every decision is
|
||||
* made from the live bytes the native body will actually execute.
|
||||
*
|
||||
* exec_pc is the dispatch/resume address. Ranges that end at or before that PC
|
||||
* are skipped, and a range that straddles it is clipped to [exec_pc, end). A
|
||||
* runtime patch of a function prologue must not block a compiled continuation
|
||||
* that never fetches the patched bytes. */
|
||||
* exec_pc is the dispatch/resume address, kept for observability and future
|
||||
* use; ranges are validated IN FULL regardless of it. The former
|
||||
* [exec_pc, end) clip assumed a continuation "never fetches the patched
|
||||
* bytes" behind its resume PC — false for any entry with a backward edge:
|
||||
* a post-call continuation re-enters mid-function and loops back into the
|
||||
* clipped-away region, executing stale static code (CMR2 overlay
|
||||
* corruption, second locus 0x80016B34). A genuinely patched prologue now
|
||||
* blocks the whole entry to the interpreter — correct, merely slower. */
|
||||
int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs,
|
||||
uint32_t count,
|
||||
uint32_t exec_pc) {
|
||||
if (!text_ref_image || !lo_len_pairs || count == 0) return 0;
|
||||
uint32_t at = exec_pc & 0x1FFFFFFFu;
|
||||
(void)exec_pc;
|
||||
int any = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint32_t phys = lo_len_pairs[i * 2u] & 0x1FFFFFFFu;
|
||||
@@ -500,12 +504,26 @@ int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs,
|
||||
g_text_native_blocked++;
|
||||
return 0;
|
||||
}
|
||||
if (phys + len <= at) continue;
|
||||
if (phys < at) {
|
||||
len -= at - phys;
|
||||
phys = at;
|
||||
}
|
||||
any = 1;
|
||||
/* Page-clean fast path: every page this range touches is neither
|
||||
* guard-modified nor runtime-dirty, so its bytes still equal the
|
||||
* reference image — skip the memcmp. This keeps the emitted
|
||||
* stale-static guards on inter-piece host transfers near-free on
|
||||
* the (overwhelmingly common) pristine pages. */
|
||||
{
|
||||
uint32_t p0 = phys >> DIRTY_RAM_PAGE_SHIFT;
|
||||
uint32_t p1 = (phys + len - 1u) >> DIRTY_RAM_PAGE_SHIFT;
|
||||
uint32_t p;
|
||||
int clean = 1;
|
||||
for (p = p0; p <= p1; p++) {
|
||||
if ((text_modified_bitmap[p >> 5] & (1u << (p & 31u))) ||
|
||||
dirty_ram_is_dirty(p << DIRTY_RAM_PAGE_SHIFT)) {
|
||||
clean = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clean) continue;
|
||||
}
|
||||
if (memcmp(ram + phys, text_ref_image + (phys - text_ref_lo), len) != 0) {
|
||||
uint32_t off = 0;
|
||||
const uint8_t *live = ram + phys;
|
||||
|
||||
+5
-2
@@ -200,8 +200,11 @@ def main():
|
||||
for _ in range(max(0, n)):
|
||||
cmd(sa, "step 1", timeout=1200)
|
||||
cmd(sb, "step 1", timeout=1200)
|
||||
da = parse_cpu(cmd(sa, "cpu"))
|
||||
db = parse_cpu(cmd(sb, "cpu"))
|
||||
ra = cmd(sa, "cpu"); rb = cmd(sb, "cpu")
|
||||
da = parse_cpu(ra)
|
||||
db = parse_cpu(rb)
|
||||
print(f"A cpu dump: {ra}", flush=True)
|
||||
print(f"B cpu dump: {rb}", flush=True)
|
||||
print(f"CPU field-diff at cp {args.cpudiff_at_cp} (A={args.a} B={args.b}):", flush=True)
|
||||
diffs = [k for k in da if da.get(k) != db.get(k)]
|
||||
if not diffs:
|
||||
|
||||
Reference in New Issue
Block a user