Files
PS2Recomp/ps2xTest/src/ps2_recompiler_tests.cpp
T
Ran-j a293fa433a feat: IOP emulator
refactor: codegen to catch callbacks on mips code
feat: added a lot of entries or IOP emulator
2026-08-19 16:53:00 -03:00

1242 lines
57 KiB
C++

#include "MiniTest.h"
#include "ps2recomp/ps2_recompiler.h"
#include "ps2recomp/config_manager.h"
#include "ps2recomp/elf_parser.h"
#include "ps2recomp/instructions.h"
#include "ps2recomp/types.h"
#include "ps2_runtime_calls.h"
#include <elfio/elfio.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <unordered_map>
#include <vector>
using namespace ps2recomp;
static Instruction makeNopLike(uint32_t address)
{
Instruction inst{};
inst.address = address;
inst.opcode = OPCODE_ADDIU;
inst.rt = 0;
inst.raw = 0;
return inst;
}
static Instruction makeAbsJump(uint32_t address, uint32_t target, uint32_t opcode)
{
Instruction inst{};
inst.address = address;
inst.opcode = opcode;
inst.target = (target >> 2) & 0x03FFFFFFu;
inst.hasDelaySlot = true;
inst.raw = (opcode << 26) | inst.target;
return inst;
}
static Instruction makeJrRa(uint32_t address)
{
Instruction inst{};
inst.address = address;
inst.opcode = OPCODE_SPECIAL;
inst.function = SPECIAL_JR;
inst.rs = 31;
inst.hasDelaySlot = true;
inst.raw = 0x03E00008u;
return inst;
}
static Function makeFunction(const std::string &name, uint32_t start, uint32_t end)
{
Function fn{};
fn.name = name;
fn.start = start;
fn.end = end;
fn.isRecompiled = true;
fn.isStub = false;
fn.isSkipped = false;
return fn;
}
static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
const char textBytes[] = {0x08, 0x00, static_cast<char>(0xE0), 0x03, 0x00, 0x00, 0x00, 0x00};
text->set_data(textBytes, sizeof(textBytes));
ELFIO::section *data = writer.sections.add(".data");
data->set_type(ELFIO::SHT_PROGBITS);
data->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE);
data->set_addr_align(4);
data->set_address(0x00200000u);
const char dataBytes[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, static_cast<char>(0x88)};
data->set_data(dataBytes, sizeof(dataBytes));
ELFIO::section *strtab = writer.sections.add(".strtab");
strtab->set_type(ELFIO::SHT_STRTAB);
strtab->set_addr_align(1);
ELFIO::section *symtab = writer.sections.add(".symtab");
symtab->set_type(ELFIO::SHT_SYMTAB);
symtab->set_info(1);
symtab->set_link(strtab->get_index());
symtab->set_addr_align(4);
symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB));
ELFIO::symbol_section_accessor symbols(writer, symtab);
ELFIO::string_section_accessor strings(strtab);
symbols.add_symbol(strings, "", 0, 0, ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF);
symbols.add_symbol(strings, "code_func", text->get_address(), text->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
symbols.add_symbol(strings, "data_func", data->get_address(), data->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, data->get_index());
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
ELFIO::segment *dataSegment = writer.segments.add();
dataSegment->set_type(ELFIO::PT_LOAD);
dataSegment->set_flags(ELFIO::PF_R | ELFIO::PF_W);
dataSegment->set_align(0x1000);
dataSegment->add_section_index(data->get_index(), data->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
const std::array<uint32_t, 6> textWords = {
0x0C040004u, // jal 0x00100010
0x00000000u, // nop
0x03E00008u, // jr $ra
0x00000000u, // nop
0x03E00008u, // jr $ra
0x00000000u // nop
};
text->set_data(reinterpret_cast<const char *>(textWords.data()),
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
std::array<uint32_t, 30> textWords{};
textWords[0] = 0x3C040010u; // lui a0,0x10
textWords[1] = 0xAC800000u; // sw zero,0(a0)
textWords[2] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[3] = 0x24840040u; // addiu a0,a0,0x40 (delay slot)
textWords[4] = 0x03E00008u; // jr ra
textWords[5] = 0x00000000u; // nop
textWords[6] = 0x3C080010u; // lui t0,0x10
textWords[7] = 0x25080070u; // addiu t0,t0,0x70 (code label, not a callback argument)
textWords[8] = 0x03E00008u; // registrar at 0x00100020
textWords[9] = 0x00000000u;
textWords[16] = 0x27BDFFF0u; // callback at 0x00100040: addiu sp,sp,-0x10
textWords[17] = 0xFFBF0000u; // sd ra,0(sp)
textWords[18] = 0xDFBF0000u; // ld ra,0(sp)
textWords[19] = 0x03E00008u; // jr ra
textWords[20] = 0x27BD0010u; // addiu sp,sp,0x10
textWords[24] = 0x03E00008u; // table leaf at 0x00100060
textWords[25] = 0x00000000u;
textWords[26] = 0x03E00008u; // table leaf at 0x00100068
textWords[27] = 0x00000000u;
textWords[28] = 0x03E00008u; // isolated pointer target at 0x00100070
textWords[29] = 0x00000000u;
text->set_data(reinterpret_cast<const char *>(textWords.data()),
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
ELFIO::section *rodata = writer.sections.add(".rodata");
rodata->set_type(ELFIO::SHT_PROGBITS);
rodata->set_flags(ELFIO::SHF_ALLOC);
rodata->set_addr_align(4);
rodata->set_address(0x00200000u);
std::array<uint32_t, 20> tableWords{};
tableWords[1] = 0x00100060u;
tableWords[3] = 0x00100068u;
tableWords[16] = 0x00100070u; // plausible entry, but not part of a pointer cluster
rodata->set_data(reinterpret_cast<const char *>(tableWords.data()),
static_cast<ELFIO::Elf_Word>(tableWords.size() * sizeof(uint32_t)));
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
ELFIO::segment *dataSegment = writer.segments.add();
dataSegment->set_type(ELFIO::PT_LOAD);
dataSegment->set_flags(ELFIO::PF_R);
dataSegment->set_align(0x1000);
dataSegment->add_section_index(rodata->get_index(), rodata->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithInitializer(const std::filesystem::path &elfPath,
const std::string &functionName,
uint32_t initializerTarget)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
const std::array<uint32_t, 2> textWords = {
0x03E00008u, // jr $ra
0x00000000u, // nop
};
text->set_data(reinterpret_cast<const char *>(textWords.data()),
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
ELFIO::section *ctors = writer.sections.add(".ctors");
ctors->set_type(ELFIO::SHT_PROGBITS);
ctors->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE);
ctors->set_addr_align(4);
ctors->set_address(0x00200000u);
ctors->set_data(reinterpret_cast<const char *>(&initializerTarget),
static_cast<ELFIO::Elf_Word>(sizeof(initializerTarget)));
ELFIO::section *strtab = writer.sections.add(".strtab");
strtab->set_type(ELFIO::SHT_STRTAB);
strtab->set_addr_align(1);
ELFIO::section *symtab = writer.sections.add(".symtab");
symtab->set_type(ELFIO::SHT_SYMTAB);
symtab->set_info(1);
symtab->set_link(strtab->get_index());
symtab->set_addr_align(4);
symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB));
ELFIO::symbol_section_accessor symbols(writer, symtab);
ELFIO::string_section_accessor strings(strtab);
symbols.add_symbol(strings, "", 0, 0,
ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF);
symbols.add_symbol(strings, functionName.c_str(), text->get_address(), text->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
ELFIO::segment *dataSegment = writer.segments.add();
dataSegment->set_type(ELFIO::PT_LOAD);
dataSegment->set_flags(ELFIO::PF_R | ELFIO::PF_W);
dataSegment->set_align(0x1000);
dataSegment->add_section_index(ctors->get_index(), ctors->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeRecompilerTestConfig(const std::filesystem::path &configPath,
const std::filesystem::path &elfPath,
const std::filesystem::path &outputPath,
const std::vector<std::string> &skip,
const std::vector<std::string> &stubs = {})
{
std::ofstream config(configPath);
if (!config)
return false;
config << "[general]\n";
config << "input = \"" << elfPath.generic_string() << "\"\n";
config << "output = \"" << outputPath.generic_string() << "\"\n";
config << "skip = [";
for (size_t i = 0; i < skip.size(); ++i)
{
if (i != 0u)
config << ", ";
config << '"' << skip[i] << '"';
}
config << "]\n";
config << "stubs = [";
for (size_t i = 0; i < stubs.size(); ++i)
{
if (i != 0u)
config << ", ";
config << '"' << stubs[i] << '"';
}
config << "]\n";
return static_cast<bool>(config);
}
void register_ps2_recompiler_tests()
{
MiniTest::Case("PS2Recompiler", [](TestCase &tc)
{
tc.Run("game helpers are not classified as runtime stubs", [](TestCase &t) {
t.IsFalse(ps2_runtime_calls::isStubName("Pad_init"),
"Pad_init should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("Pad_set"),
"Pad_set should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("pdInitPeripheral"),
"pdInitPeripheral should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("pdGetPeripheral"),
"pdGetPeripheral should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("InitThread"),
"InitThread should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("syFree"),
"syFree should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("syMallocInit"),
"syMallocInit should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("syHwInit"),
"syHwInit should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("syHwInit2"),
"syHwInit2 should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("syRtcInit"),
"syRtcInit should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("sdDrvInit"),
"sdDrvInit should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("sdSndStopAll"),
"sdSndStopAll should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("sdSysFinish"),
"sdSysFinish should be recompiled as game code");
t.IsFalse(ps2_runtime_calls::isStubName("iopGetArea"),
"iopGetArea should be recompiled as game code");
t.IsTrue(ps2_runtime_calls::isStubName("builtin_set_imask"),
"builtin_set_imask should remain a runtime helper");
t.IsTrue(ps2_runtime_calls::isStubName("getpid"),
"getpid should remain a runtime helper");
t.IsTrue(ps2_runtime_calls::isStubName("scePadRead"),
"scePadRead should remain a runtime pad stub");
});
tc.Run("additional entries split at nearest discovered boundary", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x3000u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("container", 0x1000u, 0x1018u),
makeFunction("caller", 0x2000u, 0x2010u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeNopLike(0x1000u),
makeNopLike(0x1004u),
makeNopLike(0x1008u),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u)
};
decodedFunctions[0x2000u] = {
makeAbsJump(0x2000u, 0x1008u, OPCODE_JAL),
makeNopLike(0x2004u),
makeAbsJump(0x2008u, 0x100Cu, OPCODE_J),
makeNopLike(0x200Cu)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(3),
"expected two mid-function targets plus the JAL return entry to be discovered");
auto findByStart = [&](uint32_t start) -> const Function* {
auto it = std::find_if(functions.begin(), functions.end(),
[&](const Function &fn) { return fn.start == start; });
if (it == functions.end())
{
return nullptr;
}
return &(*it);
};
const Function *entry1008 = findByStart(0x1008u);
const Function *entry100C = findByStart(0x100Cu);
const Function *entry2008 = findByStart(0x2008u);
t.IsNotNull(entry1008, "entry at 0x1008 should exist");
t.IsNotNull(entry100C, "entry at 0x100C should exist");
t.IsNotNull(entry2008, "JAL return address entry at 0x2008 should exist");
if (entry1008 && entry100C)
{
t.Equals(entry1008->end, 0x100Cu,
"entry 0x1008 should end at nearest discovered start 0x100C");
t.Equals(entry100C->end, 0x1018u,
"entry 0x100C should end at containing function end");
}
if (entry2008)
{
t.Equals(entry2008->end, 0x2010u,
"return entry 0x2008 should slice through the caller tail");
}
auto decoded1008It = decodedFunctions.find(0x1008u);
auto decoded100CIt = decodedFunctions.find(0x100Cu);
auto decoded2008It = decodedFunctions.find(0x2008u);
t.IsTrue(decoded1008It != decodedFunctions.end(), "decoded slice for 0x1008 should exist");
t.IsTrue(decoded100CIt != decodedFunctions.end(), "decoded slice for 0x100C should exist");
t.IsTrue(decoded2008It != decodedFunctions.end(), "decoded slice for 0x2008 should exist");
if (decoded1008It != decodedFunctions.end())
{
t.Equals(decoded1008It->second.size(), static_cast<size_t>(1),
"entry 0x1008 slice should stop before 0x100C");
if (!decoded1008It->second.empty())
{
t.Equals(decoded1008It->second.front().address, 0x1008u,
"entry 0x1008 slice should begin at 0x1008");
}
}
if (decoded100CIt != decodedFunctions.end() && !decoded100CIt->second.empty())
{
t.Equals(decoded100CIt->second.front().address, 0x100Cu,
"entry 0x100C slice should begin at 0x100C");
}
if (decoded2008It != decodedFunctions.end())
{
t.Equals(decoded2008It->second.size(), static_cast<size_t>(2),
"return entry 0x2008 slice should keep the jump and its delay slot");
if (!decoded2008It->second.empty())
{
t.Equals(decoded2008It->second.front().address, 0x2008u,
"return entry 0x2008 slice should begin at the JAL fallthrough");
}
}
});
tc.Run("entry reslice trims earlier entries after late discovery", [](TestCase &t) {
std::vector<Function> functions = {
makeFunction("container", 0x1000u, 0x1018u),
makeFunction("entry_1008", 0x1008u, 0x1018u),
makeFunction("entry_100c", 0x100Cu, 0x1018u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeNopLike(0x1000u),
makeNopLike(0x1004u),
makeNopLike(0x1008u),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u)
};
decodedFunctions[0x1008u] = {
makeNopLike(0x1008u),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u)
};
decodedFunctions[0x100Cu] = {
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u)
};
size_t resliced = PS2Recompiler::ResliceEntryFunctions(functions, decodedFunctions);
t.Equals(resliced, static_cast<size_t>(1),
"expected only the earlier entry to be resliced");
auto findByStart = [&](uint32_t start) -> const Function* {
auto it = std::find_if(functions.begin(), functions.end(),
[&](const Function &fn) { return fn.start == start; });
if (it == functions.end())
{
return nullptr;
}
return &(*it);
};
const Function *entry1008 = findByStart(0x1008u);
const Function *entry100C = findByStart(0x100Cu);
t.IsNotNull(entry1008, "entry at 0x1008 should exist");
t.IsNotNull(entry100C, "entry at 0x100C should exist");
if (entry1008)
{
t.Equals(entry1008->end, 0x100Cu,
"entry 0x1008 should be trimmed to next entry start");
}
if (entry100C)
{
t.Equals(entry100C->end, 0x1018u,
"entry 0x100C should still end at containing end");
}
auto decoded1008It = decodedFunctions.find(0x1008u);
auto decoded100CIt = decodedFunctions.find(0x100Cu);
t.IsTrue(decoded1008It != decodedFunctions.end(), "decoded slice for 0x1008 should exist");
t.IsTrue(decoded100CIt != decodedFunctions.end(), "decoded slice for 0x100C should exist");
if (decoded1008It != decodedFunctions.end())
{
t.Equals(decoded1008It->second.size(), static_cast<size_t>(1),
"entry 0x1008 slice should stop before 0x100C");
if (!decoded1008It->second.empty())
{
t.Equals(decoded1008It->second.front().address, 0x1008u,
"entry 0x1008 slice should begin at 0x1008");
}
}
if (decoded100CIt != decodedFunctions.end())
{
t.Equals(decoded100CIt->second.size(), static_cast<size_t>(3),
"entry 0x100C slice should keep remaining instructions");
}
});
tc.Run("same-function JAL return addresses get entry wrappers but targets stay labels", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x40u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("container", 0x1000u, 0x101Cu)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeAbsJump(0x1000u, 0x100Cu, OPCODE_JAL),
makeNopLike(0x1004u),
makeAbsJump(0x1008u, 0x1014u, OPCODE_J),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u),
makeJrRa(0x1018u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(1),
"same-function JAL should create only the resume entry while plain J stays internal");
const bool hasResumeEntry = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x1008u; });
const bool hasCallEntry = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x100Cu; });
const bool hasJumpEntry = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x1014u && fn.name.rfind("entry_", 0) == 0; });
t.IsTrue(hasResumeEntry, "same-function JAL return address should be promoted to a resumable entry");
t.IsFalse(hasCallEntry, "same-function JAL target should remain an internal label");
t.IsFalse(hasJumpEntry, "same-function J target should remain an internal label only");
});
tc.Run("JAL return addresses get resumable entry wrappers", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("caller", 0x1000u, 0x1018u),
makeFunction("callee", 0x2000u, 0x2008u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeAbsJump(0x1000u, 0x2000u, OPCODE_JAL),
makeNopLike(0x1004u),
makeNopLike(0x1008u),
makeNopLike(0x100Cu),
makeJrRa(0x1010u),
makeNopLike(0x1014u)
};
decodedFunctions[0x2000u] = {
makeJrRa(0x2000u),
makeNopLike(0x2004u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(1),
"external JAL should create one resumable entry at the caller return address");
auto entryIt = std::find_if(functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x1008u; });
t.IsTrue(entryIt != functions.end(), "return address 0x1008 should be promoted to an entry wrapper");
if (entryIt != functions.end())
{
t.Equals(entryIt->end, 0x1018u,
"return-address entry should slice through the remainder of the caller");
}
auto decodedEntryIt = decodedFunctions.find(0x1008u);
t.IsTrue(decodedEntryIt != decodedFunctions.end(),
"decoded entry slice for the caller return address should exist");
if (decodedEntryIt != decodedFunctions.end())
{
t.Equals(decodedEntryIt->second.size(), static_cast<size_t>(4),
"return-address entry slice should keep the caller tail");
if (!decodedEntryIt->second.empty())
{
t.Equals(decodedEntryIt->second.front().address, 0x1008u,
"return-address entry slice should begin at the JAL fallthrough");
}
}
});
tc.Run("JAL to an already-known function still discovers the return entry", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("caller", 0x1000u, 0x1020u),
makeFunction("callee", 0x1100u, 0x1108u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeNopLike(0x1000u),
makeNopLike(0x1004u),
makeAbsJump(0x1008u, 0x1100u, OPCODE_JAL),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u),
makeJrRa(0x1018u),
makeNopLike(0x101Cu)
};
decodedFunctions[0x1100u] = {
makeJrRa(0x1100u),
makeNopLike(0x1104u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(1),
"return entry should still be discovered even when the JAL target is already registered");
auto entryIt = std::find_if(functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x1010u; });
t.IsTrue(entryIt != functions.end(),
"return address 0x1010 should be emitted as a resumable entry");
if (entryIt != functions.end())
{
t.Equals(entryIt->end, 0x1020u,
"return entry should cover the remaining caller tail");
}
});
tc.Run("discovery ignores synthetic entry wrappers", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("entry_1008", 0x1008u, 0x1020u),
makeFunction("callee", 0x1100u, 0x1108u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1008u] = {
makeAbsJump(0x1008u, 0x1100u, OPCODE_JAL),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u),
makeJrRa(0x1018u),
makeNopLike(0x101Cu)
};
decodedFunctions[0x1100u] = {
makeJrRa(0x1100u),
makeNopLike(0x1104u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(0),
"synthetic entry wrappers should not recursively produce more entries");
const bool hasRecursiveResumeEntry = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x1010u; });
t.IsFalse(hasRecursiveResumeEntry,
"discovery should not promote a return entry out of an existing entry wrapper");
});
tc.Run("entry reslice handles entries without containing function", [](TestCase &t) {
std::vector<Function> functions = {
makeFunction("entry_1008", 0x1008u, 0x1018u),
makeFunction("entry_100c", 0x100Cu, 0x1018u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1008u] = {
makeNopLike(0x1008u),
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u)
};
decodedFunctions[0x100Cu] = {
makeNopLike(0x100Cu),
makeNopLike(0x1010u),
makeNopLike(0x1014u)
};
size_t resliced = PS2Recompiler::ResliceEntryFunctions(functions, decodedFunctions);
t.Equals(resliced, static_cast<size_t>(1),
"expected only the earlier entry to be resliced");
auto findByStart = [&](uint32_t start) -> const Function* {
auto it = std::find_if(functions.begin(), functions.end(),
[&](const Function &fn) { return fn.start == start; });
if (it == functions.end())
{
return nullptr;
}
return &(*it);
};
const Function *entry1008 = findByStart(0x1008u);
const Function *entry100C = findByStart(0x100Cu);
t.IsNotNull(entry1008, "entry at 0x1008 should exist");
t.IsNotNull(entry100C, "entry at 0x100C should exist");
if (entry1008)
{
t.Equals(entry1008->end, 0x100Cu,
"entry 0x1008 should be trimmed to next entry start");
}
if (entry100C)
{
t.Equals(entry100C->end, 0x1018u,
"entry 0x100C should keep original end");
}
auto decoded1008It = decodedFunctions.find(0x1008u);
auto decoded100CIt = decodedFunctions.find(0x100Cu);
t.IsTrue(decoded1008It != decodedFunctions.end(), "decoded slice for 0x1008 should exist");
t.IsTrue(decoded100CIt != decodedFunctions.end(), "decoded slice for 0x100C should exist");
if (decoded1008It != decodedFunctions.end())
{
t.Equals(decoded1008It->second.size(), static_cast<size_t>(1),
"entry 0x1008 slice should stop before 0x100C");
}
if (decoded100CIt != decodedFunctions.end())
{
t.Equals(decoded100CIt->second.size(), static_cast<size_t>(3),
"entry 0x100C slice should keep remaining instructions");
}
});
tc.Run("non-executable section targets are ignored", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr},
{".data", 0x3000u, 0x1000u, 0u, false, true, false, false, nullptr}
};
std::vector<Function> functions = {
makeFunction("data_container", 0x3000u, 0x3010u),
makeFunction("caller", 0x1800u, 0x1810u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x3000u] = {
makeNopLike(0x3000u),
makeNopLike(0x3004u),
makeNopLike(0x3008u),
makeNopLike(0x300Cu)
};
decodedFunctions[0x1800u] = {
makeAbsJump(0x1800u, 0x3004u, OPCODE_J),
makeNopLike(0x1804u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(0),
"non-executable targets should not produce additional entries");
const bool hasDataEntry = std::any_of(functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x3004u; });
t.IsFalse(hasDataEntry, "target in data section must not produce entry wrapper");
});
tc.Run("entry starting at jr ra is capped to return thunk", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("container", 0x1000u, 0x1200u),
makeFunction("caller", 0x1300u, 0x1310u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeNopLike(0x1000u),
makeNopLike(0x1004u),
makeNopLike(0x1008u),
makeJrRa(0x10A0u),
makeNopLike(0x10A4u),
makeNopLike(0x10A8u),
makeNopLike(0x10ACu)
};
decodedFunctions[0x1300u] = {
makeAbsJump(0x1300u, 0x10A0u, OPCODE_J),
makeNopLike(0x1304u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(1),
"expected one additional entry from cross-function jump");
auto entryIt = std::find_if(functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x10A0u; });
t.IsTrue(entryIt != functions.end(), "entry wrapper at 0x10A0 should exist");
if (entryIt != functions.end())
{
t.Equals(entryIt->end, 0x10A8u,
"jr ra entry should end after delay slot, not at container end");
}
auto decodedEntryIt = decodedFunctions.find(0x10A0u);
t.IsTrue(decodedEntryIt != decodedFunctions.end(),
"decoded entry slice for 0x10A0 should exist");
if (decodedEntryIt != decodedFunctions.end())
{
t.Equals(decodedEntryIt->second.size(), static_cast<size_t>(2),
"jr ra entry slice should contain exactly jr+delay");
if (!decodedEntryIt->second.empty())
{
t.Equals(decodedEntryIt->second.front().address, 0x10A0u,
"entry slice should start at 0x10A0");
}
}
});
tc.Run("config manager parses jump_tables table entries", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path configPath =
std::filesystem::temp_directory_path() / ("ps2recomp-jump-table-" + uniqueSuffix + ".toml");
std::ofstream configFile(configPath);
t.IsTrue(static_cast<bool>(configFile), "temp config file should be writable");
if (!configFile)
{
return;
}
configFile << "[general]\n";
configFile << "input = \"dummy.elf\"\n";
configFile << "output = \"out\"\n\n";
configFile << "[jump_tables]\n";
configFile << "[[jump_tables.table]]\n";
configFile << "address = \"0x200000\"\n";
configFile << "base_register = 9\n";
configFile << "entries = [\n";
configFile << " { index = 0, target = \"0x1620\" },\n";
configFile << " { index = 1, target = \"0x1630\" },\n";
configFile << "]\n";
configFile.close();
ConfigManager manager(configPath.string());
RecompilerConfig config = manager.loadConfig();
t.Equals(config.jumpTables.size(), static_cast<size_t>(1),
"one configured jump table should be loaded");
if (!config.jumpTables.empty())
{
const JumpTable &table = config.jumpTables.front();
t.Equals(table.address, 0x200000u, "table address should parse from hex string");
t.Equals(table.baseRegister, 9u, "base register should parse");
t.Equals(table.entries.size(), static_cast<size_t>(2),
"two jump table entries should parse");
if (table.entries.size() >= 2)
{
t.Equals(table.entries[0].index, 0u, "first entry index should parse");
t.Equals(table.entries[0].target, 0x1620u, "first entry target should parse");
t.Equals(table.entries[1].index, 1u, "second entry index should parse");
t.Equals(table.entries[1].target, 0x1630u, "second entry target should parse");
}
}
std::error_code removeError;
std::filesystem::remove(configPath, removeError);
});
tc.Run("config manager loads modern and legacy guest entry hints", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path configPath =
std::filesystem::temp_directory_path() / ("ps2recomp-entry-hints-" + uniqueSuffix + ".toml");
std::ofstream configFile(configPath);
t.IsTrue(static_cast<bool>(configFile), "temp config file should be writable");
if (!configFile)
{
return;
}
configFile << "[general]\n";
configFile << "input = \"dummy.elf\"\n";
configFile << "output = \"out\"\n";
configFile << "entry_points = [\"callback@0x7008\"]\n";
configFile << "untracked_stubs = [\"legacy_callback@0x7018\"]\n";
configFile.close();
ConfigManager manager(configPath.string());
const RecompilerConfig config = manager.loadConfig();
t.Equals(config.entryPointHints.size(), static_cast<size_t>(2),
"modern and legacy entry metadata should be merged");
t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(),
"callback@0x7008") != config.entryPointHints.end(),
"modern entry_points metadata should load");
t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(),
"legacy_callback@0x7018") != config.entryPointHints.end(),
"legacy untracked_stubs metadata should remain compatible");
std::error_code removeError;
std::filesystem::remove(configPath, removeError);
});
tc.Run("elf parser ignores STT_FUNC symbols in non-executable sections", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-parser-" + uniqueSuffix + ".elf");
const bool writeOk = writeMinimalMipsElfWithCodeAndDataFunctionSymbols(elfPath);
t.IsTrue(writeOk, "temporary ELF should be generated");
if (!writeOk)
{
return;
}
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "generated ELF should parse");
if (!parseOk)
{
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
return;
}
const auto functions = parser.extractFunctions();
const bool hasCodeFunction = std::any_of(functions.begin(), functions.end(),
[](const Function &fn)
{ return fn.start == 0x00100000u; });
const bool hasDataFunction = std::any_of(functions.begin(), functions.end(),
[](const Function &fn)
{ return fn.start == 0x00200000u; });
t.IsTrue(hasCodeFunction, "function in executable section should be retained");
t.IsFalse(hasDataFunction, "STT_FUNC symbol in .data must be ignored");
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
});
tc.Run("ghidra map replaces JAL fallback-only auto starts", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-ghidra-merge-" + uniqueSuffix + ".elf");
const std::filesystem::path mapPath =
std::filesystem::temp_directory_path() / ("ps2recomp-ghidra-merge-" + uniqueSuffix + ".csv");
const bool writeOk = writeMinimalMipsElfWithJalFallbackTarget(elfPath);
t.IsTrue(writeOk, "temporary ELF should be generated");
if (!writeOk)
{
return;
}
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "generated ELF should parse");
if (!parseOk)
{
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
return;
}
const auto fallbackExtras = parser.extractExtraFunctions();
const bool hasFallbackStart = std::any_of(
fallbackExtras.begin(), fallbackExtras.end(),
[](const Function &fn)
{ return fn.start == 0x00100010u; });
t.IsTrue(hasFallbackStart, "JAL fallback should discover secondary start before map load");
std::ofstream mapFile(mapPath);
t.IsTrue(static_cast<bool>(mapFile), "ghidra map file should be writable");
if (!mapFile)
{
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
return;
}
mapFile << "name,start,end,size\n";
mapFile << "FUN_00100000,0x00100000,0x00100010,0x10\n";
mapFile.close();
const bool mapLoaded = parser.loadGhidraFunctionMap(mapPath.string());
t.IsTrue(mapLoaded, "ghidra map should load");
const auto functions = parser.extractFunctions();
const auto entryIt = std::find_if(
functions.begin(), functions.end(),
[](const Function &fn)
{ return fn.start == 0x00100000u; });
t.IsTrue(entryIt != functions.end(), "ghidra entry should exist");
if (entryIt != functions.end())
{
t.Equals(entryIt->name, std::string("FUN_00100000"),
"ghidra name should win over fallback auto-name");
}
const bool stillHasFallbackOnlyStart = std::any_of(
functions.begin(), functions.end(),
[](const Function &fn)
{ return fn.start == 0x00100010u; });
t.IsFalse(stillHasFallbackOnlyStart,
"fallback-only function starts should be removed once ghidra map is loaded");
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
std::filesystem::remove(mapPath, removeError);
});
tc.Run("elf parser discovers address-taken callbacks in stripped ELFs", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-address-taken-" + uniqueSuffix + ".elf");
const bool writeOk = writeMinimalMipsElfWithAddressTakenCallbacks(elfPath);
t.IsTrue(writeOk, "temporary stripped ELF should be generated");
if (!writeOk)
{
return;
}
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "generated ELF should parse");
if (!parseOk)
{
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
return;
}
const auto functions = parser.extractFunctions();
auto hasStart = [&functions](uint32_t start)
{
return std::any_of(functions.begin(), functions.end(),
[start](const Function &function)
{ return function.start == start; });
};
t.IsTrue(hasStart(0x00100040u),
"LUI plus delay-slot ADDIU should discover the callback entry");
t.IsTrue(hasStart(0x00100060u),
"clustered rodata pointers should discover the first leaf callback");
t.IsTrue(hasStart(0x00100068u),
"clustered rodata pointers should discover the second leaf callback");
t.IsFalse(hasStart(0x00100070u),
"an isolated data pointer or non-callback code materialization must not become a function");
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
});
tc.Run("runtime call resolution includes Veronica compatibility aliases", [](TestCase &t) {
t.Equals(ps2_runtime_calls::resolveSyscallName("ReleaseAlarm"), std::string_view{"ReleaseAlarm"},
"ReleaseAlarm should resolve as a syscall name");
t.Equals(ps2_runtime_calls::resolveSyscallName("_ReleaseAlarm"), std::string_view{"ReleaseAlarm"},
"underscore ReleaseAlarm alias should resolve to ReleaseAlarm");
t.Equals(ps2_runtime_calls::resolveSyscallName("EnableCache"), std::string_view{"EnableCache"},
"EnableCache should resolve as a syscall name");
t.Equals(ps2_runtime_calls::resolveSyscallName("DisableCache"), std::string_view{"DisableCache"},
"DisableCache should resolve as a syscall name");
t.Equals(ps2_runtime_calls::resolveStubName("isceSifSetDma"), std::string_view{"isceSifSetDma"},
"isceSifSetDma should resolve as a stub name");
t.Equals(ps2_runtime_calls::resolveStubName("isceSifSetDChain"), std::string_view{"isceSifSetDChain"},
"isceSifSetDChain should resolve as a stub name");
t.Equals(ps2_runtime_calls::resolveStubName("memalign"), std::string_view{"memalign"},
"memalign should resolve as a stub name");
t.Equals(ps2_runtime_calls::resolveStubName("_memalign_r"), std::string_view{"memalign_r"},
"_memalign_r should resolve to the memalign_r stub");
t.Equals(ps2_runtime_calls::resolveStubName("_realloc_r"), std::string_view{"realloc_r"},
"_realloc_r should resolve to the realloc_r stub");
t.Equals(ps2_runtime_calls::resolveStubName("malloc_extend_top"), std::string_view{"malloc_extend_top"},
"malloc_extend_top should resolve as an allocator compatibility stub");
t.Equals(ps2_runtime_calls::resolveStubName("__malloc_lock"), std::string_view{"__malloc_lock"},
"__malloc_lock should resolve as an allocator compatibility stub");
t.Equals(ps2_runtime_calls::resolveStubName("__malloc_unlock"), std::string_view{"__malloc_unlock"},
"__malloc_unlock should resolve as an allocator compatibility stub");
t.Equals(ps2_runtime_calls::resolveStubName("memclr"), std::string_view{"memclr"},
"memclr should resolve as a runtime stub");
t.Equals(ps2_runtime_calls::resolveStubName("__divdi3"), std::string_view{"__divdi3"},
"__divdi3 should resolve as a runtime stub");
t.Equals(ps2_runtime_calls::resolveStubName("__mcmp"), std::string_view{},
"__mcmp should be left for recompilation");
t.Equals(ps2_runtime_calls::resolveStubName("__sprint"), std::string_view{},
"__sprint should be left for recompilation");
t.Equals(ps2_runtime_calls::resolveStubName("__sprint_r"), std::string_view{},
"__sprint_r should be left for recompilation");
t.Equals(ps2_runtime_calls::resolveStubName("__sbprintf"), std::string_view{},
"__sbprintf should be left for recompilation");
});
tc.Run("initializer skips fall back to guest recompilation", [](TestCase &t) {
const std::string uniqueSuffix =
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const std::filesystem::path tempRoot =
std::filesystem::temp_directory_path() / ("ps2recomp-initializer-" + uniqueSuffix);
const std::filesystem::path elfPath = tempRoot / "initializer.elf";
const std::filesystem::path configPath = tempRoot / "initializer.toml";
const std::filesystem::path outputPath = tempRoot / "output";
std::filesystem::create_directories(tempRoot);
const bool elfWritten =
writeMinimalMipsElfWithInitializer(elfPath, "__sinit_test.cpp", 0x00100000u);
const bool configWritten =
writeRecompilerTestConfig(configPath, elfPath, outputPath, {"__sinit_test.cpp"});
t.IsTrue(elfWritten && configWritten,
"initializer regression inputs should be generated");
if (elfWritten && configWritten)
{
PS2Recompiler recompiler(configPath.string());
t.IsTrue(recompiler.initialize(),
"initializer regression config should initialize");
t.IsTrue(recompiler.recompile(),
"a decodable skipped initializer should use guest fallback");
const RecompilerReporter::Counters &counters = recompiler.reportCounters();
t.Equals(counters.correctnessCriticalGuestFallbacks, static_cast<size_t>(1u),
"the ignored initializer skip should be reported");
t.Equals(counters.correctnessCriticalFailures, static_cast<size_t>(0u),
"guest fallback should avoid a correctness-critical failure");
t.Equals(counters.functionsSkipped, static_cast<size_t>(0u),
"the initializer should not remain skipped");
t.Equals(counters.functionsRecompiled, static_cast<size_t>(1u),
"the original initializer body should be recompiled");
}
std::error_code removeError;
std::filesystem::remove_all(tempRoot, removeError);
});
tc.Run("missing constructor-table targets fail recompilation", [](TestCase &t) {
const std::string uniqueSuffix =
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const std::filesystem::path tempRoot =
std::filesystem::temp_directory_path() / ("ps2recomp-missing-initializer-" + uniqueSuffix);
const std::filesystem::path elfPath = tempRoot / "initializer.elf";
const std::filesystem::path configPath = tempRoot / "initializer.toml";
const std::filesystem::path outputPath = tempRoot / "output";
std::filesystem::create_directories(tempRoot);
const bool elfWritten =
writeMinimalMipsElfWithInitializer(elfPath, "ordinary_entry", 0x00100040u);
const bool configWritten =
writeRecompilerTestConfig(configPath, elfPath, outputPath, {});
t.IsTrue(elfWritten && configWritten,
"missing-initializer regression inputs should be generated");
if (elfWritten && configWritten)
{
{
PS2Recompiler recompiler(configPath.string());
t.IsTrue(recompiler.initialize(),
"missing-initializer regression config should initialize");
t.IsFalse(recompiler.recompile(),
"an unresolved .ctors target should be correctness-fatal");
t.Equals(recompiler.reportCounters().correctnessCriticalFailures,
static_cast<size_t>(1u),
"the unresolved constructor target should appear in the report");
}
const bool overrideWritten =
writeRecompilerTestConfig(
configPath, elfPath, outputPath, {},
{"memclr@0x00100040"});
t.IsTrue(overrideWritten,
"manual initializer override config should be generated");
if (overrideWritten)
{
PS2Recompiler overridden(configPath.string());
t.IsTrue(overridden.initialize(),
"manual initializer override should initialize");
t.IsTrue(overridden.recompile(),
"a resolved address-bound handler should satisfy the constructor target");
t.Equals(overridden.reportCounters().functionsStubbed,
static_cast<size_t>(1u),
"the resolved manual initializer should be emitted as a stub binding");
}
}
std::error_code removeError;
std::filesystem::remove_all(tempRoot, removeError);
});
tc.Run("respect max length for .cpp filenames", [](TestCase& t) {
t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678",".cpp",50).length() <= 50,"Function name must be max 50 characters");
t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678", ".cpp", 50).rfind("0x12345678") != std::string::npos, "Function name must mantain the function address at the end, if present");
});
});
}