mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
eac1a61e43
PR #171 fixed the PGXP hooks, which were space-joined onto emissions that end on a bare #endif (the PSX_ENABLE_BLOCK_CYCLES muldiv stall/latency blocks) and were therefore swallowed as "extra tokens" and silently discarded -- 3,187 hooks per title in Ape Escape alone, plus 1,024 in the BIOS. It fixed the instance. Two same-line appenders of the same class were left behind: 1. strict_translator.cpp -- the deferred-load PGXP_LOAD hook is still space-joined onto c_code_deferred. All five c_code_deferred producers currently end in plain statements, so it is benign TODAY, but the moment a block-cycles annotation lands on deferred loads it breaks exactly the way #171 just did, and just as silently. 2. code_generator.cpp -- the trailing disassembly comment is appended to the emission unconditionally. Reachable whenever append_pgxp_hooks returned early and left the emission ending on a directive, e.g. `mfhi $zero` / `mflo $zero` (rd == 0 takes an early return). The comment is then dropped and -Wendif-labels fires. Both now route through one predicate, emission_ends_on_preprocessor_directive(), so the hazard is closed by construction rather than one site at a time. Site 1 takes an unconditional newline (always valid where the space was); site 2 only breaks the line when the emission actually ends on a directive, so ordinary instructions keep their inline comment and the generated diff stays minimal. Measured effect on emitted code, against merged master: - SCPH1001 BIOS: byte-identical - OpenBIOS: 2 sites, both the deferred PGXP_LOAD moving to its own line, identical token sequence - Ape / Tomba 1 / Tomba 2 / MMX6: every function token-identical (3095/6891/2726/4023 functions each, none added, none removed, zero body changes) Adds emitter_directive_line_test (10 checks, registered with add_test) so the predicate cannot regress: it pins both real emission shapes that end on #endif and, just as importantly, the negative cases -- a false positive would inject stray newlines into every instruction. ctest before and after this change fails the identical 6 pre-existing tests (aot_overlay_discovery, launcher_vulkan_option, mod_load_acceleration, release_zip, sdl3_main_single_include, vk_present_wait_stage); no new failures. full_function_emitter_test and l2_structural_test both pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1491 lines
73 KiB
C++
1491 lines
73 KiB
C++
// strict_translator.cpp — see header for design contract.
|
|
//
|
|
// FIRST_MILESTONE.md spec note: the milestone document literally lists
|
|
// "ERET" as a termination condition. The PS1's R3000A has no ERET — it has
|
|
// RFE (COP0 funct 0x10, full word 0x42000010). Per session approval on
|
|
// 2026-04-06, RFE is treated as the equivalent terminator and the manifest
|
|
// records this substitution explicitly.
|
|
|
|
#include "strict_translator.h"
|
|
#include "pgxp_hook_emitter.h" /* shared PGXP hook grammar */
|
|
#include "gte_register_classification.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
#include "fmt/format.h"
|
|
|
|
namespace PSXRecompV4 {
|
|
|
|
// RECURSION_BUG.md §25 — continuation-passing call/return. Under CPS, a
|
|
// directly-handled "void" syscall (Enter/ExitCriticalSection) must fall
|
|
// through to the inline post-syscall code (the guest's own jr $ra) instead of
|
|
// returning to a nested dispatcher, so the syscall is emitted as
|
|
// `if (psx_syscall(...)) return;`. psx_syscall returns 0 for those void
|
|
// syscalls and 1 when control transfers (cpu->pc set). Gated so legacy codegen
|
|
// stays byte-identical.
|
|
// CPS is the DEFAULT (RECURSION_BUG.md §25). Opt out (legacy) with PSX_CPS=0.
|
|
static const bool g_strict_cps = []() {
|
|
const char* e = std::getenv("PSX_CPS");
|
|
return e == nullptr || e[0] != '0';
|
|
}();
|
|
|
|
namespace {
|
|
|
|
// Emit a register-write that respects $zero being hardwired to 0.
|
|
// LUI/ORI/ADDIU/SLL with rd/rt == 0 are silent NOPs on real hardware.
|
|
std::string emit_gpr_write(uint8_t dest_reg, const std::string& rhs_expr) {
|
|
if (dest_reg == 0) {
|
|
return fmt::format("/* discarded write to $zero ({}) */", rhs_expr);
|
|
}
|
|
return fmt::format("cpu->gpr[{}] = {};", static_cast<int>(dest_reg), rhs_expr);
|
|
}
|
|
|
|
const char* gpr_name(uint8_t r) {
|
|
static const char* names[32] = {
|
|
"$zero","$at","$v0","$v1","$a0","$a1","$a2","$a3",
|
|
"$t0","$t1","$t2","$t3","$t4","$t5","$t6","$t7",
|
|
"$s0","$s1","$s2","$s3","$s4","$s5","$s6","$s7",
|
|
"$t8","$t9","$k0","$k1","$gp","$sp","$fp","$ra"
|
|
};
|
|
return names[r & 0x1F];
|
|
}
|
|
|
|
TranslateResult unsupported(const PSXRecomp::DecodedInstruction& d, const std::string& why) {
|
|
TranslateResult r;
|
|
r.supported = false;
|
|
r.fail_reason = fmt::format(
|
|
"addr=0x{:08X} raw=0x{:08X} opcode=0x{:02X}: {}",
|
|
d.address, d.raw, d.opcode, why);
|
|
return r;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction& d) {
|
|
TranslateResult r = translate_impl(d);
|
|
if (!r.supported) return r;
|
|
const uint32_t opcode = (d.raw >> 26) & 0x3F;
|
|
/* Shared PGXP hook grammar (ENHANCEMENTS.md G1.10). LWC2/SWC2 capture
|
|
* their raw word inside translate_impl and are skipped here. */
|
|
if (opcode != 0x32 && opcode != 0x3A && !r.c_code.empty())
|
|
PSXRecomp::append_pgxp_hooks(d.raw, r.c_code);
|
|
/* The deferred load-delay variant assigns into the function-scope
|
|
* psx_ldd_<addr> temp instead of the GPR; hook it with that temp as the
|
|
* loaded value (the shadow keys on the value, not on where it lands —
|
|
* the emitter's later writeback makes the GPR match). */
|
|
if (r.load_dest >= 0 && !r.c_code_deferred.empty()) {
|
|
const uint32_t rs = (d.raw >> 21) & 0x1F;
|
|
const int16_t offset = static_cast<int16_t>(d.raw & 0xFFFF);
|
|
/* Newline, never a space: if the deferred emission ever ends on a
|
|
* preprocessor directive (the block-cycles annotations already do
|
|
* elsewhere in this file), a space-joined hook lands on the directive
|
|
* line and is silently discarded -- the PR #171 defect. Unconditional
|
|
* "\n" is always valid where the space was, so non-directive
|
|
* emissions are unaffected apart from whitespace. */
|
|
r.c_code_deferred = fmt::format(
|
|
"{{ uint32_t _pgxa = cpu->gpr[{}] + {}; {}\n "
|
|
"PGXP_LOAD(0x{:08X}u, _pgxa, psx_ldd_{:08X}); }}",
|
|
static_cast<int>(rs), static_cast<int>(offset),
|
|
r.c_code_deferred, d.raw, d.address);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
TranslateResult StrictTranslator::translate_impl(const PSXRecomp::DecodedInstruction& d) {
|
|
TranslateResult r;
|
|
|
|
// NOP (raw == 0, i.e. SLL $zero,$zero,0) — emit as a comment with no semantics.
|
|
if (d.raw == 0) {
|
|
r.supported = true;
|
|
r.c_code = "/* nop */";
|
|
r.comment = "nop";
|
|
return r;
|
|
}
|
|
|
|
const uint32_t opcode = (d.raw >> 26) & 0x3F;
|
|
|
|
// SPECIAL — funct field selects op
|
|
if (opcode == 0x00) {
|
|
const uint32_t funct = d.raw & 0x3F;
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint8_t rd = (d.raw >> 11) & 0x1F;
|
|
const uint8_t shamt = (d.raw >> 6) & 0x1F;
|
|
|
|
switch (funct) {
|
|
case 0x00: { // SLL rd, rt, shamt (raw==0 caught above as NOP)
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] << {}u", static_cast<int>(rt), static_cast<int>(shamt)));
|
|
r.comment = fmt::format("sll {}, {}, {}", gpr_name(rd), gpr_name(rt), shamt);
|
|
return r;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Shifts (Phase 1b B(3)). The shamt field is already 5 bits in the
|
|
// SPECIAL encoding so SRL/SRA emit a literal in [0,31] — that is
|
|
// always a defined shift count for a 32-bit C type. SLLV/SRLV/SRAV
|
|
// take the shift count from rs[4:0]; we mask explicitly with
|
|
// `& 0x1Fu` both to match real R3000A behavior and to keep the C
|
|
// operation out of undefined-behavior land. SRL/SRLV are logical
|
|
// (zero-fill); SRA/SRAV are arithmetic (sign-fill) and are emitted
|
|
// via a cast through int32_t — gcc and MSVC both implement signed
|
|
// right shift as arithmetic, and the C standard permits this as
|
|
// implementation-defined behavior.
|
|
//
|
|
// SRLV (funct 0x06) has zero inventory hits in this BIOS but is
|
|
// implemented anyway for ISA completeness, per session approval
|
|
// (B(3) checkpoint). It is the *only* opcode in the strict
|
|
// translator that is intentionally implemented without a current
|
|
// inventory hit — every other "no inventory hit, no handler" rule
|
|
// still applies. If a future audit asks "why is this here", the
|
|
// answer is in the B(3) checkpoint history: ISA-completeness
|
|
// exception, deliberately scoped to SRLV alone.
|
|
// -----------------------------------------------------------------
|
|
|
|
case 0x02: { // SRL rd, rt, shamt -- logical right shift
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] >> {}u",
|
|
static_cast<int>(rt), static_cast<int>(shamt)));
|
|
r.comment = fmt::format("srl {}, {}, {}", gpr_name(rd), gpr_name(rt), shamt);
|
|
return r;
|
|
}
|
|
|
|
case 0x03: { // SRA rd, rt, shamt -- arithmetic right shift
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] >> {}u)",
|
|
static_cast<int>(rt), static_cast<int>(shamt)));
|
|
r.comment = fmt::format("sra {}, {}, {}", gpr_name(rd), gpr_name(rt), shamt);
|
|
return r;
|
|
}
|
|
|
|
case 0x04: { // SLLV rd, rt, rs -- variable left shift, rs[4:0]
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] << (cpu->gpr[{}] & 0x1Fu)",
|
|
static_cast<int>(rt), static_cast<int>(rs)));
|
|
r.comment = fmt::format("sllv {}, {}, {}", gpr_name(rd), gpr_name(rt), gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
case 0x06: { // SRLV rd, rt, rs -- variable logical right shift, rs[4:0]
|
|
// ISA-completeness exception: zero inventory hits, but
|
|
// implemented to keep the SLLV/SRLV/SRAV trio symmetric.
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] >> (cpu->gpr[{}] & 0x1Fu)",
|
|
static_cast<int>(rt), static_cast<int>(rs)));
|
|
r.comment = fmt::format("srlv {}, {}, {}", gpr_name(rd), gpr_name(rt), gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
case 0x07: { // SRAV rd, rt, rs -- variable arithmetic right, rs[4:0]
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] >> (cpu->gpr[{}] & 0x1Fu))",
|
|
static_cast<int>(rt), static_cast<int>(rs)));
|
|
r.comment = fmt::format("srav {}, {}, {}", gpr_name(rd), gpr_name(rt), gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// HI/LO + multiply/divide (Phase 1b B(4)).
|
|
//
|
|
// Architectural result semantics only — no load-delay simulation,
|
|
// no interlock modelling, no per-instruction cycle counts. Each
|
|
// op writes/reads HI and LO directly via cpu->hi / cpu->lo.
|
|
//
|
|
// Divide-by-zero and INT32_MIN/-1 edge cases match documented
|
|
// R3000A behavior (PSX-SPX / IDT R3000 manual). The C-level
|
|
// guards exist because:
|
|
// - C `a / 0u` and `a / 0` are undefined behavior, so we must
|
|
// branch around them or the compiler is free to delete code
|
|
// that depends on the result.
|
|
// - C `INT32_MIN / -1` is undefined behavior on two's-complement
|
|
// hosts (the mathematical result is 2^31, unrepresentable in
|
|
// int32_t), so we must branch around it explicitly.
|
|
//
|
|
// MULT (signed) is NOT implemented: zero inventory hits, no
|
|
// ISA-completeness exception requested.
|
|
// -----------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------
|
|
// Traps (Phase 1b B(10)). SYSCALL and BREAK both translate to a
|
|
// fail-loud extern call followed by an explicit `return;`. They
|
|
// are NON-terminators in the strict-translator sense — the slice
|
|
// walker continues past them — because:
|
|
// 1. Real R3000A SYSCALL/BREAK do NOT have a delay slot. The
|
|
// next sequential instruction is reached only after the
|
|
// kernel returns via RFE, not as a delay slot.
|
|
// 2. Marking them as terminators would cause the walker to
|
|
// fetch the next 4 bytes as a "delay slot", which in
|
|
// practice is real post-trap code (often `jr $ra; nop`
|
|
// for syscall wrappers like the one at 0xbfc0d8c4). The
|
|
// walker explicitly fails loud on terminator-in-delay-
|
|
// slot, so the slice would refuse to translate.
|
|
// 3. The runtime `return;` statement after the extern call
|
|
// halts the slice C function so the post-trap C code is
|
|
// dead at execution time. The walker still emits it as
|
|
// additional C, which is harmless.
|
|
//
|
|
// SYSCALL: per PS1 ABI the syscall number lives in $v0 (gpr[2]).
|
|
// The 20-bit immediate field [25:6] of the SYSCALL instruction is
|
|
// ignored by the BIOS and not passed to psx_syscall.
|
|
//
|
|
// BREAK: the 20-bit immediate field [25:6] is the trap code; the
|
|
// BIOS uses specific codes like 0x1c00 for divide-by-zero.
|
|
// psx_break receives the code AND the PC for diagnostic
|
|
// purposes.
|
|
//
|
|
// No exception model. No control-flow emulation. No return to
|
|
// execution. The Phase 2 runtime defines psx_syscall and
|
|
// psx_break; until then, linking fails by design.
|
|
// -----------------------------------------------------------------
|
|
|
|
case 0x0C: { // SYSCALL
|
|
r.supported = true;
|
|
if (g_strict_cps) {
|
|
// CPS: void syscalls (psx_syscall returns 0) fall through to
|
|
// the inline post-syscall code (the guest's jr $ra), which
|
|
// returns to the caller via the flat trampoline. Transfers
|
|
// (return 1) set cpu->pc and return to the trampoline.
|
|
r.c_code = fmt::format(
|
|
"cpu->pc = 0x{:08X}u; if (psx_syscall(cpu, cpu->gpr[2])) return;",
|
|
d.address);
|
|
} else {
|
|
r.c_code = fmt::format(
|
|
"cpu->pc = 0x{:08X}u; psx_syscall(cpu, cpu->gpr[2]); return;",
|
|
d.address);
|
|
}
|
|
r.comment = "syscall";
|
|
return r;
|
|
}
|
|
|
|
case 0x0D: { // BREAK
|
|
const uint32_t code = (d.raw >> 6) & 0xFFFFFu;
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"psx_break(cpu, 0x{:05X}u, 0x{:08X}u); return;",
|
|
code, d.address);
|
|
r.comment = fmt::format("break 0x{:X}", code);
|
|
return r;
|
|
}
|
|
|
|
case 0x10: { // MFHI rd
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd, "cpu->hi")
|
|
+ "\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_stall(cpu);\n#endif";
|
|
r.comment = fmt::format("mfhi {}", gpr_name(rd));
|
|
return r;
|
|
}
|
|
|
|
case 0x11: { // MTHI rs
|
|
r.supported = true;
|
|
r.c_code = fmt::format("cpu->hi = cpu->gpr[{}];",
|
|
static_cast<int>(rs));
|
|
r.comment = fmt::format("mthi {}", gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
case 0x12: { // MFLO rd
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd, "cpu->lo")
|
|
+ "\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_stall(cpu);\n#endif";
|
|
r.comment = fmt::format("mflo {}", gpr_name(rd));
|
|
return r;
|
|
}
|
|
|
|
case 0x13: { // MTLO rs
|
|
r.supported = true;
|
|
r.c_code = fmt::format("cpu->lo = cpu->gpr[{}];",
|
|
static_cast<int>(rs));
|
|
r.comment = fmt::format("mtlo {}", gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
case 0x18: { // MULT rs, rt -- 32x32 -> 64 signed, HI:LO
|
|
// No MULT was ever reached from the SCPH1001 seed set, so
|
|
// this case first became reachable with OpenBIOS (modern GCC
|
|
// emits MULT freely). Same shape as MULTU below, signed.
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"{{ int64_t psx_p = (int64_t)(int32_t)cpu->gpr[{}] * (int64_t)(int32_t)cpu->gpr[{}]; "
|
|
"cpu->lo = (uint32_t)((uint64_t)psx_p & 0xFFFFFFFFu); "
|
|
"cpu->hi = (uint32_t)((uint64_t)psx_p >> 32); }}"
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, psx_mult_latency_s(cpu->gpr[{}]));\n#endif",
|
|
static_cast<int>(rs), static_cast<int>(rt), static_cast<int>(rs));
|
|
r.comment = fmt::format("mult {}, {}", gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x19: { // MULTU rs, rt -- 32x32 -> 64 unsigned, HI:LO
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"{{ uint64_t psx_p = (uint64_t)cpu->gpr[{}] * (uint64_t)cpu->gpr[{}]; "
|
|
"cpu->lo = (uint32_t)(psx_p & 0xFFFFFFFFu); "
|
|
"cpu->hi = (uint32_t)(psx_p >> 32); }}"
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, psx_mult_latency_u(cpu->gpr[{}]));\n#endif",
|
|
static_cast<int>(rs), static_cast<int>(rt), static_cast<int>(rs));
|
|
r.comment = fmt::format("multu {}, {}", gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x1A: { // DIV rs, rt -- signed, with divide-by-zero and overflow guards
|
|
r.supported = true;
|
|
// R3000A divide-by-zero (signed):
|
|
// if numerator >= 0: LO = 0xFFFFFFFF (-1), HI = numerator
|
|
// if numerator < 0: LO = 0x00000001 ( 1), HI = numerator
|
|
// R3000A INT32_MIN / -1 (would overflow int32_t):
|
|
// LO = 0x80000000, HI = 0
|
|
// Normal path: compute remainder via subtraction
|
|
// (r = n - q*d) instead of the C `%` operator, so the
|
|
// result does not depend on whether the host C compiler
|
|
// implements `%` as truncated, floored, or
|
|
// implementation-defined for signed operands. Since
|
|
// |q*d| <= |n|, neither the multiply nor the subtract
|
|
// can overflow int32_t when n is representable.
|
|
r.c_code = fmt::format(
|
|
"{{ int32_t psx_n = (int32_t)cpu->gpr[{}]; "
|
|
"int32_t psx_d = (int32_t)cpu->gpr[{}]; "
|
|
"if (psx_d == 0) {{ "
|
|
"cpu->lo = (psx_n >= 0) ? 0xFFFFFFFFu : 0x00000001u; "
|
|
"cpu->hi = (uint32_t)psx_n; "
|
|
"}} else if (psx_n == INT32_MIN && psx_d == -1) {{ "
|
|
"cpu->lo = 0x80000000u; "
|
|
"cpu->hi = 0u; "
|
|
"}} else {{ "
|
|
"int32_t psx_q = psx_n / psx_d; "
|
|
"int32_t psx_r = psx_n - (psx_q * psx_d); "
|
|
"cpu->lo = (uint32_t)psx_q; "
|
|
"cpu->hi = (uint32_t)psx_r; "
|
|
"}} }}"
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, 37u);\n#endif",
|
|
static_cast<int>(rs), static_cast<int>(rt));
|
|
r.comment = fmt::format("div {}, {}", gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x1B: { // DIVU rs, rt -- unsigned, with divide-by-zero guard
|
|
r.supported = true;
|
|
// R3000A divide-by-zero (unsigned):
|
|
// LO = 0xFFFFFFFF, HI = numerator
|
|
// Normal path: compute remainder via subtraction so the
|
|
// result is independent of host `%` behavior, matching
|
|
// the DIV case above. Unsigned `*` wraps mod 2^32 in C
|
|
// but |q*d| <= n so no wrap actually occurs here.
|
|
r.c_code = fmt::format(
|
|
"{{ uint32_t psx_n = cpu->gpr[{}]; "
|
|
"uint32_t psx_d = cpu->gpr[{}]; "
|
|
"if (psx_d == 0) {{ "
|
|
"cpu->lo = 0xFFFFFFFFu; "
|
|
"cpu->hi = psx_n; "
|
|
"}} else {{ "
|
|
"uint32_t psx_q = psx_n / psx_d; "
|
|
"uint32_t psx_r = psx_n - (psx_q * psx_d); "
|
|
"cpu->lo = psx_q; "
|
|
"cpu->hi = psx_r; "
|
|
"}} }}"
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_muldiv_set(cpu, 37u);\n#endif",
|
|
static_cast<int>(rs), static_cast<int>(rt));
|
|
r.comment = fmt::format("divu {}, {}", gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
// MULT (signed) intentionally not implemented:
|
|
// no inventory hit -> forbidden by project rules.
|
|
// (See B(4) checkpoint and the SRLV ISA-completeness exception
|
|
// memory: SRLV is the *only* opcode allowed without an inventory
|
|
// hit, deliberately scoped to one carve-out.)
|
|
|
|
// -----------------------------------------------------------------
|
|
// ALU R-type batch (Phase 1b B(1)). All read rs/rt/rd from the
|
|
// standard SPECIAL fields decoded above. None of these touch HI/LO,
|
|
// none take a shift amount.
|
|
// -----------------------------------------------------------------
|
|
|
|
case 0x20: { // ADD rd, rs, rt -- signed; traps on overflow
|
|
r.supported = true;
|
|
// Even if rd == 0 the trap check must still execute, because
|
|
// real R3000A raises overflow before discarding the result.
|
|
// We keep the rs/rt fetch + overflow probe + (optional) write
|
|
// in one statement block. The widened int64 path is portable
|
|
// across mingw64 and MSVC; __builtin_add_overflow would also
|
|
// work but is gcc-specific.
|
|
std::string write_part;
|
|
if (rd == 0) {
|
|
write_part = "/* discarded write to $zero */";
|
|
} else {
|
|
write_part = fmt::format("cpu->gpr[{}] = (uint32_t)(int32_t)psx_r;",
|
|
static_cast<int>(rd));
|
|
}
|
|
r.c_code = fmt::format(
|
|
"{{ int32_t psx_a = (int32_t)cpu->gpr[{}]; "
|
|
"int32_t psx_b = (int32_t)cpu->gpr[{}]; "
|
|
"int64_t psx_r = (int64_t)psx_a + (int64_t)psx_b; "
|
|
"if (psx_r < (int64_t)INT32_MIN || psx_r > (int64_t)INT32_MAX) "
|
|
"{{ psx_arith_overflow(cpu); return; }} "
|
|
"{} }}",
|
|
static_cast<int>(rs), static_cast<int>(rt), write_part);
|
|
r.comment = fmt::format("add {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x21: { // ADDU rd, rs, rt -- unsigned wrap, no trap
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] + cpu->gpr[{}]",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("addu {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x22: // SUB rd, rs, rt -- same as SUBU on PSX (no overflow trap in recomp)
|
|
case 0x23: { // SUBU rd, rs, rt -- unsigned wrap, no trap
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] - cpu->gpr[{}]",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("subu {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x24: { // AND rd, rs, rt
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] & cpu->gpr[{}]",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("and {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x25: { // OR rd, rs, rt
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] | cpu->gpr[{}]",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("or {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x26: { // XOR rd, rs, rt
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("cpu->gpr[{}] ^ cpu->gpr[{}]",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("xor {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x27: { // NOR rd, rs, rt -- ~(rs | rt)
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("~(cpu->gpr[{}] | cpu->gpr[{}])",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("nor {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x2A: { // SLT rd, rs, rt -- signed compare
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] < (int32_t)cpu->gpr[{}] ? 1 : 0)",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("slt {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x2B: { // SLTU rd, rs, rt -- unsigned compare
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rd,
|
|
fmt::format("(uint32_t)(cpu->gpr[{}] < cpu->gpr[{}] ? 1 : 0)",
|
|
static_cast<int>(rs), static_cast<int>(rt)));
|
|
r.comment = fmt::format("sltu {}, {}, {}", gpr_name(rd), gpr_name(rs), gpr_name(rt));
|
|
return r;
|
|
}
|
|
|
|
case 0x08: { // JR rs (slice terminator)
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "jr";
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_jt_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs));
|
|
r.c_code = fmt::format(
|
|
"cpu->pc = psx_jt_{:08X}; /* jr {} -- slice terminator (latched indirect) */ return;",
|
|
d.address, gpr_name(rs));
|
|
r.comment = fmt::format("jr {}", gpr_name(rs));
|
|
return r;
|
|
}
|
|
case 0x09: { // JALR rd, rs (slice terminator)
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "jalr";
|
|
std::string link;
|
|
if (rd != 0) {
|
|
link = fmt::format(
|
|
"cpu->gpr[{}] = 0x{:08X}u; /* link */ ",
|
|
static_cast<int>(rd), d.address + 8);
|
|
}
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_jt_{:08X} = cpu->gpr[{}]; {}",
|
|
d.address, static_cast<int>(rs), link);
|
|
r.c_code = fmt::format(
|
|
"cpu->pc = psx_jt_{:08X}; /* jalr {}, {} -- slice terminator (latched indirect) */ return;",
|
|
d.address, gpr_name(rd), gpr_name(rs));
|
|
r.comment = fmt::format("jalr {}, {}", gpr_name(rd), gpr_name(rs));
|
|
return r;
|
|
}
|
|
default:
|
|
return unsupported(d,
|
|
fmt::format("SPECIAL funct 0x{:02X} not implemented in Phase 1a strict translator", funct));
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Conditional branches (Phase 1b B(5), corrected after hazard scan).
|
|
//
|
|
// Encoding:
|
|
// - BEQ (op 0x04), BNE (op 0x05): rs, rt, simm16
|
|
// - BLEZ (op 0x06), BGTZ (op 0x07): rs, simm16 (rt field == 0)
|
|
// - BLTZ (REGIMM op 0x01, rt 0x00), BGEZ (REGIMM op 0x01, rt 0x01): rs, simm16
|
|
//
|
|
// Target = (branch_pc + 4) + (sign_extend(simm16) * 4). Computed at
|
|
// translate time so the emitted C contains a literal target address.
|
|
//
|
|
// Pre-delay snapshot model:
|
|
//
|
|
// Real R3000A: the branch decision uses rs/rt values from BEFORE
|
|
// the delay-slot instruction executes. The slice walker's emit
|
|
// order is delay-slot-first then terminator, so any naive
|
|
// `cpu->gpr[rs]` read in the terminator c_code would see
|
|
// POST-delay state. The BIOS hits this case 441 times (per
|
|
// `tools/scan_branch_delay_hazards.py`) — it is not a rare edge.
|
|
//
|
|
// Fix: TranslateResult.pre_delay_code captures snapshots of all
|
|
// branch operands into uniquely-named function-scope C locals,
|
|
// and the walker emits pre_delay_code BEFORE the delay slot.
|
|
// Each branch's c_code reads ONLY the snapshot variables, never
|
|
// cpu->gpr[rs/rt] directly. Snapshot variable names embed the
|
|
// branch's address (uppercase hex) so they are unique within the
|
|
// slice C function and never collide.
|
|
//
|
|
// This makes the C output architecturally correct for ALL
|
|
// branches, regardless of what their delay slot does.
|
|
//
|
|
// BLTZAL / BGEZAL (REGIMM rt 0x10, 0x11) are NOT implemented: zero
|
|
// inventory hits.
|
|
// -----------------------------------------------------------------
|
|
|
|
// BEQ rs, rt, simm16 (op 0x04)
|
|
if (opcode == 0x04) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t target = d.address + 4 + static_cast<uint32_t>(simm * 4);
|
|
const uint32_t fallthrough = d.address + 8;
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "branch_beq";
|
|
r.terminator_target = target;
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_brA_{:08X} = cpu->gpr[{}]; "
|
|
"uint32_t psx_brB_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs),
|
|
d.address, static_cast<int>(rt));
|
|
r.c_code = fmt::format(
|
|
"if (psx_brA_{:08X} == psx_brB_{:08X}) {{ cpu->pc = 0x{:08X}u; return; }} "
|
|
"cpu->pc = 0x{:08X}u; return;",
|
|
d.address, d.address, target, fallthrough);
|
|
r.comment = fmt::format("beq {}, {}, 0x{:08X}", gpr_name(rs), gpr_name(rt), target);
|
|
return r;
|
|
}
|
|
|
|
// BNE rs, rt, simm16 (op 0x05)
|
|
if (opcode == 0x05) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t target = d.address + 4 + static_cast<uint32_t>(simm * 4);
|
|
const uint32_t fallthrough = d.address + 8;
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "branch_bne";
|
|
r.terminator_target = target;
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_brA_{:08X} = cpu->gpr[{}]; "
|
|
"uint32_t psx_brB_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs),
|
|
d.address, static_cast<int>(rt));
|
|
r.c_code = fmt::format(
|
|
"if (psx_brA_{:08X} != psx_brB_{:08X}) {{ cpu->pc = 0x{:08X}u; return; }} "
|
|
"cpu->pc = 0x{:08X}u; return;",
|
|
d.address, d.address, target, fallthrough);
|
|
r.comment = fmt::format("bne {}, {}, 0x{:08X}", gpr_name(rs), gpr_name(rt), target);
|
|
return r;
|
|
}
|
|
|
|
// BLEZ rs, simm16 (op 0x06) -- branch if (int32)rs <= 0
|
|
if (opcode == 0x06) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t target = d.address + 4 + static_cast<uint32_t>(simm * 4);
|
|
const uint32_t fallthrough = d.address + 8;
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "branch_blez";
|
|
r.terminator_target = target;
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_brA_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs));
|
|
r.c_code = fmt::format(
|
|
"if ((int32_t)psx_brA_{:08X} <= 0) {{ cpu->pc = 0x{:08X}u; return; }} "
|
|
"cpu->pc = 0x{:08X}u; return;",
|
|
d.address, target, fallthrough);
|
|
r.comment = fmt::format("blez {}, 0x{:08X}", gpr_name(rs), target);
|
|
return r;
|
|
}
|
|
|
|
// BGTZ rs, simm16 (op 0x07) -- branch if (int32)rs > 0
|
|
if (opcode == 0x07) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t target = d.address + 4 + static_cast<uint32_t>(simm * 4);
|
|
const uint32_t fallthrough = d.address + 8;
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "branch_bgtz";
|
|
r.terminator_target = target;
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_brA_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs));
|
|
r.c_code = fmt::format(
|
|
"if ((int32_t)psx_brA_{:08X} > 0) {{ cpu->pc = 0x{:08X}u; return; }} "
|
|
"cpu->pc = 0x{:08X}u; return;",
|
|
d.address, target, fallthrough);
|
|
r.comment = fmt::format("bgtz {}, 0x{:08X}", gpr_name(rs), target);
|
|
return r;
|
|
}
|
|
|
|
// REGIMM (op 0x01): BLTZ (rt 0x00), BGEZ (rt 0x01).
|
|
// BLTZAL / BGEZAL (rt 0x10, 0x11) intentionally not implemented:
|
|
// no inventory hits.
|
|
if (opcode == 0x01) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt_field = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t target = d.address + 4 + static_cast<uint32_t>(simm * 4);
|
|
const uint32_t fallthrough = d.address + 8;
|
|
if (rt_field == 0x00) { // BLTZ
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "branch_bltz";
|
|
r.terminator_target = target;
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_brA_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs));
|
|
r.c_code = fmt::format(
|
|
"if ((int32_t)psx_brA_{:08X} < 0) {{ cpu->pc = 0x{:08X}u; return; }} "
|
|
"cpu->pc = 0x{:08X}u; return;",
|
|
d.address, target, fallthrough);
|
|
r.comment = fmt::format("bltz {}, 0x{:08X}", gpr_name(rs), target);
|
|
return r;
|
|
}
|
|
if (rt_field == 0x01) { // BGEZ
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "branch_bgez";
|
|
r.terminator_target = target;
|
|
r.pre_delay_code = fmt::format(
|
|
"uint32_t psx_brA_{:08X} = cpu->gpr[{}];",
|
|
d.address, static_cast<int>(rs));
|
|
r.c_code = fmt::format(
|
|
"if ((int32_t)psx_brA_{:08X} >= 0) {{ cpu->pc = 0x{:08X}u; return; }} "
|
|
"cpu->pc = 0x{:08X}u; return;",
|
|
d.address, target, fallthrough);
|
|
r.comment = fmt::format("bgez {}, 0x{:08X}", gpr_name(rs), target);
|
|
return r;
|
|
}
|
|
return unsupported(d,
|
|
fmt::format("REGIMM rt 0x{:02X} not implemented (only BLTZ/BGEZ have inventory hits in this BIOS)", rt_field));
|
|
}
|
|
|
|
// J (0x02) — direct jump, slice terminator per Option A (2026-04-06).
|
|
if (opcode == 0x02) {
|
|
const uint32_t target = ((d.address + 4) & 0xF0000000u) | ((d.raw & 0x03FFFFFFu) << 2);
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "j";
|
|
r.terminator_target = target;
|
|
r.c_code = fmt::format(
|
|
"cpu->pc = 0x{:08X}u; /* j 0x{:08X} -- slice terminator (direct) */ return;",
|
|
target, target);
|
|
r.comment = fmt::format("j 0x{:08X}", target);
|
|
return r;
|
|
}
|
|
|
|
// JAL (0x03) — direct call, slice terminator.
|
|
if (opcode == 0x03) {
|
|
const uint32_t target = ((d.address + 4) & 0xF0000000u) | ((d.raw & 0x03FFFFFFu) << 2);
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "jal";
|
|
r.terminator_target = target;
|
|
r.c_code = fmt::format(
|
|
"cpu->gpr[31] = 0x{:08X}u; /* link $ra */ "
|
|
"cpu->pc = 0x{:08X}u; /* jal 0x{:08X} -- slice terminator (direct) */ return;",
|
|
d.address + 8, target, target);
|
|
r.comment = fmt::format("jal 0x{:08X}", target);
|
|
return r;
|
|
}
|
|
|
|
// ADDI rt, rs, simm16 -- signed; traps on overflow
|
|
if (opcode == 0x08) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
// Same overflow rule as ADD: the trap probe must run even when
|
|
// rt == $zero, because real R3000A raises overflow before
|
|
// discarding the result.
|
|
std::string write_part;
|
|
if (rt == 0) {
|
|
write_part = "/* discarded write to $zero */";
|
|
} else {
|
|
write_part = fmt::format("cpu->gpr[{}] = (uint32_t)(int32_t)psx_r;",
|
|
static_cast<int>(rt));
|
|
}
|
|
r.c_code = fmt::format(
|
|
"{{ int32_t psx_a = (int32_t)cpu->gpr[{}]; "
|
|
"int32_t psx_b = ({}); "
|
|
"int64_t psx_r = (int64_t)psx_a + (int64_t)psx_b; "
|
|
"if (psx_r < (int64_t)INT32_MIN || psx_r > (int64_t)INT32_MAX) "
|
|
"{{ psx_arith_overflow(cpu); return; }} "
|
|
"{} }}",
|
|
static_cast<int>(rs), simm, write_part);
|
|
r.comment = fmt::format("addi {}, {}, {}", gpr_name(rt), gpr_name(rs), simm);
|
|
return r;
|
|
}
|
|
|
|
// ADDIU rt, rs, simm16
|
|
if (opcode == 0x09) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))",
|
|
static_cast<int>(rs), simm));
|
|
r.comment = fmt::format("addiu {}, {}, {}", gpr_name(rt), gpr_name(rs), simm);
|
|
return r;
|
|
}
|
|
|
|
// SLTI rt, rs, simm16 -- signed compare against sign-extended imm
|
|
if (opcode == 0x0A) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] < ({}) ? 1 : 0)",
|
|
static_cast<int>(rs), simm));
|
|
r.comment = fmt::format("slti {}, {}, {}", gpr_name(rt), gpr_name(rs), simm);
|
|
return r;
|
|
}
|
|
|
|
// SLTIU rt, rs, simm16 -- unsigned compare against SIGN-extended imm
|
|
// (the immediate is sign-extended to 32 bits FIRST, then both operands
|
|
// are compared as unsigned. Standard MIPS quirk.)
|
|
if (opcode == 0x0B) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t uimm_sext = static_cast<uint32_t>(simm);
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("(uint32_t)(cpu->gpr[{}] < 0x{:08X}u ? 1 : 0)",
|
|
static_cast<int>(rs), uimm_sext));
|
|
r.comment = fmt::format("sltiu {}, {}, {}", gpr_name(rt), gpr_name(rs), simm);
|
|
return r;
|
|
}
|
|
|
|
// ANDI rt, rs, uimm16 -- zero-extended immediate
|
|
if (opcode == 0x0C) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint16_t uimm = static_cast<uint16_t>(d.raw & 0xFFFF);
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("cpu->gpr[{}] & 0x{:X}u", static_cast<int>(rs), uimm));
|
|
r.comment = fmt::format("andi {}, {}, 0x{:X}", gpr_name(rt), gpr_name(rs), uimm);
|
|
return r;
|
|
}
|
|
|
|
// ORI rt, rs, uimm16
|
|
if (opcode == 0x0D) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint16_t uimm = static_cast<uint16_t>(d.raw & 0xFFFF);
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("cpu->gpr[{}] | 0x{:X}u", static_cast<int>(rs), uimm));
|
|
r.comment = fmt::format("ori {}, {}, 0x{:X}", gpr_name(rt), gpr_name(rs), uimm);
|
|
return r;
|
|
}
|
|
|
|
// XORI rt, rs, uimm16 -- zero-extended immediate
|
|
if (opcode == 0x0E) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint16_t uimm = static_cast<uint16_t>(d.raw & 0xFFFF);
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("cpu->gpr[{}] ^ 0x{:X}u", static_cast<int>(rs), uimm));
|
|
r.comment = fmt::format("xori {}, {}, 0x{:X}", gpr_name(rt), gpr_name(rs), uimm);
|
|
return r;
|
|
}
|
|
|
|
// LUI rt, imm16 (rs field is unused)
|
|
if (opcode == 0x0F) {
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint16_t imm = static_cast<uint16_t>(d.raw & 0xFFFF);
|
|
r.supported = true;
|
|
r.c_code = emit_gpr_write(rt,
|
|
fmt::format("0x{:08X}u", static_cast<uint32_t>(imm) << 16));
|
|
r.comment = fmt::format("lui {}, 0x{:X}", gpr_name(rt), imm);
|
|
return r;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Phase 1 rule: unaligned access = fail loud (no exception model yet)
|
|
// This will be replaced by proper AdEL/AdES handling in Phase 2 runtime
|
|
// -----------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------
|
|
// Loads (Phase 1b B(6)). I-type: base in rs, dest in rt, signed
|
|
// 16-bit byte offset.
|
|
//
|
|
// addr = (int32_t)cpu->gpr[base] + sign_extend(simm16)
|
|
//
|
|
// Sign vs zero extension:
|
|
// LB (0x20) -> sign-extend 8 -> 32 via (int32_t)(int8_t)
|
|
// LH (0x21) -> sign-extend 16 -> 32 via (int32_t)(int16_t)
|
|
// LBU (0x24) -> zero-extend 8 -> 32 via (uint32_t)
|
|
// LHU (0x25) -> zero-extend 16 -> 32 via (uint32_t)
|
|
// LW (0x23) -> 32-bit, no extension
|
|
//
|
|
// $zero handling: even when rt == $zero, the memory read still
|
|
// executes — real R3000A reads memory regardless of the destination
|
|
// register, and MMIO reads can have side effects (status registers
|
|
// that auto-clear on read, FIFOs that pop, etc.). Therefore the
|
|
// emit-to-$zero variant wraps the read in `(void)expr;` so the
|
|
// side effect happens but the value is discarded. emit_gpr_write
|
|
// CANNOT be used for loads because it drops its rhs into a comment.
|
|
//
|
|
// No load-delay-slot modeling: per project decision, the recompiler
|
|
// writes the destination register at the load instruction's
|
|
// position rather than one instruction later. This relies on the
|
|
// BIOS already respecting the architectural load-delay rule (which
|
|
// it does — assemblers/compilers schedule loads correctly).
|
|
//
|
|
// LWL (0x22) and LWR (0x26) are NOT implemented in this batch:
|
|
// they are inventory hits but were not in the explicit B(6) list.
|
|
// -----------------------------------------------------------------
|
|
|
|
// LB rt, simm16(rs) -- sign-extended byte load
|
|
if (opcode == 0x20) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs; /* GPR_DEP rs; dest rt armed via LDWhich */
|
|
r.supported = true;
|
|
const std::string addr_expr = fmt::format(
|
|
"(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), simm);
|
|
if (rt == 0) {
|
|
r.c_code = fmt::format("(void)psx_cyc_load_byte(cpu, {}, 0, 0x{:X}u);", addr_expr, mask);
|
|
} else {
|
|
r.c_code = fmt::format(
|
|
"cpu->gpr[{}] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
|
static_cast<int>(rt), addr_expr, static_cast<int>(rt), mask);
|
|
r.load_dest = rt;
|
|
r.c_code_deferred = fmt::format(
|
|
"psx_ldd_{:08X} = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
|
d.address, addr_expr, static_cast<int>(rt), mask);
|
|
}
|
|
r.comment = fmt::format("lb {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// LWL / LWR (Phase 1b B(9)) -- unaligned word loads.
|
|
//
|
|
// Little-endian R3000A semantics. The address may be ANY alignment;
|
|
// these instructions are *defined* on unaligned addresses by
|
|
// construction (BIOS uses them precisely when it needs to load an
|
|
// unaligned 32-bit value via the standard `lwl rt, 3(rs); lwr rt,
|
|
// 0(rs);` pair). NO alignment fault.
|
|
//
|
|
// Both ops read the aligned base word at `addr & ~3` and merge it
|
|
// with the existing rt value:
|
|
//
|
|
// LWL (op 0x22): byte_offset = addr & 3
|
|
// shift_left = (3 - byte_offset) * 8
|
|
// keep_mask = (1u << shift_left) - 1u (low bits to KEEP from rt)
|
|
// rt = (old_rt & keep_mask) | (word << shift_left)
|
|
//
|
|
// LWR (op 0x26): byte_offset = addr & 3
|
|
// shift_right = byte_offset * 8
|
|
// keep_mask = ~(0xFFFFFFFFu >> shift_right) (high bits to KEEP from rt)
|
|
// rt = (old_rt & keep_mask) | (word >> shift_right)
|
|
//
|
|
// Snapshot rule: the formula reads cpu->gpr[rt] in the keep-mask
|
|
// path. C evaluates the RHS before the assignment, so a single
|
|
// expression `cpu->gpr[rt] = (cpu->gpr[rt] & ...) | ...;` is well
|
|
// defined, but to make the merge intent explicit and audit-friendly
|
|
// we snapshot `psx_old_rt` first.
|
|
//
|
|
// $zero handling: when rt == 0, the memory read still executes (MMIO
|
|
// side effects, FIFO pops, etc.) but the result is discarded — we
|
|
// must NOT write to cpu->gpr[0]. The variable form for rt != 0
|
|
// performs the merge; the rt == 0 form does just `(void)read`.
|
|
//
|
|
// No alignment checks. No speculative simplification. The keep_mask
|
|
// formulas for shift_left == 0 (byte_offset == 3) and shift_right
|
|
// == 0 (byte_offset == 0) both yield the "fully overwrite rt"
|
|
// result naturally; the constants are computed at runtime, not
|
|
// baked in.
|
|
// -----------------------------------------------------------------
|
|
|
|
// LWL rt, simm16(rs)
|
|
if (opcode == 0x22) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs; /* GPR_DEP rs (LWL does not GPR_DEP rt) */
|
|
r.supported = true;
|
|
const std::string body = (rt == 0)
|
|
? fmt::format("(void)psx_cyc_load_word(cpu, psx_aligned, 0, 0x{:X}u);", mask)
|
|
: fmt::format(
|
|
"uint32_t psx_byte_offset = psx_addr & 3u; "
|
|
"uint32_t psx_shift_left = (3u - psx_byte_offset) * 8u; "
|
|
"uint32_t psx_keep_mask = (1u << psx_shift_left) - 1u; "
|
|
"uint32_t psx_word = psx_cyc_load_word(cpu, psx_aligned, {}, 0x{:X}u); "
|
|
"uint32_t psx_old_rt = cpu->gpr[{}]; "
|
|
"cpu->gpr[{}] = (psx_old_rt & psx_keep_mask) | (psx_word << psx_shift_left);",
|
|
static_cast<int>(rt), mask, static_cast<int>(rt), static_cast<int>(rt));
|
|
r.c_code = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"uint32_t psx_aligned = psx_addr & ~3u; "
|
|
"{} }}",
|
|
static_cast<int>(rs), simm, body);
|
|
r.comment = fmt::format("lwl {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// LWR rt, simm16(rs)
|
|
if (opcode == 0x26) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs; /* GPR_DEP rs (LWR does not GPR_DEP rt) */
|
|
r.supported = true;
|
|
const std::string body = (rt == 0)
|
|
? fmt::format("(void)psx_cyc_load_word(cpu, psx_aligned, 0, 0x{:X}u);", mask)
|
|
: fmt::format(
|
|
"uint32_t psx_byte_offset = psx_addr & 3u; "
|
|
"uint32_t psx_shift_right = psx_byte_offset * 8u; "
|
|
"uint32_t psx_keep_mask = ~(0xFFFFFFFFu >> psx_shift_right); "
|
|
"uint32_t psx_word = psx_cyc_load_word(cpu, psx_aligned, {}, 0x{:X}u); "
|
|
"uint32_t psx_old_rt = cpu->gpr[{}]; "
|
|
"cpu->gpr[{}] = (psx_old_rt & psx_keep_mask) | (psx_word >> psx_shift_right);",
|
|
static_cast<int>(rt), mask, static_cast<int>(rt), static_cast<int>(rt));
|
|
r.c_code = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"uint32_t psx_aligned = psx_addr & ~3u; "
|
|
"{} }}",
|
|
static_cast<int>(rs), simm, body);
|
|
r.comment = fmt::format("lwr {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// LH rt, simm16(rs) -- sign-extended halfword load (addr must be 2-aligned)
|
|
if (opcode == 0x21) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs;
|
|
r.supported = true;
|
|
const std::string body = (rt == 0)
|
|
? fmt::format("(void)psx_cyc_load_half(cpu, psx_addr, 0, 0x{:X}u);", mask)
|
|
: fmt::format("cpu->gpr[{}] = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, psx_addr, {}, 0x{:X}u);",
|
|
static_cast<int>(rt), static_cast<int>(rt), mask);
|
|
r.c_code = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"{} }}",
|
|
static_cast<int>(rs), simm, d.address, body);
|
|
if (rt != 0) {
|
|
r.load_dest = rt;
|
|
r.c_code_deferred = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"psx_ldd_{:08X} = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, psx_addr, {}, 0x{:X}u); }}",
|
|
static_cast<int>(rs), simm, d.address, d.address, static_cast<int>(rt), mask);
|
|
}
|
|
r.comment = fmt::format("lh {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// LW rt, simm16(rs) -- 32-bit word load (addr must be 4-aligned)
|
|
if (opcode == 0x23) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs;
|
|
r.supported = true;
|
|
const std::string body = (rt == 0)
|
|
? fmt::format("(void)psx_cyc_load_word(cpu, psx_addr, 0, 0x{:X}u);", mask)
|
|
: fmt::format("cpu->gpr[{}] = psx_cyc_load_word(cpu, psx_addr, {}, 0x{:X}u);",
|
|
static_cast<int>(rt), static_cast<int>(rt), mask);
|
|
r.c_code = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 3u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"{} }}",
|
|
static_cast<int>(rs), simm, d.address, body);
|
|
if (rt != 0) {
|
|
r.load_dest = rt;
|
|
r.c_code_deferred = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 3u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"psx_ldd_{:08X} = psx_cyc_load_word(cpu, psx_addr, {}, 0x{:X}u); }}",
|
|
static_cast<int>(rs), simm, d.address, d.address, static_cast<int>(rt), mask);
|
|
}
|
|
r.comment = fmt::format("lw {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// LBU rt, simm16(rs) -- zero-extended byte load
|
|
if (opcode == 0x24) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs;
|
|
r.supported = true;
|
|
const std::string addr_expr = fmt::format(
|
|
"(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), simm);
|
|
if (rt == 0) {
|
|
r.c_code = fmt::format("(void)psx_cyc_load_byte(cpu, {}, 0, 0x{:X}u);", addr_expr, mask);
|
|
} else {
|
|
r.c_code = fmt::format(
|
|
"cpu->gpr[{}] = (uint32_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
|
static_cast<int>(rt), addr_expr, static_cast<int>(rt), mask);
|
|
r.load_dest = rt;
|
|
r.c_code_deferred = fmt::format(
|
|
"psx_ldd_{:08X} = (uint32_t)psx_cyc_load_byte(cpu, {}, {}, 0x{:X}u);",
|
|
d.address, addr_expr, static_cast<int>(rt), mask);
|
|
}
|
|
r.comment = fmt::format("lbu {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// LHU rt, simm16(rs) -- zero-extended halfword load (addr must be 2-aligned)
|
|
if (opcode == 0x25) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
const uint32_t mask = 1u << rs;
|
|
r.supported = true;
|
|
const std::string body = (rt == 0)
|
|
? fmt::format("(void)psx_cyc_load_half(cpu, psx_addr, 0, 0x{:X}u);", mask)
|
|
: fmt::format("cpu->gpr[{}] = (uint32_t)psx_cyc_load_half(cpu, psx_addr, {}, 0x{:X}u);",
|
|
static_cast<int>(rt), static_cast<int>(rt), mask);
|
|
r.c_code = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"{} }}",
|
|
static_cast<int>(rs), simm, d.address, body);
|
|
if (rt != 0) {
|
|
r.load_dest = rt;
|
|
r.c_code_deferred = fmt::format(
|
|
"{{ uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"psx_ldd_{:08X} = (uint32_t)psx_cyc_load_half(cpu, psx_addr, {}, 0x{:X}u); }}",
|
|
static_cast<int>(rs), simm, d.address, d.address, static_cast<int>(rt), mask);
|
|
}
|
|
r.comment = fmt::format("lhu {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// -----------------------------------------------------------------
|
|
// Stores (Phase 1b B(7), and SW updated for alignment-check
|
|
// consistency with B(6) loads).
|
|
//
|
|
// addr = (uint32_t)((int32_t)cpu->gpr[base] + sign_extend(simm16))
|
|
//
|
|
// Alignment rules (R3000A AdES on violation; we fail loud via
|
|
// psx_unaligned_access — not real exception delivery):
|
|
// SB (0x28): no alignment requirement
|
|
// SH (0x29): addr must be 2-aligned (addr & 1 == 0)
|
|
// SW (0x2B): addr must be 4-aligned (addr & 3 == 0)
|
|
// SWL (0x2A): NO alignment check — SWL operates on the
|
|
// unaligned high half of a word and is *defined* on
|
|
// non-4-aligned addresses by construction. The aligned
|
|
// base word is `addr & ~3`. Implementation reads the
|
|
// existing memory word, replaces the high (4 - (addr&3))
|
|
// bytes with the high bytes of cpu->gpr[rt], writes back.
|
|
// SWR (0x2E): same — operates on the unaligned low half.
|
|
//
|
|
// No special-casing based on the value being stored. The store
|
|
// always happens (modulo the alignment fault). Stores never write
|
|
// a destination register so there is no $zero handling concern;
|
|
// the value to store may come from $zero, in which case the
|
|
// emitted C reads cpu->gpr[0] (which is always 0) and writes that
|
|
// — exactly what real R3000A does.
|
|
//
|
|
// Block scoping: every store is wrapped in `{ uint32_t psx_addr =
|
|
// ...; ... }` so the local doesn't collide with neighboring
|
|
// stores in the same slice C function.
|
|
// -----------------------------------------------------------------
|
|
|
|
// SB rt, simm16(rs)
|
|
if (opcode == 0x28) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"psx_store_cycle_barrier(); g_debug_last_store_pc = 0x{:08X}u; "
|
|
"cpu->write_byte((uint32_t)((int32_t)cpu->gpr[{}] + ({})), (uint8_t)(cpu->gpr[{}] & 0xFFu));",
|
|
d.address, static_cast<int>(rs), simm, static_cast<int>(rt));
|
|
r.comment = fmt::format("sb {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// SH rt, simm16(rs) -- 2-aligned
|
|
if (opcode == 0x29) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"{{ psx_store_cycle_barrier(); uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 1u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"g_debug_last_store_pc = 0x{:08X}u; "
|
|
"cpu->write_half(psx_addr, (uint16_t)(cpu->gpr[{}] & 0xFFFFu)); }}",
|
|
static_cast<int>(rs), simm, d.address, d.address, static_cast<int>(rt));
|
|
r.comment = fmt::format("sh {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// SWL rt, simm16(rs) -- unaligned word store, high bytes
|
|
if (opcode == 0x2A) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
// SWL semantics (little-endian R3000A, which is the PS1
|
|
// configuration): given an unaligned addr, base = addr & ~3,
|
|
// shift = (addr & 3). The store writes the (shift+1)
|
|
// high-numbered bytes of the aligned word, taking them from
|
|
// the LOW (shift+1) bytes of cpu->gpr[rt] (no, wait — this
|
|
// depends on endianness; on little-endian it's the high
|
|
// bytes of rt going to the high bytes of the aligned word).
|
|
//
|
|
// The clean formulation, working in 32-bit values with a
|
|
// mask: keep the (3 - shift) low bytes of memory unchanged,
|
|
// overwrite the (shift + 1) high bytes from rt's high bytes
|
|
// shifted right by ((3 - shift) * 8). Equivalently:
|
|
//
|
|
// shift_bytes = addr & 3
|
|
// shift_bits = (3 - shift_bytes) * 8
|
|
// keep_mask = 0x00FFFFFFu >> (shift_bytes * 8) (lower bits to keep)
|
|
// new_word = (mem & keep_mask) | (rt >> shift_bits)
|
|
//
|
|
// Verified against the IDT R3000 manual SWL pseudocode and
|
|
// PSX-SPX SWL/SWR description. SWL has no alignment fault by
|
|
// design — it is the instruction PS1 code uses precisely
|
|
// when the address is not 4-aligned.
|
|
r.c_code = fmt::format(
|
|
"{{ psx_store_cycle_barrier(); uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"uint32_t psx_aligned = psx_addr & ~3u; "
|
|
"uint32_t psx_shift_bytes = psx_addr & 3u; "
|
|
"uint32_t psx_shift_bits = (3u - psx_shift_bytes) * 8u; "
|
|
"uint32_t psx_keep_mask = 0x00FFFFFFu >> (psx_shift_bytes * 8u); "
|
|
"uint32_t psx_mem = cpu->read_word(psx_aligned); "
|
|
"uint32_t psx_new = (psx_mem & psx_keep_mask) | (cpu->gpr[{}] >> psx_shift_bits); "
|
|
"g_debug_last_store_pc = 0x{:08X}u; "
|
|
"cpu->write_word(psx_aligned, psx_new); }}",
|
|
static_cast<int>(rs), simm, static_cast<int>(rt), d.address);
|
|
r.comment = fmt::format("swl {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// SW rt, simm16(rs) -- 4-aligned
|
|
if (opcode == 0x2B) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"{{ psx_store_cycle_barrier(); uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"if (psx_addr & 3u) {{ psx_unaligned_access(cpu, psx_addr, 0x{:08X}u); return; }} "
|
|
"g_debug_last_store_pc = 0x{:08X}u; "
|
|
"cpu->write_word(psx_addr, cpu->gpr[{}]); }}",
|
|
static_cast<int>(rs), simm, d.address, d.address, static_cast<int>(rt));
|
|
r.comment = fmt::format("sw {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// SWR rt, simm16(rs) -- unaligned word store, low bytes
|
|
if (opcode == 0x2E) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int32_t simm = static_cast<int32_t>(static_cast<int16_t>(d.raw & 0xFFFF));
|
|
r.supported = true;
|
|
// SWR (little-endian R3000A): mirror of SWL. Given an
|
|
// unaligned addr, base = addr & ~3, shift = (addr & 3). The
|
|
// store writes the (4 - shift) low-numbered bytes of the
|
|
// aligned word, taking them from the HIGH (4 - shift) bytes
|
|
// of cpu->gpr[rt] (formulated below in shift terms):
|
|
//
|
|
// shift_bytes = addr & 3
|
|
// shift_bits = shift_bytes * 8
|
|
// keep_mask = 0xFFFFFF00u << ((3 - shift_bytes) * 8) (upper bits to keep)
|
|
// new_word = (mem & keep_mask) | (rt << shift_bits)
|
|
//
|
|
// Note keep_mask for shift_bytes==0 is 0xFFFFFF00 << 24 = 0,
|
|
// i.e., overwrite the entire word — that's correct because
|
|
// SWR with addr & 3 == 0 stores the full word at addr.
|
|
// Verified against IDT R3000 manual and PSX-SPX.
|
|
r.c_code = fmt::format(
|
|
"{{ psx_store_cycle_barrier(); uint32_t psx_addr = (uint32_t)((int32_t)cpu->gpr[{}] + ({})); "
|
|
"uint32_t psx_aligned = psx_addr & ~3u; "
|
|
"uint32_t psx_shift_bytes = psx_addr & 3u; "
|
|
"uint32_t psx_shift_bits = psx_shift_bytes * 8u; "
|
|
"uint32_t psx_keep_mask = 0xFFFFFF00u << ((3u - psx_shift_bytes) * 8u); "
|
|
"uint32_t psx_mem = cpu->read_word(psx_aligned); "
|
|
"uint32_t psx_new = (psx_mem & psx_keep_mask) | (cpu->gpr[{}] << psx_shift_bits); "
|
|
"g_debug_last_store_pc = 0x{:08X}u; "
|
|
"cpu->write_word(psx_aligned, psx_new); }}",
|
|
static_cast<int>(rs), simm, static_cast<int>(rt), d.address);
|
|
r.comment = fmt::format("swr {}, {}({})", gpr_name(rt), simm, gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// COP0 (0x10): MFC0 (rs=0x00), MTC0 (rs=0x04), RFE (full word 0x42000010).
|
|
//
|
|
// Phase 1b B(8) decision: COP0 registers live in CPUState.cop0[32]
|
|
// and MFC0/MTC0 are direct register transfers with NO side effects
|
|
// beyond moving 32 bits. No SR-bit-write hooks, no IRQ delivery on
|
|
// SR write, no Cause-write side effects, no exception modeling.
|
|
// The Phase 2 runtime will revisit this when it needs IRQ delivery
|
|
// (PLAN.md HP1 Q1).
|
|
//
|
|
// Note: cop0[0] is the Index register, NOT a hardwired zero. Do not
|
|
// apply $zero rules to cop0[]. Only the GPR side (rt for MFC0)
|
|
// gets the $zero discard via emit_gpr_write.
|
|
if (opcode == 0x10) {
|
|
// RFE is the entire instruction word 0x42000010.
|
|
if (d.raw == 0x42000010u) {
|
|
r.supported = true;
|
|
r.is_terminator = true;
|
|
r.terminator_kind = "rfe";
|
|
// RFE pops the IE/KU stack in SR (cop0[12]) by shifting bits [5:0] right by 2,
|
|
// preserving the top bits.
|
|
r.c_code =
|
|
"{ uint32_t sr = cpu->cop0[12]; "
|
|
"cpu->cop0[12] = (sr & 0xFFFFFFC0u) | ((sr >> 2) & 0x0Fu); } "
|
|
"/* rfe -- slice terminator (per session approval, RFE substitutes for "
|
|
"FIRST_MILESTONE.md \"ERET\" since R3000A has no ERET) */ return;";
|
|
r.comment = "rfe";
|
|
return r;
|
|
}
|
|
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint8_t rd = (d.raw >> 11) & 0x1F;
|
|
|
|
if (rs == 0x00) { // MFC0 rt, rd -- gpr[rt] = cop0[rd]
|
|
r.supported = true;
|
|
// MFC0 is a delayed load (Beetle: LDAbsorb=0, LDWhich=rt): no give-back
|
|
// cycles, but sets ReadFudge=rt so a load in the next slot gets no fudge.
|
|
r.c_code = emit_gpr_write(rt, fmt::format("cpu->cop0[{}]", static_cast<int>(rd)))
|
|
+ fmt::format("\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n cpu->ld_absorb = 0u; cpu->ld_which_t = {}u;\n#endif",
|
|
static_cast<int>(rt));
|
|
r.comment = fmt::format("mfc0 {}, cop0[{}]", gpr_name(rt), rd);
|
|
return r;
|
|
}
|
|
if (rs == 0x04) { // MTC0 rt, rd -- cop0[rd] = gpr[rt]
|
|
r.supported = true;
|
|
// cop0[rd] is NOT subject to $zero rules; even cop0[0] is a
|
|
// real writable register (Index). Direct assignment.
|
|
r.c_code = fmt::format(
|
|
"cpu->cop0[{}] = cpu->gpr[{}];",
|
|
static_cast<int>(rd), static_cast<int>(rt));
|
|
r.comment = fmt::format("mtc0 {}, cop0[{}]", gpr_name(rt), rd);
|
|
return r;
|
|
}
|
|
|
|
return unsupported(d,
|
|
fmt::format("COP0 rs 0x{:02X} not implemented (only MFC0/MTC0/RFE in Phase 1b)", rs));
|
|
}
|
|
|
|
// COP2 / GTE (opcode 0x12)
|
|
if (opcode == 0x12) {
|
|
const uint8_t cop_op = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const uint8_t rd = (d.raw >> 11) & 0x1F;
|
|
|
|
// Faithful GTE: any COP2 register access stalls to the pending command
|
|
// completion deadline (gte_execute arms it). Cost-free unless in flight.
|
|
const std::string gte_stall =
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
|
|
// MFC2/CFC2 (GPR-dest reads): stall to the GTE deadline AND hand the stall
|
|
// amount to the next instruction(s) as a load-delay give-back (Beetle
|
|
// MFC2/CFC2: LDAbsorb=gte_ts_done-ts, LDWhich=rt). §1+DO_LDS ran in the
|
|
// function's per-instruction psx_cyc_step (COP2 is non-load).
|
|
const std::string gte_read = fmt::format(
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_read(cpu, {});\n#endif\n ", static_cast<int>(rt));
|
|
|
|
if (cop_op == 0x00) { // MFC2 — move from COP2 data register
|
|
r.supported = true;
|
|
// Computed/sign-extended data registers must route through the
|
|
// helper instead of reading the backing array directly.
|
|
if (PSXRecompGTERegisters::data_read_needs_helper(rd)) {
|
|
r.c_code = gte_read + emit_gpr_write(rt,
|
|
fmt::format("gte_read_data(cpu, {})", static_cast<int>(rd)));
|
|
} else {
|
|
r.c_code = gte_read + emit_gpr_write(rt,
|
|
fmt::format("cpu->gte_data[{}]", static_cast<int>(rd)));
|
|
}
|
|
r.comment = fmt::format("mfc2 {}, gte_data[{}]", gpr_name(rt), rd);
|
|
return r;
|
|
}
|
|
if (cop_op == 0x02) { // CFC2 — move from COP2 control register
|
|
r.supported = true;
|
|
if (PSXRecompGTERegisters::ctrl_read_needs_helper(rd)) {
|
|
r.c_code = gte_read + emit_gpr_write(rt,
|
|
fmt::format("gte_read_ctrl(cpu, {})", static_cast<int>(rd)));
|
|
} else {
|
|
r.c_code = gte_read + emit_gpr_write(rt,
|
|
fmt::format("cpu->gte_ctrl[{}]", static_cast<int>(rd)));
|
|
}
|
|
r.comment = fmt::format("cfc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
|
|
return r;
|
|
}
|
|
if (cop_op == 0x04) { // MTC2 — move to COP2 data register
|
|
r.supported = true;
|
|
// Masked, aliased, read-only, derived, and precision-shadow data
|
|
// registers share the runtime helper's exact semantics.
|
|
if (PSXRecompGTERegisters::data_write_needs_helper(rd)) {
|
|
r.c_code = gte_stall + fmt::format(
|
|
"gte_write_data(cpu, {}, cpu->gpr[{}]);",
|
|
static_cast<int>(rd), static_cast<int>(rt));
|
|
} else {
|
|
r.c_code = gte_stall + fmt::format(
|
|
"cpu->gte_data[{}] = cpu->gpr[{}];",
|
|
static_cast<int>(rd), static_cast<int>(rt));
|
|
}
|
|
r.comment = fmt::format("mtc2 {}, gte_data[{}]", gpr_name(rt), rd);
|
|
return r;
|
|
}
|
|
if (cop_op == 0x06) { // CTC2 — move to COP2 control register
|
|
r.supported = true;
|
|
if (PSXRecompGTERegisters::ctrl_write_needs_helper(rd)) {
|
|
r.c_code = gte_stall + fmt::format(
|
|
"gte_write_ctrl(cpu, {}, cpu->gpr[{}]);",
|
|
static_cast<int>(rd), static_cast<int>(rt));
|
|
} else {
|
|
r.c_code = gte_stall + fmt::format(
|
|
"cpu->gte_ctrl[{}] = cpu->gpr[{}];",
|
|
static_cast<int>(rd), static_cast<int>(rt));
|
|
}
|
|
r.comment = fmt::format("ctc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
|
|
return r;
|
|
}
|
|
if (cop_op & 0x10) { // GTE command (bit 25 set)
|
|
uint32_t gte_cmd = d.raw & 0x1FFFFFF;
|
|
r.supported = true;
|
|
r.c_code = fmt::format(
|
|
"gte_execute(cpu, 0x{:07X});",
|
|
gte_cmd);
|
|
r.comment = fmt::format("gte cmd 0x{:02X}", gte_cmd & 0x3F);
|
|
return r;
|
|
}
|
|
|
|
return unsupported(d,
|
|
fmt::format("COP2 rs 0x{:02X} not implemented", cop_op));
|
|
}
|
|
|
|
// LWC2 (opcode 0x32) — load word to COP2 data register
|
|
if (opcode == 0x32) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int16_t offset = static_cast<int16_t>(d.raw & 0xFFFF);
|
|
r.supported = true;
|
|
// Faithful GTE: COP2 reg write stalls to the command deadline.
|
|
const std::string gte_stall =
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
|
|
// §1+DO_LDS charged by full_function_emitter's psx_cyc_step (op 0x32 is
|
|
// non-load); the GTE deadline stall by psx_gte_stall; psx_cyc_lwc2_read does
|
|
// the ReadMemory timing (completion +1, no LDWhich arm — dest is a GTE reg).
|
|
std::string addr = offset == 0
|
|
? fmt::format("cpu->gpr[{}]", static_cast<int>(rs))
|
|
: fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), static_cast<int>(offset));
|
|
bool special = PSXRecompGTERegisters::data_write_needs_helper(rt);
|
|
/* The raw loaded word is captured for the PGXP hook: a masked
|
|
* register write must validate against the word as loaded. */
|
|
if (special) {
|
|
r.c_code = gte_stall + fmt::format(
|
|
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); "
|
|
"gte_write_data(cpu, {}, _pgxv); PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}",
|
|
addr, static_cast<int>(rt), d.raw);
|
|
} else {
|
|
r.c_code = gte_stall + fmt::format(
|
|
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); "
|
|
"cpu->gte_data[{}] = _pgxv; PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}",
|
|
addr, static_cast<int>(rt), d.raw);
|
|
}
|
|
r.comment = fmt::format("lwc2 gte[{}], {}({})",
|
|
rt, static_cast<int>(offset), gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
// SWC2 (opcode 0x3A) — store word from COP2 data register
|
|
if (opcode == 0x3A) {
|
|
const uint8_t rs = (d.raw >> 21) & 0x1F;
|
|
const uint8_t rt = (d.raw >> 16) & 0x1F;
|
|
const int16_t offset = static_cast<int16_t>(d.raw & 0xFFFF);
|
|
r.supported = true;
|
|
// Faithful GTE: COP2 reg read stalls to the command deadline.
|
|
const std::string gte_stall =
|
|
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
|
|
std::string value = PSXRecompGTERegisters::data_read_needs_helper(rt)
|
|
? fmt::format("gte_read_data(cpu, {})", static_cast<int>(rt))
|
|
: fmt::format("cpu->gte_data[{}]", static_cast<int>(rt));
|
|
{
|
|
std::string addr = offset == 0
|
|
? fmt::format("cpu->gpr[{}]", static_cast<int>(rs))
|
|
: fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))",
|
|
static_cast<int>(rs), static_cast<int>(offset));
|
|
r.c_code = gte_stall + fmt::format(
|
|
"psx_store_cycle_barrier(); g_debug_last_store_pc = 0x{:08X}u; "
|
|
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = {}; "
|
|
"cpu->write_word(_pgxa, _pgxv); "
|
|
"gte_precision_store_word(_pgxa, {}); "
|
|
"PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}",
|
|
d.address, addr, value, static_cast<int>(rt), d.raw);
|
|
}
|
|
r.comment = fmt::format("swc2 gte[{}], {}({})",
|
|
rt, static_cast<int>(offset), gpr_name(rs));
|
|
return r;
|
|
}
|
|
|
|
return unsupported(d, fmt::format("top-level opcode 0x{:02X} not implemented in Phase 1a", opcode));
|
|
}
|
|
|
|
} // namespace PSXRecompV4
|