mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 16:59:35 -04:00
f49ca4edbc
* feat: implement fix and changes based on dark cloud report fix: fix GS AFAIL for RGB/alpha/Z, ZMSK fix: fix VU1 flags mask and pipeline fix: small VU1 cache fix feat: __ct__, __sinit_ are not sillent stubs anymore * feat: fix song JP pulling * feat: sound update for lotR * feat: prevent guest execution to be very slow * fix: small gs size bug * feat: refactor VU fix: fix cliping and other issues on gs fix: fix wrong vu0 register on recompiler * fix fix ACC scheduler stall feat: remove unused test fix: .fix overflow e underflow on FMAC * feat: small setting for windows test
1090 lines
50 KiB
C++
1090 lines
50 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 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("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("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");
|
|
|
|
});
|
|
});
|
|
}
|