feat: IOP emulator

refactor: codegen to catch callbacks on mips code
feat: added a lot of entries or IOP emulator
This commit is contained in:
Ran-j
2026-08-19 16:53:00 -03:00
parent a6739395b3
commit a293fa433a
53 changed files with 8068 additions and 155 deletions
+22
View File
@@ -74,6 +74,27 @@ namespace ps2recomp
config.stubImplementations = toml::find<std::vector<std::string>>(data, "stubs");
}
auto appendEntryPointHints = [&](const toml::value &table, const char *key)
{
if (!table.contains(key) || !table.at(key).is_array())
{
return;
}
const auto values = toml::find<std::vector<std::string>>(table, key);
config.entryPointHints.insert(
config.entryPointHints.end(), values.begin(), values.end());
};
appendEntryPointHints(general, "entry_points");
appendEntryPointHints(data, "entry_points");
// Backward compatibility
appendEntryPointHints(general, "untracked_stubs");
appendEntryPointHints(data, "untracked_stubs");
std::sort(config.entryPointHints.begin(), config.entryPointHints.end());
config.entryPointHints.erase(
std::unique(config.entryPointHints.begin(), config.entryPointHints.end()),
config.entryPointHints.end());
if (general.contains("skip") && general.at("skip").is_array())
{
config.skipFunctions = toml::find<std::vector<std::string>>(general, "skip");
@@ -276,6 +297,7 @@ namespace ps2recomp
general["patch_cache"] = config.patchCache;
general["skip"] = config.skipFunctions;
general["stubs"] = config.stubImplementations;
general["entry_points"] = config.entryPointHints;
data["general"] = general;
if (!config.mmioByInstructionAddress.empty())
@@ -135,6 +135,11 @@ namespace ps2recomp
for (const auto &inst : instructions)
{
if (inst.opcode == OPCODE_SPECIAL && inst.function == SPECIAL_SYSCALL)
{
queueResumeEntryTarget(inst.address + 4u);
}
bool isStaticJump = (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL);
if (inst.isBranch && inst.opcode != OPCODE_J && inst.opcode != OPCODE_JAL)
{
+339 -9
View File
@@ -1,4 +1,5 @@
#include "ps2recomp/elf_parser.h"
#include "ps2recomp/instructions.h"
#include "ps2recomp/recompiler_reporter.h"
#include "ps2recomp/types.h"
#include <iostream>
@@ -116,6 +117,8 @@ namespace
namespace
{
using namespace ps2recomp;
bool HasDwarfSections(const ELFIO::elfio &elf)
{
for (ELFIO::Elf_Half i = 0; i < elf.sections.size(); ++i)
@@ -453,7 +456,328 @@ namespace
}
}
void ScanJalTargetsFallback(ps2recomp::ElfParser *parser, std::vector<ps2recomp::Function> &outFunctions)
bool ReadSectionWord(const ps2recomp::Section &section, uint32_t offset, uint32_t &outWord)
{
if (!section.data || offset > section.size || section.size - offset < sizeof(uint32_t))
{
return false;
}
std::memcpy(&outWord, section.data + offset, sizeof(uint32_t));
return true;
}
bool LooksLikeCallableEntry(const std::vector<ps2recomp::Section> &sections, uint32_t address, bool allowLeafThunk)
{
if ((address % MIPS_INSTRUCTION_SIZE) != 0)
{
return false;
}
const ps2recomp::Section *section = FindCodeSectionByAddress(sections, address);
if (!section || !section->data)
{
return false;
}
const uint32_t startOffset = address - section->address;
constexpr uint32_t kProbeWords = 8;
for (uint32_t index = 0; index < kProbeWords; ++index)
{
uint32_t raw = 0;
if (!ReadSectionWord(*section, startOffset + (index * MIPS_INSTRUCTION_SIZE), raw))
{
break;
}
const uint32_t opcode = OPCODE(raw);
const uint32_t rs = RS(raw);
const uint32_t rt = RT(raw);
const uint16_t immediate = static_cast<uint16_t>(IMMEDIATE(raw));
// Non-leaf functions normally allocate their stack frame immediately.
// Accept ADDIU/DADDIU $sp,$sp,-N in the first few instructions.
if (index < 4 &&
(opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU) &&
rs == GPR_SP && rt == GPR_SP &&
(immediate & MIPS_IMMEDIATE_SIGN_BIT) != 0)
{
return true;
}
// Some prologues set up GP before saving RA, so also recognize the
// common SW/SD/SQ $ra,offset($sp) forms in the entry window.
if ((opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) &&
rs == GPR_SP && rt == GPR_RA)
{
return true;
}
// Leaf callbacks and vtable thunks often have no stack frame at all.
if (allowLeafThunk &&
opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JR && rs == GPR_RA)
{
return true;
}
}
return false;
}
bool WritesGpr(uint32_t raw, uint32_t reg)
{
if (reg == GPR_ZERO)
{
return false;
}
const uint32_t opcode = OPCODE(raw);
const uint32_t rt = RT(raw);
const uint32_t rd = RD(raw);
if (opcode == OPCODE_SPECIAL || opcode == OPCODE_MMI)
{
return rd == reg;
}
if (opcode == OPCODE_JAL)
{
return reg == GPR_RA;
}
bool writesRt = false;
switch (opcode)
{
case OPCODE_ADDI:
case OPCODE_ADDIU:
case OPCODE_SLTI:
case OPCODE_SLTIU:
case OPCODE_ANDI:
case OPCODE_ORI:
case OPCODE_XORI:
case OPCODE_LUI:
case OPCODE_DADDI:
case OPCODE_DADDIU:
case OPCODE_LDL:
case OPCODE_LDR:
case OPCODE_LQ:
case OPCODE_LB:
case OPCODE_LH:
case OPCODE_LWL:
case OPCODE_LW:
case OPCODE_LBU:
case OPCODE_LHU:
case OPCODE_LWR:
case OPCODE_LWU:
case OPCODE_LL:
case OPCODE_LLD:
case OPCODE_LD:
writesRt = true;
break;
default:
break;
}
return writesRt && rt == reg;
}
bool IsControlTransfer(uint32_t raw)
{
const uint32_t opcode = OPCODE(raw);
switch (opcode)
{
case OPCODE_REGIMM:
case OPCODE_J:
case OPCODE_JAL:
case OPCODE_BEQ:
case OPCODE_BNE:
case OPCODE_BLEZ:
case OPCODE_BGTZ:
case OPCODE_BEQL:
case OPCODE_BNEL:
case OPCODE_BLEZL:
case OPCODE_BGTZL:
return true;
default:
break;
}
if (opcode != OPCODE_SPECIAL)
{
return false;
}
const uint32_t function = FUNCTION(raw);
return function == SPECIAL_JR || function == SPECIAL_JALR;
}
bool IsCallInstruction(uint32_t raw)
{
const uint32_t opcode = OPCODE(raw);
return opcode == OPCODE_JAL ||
(opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JALR);
}
void ScanMaterializedCodeAddresses(const std::vector<ps2recomp::Section> &sections,
std::unordered_set<uint32_t> &starts)
{
constexpr uint32_t kMaxLookaheadWords = 4;
for (const auto &section : sections)
{
if (!section.isCode || !section.data || section.size < (2u * MIPS_INSTRUCTION_SIZE))
{
continue;
}
for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size;
offset += MIPS_INSTRUCTION_SIZE)
{
uint32_t upperRaw = 0;
if (!ReadSectionWord(section, offset, upperRaw) ||
OPCODE(upperRaw) != OPCODE_LUI)
{
continue;
}
const uint32_t upperReg = RT(upperRaw);
if (upperReg == GPR_ZERO)
{
continue;
}
const uint32_t upperValue = IMMEDIATE(upperRaw) << 16;
bool sawControlTransfer = false;
bool sawCallTransfer = false;
for (uint32_t lookahead = 1; lookahead <= kMaxLookaheadWords; ++lookahead)
{
uint32_t lowRaw = 0;
if (!ReadSectionWord(section, offset + (lookahead * MIPS_INSTRUCTION_SIZE), lowRaw))
{
break;
}
const uint32_t opcode = OPCODE(lowRaw);
const uint32_t rs = RS(lowRaw);
const uint32_t rt = RT(lowRaw);
if ((opcode == OPCODE_ADDIU || opcode == OPCODE_ORI || opcode == OPCODE_DADDIU) &&
rs == upperReg)
{
const uint16_t immediate = static_cast<uint16_t>(IMMEDIATE(lowRaw));
uint32_t target = 0;
if (opcode == OPCODE_ORI)
{
target = upperValue | static_cast<uint32_t>(immediate);
}
else // ADDIU/DADDIU use a signed low half
{
target = upperValue + static_cast<uint32_t>(
static_cast<int32_t>(static_cast<int16_t>(immediate)));
}
uint32_t nextRaw = 0;
const bool followedByCall =
ReadSectionWord(section,
offset + ((lookahead + 1u) * MIPS_INSTRUCTION_SIZE),
nextRaw) &&
IsCallInstruction(nextRaw);
const bool materializedAsCallArgument =
rt >= GPR_A0 && rt <= GPR_A3 && (sawCallTransfer || followedByCall);
if (LooksLikeCallableEntry(sections, target, materializedAsCallArgument))
{
starts.insert(target);
}
break;
}
// The instruction immediately after a branch/call is its
// delay slot. It may complete a callback address, but no
// later instruction is in the same straight-line state.
if (sawControlTransfer)
{
break;
}
if (WritesGpr(lowRaw, upperReg))
{
break;
}
if (IsControlTransfer(lowRaw))
{
sawControlTransfer = true;
sawCallTransfer = IsCallInstruction(lowRaw);
}
}
}
}
}
bool IsDedicatedFunctionPointerSection(const std::string &name)
{
return name == ".ctors" || name == ".dtors" ||
name == ".init_array" || name == ".fini_array";
}
void ScanDataFunctionPointerTables(const std::vector<ps2recomp::Section> &sections,
std::unordered_set<uint32_t> &starts)
{
struct PointerCandidate
{
uint32_t sourceOffset;
uint32_t target;
};
constexpr uint32_t kClusterDistanceBytes = 32;
for (const auto &section : sections)
{
if (!section.isData || section.isCode || section.isBSS ||
!section.data || section.size < MIPS_INSTRUCTION_SIZE)
{
continue;
}
std::vector<PointerCandidate> candidates;
for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size;
offset += MIPS_INSTRUCTION_SIZE)
{
uint32_t target = 0;
if (ReadSectionWord(section, offset, target) &&
LooksLikeCallableEntry(sections, target, true))
{
candidates.push_back({offset, target});
}
}
const bool dedicatedPointerSection = IsDedicatedFunctionPointerSection(section.name);
for (size_t index = 0; index < candidates.size(); ++index)
{
bool clustered = dedicatedPointerSection;
if (index > 0 &&
candidates[index].sourceOffset - candidates[index - 1].sourceOffset <= kClusterDistanceBytes)
{
clustered = true;
}
if (index + 1 < candidates.size() &&
candidates[index + 1].sourceOffset - candidates[index].sourceOffset <= kClusterDistanceBytes)
{
clustered = true;
}
if (clustered)
{
starts.insert(candidates[index].target);
}
}
}
}
void ScanFunctionStartsFallback(ps2recomp::ElfParser *parser, std::vector<ps2recomp::Function> &outFunctions)
{
std::unordered_set<uint32_t> starts;
starts.reserve(4096);
@@ -467,26 +791,29 @@ namespace
const auto &sections = parser->getSections();
for (const auto &section : sections)
{
if (!section.isCode || !section.data || section.size < 4)
if (!section.isCode || !section.data || section.size < MIPS_INSTRUCTION_SIZE)
{
continue;
}
for (uint32_t offset = 0; offset + 4 <= section.size; offset += 4)
for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size;
offset += MIPS_INSTRUCTION_SIZE)
{
const uint32_t pc = section.address + offset;
uint32_t raw = 0;
std::memcpy(&raw, section.data + offset, sizeof(uint32_t));
const uint32_t op = (raw >> 26) & 0x3F;
if (op != 0x03) // JAL
const uint32_t op = OPCODE(raw);
if (op != OPCODE_JAL)
{
continue;
}
const uint32_t index = raw & 0x03FFFFFF;
const uint32_t target = ((pc + 4) & 0xF0000000u) | (index << 2);
const uint32_t index = TARGET(raw);
const uint32_t target =
((pc + MIPS_INSTRUCTION_SIZE) & MIPS_JUMP_REGION_MASK) |
(index << MIPS_JUMP_TARGET_SHIFT);
if (FindCodeSectionByAddress(sections, target))
{
@@ -494,6 +821,9 @@ namespace
}
}
}
ScanMaterializedCodeAddresses(sections, starts);
ScanDataFunctionPointerTables(sections, starts);
std::vector<uint32_t> sortedStarts(starts.begin(), starts.end());
std::sort(sortedStarts.begin(), sortedStarts.end());
@@ -523,7 +853,7 @@ namespace
ps2recomp::Function func{};
func.name = MakeAutoFunctionName(start);
func.start = start;
func.end = (end > start) ? end : (start + 4);
func.end = (end > start) ? end : (start + MIPS_INSTRUCTION_SIZE);
func.isRecompiled = false;
func.isStub = false;
func.isSkipped = false;
@@ -1420,7 +1750,7 @@ namespace ps2recomp
if (m_extraFunctions.empty())
{
ScanJalTargetsFallback(this, m_extraFunctions);
ScanFunctionStartsFallback(this, m_extraFunctions);
}
std::sort(m_extraFunctions.begin(), m_extraFunctions.end(),
+2 -2
View File
@@ -45,8 +45,8 @@ namespace ps2recomp
ss << "#include <stdexcept>\n";
ss << "#include \"ps2_runtime_macros.h\"\n";
ss << "#include \"ps2_runtime.h\"\n";
ss << "#include \"ps2_recompiled_functions.h\"\n";
ss << "#include \"ps2_recompiled_stubs.h\"\n\n";
ss << "#include <ps2_recompiled_functions.h>\n";
ss << "#include <ps2_recompiled_stubs.h>\n\n";
ss << "#include \"ps2_syscalls.h\"\n";
ss << "#include \"ps2_stubs.h\"\n\n";
ss << "#ifdef PS2_FUNCTION_LOG_TRACKER\n";
+112 -12
View File
@@ -102,10 +102,10 @@ namespace ps2recomp
void writeCombinedOutputPreamble(std::ostream &output)
{
output << "#include <stdexcept>\n";
output << "#include \"ps2_recompiled_functions.h\"\n\n";
output << "#include <ps2_recompiled_functions.h>\n\n";
output << "#include \"ps2_runtime_macros.h\"\n";
output << "#include \"ps2_runtime.h\"\n";
output << "#include \"ps2_recompiled_stubs.h\"\n";
output << "#include <ps2_recompiled_stubs.h>\n";
output << "#include \"ps2_syscalls.h\"\n";
output << "#include \"ps2_stubs.h\"\n";
output << "#ifdef _DEBUG\n";
@@ -725,6 +725,71 @@ namespace ps2recomp
return reslicedCount;
}
size_t collectInternalEntryTargetsImpl(
const std::vector<Function> &functions,
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
const std::unordered_set<uint32_t> &entryAddresses,
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner)
{
std::unordered_set<uint32_t> functionStarts;
functionStarts.reserve(functions.size());
for (const auto &function : functions)
{
functionStarts.insert(function.start);
}
size_t addedCount = 0u;
for (uint32_t entryAddress : entryAddresses)
{
if (functionStarts.contains(entryAddress))
{
continue;
}
const Function *owner = nullptr;
for (const auto &function : functions)
{
if (!function.isRecompiled || function.isStub || function.isSkipped ||
entryAddress <= function.start || entryAddress >= function.end)
{
continue;
}
const auto decodedIt = decodedFunctions.find(function.start);
if (decodedIt == decodedFunctions.end())
{
continue;
}
const bool containsInstruction = std::any_of(decodedIt->second.begin(), decodedIt->second.end(), [entryAddress](const Instruction &instruction)
{ return instruction.address == entryAddress; });
if (!containsInstruction)
{
continue;
}
if (!owner || function.start > owner->start)
{
owner = &function;
}
}
if (!owner)
{
continue;
}
auto &targets = targetsByOwner[owner->start];
if (std::find(targets.begin(), targets.end(), entryAddress) == targets.end())
{
targets.push_back(entryAddress);
++addedCount;
}
}
return addedCount;
}
}
PS2Recompiler::PS2Recompiler(const std::string &configPath)
@@ -751,6 +816,7 @@ namespace ps2recomp
m_stubFunctions.clear();
m_stubFunctionStarts.clear();
m_stubHandlerBindingsByStart.clear();
m_entryPointHintStarts.clear();
m_correctnessCriticalFunctionStarts.clear();
for (const auto &name : m_config.skipFunctions)
@@ -792,6 +858,14 @@ namespace ps2recomp
}
}
}
for (const auto &hint : m_config.entryPointHints)
{
const FunctionSelector selector = parseFunctionSelector(hint);
if (selector.start.has_value())
{
m_entryPointHintStarts.insert(*selector.start);
}
}
m_reporter.progress("parsing ELF");
m_elfParser = std::make_unique<ElfParser>(m_config.inputPath);
@@ -983,7 +1057,7 @@ namespace ps2recomp
if (isStubFunction(function))
{
if (!correctnessCritical || hasResolvedStubHandler(function))
if (hasResolvedStubHandler(function))
{
function.isStub = true;
function.isSkipped = false;
@@ -991,12 +1065,15 @@ namespace ps2recomp
continue;
}
m_reporter.recordCorrectnessCriticalGuestFallback();
if (correctnessCritical)
{
m_reporter.recordCorrectnessCriticalGuestFallback();
}
m_reporter.warningAt(
"correctness-critical",
"stub",
function.name,
function.start,
"Unresolved initializer stub ignored; recompiling the original guest function");
"Configured stub has no runtime handler; recompiling the original guest function");
}
if (shouldSkipFunction(function))
@@ -1882,6 +1959,22 @@ namespace ps2recomp
targets.push_back(target);
}
}
std::unordered_set<uint32_t> guestFallbackEntryAddresses = m_entryPointHintStarts;
for (uint32_t address : m_stubFunctionStarts)
{
const auto bindingIt = m_stubHandlerBindingsByStart.find(address);
if (bindingIt == m_stubHandlerBindingsByStart.end() ||
resolveStubTarget(bindingIt->second) == StubTarget::Unknown)
{
guestFallbackEntryAddresses.insert(address);
}
}
collectInternalEntryTargetsImpl(
m_functions,
m_decodedFunctions,
guestFallbackEntryAddresses,
m_resumeEntryTargetsByOwner);
size_t totalTargets = 0u;
for (auto it = m_resumeEntryTargetsByOwner.begin(); it != m_resumeEntryTargetsByOwner.end();)
@@ -2152,12 +2245,12 @@ namespace ps2recomp
return outputPath;
}
std::string PS2Recompiler::clampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
std::string PS2Recompiler::clampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength)
{
if (maxLength == 0)
{
// Keep this static helper side-effect free; callers validate arguments.
//Better go over the limit than create files with an empty path
// Better go over the limit than create files with an empty path
return baseName + extension;
}
@@ -2224,13 +2317,20 @@ namespace ps2recomp
return stats.discoveredCount;
}
size_t PS2Recompiler::ResliceEntryFunctions(
std::vector<Function> &functions,
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
size_t PS2Recompiler::ResliceEntryFunctions(std::vector<Function> &functions, std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
{
return resliceEntryFunctionsImpl(functions, decodedFunctions);
}
size_t PS2Recompiler::CollectInternalEntryTargets(
const std::vector<Function> &functions,
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
const std::unordered_set<uint32_t> &entryAddresses,
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner)
{
return collectInternalEntryTargetsImpl(functions, decodedFunctions, entryAddresses, targetsByOwner);
}
StubTarget PS2Recompiler::resolveStubTarget(const std::string &name)
{
if (!ps2_runtime_calls::resolveSyscallName(name).empty())
@@ -2244,7 +2344,7 @@ namespace ps2recomp
return StubTarget::Unknown;
}
std::string PS2Recompiler::ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
std::string PS2Recompiler::ClampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength)
{
return clampFilenameLength(baseName, extension, maxLength);
}