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
+99
View File
@@ -157,6 +157,29 @@ void register_code_generator_tests()
{
MiniTest::Case("CodeGenerator", [](TestCase &tc)
{
tc.Run("Generated sources cannot be shadowed by stale local declaration headers", [](TestCase &t) {
Function func;
func.name = "header_lookup";
func.start = 0x8F00;
func.end = 0x8F04;
func.isRecompiled = true;
CodeGenerator gen({}, {});
gen.setRenamedFunctions({{func.start, "header_lookup_0x8f00"}});
const std::string generated = gen.generateFunction(func, {makeNop(func.start)}, true);
const std::string registration = gen.generateFunctionRegistration({func}, {});
t.IsTrue(generated.find("#include <ps2_recompiled_functions.h>") != std::string::npos,
"function sources must resolve declarations through the configured include path");
t.IsTrue(generated.find("#include <ps2_recompiled_stubs.h>") != std::string::npos,
"function sources must resolve stub declarations through the configured include path");
t.IsTrue(registration.find("#include <ps2_recompiled_functions.h>") != std::string::npos,
"the registration source must use the same unambiguous declaration header");
t.IsTrue(registration.find("#include <ps2_recompiled_stubs.h>") != std::string::npos,
"the registration source must use the same unambiguous stub header");
});
tc.Run("SYSCALL publishes its continuation before entering the runtime", [](TestCase &t) {
Function func;
func.name = "syscall_resume";
@@ -185,6 +208,42 @@ void register_code_generator_tests()
"the continuation PC must be visible before a syscall can transfer to the scheduler");
});
tc.Run("SYSCALL fallthrough is a resumable entry", [](TestCase &t) {
Function func;
func.name = "syscall_resume_entry";
func.start = 0x9100;
func.end = 0x9108;
func.isRecompiled = true;
Instruction syscall{};
syscall.address = 0x9100;
syscall.opcode = OPCODE_SPECIAL;
syscall.function = SPECIAL_SYSCALL;
syscall.raw = (0x83u << 6) | SPECIAL_SYSCALL;
Instruction after = makeNop(0x9104);
CodeGenerator gen({}, {});
CodeGenerator::AnalysisResult analysis =
gen.collectInternalBranchTargets(func, {syscall, after});
t.IsTrue(analysis.resumeEntryPoints.contains(0x9104u),
"a syscall can yield through a guest override, so its fallthrough must be resumable");
const std::string generated = gen.generateFunction(func, {syscall, after}, false);
t.IsTrue(generated.find("case 0x9104u: goto label_9104;") != std::string::npos,
"the owner wrapper must resume directly after the syscall");
gen.setRenamedFunctions({{0x9100u, "syscall_resume_entry_0x9100"}});
gen.setResumeEntryTargets({{0x9100u,
std::vector<uint32_t>(analysis.resumeEntryPoints.begin(),
analysis.resumeEntryPoints.end())}});
const std::string registration = gen.generateFunctionRegistration({func}, {});
t.IsTrue(registration.find(
"g_ps2RecompiledFunctionTable[1] = syscall_resume_entry_0x9100; // 0x9104") !=
std::string::npos,
"the syscall continuation must register to the owner wrapper");
});
tc.Run("R5900 MULT writes rd when rd is non-zero", [](TestCase &t) {
CodeGenerator gen({}, {});
@@ -606,6 +665,46 @@ void register_code_generator_tests()
"multiple resume pcs should register to the same owner wrapper");
});
tc.Run("configured internal guest handlers register to their owner wrapper", [](TestCase &t) {
Function owner;
owner.name = "sdk_bootstrap_owner";
owner.start = 0x7000;
owner.end = 0x7020;
owner.isRecompiled = true;
owner.isStub = false;
std::vector<Instruction> instructions{
makeNop(0x7000), makeNop(0x7004), makeNop(0x7008), makeNop(0x700C),
makeNop(0x7010), makeNop(0x7014), makeNop(0x7018), makeNop(0x701C)};
std::vector<Function> functions{owner};
std::unordered_map<uint32_t, std::vector<Instruction>> decoded{{owner.start, instructions}};
std::unordered_map<uint32_t, std::vector<uint32_t>> targetsByOwner;
const size_t added = PS2Recompiler::CollectInternalEntryTargets(
functions, decoded, {0x7008u, 0x7018u}, targetsByOwner);
t.IsTrue(added == 2u,
"both address-qualified internal handlers should be promoted");
t.IsTrue(targetsByOwner.at(owner.start).size() == 2u,
"both handlers should belong to the containing generated wrapper");
CodeGenerator gen({}, {});
gen.setRenamedFunctions({{owner.start, "sdk_bootstrap_owner_0x7000"}});
gen.setResumeEntryTargets(targetsByOwner);
const std::string generated = gen.generateFunction(owner, instructions, false);
const std::string registration = gen.generateFunctionRegistration(functions, {});
t.IsTrue(generated.find("case 0x7008u: goto label_7008;") != std::string::npos,
"the owner must enter directly at the first installed handler");
t.IsTrue(generated.find("case 0x7018u: goto label_7018;") != std::string::npos,
"the owner must enter directly at the second installed handler");
t.IsTrue(registration.find("sdk_bootstrap_owner_0x7000; // 0x7008") != std::string::npos,
"the first handler address must dispatch to its owner wrapper");
t.IsTrue(registration.find("sdk_bootstrap_owner_0x7000; // 0x7018") != std::string::npos,
"the second handler address must dispatch to its owner wrapper");
});
tc.Run("external mid-function entry can register to the owner wrapper", [](TestCase &t) {
Function caller;
caller.name = "caller";
+152
View File
@@ -155,6 +155,78 @@ static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
std::array<uint32_t, 30> textWords{};
textWords[0] = 0x3C040010u; // lui a0,0x10
textWords[1] = 0xAC800000u; // sw zero,0(a0)
textWords[2] = 0x0C040008u; // jal 0x00100020 (callback registrar)
textWords[3] = 0x24840040u; // addiu a0,a0,0x40 (delay slot)
textWords[4] = 0x03E00008u; // jr ra
textWords[5] = 0x00000000u; // nop
textWords[6] = 0x3C080010u; // lui t0,0x10
textWords[7] = 0x25080070u; // addiu t0,t0,0x70 (code label, not a callback argument)
textWords[8] = 0x03E00008u; // registrar at 0x00100020
textWords[9] = 0x00000000u;
textWords[16] = 0x27BDFFF0u; // callback at 0x00100040: addiu sp,sp,-0x10
textWords[17] = 0xFFBF0000u; // sd ra,0(sp)
textWords[18] = 0xDFBF0000u; // ld ra,0(sp)
textWords[19] = 0x03E00008u; // jr ra
textWords[20] = 0x27BD0010u; // addiu sp,sp,0x10
textWords[24] = 0x03E00008u; // table leaf at 0x00100060
textWords[25] = 0x00000000u;
textWords[26] = 0x03E00008u; // table leaf at 0x00100068
textWords[27] = 0x00000000u;
textWords[28] = 0x03E00008u; // isolated pointer target at 0x00100070
textWords[29] = 0x00000000u;
text->set_data(reinterpret_cast<const char *>(textWords.data()),
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
ELFIO::section *rodata = writer.sections.add(".rodata");
rodata->set_type(ELFIO::SHT_PROGBITS);
rodata->set_flags(ELFIO::SHF_ALLOC);
rodata->set_addr_align(4);
rodata->set_address(0x00200000u);
std::array<uint32_t, 20> tableWords{};
tableWords[1] = 0x00100060u;
tableWords[3] = 0x00100068u;
tableWords[16] = 0x00100070u; // plausible entry, but not part of a pointer cluster
rodata->set_data(reinterpret_cast<const char *>(tableWords.data()),
static_cast<ELFIO::Elf_Word>(tableWords.size() * sizeof(uint32_t)));
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
ELFIO::segment *dataSegment = writer.segments.add();
dataSegment->set_type(ELFIO::PT_LOAD);
dataSegment->set_flags(ELFIO::PF_R);
dataSegment->set_align(0x1000);
dataSegment->add_section_index(rodata->get_index(), rodata->get_addr_align());
return writer.save(elfPath.string());
}
static bool writeMinimalMipsElfWithInitializer(const std::filesystem::path &elfPath,
const std::string &functionName,
uint32_t initializerTarget)
@@ -838,6 +910,42 @@ void register_ps2_recompiler_tests()
std::filesystem::remove(configPath, removeError);
});
tc.Run("config manager loads modern and legacy guest entry hints", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path configPath =
std::filesystem::temp_directory_path() / ("ps2recomp-entry-hints-" + uniqueSuffix + ".toml");
std::ofstream configFile(configPath);
t.IsTrue(static_cast<bool>(configFile), "temp config file should be writable");
if (!configFile)
{
return;
}
configFile << "[general]\n";
configFile << "input = \"dummy.elf\"\n";
configFile << "output = \"out\"\n";
configFile << "entry_points = [\"callback@0x7008\"]\n";
configFile << "untracked_stubs = [\"legacy_callback@0x7018\"]\n";
configFile.close();
ConfigManager manager(configPath.string());
const RecompilerConfig config = manager.loadConfig();
t.Equals(config.entryPointHints.size(), static_cast<size_t>(2),
"modern and legacy entry metadata should be merged");
t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(),
"callback@0x7008") != config.entryPointHints.end(),
"modern entry_points metadata should load");
t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(),
"legacy_callback@0x7018") != config.entryPointHints.end(),
"legacy untracked_stubs metadata should remain compatible");
std::error_code removeError;
std::filesystem::remove(configPath, removeError);
});
tc.Run("elf parser ignores STT_FUNC symbols in non-executable sections", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
@@ -947,6 +1055,50 @@ void register_ps2_recompiler_tests()
std::filesystem::remove(mapPath, removeError);
});
tc.Run("elf parser discovers address-taken callbacks in stripped ELFs", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-address-taken-" + uniqueSuffix + ".elf");
const bool writeOk = writeMinimalMipsElfWithAddressTakenCallbacks(elfPath);
t.IsTrue(writeOk, "temporary stripped ELF should be generated");
if (!writeOk)
{
return;
}
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "generated ELF should parse");
if (!parseOk)
{
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
return;
}
const auto functions = parser.extractFunctions();
auto hasStart = [&functions](uint32_t start)
{
return std::any_of(functions.begin(), functions.end(),
[start](const Function &function)
{ return function.start == start; });
};
t.IsTrue(hasStart(0x00100040u),
"LUI plus delay-slot ADDIU should discover the callback entry");
t.IsTrue(hasStart(0x00100060u),
"clustered rodata pointers should discover the first leaf callback");
t.IsTrue(hasStart(0x00100068u),
"clustered rodata pointers should discover the second leaf callback");
t.IsFalse(hasStart(0x00100070u),
"an isolated data pointer or non-callback code materialization must not become a function");
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
});
tc.Run("runtime call resolution includes Veronica compatibility aliases", [](TestCase &t) {
t.Equals(ps2_runtime_calls::resolveSyscallName("ReleaseAlarm"), std::string_view{"ReleaseAlarm"},
"ReleaseAlarm should resolve as a syscall name");
@@ -52,6 +52,11 @@ namespace
constexpr uint32_t kIrqWaitPc = 0x00160200u;
constexpr uint32_t kIrqResumePc = 0x00160210u;
constexpr uint32_t kIntcHandlerPc = 0x00160220u;
constexpr uint32_t kIrqStackWaitPc = 0x00160230u;
constexpr uint32_t kIrqStackResumePc = 0x00160240u;
constexpr uint32_t kIrqStackHandlerPc = 0x00160250u;
constexpr uint32_t kIrqRegistrationSp = 0x001E0000u;
constexpr uint32_t kIrqRegistrationGuardAddr = kIrqRegistrationSp - 16u;
constexpr uint32_t kISemaWaitPc = 0x00160300u;
constexpr uint32_t kISemaResumePc = 0x00160310u;
constexpr uint32_t kISemaDriverPc = 0x00160320u;
@@ -83,6 +88,7 @@ namespace
uint64_t g_vsyncTick = 0;
uint64_t g_vsyncCsr = 0;
std::atomic<bool> g_timer2Resumed{false};
uint32_t g_irqObservedSp = 0u;
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
@@ -184,6 +190,34 @@ namespace
runtime->requestStop();
}
void schedulerIrqStackHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *)
{
g_irqObservedSp = getRegU32(ctx, 29);
const uint64_t clobber = 0u;
std::memcpy(rdram + g_irqObservedSp - sizeof(clobber), &clobber, sizeof(clobber));
ctx->pc = 0u;
}
void schedulerIrqStackWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
EeScheduler &scheduler = runtime->eeScheduler();
scheduler.addIrqHandler(false,
2u,
kIrqStackHandlerPc,
true,
0u,
0u,
kIrqRegistrationSp);
ctx->pc = kIrqStackResumePc;
scheduler.waitVSync(scheduler.currentVSyncTick());
}
void schedulerIrqStackResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerISemaHandler(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_dispatchTrace.push_back(3);
@@ -467,6 +501,32 @@ void register_ps2_runtime_interrupt_tests()
"the IRQ frame should receive its registered argument");
});
tc.Run("IRQ callbacks use an isolated invocation stack", [](TestCase &t)
{
TestEnv env;
env.runtime.registerFunction(kIrqStackWaitPc, schedulerIrqStackWait);
env.runtime.registerFunction(kIrqStackResumePc, schedulerIrqStackResume);
env.runtime.registerFunction(kIrqStackHandlerPc, schedulerIrqStackHandler);
constexpr uint64_t guard = 0x1122334455667788ull;
std::memcpy(env.rdram.data() + kIrqRegistrationGuardAddr, &guard, sizeof(guard));
g_irqObservedSp = 0u;
R5900Context mainContext{};
mainContext.pc = kIrqStackWaitPc;
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
uint64_t guardAfter = 0u;
std::memcpy(&guardAfter,
env.rdram.data() + kIrqRegistrationGuardAddr,
sizeof(guardAfter));
t.IsTrue(g_irqObservedSp != 0u && g_irqObservedSp != kIrqRegistrationSp,
"IRQ handler must not reuse the transient stack captured at registration");
t.Equals(guardAfter, guard,
"IRQ handler stack writes must not clobber the registering thread's live frame");
});
tc.Run("iSignalSema defers selection until IRQ return", [](TestCase &t)
{
TestEnv env;
+38 -7
View File
@@ -319,6 +319,39 @@ void register_ps2_sif_rpc_tests()
{
MiniTest::Case("PS2SifRpc", [](TestCase &tc)
{
tc.Run("SifInitRpc does not reset the running IOP", [](TestCase &t)
{
TestEnv env;
env.runtime.eeScheduler().accountCycles(80u);
const uint64_t cyclesBeforeInit = env.runtime.iopDebugSnapshot().emulatorCycles;
t.IsTrue(cyclesBeforeInit != 0u, "IOP cycle counter should advance before RPC initialization");
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(env.runtime.iopDebugSnapshot().emulatorCycles, cyclesBeforeInit,
"SifInitRpc must not reboot or reset the IOP");
});
tc.Run("emulated RPC bind waits for a registered IOP server", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kClientAddr = 0x00021F00u;
constexpr uint32_t kUnregisteredSid = 0x13572468u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kUnregisteredSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc transport should complete");
const SifRpcClientData client = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
t.Equals(client.server, 0u,
"client server pointer must stay null until the emulated IRX registers its SID");
});
tc.Run("register bind call updates descriptors and payload", [](TestCase &t)
{
TestEnv env;
@@ -826,7 +859,7 @@ void register_ps2_sif_rpc_tests()
t.Equals(getRegS32(env.ctx, 2), 0, "RECVX snddrv client should no longer be RPC-busy");
});
tc.Run("bind before register creates placeholder then remaps", [](TestCase &t)
tc.Run("hybrid bind before register waits then remaps", [](TestCase &t)
{
TestEnv env;
@@ -846,11 +879,9 @@ void register_ps2_sif_rpc_tests()
t.Equals(getRegS32(env.ctx, 2), KE_OK, "initial bind without registered server should still succeed");
const SifRpcClientData clientBeforeRegister = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
t.IsTrue(clientBeforeRegister.server != 0u, "bind should allocate placeholder server when sid is missing");
t.IsTrue(clientBeforeRegister.server >= 0x01F10000u && clientBeforeRegister.server < 0x01F20000u,
"placeholder server should come from rpc server pool");
t.Equals(clientBeforeRegister.buf, 0u, "placeholder server starts with empty buf");
t.Equals(clientBeforeRegister.cbuf, 0u, "placeholder server starts with empty cbuf");
t.Equals(clientBeforeRegister.server, 0u, "bind must wait until a hybrid backend owns the SID");
t.Equals(clientBeforeRegister.buf, 0u, "unbound client starts with empty buf");
t.Equals(clientBeforeRegister.cbuf, 0u, "unbound client starts with empty cbuf");
setRegU32(env.ctx, 4, kQdAddr);
setRegU32(env.ctx, 5, 0x44u);
@@ -873,7 +904,7 @@ void register_ps2_sif_rpc_tests()
t.Equals(clientAfterRegister.server, kSdAddr, "register should remap pre-bound clients to concrete server descriptor");
t.Equals(clientAfterRegister.buf, kServerBufAddr, "register should update client buf from server descriptor");
t.Equals(clientAfterRegister.cbuf, kServerCbufAddr, "register should update client cbuf from server descriptor");
t.IsTrue(clientAfterRegister.server != clientBeforeRegister.server, "client server pointer should switch from placeholder to real server");
t.IsTrue(clientAfterRegister.server != clientBeforeRegister.server, "client server pointer should switch from unbound to real server");
setRegU32(env.ctx, 4, kSdAddr);
setRegU32(env.ctx, 5, kQdAddr);