mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56: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:
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user