mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
dispatch call contract: continuations only run when the guest really returns
Bug D root cause (Tomba chest freeze, also QA issue #1): the game's own dispatcher func_80056E08 has a dead jumptable case (type 0x21 -> jal 0x80120B3C) whose target is mid-function in EVERY overlay variant on the disc (only the boot EXE references it; the real handler entry is 0x80120AC8). Entering mid-function skips the prologue, runs an epilogue for a frame that was never pushed, and returns through garbage. Our C model then resumed the suspended generated continuations on the moved guest stack -> double-executed tails -> infinite zombie loop (~3K wild calls/frame) -> MMIO fatal. Family root of Bugs A/C/D. The contract: a generated C continuation may only run if the guest actually arrived at it ($ra == call site return address, $sp == caller sp at the call). Violations begin a "bail" unwind: cpu->pc := the wild jr's true target, every generated frame returns immediately, and the unwind resolves at the first enclosing site whose contract matches (or flattens at the outermost dispatch loop, re-dispatching the wild target on a clean host stack -- matching real-HW one-stack semantics). - cpu_state.h: bail state + psx_call_contract() helper (+ overlay-DLL pointer indirection under PSX_OVERLAY_DLL_BUILD) - emitters: (ra, sp) checks at every BIOS and game jal/jalr site; psx_dispatch_impl validates pc==0 and stop-hits against (stop, sp), handles bail resolve/propagate/flatten - dirty_ram_interp: surfaced calls validate the contract on return; bail aborts the interp run; tail transfers into compiled code now run with pc cleared (closes a latent double-run on plain C returns) - overlay cache ABI v2: DLLs export overlay_abi(); loader rejects and deletes stale DLLs (autocompile regenerates with contract checks) - telemetry: bail_first/resolved/flattened/anomaly in heartbeat + dump headers; anomaly guard at exception entry; TCP_COMMANDS.md docs Verified: BIOS+Tomba regen, boot to intro at locked 60.0 fps, all bail counters 0 through boot/title/attract (no false positives). Chest-area gameplay verification pending (requires user play). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,26 @@ samples inside `WS2_32!send`). A slow-frames wedge with a large
|
||||
`tcp_send_stall_ms` delta over the same window is observer interference,
|
||||
not a guest bug.
|
||||
|
||||
## Call-contract (bail) telemetry
|
||||
|
||||
The dispatch call contract (Bug D family fix, 2026-06-10) guards every
|
||||
generated continuation: it may only run if the guest actually returned to
|
||||
the call site ($ra == site return address, $sp == caller's sp at the
|
||||
call). Violations begin a "bail" unwind that abandons stale C frames and
|
||||
re-dispatches the guest's true target. Counters in
|
||||
`psx_freeze_heartbeat.json` and dump headers:
|
||||
|
||||
- `bail_first` — contract violations detected (wild returns). Nonzero
|
||||
during gameplay means the game executed a wild control transfer (e.g.
|
||||
Tomba's dead jumptable case `jal 0x80120B3C`, the chest freeze). A
|
||||
small count with the game continuing normally is the fix working.
|
||||
- `bail_resolved` — unwinds that resolved at an enclosing call site whose
|
||||
contract matched (multi-level return).
|
||||
- `bail_flattened` — unwinds that reached the outermost dispatch loop and
|
||||
re-dispatched the wild target on a clean host stack.
|
||||
- `bail_anomaly` — bail flag observed at exception entry (must stay 0;
|
||||
anything else is a runtime bug).
|
||||
|
||||
---
|
||||
|
||||
## Rule when the server can't answer your question
|
||||
|
||||
@@ -1199,13 +1199,20 @@ std::string CodeGenerator::translate_basic_block(
|
||||
ss << config_.indent << "return;\n";
|
||||
}
|
||||
} else if (block.exit_instr.type == ControlFlowType::JumpLink) {
|
||||
// Function call (jal)
|
||||
// Function call (jal). The call contract (Bug D family)
|
||||
// guards the continuation: it may only run if the guest
|
||||
// actually returned here with the caller's $sp.
|
||||
ss << config_.indent << "{ uint32_t _csp = cpu->gpr[29];\n";
|
||||
ss << config_.indent << "cpu->gpr[31] = " << fmt::format("0x{:08X}; /* return address */\n", addr + 8);
|
||||
uint32_t target = block.exit_instr.target;
|
||||
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);
|
||||
} 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;
|
||||
* only propagate an active bail unwind here. */
|
||||
ss << config_.indent << "if (g_psx_call_bail) return; (void)_csp; }\n";
|
||||
}
|
||||
if (!block.successors.empty()) {
|
||||
ss << config_.indent
|
||||
@@ -1234,6 +1241,9 @@ std::string CodeGenerator::translate_basic_block(
|
||||
// Dispatch indirect call — target is a runtime register value
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jalr {} */\n",
|
||||
reg_name(rs), reg_name(rs));
|
||||
/* psx_dispatch_call validated the (ra, sp) contract;
|
||||
* only propagate an active bail unwind here. */
|
||||
ss << config_.indent << "if (g_psx_call_bail) return;\n";
|
||||
|
||||
// Continue to successor block (instruction at return address)
|
||||
if (!block.successors.empty()) {
|
||||
|
||||
@@ -521,8 +521,11 @@ bool FullFunctionEmitter::emit_function(
|
||||
}
|
||||
uint32_t target = relocate_j_target(addr, tr.terminator_target);
|
||||
uint32_t return_addr = relocate_ra(addr + 8);
|
||||
out += " { uint32_t _csp = cpu->gpr[29];\n";
|
||||
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
|
||||
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u);\n", target);
|
||||
out += fmt::format(" if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n",
|
||||
return_addr);
|
||||
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", return_addr);
|
||||
} else if (kind == "jalr") {
|
||||
uint32_t ds_addr = addr + 4;
|
||||
@@ -542,12 +545,20 @@ bool FullFunctionEmitter::emit_function(
|
||||
uint8_t rs = (raw >> 21) & 0x1F;
|
||||
uint8_t rd = (raw >> 11) & 0x1F;
|
||||
uint32_t return_addr = relocate_ra(addr + 8);
|
||||
out += " { uint32_t _csp = cpu->gpr[29];\n";
|
||||
if (rd != 0) {
|
||||
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u;\n",
|
||||
static_cast<int>(rd), return_addr);
|
||||
}
|
||||
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
|
||||
static_cast<int>(rs));
|
||||
if (rd == 31) {
|
||||
out += fmt::format(" if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n",
|
||||
return_addr);
|
||||
} else {
|
||||
/* Return register isn't $ra: only propagate an active bail. */
|
||||
out += " if (g_psx_call_bail) return; (void)_csp; }\n";
|
||||
}
|
||||
out += fmt::format(" cpu->pc = 0x{:08X}u; return;\n", return_addr);
|
||||
}
|
||||
}
|
||||
@@ -652,6 +663,7 @@ bool FullFunctionEmitter::emit_function(
|
||||
} else if (kind == "jal") {
|
||||
uint32_t target = pb.target;
|
||||
uint32_t return_addr = relocate_ra(pb.terminator_addr + 8);
|
||||
out += " { uint32_t _csp = cpu->gpr[29];\n";
|
||||
out += fmt::format(" cpu->gpr[31] = 0x{:08X}u;\n", return_addr);
|
||||
// Regular call: always go through psx_dispatch (handles tail-call loop).
|
||||
if (target == 0x00006380u) {
|
||||
@@ -663,6 +675,8 @@ bool FullFunctionEmitter::emit_function(
|
||||
out += fmt::format(" debug_server_log_probe(0x{:08X}u, cpu);\n",
|
||||
pb.terminator_addr + 1);
|
||||
}
|
||||
out += fmt::format(" if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n",
|
||||
return_addr);
|
||||
// Safety net: if continuation falls outside this function, tail-call to it.
|
||||
if (!addr_to_raw.count(pb.terminator_addr + 8)) {
|
||||
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u); /* jal cont: outside func */\n", return_addr);
|
||||
@@ -683,12 +697,20 @@ bool FullFunctionEmitter::emit_function(
|
||||
uint8_t rs = (pb.raw >> 21) & 0x1F;
|
||||
uint8_t rd = (pb.raw >> 11) & 0x1F;
|
||||
uint32_t return_addr = relocate_ra(pb.terminator_addr + 8);
|
||||
out += " { uint32_t _csp = cpu->gpr[29];\n";
|
||||
if (rd != 0) {
|
||||
out += fmt::format(" cpu->gpr[{}] = 0x{:08X}u;\n",
|
||||
static_cast<int>(rd), return_addr);
|
||||
}
|
||||
out += fmt::format(" psx_dispatch(cpu, cpu->gpr[{}]);\n",
|
||||
static_cast<int>(rs));
|
||||
if (rd == 31) {
|
||||
out += fmt::format(" if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n",
|
||||
return_addr);
|
||||
} else {
|
||||
/* Return register isn't $ra: only propagate an active bail. */
|
||||
out += " if (g_psx_call_bail) return; (void)_csp; }\n";
|
||||
}
|
||||
// Safety net: if continuation falls outside this function, tail-call to it.
|
||||
if (!addr_to_raw.count(pb.terminator_addr + 8)) {
|
||||
out += fmt::format(" psx_dispatch(cpu, 0x{:08X}u); /* jalr cont: outside func */\n", return_addr);
|
||||
@@ -1085,6 +1107,10 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " * the outermost dispatch returns, so a nested callee cannot interrupt\n";
|
||||
out += " * before its generated caller runs the post-call continuation. */\n";
|
||||
out += " int outermost = (g_psx_dispatch_depth++ == 0);\n";
|
||||
out += " /* Call contract (Bug D family): the guest $sp at the call. A C\n";
|
||||
out += " * continuation behind this dispatch may only run if the guest\n";
|
||||
out += " * actually returns here ($ra == stop_addr) with this $sp. */\n";
|
||||
out += " uint32_t sp_at_call = cpu->gpr[29];\n";
|
||||
out += " for (;;) {\n";
|
||||
out += " /* Always-on call ring: every iteration counts as a separate\n";
|
||||
out += " * call (initial entry + each tail-call re-dispatch). a0..a3\n";
|
||||
@@ -1133,7 +1159,53 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " psx_unknown_dispatch(cpu, addr, phys);\n";
|
||||
out += " }\n";
|
||||
out += " }\n";
|
||||
out += " if (g_psx_call_bail) {\n";
|
||||
out += " /* A nested generated frame began a bail unwind; cpu->pc\n";
|
||||
out += " * holds the guest's true target. Resolve here iff the\n";
|
||||
out += " * wild flow arrived exactly at this call's contract. */\n";
|
||||
out += " if (stop_addr != 0 &&\n";
|
||||
out += " ((cpu->pc ^ stop_addr) & 0x1FFFFFFFu) == 0 &&\n";
|
||||
out += " cpu->gpr[29] == sp_at_call) {\n";
|
||||
out += " g_psx_call_bail = 0;\n";
|
||||
out += " g_psx_bail_resolved++;\n";
|
||||
out += " cpu->pc = 0;\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
out += " }\n";
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
out += " if (!outermost) {\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " return; /* propagate to the enclosing call site */\n";
|
||||
out += " }\n";
|
||||
out += " /* Outermost: flatten — host stack above is clean, keep\n";
|
||||
out += " * executing the wild flow as a tail dispatch. */\n";
|
||||
out += " g_psx_call_bail = 0;\n";
|
||||
out += " g_psx_bail_flattened++;\n";
|
||||
out += " addr = cpu->pc;\n";
|
||||
out += " continue;\n";
|
||||
out += " }\n";
|
||||
out += " if (cpu->pc == 0) {\n";
|
||||
out += " if (stop_addr != 0 &&\n";
|
||||
out += " (cpu->gpr[29] != sp_at_call ||\n";
|
||||
out += " ((cpu->gpr[31] ^ stop_addr) & 0x1FFFFFFFu) != 0)) {\n";
|
||||
out += " /* Callee C-returned but the guest did not return to\n";
|
||||
out += " * this call site ($ra holds the wild jr's target):\n";
|
||||
out += " * begin the bail unwind instead of resuming the\n";
|
||||
out += " * suspended C continuation. */\n";
|
||||
out += " g_psx_call_bail = 1;\n";
|
||||
out += " g_psx_bail_first++;\n";
|
||||
out += " cpu->pc = cpu->gpr[31];\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " g_psx_call_bail = 0;\n";
|
||||
out += " g_psx_bail_flattened++;\n";
|
||||
out += " addr = cpu->pc;\n";
|
||||
out += " continue;\n";
|
||||
out += " }\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
out += " psx_check_interrupts(cpu);\n";
|
||||
@@ -1141,6 +1213,13 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " return;\n";
|
||||
out += " }\n";
|
||||
out += " if (stop_addr != 0 && cpu->pc == stop_addr) {\n";
|
||||
out += " if (cpu->gpr[29] != sp_at_call) {\n";
|
||||
out += " /* Same address, different frame (recursion or a wild\n";
|
||||
out += " * arrival): not this call's return — keep executing\n";
|
||||
out += " * via tail dispatch (interior alias route). */\n";
|
||||
out += " addr = cpu->pc;\n";
|
||||
out += " continue;\n";
|
||||
out += " }\n";
|
||||
out += " cpu->pc = 0;\n";
|
||||
out += " --g_psx_dispatch_depth;\n";
|
||||
out += " if (outermost) {\n";
|
||||
|
||||
@@ -55,6 +55,81 @@ extern uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg);
|
||||
extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);
|
||||
extern void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val);
|
||||
|
||||
/* ============================================================================
|
||||
* Dispatch call contract (Bug D / wild-return family fix)
|
||||
* ----------------------------------------------------------------------------
|
||||
* Generated C continuations mirror the guest call graph: after `jal F`, the C
|
||||
* code calls F's C function and then falls into the continuation block. Real
|
||||
* hardware has only one stack; if F's flow goes wild (returns to an address
|
||||
* other than the call site's $ra, or with a shifted $sp — e.g. a mid-function
|
||||
* entry running an epilogue for a frame it never pushed), the hardware simply
|
||||
* keeps executing wherever the guest jumped, and the interrupted caller's
|
||||
* code never resumes. Our C model, by contrast, resumes the suspended C
|
||||
* continuation whenever the C call returns — re-executing tails on a moved
|
||||
* guest stack (the Bug A/C/D zombie family).
|
||||
*
|
||||
* The contract: a C continuation may run ONLY if the guest actually arrived
|
||||
* at it — i.e. the callee came back with $ra == the call site's return
|
||||
* address and $sp == the caller's stack pointer at the call. When the
|
||||
* contract is violated, we begin a "bail" unwind: cpu->pc is set to the
|
||||
* guest's true target (the wild jr's destination), g_psx_call_bail is set,
|
||||
* and every generated frame returns immediately without running its
|
||||
* continuation. The unwind resolves at the first enclosing call site (or
|
||||
* dispatch loop) whose (return address, sp) contract matches the guest's
|
||||
* arrival state; if none matches, the outermost dispatch loop clears the
|
||||
* flag and tail-dispatches the wild target with a clean host stack.
|
||||
*
|
||||
* g_psx_call_bail is defined in runtime/src/traps.c. Overlay DLLs share
|
||||
* the runtime's state through pointers wired by overlay_init (the
|
||||
* PSX_OVERLAY_DLL_BUILD branch; see overlay_api.h / compile_overlays.py).
|
||||
* ========================================================================== */
|
||||
#ifdef PSX_OVERLAY_DLL_BUILD
|
||||
extern int *g_psx_call_bail_p;
|
||||
extern uint64_t *g_psx_bail_first_p;
|
||||
extern uint64_t *g_psx_bail_resolved_p;
|
||||
#define g_psx_call_bail (*g_psx_call_bail_p)
|
||||
#define g_psx_bail_first (*g_psx_bail_first_p)
|
||||
#define g_psx_bail_resolved (*g_psx_bail_resolved_p)
|
||||
#else
|
||||
extern int g_psx_call_bail;
|
||||
extern uint64_t g_psx_bail_first; /* contract violations detected */
|
||||
extern uint64_t g_psx_bail_resolved; /* unwinds resolved at a call site */
|
||||
extern uint64_t g_psx_bail_flattened; /* unwinds flattened at outermost */
|
||||
extern uint64_t g_psx_bail_anomaly; /* bail flag seen where impossible */
|
||||
#endif
|
||||
|
||||
/* Validate a direct call site after the callee's C return.
|
||||
* Returns 1 if the caller must `return;` immediately (bail in progress),
|
||||
* 0 if the continuation is valid. site_ra = the call's return address,
|
||||
* site_sp = guest $sp recorded immediately before the call. */
|
||||
static inline int psx_call_contract(CPUState* cpu, uint32_t site_ra,
|
||||
uint32_t site_sp) {
|
||||
if (g_psx_call_bail) {
|
||||
/* An inner frame began a bail unwind. Resolve here iff the guest's
|
||||
* arrival state matches this site's contract. */
|
||||
if (((cpu->pc ^ site_ra) & 0x1FFFFFFFu) == 0 &&
|
||||
cpu->gpr[29] == site_sp) {
|
||||
g_psx_call_bail = 0;
|
||||
g_psx_bail_resolved++;
|
||||
cpu->pc = 0;
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (cpu->gpr[29] != site_sp ||
|
||||
((cpu->gpr[31] ^ site_ra) & 0x1FFFFFFFu) != 0) {
|
||||
/* First detection: the callee C-returned but the guest did not
|
||||
* return here. $ra holds the wild jr's true destination (the
|
||||
* longjmp-return emission sets cpu->pc = $ra before returning,
|
||||
* which is the same value). */
|
||||
g_psx_call_bail = 1;
|
||||
g_psx_bail_first++;
|
||||
cpu->pc = cpu->gpr[31];
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
#include "cpu_state.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* ABI version exported by every overlay DLL as `overlay_abi()`. The loader
|
||||
* rejects (and deletes, so autocompile regenerates) any DLL whose version
|
||||
* doesn't match — including all pre-versioning DLLs, which lack the export.
|
||||
*
|
||||
* v2: dispatch call-contract (Bug D family) — DLL call sites carry (ra, sp)
|
||||
* contract checks and share the runtime's bail state via the appended
|
||||
* callback pointers below. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 2
|
||||
|
||||
typedef struct {
|
||||
/* Core dispatch: routes call_by_address() and out-of-overlay jal */
|
||||
void (*dispatch_call)(CPUState *cpu, uint32_t addr, uint32_t ra);
|
||||
@@ -29,6 +38,12 @@ typedef struct {
|
||||
* overlay_init copies the struct by value, so older DLLs built against
|
||||
* the shorter struct simply never read this member. */
|
||||
void (*psx_restore_state_escape)(void);
|
||||
/* Call-contract state shared with the runtime (ABI v2; see the contract
|
||||
* model in cpu_state.h). DLL code reads the bail flag and bumps the
|
||||
* counters through these pointers. */
|
||||
int *call_bail_flag;
|
||||
uint64_t *bail_first;
|
||||
uint64_t *bail_resolved;
|
||||
} OverlayCallbacks;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -350,6 +350,9 @@ static int dispatch_nonlocal_call(CPUState *cpu, uint32_t target,
|
||||
uint32_t *next_pc_out) {
|
||||
cpu->pc = 0;
|
||||
psx_dispatch_call(cpu, target, return_pc);
|
||||
/* psx_dispatch_call validated the (return_pc, sp) contract; a bail
|
||||
* unwind in progress surfaces with cpu->pc = the guest's true target. */
|
||||
if (g_psx_call_bail) return 1;
|
||||
if (cpu->pc != 0) return 1;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
@@ -445,10 +448,15 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
cpu->gpr[rd ? rd : 31] = return_pc;
|
||||
cpu->gpr[0] = 0;
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
cpu->pc = 0;
|
||||
if (psx_dispatch_game_compiled(cpu, target)) {
|
||||
if (g_psx_call_bail) return 1; /* wild unwind: cpu->pc = true target */
|
||||
if (cpu->pc != 0) return 1;
|
||||
if (rd == 0 || rd == 31) {
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
}
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
}
|
||||
@@ -462,7 +470,11 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
extern int overlay_loader_call_native(CPUState *cpu, uint32_t addr);
|
||||
cpu->pc = 0;
|
||||
if (overlay_loader_call_native(cpu, target)) {
|
||||
if (g_psx_call_bail) return 1;
|
||||
if (cpu->pc != 0) return 1;
|
||||
if (rd == 0 || rd == 31) {
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
}
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
}
|
||||
@@ -582,10 +594,13 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
uint32_t return_pc = pc + 8;
|
||||
cpu->gpr[31] = return_pc;
|
||||
exec_delay_slot(cpu, pc + 4);
|
||||
uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
cpu->pc = 0;
|
||||
if (psx_dispatch_game_compiled(cpu, target)) {
|
||||
if (g_psx_call_bail) return 1; /* wild unwind: cpu->pc = true target */
|
||||
if (cpu->pc != 0) return 1;
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
}
|
||||
@@ -600,7 +615,9 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
|
||||
extern int overlay_loader_call_native(CPUState *cpu, uint32_t addr);
|
||||
cpu->pc = 0;
|
||||
if (overlay_loader_call_native(cpu, target)) {
|
||||
if (g_psx_call_bail) return 1;
|
||||
if (cpu->pc != 0) return 1;
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) return 1;
|
||||
*next_pc_out = return_pc;
|
||||
return 0;
|
||||
}
|
||||
@@ -989,13 +1006,30 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
g_dirty_ram_insns_run++;
|
||||
insns_executed++;
|
||||
if (transferred) {
|
||||
uint32_t target = cpu->pc;
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
if (target != 0 && psx_dispatch_game_compiled(cpu, target)) {
|
||||
if (g_psx_call_bail) {
|
||||
/* A bail unwind began inside a surfaced call: stop the interp
|
||||
* run and hand the wild target (cpu->pc) up to the dispatch
|
||||
* loop's bail handling. */
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
uint32_t target = cpu->pc;
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
if (target != 0) {
|
||||
/* Tail transfer into compiled code: run with pc cleared so a
|
||||
* plain C return surfaces as a genuine return (pc==0) to the
|
||||
* dispatch loop's contract checks, instead of leaving the
|
||||
* stale target in pc (which would re-dispatch — and re-run —
|
||||
* the same function). */
|
||||
cpu->pc = 0;
|
||||
if (psx_dispatch_game_compiled(cpu, target)) {
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
cpu->pc = target; /* not compiled: restore surfaced target */
|
||||
}
|
||||
#endif
|
||||
uint32_t target_phys = target & 0x1FFFFFFFu;
|
||||
if (allow_local_dirty_flow && target != 0 &&
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "freeze_heartbeat.h"
|
||||
#include "debug_server.h"
|
||||
#include "crash_trace.h" /* g_psx_fatal_reason */
|
||||
#include "cpu_state.h" /* g_psx_bail_* call-contract counters */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -329,6 +330,10 @@ static void freeze_dump_write(long long wall, uint64_t frame, uint64_t cyc,
|
||||
" \"dirty_ram_insns\":%llu,\n"
|
||||
" \"tcp_send_stall_ms\":%llu,\n"
|
||||
" \"tcp_clients_dropped\":%u,\n"
|
||||
" \"bail_first\":%llu,\n"
|
||||
" \"bail_resolved\":%llu,\n"
|
||||
" \"bail_flattened\":%llu,\n"
|
||||
" \"bail_anomaly\":%llu,\n"
|
||||
" \"present_slow_count\":%u,\n"
|
||||
" \"present_vsync_disabled\":%d,\n"
|
||||
" \"wedge_kind\":%u,\n"
|
||||
@@ -351,6 +356,10 @@ static void freeze_dump_write(long long wall, uint64_t frame, uint64_t cyc,
|
||||
(unsigned long long)g_dirty_ram_insns_run,
|
||||
(unsigned long long)debug_server_get_tcp_stall_ms(),
|
||||
debug_server_get_tcp_drops(),
|
||||
(unsigned long long)g_psx_bail_first,
|
||||
(unsigned long long)g_psx_bail_resolved,
|
||||
(unsigned long long)g_psx_bail_flattened,
|
||||
(unsigned long long)g_psx_bail_anomaly,
|
||||
g_present_slow_count,
|
||||
g_present_vsync_disabled,
|
||||
wedge_kind,
|
||||
@@ -620,6 +629,10 @@ static void heartbeat_write(void) {
|
||||
" \"dirty_ram_insns\":%llu,\n"
|
||||
" \"tcp_send_stall_ms\":%llu,\n"
|
||||
" \"tcp_clients_dropped\":%u,\n"
|
||||
" \"bail_first\":%llu,\n"
|
||||
" \"bail_resolved\":%llu,\n"
|
||||
" \"bail_flattened\":%llu,\n"
|
||||
" \"bail_anomaly\":%llu,\n"
|
||||
" \"fatal\":%s%s%s\n"
|
||||
"}\n",
|
||||
s_backend,
|
||||
@@ -646,6 +659,10 @@ static void heartbeat_write(void) {
|
||||
(unsigned long long)g_dirty_ram_insns_run,
|
||||
(unsigned long long)debug_server_get_tcp_stall_ms(),
|
||||
debug_server_get_tcp_drops(),
|
||||
(unsigned long long)g_psx_bail_first,
|
||||
(unsigned long long)g_psx_bail_resolved,
|
||||
(unsigned long long)g_psx_bail_flattened,
|
||||
(unsigned long long)g_psx_bail_anomaly,
|
||||
g_psx_fatal_reason ? "\"" : "",
|
||||
g_psx_fatal_reason ? g_psx_fatal_reason : "null",
|
||||
g_psx_fatal_reason ? "\"" : "");
|
||||
|
||||
@@ -399,6 +399,14 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
int prev_pending = g_pending_exception_longjmp;
|
||||
g_exception_owner_fiber = psx_fiber_current();
|
||||
g_pending_exception_longjmp = 0;
|
||||
/* A bail unwind can never be in flight at exception entry: bail-mode
|
||||
* returns skip every block leader, so psx_check_interrupts is never
|
||||
* reached while g_psx_call_bail is set. If it ever is, count the
|
||||
* anomaly and clear so the handler dispatch isn't poisoned. */
|
||||
if (g_psx_call_bail) {
|
||||
g_psx_bail_anomaly++;
|
||||
g_psx_call_bail = 0;
|
||||
}
|
||||
/* The static BIOS exception handler is not dirty-RAM-interp code. Clear the
|
||||
* interp mode flag across the handler dispatch so events recorded inside it
|
||||
* are tagged STATIC. The restore sits after the loop the longjmp lands in,
|
||||
|
||||
@@ -408,6 +408,11 @@ static void init_callbacks(void) {
|
||||
s_callbacks.psx_unknown_dispatch = psx_unknown_dispatch;
|
||||
s_callbacks.log_call_entry = debug_server_log_call_entry;
|
||||
s_callbacks.psx_restore_state_escape = psx_restore_state_escape;
|
||||
/* Call-contract state (ABI v2): DLL code shares the runtime's bail
|
||||
* flag and counters through these pointers. */
|
||||
s_callbacks.call_bail_flag = &g_psx_call_bail;
|
||||
s_callbacks.bail_first = &g_psx_bail_first;
|
||||
s_callbacks.bail_resolved = &g_psx_bail_resolved;
|
||||
}
|
||||
|
||||
/* ---- DLL loading and export enumeration -------------------------------- */
|
||||
@@ -419,6 +424,21 @@ static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll
|
||||
loader_log("LoadLibrary(%s) failed: %lu", dll_path, GetLastError());
|
||||
return 0;
|
||||
}
|
||||
/* ABI gate: reject any DLL whose contract ABI doesn't match this
|
||||
* runtime (pre-versioning DLLs lack the export entirely). Delete the
|
||||
* stale file so the autocompile path regenerates it with the current
|
||||
* emitter. */
|
||||
typedef int (*AbiFn)(void);
|
||||
AbiFn abi_fn = (AbiFn)GetProcAddress(h, "overlay_abi");
|
||||
int abi = abi_fn ? abi_fn() : 0;
|
||||
if (abi != PSX_OVERLAY_ABI_VERSION) {
|
||||
loader_log("ABI mismatch in %s: dll=%d runtime=%d — rejecting and "
|
||||
"deleting stale cache entry", dll_path, abi,
|
||||
PSX_OVERLAY_ABI_VERSION);
|
||||
FreeLibrary(h);
|
||||
DeleteFileA(dll_path);
|
||||
return 0;
|
||||
}
|
||||
typedef void (*InitFn)(const OverlayCallbacks *);
|
||||
InitFn init_fn = (InitFn)GetProcAddress(h, "overlay_init");
|
||||
if (!init_fn) {
|
||||
@@ -472,6 +492,18 @@ static int load_overlay_dll(const char *dll_path, ManFn *man, int man_n, int dll
|
||||
(void)man; (void)man_n; (void)dll;
|
||||
void *h = dlopen(dll_path, RTLD_NOW | RTLD_LOCAL);
|
||||
if (!h) { loader_log("dlopen(%s) failed: %s", dll_path, dlerror()); return 0; }
|
||||
/* ABI gate (see the _WIN32 branch). */
|
||||
typedef int (*AbiFn)(void);
|
||||
AbiFn abi_fn = (AbiFn)dlsym(h, "overlay_abi");
|
||||
int abi = abi_fn ? abi_fn() : 0;
|
||||
if (abi != PSX_OVERLAY_ABI_VERSION) {
|
||||
loader_log("ABI mismatch in %s: dll=%d runtime=%d — rejecting and "
|
||||
"deleting stale cache entry", dll_path, abi,
|
||||
PSX_OVERLAY_ABI_VERSION);
|
||||
dlclose(h);
|
||||
remove(dll_path);
|
||||
return 0;
|
||||
}
|
||||
typedef void (*InitFn)(const OverlayCallbacks *);
|
||||
InitFn init_fn = (InitFn)dlsym(h, "overlay_init");
|
||||
if (!init_fn) { loader_log("no overlay_init in %s", dll_path); dlclose(h); return 0; }
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
int psx_get_in_exception(void);
|
||||
void psx_exception_longjmp(void);
|
||||
|
||||
/* Dispatch call contract state (see cpu_state.h for the model). */
|
||||
int g_psx_call_bail = 0;
|
||||
uint64_t g_psx_bail_first = 0;
|
||||
uint64_t g_psx_bail_resolved = 0;
|
||||
uint64_t g_psx_bail_flattened = 0;
|
||||
uint64_t g_psx_bail_anomaly = 0;
|
||||
|
||||
static void trap_crash(const char* msg) {
|
||||
FILE* cf = fopen("psx_crash.txt", "w");
|
||||
if (cf) { fprintf(cf, "%s\n", msg); fclose(cf); }
|
||||
|
||||
@@ -742,12 +742,33 @@ DISPATCH_PREAMBLE = """\
|
||||
/* ---- Overlay dispatch shim (inserted by compile_overlays.py) ----------- */
|
||||
static OverlayCallbacks g_cbs;
|
||||
|
||||
/* Call-contract state (ABI v2): generated code reads/bumps the runtime's
|
||||
* bail state through these pointers (cpu_state.h PSX_OVERLAY_DLL_BUILD
|
||||
* branch). They start at local dummies so a pre-init call can't fault. */
|
||||
static int s_bail_dummy;
|
||||
static uint64_t s_ctr_dummy0, s_ctr_dummy1;
|
||||
int *g_psx_call_bail_p = &s_bail_dummy;
|
||||
uint64_t *g_psx_bail_first_p = &s_ctr_dummy0;
|
||||
uint64_t *g_psx_bail_resolved_p = &s_ctr_dummy1;
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#else
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
void overlay_init(const OverlayCallbacks *cbs) { g_cbs = *cbs; }
|
||||
int overlay_abi(void) { return PSX_OVERLAY_ABI_VERSION; }
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#else
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
void overlay_init(const OverlayCallbacks *cbs) {
|
||||
g_cbs = *cbs;
|
||||
if (g_cbs.call_bail_flag) g_psx_call_bail_p = g_cbs.call_bail_flag;
|
||||
if (g_cbs.bail_first) g_psx_bail_first_p = g_cbs.bail_first;
|
||||
if (g_cbs.bail_resolved) g_psx_bail_resolved_p = g_cbs.bail_resolved;
|
||||
}
|
||||
|
||||
void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra) {
|
||||
(void)ra;
|
||||
@@ -1010,6 +1031,7 @@ def compile_dll(c_path: str, out_dll: str, include_dirs: list[str],
|
||||
pic_flag = [] if platform.system() == 'Windows' else ['-fPIC']
|
||||
cmd = [
|
||||
gcc, '-shared', *pic_flag, '-O2',
|
||||
'-DPSX_OVERLAY_DLL_BUILD',
|
||||
c_path,
|
||||
'-o', out_dll,
|
||||
*includes,
|
||||
|
||||
Reference in New Issue
Block a user