mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
4d50c2214a
Brings the fail-closed native entry guard from wt/tomba2 (b5a6940 / bd60381) to master, where it had never been merged — which is why Tomba (and any master-built title) still wedged. This is the long-standing "blue screen" / shared-RAM- divergence-between-interp-and-native class. Root cause: an overlay function dispatched at a FOREIGN INTERIOR PC (a range- ownership mismatch routed a PC the function does not own into it) hit the CPS entry-switch `default: break`, which fell through and ran the function FROM ITS TOP — re-running its prologue and corrupting shared CPU/RAM state, so the guest's main loop then waits forever on a condition that never gets set (the 0xCF0 SIO spin / blue screen). Reproduced on Tomba's Village of All Beginnings: a native 0x000E7000-region shard function entered at a foreign PC wedged; the interpreter ran the same bytes correctly. Fix: the generated CPS entry-switch for OVERLAY functions (config_.overlay_mode) now FAILS CLOSED on an unknown non-zero continuation PC — it restores the PC and calls psx_native_bad_entry instead of running from the top. overlay_loader_dispatch consumes g_native_bad_entry and routes the PC to the sanctioned dirty-RAM interpreter (the live bytes run correctly — NOT a stub/HLE). True-prologue entry still runs from the top. Static (BIOS/boot-EXE) functions keep the legacy fall- through. ABI bumped 5->6 so stale ABI-5 caches are rejected and rebuilt with the guard. Minimal port: kept only the fail-closed mechanism + its ABI plumbing. The Tomba2 host_aliases interior-routing and the native-block / CPS-probe diagnostics (which depend on the overlay-cache-v2 multi-range Candidate fields not on master) were intentionally omitted — the fail-closed default alone is the safety net. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
8.9 KiB
C
176 lines
8.9 KiB
C
/* ============================================================================
|
|
* cpu_state.h — Phase 2 runtime version
|
|
* ----------------------------------------------------------------------------
|
|
* This is the RUNTIME version of the CPUState header. It has the same struct
|
|
* layout as generated/cpu_state.h (Phase 1a compile-only artifact) but is
|
|
* intended to be linked into a runnable binary.
|
|
*
|
|
* The generated C files (SCPH1001_full.c, SCPH1001_dispatch.c) include
|
|
* "cpu_state.h" and the build uses -I runtime/include so they find this file.
|
|
* ========================================================================== */
|
|
|
|
#ifndef PSXRECOMP_CPU_STATE_H
|
|
#define PSXRECOMP_CPU_STATE_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* sljit shard host-helper table — see SLJIT_PERSIST_CACHE.md Stage 1. The sljit
|
|
* JIT routes its host-helper calls through this cpu-relative table (indexed by
|
|
* the enum below) instead of baking absolute function pointers as SLJIT_IMM
|
|
* operands, so a serialized shard's LIR contains zero absolute host addresses —
|
|
* the prerequisite for persisting shards to disk (a reloaded process is at a
|
|
* different ASLR base, so baked pointers would be stale). The ENUM ORDER is part
|
|
* of the on-disk shard format: never reorder, only append, and bump the sljit
|
|
* serialization tag when it changes. gcc codegen does NOT use this table. */
|
|
enum {
|
|
SLJIT_HLP_MEMX = 0, /* psx_sljit_memx — LWL/LWR/SWL/SWR */
|
|
SLJIT_HLP_COP2, /* psx_sljit_cop2 — COP2/GTE, LWC2/SWC2 */
|
|
SLJIT_HLP_WS_CULL, /* psx_ws_cull_sltiu — widescreen render-funnel cull */
|
|
SLJIT_HLP_CALL, /* psx_sljit_call — jal/jalr call contract */
|
|
SLJIT_HLP_COUNT
|
|
};
|
|
|
|
typedef struct CPUState {
|
|
uint32_t gpr[32]; /* $0..$31; gpr[0] is hardwired zero, never written */
|
|
uint32_t pc; /* program counter */
|
|
uint32_t hi, lo; /* mult/div result registers */
|
|
uint32_t cop0[32]; /* COP0 system control registers (SR, Cause, EPC, ...) */
|
|
uint32_t gte_data[32]; /* COP2 (GTE) data registers */
|
|
uint32_t gte_ctrl[32]; /* COP2 (GTE) control registers */
|
|
|
|
/* Memory access function pointers -- wired at init to psx_read/psx_write. */
|
|
uint32_t (*read_word)(uint32_t addr);
|
|
void (*write_word)(uint32_t addr, uint32_t value);
|
|
uint16_t (*read_half)(uint32_t addr);
|
|
void (*write_half)(uint32_t addr, uint16_t value);
|
|
uint8_t (*read_byte)(uint32_t addr);
|
|
void (*write_byte)(uint32_t addr, uint8_t value);
|
|
|
|
/* sljit host-helper pointer table (indexed by the SLJIT_HLP_* enum above).
|
|
* Appended at the END of CPUState so existing field offsets are unchanged —
|
|
* precompiled gcc overlay DLLs (which bake field offsets) are unaffected;
|
|
* only the sljit JIT reads it. Populated once at startup. */
|
|
void *sljit_helpers[SLJIT_HLP_COUNT];
|
|
} CPUState;
|
|
|
|
/* Trap trampolines — defined in runtime/src/traps.c */
|
|
/* psx_syscall returns 1 if control transfers (cpu->pc set to a dispatch target;
|
|
* the caller must return to the trampoline), 0 for a directly-handled "void"
|
|
* syscall (Enter/ExitCriticalSection). Under CPS the generated code emits
|
|
* `if (psx_syscall(...)) return;` so a void syscall falls through to the inline
|
|
* post-syscall code (the guest's own jr $ra); legacy callers ignore the return
|
|
* value and rely on cpu->pc (0 = handled, resume at caller). */
|
|
extern int psx_syscall(CPUState* cpu, uint32_t code);
|
|
extern void psx_arith_overflow(CPUState* cpu);
|
|
extern void psx_unaligned_access(CPUState* cpu, uint32_t addr, uint32_t pc);
|
|
extern void psx_break(CPUState* cpu, uint32_t code, uint32_t pc);
|
|
/* Fail-closed native entry guard: a function's CPS entry-switch calls this when
|
|
* dispatched at a PC that is not one of its legal entries (foreign interior PC
|
|
* from a range-ownership mismatch). The function returns without executing; the
|
|
* overlay dispatch then routes the PC to the sanctioned dirty-RAM interpreter. */
|
|
extern void psx_native_bad_entry(CPUState* cpu, uint32_t owner, uint32_t pc);
|
|
|
|
/* Dispatch — defined in generated/SCPH1001_dispatch.c */
|
|
extern void psx_dispatch(CPUState* cpu, uint32_t target_addr);
|
|
extern void psx_dispatch_call(CPUState* cpu, uint32_t target_addr, uint32_t return_addr);
|
|
|
|
/* Unknown dispatch — defined in runtime/src/traps.c */
|
|
extern void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys);
|
|
|
|
/* GTE (COP2) — defined in runtime/src/gte.cpp */
|
|
extern void gte_execute(CPUState* cpu, uint32_t cmd);
|
|
/* Widescreen X-squash: display aspect num:den (4,3 = identity/off). Scales
|
|
* RTPS/RTPT screen-X around the game's OFX so a 4:3 frame stretched to the
|
|
* wide aspect shows a wider field of view. */
|
|
extern void gte_set_display_aspect(int num, int den);
|
|
extern void gte_ws_set_suppress(int on); /* 8C: un-squash far backdrop draws */
|
|
extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);
|
|
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
|
|
|
|
#endif /* PSXRECOMP_CPU_STATE_H */
|