Files
PS2Recomp/ps2xIOP/tests/iop_import_tests.cpp
Ran-j e27a658b32 feat: wip performance patch
feat: added parallel gs
feat: optmized IOP emulator
feat: added guest and game fps count
feat: small vu1 optmization and guards
2026-09-25 14:29:05 -03:00

461 lines
19 KiB
C++

#include "emulator/core/iop_cpu.h"
#include "emulator/core/iop_kernel.h"
#include "emulator/core/iop_memory.h"
#include "emulator/imports/iop_cdvd.h"
#include "emulator/imports/iop_imports.h"
#include "emulator/imports/iop_loadcore.h"
#include "emulator/imports/iop_timrman.h"
#include "emulator/services/iop_rpc.h"
#include "ps2x/iop/iop_host.h"
#include <cstdint>
#include <chrono>
#include <cstring>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <string_view>
namespace
{
using namespace ps2x::iop;
using namespace ps2x::iop::detail;
constexpr uint32_t kExportMagic = 0x41C00000u;
constexpr int32_t kLibraryNotFound = -213;
constexpr int32_t kIllegalLibrary = -214;
class NullHost : public IopHost
{
public:
bool readGuest(uint32_t, void *, size_t) const override { return false; }
bool writeGuest(uint32_t, const void *, size_t) override { return false; }
bool zeroGuest(uint32_t, size_t) override { return false; }
bool normalizeGuestAddress(uint32_t, uint32_t &) const override { return false; }
uint32_t allocateIopHandle(IopHandleKind) override { return 1u; }
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
void freeGuest(uint32_t) override {}
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {}
std::string hostPath(HostPathKind) const override { return {}; }
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
uint64_t openHostFile(std::string_view) override { return 0u; }
bool hostFileSize(uint64_t, uint64_t &) const override { return false; }
bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; }
void closeHostFile(uint64_t) override {}
int32_t memoryCard(const MemoryCardRequest &) override { return 0; }
bool hasGuestFunction(uint32_t) const override { return false; }
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; }
void log(LogLevel, std::string_view) override {}
};
class CdRootHost final : public NullHost
{
public:
explicit CdRootHost(std::filesystem::path rootPath)
: root(std::move(rootPath))
{
}
std::string hostPath(HostPathKind kind) const override
{
return kind == HostPathKind::CdRoot ? root.string() : std::string{};
}
private:
std::filesystem::path root;
};
class RecordingExecutor final : public IopGuestExecutor
{
public:
uint32_t executeGuestFunction(uint32_t address,
uint32_t a0,
uint32_t,
uint32_t,
uint32_t,
uint32_t gp) override
{
++calls;
lastAddress = address;
lastArgument = a0;
lastGp = gp;
return callbackResult;
}
uint32_t callbackResult = 0u;
uint32_t calls = 0u;
uint32_t lastAddress = 0u;
uint32_t lastArgument = 0u;
uint32_t lastGp = 0u;
};
bool expect(bool condition, std::string_view message)
{
if (condition)
return true;
std::cerr << "FAIL: " << message << '\n';
return false;
}
bool testLoadcoreRebootLibraryMode()
{
IopMemory memory;
IopImportRegistry imports(memory);
IopLoadcore loadcore(memory, imports);
IopCpuState cpu{};
cpu.gpr[4] = 0u;
cpu.gpr[5] = 2u;
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 was not handled") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kIllegalLibrary,
"loadcore:27 did not reject a null export table"))
return false;
constexpr uint32_t table = 0x1000u;
memory.write32(table, kExportMagic);
memory.write16(table + 8u, 0x0101u);
memory.write16(table + 10u, 0x1234u);
const char name[8] = {'t', 'e', 's', 't', 'l', 'i', 'b', '\0'};
(void)memory.writeRam(table + 12u, name, sizeof(name));
memory.write32(table + 20u, 0u);
cpu = {};
cpu.gpr[4] = table;
cpu.gpr[5] = 2u;
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a valid export table") ||
!expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a valid export table") ||
!expect(memory.read16(table + 10u) == 0x1232u,
"loadcore:27 did not replace only export mode bits 1 and 2"))
return false;
constexpr uint32_t invalidTable = 0x1100u;
memory.write32(invalidTable, 0xDEADBEEFu);
cpu = {};
cpu.gpr[4] = invalidTable;
cpu.gpr[5] = 6u;
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 did not consume an invalid-table call") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kLibraryNotFound,
"loadcore:27 returned the wrong invalid-table error"))
return false;
if (!expect(imports.registerExportTable(table), "test export table did not register"))
return false;
memory.write32(table, 0u);
cpu = {};
cpu.gpr[4] = table;
cpu.gpr[5] = 6u;
return expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a registered table") &&
expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a registered table") &&
expect(memory.read16(table + 10u) == 0x1236u,
"loadcore:27 did not update a registered table's mode");
}
bool pollEvent(IopKernel &kernel, int eventId, uint32_t bits, uint32_t resultAddress, int32_t expected)
{
IopCpuState cpu{};
cpu.gpr[4] = static_cast<uint32_t>(eventId);
cpu.gpr[5] = bits;
cpu.gpr[6] = 0u; // WEF_AND
cpu.gpr[7] = resultAddress;
return expect(kernel.dispatchEventImport(11u, cpu), "PollEventFlag was not handled") &&
expect(static_cast<int32_t>(cpu.gpr[2]) == expected, "PollEventFlag returned an unexpected result");
}
bool testCdvdSpecialControl()
{
NullHost host;
IopMemory memory;
IopKernel kernel(memory);
kernel.reset();
IopCdvd cdvd(host, memory, kernel);
cdvd.reset();
constexpr uint32_t param = 0x2000u;
constexpr uint32_t eventResult = 0x2010u;
IopCpuState cpu{};
cpu.gpr[4] = static_cast<uint32_t>(-11); // sceCdSC: return cdvdman interrupt event flag
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "cdvdman:50 was not handled") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "sceCdSC(-11) did not return a valid event flag"))
return false;
const int eventId = static_cast<int>(cpu.gpr[2]);
if (!pollEvent(kernel, eventId, 0x29u, eventResult, 0) ||
!expect(memory.read32(eventResult) == 0x29u, "cdvdman event flag did not start with bits 0x29"))
return false;
IopCpuState clear{};
clear.gpr[4] = static_cast<uint32_t>(eventId);
clear.gpr[5] = ~0x29u;
if (!expect(kernel.dispatchEventImport(8u, clear), "ClearEventFlag was not handled") ||
!pollEvent(kernel, eventId, 0x29u, eventResult, -418))
return false;
cpu = {};
cpu.gpr[4] = 0x12345u;
if (!expect(cdvd.dispatchImport(7u, cpu), "sceCdSeek was not handled") ||
!pollEvent(kernel, eventId, 0x29u, eventResult, 0))
return false;
memory.write8(param, 0x30u);
cpu = {};
cpu.gpr[4] = static_cast<uint32_t>(-2);
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-2) was not handled") ||
!expect(cpu.gpr[2] == 0x30u, "sceCdSC(-2) did not store the low-byte error"))
return false;
memory.write32(param, 0u);
cpu = {};
cpu.gpr[4] = static_cast<uint32_t>(-1);
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-1) was not handled") ||
!expect(cpu.gpr[2] == 0u, "sceCdSC(-1) returned the wrong initial stream state") ||
!expect(memory.read32(param) == 0x30u, "sceCdSC(-1) did not publish the last error"))
return false;
cpu = {};
cpu.gpr[4] = 2u;
cpu.gpr[5] = param;
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(2) was not handled") ||
!expect(cpu.gpr[2] == 2u, "sceCdSC(2) did not update the stream state"))
return false;
cpu = {};
cpu.gpr[4] = static_cast<uint32_t>(-1);
cpu.gpr[5] = param;
return expect(cdvd.dispatchImport(50u, cpu), "second sceCdSC(-1) was not handled") &&
expect(cpu.gpr[2] == 2u, "sceCdSC(-1) did not preserve the stream state");
}
bool testCdvdSearchFile()
{
const auto suffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path root =
std::filesystem::temp_directory_path() / ("ps2x-iop-cdvd-search-" + suffix);
const std::filesystem::path movieDirectory = root / "MOVIE";
const std::filesystem::path moviePath = movieDirectory / "OPENING.PSS";
std::error_code error;
std::filesystem::create_directories(movieDirectory, error);
if (!expect(!error, "could not create the temporary CD root"))
return false;
{
std::ofstream movie(moviePath, std::ios::binary);
movie.write("PSS!", 4);
}
CdRootHost host(root);
IopMemory memory;
IopKernel kernel(memory);
kernel.reset();
IopCdvd cdvd(host, memory, kernel);
cdvd.reset();
constexpr uint32_t resultAddress = 0x2400u;
constexpr uint32_t pathAddress = 0x2480u;
const char path[] = "cdrom0:\\movie\\opening.pss;1";
(void)memory.writeRam(pathAddress, path, sizeof(path));
IopCpuState cpu{};
cpu.gpr[4] = resultAddress;
cpu.gpr[5] = pathAddress;
const bool handled = cdvd.dispatchImport(10u, cpu);
const bool passed =
expect(handled, "cdvdman:10 was not handled") &&
expect(cpu.gpr[2] == 1u, "sceCdSearchFile did not find a case-insensitive ISO path") &&
expect(memory.read32(resultAddress) >= 20u, "sceCdSearchFile returned an invalid LSN") &&
expect(memory.read32(resultAddress + 4u) == 4u, "sceCdSearchFile returned the wrong size") &&
expect(memory.readString(resultAddress + 8u, 16u) == "OPENING.PSS",
"sceCdSearchFile returned the wrong file name");
std::filesystem::remove_all(root, error);
return passed;
}
bool testKernelSchedulingTransitions()
{
IopMemory memory;
IopKernel kernel(memory);
IopCpuState caller{};
const auto createThread = [&](uint32_t entry, uint32_t priority)
{
memory.write32(0x2008, entry);
memory.write32(0x200c, 0x100);
memory.write32(0x2010, priority);
caller.gpr[4] = 0x2000;
(void)kernel.dispatchThreadImport(4, caller, 0);
const int id = static_cast<int>(caller.gpr[2]);
caller.gpr[4] = static_cast<uint32_t>(id);
(void)kernel.dispatchThreadImport(6, caller, 0);
return id;
};
const auto select = [&](int id, uint64_t cycle)
{
IopThread *thread = kernel.beginNextReady(cycle);
if (!expect(thread && thread->id == id, "scheduler selected the wrong thread"))
return static_cast<IopThread *>(nullptr);
return thread;
};
const auto threadImport = [&](uint16_t ordinal, int id, uint32_t value = 0)
{
caller.gpr[4] = static_cast<uint32_t>(id);
caller.gpr[5] = value;
return kernel.dispatchThreadImport(ordinal, caller, 0) && caller.gpr[2] == 0;
};
constexpr uint32_t sentinel = 0xfffffffcu;
const int first = createThread(0x10000, 32);
const int second = createThread(0x20000, 64);
for (int i = 0; i < 128; ++i)
{
auto *thread = select(first, 0);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
}
if (!threadImport(14, second, 16)) return false;
auto *thread = select(second, 0);
if (!thread) return false;
kernel.delayCurrentUntil(100, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.nextWakeCycle(1000) == 100, "delay deadline lost after blocking")) return false;
thread = select(first, 99);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
thread = select(second, 100);
if (!thread) return false;
kernel.sleepCurrent(thread->cpu);
kernel.endTimeslice(*thread, sentinel);
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(25, second)) return false;
thread = select(second, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(29, second)) return false;
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(31, second)) return false;
memory.write32(0x202c, 1); // semaphore maximum, initially empty
caller.gpr[4] = 0x2020;
(void)kernel.dispatchSemaphoreImport(4, caller);
const uint32_t semaphore = caller.gpr[2];
thread = select(second, 101);
if (!thread) return false;
thread->cpu.gpr[4] = semaphore;
(void)kernel.dispatchSemaphoreImport(8, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
caller.gpr[4] = semaphore;
(void)kernel.dispatchSemaphoreImport(6, caller);
thread = select(second, 101);
if (!thread) return false;
const int event = kernel.createInternalEventFlag(0, 0, 0);
thread->cpu.gpr[4] = static_cast<uint32_t>(event);
thread->cpu.gpr[5] = 1;
thread->cpu.gpr[6] = 0;
thread->cpu.gpr[7] = 0;
(void)kernel.dispatchEventImport(10, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!kernel.setInternalEventFlag(event, 1)) return false;
const int third = createThread(0x30000, 16);
thread = select(second, 101); // Equal priorities retain ascending ID order.
if (!thread) return false;
(void)kernel.dispatchThreadImport(9, thread->cpu, 101);
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.threadCount() == 2, "ExitDeleteThread must not turn Dead into Dormant")) return false;
thread = select(third, 101);
if (!thread) return false;
kernel.terminateThreadsInRange(0x30000, 0x100);
kernel.cleanupDeadThreads();
if (!expect(kernel.threadCount() == 2 && thread->cpu.stopped, "active CPU reference must survive module unload")) return false;
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.threadCount() == 1, "unloaded thread must retire after its timeslice")) return false;
thread = select(first, 101);
if (!thread) return false;
kernel.delayCurrentUntil(1000, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.beginNextReady(999) == nullptr && kernel.nextWakeCycle(2000) == 1000,
"idle scheduling must preserve the next wake deadline")) return false;
thread = select(first, 1000);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(5, first)) return false;
if (!expect(kernel.beginNextReady(1000) == nullptr, "deletion must invalidate the cached ready thread")) return false;
kernel.reset();
return expect(kernel.beginNextReady(0) == nullptr && kernel.threadCount() == 0,
"reset must discard scheduling state");
}
bool testTimrmanPeriodicCallback()
{
IopTimrman timrman;
timrman.reset();
IopCpuState cpu{};
cpu.gpr[4] = 1u; // SYSCLK
cpu.gpr[5] = 32u;
cpu.gpr[6] = 1u;
if (!expect(timrman.dispatchImport(4u, cpu, 100u), "AllocHardTimer was not handled") ||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "AllocHardTimer did not allocate a 32-bit timer"))
return false;
const uint32_t timerId = cpu.gpr[2];
cpu = {};
cpu.gpr[4] = timerId;
cpu.gpr[5] = 100u;
cpu.gpr[6] = 0x12340u;
cpu.gpr[7] = 0x45670u;
cpu.gpr[28] = 0x89AB0u;
if (!expect(timrman.dispatchImport(20u, cpu, 100u), "SetTimerHandler was not handled") ||
!expect(cpu.gpr[2] == 0u, "SetTimerHandler failed"))
return false;
cpu = {};
cpu.gpr[4] = timerId;
cpu.gpr[5] = 1u;
cpu.gpr[6] = 0u;
cpu.gpr[7] = 1u;
if (!expect(timrman.dispatchImport(22u, cpu, 100u), "SetupHardTimer was not handled") ||
!expect(cpu.gpr[2] == 0u, "SetupHardTimer failed"))
return false;
cpu = {};
cpu.gpr[4] = timerId;
if (!expect(timrman.dispatchImport(23u, cpu, 100u), "StartHardTimer was not handled") ||
!expect(cpu.gpr[2] == 0u, "StartHardTimer failed") ||
!expect(timrman.nextEventCycle(1000u) == 200u, "timer compare was scheduled at the wrong cycle"))
return false;
RecordingExecutor executor;
executor.callbackResult = 100u;
timrman.serviceDue(199u, executor);
if (!expect(executor.calls == 0u, "timer callback ran too early"))
return false;
timrman.serviceDue(200u, executor);
return expect(executor.calls == 1u, "timer callback did not run") &&
expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") &&
expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") &&
expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") &&
expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare");
}
}
int main()
{
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
!testTimrmanPeriodicCallback() || !testKernelSchedulingTransitions())
return 1;
std::cout << "ps2xIOP import tests passed\n";
return 0;
}