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:
Matthew Stanley
2026-06-10 22:30:41 -07:00
parent 4bdb7e23e9
commit 2439d4dd44
11 changed files with 324 additions and 5 deletions
+20
View File
@@ -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
+11 -1
View File
@@ -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()) {
+79
View File
@@ -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";
+75
View File
@@ -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
+15
View File
@@ -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
+37 -3
View File
@@ -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 &&
+17
View File
@@ -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 ? "\"" : "");
+8
View File
@@ -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,
+32
View File
@@ -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; }
+7
View File
@@ -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); }
+23 -1
View File
@@ -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,