mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
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:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user