mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
recursion(stage3): overlay (sljit + gcc-DLL) CPS parity — full native overlay coverage under CPS
RECURSION_BUG.md §25.5. Brings the overlay native path under continuation-passing so the idle-freeze leak fix holds with overlay_cache=true (the production gcc>sljit>interp path). Runtime CPS-mode flag g_psx_cps_mode (overlay_loader.c) set by a __attribute__((constructor)) emitted in the BIOS + game CPS dispatch (full_function_emitter.cpp / main_psx.cpp). Gates all overlay/sljit CPS behavior; legacy unaffected. sljit JIT (overlay_sljit.c), scoped: - jr $ra publishes cpu->pc (was a bare return). jr rX already tail-transferred. - call-containing fragments DECLINE to the interpreter (CPS-safe, depth-1) — leaf shards still JIT. (Full sljit call coverage / per-fragment entry-switch is future work.) gcc-DLL path — FULL native overlay coverage (validated): - compile_overlays.py --cps sets PSX_CPS for the recompiler -> CPS overlay C (tail-transfer + per-function entry-switch; --overlay mode only affects widescreen, not CPS). DISPATCH_PREAMBLE psx_syscall decl fixed void->int. - PSX_OVERLAY_CODEGEN_VER 3->4: fresh cg4/ cache namespace; legacy unit-model DLLs (cg3) ignored. - overlay_loader_dispatch: under CPS, when a dispatched addr is not an overlay ENTRY but lies inside a registered candidate's code range (a tail-transferred call's continuation), overlay_find_by_range() re-enters that function with cpu->pc=addr so its entry-switch routes to the block. gcc DLLs are the trusted tier (diff-validated at entry) -> run native directly. This is the CPS equivalent of the old unit-model that fixed the dwarf->overworld blue screen. Validated: CPS overlay DLLs compile + load + dispatch_native>0; with continuation routing dispatch_interp_fallback -> 0 (full native), ce_profile FLAT (6 KB), boots clean. Addresses are KSEG0-consistent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-4
@@ -1266,8 +1266,36 @@ it decelerates and is unrelated to the (now-fixed) host-stack leak. Run gameplay
|
||||
`PSX_FNTRACE_ARM` for a normal footprint.
|
||||
|
||||
**Validation gate status:** ce_profile FLAT ✅ · 56.9k-frame soak no-freeze ✅ · boots+plays ✅.
|
||||
REMAINING: dwarf→overworld playtest (overlay-native `pc==0` return contract = the regression
|
||||
risk) · Stage 3 = sljit + overlay CPS parity (overlays must be recompiled under CPS; overlay ABI
|
||||
already bumped 4→5) · then consider making CPS the default and removing the now-inert
|
||||
`psx_call_contract`/`g_psx_call_bail` machinery. Commit the branch when the user is satisfied.
|
||||
|
||||
### 25.5 Stage 3 — overlay (sljit + gcc-DLL) CPS parity (2026-06-18)
|
||||
|
||||
**sljit JIT (overlay_sljit.c), scoped:** `jr $ra` publishes `cpu->pc`; call-containing
|
||||
fragments **decline to the interp** (CPS-safe, depth-1); `jr rX` already tail-transferred.
|
||||
So only leaf shards JIT — `jr` covered, calls interp. Validated: boots, leak flat, shards
|
||||
persist. A runtime CPS flag `g_psx_cps_mode` (defined in overlay_loader.c, set by a
|
||||
`__attribute__((constructor))` emitted in the BIOS + game CPS dispatch) gates the sljit/overlay
|
||||
CPS behavior.
|
||||
|
||||
**gcc-DLL path — FULL native overlay coverage under CPS (the production path), DONE + proven:**
|
||||
- `compile_overlays.py --cps` sets `PSX_CPS` for the recompiler → CPS overlay C (tail-transfer
|
||||
+ per-function entry-switch; `code_generator` `--overlay` mode only affects widescreen, not
|
||||
CPS). The DISPATCH_PREAMBLE's `psx_syscall` decl was fixed void→int. `PSX_OVERLAY_CODEGEN_VER`
|
||||
bumped 3→4 (fresh `cg4/` cache namespace; legacy unit-model DLLs in cg3 ignored).
|
||||
- **Loader continuation routing** (`overlay_loader_dispatch`, gated `g_psx_cps_mode`): under CPS
|
||||
an overlay function is flat-dispatched (one block then tail-transfer), so a callee returns to
|
||||
a mid-function continuation that idx_head() (entry-keyed) misses. New `overlay_find_by_range()`
|
||||
finds the owning candidate by code range and re-enters it with `cpu->pc = addr` → its
|
||||
entry-switch routes to the block. gcc DLLs are the trusted tier (diff-validated at entry), run
|
||||
native directly. This is the CPS equivalent of the old unit-model that fixed the dwarf→overworld
|
||||
blue screen — overlay funcs no longer run as units; continuations re-dispatch flat.
|
||||
- **Result:** CPS overlay DLLs compile + load + `dispatch_native > 0`; with continuation routing
|
||||
**`dispatch_interp_fallback` → 0** (full native overlay coverage), ce_profile FLAT (6 KB),
|
||||
boots clean. Addresses are KSEG0-consistent (overlay C sets `gpr[31]=0x800E…` continuations =
|
||||
the entry-switch cases).
|
||||
|
||||
REMAINING: capture breadth — only the 2 overlays my short run captured are gcc-compiled; other
|
||||
scenes' overlays gcc-miss → sljit-leaf/interp (still CPS-safe) until captured+compiled (play
|
||||
through → `overlay_captures.json` grows → re-run `compile_overlays.py --cps`). dwarf→overworld
|
||||
playtest (the regression check). Then consider CPS default + removing inert
|
||||
`psx_call_contract`/`g_psx_call_bail`. Commit Stage 3 when the user is satisfied.
|
||||
|
||||
|
||||
@@ -1336,6 +1336,15 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += "void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr) {\n";
|
||||
out += " psx_dispatch_impl(cpu, addr, return_addr);\n";
|
||||
out += "}\n";
|
||||
|
||||
if (cps) {
|
||||
// RECURSION_BUG.md §25 — mark CPS mode at startup for runtime code that
|
||||
// must emit the CPS contract (the overlay sljit JIT, overlay_sljit.c).
|
||||
out += "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n";
|
||||
out += "__attribute__((constructor)) static void psx_cps_mark_bios(void) {\n";
|
||||
out += " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
|
||||
out += "}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -838,6 +838,16 @@ int main(int argc, char** argv) {
|
||||
ds << " }\n";
|
||||
ds << "}\n";
|
||||
|
||||
if (codegen.cps_enabled()) {
|
||||
// RECURSION_BUG.md §25 — mark CPS mode at startup so the overlay sljit
|
||||
// JIT (overlay_sljit.c) emits the CPS contract. Static ctor: no clash
|
||||
// with the BIOS dispatch's marker.
|
||||
ds << "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n";
|
||||
ds << "__attribute__((constructor)) static void psx_cps_mark_game(void) {\n";
|
||||
ds << " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
|
||||
ds << "}\n";
|
||||
}
|
||||
|
||||
std::ofstream dispatch_file(dispatch_filename);
|
||||
if (dispatch_file.is_open()) {
|
||||
dispatch_file << ds.str();
|
||||
|
||||
@@ -68,8 +68,11 @@
|
||||
* 1: baseline + auto_screen_x render-funnel cull via psx_ws_cull_sltiu.
|
||||
* 2: + auto_backdrop far-backdrop column preload via psx_ws_backdrop_value.
|
||||
* 3: auto_backdrop switched to camera-tracked window WIDENING
|
||||
* (psx_ws_backdrop_value gains a window_cols arg). */
|
||||
#define PSX_OVERLAY_CODEGEN_VER 3
|
||||
* (psx_ws_backdrop_value gains a window_cols arg).
|
||||
* 4: continuation-passing (PSX_CPS, RECURSION_BUG.md §25) — overlay funcs
|
||||
* tail-transfer + carry an entry-switch; the loader routes continuations.
|
||||
* Fresh namespace so CPS DLLs never reuse a legacy (unit-model) cg3 DLL. */
|
||||
#define PSX_OVERLAY_CODEGEN_VER 4
|
||||
|
||||
typedef struct {
|
||||
/* Core dispatch: routes call_by_address() and out-of-overlay jal */
|
||||
|
||||
@@ -80,6 +80,12 @@ typedef struct {
|
||||
static Candidate s_cand[CAND_CAP];
|
||||
static int s_cand_n = 0;
|
||||
|
||||
/* RECURSION_BUG.md §25 — 1 when the build is continuation-passing (set by a
|
||||
* constructor in the generated CPS dispatch). The overlay dispatch + sljit JIT
|
||||
* read it to emit/route the CPS tail-transfer contract. Defined here (before
|
||||
* overlay_loader_dispatch) so both can see it. 0 = legacy unit-model. */
|
||||
int g_psx_cps_mode = 0;
|
||||
|
||||
/* Open-addressed index: phys entry addr -> head candidate index (-1 sentinel
|
||||
* stored as chain terminator on each Candidate). addr 0 = empty slot. */
|
||||
#define IDX_CAP 32768u
|
||||
@@ -990,6 +996,25 @@ static void try_load_region(uint32_t phys) {
|
||||
|
||||
/* ---- Dispatch ---------------------------------------------------------- */
|
||||
|
||||
/* CPS (§25): find a non-blacklisted candidate whose code range CONTAINS phys —
|
||||
* i.e. phys is a continuation / return point INSIDE an overlay function, not its
|
||||
* registered entry. Under continuation-passing, an overlay function tail-
|
||||
* transfers after each block, so a callee returns to a mid-function address that
|
||||
* idx_head() (entry-keyed) misses; we re-enter the owning function with
|
||||
* cpu->pc = that address so its entry-switch routes to the right block. Returns
|
||||
* the candidate index, or -1. */
|
||||
static int overlay_find_by_range(uint32_t phys) {
|
||||
for (int i = 0; i < s_cand_n; i++) {
|
||||
const Candidate *c = &s_cand[i];
|
||||
if (c->state == ENTRY_BLACKLIST) continue;
|
||||
for (int r = 0; r < c->nranges; r++) {
|
||||
uint32_t lo = c->range_lo[r];
|
||||
if (phys >= lo && phys < lo + c->range_len[r]) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
|
||||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
int head = idx_head(phys);
|
||||
@@ -1013,6 +1038,47 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
|
||||
}
|
||||
}
|
||||
|
||||
/* CPS (§25) continuation re-entry: phys is not an overlay ENTRY, but it may
|
||||
* be a return point inside an already-registered overlay function (the
|
||||
* caller's continuation after a tail-transferred call). Re-enter that
|
||||
* function natively with cpu->pc = addr so its entry-switch routes to the
|
||||
* continuation block — without this the continuation falls to the interp and
|
||||
* native coverage stops at the first call. gcc-DLL candidates are the trusted
|
||||
* tier (diff-validated at their entry; the continuation is the same DLL code),
|
||||
* so they run native directly here; device-touch funcs still go to interp. */
|
||||
if (head < 0 && g_psx_cps_mode) {
|
||||
int ci = overlay_find_by_range(phys);
|
||||
if (ci >= 0) {
|
||||
Candidate *c = &s_cand[ci];
|
||||
uint32_t live = cand_crc(c);
|
||||
s_rehashes++;
|
||||
s_last_crc = live;
|
||||
if (live == c->crc_code) {
|
||||
if (c->state != ENTRY_VALID) { c->state = ENTRY_VALID; s_valid_count++; }
|
||||
if (c->device_touch) { s_disp_interp++; return 0; }
|
||||
if (!s_native_exec) { s_would_run_native++; s_disp_interp++; return 0; }
|
||||
uint32_t slot = s_nring_pos++ & (NRING_CAP - 1u);
|
||||
s_nring[slot].addr = addr;
|
||||
s_nring[slot].crc = c->crc_code;
|
||||
s_nring[slot].seq = ++s_nring_seq;
|
||||
s_nring[slot].returned = 0;
|
||||
uint32_t prev_inprogress = s_native_inprogress;
|
||||
s_native_inprogress = c->addr;
|
||||
s_native_calls_total++;
|
||||
if (s_active_depth < (int)(sizeof(s_active_stack) / sizeof(s_active_stack[0])))
|
||||
s_active_stack[s_active_depth++] = ci;
|
||||
s_disp_native++;
|
||||
cpu->pc = addr; /* route the func's entry-switch to the block */
|
||||
c->fn(cpu);
|
||||
if (s_active_depth > 0) s_active_depth--;
|
||||
s_nring[slot].returned = 1;
|
||||
s_native_inprogress = prev_inprogress;
|
||||
return 1;
|
||||
}
|
||||
/* stale code bytes: fall through to the interpreter */
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = head; i >= 0; i = s_cand[i].next) {
|
||||
Candidate *c = &s_cand[i];
|
||||
if (c->state == ENTRY_BLACKLIST) continue;
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
|
||||
/* ---- counters ---------------------------------------------------------- */
|
||||
static OverlayBackend s_active = OVERLAY_BACKEND_AUTO;
|
||||
/* RECURSION_BUG.md §25 — 1 when the build is continuation-passing (set by a
|
||||
* constructor in the generated CPS dispatch). Under CPS this JIT must emit the
|
||||
* tail-transfer contract: jr $ra publishes cpu->pc, and call-containing
|
||||
* fragments decline to the (CPS-safe, depth-1) interpreter instead of the legacy
|
||||
* nested psx_sljit_call. Defined in overlay_loader.c. */
|
||||
extern int g_psx_cps_mode;
|
||||
|
||||
static int s_resolved = 0;
|
||||
static int s_selftest_ok = -1; /* -1 = not run */
|
||||
static uint64_t s_compiles = 0;
|
||||
@@ -980,6 +987,15 @@ void overlay_sljit_try_compile(uint32_t entry,
|
||||
if (emit_one(C, insn, s_bd_kind[i], s_bd_cols[i]) != EMIT_OK) { aborted = 1; break; }
|
||||
gpr_flush(C);
|
||||
if (pending == PEND_RET) {
|
||||
if (g_psx_cps_mode) {
|
||||
/* CPS: publish $ra (jr $ra) so the flat trampoline dispatches
|
||||
* the caller's continuation. gpr[] already flushed to memory
|
||||
* by gpr_flush() above. */
|
||||
sljit_emit_op1(C, SLJIT_MOV32, SLJIT_R0, 0, SLJIT_MEM1(R_CPU),
|
||||
(sljit_sw)(offsetof(CPUState, gpr) + 31 * sizeof(uint32_t)));
|
||||
sljit_emit_op1(C, SLJIT_MOV32, SLJIT_MEM1(R_CPU),
|
||||
(sljit_sw)offsetof(CPUState, pc), SLJIT_R0, 0);
|
||||
}
|
||||
sljit_emit_return_void(C);
|
||||
gpr_reset();
|
||||
} else if (pending == PEND_TAIL) {
|
||||
@@ -1000,6 +1016,15 @@ void overlay_sljit_try_compile(uint32_t entry,
|
||||
* the following code is dead or label-guarded — reset. */
|
||||
if (!pend_cond) gpr_reset();
|
||||
} else { /* PEND_CALL: psx_sljit_call(cpu, target, return_pc, check) */
|
||||
if (g_psx_cps_mode) {
|
||||
/* CPS: a call's continuation (return_pc, inside this fragment)
|
||||
* would need a re-entry entry-switch this JIT doesn't emit;
|
||||
* and psx_sljit_call uses the legacy nested contract. Decline
|
||||
* the whole fragment — it falls to the interpreter, which
|
||||
* handles the call CPS-safely (depth-1). Leaf fragments (no
|
||||
* calls) still JIT. */
|
||||
aborted = 1; break;
|
||||
}
|
||||
sljit_emit_op1(C, SLJIT_MOV, SLJIT_R0, 0, R_CPU, 0); /* cpu */
|
||||
if (pend_call_dynamic)
|
||||
sljit_emit_op1(C, SLJIT_MOV32, SLJIT_R1, 0, R_CTRL, 0); /* jalr target */
|
||||
|
||||
@@ -815,8 +815,8 @@ void psx_check_interrupts(CPUState *cpu) {
|
||||
void gte_execute(CPUState *cpu, uint32_t cmd) {
|
||||
g_cbs.gte_execute(cpu, cmd);
|
||||
}
|
||||
void psx_syscall(CPUState *cpu, uint32_t code) {
|
||||
g_cbs.psx_syscall(cpu, code);
|
||||
int psx_syscall(CPUState *cpu, uint32_t code) {
|
||||
return g_cbs.psx_syscall(cpu, code);
|
||||
}
|
||||
void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys) {
|
||||
g_cbs.psx_unknown_dispatch(cpu, addr, phys);
|
||||
@@ -1129,6 +1129,10 @@ def main():
|
||||
help='codegen flavor id baked into overlay_abi() (0=base/master; '
|
||||
'widescreen build passes 1). The loader rejects DLLs whose '
|
||||
'flavor != the runtime flavor, keeping caches separate.')
|
||||
ap.add_argument('--cps', action='store_true',
|
||||
help='continuation-passing (RECURSION_BUG.md §25): set PSX_CPS '
|
||||
'when invoking the recompiler so overlay funcs tail-transfer '
|
||||
'+ carry an entry-switch. Must match the runtime build.')
|
||||
args = ap.parse_args()
|
||||
|
||||
# Read game ID from game.toml (strip BOM if present)
|
||||
@@ -1259,10 +1263,13 @@ def main():
|
||||
# sites that resolve into overlay code) are applied. --ws-config
|
||||
# only adopts the widescreen lists, not the game's exe/paths.
|
||||
'--ws-config', os.path.abspath(args.game_toml)]
|
||||
print(f' recompile: {args.recompiler} ...')
|
||||
print(f' recompile: {args.recompiler} ...{" [CPS]" if args.cps else ""}')
|
||||
toml_dir = os.path.dirname(os.path.abspath(args.game_toml))
|
||||
sub_env = dict(os.environ)
|
||||
if args.cps:
|
||||
sub_env['PSX_CPS'] = '1' # §25: emit continuation-passing overlay C
|
||||
r = subprocess.run(cmd, capture_output=True, text=True,
|
||||
cwd=toml_dir)
|
||||
cwd=toml_dir, env=sub_env)
|
||||
if r.returncode != 0:
|
||||
print(f' RECOMPILER ERROR:\n{r.stderr or r.stdout}')
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user