recompiler: image-edge fallthrough must tail-transfer — the WipEout 3 static-bake boot exit root cause

A region-truncated final block that runs off the end of its function fell
off the C function body when no in-image fallthrough function existed:
the entry-switch prologue had consumed cpu->pc to 0, so the dispatcher
returned "handled" with pc==0 and the top-level trampoline read it as
"program ended". Witnessed live in the WipEout 3 ntsc120full8 static
bake: the 0xD5000 region variant's block_800D7FC0 ends at the region
edge (continuation 0x800D8004 lies outside the image); the moment CD
streaming loaded content matching that variant's identity ranges the
dispatcher routed 0x800D7FC0 into the truncated block and the run died —
the deterministic "execution completed, PC=0" at ~10 s into boot, ~frame
1221, byte-for-byte reproducible.

Both code_generator tail sites (plain functions and alias hosts) now
emit `cpu->pc = <end+4>; return;` when the last live block runs off the
image edge — the CPS tail-transfer the full_function_emitter already
emits for in-image fallthroughs. The dispatcher routes the published
continuation to whoever owns it (a neighboring region variant, AOT
text, or the interpreter). Faithful and class-level: no per-game
anything.

Diagnosis chain that cornered it (all landed as permanent tooling):
- pc0 escape journal site 14 (trampoline top-level exit) named the last
  dispatched target: 0x800D7FC0.
- site 15 (dirty-dispatch handled-with-pc0 tripwire, now journaled with
  a route tag) proved the producer was route 3 = the static overlay
  dispatcher, first occurrence, not in exception context.
- Return-shape census of every candidate variant body showed no
  return-with-pc==0 path — forcing the discovery that the dispatch case
  for 0x800D7FC0 routes FIRST to the 0xD5000 variant, whose truncated
  tail block falls off the end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tetrisgm
