Files
psxrecomp/recompiler/src/code_generator.cpp
T
Matthew Stanley b00b81f5e9 feat(timing): single-source cycle-cost seam (psx_instr_base_cycles), identity
P3 step 1 (FAITHFUL_TIMING_PLAN.md): both backends now account guest CPU cycles
through ONE function, runtime/include/psx_instr_cost.h::psx_instr_base_cycles(insn),
so they can never disagree (the -8 drift class). Interp routes its 3 per-insn
charges (exec_delay_slot, dirty dispatch loop, precise slice) through it; the
recompiler sums it over each block (+ the outside delay-slot clone) to fold into
the compile-time block charge. Stage 1 = identity (1 cycle/insn) = the proven
baseline, so the seam is BEHAVIOR-PRESERVING: regen is byte-identical (verified,
full.c + dispatch.c) and Tomba 2 still reaches the intro FMV (i_stat 0x8D
streaming). Stage 2 edits ONLY psx_instr_base_cycles (transcribe real R3000A
costs from in-tree Beetle cpu.cpp/gte.cpp + psx-spx, verify each vs Beetle) and
both backends update together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 19:20:20 -07:00

2642 lines
126 KiB
C++

#include "code_generator.h"
#include "control_flow.h"
// Shared widescreen backdrop-window detector (single source of truth across the
// recompiler, the interpreter, and the sljit emitter). Self-contained C header;
// included via relative path to avoid an include-dir collision (recompiler and
// runtime both ship a gte.h).
#include "../../runtime/include/ws_backdrop_detect.h"
#include "../../runtime/include/psx_instr_cost.h" /* single-source CPU cycle cost (shared with interp) */
#include <fmt/format.h>
#include <algorithm>
#include <cstdlib>
#include <set>
#include <vector>
namespace PSXRecomp {
static bool codegen_cycle_per_insn() {
const char* e = std::getenv("PSX_CODEGEN_CYCLE_PER_INSN");
return e != nullptr && e[0] != '\0' && e[0] != '0';
}
/*
* Translate a runtime RAM address to an address that exe_.read_word() can
* resolve.
*
* Game EXEs are read directly at their load address. The BIOS shell code
* lives at ROM 0xBFC18000-0xBFC427FF and gets copied to RAM at
* 0x80030000-0x8005A7FF during BIOS init. Jump tables within shell
* functions store RAM-space target addresses (0x80058xxx), but at recompile
* time we can only read the ROM. This helper maps:
*
* active EXE load range -> active EXE load range
* RAM phys 0x00030000-0x0005AFFF → ROM kseg1 0xBFC18000+
*
* Non-shell addresses pass through unchanged.
*/
static uint32_t ram_to_rom(uint32_t addr, const PS1Executable& exe) {
uint32_t phys = addr & 0x1FFFFFFFu;
/*
* Game EXEs can legitimately occupy physical 0x30000-0x5AFFF. That
* overlaps the BIOS shell copy window below, so prefer the active EXE's
* load range before applying any BIOS-specific remap.
*/
uint32_t exe_phys = exe.load_address() & 0x1FFFFFFFu;
uint32_t exe_size = exe.code_size();
if (exe_size != 0 && phys >= exe_phys && phys < exe_phys + exe_size) {
return exe.load_address() + (phys - exe_phys);
}
if (phys >= 0x00030000u && phys <= 0x0005AFFFu) {
return 0xBFC18000u + (phys - 0x00030000u);
}
return addr;
}
CodeGenerator::CodeGenerator(const PS1Executable& exe, const CodeGenConfig& 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 is the DEFAULT (RECURSION_BUG.md §25). Opt out (legacy) with PSX_CPS=0.
{ const char* e = std::getenv("PSX_CPS"); cps_enabled_ = (e == nullptr || e[0] != '0'); }
}
uint32_t CodeGenerator::partial_block_cycle_count(uint32_t addr,
const ControlFlowGraph& cfg) const {
if (cfg.blocks.count(addr)) {
return 0;
}
for (const auto& [block_addr, block] : cfg.blocks) {
(void)block_addr;
if (addr <= block.start_addr || addr > block.end_addr) {
continue;
}
if (((addr - block.start_addr) & 3u) != 0) {
continue;
}
return ((block.end_addr - addr) / 4u) + 1u;
}
return 0;
}
std::string CodeGenerator::emit_mid_block_cycle_charge(uint32_t addr,
const ControlFlowGraph& cfg,
const std::string& indent) const {
if (codegen_cycle_per_insn()) {
return "";
}
uint32_t cycles = partial_block_cycle_count(addr, cfg);
if (cycles == 0) {
return "";
}
std::stringstream ss;
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
ss << indent << fmt::format("psx_advance_cycles({}u);\n", cycles);
ss << "#endif\n";
return ss.str();
}
std::string CodeGenerator::emit_interrupt_check(uint32_t resume_pc,
const std::string& indent) const {
return indent + fmt::format("psx_check_interrupts_at(cpu, 0x{:08X}u);\n", resume_pc);
}
std::string CodeGenerator::emit_interrupt_check_expr(const std::string& resume_pc_expr,
const std::string& indent) const {
return indent + fmt::format("psx_check_interrupts_at(cpu, {});\n", resume_pc_expr);
}
std::string CodeGenerator::reg_name(int reg_num) {
if (reg_num >= 0 && reg_num < 32) {
return fmt::format("cpu->gpr[{}]", reg_num);
}
return fmt::format("cpu->gpr[{}]", reg_num);
}
std::string CodeGenerator::translate_addiu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t imm = get_imm16(instr);
// Optimize: loading immediate into register (from $zero)
if (rs == 0) {
return fmt::format("{} = {};", reg_name(rt), imm);
}
// Optimize: don't assign to $zero
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} + {};", reg_name(rt), reg_name(rs), imm);
}
std::string CodeGenerator::translate_lui(uint32_t instr) {
uint32_t rt = get_rt(instr);
uint16_t imm = get_imm16_u(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = 0x{:04X} << 16; /* 0x{:08X} */",
reg_name(rt), imm, ((uint32_t)imm) << 16);
}
std::string CodeGenerator::translate_lw(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
if (offset == 0) {
return fmt::format("{} = cpu->read_word({});", reg_name(rt), reg_name(rs));
} else {
return fmt::format("{} = cpu->read_word({} + {});",
reg_name(rt), reg_name(rs), offset);
}
}
std::string CodeGenerator::translate_sw(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (offset == 0) {
return fmt::format("cpu->write_word({}, {});", reg_name(rs), reg_name(rt));
} else {
return fmt::format("cpu->write_word({} + {}, {});",
reg_name(rs), offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_or(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
// Optimize: move instruction (or $rd, $rs, $zero)
if (rt == 0) {
return fmt::format("{} = {}; /* move */", reg_name(rd), reg_name(rs));
}
if (rs == 0) {
return fmt::format("{} = {}; /* move */", reg_name(rd), reg_name(rt));
}
return fmt::format("{} = {} | {};", reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_and(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} & {};", reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_sll(uint32_t instr) {
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
uint32_t shamt = get_shamt(instr);
// nop instruction (sll $zero, $zero, 0)
if (rd == 0 && rt == 0 && shamt == 0) {
return "/* nop */";
}
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
if (shamt == 0) {
return fmt::format("{} = {}; /* no shift */", reg_name(rd), reg_name(rt));
}
return fmt::format("{} = {} << {};", reg_name(rd), reg_name(rt), shamt);
}
std::string CodeGenerator::translate_srl(uint32_t instr) {
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
uint32_t shamt = get_shamt(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = (uint32_t){} >> {};", reg_name(rd), reg_name(rt), shamt);
}
std::string CodeGenerator::translate_slt(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = ((int32_t){} < (int32_t){}) ? 1 : 0;",
reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_sltu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = ({} < {}) ? 1 : 0;",
reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_addu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
// Optimize: move instruction (addu $rd, $rs, $zero or addu $rd, $zero, $rt)
if (rt == 0) {
return fmt::format("{} = {}; /* move */", reg_name(rd), reg_name(rs));
}
if (rs == 0) {
return fmt::format("{} = {}; /* move */", reg_name(rd), reg_name(rt));
}
return fmt::format("{} = {} + {};", reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_add(uint32_t instr) {
// add and addu are functionally identical in practice (trap on overflow not used)
return translate_addu(instr);
}
std::string CodeGenerator::translate_subu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} - {};", reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_sub(uint32_t instr) {
// sub and subu are functionally identical in practice (trap on overflow not used)
return translate_subu(instr);
}
std::string CodeGenerator::translate_lb(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
if (offset == 0) {
return fmt::format("{} = (int32_t)(int8_t)cpu->read_byte({});", reg_name(rt), reg_name(rs));
} else {
return fmt::format("{} = (int32_t)(int8_t)cpu->read_byte({} + {});",
reg_name(rt), reg_name(rs), offset);
}
}
std::string CodeGenerator::translate_lbu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
if (offset == 0) {
return fmt::format("{} = cpu->read_byte({});", reg_name(rt), reg_name(rs));
} else {
return fmt::format("{} = cpu->read_byte({} + {});",
reg_name(rt), reg_name(rs), offset);
}
}
std::string CodeGenerator::translate_lh(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
if (offset == 0) {
return fmt::format("{} = (int32_t)(int16_t)cpu->read_half({});", reg_name(rt), reg_name(rs));
} else {
return fmt::format("{} = (int32_t)(int16_t)cpu->read_half({} + {});",
reg_name(rt), reg_name(rs), offset);
}
}
std::string CodeGenerator::translate_lhu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
if (offset == 0) {
return fmt::format("{} = cpu->read_half({});", reg_name(rt), reg_name(rs));
} else {
return fmt::format("{} = cpu->read_half({} + {});",
reg_name(rt), reg_name(rs), offset);
}
}
std::string CodeGenerator::translate_lwl(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
// LWL: Load Word Left - loads bytes into the most-significant portion of rt
// addr = rs + offset; byte_pos = addr & 3
// Result merges high bytes from aligned word with low bytes from current rt value
// Generate a helper call: cpu->rt = psx_lwl(cpu, addr, cpu->rt)
if (offset == 0) {
return fmt::format("{} = psx_lwl(cpu, {}, {});", reg_name(rt), reg_name(rs), reg_name(rt));
} else {
return fmt::format("{} = psx_lwl(cpu, {} + {}, {});",
reg_name(rt), reg_name(rs), (int32_t)offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_lwr(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: load to $zero */";
}
// LWR: Load Word Right - loads bytes into the least-significant portion of rt
// addr = rs + offset; byte_pos = addr & 3
// Result merges low bytes from aligned word with high bytes from current rt value
// Generate a helper call: cpu->rt = psx_lwr(cpu, addr, cpu->rt)
if (offset == 0) {
return fmt::format("{} = psx_lwr(cpu, {}, {});", reg_name(rt), reg_name(rs), reg_name(rt));
} else {
return fmt::format("{} = psx_lwr(cpu, {} + {}, {});",
reg_name(rt), reg_name(rs), (int32_t)offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_swl(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
// SWL: Store Word Left - stores rt into memory using addr, MSB side
// Generate a helper call: psx_swl(cpu, addr, cpu->rt)
if (offset == 0) {
return fmt::format("psx_swl(cpu, {}, {});", reg_name(rs), reg_name(rt));
} else {
return fmt::format("psx_swl(cpu, {} + {}, {});",
reg_name(rs), (int32_t)offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_swr(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
// SWR: Store Word Right - stores rt into memory using addr, LSB side
// Generate a helper call: psx_swr(cpu, addr, cpu->rt)
if (offset == 0) {
return fmt::format("psx_swr(cpu, {}, {});", reg_name(rs), reg_name(rt));
} else {
return fmt::format("psx_swr(cpu, {} + {}, {});",
reg_name(rs), (int32_t)offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_sb(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (offset == 0) {
return fmt::format("cpu->write_byte({}, (uint8_t){});", reg_name(rs), reg_name(rt));
} else {
return fmt::format("cpu->write_byte({} + {}, (uint8_t){});",
reg_name(rs), offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_sh(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (offset == 0) {
return fmt::format("cpu->write_half({}, (uint16_t){});", reg_name(rs), reg_name(rt));
} else {
return fmt::format("cpu->write_half({} + {}, (uint16_t){});",
reg_name(rs), offset, reg_name(rt));
}
}
std::string CodeGenerator::translate_sra(uint32_t instr) {
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
uint32_t shamt = get_shamt(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = (int32_t){} >> {};", reg_name(rd), reg_name(rt), shamt);
}
std::string CodeGenerator::translate_andi(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint16_t imm = get_imm16_u(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} & 0x{:04X};", reg_name(rt), reg_name(rs), imm);
}
std::string CodeGenerator::translate_ori(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint16_t imm = get_imm16_u(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
// Optimize: loading immediate into register (from $zero)
if (rs == 0) {
return fmt::format("{} = 0x{:04X};", reg_name(rt), imm);
}
return fmt::format("{} = {} | 0x{:04X};", reg_name(rt), reg_name(rs), imm);
}
std::string CodeGenerator::translate_xori(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint16_t imm = get_imm16_u(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} ^ 0x{:04X};", reg_name(rt), reg_name(rs), imm);
}
std::string CodeGenerator::translate_xor(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} ^ {};", reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_nor(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
// Optimize: not instruction (nor $rd, $rs, $zero)
if (rt == 0) {
return fmt::format("{} = ~{}; /* not */", reg_name(rd), reg_name(rs));
}
if (rs == 0) {
return fmt::format("{} = ~{}; /* not */", reg_name(rd), reg_name(rt));
}
return fmt::format("{} = ~({} | {});", reg_name(rd), reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_sllv(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = {} << ({} & 0x1F);", reg_name(rd), reg_name(rt), reg_name(rs));
}
std::string CodeGenerator::translate_srlv(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = (uint32_t){} >> ({} & 0x1F);", reg_name(rd), reg_name(rt), reg_name(rs));
}
std::string CodeGenerator::translate_srav(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = (int32_t){} >> ({} & 0x1F);", reg_name(rd), reg_name(rt), reg_name(rs));
}
std::string CodeGenerator::translate_slti(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t imm = get_imm16(instr);
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = ((int32_t){} < {}) ? 1 : 0;",
reg_name(rt), reg_name(rs), imm);
}
std::string CodeGenerator::translate_sltiu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t imm = get_imm16(instr); // Sign-extended for comparison
if (config_.optimize_zero_reg && rt == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = ({} < (uint32_t){}) ? 1 : 0;",
reg_name(rt), reg_name(rs), imm);
}
std::string CodeGenerator::translate_mult(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
return fmt::format("{{ int64_t result = (int64_t)(int32_t){} * (int64_t)(int32_t){}; cpu->lo = (uint32_t)result; cpu->hi = (uint32_t)(result >> 32); }}",
reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_multu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
return fmt::format("{{ uint64_t result = (uint64_t){} * (uint64_t){}; cpu->lo = (uint32_t)result; cpu->hi = (uint32_t)(result >> 32); }}",
reg_name(rs), reg_name(rt));
}
std::string CodeGenerator::translate_div(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
// MIPS division: quotient in LO, remainder in HI
// Handle division by zero (MIPS behavior: result is unpredictable but doesn't trap)
return fmt::format("if ({} != 0) {{ cpu->lo = (uint32_t)((int32_t){} / (int32_t){}); cpu->hi = (uint32_t)((int32_t){} % (int32_t){}); }} else {{ cpu->lo = ((int32_t){} >= 0) ? 0xFFFFFFFF : 1; cpu->hi = (uint32_t){}; }}",
reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs), reg_name(rs));
}
std::string CodeGenerator::translate_divu(uint32_t instr) {
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
// MIPS division: quotient in LO, remainder in HI
// Handle division by zero (MIPS behavior: result is unpredictable but doesn't trap)
return fmt::format("if ({} != 0) {{ cpu->lo = {} / {}; cpu->hi = {} % {}; }} else {{ cpu->lo = 0xFFFFFFFF; cpu->hi = {}; }}",
reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs), reg_name(rt), reg_name(rs));
}
std::string CodeGenerator::translate_mfhi(uint32_t instr) {
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = cpu->hi;", reg_name(rd));
}
std::string CodeGenerator::translate_mflo(uint32_t instr) {
uint32_t rd = get_rd(instr);
if (config_.optimize_zero_reg && rd == 0) {
return "/* nop: write to $zero */";
}
return fmt::format("{} = cpu->lo;", reg_name(rd));
}
std::string CodeGenerator::translate_mthi(uint32_t instr) {
uint32_t rs = get_rs(instr);
return fmt::format("cpu->hi = {};", reg_name(rs));
}
std::string CodeGenerator::translate_mtlo(uint32_t instr) {
uint32_t rs = get_rs(instr);
return fmt::format("cpu->lo = {};", reg_name(rs));
}
std::string CodeGenerator::generate_branch_condition(uint32_t instr) {
uint32_t opcode = (instr >> 26) & 0x3F;
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
// REGIMM branches (bltz, bgez, bltzal, bgezal)
if (opcode == 0x01) {
uint32_t regimm_op = (instr >> 16) & 0x1F;
if (regimm_op == 0x00) { // bltz
return fmt::format("(int32_t){} < 0", reg_name(rs));
} else if (regimm_op == 0x01) { // bgez
return fmt::format("(int32_t){} >= 0", reg_name(rs));
} else if (regimm_op == 0x10) { // bltzal
return fmt::format("(int32_t){} < 0", reg_name(rs));
} else if (regimm_op == 0x11) { // bgezal
return fmt::format("(int32_t){} >= 0", reg_name(rs));
}
}
// Standard branches
switch (opcode) {
case 0x04: // beq
case 0x14: // beql
return fmt::format("{} == {}", reg_name(rs), reg_name(rt));
case 0x05: // bne
case 0x15: // bnel
return fmt::format("{} != {}", reg_name(rs), reg_name(rt));
case 0x06: // blez
case 0x16: // blezl
return fmt::format("(int32_t){} <= 0", reg_name(rs));
case 0x07: // bgtz
case 0x17: // bgtzl
return fmt::format("(int32_t){} > 0", reg_name(rs));
}
return "0 /* unknown branch condition: defaults to not-taken */";
}
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
uint32_t opcode = (instr >> 26) & 0x3F;
uint32_t funct = instr & 0x3F;
// Instruction comment (if enabled)
std::string comment;
if (config_.emit_comments) {
comment = fmt::format(" /* 0x{:08X}: 0x{:08X} */", addr, instr);
}
std::string code;
// Widescreen automatic far-backdrop column PRELOAD ([widescreen.cull]
// auto_backdrop). At a detected window's START/END finalize, route the value
// through psx_ws_backdrop_value(orig, is_end): identity at 4:3, but in
// native-wide it forces START->0 / END->0x100, so the generator's own low/
// high clamps preload the whole finite tile row (filling the revealed 16:9
// margin). ws_backdrop_sites_ is populated per function by
// detect_backdrop_windows() only when config_.ws_auto_backdrop_preload is
// set — inert and byte-identical when the feature is off. The instruction
// shape is verified; a mismatch is a loud build error (detector/codegen drift).
if (!ws_backdrop_sites_.empty()) {
auto bd = ws_backdrop_sites_.find(addr);
if (bd != ws_backdrop_sites_.end()) {
int is_end = (bd->second.first == WS_BD_END) ? 1 : 0;
int wcols = bd->second.second;
const char* tag = is_end ? "END" : "START";
if (opcode == 0x00 && (funct == 0x21 || funct == 0x25)) { // addu/or (move)
uint32_t rd = get_rd(instr), rs = get_rs(instr), rt = get_rt(instr);
uint32_t src = (rt == 0) ? rs : rt;
return fmt::format("{} = psx_ws_backdrop_value({}, {}, {});"
" /* ws backdrop preload {} */{}",
reg_name(rd), reg_name(src), is_end, wcols, tag, comment);
}
if (opcode == 0x09 || opcode == 0x08) { // addiu / addi
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t imm = get_imm16(instr);
std::string orig = (rs == 0)
? fmt::format("(uint32_t){}", (int)imm)
: fmt::format("(uint32_t)({} + {})", reg_name(rs), (int)imm);
return fmt::format("{} = psx_ws_backdrop_value({}, {}, {});"
" /* ws backdrop preload {} */{}",
reg_name(rt), orig, is_end, wcols, tag, comment);
}
fmt::print(stderr, "ERROR: [widescreen.cull] auto_backdrop site 0x{:08X} "
"is not addu/or/addiu (opcode 0x{:02X} funct 0x{:02X})\n",
addr, opcode, funct);
std::exit(1);
}
}
// Widescreen cull-margin widening ([widescreen.cull] sites). Emit the
// window immediate with a runtime margin term psx_ws_x_margin() — 0 at
// 4:3/boot/menu/FMV, ~half-the-extra-width when stretching — so the world-
// space draw cull tracks the aspect and one build serves both. Each site's
// instruction type is verified; a mismatch is a loud build error in main-EXE
// mode (a bad address would silently mis-emit otherwise). In OVERLAY mode the
// same address holds different code across scene variants, so a mismatch is
// expected — apply the transform only where the bytes match, else fall
// through to the vanilla translation (see CodeGenConfig::overlay_mode).
if (config_.ws_cull_bias_sites.count(addr)) {
if (opcode == 0x08 || opcode == 0x09) { // addi / addiu
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t imm = get_imm16(instr);
return fmt::format("{} = {} + ((int32_t){} + psx_ws_x_margin());{}",
reg_name(rt), reg_name(rs), imm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.cull] bias site 0x{:08X} is not "
"addi/addiu (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
// overlay variant: addr is different code here — fall through to vanilla.
}
if (config_.ws_cull_range_sites.count(addr)) {
if (opcode == 0x0B) { // sltiu
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t imm = get_imm16(instr);
return fmt::format("{} = ({} < (uint32_t)((int32_t){} + 2*psx_ws_x_margin())) ? 1 : 0;{}",
reg_name(rt), reg_name(rs), imm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.cull] range site 0x{:08X} is not "
"sltiu (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
// overlay variant: addr is different code here — fall through to vanilla.
}
if (config_.ws_cull_a1_sites.count(addr)) {
if (instr == 0x00000000u) { // must be a nop we can safely repurpose
// a1 = $5: widen the caller-supplied margin (param-margin classifiers).
return fmt::format("cpu->gpr[5] = cpu->gpr[5] + psx_ws_x_margin();"
" /* ws cull a1 bias */{}", comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.cull] a1 site 0x{:08X} is not a "
"nop (0x{:08X})\n", addr, instr);
std::exit(1);
}
// overlay variant: addr is different code here — fall through to vanilla.
}
// Widescreen automatic horizontal-FOV cull widening ([widescreen.cull]
// auto_screen_x). ws_auto_cull_func_ is set when this function carries the
// GTE screen-extent reject signature, so a per-vertex width compare here is
// `sltiu rt, SX, 0x140` (or inclusive 0x141). Emit it widened by
// +2*psx_ws_x_margin() (0 at 4:3 ⇒ byte-identical; = the wide-surface extra
// when 16:9), so geometry out to the wide frame edge is submitted rather
// than culled at the 320 boundary. Same shape as an explicit range_site but
// applied by signature — no per-address list. (Reached only when the addr is
// not already an explicit cull site, which returns above.)
if (ws_auto_cull_func_ && opcode == 0x0B) { // sltiu
uint16_t uimm = get_imm16_u(instr);
if (uimm == 0x140 || uimm == 0x141) {
uint32_t rs = get_rs(instr), rt = get_rt(instr);
// Route through the shared runtime helper psx_ws_cull_sltiu so the
// gcc emit, the sljit JIT, and the interpreter all widen identically.
// It sign-extends SX and shifts by +margin (wide window
// -margin <= SX < imm+margin, both edges); at margin=0 it reduces
// bit-for-bit to the vanilla `SX <u imm` verdict (4:3 byte-identical).
return fmt::format("{} = psx_ws_cull_sltiu({}, {});"
" /* ws auto screen-x cull (both edges) */{}",
reg_name(rt), reg_name(rs), (int)uimm, comment);
}
}
// Widescreen backdrop screenX squash ([widescreen.backdrop] x_sites). The
// site is the `sh rt,off(base)` storing a parallax 2D backdrop layer's
// final screen-X; squash the stored value around the screen centre so the
// un-GTE'd backdrop tracks the 16:9 FOV (psx_ws_backdrop_x is identity at
// 4:3). Instruction type is verified; a mismatch is a loud build error.
if (config_.ws_backdrop_x_sites.count(addr)) {
if (opcode == 0x29) { // sh
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t offset = get_imm16(instr);
if (offset == 0)
return fmt::format("cpu->write_half({}, (uint16_t)psx_ws_backdrop_x((int16_t){}));{}",
reg_name(rs), reg_name(rt), comment);
return fmt::format("cpu->write_half({} + {}, (uint16_t)psx_ws_backdrop_x((int16_t){}));{}",
reg_name(rs), offset, reg_name(rt), comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.backdrop] x site 0x{:08X} is not "
"sh (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
// overlay variant: addr is different code here — fall through to vanilla.
}
// Widescreen pure-2D background tile-loop widen ([widescreen.bg2d]). MMX6's
// per-layer BG renderer draws `count` 16px tile columns from a start column /
// start screen-x derived from the camera scroll. Rewrite those three values
// (via the gpu.c psx_ws_mmx6_bg_* helpers — identity at 4:3 / 512 hi-res) so
// the loop draws the 16:9 reveal columns on both sides. Each site's opcode is
// verified; a mismatch is a loud build error (main-EXE addresses).
if (config_.ws_bg2d_count_site && addr == config_.ws_bg2d_count_site) {
if (opcode == 0x09 || opcode == 0x0D) { // addiu / ori (li rt,imm)
uint32_t rt = get_rt(instr);
uint16_t imm = get_imm16_u(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_cols({});{}",
reg_name(rt), (int)imm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] count_site 0x{:08X} is not "
"addiu/ori (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
}
if (config_.ws_bg2d_startcol_site && addr == config_.ws_bg2d_startcol_site) {
if (opcode == 0x0C) { // andi rt,rs,imm (start tile-column mask)
uint32_t rs = get_rs(instr), rt = get_rt(instr);
uint16_t imm = get_imm16_u(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_startcol((int)({} & 0x{:X}u));{}",
reg_name(rt), reg_name(rs), imm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] startcol_site 0x{:08X} is not "
"andi (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
}
if (config_.ws_bg2d_startx_site && addr == config_.ws_bg2d_startx_site) {
if (opcode == 0x00 && (instr & 0x3F) == 0x03) { // sra rd,rt,sa (start screen-x)
uint32_t rt = get_rt(instr), rd = get_rd(instr), sh = get_shamt(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_startx((int32_t){} >> {});{}",
reg_name(rd), reg_name(rt), sh, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] startx_site 0x{:08X} is not "
"sra (instr 0x{:08X})\n", addr, instr);
std::exit(1);
}
}
// Tile-ring streamer leading-edge widen: two addiu computing the left
// (scrollX-16) and right (scrollX+16) stream world-X; push them out by
// LEFT*16 so the ring is populated across the widened column window.
if (config_.ws_bg2d_stream_left_site && addr == config_.ws_bg2d_stream_left_site) {
if (opcode == 0x08 || opcode == 0x09) { // addi / addiu
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t imm = get_imm16(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_stream_left((int32_t){} + ({}));{}",
reg_name(rt), reg_name(rs), imm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] stream_left_site 0x{:08X} is not "
"addi/addiu (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
}
if (config_.ws_bg2d_stream_right_site && addr == config_.ws_bg2d_stream_right_site) {
if (opcode == 0x08 || opcode == 0x09) { // addi / addiu
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t imm = get_imm16(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_stream_right((int32_t){} + ({}));{}",
reg_name(rt), reg_name(rs), imm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] stream_right_site 0x{:08X} is not "
"addi/addiu (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
}
// BG packet-buffer RELOCATION (cure for the 1024-slot/1000-cap overflow that the
// widen causes in dense stages). bufbase_site = the driver addu computing the buffer
// address (base + bufidx*0x4000); cap_site = the renderer's per-frame tile-cap slti.
// Helpers relocate to a larger free buffer + raise the cap when the widen is active,
// identity otherwise (4:3 / other games byte-identical).
if (config_.ws_bg2d_bufbase_site && addr == config_.ws_bg2d_bufbase_site) {
if (opcode == 0x00 && (instr & 0x3F) == 0x21) { // addu rd,rs,rt (BG buffer address)
uint32_t rs = get_rs(instr), rt = get_rt(instr), rd = get_rd(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_bufbase((int)({} + {}));{}",
reg_name(rd), reg_name(rs), reg_name(rt), comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] bufbase_site 0x{:08X} is not "
"addu (instr 0x{:08X})\n", addr, instr);
std::exit(1);
}
}
if (config_.ws_bg2d_cap_site && addr == config_.ws_bg2d_cap_site) {
if (opcode == 0x0A) { // slti rt,rs,imm (BG per-frame tile cap compare)
uint32_t rs = get_rs(instr), rt = get_rt(instr);
return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_undercap((int32_t){});{}",
reg_name(rt), reg_name(rs), comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.bg2d] cap_site 0x{:08X} is not "
"slti (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
}
// Persistent game-option init store ([persist_options] in game_options.toml).
// The site is the boot-init sb/sh that writes a config global's DEFAULT value
// (Tomba MESSAGE / SOUND / VIBRATION / ADJUST SCREEN). Route the stored value
// through psx_game_option_store(addr, val): it returns a value persisted from a
// prior session for that address, else `val` unchanged — so the saved setting
// overrides the default exactly once, at initialization, and a fresh install
// (no saved file) is byte-identical. The in-OPTION write is a different
// instruction (option overlay), untouched, so the player can still change it.
// Verified sb/sh; a mismatch is a loud build error.
if (config_.persist_init_store_sites.count(addr)) {
uint32_t rs = get_rs(instr), rt = get_rt(instr);
int16_t offset = get_imm16(instr);
std::string aexpr = (offset != 0)
? fmt::format("({} + {})", reg_name(rs), offset)
: std::string(reg_name(rs));
if (opcode == 0x28) // sb
return fmt::format("cpu->write_byte({0}, (uint8_t)psx_game_option_store({0}, (int){1}));{2}",
aexpr, reg_name(rt), comment);
if (opcode == 0x29) // sh
return fmt::format("cpu->write_half({0}, (uint16_t)psx_game_option_store({0}, (int){1}));{2}",
aexpr, reg_name(rt), comment);
fmt::print(stderr, "ERROR: [persist_options] init site 0x{:08X} is not sb/sh "
"(opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
// SPECIAL opcode (0x00)
if (opcode == 0x00) {
switch (funct) {
case 0x00: code = translate_sll(instr); break; // sll
case 0x02: code = translate_srl(instr); break; // srl
case 0x03: code = translate_sra(instr); break; // sra
case 0x04: code = translate_sllv(instr); break; // sllv
case 0x06: code = translate_srlv(instr); break; // srlv
case 0x07: code = translate_srav(instr); break; // srav
case 0x08: code = "return; /* jr $ra */"; break; // jr (assume return)
case 0x09: // jalr
{
uint32_t rs = get_rs(instr);
uint32_t rd = get_rd(instr);
code = fmt::format("{} = 0x{:08X}; cpu->pc = {}; /* jalr */",
reg_name(rd), addr + 8, reg_name(rs));
}
break;
case 0x0C: // syscall
{
uint32_t syscall_code = (instr >> 6) & 0xFFFFF;
code = fmt::format("psx_syscall(cpu, {}); /* syscall {} */", syscall_code, syscall_code);
}
break;
case 0x0D: // break
{
uint32_t break_code = (instr >> 6) & 0xFFFFF;
code = fmt::format("/* break({}) — trap, no-op in recompiler */", break_code);
}
break;
case 0x10: code = translate_mfhi(instr); break; // mfhi
case 0x11: code = translate_mthi(instr); break; // mthi
case 0x12: code = translate_mflo(instr); break; // mflo
case 0x13: code = translate_mtlo(instr); break; // mtlo
case 0x18: code = translate_mult(instr); break; // mult
case 0x19: code = translate_multu(instr); break; // multu
case 0x1A: code = translate_div(instr); break; // div
case 0x1B: code = translate_divu(instr); break; // divu
case 0x20: code = translate_add(instr); break; // add
case 0x21: code = translate_addu(instr); break; // addu
case 0x22: code = translate_sub(instr); break; // sub
case 0x23: code = translate_subu(instr); break; // subu
case 0x24: code = translate_and(instr); break; // and
case 0x25: code = translate_or(instr); break; // or
case 0x26: code = translate_xor(instr); break; // xor
case 0x27: code = translate_nor(instr); break; // nor
case 0x2A: code = translate_slt(instr); break; // slt
case 0x2B: code = translate_sltu(instr); break; // sltu
default:
code = fmt::format("/* TODO: SPECIAL funct=0x{:02X} */", funct);
}
}
// Immediate and load/store instructions
else {
switch (opcode) {
case 0x08: code = translate_addiu(instr); break; // addi (same as addiu)
case 0x09: code = translate_addiu(instr); break; // addiu
case 0x0A: code = translate_slti(instr); break; // slti
case 0x0B: code = translate_sltiu(instr); break; // sltiu
case 0x0C: code = translate_andi(instr); break; // andi
case 0x0D: code = translate_ori(instr); break; // ori
case 0x0E: code = translate_xori(instr); break; // xori
case 0x0F: code = translate_lui(instr); break; // lui
case 0x10: // COP0
{
uint32_t cop_op = (instr >> 21) & 0x1F;
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (cop_op == 0x00) { // MFC0 - move from COP0
code = fmt::format("{} = cpu->cop0[{}]; /* mfc0 */", reg_name(rt), rd);
} else if (cop_op == 0x04) { // MTC0 - move to COP0
code = fmt::format("cpu->cop0[{}] = {}; /* mtc0 */", rd, reg_name(rt));
} else if (cop_op == 0x10 && (instr & 0x3F) == 0x10) { // RFE
// Restore interrupt enable bits: shift bits 5:2 → 3:0
code = "{ uint32_t sr = cpu->cop0[12]; "
"cpu->cop0[12] = (sr & 0xFFFFFFF0u) | ((sr >> 2) & 0x0Fu); } /* rfe */";
} else {
code = fmt::format("/* cop0: 0x{:08X} */", instr);
}
}
break;
case 0x12: // COP2 (GTE)
{
uint32_t cop_op = (instr >> 21) & 0x1F;
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
if (cop_op == 0x00) { // MFC2 - move from COP2 data
if ((rd >= 8 && rd <= 11) || rd == 15 || rd == 28 || rd == 29 || rd == 31) {
code = fmt::format("{} = gte_read_data(cpu, {}); /* mfc2 */", reg_name(rt), rd);
} else {
code = fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
}
} else if (cop_op == 0x02) { // CFC2 - move from COP2 control
code = fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
} else if (cop_op == 0x04) { // MTC2 - move to COP2 data
if (rd == 7 || (rd >= 8 && rd <= 11) || rd == 14 || rd == 15 || rd == 28 || rd == 30) {
code = fmt::format("gte_write_data(cpu, {}, {}); /* mtc2 */", rd, reg_name(rt));
} else {
code = fmt::format("cpu->gte_data[{}] = {}; /* mtc2 */", rd, reg_name(rt));
}
} else if (cop_op == 0x06) { // CTC2 - move to COP2 control
code = fmt::format("cpu->gte_ctrl[{}] = {}; /* ctc2 */", rd, reg_name(rt));
} else if ((cop_op & 0x10) != 0) { // GTE command (bit 25 set)
uint32_t gte_cmd = instr & 0x1FFFFFF;
// Route ALL GTE commands through gte_execute() for correct behavior
code = fmt::format("gte_execute(cpu, 0x{:07X}); /* gte cmd 0x{:02X} */", gte_cmd, gte_cmd & 0x3F);
} else {
code = fmt::format("/* cop2: 0x{:08X} */", instr);
}
}
break;
case 0x20: code = translate_lb(instr); break; // lb
case 0x21: code = translate_lh(instr); break; // lh
case 0x22: code = translate_lwl(instr); break; // lwl
case 0x23: code = translate_lw(instr); break; // lw
case 0x24: code = translate_lbu(instr); break; // lbu
case 0x25: code = translate_lhu(instr); break; // lhu
case 0x26: code = translate_lwr(instr); break; // lwr
case 0x28: code = translate_sb(instr); break; // sb
case 0x29: code = translate_sh(instr); break; // sh
case 0x2A: code = translate_swl(instr); break; // swl
case 0x2B: code = translate_sw(instr); break; // sw
case 0x2E: code = translate_swr(instr); break; // swr
case 0x2F: // CACHE - cache op (no-op for static recomp)
code = "/* cache (no-op: static recompilation has no I/D cache) */";
break;
case 0x32: // LWC2 - load word to COP2
{
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
bool special = (rt == 7 || (rt >= 8 && rt <= 11) || rt == 14 || rt == 15 || rt == 28 || rt == 30);
if (special) {
std::string addr = (offset == 0)
? reg_name(rs)
: fmt::format("{} + {}", reg_name(rs), offset);
code = fmt::format("gte_write_data(cpu, {}, cpu->read_word({})); /* lwc2 gte[{}] */",
rt, addr, rt);
} else if (offset == 0) {
code = fmt::format("cpu->gte_data[{}] = cpu->read_word({}); /* lwc2 gte[{}], ({}) */",
rt, reg_name(rs), rt, reg_name(rs));
} else {
code = fmt::format("cpu->gte_data[{}] = cpu->read_word({} + {}); /* lwc2 gte[{}], {}({}) */",
rt, reg_name(rs), offset, rt, offset, reg_name(rs));
}
}
break;
case 0x3A: // SWC2 - store word from COP2
{
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
std::string value = ((rt >= 8 && rt <= 11) || rt == 15 || rt == 28 || rt == 29 || rt == 31)
? fmt::format("gte_read_data(cpu, {})", rt)
: fmt::format("cpu->gte_data[{}]", rt);
if (offset == 0) {
code = fmt::format("cpu->write_word({}, {}); /* swc2 gte[{}], ({}) */",
reg_name(rs), value, rt, reg_name(rs));
} else {
code = fmt::format("cpu->write_word({} + {}, {}); /* swc2 gte[{}], {}({}) */",
reg_name(rs), offset, value, rt, offset, reg_name(rs));
}
}
break;
default:
code = fmt::format("/* TODO: opcode=0x{:02X} */", opcode);
}
}
return config_.indent + code + comment;
}
std::string CodeGenerator::translate_basic_block(
const BasicBlock& block,
const ControlFlowGraph& cfg) {
std::stringstream ss;
// Block label
ss << fmt::format("block_{:08X}:\n", block.start_addr);
// Cycle-budgeted precise event slicing (PRECISE_IRQ_SLICE.md). At the block
// leader — before any cycle is charged or the body runs — divert to the
// per-instruction interpreter when an interrupt could be taken inside this
// block, so the IRQ lands at its exact architectural instruction instead of
// the coarse block edge. psx_slice_block returns nonzero iff it sliced (it
// interpreted the block — and possibly more — and left cpu->pc at a
// dispatchable resume point), in which case this function returns so its
// compiled body does not re-execute the same instructions.
// side_effects=1 marks blocks that change interrupt visibility on the CPU
// (mtc0 Status/Cause, rfe), which can make a pending+masked IRQ deliverable
// mid-block with no external event deadline.
// Faithful cycle accounting (FAITHFUL_TIMING_PLAN.md P2): the exit branch/jump
// ALWAYS executes its delay-slot instruction (emitted as a clone before the
// transfer, below). When the delay slot lives INSIDE this block
// (block.exit_instr.address < end_addr) it is already part of
// instruction_count. But when the delay slot is a SEPARATE block leader (a
// branch target, or a split-function edge) the branch sits AT end_addr and the
// delay slot at end_addr+4 is outside [start,end]; instruction_count then
// EXCLUDES it, yet the clone still executes — so the block undercharges by one
// cycle vs the per-instruction interpreter on BOTH paths. (This is the measured
// -8 drift in the Tomba 2 logo init subtree: ~8 such sites on the boot path,
// native runs behind interp, the elapsed-Timer1 logo-delay loop exits early.)
// Charge the always-executed clone here so every dynamically executed
// instruction is charged exactly once. (R3000 is MIPS-I: no branch-likely, so
// the delay slot is unconditional.) cycle_per_insn mode already charges the
// clone per-instruction at its emit site, so this adjustment is block-mode only.
const bool exit_delay_slot_outside =
block.exit_instr.has_delay_slot &&
block.exit_instr.type != ControlFlowType::None &&
block.exit_instr.address == block.end_addr;
// Block cycle charge = sum of the single-source per-instruction cost
// (psx_instr_cost.h) over every instruction the block executes, INCLUDING
// the always-executed delay-slot clone when the exit branch sits at end_addr
// (delay slot outside [start,end]). The interpreter charges the same cost
// per instruction through the same function, so the two backends cannot
// disagree. Stage 1 cost is identity (1/insn) => this equals
// instruction_count(+1 outside delay) exactly => byte-identical regen; Stage 2
// edits ONLY psx_instr_base_cycles and both backends update together.
uint32_t block_exec_cycles = 0;
for (uint32_t a = block.start_addr; a <= block.end_addr; a += 4) {
auto w = exe_.read_word(a);
if (!w.has_value()) break;
block_exec_cycles += psx_instr_base_cycles(*w);
}
if (exit_delay_slot_outside) {
auto dw = exe_.read_word(block.end_addr + 4u); // the delay-slot clone
if (dw.has_value()) block_exec_cycles += psx_instr_base_cycles(*dw);
}
{
uint32_t slice_bcyc = block_exec_cycles;
if (slice_bcyc == 0) slice_bcyc = 1;
int slice_side_effects = 0;
for (uint32_t a = block.start_addr; a <= block.end_addr; a += 4) {
auto w = exe_.read_word(a);
if (!w.has_value()) break;
uint32_t insn = *w;
if ((insn >> 26) == 0x10u) { // COP0
uint32_t rs = (insn >> 21) & 0x1Fu;
uint32_t co = (insn >> 25) & 0x1u;
uint32_t funct = insn & 0x3Fu;
if (rs == 0x04u) slice_side_effects = 1; // MTC0 (writes Status/Cause)
if (co && funct == 0x10u) slice_side_effects = 1; // RFE
}
}
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
ss << config_.indent
<< fmt::format("if (psx_slice_block(cpu, 0x{:08X}u, {}u, {})) return;\n",
block.start_addr, slice_bcyc, slice_side_effects);
ss << "#endif\n";
}
const bool cycle_per_insn = codegen_cycle_per_insn();
auto emit_one_cycle = [&](const std::string& indent) {
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
ss << indent << "psx_advance_cycles(1u);\n";
ss << "#endif\n";
};
if (!cycle_per_insn && block_exec_cycles > 0) {
ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n";
ss << config_.indent << fmt::format("psx_advance_cycles({}u);\n",
block_exec_cycles);
ss << "#endif\n";
}
// Translate each instruction in the block
uint32_t addr = block.start_addr;
// Determine if the exit instruction uses a delay slot pattern.
// Normal case: branch at end_addr-4, delay slot at end_addr.
// Split-function edge case: exit instruction IS at end_addr (delay slot
// is outside the function), so exit_branch_addr = end_addr not end_addr-4.
bool exit_has_delay = (block.exit_instr.has_delay_slot &&
block.exit_instr.type != ControlFlowType::None);
bool delay_slot_in_block = (exit_has_delay &&
block.exit_instr.address < block.end_addr);
bool exit_uses_delay_slot = delay_slot_in_block;
uint32_t exit_branch_addr;
if (delay_slot_in_block) {
exit_branch_addr = block.end_addr - 4; // branch at end-4, delay slot at end
} else if (exit_has_delay) {
exit_branch_addr = block.end_addr; // branch at end, delay slot outside
} else {
exit_branch_addr = block.end_addr;
}
while (addr <= block.end_addr) {
auto instr_opt = exe_.read_word(addr);
if (!instr_opt.has_value()) {
break;
}
uint32_t instr = *instr_opt;
// Skip the delay slot at end_addr - it's emitted as part of the branch handling below
if (exit_uses_delay_slot && addr == block.end_addr) {
addr += 4;
break;
}
// Don't emit control flow instructions here (handled separately)
bool is_cf = ControlFlowAnalyzer::is_control_flow(instr);
// Emit inline label for mid-block jump table targets
if (addr != block.start_addr && extra_labels_.count(addr)) {
ss << fmt::format("block_{:08X}:;\n", addr);
}
// Instruction-level annotation (skipped at function start — already shown above signature)
if (annotations_ && addr != cfg.function_start) {
const std::string& inote = annotations_->lookup(addr);
if (!inote.empty())
ss << config_.indent << fmt::format("/* [NOTE] {} */\n", inote);
}
if (!is_cf) {
ss << translate_instruction(addr, instr) << "\n";
if (cycle_per_insn) {
emit_one_cycle(config_.indent);
}
} else {
// Control flow is handled at block exit
if (addr == exit_branch_addr) {
// MIPS delay slot handling: emit delay slot instruction BEFORE branch/jump
std::string delay_saved_cond; // non-empty if condition was pre-captured
if (block.exit_instr.has_delay_slot) {
uint32_t delay_slot_addr = addr + 4;
auto delay_instr_opt = exe_.read_word(delay_slot_addr);
if (delay_instr_opt.has_value()) {
uint32_t delay_instr = *delay_instr_opt;
// For branch-likely variants, delay slot is conditional
if (block.exit_instr.is_likely) {
ss << config_.indent << "/* delay slot (likely) - conditional execution */\n";
ss << config_.indent << "if (" << generate_branch_condition(block.exit_instr.instruction) << ") {\n";
ss << config_.indent << translate_instruction(delay_slot_addr, delay_instr) << "\n";
ss << config_.indent << "}\n";
} else {
// Normal delay slot - always executes.
// In MIPS, the branch condition is evaluated at the branch
// instruction (before the delay slot). If the delay slot
// modifies a condition register we must save the result first.
if (block.exit_instr.type == ControlFlowType::Branch) {
std::string cond = generate_branch_condition(block.exit_instr.instruction);
delay_saved_cond = fmt::format("_bc_{:08X}", addr);
ss << config_.indent
<< fmt::format("int {} = ({}); /* save branch cond before delay slot */\n",
delay_saved_cond, cond);
}
ss << config_.indent << "/* delay slot (always executes) */\n";
ss << translate_instruction(delay_slot_addr, delay_instr) << "\n";
if (cycle_per_insn) {
emit_one_cycle(config_.indent);
}
}
}
}
if (cycle_per_insn) {
emit_one_cycle(config_.indent);
}
// Now emit the branch/jump
if (block.exit_instr.type == ControlFlowType::Branch) {
// Check if this is a branch-and-link (bgezal/bltzal)
// For REGIMM (opcode=0x01), rt=0x10 (bltzal) or rt=0x11 (bgezal)
// The link always happens unconditionally (even if branch not taken)
{
uint32_t branch_instr = block.exit_instr.instruction;
uint32_t b_opcode = (branch_instr >> 26) & 0x3F;
if (b_opcode == 0x01) {
uint32_t regimm_op = (branch_instr >> 16) & 0x1F;
if (regimm_op == 0x10 || regimm_op == 0x11) {
// bltzal or bgezal: link register always set
ss << config_.indent
<< fmt::format("cpu->gpr[31] = 0x{:08X}; /* branch-and-link return addr */\n", addr + 8);
}
}
}
// Conditional branch - use pre-captured condition if available
std::string condition = delay_saved_cond.empty()
? generate_branch_condition(block.exit_instr.instruction)
: delay_saved_cond;
if (block.successors.size() == 2) {
ss << config_.indent << "if (" << condition << ") {\n";
ss << emit_interrupt_check(block.successors[0], config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("goto block_{:08X}; /* taken */\n", block.successors[0]);
ss << config_.indent << "} else {\n";
ss << emit_interrupt_check(block.successors[1], config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("goto block_{:08X}; /* not taken */\n", block.successors[1]);
ss << config_.indent << "}\n";
} else {
// Split-function: one or both branch targets are outside
// this function piece. Emit function calls for out-of-function
// targets to reconnect the split pieces.
uint32_t branch_target = block.exit_instr.target;
uint32_t fall_through_addr = block.exit_instr.address + 8;
bool target_in = cfg.blocks.count(branch_target) > 0;
bool fallthru_in = cfg.blocks.count(fall_through_addr) > 0;
ss << config_.indent << "if (" << condition << ") {\n";
if (target_in) {
ss << emit_interrupt_check(branch_target, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("goto block_{:08X}; /* taken */\n", branch_target);
} else if (cps_enabled_) {
ss << emit_interrupt_check(branch_target, config_.indent + config_.indent);
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 << emit_interrupt_check(branch_target, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* taken: split piece */\n", branch_target);
} else {
ss << emit_interrupt_check(branch_target, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("call_by_address(cpu, 0x{:08X}u); return; /* taken: split (mid-func) */\n", branch_target);
}
ss << config_.indent << "} else {\n";
if (fallthru_in) {
ss << emit_interrupt_check(fall_through_addr, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("goto block_{:08X}; /* not taken */\n", fall_through_addr);
} else if (cps_enabled_) {
ss << emit_interrupt_check(fall_through_addr, config_.indent + config_.indent);
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 << emit_interrupt_check(fall_through_addr, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* not taken: split piece */\n", fall_through_addr);
} else {
ss << emit_interrupt_check(fall_through_addr, config_.indent + config_.indent);
ss << config_.indent << config_.indent
<< fmt::format("call_by_address(cpu, 0x{:08X}u); return; /* not taken: split (mid-func) */\n", fall_through_addr);
}
ss << config_.indent << "}\n";
}
} else if (block.exit_instr.type == ControlFlowType::Jump) {
// Unconditional jump
if (!block.successors.empty()) {
ss << emit_interrupt_check(block.successors[0], config_.indent);
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 << emit_interrupt_check(block.exit_instr.target, config_.indent);
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 << emit_interrupt_check(block.exit_instr.target, config_.indent);
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* j to split piece */\n",
block.exit_instr.target);
} else if (block.exit_instr.target != 0) {
// Jump target is a mid-function address — dispatch dynamically
ss << emit_interrupt_check(block.exit_instr.target, config_.indent);
ss << config_.indent
<< fmt::format("call_by_address(cpu, 0x{:08X}u); return; /* j to split (mid-func) */\n",
block.exit_instr.target);
}
} else if (block.exit_instr.type == ControlFlowType::Return) {
/* Return instruction.
*
* Detect "longjmp-return" pattern: if $ra was loaded from a
* non-$sp source anywhere in this function (e.g. RestoreState
* loads $ra from a save buffer via $a0), the jr $ra must set
* cpu->pc so the dispatch loop tail-calls to the restored
* address — the C `return` alone would go back to the wrong
* caller. Normal functions that save/restore $ra on the $sp
* stack get the plain `return;`. */
bool ra_loaded_from_non_sp = false;
for (const auto& [baddr, blk] : cfg.blocks) {
for (uint32_t ia = blk.start_addr; ia < blk.end_addr; ia += 4) {
auto wopt = exe_.read_word(ia);
if (!wopt) continue;
uint32_t w = *wopt;
uint32_t i_op = (w >> 26) & 0x3F;
uint32_t i_rt = (w >> 16) & 0x1F;
uint32_t i_rs = (w >> 21) & 0x1F;
if (i_op == 0x23 && i_rt == 31 && i_rs == 4) {
/* lw $ra, offset($a0) — RestoreState/longjmp pattern */
ra_loaded_from_non_sp = true;
break;
}
}
if (ra_loaded_from_non_sp) break;
}
if (config_.ws_backdrop_unsquash_funcs.count(cfg.function_start)) {
ss << config_.indent
<< "gte_ws_set_suppress(0); /* widescreen: end far-backdrop un-squash (8C) */\n";
}
if (ra_loaded_from_non_sp) {
ss << emit_interrupt_check_expr("cpu->gpr[31]", config_.indent);
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 << emit_interrupt_check_expr("cpu->gpr[31]", config_.indent);
ss << config_.indent
<< "cpu->pc = cpu->gpr[31]; return; /* CPS: jr $ra */\n";
} else {
ss << emit_interrupt_check_expr("cpu->gpr[31]", config_.indent);
ss << config_.indent << "return; /* jr $ra */\n";
}
} else if (block.exit_instr.type == ControlFlowType::JumpRegister) {
// jr $rs — may be: BIOS call (jr $t2, t2=0xA0/0xB0/0xC0)
// or: jump table (jr $v0, v0 loaded from LW+ADDU+LUI)
uint32_t jr_rs = get_rs(block.exit_instr.instruction);
// Detect MIPS jump-table pattern by scanning backwards from jr.
// Handles two common patterns:
// P1: lui $R,hi; addiu $R,$R,lo; addu $D,$idx,$R; lw $D,0($D)
// P2: lui $R,hi; addu $D,$R,$idx; lw $D,off($D)
// table_base = lui_val + addiu_val + lw_offset
uint32_t table_base = 0;
uint32_t table_count = 0;
{
uint32_t lw_base_reg = 0xFF; // base reg in the LW
int32_t lw_offset = 0; // offset in the LW instruction
uint32_t addu_cand[2] = {0xFF, 0xFF}; // rs, rt of ADDU
uint32_t table_reg = 0xFF; // candidate for table base reg
uint32_t lui_val = 0;
int16_t addiu_val[2] = {0, 0}; // per-candidate ADDIU
bool found_addiu[2]= {false, false};
bool found_lui = false;
for (int back = 1; back <= 40; back++) {
uint32_t sa = block.exit_instr.address - (uint32_t)(back * 4);
if (sa < cfg.function_start) break;
auto sopt = exe_.read_word(sa);
if (!sopt) break;
uint32_t si = *sopt;
uint32_t s_op = (si >> 26) & 0x3F;
uint32_t s_rs = (si >> 21) & 0x1F;
uint32_t s_rt = (si >> 16) & 0x1F;
uint32_t s_rd = (si >> 11) & 0x1F;
uint32_t s_fn = si & 0x3F;
// LW $jr_reg, offset($base)
if (s_op == 0x23 && s_rt == jr_rs && lw_base_reg == 0xFF) {
lw_base_reg = s_rs;
lw_offset = (int32_t)(int16_t)(si & 0xFFFF);
continue;
}
// ADDU $lw_base, $a, $b — save both operands as candidates
if (s_op == 0x00 && s_fn == 0x21 &&
s_rd == lw_base_reg && lw_base_reg != 0xFF &&
addu_cand[0] == 0xFF) {
addu_cand[0] = s_rs;
addu_cand[1] = s_rt;
table_reg = s_rt; // initial guess; may flip on LUI match
continue;
}
// ADDIU $cand, $cand, imm (scanning back → before LUI)
if (s_op == 0x09 && addu_cand[0] != 0xFF) {
for (int c = 0; c < 2; c++) {
if (!found_addiu[c] && addu_cand[c] != 0xFF &&
s_rs == addu_cand[c] && s_rt == addu_cand[c]) {
addiu_val[c] = (int16_t)(si & 0xFFFF);
found_addiu[c] = true;
break;
}
}
continue;
}
// LUI $cand, hi — determines which candidate is the table base
if (s_op == 0x0F && addu_cand[0] != 0xFF && !found_lui) {
for (int c = 0; c < 2; c++) {
if (addu_cand[c] != 0xFF && s_rt == addu_cand[c]) {
lui_val = ((uint32_t)(si & 0xFFFF)) << 16;
table_reg = addu_cand[c];
// carry over the ADDIU for this candidate
if (!found_addiu[c]) { addiu_val[c] = 0; }
// store addiu in slot 0 for convenience
addiu_val[0] = addiu_val[c];
found_addiu[0] = found_addiu[c];
found_lui = true;
break;
}
}
continue;
}
// SLTIU — table entry count
if (s_op == 0x0B && table_count == 0) {
table_count = si & 0xFFFF;
continue;
}
if (found_lui && table_count != 0) break;
}
if (found_lui && table_count > 0 && table_count < 512) {
uint32_t base = lui_val +
(found_addiu[0] ? (uint32_t)(int32_t)addiu_val[0] : 0u);
table_base = base + (uint32_t)lw_offset;
}
}
// If we found a valid table, read entries and emit switch.
// table_base may be a RAM address (BIOS shell code copies
// ROM 0xBFC18000+ to RAM 0x80030000+). Translate to ROM
// so exe_.read_word() can read the data, and translate each
// entry so it matches cfg.blocks / extra_labels_ (ROM space).
bool emitted_switch = false;
if (table_base != 0 && table_count > 0) {
uint32_t rom_table_base = ram_to_rom(table_base, exe_);
std::set<uint32_t> seen;
std::vector<std::pair<uint32_t,uint32_t>> targets; // {runtime_addr, rom_addr}
for (uint32_t i = 0; i < table_count; i++) {
auto eopt = exe_.read_word(rom_table_base + i * 4);
if (!eopt) continue;
uint32_t runtime_addr = *eopt;
uint32_t rom_addr = ram_to_rom(runtime_addr, exe_);
if (seen.insert(rom_addr).second &&
(cfg.blocks.count(rom_addr) || extra_labels_.count(rom_addr))) {
targets.push_back({runtime_addr, rom_addr});
}
}
if (!targets.empty()) {
ss << config_.indent << fmt::format("/* jump table 0x{:08X} (rom 0x{:08X}), {} entries */\n",
table_base, rom_table_base, table_count);
ss << config_.indent << fmt::format("switch ({}) {{\n", reg_name(jr_rs));
for (auto& [rt, rom] : targets) {
if (partial_block_cycle_count(rom, cfg) != 0) {
ss << config_.indent << fmt::format(" case 0x{:08X}u:\n", rt);
ss << emit_interrupt_check(rom, config_.indent + " ");
ss << emit_mid_block_cycle_charge(rom, cfg, config_.indent + " ");
ss << config_.indent << fmt::format(" goto block_{:08X};\n", rom);
} else {
ss << config_.indent << fmt::format(" case 0x{:08X}u:\n", rt);
ss << emit_interrupt_check(rom, config_.indent + " ");
ss << config_.indent << fmt::format(" goto block_{:08X};\n", rom);
}
}
if (cps_enabled_) {
ss << config_.indent << " default:\n";
ss << emit_interrupt_check_expr(reg_name(jr_rs), config_.indent + " ");
ss << config_.indent << " cpu->pc = " << reg_name(jr_rs)
<< "; return; /* CPS: jr table miss — tail-transfer */\n";
} else {
ss << config_.indent << " default:\n";
ss << emit_interrupt_check_expr(reg_name(jr_rs), config_.indent + " ");
ss << config_.indent << " call_by_address(cpu, " << reg_name(jr_rs) << "); return;\n";
}
ss << config_.indent << "}\n";
emitted_switch = true;
}
}
if (!emitted_switch) {
if (cps_enabled_) {
// CPS: indirect jump / BIOS-call gate — tail-transfer
// to the target (the flat trampoline dispatches it).
ss << emit_interrupt_check_expr(reg_name(jr_rs), config_.indent);
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 << emit_interrupt_check_expr(reg_name(jr_rs), config_.indent);
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 << emit_interrupt_check(target, config_.indent);
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);
ss << emit_interrupt_check(target, config_.indent);
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);
} else {
ss << config_.indent << fmt::format("call_by_address(cpu, 0x{:08X}u); /* external jal */\n", target);
/* psx_dispatch_call validated the (ra, sp) contract;
* only propagate an active bail unwind here. */
ss << config_.indent << "if (g_psx_call_bail) return; (void)_csp; }\n";
}
if (!block.successors.empty()) {
ss << config_.indent
<< fmt::format("goto block_{:08X}; /* continue after call */\n", block.successors[0]);
} else {
// Split-function: JAL continuation is outside this function piece.
// Tail-call to the continuation piece (at exit_addr + 8, past delay slot).
if (known_functions_.count(cont_addr)) {
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* jal cont: split piece */\n", cont_addr);
} else {
ss << config_.indent
<< 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 << emit_interrupt_check_expr("_t", config_.indent + " ");
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);
// Dispatch indirect call — target is a runtime register value
ss << emit_interrupt_check_expr(reg_name(rs), config_.indent);
ss << config_.indent << fmt::format("call_by_address(cpu, {}); /* jalr {} */\n",
reg_name(rs), reg_name(rs));
/* psx_dispatch_call validated the (ra, sp) contract;
* only propagate an active bail unwind here. */
ss << config_.indent << "if (g_psx_call_bail) return;\n";
// Continue to successor block (instruction at return address)
if (!block.successors.empty()) {
ss << config_.indent
<< fmt::format("goto block_{:08X}; /* continue after jalr */\n",
block.successors[0]);
} else {
// Split-function: JALR continuation is outside this function piece.
if (known_functions_.count(cont_addr)) {
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* jalr cont: split piece */\n", cont_addr);
} else {
ss << config_.indent
<< fmt::format("call_by_address(cpu, 0x{:08X}u); return; /* jalr cont: split */\n", cont_addr);
}
}
}
}
}
}
addr += 4;
}
// If no explicit control flow, fall through to the next block. When a
// mid-function seed split placed the next instruction in a different C
// function, tail-call that split piece so the physical fall-through is
// preserved.
if (block.exit_instr.type == ControlFlowType::None && !block.successors.empty()) {
ss << emit_interrupt_check(block.successors[0], config_.indent);
ss << config_.indent << fmt::format("/* fall through to block_{:08X} */\n",
block.successors[0]);
} else if (block.exit_instr.type == ControlFlowType::None) {
uint32_t next_addr = block.end_addr + 4;
ss << emit_interrupt_check(next_addr, config_.indent);
if (known_functions_.count(next_addr) > 0) {
ss << config_.indent
<< fmt::format("func_{:08X}(cpu); return; /* fallthrough to split piece */\n",
next_addr);
}
}
return ss.str();
}
bool CodeGenerator::func_has_screen_extent_cull(const ControlFlowGraph& cfg) const {
// The GTE per-vertex trivial-reject pairs a width compare (sltiu …,0x140 or
// inclusive 0x141) with a height compare (sltiu …,0xE0 or 0xF1) in the same
// function. Presence of both is the signature of a screen-extent render
// funnel; a lone width compare elsewhere (rare) is left alone.
bool has_x = false, has_y = false;
for (uint32_t block_addr : cfg.block_order) {
const BasicBlock& block = cfg.blocks.at(block_addr);
for (uint32_t a = block.start_addr; a <= block.end_addr; a += 4) {
auto io = exe_.read_word(a);
if (!io.has_value()) continue;
uint32_t in = *io;
if ((in & 0xFC000000u) != 0x2C000000u) continue; // not sltiu
uint16_t im = (uint16_t)(in & 0xFFFF);
if (im == 0x140 || im == 0x141) has_x = true;
else if (im == 0xE0 || im == 0xF1) has_y = true;
if (has_x && has_y) return true;
}
}
return false;
}
int CodeGenerator::detect_backdrop_windows(const ControlFlowGraph& cfg) {
// Always clear so a stale map from the previous function can never leak.
ws_backdrop_sites_.clear();
if (!config_.ws_auto_backdrop_preload) return 0;
// Build a contiguous word image of the whole function range so the shared
// detector's absolute-PC math (base_pc + i*4) lands on real addresses; any
// non-code gap (jump table / constant pool) simply never matches the
// invariant and is harmless. base_pc = function_start.
uint32_t lo = cfg.function_start, hi = cfg.function_end;
if (hi <= lo || (hi - lo) > 0x40000u) return 0; // sanity bound
std::vector<uint32_t> words;
words.reserve((hi - lo) / 4u);
for (uint32_t a = lo; a < hi; a += 4) {
auto io = exe_.read_word(a);
words.push_back(io.has_value() ? *io : 0u);
}
WsBackdropSite sites[64];
int ns = psx_ws_find_backdrop_windows(words.data(), (int)words.size(), lo,
sites, 64);
for (int i = 0; i < ns; i++)
ws_backdrop_sites_[sites[i].pc] = { sites[i].kind, sites[i].window_cols };
return ns;
}
GeneratedFunction CodeGenerator::generate_function(
const Function& func,
const ControlFlowGraph& cfg,
const std::string& fallthrough_name) {
GeneratedFunction result;
result.function_name = func.name;
result.signature = fmt::format("void {}(CPUState* cpu)", func.name);
// Widescreen auto cull (gated): detect the screen-extent reject signature so
// translate_instruction widens this function's width compares. Cleared for
// every function so it never leaks across functions.
ws_auto_cull_func_ = config_.ws_auto_screen_x_cull &&
func_has_screen_extent_cull(cfg);
// Widescreen backdrop preload (gated): detect each scrolling-backdrop column
// window so translate_instruction rewrites its START/END finalize. Cleared +
// repopulated per function so sites never leak across functions.
{
int bd_windows = detect_backdrop_windows(cfg);
if (bd_windows > 0)
fmt::print(" [ws backdrop] {} site(s) in {} (0x{:08X})\n",
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;
if (partial_block_cycle_count(c, cfg) != 0) {
body_ss << config_.indent << fmt::format(" case 0x{:08X}u:\n", c);
body_ss << emit_mid_block_cycle_charge(c, cfg, config_.indent + " ");
body_ss << config_.indent << fmt::format(" goto block_{:08X};\n", c);
} else {
body_ss << config_.indent
<< fmt::format(" case 0x{:08X}u: goto block_{:08X};\n", c, c);
}
cps_continuation_owner_[c] = func.start_addr;
}
if (config_.overlay_mode) {
// Overlay functions: FAIL CLOSED on a foreign interior entry (the
// Tomba/Tomba2 native-overlay "blue screen" wedge). Entry at the true
// prologue (cpu->pc == start_addr) runs from the top. Any OTHER non-zero
// _cont is a foreign interior entry (a range-ownership mismatch routed a
// PC this function does not own into it). The old `default: break` ran
// the function FROM ITS TOP -- corrupting shared CPU/RAM state and
// wedging. Instead restore the requested PC and signal the overlay
// dispatch to route it to the sanctioned dirty-RAM interpreter
// (psx_native_bad_entry). NEVER run from the top.
if (seen.insert(func.start_addr).second) {
body_ss << config_.indent
<< fmt::format(" case 0x{:08X}u: break; /* entry at prologue */\n",
func.start_addr);
}
body_ss << config_.indent
<< fmt::format(" default: cpu->pc = _cont; "
"psx_native_bad_entry(cpu, 0x{:08X}u, _cont); return;\n",
func.start_addr);
} else {
// Static (BIOS/boot-EXE) functions keep the legacy fall-through: the
// generated static trampoline does not consume the bad-entry signal,
// and static ranges are discovered once with no overlay multi-variant
// ownership ambiguity. (The fail-closed default is overlay-scoped.)
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);
if (config_.ws_sprite_tag_funcs.count(func.start_addr)) {
body_ss << config_.indent
<< "psx_ws_sprite_tag(cpu); /* widescreen: record prim ($a0) + anchor */\n";
}
if (config_.ws_backdrop_unsquash_funcs.count(func.start_addr)) {
body_ss << config_.indent
<< "gte_ws_set_suppress(1); /* widescreen: un-squash far backdrop (8C) */\n";
}
// Add function comment
if (config_.emit_comments) {
body_ss << config_.indent
<< fmt::format("/* Address: 0x{:08X}, Size: {} bytes, Blocks: {} */\n",
func.start_addr, func.size, cfg.blocks.size());
}
body_ss << blocks_ss.str();
// Emit fallthrough call if the function ends without explicit control flow.
// A MIPS function truly falls through when:
// 1. It has NO jr/jr $ra block anywhere (no explicit return or indirect jump)
// 2. Its last basic block (by address) ends with no control flow (ControlFlowType::None)
// OR it ends with a branch/jump whose targets are all outside this function
// (indicating a split-function where the branch targets are in the next piece)
// Functions that have dead code after a jr $ra (e.g., padding) must NOT get a
// fallthrough call — condition 1 guards against that.
if (!fallthrough_name.empty() && !cfg.block_order.empty()) {
const BasicBlock& last_block = cfg.blocks.at(cfg.block_order.back());
bool needs_fallthrough = false;
if (last_block.exit_instr.type == ControlFlowType::None) {
needs_fallthrough = true;
} else if ((last_block.exit_instr.type == ControlFlowType::Branch ||
last_block.exit_instr.type == ControlFlowType::Jump) &&
last_block.successors.empty()) {
// Branch/jump with no in-function successors — likely a split-function bug.
// All targets are in the next function piece. Emit fallthrough as a safety net.
fmt::print(" WARNING: func_{:08X} has out-of-function {} at block_{:08X}, "
"emitting fallthrough to {}\n",
func.start_addr,
last_block.exit_instr.type == ControlFlowType::Branch ? "branch" : "jump",
last_block.start_addr, fallthrough_name);
needs_fallthrough = true;
}
if (needs_fallthrough) {
// Emit fallthrough if the last block is reachable (has predecessors
// or is the entry block). Dead code after a return (e.g., padding
// nops) has no predecessors and should NOT get a fallthrough call.
const BasicBlock& last = cfg.blocks.at(cfg.block_order.back());
bool is_reachable = last.is_entry || !last.predecessors.empty();
if (is_reachable) {
body_ss << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
fallthrough_name);
}
}
}
body_ss << " ; /* label compatibility: C requires a statement after the last label */\n";
body_ss << "}\n";
result.body = body_ss.str();
// Prepend annotation comment if one exists for this function's address
std::string annotation_prefix;
if (annotations_) {
const std::string& note = annotations_->lookup(func.start_addr);
if (!note.empty())
annotation_prefix = fmt::format("/* [NOTE] {} */\n", note);
}
result.full_code = annotation_prefix + result.signature + "\n" + result.body;
result.line_count = std::count(result.full_code.begin(), result.full_code.end(), '\n');
return result;
}
void CodeGenerator::scan_jr_tables(
const ControlFlowGraph& cfg,
std::map<uint32_t, std::vector<uint32_t>>& out_edges) {
for (const auto& [baddr, blk] : cfg.blocks) {
if (blk.exit_instr.type != ControlFlowType::JumpRegister) continue;
uint32_t jr_r = get_rs(blk.exit_instr.instruction);
// Run same backward scan as translate_basic_block to find table_base/count
uint32_t tb = 0, tc = 0;
{
uint32_t lw_base = 0xFF; int32_t lw_off = 0;
uint32_t ac[2] = {0xFF,0xFF}; uint32_t lui_v = 0;
int16_t av[2] = {0,0}; bool fa[2] = {false,false}, fl = false;
for (int b = 1; b <= 40; b++) {
uint32_t sa = blk.exit_instr.address - (uint32_t)(b*4);
if (sa < cfg.function_start) break;
auto so = exe_.read_word(sa); if (!so) break;
uint32_t si=*so, s_op=(si>>26)&0x3F, s_rs=(si>>21)&0x1F,
s_rt=(si>>16)&0x1F, s_rd=(si>>11)&0x1F, s_fn=si&0x3F;
if (s_op==0x23 && s_rt==jr_r && lw_base==0xFF) { lw_base=s_rs; lw_off=(int32_t)(int16_t)(si&0xFFFF); continue; }
if (s_op==0x00 && s_fn==0x21 && s_rd==lw_base && lw_base!=0xFF && ac[0]==0xFF) { ac[0]=s_rs; ac[1]=s_rt; continue; }
if (s_op==0x09 && ac[0]!=0xFF) { for(int c=0;c<2;c++){if(!fa[c]&&ac[c]!=0xFF&&s_rs==ac[c]&&s_rt==ac[c]){av[c]=(int16_t)(si&0xFFFF);fa[c]=true;break;}} continue; }
if (s_op==0x0F && ac[0]!=0xFF && !fl) { for(int c=0;c<2;c++){if(ac[c]!=0xFF&&s_rt==ac[c]){lui_v=((uint32_t)(si&0xFFFF))<<16;if(!fa[c]){av[c]=0;}av[0]=av[c];fa[0]=fa[c];fl=true;break;}} continue; }
if (s_op==0x0B && tc==0) { tc=si&0xFFFF; continue; }
if (fl && tc!=0) break;
}
if (fl && tc>0 && tc<512) tb = (lui_v+(fa[0]?(uint32_t)(int32_t)av[0]:0u)) + (uint32_t)lw_off;
}
if (tb==0 || tc==0) continue;
uint32_t rom_tb = ram_to_rom(tb, exe_);
for (uint32_t i = 0; i < tc; i++) {
auto eopt = exe_.read_word(rom_tb + i*4);
if (!eopt) continue;
uint32_t t = ram_to_rom(*eopt, exe_);
if (t >= cfg.function_start && t < cfg.function_end) {
out_edges[baddr].push_back(t);
if (!cfg.blocks.count(t))
extra_labels_.insert(t);
}
}
}
}
std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
const std::vector<const Function*>& aliases,
const ControlFlowGraph& cfg,
const std::string& fallthrough_name) {
std::vector<GeneratedFunction> results;
if (aliases.empty()) return results;
uint32_t host = aliases[0]->alias_walk_lo;
// Widescreen auto cull (gated): set per-group so a stale value from the last
// generate_function() call can't leak into this body. (See generate_function.)
ws_auto_cull_func_ = config_.ws_auto_screen_x_cull &&
func_has_screen_extent_cull(cfg);
// Backdrop preload sites for this alias group's shared CFG (see
// generate_function). Set per-group so a stale map can't leak in.
{
int bd_windows = detect_backdrop_windows(cfg);
if (bd_windows > 0)
fmt::print(" [ws backdrop] {} site(s) in alias host 0x{:08X}\n",
bd_windows, host);
}
// Jump-table edges + mid-block labels for this host's CFG.
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 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).
auto containing_block = [&](uint32_t a) -> uint32_t {
auto it = cfg.blocks.upper_bound(a);
if (it == cfg.blocks.begin()) return 0;
--it;
return (a >= it->second.start_addr && a <= it->second.end_addr)
? it->first : 0;
};
std::set<uint32_t> live_blocks;
std::vector<uint32_t> work;
for (const Function* a : aliases) {
uint32_t eb = containing_block(a->start_addr);
if (eb != 0) work.push_back(eb);
}
while (!work.empty()) {
uint32_t b = work.back();
work.pop_back();
if (!live_blocks.insert(b).second) continue;
const BasicBlock& blk = cfg.blocks.at(b);
for (uint32_t s : blk.successors) {
if (cfg.blocks.count(s)) work.push_back(s);
}
auto je = jr_table_edges.find(b);
if (je != jr_table_edges.end()) {
for (uint32_t t : je->second) {
uint32_t cb = containing_block(t);
if (cb != 0) work.push_back(cb);
}
}
}
// 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
<< fmt::format(" case 0x{:08X}u: goto block_{:08X};\n",
a->start_addr, a->start_addr);
}
body << config_.indent << " default: return;\n";
body << config_.indent << "}\n";
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.
if (!fallthrough_name.empty() && !cfg.block_order.empty() &&
live_blocks.count(cfg.block_order.back())) {
const BasicBlock& last_block = cfg.blocks.at(cfg.block_order.back());
bool needs_fallthrough =
(last_block.exit_instr.type == ControlFlowType::None) ||
((last_block.exit_instr.type == ControlFlowType::Branch ||
last_block.exit_instr.type == ControlFlowType::Jump) &&
last_block.successors.empty());
if (needs_fallthrough) {
body << fmt::format(" {}(cpu); /* fallthrough to next function */\n",
fallthrough_name);
}
}
body << " ; /* label compatibility */\n";
body << "}\n";
// One dispatchable wrapper per alias entry; the first carries the body.
for (size_t i = 0; i < aliases.size(); ++i) {
const Function* a = aliases[i];
GeneratedFunction gf;
gf.function_name = a->name;
gf.signature = fmt::format("void {}(CPUState* cpu)", a->name);
gf.body = fmt::format(
"{{\n"
" debug_server_log_call_entry(0x{:08X}u);\n"
" psx_alias_body_{:08X}(cpu, 0x{:08X}u); /* alias entry into host func_{:08X} */\n"
"}}\n",
a->start_addr, host, a->start_addr, host);
gf.full_code = (i == 0 ? body.str() + "\n" : std::string()) +
gf.signature + "\n" + gf.body;
gf.line_count = std::count(gf.full_code.begin(), gf.full_code.end(), '\n');
results.push_back(std::move(gf));
}
return results;
}
std::vector<GeneratedFunction> CodeGenerator::generate_all_functions(
const std::vector<Function>& functions,
const std::map<uint32_t, ControlFlowGraph>& cfgs) {
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());
// Build address -> function name map for fallthrough detection.
// When a function ends without explicit control flow (ControlFlowType::None)
// and the very next address is another function's start, it's a MIPS
// fall-through: the recompiler must emit a tail call to the continuation.
std::map<uint32_t, std::string> func_name_by_addr;
for (const Function& f : functions) {
func_name_by_addr[f.start_addr] = f.name;
}
// Group overlapping-alias entries by host: each group is emitted as one
// shared static body plus per-entry wrappers (see generate_alias_group),
// not as per-alias duplicates of the host's blocks.
std::map<uint32_t, std::vector<const Function*>> alias_groups;
for (const Function& f : functions) {
if (f.alias_walk_lo != 0) alias_groups[f.alias_walk_lo].push_back(&f);
}
std::set<uint32_t> alias_groups_emitted;
int total_lines = 0;
for (const Function& func : functions) {
if (func.alias_walk_lo != 0) {
if (!alias_groups_emitted.insert(func.alias_walk_lo).second) continue;
const auto& group = alias_groups.at(func.alias_walk_lo);
if (cfgs.count(func.start_addr) == 0) continue;
const ControlFlowGraph& cfg = cfgs.at(func.start_addr);
// Host-end fallthrough target (all group members share end_addr).
std::string fallthrough_name;
auto ft_it = func_name_by_addr.find(func.end_addr);
if (ft_it != func_name_by_addr.end()) fallthrough_name = ft_it->second;
auto group_funcs = generate_alias_group(group, cfg, fallthrough_name);
for (auto& gf : group_funcs) {
total_lines += gf.line_count;
results.push_back(std::move(gf));
}
continue;
}
if (func.is_data_section) {
GeneratedFunction stub;
stub.function_name = func.name;
stub.signature = fmt::format("void {}(CPUState* cpu)", func.name);
stub.body = fmt::format(
"{{\n psx_unknown_dispatch(cpu, 0x{:08X}u, 0x{:08X}u);\n}}\n",
func.start_addr, func.start_addr & 0x1FFFFFFFu);
stub.full_code = stub.signature + "\n" + stub.body;
stub.line_count = 4;
results.push_back(stub);
continue;
}
if (cfgs.count(func.start_addr) == 0) {
continue; // Skip if no CFG
}
const ControlFlowGraph& cfg = cfgs.at(func.start_addr);
// Detect fallthrough: is there a function immediately after this one?
std::string fallthrough_name;
uint32_t next_addr = func.start_addr + func.size;
auto ft_it = func_name_by_addr.find(next_addr);
if (ft_it != func_name_by_addr.end()) {
fallthrough_name = ft_it->second;
}
GeneratedFunction gen_func = generate_function(func, cfg, fallthrough_name);
total_lines += gen_func.line_count;
results.push_back(gen_func);
}
fmt::print("✓ Generated {} functions\n", results.size());
fmt::print("✓ Total C code: {} lines\n\n", total_lines);
return results;
}
std::string CodeGenerator::generate_file(
const std::vector<Function>& functions,
const std::map<uint32_t, ControlFlowGraph>& cfgs) {
std::stringstream ss;
// File header
ss << "/* Generated by PSXRecomp - PlayStation 1 Static Recompiler */\n\n";
// Include the generic PSX runtime header.
// This provides CPUState, GTE/trap declarations, and call_by_address().
ss << "#include \"psx_runtime.h\"\n\n";
ss << "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n";
ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_cols(int base); /* ws 2D bg tile-loop widen: col count (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_startcol(int col); /* ws 2D bg tile-loop widen: start tile col (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_startx(int x); /* ws 2D bg tile-loop widen: start screen-x (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_stream_left(int x); /* ws 2D bg tile-ring streamer: left edge (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_stream_right(int x); /* ws 2D bg tile-ring streamer: right edge (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_bufbase(int addr); /* ws 2D bg packet-buffer relocation (gpu.c) */\n";
ss << "extern int psx_ws_mmx6_bg_undercap(int counter);/* ws 2D bg per-frame tile cap (gpu.c) */\n";
ss << "extern int psx_game_option_store(uint32_t addr, int val); /* persisted OPTION restore-at-init (game_options.c) */\n";
ss << "extern uint32_t psx_ws_backdrop_value(uint32_t orig, int is_end, int window_cols); /* ws backdrop preload (gpu.c) */\n";
ss << "extern void gte_ws_set_suppress(int on); /* widescreen far-backdrop un-squash (gte.cpp) */\n\n";
// Emit reference implementations for unaligned memory helpers.
// These implement the MIPS lwl/lwr/swl/swr semantics.
// The runtime may override these by setting the corresponding function pointers
// in CPUState, but these standalone functions provide correct reference behavior.
ss << "/* --- Unaligned memory access helper implementations --- */\n";
ss << "\n";
ss << "/* LWL/LWR/SWL/SWR implement little-endian R3000A unaligned word\n";
ss << " * semantics. Keep these switch tables in sync with psx_interpreter.c\n";
ss << " * and dirty_ram_interp.c.\n";
ss << " */\n";
ss << "\n";
ss << "/* LWL: Load Word Left - merges high bytes from aligned word into rt.\n";
ss << " * addr is the effective (possibly unaligned) address.\n";
ss << " * rt_value is the current value of the destination register.\n";
ss << " */\n";
ss << "static uint32_t psx_lwl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n";
ss << " uint32_t word = cpu->read_word(addr & ~3u);\n";
ss << " switch (addr & 3u) {\n";
ss << " case 0: return (rt_value & 0x00FFFFFFu) | (word << 24);\n";
ss << " case 1: return (rt_value & 0x0000FFFFu) | (word << 16);\n";
ss << " case 2: return (rt_value & 0x000000FFu) | (word << 8);\n";
ss << " default: return word;\n";
ss << " }\n";
ss << "}\n";
ss << "\n";
ss << "/* LWR: Load Word Right - merges low bytes from aligned word into rt.\n";
ss << " * addr is the effective (possibly unaligned) address.\n";
ss << " * rt_value is the current value of the destination register.\n";
ss << " */\n";
ss << "static uint32_t psx_lwr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n";
ss << " uint32_t word = cpu->read_word(addr & ~3u);\n";
ss << " switch (addr & 3u) {\n";
ss << " case 0: return word;\n";
ss << " case 1: return (rt_value & 0xFF000000u) | (word >> 8);\n";
ss << " case 2: return (rt_value & 0xFFFF0000u) | (word >> 16);\n";
ss << " default: return (rt_value & 0xFFFFFF00u) | (word >> 24);\n";
ss << " }\n";
ss << "}\n";
ss << "\n";
ss << "/* SWL: Store Word Left - stores the high bytes of rt_value into memory.\n";
ss << " * addr is the effective (possibly unaligned) address.\n";
ss << " * rt_value is the value of the source register.\n";
ss << " */\n";
ss << "static void psx_swl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n";
ss << " uint32_t aligned_addr = addr & ~3u;\n";
ss << " uint32_t word = cpu->read_word(aligned_addr);\n";
ss << " switch (addr & 3u) {\n";
ss << " case 0: word = (word & 0xFFFFFF00u) | (rt_value >> 24); break;\n";
ss << " case 1: word = (word & 0xFFFF0000u) | (rt_value >> 16); break;\n";
ss << " case 2: word = (word & 0xFF000000u) | (rt_value >> 8); break;\n";
ss << " default: word = rt_value; break;\n";
ss << " }\n";
ss << " cpu->write_word(aligned_addr, word);\n";
ss << "}\n";
ss << "\n";
ss << "/* SWR: Store Word Right - stores the low bytes of rt_value into memory.\n";
ss << " * addr is the effective (possibly unaligned) address.\n";
ss << " * rt_value is the value of the source register.\n";
ss << " */\n";
ss << "static void psx_swr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n";
ss << " uint32_t aligned_addr = addr & ~3u;\n";
ss << " uint32_t word = cpu->read_word(aligned_addr);\n";
ss << " switch (addr & 3u) {\n";
ss << " case 0: word = rt_value; break;\n";
ss << " case 1: word = (word & 0x000000FFu) | (rt_value << 8); break;\n";
ss << " case 2: word = (word & 0x0000FFFFu) | (rt_value << 16); break;\n";
ss << " default: word = (word & 0x00FFFFFFu) | (rt_value << 24); break;\n";
ss << " }\n";
ss << " cpu->write_word(aligned_addr, word);\n";
ss << "}\n";
ss << "\n";
// Make mutable copies for potential function splitting
std::vector<Function> functions_mut(functions);
std::map<uint32_t, ControlFlowGraph> cfgs_mut(cfgs);
// Build initial set of known functions
std::set<uint32_t> known_addrs;
for (const auto& func : functions_mut) {
known_addrs.insert(func.start_addr);
}
// ---- Pre-pass: find mid-function branch/jump targets and split ----
// Scan all CFGs for branch/jump targets that fall within a function's
// range but are NOT at any function start. Split containing functions
// at discovered targets. Iterate until convergence (no new mid-targets
// found), since splitting creates new pieces whose branches may target
// other mid-piece addresses. Each subsequent pass only scans CFGs that
// were rebuilt in the prior pass.
//
// Previously hard-capped at 3 iterations; Tomba's `func_800905DC` had
// 2 mid-func targets (0x800905E4, 0x80090600) that needed a 4th pass.
// The unconverged stragglers fell through to the
// `call_by_address(cpu, 0xX); return;` emit path (code_generator.cpp
// lines 970/981/998), which at runtime missed the dispatch table and
// hit `psx_unknown_dispatch`. Surfaced by Phase B2 audit. The 16-pass
// cap is a safety limit; in practice each pass strictly reduces the
// remaining target set so convergence is fast.
//
// Overlay exact mode disables this: overlay branch targets are basic-block
// labels, not callable function entries, so splitting them into standalone
// functions produces broken dispatch — the mid-function-seed failure mode.
if (config_.split_mid_function_targets) {
std::set<uint32_t> cfgs_to_scan; // Which CFGs to scan (empty = all)
uint32_t exe_start = exe_.header.load_address;
uint32_t exe_end = exe_.end_address();
int total_new = 0;
// Safety cap only — the loop must run to convergence. Unconverged
// targets emit `call_by_address(mid-func); return;` which misses the
// dispatch table at runtime. 16 proved too low once data-scan
// promotions and alias entries widened the function set.
const int MAX_PASSES = 256;
bool converged = false;
for (int pass = 0; pass < MAX_PASSES; pass++) {
std::set<uint32_t> mid_targets;
// Scan either all CFGs (pass 0) or only rebuilt CFGs (pass 1+)
auto scan_cfg = [&](const ControlFlowGraph& cfg) {
for (const auto& [baddr, block] : cfg.blocks) {
auto check_target = [&](uint32_t target) {
if (target == 0) return;
if (cfg.blocks.count(target)) return;
if (known_addrs.count(target)) return;
if (target < exe_start || target >= exe_end) return;
if (target & 3) return;
mid_targets.insert(target);
};
if (block.exit_instr.type == ControlFlowType::Branch) {
check_target(block.exit_instr.target);
check_target(block.exit_instr.address + 8);
} else if (block.exit_instr.type == ControlFlowType::Jump) {
check_target(block.exit_instr.target);
}
}
};
if (cfgs_to_scan.empty()) {
for (const auto& [cfg_addr, cfg] : cfgs_mut) scan_cfg(cfg);
} else {
for (uint32_t addr : cfgs_to_scan) {
auto it = cfgs_mut.find(addr);
if (it != cfgs_mut.end()) scan_cfg(it->second);
}
}
if (mid_targets.empty()) { converged = true; break; }
fmt::print("Pre-pass {}: {} mid-function branch targets\n", pass + 1, mid_targets.size());
// Sort functions for binary search. Alias entries overlap their
// host's range and must never be treated as the containing
// function of a mid-target (that would split/truncate the alias
// instead of the host) — exclude them from containment.
std::sort(functions_mut.begin(), functions_mut.end(),
[](const Function& a, const Function& b) { return a.start_addr < b.start_addr; });
std::vector<uint32_t> func_starts;
for (const auto& f : functions_mut) {
if (f.alias_walk_lo == 0) func_starts.push_back(f.start_addr);
}
// Group targets by containing function
std::map<uint32_t, std::vector<uint32_t>> splits_by_func;
for (uint32_t target : mid_targets) {
auto it = std::upper_bound(func_starts.begin(), func_starts.end(), target);
if (it == func_starts.begin()) continue;
--it;
splits_by_func[*it].push_back(target);
}
// Split each affected function
std::vector<Function> new_funcs;
std::set<uint32_t> affected;
for (auto& [func_start, targets] : splits_by_func) {
std::sort(targets.begin(), targets.end());
for (auto& f : functions_mut) {
if (f.start_addr != func_start) continue;
if (targets.back() >= f.end_addr) break;
uint32_t orig_end = f.end_addr;
bool orig_data = f.is_data_section;
f.end_addr = targets[0];
f.size = f.end_addr - f.start_addr;
affected.insert(f.start_addr);
for (size_t ti = 0; ti < targets.size(); ti++) {
Function nf;
nf.start_addr = targets[ti];
nf.end_addr = (ti + 1 < targets.size()) ? targets[ti + 1] : orig_end;
nf.size = nf.end_addr - nf.start_addr;
nf.name = fmt::format("func_{:08X}", nf.start_addr);
nf.has_prologue = false;
nf.has_epilogue = false;
nf.stack_frame_size = 0;
nf.is_data_section = orig_data;
new_funcs.push_back(nf);
known_addrs.insert(nf.start_addr);
affected.insert(nf.start_addr);
}
break;
}
}
functions_mut.insert(functions_mut.end(), new_funcs.begin(), new_funcs.end());
std::sort(functions_mut.begin(), functions_mut.end(),
[](const Function& a, const Function& b) { return a.start_addr < b.start_addr; });
// Rebuild CFGs for affected functions
ControlFlowAnalyzer cfg_analyzer(exe_);
cfgs_to_scan.clear();
for (auto& f : functions_mut) {
if (affected.count(f.start_addr)) {
cfgs_mut[f.start_addr] = cfg_analyzer.analyze_function(f);
cfgs_to_scan.insert(f.start_addr);
}
}
total_new += (int)new_funcs.size();
fmt::print(" Created {} new entry points\n", new_funcs.size());
}
if (total_new > 0) {
fmt::print("Pre-pass total: {} new entry points, {} functions now\n",
total_new, functions_mut.size());
}
if (!converged) {
fmt::print("WARNING: mid-function split pre-pass did NOT converge after {} passes; "
"remaining targets will dispatch-miss at runtime\n", MAX_PASSES);
}
}
set_known_functions(known_addrs);
// Generate all functions first (to collect names)
auto gen_funcs = generate_all_functions(functions_mut, cfgs_mut);
// Forward declarations for all functions
if (!gen_funcs.empty()) {
ss << "/* Forward declarations */\n";
for (const auto& gen_func : gen_funcs) {
ss << gen_func.signature << ";\n";
}
ss << "\n";
}
// Function definitions
for (const auto& gen_func : gen_funcs) {
ss << gen_func.full_code << "\n";
}
return ss.str();
}
std::string CodeGenerator::generate_ranges_manifest(
const std::vector<Function>& functions,
const std::map<uint32_t, ControlFlowGraph>& cfgs) {
std::stringstream ss;
ss << "# psxrecomp overlay code-range manifest v1\n";
ss << "# F <entry> one per function (virtual entry address)\n";
ss << "# R <lo> <len> one per coalesced code range (hex, virtual)\n";
for (const auto& func : functions) {
auto it = cfgs.find(func.start_addr);
if (it == cfgs.end()) continue;
const ControlFlowGraph& cfg = it->second;
// Byte intervals [start_addr, end_addr+4) per block (end_addr is the
// last instruction, inclusive).
std::vector<std::pair<uint32_t, uint32_t>> iv;
for (const auto& [baddr, blk] : cfg.blocks) {
(void)baddr;
uint32_t lo = blk.start_addr;
uint32_t hi = blk.end_addr + 4u;
if (hi > lo) iv.emplace_back(lo, hi);
}
if (iv.empty()) continue;
// Merge overlapping/adjacent intervals. A gap > 0 between blocks is
// non-code (e.g. a jump table) and is intentionally left out.
std::sort(iv.begin(), iv.end());
std::vector<std::pair<uint32_t, uint32_t>> merged;
for (const auto& r : iv) {
if (!merged.empty() && r.first <= merged.back().second) {
if (r.second > merged.back().second) merged.back().second = r.second;
} else {
merged.push_back(r);
}
}
ss << fmt::format("F {:08X}\n", func.start_addr);
for (const auto& r : merged)
ss << fmt::format("R {:08X} {:X}\n", r.first, r.second - r.first);
}
return ss.str();
}
} // namespace PSXRecomp