#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 #include #include #include #include #include #include #include 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(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(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 textWords = { 0x0C040004u, // jal 0x00100010 0x00000000u, // nop 0x03E00008u, // jr $ra 0x00000000u, // nop 0x03E00008u, // jr $ra 0x00000000u // nop }; text->set_data(reinterpret_cast(textWords.data()), static_cast(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 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(textWords.data()), static_cast(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 tableWords{}; tableWords[1] = 0x00100060u; tableWords[3] = 0x00100068u; tableWords[16] = 0x00100070u; // plausible entry, but not part of a pointer cluster rodata->set_data(reinterpret_cast(tableWords.data()), static_cast(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 textWords = { 0x03E00008u, // jr $ra 0x00000000u, // nop }; text->set_data(reinterpret_cast(textWords.data()), static_cast(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(&initializerTarget), static_cast(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 &skip, const std::vector &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(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
sections = { {".text", 0x1000u, 0x3000u, 0u, true, false, false, true, nullptr} }; std::vector functions = { makeFunction("container", 0x1000u, 0x1018u), makeFunction("caller", 0x2000u, 0x2010u) }; std::unordered_map> 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(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(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(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 functions = { makeFunction("container", 0x1000u, 0x1018u), makeFunction("entry_1008", 0x1008u, 0x1018u), makeFunction("entry_100c", 0x100Cu, 0x1018u) }; std::unordered_map> 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(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(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(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
sections = { {".text", 0x1000u, 0x40u, 0u, true, false, false, true, nullptr} }; std::vector functions = { makeFunction("container", 0x1000u, 0x101Cu) }; std::unordered_map> 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(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
sections = { {".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr} }; std::vector functions = { makeFunction("caller", 0x1000u, 0x1018u), makeFunction("callee", 0x2000u, 0x2008u) }; std::unordered_map> 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(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(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
sections = { {".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr} }; std::vector functions = { makeFunction("caller", 0x1000u, 0x1020u), makeFunction("callee", 0x1100u, 0x1108u) }; std::unordered_map> 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(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
sections = { {".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr} }; std::vector functions = { makeFunction("entry_1008", 0x1008u, 0x1020u), makeFunction("callee", 0x1100u, 0x1108u) }; std::unordered_map> 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(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 functions = { makeFunction("entry_1008", 0x1008u, 0x1018u), makeFunction("entry_100c", 0x100Cu, 0x1018u) }; std::unordered_map> 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(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(1), "entry 0x1008 slice should stop before 0x100C"); } if (decoded100CIt != decodedFunctions.end()) { t.Equals(decoded100CIt->second.size(), static_cast(3), "entry 0x100C slice should keep remaining instructions"); } }); tc.Run("non-executable section targets are ignored", [](TestCase &t) { std::vector
sections = { {".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr}, {".data", 0x3000u, 0x1000u, 0u, false, true, false, false, nullptr} }; std::vector functions = { makeFunction("data_container", 0x3000u, 0x3010u), makeFunction("caller", 0x1800u, 0x1810u) }; std::unordered_map> 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(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
sections = { {".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr} }; std::vector functions = { makeFunction("container", 0x1000u, 0x1200u), makeFunction("caller", 0x1300u, 0x1310u) }; std::unordered_map> 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(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(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(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(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(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(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(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(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(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(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(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(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(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(1u), "the ignored initializer skip should be reported"); t.Equals(counters.correctnessCriticalFailures, static_cast(0u), "guest fallback should avoid a correctness-critical failure"); t.Equals(counters.functionsSkipped, static_cast(0u), "the initializer should not remain skipped"); t.Equals(counters.functionsRecompiled, static_cast(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(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(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"); }); }); }