2026-08-19 13:55:50 -07:00
parent 21a8457ed8
commit 3b6986827e
6 changed files with 80 additions and 1 deletions
+43
View File
@@ -2808,6 +2808,30 @@ GeneratedFunction CodeGenerator::generate_function(
fallthrough_name);
}
}
} else if (!cfg.block_order.empty()) {
// No in-image fallthrough function exists (region/image boundary):
// a reachable last block that simply runs off the end must TAIL-
// TRANSFER to its continuation PC, never fall off the C function —
// the entry-switch prologue consumed cpu->pc to 0, so falling off
// returns pc==0 to the dispatcher and the top-level trampoline reads
// it as "program ended" (WipEout 3 static bake: region-truncated
// block_800D7FC0 in the 0xD5000 variant fell off at 0x800D8004 →
// deterministic "execution completed, PC=0" ~10 s into boot). The
// published continuation is routed by the dispatcher to whoever owns
// it (a neighboring region variant, AOT text, or the interpreter).
const BasicBlock& last_block = cfg.blocks.at(cfg.block_order.back());
bool runs_off_end =
last_block.exit_instr.type == ControlFlowType::None ||
((last_block.exit_instr.type == ControlFlowType::Branch ||
last_block.exit_instr.type == ControlFlowType::Jump) &&
last_block.successors.empty());
bool is_reachable = last_block.is_entry || !last_block.predecessors.empty();
if (runs_off_end && is_reachable) {
uint32_t cont = last_block.end_addr + 4u;
body_ss << fmt::format(
" cpu->pc = 0x{:08X}u; return; /* image-edge fallthrough: tail-transfer */\n",
cont);
}
}
body_ss << " ; /* label compatibility: C requires a statement after the last label */\n";
body_ss << "}\n";
@@ -3037,6 +3061,25 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
body << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
fallthrough_name);
}
} else if (!cfg.block_order.empty() &&
live_blocks.count(cfg.block_order.back())) {
// Image-edge fallthrough (mirrors generate_function): no in-image next
// function exists, so a live final block that runs off the end must
// tail-transfer to its continuation PC — falling off the C function
// returns the entry-switch's consumed pc==0 and the top-level
// trampoline reads "program ended" (the WipEout 3 static-bake boot
// exit: truncated block_800D7FC0 in the 0xD5000 region variant).
const BasicBlock& last_block = cfg.blocks.at(cfg.block_order.back());
bool runs_off_end =
(last_block.exit_instr.type == ControlFlowType::None) ||
((last_block.exit_instr.type == ControlFlowType::Branch ||
last_block.exit_instr.type == ControlFlowType::Jump) &&
last_block.successors.empty());
if (runs_off_end) {
body << fmt::format(
" cpu->pc = 0x{:08X}u; return; /* image-edge fallthrough: tail-transfer */\n",
last_block.end_addr + 4u);
}
}
body << " ; /* label compatibility */\n";
body << "}\n";
+6
View File
@@ -2162,6 +2162,12 @@ void FullFunctionEmitter::emit_dispatch(
out += " if (outermost) {\n";
out += " psx_dispatch_check_return_boundary(cpu, stop_addr);\n";
out += " }\n";
out += " /* Top-level pc==0 exit is the abnormal-run terminator: name the\n";
out += " * last dispatched target in the pc0 journal before surfacing. */\n";
out += " if (stop_addr == 0u) {\n";
out += " extern void psx_pc0_trampoline_exit(CPUState* cpu, uint32_t last_target);\n";
out += " psx_pc0_trampoline_exit(cpu, addr);\n";
out += " }\n";
out += " return;\n";
out += " }\n";
out += " if (stop_addr != 0 && cpu->pc == stop_addr) {\n";
+2
View File
@@ -247,6 +247,8 @@ enum {
PSX_PC0J_IRQ_RESCUE = 11, /* a: published resume; d: reason */
PSX_PC0J_DIRTY_SENTINEL = 12, /* d: bit0 in_exception, bit1 async-armed */
PSX_PC0J_DIRTY_UNSUPPORTED = 13, /* a: unsupported pc; d: reason */
PSX_PC0J_TOP_EXIT = 14, /* trampoline top-level pc==0 exit;
* a: last dispatched target; d: $sp */
};
typedef struct Pc0JournalEntry {
uint64_t seq;
+14 -1
View File
@@ -200,6 +200,10 @@ uint32_t g_pczero_in_exc = 0;
uint32_t g_pczero_async_rfe = 0;
uint32_t g_pczero_dirty_safe = 0;
uint64_t g_pczero_count = 0;
/* Which dirty_ram_dispatch_inner sub-path produced the last handled dispatch:
* 1=sentinel 2=AOT-compiled 3=static-overlay 4=DLL-shard 5=interp. Read by the
* pc0 tripwire so a null-PC return names its producer. */
uint32_t g_dd_last_route = 0;
/* Mid-block unsupported-opcode counters. Bumped instead of fprintf-spamming
* stderr (CLAUDE.md §3). Read via dirty_ram_get_unsupported(). The "last_*"
@@ -2355,6 +2359,11 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
if (r == 1 && cpu->pc == 0u) {
extern uint32_t g_async_rfe_resume_pc, g_dirty_safe_resume_pc;
g_pczero_count++;
/* Journal every occurrence (site 15): the dirty/overlay dispatch chain
* reported handled yet published a null PC — at top level this IS the
* abnormal exit. a = dispatched addr; d = occurrence count. */
psx_pc0_journal_note(15u, cpu, addr,
(uint32_t)g_pczero_count | (g_dd_last_route << 28));
if (!g_pczero_latched) {
g_pczero_latched = 1;
g_pczero_addr = addr;
@@ -2679,6 +2688,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
int clean_game_text_miss = 0;
if (addr == 0x80000048u) {
g_dd_last_route = 1;
g_sentinel_reach_dirty++;
if (!psx_get_in_exception()) g_sentinel_reach_traps++; /* reuse: in_exc==0 dirty reaches */
g_sentinel_reach_async = g_async_rfe_resume_pc;
@@ -2713,6 +2723,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
ls_func_enter(addr, cpu);
int prev_phase = g_exec_phase;
g_exec_phase = 3;
g_dd_last_route = 2;
int _gc = psx_dispatch_game_compiled(cpu, addr);
g_exec_phase = prev_phase;
ls_func_exit(addr, cpu, _gc);
@@ -2740,6 +2751,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
#ifdef PSX_HAS_OVERLAY_DISPATCH
{
extern int psx_overlay_dispatch(CPUState *cpu, uint32_t addr);
g_dd_last_route = 3;
if (psx_overlay_dispatch(cpu, addr)) return 1;
}
#endif
@@ -2770,6 +2782,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
{
extern int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
g_dd_last_route = 4;
if (overlay_loader_dispatch(cpu, addr)) {
if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 1);
return 1;
@@ -2778,7 +2791,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
/* Every exit retires any deferred load writeback: once we hand control back to
* compiled code (or the dispatch loop) nothing downstream knows a register
* write is still owed, and the pipeline would have drained by then anyway. */
#define OV_FPLOG_RET1() do { dirty_ram_ld_delay_flush(cpu); if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 0); return 1; } while (0)
#define OV_FPLOG_RET1() do { g_dd_last_route = 5; dirty_ram_ld_delay_flush(cpu); if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 0); return 1; } while (0)
if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) {
/* Bulk host transfers can populate post-EXE executable RAM without
+5
View File
@@ -14412,6 +14412,11 @@ session_reboot:
(unsigned long long)g_dirty_ram_insns_run, g_slice_exit_pc, g_slice_exit_reason,
g_slice_exit_iter, g_slice_exit_dispatchable, g_slice_exit_dirty,
g_slice_exit_in_text, g_slice_exit_want); }
{ extern int g_pczero_latched; extern uint32_t g_pczero_addr, g_pczero_ra, g_pczero_in_exc;
extern uint64_t g_pczero_count;
std::fprintf(stdout, "psxrecomp runtime: [pczero tripwire] latched=%u count=%llu addr=0x%08X ra=0x%08X in_exc=%u\n",
g_pczero_latched, (unsigned long long)g_pczero_count,
g_pczero_addr, g_pczero_ra, g_pczero_in_exc); }
/* pc=0 escape journal tail: names the exact runtime site that published
* (or rescued) each recent null PC, with frame/depth/ra/epc context —
* the play-build replacement for the debug-only fntrace ring, which is
+10
View File
@@ -258,6 +258,16 @@ void psx_pc0_journal_note(uint32_t site, CPUState *cpu, uint32_t a, uint32_t d)
e->d = d;
}
/* Called by the emitted trampoline when a top-level dispatch loop exits on
* pc==0 (the abnormal-run terminator): journals the LAST dispatched target,
* which no runtime site can otherwise see (the fatal 0 is published inside
* generated code — a null jr or a plain C-return). site 14; a=last target,
* d=$sp. */
void psx_pc0_trampoline_exit(CPUState *cpu, uint32_t last_target)
{
psx_pc0_journal_note(14u, cpu, last_target, cpu->gpr[29]);
}
static uint32_t psx_current_tcb_ptr(CPUState* cpu)
{
uint32_t tcbh = cpu->read_word(0x00000108u);