mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
recursion(fix): universal continuation-passing — FIXES the idle-freeze host-stack leak (BIOS+game)
The complete §25 fix: make BOTH recompiler emitters emit guest calls as TAIL-TRANSFERS driven by the unified flat trampoline + the guest's own $ra/$sp, instead of nesting one host C frame per guest call. The host stack can no longer mirror unbounded guest re-entrancy — the architectural root of the ~14-min idle freeze. Gated gen-time by PSX_CPS; legacy codegen byte-identical when unset. VALIDATED (user): full-CPS Tomba boots BIOS -> FMV -> New Game -> village idle and plays; ce_profile cur_max_kb FLAT at 5 KB from frame ~9.9k through 56,890 (turbo soak) — blew past the ~50k-frame freeze threshold with the host stack bounded (legacy climbed +1.17 KB/frame -> 48 MB -> freeze). KEY CORRECTION to the prior session's §25.1: there are TWO emitters. The BIOS (full_function_emitter.cpp) was done last session but the §8 leaking chain (func_8001A954 ...) is GAME code, emitted by code_generator.cpp (via main_psx.cpp) — never converted. Also, §25.1's "continuations not in the dispatch table" hypothesis was wrong (they were always added); the real boot regressions were found by dumping the always-on fntrace ring on the abnormal psx_dispatch return. Emitter changes (gated PSX_CPS): - full_function_emitter.cpp (BIOS): BIOS A0/B0/C0 vector runtime-fallback tail-transfers instead of nested psx_dispatch_call (was the frame-0 exit). - code_generator.cpp/.h (game): jal/jalr/jr-$ra/jr-table/branch-split/j-split -> tail-transfer (jalr captures rs before the link, alias-safe); each call's return point registered as a dispatchable continuation; entry-switch (keyed on cpu->pc) at the top of every function AND every overlapping-alias body. 0 psx_call_contract / call_by_address emitted under CPS. - main_psx.cpp: psx_dispatch_game_compiled gains continuation cases (case cont: cpu->pc=cont; func_owner(cpu)); entries clear cpu->pc. - strict_translator.cpp: syscall emits `if (psx_syscall(...)) return;` so a void syscall falls through to the inline post-syscall jr $ra (real-HW EPC+4). Runtime changes (the legacy "return via nested dispatch / cpu->pc=0" idioms that exit the flat CPS trampoline): - traps.c: psx_syscall returns int (0=void/fall-through, 1=transfer); Enter/ExitCriticalSection return 0; case-3 ChangeThread-success returns 0 — the fiber scheduler leaves cpu->pc=0 on resume, which was the frame-712 exit. - cpu_state.h / overlay_api.h: psx_syscall signature void->int; overlay callback ABI bumped 4->5 (stale overlay caches rejected). - dirty_ram_interp.c: gate interp_enter_compiled behind PSX_HAS_GAME_DISPATCH (un-breaks the BIOS-only build). - main.cpp: dump the always-on fntrace ring tail + stack to psx_cps_exit_trace.json on an abnormal psx_dispatch return (diagnostic; CLAUDE.md ring-buffer model). Docs: RECURSION_BUG.md §25.2-§25.4. Remaining: dwarf->overworld playtest; Stage 3 (sljit/overlay CPS parity); then consider making CPS default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1186,3 +1186,88 @@ entries. Then re-regen `PSX_CPS=1`, boot-test, and run the §25 validation gate
|
||||
+ oracle-identical + 15-min soak + dwarf→overworld). Stages 2 (interp `interp_enter_compiled`
|
||||
surface) and 3 (sljit/overlay parity) follow once the C-emitter path boots+flattens.
|
||||
|
||||
### 25.2 BIOS-only CPS BOOTS — root cause was NOT the dispatch table (2026-06-18)
|
||||
|
||||
The §25.1 "continuations not in the dispatch table" hypothesis was **WRONG** — the
|
||||
continuation-wrapper machinery already adds every continuation to the BIOS dispatch table
|
||||
(`func_X_cont_Y` entries; 6256 in the legacy build). Verified by inspecting generated
|
||||
`SCPH1001_dispatch.c`. The real boot regression (frame-0 exit) had two distinct causes, found
|
||||
by **dumping the always-on fntrace ring on the abnormal `psx_dispatch` return** (added a tail
|
||||
dump to `main.cpp`; `psx_cps_exit_trace.json`). Tooling fix on the way: fntrace defaults to
|
||||
record-NOTHING now (`armed_match` returns 0 when nothing armed; the header comment is stale) —
|
||||
set `PSX_FNTRACE_ARM=all`.
|
||||
|
||||
1. **BIOS A0/B0/C0 vector runtime-fallback** (`emit_dispatch`): used nested
|
||||
`psx_dispatch_call(target, stop=ra)`. A nested dispatch runs its own trampoline with
|
||||
`stop_addr = the caller's return`; when the handler returns there it **zeroes `cpu->pc` and
|
||||
C-returns** to the flat main trampoline → the outermost loop sees `pc==0` and exits. In
|
||||
legacy this was safe because the caller's `jal gate` was itself a nested dispatch whose
|
||||
`psx_call_contract` resumed the continuation; under CPS the caller tail-transferred, so the
|
||||
B-call return must flow back to the SAME flat trampoline. **Fix:** under CPS the vector
|
||||
fallback **tail-transfers** (`cpu->pc = target; return;` — `$ra` already correct).
|
||||
2. **`psx_syscall` Enter/ExitCriticalSection** (`traps.c`): set `cpu->pc = 0` (the legacy
|
||||
"return via nested dispatch" idiom) → outermost flat trampoline exits. **Fix:** `psx_syscall`
|
||||
now returns `int` (0 = void syscall, 1 = transfer); under CPS the emitter (strict_translator)
|
||||
emits `if (psx_syscall(...)) return;` so a void syscall **falls through to the inline
|
||||
post-syscall code** (the guest's own `jr $ra`), matching real-HW resume at EPC+4. Keeps
|
||||
`cpu->pc==0` so legacy + the dirty interp (`return cpu->pc != 0`) are unchanged. Overlay ABI
|
||||
bumped 4→5 (callback signature change).
|
||||
|
||||
Result: **BIOS reaches the shell under CPS and runs (user-confirmed).** Also fixed: BIOS-only
|
||||
build was broken (`interp_enter_compiled` called `psx_dispatch_game_compiled` outside the
|
||||
`PSX_HAS_GAME_DISPATCH` guard) — gated the whole function.
|
||||
|
||||
### 25.3 GAME emitter is a SEPARATE codebase — CPS ported there too (2026-06-18, in progress)
|
||||
|
||||
KEY CORRECTION to §25.1's staging: the **game** (`SCUS_*`) is generated by
|
||||
`code_generator.cpp` (via `main_psx.cpp`), NOT `full_function_emitter.cpp`. The previous
|
||||
session's `PSX_CPS=1` regen left the game **fully legacy** (0 CPS markers, 3406
|
||||
`psx_call_contract`) — and the §8 leaking chain (`func_8001A954 …`) lives in GAME code, so the
|
||||
BIOS CPS work alone could not fix the leak. The game emitter uses a different model (one C
|
||||
function per guest fn, `block_X:` labels + `goto`, calls as DIRECT nested `func_target(cpu)`
|
||||
calls with inline `goto` continuations — that nesting IS the leak).
|
||||
|
||||
Ported CPS to `code_generator.cpp` (gated `PSX_CPS`):
|
||||
- jal/jalr/jr-$ra/jr-table/branch-split/j-split → **tail-transfer** (`cpu->pc = target; return;`
|
||||
with `$ra`/`$rd` set; jalr captures rs before the link, alias-safe). `psx_call_contract` /
|
||||
`call_by_address` no longer emitted (0 of each in CPS game).
|
||||
- Each call's return point registered as a dispatchable **continuation**; an **entry-switch**
|
||||
(keyed on `cpu->pc`) is emitted at the top of every function AND every overlapping-alias body
|
||||
(`psx_alias_body`, 1549 in Tomba — alias groups route a continuation by calling the first
|
||||
alias wrapper with `cpu->pc` set, which overrides the `entry` arg).
|
||||
- `psx_dispatch_game_compiled` (in `main_psx.cpp`) gains continuation cases
|
||||
(`case cont: cpu->pc = cont; func_owner(cpu);`) + entries clear `cpu->pc`. 3340 continuations.
|
||||
|
||||
Unified flat trampoline: the BIOS `psx_dispatch_impl` already routes ALL game text through
|
||||
`dirty_ram_dispatch → psx_dispatch_game_compiled`, so one trampoline drives BIOS + game flat.
|
||||
Under CPS the game funcs tail-transfer after one block, so the interp→`psx_dispatch_game_compiled`
|
||||
boundary is depth-1 (no accumulation) → Stage-1b alone should bound the host stack. Mixed
|
||||
CPS-BIOS + legacy-game gave "black screen after PS logo" (boundary contract mismatch) — expected;
|
||||
full CPS is the destination.
|
||||
|
||||
### 25.4 LEAK FIXED — full-CPS Tomba boots, plays, soak FLAT (2026-06-18) ✅
|
||||
|
||||
One more runtime fix was needed past first-boot: **`psx_syscall` case 3 (ChangeThread)**. The
|
||||
fiber thread-scheduler (`psx_change_thread_fiber`) leaves `cpu->pc == 0` on thread resume
|
||||
(lines 289/343); legacy resumed via `cpu->pc==0` + the nested-dispatch C-return, but under the
|
||||
flat CPS trampoline `pc==0` at the outermost level = exit. Tomba exited at **frame 712** on a
|
||||
ChangeThread for exactly this. Fix: case-3 ChangeThread-success returns **0** (CPS falls through
|
||||
to the wrapper's own `jr $ra` so the thread resumes at its caller via the flat trampoline);
|
||||
legacy ignores the return value.
|
||||
|
||||
**Result (user-validated):** full-CPS Tomba boots BIOS → FMV → New Game → village idle (the soak
|
||||
repro spot) and is playable. **The host-stack leak is ELIMINATED:** `ce_profile` `cur_max_kb`
|
||||
held **FLAT at 5 KB** from frame ~9.9k through **frame 56,890** (turbo soak, ~zero climb) —
|
||||
legacy climbed +1.17 KB/frame to 20,000+ KB → 48 MB → the ~14-min/~50k-frame freeze. We blew
|
||||
straight past the freeze threshold with the stack bounded. Branch `bug/recursion`, UNCOMMITTED.
|
||||
|
||||
NOTE: process RSS grows under `PSX_FNTRACE_ARM=all` (the 144 MB fntrace ring + turbo buffering);
|
||||
it decelerates and is unrelated to the (now-fixed) host-stack leak. Run gameplay WITHOUT
|
||||
`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.
|
||||
|
||||
|
||||
@@ -176,6 +176,32 @@ private:
|
||||
std::set<uint32_t> extra_labels_; // Mid-block addresses that need inline labels (jump table targets)
|
||||
const AnnotationTable* annotations_ = nullptr;
|
||||
|
||||
// RECURSION_BUG.md §25 — continuation-passing call/return (the universal fix
|
||||
// for the idle-freeze host-stack leak). When set (gen-time env PSX_CPS),
|
||||
// guest calls (jal/jalr/jr-table/external) are emitted as TAIL-TRANSFERS
|
||||
// (set $ra, cpu->pc = target, return) and each call's return point is
|
||||
// registered as a dispatchable continuation; jr $ra publishes cpu->pc.
|
||||
// The unified flat trampoline (psx_dispatch_impl) drives the whole
|
||||
// call/return via the guest's own $ra/$sp — so the host stack can never
|
||||
// mirror unbounded guest re-entrancy. The §8 leaking chain (func_8001A954
|
||||
// ...) lives in GAME code, so the game emitter MUST honor CPS too.
|
||||
bool cps_enabled_ = false;
|
||||
// Continuations (return points) collected during the CURRENT function's
|
||||
// block translation; consumed by generate_function to emit the entry-switch.
|
||||
std::vector<uint32_t> cps_cur_continuations_;
|
||||
// Global map: continuation address -> owning function entry, for the game
|
||||
// dispatch table (psx_dispatch_game_compiled) to route a returned-to
|
||||
// continuation into its function's entry-switch.
|
||||
std::map<uint32_t, uint32_t> cps_continuation_owner_;
|
||||
|
||||
public:
|
||||
bool cps_enabled() const { return cps_enabled_; }
|
||||
// continuation_addr -> owning function entry (CPS dispatch routing).
|
||||
const std::map<uint32_t, uint32_t>& cps_continuations() const {
|
||||
return cps_continuation_owner_;
|
||||
}
|
||||
private:
|
||||
|
||||
// Set per-function by generate_function when config_.ws_auto_screen_x_cull is
|
||||
// on AND the function carries the GTE screen-extent reject signature; read by
|
||||
// translate_instruction to widen that function's width compares. See
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "../../runtime/include/ws_backdrop_detect.h"
|
||||
#include <fmt/format.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
@@ -48,7 +49,11 @@ static uint32_t ram_to_rom(uint32_t addr, const PS1Executable& exe) {
|
||||
}
|
||||
|
||||
CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& config)
|
||||
: exe_(exe), config_(config) {}
|
||||
: exe_(exe), config_(config) {
|
||||
// RECURSION_BUG.md §25 — continuation-passing call/return. Gen-time opt-in
|
||||
// via PSX_CPS so legacy codegen stays byte-identical when unset.
|
||||
cps_enabled_ = (std::getenv("PSX_CPS") != nullptr);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::reg_name(int reg_num) {
|
||||
if (reg_num >= 0 && reg_num < 32) {
|
||||
@@ -1159,6 +1164,9 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (target_in) {
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("goto block_{:08X}; /* taken */\n", branch_target);
|
||||
} else if (cps_enabled_) {
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS taken: split */\n", branch_target);
|
||||
} else if (known_functions_.count(branch_target)) {
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* taken: split piece */\n", branch_target);
|
||||
@@ -1170,6 +1178,9 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (fallthru_in) {
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("goto block_{:08X}; /* not taken */\n", fall_through_addr);
|
||||
} else if (cps_enabled_) {
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS not taken: split */\n", fall_through_addr);
|
||||
} else if (known_functions_.count(fall_through_addr)) {
|
||||
ss << config_.indent << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* not taken: split piece */\n", fall_through_addr);
|
||||
@@ -1184,6 +1195,12 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (!block.successors.empty()) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("goto block_{:08X}; /* j */\n", block.successors[0]);
|
||||
} else if (cps_enabled_ && block.exit_instr.target != 0) {
|
||||
// CPS: out-of-function jump target — tail-transfer (the
|
||||
// flat trampoline dispatches the split piece / mid-func).
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS j: split */\n",
|
||||
block.exit_instr.target);
|
||||
} else if (block.exit_instr.target != 0 && known_functions_.count(block.exit_instr.target)) {
|
||||
// Jump target is out-of-function and is a known function start
|
||||
ss << config_.indent
|
||||
@@ -1230,6 +1247,11 @@ std::string CodeGenerator::translate_basic_block(
|
||||
ss << config_.indent
|
||||
<< "cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return;"
|
||||
<< " /* jr $ra — longjmp-return (ra loaded from non-sp) */\n";
|
||||
} else if (cps_enabled_) {
|
||||
// CPS: publish $ra so the flat trampoline dispatches the
|
||||
// caller's continuation (no host C-return to nest).
|
||||
ss << config_.indent
|
||||
<< "cpu->pc = cpu->gpr[31]; return; /* CPS: jr $ra */\n";
|
||||
} else {
|
||||
ss << config_.indent << "return; /* jr $ra */\n";
|
||||
}
|
||||
@@ -1353,24 +1375,52 @@ std::string CodeGenerator::translate_basic_block(
|
||||
for (auto& [rt, rom] : targets) {
|
||||
ss << config_.indent << fmt::format(" case 0x{:08X}u: goto block_{:08X};\n", rt, rom);
|
||||
}
|
||||
ss << config_.indent << " default: call_by_address(cpu, " << reg_name(jr_rs) << "); return;\n";
|
||||
if (cps_enabled_) {
|
||||
ss << config_.indent << " default: cpu->pc = " << reg_name(jr_rs)
|
||||
<< "; return; /* CPS: jr table miss — tail-transfer */\n";
|
||||
} else {
|
||||
ss << config_.indent << " default: call_by_address(cpu, " << reg_name(jr_rs) << "); return;\n";
|
||||
}
|
||||
ss << config_.indent << "}\n";
|
||||
emitted_switch = true;
|
||||
}
|
||||
}
|
||||
if (!emitted_switch) {
|
||||
// BIOS call or unrecognised indirect jump
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jr {} */\n",
|
||||
reg_name(jr_rs), reg_name(jr_rs));
|
||||
ss << config_.indent << "return;\n";
|
||||
if (cps_enabled_) {
|
||||
// CPS: indirect jump / BIOS-call gate — tail-transfer
|
||||
// to the target (the flat trampoline dispatches it).
|
||||
ss << config_.indent << fmt::format("cpu->pc = {}; return; /* CPS: jr {} */\n",
|
||||
reg_name(jr_rs), reg_name(jr_rs));
|
||||
} else {
|
||||
// BIOS call or unrecognised indirect jump
|
||||
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jr {} */\n",
|
||||
reg_name(jr_rs), reg_name(jr_rs));
|
||||
ss << config_.indent << "return;\n";
|
||||
}
|
||||
}
|
||||
} else if (block.exit_instr.type == ControlFlowType::JumpLink) {
|
||||
uint32_t target = block.exit_instr.target;
|
||||
uint32_t cont_addr = block.exit_instr.address + 8;
|
||||
if (cps_enabled_) {
|
||||
// CPS: tail-transfer to the callee. Set $ra to the
|
||||
// return point and register it as a dispatchable
|
||||
// continuation; the callee's jr $ra publishes cpu->pc =
|
||||
// $ra and the flat trampoline dispatches back into this
|
||||
// function's entry-switch. No host nesting.
|
||||
if (!block.successors.empty()) {
|
||||
cps_cur_continuations_.push_back(cont_addr);
|
||||
}
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->gpr[31] = 0x{:08X}u; /* CPS jal return addr */\n", cont_addr);
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->pc = 0x{:08X}u; return; /* CPS jal -> 0x{:08X} */\n",
|
||||
target, target);
|
||||
} else {
|
||||
// Function call (jal). The call contract (Bug D family)
|
||||
// guards the continuation: it may only run if the guest
|
||||
// actually returned here with the caller's $sp.
|
||||
ss << config_.indent << "{ uint32_t _csp = cpu->gpr[29];\n";
|
||||
ss << config_.indent << "cpu->gpr[31] = " << fmt::format("0x{:08X}; /* return address */\n", addr + 8);
|
||||
uint32_t target = block.exit_instr.target;
|
||||
if (known_functions_.count(target) > 0) {
|
||||
ss << config_.indent << fmt::format("func_{:08X}(cpu); /* jal */\n", target);
|
||||
ss << config_.indent << fmt::format("if (psx_call_contract(cpu, 0x{:08X}u, _csp)) return; }}\n", addr + 8);
|
||||
@@ -1386,7 +1436,6 @@ std::string CodeGenerator::translate_basic_block(
|
||||
} else {
|
||||
// Split-function: JAL continuation is outside this function piece.
|
||||
// Tail-call to the continuation piece (at exit_addr + 8, past delay slot).
|
||||
uint32_t cont_addr = block.exit_instr.address + 8;
|
||||
if (known_functions_.count(cont_addr)) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* jal cont: split piece */\n", cont_addr);
|
||||
@@ -1395,11 +1444,28 @@ std::string CodeGenerator::translate_basic_block(
|
||||
<< fmt::format("call_by_address(cpu, 0x{:08X}u); return; /* jal cont: split */\n", cont_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (block.exit_instr.type == ControlFlowType::JumpLinkReg) {
|
||||
// Register indirect call (jalr $rs, $rd) — call function at rs, return to rd
|
||||
uint32_t rs = get_rs(block.exit_instr.instruction);
|
||||
uint32_t rd = get_rd(block.exit_instr.instruction);
|
||||
uint32_t cont_addr = block.exit_instr.address + 8;
|
||||
|
||||
if (cps_enabled_) {
|
||||
// CPS: capture the target reg BEFORE writing the link
|
||||
// (rd==rs alias-safe), set $rd to the return point and
|
||||
// register it as a dispatchable continuation, then
|
||||
// tail-transfer. The flat trampoline drives the rest.
|
||||
if (!block.successors.empty()) {
|
||||
cps_cur_continuations_.push_back(cont_addr);
|
||||
}
|
||||
ss << config_.indent << fmt::format("{{ uint32_t _t = {};\n", reg_name(rs));
|
||||
if (rd != 0) {
|
||||
ss << config_.indent << fmt::format(" {} = 0x{:08X}; /* CPS jalr return addr */\n",
|
||||
reg_name(rd), cont_addr);
|
||||
}
|
||||
ss << config_.indent << "cpu->pc = _t; return; } /* CPS jalr */\n";
|
||||
} else {
|
||||
// Set link register to return address (PC + 8, past delay slot)
|
||||
ss << config_.indent << fmt::format("{} = 0x{:08X}; /* jalr return addr */\n",
|
||||
reg_name(rd), addr + 8);
|
||||
@@ -1418,7 +1484,6 @@ std::string CodeGenerator::translate_basic_block(
|
||||
block.successors[0]);
|
||||
} else {
|
||||
// Split-function: JALR continuation is outside this function piece.
|
||||
uint32_t cont_addr = block.exit_instr.address + 8;
|
||||
if (known_functions_.count(cont_addr)) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("func_{:08X}(cpu); return; /* jalr cont: split piece */\n", cont_addr);
|
||||
@@ -1427,6 +1492,7 @@ std::string CodeGenerator::translate_basic_block(
|
||||
<< fmt::format("call_by_address(cpu, 0x{:08X}u); return; /* jalr cont: split */\n", cont_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1526,8 +1592,50 @@ GeneratedFunction CodeGenerator::generate_function(
|
||||
bd_windows, func.name, func.start_addr);
|
||||
}
|
||||
|
||||
// Pre-scan: find jump table targets that land mid-block (not a block boundary).
|
||||
// These need inline labels so goto statements in the switch can reach them.
|
||||
// Must run BEFORE translating blocks (translate_basic_block reads extra_labels_).
|
||||
extra_labels_.clear();
|
||||
std::map<uint32_t, std::vector<uint32_t>> jr_table_edges;
|
||||
scan_jr_tables(cfg, jr_table_edges);
|
||||
|
||||
// CPS (§25): collect this function's continuations (call return points) as
|
||||
// blocks are translated, so the entry-switch below can route a dispatched
|
||||
// continuation address into the right block.
|
||||
cps_cur_continuations_.clear();
|
||||
|
||||
// Translate blocks into a temp buffer first: the CPS entry-switch is emitted
|
||||
// at the top (before the entry hooks) and must know every continuation.
|
||||
std::stringstream blocks_ss;
|
||||
for (uint32_t block_addr : cfg.block_order) {
|
||||
const BasicBlock& block = cfg.blocks.at(block_addr);
|
||||
blocks_ss << "\n" << translate_basic_block(block, cfg);
|
||||
}
|
||||
|
||||
std::stringstream body_ss;
|
||||
body_ss << "{\n";
|
||||
|
||||
// CPS entry-switch: when the unified flat trampoline dispatches a
|
||||
// continuation address (a callee published cpu->pc = $ra back to us), route
|
||||
// into the owning block. Emitted BEFORE the entry hooks so a continuation
|
||||
// re-entry bypasses debug_server_log_call_entry / widescreen entry hooks
|
||||
// (those must run only on a true function entry, cpu->pc == 0).
|
||||
if (cps_enabled_ && !cps_cur_continuations_.empty()) {
|
||||
std::set<uint32_t> seen;
|
||||
body_ss << config_.indent << "if (cpu->pc != 0u) {\n";
|
||||
body_ss << config_.indent << " uint32_t _cont = cpu->pc; cpu->pc = 0;\n";
|
||||
body_ss << config_.indent << " switch (_cont) {\n";
|
||||
for (uint32_t c : cps_cur_continuations_) {
|
||||
if (!seen.insert(c).second) continue;
|
||||
body_ss << config_.indent
|
||||
<< fmt::format(" case 0x{:08X}u: goto block_{:08X};\n", c, c);
|
||||
cps_continuation_owner_[c] = func.start_addr;
|
||||
}
|
||||
body_ss << config_.indent << " default: break;\n";
|
||||
body_ss << config_.indent << " }\n";
|
||||
body_ss << config_.indent << "}\n";
|
||||
}
|
||||
|
||||
body_ss << config_.indent
|
||||
<< fmt::format("debug_server_log_call_entry(0x{:08X}u);\n",
|
||||
func.start_addr);
|
||||
@@ -1547,17 +1655,7 @@ GeneratedFunction CodeGenerator::generate_function(
|
||||
func.start_addr, func.size, cfg.blocks.size());
|
||||
}
|
||||
|
||||
// Pre-scan: find jump table targets that land mid-block (not a block boundary).
|
||||
// These need inline labels so goto statements in the switch can reach them.
|
||||
extra_labels_.clear();
|
||||
std::map<uint32_t, std::vector<uint32_t>> jr_table_edges;
|
||||
scan_jr_tables(cfg, jr_table_edges);
|
||||
|
||||
// Generate code for each basic block
|
||||
for (uint32_t block_addr : cfg.block_order) {
|
||||
const BasicBlock& block = cfg.blocks.at(block_addr);
|
||||
body_ss << "\n" << translate_basic_block(block, cfg);
|
||||
}
|
||||
body_ss << blocks_ss.str();
|
||||
|
||||
// Emit fallthrough call if the function ends without explicit control flow.
|
||||
// A MIPS function truly falls through when:
|
||||
@@ -1688,6 +1786,11 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
|
||||
std::map<uint32_t, std::vector<uint32_t>> jr_table_edges;
|
||||
scan_jr_tables(cfg, jr_table_edges);
|
||||
|
||||
// CPS (§25): collect this alias group's continuations during block
|
||||
// translation (also prevents a stale list from leaking into the next
|
||||
// generate_function call).
|
||||
cps_cur_continuations_.clear();
|
||||
|
||||
// Union of blocks reachable from any alias entry. Edges are
|
||||
// BasicBlock::successors plus jump-table targets (mapped to their
|
||||
// containing block — table targets may be mid-block labels).
|
||||
@@ -1721,12 +1824,41 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
|
||||
}
|
||||
}
|
||||
|
||||
// Translate live blocks first (populates cps_cur_continuations_ for the CPS
|
||||
// continuation switch below).
|
||||
std::stringstream blocks_buf;
|
||||
for (uint32_t block_addr : cfg.block_order) {
|
||||
if (!live_blocks.count(block_addr)) continue;
|
||||
const BasicBlock& block = cfg.blocks.at(block_addr);
|
||||
blocks_buf << "\n" << translate_basic_block(block, cfg);
|
||||
}
|
||||
|
||||
// Shared body: host-range blocks (live subset) behind an entry switch.
|
||||
std::stringstream body;
|
||||
body << fmt::format("/* Overlapping-alias body for host func_{:08X}: {} entries */\n",
|
||||
host, aliases.size());
|
||||
body << fmt::format("static void psx_alias_body_{:08X}(CPUState* cpu, uint32_t entry)\n{{\n",
|
||||
host);
|
||||
// CPS (§25): a dispatched continuation (callee published cpu->pc = $ra) is
|
||||
// routed into its block here, BEFORE the entry switch — cpu->pc overrides
|
||||
// the `entry` arg. The dispatch routes a continuation by calling the first
|
||||
// alias wrapper with cpu->pc set (entry is then ignored). Owner =
|
||||
// aliases[0]->start_addr.
|
||||
if (cps_enabled_ && !cps_cur_continuations_.empty()) {
|
||||
std::set<uint32_t> seen;
|
||||
body << config_.indent << "if (cpu->pc != 0u) {\n";
|
||||
body << config_.indent << " uint32_t _cont = cpu->pc; cpu->pc = 0;\n";
|
||||
body << config_.indent << " switch (_cont) {\n";
|
||||
for (uint32_t c : cps_cur_continuations_) {
|
||||
if (!seen.insert(c).second) continue;
|
||||
body << config_.indent
|
||||
<< fmt::format(" case 0x{:08X}u: goto block_{:08X};\n", c, c);
|
||||
cps_continuation_owner_[c] = aliases[0]->start_addr;
|
||||
}
|
||||
body << config_.indent << " default: break;\n";
|
||||
body << config_.indent << " }\n";
|
||||
body << config_.indent << "}\n";
|
||||
}
|
||||
body << config_.indent << "switch (entry) {\n";
|
||||
for (const Function* a : aliases) {
|
||||
body << config_.indent
|
||||
@@ -1736,11 +1868,7 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
|
||||
body << config_.indent << " default: return;\n";
|
||||
body << config_.indent << "}\n";
|
||||
|
||||
for (uint32_t block_addr : cfg.block_order) {
|
||||
if (!live_blocks.count(block_addr)) continue;
|
||||
const BasicBlock& block = cfg.blocks.at(block_addr);
|
||||
body << "\n" << translate_basic_block(block, cfg);
|
||||
}
|
||||
body << blocks_buf.str();
|
||||
|
||||
// Host-end fallthrough (mirrors generate_function): only relevant when the
|
||||
// host's final block is live and ends without explicit control flow.
|
||||
@@ -1787,6 +1915,11 @@ std::vector<GeneratedFunction> CodeGenerator::generate_all_functions(
|
||||
std::vector<GeneratedFunction> results;
|
||||
results.reserve(functions.size());
|
||||
|
||||
// CPS (§25): rebuild the continuation->owner map for this full pass so the
|
||||
// game dispatch table (psx_dispatch_game_compiled) reflects exactly the
|
||||
// functions emitted here (the final generate_file pass is authoritative).
|
||||
cps_continuation_owner_.clear();
|
||||
|
||||
fmt::print("\n=== C Code Generation ===\n\n");
|
||||
fmt::print("Generating C code for {} functions...\n", functions.size());
|
||||
|
||||
|
||||
@@ -1006,6 +1006,18 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
const std::vector<BiosVectorTable>& bios_vectors,
|
||||
const std::vector<BiosAlias>& bios_aliases)
|
||||
{
|
||||
// RECURSION_BUG.md §25 — continuation-passing. Under CPS the BIOS A0/B0/C0
|
||||
// vector dispatch must TAIL-TRANSFER to the handler (set cpu->pc, return)
|
||||
// rather than nest psx_dispatch_call. A nested psx_dispatch_call runs its
|
||||
// own trampoline with stop_addr = the caller's return; when the handler
|
||||
// returns there it zeroes cpu->pc and C-returns to the (flat) main
|
||||
// trampoline, which then sees pc==0 and exits at boot. In legacy this was
|
||||
// safe because the caller's `jal gate` was itself a nested dispatch whose
|
||||
// psx_call_contract resumed the continuation; in CPS the caller
|
||||
// tail-transferred, so the return must flow back to the SAME flat
|
||||
// trampoline — which only happens if the vector handler tail-transfers too.
|
||||
static const bool cps = (std::getenv("PSX_CPS") != nullptr);
|
||||
|
||||
out += "/* AUTO-GENERATED by psxrecomp-bios --emit-full. DO NOT EDIT.\n";
|
||||
out += " *\n";
|
||||
out += fmt::format(" * BIOS SHA256: {}\n", bios_sha256);
|
||||
@@ -1114,13 +1126,28 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
// Runtime fallback: for Shell-patched entries not in the ROM table,
|
||||
// read the live function pointer from RAM and dispatch.
|
||||
if (bvt.table_ram_addr != 0u) {
|
||||
out += fmt::format(
|
||||
" /* Runtime fallback for Shell-patched / out-of-range entries */\n"
|
||||
" {{\n"
|
||||
" uint32_t target = cpu->read_word(0x{:08X}u + (uint32_t)cpu->gpr[{}] * 4u);\n"
|
||||
" if (target) psx_dispatch_call(cpu, target, cpu->gpr[31]);\n"
|
||||
" }}\n",
|
||||
bvt.table_ram_addr, bvt.index_reg);
|
||||
if (cps) {
|
||||
// CPS: tail-transfer to the handler. $ra already holds the
|
||||
// caller's return (set by the guest's jal to the A0/B0/C0
|
||||
// gate, preserved through the gate's jr and this vector).
|
||||
// The flat trampoline dispatches the handler; its jr $ra
|
||||
// then continues at the caller's return — no nesting.
|
||||
out += fmt::format(
|
||||
" /* Runtime fallback (CPS tail-transfer) */\n"
|
||||
" {{\n"
|
||||
" uint32_t target = cpu->read_word(0x{:08X}u + (uint32_t)cpu->gpr[{}] * 4u);\n"
|
||||
" if (target) {{ cpu->pc = target; return; }}\n"
|
||||
" }}\n",
|
||||
bvt.table_ram_addr, bvt.index_reg);
|
||||
} else {
|
||||
out += fmt::format(
|
||||
" /* Runtime fallback for Shell-patched / out-of-range entries */\n"
|
||||
" {{\n"
|
||||
" uint32_t target = cpu->read_word(0x{:08X}u + (uint32_t)cpu->gpr[{}] * 4u);\n"
|
||||
" if (target) psx_dispatch_call(cpu, target, cpu->gpr[31]);\n"
|
||||
" }}\n",
|
||||
bvt.table_ram_addr, bvt.index_reg);
|
||||
}
|
||||
}
|
||||
out += "}\n\n";
|
||||
vec_handlers.push_back({bvt.ram_addr, hname});
|
||||
|
||||
@@ -812,9 +812,27 @@ int main(int argc, char** argv) {
|
||||
ds << "/* Maps PS1 address to compiled game code. Returns 1 if dispatched, 0 if unknown. */\n";
|
||||
ds << "int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr) {\n";
|
||||
ds << " switch (addr) {\n";
|
||||
for (uint32_t addr : dispatch_addrs) {
|
||||
ds << fmt::format(" case 0x{:08X}u: func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr);
|
||||
if (codegen.cps_enabled()) {
|
||||
// CPS (§25): entries clear cpu->pc so the function's entry-switch is
|
||||
// skipped (runs from the top); continuations set cpu->pc so the
|
||||
// entry-switch routes into the owning block. The unified flat
|
||||
// trampoline (psx_dispatch_impl) then drives every tail-transfer.
|
||||
for (uint32_t addr : dispatch_addrs) {
|
||||
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0; func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr);
|
||||
}
|
||||
const auto& conts = codegen.cps_continuations();
|
||||
ds << fmt::format(" /* {} continuation return-points (CPS) */\n", conts.size());
|
||||
for (const auto& [cont, owner] : conts) {
|
||||
if (dispatch_addrs.count(cont)) continue; /* already a function entry */
|
||||
ds << fmt::format(" case 0x{:08X}u: cpu->pc = 0x{:08X}u; func_{:08X}(cpu); return 1;\n",
|
||||
cont, cont, owner);
|
||||
}
|
||||
} else {
|
||||
for (uint32_t addr : dispatch_addrs) {
|
||||
ds << fmt::format(" case 0x{:08X}u: func_{:08X}(cpu); return 1;\n",
|
||||
addr, addr);
|
||||
}
|
||||
}
|
||||
ds << " default: return 0;\n";
|
||||
ds << " }\n";
|
||||
|
||||
@@ -9,12 +9,22 @@
|
||||
#include "strict_translator.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
namespace PSXRecompV4 {
|
||||
|
||||
// RECURSION_BUG.md §25 — continuation-passing call/return. Under CPS, a
|
||||
// directly-handled "void" syscall (Enter/ExitCriticalSection) must fall
|
||||
// through to the inline post-syscall code (the guest's own jr $ra) instead of
|
||||
// returning to a nested dispatcher, so the syscall is emitted as
|
||||
// `if (psx_syscall(...)) return;`. psx_syscall returns 0 for those void
|
||||
// syscalls and 1 when control transfers (cpu->pc set). Gated so legacy codegen
|
||||
// stays byte-identical.
|
||||
static const bool g_strict_cps = (std::getenv("PSX_CPS") != nullptr);
|
||||
|
||||
namespace {
|
||||
|
||||
// Emit a register-write that respects $zero being hardwired to 0.
|
||||
@@ -202,9 +212,19 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
|
||||
|
||||
case 0x0C: { // SYSCALL
|
||||
r.supported = true;
|
||||
r.c_code = fmt::format(
|
||||
"cpu->pc = 0x{:08X}u; psx_syscall(cpu, cpu->gpr[2]); return;",
|
||||
d.address);
|
||||
if (g_strict_cps) {
|
||||
// CPS: void syscalls (psx_syscall returns 0) fall through to
|
||||
// the inline post-syscall code (the guest's jr $ra), which
|
||||
// returns to the caller via the flat trampoline. Transfers
|
||||
// (return 1) set cpu->pc and return to the trampoline.
|
||||
r.c_code = fmt::format(
|
||||
"cpu->pc = 0x{:08X}u; if (psx_syscall(cpu, cpu->gpr[2])) return;",
|
||||
d.address);
|
||||
} else {
|
||||
r.c_code = fmt::format(
|
||||
"cpu->pc = 0x{:08X}u; psx_syscall(cpu, cpu->gpr[2]); return;",
|
||||
d.address);
|
||||
}
|
||||
r.comment = "syscall";
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,13 @@ typedef struct CPUState {
|
||||
} CPUState;
|
||||
|
||||
/* Trap trampolines — defined in runtime/src/traps.c */
|
||||
extern void psx_syscall(CPUState* cpu, uint32_t code);
|
||||
/* 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);
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
* window START/END finalize, and the callback struct grows a
|
||||
* ws_backdrop_value pointer (appended last). Bumping rejects pre-preload
|
||||
* DLLs (which lack both the emit and a host that supplies the callback). */
|
||||
#define PSX_OVERLAY_ABI_VERSION 4
|
||||
/* v5: psx_syscall callback return type void->int (CPS, RECURSION_BUG.md §25);
|
||||
* overlays compiled under CPS emit `if (psx_syscall(...)) return;`. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 5
|
||||
|
||||
/* Codegen flavor of the recompiled output the overlays + runtime were built
|
||||
* against. Overlays are keyed in the cache by guest-bytes CRC, which is
|
||||
@@ -76,8 +78,11 @@ typedef struct {
|
||||
void (*check_interrupts)(CPUState *cpu);
|
||||
/* GTE coprocessor 2 execution */
|
||||
void (*gte_execute)(CPUState *cpu, uint32_t cmd);
|
||||
/* MIPS syscall (break/syscall instructions) */
|
||||
void (*psx_syscall)(CPUState *cpu, uint32_t code);
|
||||
/* MIPS syscall (break/syscall instructions). Returns 1 if control
|
||||
* transfers (cpu->pc set), 0 for a directly-handled void syscall. See
|
||||
* cpu_state.h. The signature changed with CPS (RECURSION_BUG.md §25), so
|
||||
* PSX_OVERLAY_ABI_VERSION was bumped to reject stale overlay caches. */
|
||||
int (*psx_syscall)(CPUState *cpu, uint32_t code);
|
||||
/* Unresolved dispatch target */
|
||||
void (*psx_unknown_dispatch)(CPUState *cpu, uint32_t addr, uint32_t phys);
|
||||
/* Debug instrumentation: called at every function entry (may be NULL) */
|
||||
|
||||
@@ -721,7 +721,12 @@ int dirty_ram_xprobe_json(char *out, int cap) {
|
||||
/* Enter compiled code from the interpreter for a guest control transfer. Returns
|
||||
* like psx_dispatch_game_compiled (1 = handled, OR a stack-watermark bail was
|
||||
* surfaced; 0 = not a compiled target — caller falls through to overlay/dirty/
|
||||
* nonlocal paths). On surface: cpu->pc = target and g_psx_call_bail set. */
|
||||
* nonlocal paths). On surface: cpu->pc = target and g_psx_call_bail set.
|
||||
*
|
||||
* Only present when a game dispatch table is linked (PSX_HAS_GAME_DISPATCH).
|
||||
* In the BIOS-only build psx_dispatch_game_compiled does not exist, and the
|
||||
* two callers below are themselves gated behind the same macro. */
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
|
||||
if (target == 0x8001A954u) site_note(&g_site_interp);
|
||||
if (psx_mixed_owner_enabled()
|
||||
@@ -735,6 +740,7 @@ static int interp_enter_compiled(CPUState *cpu, uint32_t target) {
|
||||
g_mixed_depth--;
|
||||
return r;
|
||||
}
|
||||
#endif /* PSX_HAS_GAME_DISPATCH */
|
||||
|
||||
/* Execute ONE instruction at *pc on the given CPU state. Returns:
|
||||
* 0 = continue (advance pc by 4)
|
||||
|
||||
+39
-1
@@ -2410,7 +2410,45 @@ int main(int argc, char** argv) {
|
||||
psx_dispatch(&cpu, cpu.pc);
|
||||
#endif
|
||||
|
||||
/* If we reach here, all execution completed without MMIO abort. */
|
||||
/* If we reach here, all execution completed without MMIO abort.
|
||||
* During normal operation the guest runs an infinite main loop and the
|
||||
* top-level psx_dispatch NEVER returns. Reaching this point means the
|
||||
* outermost trampoline loop saw cpu->pc == 0 (some jr/tail-transfer
|
||||
* published a null PC) — an abnormal boot exit. Dump the always-on
|
||||
* fntrace ring tail (last dispatch chain) to a JSON artifact so we can
|
||||
* see exactly which targets led to the null PC. (CLAUDE.md ring-buffer
|
||||
* model: consume the always-on ring after the fact, not arm-and-time.) */
|
||||
{
|
||||
FILE* tf = std::fopen("psx_cps_exit_trace.json", "wb");
|
||||
if (tf) {
|
||||
std::fprintf(tf, "{\n \"final_pc\": \"0x%08X\",\n \"final_ra\": \"0x%08X\",\n"
|
||||
" \"final_sp\": \"0x%08X\",\n \"fntrace_seq\": %llu,\n \"tail\": [\n",
|
||||
cpu.pc, cpu.gpr[31], cpu.gpr[29],
|
||||
(unsigned long long)g_fntrace_seq);
|
||||
uint64_t seq = g_fntrace_seq;
|
||||
uint32_t n = seq < 128u ? (uint32_t)seq : 128u;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
uint64_t idx = seq - n + i;
|
||||
const FntraceEntry* e = &g_fntrace_ring[idx % FNTRACE_RING_CAP];
|
||||
std::fprintf(tf,
|
||||
" {\"seq\":%llu,\"frame\":%u,\"target\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"sp\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\"}%s\n",
|
||||
(unsigned long long)idx, e->frame, e->target, e->ra, e->sp,
|
||||
e->a0, e->a1, (i + 1 < n) ? "," : "");
|
||||
}
|
||||
std::fprintf(tf, " ],\n \"stack\": [\n");
|
||||
uint32_t sbase = (cpu.gpr[29] - 0x40u) & ~3u;
|
||||
for (int w = 0; w < 40; w++) {
|
||||
uint32_t a = sbase + (uint32_t)w * 4u;
|
||||
uint32_t v = cpu.read_word(a);
|
||||
std::fprintf(tf, " {\"addr\":\"0x%08X\",\"val\":\"0x%08X\"}%s\n",
|
||||
a, v, (w < 39) ? "," : "");
|
||||
}
|
||||
std::fprintf(tf, " ]\n}\n");
|
||||
std::fclose(tf);
|
||||
}
|
||||
}
|
||||
|
||||
std::fprintf(stdout, "psxrecomp runtime: execution completed, PC=0x%08X\n", cpu.pc);
|
||||
|
||||
shutdown_runtime();
|
||||
|
||||
@@ -666,7 +666,7 @@ int overlay_loader_has_cached_crc(uint32_t region_start, uint32_t crc) {
|
||||
extern void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra);
|
||||
extern void psx_check_interrupts(CPUState *cpu);
|
||||
extern void gte_execute(CPUState *cpu, uint32_t cmd);
|
||||
extern void psx_syscall(CPUState *cpu, uint32_t code);
|
||||
extern int psx_syscall(CPUState *cpu, uint32_t code);
|
||||
extern void psx_unknown_dispatch(CPUState *cpu, uint32_t addr, uint32_t phys);
|
||||
extern void debug_server_log_call_entry(uint32_t func_addr);
|
||||
|
||||
|
||||
+20
-8
@@ -354,7 +354,7 @@ static int psx_change_thread(CPUState* cpu, uint32_t target_tcb)
|
||||
return psx_change_thread_fiber(cpu, target_tcb);
|
||||
}
|
||||
|
||||
void psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
int psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
/*
|
||||
* PS1 BIOS SYSCALL convention:
|
||||
* $a0 = 1: EnterCriticalSection — disable interrupts, return old SR
|
||||
@@ -377,18 +377,29 @@ void psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
cpu->cop0[12] = sr & ~1u; /* clear IEc (bit 0) */
|
||||
cpu->gpr[2] = sr & 1u; /* return old IEc */
|
||||
cpu->pc = 0;
|
||||
return;
|
||||
/* Directly handled "void" syscall. Return 0: under CPS the caller
|
||||
* (`if (psx_syscall(...)) return;`) falls through to the inline
|
||||
* post-syscall code (the guest's own jr $ra), which returns to the
|
||||
* caller via the flat trampoline — matching real-HW resume at
|
||||
* EPC+4. Legacy callers ignore this and rely on cpu->pc==0. */
|
||||
return 0;
|
||||
|
||||
case 2: /* ExitCriticalSection: enable interrupts */
|
||||
cpu->cop0[12] = sr | 0x0401u; /* set IEc (bit 0) + IM[2] (bit 10) */
|
||||
cpu->gpr[2] = 0;
|
||||
cpu->pc = 0;
|
||||
return;
|
||||
return 0;
|
||||
|
||||
case 3: { /* ChangeThread / ReturnFromException */
|
||||
uint32_t target_tcb = cpu->gpr[5];
|
||||
if (!psx_get_in_exception() && psx_change_thread(cpu, target_tcb)) {
|
||||
return;
|
||||
/* Thread switched (and this thread has since been resumed): the
|
||||
* fiber scheduler leaves cpu->pc == 0. Under CPS that must NOT
|
||||
* be a transfer — return 0 so the syscall wrapper falls through
|
||||
* to its own jr $ra and this thread resumes at its caller via
|
||||
* the flat trampoline. Legacy ignores the return value (it uses
|
||||
* cpu->pc == 0 + the nested-dispatch C-return to resume). */
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t tcb_ptr_addr = cpu->read_word(0x00000108u);
|
||||
@@ -412,13 +423,13 @@ void psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
if (psx_get_in_exception()) {
|
||||
psx_exception_longjmp(); /* unwind handler */
|
||||
}
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
/* Fallback: simple RFE on current SR. */
|
||||
cpu->cop0[12] = (sr & ~0x0Fu) | ((sr >> 2) & 0x0Fu);
|
||||
cpu->pc = cpu->cop0[14];
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -434,14 +445,14 @@ void psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
cpu->cop0[12] = (sr & ~0x3Fu) | ((sr & 0x0Fu) << 2); /* SR push */
|
||||
uint32_t vector = (sr & 0x00400000u) ? 0xBFC00180u : 0x80000080u;
|
||||
psx_dispatch(cpu, vector);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Early boot fallback for syscall 3. */
|
||||
if (func == 3) {
|
||||
cpu->cop0[12] = (sr & ~0x0Fu) | ((sr >> 2) & 0x0Fu);
|
||||
cpu->pc = cpu->cop0[14];
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Unknown syscall number and no handler — fatal. */
|
||||
@@ -453,6 +464,7 @@ void psx_syscall(CPUState* cpu, uint32_t code) {
|
||||
fprintf(stderr, "%s\n", buf); fflush(stderr);
|
||||
exit(1);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void psx_break(CPUState* cpu, uint32_t code, uint32_t pc) {
|
||||
|
||||
Reference in New Issue
Block a user