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
+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()) {