diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 94b3507e..4194abee 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -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 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"; diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 3f33f107..6a438311 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -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; diff --git a/tools/cosim.py b/tools/cosim.py index 3f3a20d9..74bd17db 100644 --- a/tools/cosim.py +++ b/tools/cosim.py @@ -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: