Skip to content
Closed
16 changes: 12 additions & 4 deletions recompiler/include/gte_register_classification.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
namespace PSXRecompGTERegisters {

constexpr bool data_read_needs_helper(uint8_t reg) {
return (reg >= 8 && reg <= 11) || reg == 15 || reg == 23 ||
/* Must cover every reg gte_read_data() treats specially (gte.cpp):
* 1,3,5,8-11 sign-extend; 7,16-19 mask to 16 bits; 15 mirrors 14;
* 28/29 pack IRGB; 31 computes LZCR. 23 kept from the legacy set. */
return reg == 1 || reg == 3 || reg == 5 || reg == 7 ||
(reg >= 8 && reg <= 11) || reg == 15 ||
(reg >= 16 && reg <= 19) || reg == 23 ||
reg == 28 || reg == 29 || reg == 31;
}

Expand All @@ -19,7 +24,10 @@ constexpr bool data_write_needs_helper(uint8_t reg) {
}

constexpr bool ctrl_read_needs_helper(uint8_t reg) {
return reg == 26 || reg == 27 || reg == 29 || reg == 30 || reg == 31;
/* Must cover every reg gte_read_ctrl() sign-extends (gte.cpp):
* 4, 12, 20, 26, 27, 29, 30. 31 kept from the legacy set. */
return reg == 4 || reg == 12 || reg == 20 || reg == 26 ||
reg == 27 || reg == 29 || reg == 30 || reg == 31;
}

constexpr bool ctrl_write_needs_helper(uint8_t reg) {
Expand All @@ -37,11 +45,11 @@ constexpr uint32_t helper_mask(bool (*predicate)(uint8_t)) {
/* Independent architectural expectations. These deliberately do not derive
* from one another: changing a predicate requires an explicit review of the
* register mask, rather than letting both emitter tests agree on a bad table. */
static_assert(helper_mask(data_read_needs_helper) == 0xB0808F00u,
static_assert(helper_mask(data_read_needs_helper) == 0xB08F8FAAu,
"GTE data-read helper set changed");
static_assert(helper_mask(data_write_needs_helper) == 0xF08FFFAAu,
"GTE data-write helper set changed");
static_assert(helper_mask(ctrl_read_needs_helper) == 0xEC000000u,
static_assert(helper_mask(ctrl_read_needs_helper) == 0xEC101010u,
"GTE control-read helper set changed");
static_assert(helper_mask(ctrl_write_needs_helper) == 0xEC101010u,
"GTE control-write helper set changed");
Expand Down
45 changes: 43 additions & 2 deletions recompiler/src/code_generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a stale split piece is reached through a nested non-CPS call, this return unwinds only the current generated function and the caller resumes its continuation as if the callee returned normally. Set the call-bail flag before returning so enclosing generated frames stop and the trampoline dispatches cpu->pc through the interpreter fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At recompiler/src/code_generator.cpp, line 115:

<comment>When a stale split piece is reached through a nested non-CPS call, this `return` unwinds only the current generated function and the caller resumes its continuation as if the callee returned normally. Set the call-bail flag before returning so enclosing generated frames stop and the trampoline dispatches `cpu->pc` through the interpreter fallback.</comment>

<file context>
@@ -101,6 +101,33 @@ CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& conf
+ * 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);
+}
</file context>
Suggested change
"{0}if (!psx_game_text_native_ok(0x{1:08X}u)) {{ cpu->pc = 0x{1:08X}u; return; }} /* stale-static guard */\n",
"{0}if (!psx_game_text_native_ok(0x{1:08X}u)) {{ cpu->pc = 0x{1:08X}u; g_psx_call_bail = 1; 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)) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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";
Expand Down
77 changes: 39 additions & 38 deletions runtime/src/cosim.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ typedef struct { uint64_t cp; uint32_t pc; uint64_t hash; uint32_t istat, imask;
static Entry g_ring[RING_N];
static uint64_t g_cp = 0; /* checkpoints crossed so far */
static uint64_t g_chain = 1469598103934665603ULL; /* cumulative FNV over checkpoints */
static uint64_t g_stride = 4096; /* guest cycles per checkpoint (coordinator sets) */
static uint64_t g_next_cp = 0; /* next cycle at which to checkpoint */
static uint64_t g_stride = 4096; /* retired instructions per checkpoint */
static uint64_t g_next_cp = 0; /* icount at which to take the next checkpoint */
static uint32_t g_last_leader_pc = 0; /* set by cosim_block, reported at checkpoints */
static uint64_t g_pending_first_cycle = 0;
static uint64_t g_pending_count = 0;
static uint64_t g_icount = 0; /* retirement events (cosim_instr calls) */

/* lockstep control (written by TCP thread, read by guest thread).
* The guest parks at EVERY checkpoint boundary (a deterministic guest cycle = multiple
Expand All @@ -77,25 +76,29 @@ void cosim_block(uint32_t pc) { g_last_leader_pc = pc; }
uint32_t cosim_last_block(void) { return g_last_leader_pc; }

uint32_t cosim_cycles_to_next_checkpoint(void) {
uint64_t now = psx_cycle_count;
if (now >= g_next_cp) return 0;
uint64_t d = g_next_cp - now;
return d > 0xFFFFFFFFULL ? 0xFFFFFFFFu : (uint32_t)d;
return 0; /* checkpoints are icount-keyed; no cycle sub-step cap needed */
}

/* Cycle-keyed checkpoint — called from psx_advance_cycles (both backends, identical
* per-instruction charges). Folds a full-state hash into the chain at each guest-cycle
* stride and parks at the coordinator's stop cycle. This is the alignment clock. */
static void cosim_record_checkpoint(uint64_t cycle, uint32_t pc) {
/* Instruction-count-keyed checkpoint — taken inside cosim_instr, which both
* backends call exactly once per retirement event (a branch and its delay slot
* retire as ONE event in both the dirty-RAM interp and the emitted code). That
* makes checkpoint k "the state after k*stride retirements" by construction, so
* the two instances always hash the SAME architectural position. The guest-cycle
* clock is part of the hash (cosim_state.c), so any cycle-accounting drift shows
* up as a chain divergence AT an exact instruction instead of silently skewing
* where the two instances park (the old cycle-keyed design parked at the first
* instruction boundary after the stride cycle, which the two backends could
* reach at different retirement positions — phantom divergences). */
static void cosim_record_checkpoint(uint64_t icount, uint32_t pc) {
uint64_t h = cosim_state_hash(NULL);
uint64_t cp = ++g_cp;
g_chain = fold(fold(g_chain, cycle), h);
g_chain = fold(fold(g_chain, icount), h);
Entry *e = &g_ring[cp & (RING_N - 1u)];
e->cp = cp; e->pc = pc; e->hash = h;
e->istat = i_stat; e->imask = i_mask; e->cycle = cycle;
e->istat = i_stat; e->imask = i_mask; e->cycle = psx_cycle_count;

/* deterministic park: consume one checkpoint of budget, else block for `step`.
* The guest ALWAYS stops here (a fixed cycle boundary), never at a wall-time point. */
* The guest ALWAYS stops here (a fixed icount boundary), never at a wall-time point. */
if (g_run_budget > 0) { g_run_budget--; return; }
g_parked = 1;
while (g_run_budget <= 0) {
Expand All @@ -107,29 +110,24 @@ static void cosim_record_checkpoint(uint64_t cycle, uint32_t pc) {
}

void cosim_tick(void) {
uint64_t now = psx_cycle_count;
if (now < g_next_cp) return;

if (g_cp == 0 && now == 0 && g_next_cp == 0) {
cosim_record_checkpoint(0, g_last_leader_pc);
g_next_cp = g_stride ? g_stride : 1;
return;
}

while (now >= g_next_cp) {
if (g_pending_count == 0) g_pending_first_cycle = g_next_cp;
g_pending_count++;
g_next_cp += g_stride ? g_stride : 1;
}
/* Retained for the psx_advance_cycles call sites; checkpointing moved to
* the icount key in cosim_instr (see cosim_record_checkpoint comment). */
}

void cosim_instr(uint32_t pc) {
g_last_leader_pc = pc;
while (g_pending_count > 0) {
uint64_t cycle = g_pending_first_cycle;
g_pending_first_cycle += g_stride ? g_stride : 1;
g_pending_count--;
cosim_record_checkpoint(cycle, pc);
g_icount++;
if (g_cp == 0 && g_next_cp == 0) {
/* Initial park at the very first retirement: the coordinator gains
* control before the guest free-runs (unless PSX_COSIM_START_CYCLE
* set a free-run target, which makes g_next_cp nonzero). */
cosim_record_checkpoint(g_icount, pc);
g_next_cp = g_icount + (g_stride ? g_stride : 1);
return;
}
if (g_icount >= g_next_cp) {
cosim_record_checkpoint(g_icount, pc);
g_next_cp = g_icount + (g_stride ? g_stride : 1);
}
}

Expand Down Expand Up @@ -200,8 +198,9 @@ static void handle_line(sock_t s, char *line) {
if (sscanf(line, "%31s", cmd) != 1) { send_line(s, "err empty\n"); return; }

if (!strcmp(cmd, "status")) {
snprintf(out, sizeof out, "cp %llu cycle %llu chain %016llx stride %llu parked %d\n",
snprintf(out, sizeof out, "cp %llu cycle %llu icnt %llu chain %016llx stride %llu parked %d\n",
(unsigned long long)g_cp, (unsigned long long)psx_cycle_count,
(unsigned long long)g_icount,
(unsigned long long)g_chain, (unsigned long long)g_stride, g_parked);
send_line(s, out); return;
}
Expand Down Expand Up @@ -229,9 +228,10 @@ static void handle_line(sock_t s, char *line) {
while (g_run_budget > 0 && spins < 1200000) { COSIM_SLEEP(1); spins++; }
/* small settle so g_parked/g_chain reflect the checkpoint just recorded */
int s2 = 0; while (!g_parked && g_run_budget <= 0 && s2 < 2000) { COSIM_SLEEP(1); s2++; }
snprintf(out, sizeof out, "%s cp %llu cycle %llu chain %016llx\n",
snprintf(out, sizeof out, "%s cp %llu cycle %llu icnt %llu chain %016llx\n",
g_parked ? "parked" : "running",
(unsigned long long)g_cp, (unsigned long long)psx_cycle_count,
(unsigned long long)g_icount,
(unsigned long long)g_chain);
send_line(s, out); return;
}
Expand Down Expand Up @@ -369,8 +369,9 @@ void cosim_init(void) {
unsigned short port = 4600;
const char *e = getenv("PSX_COSIM_PORT");
if (e && *e) port = (unsigned short)atoi(e);
/* Stride fixed at launch (env) so the checkpoint cycle boundaries are identical in
* both processes before either runs a single instruction — no set-stride race. */
/* Stride fixed at launch (env) so the checkpoint icount boundaries are identical
* in both processes before either runs a single instruction — no set-stride race.
* Both stride and start are in RETIRED INSTRUCTIONS (icount), not guest cycles. */
const char *st = getenv("PSX_COSIM_STRIDE");
if (st && *st) { unsigned long long v = strtoull(st, 0, 10); if (v) g_stride = v; }
const char *sc = getenv("PSX_COSIM_START_CYCLE");
Expand Down
Loading