diff --git a/.gitignore b/.gitignore index 15dcc75..c69ca20 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,6 @@ build # Now we can place the dump code here and test while expand the Runtime ps2xRuntime/include/ps2_recompiled_functions.h ps2xRuntime/include/ps2_runtime_macros.h +ps2xRuntime/include/ps2_recompiled_stubs.h ps2xRuntime/src/runner/ps2_recompiled_functions.cpp ps2xRuntime/src/runner/register_functions.cpp \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index c041abb..4741ff2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,4 +6,5 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_subdirectory("ps2xRecomp") add_subdirectory("ps2xRuntime") -add_subdirectory("ps2xAnalyzer") \ No newline at end of file +add_subdirectory("ps2xAnalyzer") +add_subdirectory("ps2xTest") diff --git a/ps2xRecomp/include/ps2recomp/code_generator.h b/ps2xRecomp/include/ps2recomp/code_generator.h index d9f8add..77bcc85 100644 --- a/ps2xRecomp/include/ps2recomp/code_generator.h +++ b/ps2xRecomp/include/ps2recomp/code_generator.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace ps2recomp { @@ -18,10 +20,16 @@ namespace ps2recomp std::string generateFunction(const Function &function, const std::vector &instructions, const bool &useHeaders); std::string generateFunctionRegistration(const std::vector &functions, const std::map &stubs); std::string generateMacroHeader(); - std::string handleBranchDelaySlots(const Instruction &branchInst, const Instruction &delaySlot); + std::string handleBranchDelaySlots(const Instruction &branchInst, const Instruction &delaySlot, + const Function &function, const std::unordered_set &internalTargets); - private: + void setRenamedFunctions(const std::unordered_map &renames); + std::unordered_set collectInternalBranchTargets(const Function &function, + const std::vector &instructions); + + public: std::vector m_symbols; + std::unordered_map m_renamedFunctions; std::string translateInstruction(const Instruction &inst); std::string translateMMIInstruction(const Instruction &inst); @@ -98,8 +106,9 @@ namespace ps2recomp const std::vector &entries); Symbol *findSymbolByAddress(uint32_t address); + std::string getFunctionName(uint32_t address); }; } -#endif // PS2RECOMP_CODE_GENERATOR_H \ No newline at end of file +#endif // PS2RECOMP_CODE_GENERATOR_H diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index 905304b..0c528ae 100644 --- a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h +++ b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h @@ -41,14 +41,16 @@ namespace ps2recomp std::map m_generatedStubs; bool decodeFunction(Function &function); + void discoverAdditionalEntryPoints(); bool shouldSkipFunction(const std::string &name) const; std::string generateRuntimeHeader(); bool generateFunctionHeader(); bool generateStubHeader(); bool writeToFile(const std::string &path, const std::string &content); std::filesystem::path getOutputPath(const Function &function) const; + std::string sanitizeFunctionName(const std::string &name) const; }; } -#endif \ No newline at end of file +#endif diff --git a/ps2xRecomp/src/code_generator.cpp b/ps2xRecomp/src/code_generator.cpp index 05a4c20..4bd4a6c 100644 --- a/ps2xRecomp/src/code_generator.cpp +++ b/ps2xRecomp/src/code_generator.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include namespace ps2recomp { @@ -13,7 +15,46 @@ namespace ps2recomp { } - std::string CodeGenerator::handleBranchDelaySlots(const Instruction &branchInst, const Instruction &delaySlot) + void CodeGenerator::setRenamedFunctions(const std::unordered_map &renames) + { + m_renamedFunctions = renames; + } + + std::string CodeGenerator::getFunctionName(uint32_t address) + { + auto it = m_renamedFunctions.find(address); + if (it != m_renamedFunctions.end()) + { + return it->second; + } + + Symbol *sym = findSymbolByAddress(address); + if (sym && sym->isFunction) + { + return sym->name; + } + + return ""; + } + + static bool isReservedCxxIdentifier(const std::string &name) + { + if (name.size() >= 2 && name[0] == '_' && name[1] == '_') + return true; + if (!name.empty() && name[0] == '_' && std::isupper(static_cast(name[1]))) + return true; + return false; + } + + static std::string sanitizeFunctionName(const std::string &name) + { + if (!isReservedCxxIdentifier(name)) + return name; + return "ps2_" + name; + } + + std::string CodeGenerator::handleBranchDelaySlots(const Instruction &branchInst, const Instruction &delaySlot, + const Function &function, const std::unordered_set &internalTargets) { std::stringstream ss; bool hasValidDelaySlot = (delaySlot.raw != 0); @@ -34,10 +75,10 @@ namespace ps2recomp ss << " " << delaySlotCode << "\n"; } uint32_t target = (branchInst.address & 0xF0000000) | (branchInst.target << 2); - Symbol *sym = findSymbolByAddress(target); - if (sym && sym->isFunction) + std::string funcName = getFunctionName(target); + if (!funcName.empty()) { - ss << " " << sym->name << "(rdram, ctx, runtime); return;\n"; + ss << " " << funcName << "(rdram, ctx, runtime); return;\n"; } else { @@ -58,14 +99,7 @@ namespace ps2recomp { ss << " " << delaySlotCode << "\n"; } - if (rs_reg == 31 && branchInst.function == SPECIAL_JR) - { - ss << " return;\n"; - } - else - { - ss << " ctx->pc = GPR_U32(ctx, " << (int)rs_reg << "); return;\n"; - } + ss << " ctx->pc = GPR_U32(ctx, " << (int)rs_reg << "); return;\n"; } else if (branchInst.isBranch) { @@ -164,12 +198,17 @@ namespace ps2recomp int32_t offset = branchInst.simmediate << 2; uint32_t target = branchInst.address + 4 + offset; - Symbol *sym = findSymbolByAddress(target); std::string targetAction; + std::string funcName = getFunctionName(target); + bool isInternalTarget = (internalTargets.find(target) != internalTargets.end()); - if (sym && sym->isFunction) + if (!funcName.empty()) { - targetAction = fmt::format("{}(rdram, ctx, runtime); return;", sym->name); + targetAction = fmt::format("{}(rdram, ctx, runtime); return;", funcName); + } + else if (isInternalTarget) + { + targetAction = fmt::format("goto label_{:x};", target); } else { @@ -471,6 +510,45 @@ namespace ps2recomp return ss.str(); } + std::unordered_set CodeGenerator::collectInternalBranchTargets( + const Function &function, const std::vector &instructions) + { + std::unordered_set targets; + + for (const auto &inst : instructions) + { + bool isStaticJump = (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL); + if (inst.isBranch && inst.opcode != OPCODE_J && inst.opcode != OPCODE_JAL) + { + int32_t offset = inst.simmediate << 2; + uint32_t target = inst.address + 4 + offset; + + if (target >= function.start && target < function.end) + { + std::string funcName = getFunctionName(target); + if (funcName.empty()) + { + targets.insert(target); + } + } + } + else if (isStaticJump) + { + uint32_t target = (inst.address & 0xF0000000) | (inst.target << 2); + if (target >= function.start && target < function.end) + { + std::string funcName = getFunctionName(target); + if (funcName.empty()) + { + targets.insert(target); + } + } + } + } + + return targets; + } + std::string CodeGenerator::generateFunction(const Function &function, const std::vector &instructions, const bool &useHeaders) { std::stringstream ss; @@ -483,14 +561,22 @@ namespace ps2recomp ss << "#include \"ps2_recompiled_stubs.h\"\n\n"; } + std::unordered_set internalTargets = collectInternalBranchTargets(function, instructions); + ss << "// Function: " << function.name << "\n"; ss << "// Address: 0x" << std::hex << function.start << " - 0x" << function.end << std::dec << "\n"; - ss << "void " << function.name << "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) {\n\n"; + std::string sanitizedName = sanitizeFunctionName(function.name); + ss << "void " << sanitizedName << "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) {\n\n"; for (size_t i = 0; i < instructions.size(); ++i) { const Instruction &inst = instructions[i]; + if (internalTargets.find(inst.address) != internalTargets.end()) + { + ss << "label_" << std::hex << inst.address << std::dec << ":\n"; + } + ss << " // 0x" << std::hex << inst.address << ": 0x" << inst.raw << std::dec << "\n"; try @@ -498,7 +584,13 @@ namespace ps2recomp if (inst.hasDelaySlot && i + 1 < instructions.size()) { const Instruction &delaySlot = instructions[i + 1]; - ss << handleBranchDelaySlots(inst, delaySlot); + + if (internalTargets.find(delaySlot.address) != internalTargets.end()) + { + ss << "label_" << std::hex << delaySlot.address << std::dec << ":\n"; + } + + ss << handleBranchDelaySlots(inst, delaySlot, function, internalTargets); // Skip the delay slot instruction as we've already handled it ++i; @@ -2468,11 +2560,11 @@ namespace ps2recomp if (function.isStub) { - stubFunctions.push_back({function.start, function.name}); + stubFunctions.push_back({function.start, sanitizeFunctionName(function.name)}); } else { - normalFunctions.push_back({function.start, function.name}); + normalFunctions.push_back({function.start, sanitizeFunctionName(function.name)}); } } @@ -2522,10 +2614,10 @@ namespace ps2recomp { ss << " case " << entry.index << ": {\n"; - Symbol *sym = findSymbolByAddress(entry.target); - if (sym && sym->isFunction) + std::string funcName = getFunctionName(entry.target); + if (!funcName.empty()) { - ss << " " << sym->name << "(rdram, ctx, runtime);\n"; + ss << " " << funcName << "(rdram, ctx, runtime);\n"; } else { @@ -2556,4 +2648,4 @@ namespace ps2recomp return nullptr; } -}; \ No newline at end of file +}; diff --git a/ps2xRecomp/src/ps2_recompiler.cpp b/ps2xRecomp/src/ps2_recompiler.cpp index 38f3652..35e4cd5 100644 --- a/ps2xRecomp/src/ps2_recompiler.cpp +++ b/ps2xRecomp/src/ps2_recompiler.cpp @@ -1,10 +1,14 @@ #include "ps2recomp/ps2_recompiler.h" +#include "ps2recomp/instructions.h" #include #include #include #include #include #include +#include +#include +#include namespace fs = std::filesystem; @@ -96,6 +100,8 @@ namespace ps2recomp #endif } + discoverAdditionalEntryPoints(); + std::cout << "Recompilation completed successfully." << std::endl; return true; } @@ -110,6 +116,22 @@ namespace ps2recomp { try { + std::unordered_map renamed; + for (const auto &function : m_functions) + { + if (!function.isRecompiled) + continue; + std::string sanitized = sanitizeFunctionName(function.name); + if (sanitized != function.name) + { + renamed[function.start] = sanitized; + } + } + if (m_codeGenerator) + { + m_codeGenerator->setRenamedFunctions(renamed); + } + generateFunctionHeader(); if (m_config.singleFileOutput) @@ -260,7 +282,7 @@ namespace ps2recomp { if (function.isRecompiled) { - ss << "void " << function.name << "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime);\n"; + ss << "void " << sanitizeFunctionName(function.name) << "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime);\n"; } } @@ -279,6 +301,136 @@ namespace ps2recomp } } + void PS2Recompiler::discoverAdditionalEntryPoints() + { + std::unordered_set existingStarts; + for (const auto &function : m_functions) + { + existingStarts.insert(function.start); + } + + auto getStaticBranchTarget = [](const Instruction &inst) -> std::optional + { + if (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL) + { + return (inst.address & 0xF0000000) | (inst.target << 2); + } + + if (inst.opcode == OPCODE_SPECIAL && + (inst.function == SPECIAL_JR || inst.function == SPECIAL_JALR)) + { + return std::nullopt; + } + + if (inst.isBranch) + { + int32_t offset = static_cast(inst.simmediate) << 2; + return inst.address + 4 + offset; + } + + return std::nullopt; + }; + + auto findContainingFunction = [&](uint32_t address) -> const Function * + { + for (const auto &function : m_functions) + { + if (address >= function.start && address < function.end) + { + return &function; + } + } + return nullptr; + }; + + std::vector newEntries; + + for (const auto &function : m_functions) + { + if (!function.isRecompiled || function.isStub) + { + continue; + } + + auto decodedIt = m_decodedFunctions.find(function.start); + if (decodedIt == m_decodedFunctions.end()) + { + continue; + } + + const auto &instructions = decodedIt->second; + + for (const auto &inst : instructions) + { + auto targetOpt = getStaticBranchTarget(inst); + if (!targetOpt.has_value()) + { + continue; + } + + uint32_t target = targetOpt.value(); + + if ((target & 0x3) != 0 || !m_elfParser->isValidAddress(target)) + { + continue; + } + + if (existingStarts.find(target) != existingStarts.end()) + { + continue; + } + + const Function *containingFunction = findContainingFunction(target); + if (!containingFunction || containingFunction->isStub || !containingFunction->isRecompiled) + { + continue; + } + + auto containingDecodedIt = m_decodedFunctions.find(containingFunction->start); + if (containingDecodedIt == m_decodedFunctions.end()) + { + continue; + } + + const auto &containingInstructions = containingDecodedIt->second; + auto sliceIt = std::find_if(containingInstructions.begin(), containingInstructions.end(), + [&](const Instruction &candidate) + { return candidate.address == target; }); + + if (sliceIt == containingInstructions.end()) + { + continue; + } + + std::vector slicedInstructions(sliceIt, containingInstructions.end()); + m_decodedFunctions[target] = slicedInstructions; + + Function entryFunction; + std::stringstream name; + name << "entry_" << std::hex << target; + entryFunction.name = name.str(); + entryFunction.start = target; + entryFunction.end = containingFunction->end; + entryFunction.isRecompiled = true; + entryFunction.isStub = false; + + newEntries.push_back(entryFunction); + existingStarts.insert(target); + } + } + + if (!newEntries.empty()) + { + m_functions.insert(m_functions.end(), newEntries.begin(), newEntries.end()); + std::sort(m_functions.begin(), m_functions.end(), + [](const Function &a, const Function &b) + { return a.start < b.start; }); + + std::cout << "Discovered " << newEntries.size() + << " additional entry point(s) inside existing functions." << std::endl; + } + } + bool PS2Recompiler::decodeFunction(Function &function) { std::vector instructions; @@ -369,4 +521,13 @@ namespace ps2recomp return outputPath; } + + std::string PS2Recompiler::sanitizeFunctionName(const std::string &name) const + { + if (name.size() >= 2 && name[0] == '_' && (name[1] == '_' || std::isupper(static_cast(name[1])))) + { + return "ps2_" + name; + } + return name; + } } diff --git a/ps2xRuntime/src/lib/ps2_syscalls.cpp b/ps2xRuntime/src/lib/ps2_syscalls.cpp index 830190d..7a75b81 100644 --- a/ps2xRuntime/src/lib/ps2_syscalls.cpp +++ b/ps2xRuntime/src/lib/ps2_syscalls.cpp @@ -946,4 +946,32 @@ namespace ps2_syscalls // Return generic error for unimplemented ones setReturnS32(ctx, -1); // Return -ENOSYS or similar? Use -1 for simplicity. } + + // 0x3C SetupThread: returns stack pointer (stack + stack_size) + // args: $a0 = stack base, $a1 = stack size, $a2 = gp, $a3 = entry point + void SetupThread(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) + { + uint32_t stackBase = getRegU32(ctx, 4); + uint32_t stackSize = getRegU32(ctx, 5); + uint32_t sp = stackBase + stackSize; + setReturnS32(ctx, sp); + } + + // 0x5A QueryBootMode (stub): return 0 for now + void QueryBootMode(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) + { + setReturnS32(ctx, 0); + } + + // 0x5B GetThreadTLS (stub): return 0 + void GetThreadTLS(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) + { + setReturnS32(ctx, 0); + } + + // 0x74 RegisterExitHandler (stub): return 0 + void RegisterExitHandler(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) + { + setReturnS32(ctx, 0); + } } diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt new file mode 100644 index 0000000..cf35bde --- /dev/null +++ b/ps2xTest/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.21) + +project(ps2xTest LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(ps2x_tests + src/main.cpp + src/code_generator_tests.cpp + src/r5900_decoder_tests.cpp +) + +target_include_directories(ps2x_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/ps2xRecomp/include +) + +target_link_libraries(ps2x_tests PRIVATE + ps2_recomp +) diff --git a/ps2xTest/include/MiniTest.h b/ps2xTest/include/MiniTest.h new file mode 100644 index 0000000..4a9d0ab --- /dev/null +++ b/ps2xTest/include/MiniTest.h @@ -0,0 +1,200 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class TestCase; +using TestRunnerCallback = std::function; +using TestBeforeCallback = std::function; + +class TestCase +{ +private: + std::map m_cases; + std::vector m_failReason; + + TestBeforeCallback m_beforeEach; + TestBeforeCallback m_afterEach; + TestBeforeCallback m_before; + TestBeforeCallback m_after; + +public: + void Run(const std::string& testName, TestRunnerCallback fn) + { + m_cases[testName] = fn; + } + + template + inline void Equals(const T& a, const B& b, const std::string& message) + { + if (!(a == b)) + { + m_failReason.emplace_back(message); + } + } + + inline void IsTrue(bool condition, const std::string& message) + { + if (!condition) + { + m_failReason.emplace_back(message); + } + } + + inline void IsFalse(bool condition, const std::string& message) + { + if (condition) + { + m_failReason.emplace_back(message); + } + } + + inline void IsNull(const void* ptr, const std::string& message) + { + if (ptr != nullptr) + { + m_failReason.emplace_back(message); + } + } + + inline void IsNotNull(const void* ptr, const std::string& message) + { + if (ptr == nullptr) + { + m_failReason.emplace_back(message); + } + } + + inline void Fail(const std::string& message) + { + m_failReason.emplace_back(message); + } + + void BeforeEach(const TestBeforeCallback& fn) + { + m_beforeEach = fn; + } + + void AfterEach(const TestBeforeCallback& fn) + { + m_afterEach = fn; + } + + void Before(const TestBeforeCallback& fn) + { + m_before = fn; + } + + void After(const TestBeforeCallback& fn) + { + m_after = fn; + } + + void ClearFailures() + { + m_failReason.clear(); + } + + friend class MiniTest; +}; + +using TestCaseCallback = std::function; + +class MiniTest +{ +private: + inline static std::map m_cases; + +public: + static void Case(const std::string& caseName, const TestCaseCallback& fn) + { + m_cases[caseName] = fn; + } + + static int Run() + { + int failedCount = 0; + int totalTests = 0; + + for (auto& c : m_cases) + { + const std::string& suiteName = c.first; + const TestCaseCallback& suiteCallback = c.second; + + std::cout << "\n[Suite]: " << suiteName << std::endl; + + TestCase testCase; + suiteCallback(testCase); + + if (testCase.m_before) + { + testCase.m_before(); + } + + for (auto& cc : testCase.m_cases) + { + const std::string& testName = cc.first; + const TestRunnerCallback& testFn = cc.second; + + totalTests++; + testCase.ClearFailures(); + + if (testCase.m_beforeEach) + { + testCase.m_beforeEach(); + } + + try + { + std::cout << "\033[33m" << " [Run]: " << "\033[0m" << testName << " "; + testFn(testCase); + + if (!testCase.m_failReason.empty()) + { + failedCount++; + std::cout << "\033[31m" << " [Failed]" << "\033[0m" << std::endl; + for (const auto& reason : testCase.m_failReason) + { + std::cerr << " - " << reason << std::endl; + } + } + else + { + std::cout << "\033[32m" << " [Passed]" << "\033[0m" << std::endl; + } + } + catch (const std::exception& ex) + { + std::cout << " [Error]: " << ex.what() << std::endl; + failedCount++; + } + catch (...) + { + failedCount++; + std::cerr << " [Error]: " << std::endl; + } + + if (testCase.m_afterEach) + { + testCase.m_afterEach(); + } + } + + if (testCase.m_after) + { + testCase.m_after(); + } + } + + std::cout << "\n========================================" << std::endl; + std::cout << "Total Tests: " << totalTests << std::endl; + std::cout << "Passed: " << (totalTests - failedCount) << std::endl; + std::cout << "Failed: " << failedCount << std::endl; + std::cout << "========================================" << std::endl; + + return failedCount; + } +}; \ No newline at end of file diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp new file mode 100644 index 0000000..b0c9f72 --- /dev/null +++ b/ps2xTest/src/code_generator_tests.cpp @@ -0,0 +1,223 @@ +#include "MiniTest.h" +#include "ps2recomp/code_generator.h" +#include "ps2recomp/instructions.h" + +using namespace ps2recomp; + +static Instruction makeBranch(uint32_t address, uint32_t targetOffsetWords) +{ + Instruction inst; + inst.address = address; + inst.raw = 0x10000000 | (address & 0xFFFF); // arbitrary debug value + inst.opcode = OPCODE_BEQ; + inst.rs = 1; + inst.rt = 1; // always equal + inst.simmediate = static_cast(targetOffsetWords); + inst.isBranch = true; + inst.hasDelaySlot = true; + return inst; +} + +static Instruction makeNop(uint32_t address) +{ + Instruction inst; + inst.address = address; + inst.raw = 0; + inst.opcode = OPCODE_ADDIU; + inst.rt = 0; // encode as nop in translator + inst.hasDelaySlot = false; + return inst; +} + +void register_code_generator_tests() +{ + MiniTest::Case("CodeGenerator", [](TestCase &tc) + { + tc.Run("emits labels and gotos for internal branches", [](TestCase &t) { + Function func; + func.name = "test_func"; + func.start = 0x1000; + func.end = 0x1020; + func.isRecompiled = true; + func.isStub = false; + + // Build a small function: + // 0x1000: nop + // 0x1004: beq $1,$1, target (0x100c) with delay slot at 0x1008 + // 0x1008: nop (delay slot) + // 0x100c: nop (branch target) + // 0x1010: nop (fallthrough) + std::vector instructions; + instructions.push_back(makeNop(0x1000)); + instructions.push_back(makeBranch(0x1004, 1)); // target = 0x1004 + 4 + (1<<2) = 0x100c + instructions.push_back(makeNop(0x1008)); // delay slot + instructions.push_back(makeNop(0x100c)); // branch target + instructions.push_back(makeNop(0x1010)); // extra + + CodeGenerator gen({}); + std::string generated = gen.generateFunction(func, instructions, false); + + t.IsTrue(generated.find("label_100c:") != std::string::npos, "branch target should emit a label"); + t.IsTrue(generated.find("goto label_100c;") != std::string::npos, "internal branch should jump via goto"); + t.IsTrue(generated.find("label_1008:") == std::string::npos, "delay slot without incoming branch should not get a label"); + }); + + tc.Run("labels delay slot when it is a branch target", [](TestCase &t) { + Function func; + func.name = "delay_slot_label"; + func.start = 0x2000; + func.end = 0x2020; + func.isRecompiled = true; + func.isStub = false; + + // Branch at 0x2000 targets 0x2004 (its own delay slot) + std::vector instructions; + instructions.push_back(makeBranch(0x2000, 0)); // target = 0x2004 + instructions.push_back(makeNop(0x2004)); // delay slot and target + instructions.push_back(makeNop(0x2008)); // extra + + CodeGenerator gen({}); + std::string generated = gen.generateFunction(func, instructions, false); + + t.IsTrue(generated.find("label_2004:") != std::string::npos, "delay slot that is a target should emit a label"); + t.IsTrue(generated.find("goto label_2004;") != std::string::npos, "branch to delay slot should use goto"); + }); + + tc.Run("branches outside function still set pc", [](TestCase &t) { + Function func; + func.name = "external_branch"; + func.start = 0x3000; + func.end = 0x3020; + func.isRecompiled = true; + func.isStub = false; + + // Branch targets outside the function range + std::vector instructions; + instructions.push_back(makeBranch(0x3000, 4)); // target = 0x3014 (inside) -> make it outside by adjusting end? easier: set end smaller? Instead use large offset + instructions.clear(); + Instruction br = makeBranch(0x3000, 0x100); // target far outside + instructions.push_back(br); + instructions.push_back(makeNop(0x3004)); // delay slot + + CodeGenerator gen({}); + std::string generated = gen.generateFunction(func, instructions, false); + + t.IsTrue(generated.find("ctx->pc = 0x") != std::string::npos, "external branch should set ctx->pc"); + t.IsTrue(generated.find("goto label_") == std::string::npos, "external branch should not use goto"); + }); + + tc.Run("jumps to known symbols call by name", [](TestCase &t) { + Function func; + func.name = "call_symbol"; + func.start = 0x4000; + func.end = 0x4018; + func.isRecompiled = true; + func.isStub = false; + + Symbol targetSym; + targetSym.name = "target_func"; + targetSym.address = 0x5000; + targetSym.isFunction = true; + + Instruction j{}; + j.address = 0x4000; + j.opcode = OPCODE_J; + j.target = (targetSym.address >> 2) & 0x3FFFFFF; + j.hasDelaySlot = true; + j.raw = 0x08000000 | (j.target & 0x3FFFFFF); + + Instruction delay = makeNop(0x4004); + + std::vector instructions{j, delay, makeNop(0x4008)}; + + CodeGenerator gen({targetSym}); + std::string generated = gen.generateFunction(func, instructions, false); + + t.IsTrue(generated.find("target_func(rdram, ctx, runtime); return;") != std::string::npos, + "jump to known function should emit direct call"); + }); + + tc.Run("jump to unknown target sets pc", [](TestCase &t) { + Function func; + func.name = "jump_unknown"; + func.start = 0x6000; + func.end = 0x6010; + func.isRecompiled = true; + func.isStub = false; + + Instruction j{}; + j.address = 0x6000; + j.opcode = OPCODE_J; + j.target = 0x001234; // target = 0x00048d0 + j.hasDelaySlot = true; + j.raw = (OPCODE_J << 26) | (j.target & 0x3FFFFFF); + Instruction delay = makeNop(0x6004); + + std::vector instructions{j, delay}; + + CodeGenerator gen({}); + std::string generated = gen.generateFunction(func, instructions, false); + + t.IsTrue(generated.find("ctx->pc = 0x") != std::string::npos, "unknown jump target should set ctx->pc"); + t.IsTrue(generated.find("goto label_") == std::string::npos, "external jump should not use goto"); + }); + + tc.Run("renamed function used in jump table", [](TestCase &t) { + Function func; + func.name = "jt_func"; + func.start = 0x7000; + func.end = 0x7010; + func.isRecompiled = true; + func.isStub = false; + + JumpTableEntry entry; + entry.index = 0; + entry.target = 0x8000; + std::vector entries{entry}; + + Instruction inst{}; + inst.opcode = OPCODE_REGIMM; + + CodeGenerator gen({}); + gen.setRenamedFunctions({{0x8000, "renamed_target"}}); + + std::string sw = gen.generateJumpTableSwitch(inst, 0x0, entries); + + t.IsTrue(sw.find("renamed_target(rdram, ctx, runtime);") != std::string::npos, + "jump table should use renamed function name"); + }); + + tc.Run("reserved identifiers are sanitized and used in calls", [](TestCase &t) { + Function func; + func.name = "__is_pointer"; + func.start = 0x9000; + func.end = 0x9010; + func.isRecompiled = true; + func.isStub = false; + + Symbol targetSym; + targetSym.name = "__is_pointer"; + targetSym.address = func.start; + targetSym.isFunction = true; + + Instruction j{}; + j.address = 0x8000; + j.opcode = OPCODE_J; + j.target = (targetSym.address >> 2) & 0x3FFFFFF; + j.hasDelaySlot = true; + j.raw = (OPCODE_J << 26) | (j.target & 0x3FFFFFF); + Instruction delay = makeNop(0x8004); + + std::vector instructions{j, delay}; + + CodeGenerator gen({targetSym}); + gen.setRenamedFunctions({{targetSym.address, "ps2___is_pointer"}}); + + std::string generated = gen.generateFunction(func, instructions, false); + + t.IsTrue(generated.find("void ps2___is_pointer(") != std::string::npos, + "definition should use sanitized name"); + t.IsTrue(generated.find("ps2___is_pointer(rdram, ctx, runtime); return;") != std::string::npos, + "call should use sanitized name"); + }); }); +} diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp new file mode 100644 index 0000000..729b92e --- /dev/null +++ b/ps2xTest/src/main.cpp @@ -0,0 +1,11 @@ +#include "MiniTest.h" + +void register_code_generator_tests(); +void register_r5900_decoder_tests(); + +int main() +{ + register_code_generator_tests(); + register_r5900_decoder_tests(); + return MiniTest::Run(); +} diff --git a/ps2xTest/src/r5900_decoder_tests.cpp b/ps2xTest/src/r5900_decoder_tests.cpp new file mode 100644 index 0000000..2affebe --- /dev/null +++ b/ps2xTest/src/r5900_decoder_tests.cpp @@ -0,0 +1,193 @@ +#include "MiniTest.h" +#include "ps2recomp/r5900_decoder.h" + +using namespace ps2recomp; + +void register_r5900_decoder_tests() +{ + MiniTest::Case("R5900Decoder", [](TestCase &tc) + { + tc.Run("decodes JAL with jump target and call flag", [](TestCase &t) { + // jal 0x00400000 at address 0x1000 => opcode 0x0C100000 (target = 0x00400000 >> 2) + uint32_t address = 0x1000; + uint32_t target = 0x00400000; + uint32_t raw = (OPCODE_JAL << 26) | ((target >> 2) & 0x03FFFFFF); + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isJump, "jal should be marked as jump"); + t.IsTrue(inst.isCall, "jal should be marked as call"); + t.IsTrue(inst.hasDelaySlot, "jal has a delay slot"); + t.Equals(decoder.getJumpTarget(inst), target, "jal jump target should match encoded target"); + }); + + tc.Run("J computes target with upper PC bits", [](TestCase &t) { + // Place J at address 0x8FFF_FFFC targeting 0x8123_4560 (upper bits from PC+4) + uint32_t address = 0x8FFFFFFC; + uint32_t encodedTarget = 0x0123456; // 0x048D1598 >> 2, but we want lower bits of 0x1234560 + uint32_t raw = (OPCODE_J << 26) | (encodedTarget & 0x03FFFFFF); + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + uint32_t expectedPcUpper = (address + 4) & 0xF0000000; + uint32_t expected = expectedPcUpper | (encodedTarget << 2); + t.Equals(decoder.getJumpTarget(inst), expected, "J target should combine PC upper bits with encoded target"); + }); + + tc.Run("JR/JALR jump target is zero (dynamic)", [](TestCase &t) { + uint32_t address = 0x1200; + uint32_t jrRaw = (OPCODE_SPECIAL << 26) | (2 << 21) | SPECIAL_JR; + uint32_t jalrRaw = (OPCODE_SPECIAL << 26) | (3 << 21) | (31 << 11) | SPECIAL_JALR; + + R5900Decoder decoder; + Instruction jr = decoder.decodeInstruction(address, jrRaw); + Instruction jalr = decoder.decodeInstruction(address + 4, jalrRaw); + + t.Equals(decoder.getJumpTarget(jr), 0u, "JR jump target should be unknown (0)"); + t.Equals(decoder.getJumpTarget(jalr), 0u, "JALR jump target should be unknown (0)"); + }); + + tc.Run("decodes BEQ sets branch flags and target", [](TestCase &t) { + // beq r1, r2, offset 0x4 (word offset) at address 0x2000 + uint32_t address = 0x2000; + uint16_t offset = 0x0004; + uint32_t raw = (OPCODE_BEQ << 26) | (1 << 21) | (2 << 16) | offset; + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isBranch, "beq should be marked as branch"); + t.IsTrue(inst.hasDelaySlot, "beq has a delay slot"); + uint32_t expectedTarget = address + 4 + (static_cast(offset) << 2); + t.Equals(decoder.getBranchTarget(inst), expectedTarget, "beq target should be computed from simmediate"); + }); + + tc.Run("branch target sign-extends negative offset", [](TestCase &t) { + uint32_t address = 0x2100; + int16_t negOffset = -4; // jump back 16 bytes + uint32_t raw = (OPCODE_BNE << 26) | (1 << 21) | (2 << 16) | (negOffset & 0xFFFF); + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + uint32_t expectedTarget = address + 4 + (static_cast(negOffset) << 2); + t.Equals(decoder.getBranchTarget(inst), expectedTarget, "negative branch offsets should sign-extend"); + }); + + tc.Run("decodes load/store flags", [](TestCase &t) { + uint32_t address = 0x3000; + uint32_t lwRaw = (OPCODE_LW << 26) | (1 << 21) | (2 << 16) | 0x10; + uint32_t swRaw = (OPCODE_SW << 26) | (3 << 21) | (4 << 16) | 0x20; + + R5900Decoder decoder; + Instruction lw = decoder.decodeInstruction(address, lwRaw); + Instruction sw = decoder.decodeInstruction(address + 4, swRaw); + + t.IsTrue(lw.isLoad, "lw should be marked as load"); + t.IsFalse(lw.isStore, "lw should not be marked as store"); + t.IsTrue(sw.isStore, "sw should be marked as store"); + t.IsFalse(sw.isLoad, "sw should not be marked as load"); + }); + + tc.Run("JR is marked as return when rs is $ra", [](TestCase &t) { + uint32_t address = 0x4000; + uint32_t raw = (OPCODE_SPECIAL << 26) | (31 << 21) | SPECIAL_JR; // jr $ra + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isJump, "jr should be jump"); + t.IsTrue(inst.isReturn, "jr $ra should be marked as return"); + t.IsTrue(inst.hasDelaySlot, "jr has delay slot"); + }); + + tc.Run("JALR marks call and writes rd when non-zero", [](TestCase &t) { + uint32_t address = 0x5000; + uint32_t rd = 5; + uint32_t raw = (OPCODE_SPECIAL << 26) | (2 << 21) | (rd << 11) | SPECIAL_JALR; // jalr $v0, $a0 + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isJump, "jalr should be jump"); + t.IsTrue(inst.isCall, "jalr should be call"); + t.IsTrue(inst.hasDelaySlot, "jalr has delay slot"); + t.IsTrue(inst.modificationInfo.modifiesGPR, "jalr with rd!=0 should mark GPR modification"); + }); + + tc.Run("MMI instruction sets MMI flags", [](TestCase &t) { + uint32_t address = 0x6000; + // Use opcode 0x1C (MMI), rs=1, rt=2, rd=3, sa=MMI0_PADDW (0) + uint32_t raw = (OPCODE_MMI << 26) | (1 << 21) | (2 << 16) | (3 << 11) | MMI0_PADDW; + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isMMI, "MMI opcode should set isMMI"); + t.IsTrue(inst.isMultimedia, "MMI opcode should set multimedia flag"); + t.Equals(inst.mmiType, static_cast(0), "MMI0 should set mmiType to 0"); + t.Equals(inst.mmiFunction, static_cast(MMI0_PADDW), "MMI function should match sa field"); + }); + + tc.Run("COP2 VU macro op marks VU flags", [](TestCase &t) { + uint32_t address = 0x7000; + // COP2, rs = COP2_CO (macro), function = VU0_S2_VDIV (0x31) + uint32_t raw = (OPCODE_COP2 << 26) | (COP2_CO << 21) | VU0_S2_VDIV; + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isVU, "VU macro should set isVU"); + t.IsTrue(inst.isMultimedia, "VU macro should set multimedia"); + t.IsTrue(inst.modificationInfo.modifiesControl, "VDIV should mark control modification"); + t.IsTrue(inst.vectorInfo.usesQReg, "VDIV should use Q register"); + uint8_t expectedVecField = static_cast((raw >> 21) & 0xF); + t.Equals(inst.vectorInfo.vectorField, expectedVecField, "vector field should reflect encoding"); + }); + + tc.Run("REGIMM branch and link marks call and GPR modification", [](TestCase &t) { + uint32_t address = 0x8000; + uint16_t offset = 0x2; + uint32_t raw = (OPCODE_REGIMM << 26) | (1 << 21) | (REGIMM_BGEZAL << 16) | offset; + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isBranch, "bgezal should be branch"); + t.IsTrue(inst.isCall, "bgezal should be call (link)"); + t.IsTrue(inst.hasDelaySlot, "bgezal has delay slot"); + t.IsTrue(inst.modificationInfo.modifiesGPR, "bgezal should mark GPR modification for $ra"); + uint32_t expectedTarget = address + 4 + (static_cast(offset) << 2); + t.Equals(decoder.getBranchTarget(inst), expectedTarget, "bgezal target should be computed"); + }); + + tc.Run("LL/SC modify control and set load/store flags", [](TestCase &t) { + uint32_t address = 0x9000; + uint32_t llRaw = (OPCODE_LL << 26) | (2 << 21) | (3 << 16) | 0x10; + uint32_t scRaw = (OPCODE_SC << 26) | (4 << 21) | (5 << 16) | 0x20; + + R5900Decoder decoder; + Instruction ll = decoder.decodeInstruction(address, llRaw); + Instruction sc = decoder.decodeInstruction(address + 4, scRaw); + + t.IsTrue(ll.isLoad, "ll should be load"); + t.IsTrue(ll.modificationInfo.modifiesControl, "ll should modify control (LL bit)"); + t.IsTrue(sc.isStore, "sc should be store"); + t.IsTrue(sc.modificationInfo.modifiesControl, "sc should modify control (LL bit)"); + t.IsTrue(sc.modificationInfo.modifiesGPR, "sc writes success flag to rt"); + }); + + tc.Run("COP0 ERET is marked as return without delay slot", [](TestCase &t) { + uint32_t address = 0xA000; + uint32_t raw = (OPCODE_COP0 << 26) | (COP0_CO << 21) | COP0_CO_ERET; + + R5900Decoder decoder; + Instruction inst = decoder.decodeInstruction(address, raw); + + t.IsTrue(inst.isReturn, "eret should be marked as return"); + t.IsFalse(inst.hasDelaySlot, "eret should not have a delay slot"); + t.IsTrue(inst.modificationInfo.modifiesControl, "eret changes control state"); + }); }); +}