mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-28 09:24:41 -04:00
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:
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}); });
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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");
|
||||
}); });
|
||||
}
|
||||
Reference in New Issue
Block a user