mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
recompiler/runtime: honor IRQ redirects at compiled leaders; revive async-RFE latch; pc0 escape journal
Fixes the deterministic top-level "execution completed, PC=0" ~10 s
into WipEout 3 boot with the static bake linked (the 120 Hz core bug).
Root cause (CPS-contract audit, H1): every emitted block-leader IRQ
check was `psx_check_interrupts_at(cpu, X); cpu->pc = X;` — whatever
resume PC the exception epilogue published (guest RFE to a non-EPC
target: longjmp-style card-ISR handlers, SetCustomExitFromException,
thread switch) was unconditionally overwritten by the static transfer.
The dirty interpreter honors these redirects ("Handler resumed
elsewhere — surface to dispatch"); the static bake moved the IRQ
take-points from interp pumps onto compiled leaders, where the redirect
was dropped — the guest's continuation vanished, downstream state was
never established, and a later jr through a null pointer surfaced as a
silent top-level PC=0 exit.
- code_generator/full_function_emitter: both check emitters (and the
install-slot hook) now emit the compiled analogue of the interp
contract: `if (cpu->pc != 0 && ((cpu->pc ^ X) & 0x1FFFFFFF)) return;`
after the check, surfacing the redirect to the trampoline. Applies to
branch/jr/jalr/jal/split sites in the AOT text, the recompiled BIOS,
and every overlay shard/static bake on regeneration.
- interrupts.c: the async-RFE resume latch (g_async_rfe_resume_pc,
"Tomba 2 frame-1997 fix") was documented but NEVER ASSIGNED — the
whole rescue in the traps/dirty sentinel gates was inert (audit H3).
Latch it at every exception entry that installs a real EPC.
- pc0 escape journal (always-on, play builds): every runtime site that
publishes/rescues a null PC records {site, frame, dispatch depth, ra,
epc, extras} in a 64-entry ring, dumped at the abnormal-exit print.
The play build's exit trace was blind (fntrace is debug-only:
fntrace_seq=0 in the death artifact); publishes are rare so the
always-on cost is a few stores.
- compile_overlays.py: PSX_STATIC_NO_ISOLATED=1 A/B guard to skip the
isolated-fragment pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,18 +140,34 @@ std::string CodeGenerator::emit_mid_block_cycle_charge(uint32_t addr,
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
/* Both check emitters honor an exception REDIRECT: when the delivery's
|
||||
* epilogue publishes a resume PC different from this site's static
|
||||
* continuation (guest RFE to a non-EPC target — longjmp-style handlers,
|
||||
* SetCustomExitFromException, thread switch), the compiled code must
|
||||
* surface it to the trampoline instead of overwriting it with the static
|
||||
* transfer. This is the compiled analogue of the dirty interpreter's
|
||||
* "Handler resumed elsewhere — surface to dispatch" contract; without it a
|
||||
* baked/compiled leader silently drops the guest's continuation (WipEout 3
|
||||
* static bake: deterministic top-level "execution completed, PC=0" ~10 s
|
||||
* into boot, root-caused to redirects dropped at compiled leaders). A
|
||||
* published pc equal to the static continuation falls through (same-thread
|
||||
* restore publishes 0; the depth-guard rescue publishes the site PC). */
|
||||
std::string CodeGenerator::emit_interrupt_check(uint32_t resume_pc,
|
||||
const std::string& indent) const {
|
||||
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
|
||||
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
|
||||
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc);
|
||||
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc) + indent +
|
||||
fmt::format("if (cpu->pc != 0u && ((cpu->pc ^ 0x{:08X}u) & 0x1FFFFFFFu)) return; /* IRQ redirect */\n",
|
||||
resume_pc);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::emit_interrupt_check_expr(const std::string& resume_pc_expr,
|
||||
const std::string& indent) const {
|
||||
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
|
||||
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
|
||||
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
|
||||
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr) + indent +
|
||||
fmt::format("if (cpu->pc != 0u && ((cpu->pc ^ ({})) & 0x1FFFFFFFu)) return; /* IRQ redirect */\n",
|
||||
resume_pc_expr);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::reg_name(int reg_num) {
|
||||
|
||||
@@ -171,17 +171,25 @@ bool FullFunctionEmitter::emit_function(
|
||||
const char* e = std::getenv("PSX_CPS");
|
||||
return e == nullptr || e[0] != '0';
|
||||
}();
|
||||
/* Redirect-honoring checks — see CodeGenerator::emit_interrupt_check:
|
||||
* an exception epilogue that publishes a resume PC different from this
|
||||
* site's static continuation must surface to the trampoline, never be
|
||||
* overwritten by the static transfer (the dropped-continuation class
|
||||
* behind the WipEout 3 static-bake top-level PC=0 exit). */
|
||||
auto emit_irq_check = [](uint32_t resume_pc, const std::string& indent = " ") {
|
||||
uint32_t rt = bios_runtime_pc(resume_pc);
|
||||
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
|
||||
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
|
||||
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n",
|
||||
bios_runtime_pc(resume_pc));
|
||||
fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", rt) + indent +
|
||||
fmt::format("if (cpu->pc != 0u && ((cpu->pc ^ 0x{:08X}u) & 0x1FFFFFFFu)) return; /* IRQ redirect */\n", rt);
|
||||
};
|
||||
auto emit_irq_check_expr = [](const std::string& resume_pc_expr,
|
||||
const std::string& indent = " ") {
|
||||
return std::string("#ifdef PSX_ENABLE_BLOCK_CYCLES\n") + indent +
|
||||
"psx_cyc_bb_defer_flush();\n#endif\n" + indent +
|
||||
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
|
||||
fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr) + indent +
|
||||
fmt::format("if (cpu->pc != 0u && ((cpu->pc ^ ({})) & 0x1FFFFFFFu)) return; /* IRQ redirect */\n",
|
||||
resume_pc_expr);
|
||||
};
|
||||
auto emit_cosim_instr = [](uint32_t pc, const std::string& indent = " ") {
|
||||
return "#ifdef PSX_COSIM\n" + indent +
|
||||
@@ -1202,9 +1210,10 @@ bool FullFunctionEmitter::emit_function(
|
||||
" * back here as a registered continuation target. */\n"
|
||||
" if (cpu->read_word(0x{:08X}u) != 0u) {{\n"
|
||||
" psx_check_interrupts_at(cpu, 0x{:08X}u);\n"
|
||||
" if (cpu->pc != 0u && ((cpu->pc ^ 0x{:08X}u) & 0x1FFFFFFFu)) return; /* IRQ redirect */\n"
|
||||
" cpu->pc = 0x{:08X}u; return;\n"
|
||||
" }}\n",
|
||||
addr, ram_pc, ram_pc + 0x10u, ram_pc, ram_pc, ram_pc);
|
||||
addr, ram_pc, ram_pc + 0x10u, ram_pc, ram_pc, ram_pc, ram_pc);
|
||||
}
|
||||
|
||||
// Non-terminator: emit normally — unless this load's successor reads
|
||||
|
||||
@@ -2682,6 +2682,9 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
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;
|
||||
psx_pc0_journal_note(PSX_PC0J_DIRTY_SENTINEL, cpu, addr,
|
||||
(psx_get_in_exception() ? 1u : 0u) |
|
||||
(g_async_rfe_resume_pc ? 2u : 0u));
|
||||
cpu->pc = 0;
|
||||
if (psx_get_in_exception()) {
|
||||
psx_exception_longjmp(); /* does not return */
|
||||
@@ -3032,6 +3035,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
g_dirty_ram_last_unsupported_pc = g_unsupported_pc;
|
||||
g_dirty_ram_last_unsupported_insn = g_unsupported_insn;
|
||||
g_dirty_ram_last_unsupported_reason = g_unsupported_reason;
|
||||
psx_pc0_journal_note(PSX_PC0J_DIRTY_UNSUPPORTED, cpu,
|
||||
g_unsupported_pc, g_unsupported_insn);
|
||||
cpu->pc = 0;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
|
||||
@@ -1471,6 +1471,8 @@ irq_deliver_eval:
|
||||
* disarms the RFE-flag escape). */
|
||||
g_rfe_escape_pending = 0;
|
||||
{
|
||||
extern uint32_t g_async_rfe_resume_pc; /* dirty_ram_interp.c */
|
||||
extern uint64_t g_async_rfe_set_count;
|
||||
uint32_t real_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc
|
||||
: s_compiled_interrupt_resume_pc;
|
||||
/* Top-level flush_resume / savestate: resync cleared the latches and
|
||||
@@ -1514,6 +1516,15 @@ irq_deliver_eval:
|
||||
cpu->cop0[COP0_EPC] = real_pc; /* architectural: the real resume PC */
|
||||
g_exception_real_epc = real_pc;
|
||||
g_exc_escape_reason = PSX_EXC_ESCAPE_NONE; /* set at the actual RFE/SYSCALL return */
|
||||
/* Async-RFE latch (Tomba 2 frame-1997 fix): persist the most
|
||||
* recent real interruption PC so a game-driven asynchronous
|
||||
* ReturnFromException (sentinel RFE with in_exception==0, e.g. a
|
||||
* card-ISR longjmp installed via HookEntryInt) resumes the guest
|
||||
* here instead of resolving to pc=0 (abnormal top-level exit).
|
||||
* The latch was documented but never assigned — the whole rescue
|
||||
* in the traps/dirty sentinel gates was inert. */
|
||||
g_async_rfe_resume_pc = real_pc;
|
||||
g_async_rfe_set_count++;
|
||||
} else {
|
||||
uint32_t sentinel = PSX_EXC_SENTINEL_PC;
|
||||
cpu->write_word(sentinel, 0x00000000u); /* NOP, read by the handler's BD check */
|
||||
@@ -1862,7 +1873,12 @@ irq_deliver_eval:
|
||||
if (resume != 0u)
|
||||
cpu->pc = resume;
|
||||
/* else keep post-RFE cpu->pc — never publish 0 */
|
||||
psx_pc0_journal_note(PSX_PC0J_IRQ_RESCUE, cpu, resume,
|
||||
g_exc_escape_reason);
|
||||
} else {
|
||||
psx_pc0_journal_note(PSX_PC0J_IRQ_SAME_THREAD, cpu,
|
||||
s_compiled_interrupt_resume_pc,
|
||||
g_exc_escape_reason);
|
||||
cpu->pc = 0; /* continue the interrupted live chain */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14412,6 +14412,24 @@ 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); }
|
||||
/* 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
|
||||
* compiled out here (the exit trace JSON was blind: fntrace_seq=0). */
|
||||
{
|
||||
uint64_t n = g_pc0j_seq < PSX_PC0J_CAP ? g_pc0j_seq : PSX_PC0J_CAP;
|
||||
std::fprintf(stdout, "psxrecomp runtime: [pc0 journal] %llu total, last %llu:\n",
|
||||
(unsigned long long)g_pc0j_seq, (unsigned long long)n);
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
const Pc0JournalEntry *e =
|
||||
&g_pc0j_ring[(g_pc0j_seq - n + i) % PSX_PC0J_CAP];
|
||||
std::fprintf(stdout,
|
||||
" seq=%llu site=%u frame=%u depth=%d a=0x%08X "
|
||||
"ra=0x%08X epc=0x%08X d=0x%08X\n",
|
||||
(unsigned long long)e->seq, e->site, e->frame,
|
||||
e->depth, e->a, e->b, e->c, e->d);
|
||||
}
|
||||
}
|
||||
|
||||
shutdown_runtime();
|
||||
if (g_gl_active) gl_renderer_shutdown();
|
||||
|
||||
@@ -240,6 +240,24 @@ enum {
|
||||
};
|
||||
uint32_t g_pc0_reason = PSX_PC0_GUEST_RETURN;
|
||||
|
||||
/* pc=0 escape journal — see cpu_state.h for the site enum and rationale. */
|
||||
Pc0JournalEntry g_pc0j_ring[PSX_PC0J_CAP];
|
||||
uint64_t g_pc0j_seq = 0;
|
||||
void psx_pc0_journal_note(uint32_t site, CPUState *cpu, uint32_t a, uint32_t d)
|
||||
{
|
||||
extern int g_psx_dispatch_depth;
|
||||
extern uint64_t s_frame_count; /* present frame counter (debug_server.c) */
|
||||
Pc0JournalEntry *e = &g_pc0j_ring[g_pc0j_seq % PSX_PC0J_CAP];
|
||||
e->seq = g_pc0j_seq++;
|
||||
e->site = site;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->depth = g_psx_dispatch_depth;
|
||||
e->a = a;
|
||||
e->b = cpu->gpr[31];
|
||||
e->c = cpu->cop0[14];
|
||||
e->d = d;
|
||||
}
|
||||
|
||||
static uint32_t psx_current_tcb_ptr(CPUState* cpu)
|
||||
{
|
||||
uint32_t tcbh = cpu->read_word(0x00000108u);
|
||||
@@ -555,6 +573,7 @@ static int psx_change_thread_fiber(CPUState* cpu, uint32_t target_tcb)
|
||||
if (current_tcb == target_tcb) {
|
||||
debug_server_log_thread_event(5, cpu, current_tcb, target_tcb, cpu->gpr[31]);
|
||||
g_pc0_reason = PSX_PC0_CHANGE_SELF;
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_CHANGE_SELF, cpu, target_tcb, 0);
|
||||
cpu->pc = 0;
|
||||
return 1;
|
||||
}
|
||||
@@ -625,6 +644,7 @@ static int psx_change_thread_fiber(CPUState* cpu, uint32_t target_tcb)
|
||||
|
||||
debug_server_log_thread_event(9, cpu, target_tcb, current_tcb, 0);
|
||||
g_pc0_reason = PSX_PC0_FIBER_SWITCH;
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_FIBER_SWITCH, cpu, 0, 0);
|
||||
cpu->pc = 0;
|
||||
return 1;
|
||||
}
|
||||
@@ -943,6 +963,7 @@ int psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
cpu->cop0[12] = sr & ~1u; /* clear IEc (bit 0) */
|
||||
cpu->gpr[2] = sr & 1u; /* return old IEc */
|
||||
g_pc0_reason = PSX_PC0_CRIT_SECTION;
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_CRIT_ENTER, cpu, sr, 0);
|
||||
cpu->pc = 0;
|
||||
/* Directly handled "void" syscall. Return 0: under CPS the caller
|
||||
* (`if (psx_syscall(...)) return;`) falls through to the inline
|
||||
@@ -955,6 +976,7 @@ int psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
cpu->cop0[12] = sr | 0x0401u; /* set IEc (bit 0) + IM[2] (bit 10) */
|
||||
cpu->gpr[2] = 0;
|
||||
g_pc0_reason = PSX_PC0_CRIT_SECTION;
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_CRIT_EXIT, cpu, sr, 0);
|
||||
cpu->pc = 0;
|
||||
return 0;
|
||||
|
||||
@@ -1106,6 +1128,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
|
||||
* or uninitialized callback slot is dispatched. */
|
||||
if (addr == 0) {
|
||||
g_pc0_reason = PSX_PC0_DISPATCH_MISS; /* guest jumped/returned to a null (0) target */
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_NULL_TARGET, cpu, addr, 0);
|
||||
cpu->pc = 0;
|
||||
return;
|
||||
}
|
||||
@@ -1117,6 +1140,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
|
||||
* the exception handler, longjmp back to psx_check_interrupts to
|
||||
* properly unwind the handler call tree. */
|
||||
if (addr == 0x80000048u && psx_get_in_exception()) {
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_SENTINEL, cpu, addr, 1);
|
||||
cpu->pc = 0;
|
||||
psx_exception_longjmp(); /* does not return */
|
||||
}
|
||||
@@ -1136,6 +1160,8 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
|
||||
g_sentinel_reach_async = g_async_rfe_resume_pc;
|
||||
if (g_async_rfe_resume_pc != 0u) {
|
||||
g_async_rfe_fire_count++;
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_ASYNC_RFE, cpu,
|
||||
g_async_rfe_resume_pc, 0);
|
||||
cpu->pc = g_async_rfe_resume_pc;
|
||||
return;
|
||||
}
|
||||
@@ -1151,6 +1177,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
|
||||
* the psx_dispatch tail-call loop picking up $ra), it's a no-op — the
|
||||
* continuation was already handled by the merged function. */
|
||||
if (phys == 0x00000E10u) {
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_E10_NOOP, cpu, addr, 0);
|
||||
cpu->pc = 0;
|
||||
return;
|
||||
}
|
||||
@@ -1422,6 +1449,7 @@ void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys) {
|
||||
* ring-recorded above. Stale registers WILL corrupt the caller —
|
||||
* diagnostic use only. */
|
||||
g_pc0_reason = PSX_PC0_DISPATCH_MISS;
|
||||
psx_pc0_journal_note(PSX_PC0J_TRAP_STALE_MISS, cpu, addr, 0);
|
||||
cpu->pc = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6598,6 +6598,10 @@ def main():
|
||||
# interiors. Compile each as an isolated dispatch-root shard, then give
|
||||
# every block in those fragments the same universal resume treatment.
|
||||
unresolved = sorted(static_requested_entries - existing_entries)
|
||||
# PSX_STATIC_NO_ISOLATED=1: A/B guard — skip the isolated-fragment pass
|
||||
# entirely (its universal-resume treatment is the newest machinery).
|
||||
if os.environ.get('PSX_STATIC_NO_ISOLATED'):
|
||||
unresolved = []
|
||||
fragment_built = 0
|
||||
new_fragment_parts = []
|
||||
for entry in unresolved:
|
||||
|
||||
Reference in New Issue
Block a user