feat: added minitest framework

feat: added some unit tests for code gen and decoder
fix: backport some bugfix from chrisking1981 branch
This commit is contained in:
Ran-j
2025-12-26 18:33:52 -03:00
parent c05d8f1809
commit 409570c709
12 changed files with 971 additions and 29 deletions
+1
View File
@@ -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
+2 -1
View File
@@ -6,4 +6,5 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_subdirectory("ps2xRecomp")
add_subdirectory("ps2xRuntime")
add_subdirectory("ps2xAnalyzer")
add_subdirectory("ps2xAnalyzer")
add_subdirectory("ps2xTest")
+12 -3
View File
@@ -5,6 +5,8 @@
#include <string>
#include <vector>
#include <map>
#include <unordered_map>
#include <unordered_set>
namespace ps2recomp
{
@@ -18,10 +20,16 @@ namespace ps2recomp
std::string generateFunction(const Function &function, const std::vector<Instruction> &instructions, const bool &useHeaders);
std::string generateFunctionRegistration(const std::vector<Function> &functions, const std::map<uint32_t, std::string> &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<uint32_t> &internalTargets);
private:
void setRenamedFunctions(const std::unordered_map<uint32_t, std::string> &renames);
std::unordered_set<uint32_t> collectInternalBranchTargets(const Function &function,
const std::vector<Instruction> &instructions);
public:
std::vector<Symbol> m_symbols;
std::unordered_map<uint32_t, std::string> m_renamedFunctions;
std::string translateInstruction(const Instruction &inst);
std::string translateMMIInstruction(const Instruction &inst);
@@ -98,8 +106,9 @@ namespace ps2recomp
const std::vector<JumpTableEntry> &entries);
Symbol *findSymbolByAddress(uint32_t address);
std::string getFunctionName(uint32_t address);
};
}
#endif // PS2RECOMP_CODE_GENERATOR_H
#endif // PS2RECOMP_CODE_GENERATOR_H
@@ -41,14 +41,16 @@ namespace ps2recomp
std::map<uint32_t, std::string> 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
#endif
+115 -23
View File
@@ -4,7 +4,9 @@
#include <sstream>
#include <algorithm>
#include <unordered_set>
#include <unordered_map>
#include <iostream>
#include <cctype>
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<uint32_t, std::string> &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<unsigned char>(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<uint32_t> &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<uint32_t> CodeGenerator::collectInternalBranchTargets(
const Function &function, const std::vector<Instruction> &instructions)
{
std::unordered_set<uint32_t> 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<Instruction> &instructions, const bool &useHeaders)
{
std::stringstream ss;
@@ -483,14 +561,22 @@ namespace ps2recomp
ss << "#include \"ps2_recompiled_stubs.h\"\n\n";
}
std::unordered_set<uint32_t> 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;
}
};
};
+162 -1
View File
@@ -1,10 +1,14 @@
#include "ps2recomp/ps2_recompiler.h"
#include "ps2recomp/instructions.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <stdexcept>
#include <filesystem>
#include <cctype>
#include <unordered_set>
#include <optional>
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<uint32_t, std::string> 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<uint32_t> existingStarts;
for (const auto &function : m_functions)
{
existingStarts.insert(function.start);
}
auto getStaticBranchTarget = [](const Instruction &inst) -> std::optional<uint32_t>
{
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<int32_t>(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<Function> 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<Instruction> 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<Instruction> 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<unsigned char>(name[1]))))
{
return "ps2_" + name;
}
return name;
}
}
+28
View File
@@ -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);
}
}
+21
View File
@@ -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
)
+200
View File
@@ -0,0 +1,200 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include <map>
#include <iostream>
#include <exception>
class TestCase;
using TestRunnerCallback = std::function<void(TestCase&)>;
using TestBeforeCallback = std::function<void()>;
class TestCase
{
private:
std::map<std::string, TestRunnerCallback> m_cases;
std::vector<std::string> 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<typename T, typename B>
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<void(TestCase&)>;
class MiniTest
{
private:
inline static std::map<std::string, TestCaseCallback> 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]: <unknown>" << 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;
}
};
+223
View File
@@ -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<uint32_t>(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<Instruction> 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<Instruction> 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<Instruction> 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<Instruction> 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<Instruction> 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<JumpTableEntry> 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<Instruction> 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");
}); });
}
+11
View File
@@ -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();
}
+193
View File
@@ -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<int16_t>(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<int16_t>(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<uint8_t>(0), "MMI0 should set mmiType to 0");
t.Equals(inst.mmiFunction, static_cast<uint8_t>(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<uint8_t>((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<int16_t>(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");
}); });
}