From 3b3064b185aa5bbf8d2150af30d676576b50a57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lui=CC=81s=20Braganc=CC=A7a?= Date: Sat, 10 Oct 2026 10:31:15 +0100 Subject: [PATCH] test(recomp): per-instruction tests for R5900 general instructions test(recomp): per-instruction tests for R5900 general instructions Add ps2xTest/src/r5900_general_instruction_tests.cpp: three ps2x_tests suites that cover every MipsOpcodes, SpecialFunctions and RegimmFunctions encoding (46 cases). Each case decodes a real instruction word, pins the C++ the recompiler emits for it, and then executes that same C++ against an R5900Context and guest RAM through ps2_runtime_macros.h, so a case fails both when the emission changes and when the emitted code misbehaves. Branches and jumps go through the function emitter and check the condition expression, delay-slot placement, link writes and dispatch kinds. Manifest cases walk all opcode and function codes and assert that exactly the EE-reserved encodings are rejected. Edge behaviour covered: integer-overflow traps (and that rd/rt is left untouched), MIPS division by zero and INT32_MIN / -1, sign and zero extension per load width, 64-bit versus 32-bit compares, shift-amount masking, LWL/LWR/LDL/LDR/SWL/SWR/SDL/SDR at every offset against a byte-level reference model, LQ/SQ alignment, LL/SC, MTSAB/MTSAH, and the preservation of bits 64..127 across scalar writes. Fix the deviations the tests exposed, cross-checked against the EE Core manual and PCSX2's interpreter: - BLEZ/BGTZ/BLTZ/BGEZ (and their likely and link forms) compared only the low 32 bits; the R5900 tests the full 64-bit register. - BLTZALL/BGEZALL wrote $ra only when taken; every "and link" branch links unconditionally. - MOVZ/MOVN copied the whole 128-bit register and clobbered bits 64..127 of rd; they are 64-bit moves. - LQ/SQ used the unmasked effective address; the hardware ignores the low four bits. LQC2/SQC2 are left as they were. - ADD32_OV/SUB32_OV, DADDI, DADD, DSUB and DADDIU formed the overflowing sum in signed arithmetic, which is undefined behaviour on exactly the inputs the overflow tests use (and the Release build uses LTO). The sums are now unsigned; non-overflowing results are unchanged. ps2x_tests: 483 passed, 0 failed (437 before). Closes #160 --- ps2xRecomp/src/lib/control_flow_emitter.cpp | 40 +- ps2xRecomp/src/lib/instruction_translator.cpp | 9 +- ps2xRecomp/src/lib/special_translator.cpp | 9 +- ps2xRuntime/include/ps2_runtime_macros.h | 4 +- ps2xTest/CMakeLists.txt | 1 + ps2xTest/src/main.cpp | 2 + .../src/r5900_general_instruction_tests.cpp | 1767 +++++++++++++++++ 7 files changed, 1796 insertions(+), 36 deletions(-) create mode 100644 ps2xTest/src/r5900_general_instruction_tests.cpp diff --git a/ps2xRecomp/src/lib/control_flow_emitter.cpp b/ps2xRecomp/src/lib/control_flow_emitter.cpp index 4bf8dc550..6eb771ab8 100644 --- a/ps2xRecomp/src/lib/control_flow_emitter.cpp +++ b/ps2xRecomp/src/lib/control_flow_emitter.cpp @@ -404,12 +404,13 @@ namespace ps2recomp case OPCODE_BNE: case OPCODE_BNEL: return fmt::format("GPR_U64(ctx, {}) != GPR_U64(ctx, {})", rsReg, rtReg); + // Sign tests look at the full 64-bit register, like every other R5900 compare. case OPCODE_BLEZ: case OPCODE_BLEZL: - return fmt::format("GPR_S32(ctx, {}) <= 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) <= 0", rsReg); case OPCODE_BGTZ: case OPCODE_BGTZL: - return fmt::format("GPR_S32(ctx, {}) > 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) > 0", rsReg); case OPCODE_REGIMM: switch (m_branchInst.rt) { @@ -417,12 +418,12 @@ namespace ps2recomp case REGIMM_BLTZL: case REGIMM_BLTZAL: case REGIMM_BLTZALL: - return fmt::format("GPR_S32(ctx, {}) < 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) < 0", rsReg); case REGIMM_BGEZ: case REGIMM_BGEZL: case REGIMM_BGEZAL: case REGIMM_BGEZALL: - return fmt::format("GPR_S32(ctx, {}) >= 0", rsReg); + return fmt::format("GPR_S64(ctx, {}) >= 0", rsReg); default: return "false"; } @@ -463,36 +464,28 @@ namespace ps2recomp const uint32_t target = conditionalBranchTarget(); const bool likely = isLikelyBranch(); const std::string branchTakenVar = fmt::format("branch_taken_0x{:x}", m_branchInst.address); - std::string unconditionalLinkCode; - std::string conditionalLinkCode; + std::string linkCode; - if (m_branchInst.opcode == OPCODE_REGIMM) + if (m_branchInst.opcode == OPCODE_REGIMM && + (m_branchInst.rt == REGIMM_BLTZAL || m_branchInst.rt == REGIMM_BGEZAL || + m_branchInst.rt == REGIMM_BLTZALL || m_branchInst.rt == REGIMM_BGEZALL)) { - if (m_branchInst.rt == REGIMM_BLTZAL || m_branchInst.rt == REGIMM_BGEZAL) - { - unconditionalLinkCode = fmt::format("SET_GPR_U32(ctx, 31, 0x{:X}u);", fallthroughPc()); - } - else if (m_branchInst.rt == REGIMM_BLTZALL || m_branchInst.rt == REGIMM_BGEZALL) - { - conditionalLinkCode = fmt::format("SET_GPR_U32(ctx, 31, 0x{:X}u);", fallthroughPc()); - } + // Every "and link" branch, likely or not, writes $ra whether or not it is taken. + // The condition is captured first so a link into rs sees the pre-link value. + linkCode = fmt::format("SET_GPR_U32(ctx, 31, 0x{:X}u);", fallthroughPc()); } m_ss << " {\n"; m_ss << " const bool " << branchTakenVar << " = (" << conditionalBranchExpression() << ");\n"; - if (!unconditionalLinkCode.empty()) + if (!linkCode.empty()) { - m_ss << " " << unconditionalLinkCode << "\n"; + m_ss << " " << linkCode << "\n"; } if (likely) { m_ss << " if (" << branchTakenVar << ") {\n"; - if (!conditionalLinkCode.empty()) - { - m_ss << " " << conditionalLinkCode << "\n"; - } emitDelaySlot(" "); if (isInternalTarget(target)) @@ -508,11 +501,6 @@ namespace ps2recomp } else { - if (!conditionalLinkCode.empty()) - { - m_ss << " if (" << branchTakenVar << ") { " << conditionalLinkCode << " }\n"; - } - emitDelaySlot(" "); m_ss << " if (" << branchTakenVar << ") {\n"; diff --git a/ps2xRecomp/src/lib/instruction_translator.cpp b/ps2xRecomp/src/lib/instruction_translator.cpp index 768409759..c65b9ca59 100644 --- a/ps2xRecomp/src/lib/instruction_translator.cpp +++ b/ps2xRecomp/src/lib/instruction_translator.cpp @@ -195,10 +195,11 @@ namespace ps2recomp return genWrite(16, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("(uint16_t)GPR_U32(ctx, {})", inst.rt)) + ";"; case OPCODE_SW: return genWrite(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("GPR_U32(ctx, {})", inst.rt)) + ";"; + // LQ/SQ silently ignore the low four address bits instead of raising an address error. case OPCODE_LQ: - return fmt::format("SET_GPR_VEC(ctx, {}, {});", inst.rt, genRead(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("SET_GPR_VEC(ctx, {}, {});", inst.rt, genRead(128, fmt::format("ADD32(GPR_U32(ctx, {}), {}) & ~0xFu", inst.rs, inst.simmediate))); case OPCODE_SQ: - return genWrite(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("GPR_VEC(ctx, {})", inst.rt)) + ";"; + return genWrite(128, fmt::format("ADD32(GPR_U32(ctx, {}), {}) & ~0xFu", inst.rs, inst.simmediate), fmt::format("GPR_VEC(ctx, {})", inst.rt)) + ";"; case OPCODE_LD: return fmt::format("SET_GPR_U64(ctx, {}, {});", inst.rt, genRead(64, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); case OPCODE_SD: @@ -218,14 +219,14 @@ namespace ps2recomp return fmt::format( "{{ int64_t src = (int64_t)GPR_S64(ctx, {}); " "int64_t imm = (int64_t)(int32_t){}; " - "int64_t res = src + imm; " + "int64_t res = (int64_t)((uint64_t)src + (uint64_t)imm); " "if (((src ^ imm) >= 0) && ((src ^ res) < 0)) " " runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); " "else SET_GPR_S64(ctx, {}, res); }}", inst.rs, inst.simmediate, inst.rt); case OPCODE_DADDIU: return fmt::format( - "SET_GPR_S64(ctx, {}, (int64_t)GPR_S64(ctx, {}) + (int64_t)(int32_t){});", + "SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) + (uint64_t)(int64_t)(int32_t){});", inst.rt, inst.rs, inst.simmediate); case OPCODE_J: return fmt::format("// J 0x{:X} - Handled by branch logic", buildAbsoluteJumpTarget(inst.address, inst.target)); diff --git a/ps2xRecomp/src/lib/special_translator.cpp b/ps2xRecomp/src/lib/special_translator.cpp index b01a93771..67c5c3e92 100644 --- a/ps2xRecomp/src/lib/special_translator.cpp +++ b/ps2xRecomp/src/lib/special_translator.cpp @@ -120,10 +120,11 @@ namespace ps2recomp return fmt::format("SET_GPR_U64(ctx, {}, ((int64_t)GPR_S64(ctx, {}) < (int64_t)GPR_S64(ctx, {})) ? 1 : 0);", inst.rd, inst.rs, inst.rt); case SPECIAL_SLTU: return fmt::format("SET_GPR_U64(ctx, {}, ((uint64_t)GPR_U64(ctx, {}) < (uint64_t)GPR_U64(ctx, {})) ? 1 : 0);", inst.rd, inst.rs, inst.rt); + // MOVZ/MOVN are 64-bit moves: the upper half of the 128-bit destination is preserved. case SPECIAL_MOVZ: - return fmt::format("if (GPR_U64(ctx, {}) == 0) SET_GPR_VEC(ctx, {}, GPR_VEC(ctx, {}));", inst.rt, inst.rd, inst.rs); + return fmt::format("if (GPR_U64(ctx, {}) == 0) SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}));", inst.rt, inst.rd, inst.rs); case SPECIAL_MOVN: - return fmt::format("if (GPR_U64(ctx, {}) != 0) SET_GPR_VEC(ctx, {}, GPR_VEC(ctx, {}));", inst.rt, inst.rd, inst.rs); + return fmt::format("if (GPR_U64(ctx, {}) != 0) SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}));", inst.rt, inst.rd, inst.rs); case SPECIAL_MFSA: return fmt::format("SET_GPR_U32(ctx, {}, ctx->sa);", inst.rd); case SPECIAL_MTSA: @@ -132,7 +133,7 @@ namespace ps2recomp return fmt::format( "{{ int64_t a = (int64_t)GPR_S64(ctx, {}); " "int64_t b = (int64_t)GPR_S64(ctx, {}); " - "int64_t r = a + b; " + "int64_t r = (int64_t)((uint64_t)a + (uint64_t)b); " "if (((a ^ b) >= 0) && ((a ^ r) < 0)) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); " "else SET_GPR_S64(ctx, {}, r); }}", inst.rs, inst.rt, inst.rd); @@ -144,7 +145,7 @@ namespace ps2recomp return fmt::format( "{{ int64_t a = (int64_t)GPR_S64(ctx, {}); " "int64_t b = (int64_t)GPR_S64(ctx, {}); " - "int64_t r = a - b; " + "int64_t r = (int64_t)((uint64_t)a - (uint64_t)b); " "if (((a ^ b) < 0) && ((a ^ r) < 0)) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); " "else SET_GPR_S64(ctx, {}, r); }}", inst.rs, inst.rt, inst.rd); diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index 0f19c9f24..9e603d74a 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -92,7 +92,7 @@ static inline uint32_t ps2_plzcw32(uint32_t x) { \ int32_t _a = (int32_t)(rs); \ int32_t _b = (int32_t)(rt); \ - int32_t _r = _a + _b; \ + int32_t _r = (int32_t)((uint32_t)_a + (uint32_t)_b); \ overflow = (((_a ^ _b) >= 0) && ((_a ^ _r) < 0)); \ result32 = (uint32_t)_r; \ } while (0); @@ -102,7 +102,7 @@ static inline uint32_t ps2_plzcw32(uint32_t x) { \ int32_t _a = (int32_t)(rs); \ int32_t _b = (int32_t)(rt); \ - int32_t _r = _a - _b; \ + int32_t _r = (int32_t)((uint32_t)_a - (uint32_t)_b); \ overflow = (((_a ^ _b) < 0) && ((_a ^ _r) < 0)); \ result32 = (uint32_t)_r; \ } while (0); diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 5db04b923..5f394533c 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -23,6 +23,7 @@ target_include_directories(ps2_test_function_table PRIVATE add_library(ps2_test_lib STATIC src/code_generator_tests.cpp src/r5900_decoder_tests.cpp + src/r5900_general_instruction_tests.cpp src/elf_analyzer_tests.cpp src/pad_input_tests.cpp src/ps2_runtime_io_tests.cpp diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 40a68b16b..cbf5a022c 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -4,6 +4,7 @@ void register_code_generator_tests(); void register_r5900_decoder_tests(); +void register_r5900_general_instruction_tests(); void register_elf_analyzer_tests(); void register_pad_input_tests(); void register_ps2_runtime_io_tests(); @@ -26,6 +27,7 @@ int main() register_code_generator_tests(); register_r5900_decoder_tests(); + register_r5900_general_instruction_tests(); register_elf_analyzer_tests(); register_pad_input_tests(); register_ps2_runtime_io_tests(); diff --git a/ps2xTest/src/r5900_general_instruction_tests.cpp b/ps2xTest/src/r5900_general_instruction_tests.cpp new file mode 100644 index 000000000..1cf98360f --- /dev/null +++ b/ps2xTest/src/r5900_general_instruction_tests.cpp @@ -0,0 +1,1767 @@ +// Per-instruction unit tests for the R5900 general instruction set: MipsOpcodes, +// SpecialFunctions and RegimmFunctions (GitHub issue #160). +// +// Each test decodes a real 32-bit instruction word, translates it with the recompiler and +// pins the emitted C++ verbatim. The pinned C++ is then executed against a guest context and +// guest RAM through the same runtime macros a recompiled game uses, so every case checks both +// halves of an instruction: what the recompiler emits and what that code does at runtime. +// Branches and jumps are materialised by the function emitter rather than the per-instruction +// translator, so those cases emit a small function around the instruction under test. +#include "MiniTest.h" +#include "ps2recomp/code_generator.h" +#include "ps2recomp/instructions.h" +#include "ps2recomp/r5900_decoder.h" +#include "ps2recomp/types.h" +#include "ps2_runtime.h" +#include "ps2_runtime_macros.h" +#include "runtime/ps2_memory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ps2recomp; + +namespace +{ + constexpr uint32_t kCodeAddress = 0x1000u; + constexpr uint32_t kDataAddress = 0x2000u; + constexpr uint32_t kScratchBytes = 0x10000u; + + constexpr uint32_t kCop0CauseExcCodeMask = 0x0000007Cu; + constexpr uint32_t kCop0StatusExl = 0x00000002u; + constexpr uint32_t kGeneralExceptionVector = 0x80000080u; + constexpr uint32_t kResetStatus = 0x00010001u; // EIE | IE, as R5900Context leaves it + + // ---------------------------------------------------------------- encoding helpers + + constexpr uint32_t encodeI(uint32_t opcode, uint32_t rs, uint32_t rt, uint16_t imm) + { + return (opcode << 26) | (rs << 21) | (rt << 16) | imm; + } + + constexpr uint32_t encodeR(uint32_t function, uint32_t rs, uint32_t rt, uint32_t rd, uint32_t sa = 0u) + { + return (static_cast(OPCODE_SPECIAL) << 26) | (rs << 21) | (rt << 16) | (rd << 11) | (sa << 6) | function; + } + + constexpr uint32_t encodeRegimm(uint32_t function, uint32_t rs, uint16_t imm) + { + return (static_cast(OPCODE_REGIMM) << 26) | (rs << 21) | (function << 16) | imm; + } + + constexpr uint32_t encodeJ(uint32_t opcode, uint32_t targetField) + { + return (opcode << 26) | (targetField & 0x03FFFFFFu); + } + + constexpr uint16_t imm16(int32_t value) + { + return static_cast(value); + } + + Instruction decode(uint32_t raw, uint32_t address = kCodeAddress) + { + R5900Decoder decoder; + return decoder.decodeInstruction(address, raw); + } + + std::string hex(uint64_t value) + { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "0x%llx", static_cast(value)); + return buffer; + } + + std::string describe(const Instruction &inst) + { + return inst.disassembly.empty() ? ("raw " + hex(inst.raw)) : inst.disassembly; + } + + // ---------------------------------------------------------------- translation helpers + + const std::vector kNoSymbols; + const std::vector
kNoSections; + + std::string translate(const Instruction &inst) + { + CodeGenerator generator(kNoSymbols, kNoSections); + return generator.translateInstruction(inst); + } + + std::string normalizeWhitespace(std::string_view text) + { + std::string out; + bool pendingSpace = false; + for (char c : text) + { + if (std::isspace(static_cast(c))) + { + pendingSpace = !out.empty(); + continue; + } + if (pendingSpace) + { + out.push_back(' '); + pendingSpace = false; + } + out.push_back(c); + } + return out; + } + + void expectTranslation(TestCase &t, const Instruction &inst, std::string_view expected) + { + const std::string actual = translate(inst); + if (normalizeWhitespace(actual) != normalizeWhitespace(expected)) + { + t.Fail(describe(inst) + " translated to <" + actual + "> but the test expects <" + + std::string(expected) + ">"); + } + } + + bool emitsRuntimeThrow(const std::string &code) + { + return code.find("throw std::runtime_error(") != std::string::npos; + } + + std::string emitFunction(uint32_t address, std::initializer_list words) + { + R5900Decoder decoder; + std::vector instructions; + uint32_t pc = address; + for (uint32_t word : words) + { + instructions.push_back(decoder.decodeInstruction(pc, word)); + pc += 4u; + } + + Function function{}; + function.name = "instruction_under_test"; + function.start = address; + function.end = pc; + function.isRecompiled = true; + + CodeGenerator generator(kNoSymbols, kNoSections); + return generator.generateFunction(function, instructions, false); + } + + // Branch and jump cases share one layout at kCodeAddress: + // +0 branch or jump under test (conditional branches target +12) + // +4 addiu $3, $zero, 7 (delay slot; its translation is distinctive) + // +8 addiu $4, $zero, 9 (fallthrough) + // +12 addiu $5, $zero, 11 (branch target) + // +16 jr $ra + // +20 nop + constexpr uint32_t kDelaySlotWord = encodeI(OPCODE_ADDIU, 0, 3, 7); + constexpr uint32_t kFallthroughWord = encodeI(OPCODE_ADDIU, 0, 4, 9); + constexpr uint32_t kTargetWord = encodeI(OPCODE_ADDIU, 0, 5, 11); + constexpr uint32_t kReturnWord = encodeR(SPECIAL_JR, 31, 0, 0); + constexpr const char *kDelaySlotCode = "SET_GPR_S32(ctx, 3, (int32_t)ADD32(GPR_U32(ctx, 0), 7));"; + constexpr uint16_t kBranchOffsetWords = 2u; // from the delay slot to +12 + + std::string emitBranchFunction(uint32_t branchWord, uint32_t address = kCodeAddress) + { + return emitFunction(address, {branchWord, kDelaySlotWord, kFallthroughWord, kTargetWord, kReturnWord, 0u}); + } + + std::string extractBranchCondition(const std::string &code, uint32_t address) + { + char marker[64]; + std::snprintf(marker, sizeof(marker), "const bool branch_taken_0x%x = (", address); + const size_t begin = code.find(marker); + if (begin == std::string::npos) + { + return {}; + } + const size_t start = begin + std::strlen(marker); + const size_t end = code.find(");\n", start); + if (end == std::string::npos) + { + return {}; + } + return code.substr(start, end - start); + } + + void expectBranchCondition(TestCase &t, uint32_t branchWord, std::string_view expected) + { + const std::string condition = extractBranchCondition(emitBranchFunction(branchWord), kCodeAddress); + if (normalizeWhitespace(condition) != normalizeWhitespace(expected)) + { + t.Fail(describe(decode(branchWord)) + " branch condition is <" + condition + + "> but the test expects <" + std::string(expected) + ">"); + } + } + + // Structural checks shared by every conditional branch: delay slot placement relative to the + // branch decision, the in-function target, and whether $ra is written. + void expectConditionalBranchShape(TestCase &t, uint32_t branchWord, bool likely, bool links) + { + const std::string name = describe(decode(branchWord)); + const std::string code = emitBranchFunction(branchWord); + const size_t slot = code.find(kDelaySlotCode); + const size_t decision = code.find("if (branch_taken_0x1000)"); + const size_t link = code.find("SET_GPR_U32(ctx, 31, 0x1008u);"); + + t.IsTrue(slot != std::string::npos, name + ": delay slot must be translated"); + t.IsTrue(decision != std::string::npos, name + ": branch decision must test branch_taken"); + t.IsTrue(code.find("goto label_100c;") != std::string::npos, name + ": taken path must jump to the in-function target"); + if (slot == std::string::npos || decision == std::string::npos) + { + return; + } + + if (likely) + { + t.IsTrue(decision < slot, name + ": a likely branch executes its delay slot only when taken"); + } + else + { + t.IsTrue(slot < decision, name + ": the delay slot executes before the branch decision"); + } + + if (links) + { + t.IsTrue(link != std::string::npos, name + ": must write the return address to $ra"); + t.IsTrue(link != std::string::npos && link < decision && link < slot, + name + ": $ra is written unconditionally, before the delay slot runs"); + } + else + { + t.IsTrue(link == std::string::npos, name + ": must not write $ra"); + } + } + + // ---------------------------------------------------------------- guest fixture + + struct GuestMachine + { + PS2Runtime runtime; + std::vector ram = std::vector(PS2_RAM_SIZE, 0u); + R5900Context ctx{}; + + void reset() + { + ctx = R5900Context{}; + std::memset(ram.data(), 0, kScratchBytes); + } + }; + + std::unique_ptr gGuest; + + GuestMachine &guest() + { + if (!gGuest) + { + gGuest = std::make_unique(); + } + gGuest->reset(); + return *gGuest; + } + + void releaseGuest() + { + gGuest.reset(); + } + + // Register access through plain memcpy, independent of the macros under test. + uint64_t gprLo(const R5900Context &ctx, int reg) + { + uint64_t value = 0; + std::memcpy(&value, &ctx.r[reg], sizeof(value)); + return value; + } + + uint64_t gprHi(const R5900Context &ctx, int reg) + { + uint64_t value = 0; + std::memcpy(&value, reinterpret_cast(&ctx.r[reg]) + 8, sizeof(value)); + return value; + } + + void setGpr(R5900Context &ctx, int reg, uint64_t lo, uint64_t hi = 0u) + { + std::memcpy(&ctx.r[reg], &lo, sizeof(lo)); + std::memcpy(reinterpret_cast(&ctx.r[reg]) + 8, &hi, sizeof(hi)); + } + + constexpr uint64_t sext32(uint32_t value) + { + return static_cast(static_cast(static_cast(value))); + } + + // What a 32-bit operation leaves in a 64-bit register. + void setGpr32(R5900Context &ctx, int reg, uint32_t value) + { + setGpr(ctx, reg, sext32(value)); + } + + template + T readRam(const uint8_t *rdram, uint32_t addr) + { + T value; + std::memcpy(&value, rdram + addr, sizeof(value)); + return value; + } + + template + void writeRam(uint8_t *rdram, uint32_t addr, T value) + { + std::memcpy(rdram + addr, &value, sizeof(value)); + } + + void expectU64(TestCase &t, uint64_t actual, uint64_t expected, const std::string &what) + { + if (actual != expected) + { + t.Fail(what + ": got " + hex(actual) + ", expected " + hex(expected)); + } + } + + void expectGpr(TestCase &t, const R5900Context &ctx, int reg, uint64_t expected, const std::string &what) + { + expectU64(t, gprLo(ctx, reg), expected, what + " ($" + std::to_string(reg) + " low 64 bits)"); + } + + // Arithmetic traps reach COP0 through PS2Runtime, exactly as a recompiled game would see them. + void armTrapCheck(R5900Context &ctx) + { + ctx.pc = kCodeAddress; + ctx.cop0_status = kResetStatus; + ctx.cop0_cause = 0u; + ctx.in_delay_slot = false; + } + + bool exceptionRaised(const R5900Context &ctx, PS2Exception exception) + { + return ((ctx.cop0_cause & kCop0CauseExcCodeMask) >> 2) == static_cast(exception) && + (ctx.cop0_status & kCop0StatusExl) != 0u && + ctx.cop0_epc == kCodeAddress && + ctx.pc == kGeneralExceptionVector; + } + + bool noException(const R5900Context &ctx) + { + return (ctx.cop0_status & kCop0StatusExl) == 0u; + } + + // Reference model for the unaligned load/store family, written in terms of guest byte + // addresses instead of shift tables. "Left" instructions pair the register's most significant + // byte with the effective address and walk down to the start of the aligned word; "right" + // instructions pair the least significant byte with the effective address and walk up to the + // end of the aligned word (little-endian guest). + uint64_t replaceByte(uint64_t value, unsigned index, uint8_t byte) + { + const uint64_t mask = 0xFFull << (8u * index); + return (value & ~mask) | (static_cast(byte) << (8u * index)); + } + + uint8_t byteOf(uint64_t value, unsigned index) + { + return static_cast(value >> (8u * index)); + } + + uint64_t refLoadLeft(const uint8_t *rdram, uint32_t addr, unsigned width, uint64_t reg) + { + const uint32_t base = addr & ~(width - 1u); + const unsigned offset = addr & (width - 1u); + for (unsigned i = 0; i <= offset; ++i) + { + reg = replaceByte(reg, width - 1u - offset + i, rdram[base + i]); + } + return reg; + } + + uint64_t refLoadRight(const uint8_t *rdram, uint32_t addr, unsigned width, uint64_t reg) + { + const uint32_t base = addr & ~(width - 1u); + const unsigned offset = addr & (width - 1u); + for (unsigned i = 0; offset + i < width; ++i) + { + reg = replaceByte(reg, i, rdram[base + offset + i]); + } + return reg; + } + + void refStoreLeft(uint8_t *memory, uint32_t addr, unsigned width, uint64_t reg) + { + const uint32_t base = addr & ~(width - 1u); + const unsigned offset = addr & (width - 1u); + for (unsigned i = 0; i <= offset; ++i) + { + memory[base + i] = byteOf(reg, width - 1u - offset + i); + } + } + + void refStoreRight(uint8_t *memory, uint32_t addr, unsigned width, uint64_t reg) + { + const uint32_t base = addr & ~(width - 1u); + const unsigned offset = addr & (width - 1u); + for (unsigned i = 0; offset + i < width; ++i) + { + memory[base + offset + i] = byteOf(reg, i); + } + } + + constexpr uint8_t kFillByte = 0xEEu; + + void fillRam(uint8_t *rdram, uint32_t addr, uint32_t bytes) + { + std::memset(rdram + addr, kFillByte, bytes); + } + + bool ramUntouched(const uint8_t *rdram, uint32_t addr, uint32_t bytes) + { + for (uint32_t i = 0; i < bytes; ++i) + { + if (rdram[addr + i] != kFillByte) + { + return false; + } + } + return true; + } +} + +// Brings the names the emitted code expects (rdram, ctx, runtime) into scope on a fresh guest. +#define PS2X_GUEST_SCOPE() \ + GuestMachine &machine = guest(); \ + uint8_t *rdram = machine.ram.data(); \ + R5900Context *ctx = &machine.ctx; \ + PS2Runtime *runtime = &machine.runtime; \ + (void)rdram; \ + (void)runtime + +// Pins the recompiler output for `inst` to the given C++ and then executes that very C++. +#define PS2X_EXEC_EMITTED(t, inst, ...) \ + do \ + { \ + expectTranslation(t, inst, #__VA_ARGS__); \ + __VA_ARGS__ \ + } while (0) + +// Pins the condition the function emitter evaluates for a conditional branch and evaluates it. +#define PS2X_BRANCH_TAKEN(t, branchWord, ...) (expectBranchCondition(t, branchWord, #__VA_ARGS__), (__VA_ARGS__)) + +void register_r5900_general_instruction_tests() +{ + MiniTest::Case("R5900Instructions.MipsOpcodes", [](TestCase &tc) + { + tc.After(releaseGuest); + + tc.Run("ADDI adds a sign-extended immediate and traps on signed overflow", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction addi = decode(encodeI(OPCODE_ADDI, 1, 2, 5)); // addi $2, $1, 5 + const Instruction addiNeg = decode(encodeI(OPCODE_ADDI, 1, 2, imm16(-16))); // addi $2, $1, -16 + auto runAddi = [&] { + PS2X_EXEC_EMITTED(t, addi, { uint32_t tmp; bool ov; ADD32_OV(GPR_U32(ctx, 1), (int32_t)5, tmp, ov); if (ov) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S32(ctx, 2, (int32_t)tmp); }); + }; + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x7FFFFFF0u); + runAddi(); + expectGpr(t, *ctx, 2, 0x7FFFFFF5u, "addi 0x7FFFFFF0 + 5"); + t.IsTrue(noException(*ctx), "addi without overflow must not raise an exception"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 5u); + PS2X_EXEC_EMITTED(t, addiNeg, { uint32_t tmp; bool ov; ADD32_OV(GPR_U32(ctx, 1), (int32_t)4294967280, tmp, ov); if (ov) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S32(ctx, 2, (int32_t)tmp); }); + expectGpr(t, *ctx, 2, sext32(0xFFFFFFF5u), "addi 5 + (-16) sign-extends the 32-bit result"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x7FFFFFFFu); + setGpr(*ctx, 2, 0x1234u); + runAddi(); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), + "addi 0x7FFFFFFF + 5 must raise an integer overflow exception at the faulting pc"); + expectGpr(t, *ctx, 2, 0x1234u, "addi overflow must leave rt unchanged"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x80000000u); + PS2X_EXEC_EMITTED(t, addiNeg, { uint32_t tmp; bool ov; ADD32_OV(GPR_U32(ctx, 1), (int32_t)4294967280, tmp, ov); if (ov) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S32(ctx, 2, (int32_t)tmp); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "addi INT32_MIN + (-16) must raise an overflow exception"); + + expectTranslation(t, decode(encodeI(OPCODE_ADDI, 1, 0, 5)), "// NOP (addi to $zero)"); + }); + + tc.Run("ADDIU wraps at 32 bits, sign-extends and never traps", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction addiu = decode(encodeI(OPCODE_ADDIU, 1, 2, 1)); // addiu $2, $1, 1 + const Instruction addiuNeg = decode(encodeI(OPCODE_ADDIU, 1, 2, imm16(-1))); // addiu $2, $1, -1 + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + + setGpr32(*ctx, 1, 0x7FFFFFFFu); + setGpr(*ctx, 2, 0u, kUpperHalf); + PS2X_EXEC_EMITTED(t, addiu, SET_GPR_S32(ctx, 2, (int32_t)ADD32(GPR_U32(ctx, 1), 1));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000000ull, "addiu 0x7FFFFFFF + 1 wraps and sign-extends without trapping"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "addiu preserves bits 64..127 of rt"); + + setGpr(*ctx, 1, 0x00000000FFFFFFFFull); // only the low 32 bits of rs take part + PS2X_EXEC_EMITTED(t, addiu, SET_GPR_S32(ctx, 2, (int32_t)ADD32(GPR_U32(ctx, 1), 1));); + expectGpr(t, *ctx, 2, 0u, "addiu uses the low 32 bits of rs"); + + setGpr(*ctx, 1, 0u); + PS2X_EXEC_EMITTED(t, addiuNeg, SET_GPR_S32(ctx, 2, (int32_t)ADD32(GPR_U32(ctx, 1), 4294967295));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFFull, "addiu 0 + (-1)"); + + expectTranslation(t, decode(encodeI(OPCODE_ADDIU, 1, 0, 1)), "// NOP (addiu $zero, ...)"); + }); + + tc.Run("SLTI compares the full 64-bit register with the sign-extended immediate", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction sltiZero = decode(encodeI(OPCODE_SLTI, 1, 2, 0)); // slti $2, $1, 0 + const Instruction sltiNeg = decode(encodeI(OPCODE_SLTI, 1, 2, imm16(-1))); // slti $2, $1, -1 + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); + PS2X_EXEC_EMITTED(t, sltiZero, SET_GPR_U64(ctx, 2, ((int64_t)GPR_S64(ctx, 1) < (int64_t)(int32_t)0) ? 1 : 0);); + expectGpr(t, *ctx, 2, 1u, "slti -1 < 0"); + + setGpr(*ctx, 1, 0x00000000FFFFFFFFull); // positive as a 64-bit value even though bit 31 is set + PS2X_EXEC_EMITTED(t, sltiZero, SET_GPR_U64(ctx, 2, ((int64_t)GPR_S64(ctx, 1) < (int64_t)(int32_t)0) ? 1 : 0);); + expectGpr(t, *ctx, 2, 0u, "slti compares 64 bits, not the low word"); + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFEull); + PS2X_EXEC_EMITTED(t, sltiNeg, SET_GPR_U64(ctx, 2, ((int64_t)GPR_S64(ctx, 1) < (int64_t)(int32_t)4294967295) ? 1 : 0);); + expectGpr(t, *ctx, 2, 1u, "slti -2 < -1"); + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); + PS2X_EXEC_EMITTED(t, sltiNeg, SET_GPR_U64(ctx, 2, ((int64_t)GPR_S64(ctx, 1) < (int64_t)(int32_t)4294967295) ? 1 : 0);); + expectGpr(t, *ctx, 2, 0u, "slti -1 < -1 is false"); + }); + + tc.Run("SLTIU sign-extends the immediate and then compares unsigned", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction sltiuNeg = decode(encodeI(OPCODE_SLTIU, 1, 2, imm16(-1))); // sltiu $2, $1, -1 + const Instruction sltiuFive = decode(encodeI(OPCODE_SLTIU, 1, 2, 5)); // sltiu $2, $1, 5 + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFEull); + PS2X_EXEC_EMITTED(t, sltiuNeg, SET_GPR_U64(ctx, 2, ((uint64_t)GPR_U64(ctx, 1) < (uint64_t)(int64_t)(int32_t)4294967295) ? 1 : 0);); + expectGpr(t, *ctx, 2, 1u, "sltiu: -1 becomes 0xFFFFFFFFFFFFFFFF, so 0x...FFFE is below it"); + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); + PS2X_EXEC_EMITTED(t, sltiuNeg, SET_GPR_U64(ctx, 2, ((uint64_t)GPR_U64(ctx, 1) < (uint64_t)(int64_t)(int32_t)4294967295) ? 1 : 0);); + expectGpr(t, *ctx, 2, 0u, "sltiu: nothing is below 0xFFFFFFFFFFFFFFFF"); + + setGpr(*ctx, 1, 0xFFFFFFFF00000004ull); + PS2X_EXEC_EMITTED(t, sltiuFive, SET_GPR_U64(ctx, 2, ((uint64_t)GPR_U64(ctx, 1) < (uint64_t)(int64_t)(int32_t)5) ? 1 : 0);); + expectGpr(t, *ctx, 2, 0u, "sltiu compares all 64 bits unsigned"); + + setGpr(*ctx, 1, 4u); + PS2X_EXEC_EMITTED(t, sltiuFive, SET_GPR_U64(ctx, 2, ((uint64_t)GPR_U64(ctx, 1) < (uint64_t)(int64_t)(int32_t)5) ? 1 : 0);); + expectGpr(t, *ctx, 2, 1u, "sltiu 4 < 5"); + }); + + tc.Run("ANDI, ORI and XORI zero-extend the immediate over 64-bit operands", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction andi = decode(encodeI(OPCODE_ANDI, 1, 2, 0xFFFF)); // andi $2, $1, 0xFFFF + const Instruction ori = decode(encodeI(OPCODE_ORI, 1, 2, 0x8000)); // ori $2, $1, 0x8000 + const Instruction xori = decode(encodeI(OPCODE_XORI, 1, 2, 0xFFFF)); // xori $2, $1, 0xFFFF + + setGpr(*ctx, 1, 0xFFFFFFFFFFFF1234ull); + PS2X_EXEC_EMITTED(t, andi, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) & (uint64_t)(uint16_t)65535);); + expectGpr(t, *ctx, 2, 0x1234u, "andi keeps only the low 16 bits"); + + setGpr(*ctx, 1, 0x0000000100000000ull); + PS2X_EXEC_EMITTED(t, ori, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) | (uint64_t)(uint16_t)32768);); + expectGpr(t, *ctx, 2, 0x0000000100008000ull, "ori does not sign-extend bit 15 and keeps the upper bits of rs"); + + setGpr(*ctx, 1, 0x00000000FFFFFFFFull); + PS2X_EXEC_EMITTED(t, xori, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) ^ (uint64_t)(uint16_t)65535);); + expectGpr(t, *ctx, 2, 0x00000000FFFF0000ull, "xori flips only the low 16 bits"); + }); + + tc.Run("LUI loads bits 16..31 and sign-extends to 64 bits", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction luiHigh = decode(encodeI(OPCODE_LUI, 0, 2, 0x8000)); // lui $2, 0x8000 + const Instruction luiLow = decode(encodeI(OPCODE_LUI, 0, 2, 0x1234)); // lui $2, 0x1234 + + PS2X_EXEC_EMITTED(t, luiHigh, SET_GPR_S32(ctx, 2, (int32_t)((uint32_t)32768 << 16));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000000ull, "lui 0x8000"); + + PS2X_EXEC_EMITTED(t, luiLow, SET_GPR_S32(ctx, 2, (int32_t)((uint32_t)4660 << 16));); + expectGpr(t, *ctx, 2, 0x12340000u, "lui 0x1234"); + }); + + tc.Run("DADDI and DADDIU add 64-bit values and only DADDI traps on overflow", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction daddi = decode(encodeI(OPCODE_DADDI, 1, 2, imm16(-1))); // daddi $2, $1, -1 + const Instruction daddiOne = decode(encodeI(OPCODE_DADDI, 1, 2, 1)); // daddi $2, $1, 1 + const Instruction daddiu = decode(encodeI(OPCODE_DADDIU, 1, 2, 1)); // daddiu $2, $1, 1 + const Instruction daddiuNeg = decode(encodeI(OPCODE_DADDIU, 1, 2, imm16(-1))); + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0x0000000100000000ull); + PS2X_EXEC_EMITTED(t, daddi, { int64_t src = (int64_t)GPR_S64(ctx, 1); int64_t imm = (int64_t)(int32_t)4294967295; int64_t res = (int64_t)((uint64_t)src + (uint64_t)imm); if (((src ^ imm) >= 0) && ((src ^ res) < 0)) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S64(ctx, 2, res); }); + expectGpr(t, *ctx, 2, 0x00000000FFFFFFFFull, "daddi 0x100000000 + (-1) is a 64-bit add"); + t.IsTrue(noException(*ctx), "daddi without overflow must not raise an exception"); + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0x7FFFFFFFFFFFFFFFull); + setGpr(*ctx, 2, 0x1234u); + PS2X_EXEC_EMITTED(t, daddiOne, { int64_t src = (int64_t)GPR_S64(ctx, 1); int64_t imm = (int64_t)(int32_t)1; int64_t res = (int64_t)((uint64_t)src + (uint64_t)imm); if (((src ^ imm) >= 0) && ((src ^ res) < 0)) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S64(ctx, 2, res); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "daddi INT64_MAX + 1 must raise an integer overflow exception"); + expectGpr(t, *ctx, 2, 0x1234u, "daddi overflow must leave rt unchanged"); + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0x7FFFFFFFFFFFFFFFull); + PS2X_EXEC_EMITTED(t, daddiu, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) + (uint64_t)(int64_t)(int32_t)1);); + expectGpr(t, *ctx, 2, 0x8000000000000000ull, "daddiu INT64_MAX + 1 wraps"); + t.IsTrue(noException(*ctx), "daddiu never raises an exception"); + + setGpr(*ctx, 1, 0x0000000100000000ull); + PS2X_EXEC_EMITTED(t, daddiuNeg, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) + (uint64_t)(int64_t)(int32_t)4294967295);); + expectGpr(t, *ctx, 2, 0x00000000FFFFFFFFull, "daddiu sign-extends the immediate to 64 bits"); + }); + + tc.Run("Byte, halfword, word and doubleword loads extend as encoded", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + writeRam(rdram, kDataAddress + 0u, 0x80u); + writeRam(rdram, kDataAddress + 2u, 0x8000u); + writeRam(rdram, kDataAddress + 4u, 0x80000000u); + writeRam(rdram, kDataAddress + 8u, 0x8000000000000000ull); + setGpr(*ctx, 1, kDataAddress); + setGpr(*ctx, 2, 0u, kUpperHalf); + + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LB, 1, 2, 0)), SET_GPR_S32(ctx, 2, (int8_t)READ8(ADD32(GPR_U32(ctx, 1), 0)));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFF80ull, "lb sign-extends"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LBU, 1, 2, 0)), SET_GPR_ZE32(ctx, 2, (uint8_t)READ8(ADD32(GPR_U32(ctx, 1), 0)));); + expectGpr(t, *ctx, 2, 0x80u, "lbu zero-extends"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LH, 1, 2, 2)), SET_GPR_S32(ctx, 2, (int16_t)READ16(ADD32(GPR_U32(ctx, 1), 2)));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFF8000ull, "lh sign-extends"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LHU, 1, 2, 2)), SET_GPR_ZE32(ctx, 2, (uint16_t)READ16(ADD32(GPR_U32(ctx, 1), 2)));); + expectGpr(t, *ctx, 2, 0x8000u, "lhu zero-extends"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LW, 1, 2, 4)), SET_GPR_S32(ctx, 2, (int32_t)READ32(ADD32(GPR_U32(ctx, 1), 4)));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000000ull, "lw sign-extends"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LWU, 1, 2, 4)), SET_GPR_ZE32(ctx, 2, READ32(ADD32(GPR_U32(ctx, 1), 4)));); + expectGpr(t, *ctx, 2, 0x80000000u, "lwu zero-extends"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LD, 1, 2, 8)), SET_GPR_U64(ctx, 2, READ64(ADD32(GPR_U32(ctx, 1), 8)));); + expectGpr(t, *ctx, 2, 0x8000000000000000ull, "ld loads 64 bits"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "scalar loads preserve bits 64..127 of rt"); + + // Negative displacements are sign-extended before the 32-bit address add. + setGpr(*ctx, 1, kDataAddress + 16u); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LB, 1, 2, imm16(-16))), SET_GPR_S32(ctx, 2, (int8_t)READ8(ADD32(GPR_U32(ctx, 1), 4294967280)));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFF80ull, "lb with a negative displacement"); + }); + + tc.Run("Byte, halfword, word and doubleword stores write only the addressed bytes", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + fillRam(rdram, kDataAddress, 32u); + setGpr(*ctx, 1, kDataAddress); + setGpr(*ctx, 2, 0x1122334455667788ull, 0x99AABBCCDDEEFF00ull); + + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_SB, 1, 2, 0)), WRITE8(ADD32(GPR_U32(ctx, 1), 0), (uint8_t)GPR_U32(ctx, 2));); + expectU64(t, readRam(rdram, kDataAddress + 0u), 0x88u, "sb stores the low byte"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 1u, 1u), "sb must not touch the next byte"); + + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_SH, 1, 2, 2)), WRITE16(ADD32(GPR_U32(ctx, 1), 2), (uint16_t)GPR_U32(ctx, 2));); + expectU64(t, readRam(rdram, kDataAddress + 2u), 0x7788u, "sh stores the low halfword"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 4u, 1u), "sh must not touch the next byte"); + + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_SW, 1, 2, 4)), WRITE32(ADD32(GPR_U32(ctx, 1), 4), GPR_U32(ctx, 2));); + expectU64(t, readRam(rdram, kDataAddress + 4u), 0x55667788u, "sw stores the low word"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 8u, 1u), "sw must not touch the next byte"); + + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_SD, 1, 2, 8)), WRITE64(ADD32(GPR_U32(ctx, 1), 8), GPR_U64(ctx, 2));); + expectU64(t, readRam(rdram, kDataAddress + 8u), 0x1122334455667788ull, "sd stores the low doubleword"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 16u, 16u), "sd must not touch the following bytes"); + + // Negative displacement. + setGpr(*ctx, 1, kDataAddress + 32u); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_SB, 1, 2, imm16(-16))), WRITE8(ADD32(GPR_U32(ctx, 1), 4294967280), (uint8_t)GPR_U32(ctx, 2));); + expectU64(t, readRam(rdram, kDataAddress + 16u), 0x88u, "sb with a negative displacement"); + }); + + tc.Run("LQ and SQ move 128 bits and ignore the low four address bits", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction lq = decode(encodeI(OPCODE_LQ, 1, 2, 16)); // lq $2, 16($1) + const Instruction sq = decode(encodeI(OPCODE_SQ, 1, 2, 16)); // sq $2, 16($1) + constexpr uint64_t kLo = 0x1122334455667788ull; + constexpr uint64_t kHi = 0x99AABBCCDDEEFF00ull; + + writeRam(rdram, kDataAddress + 16u, kLo); + writeRam(rdram, kDataAddress + 24u, kHi); + setGpr(*ctx, 1, kDataAddress); + PS2X_EXEC_EMITTED(t, lq, SET_GPR_VEC(ctx, 2, READ128(ADD32(GPR_U32(ctx, 1), 16) & ~0xFu));); + expectGpr(t, *ctx, 2, kLo, "lq low 64 bits"); + expectU64(t, gprHi(*ctx, 2), kHi, "lq high 64 bits"); + + setGpr(*ctx, 1, kDataAddress + 5u); // effective address 0x2015 is silently aligned to 0x2010 + setGpr(*ctx, 2, 0u, 0u); + PS2X_EXEC_EMITTED(t, lq, SET_GPR_VEC(ctx, 2, READ128(ADD32(GPR_U32(ctx, 1), 16) & ~0xFu));); + expectGpr(t, *ctx, 2, kLo, "lq ignores the low four address bits (low half)"); + expectU64(t, gprHi(*ctx, 2), kHi, "lq ignores the low four address bits (high half)"); + + fillRam(rdram, kDataAddress + 32u, 32u); + setGpr(*ctx, 1, kDataAddress + 16u + 7u); // effective address 0x2027 is silently aligned to 0x2020 + setGpr(*ctx, 2, kHi, kLo); + PS2X_EXEC_EMITTED(t, sq, WRITE128(ADD32(GPR_U32(ctx, 1), 16) & ~0xFu, GPR_VEC(ctx, 2));); + expectU64(t, readRam(rdram, kDataAddress + 32u), kHi, "sq low 64 bits land at the aligned address"); + expectU64(t, readRam(rdram, kDataAddress + 40u), kLo, "sq high 64 bits land at the aligned address"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 48u, 16u), "sq must not spill past the aligned quadword"); + + // $zero reads as an all-zero quadword and is never written. + setGpr(*ctx, 1, kDataAddress); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_SQ, 1, 0, 32)), WRITE128(ADD32(GPR_U32(ctx, 1), 32) & ~0xFu, GPR_VEC(ctx, 0));); + expectU64(t, readRam(rdram, kDataAddress + 32u), 0u, "sq $zero stores zeros (low)"); + expectU64(t, readRam(rdram, kDataAddress + 40u), 0u, "sq $zero stores zeros (high)"); + PS2X_EXEC_EMITTED(t, decode(encodeI(OPCODE_LQ, 1, 0, 16)), SET_GPR_VEC(ctx, 0, READ128(ADD32(GPR_U32(ctx, 1), 16) & ~0xFu));); + expectGpr(t, *ctx, 0, 0u, "lq $zero leaves $zero at zero"); + expectU64(t, gprHi(*ctx, 0), 0u, "lq $zero leaves the upper half of $zero at zero"); + }); + + tc.Run("LWL and LWR merge unaligned words and extend the way the hardware does", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction lwl = decode(encodeI(OPCODE_LWL, 1, 2, 0)); // lwl $2, 0($1) + const Instruction lwr = decode(encodeI(OPCODE_LWR, 1, 2, 0)); // lwr $2, 0($1) + constexpr uint32_t kMemoryWord = 0x11223344u; + constexpr uint32_t kRegisterWord = 0xAABBCCDDu; + constexpr uint64_t kUpperWord = 0x5555AAAA00000000ull; // bits 32..63 of rt before the load + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; // bits 64..127 of rt + writeRam(rdram, kDataAddress, kMemoryWord); + + for (uint32_t offset = 0; offset < 4u; ++offset) + { + const std::string at = " at offset " + std::to_string(offset); + setGpr(*ctx, 1, kDataAddress + offset); + + setGpr(*ctx, 2, kUpperWord | kRegisterWord, kUpperHalf); + PS2X_EXEC_EMITTED(t, lwl, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~3u; uint32_t offset = addr & 3u; uint32_t mem = READ32(aligned_addr); uint32_t shift = (3u - offset) << 3; uint32_t keepMask = (shift == 0) ? 0u : ((1u << shift) - 1u); uint32_t merged = (GPR_U32(ctx, 2) & keepMask) | (mem << shift); SET_GPR_S32(ctx, 2, (int32_t)merged); }); + const uint32_t expectedLeft = static_cast(refLoadLeft(rdram, kDataAddress + offset, 4u, kRegisterWord)); + expectGpr(t, *ctx, 2, sext32(expectedLeft), "lwl merges and sign-extends" + at); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "lwl preserves bits 64..127" + at); + + setGpr(*ctx, 2, kUpperWord | kRegisterWord, kUpperHalf); + PS2X_EXEC_EMITTED(t, lwr, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~3u; uint32_t offset = addr & 3u; uint32_t mem = READ32(aligned_addr); uint32_t shift = offset << 3; uint32_t keepMask = (offset == 0) ? 0u : (0xFFFFFFFFu << ((4u - offset) << 3)); uint32_t merged32 = (GPR_U32(ctx, 2) & keepMask) | (mem >> shift); uint64_t merged64 = (GPR_U64(ctx, 2) & 0xFFFFFFFF00000000ull) | (uint64_t)merged32; if (offset == 0) merged64 = (uint64_t)(int64_t)(int32_t)merged32; SET_GPR_U64(ctx, 2, merged64); }); + const uint32_t expectedRight = static_cast(refLoadRight(rdram, kDataAddress + offset, 4u, kRegisterWord)); + // A full-word LWR sign-extends; a partial one leaves bits 32..63 alone. + const uint64_t expected = (offset == 0u) ? sext32(expectedRight) : (kUpperWord | expectedRight); + expectGpr(t, *ctx, 2, expected, "lwr merges" + at); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "lwr preserves bits 64..127" + at); + } + + // Spot-check the reference model against the documented merge tables (mem 11223344, reg AABBCCDD). + expectU64(t, refLoadLeft(rdram, kDataAddress + 1u, 4u, kRegisterWord), 0x3344CCDDu, "lwl reference at offset 1"); + expectU64(t, refLoadRight(rdram, kDataAddress + 1u, 4u, kRegisterWord), 0xAA112233u, "lwr reference at offset 1"); + expectU64(t, refLoadLeft(rdram, kDataAddress + 3u, 4u, kRegisterWord), 0x11223344u, "lwl reference at offset 3 loads the whole word"); + expectU64(t, refLoadRight(rdram, kDataAddress + 0u, 4u, kRegisterWord), 0x11223344u, "lwr reference at offset 0 loads the whole word"); + }); + + tc.Run("LDL and LDR merge unaligned doublewords", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction ldl = decode(encodeI(OPCODE_LDL, 1, 2, 0)); // ldl $2, 0($1) + const Instruction ldr = decode(encodeI(OPCODE_LDR, 1, 2, 0)); // ldr $2, 0($1) + constexpr uint64_t kMemoryDword = 0x1122334455667788ull; + constexpr uint64_t kRegisterDword = 0xAABBCCDDEEFF0011ull; + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + writeRam(rdram, kDataAddress, kMemoryDword); + + for (uint32_t offset = 0; offset < 8u; ++offset) + { + const std::string at = " at offset " + std::to_string(offset); + setGpr(*ctx, 1, kDataAddress + offset); + + setGpr(*ctx, 2, kRegisterDword, kUpperHalf); + PS2X_EXEC_EMITTED(t, ldl, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~7u; uint32_t offset = addr & 7u; uint64_t mem = READ64(aligned_addr); uint32_t shift = (7u - offset) << 3; uint64_t keepMask = (shift == 0) ? 0ull : ((1ull << shift) - 1ull); SET_GPR_U64(ctx, 2, (GPR_U64(ctx, 2) & keepMask) | (mem << shift)); }); + expectGpr(t, *ctx, 2, refLoadLeft(rdram, kDataAddress + offset, 8u, kRegisterDword), "ldl merges" + at); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "ldl preserves bits 64..127" + at); + + setGpr(*ctx, 2, kRegisterDword, kUpperHalf); + PS2X_EXEC_EMITTED(t, ldr, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~7u; uint32_t offset = addr & 7u; uint64_t mem = READ64(aligned_addr); uint32_t shift = offset << 3; uint64_t keepMask = (offset == 0) ? 0ull : (0xFFFFFFFFFFFFFFFFull << ((8u - offset) << 3)); SET_GPR_U64(ctx, 2, (GPR_U64(ctx, 2) & keepMask) | (mem >> shift)); }); + expectGpr(t, *ctx, 2, refLoadRight(rdram, kDataAddress + offset, 8u, kRegisterDword), "ldr merges" + at); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "ldr preserves bits 64..127" + at); + } + + expectU64(t, refLoadLeft(rdram, kDataAddress + 3u, 8u, kRegisterDword), 0x55667788EEFF0011ull, "ldl reference at offset 3"); + expectU64(t, refLoadRight(rdram, kDataAddress + 3u, 8u, kRegisterDword), 0xAABBCC1122334455ull, "ldr reference at offset 3"); + }); + + tc.Run("SWL and SWR store the unaligned parts of a word", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction swl = decode(encodeI(OPCODE_SWL, 1, 2, 0)); // swl $2, 0($1) + const Instruction swr = decode(encodeI(OPCODE_SWR, 1, 2, 0)); // swr $2, 0($1) + constexpr uint32_t kMemoryWord = 0x11223344u; + constexpr uint64_t kRegisterValue = 0x5555AAAAAABBCCDDull; // only the low word takes part + setGpr(*ctx, 2, kRegisterValue); + + for (uint32_t offset = 0; offset < 4u; ++offset) + { + const std::string at = " at offset " + std::to_string(offset); + setGpr(*ctx, 1, kDataAddress + offset); + + uint8_t expected[8]; + fillRam(rdram, kDataAddress, 8u); + writeRam(rdram, kDataAddress, kMemoryWord); + std::memcpy(expected, rdram + kDataAddress, sizeof(expected)); + refStoreLeft(expected, offset, 4u, kRegisterValue & 0xFFFFFFFFu); + PS2X_EXEC_EMITTED(t, swl, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~3u; uint32_t offset = addr & 3u; uint32_t shift = (3u - offset) << 3; uint32_t mask = 0xFFFFFFFFu >> shift; uint32_t old_data = READ32(aligned_addr); uint32_t val = GPR_U32(ctx, 2); uint32_t new_data = (old_data & ~mask) | ((val >> shift) & mask); WRITE32(aligned_addr, new_data); }); + t.IsTrue(std::memcmp(expected, rdram + kDataAddress, sizeof(expected)) == 0, "swl stores the high bytes of rt from the effective address down" + at); + + fillRam(rdram, kDataAddress, 8u); + writeRam(rdram, kDataAddress, kMemoryWord); + std::memcpy(expected, rdram + kDataAddress, sizeof(expected)); + refStoreRight(expected, offset, 4u, kRegisterValue & 0xFFFFFFFFu); + PS2X_EXEC_EMITTED(t, swr, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~3u; uint32_t offset = addr & 3u; uint32_t shift = offset << 3; uint32_t mask = 0xFFFFFFFFu << shift; uint32_t old_data = READ32(aligned_addr); uint32_t val = GPR_U32(ctx, 2); uint32_t new_data = (old_data & ~mask) | ((val << shift) & mask); WRITE32(aligned_addr, new_data); }); + t.IsTrue(std::memcmp(expected, rdram + kDataAddress, sizeof(expected)) == 0, "swr stores the low bytes of rt from the effective address up" + at); + } + + // Spot-check the reference model against the documented tables (mem 11223344, reg AABBCCDD). + uint8_t word[4]; + writeRam(word, 0u, kMemoryWord); + refStoreLeft(word, 1u, 4u, 0xAABBCCDDu); + expectU64(t, readRam(word, 0u), 0x1122AABBu, "swl reference at offset 1"); + writeRam(word, 0u, kMemoryWord); + refStoreRight(word, 1u, 4u, 0xAABBCCDDu); + expectU64(t, readRam(word, 0u), 0xBBCCDD44u, "swr reference at offset 1"); + }); + + tc.Run("SDL and SDR store the unaligned parts of a doubleword", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction sdl = decode(encodeI(OPCODE_SDL, 1, 2, 0)); // sdl $2, 0($1) + const Instruction sdr = decode(encodeI(OPCODE_SDR, 1, 2, 0)); // sdr $2, 0($1) + constexpr uint64_t kMemoryDword = 0x1122334455667788ull; + constexpr uint64_t kRegisterDword = 0xAABBCCDDEEFF0011ull; + setGpr(*ctx, 2, kRegisterDword); + + for (uint32_t offset = 0; offset < 8u; ++offset) + { + const std::string at = " at offset " + std::to_string(offset); + setGpr(*ctx, 1, kDataAddress + offset); + + uint8_t expected[16]; + fillRam(rdram, kDataAddress, 16u); + writeRam(rdram, kDataAddress, kMemoryDword); + std::memcpy(expected, rdram + kDataAddress, sizeof(expected)); + refStoreLeft(expected, offset, 8u, kRegisterDword); + PS2X_EXEC_EMITTED(t, sdl, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~7u; uint32_t offset = addr & 7u; uint32_t shift = (7u - offset) << 3; uint64_t mask = 0xFFFFFFFFFFFFFFFFull >> shift; uint64_t old_data = READ64(aligned_addr); uint64_t val = GPR_U64(ctx, 2); uint64_t new_data = (old_data & ~mask) | ((val >> shift) & mask); WRITE64(aligned_addr, new_data); }); + t.IsTrue(std::memcmp(expected, rdram + kDataAddress, sizeof(expected)) == 0, "sdl stores the high bytes of rt from the effective address down" + at); + + fillRam(rdram, kDataAddress, 16u); + writeRam(rdram, kDataAddress, kMemoryDword); + std::memcpy(expected, rdram + kDataAddress, sizeof(expected)); + refStoreRight(expected, offset, 8u, kRegisterDword); + PS2X_EXEC_EMITTED(t, sdr, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); uint32_t aligned_addr = addr & ~7u; uint32_t offset = addr & 7u; uint32_t shift = offset << 3; uint64_t mask = 0xFFFFFFFFFFFFFFFFull << shift; uint64_t old_data = READ64(aligned_addr); uint64_t val = GPR_U64(ctx, 2); uint64_t new_data = (old_data & ~mask) | ((val << shift) & mask); WRITE64(aligned_addr, new_data); }); + t.IsTrue(std::memcmp(expected, rdram + kDataAddress, sizeof(expected)) == 0, "sdr stores the low bytes of rt from the effective address up" + at); + } + }); + + tc.Run("LWC1 and SWC1 copy raw bits between memory and the FPU", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction lwc1 = decode(encodeI(OPCODE_LWC1, 1, 5, 4)); // lwc1 $f5, 4($1) + const Instruction swc1 = decode(encodeI(OPCODE_SWC1, 1, 5, 8)); // swc1 $f5, 8($1) + constexpr uint32_t kMinusOne = 0xBF800000u; + + writeRam(rdram, kDataAddress + 4u, kMinusOne); + setGpr(*ctx, 1, kDataAddress); + PS2X_EXEC_EMITTED(t, lwc1, { uint32_t bits = READ32(ADD32(GPR_U32(ctx, 1), 4)); float f; std::memcpy(&f, &bits, sizeof(f)); ctx->f[5] = f; }); + t.IsTrue(ctx->f[5] == -1.0f, "lwc1 loads the word into the FPU register"); + + fillRam(rdram, kDataAddress + 8u, 8u); + PS2X_EXEC_EMITTED(t, swc1, { float f = ctx->f[5]; uint32_t bits; std::memcpy(&bits, &f, sizeof(bits)); WRITE32(ADD32(GPR_U32(ctx, 1), 8), bits); }); + expectU64(t, readRam(rdram, kDataAddress + 8u), kMinusOne, "swc1 stores the FPU register bits"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 12u, 4u), "swc1 writes exactly one word"); + }); + + tc.Run("LQC2 and SQC2 move quadwords between memory and VU0 registers", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction lqc2 = decode(encodeI(OPCODE_LQC2, 1, 5, 16)); // lqc2 $vf5, 16($1) + const Instruction sqc2 = decode(encodeI(OPCODE_SQC2, 1, 5, 32)); // sqc2 $vf5, 32($1) + const uint32_t lanes[4] = {0x3F800000u, 0x40000000u, 0x40400000u, 0x40800000u}; // 1.0f, 2.0f, 3.0f, 4.0f + + std::memcpy(rdram + kDataAddress + 16u, lanes, sizeof(lanes)); + setGpr(*ctx, 1, kDataAddress); + PS2X_EXEC_EMITTED(t, lqc2, ctx->vu0_vf[5] = _mm_castsi128_ps(READ128(ADD32(GPR_U32(ctx, 1), 16)));); + uint32_t loaded[4]; + std::memcpy(loaded, &ctx->vu0_vf[5], sizeof(loaded)); + t.IsTrue(std::memcmp(loaded, lanes, sizeof(lanes)) == 0, "lqc2 loads x, y, z, w from consecutive words"); + + fillRam(rdram, kDataAddress + 32u, 32u); + PS2X_EXEC_EMITTED(t, sqc2, WRITE128(ADD32(GPR_U32(ctx, 1), 32), _mm_castps_si128(ctx->vu0_vf[5]));); + t.IsTrue(std::memcmp(rdram + kDataAddress + 32u, lanes, sizeof(lanes)) == 0, "sqc2 stores x, y, z, w to consecutive words"); + t.IsTrue(ramUntouched(rdram, kDataAddress + 48u, 16u), "sqc2 writes exactly one quadword"); + }); + + tc.Run("LL and SC implement the load-linked reservation", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction ll = decode(encodeI(OPCODE_LL, 1, 2, 0)); // ll $2, 0($1) + const Instruction sc = decode(encodeI(OPCODE_SC, 1, 2, 0)); // sc $2, 0($1) + auto runSc = [&] { + PS2X_EXEC_EMITTED(t, sc, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); if (ctx->llbit && ctx->lladdr == addr) { WRITE32(addr, GPR_U32(ctx, 2)); SET_GPR_S32(ctx, 2, 1); } else { SET_GPR_S32(ctx, 2, 0); } ctx->llbit = 0; ctx->lladdr = 0; }); + }; + + writeRam(rdram, kDataAddress, 0x80000001u); + setGpr(*ctx, 1, kDataAddress); + PS2X_EXEC_EMITTED(t, ll, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); SET_GPR_S32(ctx, 2, (int32_t)READ32(addr)); ctx->llbit = 1; ctx->lladdr = addr; }); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000001ull, "ll loads and sign-extends the word"); + t.IsTrue(ctx->llbit == 1u && ctx->lladdr == kDataAddress, "ll records the reservation address"); + + setGpr(*ctx, 2, 0x55u); + runSc(); + expectU64(t, readRam(rdram, kDataAddress), 0x55u, "sc with a live reservation stores the word"); + expectGpr(t, *ctx, 2, 1u, "sc success writes 1 to rt"); + t.IsTrue(ctx->llbit == 0u && ctx->lladdr == 0u, "sc consumes the reservation"); + + setGpr(*ctx, 2, 0x66u); + runSc(); + expectU64(t, readRam(rdram, kDataAddress), 0x55u, "sc without a reservation must not store"); + expectGpr(t, *ctx, 2, 0u, "sc failure writes 0 to rt"); + + PS2X_EXEC_EMITTED(t, ll, { uint32_t addr = ADD32(GPR_U32(ctx, 1), 0); SET_GPR_S32(ctx, 2, (int32_t)READ32(addr)); ctx->llbit = 1; ctx->lladdr = addr; }); + setGpr(*ctx, 1, kDataAddress + 4u); // reservation is for a different address + setGpr(*ctx, 2, 0x77u); + runSc(); + expectU64(t, readRam(rdram, kDataAddress + 4u), 0u, "sc to another address than the reservation must not store"); + expectGpr(t, *ctx, 2, 0u, "sc to another address fails"); + t.IsTrue(ctx->llbit == 0u, "a failed sc still clears the reservation"); + }); + + tc.Run("CACHE and PREF are hints that emit no code", [](TestCase &t) { + expectTranslation(t, decode(encodeI(OPCODE_CACHE, 1, 0x14, 0)), "// CACHE instruction (ignored)"); + expectTranslation(t, decode(encodeI(OPCODE_PREF, 1, 0, 32)), "// PREF instruction (ignored)"); + }); + + tc.Run("J and JAL jump within the current 256 MiB segment and JAL links $ra", [](TestCase &t) { + constexpr uint32_t kTargetField = 0x100000u; // byte target 0x00400000 + const uint32_t jWord = encodeJ(OPCODE_J, kTargetField); + const uint32_t jalWord = encodeJ(OPCODE_JAL, kTargetField); + + expectTranslation(t, decode(jWord), "// J 0x400000 - Handled by branch logic"); + expectTranslation(t, decode(jalWord), "// JAL 0x400000 - Handled by branch logic"); + + const std::string j = emitBranchFunction(jWord); + const size_t jSlot = j.find(kDelaySlotCode); + const size_t jTarget = j.find("ctx->pc = 0x400000u;"); + t.IsTrue(jSlot != std::string::npos && jTarget != std::string::npos && jSlot < jTarget, + "j executes its delay slot before leaving for the target"); + t.IsTrue(j.find("runtime->dispatchGuestBranch(rdram, ctx, 0x400000u, 0x1000u, 0x0u, PS2Runtime::GuestBranchKind::DirectJump, \"J\")") != std::string::npos, + "j to an external target dispatches as a direct jump"); + t.IsTrue(j.find("SET_GPR_U32(ctx, 31") == std::string::npos, "j must not write $ra"); + + const std::string jal = emitBranchFunction(jalWord); + const size_t jalLink = jal.find("SET_GPR_U32(ctx, 31, 0x1008u);"); + const size_t jalSlot = jal.find(kDelaySlotCode); + t.IsTrue(jalLink != std::string::npos, "jal writes the return address (pc + 8) to $ra"); + t.IsTrue(jalLink != std::string::npos && jalSlot != std::string::npos && jalLink < jalSlot, + "jal links before the delay slot so the slot can observe $ra"); + t.IsTrue(jal.find("runtime->dispatchGuestBranch(rdram, ctx, 0x400000u, 0x1000u, 0x1008u, PS2Runtime::GuestBranchKind::DirectCall, \"JAL\")") != std::string::npos, + "jal to an external target dispatches as a direct call with its return pc"); + + // The upper four bits of the target come from the delay-slot pc. + const std::string far = emitBranchFunction(jWord, 0x10001000u); + t.IsTrue(far.find("ctx->pc = 0x10400000u;") != std::string::npos, "j keeps the current 256 MiB segment"); + + // An in-function target becomes a local jump. + const std::string local = emitBranchFunction(encodeJ(OPCODE_J, (kCodeAddress + 12u) >> 2)); + t.IsTrue(local.find("goto label_100c;") != std::string::npos, "j to an in-function address jumps locally"); + }); + + tc.Run("BEQ and BNE compare 64 bits; BLEZ and BGTZ test the 64-bit sign", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const uint32_t beq = encodeI(OPCODE_BEQ, 1, 2, kBranchOffsetWords); + const uint32_t bne = encodeI(OPCODE_BNE, 1, 2, kBranchOffsetWords); + const uint32_t blez = encodeI(OPCODE_BLEZ, 1, 0, kBranchOffsetWords); + const uint32_t bgtz = encodeI(OPCODE_BGTZ, 1, 0, kBranchOffsetWords); + + setGpr(*ctx, 1, 0x0000000100000005ull); + setGpr(*ctx, 2, 0x0000000000000005ull); // same low word, different high word + t.IsFalse(PS2X_BRANCH_TAKEN(t, beq, GPR_U64(ctx, 1) == GPR_U64(ctx, 2)), "beq compares all 64 bits"); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bne, GPR_U64(ctx, 1) != GPR_U64(ctx, 2)), "bne compares all 64 bits"); + setGpr(*ctx, 2, 0x0000000100000005ull); + t.IsTrue(PS2X_BRANCH_TAKEN(t, beq, GPR_U64(ctx, 1) == GPR_U64(ctx, 2)), "beq is taken on equal registers"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bne, GPR_U64(ctx, 1) != GPR_U64(ctx, 2)), "bne is not taken on equal registers"); + + setGpr(*ctx, 1, 0x0000000080000000ull); // positive as a 64-bit value even though bit 31 is set + t.IsFalse(PS2X_BRANCH_TAKEN(t, blez, GPR_S64(ctx, 1) <= 0), "blez tests the 64-bit sign, not bit 31"); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bgtz, GPR_S64(ctx, 1) > 0), "bgtz tests the 64-bit sign, not bit 31"); + setGpr(*ctx, 1, 0u); + t.IsTrue(PS2X_BRANCH_TAKEN(t, blez, GPR_S64(ctx, 1) <= 0), "blez is taken on zero"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bgtz, GPR_S64(ctx, 1) > 0), "bgtz is not taken on zero"); + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); + t.IsTrue(PS2X_BRANCH_TAKEN(t, blez, GPR_S64(ctx, 1) <= 0), "blez is taken on a negative value"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bgtz, GPR_S64(ctx, 1) > 0), "bgtz is not taken on a negative value"); + + for (uint32_t word : {beq, bne, blez, bgtz}) + { + expectConditionalBranchShape(t, word, false, false); + } + }); + + tc.Run("BEQL, BNEL, BLEZL and BGTZL execute the delay slot only when taken", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const uint32_t beql = encodeI(OPCODE_BEQL, 1, 2, kBranchOffsetWords); + const uint32_t bnel = encodeI(OPCODE_BNEL, 1, 2, kBranchOffsetWords); + const uint32_t blezl = encodeI(OPCODE_BLEZL, 1, 0, kBranchOffsetWords); + const uint32_t bgtzl = encodeI(OPCODE_BGTZL, 1, 0, kBranchOffsetWords); + + setGpr(*ctx, 1, 7u); + setGpr(*ctx, 2, 7u); + t.IsTrue(PS2X_BRANCH_TAKEN(t, beql, GPR_U64(ctx, 1) == GPR_U64(ctx, 2)), "beql is taken on equal registers"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bnel, GPR_U64(ctx, 1) != GPR_U64(ctx, 2)), "bnel is not taken on equal registers"); + setGpr(*ctx, 1, 0xFFFFFFFF00000000ull); // negative as a 64-bit value even though bit 31 is clear + t.IsTrue(PS2X_BRANCH_TAKEN(t, blezl, GPR_S64(ctx, 1) <= 0), "blezl tests the 64-bit sign"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bgtzl, GPR_S64(ctx, 1) > 0), "bgtzl tests the 64-bit sign"); + + for (uint32_t word : {beql, bnel, blezl, bgtzl}) + { + expectConditionalBranchShape(t, word, true, false); + } + }); + + tc.Run("every MipsOpcodes entry translates and only EE-reserved opcodes reject", [](TestCase &t) { + // Opcodes the R5900 does not implement: no COP3, no 64-bit FPU loads/stores, no LL/SC + // doubleword forms and no COP2 word loads/stores (COP2 uses LQC2/SQC2). + const uint32_t reserved[] = {OPCODE_COP3, OPCODE_LWC2, OPCODE_LLD, OPCODE_LDC1, + OPCODE_SWC2, OPCODE_SCD, OPCODE_SDC1, 0x1Du, 0x3Bu}; + const uint32_t implemented[] = { + OPCODE_SPECIAL, OPCODE_REGIMM, OPCODE_J, OPCODE_JAL, OPCODE_BEQ, OPCODE_BNE, OPCODE_BLEZ, OPCODE_BGTZ, + OPCODE_ADDI, OPCODE_ADDIU, OPCODE_SLTI, OPCODE_SLTIU, OPCODE_ANDI, OPCODE_ORI, OPCODE_XORI, OPCODE_LUI, + OPCODE_COP0, OPCODE_COP1, OPCODE_COP2, OPCODE_BEQL, OPCODE_BNEL, OPCODE_BLEZL, OPCODE_BGTZL, + OPCODE_DADDI, OPCODE_DADDIU, OPCODE_LDL, OPCODE_LDR, OPCODE_MMI, OPCODE_LQ, OPCODE_SQ, + OPCODE_LB, OPCODE_LH, OPCODE_LWL, OPCODE_LW, OPCODE_LBU, OPCODE_LHU, OPCODE_LWR, OPCODE_LWU, + OPCODE_SB, OPCODE_SH, OPCODE_SWL, OPCODE_SW, OPCODE_SDL, OPCODE_SDR, OPCODE_SWR, OPCODE_CACHE, + OPCODE_LL, OPCODE_LWC1, OPCODE_PREF, OPCODE_LQC2, OPCODE_LD, OPCODE_SC, OPCODE_SWC1, OPCODE_SQC2, OPCODE_SD}; + + auto probeWord = [](uint32_t opcode) -> uint32_t { + switch (opcode) + { + case OPCODE_SPECIAL: + return encodeR(SPECIAL_ADDU, 1, 3, 2); + case OPCODE_REGIMM: + return encodeRegimm(REGIMM_BGEZ, 1, 4); + case OPCODE_J: + case OPCODE_JAL: + return encodeJ(opcode, 0x100000u); + case OPCODE_COP0: + return (static_cast(OPCODE_COP0) << 26) | (COP0_MF << 21) | (2u << 16) | (COP0_REG_STATUS << 11); + case OPCODE_COP1: + return (static_cast(OPCODE_COP1) << 26) | (COP1_MF << 21) | (2u << 16) | (3u << 11); + case OPCODE_COP2: + return (static_cast(OPCODE_COP2) << 26) | (COP2_QMFC2 << 21) | (2u << 16) | (3u << 11); + case OPCODE_MMI: + return (static_cast(OPCODE_MMI) << 26) | (1u << 21) | (3u << 16) | (2u << 11) | MMI_MADD; + default: + return encodeI(opcode, 1, 2, 4); + } + }; + + auto contains = [](const uint32_t *values, size_t count, uint32_t value) { + for (size_t i = 0; i < count; ++i) + { + if (values[i] == value) + return true; + } + return false; + }; + + for (uint32_t opcode = 0; opcode < 64u; ++opcode) + { + const bool isReserved = contains(reserved, std::size(reserved), opcode); + const bool isImplemented = contains(implemented, std::size(implemented), opcode); + t.IsTrue(isReserved != isImplemented, "opcode " + hex(opcode) + " must be listed exactly once in this manifest"); + + const Instruction inst = decode(probeWord(opcode)); + const bool rejects = emitsRuntimeThrow(translate(inst)); + t.Equals(rejects, isReserved, describe(inst) + " (opcode " + hex(opcode) + "): " + + (isReserved ? "an EE-reserved opcode must emit a runtime error" + : "an implemented opcode must not emit a runtime error")); + } + }); }); + + MiniTest::Case("R5900Instructions.SpecialFunctions", [](TestCase &tc) + { + tc.After(releaseGuest); + + tc.Run("SLL shifts the low word, sign-extends and encodes NOP", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction sll4 = decode(encodeR(SPECIAL_SLL, 0, 1, 2, 4)); // sll $2, $1, 4 + const Instruction sll31 = decode(encodeR(SPECIAL_SLL, 0, 1, 2, 31)); // sll $2, $1, 31 + const Instruction sll0 = decode(encodeR(SPECIAL_SLL, 0, 1, 2, 0)); // sll $2, $1, 0 + + setGpr32(*ctx, 1, 0x12345678u); + PS2X_EXEC_EMITTED(t, sll4, SET_GPR_S32(ctx, 2, (int32_t)SLL32(GPR_U32(ctx, 1), 4));); + expectGpr(t, *ctx, 2, 0x23456780u, "sll by 4"); + + setGpr32(*ctx, 1, 1u); + PS2X_EXEC_EMITTED(t, sll31, SET_GPR_S32(ctx, 2, (int32_t)SLL32(GPR_U32(ctx, 1), 31));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000000ull, "sll into bit 31 sign-extends"); + + setGpr(*ctx, 1, 0x00000000FFFFFFFFull); + PS2X_EXEC_EMITTED(t, sll0, SET_GPR_S32(ctx, 2, (int32_t)SLL32(GPR_U32(ctx, 1), 0));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFFull, "sll by 0 canonicalises a 32-bit value into 64 bits"); + + expectTranslation(t, decode(0u), "// NOP"); + expectTranslation(t, decode(encodeR(SPECIAL_SLL, 0, 1, 0, 4)), ""); + }); + + tc.Run("SRL and SRA shift right logically and arithmetically", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction srl4 = decode(encodeR(SPECIAL_SRL, 0, 1, 2, 4)); // srl $2, $1, 4 + const Instruction srl0 = decode(encodeR(SPECIAL_SRL, 0, 1, 2, 0)); // srl $2, $1, 0 + const Instruction sra4 = decode(encodeR(SPECIAL_SRA, 0, 1, 2, 4)); // sra $2, $1, 4 + const Instruction sra31 = decode(encodeR(SPECIAL_SRA, 0, 1, 2, 31)); // sra $2, $1, 31 + + setGpr32(*ctx, 1, 0x80000000u); + PS2X_EXEC_EMITTED(t, srl4, SET_GPR_S32(ctx, 2, (int32_t)SRL32(GPR_U32(ctx, 1), 4));); + expectGpr(t, *ctx, 2, 0x08000000u, "srl shifts in zeros"); + PS2X_EXEC_EMITTED(t, srl0, SET_GPR_S32(ctx, 2, (int32_t)SRL32(GPR_U32(ctx, 1), 0));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000000ull, "srl by 0 still sign-extends the 32-bit result"); + PS2X_EXEC_EMITTED(t, sra4, SET_GPR_S32(ctx, 2, SRA32(GPR_S32(ctx, 1), 4));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFF8000000ull, "sra shifts in the sign bit"); + PS2X_EXEC_EMITTED(t, sra31, SET_GPR_S32(ctx, 2, SRA32(GPR_S32(ctx, 1), 31));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFFull, "sra by 31 of a negative value is -1"); + + setGpr32(*ctx, 1, 0x7FFFFFFFu); + PS2X_EXEC_EMITTED(t, sra31, SET_GPR_S32(ctx, 2, SRA32(GPR_S32(ctx, 1), 31));); + expectGpr(t, *ctx, 2, 0u, "sra by 31 of a positive value is 0"); + }); + + tc.Run("SLLV, SRLV and SRAV use the low five bits of rs", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction sllv = decode(encodeR(SPECIAL_SLLV, 3, 1, 2)); // sllv $2, $1, $3 + const Instruction srlv = decode(encodeR(SPECIAL_SRLV, 3, 1, 2)); // srlv $2, $1, $3 + const Instruction srav = decode(encodeR(SPECIAL_SRAV, 3, 1, 2)); // srav $2, $1, $3 + + setGpr32(*ctx, 1, 0x80000001u); + setGpr(*ctx, 3, 33u); // only bits 0..4 count: shift by 1 + PS2X_EXEC_EMITTED(t, sllv, SET_GPR_S32(ctx, 2, (int32_t)SLL32(GPR_U32(ctx, 1), GPR_U32(ctx, 3) & 0x1F));); + expectGpr(t, *ctx, 2, 2u, "sllv masks the shift amount to 5 bits"); + PS2X_EXEC_EMITTED(t, srlv, SET_GPR_S32(ctx, 2, (int32_t)SRL32(GPR_U32(ctx, 1), GPR_U32(ctx, 3) & 0x1F));); + expectGpr(t, *ctx, 2, 0x40000000u, "srlv masks the shift amount to 5 bits"); + PS2X_EXEC_EMITTED(t, srav, SET_GPR_S32(ctx, 2, SRA32(GPR_S32(ctx, 1), GPR_U32(ctx, 3) & 0x1F));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFC0000000ull, "srav masks the shift amount to 5 bits and keeps the sign"); + + setGpr(*ctx, 3, 32u); // shift by 0 + PS2X_EXEC_EMITTED(t, sllv, SET_GPR_S32(ctx, 2, (int32_t)SLL32(GPR_U32(ctx, 1), GPR_U32(ctx, 3) & 0x1F));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000001ull, "sllv by 32 is a shift by 0"); + }); + + tc.Run("JR and JALR read the target before the delay slot and JALR links", [](TestCase &t) { + const uint32_t jrRa = encodeR(SPECIAL_JR, 31, 0, 0); // jr $ra + const uint32_t jrT9 = encodeR(SPECIAL_JR, 25, 0, 0); // jr $t9 + const uint32_t jalrT9 = encodeR(SPECIAL_JALR, 25, 0, 31); // jalr $t9 + const uint32_t jalrT0 = encodeR(SPECIAL_JALR, 25, 0, 8); // jalr $t0, $t9 + + expectTranslation(t, decode(jrRa), "// JR $31 - Handled by branch logic"); + expectTranslation(t, decode(jalrT0), "// JALR $8, $25 - Handled by branch logic"); + + // The emitter may also replay the delay slot in a resume-from-delay-slot prologue, so the + // ordering checks start from the jump-target read of the main path. + const std::string ret = emitBranchFunction(jrRa); + const size_t retRead = ret.find("const uint32_t jumpTarget = GPR_U32(ctx, 31);"); + t.IsTrue(retRead != std::string::npos, "jr $ra reads $ra into the jump target"); + const size_t retSlot = ret.find(kDelaySlotCode, retRead); + const size_t retSet = ret.find("ctx->pc = jumpTarget;", retRead); + t.IsTrue(retSlot != std::string::npos && retSet != std::string::npos, + "jr $ra runs the delay slot and publishes the target"); + t.IsTrue(retSlot < retSet, "jr $ra reads its target before the delay slot can modify it"); + t.IsTrue(ret.find("PS2Runtime::GuestBranchKind::Return") != std::string::npos, "jr $ra is a return"); + t.IsTrue(ret.find("SET_GPR_U32(ctx, 31") == std::string::npos, "jr must not write $ra"); + + const std::string jump = emitBranchFunction(jrT9); + t.IsTrue(jump.find("const uint32_t jumpTarget = GPR_U32(ctx, 25);") != std::string::npos, "jr $t9 reads $t9"); + t.IsTrue(jump.find("runtime->dispatchGuestBranch(rdram, ctx, jumpTarget, 0x1000u, 0x0u, PS2Runtime::GuestBranchKind::IndirectJump, \"JR\")") != std::string::npos, + "jr through a non-return register dispatches as an indirect jump"); + + const std::string call = emitBranchFunction(jalrT9); + const size_t callRead = call.find("const uint32_t jumpTarget = GPR_U32(ctx, 25);"); + t.IsTrue(callRead != std::string::npos, "jalr reads $t9 into the jump target"); + const size_t callLink = call.find("SET_GPR_U32(ctx, 31, 0x1008u);", callRead); + const size_t callSlot = call.find(kDelaySlotCode, callRead); + t.IsTrue(callLink != std::string::npos && callSlot != std::string::npos, + "jalr links and runs the delay slot after reading its target"); + t.IsTrue(callLink < callSlot, "jalr links before the delay slot so the slot can observe $ra"); + t.IsTrue(call.find("runtime->dispatchGuestBranch(rdram, ctx, jumpTarget, 0x1000u, 0x1008u, PS2Runtime::GuestBranchKind::IndirectCall, \"JALR\")") != std::string::npos, + "jalr dispatches as an indirect call with its return pc"); + + const std::string callT0 = emitBranchFunction(jalrT0); + t.IsTrue(callT0.find("SET_GPR_U32(ctx, 8, 0x1008u);") != std::string::npos, "jalr links into the encoded rd"); + t.IsTrue(callT0.find("SET_GPR_U32(ctx, 31") == std::string::npos, "jalr with an explicit rd must not write $ra"); + }); + + tc.Run("MOVZ and MOVN test 64 bits of rt and move 64 bits of rs", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction movz = decode(encodeR(SPECIAL_MOVZ, 1, 3, 2)); // movz $2, $1, $3 + const Instruction movn = decode(encodeR(SPECIAL_MOVN, 1, 3, 2)); // movn $2, $1, $3 + constexpr uint64_t kSource = 0x1122334455667788ull; + constexpr uint64_t kOriginal = 0x99AABBCCDDEEFF00ull; + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + setGpr(*ctx, 1, kSource, 0xFFFFFFFFFFFFFFFFull); + + setGpr(*ctx, 3, 0u); + setGpr(*ctx, 2, kOriginal, kUpperHalf); + PS2X_EXEC_EMITTED(t, movz, if (GPR_U64(ctx, 3) == 0) SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1));); + expectGpr(t, *ctx, 2, kSource, "movz moves when rt is zero"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "movz preserves bits 64..127 of rd"); + setGpr(*ctx, 2, kOriginal, kUpperHalf); + PS2X_EXEC_EMITTED(t, movn, if (GPR_U64(ctx, 3) != 0) SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1));); + expectGpr(t, *ctx, 2, kOriginal, "movn does not move when rt is zero"); + + setGpr(*ctx, 3, 0x0000000100000000ull); // non-zero only above bit 31 + setGpr(*ctx, 2, kOriginal, kUpperHalf); + PS2X_EXEC_EMITTED(t, movz, if (GPR_U64(ctx, 3) == 0) SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1));); + expectGpr(t, *ctx, 2, kOriginal, "movz tests all 64 bits of rt"); + PS2X_EXEC_EMITTED(t, movn, if (GPR_U64(ctx, 3) != 0) SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1));); + expectGpr(t, *ctx, 2, kSource, "movn moves when any of the 64 bits of rt is set"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "movn preserves bits 64..127 of rd"); + }); + + tc.Run("SYSCALL, BREAK and SYNC hand control to the runtime or do nothing", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction syscall = decode((0x1234u << 6) | SPECIAL_SYSCALL); // syscall 0x1234 + const Instruction brk = decode((0x7u << 6) | SPECIAL_BREAK); // break 7 + const Instruction sync = decode((0x10u << 6) | SPECIAL_SYNC); // sync.p + + expectTranslation(t, syscall, "ctx->pc = 0x1004u;\nruntime->handleSyscall(rdram, ctx, 0x1234u);"); + + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, brk, runtime->handleBreak(rdram, ctx);); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_BREAKPOINT), "break raises a breakpoint exception at the faulting pc"); + + expectTranslation(t, sync, "// SYNC instruction - memory barrier\n// In recompiled code, we don't need explicit memory barriers"); + }); + + tc.Run("MFHI, MTHI, MFLO and MTLO move 64 bits", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + setGpr(*ctx, 1, 0x8000000000000001ull); + setGpr(*ctx, 2, 0u, kUpperHalf); + + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_MTHI, 1, 0, 0)), ctx->hi = GPR_U64(ctx, 1);); + expectU64(t, ctx->hi, 0x8000000000000001ull, "mthi copies all 64 bits"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_MFHI, 0, 0, 2)), SET_GPR_U64(ctx, 2, ctx->hi);); + expectGpr(t, *ctx, 2, 0x8000000000000001ull, "mfhi copies all 64 bits"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "mfhi preserves bits 64..127 of rd"); + + setGpr(*ctx, 1, 0x7FFFFFFFFFFFFFFEull); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_MTLO, 1, 0, 0)), ctx->lo = GPR_U64(ctx, 1);); + expectU64(t, ctx->lo, 0x7FFFFFFFFFFFFFFEull, "mtlo copies all 64 bits"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_MFLO, 0, 0, 2)), SET_GPR_U64(ctx, 2, ctx->lo);); + expectGpr(t, *ctx, 2, 0x7FFFFFFFFFFFFFFEull, "mflo copies all 64 bits"); + }); + + tc.Run("DSLLV, DSRLV and DSRAV use the low six bits of rs", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction dsllv = decode(encodeR(SPECIAL_DSLLV, 3, 1, 2)); // dsllv $2, $1, $3 + const Instruction dsrlv = decode(encodeR(SPECIAL_DSRLV, 3, 1, 2)); // dsrlv $2, $1, $3 + const Instruction dsrav = decode(encodeR(SPECIAL_DSRAV, 3, 1, 2)); // dsrav $2, $1, $3 + + setGpr(*ctx, 1, 0x8000000000000001ull); + setGpr(*ctx, 3, 65u); // only bits 0..5 count: shift by 1 + PS2X_EXEC_EMITTED(t, dsllv, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) << (GPR_U32(ctx, 3) & 0x3F));); + expectGpr(t, *ctx, 2, 2u, "dsllv masks the shift amount to 6 bits"); + PS2X_EXEC_EMITTED(t, dsrlv, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) >> (GPR_U32(ctx, 3) & 0x3F));); + expectGpr(t, *ctx, 2, 0x4000000000000000ull, "dsrlv shifts in zeros"); + PS2X_EXEC_EMITTED(t, dsrav, SET_GPR_S64(ctx, 2, GPR_S64(ctx, 1) >> (GPR_U32(ctx, 3) & 0x3F));); + expectGpr(t, *ctx, 2, 0xC000000000000000ull, "dsrav shifts in the sign bit"); + + setGpr(*ctx, 3, 63u); + PS2X_EXEC_EMITTED(t, dsrav, SET_GPR_S64(ctx, 2, GPR_S64(ctx, 1) >> (GPR_U32(ctx, 3) & 0x3F));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFFull, "dsrav by 63 of a negative value is -1"); + }); + + tc.Run("MULT and MULTU write sign-extended halves to LO/HI and optionally rd", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction mult = decode(encodeR(SPECIAL_MULT, 1, 3, 0)); // mult $1, $3 + const Instruction mult3 = decode(encodeR(SPECIAL_MULT, 1, 3, 2)); // mult $2, $1, $3 + const Instruction multu = decode(encodeR(SPECIAL_MULTU, 1, 3, 0)); // multu $1, $3 + const Instruction multu3 = decode(encodeR(SPECIAL_MULTU, 1, 3, 2)); // multu $2, $1, $3 + + setGpr32(*ctx, 1, static_cast(-2)); + setGpr32(*ctx, 3, 3u); + PS2X_EXEC_EMITTED(t, mult, { int64_t result = (int64_t)GPR_S32(ctx, 1) * (int64_t)GPR_S32(ctx, 3); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); }); + expectU64(t, ctx->lo, 0xFFFFFFFFFFFFFFFAull, "mult -2 * 3: LO holds the sign-extended low word"); + expectU64(t, ctx->hi, 0xFFFFFFFFFFFFFFFFull, "mult -2 * 3: HI holds the sign-extended high word"); + + setGpr32(*ctx, 1, 0x7FFFFFFFu); + setGpr32(*ctx, 3, 0x7FFFFFFFu); + setGpr(*ctx, 2, 0u); + PS2X_EXEC_EMITTED(t, mult3, { int64_t result = (int64_t)GPR_S32(ctx, 1) * (int64_t)GPR_S32(ctx, 3); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); SET_GPR_S32(ctx, 2, (int32_t)result); }); + expectU64(t, ctx->lo, 1u, "mult INT32_MAX^2: LO"); + expectU64(t, ctx->hi, 0x3FFFFFFFu, "mult INT32_MAX^2: HI"); + expectGpr(t, *ctx, 2, 1u, "the three-operand mult also writes LO to rd"); + + setGpr32(*ctx, 1, 0xFFFFFFFFu); + setGpr32(*ctx, 3, 0xFFFFFFFFu); + PS2X_EXEC_EMITTED(t, multu, { uint64_t result = (uint64_t)GPR_U32(ctx, 1) * (uint64_t)GPR_U32(ctx, 3); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); }); + expectU64(t, ctx->lo, 1u, "multu 0xFFFFFFFF^2: LO"); + expectU64(t, ctx->hi, 0xFFFFFFFFFFFFFFFEull, "multu 0xFFFFFFFF^2: HI is sign-extended even though the multiply is unsigned"); + + setGpr32(*ctx, 1, 0x80000000u); + setGpr32(*ctx, 3, 2u); + PS2X_EXEC_EMITTED(t, multu3, { uint64_t result = (uint64_t)GPR_U32(ctx, 1) * (uint64_t)GPR_U32(ctx, 3); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); SET_GPR_S32(ctx, 2, (int32_t)result); }); + expectU64(t, ctx->lo, 0u, "multu 0x80000000 * 2: LO"); + expectU64(t, ctx->hi, 1u, "multu 0x80000000 * 2: HI"); + expectGpr(t, *ctx, 2, 0u, "the three-operand multu also writes LO to rd"); + }); + + tc.Run("DIV truncates toward zero and defines division by zero and INT32_MIN / -1", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction div = decode(encodeR(SPECIAL_DIV, 1, 3, 0)); // div $1, $3 + auto runDiv = [&] { + PS2X_EXEC_EMITTED(t, div, { int32_t divisor = GPR_S32(ctx, 3); int32_t dividend = GPR_S32(ctx, 1); if (divisor != 0) { if (divisor == -1 && dividend == INT32_MIN) { ctx->lo = (uint64_t)(int64_t)INT32_MIN; ctx->hi = 0; } else { ctx->lo = (uint64_t)(int64_t)(dividend / divisor); ctx->hi = (uint64_t)(int64_t)(dividend % divisor); } } else { ctx->lo = (dividend < 0) ? 1ull : 0xFFFFFFFFFFFFFFFFull; ctx->hi = (uint64_t)(int64_t)dividend; } }); + }; + + setGpr32(*ctx, 1, 7u); + setGpr32(*ctx, 3, 2u); + runDiv(); + expectU64(t, ctx->lo, 3u, "div 7 / 2 quotient"); + expectU64(t, ctx->hi, 1u, "div 7 / 2 remainder"); + + setGpr32(*ctx, 1, static_cast(-7)); + runDiv(); + expectU64(t, ctx->lo, 0xFFFFFFFFFFFFFFFDull, "div -7 / 2 truncates toward zero"); + expectU64(t, ctx->hi, 0xFFFFFFFFFFFFFFFFull, "div -7 / 2 remainder takes the dividend's sign"); + + setGpr32(*ctx, 1, 7u); + setGpr32(*ctx, 3, static_cast(-2)); + runDiv(); + expectU64(t, ctx->lo, 0xFFFFFFFFFFFFFFFDull, "div 7 / -2 quotient"); + expectU64(t, ctx->hi, 1u, "div 7 / -2 remainder"); + + setGpr32(*ctx, 3, 0u); + runDiv(); + expectU64(t, ctx->lo, 0xFFFFFFFFFFFFFFFFull, "div by zero of a positive dividend leaves LO = -1"); + expectU64(t, ctx->hi, 7u, "div by zero leaves HI = dividend"); + + setGpr32(*ctx, 1, static_cast(-7)); + runDiv(); + expectU64(t, ctx->lo, 1u, "div by zero of a negative dividend leaves LO = 1"); + expectU64(t, ctx->hi, 0xFFFFFFFFFFFFFFF9ull, "div by zero leaves HI = sign-extended dividend"); + + setGpr32(*ctx, 1, 0x80000000u); + setGpr32(*ctx, 3, 0xFFFFFFFFu); + runDiv(); + expectU64(t, ctx->lo, 0xFFFFFFFF80000000ull, "div INT32_MIN / -1 leaves LO = INT32_MIN"); + expectU64(t, ctx->hi, 0u, "div INT32_MIN / -1 leaves HI = 0"); + }); + + tc.Run("DIVU divides unsigned, sign-extends the results and defines division by zero", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction divu = decode(encodeR(SPECIAL_DIVU, 1, 3, 0)); // divu $1, $3 + auto runDivu = [&] { + PS2X_EXEC_EMITTED(t, divu, { uint32_t divisor = GPR_U32(ctx, 3); if (divisor != 0) { ctx->lo = (uint64_t)(int64_t)(int32_t)(GPR_U32(ctx, 1) / divisor); ctx->hi = (uint64_t)(int64_t)(int32_t)(GPR_U32(ctx, 1) % divisor); } else { ctx->lo = 0xFFFFFFFFFFFFFFFFull; ctx->hi = (uint64_t)(int64_t)(int32_t)GPR_U32(ctx,1); } }); + }; + + setGpr32(*ctx, 1, 0xFFFFFFFFu); + setGpr32(*ctx, 3, 2u); + runDivu(); + expectU64(t, ctx->lo, 0x7FFFFFFFu, "divu 0xFFFFFFFF / 2 is an unsigned divide"); + expectU64(t, ctx->hi, 1u, "divu 0xFFFFFFFF / 2 remainder"); + + setGpr32(*ctx, 3, 1u); + runDivu(); + expectU64(t, ctx->lo, 0xFFFFFFFFFFFFFFFFull, "divu results are sign-extended into LO"); + expectU64(t, ctx->hi, 0u, "divu 0xFFFFFFFF / 1 remainder"); + + setGpr32(*ctx, 1, 0x80000005u); + setGpr32(*ctx, 3, 0u); + runDivu(); + expectU64(t, ctx->lo, 0xFFFFFFFFFFFFFFFFull, "divu by zero leaves LO = -1"); + expectU64(t, ctx->hi, 0xFFFFFFFF80000005ull, "divu by zero leaves HI = sign-extended dividend"); + }); + + tc.Run("ADD and SUB trap on signed overflow; ADDU and SUBU wrap", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction add = decode(encodeR(SPECIAL_ADD, 1, 3, 2)); // add $2, $1, $3 + const Instruction addu = decode(encodeR(SPECIAL_ADDU, 1, 3, 2)); // addu $2, $1, $3 + const Instruction sub = decode(encodeR(SPECIAL_SUB, 1, 3, 2)); // sub $2, $1, $3 + const Instruction subu = decode(encodeR(SPECIAL_SUBU, 1, 3, 2)); // subu $2, $1, $3 + auto runAdd = [&] { + PS2X_EXEC_EMITTED(t, add, { int32_t rs_val = GPR_S32(ctx, 1); int32_t rt_val = GPR_S32(ctx, 3); int64_t result = (int64_t)rs_val + (int64_t)rt_val; if (result > INT32_MAX || result < INT32_MIN) { runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); } else { SET_GPR_S32(ctx, 2, (int32_t)result); } }); + }; + auto runSub = [&] { + PS2X_EXEC_EMITTED(t, sub, { uint32_t tmp; bool ov; SUB32_OV(GPR_U32(ctx, 1), GPR_U32(ctx, 3), tmp, ov); if (ov) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S32(ctx, 2, (int32_t)tmp); }); + }; + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x7FFFFFFEu); + setGpr32(*ctx, 3, 1u); + runAdd(); + expectGpr(t, *ctx, 2, 0x7FFFFFFFu, "add 0x7FFFFFFE + 1"); + t.IsTrue(noException(*ctx), "add without overflow must not raise an exception"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x7FFFFFFFu); + setGpr(*ctx, 2, 0x1234u); + runAdd(); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "add INT32_MAX + 1 must raise an integer overflow exception"); + expectGpr(t, *ctx, 2, 0x1234u, "add overflow must leave rd unchanged"); + + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, addu, SET_GPR_S32(ctx, 2, (int32_t)ADD32(GPR_U32(ctx, 1), GPR_U32(ctx, 3)));); + expectGpr(t, *ctx, 2, 0xFFFFFFFF80000000ull, "addu INT32_MAX + 1 wraps and sign-extends"); + t.IsTrue(noException(*ctx), "addu never raises an exception"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x80000000u); + setGpr32(*ctx, 3, 1u); + setGpr(*ctx, 2, 0x1234u); + runSub(); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "sub INT32_MIN - 1 must raise an integer overflow exception"); + expectGpr(t, *ctx, 2, 0x1234u, "sub overflow must leave rd unchanged"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 0x7FFFFFFFu); + setGpr32(*ctx, 3, 0xFFFFFFFFu); + runSub(); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "sub INT32_MAX - (-1) must raise an integer overflow exception"); + + armTrapCheck(*ctx); + setGpr32(*ctx, 1, 5u); + setGpr32(*ctx, 3, 7u); + runSub(); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFEull, "sub 5 - 7"); + t.IsTrue(noException(*ctx), "sub without overflow must not raise an exception"); + + setGpr32(*ctx, 1, 0x80000000u); + setGpr32(*ctx, 3, 1u); + PS2X_EXEC_EMITTED(t, subu, SET_GPR_S32(ctx, 2, (int32_t)SUB32(GPR_U32(ctx, 1), GPR_U32(ctx, 3)));); + expectGpr(t, *ctx, 2, 0x7FFFFFFFu, "subu INT32_MIN - 1 wraps"); + t.IsTrue(noException(*ctx), "subu never raises an exception"); + }); + + tc.Run("AND, OR, XOR and NOR operate on 64 bits", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + setGpr(*ctx, 1, 0xF0F0F0F0F0F0F0F0ull); + setGpr(*ctx, 3, 0xFF00FF00FF00FF00ull); + setGpr(*ctx, 2, 0u, kUpperHalf); + + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_AND, 1, 3, 2)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) & GPR_U64(ctx, 3));); + expectGpr(t, *ctx, 2, 0xF000F000F000F000ull, "and"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_OR, 1, 3, 2)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) | GPR_U64(ctx, 3));); + expectGpr(t, *ctx, 2, 0xFFF0FFF0FFF0FFF0ull, "or"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_XOR, 1, 3, 2)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) ^ GPR_U64(ctx, 3));); + expectGpr(t, *ctx, 2, 0x0FF00FF00FF00FF0ull, "xor"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_NOR, 1, 3, 2)), SET_GPR_U64(ctx, 2, ~(GPR_U64(ctx, 1) | GPR_U64(ctx, 3)));); + expectGpr(t, *ctx, 2, 0x000F000F000F000Full, "nor"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "logic ops preserve bits 64..127 of rd"); + }); + + tc.Run("SLT and SLTU compare 64 bits signed and unsigned", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction slt = decode(encodeR(SPECIAL_SLT, 1, 3, 2)); // slt $2, $1, $3 + const Instruction sltu = decode(encodeR(SPECIAL_SLTU, 1, 3, 2)); // sltu $2, $1, $3 + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); // -1 signed, max unsigned + setGpr(*ctx, 3, 1u); + PS2X_EXEC_EMITTED(t, slt, SET_GPR_U64(ctx, 2, ((int64_t)GPR_S64(ctx, 1) < (int64_t)GPR_S64(ctx, 3)) ? 1 : 0);); + expectGpr(t, *ctx, 2, 1u, "slt -1 < 1"); + PS2X_EXEC_EMITTED(t, sltu, SET_GPR_U64(ctx, 2, ((uint64_t)GPR_U64(ctx, 1) < (uint64_t)GPR_U64(ctx, 3)) ? 1 : 0);); + expectGpr(t, *ctx, 2, 0u, "sltu 0xFFFFFFFFFFFFFFFF < 1 is false"); + + setGpr(*ctx, 1, 0x00000000FFFFFFFFull); // positive as 64 bits even though bit 31 is set + setGpr(*ctx, 3, 0u); + PS2X_EXEC_EMITTED(t, slt, SET_GPR_U64(ctx, 2, ((int64_t)GPR_S64(ctx, 1) < (int64_t)GPR_S64(ctx, 3)) ? 1 : 0);); + expectGpr(t, *ctx, 2, 0u, "slt compares 64 bits, not the low word"); + setGpr(*ctx, 3, 0x0000000100000000ull); + PS2X_EXEC_EMITTED(t, sltu, SET_GPR_U64(ctx, 2, ((uint64_t)GPR_U64(ctx, 1) < (uint64_t)GPR_U64(ctx, 3)) ? 1 : 0);); + expectGpr(t, *ctx, 2, 1u, "sltu 0xFFFFFFFF < 0x100000000"); + }); + + tc.Run("MTSA and MFSA access the shift amount register", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction mtsa = decode(encodeR(SPECIAL_MTSA, 1, 0, 0)); // mtsa $1 + const Instruction mfsa = decode(encodeR(SPECIAL_MFSA, 0, 0, 2)); // mfsa $2 + constexpr uint64_t kUpperHalf = 0x0123456789ABCDEFull; + + setGpr(*ctx, 1, 0x30u); + PS2X_EXEC_EMITTED(t, mtsa, ctx->sa = GPR_U32(ctx, 1) & 0x7F;); + expectU64(t, ctx->sa, 0x30u, "mtsa stores the shift amount"); + + setGpr(*ctx, 2, 0xFFFFFFFFFFFFFFFFull, kUpperHalf); + PS2X_EXEC_EMITTED(t, mfsa, SET_GPR_U32(ctx, 2, ctx->sa);); + expectGpr(t, *ctx, 2, 0x30u, "mfsa reads the shift amount back"); + expectU64(t, gprHi(*ctx, 2), kUpperHalf, "mfsa preserves bits 64..127 of rd"); + }); + + tc.Run("DADD and DSUB trap on 64-bit overflow; DADDU and DSUBU wrap", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction dadd = decode(encodeR(SPECIAL_DADD, 1, 3, 2)); // dadd $2, $1, $3 + const Instruction daddu = decode(encodeR(SPECIAL_DADDU, 1, 3, 2)); // daddu $2, $1, $3 + const Instruction dsub = decode(encodeR(SPECIAL_DSUB, 1, 3, 2)); // dsub $2, $1, $3 + const Instruction dsubu = decode(encodeR(SPECIAL_DSUBU, 1, 3, 2)); // dsubu $2, $1, $3 + auto runDadd = [&] { + PS2X_EXEC_EMITTED(t, dadd, { int64_t a = (int64_t)GPR_S64(ctx, 1); int64_t b = (int64_t)GPR_S64(ctx, 3); int64_t r = (int64_t)((uint64_t)a + (uint64_t)b); if (((a ^ b) >= 0) && ((a ^ r) < 0)) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S64(ctx, 2, r); }); + }; + auto runDsub = [&] { + PS2X_EXEC_EMITTED(t, dsub, { int64_t a = (int64_t)GPR_S64(ctx, 1); int64_t b = (int64_t)GPR_S64(ctx, 3); int64_t r = (int64_t)((uint64_t)a - (uint64_t)b); if (((a ^ b) < 0) && ((a ^ r) < 0)) runtime->SignalException(ctx, EXCEPTION_INTEGER_OVERFLOW); else SET_GPR_S64(ctx, 2, r); }); + }; + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0x00000000FFFFFFFFull); + setGpr(*ctx, 3, 1u); + runDadd(); + expectGpr(t, *ctx, 2, 0x0000000100000000ull, "dadd carries past bit 31 without trapping"); + t.IsTrue(noException(*ctx), "dadd without overflow must not raise an exception"); + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0x7FFFFFFFFFFFFFFFull); + setGpr(*ctx, 2, 0x1234u); + runDadd(); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "dadd INT64_MAX + 1 must raise an integer overflow exception"); + expectGpr(t, *ctx, 2, 0x1234u, "dadd overflow must leave rd unchanged"); + + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, daddu, SET_GPR_U64(ctx, 2, (uint64_t)GPR_U64(ctx, 1) + (uint64_t)GPR_U64(ctx, 3));); + expectGpr(t, *ctx, 2, 0x8000000000000000ull, "daddu INT64_MAX + 1 wraps"); + t.IsTrue(noException(*ctx), "daddu never raises an exception"); + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0x8000000000000000ull); + setGpr(*ctx, 3, 1u); + setGpr(*ctx, 2, 0x1234u); + runDsub(); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_INTEGER_OVERFLOW), "dsub INT64_MIN - 1 must raise an integer overflow exception"); + expectGpr(t, *ctx, 2, 0x1234u, "dsub overflow must leave rd unchanged"); + + armTrapCheck(*ctx); + setGpr(*ctx, 1, 0u); + setGpr(*ctx, 3, 1u); + runDsub(); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFFull, "dsub 0 - 1"); + t.IsTrue(noException(*ctx), "dsub without overflow must not raise an exception"); + + setGpr(*ctx, 1, 0x8000000000000000ull); + PS2X_EXEC_EMITTED(t, dsubu, SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) - GPR_U64(ctx, 3));); + expectGpr(t, *ctx, 2, 0x7FFFFFFFFFFFFFFFull, "dsubu INT64_MIN - 1 wraps"); + t.IsTrue(noException(*ctx), "dsubu never raises an exception"); + }); + + tc.Run("TGE, TGEU, TLT, TLTU, TEQ and TNE trap exactly when their 64-bit condition holds", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction tge = decode(encodeR(SPECIAL_TGE, 1, 3, 0)); // tge $1, $3 + const Instruction tgeu = decode(encodeR(SPECIAL_TGEU, 1, 3, 0)); // tgeu $1, $3 + const Instruction tlt = decode(encodeR(SPECIAL_TLT, 1, 3, 0)); // tlt $1, $3 + const Instruction tltu = decode(encodeR(SPECIAL_TLTU, 1, 3, 0)); // tltu $1, $3 + const Instruction teq = decode(encodeR(SPECIAL_TEQ, 1, 3, 0)); // teq $1, $3 + const Instruction tne = decode(encodeR(SPECIAL_TNE, 1, 3, 0)); // tne $1, $3 + + // rs = -1 (signed) / max (unsigned), rt = 1. + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); + setGpr(*ctx, 3, 1u); + + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tge, if (GPR_S64(ctx, 1) >= GPR_S64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "tge: -1 >= 1 is false, no trap"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tgeu, if (GPR_U64(ctx, 1) >= GPR_U64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tgeu: 0xFFFFFFFFFFFFFFFF >= 1 traps"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tlt, if (GPR_S64(ctx, 1) < GPR_S64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tlt: -1 < 1 traps"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tltu, if (GPR_U64(ctx, 1) < GPR_U64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "tltu: 0xFFFFFFFFFFFFFFFF < 1 is false, no trap"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, teq, if (GPR_U64(ctx, 1) == GPR_U64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "teq on different values does not trap"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tne, if (GPR_U64(ctx, 1) != GPR_U64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tne on different values traps"); + + // Equal low words, different high words: every compare must look at all 64 bits. + setGpr(*ctx, 1, 0x0000000100000005ull); + setGpr(*ctx, 3, 0x0000000000000005ull); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, teq, if (GPR_U64(ctx, 1) == GPR_U64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "teq compares all 64 bits"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tge, if (GPR_S64(ctx, 1) >= GPR_S64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tge compares all 64 bits"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tltu, if (GPR_U64(ctx, 1) < GPR_U64(ctx, 3)) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "tltu compares all 64 bits"); + }); + + tc.Run("DSLL, DSRL, DSRA and their +32 forms shift doublewords", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + setGpr(*ctx, 1, 0x8000000000000001ull); + + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSLL, 0, 1, 2, 4)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) << 4);); + expectGpr(t, *ctx, 2, 0x0000000000000010ull, "dsll by 4"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSRL, 0, 1, 2, 4)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) >> 4);); + expectGpr(t, *ctx, 2, 0x0800000000000000ull, "dsrl by 4 shifts in zeros"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSRA, 0, 1, 2, 4)), SET_GPR_S64(ctx, 2, GPR_S64(ctx, 1) >> 4);); + expectGpr(t, *ctx, 2, 0xF800000000000000ull, "dsra by 4 shifts in the sign bit"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSLL32, 0, 1, 2, 4)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) << (32 + 4));); + expectGpr(t, *ctx, 2, 0x0000001000000000ull, "dsll32 by 4 shifts by 36"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSRL32, 0, 1, 2, 4)), SET_GPR_U64(ctx, 2, GPR_U64(ctx, 1) >> (32 + 4));); + expectGpr(t, *ctx, 2, 0x0000000008000000ull, "dsrl32 by 4 shifts by 36"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSRA32, 0, 1, 2, 4)), SET_GPR_S64(ctx, 2, GPR_S64(ctx, 1) >> (32 + 4));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFF8000000ull, "dsra32 by 4 shifts by 36 and keeps the sign"); + PS2X_EXEC_EMITTED(t, decode(encodeR(SPECIAL_DSRA32, 0, 1, 2, 31)), SET_GPR_S64(ctx, 2, GPR_S64(ctx, 1) >> (32 + 31));); + expectGpr(t, *ctx, 2, 0xFFFFFFFFFFFFFFFFull, "dsra32 by 31 of a negative value is -1"); + }); + + tc.Run("every SpecialFunctions entry translates and only reserved function codes reject", [](TestCase &t) { + // The R5900 has no DMULT/DMULTU/DDIV/DDIVU (0x1C..0x1F) and the other holes are reserved. + const uint32_t implemented[] = { + SPECIAL_SLL, SPECIAL_SRL, SPECIAL_SRA, SPECIAL_SLLV, SPECIAL_SRLV, SPECIAL_SRAV, + SPECIAL_JR, SPECIAL_JALR, SPECIAL_MOVZ, SPECIAL_MOVN, SPECIAL_SYSCALL, SPECIAL_BREAK, SPECIAL_SYNC, + SPECIAL_MFHI, SPECIAL_MTHI, SPECIAL_MFLO, SPECIAL_MTLO, SPECIAL_DSLLV, SPECIAL_DSRLV, SPECIAL_DSRAV, + SPECIAL_MULT, SPECIAL_MULTU, SPECIAL_DIV, SPECIAL_DIVU, + SPECIAL_ADD, SPECIAL_ADDU, SPECIAL_SUB, SPECIAL_SUBU, SPECIAL_AND, SPECIAL_OR, SPECIAL_XOR, SPECIAL_NOR, + SPECIAL_MFSA, SPECIAL_MTSA, SPECIAL_SLT, SPECIAL_SLTU, SPECIAL_DADD, SPECIAL_DADDU, SPECIAL_DSUB, SPECIAL_DSUBU, + SPECIAL_TGE, SPECIAL_TGEU, SPECIAL_TLT, SPECIAL_TLTU, SPECIAL_TEQ, SPECIAL_TNE, + SPECIAL_DSLL, SPECIAL_DSRL, SPECIAL_DSRA, SPECIAL_DSLL32, SPECIAL_DSRL32, SPECIAL_DSRA32}; + t.Equals(std::size(implemented), static_cast(52), "the manifest must list every SpecialFunctions enumerator"); + + for (uint32_t function = 0; function < 64u; ++function) + { + bool isImplemented = false; + for (uint32_t value : implemented) + { + isImplemented = isImplemented || value == function; + } + + const Instruction inst = decode(encodeR(function, 1, 3, 2, 4)); + const bool rejects = emitsRuntimeThrow(translate(inst)); + t.Equals(rejects, !isImplemented, describe(inst) + " (function " + hex(function) + "): " + + (isImplemented ? "an implemented SPECIAL function must not emit a runtime error" + : "a reserved SPECIAL function must emit a runtime error")); + } + }); }); + + MiniTest::Case("R5900Instructions.RegimmFunctions", [](TestCase &tc) + { + tc.After(releaseGuest); + + tc.Run("BLTZ, BGEZ and their likely forms test the 64-bit sign", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const uint32_t bltz = encodeRegimm(REGIMM_BLTZ, 1, kBranchOffsetWords); + const uint32_t bgez = encodeRegimm(REGIMM_BGEZ, 1, kBranchOffsetWords); + const uint32_t bltzl = encodeRegimm(REGIMM_BLTZL, 1, kBranchOffsetWords); + const uint32_t bgezl = encodeRegimm(REGIMM_BGEZL, 1, kBranchOffsetWords); + + expectTranslation(t, decode(bltz), "// REGIMM branch instruction to 0x100C - Handled by branch logic"); + + setGpr(*ctx, 1, 0x0000000080000000ull); // positive as a 64-bit value even though bit 31 is set + t.IsFalse(PS2X_BRANCH_TAKEN(t, bltz, GPR_S64(ctx, 1) < 0), "bltz tests the 64-bit sign, not bit 31"); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bgez, GPR_S64(ctx, 1) >= 0), "bgez tests the 64-bit sign, not bit 31"); + setGpr(*ctx, 1, 0xFFFFFFFF00000000ull); // negative as a 64-bit value even though bit 31 is clear + t.IsTrue(PS2X_BRANCH_TAKEN(t, bltzl, GPR_S64(ctx, 1) < 0), "bltzl tests the 64-bit sign"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bgezl, GPR_S64(ctx, 1) >= 0), "bgezl tests the 64-bit sign"); + setGpr(*ctx, 1, 0u); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bltz, GPR_S64(ctx, 1) < 0), "bltz is not taken on zero"); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bgez, GPR_S64(ctx, 1) >= 0), "bgez is taken on zero"); + + expectConditionalBranchShape(t, bltz, false, false); + expectConditionalBranchShape(t, bgez, false, false); + expectConditionalBranchShape(t, bltzl, true, false); + expectConditionalBranchShape(t, bgezl, true, false); + }); + + tc.Run("BLTZAL, BGEZAL, BLTZALL and BGEZALL link $ra unconditionally", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const uint32_t bltzal = encodeRegimm(REGIMM_BLTZAL, 1, kBranchOffsetWords); + const uint32_t bgezal = encodeRegimm(REGIMM_BGEZAL, 1, kBranchOffsetWords); + const uint32_t bltzall = encodeRegimm(REGIMM_BLTZALL, 1, kBranchOffsetWords); + const uint32_t bgezall = encodeRegimm(REGIMM_BGEZALL, 1, kBranchOffsetWords); + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFFull); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bltzal, GPR_S64(ctx, 1) < 0), "bltzal is taken on a negative value"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bgezal, GPR_S64(ctx, 1) >= 0), "bgezal is not taken on a negative value"); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bltzall, GPR_S64(ctx, 1) < 0), "bltzall is taken on a negative value"); + t.IsFalse(PS2X_BRANCH_TAKEN(t, bgezall, GPR_S64(ctx, 1) >= 0), "bgezall is not taken on a negative value"); + + expectConditionalBranchShape(t, bltzal, false, true); + expectConditionalBranchShape(t, bgezal, false, true); + expectConditionalBranchShape(t, bltzall, true, true); + expectConditionalBranchShape(t, bgezall, true, true); + + // bgezal $zero is the canonical "bal": always taken, always linking. + const uint32_t bal = encodeRegimm(REGIMM_BGEZAL, 0, kBranchOffsetWords); + t.IsTrue(PS2X_BRANCH_TAKEN(t, bal, GPR_S64(ctx, 0) >= 0), "bal is always taken"); + }); + + tc.Run("TGEI, TGEIU, TLTI, TLTIU, TEQI and TNEI trap against the sign-extended immediate", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction tgei = decode(encodeRegimm(REGIMM_TGEI, 1, imm16(-1))); // tgei $1, -1 + const Instruction tgeiu = decode(encodeRegimm(REGIMM_TGEIU, 1, imm16(-1))); // tgeiu $1, -1 + const Instruction tlti = decode(encodeRegimm(REGIMM_TLTI, 1, imm16(-1))); // tlti $1, -1 + const Instruction tltiu = decode(encodeRegimm(REGIMM_TLTIU, 1, imm16(-1))); // tltiu $1, -1 + const Instruction teqi = decode(encodeRegimm(REGIMM_TEQI, 1, 5)); // teqi $1, 5 + const Instruction tnei = decode(encodeRegimm(REGIMM_TNEI, 1, 5)); // tnei $1, 5 + + setGpr(*ctx, 1, 0xFFFFFFFFFFFFFFFEull); // -2 signed, just below max unsigned + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tgei, if (GPR_S64(ctx, 1) >= (int64_t)(int32_t)4294967295) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "tgei: -2 >= -1 is false, no trap"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tlti, if (GPR_S64(ctx, 1) < (int64_t)(int32_t)4294967295) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tlti: -2 < -1 traps"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tgeiu, if (GPR_U64(ctx, 1) >= (uint64_t)(int64_t)(int32_t)4294967295) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "tgeiu: the immediate -1 is 0xFFFFFFFFFFFFFFFF unsigned, so 0x...FFFE is below it"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tltiu, if (GPR_U64(ctx, 1) < (uint64_t)(int64_t)(int32_t)4294967295) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tltiu: 0x...FFFE < 0xFFFFFFFFFFFFFFFF traps"); + + setGpr(*ctx, 1, 5u); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, teqi, if (GPR_S64(ctx, 1) == (int64_t)(int32_t)5) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "teqi traps on an equal value"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tnei, if (GPR_S64(ctx, 1) != (int64_t)(int32_t)5) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "tnei does not trap on an equal value"); + + setGpr(*ctx, 1, 0x0000000100000005ull); // same low word, so the compare must use all 64 bits + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, teqi, if (GPR_S64(ctx, 1) == (int64_t)(int32_t)5) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(noException(*ctx), "teqi compares all 64 bits"); + armTrapCheck(*ctx); + PS2X_EXEC_EMITTED(t, tnei, if (GPR_S64(ctx, 1) != (int64_t)(int32_t)5) { runtime->handleTrap(rdram, ctx); }); + t.IsTrue(exceptionRaised(*ctx, EXCEPTION_TRAP), "tnei compares all 64 bits"); + }); + + tc.Run("MTSAB and MTSAH set the shift amount in bytes and halfwords", [](TestCase &t) { + PS2X_GUEST_SCOPE(); + const Instruction mtsab = decode(encodeRegimm(REGIMM_MTSAB, 1, 3)); // mtsab $1, 3 + const Instruction mtsabNeg = decode(encodeRegimm(REGIMM_MTSAB, 1, imm16(-1))); // mtsab $1, -1 + const Instruction mtsah = decode(encodeRegimm(REGIMM_MTSAH, 1, 1)); // mtsah $1, 1 + + setGpr(*ctx, 1, 1u); + PS2X_EXEC_EMITTED(t, mtsab, ctx->sa = ((GPR_U32(ctx, 1) ^ (uint32_t)3) & 0xF) << 3;); + expectU64(t, ctx->sa, 16u, "mtsab: (1 ^ 3) bytes = 2 bytes = 16 bits"); + + setGpr(*ctx, 1, 0x10u); // bits above the byte index are ignored + PS2X_EXEC_EMITTED(t, mtsabNeg, ctx->sa = ((GPR_U32(ctx, 1) ^ (uint32_t)4294967295) & 0xF) << 3;); + expectU64(t, ctx->sa, 15u * 8u, "mtsab: (0x10 ^ -1) & 0xF = 15 bytes"); + + setGpr(*ctx, 1, 2u); + PS2X_EXEC_EMITTED(t, mtsah, ctx->sa = ((GPR_U32(ctx, 1) ^ (uint32_t)1) & 0x7) << 4;); + expectU64(t, ctx->sa, 48u, "mtsah: (2 ^ 1) halfwords = 3 halfwords = 48 bits"); + }); + + tc.Run("every RegimmFunctions entry translates and only reserved rt codes reject", [](TestCase &t) { + const uint32_t implemented[] = { + REGIMM_BLTZ, REGIMM_BGEZ, REGIMM_BLTZL, REGIMM_BGEZL, + REGIMM_TGEI, REGIMM_TGEIU, REGIMM_TLTI, REGIMM_TLTIU, REGIMM_TEQI, REGIMM_TNEI, + REGIMM_BLTZAL, REGIMM_BGEZAL, REGIMM_BLTZALL, REGIMM_BGEZALL, REGIMM_MTSAB, REGIMM_MTSAH}; + t.Equals(std::size(implemented), static_cast(16), "the manifest must list every RegimmFunctions enumerator"); + + for (uint32_t function = 0; function < 32u; ++function) + { + bool isImplemented = false; + for (uint32_t value : implemented) + { + isImplemented = isImplemented || value == function; + } + + const Instruction inst = decode(encodeRegimm(function, 1, 4)); + const bool rejects = emitsRuntimeThrow(translate(inst)); + t.Equals(rejects, !isImplemented, describe(inst) + " (rt " + hex(function) + "): " + + (isImplemented ? "an implemented REGIMM function must not emit a runtime error" + : "a reserved REGIMM function must emit a runtime error")); + } + }); }); +}