5 Commits

Author SHA1 Message Date
Ranieri 669114f3f6 Feature/runtime review codegen fixes (#87)
* feat: small fixes on code gen

* feat: added code gen test

* feat: rename IOP

* fix: fix special case on JR
feat: added code generator test

* feat: ps2 logs now need special macros

* feat: a lot of regressions test
feat: use test to fix bugs on runtime
fix: fix incorrect instructions on code generator
feat: added missing decode on r5900 decoder
feat: added scissor on rasterizer

* feat: better ghidra plugin analyzer
fix: fix real bug on function finding on elf analyzer

* feat: some logs on GS
feat: added more syscalls stubs
feat: added more ps2 stubs

* feat: added missing stub
2026-02-27 03:44:59 -03:00
DanielSvoboda 8d1f1c5672 Fix 'max' (#81) 2026-02-24 19:05:25 -03:00
Aslan Hud 1551d59fbc Feat: GS, VIF1, VU1 pathways + font rendering stubs + mpeg stubs (#80)
* GS stubs (ps2_stubs_gs.inl):
- Replace direct VRAM writes with GIF packet path
- sceGsExecLoadImage: build GIF packet, set MADR/QWC/CHCR
- sceGsExecStoreImage: GIF packet, processPendingTransfers, consumeLocalToHostBytes
- sceGsPutDispEnv/sceGsPutDrawEnv: program GIF DMA Path3 (5/9 QWs)
- sceGsResetGraph: GIF packet + writeIORegister for pmode/smode2/dispfb/display/bgcolor
- sceGsSetDefDrawEnv: GIF packet layout, sceGszbufaddr for zbuf
- sceGsSyncPath: processPendingTransfers, poll DMA channels
- sceGsSyncV/sceGsSyncVCallback: return 0
- sceGszbufaddr: zbuf calculation (width/height blocks, gparam)

Helpers (ps2_stubs_helpers.inl):
- GsDispEnvMem: 5 fields (pmode, smode2, dispfb, display, bgcolor) for GIF layout
- writeGsDispEnv: read-modify-write pattern
- toDmaPhys: SPR (scratchpad) handling for DMA MADR bit 31
- submitDmaSend: chain mode (chcr=0x185) for sceDmaSend/sceDmaSendI/sceDmaSendM

Misc stubs (ps2_stubs_misc.inl):
- Add sceeFontInit, sceeFontLoadFont, sceeFontPrintfAt, sceeFontPrintfAt2
- Add sceeFontClose, sceeFontSetColour, sceeFontSetMode, sceeFontSetFont, sceeFontSetScale
- Font stubs use GIF packets for CLUT and texture upload

Call list (ps2_call_list.h):
- Add sceeFont* entries between sceGszbufaddr and sceIoctl

* added: added more scee font stubs in stubs misc
fix: fixed flickering error when rendering in memory.cpp

* fix: fixed linux build error
2026-02-24 19:04:52 -03:00
Aslan Hud c1e98c9398 Add IOP, audio, pad, fio changes, VAG handling, and call-stack logger (#76)
- Add IOP with RPC handling (handleRPC, LibSd SID) and init/reset using RDRAM
- Add IOP audio layer for LibSd RPC (handleLibSdRpc)
- Add PS2AudioBackend with VAG decode, onVagTransfer/onSoundCommand, raylib playback
- Add PSPadBackend with readState and raylib input
- Add IOP RAM mapping in Memory (getIOPRAM) for EE↔IOP access
- Add fio syscalls: fioOpen, fioRead, fioWrite, fioClose, fioLseek, fioMkdir
- Add VAG accumulation for fio reads (multi-chunk VagAccumEntry)
- Add ps2_log.h call-stack logger (tab-indented, Debug only, ps2_log.txt next to exe)
- Code generator: emit ps2_log include and PS_LOG_ENTRY for full functions
- Recompiler: emit ps2_log include and PS_LOG_ENTRY for stubs
- Runtime: print "[PS2 LOG] Logs saved at <path>" on exit (Debug only)
2026-02-24 11:45:52 -03:00
DanielSvoboda bac1fc9ef8 Add game name to window title (#74)
* Add game title resolution from internal database

* +

* -
2026-02-22 22:30:27 -03:00
72 changed files with 14127 additions and 2009 deletions
@@ -37,6 +37,7 @@ namespace ps2recomp
static bool isReliableSymbolNameForHeuristics(const std::string &name);
static bool isSystemSymbolNameForHeuristics(const std::string &name);
static bool shouldAutoSkipNameForHeuristics(const std::string &name);
static bool shouldSkipSystemSymbolForHeuristics(const std::string &name, const std::unordered_set<std::string> &forcedRecompileNames);
static int findEntryFunctionIndexForHeuristics(const std::vector<Function> &functions, uint32_t entryAddress);
static int findFallbackEntryFunctionIndexForHeuristics(const std::vector<Function> &functions);
static bool hasHardwareIOSignalForHeuristics(const std::vector<Instruction> &instructions);
+53 -3
View File
@@ -24,6 +24,7 @@ namespace ps2recomp
static bool hasPs2ApiPrefix(const std::string &name);
static bool hasReliableSymbolName(const std::string &name);
static bool isDoNotSkipOrStub(const std::string &name);
static bool matchesKernelRuntimeName(const std::string &name);
static uint32_t decodeAbsoluteJumpTarget(uint32_t instructionAddress, uint32_t targetField);
static bool tryReadWord(const ElfParser *parser, uint32_t address, uint32_t &outWord);
@@ -444,6 +445,16 @@ namespace ps2recomp
void ElfAnalyzer::analyzeLibraryFunctions()
{
std::unordered_set<std::string> forcedRecompileNames;
forcedRecompileNames.reserve(m_forceRecompileStarts.size());
for (const auto &func : m_functions)
{
if (m_forceRecompileStarts.contains(func.start))
{
forcedRecompileNames.insert(func.name);
}
}
for (const auto &symbol : m_symbols)
{
if (symbol.isFunction)
@@ -457,7 +468,7 @@ namespace ps2recomp
{
m_libFunctions.insert(symbol.name);
}
else if (isSystemFunction(symbol.name))
else if (shouldSkipSystemSymbolForHeuristics(symbol.name, forcedRecompileNames))
{
m_skipFunctions.insert(symbol.name);
}
@@ -475,7 +486,7 @@ namespace ps2recomp
{
m_libFunctions.insert(func.name);
}
else if (isSystemFunction(func.name))
else if (shouldSkipSystemSymbolForHeuristics(func.name, forcedRecompileNames))
{
m_skipFunctions.insert(func.name);
}
@@ -1606,6 +1617,10 @@ namespace ps2recomp
if (funcIt != m_functions.end())
{
const Function &func = *funcIt;
if (m_forceRecompileStarts.contains(func.start))
{
continue;
}
if (patchAddrs.size() > 3)
{
@@ -2032,6 +2047,18 @@ namespace ps2recomp
return false;
}
static bool matchesKernelRuntimeName(const std::string &name)
{
if (name.empty())
{
return false;
}
static const std::regex kernelRuntimePattern(
"^(?:(?:Create|Delete|Start|ExitDelete|Exit|Terminate|Suspend|Resume|Sleep|Wakeup|CancelWakeup|Change|Rotate|Release|Setup|Register|Query|Get|Set|Refer|Poll|Wait|Signal|Enable|Disable|Flush|Reset|Add|Init)(?:Thread|Sema|EventFlag|Alarm|Intc|IntcHandler2|Dmac|DmacHandler2|OsdConfigParam|MemorySize|VSyncFlag|Heap|TLS|Status|Cache|Syscall|TLB|TLBEntry|GsCrt)|EndOfHeap|GsGetIMR|GsPutIMR|Deci2Call|Sif[A-Za-z0-9_]+|i(?:SignalSema|PollSema|ReferSemaStatus|SetEventFlag|ClearEventFlag|PollEventFlag|ReferEventFlagStatus|WakeupThread|CancelWakeupThread|ReleaseWaitThread|SetAlarm|CancelAlarm|FlushCache|sceSifSetDma|sceSifSetDChain))$");
return std::regex_match(name, kernelRuntimePattern);
}
static bool isDoNotSkipOrStub(const std::string &name)
{
static const std::unordered_set<std::string> kDoNotSkipOrStub = {
@@ -2131,6 +2158,17 @@ namespace ps2recomp
return isSystemSymbolNameForHeuristics(name);
}
bool ElfAnalyzer::shouldSkipSystemSymbolForHeuristics(
const std::string &name,
const std::unordered_set<std::string> &forcedRecompileNames)
{
if (forcedRecompileNames.contains(name))
{
return false;
}
return isSystemSymbolNameForHeuristics(name);
}
bool ElfAnalyzer::isSystemFunction(const std::string &name) const
{
return isSystemSymbolNameForHeuristics(name);
@@ -2144,15 +2182,27 @@ namespace ps2recomp
if (!hasReliableSymbolName(name))
return false;
std::string normalizedName = name;
if (normalizedName[0] == '_' && normalizedName.size() > 1)
{
normalizedName = normalizedName.substr(1);
}
if (matchesKernelRuntimeName(normalizedName))
return true;
if (m_knownLibNames.find(name) != m_knownLibNames.end())
return true;
if (m_knownLibNames.find(normalizedName) != m_knownLibNames.end())
return true;
if (hasPs2ApiPrefix(name))
return true;
// Check for common C/C++ library function names
static const std::regex cLibPattern("^_*(mem|str|time|f?printf|f?scanf|malloc|free|calloc|realloc|atoi|itoa|rand|srand|abort|exit|atexit|getenv|system|bsearch|qsort|abs|labs|div|ldiv|mblen|mbtowc|wctomb|mbstowcs|wcstombs).*");
if (std::regex_match(name, cLibPattern))
if (std::regex_match(normalizedName, cLibPattern))
{
return true;
}
@@ -11,6 +11,7 @@
namespace ps2recomp
{
struct JumpTableEntry;
struct JumpTable;
struct Instruction;
struct Function;
struct Symbol;
@@ -47,6 +48,7 @@ namespace ps2recomp
void setRenamedFunctions(const std::unordered_map<uint32_t, std::string> &renames);
void setBootstrapInfo(const BootstrapInfo &info);
void setRelocationCallNames(const std::unordered_map<uint32_t, std::string> &callNames);
void setConfiguredJumpTables(const std::vector<JumpTable> &jumpTables);
AnalysisResult collectInternalBranchTargets(const Function &function,
const std::vector<Instruction> &instructions);
@@ -55,6 +57,7 @@ namespace ps2recomp
std::unordered_map<uint32_t, Symbol> m_symbols;
std::unordered_map<uint32_t, std::string> m_renamedFunctions;
std::unordered_map<uint32_t, std::string> m_relocationCallNames;
std::unordered_map<uint32_t, std::vector<uint32_t>> m_configJumpTableTargetsByAddress;
const std::vector<Section>& m_sections;
BootstrapInfo m_bootstrapInfo;
+1
View File
@@ -180,6 +180,7 @@ namespace ps2recomp
std::unordered_map<uint32_t, std::string> patches;
std::vector<std::string> stubImplementations;
std::unordered_map<uint32_t, uint32_t> mmioByInstructionAddress;
std::vector<JumpTable> jumpTables;
};
} // namespace ps2recomp
+130 -55
View File
@@ -123,6 +123,26 @@ namespace ps2recomp
m_relocationCallNames = callNames;
}
void CodeGenerator::setConfiguredJumpTables(const std::vector<JumpTable> &jumpTables)
{
m_configJumpTableTargetsByAddress.clear();
for (const auto &table : jumpTables)
{
auto &targets = m_configJumpTableTargetsByAddress[table.address];
for (const auto &entry : table.entries)
{
targets.push_back(entry.target);
}
}
for (auto &[address, targets] : m_configJumpTableTargetsByAddress)
{
(void)address;
std::sort(targets.begin(), targets.end());
targets.erase(std::unique(targets.begin(), targets.end()), targets.end());
}
}
std::string CodeGenerator::getFunctionName(uint32_t address) const
{
auto it = m_renamedFunctions.find(address);
@@ -376,6 +396,13 @@ namespace ps2recomp
ss << " " << delaySlotPrefix << delaySlotCode << delaySlotSuffix << "\n";
}
if (branchInst.function == SPECIAL_JALR)
{
ss << " if (jumpTarget == 0u) {\n";
ss << fmt::format(" ctx->pc = 0x{:X}u;\n", fallthroughPc);
ss << " } else {\n";
}
ss << " ctx->pc = jumpTarget;\n";
if (!sortedInternalTargets.empty())
@@ -404,6 +431,11 @@ namespace ps2recomp
ss << " }\n";
}
if (branchInst.function == SPECIAL_JALR)
{
ss << " }\n";
}
ss << " }\n";
}
// -------------------------
@@ -658,7 +690,7 @@ namespace ps2recomp
if (hasIndirectRegisterJump)
{
bool hasFallback = false;
bool needsJrFallback = false;
for (const Instruction* jrInst : indirectJumps) {
bool foundTable = false;
@@ -726,6 +758,33 @@ namespace ps2recomp
if (foundTableAddress) {
tableAddress += lwOffset;
const auto configuredTableIt = m_configJumpTableTargetsByAddress.find(tableAddress);
if (configuredTableIt != m_configJumpTableTargetsByAddress.end())
{
std::vector<uint32_t> jrTargets;
jrTargets.reserve(configuredTableIt->second.size());
for (uint32_t target : configuredTableIt->second)
{
if (target >= function.start && target < function.end &&
instructionAddresses.contains(target))
{
jrTargets.push_back(target);
}
}
if (!jrTargets.empty())
{
std::sort(jrTargets.begin(), jrTargets.end());
jrTargets.erase(std::unique(jrTargets.begin(), jrTargets.end()), jrTargets.end());
result.jumpTableTargets[jrInst->address] = jrTargets;
for (uint32_t target : jrTargets)
{
result.entryPoints.insert(target);
}
foundTable = true;
}
}
uint32_t unshiftedIndexReg = 0;
for (int i = adduIndex - 1; i >= 0 && i >= adduIndex - 10; --i) {
const auto& inst = instructions[i];
@@ -746,7 +805,7 @@ namespace ps2recomp
}
}
if (numCases > 0 && numCases <= 1000) {
if (!foundTable && numCases > 0 && numCases <= 1000) {
const Section* rodata = nullptr;
for (const auto& sec : m_sections) {
if (tableAddress >= sec.address && tableAddress < sec.address + sec.size) {
@@ -788,11 +847,14 @@ namespace ps2recomp
}
}
if (!foundTable) {
hasFallback = true;
if (!(jrInst->function == SPECIAL_JALR))
{
needsJrFallback = true;
}
}
}
if (hasFallback) {
if (needsJrFallback) {
for (uint32_t addr : instructionAddresses)
{
if (addr >= function.start && addr < function.end)
@@ -821,6 +883,9 @@ namespace ps2recomp
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";
ss << "#include \"ps2_log.h\"\n";
ss << "#endif\n\n";
}
AnalysisResult analysisResult = collectInternalBranchTargets(function, instructions);
@@ -836,7 +901,11 @@ namespace ps2recomp
sanitizedName = nameBuilder.str();
}
ss << "void " << sanitizedName << "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) {\n\n";
ss << "void " << sanitizedName << "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) {\n";
ss << "#ifdef PS2_FUNCTION_LOG_TRACKER\n";
ss << " PS_LOG_ENTRY(\"" << sanitizedName << "\");\n";
ss << "#endif\n";
ss << "\n";
ss << " ctx->pc = 0x" << std::hex << function.start << "u;\n"
<< std::dec;
ss << "\n";
@@ -957,11 +1026,11 @@ namespace ps2recomp
case OPCODE_SLTIU:
return fmt::format("SET_GPR_U64(ctx, {}, ((uint64_t)GPR_U64(ctx, {}) < (uint64_t)(int64_t)(int32_t){}) ? 1 : 0);", inst.rt, inst.rs, inst.simmediate);
case OPCODE_ANDI:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PAND(GPR_VEC(ctx, {}), _mm_cvtsi32_si128((int){}{})));", inst.rt, inst.rs, inst.immediate, "u");
return fmt::format("SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) & (uint64_t)(uint16_t){});", inst.rt, inst.rs, inst.immediate);
case OPCODE_ORI:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_POR(GPR_VEC(ctx, {}), _mm_cvtsi32_si128((int){}{})));", inst.rt, inst.rs, inst.immediate, "u");
return fmt::format("SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) | (uint64_t)(uint16_t){});", inst.rt, inst.rs, inst.immediate);
case OPCODE_XORI:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PXOR(GPR_VEC(ctx, {}), _mm_cvtsi32_si128((int){}{})));", inst.rt, inst.rs, inst.immediate, "u");
return fmt::format("SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) ^ (uint64_t)(uint16_t){});", inst.rt, inst.rs, inst.immediate);
case OPCODE_LUI:
return fmt::format("SET_GPR_S32(ctx, {}, (int32_t)((uint32_t){} << 16));", inst.rt, inst.immediate);
case OPCODE_LB:
@@ -1133,10 +1202,10 @@ namespace ps2recomp
case OPCODE_SC:
return fmt::format(
"{{ uint32_t addr = ADD32(GPR_U32(ctx, {}), {}); "
"if (ctx->llbit) {{ WRITE32(addr, GPR_U32(ctx, {})); "
"if (ctx->llbit && ctx->lladdr == addr) {{ WRITE32(addr, GPR_U32(ctx, {})); "
"SET_GPR_S32(ctx, {}, 1); }} "
"else {{ SET_GPR_S32(ctx, {}, 0); }} "
"ctx->llbit = 0; }}",
"ctx->llbit = 0; ctx->lladdr = 0; }}",
inst.rs, inst.simmediate, inst.rt, inst.rt, inst.rt);
default:
return fmt::format("// Unhandled opcode: 0x{:X}", inst.opcode);
@@ -1182,8 +1251,16 @@ namespace ps2recomp
case SPECIAL_MTLO:
return fmt::format("ctx->lo = GPR_U64(ctx, {});", inst.rs);
case SPECIAL_MULT:
if (inst.rd != 0)
{
return fmt::format("{{ int64_t result = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); SET_GPR_S32(ctx, {}, (int32_t)result); }}", inst.rs, inst.rt, inst.rd);
}
return fmt::format("{{ int64_t result = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); }}", inst.rs, inst.rt);
case SPECIAL_MULTU:
if (inst.rd != 0)
{
return fmt::format("{{ uint64_t result = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); SET_GPR_S32(ctx, {}, (int32_t)result); }}", inst.rs, inst.rt, inst.rd);
}
return fmt::format("{{ uint64_t result = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); ctx->lo = (uint64_t)(int64_t)(int32_t)result; ctx->hi = (uint64_t)(int64_t)(int32_t)(result >> 32); }}", inst.rs, inst.rt);
case SPECIAL_DIV:
return fmt::format("{{ int32_t divisor = GPR_S32(ctx, {}); "
@@ -1226,13 +1303,13 @@ namespace ps2recomp
case SPECIAL_SUBU:
return fmt::format("SET_GPR_S32(ctx, {}, (int32_t)SUB32(GPR_U32(ctx, {}), GPR_U32(ctx, {})));", inst.rd, inst.rs, inst.rt);
case SPECIAL_AND:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PAND(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));", inst.rd, inst.rs, inst.rt);
return fmt::format("SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) & GPR_U64(ctx, {}));", inst.rd, inst.rs, inst.rt);
case SPECIAL_OR:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_POR(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));", inst.rd, inst.rs, inst.rt);
return fmt::format("SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) | GPR_U64(ctx, {}));", inst.rd, inst.rs, inst.rt);
case SPECIAL_XOR:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PXOR(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));", inst.rd, inst.rs, inst.rt);
return fmt::format("SET_GPR_U64(ctx, {}, GPR_U64(ctx, {}) ^ GPR_U64(ctx, {}));", inst.rd, inst.rs, inst.rt);
case SPECIAL_NOR:
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PNOR(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));", inst.rd, inst.rs, inst.rt);
return fmt::format("SET_GPR_U64(ctx, {}, ~(GPR_U64(ctx, {}) | GPR_U64(ctx, {})));", inst.rd, inst.rs, inst.rt);
case SPECIAL_SLT:
return fmt::format("SET_GPR_U64(ctx, {}, ((int64_t)GPR_S64(ctx, {}) < (int64_t)GPR_S64(ctx, {})) ? 1 : 0);", inst.rd, inst.rs, inst.rt);
case SPECIAL_SLTU:
@@ -1634,8 +1711,16 @@ namespace ps2recomp
case MMI_MTLO1:
return fmt::format("ctx->lo1 = GPR_U64(ctx, {});", rs);
case MMI_MULT1:
if (rd != 0)
{
return fmt::format("{{ int64_t result = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); ctx->lo1 = (uint64_t)(int64_t)(int32_t)result; ctx->hi1 = (uint64_t)(int64_t)(int32_t)(result >> 32); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ int64_t result = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); ctx->lo1 = (uint64_t)(int64_t)(int32_t)result; ctx->hi1 = (uint64_t)(int64_t)(int32_t)(result >> 32); }}", rs, rt);
case MMI_MULTU1:
if (rd != 0)
{
return fmt::format("{{ uint64_t result = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); ctx->lo1 = (uint64_t)(int64_t)(int32_t)result; ctx->hi1 = (uint64_t)(int64_t)(int32_t)(result >> 32); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t result = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); ctx->lo1 = (uint64_t)(int64_t)(int32_t)result; ctx->hi1 = (uint64_t)(int64_t)(int32_t)(result >> 32); }}", rs, rt);
case MMI_DIV1:
return fmt::format("{{ int32_t divisor = GPR_S32(ctx, {}); "
@@ -1654,16 +1739,40 @@ namespace ps2recomp
case MMI_DIVU1:
return fmt::format("{{ uint32_t divisor = GPR_U32(ctx, {}); if (divisor != 0) {{ ctx->lo1 = (uint64_t)(int64_t)(int32_t)(GPR_U32(ctx, {}) / divisor); ctx->hi1 = (uint64_t)(int64_t)(int32_t)(GPR_U32(ctx, {}) % divisor); }} else {{ ctx->lo1=0xFFFFFFFFFFFFFFFFull; ctx->hi1=(uint64_t)(int64_t)(int32_t)GPR_U32(ctx,{}); }} }}", rt, rs, rs, rs);
case MMI_MADD:
if (rd != 0)
{
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); int64_t prod = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); int64_t result = acc + prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); int64_t prod = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); int64_t result = acc + prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); }}", rs, rt);
case MMI_MADDU:
if (rd != 0)
{
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); uint64_t prod = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); uint64_t result = acc + prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); uint64_t prod = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); uint64_t result = acc + prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); }}", rs, rt);
case MMI_MSUB:
if (rd != 0)
{
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); int64_t prod = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); int64_t result = acc - prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); int64_t prod = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); int64_t result = acc - prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); }}", rs, rt);
case MMI_MSUBU:
if (rd != 0)
{
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); uint64_t prod = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); uint64_t result = acc - prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi, ctx->lo); uint64_t prod = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); uint64_t result = acc - prod; ctx->lo = Ps2SignExt32ToU64((uint32_t)result); ctx->hi = Ps2SignExt32ToU64((uint32_t)(result >> 32)); }}", rs, rt);
case MMI_MADD1:
if (rd != 0)
{
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi1, ctx->lo1); int64_t prod = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); int64_t result = acc + prod; ctx->lo1 = Ps2SignExt32ToU64((uint32_t)result); ctx->hi1 = Ps2SignExt32ToU64((uint32_t)(result >> 32)); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi1, ctx->lo1); int64_t prod = (int64_t)GPR_S32(ctx, {}) * (int64_t)GPR_S32(ctx, {}); int64_t result = acc + prod; ctx->lo1 = Ps2SignExt32ToU64((uint32_t)result); ctx->hi1 = Ps2SignExt32ToU64((uint32_t)(result >> 32)); }}", rs, rt);
case MMI_MADDU1:
if (rd != 0)
{
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi1, ctx->lo1); uint64_t prod = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); uint64_t result = acc + prod; ctx->lo1 = Ps2SignExt32ToU64((uint32_t)result); ctx->hi1 = Ps2SignExt32ToU64((uint32_t)(result >> 32)); SET_GPR_S32(ctx, {}, (int32_t)result); }}", rs, rt, rd);
}
return fmt::format("{{ uint64_t acc = Ps2HiLoToU64(ctx->hi1, ctx->lo1); uint64_t prod = (uint64_t)GPR_U32(ctx, {}) * (uint64_t)GPR_U32(ctx, {}); uint64_t result = acc + prod; ctx->lo1 = Ps2SignExt32ToU64((uint32_t)result); ctx->hi1 = Ps2SignExt32ToU64((uint32_t)(result >> 32)); }}", rs, rt);
case MMI_PLZCW:
return fmt::format(
@@ -2528,9 +2637,9 @@ namespace ps2recomp
std::string CodeGenerator::translatePCPYLD(const Instruction &inst)
{
// Copies lower 64 of rs to lower 64 of rd, lower 64 of rt to upper 64 of rd
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_unpacklo_epi64(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));",
inst.rd, inst.rs, inst.rt); // Order matters for unpack
// PCPYLD uses rs as the upper source and rt as the lower source.
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PCPYLD(GPR_VEC(ctx, {}), GPR_VEC(ctx, {})));",
inst.rd, inst.rs, inst.rt);
}
std::string CodeGenerator::translatePMADDH(const Instruction &inst)
@@ -2656,8 +2765,7 @@ namespace ps2recomp
std::string CodeGenerator::translatePEXEW(const Instruction &inst)
{
// Swaps words 0<->2 and 1<->3
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shuffle_epi32(GPR_VEC(ctx, {}), _MM_SHUFFLE(1,0,3,2)));",
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PEXEW(GPR_VEC(ctx, {})));",
inst.rd, inst.rs);
}
@@ -3546,42 +3654,9 @@ namespace ps2recomp
uint8_t rd = inst.rd;
uint8_t rs = inst.rs;
uint8_t rt = inst.rt;
// PS2 MMI QFSRV uses the lower 7 bits of the SA register.
return fmt::format(
"{{ \n"
" __m128i val_rt = GPR_VEC(ctx, {});\n" // Get rt (higher bits of the 256-bit value)
" __m128i val_rs = GPR_VEC(ctx, {});\n" // Get rs (lower bits of the 256-bit value)
" uint32_t shift_amount = ctx->sa & 0x7F; \n" // Get shift amount (0-127) from SA reg
// Perform the shift using 64-bit parts for easier SSE2 implementation
" uint64_t rt_hi = _mm_cvtsi128_si64(_mm_srli_si128(val_rt, 8));\n"
" uint64_t rt_lo = _mm_cvtsi128_si64(val_rt);\n"
" uint64_t rs_hi = _mm_cvtsi128_si64(_mm_srli_si128(val_rs, 8));\n"
" uint64_t rs_lo = _mm_cvtsi128_si64(val_rs);\n"
" __m128i result; \n"
" if (shift_amount == 0) {{ \n"
" result = val_rs; \n" // No shift, result is just rs
" }} else if (shift_amount < 64) {{ \n"
" uint64_t res_lo = (rs_lo >> shift_amount) | (rs_hi << (64 - shift_amount)); \n"
" uint64_t res_hi = (rs_hi >> shift_amount) | (rt_lo << (64 - shift_amount)); \n"
" result = _mm_set_epi64x(res_hi, res_lo); \n"
" }} else if (shift_amount == 64) {{ \n"
" result = _mm_set_epi64x(rt_lo, rs_hi); \n" // Shift exactly 64 bits
" }} else if (shift_amount < 128) {{ \n" // shift_amount > 64
" uint32_t sub_shift = shift_amount - 64; \n"
" uint64_t res_lo = (rs_hi >> sub_shift) | (rt_lo << (64 - sub_shift)); \n"
" uint64_t res_hi = (rt_lo >> sub_shift) | (rt_hi << (64 - sub_shift)); \n"
" result = _mm_set_epi64x(res_hi, res_lo); \n"
" }} else {{ // shift_amount >= 128 \n"
" uint32_t sub_shift = shift_amount - 128; \n"
" uint64_t res_lo = (rt_lo >> sub_shift) | (rt_hi << (64 - sub_shift)); \n" // Shift rt into result
" uint64_t res_hi = (rt_hi >> sub_shift); \n" // Shift hi part of rt
" result = _mm_set_epi64x(res_hi, res_lo); \n"
" }} \n"
" SET_GPR_VEC(ctx, {}, result); \n"
"}}",
rt, rs, rd);
// QFSRV semantics are centralized in runtime macro helpers.
return fmt::format("SET_GPR_VEC(ctx, {}, PS2_QFSRV(GPR_VEC(ctx, {}), GPR_VEC(ctx, {}), ctx->sa & 0x7F));",
rd, rs, rt);
}
std::string CodeGenerator::generateFunctionRegistration(const std::vector<Function> &functions,
+132
View File
@@ -112,6 +112,109 @@ namespace ps2recomp
config.mmioByInstructionAddress[instAddr] = mmioAddr;
}
}
if (data.contains("jump_tables") && data.at("jump_tables").is_table())
{
const auto &jumpTablesNode = data.at("jump_tables");
if (jumpTablesNode.contains("table") && jumpTablesNode.at("table").is_array())
{
const auto &tables = jumpTablesNode.at("table").as_array();
for (const auto &tableNode : tables)
{
if (!tableNode.is_table())
{
continue;
}
JumpTable table{};
if (tableNode.contains("address"))
{
const auto &addressValue = tableNode.at("address");
if (addressValue.is_string())
{
table.address = std::stoul(addressValue.as_string(), nullptr, 0);
}
else if (addressValue.is_integer())
{
table.address = static_cast<uint32_t>(addressValue.as_integer());
}
}
if (tableNode.contains("base_register"))
{
const auto &baseRegisterValue = tableNode.at("base_register");
if (baseRegisterValue.is_string())
{
table.baseRegister = std::stoul(baseRegisterValue.as_string(), nullptr, 0);
}
else if (baseRegisterValue.is_integer())
{
table.baseRegister = static_cast<uint32_t>(baseRegisterValue.as_integer());
}
}
if (table.address == 0u || !tableNode.contains("entries") || !tableNode.at("entries").is_array())
{
continue;
}
const auto &entries = tableNode.at("entries").as_array();
uint32_t fallbackIndex = 0u;
for (const auto &entryNode : entries)
{
if (!entryNode.is_table())
{
++fallbackIndex;
continue;
}
JumpTableEntry entry{};
entry.index = fallbackIndex;
if (entryNode.contains("index"))
{
const auto &indexValue = entryNode.at("index");
if (indexValue.is_string())
{
entry.index = std::stoul(indexValue.as_string(), nullptr, 0);
}
else if (indexValue.is_integer())
{
entry.index = static_cast<uint32_t>(indexValue.as_integer());
}
}
bool hasTarget = false;
if (entryNode.contains("target"))
{
const auto &targetValue = entryNode.at("target");
if (targetValue.is_string())
{
entry.target = std::stoul(targetValue.as_string(), nullptr, 0);
hasTarget = true;
}
else if (targetValue.is_integer())
{
entry.target = static_cast<uint32_t>(targetValue.as_integer());
hasTarget = true;
}
}
if (hasTarget)
{
table.entries.push_back(entry);
}
++fallbackIndex;
}
if (!table.entries.empty())
{
config.jumpTables.push_back(std::move(table));
}
}
}
}
}
catch (const std::exception &e)
{
@@ -152,6 +255,35 @@ namespace ps2recomp
data["mmio"] = mmioTable;
}
if (!config.jumpTables.empty())
{
toml::table jumpTables;
toml::array tableArray;
for (const auto &table : config.jumpTables)
{
toml::table tableNode;
std::ostringstream addressStream;
addressStream << "0x" << std::hex << table.address;
tableNode["address"] = addressStream.str();
tableNode["base_register"] = static_cast<int64_t>(table.baseRegister);
toml::array entries;
for (const auto &entry : table.entries)
{
toml::table entryNode;
entryNode["index"] = static_cast<int64_t>(entry.index);
std::ostringstream targetStream;
targetStream << "0x" << std::hex << entry.target;
entryNode["target"] = targetStream.str();
entries.push_back(entryNode);
}
tableNode["entries"] = entries;
tableArray.push_back(tableNode);
}
jumpTables["table"] = tableArray;
data["jump_tables"] = jumpTables;
}
toml::table patches;
toml::array instPatches;
for (const auto &[addr, value] : config.patches)
+156 -8
View File
@@ -143,6 +143,69 @@ namespace
return nullptr;
}
bool HasAnyExecutableSection(const std::vector<ps2recomp::Section> &sections)
{
for (const auto &section : sections)
{
if (section.isCode)
{
return true;
}
}
return false;
}
const ps2recomp::Section *FindFunctionSectionByAddress(const std::vector<ps2recomp::Section> &sections, uint32_t address)
{
const ps2recomp::Section *codeSection = FindCodeSectionByAddress(sections, address);
if (codeSection)
{
return codeSection;
}
// Some malformed/stripped ELFs may not carry executable section flags.
if (!HasAnyExecutableSection(sections))
{
return FindSectionByAddress(sections, address);
}
return nullptr;
}
uint32_t ClampFunctionEndToSection(const ps2recomp::Section *section, uint32_t start, uint32_t requestedEnd)
{
if (!section)
{
return requestedEnd;
}
const uint64_t sectionEnd64 = static_cast<uint64_t>(section->address) + static_cast<uint64_t>(section->size);
const uint32_t sectionEnd = (sectionEnd64 > 0xFFFFFFFFull)
? 0xFFFFFFFFu
: static_cast<uint32_t>(sectionEnd64);
uint32_t end = requestedEnd;
if (end == 0 || end > sectionEnd)
{
end = sectionEnd;
}
if (end <= start)
{
const uint64_t minimumEnd64 = static_cast<uint64_t>(start) + 4ull;
if (minimumEnd64 <= sectionEnd64)
{
end = static_cast<uint32_t>(minimumEnd64);
}
else
{
end = sectionEnd;
}
}
return end;
}
std::string MakeAutoFunctionName(uint32_t address)
{
char buffer[32]{};
@@ -503,7 +566,22 @@ namespace ps2recomp
continue;
}
const uint32_t symbolEnd = symbol.address + symbol.size;
const Section *functionSection = FindFunctionSectionByAddress(m_sections, symbol.address);
if (!functionSection)
{
continue;
}
const uint64_t symbolEnd64 = static_cast<uint64_t>(symbol.address) + static_cast<uint64_t>(symbol.size);
uint32_t symbolEnd = (symbolEnd64 > 0xFFFFFFFFull)
? 0xFFFFFFFFu
: static_cast<uint32_t>(symbolEnd64);
symbolEnd = ClampFunctionEndToSection(functionSection, symbol.address, symbolEnd);
if (symbolEnd <= symbol.address)
{
continue;
}
auto inserted = authoritativeEndByStart.emplace(symbol.address, symbolEnd);
if (!inserted.second && symbolEnd > inserted.first->second)
{
@@ -519,10 +597,22 @@ namespace ps2recomp
continue;
}
auto inserted = authoritativeEndByStart.emplace(extra.start, extra.end);
if (!inserted.second && extra.end > inserted.first->second)
const Section *functionSection = FindFunctionSectionByAddress(m_sections, extra.start);
if (!functionSection)
{
inserted.first->second = extra.end;
continue;
}
const uint32_t clampedEnd = ClampFunctionEndToSection(functionSection, extra.start, extra.end);
if (clampedEnd <= extra.start)
{
continue;
}
auto inserted = authoritativeEndByStart.emplace(extra.start, clampedEnd);
if (!inserted.second && clampedEnd > inserted.first->second)
{
inserted.first->second = clampedEnd;
}
}
@@ -566,6 +656,11 @@ namespace ps2recomp
return;
}
if (!FindFunctionSectionByAddress(m_sections, newFunction.start))
{
return;
}
const bool insideAuthoritativeRange = isInsideAuthoritativeRange(newFunction.start);
const bool hasOwnAuthoritativeRange = authoritativeEndByStart.contains(newFunction.start);
const bool hasAutoName = newFunction.name.empty() || IsAutoGeneratedName(newFunction.name);
@@ -585,7 +680,10 @@ namespace ps2recomp
auto authoritativeIt = authoritativeEndByStart.find(insertedFunction.start);
if (authoritativeIt != authoritativeEndByStart.end())
{
insertedFunction.end = authoritativeIt->second;
insertedFunction.end = ClampFunctionEndToSection(
FindFunctionSectionByAddress(m_sections, insertedFunction.start),
insertedFunction.start,
authoritativeIt->second);
}
return;
}
@@ -603,7 +701,10 @@ namespace ps2recomp
auto authoritativeIt = authoritativeEndByStart.find(existing.start);
if (authoritativeIt != authoritativeEndByStart.end())
{
existing.end = authoritativeIt->second;
existing.end = ClampFunctionEndToSection(
FindFunctionSectionByAddress(m_sections, existing.start),
existing.start,
authoritativeIt->second);
}
else if (newFunction.end > existing.end)
{
@@ -620,10 +721,24 @@ namespace ps2recomp
{
continue;
}
if (!FindFunctionSectionByAddress(m_sections, symbol.address))
{
continue;
}
Function func;
func.name = symbol.name;
func.start = symbol.address;
func.end = (symbol.size > 0) ? (symbol.address + symbol.size) : 0;
if (symbol.size > 0)
{
const uint64_t end64 = static_cast<uint64_t>(symbol.address) + static_cast<uint64_t>(symbol.size);
func.end = (end64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast<uint32_t>(end64);
}
else
{
func.end = 0;
}
func.isRecompiled = false;
func.isStub = false;
func.isSkipped = false;
@@ -656,7 +771,7 @@ namespace ps2recomp
continue;
}
const Section *section = FindSectionByAddress(m_sections, func.start);
const Section *section = FindFunctionSectionByAddress(m_sections, func.start);
uint32_t sectionEnd = section ? (section->address + section->size) : (func.start + 4);
uint32_t nextStart = sectionEnd;
@@ -846,6 +961,8 @@ namespace ps2recomp
}
int count = 0;
int skippedNonExecutable = 0;
int skippedInvalidRange = 0;
while (std::getline(file, line))
{
if (line.empty())
@@ -867,6 +984,20 @@ namespace ps2recomp
uint32_t start = std::stoul(startStr, nullptr, 0);
uint32_t end = std::stoul(endStr, nullptr, 0);
const Section *section = FindFunctionSectionByAddress(m_sections, start);
if (!section)
{
++skippedNonExecutable;
continue;
}
end = ClampFunctionEndToSection(section, start, end);
if (end <= start)
{
++skippedInvalidRange;
continue;
}
Function func{};
func.name = name;
func.start = start;
@@ -887,6 +1018,16 @@ namespace ps2recomp
if (count > 0)
{
std::cout << "Loaded " << count << " functions from Ghidra map" << std::endl;
if (skippedNonExecutable > 0)
{
std::cout << "Ignored " << skippedNonExecutable
<< " Ghidra function(s) outside executable sections." << std::endl;
}
if (skippedInvalidRange > 0)
{
std::cout << "Ignored " << skippedInvalidRange
<< " Ghidra function(s) with invalid ranges after section clamping." << std::endl;
}
std::sort(m_extraFunctions.begin(), m_extraFunctions.end(),
[](const Function &a, const Function &b)
@@ -908,6 +1049,13 @@ namespace ps2recomp
return true;
}
if (skippedNonExecutable > 0 || skippedInvalidRange > 0)
{
std::cout << "Loaded 0 functions from Ghidra map after filtering ("
<< skippedNonExecutable << " non-executable, "
<< skippedInvalidRange << " invalid range)." << std::endl;
}
return false;
}
+31 -3
View File
@@ -285,6 +285,13 @@ namespace ps2recomp
return std::nullopt;
};
auto isSimpleReturnThunkStart = [](const Instruction &inst) -> bool
{
return inst.opcode == OPCODE_SPECIAL &&
inst.function == SPECIAL_JR &&
inst.rs == 31;
};
auto findContainingFunction = [&](uint32_t address) -> const Function *
{
const Function *best = nullptr;
@@ -460,6 +467,16 @@ namespace ps2recomp
sliceEndAddress = nextStartOpt.value();
}
if (isSimpleReturnThunkStart(*sliceIt) &&
target <= (std::numeric_limits<uint32_t>::max() - 8u))
{
const uint32_t returnThunkEnd = target + 8u;
if (returnThunkEnd < sliceEndAddress)
{
sliceEndAddress = returnThunkEnd;
}
}
if (sliceEndAddress <= target)
{
continue;
@@ -837,6 +854,7 @@ namespace ps2recomp
}
m_codeGenerator->setRelocationCallNames(relocationCallNames);
m_codeGenerator->setBootstrapInfo(m_bootstrapInfo);
m_codeGenerator->setConfiguredJumpTables(m_config.jumpTables);
fs::create_directories(m_config.outputPath);
@@ -984,8 +1002,11 @@ namespace ps2recomp
std::string generatedName = m_codeGenerator->getFunctionName(function.start);
std::stringstream stub;
stub << "void " << generatedName
<< "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) {\n"
<< " const uint32_t __entryPc = ctx->pc;\n"
<< "(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime) {\n";
stub << "#ifdef _DEBUG\n";
stub << " PS_LOG_ENTRY(\"" << generatedName << "\");\n";
stub << "#endif\n";
stub << " const uint32_t __entryPc = ctx->pc;\n"
<< " ";
if (function.isSkipped)
@@ -1044,6 +1065,9 @@ namespace ps2recomp
combinedOutput << "#include \"ps2_recompiled_stubs.h\"\n";
combinedOutput << "#include \"ps2_syscalls.h\"\n";
combinedOutput << "#include \"ps2_stubs.h\"\n";
combinedOutput << "#ifdef _DEBUG\n";
combinedOutput << "#include \"ps2_log.h\"\n";
combinedOutput << "#endif\n";
combinedOutput << "\n";
for (const auto &function : m_functions)
@@ -1100,7 +1124,11 @@ namespace ps2recomp
std::stringstream stubFile;
stubFile << "#include \"ps2_runtime.h\"\n";
stubFile << "#include \"ps2_syscalls.h\"\n";
stubFile << "#include \"ps2_stubs.h\"\n\n";
stubFile << "#include \"ps2_stubs.h\"\n";
stubFile << "#ifdef _DEBUG\n";
stubFile << "#include \"ps2_log.h\"\n";
stubFile << "#endif\n";
stubFile << "\n";
stubFile << m_generatedStubs.at(function.start) << "\n";
code = stubFile.str();
}
+22 -6
View File
@@ -162,6 +162,15 @@ namespace ps2recomp
inst.isMultimedia = true;
}
if (inst.opcode == OPCODE_SPECIAL)
{
decodeSpecial(inst);
}
else if (inst.opcode == OPCODE_MMI)
{
decodeMMI(inst);
}
if (inst.isMMI || inst.isVU)
{
inst.isMultimedia = true;
@@ -258,16 +267,18 @@ namespace ps2recomp
inst.modificationInfo.modifiesGPR = false; // Doesn't modify rd
inst.modificationInfo.modifiesControl = true; // HI/LO
break;
case SPECIAL_MULT:
case SPECIAL_MULTU:
case SPECIAL_DIV:
case SPECIAL_DIVU:
// Multiplication and division operations
inst.modificationInfo.modifiesGPR = false; // Doesn't modify rd
inst.modificationInfo.modifiesControl = true; // HI/LO
break;
case SPECIAL_MULT:
case SPECIAL_MULTU:
// R5900 MULT/MULTU also write rd when rd != 0.
inst.modificationInfo.modifiesGPR = (inst.rd != 0);
inst.modificationInfo.modifiesControl = true; // HI/LO
break;
case SPECIAL_ADD:
case SPECIAL_ADDU:
case SPECIAL_SUB:
@@ -473,11 +484,17 @@ namespace ps2recomp
case MMI_MSUBU:
case MMI_MADD1:
case MMI_MADDU1:
inst.modificationInfo.modifiesGPR = (inst.rd != 0); // Also writes rd on R5900 I checkd on EE manual
inst.modificationInfo.modifiesControl = true;
break;
case MMI_MULT1:
case MMI_MULTU1:
inst.modificationInfo.modifiesGPR = (inst.rd != 0); // same
inst.modificationInfo.modifiesControl = true;
break;
case MMI_DIV1:
case MMI_DIVU1:
inst.modificationInfo.modifiesGPR = false; // Writes to HI/LO or HI1/LO1
inst.modificationInfo.modifiesGPR = false; // Writes to HI1/LO1
inst.modificationInfo.modifiesControl = true;
break;
case MMI_PMTHL:
@@ -490,7 +507,6 @@ namespace ps2recomp
decodePMFHL(inst);
break;
default:
// Unknown or unsupported MMI function
std::cerr << "Unknown MMI function: " << std::hex << mmiFunction << std::endl;
break;
}
+415 -32
View File
@@ -1,4 +1,4 @@
// Exports function addresses and names to CSV for PS2Recomp
// Exports PS2Recomp TOML config (+ optional CSV) from Ghidra
// @category PS2Recomp
import ghidra.app.script.GhidraScript;
@@ -9,46 +9,429 @@ import ghidra.program.model.listing.FunctionManager;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
public class ExportPS2Functions extends GhidraScript {
@Override
public void run() throws Exception {
File file = askFile("Choose output CSV file", "Save");
private static final Set<String> SYSTEM_FUNCTION_NAMES = new HashSet<>(Arrays.asList(
"entry", "_start", "_init", "_fini",
"abort", "exit", "_exit",
"_profiler_start", "_profiler_stop",
"__main", "__do_global_ctors", "__do_global_dtors",
"_GLOBAL__sub_I_", "_GLOBAL__sub_D_",
"__ctor_list", "__dtor_list", "_edata", "_end",
"etext", "__exidx_start", "__exidx_end",
"_ftext", "__bss_start", "__bss_start__",
"__bss_end__", "__end__", "_stack", "_dso_handle"
));
if (file == null) {
return;
private static final Set<String> DO_NOT_SKIP_OR_STUB = new HashSet<>(Arrays.asList(
"entry",
"_start",
"_init",
"topThread",
"cmd_sem_init"
));
private static final Set<String> PS2_API_PREFIXES = new HashSet<>(Arrays.asList(
"sce", "sif", "pad", "gs", "dma", "iop", "vif", "spu", "mc", "libc"
));
private static final Set<String> KNOWN_STDLIB_NAMES = new HashSet<>(Arrays.asList(
"printf", "sprintf", "snprintf", "fprintf", "vprintf", "vfprintf", "vsprintf", "vsnprintf",
"puts", "putchar", "getchar", "gets", "fgets", "fputs", "scanf", "fscanf", "sscanf",
"sprint", "sbprintf",
"malloc", "free", "calloc", "realloc", "aligned_alloc", "posix_memalign",
"memcpy", "memset", "memmove", "memcmp", "memcpy2", "memchr", "bcopy", "bzero",
"strcpy", "strncpy", "strcat", "strncat", "strcmp", "strncmp", "strlen", "strstr",
"strchr", "strrchr", "strdup", "strtok", "strtok_r", "strerror",
"fopen", "fclose", "fread", "fwrite", "fseek", "ftell", "rewind", "fflush",
"fgetc", "feof", "ferror", "clearerr", "fileno", "tmpfile", "remove", "rename",
"open", "close", "read", "write", "lseek", "stat", "fstat",
"atoi", "atol", "atoll", "atof", "strtol", "strtoul", "strtoll", "strtoull", "strtod", "strtof",
"rand", "srand", "random", "srandom", "drand48", "sqrt", "pow", "exp", "log", "log10",
"sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh",
"floor", "ceil", "fabs", "fmod", "frexp", "ldexp", "modf",
"time", "ctime", "clock", "difftime", "mktime", "localtime", "gmtime", "asctime", "strftime",
"gettimeofday", "nanosleep", "usleep",
"atexit", "system", "getpid", "fork", "waitpid",
"qsort", "bsearch", "abs", "div", "labs", "ldiv", "llabs", "lldiv",
"isalnum", "isalpha", "isdigit", "islower", "isupper", "isspace", "tolower", "toupper",
"setjmp", "longjmp", "getenv", "setenv", "unsetenv",
"perror", "fputc", "getc", "ungetc", "freopen", "setvbuf", "setbuf",
"strnlen", "strspn", "strcspn", "strcasecmp", "strncasecmp"
));
private static final Pattern C_LIB_PATTERN = Pattern.compile(
"^_*(mem|str|time|f?printf|f?scanf|malloc|free|calloc|realloc|atoi|itoa|rand|srand|abort|exit|atexit|getenv|system|bsearch|qsort|abs|labs|div|ldiv|mblen|mbtowc|wctomb|mbstowcs|wcstombs).*"
);
private static final Pattern KERNEL_RUNTIME_NAME_PATTERN = Pattern.compile(
"^(?:"
+ "(?:Create|Delete|Start|ExitDelete|Exit|Terminate|Suspend|Resume|Sleep|Wakeup|CancelWakeup|Change|Rotate|Release|Setup|Register|Query|Get|Set|Refer|Poll|Wait|Signal|Enable|Disable|Flush|Reset|Add|Init)"
+ "(?:Thread|Sema|EventFlag|Alarm|Intc|IntcHandler2|Dmac|DmacHandler2|OsdConfigParam|MemorySize|VSyncFlag|Heap|TLS|Status|Cache|Syscall|TLB|TLBEntry|GsCrt)"
+ "|EndOfHeap"
+ "|GsGetIMR|GsPutIMR"
+ "|Deci2Call"
+ "|Sif[A-Za-z0-9_]+"
+ "|i(?:SignalSema|PollSema|ReferSemaStatus|SetEventFlag|ClearEventFlag|PollEventFlag|ReferEventFlagStatus|WakeupThread|CancelWakeupThread|ReleaseWaitThread|SetAlarm|CancelAlarm|FlushCache|sceSifSetDma|sceSifSetDChain)"
+ ")$"
);
private static final class FunctionRecord {
String name;
long start;
long endExclusive;
long size;
}
private enum ClassificationKind {
STUB,
SKIP,
NONE
}
private static final class ClassificationResult {
final ClassificationKind kind;
final String name;
ClassificationResult(ClassificationKind kind, String name) {
this.kind = kind;
this.name = name;
}
}
private static String hex(long value) {
return String.format("0x%08X", value & 0xFFFFFFFFL);
}
private static String tomlString(String value) {
if (value == null) {
return "\"\"";
}
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
private static String normalizeOptionalLeadingUnderscore(String value) {
if (value == null || value.isEmpty()) {
return "";
}
return value.startsWith("_") && value.length() > 1 ? value.substring(1) : value;
}
private static boolean hasReliableSymbolName(String name) {
if (name == null || name.isEmpty()) {
return false;
}
int count = 0;
try (PrintWriter writer = new PrintWriter(file)) {
writer.println("Name,Start,End,Size");
if (name.startsWith("sub_") || name.startsWith("FUN_") || name.startsWith("func_") ||
name.startsWith("entry_") || name.startsWith("function_") || name.startsWith("LAB_")) {
return false;
}
FunctionManager fm = currentProgram.getFunctionManager();
FunctionIterator it = fm.getFunctions(true);
while (it.hasNext() && !monitor.isCancelled()) {
Function func = it.next();
String name = func.getName();
long start = func.getEntryPoint().getOffset();
AddressSetView body = func.getBody();
long maxAddr = body.getMaxAddress().getOffset();
long size = body.getNumAddresses();
writer.printf("%s,0x%08X,0x%08X,%d%n",
name,
start,
maxAddr + 1, // End address is exclusive
size
);
count++;
boolean hasAlpha = false;
boolean allHexOrPrefix = true;
for (int i = 0; i < name.length(); ++i) {
char c = name.charAt(i);
if (Character.isAlphabetic(c)) {
hasAlpha = true;
}
if (!(Character.digit(c, 16) >= 0 || c == 'x' || c == 'X' || c == '_')) {
allHexOrPrefix = false;
}
}
println(String.format("Exported %d functions to %s", count, file.getAbsolutePath()));
if (!hasAlpha) {
return false;
}
if ((name.startsWith("0x") || name.startsWith("0X")) && allHexOrPrefix) {
return false;
}
return true;
}
private static boolean hasPs2ApiPrefix(String name) {
if (name == null || name.isEmpty()) {
return false;
}
String base = normalizeOptionalLeadingUnderscore(name).toLowerCase();
for (String prefix : PS2_API_PREFIXES) {
if (base.startsWith(prefix)) {
return true;
}
}
return false;
}
private static boolean isSystemSymbolNameForHeuristics(String name) {
if (!hasReliableSymbolName(name)) {
return false;
}
return SYSTEM_FUNCTION_NAMES.contains(name) || name.startsWith("__") || name.startsWith(".");
}
private static boolean matchesWithOptionalLeadingUnderscoreAlias(String candidate, Set<String> names) {
if (candidate == null || candidate.isEmpty() || names == null || names.isEmpty()) {
return false;
}
if (names.contains(candidate)) {
return true;
}
String normalized = normalizeOptionalLeadingUnderscore(candidate);
if (!normalized.equals(candidate) && names.contains(normalized)) {
return true;
}
if (!candidate.startsWith("_") && names.contains("_" + candidate)) {
return true;
}
return false;
}
private static boolean isLibraryFunctionName(String name) {
if (name == null || name.isEmpty() || !hasReliableSymbolName(name)) {
return false;
}
String normalized = normalizeOptionalLeadingUnderscore(name);
if (KERNEL_RUNTIME_NAME_PATTERN.matcher(normalized).matches()) {
return true;
}
if (matchesWithOptionalLeadingUnderscoreAlias(normalized, KNOWN_STDLIB_NAMES)) {
return true;
}
if (hasPs2ApiPrefix(normalized)) {
return true;
}
return C_LIB_PATTERN.matcher(normalized).matches();
}
private static ClassificationResult classifyFunction(Function function) {
if (function == null) {
return new ClassificationResult(ClassificationKind.NONE, "");
}
String name = function.getName();
if (name == null || name.isEmpty() || DO_NOT_SKIP_OR_STUB.contains(name)) {
return new ClassificationResult(ClassificationKind.NONE, name == null ? "" : name);
}
if (function.isThunk()) {
if (isLibraryFunctionName(name)) {
return new ClassificationResult(ClassificationKind.STUB, name);
}
Function target = function.getThunkedFunction(true);
if (target != null) {
String targetName = target.getName();
if (isLibraryFunctionName(targetName)) {
return new ClassificationResult(ClassificationKind.STUB, targetName);
}
}
if (isSystemSymbolNameForHeuristics(name)) {
return new ClassificationResult(ClassificationKind.SKIP, name);
}
return new ClassificationResult(ClassificationKind.NONE, name);
}
if (isLibraryFunctionName(name)) {
return new ClassificationResult(ClassificationKind.STUB, name);
}
if (isSystemSymbolNameForHeuristics(name)) {
return new ClassificationResult(ClassificationKind.SKIP, name);
}
return new ClassificationResult(ClassificationKind.NONE, name);
}
private static String makeSelector(String name, long start, boolean includeAddress) {
if (includeAddress) {
return name + "@" + hex(start);
}
return name;
}
private static List<String> collectFunctionSelectors(
Set<String> names,
List<FunctionRecord> records,
boolean includeAddress
) {
List<FunctionRecord> ordered = new ArrayList<>(records);
ordered.sort(Comparator.comparingLong(r -> r.start));
List<String> selectors = new ArrayList<>();
Set<String> seenSelectors = new LinkedHashSet<>();
Set<String> coveredNames = new HashSet<>();
for (FunctionRecord record : ordered) {
if (record.name == null || !names.contains(record.name)) {
continue;
}
coveredNames.add(record.name);
String selector = makeSelector(record.name, record.start, includeAddress);
if (seenSelectors.add(selector)) {
selectors.add(selector);
}
}
if (includeAddress) {
List<String> unresolved = new ArrayList<>();
for (String name : names) {
if (!coveredNames.contains(name)) {
unresolved.add(name);
}
}
Collections.sort(unresolved);
for (String name : unresolved) {
System.out.println("Warning: unresolved selector name without address, omitting from TOML: " + name);
}
} else {
Collections.sort(selectors);
}
return selectors;
}
@Override
public void run() throws Exception {
File tomlFile = askFile("Choose output TOML config file", "Save");
if (tomlFile == null) {
return;
}
boolean exportCsv = askYesNo("Export CSV", "Also export compatibility CSV function map?");
File csvFile = null;
if (exportCsv) {
csvFile = askFile("Choose output CSV file", "Save");
if (csvFile == null) {
exportCsv = false;
}
}
FunctionManager fm = currentProgram.getFunctionManager();
FunctionIterator it = fm.getFunctions(true);
List<FunctionRecord> functionRecords = new ArrayList<>();
Set<String> stubNames = new LinkedHashSet<>();
Set<String> skipNames = new LinkedHashSet<>();
int uncategorizedCount = 0;
while (it.hasNext() && !monitor.isCancelled()) {
Function func = it.next();
AddressSetView body = func.getBody();
if (body == null || body.getNumAddresses() == 0) {
continue;
}
FunctionRecord record = new FunctionRecord();
record.name = func.getName();
record.start = func.getEntryPoint().getOffset();
record.endExclusive = body.getMaxAddress().getOffset() + 1L;
record.size = body.getNumAddresses();
functionRecords.add(record);
ClassificationResult classification = classifyFunction(func);
if (classification.kind == ClassificationKind.STUB) {
stubNames.add(classification.name);
} else if (classification.kind == ClassificationKind.SKIP) {
skipNames.add(classification.name);
} else {
uncategorizedCount++;
}
}
List<String> stubSelectors = collectFunctionSelectors(stubNames, functionRecords, true);
List<String> skipSelectors = collectFunctionSelectors(skipNames, functionRecords, true);
if (exportCsv && csvFile != null) {
try (PrintWriter writer = new PrintWriter(csvFile)) {
writer.println("Name,Start,End,Size");
functionRecords.sort(Comparator.comparingLong(r -> r.start));
for (FunctionRecord record : functionRecords) {
writer.printf("%s,0x%08X,0x%08X,%d%n",
record.name,
record.start,
record.endExclusive,
record.size
);
}
}
}
String programPath = currentProgram.getExecutablePath();
if (programPath == null) {
programPath = "";
}
File outputDir = tomlFile.getParentFile() == null ? new File("output") : new File(tomlFile.getParentFile(), "output");
String ghidraCsvPath = (exportCsv && csvFile != null) ? csvFile.getAbsolutePath() : "";
try (PrintWriter writer = new PrintWriter(tomlFile)) {
writer.println("# Auto-generated by ExportPS2Functions.java");
writer.println("#");
writer.println("# Classification policy (aligned with analyzer intent):");
writer.println("# - library/runtime names -> [general].stubs");
writer.println("# - system names -> [general].skip");
writer.println("# - others are left for recompilation");
writer.println();
writer.println("[general]");
writer.println("input = " + tomlString(programPath));
writer.println("output = " + tomlString(outputDir.getAbsolutePath()));
writer.println("ghidra_output = " + tomlString(ghidraCsvPath));
writer.println("single_file_output = false");
writer.println("patch_syscalls = false");
writer.println("patch_cop0 = true");
writer.println("patch_cache = true");
writer.println("stubs = [");
for (String selector : stubSelectors) {
writer.println(" " + tomlString(selector) + ",");
}
writer.println("]");
writer.println("skip = [");
for (String selector : skipSelectors) {
writer.println(" " + tomlString(selector) + ",");
}
writer.println("]");
writer.println();
writer.println("[ghidra_export]");
writer.println("function_count = " + functionRecords.size());
writer.println("stub_count = " + stubSelectors.size());
writer.println("skip_count = " + skipSelectors.size());
writer.println("uncategorized_count = " + uncategorizedCount);
writer.println("runtime_call_name_count = 0");
writer.println("runtime_call_source = \"regex_only\"");
}
if (exportCsv && csvFile != null) {
println(String.format("Exported %d functions to %s", functionRecords.size(), csvFile.getAbsolutePath()));
}
println("Using regex-only runtime/library classification (no ps2_call_list.h).");
println(String.format("Exported TOML config to %s", tomlFile.getAbsolutePath()));
}
}
+7
View File
@@ -20,13 +20,20 @@ FetchContent_MakeAvailable(raylib)
add_library(ps2_runtime STATIC
src/lib/game_overrides.cpp
src/lib/ps2_gif_arbiter.cpp
src/lib/ps2_audio.cpp
src/lib/ps2_audio_vag.cpp
src/lib/ps2_gs_gpu.cpp
src/lib/ps2_gs_rasterizer.cpp
src/lib/ps2_iop.cpp
src/lib/ps2_iop_audio.cpp
src/lib/ps2_memory.cpp
src/lib/ps2_pad.cpp
src/lib/ps2_runtime.cpp
src/lib/ps2_stubs.cpp
src/lib/ps2_syscalls.cpp
src/lib/ps2_vif1_interpreter.cpp
src/lib/ps2_vu1.cpp
)
file(GLOB RUNNER_SRC_FILES CONFIGURE_DEPENDS
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <string>
const char* getGameName(const std::string& gameId);
+48
View File
@@ -0,0 +1,48 @@
#ifndef PS2_AUDIO_H
#define PS2_AUDIO_H
#include <cstdint>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <vector>
class PS2AudioBackend
{
public:
PS2AudioBackend();
~PS2AudioBackend();
void onVagTransfer(const uint8_t *rdram, uint32_t srcAddr, uint32_t sizeBytes);
void onVagTransferFromBuffer(const uint8_t *data, uint32_t sizeBytes, uint32_t keyAddr);
void onSoundCommand(uint32_t sid, uint32_t rpcNum,
const uint8_t *sendBuf, uint32_t sendSize,
uint8_t *recvBuf, uint32_t recvSize);
void play(uint32_t sampleAddr, float pitch = 1.0f, float volume = 1.0f,
uint32_t voiceIndex = 0xFFFFFFFFu);
void stop(uint32_t voiceId);
void stopAll();
void setAudioReady(bool ready) { m_audioReady = ready; }
private:
struct DecodedSample
{
std::vector<int16_t> pcm;
uint32_t sampleRate = 44100;
};
struct Impl;
std::unique_ptr<Impl> m_impl;
bool m_audioReady = false;
uint32_t m_mostRecentSampleKey = 0;
std::vector<DecodedSample> m_loadOrderSamples;
std::unordered_map<uint32_t, DecodedSample> m_sampleBank;
std::mutex m_mutex;
void playDecodedSample(uint32_t sampleKey, DecodedSample &sample, float pitch, float volume,
bool isBgm = false);
void pruneFinishedSounds();
};
#endif
+32
View File
@@ -4,6 +4,7 @@
#define PS2_SYSCALL_LIST(X) \
X(FlushCache) \
X(iFlushCache) \
X(ResetEE) \
X(SetMemoryMode) \
\
@@ -17,13 +18,16 @@
X(ResumeThread) \
X(GetThreadId) \
X(ReferThreadStatus) \
X(iReferThreadStatus) \
X(SleepThread) \
X(WakeupThread) \
X(iWakeupThread) \
X(CancelWakeupThread) \
X(iCancelWakeupThread) \
X(ChangeThreadPriority) \
X(iChangeThreadPriority) \
X(RotateThreadReadyQueue) \
X(iRotateThreadReadyQueue)\
X(ReleaseWaitThread) \
X(iReleaseWaitThread) \
\
@@ -54,10 +58,20 @@
X(CancelAlarm) \
X(iCancelAlarm) \
\
X(AddIntcHandler) \
X(AddIntcHandler2) \
X(RemoveIntcHandler) \
X(AddDmacHandler) \
X(AddDmacHandler2) \
X(RemoveDmacHandler) \
X(EnableIntc) \
X(iEnableIntc) \
X(DisableIntc) \
X(iDisableIntc) \
X(EnableDmac) \
X(iEnableDmac) \
X(DisableDmac) \
X(iDisableDmac) \
\
X(SifStopModule) \
X(SifLoadModule) \
@@ -84,9 +98,13 @@
X(fioGetstat) \
X(fioRemove) \
\
X(SetGsCrt) \
X(GsSetCrt) \
X(GsGetIMR) \
X(iGsGetIMR) \
X(GsPutIMR) \
X(iGsPutIMR) \
X(SetVSyncFlag) \
X(GsSetVideoMode) \
\
X(GetOsdConfigParam) \
@@ -99,6 +117,9 @@
X(sceSifLoadModuleBuffer) \
\
X(SetupThread) \
X(EndOfHeap) \
X(GetMemorySize) \
X(Deci2Call) \
X(QueryBootMode) \
X(GetThreadTLS) \
X(RegisterExitHandler)
@@ -336,6 +357,16 @@
X(sceGsSyncV) \
X(sceGsSyncVCallback) \
X(sceGszbufaddr) \
X(sceeFontInit) \
X(sceeFontLoadFont) \
X(sceeFontPrintfAt) \
X(sceeFontPrintfAt2) \
X(sceeFontGenerateString) \
X(sceeFontClose) \
X(sceeFontSetColour) \
X(sceeFontSetMode) \
X(sceeFontSetFont) \
X(sceeFontSetScale) \
X(sceIoctl) \
X(sceIpuInit) \
X(sceIpuRestartDMA) \
@@ -611,4 +642,5 @@
X(syHwInit2) \
X(syMallocInit) \
X(syRtcInit) \
X(InitThread) \
/* Game/middleware */
+45
View File
@@ -0,0 +1,45 @@
#ifndef PS2_GIF_ARBITER_H
#define PS2_GIF_ARBITER_H
#include <cstdint>
#include <functional>
#include <vector>
enum class GifPathId : uint8_t
{
Path1 = 1,
Path2 = 2,
Path3 = 3,
};
struct GifArbiterPacket
{
GifPathId pathId;
bool path2DirectHl = false;
bool path3Image = false;
std::vector<uint8_t> data;
};
class GifArbiter
{
public:
using ProcessPacketFn = std::function<void(const uint8_t *, uint32_t)>;
GifArbiter() = default;
explicit GifArbiter(ProcessPacketFn processFn);
void setProcessPacketFn(ProcessPacketFn fn) { m_processFn = std::move(fn); }
void submit(GifPathId pathId, const uint8_t *data, uint32_t sizeBytes, bool path2DirectHl = false);
void drain();
private:
ProcessPacketFn m_processFn;
std::vector<GifArbiterPacket> m_queue;
static bool isImagePacket(const uint8_t *data, uint32_t sizeBytes);
static uint8_t pathPriority(GifPathId id);
};
#endif
+57
View File
@@ -0,0 +1,57 @@
#ifndef PS2_GS_COMMON_H
#define PS2_GS_COMMON_H
#include "ps2_gs_gpu.h"
#include <cstdint>
namespace GSInternal
{
static inline uint32_t bitsPerPixel(uint8_t psm)
{
switch (psm)
{
case GS_PSM_CT32:
case GS_PSM_Z32:
return 32;
case GS_PSM_CT24:
case GS_PSM_Z24:
return 32;
case GS_PSM_CT16:
case GS_PSM_CT16S:
case GS_PSM_Z16:
case GS_PSM_Z16S:
return 16;
case GS_PSM_T8:
case GS_PSM_T8H:
return 8;
case GS_PSM_T4:
case GS_PSM_T4HL:
case GS_PSM_T4HH:
return 4;
default:
return 32;
}
}
static inline uint32_t fbStride(uint32_t fbw, uint8_t psm)
{
uint32_t pixelsPerRow = fbw * 64u;
return pixelsPerRow * (bitsPerPixel(psm) / 8u);
}
static inline int clampInt(int v, int lo, int hi)
{
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
static inline uint8_t clampU8(int v)
{
if (v < 0) return 0;
if (v > 255) return 255;
return static_cast<uint8_t>(v);
}
}
#endif
+246 -37
View File
@@ -1,62 +1,271 @@
#ifndef PS2_GS_GPU_H
#define PS2_GS_GPU_H
#include "ps2_gs_rasterizer.h"
#include <cstdint>
#include <vector>
#include <cstring>
#include <mutex>
#include <atomic>
#include <vector>
enum GsGpuPrimType : uint8_t
enum GSPrimType : uint8_t
{
GS_GPU_POINT = 0,
GS_GPU_LINE = 1,
GS_GPU_TRIANGLE = 2,
GS_GPU_QUAD = 3,
GS_PRIM_POINT = 0,
GS_PRIM_LINE = 1,
GS_PRIM_LINESTRIP = 2,
GS_PRIM_TRIANGLE = 3,
GS_PRIM_TRISTRIP = 4,
GS_PRIM_TRIFAN = 5,
GS_PRIM_SPRITE = 6,
};
struct GsGpuVertex
enum GSPsm : uint8_t
{
float x, y, z; // screen-space position (after PS2 12.4 fixed → float)
uint8_t r, g, b, a; // vertex color
float u, v; // texture coords (for future use)
GS_PSM_CT32 = 0,
GS_PSM_CT24 = 1,
GS_PSM_CT16 = 2,
GS_PSM_CT16S = 10,
GS_PSM_T8 = 19,
GS_PSM_T4 = 20,
GS_PSM_T8H = 27,
GS_PSM_T4HL = 36,
GS_PSM_T4HH = 44,
GS_PSM_Z32 = 48,
GS_PSM_Z24 = 49,
GS_PSM_Z16 = 50,
GS_PSM_Z16S = 58,
};
struct GsGpuPrimitive
enum GSGifFormat : uint8_t
{
GsGpuPrimType type;
uint8_t vertexCount; // 1 (point), 2 (line), 3 (tri), 4 (quad)
GsGpuVertex verts[4];
GIF_FMT_PACKED = 0,
GIF_FMT_REGLIST = 1,
GIF_FMT_IMAGE = 2,
GIF_FMT_DISABLED = 3,
};
class GsGpuFrameData
enum GSRegId : uint8_t
{
GS_REG_PRIM = 0x00,
GS_REG_RGBAQ = 0x01,
GS_REG_ST = 0x02,
GS_REG_UV = 0x03,
GS_REG_XYZF2 = 0x04,
GS_REG_XYZ2 = 0x05,
GS_REG_TEX0_1 = 0x06,
GS_REG_TEX0_2 = 0x07,
GS_REG_CLAMP_1 = 0x08,
GS_REG_CLAMP_2 = 0x09,
GS_REG_FOG = 0x0A,
GS_REG_XYZF3 = 0x0C,
GS_REG_XYZ3 = 0x0D,
GS_REG_AD = 0x0F,
GS_REG_TEX1_1 = 0x14,
GS_REG_TEX1_2 = 0x15,
GS_REG_TEX2_1 = 0x16,
GS_REG_TEX2_2 = 0x17,
GS_REG_XYOFFSET_1 = 0x18,
GS_REG_XYOFFSET_2 = 0x19,
GS_REG_PRMODECONT = 0x1A,
GS_REG_PRMODE = 0x1B,
GS_REG_TEXCLUT = 0x1C,
GS_REG_SCANMSK = 0x22,
GS_REG_MIPTBP1_1 = 0x34,
GS_REG_MIPTBP1_2 = 0x35,
GS_REG_MIPTBP2_1 = 0x36,
GS_REG_MIPTBP2_2 = 0x37,
GS_REG_TEXA = 0x3B,
GS_REG_FOGCOL = 0x3D,
GS_REG_TEXFLUSH = 0x3F,
GS_REG_SCISSOR_1 = 0x40,
GS_REG_SCISSOR_2 = 0x41,
GS_REG_ALPHA_1 = 0x42,
GS_REG_ALPHA_2 = 0x43,
GS_REG_DIMX = 0x44,
GS_REG_DTHE = 0x45,
GS_REG_COLCLAMP = 0x46,
GS_REG_TEST_1 = 0x47,
GS_REG_TEST_2 = 0x48,
GS_REG_PABE = 0x49,
GS_REG_FBA_1 = 0x4A,
GS_REG_FBA_2 = 0x4B,
GS_REG_FRAME_1 = 0x4C,
GS_REG_FRAME_2 = 0x4D,
GS_REG_ZBUF_1 = 0x4E,
GS_REG_ZBUF_2 = 0x4F,
GS_REG_BITBLTBUF = 0x50,
GS_REG_TRXPOS = 0x51,
GS_REG_TRXREG = 0x52,
GS_REG_TRXDIR = 0x53,
GS_REG_HWREG = 0x54,
GS_REG_SIGNAL = 0x60,
GS_REG_FINISH = 0x61,
GS_REG_LABEL = 0x62,
};
struct GSVertex
{
float x, y, z;
uint8_t r, g, b, a;
float q;
float s, t;
uint16_t u, v;
uint8_t fog;
};
struct GSFrameReg
{
uint32_t fbp;
uint32_t fbw;
uint8_t psm;
uint32_t fbmsk;
};
struct GSScissorReg
{
uint16_t x0, x1, y0, y1;
};
struct GSTex0Reg
{
uint32_t tbp0;
uint8_t tbw;
uint8_t psm;
uint8_t tw;
uint8_t th;
uint8_t tcc;
uint8_t tfx;
uint32_t cbp;
uint8_t cpsm;
uint8_t csm;
uint8_t csa;
uint8_t cld;
};
struct GSXYOffsetReg
{
uint16_t ofx;
uint16_t ofy;
};
struct GSContext
{
GSFrameReg frame;
GSScissorReg scissor;
GSTex0Reg tex0;
GSXYOffsetReg xyoffset;
uint64_t zbuf;
uint64_t tex1;
uint64_t clamp;
uint64_t alpha;
uint64_t test;
uint64_t fba;
};
struct GSPrimReg
{
GSPrimType type;
bool iip;
bool tme;
bool fge;
bool abe;
bool aa1;
bool fst;
bool ctxt;
bool fix;
};
struct GSBitBltBuf
{
uint32_t sbp;
uint8_t sbw;
uint8_t spsm;
uint32_t dbp;
uint8_t dbw;
uint8_t dpsm;
};
struct GSTrxPos
{
uint16_t ssax, ssay;
uint16_t dsax, dsay;
uint8_t dir;
};
struct GSTrxReg
{
uint16_t rrw, rrh;
};
class GSRasterizer;
class GS
{
friend class GSRasterizer;
public:
GsGpuFrameData();
GS();
~GS() = default;
void pushPrimitive(const GsGpuPrimitive &prim);
void init(uint8_t *vram, uint32_t vramSize, struct GSRegisters *privRegs = nullptr);
void reset();
const std::vector<GsGpuPrimitive> &swapAndGetFront();
void processGIFPacket(const uint8_t *data, uint32_t sizeBytes);
void writeRegister(uint8_t regAddr, uint64_t value);
bool hasGpuPrimitives() const;
const uint8_t *lockDisplaySnapshot(uint32_t &outSize);
void unlockDisplaySnapshot();
uint32_t getLastDisplayBaseBytes() const;
void setScreenSize(uint32_t w, uint32_t h)
{
m_screenW = w;
m_screenH = h;
}
uint32_t screenWidth() const { return m_screenW; }
uint32_t screenHeight() const { return m_screenH; }
uint32_t consumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes);
void refreshDisplaySnapshot();
private:
std::vector<GsGpuPrimitive> m_buffers[2];
int m_backIdx = 0; // index into m_buffers for the current write target
mutable std::mutex m_mutex;
std::atomic<bool> m_hasData{false};
uint32_t m_screenW = 640;
uint32_t m_screenH = 448;
void snapshotVRAM();
void writeRegisterPacked(uint8_t regDesc, uint64_t lo, uint64_t hi);
void vertexKick(bool drawing);
void processImageData(const uint8_t *data, uint32_t sizeBytes);
void performLocalToHostToBuffer();
GSContext &activeContext();
uint8_t *m_vram = nullptr;
uint32_t m_vramSize = 0;
struct GSRegisters *m_privRegs = nullptr;
GSContext m_ctx[2];
GSPrimReg m_prim{};
uint8_t m_curR = 0x80, m_curG = 0x80, m_curB = 0x80, m_curA = 0x80;
float m_curQ = 1.0f;
float m_curS = 0.0f, m_curT = 0.0f;
uint16_t m_curU = 0, m_curV = 0;
uint8_t m_curFog = 0;
bool m_prmodecont = true;
GSBitBltBuf m_bitbltbuf{};
GSTrxPos m_trxpos{};
GSTrxReg m_trxreg{};
uint32_t m_trxdir = 3;
uint32_t m_hwregX = 0;
uint32_t m_hwregY = 0;
static constexpr int kMaxVerts = 6;
GSVertex m_vtxQueue[kMaxVerts];
int m_vtxCount = 0;
int m_vtxIndex = 0;
std::vector<uint8_t> m_displaySnapshot;
std::mutex m_snapshotMutex;
uint32_t m_lastDisplayBaseBytes = 0;
std::vector<uint8_t> m_localToHostBuffer;
size_t m_localToHostReadPos = 0;
GSRasterizer m_rasterizer;
};
GsGpuFrameData &gsGpuGetFrameData();
bool gsGpuRenderFrame();
#endif // PS2_GS_GPU_H
#endif
+59
View File
@@ -0,0 +1,59 @@
#ifndef PS2_GS_PSMT4_H
#define PS2_GS_PSMT4_H
#include <cstdint>
namespace GSPSMT4
{
static const uint8_t blockTable4[8][4] = {
{ 0, 2, 8, 10 },
{ 1, 3, 9, 11 },
{ 4, 6, 12, 14 },
{ 5, 7, 13, 15 },
{ 16, 18, 24, 26 },
{ 17, 19, 25, 27 },
{ 20, 22, 28, 30 },
{ 21, 23, 29, 31 },
};
static const uint16_t columnTable4[16][32] = {
{ 0, 8, 32, 40, 64, 72, 96, 104, 2, 10, 34, 42, 66, 74, 98, 106, 4, 12, 36, 44, 68, 76, 100, 108, 6, 14, 38, 46, 70, 78, 102, 110 },
{ 16, 24, 48, 56, 80, 88, 112, 120, 18, 26, 50, 58, 82, 90, 114, 122, 20, 28, 52, 60, 84, 92, 116, 124, 22, 30, 54, 62, 86, 94, 118, 126 },
{ 65, 73, 97, 105, 1, 9, 33, 41, 67, 75, 99, 107, 3, 11, 35, 43, 69, 77, 101, 109, 5, 13, 37, 45, 71, 79, 103, 111, 7, 15, 39, 47 },
{ 81, 89, 113, 121, 17, 25, 49, 57, 83, 91, 115, 123, 19, 27, 51, 59, 85, 93, 117, 125, 21, 29, 53, 61, 87, 95, 119, 127, 23, 31, 55, 63 },
{ 192, 200, 224, 232, 128, 136, 160, 168, 194, 202, 226, 234, 130, 138, 162, 170, 196, 204, 228, 236, 132, 140, 164, 172, 198, 206, 230, 238, 134, 142, 166, 174 },
{ 208, 216, 240, 248, 144, 152, 176, 184, 210, 218, 242, 250, 146, 154, 178, 186, 212, 220, 244, 252, 148, 156, 180, 188, 214, 222, 246, 254, 150, 158, 182, 190 },
{ 129, 137, 161, 169, 193, 201, 225, 233, 131, 139, 163, 171, 195, 203, 227, 235, 133, 141, 165, 173, 197, 205, 229, 237, 135, 143, 167, 175, 199, 207, 231, 239 },
{ 145, 153, 177, 185, 209, 217, 241, 249, 147, 155, 179, 187, 211, 219, 243, 251, 149, 157, 181, 189, 213, 221, 245, 253, 151, 159, 183, 191, 215, 223, 247, 255 },
{ 256, 264, 288, 296, 320, 328, 352, 360, 258, 266, 290, 298, 322, 330, 354, 362, 260, 268, 292, 300, 324, 332, 356, 364, 262, 270, 294, 302, 326, 334, 358, 366 },
{ 272, 280, 304, 312, 336, 344, 368, 376, 274, 282, 306, 314, 338, 346, 370, 378, 276, 284, 308, 316, 340, 348, 372, 380, 278, 286, 310, 318, 342, 350, 374, 382 },
{ 321, 329, 353, 361, 257, 265, 289, 297, 323, 331, 355, 363, 259, 267, 291, 299, 325, 333, 357, 365, 261, 269, 293, 301, 327, 335, 359, 367, 263, 271, 295, 303 },
{ 337, 345, 369, 377, 273, 281, 305, 313, 339, 347, 371, 379, 275, 283, 307, 315, 341, 349, 373, 381, 277, 285, 309, 317, 343, 351, 375, 383, 279, 287, 311, 319 },
{ 448, 456, 480, 488, 384, 392, 416, 424, 450, 458, 482, 490, 386, 394, 418, 426, 452, 460, 484, 492, 388, 396, 420, 428, 454, 462, 486, 494, 390, 398, 422, 430 },
{ 464, 472, 496, 504, 400, 408, 432, 440, 466, 474, 498, 506, 402, 410, 434, 442, 468, 476, 500, 508, 404, 412, 436, 444, 470, 478, 502, 510, 406, 414, 438, 446 },
{ 385, 393, 417, 425, 449, 457, 481, 489, 387, 395, 419, 427, 451, 459, 483, 491, 389, 397, 421, 429, 453, 461, 485, 493, 391, 399, 423, 431, 455, 463, 487, 495 },
{ 401, 409, 433, 441, 465, 473, 497, 505, 403, 411, 435, 443, 467, 475, 499, 507, 405, 413, 437, 445, 469, 477, 501, 509, 407, 415, 439, 447, 471, 479, 503, 511 },
};
inline uint32_t blockIdPSMT4(uint32_t block, uint32_t width, uint32_t x, uint32_t y)
{
return block + ((y >> 2) & ~0x1Fu) * (width >> 7) + ((x >> 2) & ~0x1Fu)
+ blockTable4[(y >> 4) & 7][(x >> 5) & 3];
}
inline uint32_t addrPSMT4(uint32_t block, uint32_t width, uint32_t x, uint32_t y)
{
uint32_t page = (block >> 5) + (y >> 7) * (width >> 1) + (x >> 7);
uint32_t blk = block & 0x1Fu;
uint32_t yy = y & 0x7Fu;
uint32_t xx = x & 0x7Fu;
uint32_t blockId = blk + blockTable4[(yy >> 4) & 7][(xx >> 5) & 3];
uint32_t column = columnTable4[yy & 15u][xx & 31u];
uint32_t offset = (blockId << 9) + column;
return (page << 14) + offset;
}
}
#endif
+24
View File
@@ -0,0 +1,24 @@
#ifndef PS2_GS_RASTERIZER_H
#define PS2_GS_RASTERIZER_H
#include <cstdint>
class GS;
class GSRasterizer
{
public:
void drawPrimitive(GS *gs);
void writePixel(GS *gs, int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a);
uint32_t sampleTexture(GS *gs, float s, float t, uint16_t u, uint16_t v);
uint32_t readTexelPSMCT32(GS *gs, uint32_t tbp0, uint32_t tbw, int texU, int texV);
uint32_t readTexelPSMT4(GS *gs, uint32_t tbp0, uint32_t tbw, int texU, int texV);
uint32_t lookupCLUT(GS *gs, uint8_t index, uint32_t cbp, uint8_t cpsm, uint8_t csa);
private:
void drawSprite(GS *gs);
void drawTriangle(GS *gs);
void drawLine(GS *gs);
};
#endif
+25
View File
@@ -0,0 +1,25 @@
#ifndef PS2_IOP_H
#define PS2_IOP_H
#include <cstdint>
constexpr uint32_t IOP_SID_LIBSD = 0x80000701u;
class ps2_iop
{
public:
ps2_iop();
~ps2_iop() = default;
void init(uint8_t *rdram);
void reset();
bool handleRPC(uint32_t sid, uint32_t rpcNum,
uint32_t sendBufAddr, uint32_t sendSize,
uint32_t recvBufAddr, uint32_t recvSize);
private:
uint8_t *m_rdram = nullptr;
};
#endif
+15
View File
@@ -0,0 +1,15 @@
#ifndef PS2_IOP_AUDIO_H
#define PS2_IOP_AUDIO_H
#include <cstdint>
class PS2Runtime;
namespace ps2_iop_audio
{
void handleLibSdRpc(PS2Runtime *runtime, uint32_t sid, uint32_t rpcNum,
const uint8_t *sendBuf, uint32_t sendSize,
uint8_t *recvBuf, uint32_t recvSize);
}
#endif
+78
View File
@@ -0,0 +1,78 @@
#ifndef PS2_LOG_H
#define PS2_LOG_H
#include <fstream>
#include <iostream>
#include <filesystem>
#if defined(_WIN32)
#define NOMINMAX
#include <windows.h>
#endif
#ifdef _DEBUG
namespace ps2_log
{
inline std::string log_path()
{
static std::string path;
if (path.empty())
{
#if defined(_WIN32)
char buf[MAX_PATH];
if (GetModuleFileNameA(nullptr, buf, sizeof(buf)))
path = (std::filesystem::path(buf).parent_path() / "ps2_log.txt").string();
#endif
if (path.empty())
path = (std::filesystem::current_path() / "ps2_log.txt").string();
}
return path;
}
inline std::ostream &log_stream()
{
static std::ofstream f(log_path(), std::ios::out);
return f.is_open() ? f : std::cerr;
}
inline int &depth()
{
static thread_local int d = 0;
return d;
}
inline void log_entry(const char *name)
{
for (int i = 0; i < depth(); ++i)
log_stream() << '\t';
log_stream() << ">> " << name << " enter\n";
log_stream().flush();
depth()++;
}
inline void log_exit(const char *name)
{
depth()--;
for (int i = 0; i < depth(); ++i)
log_stream() << '\t';
log_stream() << "<< " << name << " exit\n";
log_stream().flush();
}
inline void print_saved_location()
{
std::cout << "[PS2 LOG] Logs saved at " << log_path() << std::endl;
}
}
#define PS_LOG_ENTRY(name) \
ps2_log::log_entry(name); \
struct _ps2_log_guard_ { const char *_n; _ps2_log_guard_(const char *n) : _n(n) {} \
~_ps2_log_guard_() { ps2_log::log_exit(_n); } } _ps2_log_guard_(name)
#else
namespace ps2_log
{
inline void print_saved_location() {}
}
#define PS_LOG_ENTRY(name) ((void)0)
#endif
#endif
+42 -26
View File
@@ -3,10 +3,13 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <vector>
#include <unordered_map>
#include <atomic>
#include <iostream>
#include "ps2_gif_arbiter.h"
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(USE_SSE2NEON)
@@ -267,36 +270,33 @@ public:
bool writeIORegister(uint32_t address, uint32_t value);
uint32_t readIORegister(uint32_t address);
// Software GS/VIF path used by GIF and VIF1 DMA channels.
using GifPacketCallback = std::function<void(const uint8_t *, uint32_t)>;
void setGifPacketCallback(GifPacketCallback cb) { m_gifPacketCallback = std::move(cb); }
void setGifArbiter(GifArbiter *arbiter) { m_gifArbiter = arbiter; }
using Vu1MscalCallback = std::function<void(uint32_t startPC, uint32_t itop)>;
void setVu1MscalCallback(Vu1MscalCallback cb) { m_vu1MscalCallback = std::move(cb); }
uint8_t *getVU1Code() { return m_vu1Code; }
const uint8_t *getVU1Code() const { return m_vu1Code; }
uint8_t *getVU1Data() { return m_vu1Data; }
const uint8_t *getVU1Data() const { return m_vu1Data; }
bool isPath3Masked() const { return m_path3Masked; }
void flushMaskedPath3Packets(bool drainImmediately = true);
void submitGifPacket(GifPathId pathId, const uint8_t *data, uint32_t sizeBytes, bool drainImmediately = true, bool path2DirectHl = false);
void processGIFPacket(uint32_t srcPhysAddr, uint32_t qwCount);
void processGIFPacket(const uint8_t *data, uint32_t sizeBytes);
void processVIF1Data(uint32_t srcPhysAddr, uint32_t sizeBytes);
void processVIF1Data(const uint8_t *data, uint32_t sizeBytes);
void processPendingTransfers();
// Poll DMA registers from rdram shadow (workaround for KSEG1 fast-path bypass)
int pollDmaRegisters();
struct GSDrawContext
{
uint64_t bitbltbuf = 0;
uint64_t trxpos = 0;
uint64_t trxreg = 0;
uint64_t trxdir = 0;
bool xferActive = false;
uint32_t xferDestX = 0;
uint32_t xferDestY = 0;
uint32_t xferWidth = 0;
uint32_t xferHeight = 0;
uint32_t xferDBP = 0;
uint32_t xferDBW = 0;
uint32_t xferDPSM = 0;
uint32_t xferPixelsWritten = 0;
uint32_t gifTagsProcessed = 0;
uint32_t adWrites = 0;
uint32_t imageTransfers = 0;
uint32_t primitivesDrawn = 0;
};
// Track code modifications for self-modifying code
void registerCodeRegion(uint32_t start, uint32_t end);
bool isCodeAddress(uint32_t address) const;
bool isCodeModified(uint32_t address, uint32_t size);
void clearModifiedFlag(uint32_t address, uint32_t size);
@@ -305,8 +305,6 @@ public:
const GSRegisters &gs() const { return gs_regs; }
uint8_t *getGSVRAM() { return m_gsVRAM; }
const uint8_t *getGSVRAM() const { return m_gsVRAM; }
GSDrawContext &gsDrawCtx() { return m_gsDrawCtx; }
const GSDrawContext &gsDrawCtx() const { return m_gsDrawCtx; }
bool hasSeenGifCopy() const { return m_seenGifCopy; }
// Main RAM (32MB)
uint8_t *m_rdram;
@@ -327,7 +325,6 @@ public:
// Registers
GSRegisters gs_regs;
GSDrawContext m_gsDrawCtx;
uint8_t *m_gsVRAM;
VIFRegisters vif0_regs;
VIFRegisters vif1_regs;
@@ -344,6 +341,25 @@ public:
std::vector<TLBEntry> m_tlbEntries;
GifPacketCallback m_gifPacketCallback;
GifArbiter *m_gifArbiter = nullptr;
Vu1MscalCallback m_vu1MscalCallback;
uint8_t *m_vu1Code = nullptr;
uint8_t *m_vu1Data = nullptr;
bool m_path3Masked = false;
std::vector<std::vector<uint8_t>> m_path3MaskedFifo;
struct PendingTransfer
{
bool fromScratchpad = false;
uint32_t srcAddr = 0;
uint32_t qwc = 0;
std::vector<uint8_t> chainData;
};
std::vector<PendingTransfer> m_pendingGifTransfers;
std::vector<PendingTransfer> m_pendingVif1Transfers;
struct CodeRegion
{
uint32_t start;
+16
View File
@@ -0,0 +1,16 @@
#ifndef PS2_PAD_H
#define PS2_PAD_H
#include <cstddef>
#include <cstdint>
class PSPadBackend
{
public:
PSPadBackend() = default;
~PSPadBackend() = default;
bool readState(int port, int slot, uint8_t *data, size_t size);
};
#endif
+29 -3
View File
@@ -21,7 +21,13 @@
#include <iostream>
#include <iomanip>
#include "ps2_gif_arbiter.h"
#include "ps2_memory.h"
#include "ps2_gs_gpu.h"
#include "ps2_iop.h"
#include "ps2_vu1.h"
#include "ps2_audio.h"
#include "ps2_pad.h"
enum PS2Exception
{
@@ -196,9 +202,9 @@ inline void setReturnU64(R5900Context *ctx, uint64_t value)
ctx->r[3] = _mm_set_epi64x(0, static_cast<int64_t>(static_cast<uint32_t>(value >> 32)));
}
inline constexpr uint32_t PS2_PATH_WATCH_ADDR = 0x00369F2Fu;
inline constexpr uint32_t PS2_PATH_WATCH_BYTES = 32u;
inline constexpr uint32_t PS2_PATH_WATCH_MAX_LOGS = 512u;
inline constexpr uint32_t PS2_PATH_WATCH_ADDR = 0x01EFFFA0u;
inline constexpr uint32_t PS2_PATH_WATCH_BYTES = 0x200u;
inline constexpr uint32_t PS2_PATH_WATCH_MAX_LOGS = 4096u;
inline std::atomic<uint32_t> g_ps2PathWatchLogCount{0};
inline uint32_t ps2PathWatchPhysAddr()
@@ -457,6 +463,20 @@ public:
inline PS2Memory &memory() { return m_memory; }
inline const PS2Memory &memory() const { return m_memory; }
inline GS &gs() { return m_gs; }
inline const GS &gs() const { return m_gs; }
inline GifArbiter &gifArbiter() { return m_gifArbiter; }
inline const GifArbiter &gifArbiter() const { return m_gifArbiter; }
inline VU1Interpreter &vu1() { return m_vu1; }
inline const VU1Interpreter &vu1() const { return m_vu1; }
inline ps2_iop &iop() { return m_iop; }
inline const ps2_iop &iop() const { return m_iop; }
inline PS2AudioBackend &audioBackend() { return m_audioBackend; }
inline const PS2AudioBackend &audioBackend() const { return m_audioBackend; }
inline PSPadBackend &padBackend() { return m_padBackend; }
inline const PSPadBackend &padBackend() const { return m_padBackend; }
private:
struct GuestHeapBlock
{
@@ -481,6 +501,12 @@ private:
private:
PS2Memory m_memory;
GifArbiter m_gifArbiter;
GS m_gs;
ps2_iop m_iop;
PS2AudioBackend m_audioBackend;
PSPadBackend m_padBackend;
VU1Interpreter m_vu1;
R5900Context m_cpuContext;
mutable std::mutex m_guestHeapMutex;
std::vector<GuestHeapBlock> m_guestHeapBlocks;
+173 -48
View File
@@ -152,6 +152,11 @@ static inline uint32_t ps2_plzcw32(uint32_t x)
// Fast path: Direct RDRAM access (masked).
// Slow path: Full runtime->Load/Store
static inline bool Ps2FastRangeIsContiguous(uint32_t offset, uint32_t bytes)
{
return offset <= (PS2_RAM_SIZE - bytes);
}
static inline uint8_t Ps2FastRead8(const uint8_t *rdram, uint32_t addr)
{
return rdram[addr & PS2_RAM_MASK];
@@ -159,29 +164,81 @@ static inline uint8_t Ps2FastRead8(const uint8_t *rdram, uint32_t addr)
static inline uint16_t Ps2FastRead16(const uint8_t *rdram, uint32_t addr)
{
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(uint16_t)))
{
uint8_t wrapped[sizeof(uint16_t)];
for (uint32_t i = 0; i < sizeof(uint16_t); ++i)
{
wrapped[i] = rdram[(offset + i) & PS2_RAM_MASK];
}
uint16_t value;
std::memcpy(&value, wrapped, sizeof(value));
return value;
}
uint16_t value;
std::memcpy(&value, rdram + (addr & PS2_RAM_MASK), sizeof(value));
std::memcpy(&value, rdram + offset, sizeof(value));
return value;
}
static inline uint32_t Ps2FastRead32(const uint8_t *rdram, uint32_t addr)
{
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(uint32_t)))
{
uint8_t wrapped[sizeof(uint32_t)];
for (uint32_t i = 0; i < sizeof(uint32_t); ++i)
{
wrapped[i] = rdram[(offset + i) & PS2_RAM_MASK];
}
uint32_t value;
std::memcpy(&value, wrapped, sizeof(value));
return value;
}
uint32_t value;
std::memcpy(&value, rdram + (addr & PS2_RAM_MASK), sizeof(value));
std::memcpy(&value, rdram + offset, sizeof(value));
return value;
}
static inline uint64_t Ps2FastRead64(const uint8_t *rdram, uint32_t addr)
{
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(uint64_t)))
{
uint8_t wrapped[sizeof(uint64_t)];
for (uint32_t i = 0; i < sizeof(uint64_t); ++i)
{
wrapped[i] = rdram[(offset + i) & PS2_RAM_MASK];
}
uint64_t value;
std::memcpy(&value, wrapped, sizeof(value));
return value;
}
uint64_t value;
std::memcpy(&value, rdram + (addr & PS2_RAM_MASK), sizeof(value));
std::memcpy(&value, rdram + offset, sizeof(value));
return value;
}
static inline __m128i Ps2FastRead128(const uint8_t *rdram, uint32_t addr)
{
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(__m128i)))
{
alignas(16) uint8_t wrapped[sizeof(__m128i)];
for (uint32_t i = 0; i < sizeof(__m128i); ++i)
{
wrapped[i] = rdram[(offset + i) & PS2_RAM_MASK];
}
__m128i value;
std::memcpy(&value, wrapped, sizeof(value));
return value;
}
__m128i value;
std::memcpy(&value, rdram + (addr & PS2_RAM_MASK), sizeof(value));
std::memcpy(&value, rdram + offset, sizeof(value));
return value;
}
@@ -192,22 +249,66 @@ static inline void Ps2FastWrite8(uint8_t *rdram, uint32_t addr, uint8_t value)
static inline void Ps2FastWrite16(uint8_t *rdram, uint32_t addr, uint16_t value)
{
std::memcpy(rdram + (addr & PS2_RAM_MASK), &value, sizeof(value));
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(uint16_t)))
{
uint8_t wrapped[sizeof(uint16_t)];
std::memcpy(wrapped, &value, sizeof(value));
for (uint32_t i = 0; i < sizeof(uint16_t); ++i)
{
rdram[(offset + i) & PS2_RAM_MASK] = wrapped[i];
}
return;
}
std::memcpy(rdram + offset, &value, sizeof(value));
}
static inline void Ps2FastWrite32(uint8_t *rdram, uint32_t addr, uint32_t value)
{
std::memcpy(rdram + (addr & PS2_RAM_MASK), &value, sizeof(value));
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(uint32_t)))
{
uint8_t wrapped[sizeof(uint32_t)];
std::memcpy(wrapped, &value, sizeof(value));
for (uint32_t i = 0; i < sizeof(uint32_t); ++i)
{
rdram[(offset + i) & PS2_RAM_MASK] = wrapped[i];
}
return;
}
std::memcpy(rdram + offset, &value, sizeof(value));
}
static inline void Ps2FastWrite64(uint8_t *rdram, uint32_t addr, uint64_t value)
{
std::memcpy(rdram + (addr & PS2_RAM_MASK), &value, sizeof(value));
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(uint64_t)))
{
uint8_t wrapped[sizeof(uint64_t)];
std::memcpy(wrapped, &value, sizeof(value));
for (uint32_t i = 0; i < sizeof(uint64_t); ++i)
{
rdram[(offset + i) & PS2_RAM_MASK] = wrapped[i];
}
return;
}
std::memcpy(rdram + offset, &value, sizeof(value));
}
static inline void Ps2FastWrite128(uint8_t *rdram, uint32_t addr, __m128i value)
{
std::memcpy(rdram + (addr & PS2_RAM_MASK), &value, sizeof(value));
const uint32_t offset = addr & PS2_RAM_MASK;
if (!Ps2FastRangeIsContiguous(offset, sizeof(__m128i)))
{
alignas(16) uint8_t wrapped[sizeof(__m128i)];
std::memcpy(wrapped, &value, sizeof(value));
for (uint32_t i = 0; i < sizeof(__m128i); ++i)
{
rdram[(offset + i) & PS2_RAM_MASK] = wrapped[i];
}
return;
}
std::memcpy(rdram + offset, &value, sizeof(value));
}
#define FAST_READ8(addr) Ps2FastRead8(rdram, (uint32_t)(addr))
@@ -304,16 +405,20 @@ static inline void Ps2FastWrite128(uint8_t *rdram, uint32_t addr, __m128i value)
} \
} while (0)
#define WRITE128(addr, val) \
do \
{ \
uint32_t _addr = (addr); \
if (PS2Runtime::isSpecialAddress(_addr)) \
runtime->Store128(rdram, ctx, _addr, (val)); \
else \
{ \
FAST_WRITE128(_addr, (val)); \
} \
#define WRITE128(addr, val) \
do \
{ \
uint32_t _addr = (addr); \
__m128i _value = (val); \
if (PS2Runtime::isSpecialAddress(_addr)) \
runtime->Store128(rdram, ctx, _addr, _value); \
else \
{ \
const uint64_t _lo = static_cast<uint64_t>(PS2_EXTRACT_EPI64_0(_value)); \
const uint64_t _hi = static_cast<uint64_t>(PS2_EXTRACT_EPI64_1(_value)); \
ps2TraceGuestWrite(rdram, _addr, 16u, _lo, _hi, "WRITE128", ctx); \
FAST_WRITE128(_addr, _value); \
} \
} while (0)
// Packed Compare Greater Than (PCGT)
@@ -330,13 +435,13 @@ static inline void Ps2FastWrite128(uint8_t *rdram, uint32_t addr, __m128i value)
#define PS2_PABSW(a) _mm_abs_epi32((__m128i)(a))
#define PS2_PABSH(a) _mm_abs_epi16((__m128i)(a))
#define PS2_PABSB(a) _mm_abs_epi8((__m128i)(a))
// Packed Pack (PPAC) - Packs larger elements into smaller ones
inline __m128i ps2_paddu32(__m128i a, __m128i b)
{
__m128i sum = _mm_add_epi32(a, b);
__m128i overflow = _mm_cmpgt_epi32(_mm_xor_si128(a, _mm_set1_epi32(INT32_MIN)),
_mm_xor_si128(sum, _mm_set1_epi32(INT32_MIN)));
_mm_xor_si128(sum, _mm_set1_epi32(INT32_MIN)));
return _mm_or_si128(sum, overflow); // overflow lanes become all-1s
}
inline __m128i ps2_psubu32(__m128i a, __m128i b)
@@ -344,7 +449,7 @@ inline __m128i ps2_psubu32(__m128i a, __m128i b)
__m128i diff = _mm_sub_epi32(a, b);
// Underflow if a < b (unsigned). Clamp to 0.
__m128i underflow = _mm_cmpgt_epi32(_mm_xor_si128(b, _mm_set1_epi32(INT32_MIN)),
_mm_xor_si128(a, _mm_set1_epi32(INT32_MIN)));
_mm_xor_si128(a, _mm_set1_epi32(INT32_MIN)));
return _mm_andnot_si128(underflow, diff); // underflow lanes become 0
}
@@ -358,8 +463,8 @@ inline __m128i ps2_ppacw(__m128i rs, __m128i rt)
inline __m128i ps2_ppach(__m128i rs, __m128i rt)
{
const __m128i mask = _mm_setr_epi8(
0, 1, 4, 5, 8, 9, 12, 13, // from rt: halfwords 0,2,4,6
0, 1, 4, 5, 8, 9, 12, 13); // from rs: halfwords 0,2,4,6
0, 1, 4, 5, 8, 9, 12, 13, // from rt: halfwords 0,2,4,6
0, 1, 4, 5, 8, 9, 12, 13); // from rs: halfwords 0,2,4,6
__m128i lo = _mm_shuffle_epi8(rt, mask);
__m128i hi = _mm_shuffle_epi8(rs, mask);
return _mm_unpacklo_epi64(lo, hi);
@@ -486,21 +591,25 @@ inline __m128i ps2_u64_to_epi64_pair(uint64_t value)
// Concatenates rs || rt (256 bits) and right-shifts by SA bits, taking lower 128 bits.
inline __m128i ps2_qfsrv(__m128i rs, __m128i rt, uint32_t sa)
{
if (sa == 0) return rt;
if (sa >= 128) {
if (sa >= 256) return _mm_setzero_si128();
if (sa == 0)
return rt;
if (sa >= 128)
{
if (sa >= 256)
return _mm_setzero_si128();
uint32_t shift = sa - 128;
if (shift == 0) return rs;
if (shift == 0)
return rs;
// Shift rs right by (sa-128) bits
uint32_t byteShift = shift / 8;
uint32_t bitShift = shift % 8;
// Byte shift rs right
alignas(16) uint8_t buf[16] = {};
alignas(16) uint8_t src[16];
_mm_store_si128((__m128i*)src, rs);
_mm_store_si128((__m128i *)src, rs);
for (uint32_t i = 0; i + byteShift < 16; i++)
buf[i] = src[i + byteShift];
__m128i result = _mm_load_si128((__m128i*)buf);
__m128i result = _mm_load_si128((__m128i *)buf);
if (bitShift > 0)
result = _mm_or_si128(_mm_srli_epi64(result, bitShift),
_mm_slli_epi64(_mm_bsrli_si128(result, 8), 64 - bitShift));
@@ -510,18 +619,20 @@ inline __m128i ps2_qfsrv(__m128i rs, __m128i rt, uint32_t sa)
uint32_t byteShift = sa / 8;
uint32_t bitShift = sa % 8;
alignas(16) uint8_t combined[32];
_mm_store_si128((__m128i*)(combined), rt); // low 128 bits
_mm_store_si128((__m128i*)(combined + 16), rs); // high 128 bits
_mm_store_si128((__m128i *)(combined), rt); // low 128 bits
_mm_store_si128((__m128i *)(combined + 16), rs); // high 128 bits
// Shift right by byteShift bytes
alignas(16) uint8_t shifted[16];
for (uint32_t i = 0; i < 16; i++)
shifted[i] = (i + byteShift < 32) ? combined[i + byteShift] : 0;
__m128i result = _mm_load_si128((__m128i*)shifted);
if (bitShift > 0) {
__m128i result = _mm_load_si128((__m128i *)shifted);
if (bitShift > 0)
{
uint8_t extra = (byteShift + 16 < 32) ? combined[byteShift + 16] : 0;
__m128i hi_byte = _mm_insert_epi8(_mm_setzero_si128(), extra, 15);
alignas(16) uint8_t src32[32];
for (uint32_t i = 0; i < 32; i++) src32[i] = combined[i];
for (uint32_t i = 0; i < 32; i++)
src32[i] = combined[i];
uint64_t lo0, lo1, hi0, hi1;
std::memcpy(&lo0, src32, 8);
std::memcpy(&lo1, src32 + 8, 8);
@@ -529,15 +640,29 @@ inline __m128i ps2_qfsrv(__m128i rs, __m128i rt, uint32_t sa)
std::memcpy(&hi1, src32 + 24, 8);
// 256-bit right shift by sa bits
uint64_t r0, r1;
if (sa < 64) {
if (sa < 64)
{
r0 = (lo0 >> sa) | (lo1 << (64 - sa));
r1 = (lo1 >> sa) | (hi0 << (64 - sa));
} else if (sa < 128) {
}
else if (sa < 128)
{
uint32_t s = sa - 64;
if (s == 0) { r0 = lo1; r1 = hi0; }
else { r0 = (lo1 >> s) | (hi0 << (64 - s)); r1 = (hi0 >> s) | (hi1 << (64 - s)); }
} else {
r0 = 0; r1 = 0; // handled above
if (s == 0)
{
r0 = lo1;
r1 = hi0;
}
else
{
r0 = (lo1 >> s) | (hi0 << (64 - s));
r1 = (hi0 >> s) | (hi1 << (64 - s));
}
}
else
{
r0 = 0;
r1 = 0; // handled above
}
result = _mm_set_epi64x((long long)r1, (long long)r0);
}
@@ -567,15 +692,15 @@ static inline void Ps2SetGprLow64(R5900Context *ctx, int reg, __m128i new_low)
}
}
#define SET_GPR_U32(ctx_ptr, reg_idx, val) \
do \
{ \
if ((reg_idx) != 0) \
{ \
#define SET_GPR_U32(ctx_ptr, reg_idx, val) \
do \
{ \
if ((reg_idx) != 0) \
{ \
__m128i _newVal = _mm_cvtsi64_si128((int64_t)(int32_t)(val)); \
\
Ps2SetGprLow64(ctx_ptr, reg_idx, _newVal); \
} \
\
Ps2SetGprLow64(ctx_ptr, reg_idx, _newVal); \
} \
} while (0)
#define SET_GPR_S32(ctx_ptr, reg_idx, val) \
+3
View File
@@ -11,6 +11,9 @@ namespace ps2_stubs
PS2_STUB_LIST(PS2_DECLARE_STUB)
#undef PS2_DECLARE_STUB
void resetGsSyncVCallbackState();
void dispatchGsSyncVCallback(uint8_t *rdram, PS2Runtime *runtime);
void syMalloc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
void sndr_trans_func(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
+3
View File
@@ -7,6 +7,9 @@
#include <atomic>
#include <cstdint>
#include <cstring>
#include <string>
std::string translatePs2Path(const char *ps2Path);
extern std::atomic<int> g_activeThreads;
+62
View File
@@ -0,0 +1,62 @@
#ifndef PS2_VU1_H
#define PS2_VU1_H
#include <cstdint>
class GS;
class PS2Memory;
struct VU1State
{
float vf[32][4];
int32_t vi[16];
float acc[4];
float q;
float p;
float i;
uint32_t pc;
uint32_t mac;
uint32_t clip;
uint32_t status;
bool ebit;
uint32_t itop;
uint32_t xitop;
};
class VU1Interpreter
{
public:
VU1Interpreter();
void reset();
void execute(uint8_t *vuCode, uint32_t codeSize,
uint8_t *vuData, uint32_t dataSize,
GS &gs, PS2Memory *memory = nullptr,
uint32_t startPC = 0, uint32_t itop = 0,
uint32_t maxCycles = 65536);
void resume(uint8_t *vuCode, uint32_t codeSize,
uint8_t *vuData, uint32_t dataSize,
GS &gs, PS2Memory *memory = nullptr,
uint32_t itop = 0, uint32_t maxCycles = 65536);
VU1State &state() { return m_state; }
const VU1State &state() const { return m_state; }
private:
VU1State m_state;
void run(uint8_t *vuCode, uint32_t codeSize,
uint8_t *vuData, uint32_t dataSize,
GS &gs, PS2Memory *memory, uint32_t maxCycles);
void execUpper(uint32_t instr);
void execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr);
void applyDest(float *dst, const float *result, uint8_t dest);
void applyDestAcc(const float *result, uint8_t dest);
float broadcast(const float *vf, uint8_t bc);
};
#endif
+282
View File
@@ -0,0 +1,282 @@
#include "ps2_audio.h"
#include "ps2_memory.h"
#include "raylib.h"
#include <cstring>
#include <vector>
namespace
{
std::vector<uint8_t> buildWavFromPcm(const int16_t *pcm, size_t sampleCount, uint32_t sampleRate)
{
const uint32_t dataSize = static_cast<uint32_t>(sampleCount * 2);
const uint32_t fileSize = 36 + dataSize;
std::vector<uint8_t> wav(8 + fileSize);
uint8_t *p = wav.data();
p[0] = 'R'; p[1] = 'I'; p[2] = 'F'; p[3] = 'F';
p[4] = static_cast<uint8_t>(fileSize);
p[5] = static_cast<uint8_t>(fileSize >> 8);
p[6] = static_cast<uint8_t>(fileSize >> 16);
p[7] = static_cast<uint8_t>(fileSize >> 24);
p[8] = 'W'; p[9] = 'A'; p[10] = 'V'; p[11] = 'E';
p[12] = 'f'; p[13] = 'm'; p[14] = 't'; p[15] = ' ';
p[16] = 16; p[17] = 0; p[18] = 0; p[19] = 0;
p[20] = 1; p[21] = 0;
p[22] = 1; p[23] = 0;
p[24] = static_cast<uint8_t>(sampleRate);
p[25] = static_cast<uint8_t>(sampleRate >> 8);
p[26] = static_cast<uint8_t>(sampleRate >> 16);
p[27] = static_cast<uint8_t>(sampleRate >> 24);
const uint32_t byteRate = sampleRate * 2;
p[28] = static_cast<uint8_t>(byteRate);
p[29] = static_cast<uint8_t>(byteRate >> 8);
p[30] = static_cast<uint8_t>(byteRate >> 16);
p[31] = static_cast<uint8_t>(byteRate >> 24);
p[32] = 2; p[33] = 0;
p[34] = 16; p[35] = 0;
p[36] = 'd'; p[37] = 'a'; p[38] = 't'; p[39] = 'a';
p[40] = static_cast<uint8_t>(dataSize);
p[41] = static_cast<uint8_t>(dataSize >> 8);
p[42] = static_cast<uint8_t>(dataSize >> 16);
p[43] = static_cast<uint8_t>(dataSize >> 24);
std::memcpy(p + 44, pcm, dataSize);
return wav;
}
}
namespace ps2_vag
{
bool decode(const uint8_t *data, uint32_t sizeBytes,
std::vector<int16_t> &outPcm, uint32_t &outSampleRate);
}
struct PS2AudioBackend::Impl
{
struct TrackedSound { Sound snd; uint32_t sampleKey; };
std::vector<TrackedSound> activeSounds;
};
PS2AudioBackend::PS2AudioBackend() : m_impl(std::make_unique<Impl>())
{
}
PS2AudioBackend::~PS2AudioBackend()
{
if (m_impl)
stopAll();
}
void PS2AudioBackend::onVagTransfer(const uint8_t *rdram, uint32_t srcAddr, uint32_t sizeBytes)
{
if (!rdram || sizeBytes < 48)
return;
const uint32_t physAddr = srcAddr & PS2_RAM_MASK;
if (physAddr + sizeBytes > PS2_RAM_SIZE)
return;
std::vector<int16_t> pcm;
uint32_t sampleRate = 44100;
if (!ps2_vag::decode(rdram + physAddr, sizeBytes, pcm, sampleRate))
return;
std::lock_guard<std::mutex> lock(m_mutex);
DecodedSample sample;
sample.pcm = std::move(pcm);
sample.sampleRate = sampleRate;
m_sampleBank[physAddr] = std::move(sample);
m_mostRecentSampleKey = physAddr;
}
void PS2AudioBackend::onVagTransferFromBuffer(const uint8_t *data, uint32_t sizeBytes, uint32_t keyAddr)
{
if (!data || sizeBytes < 48)
return;
std::vector<int16_t> pcm;
uint32_t sampleRate = 44100;
if (!ps2_vag::decode(data, sizeBytes, pcm, sampleRate))
return;
const uint32_t physAddr = keyAddr & PS2_RAM_MASK;
std::lock_guard<std::mutex> lock(m_mutex);
DecodedSample sample;
sample.pcm = std::move(pcm);
sample.sampleRate = sampleRate;
m_sampleBank[physAddr] = sample;
m_mostRecentSampleKey = physAddr;
m_loadOrderSamples.push_back(std::move(sample));
constexpr size_t kMaxLoadOrderSamples = 32;
if (m_loadOrderSamples.size() > kMaxLoadOrderSamples)
m_loadOrderSamples.erase(m_loadOrderSamples.begin());
}
namespace
{
constexpr uint32_t LIBSD_CMD_SET_VOICE = 0x8010u;
}
void PS2AudioBackend::onSoundCommand(uint32_t sid, uint32_t rpcNum,
const uint8_t *sendBuf, uint32_t sendSize,
uint8_t *recvBuf, uint32_t recvSize)
{
if (sid != 0x80000701u)
return;
if ((rpcNum == LIBSD_CMD_SET_VOICE || (rpcNum & 0xFF00u) == 0x8100u) &&
sendBuf && sendSize >= 20)
{
uint32_t sampleAddr = 0;
uint32_t voiceIndex = 0xFFFFFFFFu;
for (int vo = 4; vo >= 0 && voiceIndex == 0xFFFFFFFFu; vo -= 4)
{
if (vo < static_cast<int>(sendSize))
{
uint32_t v = 0;
std::memcpy(&v, sendBuf + vo, sizeof(v));
if (v < 24u)
voiceIndex = v;
}
}
constexpr uint32_t kMinPlausibleAddr = 0x1000u;
for (int off = 12; off <= 24 && sampleAddr == 0; off += 4)
{
if (sendSize >= static_cast<uint32_t>(off + 4))
{
uint32_t cand = 0;
std::memcpy(&cand, sendBuf + off, sizeof(cand));
if (cand >= kMinPlausibleAddr && (cand <= PS2_RAM_MASK || (cand & ~PS2_RAM_MASK) == 0))
sampleAddr = cand;
}
}
if (sampleAddr == 0)
sampleAddr = m_mostRecentSampleKey;
float pitch = 1.0f;
if (sendSize >= 12)
{
uint16_t pitchHalf = 0;
std::memcpy(&pitchHalf, sendBuf + 8, sizeof(pitchHalf));
if (pitchHalf != 0)
pitch = 4096.0f / static_cast<float>(pitchHalf);
}
play(sampleAddr, pitch, 1.0f, voiceIndex);
}
}
void PS2AudioBackend::play(uint32_t sampleAddr, float pitch, float volume, uint32_t voiceIndex)
{
std::lock_guard<std::mutex> lock(m_mutex);
DecodedSample *sampleToPlay = nullptr;
uint32_t sampleKey = 0;
auto it = m_sampleBank.find(sampleAddr & PS2_RAM_MASK);
if (it != m_sampleBank.end())
{
sampleToPlay = &it->second;
sampleKey = it->first;
}
else if (voiceIndex != 0xFFFFFFFFu && voiceIndex < m_loadOrderSamples.size())
{
sampleToPlay = &m_loadOrderSamples[voiceIndex];
sampleKey = 0x1719740u + voiceIndex;
}
else
{
it = m_sampleBank.find(m_mostRecentSampleKey);
if (it == m_sampleBank.end())
return;
sampleToPlay = &it->second;
sampleKey = it->first;
}
if (!sampleToPlay || sampleToPlay->pcm.empty())
return;
const bool isBgm = (sampleToPlay->pcm.size() > static_cast<size_t>(sampleToPlay->sampleRate * 5));
playDecodedSample(sampleKey, *sampleToPlay, pitch, volume, isBgm);
}
void PS2AudioBackend::pruneFinishedSounds()
{
auto &sounds = m_impl->activeSounds;
auto it = sounds.begin();
while (it != sounds.end())
{
if (!IsSoundPlaying(it->snd))
{
UnloadSound(it->snd);
it = sounds.erase(it);
}
else
{
++it;
}
}
}
void PS2AudioBackend::playDecodedSample(uint32_t sampleKey, DecodedSample &sample, float pitch, float volume,
bool isBgm)
{
if (!m_audioReady || sample.pcm.empty())
return;
pruneFinishedSounds();
for (const auto &t : m_impl->activeSounds)
{
if (t.sampleKey == sampleKey && IsSoundPlaying(t.snd))
return;
}
auto &sounds = m_impl->activeSounds;
if (isBgm)
{
for (auto it = sounds.begin(); it != sounds.end();)
{
if (IsSoundPlaying(it->snd))
{
StopSound(it->snd);
UnloadSound(it->snd);
it = sounds.erase(it);
}
else
++it;
}
}
constexpr int kMaxConcurrentSounds = 4;
while (static_cast<int>(sounds.size()) >= kMaxConcurrentSounds)
{
StopSound(sounds.front().snd);
UnloadSound(sounds.front().snd);
sounds.erase(sounds.begin());
}
std::vector<uint8_t> wav = buildWavFromPcm(sample.pcm.data(), sample.pcm.size(), sample.sampleRate);
Wave wave = LoadWaveFromMemory(".wav", wav.data(), static_cast<int>(wav.size()));
if (wave.frameCount <= 0)
return;
Sound snd = LoadSoundFromWave(wave);
UnloadWave(wave);
SetSoundPitch(snd, pitch);
SetSoundVolume(snd, volume);
m_impl->activeSounds.push_back({snd, sampleKey});
PlaySound(snd);
}
void PS2AudioBackend::stop(uint32_t voiceId)
{
(void)voiceId;
}
void PS2AudioBackend::stopAll()
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto &t : m_impl->activeSounds)
{
StopSound(t.snd);
UnloadSound(t.snd);
}
m_impl->activeSounds.clear();
}
+112
View File
@@ -0,0 +1,112 @@
#include "ps2_memory.h"
#include <algorithm>
#include <cstdint>
#include <cstring>
namespace
{
inline int16_t clamp16(int32_t v)
{
if (v < -32768) return -32768;
if (v > 32767) return 32767;
return static_cast<int16_t>(v);
}
inline int8_t signExtend4(uint8_t nibble)
{
uint8_t s = nibble & 0x0F;
return static_cast<int8_t>((s & 8) ? static_cast<int8_t>(s | 0xF0) : static_cast<int8_t>(s));
}
}
namespace ps2_vag
{
bool decode(const uint8_t *data, uint32_t sizeBytes,
std::vector<int16_t> &outPcm, uint32_t &outSampleRate)
{
if (!data || sizeBytes < 48)
return false;
const uint32_t magic = (static_cast<uint32_t>(data[0]) << 24) |
(static_cast<uint32_t>(data[1]) << 16) |
(static_cast<uint32_t>(data[2]) << 8) |
static_cast<uint32_t>(data[3]);
if (magic != 0x56414770u)
{
const uint32_t magicLE = (static_cast<uint32_t>(data[3]) << 24) |
(static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[1]) << 8) |
static_cast<uint32_t>(data[0]);
if (magicLE != 0x56414770u)
return false;
}
uint32_t dataSize = (static_cast<uint32_t>(data[0x0c]) << 24) |
(static_cast<uint32_t>(data[0x0d]) << 16) |
(static_cast<uint32_t>(data[0x0e]) << 8) |
static_cast<uint32_t>(data[0x0f]);
outSampleRate = (static_cast<uint32_t>(data[0x10]) << 24) |
(static_cast<uint32_t>(data[0x11]) << 16) |
(static_cast<uint32_t>(data[0x12]) << 8) |
static_cast<uint32_t>(data[0x13]);
if (outSampleRate == 0)
outSampleRate = 44100;
const uint32_t numBlocks = (dataSize + 15) / 16;
outPcm.clear();
outPcm.reserve(numBlocks * 28);
int16_t s1 = 0, s2 = 0;
const uint8_t *block = data + 48;
for (uint32_t b = 0; b < numBlocks && (block + 16) <= data + sizeBytes; ++b, block += 16)
{
uint8_t shift = block[0] & 0x0F;
if (shift > 12)
shift = 9;
uint8_t filter = (block[0] >> 4) & 0x07;
if (filter > 4)
filter = 0;
for (int sampleIdx = 0; sampleIdx < 28; ++sampleIdx)
{
const uint8_t byte = block[2 + sampleIdx / 2];
const uint8_t nibble = (sampleIdx & 1) ? (byte >> 4) : (byte & 0x0F);
const int8_t rawSample = signExtend4(nibble);
const int32_t shiftedSample = rawSample << (12 - shift);
int32_t filteredSample;
const int32_t old = s1;
const int32_t older = s2;
switch (filter)
{
case 0:
filteredSample = shiftedSample;
break;
case 1:
filteredSample = shiftedSample + (60 * old + 32) / 64;
break;
case 2:
filteredSample = shiftedSample + (115 * old - 52 * older + 32) / 64;
break;
case 3:
filteredSample = shiftedSample + (98 * old - 55 * older + 32) / 64;
break;
case 4:
filteredSample = shiftedSample + (122 * old - 60 * older + 32) / 64;
break;
default:
filteredSample = shiftedSample;
break;
}
const int16_t clamped = clamp16(filteredSample);
s2 = s1;
s1 = clamped;
outPcm.push_back(clamped);
}
}
return true;
}
}
+65
View File
@@ -0,0 +1,65 @@
#include "ps2_gif_arbiter.h"
#include <algorithm>
#include <cstring>
GifArbiter::GifArbiter(ProcessPacketFn processFn)
: m_processFn(std::move(processFn))
{
}
bool GifArbiter::isImagePacket(const uint8_t *data, uint32_t sizeBytes)
{
if (!data || sizeBytes < 16u)
return false;
uint64_t tagLo = 0;
std::memcpy(&tagLo, data, sizeof(tagLo));
const uint8_t flg = static_cast<uint8_t>((tagLo >> 58) & 0x3u);
return flg == 2u;
}
void GifArbiter::submit(GifPathId pathId, const uint8_t *data, uint32_t sizeBytes, bool path2DirectHl)
{
if (!data || sizeBytes < 16 || !m_processFn)
return;
GifArbiterPacket pkt;
pkt.pathId = pathId;
pkt.path2DirectHl = (pathId == GifPathId::Path2) && path2DirectHl;
pkt.path3Image = (pathId == GifPathId::Path3) && isImagePacket(data, sizeBytes);
pkt.data.resize(sizeBytes);
std::memcpy(pkt.data.data(), data, sizeBytes);
m_queue.push_back(std::move(pkt));
}
void GifArbiter::drain()
{
if (!m_processFn)
return;
std::stable_sort(m_queue.begin(), m_queue.end(),
[](const GifArbiterPacket &a, const GifArbiterPacket &b) {
// DIRECTHL cannot preempt PATH3 IMAGE transfers.
if (a.path2DirectHl != b.path2DirectHl || a.path3Image != b.path3Image)
{
if (a.path3Image && b.path2DirectHl)
return true;
if (a.path2DirectHl && b.path3Image)
return false;
}
return pathPriority(a.pathId) < pathPriority(b.pathId);
});
for (size_t i = 0; i < m_queue.size(); ++i)
{
auto &pkt = m_queue[i];
if (!pkt.data.empty())
m_processFn(pkt.data.data(), static_cast<uint32_t>(pkt.data.size()));
}
m_queue.clear();
}
uint8_t GifArbiter::pathPriority(GifPathId id)
{
return static_cast<uint8_t>(id);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
#include "ps2_iop.h"
ps2_iop::ps2_iop()
{
reset();
}
void ps2_iop::init(uint8_t *rdram)
{
m_rdram = rdram;
}
void ps2_iop::reset()
{
}
bool ps2_iop::handleRPC(uint32_t /*sid*/, uint32_t /*rpcNum*/,
uint32_t /*sendBufAddr*/, uint32_t /*sendSize*/,
uint32_t /*recvBufAddr*/, uint32_t /*recvSize*/)
{
return false;
}
+14
View File
@@ -0,0 +1,14 @@
#include "ps2_iop_audio.h"
#include "ps2_runtime.h"
namespace ps2_iop_audio
{
void handleLibSdRpc(PS2Runtime *runtime, uint32_t sid, uint32_t rpcNum,
const uint8_t *sendBuf, uint32_t sendSize,
uint8_t *recvBuf, uint32_t recvSize)
{
if (!runtime)
return;
runtime->audioBackend().onSoundCommand(sid, rpcNum, sendBuf, sendSize, recvBuf, recvSize);
}
}
+556 -180
View File
@@ -4,6 +4,7 @@
#include <stdexcept>
#include <algorithm>
#include <string>
#include <vector>
namespace
{
@@ -38,7 +39,8 @@ namespace
inline uint64_t *gsRegPtr(GSRegisters &gs, uint32_t addr)
{
uint32_t off = addr - PS2_GS_PRIV_REG_BASE;
// Support both 64-bit base offsets and +4 dword aliases.
uint32_t off = (addr - PS2_GS_PRIV_REG_BASE) & ~0x7u;
switch (off)
{
case 0x0000:
@@ -121,6 +123,17 @@ PS2Memory::~PS2Memory()
m_gsVRAM = nullptr;
}
if (m_vu1Code)
{
delete[] m_vu1Code;
m_vu1Code = nullptr;
}
if (m_vu1Data)
{
delete[] m_vu1Data;
m_vu1Data = nullptr;
}
if (iop_ram)
{
delete[] iop_ram;
@@ -136,11 +149,15 @@ bool PS2Memory::initialize(size_t ramSize)
delete[] m_scratchpad;
delete[] iop_ram;
delete[] m_gsVRAM;
delete[] m_vu1Code;
delete[] m_vu1Data;
m_rdram = nullptr;
m_scratchpad = nullptr;
ps2SetScratchpadHostPtr(nullptr);
iop_ram = nullptr;
m_gsVRAM = nullptr;
m_vu1Code = nullptr;
m_vu1Data = nullptr;
};
cleanup();
@@ -176,12 +193,18 @@ bool PS2Memory::initialize(size_t ramSize)
// Initialize GS registers
memset(&gs_regs, 0, sizeof(gs_regs));
m_gsDrawCtx = GSDrawContext{};
gs_regs.dispfb1 = (0ULL << 0) | (10ULL << 9) | (0ULL << 15) | (0ULL << 32) | (0ULL << 43);
gs_regs.display1 = (0ULL << 0) | (0ULL << 12) | (0ULL << 23) | (0ULL << 27) | (639ULL << 32) | (447ULL << 44);
// Allocate GS VRAM (4MB)
m_gsVRAM = new uint8_t[PS2_GS_VRAM_SIZE];
std::memset(m_gsVRAM, 0, PS2_GS_VRAM_SIZE);
m_vu1Code = new uint8_t[PS2_VU1_CODE_SIZE];
m_vu1Data = new uint8_t[PS2_VU1_DATA_SIZE];
std::memset(m_vu1Code, 0, PS2_VU1_CODE_SIZE);
std::memset(m_vu1Data, 0, PS2_VU1_DATA_SIZE);
// Initialize VIF registers
memset(&vif0_regs, 0, sizeof(vif0_regs));
memset(&vif1_regs, 0, sizeof(vif1_regs));
@@ -212,6 +235,14 @@ uint32_t PS2Memory::translateAddress(uint32_t virtualAddress)
return virtualAddress - PS2_SCRATCHPAD_BASE;
}
// EE uncached aliases of main RAM (per PS2 memory map):
// 0x20000000-0x3FFFFFFF -> 32MB mirror of RDRAM
// This includes the accelerated window rooted at 0x30100000.
if (virtualAddress >= 0x20000000u && virtualAddress < 0x40000000u)
{
return virtualAddress & PS2_RAM_MASK;
}
// KSEG0/KSEG1 direct-mapped window.
if (virtualAddress >= 0x80000000 && virtualAddress < 0xC0000000)
{
@@ -517,9 +548,24 @@ void PS2Memory::write32(uint32_t address, uint32_t value)
if (reg)
{
uint32_t off = address & 7;
uint64_t mask = 0xFFFFFFFFULL << (off * 8);
uint64_t newVal = (*reg & ~mask) | ((uint64_t)value << (off * 8));
*reg = newVal;
const uint32_t regOff = (address - PS2_GS_PRIV_REG_BASE) & ~0x7u;
if (regOff == 0x1000u && off == 0u)
{
// CSR low dword: bits 0..1 are write-one-to-clear status bits.
constexpr uint32_t kW1cMask = 0x3u;
uint64_t current = *reg;
uint32_t oldLow = static_cast<uint32_t>(current & 0xFFFFFFFFull);
uint32_t mergedLow = (oldLow & kW1cMask) | (value & ~kW1cMask);
current = (current & 0xFFFFFFFF00000000ull) | static_cast<uint64_t>(mergedLow);
current &= ~static_cast<uint64_t>(value & kW1cMask);
*reg = current;
}
else
{
uint64_t mask = 0xFFFFFFFFULL << (off * 8);
uint64_t newVal = (*reg & ~mask) | ((uint64_t)value << (off * 8));
*reg = newVal;
}
}
return;
}
@@ -556,7 +602,19 @@ void PS2Memory::write64(uint32_t address, uint64_t value)
uint64_t *reg = gsRegPtr(gs_regs, address);
if (reg)
{
*reg = value;
const uint32_t regOff = (address - PS2_GS_PRIV_REG_BASE) & ~0x7u;
if (regOff == 0x1000u)
{
// CSR: bits 0..1 are write-one-to-clear status bits.
constexpr uint64_t kW1cMask = 0x3ull;
uint64_t next = (*reg & kW1cMask) | (value & ~kW1cMask);
next &= ~(value & kW1cMask);
*reg = next;
}
else
{
*reg = value;
}
}
return;
}
@@ -614,27 +672,39 @@ void PS2Memory::write128(uint32_t address, __m128i value)
bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
{
// ── IPU registers (0x10002000-0x10002030) ──────────────────
// On real PS2, IPU_CTRL bit 31 (BUSY) is READ-ONLY — set by hardware.
// We must NOT store the raw value for IPU_CTRL because the game
// might write 0x40000000 (RST) and we'd return 0 with no BUSY,
// but if any stale value had bit 31, the polling loop would hang.
if (isGsPrivReg(address))
{
m_ioRegisters[address] = value;
if (uint64_t *reg = gsRegPtr(gs_regs, address))
{
const uint32_t off = address & 7u;
const uint32_t regOff = (address - PS2_GS_PRIV_REG_BASE) & ~0x7u;
if (regOff == 0x1000u && off == 0u)
{
constexpr uint32_t kW1cMask = 0x3u;
uint64_t current = *reg;
uint32_t oldLow = static_cast<uint32_t>(current & 0xFFFFFFFFull);
uint32_t mergedLow = (oldLow & kW1cMask) | (value & ~kW1cMask);
current = (current & 0xFFFFFFFF00000000ull) | static_cast<uint64_t>(mergedLow);
current &= ~static_cast<uint64_t>(value & kW1cMask);
*reg = current;
}
else
{
const uint64_t mask = 0xFFFFFFFFull << (off * 8u);
*reg = (*reg & ~mask) | (static_cast<uint64_t>(value) << (off * 8u));
}
}
m_gsWriteCount.fetch_add(1, std::memory_order_relaxed);
return true;
}
if (address >= 0x10002000 && address <= 0x10002030)
{
static int ipuWriteLog = 0;
if (ipuWriteLog < 30)
{
std::cerr << "[IPU] write addr=0x" << std::hex << address
<< " val=0x" << value << std::dec << std::endl;
++ipuWriteLog;
}
if (address == 0x10002010)
{
// IPU_CTRL write: bit 30 = RST (reset). After reset,
// all status bits clear. Never store BUSY (bit 31).
if (value & (1u << 30))
{
// Reset IPU — clear all IPU registers
m_ioRegisters[0x10002000] = 0;
m_ioRegisters[0x10002010] = 0;
m_ioRegisters[0x10002020] = 0;
@@ -642,46 +712,113 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
}
else
{
// Store without BUSY bit
m_ioRegisters[address] = value & ~(1u << 31);
}
}
else
{
// IPU_CMD (0x10002000) — store command, don't set busy
m_ioRegisters[address] = value;
}
return true;
}
m_ioRegisters[address] = value;
if (address == 0x1000E010u)
{
static int io_total_log = 0;
if (io_total_log < 100)
{
std::cerr << "[IO_WRITE] addr=0x" << std::hex << address << " val=0x" << value << std::dec << std::endl;
++io_total_log;
}
const uint32_t current = m_ioRegisters.count(address) ? m_ioRegisters[address] : 0u;
uint32_t status = current & 0x3FFu;
uint32_t mask = (current >> 16) & 0x3FFu;
// D_STAT low bits are W1C status, high bits [16..25] toggle masks on write-one.
status &= ~(value & 0x3FFu);
mask ^= ((value >> 16) & 0x3FFu);
uint32_t next = (current & ~((0x3FFu) | (0x3FFu << 16) | (1u << 31)));
next |= status | (mask << 16);
if ((status & mask) != 0u)
next |= (1u << 31);
m_ioRegisters[address] = next;
return true;
}
if (address >= 0x10008000 && address < 0x1000F000)
m_ioRegisters[address] = value;
if (address >= 0x10003C00u && address < 0x10003E00u)
{
static int dma_io_log = 0;
if (dma_io_log < 200)
m_vifWriteCount.fetch_add(1, std::memory_order_relaxed);
switch (address)
{
uint32_t ch = (address >> 8) & 0xFF;
uint32_t off = address & 0xFF;
std::cerr << "[DMA_IO] ch=0x" << std::hex << (address & 0xFFFFFF00)
<< " off=0x" << off << " val=0x" << value << std::dec << std::endl;
++dma_io_log;
case 0x10003C10u: // VIF1_FBRST
if (value & 0x1u) // RST
{
std::memset(&vif1_regs, 0, sizeof(vif1_regs));
}
if (value & 0x8u) // STC
{
vif1_regs.stat &= ~((1u << 8) | (1u << 9) | (1u << 10) | (1u << 11) | (1u << 12) | (1u << 13));
}
break;
case 0x10003C30u:
vif1_regs.mark = value & 0xFFFFu;
vif1_regs.stat &= ~(1u << 6); // clear MRK flag on CPU write
break;
case 0x10003C40u:
vif1_regs.cycle = value & 0xFFFFu;
break;
case 0x10003C50u:
vif1_regs.mode = value & 0x3u;
break;
case 0x10003C60u:
vif1_regs.num = value & 0xFFu;
break;
case 0x10003C70u:
vif1_regs.mask = value;
break;
case 0x10003C80u:
vif1_regs.code = value;
break;
case 0x10003C90u:
vif1_regs.itops = value & 0x3FFu;
break;
case 0x10003CA0u:
vif1_regs.base = value & 0x3FFu;
break;
case 0x10003CB0u:
vif1_regs.ofst = value & 0x3FFu;
break;
case 0x10003CC0u:
vif1_regs.tops = value & 0x3FFu;
break;
case 0x10003CD0u:
vif1_regs.itop = value & 0x3FFu;
break;
case 0x10003CE0u:
vif1_regs.top = value & 0x3FFu;
break;
default:
break;
}
return true;
}
if (address >= 0x10003800u && address < 0x10003A00u)
{
m_vifWriteCount.fetch_add(1, std::memory_order_relaxed);
return true;
}
if (address >= 0x10008000 && address < 0x1000F000)
{
if ((address & 0xFF) == 0x00 && (value & 0x100))
{
const auto dctrlIt = m_ioRegisters.find(0x1000E000u);
const bool dmacEnabled = (dctrlIt == m_ioRegisters.end()) || ((dctrlIt->second & 0x1u) != 0u);
if (!dmacEnabled)
{
return true;
}
const uint32_t channelBase = address & 0xFFFFFF00;
const uint32_t madr = m_ioRegisters[channelBase + 0x10];
const uint32_t qwc = m_ioRegisters[channelBase + 0x20];
@@ -689,128 +826,209 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
if ((channelBase == 0x1000A000 || channelBase == 0x10009000) && m_gsVRAM)
{
auto dispatchTransfer = [&](uint32_t srcAddr, uint32_t qwCount)
auto enqueueTransfer = [&](uint32_t srcAddr, uint32_t qwCount)
{
if (qwCount == 0)
{
return;
}
uint32_t srcPhys = 0;
try
{
srcPhys = translateAddress(srcAddr);
}
catch (const std::exception &)
{
return;
}
if (srcPhys >= PS2_RAM_SIZE)
{
return;
}
const bool scratch = isScratchpad(srcAddr);
PendingTransfer pt;
pt.fromScratchpad = scratch;
pt.srcAddr = srcAddr;
pt.qwc = qwCount;
if (channelBase == 0x1000A000)
{
processGIFPacket(srcPhys, qwCount);
return;
}
const uint64_t bytes64 = static_cast<uint64_t>(qwCount) * 16ull;
uint32_t bytes = bytes64 > static_cast<uint64_t>(PS2_RAM_SIZE)
? PS2_RAM_SIZE
: static_cast<uint32_t>(bytes64);
if (srcPhys + bytes > PS2_RAM_SIZE)
{
bytes = PS2_RAM_SIZE - srcPhys;
}
processVIF1Data(srcPhys, bytes);
m_pendingGifTransfers.push_back(pt);
else if (channelBase == 0x10009000 && !scratch)
m_pendingVif1Transfers.push_back(pt);
};
auto walkChain = [&](uint32_t startTadr)
uint32_t chcr = value;
uint32_t mode = (chcr >> 2) & 0x3;
if (mode == 0 && qwc > 0)
{
uint32_t curTadr = startTadr;
constexpr int kMaxTags = 4096;
for (int i = 0; i < kMaxTags; ++i)
enqueueTransfer(madr, qwc);
}
else if (mode == 1)
{
uint32_t tagAddr = m_ioRegisters[channelBase + 0x30];
uint32_t asr0 = m_ioRegisters[channelBase + 0x40];
uint32_t asr1 = m_ioRegisters[channelBase + 0x50];
uint32_t asp = (chcr >> 4) & 0x3u;
const bool tieEnabled = (chcr & (1u << 7)) != 0u;
const int kMaxChainTags = 4096;
std::vector<uint8_t> chainBuf;
auto appendData = [&](uint32_t srcAddr, uint32_t qwCount)
{
const uint64_t bytes64 = static_cast<uint64_t>(qwCount) * 16ull;
uint32_t bytes = (bytes64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast<uint32_t>(bytes64);
const bool scratch = isScratchpad(srcAddr);
uint32_t src = 0;
try
{
src = translateAddress(srcAddr);
}
catch (...)
{
return;
}
const uint8_t *base2;
uint32_t maxSz2;
if (scratch)
{
base2 = m_scratchpad;
maxSz2 = PS2_SCRATCHPAD_SIZE;
}
else
{
base2 = m_rdram;
maxSz2 = PS2_RAM_SIZE;
}
if (src >= maxSz2)
return;
if (src + bytes > maxSz2)
bytes = maxSz2 - src;
if (bytes == 0)
return;
chainBuf.insert(chainBuf.end(), base2 + src, base2 + src + bytes);
};
int tagsProcessed = 0;
while (tagsProcessed < kMaxChainTags)
{
const bool tagInSPR = isScratchpad(tagAddr);
uint32_t physTag = 0;
try
{
physTag = translateAddress(curTadr);
physTag = translateAddress(tagAddr);
}
catch (const std::exception &)
catch (...)
{
break;
}
if (physTag + 16 > PS2_RAM_SIZE)
const uint8_t *tagBase;
uint32_t tagMax;
if (tagInSPR)
{
break;
tagBase = m_scratchpad;
tagMax = PS2_SCRATCHPAD_SIZE;
}
else
{
tagBase = m_rdram;
tagMax = PS2_RAM_SIZE;
}
if (physTag + 16 > tagMax)
break;
const uint64_t tag = loadScalar<uint64_t>(m_rdram, physTag, PS2_RAM_SIZE, "dma chain tag", curTadr);
const uint16_t tagQwc = static_cast<uint16_t>(tag & 0xFFFFu);
const uint32_t id = static_cast<uint32_t>((tag >> 28) & 0x7u);
const uint32_t addr = static_cast<uint32_t>((tag >> 32) & 0x7FFFFFF0u);
const bool irq = ((tag >> 31) & 0x1u) != 0;
const uint8_t *tp = tagBase + physTag;
uint64_t tag = loadScalar<uint64_t>(tp, 0, 16, "dma chain tag", tagAddr);
uint16_t tagQwc = static_cast<uint16_t>(tag & 0xFFFF);
uint32_t id = static_cast<uint32_t>((tag >> 28) & 0x7);
const bool irq = ((tag >> 31) & 0x1ull) != 0ull;
uint32_t addr = static_cast<uint32_t>((tag >> 32) & 0x7FFFFFFF);
++tagsProcessed;
uint32_t dataAddr = 0;
uint32_t nextTag = 0;
bool hasPayload = (tagQwc > 0);
bool endChain = false;
switch (id)
{
case 0: // REFE
case 0:
dataAddr = addr;
tagAddr = tagAddr + 16;
endChain = true;
break;
case 1: // CNT
dataAddr = curTadr + 16u;
nextTag = curTadr + 16u + static_cast<uint32_t>(tagQwc) * 16u;
case 1:
dataAddr = tagAddr + 16;
tagAddr = dataAddr + static_cast<uint32_t>(tagQwc) * 16u;
break;
case 2: // NEXT
dataAddr = curTadr + 16u;
nextTag = addr;
case 2:
dataAddr = tagAddr + 16;
tagAddr = addr;
break;
case 3: // REF
case 4: // REFS
case 3:
case 4:
dataAddr = addr;
nextTag = curTadr + 16u;
tagAddr = tagAddr + 16;
break;
case 7: // END
dataAddr = curTadr + 16u;
case 5:
dataAddr = tagAddr + 16;
{
const uint32_t retAddr = dataAddr + static_cast<uint32_t>(tagQwc) * 16u;
if (asp == 0u)
{
asr0 = retAddr;
asp = 1u;
}
else if (asp == 1u)
{
asr1 = retAddr;
asp = 2u;
}
}
tagAddr = addr;
break;
case 6:
dataAddr = tagAddr + 16;
if (asp == 2u)
{
tagAddr = asr1;
asp = 1u;
}
else if (asp == 1u)
{
tagAddr = asr0;
asp = 0u;
}
else
{
endChain = true;
}
break;
case 7:
dataAddr = tagAddr + 16;
endChain = true;
break;
default:
hasPayload = false;
endChain = true;
break;
}
if (tagQwc > 0 && dataAddr != 0)
{
dispatchTransfer(dataAddr, tagQwc);
}
if (endChain || irq)
{
if (hasPayload)
appendData(dataAddr, tagQwc);
if (irq && tieEnabled)
endChain = true;
if (endChain)
break;
}
curTadr = nextTag;
}
};
if (qwc > 0)
{
dispatchTransfer(madr, qwc);
}
else
{
const uint32_t tadr = m_ioRegisters[channelBase + 0x30];
walkChain(tadr);
}
m_ioRegisters[channelBase + 0x30] = tagAddr;
m_ioRegisters[channelBase + 0x40] = asr0;
m_ioRegisters[channelBase + 0x50] = asr1;
chcr = (chcr & ~(0x3u << 4)) | ((asp & 0x3u) << 4);
m_ioRegisters[channelBase + 0x00] = chcr;
m_ioRegisters[address] &= ~0x100;
if (!chainBuf.empty())
{
PendingTransfer pt;
pt.fromScratchpad = false;
pt.srcAddr = 0;
pt.qwc = 0;
pt.chainData = std::move(chainBuf);
if (channelBase == 0x1000A000)
m_pendingGifTransfers.push_back(std::move(pt));
else if (channelBase == 0x10009000)
m_pendingVif1Transfers.push_back(std::move(pt));
}
}
else if (qwc > 0)
{
enqueueTransfer(madr, qwc);
}
}
}
return true;
@@ -818,14 +1036,6 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
if (address >= 0x10000000 && address < 0x10010000)
{
if (address >= 0x10003800 && address < 0x10003A00)
{
m_vifWriteCount.fetch_add(1, std::memory_order_relaxed);
}
if (address >= 0x10003C00 && address < 0x10003E00)
{
m_vifWriteCount.fetch_add(1, std::memory_order_relaxed);
}
if (address >= 0x10000200 && address < 0x10000300)
{
return true;
@@ -836,67 +1046,234 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
}
}
if (address >= 0x12000000 && address < 0x12001000)
{
m_gsWriteCount.fetch_add(1, std::memory_order_relaxed);
return true;
}
return false;
}
// ============================================================================
// pollDmaRegisters: Workaround for KSEG1 fast-path bypass
// When libsles.a is compiled with old headers, isSpecialAddress() doesn't
// recognize KSEG0/KSEG1 addresses (0x8xxx/0xBxxx). Game writes to e.g.
// 0xB000A000 (GIF DMA CHCR via KSEG1) go through Ps2FastWrite32 which
// stores to rdram[addr & 0x01FFFFFF] = rdram[0x1000A000], bypassing
// writeIORegister entirely. This function polls those shadow locations
// and triggers DMA processing when CHCR.STR (bit 8) is set.
//
// NOTE: DISABLED — sho_runner writes DMA regs via physical addresses which
// go through writeIORegister correctly. This function was reading garbage
// from rdram shadow (ELF code area) and triggering bogus DMA transfers.
// ============================================================================
void PS2Memory::processPendingTransfers()
{
const bool hadGif = !m_pendingGifTransfers.empty();
for (size_t idx = 0; idx < m_pendingGifTransfers.size(); ++idx)
{
auto &p = m_pendingGifTransfers[idx];
if (!p.chainData.empty())
{
m_seenGifCopy = true;
m_gifCopyCount.fetch_add(1, std::memory_order_relaxed);
submitGifPacket(GifPathId::Path3, p.chainData.data(), static_cast<uint32_t>(p.chainData.size()), false);
}
else if (p.qwc > 0)
{
const uint64_t bytes64 = static_cast<uint64_t>(p.qwc) * 16ull;
uint32_t sizeBytes = (bytes64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast<uint32_t>(bytes64);
uint32_t srcPhys = 0;
try
{
srcPhys = translateAddress(p.srcAddr);
}
catch (const std::exception &)
{
continue;
}
if (p.fromScratchpad)
{
if (srcPhys + sizeBytes <= PS2_SCRATCHPAD_SIZE && sizeBytes >= 16)
{
m_seenGifCopy = true;
m_gifCopyCount.fetch_add(1, std::memory_order_relaxed);
submitGifPacket(GifPathId::Path3, m_scratchpad + srcPhys, sizeBytes, false);
}
}
else if (srcPhys < PS2_RAM_SIZE)
{
if (static_cast<uint64_t>(srcPhys) + sizeBytes > PS2_RAM_SIZE)
sizeBytes = PS2_RAM_SIZE - srcPhys;
if (sizeBytes >= 16)
{
m_seenGifCopy = true;
m_gifCopyCount.fetch_add(1, std::memory_order_relaxed);
submitGifPacket(GifPathId::Path3, m_rdram + srcPhys, sizeBytes, false);
}
}
}
}
m_pendingGifTransfers.clear();
const bool hadVif1 = !m_pendingVif1Transfers.empty();
for (auto &p : m_pendingVif1Transfers)
{
if (!p.chainData.empty())
{
processVIF1Data(p.chainData.data(), static_cast<uint32_t>(p.chainData.size()));
}
else if (p.qwc > 0 && !p.fromScratchpad)
{
uint32_t srcPhys = 0;
try
{
srcPhys = translateAddress(p.srcAddr);
}
catch (const std::exception &)
{
continue;
}
if (srcPhys < PS2_RAM_SIZE)
{
const uint64_t bytes64 = static_cast<uint64_t>(p.qwc) * 16ull;
uint32_t sizeBytes = (bytes64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast<uint32_t>(bytes64);
if (srcPhys + sizeBytes > PS2_RAM_SIZE)
sizeBytes = PS2_RAM_SIZE - srcPhys;
if (sizeBytes > 0)
processVIF1Data(srcPhys, sizeBytes);
}
}
}
m_pendingVif1Transfers.clear();
if (m_gifArbiter)
m_gifArbiter->drain();
static constexpr uint32_t GIF_CHANNEL = 0x1000A000;
static constexpr uint32_t VIF1_CHANNEL = 0x10009000;
static constexpr uint32_t D_STAT = 0x1000E010u;
auto raiseDStatChannel = [&](uint32_t channelBit)
{
uint32_t dstat = m_ioRegisters.count(D_STAT) ? m_ioRegisters[D_STAT] : 0u;
dstat |= (1u << channelBit);
const uint32_t status = dstat & 0x3FFu;
const uint32_t mask = (dstat >> 16) & 0x3FFu;
if ((status & mask) != 0u)
dstat |= (1u << 31);
else
dstat &= ~(1u << 31);
m_ioRegisters[D_STAT] = dstat;
};
if (hadGif)
{
raiseDStatChannel(2u); // GIF channel
m_ioRegisters[GIF_CHANNEL + 0x00] &= ~0x100u;
m_ioRegisters[GIF_CHANNEL + 0x20] = 0;
}
if (hadVif1)
{
raiseDStatChannel(1u); // VIF1 channel
m_ioRegisters[VIF1_CHANNEL + 0x00] &= ~0x100u;
m_ioRegisters[VIF1_CHANNEL + 0x20] = 0;
}
}
void PS2Memory::flushMaskedPath3Packets(bool drainImmediately)
{
if (m_path3Masked || m_path3MaskedFifo.empty())
return;
auto emit = [&](const uint8_t *packetData, uint32_t packetSize)
{
if (m_gifArbiter)
m_gifArbiter->submit(GifPathId::Path3, packetData, packetSize, false);
else if (m_gifPacketCallback)
m_gifPacketCallback(packetData, packetSize);
};
for (const auto &packet : m_path3MaskedFifo)
{
if (packet.size() >= 16u)
emit(packet.data(), static_cast<uint32_t>(packet.size()));
}
m_path3MaskedFifo.clear();
if (m_gifArbiter && drainImmediately)
m_gifArbiter->drain();
}
void PS2Memory::submitGifPacket(GifPathId pathId, const uint8_t *data, uint32_t sizeBytes, bool drainImmediately, bool path2DirectHl)
{
if (!data || sizeBytes < 16)
return;
if (pathId == GifPathId::Path3)
{
if (m_path3Masked)
{
m_path3MaskedFifo.emplace_back(data, data + sizeBytes);
return;
}
flushMaskedPath3Packets(false);
}
if (m_gifArbiter)
m_gifArbiter->submit(pathId, data, sizeBytes, path2DirectHl);
else if (m_gifPacketCallback)
m_gifPacketCallback(data, sizeBytes);
if (m_gifArbiter && drainImmediately)
m_gifArbiter->drain();
}
void PS2Memory::processGIFPacket(uint32_t srcPhysAddr, uint32_t qwCount)
{
if (!m_rdram || qwCount == 0)
return;
const uint64_t bytes64 = static_cast<uint64_t>(qwCount) * 16ull;
uint32_t sizeBytes = (bytes64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast<uint32_t>(bytes64);
if (srcPhysAddr >= PS2_RAM_SIZE)
return;
if (static_cast<uint64_t>(srcPhysAddr) + static_cast<uint64_t>(sizeBytes) > static_cast<uint64_t>(PS2_RAM_SIZE))
sizeBytes = PS2_RAM_SIZE - srcPhysAddr;
if (sizeBytes < 16)
return;
m_seenGifCopy = true;
m_gifCopyCount.fetch_add(1, std::memory_order_relaxed);
submitGifPacket(GifPathId::Path3, m_rdram + srcPhysAddr, sizeBytes);
}
void PS2Memory::processGIFPacket(const uint8_t *data, uint32_t sizeBytes)
{
if (m_gifArbiter)
submitGifPacket(GifPathId::Path3, data, sizeBytes);
else if (m_gifPacketCallback && data && sizeBytes >= 16)
m_gifPacketCallback(data, sizeBytes);
}
int PS2Memory::pollDmaRegisters()
{
// Disabled — DMA writes go through writeIORegister, not KSEG1 shadow
return 0;
}
uint32_t PS2Memory::readIORegister(uint32_t address)
{
// ── IPU registers (0x10002000-0x10002030) ──────────────────
// IPU_CMD 0x10002000: command result / FIFO output
// IPU_CTRL 0x10002010: status — bit 31=BUSY (always 0: we don't decode)
// IPU_BP 0x10002020: bitstream pointer
// IPU_TOP 0x10002030: top 32 bits of FIFO
if (isGsPrivReg(address))
{
if (uint64_t *reg = gsRegPtr(gs_regs, address))
{
const uint32_t off = address & 7u;
return static_cast<uint32_t>((*reg >> (off * 8u)) & 0xFFFFFFFFull);
}
return 0u;
}
if (address >= 0x10002000 && address <= 0x10002030)
{
static int ipuReadLog = 0;
uint32_t val = 0;
switch (address)
{
case 0x10002000: // IPU_CMD — command result
case 0x10002000:
val = m_ioRegisters[address];
break;
case 0x10002010: // IPU_CTRL — always NOT busy, ECD=0
val = m_ioRegisters[address] & ~(1u << 31); // clear BUSY
case 0x10002010:
val = m_ioRegisters[address] & ~(1u << 31);
break;
case 0x10002020: // IPU_BP
case 0x10002030: // IPU_TOP
case 0x10002020:
case 0x10002030:
val = m_ioRegisters[address];
break;
default:
val = 0;
break;
}
if (ipuReadLog < 30)
{
std::cerr << "[IPU] read addr=0x" << std::hex << address
<< " val=0x" << val << std::dec << std::endl;
++ipuReadLog;
}
return val;
}
if (address >= 0x10000000 && address < 0x10010000)
@@ -913,9 +1290,9 @@ uint32_t PS2Memory::readIORegister(uint32_t address)
{
if ((address & 0xFF) == 0x00)
{
// Return CHCR as-is. STR (bit 8) is cleared after DMA
// completion in writeIORegister, not on read.
return m_ioRegisters[address];
uint32_t channelStatus = m_ioRegisters[address] & ~0x100u;
m_ioRegisters[address] = channelStatus;
return channelStatus;
}
}
@@ -924,21 +1301,8 @@ uint32_t PS2Memory::readIORegister(uint32_t address)
return 0;
}
// SIF hardware registers — HLE: pretend IOP is always ready
// 0x1000F200: SIF_SMCOM — IOP communication status
// 0x1000F210: SIF_MSCOM — EE→IOP command
// 0x1000F220: SIF_MSFLG — Main→Sub flags
// 0x1000F230: SIF_SMFLG — Sub→Main flags (IOP ready bits)
// 0x1000F240: SIF_CTRL — SIF control
if (address >= 0x1000F200 && address <= 0x1000F260)
{
static std::atomic<uint64_t> sifReads{0};
uint64_t n = sifReads.fetch_add(1);
if (n < 5 || (n % 100000) == 0)
{
std::cerr << "[SIF-HW] read 0x" << std::hex << address
<< " #" << std::dec << n << std::endl;
}
if (address == 0x1000F230)
{
return 0x60000;
@@ -1000,6 +1364,18 @@ bool PS2Memory::isAddressInRegion(uint32_t address, const CodeRegion &region)
return (address >= region.start && address < region.end);
}
bool PS2Memory::isCodeAddress(uint32_t address) const
{
for (const auto &region : m_codeRegions)
{
if (address >= region.start && address < region.end)
{
return true;
}
}
return false;
}
void PS2Memory::markModified(uint32_t address, uint32_t size)
{
if (size == 0)
+94
View File
@@ -0,0 +1,94 @@
#include "ps2_pad.h"
#include "raylib.h"
#include <cstring>
namespace
{
constexpr uint8_t kPadAnalogMarker = 0x73;
constexpr uint8_t kPadStickCenter = 0x80;
constexpr uint16_t PAD_LEFT = 0x0080u;
constexpr uint16_t PAD_DOWN = 0x0040u;
constexpr uint16_t PAD_RIGHT = 0x0020u;
constexpr uint16_t PAD_UP = 0x0010u;
constexpr uint16_t PAD_START = 0x0008u;
constexpr uint16_t PAD_R3 = 0x0004u;
constexpr uint16_t PAD_L3 = 0x0002u;
constexpr uint16_t PAD_SELECT = 0x0001u;
constexpr uint16_t PAD_SQUARE = 0x8000u;
constexpr uint16_t PAD_CROSS = 0x4000u;
constexpr uint16_t PAD_CIRCLE = 0x2000u;
constexpr uint16_t PAD_TRIANGLE = 0x1000u;
constexpr uint16_t PAD_R1 = 0x0800u;
constexpr uint16_t PAD_L1 = 0x0400u;
constexpr uint16_t PAD_R2 = 0x0200u;
constexpr uint16_t PAD_L2 = 0x0100u;
}
bool PSPadBackend::readState(int /*port*/, int /*slot*/, uint8_t *data, size_t size)
{
if (!data || size < 32)
return false;
std::memset(data, 0, 32);
data[0] = 0x01;
data[1] = kPadAnalogMarker;
data[2] = 0xFF;
data[3] = 0xFF;
data[4] = data[5] = data[6] = data[7] = kPadStickCenter;
uint16_t btns = 0xFFFFu;
constexpr int kGamepad = 0;
const bool useGamepad = IsGamepadAvailable(kGamepad);
auto clearBit = [&btns](uint16_t mask) { btns &= ~mask; };
if (useGamepad)
{
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_FACE_UP)) clearBit(PAD_UP);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) clearBit(PAD_DOWN);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) clearBit(PAD_LEFT);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) clearBit(PAD_RIGHT);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) clearBit(PAD_CROSS);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)) clearBit(PAD_CIRCLE);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_FACE_LEFT)) clearBit(PAD_SQUARE);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_FACE_UP)) clearBit(PAD_TRIANGLE);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_TRIGGER_1)) clearBit(PAD_L1);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_TRIGGER_1)) clearBit(PAD_R1);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_TRIGGER_2)) clearBit(PAD_L2);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_TRIGGER_2)) clearBit(PAD_R2);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_MIDDLE_RIGHT)) clearBit(PAD_START);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_MIDDLE_LEFT)) clearBit(PAD_SELECT);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_LEFT_THUMB)) clearBit(PAD_L3);
if (IsGamepadButtonDown(kGamepad, GAMEPAD_BUTTON_RIGHT_THUMB)) clearBit(PAD_R3);
float lx = GetGamepadAxisMovement(kGamepad, GAMEPAD_AXIS_LEFT_X);
float ly = GetGamepadAxisMovement(kGamepad, GAMEPAD_AXIS_LEFT_Y);
float rx = GetGamepadAxisMovement(kGamepad, GAMEPAD_AXIS_RIGHT_X);
float ry = GetGamepadAxisMovement(kGamepad, GAMEPAD_AXIS_RIGHT_Y);
data[6] = static_cast<uint8_t>(128 + lx * 127);
data[7] = static_cast<uint8_t>(128 + ly * 127);
data[4] = static_cast<uint8_t>(128 + rx * 127);
data[5] = static_cast<uint8_t>(128 + ry * 127);
}
else
{
if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) clearBit(PAD_UP);
if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) clearBit(PAD_DOWN);
if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) clearBit(PAD_LEFT);
if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) clearBit(PAD_RIGHT);
if (IsKeyDown(KEY_ENTER) || IsKeyDown(KEY_SPACE)) clearBit(PAD_CROSS);
if (IsKeyDown(KEY_ESCAPE)) clearBit(PAD_CIRCLE);
if (IsKeyDown(KEY_KP_0) || IsKeyDown(KEY_Z)) clearBit(PAD_SQUARE);
if (IsKeyDown(KEY_KP_1) || IsKeyDown(KEY_X)) clearBit(PAD_TRIANGLE);
if (IsKeyDown(KEY_Q)) clearBit(PAD_L1);
if (IsKeyDown(KEY_E)) clearBit(PAD_R1);
if (IsKeyDown(KEY_LEFT_SHIFT)) clearBit(PAD_L2);
if (IsKeyDown(KEY_RIGHT_SHIFT)) clearBit(PAD_R2);
if (IsKeyDown(KEY_ENTER)) clearBit(PAD_START);
if (IsKeyDown(KEY_TAB)) clearBit(PAD_SELECT);
}
data[2] = static_cast<uint8_t>(btns & 0xFF);
data[3] = static_cast<uint8_t>(btns >> 8);
return true;
}
+459 -83
View File
@@ -1,5 +1,6 @@
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include "game_overrides.h"
#include "ps2_runtime_macros.h"
#include <iostream>
@@ -13,6 +14,7 @@
#include <atomic>
#include <thread>
#include <unordered_map>
#include <sstream>
#include "raylib.h"
#include "ps2_gs_gpu.h"
#include <ThreadNaming.h>
@@ -77,6 +79,85 @@ namespace
constexpr uint32_t EXCEPTION_VECTOR_TLB_REFILL = 0x80000000u;
constexpr uint32_t EXCEPTION_VECTOR_BOOT = 0xBFC00200u;
struct DispatchHistory
{
std::array<uint32_t, 64> pcs{};
uint32_t next = 0u;
bool wrapped = false;
};
thread_local DispatchHistory g_dispatchHistory;
void pushDispatchPc(uint32_t pc)
{
DispatchHistory &h = g_dispatchHistory;
h.pcs[h.next] = pc;
h.next = (h.next + 1u) % static_cast<uint32_t>(h.pcs.size());
if (h.next == 0u)
{
h.wrapped = true;
}
}
std::string formatDispatchHistory()
{
const DispatchHistory &h = g_dispatchHistory;
const uint32_t count = h.wrapped ? static_cast<uint32_t>(h.pcs.size()) : h.next;
if (count == 0u)
{
return "(empty)";
}
std::ostringstream oss;
bool first = true;
for (uint32_t i = 0u; i < count; ++i)
{
const uint32_t idx = (h.next + h.pcs.size() - count + i) % static_cast<uint32_t>(h.pcs.size());
if (!first)
{
oss << " -> ";
}
first = false;
oss << "0x" << std::hex << h.pcs[idx];
}
return oss.str();
}
uint32_t selectDispatchRecoveryPc(const PS2Runtime *runtime)
{
const DispatchHistory &h = g_dispatchHistory;
const uint32_t count = h.wrapped ? static_cast<uint32_t>(h.pcs.size()) : h.next;
if (count == 0u)
{
return 0u;
}
uint32_t firstHigh = 0u;
for (uint32_t step = 1u; step <= count; ++step)
{
const uint32_t idx = (h.next + h.pcs.size() - step) % static_cast<uint32_t>(h.pcs.size());
const uint32_t pc = h.pcs[idx];
if (pc < 0x00100000u)
{
continue;
}
if (runtime && !runtime->hasFunction(pc))
{
continue;
}
if (firstHigh == 0u)
{
firstHigh = pc;
continue;
}
return pc;
}
return firstHigh;
}
uint32_t selectExceptionVector(const R5900Context *ctx, bool tlbRefill)
{
if (ctx->cop0_status & COP0_STATUS_BEV)
@@ -155,6 +236,56 @@ namespace
return value;
}
uint64_t readGuestU64Wrapped(const uint8_t *rdram, uint32_t addr)
{
const uint64_t lo = readGuestU32Wrapped(rdram, addr);
const uint64_t hi = readGuestU32Wrapped(rdram, addr + 4u);
return lo | (hi << 32);
}
uint32_t selectStackRecoveryPc(const uint8_t *rdram, const R5900Context *ctx, const PS2Runtime *runtime)
{
if (!rdram || !ctx || !runtime)
{
return 0u;
}
const uint32_t sp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0));
constexpr uint32_t kScanBytes = 0x200u;
for (uint32_t offset = 0u; offset < kScanBytes; offset += 8u)
{
const uint32_t slotAddr = sp + offset;
const uint32_t ra32 = static_cast<uint32_t>(readGuestU64Wrapped(rdram, slotAddr));
if (ra32 < 0x00100000u)
{
continue;
}
if (!runtime->hasFunction(ra32))
{
continue;
}
return ra32;
}
for (uint32_t offset = 0u; offset < kScanBytes; offset += 4u)
{
const uint32_t slotAddr = sp + offset;
const uint32_t ra32 = readGuestU32Wrapped(rdram, slotAddr);
if (ra32 < 0x00100000u)
{
continue;
}
if (!runtime->hasFunction(ra32))
{
continue;
}
return ra32;
}
return 0u;
}
std::string readGuestPrintableString(const uint8_t *rdram, uint32_t addr, size_t maxLen)
{
std::string out;
@@ -186,71 +317,110 @@ namespace
static void UploadFrame(Texture2D &tex, PS2Runtime *rt)
{
// Try to use GS dispfb/display registers to locate the visible buffer.
// For now lets keep the display snapshot in sync with rasterized VRAM so the host frame
rt->gs().refreshDisplaySnapshot();
const GSRegisters &gs = rt->memory().gs();
// DISPFBUF1 fields: FBP bits 0-8, FBW bits 9-14, PSM bits 15-19.
uint32_t dispfb = static_cast<uint32_t>(gs.dispfb1 & 0xFFFFFFFFULL);
uint32_t fbp = dispfb & 0x1FF;
uint32_t fbw = (dispfb >> 9) & 0x3F;
uint32_t psm = (dispfb >> 15) & 0x1F;
// DISPLAY1 fields used here: DX[11:0], DY[22:12], MAGH[25:23], MAGV[27:26], DW[43:32], DH[54:44].
uint64_t display64 = gs.display1;
uint32_t magh = static_cast<uint32_t>((display64 >> 23) & 0x7); // magnification H (0-7)
uint32_t dw = static_cast<uint32_t>((display64 >> 32) & 0xFFF);
uint32_t dh = static_cast<uint32_t>((display64 >> 44) & 0x7FF);
// DW is in VCK units: actual pixel width = (DW + 1) / (MAGH + 1).
uint32_t maghDiv = magh + 1;
uint32_t width = (dw + 1) / maghDiv;
uint32_t width = (dw + 1);
uint32_t height = (dh + 1);
if (dw == 0)
if (width < 64 || height < 64)
{
width = FB_WIDTH;
if (dh == 0)
height = FB_HEIGHT;
}
if (width > FB_WIDTH)
width = FB_WIDTH;
if (height > FB_HEIGHT)
height = FB_HEIGHT;
// Only handle PSMCT32 (0).
if (psm != 0)
uint32_t baseBytes = fbp * 8192u;
const uint32_t bytesPerPixel = (psm == 2u || psm == 0x0Au) ? 2u : 4u;
uint32_t strideBytes = (fbw ? fbw : (FB_WIDTH / 64)) * 64 * bytesPerPixel;
std::vector<uint8_t> scratch(FB_WIDTH * FB_HEIGHT * 4, 0);
uint8_t *rdram = rt->memory().getRDRAM();
uint8_t *gsvram = rt->memory().getGSVRAM();
uint32_t snapSize = 0;
const uint8_t *snapVram = rt->gs().lockDisplaySnapshot(snapSize);
const uint8_t *vramSrc = (snapVram && snapSize > 0) ? snapVram : gsvram;
if (snapVram)
{
// I can`t stand a random RAM glitch screen so lets use some magenta to calm down
baseBytes = rt->gs().getLastDisplayBaseBytes();
}
if (psm == 0u)
{
for (uint32_t y = 0; y < height; ++y)
{
uint32_t srcOff = baseBytes + y * strideBytes;
uint32_t dstOff = y * FB_WIDTH * 4;
uint32_t copyW = width * 4;
uint32_t srcIdx = srcOff;
if (srcIdx + copyW <= PS2_GS_VRAM_SIZE && vramSrc)
std::memcpy(&scratch[dstOff], vramSrc + srcIdx, copyW);
else
{
uint32_t rdramIdx = srcOff & PS2_RAM_MASK;
if (rdramIdx + copyW > PS2_RAM_SIZE)
copyW = PS2_RAM_SIZE - rdramIdx;
std::memcpy(&scratch[dstOff], rdram + rdramIdx, copyW);
}
uint8_t *row = scratch.data() + dstOff;
for (uint32_t x = 0; x < width; ++x)
row[x * 4 + 3] = 255u;
}
}
else if (psm == 2u)
{
const uint32_t srcLineBytes = width * 2u;
for (uint32_t y = 0; y < height; ++y)
{
uint32_t srcOff = baseBytes + y * strideBytes;
uint32_t dstOff = y * FB_WIDTH * 4;
const uint8_t *src = nullptr;
if (srcOff + srcLineBytes <= PS2_GS_VRAM_SIZE && vramSrc)
src = vramSrc + srcOff;
else if ((srcOff & PS2_RAM_MASK) + srcLineBytes <= PS2_RAM_SIZE)
src = rdram + (srcOff & PS2_RAM_MASK);
if (!src)
continue;
uint8_t *dst = scratch.data() + dstOff;
for (uint32_t x = 0; x < width; ++x)
{
uint16_t p = *reinterpret_cast<const uint16_t *>(src + x * 2);
uint32_t r = (p >> 10) & 31u;
uint32_t g = (p >> 5) & 31u;
uint32_t b = p & 31u;
dst[x * 4 + 0] = static_cast<uint8_t>((r << 3) | (r >> 2));
dst[x * 4 + 1] = static_cast<uint8_t>((g << 3) | (g >> 2));
dst[x * 4 + 2] = static_cast<uint8_t>((b << 3) | (b >> 2));
dst[x * 4 + 3] = 255u;
}
}
}
else
{
rt->gs().unlockDisplaySnapshot();
Image blank = GenImageColor(FB_WIDTH, FB_HEIGHT, MAGENTA);
UpdateTexture(tex, blank.data);
UnloadImage(blank);
return;
}
uint32_t baseBytes = fbp * 2048;
const uint32_t bytesPerPixel = (psm == 2u || psm == 0x0Au) ? 2u : 4u;
uint32_t strideBytes = (fbw ? fbw : (FB_WIDTH / 64)) * 64 * bytesPerPixel;
static std::vector<uint8_t> scratch(FB_WIDTH * FB_HEIGHT * 4, 0);
std::memset(scratch.data(), 0, scratch.size());
uint8_t *rdram = rt->memory().getRDRAM();
uint8_t *gsvram = rt->memory().getGSVRAM();
for (uint32_t y = 0; y < height; ++y)
{
uint32_t srcOff = baseBytes + y * strideBytes;
uint32_t dstOff = y * FB_WIDTH * 4;
uint32_t copyW = width * 4;
uint32_t srcIdx = srcOff;
if (srcIdx + copyW <= PS2_GS_VRAM_SIZE && gsvram)
{
std::memcpy(&scratch[dstOff], gsvram + srcIdx, copyW);
}
else
{
uint32_t rdramIdx = srcOff & PS2_RAM_MASK;
if (rdramIdx + copyW > PS2_RAM_SIZE)
copyW = PS2_RAM_SIZE - rdramIdx;
std::memcpy(&scratch[dstOff], rdram + rdramIdx, copyW);
}
}
rt->gs().unlockDisplaySnapshot();
UpdateTexture(tex, scratch.data());
}
@@ -296,10 +466,27 @@ bool PS2Runtime::initialize(const char *title)
return false;
}
m_gs.init(m_memory.getGSVRAM(), static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &m_memory.gs());
m_gs.reset();
m_gifArbiter.setProcessPacketFn([this](const uint8_t *data, uint32_t size) { m_gs.processGIFPacket(data, size); });
m_memory.setGifArbiter(&m_gifArbiter);
m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t itop) {
m_vu1.execute(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE,
m_memory.getVU1Data(), PS2_VU1_DATA_SIZE,
m_gs, &m_memory, startPC, itop, 65536);
});
m_iop.init(m_memory.getRDRAM());
m_iop.reset();
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
InitWindow(FB_WIDTH, FB_HEIGHT, title);
InitAudioDevice();
m_audioBackend.setAudioReady(IsAudioDeviceReady());
SetTargetFPS(60);
m_vu1.reset();
return true;
}
@@ -597,17 +784,149 @@ bool PS2Runtime::hasFunction(uint32_t address) const
PS2Runtime::RecompiledFunction PS2Runtime::lookupFunction(uint32_t address)
{
pushDispatchPc(address);
auto it = m_functionTable.find(address);
if (it != m_functionTable.end())
{
return it->second;
}
// Some games dispatch to internal basic-block addresses that belong to a
// larger recompiled function. Map known hot-path aliases to their parent
// function entry so execution can resume from the current ctx->pc.
if (address == 0x2913E4u)
{
auto parent = m_functionTable.find(0x2913B0u);
if (parent != m_functionTable.end())
{
return parent->second;
}
}
std::cerr << "Warning: Function at address 0x" << std::hex << address << std::dec << " not found" << std::endl;
static RecompiledFunction defaultFunction = [](uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
std::cerr << "Error: Called unimplemented function at address 0x" << std::hex << ctx->pc << std::dec << std::endl;
const uint32_t ra = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0)) : 0u;
const uint32_t sp = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0)) : 0u;
const uint32_t gp = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0)) : 0u;
const uint32_t a0 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[4], 0)) : 0u;
const uint32_t a1 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[5], 0)) : 0u;
const uint32_t v0 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[2], 0)) : 0u;
const uint32_t v1 = ctx ? static_cast<uint32_t>(_mm_extract_epi32(ctx->r[3], 0)) : 0u;
if (ctx && runtime)
{
thread_local uint32_t s_recoverCount = 0u;
thread_local bool s_loggedContext = false;
const uint32_t pc = ctx->pc;
const bool hasPcFunction = runtime->hasFunction(pc);
if (!hasPcFunction && s_recoverCount < 8192u)
{
if (!s_loggedContext)
{
std::ostringstream stackDump;
if (rdram)
{
stackDump << " [stack]";
for (uint32_t off = 0u; off < 0x40u; off += 4u)
{
const uint32_t slot = readGuestU32Wrapped(rdram, sp + off);
stackDump << " +" << std::hex << off << "=0x" << slot;
}
}
std::cerr << "[dispatch:first-bad-pc] bad=0x" << std::hex << pc
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< " v0=0x" << v0
<< " v1=0x" << v1
<< " a0=0x" << a0
<< " a1=0x" << a1
<< " trace=" << formatDispatchHistory()
<< stackDump.str()
<< std::dec << std::endl;
s_loggedContext = true;
}
uint32_t recoveryPc = 0u;
if (ra != 0u && runtime->hasFunction(ra))
{
recoveryPc = ra;
}
if (recoveryPc == 0u)
{
recoveryPc = selectStackRecoveryPc(rdram, ctx, runtime);
}
if (recoveryPc == 0u)
{
recoveryPc = selectDispatchRecoveryPc(runtime);
}
if (recoveryPc != 0u && recoveryPc != pc)
{
if (s_recoverCount < 256u)
{
std::cerr << "[dispatch:recover-pc] bad=0x" << std::hex << pc
<< " ra=0x" << ra
<< " fallback=0x" << recoveryPc
<< " sp=0x" << sp
<< std::dec << std::endl;
}
++s_recoverCount;
ctx->pc = recoveryPc;
return;
}
}
if (hasPcFunction)
{
s_recoverCount = 0u;
s_loggedContext = false;
}
else if (pc < 0x00100000u && ra == pc && s_recoverCount < 4096u)
{
uint32_t recoveryPc = selectStackRecoveryPc(rdram, ctx, runtime);
if (recoveryPc == 0u)
{
recoveryPc = selectDispatchRecoveryPc(runtime);
}
if (recoveryPc != 0u && recoveryPc != pc)
{
if (s_recoverCount < 128u)
{
std::cerr << "[dispatch:recover-low-pc] bad=0x" << std::hex << pc
<< " ra=0x" << ra
<< " fallback=0x" << recoveryPc
<< " sp=0x" << sp
<< std::dec << std::endl;
}
++s_recoverCount;
ctx->pc = recoveryPc;
return;
}
}
}
std::ostringstream oss;
oss << "Error: Called unimplemented function at address 0x" << std::hex << (ctx ? ctx->pc : 0u)
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< " a0=0x" << a0
<< " hostTid=" << std::this_thread::get_id()
<< " pcTrace=" << formatDispatchHistory()
<< std::dec;
static std::mutex s_defaultFnLogMutex;
{
std::lock_guard<std::mutex> lock(s_defaultFnLogMutex);
std::cerr << oss.str() << std::endl;
}
runtime->requestStop();
};
@@ -1206,12 +1525,26 @@ void PS2Runtime::dispatchLoop(uint8_t *rdram, R5900Context *ctx)
m_debugGp.store(static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0)), std::memory_order_relaxed);
RecompiledFunction fn = lookupFunction(pc);
const uint32_t dispatchedPc = pc;
const uint32_t dispatchedRa = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0));
fn(rdram, ctx, this);
if (ctx->pc == 0u)
{
requestStop();
const uint32_t ra = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0));
const uint32_t sp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[29], 0));
const uint32_t gp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0));
std::cerr << "[dispatch:pc-zero] from=0x" << std::hex << dispatchedPc
<< " fromRa=0x" << dispatchedRa
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< " trace=" << formatDispatchHistory()
<< std::dec << std::endl;
// PC=0 means this guest thread returned (usually via jr $ra with RA=0).
// Do not request a global runtime stop here: other guest threads may still run.
break;
}
}
@@ -1351,11 +1684,8 @@ void PS2Runtime::Store128(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, __m
void PS2Runtime::requestStop()
{
const bool alreadyRequested = m_stopRequested.exchange(true, std::memory_order_relaxed);
if (!alreadyRequested)
{
ps2_syscalls::notifyRuntimeStop();
}
m_stopRequested.store(true, std::memory_order_relaxed);
ps2_syscalls::notifyRuntimeStop();
}
bool PS2Runtime::isStopRequested() const
@@ -1371,6 +1701,7 @@ void PS2Runtime::HandleIntegerOverflow(R5900Context *ctx)
void PS2Runtime::run()
{
m_stopRequested.store(false, std::memory_order_relaxed);
ps2_stubs::resetGsSyncVCallbackState();
m_cpuContext.r[4] = _mm_setzero_si128();
m_cpuContext.r[5] = _mm_setzero_si128();
m_cpuContext.r[29] = _mm_set_epi64x(0, static_cast<int64_t>(PS2_RAM_SIZE - 0x10u));
@@ -1411,53 +1742,87 @@ void PS2Runtime::run()
gameThreadFinished.store(true, std::memory_order_release); });
uint64_t tick = 0;
while (!gameThreadFinished.load(std::memory_order_acquire))
while (!isStopRequested() && g_activeThreads.load(std::memory_order_relaxed) > 0)
{
const uint32_t pc = m_debugPc.load(std::memory_order_relaxed);
const uint32_t ra = m_debugRa.load(std::memory_order_relaxed);
const uint32_t sp = m_debugSp.load(std::memory_order_relaxed);
const uint32_t gp = m_debugGp.load(std::memory_order_relaxed);
if ((tick++ % 120) == 0)
tick++;
ps2_stubs::dispatchGsSyncVCallback(m_memory.getRDRAM(), this);
if ((tick % 120) == 0)
{
std::cout << "[run] activeThreads=" << g_activeThreads.load(std::memory_order_relaxed);
std::cout << " pc=0x" << std::hex << pc
<< " ra=0x" << ra
<< " sp=0x" << sp
<< " gp=0x" << gp
<< std::dec << std::endl;
}
if ((tick % 600) == 0)
{
static uint64_t lastDma = 0, lastGif = 0, lastGs = 0, lastVif = 0;
uint64_t curDma = m_memory.dmaStartCount();
uint64_t curGif = m_memory.gifCopyCount();
uint64_t curGs = m_memory.gsWriteCount();
uint64_t curVif = m_memory.vifWriteCount();
if (curDma != lastDma || curGif != lastGif || curGs != lastGs || curVif != lastVif)
const GSRegisters &gs = m_memory.gs();
const uint32_t dbgPc = m_debugPc.load(std::memory_order_relaxed);
const uint32_t dbgRa = m_debugRa.load(std::memory_order_relaxed);
const uint32_t dbgSp = m_debugSp.load(std::memory_order_relaxed);
const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed);
const int activeThreads = g_activeThreads.load(std::memory_order_relaxed);
constexpr uint32_t kSndTransTypeAddr = 0x01E0E1C0u;
constexpr uint32_t kSndTransBankAddr = 0x01E0E1C8u;
constexpr uint32_t kSndTransLevelAddr = 0x01E0E1B8u;
constexpr uint32_t kSndGetAdrsAddr = 0x01E212D8u;
constexpr uint32_t kSndStatusMirrorAddr = 0x01E213C0u;
constexpr uint32_t kSndSeCheckAddr = 0x01E0EF10u;
constexpr uint32_t kSndMidiCheckAddr = 0x01E0EF20u;
const uint32_t sndTransType = readGuestU32Wrapped(m_memory.getRDRAM(), kSndTransTypeAddr);
const uint32_t sndTransLevel = readGuestU32Wrapped(m_memory.getRDRAM(), kSndTransLevelAddr);
const uint32_t sndTransBank = readGuestU32Wrapped(m_memory.getRDRAM(), kSndTransBankAddr);
const uint32_t sndGetAdrs = readGuestU32Wrapped(m_memory.getRDRAM(), kSndGetAdrsAddr);
auto readGuestS16 = [&](uint32_t addr) -> int32_t
{
std::cout << "[hw] dma_starts=" << curDma
<< " gif_copies=" << curGif
<< " gs_writes=" << curGs
<< " vif_writes=" << curVif << std::endl;
lastDma = curDma;
lastGif = curGif;
lastGs = curGs;
lastVif = curVif;
const uint8_t *rdram = m_memory.getRDRAM();
if (!rdram)
{
return 0;
}
const uint16_t raw = static_cast<uint16_t>(
static_cast<uint16_t>(rdram[(addr + 0u) & PS2_RAM_MASK]) |
(static_cast<uint16_t>(rdram[(addr + 1u) & PS2_RAM_MASK]) << 8));
return static_cast<int16_t>(raw);
};
const int32_t sndMirrorMidi0 = readGuestS16(kSndStatusMirrorAddr + 0x1Eu);
const int32_t sndMirrorSe0 = readGuestS16(kSndStatusMirrorAddr + 0x26u);
int32_t sndBankMidiCheck = 0;
int32_t sndBankSeCheck = 0;
if (sndTransBank < 4u)
{
sndBankMidiCheck = readGuestS16(kSndMidiCheckAddr + (sndTransBank * 2u));
}
if (sndTransBank < 5u)
{
sndBankSeCheck = readGuestS16(kSndSeCheckAddr + (sndTransBank * 2u));
}
std::cout << "[run:tick] tick=" << tick
<< " pc=0x" << std::hex << dbgPc
<< " ra=0x" << dbgRa
<< " sp=0x" << dbgSp
<< " gp=0x" << dbgGp
<< " dispfb1=0x" << gs.dispfb1
<< " display1=0x" << gs.display1
<< std::dec
<< " activeThreads=" << activeThreads
<< " dma=" << curDma
<< " gif=" << curGif
<< " gsw=" << curGs
<< " vif=" << curVif
<< " sndType=" << sndTransType
<< " sndLvl=" << sndTransLevel
<< " sndBank=" << sndTransBank
<< " getAdrs=0x" << std::hex << sndGetAdrs << std::dec
<< " sndMirrorMidi0=" << sndMirrorMidi0
<< " sndMirrorSe0=" << sndMirrorSe0
<< " sndChkMidi=" << sndBankMidiCheck
<< " sndChkSe=" << sndBankSeCheck
<< std::endl;
}
UploadFrame(frameTex, this);
BeginDrawing();
ClearBackground(BLACK);
bool gpuRendered = gsGpuRenderFrame();
if (!gpuRendered)
{
// lets draw for now as debug but we wont need this in future
UploadFrame(frameTex, this);
DrawTexture(frameTex, 0, 0, WHITE);
}
DrawTexture(frameTex, 0, 0, WHITE);
EndDrawing();
if (WindowShouldClose())
@@ -1490,13 +1855,24 @@ void PS2Runtime::run()
}
}
const auto workerDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(250);
const auto workerDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000);
while (g_activeThreads.load(std::memory_order_relaxed) > 0 &&
std::chrono::steady_clock::now() < workerDeadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (g_activeThreads.load(std::memory_order_relaxed) > 0)
{
requestStop();
const auto finalWorkerDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000);
while (g_activeThreads.load(std::memory_order_relaxed) > 0 &&
std::chrono::steady_clock::now() < finalWorkerDeadline)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
UnloadTexture(frameTex);
CloseWindow();
+2
View File
@@ -1,5 +1,6 @@
#include "ps2_stubs.h"
#include "ps2_runtime.h"
#include "ps2_runtime_macros.h"
#include "ps2_syscalls.h"
#include <iostream>
#include <algorithm>
@@ -38,6 +39,7 @@ namespace ps2_stubs
void TODO_NAMED(const char *name, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const std::string stubName = name ? name : "unknown";
uint32_t callCount = 0;
{
std::lock_guard<std::mutex> lock(g_stubWarningMutex);
+10
View File
@@ -1,5 +1,6 @@
#include "ps2_syscalls.h"
#include "ps2_runtime.h"
#include "ps2_iop_audio.h"
#include "ps2_runtime_macros.h"
#include "ps2_stubs.h"
#include <iostream>
@@ -327,7 +328,10 @@ namespace ps2_syscalls
threads.push_back(entry.second);
}
}
g_threads.clear();
g_nextThreadId = 2; // Reserve id 1 for main thread.
}
g_currentThreadId = 1;
for (const auto &threadInfo : threads)
{
@@ -339,6 +343,8 @@ namespace ps2_syscalls
threadInfo->cv.notify_all();
}
joinAllHostThreads();
std::vector<std::shared_ptr<SemaInfo>> semas;
{
std::lock_guard<std::mutex> lock(g_sema_map_mutex);
@@ -350,6 +356,8 @@ namespace ps2_syscalls
semas.push_back(entry.second);
}
}
g_semas.clear();
g_nextSemaId = 1;
}
for (const auto &sema : semas)
{
@@ -367,6 +375,8 @@ namespace ps2_syscalls
eventFlags.push_back(entry.second);
}
}
g_eventFlags.clear();
g_nextEventFlagId = 1;
}
for (const auto &eventFlag : eventFlags)
{
+313 -111
View File
@@ -25,17 +25,8 @@ enum VIFCmd : uint8_t
VIF_MPG = 0x4A,
VIF_DIRECT = 0x50,
VIF_DIRECTHL = 0x51,
// UNPACK range: 0x60-0x6F (V4-32..V4-5)
};
namespace
{
static int g_vifLogCount = 0;
static uint32_t g_vifDirectCount = 0;
static uint32_t g_vifUnpackCount = 0;
static uint32_t g_vifTotalCmds = 0;
} // namespace
void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes)
{
if (!m_rdram || !m_gsVRAM || sizeBytes == 0u)
@@ -47,109 +38,152 @@ void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes)
if (requestedEnd > static_cast<uint64_t>(PS2_RAM_SIZE))
sizeBytes = PS2_RAM_SIZE - srcPhys;
const uint8_t *data = m_rdram + srcPhys;
uint32_t pos = 0; // byte offset
processVIF1Data(m_rdram + srcPhys, sizeBytes);
}
void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes)
{
if (!data || !m_gsVRAM || sizeBytes == 0u)
return;
auto recomputeVif1Tops = [&]()
{
const bool dbf = (vif1_regs.stat & (1u << 7)) != 0u;
const uint32_t base = vif1_regs.base & 0x3FFu;
const uint32_t ofst = vif1_regs.ofst & 0x3FFu;
vif1_regs.tops = dbf ? ((base + ofst) & 0x3FFu) : base;
};
uint32_t pos = 0;
while (pos + 4 <= sizeBytes)
{
// Read VIF command word (32 bits)
uint32_t cmd;
memcpy(&cmd, data + pos, 4);
pos += 4;
uint8_t opcode = (cmd >> 24) & 0x7F; // bits 30:24
// bool irq = (cmd >> 31) & 1; // bit 31: interrupt
uint16_t imm = cmd & 0xFFFF; // bits 15:0 (IMMEDIATE)
uint8_t num = (cmd >> 16) & 0xFF; // bits 23:16 (NUM)
uint8_t opcode = (cmd >> 24) & 0x7F;
uint16_t imm = cmd & 0xFFFF;
uint8_t num = (cmd >> 16) & 0xFF;
const bool irq = (cmd & 0x80000000u) != 0u;
g_vifTotalCmds++;
// Track most-recent command for VIFn_CODE emulation.
vif1_regs.code = cmd;
vif1_regs.num = num;
if (irq)
vif1_regs.stat |= (1u << 11); // INT
if (opcode == VIF_NOP)
{
// No operation
continue;
}
else if (opcode == VIF_STCYCL)
{
// Set write cycle: CL in bits 7:0, WL in bits 15:8
// Used with UNPACK - store for later
vif1_regs.cycle = imm;
continue;
}
else if (opcode == VIF_OFFSET)
{
// Set double-buffer offset
const uint32_t oldTops = vif1_regs.tops & 0x3FFu;
vif1_regs.ofst = imm & 0x3FFu;
vif1_regs.base = oldTops;
vif1_regs.stat &= ~(1u << 7); // clear DBF
recomputeVif1Tops();
continue;
}
else if (opcode == VIF_BASE)
{
// Set double-buffer base
vif1_regs.base = imm & 0x3FFu;
recomputeVif1Tops();
continue;
}
else if (opcode == VIF_ITOP)
{
// Set ITOP register
vif1_regs.itop = imm & 0x3FFu;
continue;
}
else if (opcode == VIF_STMOD)
{
// Set decompression mode
vif1_regs.mode = imm & 3u;
continue;
}
else if (opcode == VIF_MSKPATH3)
{
// Mask/unmask GIF PATH3
// VIF command docs: MSKPATH3 uses IMMEDIATE bit 15.
const bool wasMasked = m_path3Masked;
m_path3Masked = (imm & 0x8000u) != 0u;
if (wasMasked && !m_path3Masked)
flushMaskedPath3Packets();
continue;
}
else if (opcode == VIF_MARK)
{
// Set MARK register
vif1_regs.mark = imm;
vif1_regs.stat |= (1u << 6); // MRK
continue;
}
else if (opcode == VIF_FLUSHE || opcode == VIF_FLUSH || opcode == VIF_FLUSHA)
{
// Wait for pipeline flush - no-op in software
continue;
}
else if (opcode == VIF_MSCAL || opcode == VIF_MSCALF)
{
// Start VU1 microprogram at address IMM - skip (no VU1 emu)
vif1_regs.itops = vif1_regs.itop & 0x3FFu;
vif1_regs.stat ^= (1u << 7); // toggle DBF
recomputeVif1Tops();
uint32_t startPC = (uint32_t)imm * 8u;
if (m_vu1MscalCallback)
m_vu1MscalCallback(startPC, vif1_regs.itop);
continue;
}
else if (opcode == VIF_MSCNT)
{
// Continue VU1 execution - skip
vif1_regs.itops = vif1_regs.itop & 0x3FFu;
vif1_regs.stat ^= (1u << 7); // toggle DBF
recomputeVif1Tops();
continue;
}
else if (opcode == VIF_STMASK)
{
// Next QW contains write mask - skip 4 bytes
pos += 4;
if (pos > sizeBytes)
if (pos + 4 > sizeBytes)
break;
uint32_t maskValue = 0;
std::memcpy(&maskValue, data + pos, sizeof(maskValue));
vif1_regs.mask = maskValue;
pos += 4;
continue;
}
else if (opcode == VIF_STROW)
{
// Next 4 words (16 bytes) = fill row registers
pos += 16;
if (pos > sizeBytes)
if (pos + 16 > sizeBytes)
break;
std::memcpy(vif1_regs.row, data + pos, 16);
pos += 16;
continue;
}
else if (opcode == VIF_STCOL)
{
// Next 4 words (16 bytes) = fill column registers
pos += 16;
if (pos > sizeBytes)
if (pos + 16 > sizeBytes)
break;
std::memcpy(vif1_regs.col, data + pos, 16);
pos += 16;
continue;
}
else if (opcode == VIF_MPG)
{
// Upload microprogram to VU1: NUM*8 bytes of data follow
uint32_t mpgBytes = (uint32_t)num * 8;
// Align to QW
mpgBytes = (mpgBytes + 15) & ~15u;
uint32_t destAddr = (uint32_t)imm * 8u;
// VIF MPG semantics: NUM==0 means 256 instructions (2048 bytes).
// MPG payload is instruction-packed and should not be QW-aligned.
const uint32_t instructionCount = (num == 0u) ? 256u : static_cast<uint32_t>(num);
const uint32_t mpgBytes = instructionCount * 8u;
if (m_vu1Code && destAddr < PS2_VU1_CODE_SIZE && mpgBytes > 0)
{
uint32_t copyBytes = mpgBytes;
if (destAddr + copyBytes > PS2_VU1_CODE_SIZE)
copyBytes = PS2_VU1_CODE_SIZE - destAddr;
if (pos + copyBytes <= sizeBytes)
std::memcpy(m_vu1Code + destAddr, data + pos, copyBytes);
}
pos += mpgBytes;
if (pos > sizeBytes)
break;
@@ -157,24 +191,18 @@ void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes)
}
else if (opcode == VIF_DIRECT || opcode == VIF_DIRECTHL)
{
// IMM = number of 128-bit quadwords of GIF data following
uint32_t qwCount = imm;
if (qwCount == 0)
qwCount = 65536; // 0 means 65536
qwCount = 65536;
const uint32_t availableQw = (sizeBytes - pos) / 16u;
const bool truncated = qwCount > availableQw;
if (qwCount > availableQw)
{
qwCount = availableQw;
}
if (qwCount > 0)
{
// The GIF data starts at current position in the source buffer
// processGIFPacket expects a physical RAM address
uint32_t gifPhysAddr = srcPhys + pos;
processGIFPacket(gifPhysAddr, qwCount);
g_vifDirectCount++;
const bool directHl = (opcode == VIF_DIRECTHL);
submitGifPacket(GifPathId::Path2, data + pos, qwCount * 16, true, directHl);
}
pos += qwCount * 16;
@@ -187,54 +215,248 @@ void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes)
}
else if ((opcode & 0x60) == 0x60)
{
// UNPACK commands (0x60-0x7F)
// Format: VN in bits 25:24, VL in bits 27:26
// NUM = number of vectors, IMM = VU addr
// Skip the data payload
uint8_t vn = (opcode >> 2) & 0x3; // 0=S, 1=V2, 2=V3, 3=V4
uint8_t vl = opcode & 0x3; // 0=32, 1=16, 2=8, 3=5
// Calculate component count and size
uint8_t vn = (opcode >> 2) & 0x3;
uint8_t vl = opcode & 0x3;
const bool maskEnable = (opcode & 0x10u) != 0u;
int components = vn + 1;
int bitsPerComponent;
int bitsPerComponent = 32;
switch (vl)
{
case 0:
bitsPerComponent = 32;
break;
case 1:
bitsPerComponent = 16;
break;
case 2:
bitsPerComponent = 8;
break;
case 3:
bitsPerComponent = 16;
break; // V4-5 is special (4x16 packed)
default:
bitsPerComponent = 32;
break;
case 0: bitsPerComponent = 32; break;
case 1: bitsPerComponent = 16; break;
case 2: bitsPerComponent = 8; break;
case 3: bitsPerComponent = (vn == 3) ? 4 : 16; break;
default: break;
}
// Total bits per vector
int bitsPerVector;
if (vl == 3 && vn == 3)
{
// V4-5: 4 components × 4-bit nibbles = 16 bits per vector.
bitsPerVector = 16;
}
else
{
bitsPerVector = components * bitsPerComponent;
}
int bitsPerVector = (vl == 3 && vn == 3) ? 16 : (components * bitsPerComponent);
uint32_t bytesPerVector = (bitsPerVector + 7) / 8;
uint32_t totalBytes = (uint32_t)num * bytesPerVector;
// Align to 32-bit word boundary
// UNPACK semantics: NUM is 8-bit and NUM==0 means 256 vectors (writes).
const uint32_t writeVectorCount = (num == 0u) ? 256u : static_cast<uint32_t>(num);
// STCYCL controls write cycles for UNPACK.
uint32_t cl = vif1_regs.cycle & 0xFFu;
uint32_t wl = (vif1_regs.cycle >> 8) & 0xFFu;
if (cl == 0u)
cl = 1u;
if (wl == 0u)
wl = 1u;
uint32_t sourceVectorCount = writeVectorCount;
if (cl < wl)
{
const uint32_t fullBlocks = writeVectorCount / wl;
uint32_t remainder = writeVectorCount % wl;
if (remainder > cl)
remainder = cl;
sourceVectorCount = fullBlocks * cl + remainder;
}
uint32_t totalBytes = sourceVectorCount * bytesPerVector;
totalBytes = (totalBytes + 3) & ~3u;
uint32_t vuAddr = (uint32_t)imm & 0x3FFu;
if ((imm & 0x8000u) != 0u)
vuAddr = (vuAddr + (vif1_regs.tops & 0x3FFu)) & 0x3FFu;
const bool zeroExtend = (imm & 0x4000u) != 0u;
if (m_vu1Data && totalBytes > 0 && pos + totalBytes <= sizeBytes)
{
const uint8_t *srcBase = data + pos;
uint32_t srcIndex = 0u;
for (uint32_t writeIndex = 0; writeIndex < writeVectorCount; ++writeIndex)
{
const uint32_t cyclePos = writeIndex % wl;
const bool sourceAvailable = (cl >= wl) || (cyclePos < cl);
uint32_t destVec = 0;
if (cl >= wl)
{
destVec = (vuAddr + (writeIndex / wl) * cl + cyclePos) & 0x3FFu;
}
else
{
destVec = (vuAddr + writeIndex) & 0x3FFu;
}
uint32_t destOff = destVec * 16u;
if (destOff + 16u > PS2_VU1_DATA_SIZE)
{
if (sourceAvailable && srcIndex < sourceVectorCount)
++srcIndex;
continue;
}
uint32_t lanes[4] = {0u, 0u, 0u, 0u};
std::memcpy(lanes, m_vu1Data + destOff, sizeof(lanes));
uint32_t decompressed[4] = {lanes[0], lanes[1], lanes[2], lanes[3]};
bool decoded = false;
const uint8_t *srcVec = nullptr;
if (sourceAvailable && srcIndex < sourceVectorCount)
{
srcVec = srcBase + srcIndex * bytesPerVector;
++srcIndex;
decoded = true;
}
auto extend16 = [&](uint16_t raw) -> uint32_t
{
if (zeroExtend)
return static_cast<uint32_t>(raw);
return static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(raw)));
};
auto extend8 = [&](uint8_t raw) -> uint32_t
{
if (zeroExtend)
return static_cast<uint32_t>(raw);
return static_cast<uint32_t>(static_cast<int32_t>(static_cast<int8_t>(raw)));
};
bool handledFormat = true;
if (!decoded)
{
handledFormat = false;
}
else if (vl == 0u)
{
if (components == 1)
{
uint32_t scalar = 0;
std::memcpy(&scalar, srcVec, sizeof(scalar));
decompressed[0] = scalar;
decompressed[1] = scalar;
decompressed[2] = scalar;
decompressed[3] = scalar;
}
else
{
const uint32_t limit = (components > 4) ? 4u : static_cast<uint32_t>(components);
for (uint32_t c = 0; c < limit; ++c)
{
uint32_t scalar = 0;
std::memcpy(&scalar, srcVec + c * 4u, sizeof(scalar));
decompressed[c] = scalar;
}
}
}
else if (vl == 1u)
{
if (components == 1)
{
uint16_t raw = 0;
std::memcpy(&raw, srcVec, sizeof(raw));
const uint32_t scalar = extend16(raw);
decompressed[0] = scalar;
decompressed[1] = scalar;
decompressed[2] = scalar;
decompressed[3] = scalar;
}
else
{
const uint32_t limit = (components > 4) ? 4u : static_cast<uint32_t>(components);
for (uint32_t c = 0; c < limit; ++c)
{
uint16_t raw = 0;
std::memcpy(&raw, srcVec + c * 2u, sizeof(raw));
decompressed[c] = extend16(raw);
}
}
}
else if (vl == 2u)
{
if (components == 1)
{
const uint32_t scalar = extend8(srcVec[0]);
decompressed[0] = scalar;
decompressed[1] = scalar;
decompressed[2] = scalar;
decompressed[3] = scalar;
}
else
{
const uint32_t limit = (components > 4) ? 4u : static_cast<uint32_t>(components);
for (uint32_t c = 0; c < limit; ++c)
{
decompressed[c] = extend8(srcVec[c]);
}
}
}
else if (vl == 3u && vn == 3u)
{
// V4-5: packed color-like format in a single 16-bit value.
uint16_t packed = 0;
std::memcpy(&packed, srcVec, sizeof(packed));
decompressed[0] = packed & 0x1Fu;
decompressed[1] = (packed >> 5) & 0x1Fu;
decompressed[2] = (packed >> 10) & 0x1Fu;
decompressed[3] = (packed >> 15) & 0x01u;
}
else
{
handledFormat = false;
}
// Unknown compressed format fallback: preserve legacy raw-copy behavior.
if (!handledFormat && decoded && !maskEnable && (vif1_regs.mode == 0u || vif1_regs.mode == 3u))
{
uint32_t copyBytes = (bytesPerVector < 16u) ? bytesPerVector : 16u;
std::memcpy(m_vu1Data + destOff, srcVec, copyBytes);
continue;
}
const bool canAdd = (vl != 3u || vn != 3u);
const uint32_t mode = vif1_regs.mode & 3u;
const uint32_t colIdx = (cyclePos > 3u) ? 3u : cyclePos;
const uint32_t maskCycle = (cyclePos > 3u) ? 3u : cyclePos;
for (uint32_t field = 0u; field < 4u; ++field)
{
uint32_t maskSpec = 0u;
if (maskEnable)
{
const uint32_t shift = ((maskCycle * 4u) + field) * 2u;
maskSpec = (vif1_regs.mask >> shift) & 0x3u;
}
// In fill-write cycles with suspended source reads, treat raw-data selections as row-fill.
if (!decoded && maskSpec == 0u)
maskSpec = 1u;
uint32_t writeVal = lanes[field];
if (maskSpec == 0u)
{
if (handledFormat)
{
writeVal = decompressed[field];
if (canAdd && (mode == 1u || mode == 2u))
{
writeVal = writeVal + vif1_regs.row[field];
if (mode == 2u)
vif1_regs.row[field] = writeVal;
}
}
}
else if (maskSpec == 1u)
{
writeVal = vif1_regs.row[field];
}
else if (maskSpec == 2u)
{
writeVal = vif1_regs.col[colIdx];
}
else
{
continue; // write-protect
}
lanes[field] = writeVal;
}
std::memcpy(m_vu1Data + destOff, lanes, sizeof(lanes));
}
}
pos += totalBytes;
g_vifUnpackCount++;
if (pos > sizeBytes)
break;
@@ -242,27 +464,7 @@ void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes)
}
else
{
// Unknown VIF command - try to continue
if (g_vifLogCount < 10)
{
std::cerr << "[VIF1] Unknown opcode 0x" << std::hex << (int)opcode
<< " at offset 0x" << (pos - 4) << std::dec << std::endl;
g_vifLogCount++;
}
continue;
}
}
static uint32_t s_logInterval = 0;
if (++s_logInterval >= 100)
{
if (g_vifLogCount < 50)
{
std::cerr << "[VIF1] stats: total_cmds=" << g_vifTotalCmds
<< " direct=" << g_vifDirectCount
<< " unpack=" << g_vifUnpackCount << std::endl;
g_vifLogCount++;
}
s_logInterval = 0;
}
}
File diff suppressed because it is too large Load Diff
@@ -1279,6 +1279,15 @@ namespace
uint32_t toDmaPhys(uint32_t addr)
{
if ((addr & 0x80000000u) != 0)
{
uint32_t lower = addr & 0x7FFFFFFFu;
if (lower >= PS2_SCRATCHPAD_BASE &&
lower < PS2_SCRATCHPAD_BASE + PS2_SCRATCHPAD_SIZE)
{
return lower;
}
}
return addr & 0x1FFFFFFFu;
}
@@ -1395,28 +1404,7 @@ namespace
}
else
{
const ParsedDmaTag tag = tryParseDmaTag(rdram, payloadPhys);
if (tag.valid && tag.qwc != 0)
{
qwc = tag.qwc;
switch (tag.id)
{
case 0: // REFE
case 3: // REF
case 4: // REFS
madr = toDmaPhys(tag.addr);
break;
default:
// CNT/NEXT/CALL/RET-style tags carry payload inline after the tag.
madr = toDmaPhys(payloadPhys + 0x10u);
break;
}
}
else
{
// Fall back to chain mode so the runtime DMA path can walk TADR.
chcr = 0x00000185u; // MODE=1 chain, DIR=1, TIE=1, STR=1.
}
chcr = 0x00000185u; // MODE=1 chain, DIR=1, TIE=1, STR=1.
}
PS2Memory &mem = runtime->memory();
@@ -1497,8 +1485,11 @@ namespace
struct GsDispEnvMem
{
uint64_t display;
uint64_t pmode;
uint64_t smode2;
uint64_t dispfb;
uint64_t display;
uint64_t bgcolor;
};
struct GsImageMem
@@ -1704,7 +1695,10 @@ namespace
uint8_t *ptr = getMemPtr(rdram, addr);
if (!ptr)
return false;
GsDispEnvMem env{display, dispfb};
GsDispEnvMem env{};
std::memcpy(&env, ptr, sizeof(env));
env.dispfb = dispfb;
env.display = display;
std::memcpy(ptr, &env, sizeof(env));
return true;
}
+451 -148
View File
@@ -1,3 +1,120 @@
namespace
{
std::mutex g_gs_sync_v_callback_mutex;
uint32_t g_gs_sync_v_callback_func = 0u;
uint32_t g_gs_sync_v_callback_gp = 0u;
uint32_t g_gs_sync_v_callback_sp = 0u;
uint32_t g_gs_sync_v_callback_stack_base = 0u;
uint32_t g_gs_sync_v_callback_stack_top = 0u;
uint64_t g_gs_sync_v_callback_tick = 0u;
uint32_t g_gs_sync_v_callback_bad_pc_logs = 0u;
}
void resetGsSyncVCallbackState()
{
std::lock_guard<std::mutex> lock(g_gs_sync_v_callback_mutex);
g_gs_sync_v_callback_func = 0u;
g_gs_sync_v_callback_gp = 0u;
g_gs_sync_v_callback_sp = 0u;
g_gs_sync_v_callback_stack_base = 0u;
g_gs_sync_v_callback_stack_top = 0u;
g_gs_sync_v_callback_tick = 0u;
g_gs_sync_v_callback_bad_pc_logs = 0u;
}
void dispatchGsSyncVCallback(uint8_t *rdram, PS2Runtime *runtime)
{
if (!rdram || !runtime)
{
return;
}
uint32_t callback = 0u;
uint32_t gp = 0u;
uint32_t sp = 0u;
uint32_t callbackStackTop = 0u;
uint64_t tick = 0u;
{
std::lock_guard<std::mutex> lock(g_gs_sync_v_callback_mutex);
callback = g_gs_sync_v_callback_func;
gp = g_gs_sync_v_callback_gp;
sp = g_gs_sync_v_callback_sp;
callbackStackTop = g_gs_sync_v_callback_stack_top;
if (callback == 0u)
{
return;
}
tick = ++g_gs_sync_v_callback_tick;
}
if (!runtime->hasFunction(callback))
{
return;
}
if (callbackStackTop == 0u)
{
constexpr uint32_t kCallbackStackSize = 0x4000u;
const uint32_t stackBase = runtime->guestMalloc(kCallbackStackSize, 16u);
if (stackBase != 0u)
{
std::lock_guard<std::mutex> lock(g_gs_sync_v_callback_mutex);
if (g_gs_sync_v_callback_stack_top == 0u)
{
g_gs_sync_v_callback_stack_base = stackBase;
g_gs_sync_v_callback_stack_top = stackBase + kCallbackStackSize - 0x10u;
}
callbackStackTop = g_gs_sync_v_callback_stack_top;
}
}
try
{
R5900Context callbackCtx{};
SET_GPR_U32(&callbackCtx, 28, gp);
SET_GPR_U32(&callbackCtx, 29, (callbackStackTop != 0u) ? callbackStackTop : ((sp != 0u) ? sp : (PS2_RAM_SIZE - 0x10u)));
SET_GPR_U32(&callbackCtx, 31, 0u);
SET_GPR_U32(&callbackCtx, 4, static_cast<uint32_t>(tick));
callbackCtx.pc = callback;
uint32_t steps = 0u;
while (callbackCtx.pc != 0u && !runtime->isStopRequested() && steps < 1024u)
{
if (!runtime->hasFunction(callbackCtx.pc))
{
if (g_gs_sync_v_callback_bad_pc_logs < 16u)
{
std::cerr << "[sceGsSyncVCallback:bad-pc] pc=0x" << std::hex << callbackCtx.pc
<< " ra=0x" << getRegU32(&callbackCtx, 31)
<< " sp=0x" << getRegU32(&callbackCtx, 29)
<< " gp=0x" << getRegU32(&callbackCtx, 28)
<< std::dec << std::endl;
++g_gs_sync_v_callback_bad_pc_logs;
}
callbackCtx.pc = 0u;
break;
}
auto step = runtime->lookupFunction(callbackCtx.pc);
if (!step)
{
break;
}
++steps;
step(rdram, &callbackCtx, runtime);
}
}
catch (const std::exception &e)
{
static uint32_t warnCount = 0u;
if (warnCount < 8u)
{
std::cerr << "[sceGsSyncVCallback] callback exception: " << e.what() << std::endl;
++warnCount;
}
}
}
void sceGsExecLoadImage(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t imgAddr = getRegU32(ctx, 4);
@@ -18,54 +135,55 @@ void sceGsExecLoadImage(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
uint32_t fbw = img.vram_width ? img.vram_width : std::max<uint32_t>(1, (img.width + 63) / 64);
uint32_t base = static_cast<uint32_t>(img.vram_addr) * 2048u;
uint32_t stride = bytesForPixels(img.psm, fbw * 64u);
if (stride == 0)
const uint32_t totalImageBytes = rowBytes * static_cast<uint32_t>(img.height);
const uint32_t headerQwc = 12u;
const uint32_t imageQwc = (totalImageBytes + 15u) / 16u;
const uint32_t totalQwc = headerQwc + imageQwc;
uint32_t pktAddr = runtime->guestMalloc(totalQwc * 16u, 16u);
if (pktAddr == 0)
{
setReturnS32(ctx, -1);
return;
}
uint8_t *gsvram = runtime->memory().getGSVRAM();
uint8_t *src = getMemPtr(rdram, srcAddr);
if (!gsvram || !src)
uint8_t *pkt = getMemPtr(rdram, pktAddr);
const uint8_t *src = getConstMemPtr(rdram, srcAddr);
if (!pkt || !src)
{
setReturnS32(ctx, -1);
return;
}
static int logCount = 0;
if (logCount < 8)
{
std::cout << "ps2_stub sceGsExecLoadImage: x=" << img.x
<< " y=" << img.y
<< " w=" << img.width
<< " h=" << img.height
<< " vram=0x" << std::hex << img.vram_addr
<< " fbw=" << std::dec << static_cast<int>(fbw)
<< " psm=" << static_cast<int>(img.psm)
<< " src=0x" << std::hex << srcAddr << std::dec << std::endl;
++logCount;
}
uint32_t dbp = (static_cast<uint32_t>(img.vram_addr) * 2048u) / 256u;
uint32_t dsax = static_cast<uint32_t>(img.x);
uint32_t dsay = static_cast<uint32_t>(img.y);
for (uint32_t row = 0; row < img.height; ++row)
{
uint32_t dstOff = base + (static_cast<uint32_t>(img.y) + row) * stride + bytesForPixels(img.psm, static_cast<uint32_t>(img.x));
uint32_t srcOff = row * rowBytes;
if (dstOff >= PS2_GS_VRAM_SIZE)
break;
uint32_t copyBytes = rowBytes;
if (dstOff + copyBytes > PS2_GS_VRAM_SIZE)
copyBytes = PS2_GS_VRAM_SIZE - dstOff;
std::memcpy(gsvram + dstOff, src + srcOff, copyBytes);
}
uint64_t *q = reinterpret_cast<uint64_t *>(pkt);
q[0] = 0x1000000000000004ULL;
q[1] = 0x0E0E0E0E0E0E0E0EULL;
q[2] = (static_cast<uint64_t>(img.psm & 0x3Fu) << 24) | (static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(dbp & 0x3FFFu) << 32) | (static_cast<uint64_t>(fbw & 0x3Fu) << 48) |
(static_cast<uint64_t>(img.psm & 0x3Fu) << 56);
q[3] = 0x50ULL;
q[4] = (static_cast<uint64_t>(dsay & 0x7FFu) << 48) | (static_cast<uint64_t>(dsax & 0x7FFu) << 32);
q[5] = 0x51ULL;
q[6] = (static_cast<uint64_t>(img.height) << 32) | static_cast<uint64_t>(img.width);
q[7] = 0x52ULL;
q[8] = 0ULL;
q[9] = 0x53ULL;
q[10] = (static_cast<uint64_t>(2) << 58) | (static_cast<uint64_t>(imageQwc) & 0x7FFF) |
(1ULL << 15);
q[11] = 0ULL;
if (img.width >= 320 && img.height >= 200)
{
auto &gs = runtime->memory().gs();
gs.dispfb1 = makeDispFb(img.vram_addr, fbw, img.psm, 0, 0);
gs.display1 = makeDisplay(0, 0, 0, 0, img.width - 1, img.height - 1);
}
std::memcpy(pkt + 12 * 8, src, totalImageBytes);
constexpr uint32_t GIF_CHANNEL = 0x1000A000;
constexpr uint32_t CHCR_STR_MODE0 = 0x101u;
auto &mem = runtime->memory();
mem.writeIORegister(GIF_CHANNEL + 0x10u, pktAddr);
mem.writeIORegister(GIF_CHANNEL + 0x20u, totalQwc & 0xFFFFu);
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
setReturnS32(ctx, 0);
}
@@ -90,48 +208,64 @@ void sceGsExecStoreImage(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
uint32_t fbw = img.vram_width ? img.vram_width : std::max<uint32_t>(1, (img.width + 63) / 64);
uint32_t base = static_cast<uint32_t>(img.vram_addr) * 2048u;
uint32_t stride = bytesForPixels(img.psm, fbw * 64u);
if (stride == 0)
{
setReturnS32(ctx, -1);
return;
}
const uint32_t totalImageBytes = rowBytes * static_cast<uint32_t>(img.height);
uint8_t *gsvram = runtime->memory().getGSVRAM();
uint8_t *dst = getMemPtr(rdram, dstAddr);
if (!gsvram || !dst)
if (!dst)
{
setReturnS32(ctx, -1);
return;
}
static int logCount = 0;
if (logCount < 8)
uint32_t sbp = (static_cast<uint32_t>(img.vram_addr) * 2048u) / 256u;
uint64_t bitbltbuf = (static_cast<uint64_t>(sbp & 0x3FFFu) << 0) |
(static_cast<uint64_t>(fbw & 0x3Fu) << 16) |
(static_cast<uint64_t>(img.psm & 0x3Fu) << 24) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(0u) << 56);
uint64_t trxpos = (static_cast<uint64_t>(img.x & 0x7FFu) << 0) |
(static_cast<uint64_t>(img.y & 0x7FFu) << 16) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(0u) << 48);
uint64_t trxreg = static_cast<uint64_t>(img.height) << 32 | static_cast<uint64_t>(img.width);
uint32_t pktAddr = runtime->guestMalloc(80u, 16u);
if (pktAddr == 0)
{
std::cout << "ps2_stub sceGsExecStoreImage: x=" << img.x
<< " y=" << img.y
<< " w=" << img.width
<< " h=" << img.height
<< " vram=0x" << std::hex << img.vram_addr
<< " fbw=" << std::dec << static_cast<int>(fbw)
<< " psm=" << static_cast<int>(img.psm)
<< " dst=0x" << std::hex << dstAddr << std::dec << std::endl;
++logCount;
setReturnS32(ctx, -1);
return;
}
for (uint32_t row = 0; row < img.height; ++row)
uint8_t *pkt = getMemPtr(rdram, pktAddr);
if (!pkt)
{
uint32_t srcOff = base + (static_cast<uint32_t>(img.y) + row) * stride + bytesForPixels(img.psm, static_cast<uint32_t>(img.x));
uint32_t dstOff = row * rowBytes;
if (srcOff >= PS2_GS_VRAM_SIZE)
break;
uint32_t copyBytes = rowBytes;
if (srcOff + copyBytes > PS2_GS_VRAM_SIZE)
copyBytes = PS2_GS_VRAM_SIZE - srcOff;
std::memcpy(dst + dstOff, gsvram + srcOff, copyBytes);
setReturnS32(ctx, -1);
return;
}
uint64_t *q = reinterpret_cast<uint64_t *>(pkt);
q[0] = 0x1000000000000004ULL;
q[1] = 0x0E0E0E0E0E0E0E0EULL;
q[2] = bitbltbuf;
q[3] = 0x50ULL;
q[4] = trxpos;
q[5] = 0x51ULL;
q[6] = trxreg;
q[7] = 0x52ULL;
q[8] = 1ULL;
q[9] = 0x53ULL;
constexpr uint32_t GIF_CHANNEL = 0x1000A000;
constexpr uint32_t CHCR_STR_MODE0 = 0x101u;
auto &mem = runtime->memory();
mem.writeIORegister(GIF_CHANNEL + 0x10u, pktAddr);
mem.writeIORegister(GIF_CHANNEL + 0x20u, 5u);
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
mem.processPendingTransfers();
runtime->gs().consumeLocalToHostBytes(dst, totalImageBytes);
setReturnS32(ctx, 0);
}
@@ -144,53 +278,40 @@ void sceGsGetGParam(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void sceGsPutDispEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t envAddr = getRegU32(ctx, 4);
GsDispEnvMem env{};
if (readGsDispEnv(rdram, envAddr, env))
uint8_t *ptr = getMemPtr(rdram, envAddr);
if (!ptr)
{
auto &gs = runtime->memory().gs();
gs.display1 = env.display;
gs.dispfb1 = env.dispfb;
setReturnS32(ctx, -1);
return;
}
constexpr uint32_t GIF_CHANNEL = 0x1000A000;
constexpr uint32_t QWC = 5;
constexpr uint32_t CHCR_STR_MODE0 = 0x101u;
auto &mem = runtime->memory();
mem.writeIORegister(GIF_CHANNEL + 0x10u, envAddr);
mem.writeIORegister(GIF_CHANNEL + 0x20u, QWC);
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
setReturnS32(ctx, 0);
}
void sceGsPutDrawEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t envAddr = getRegU32(ctx, 4);
uint32_t psm = getRegU32(ctx, 5);
uint32_t w = getRegU32(ctx, 6);
uint32_t h = getRegU32(ctx, 7);
if (w == 0)
w = 640;
if (h == 0)
h = 448;
GsDrawEnvMem env{};
env.offset_x = static_cast<uint16_t>(2048 - (w / 2));
env.offset_y = static_cast<uint16_t>(2048 - (h / 2));
env.clip_x = 0;
env.clip_y = 0;
env.clip_w = static_cast<uint16_t>(w);
env.clip_h = static_cast<uint16_t>(h);
env.vram_addr = 0;
env.fbw = static_cast<uint8_t>((w + 63) / 64);
env.psm = static_cast<uint8_t>(psm);
env.vram_x = 0;
env.vram_y = 0;
env.draw_mask = 0;
env.auto_clear = 1;
env.bg_r = 1;
env.bg_g = 1;
env.bg_b = 1;
env.bg_a = 0x80;
env.bg_q = 0.0f;
uint8_t *ptr = getMemPtr(rdram, envAddr);
if (ptr)
if (!ptr)
{
std::memcpy(ptr, &env, sizeof(env));
setReturnS32(ctx, -1);
return;
}
constexpr uint32_t GIF_CHANNEL = 0x1000A000;
constexpr uint32_t QWC = 9;
constexpr uint32_t CHCR_STR_MODE0 = 0x101u;
auto &mem = runtime->memory();
mem.writeIORegister(GIF_CHANNEL + 0x10u, envAddr);
mem.writeIORegister(GIF_CHANNEL + 0x20u, QWC);
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
setReturnS32(ctx, 0);
}
@@ -208,11 +329,42 @@ void sceGsResetGraph(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
g_gparam.ffmode = static_cast<uint8_t>(ffmode & 0x1);
writeGsGParamToScratch(runtime);
auto &gs = runtime->memory().gs();
gs.pmode = makePmode(1, 0, 0, 0, 0, 0x80);
gs.smode2 = (interlace & 0x1) | ((ffmode & 0x1) << 1);
gs.dispfb1 = makeDispFb(0, 10, 0, 0, 0);
gs.display1 = makeDisplay(0, 0, 0, 0, 639, 447);
uint64_t pmode = makePmode(1, 0, 0, 0, 0, 0x80);
uint64_t smode2 = (interlace & 0x1) | ((ffmode & 0x1) << 1);
uint64_t dispfb = makeDispFb(0, 10, 0, 0, 0);
uint64_t display = makeDisplay(0, 0, 0, 0, 639, 447);
uint64_t bgcolor = 0ULL;
if (runtime)
{
uint32_t pktAddr = runtime->guestMalloc(192u, 16u);
if (pktAddr != 0u)
{
uint8_t *pkt = getMemPtr(rdram, pktAddr);
if (pkt)
{
uint64_t *q = reinterpret_cast<uint64_t *>(pkt);
q[0] = 0x1000000000000005ULL;
q[1] = 0x0E0E0E0E0E0E0E0EULL;
q[2] = pmode;
q[3] = 0x41ULL;
q[4] = smode2;
q[5] = 0x42ULL;
q[6] = dispfb;
q[7] = 0x59ULL;
q[8] = display;
q[9] = 0x5aULL;
q[10] = bgcolor;
q[11] = 0x5fULL;
constexpr uint32_t GIF_CHANNEL = 0x1000A000;
constexpr uint32_t CHCR_STR_MODE0 = 0x101u;
auto &mem = runtime->memory();
mem.writeIORegister(GIF_CHANNEL + 0x10u, pktAddr);
mem.writeIORegister(GIF_CHANNEL + 0x20u, 12u);
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
}
}
}
}
setReturnS32(ctx, 0);
@@ -225,11 +377,9 @@ void sceGsResetPath(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void sceGsSetDefClear(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t clearAddr = getRegU32(ctx, 4);
if (uint8_t *clear = getMemPtr(rdram, clearAddr))
{
std::memset(clear, 0, 64);
}
(void)rdram;
(void)ctx;
(void)runtime;
setReturnS32(ctx, 0);
}
@@ -262,45 +412,65 @@ void sceGsSetDefDispEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void sceGsSetDefDrawEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t envAddr = getRegU32(ctx, 4);
uint32_t psm = getRegU32(ctx, 5);
uint32_t w = getRegU32(ctx, 6);
uint32_t h = getRegU32(ctx, 7);
const uint32_t vramAddr = readStackU32(rdram, ctx, 16);
const uint32_t vramX = readStackU32(rdram, ctx, 20);
const uint32_t vramY = readStackU32(rdram, ctx, 24);
uint32_t envAddr = getRegU32(ctx, 4);
uint32_t param_2 = getRegU32(ctx, 5);
int32_t w = static_cast<int32_t>(static_cast<int16_t>(getRegU32(ctx, 6) & 0xFFFF));
int32_t h = static_cast<int32_t>(static_cast<int16_t>(getRegU32(ctx, 7) & 0xFFFF));
uint32_t param_5 = readStackU32(rdram, ctx, 16);
uint32_t param_6 = readStackU32(rdram, ctx, 20);
if (w == 0)
if (w <= 0)
w = 640;
if (h == 0)
if (h <= 0)
h = 448;
GsDrawEnvMem env{};
env.offset_x = static_cast<uint16_t>(2048 - (w / 2));
env.offset_y = static_cast<uint16_t>(2048 - (h / 2));
env.clip_x = 0;
env.clip_y = 0;
env.clip_w = static_cast<uint16_t>(w);
env.clip_h = static_cast<uint16_t>(h);
env.vram_addr = static_cast<uint16_t>(vramAddr & 0xFFFFu);
env.fbw = static_cast<uint8_t>((w + 63u) / 64u);
env.psm = static_cast<uint8_t>(psm & 0xFFu);
env.vram_x = static_cast<uint16_t>(vramX & 0xFFFFu);
env.vram_y = static_cast<uint16_t>(vramY & 0xFFFFu);
env.draw_mask = 0;
env.auto_clear = 1;
env.bg_r = 0;
env.bg_g = 0;
env.bg_b = 0;
env.bg_a = 0x80;
env.bg_q = 0.0f;
uint32_t psm = param_2 & 0xFU;
uint32_t fbw = ((static_cast<uint32_t>(w) + 63u) >> 6) & 0x3FU;
sceGszbufaddr(rdram, ctx, runtime);
int32_t zbuf = static_cast<int32_t>(static_cast<int16_t>(getRegU32(ctx, 2) & 0xFFFF));
if (uint8_t *ptr = getMemPtr(rdram, envAddr))
uint8_t *const ptr = getMemPtr(rdram, envAddr);
if (!ptr)
{
std::memcpy(ptr, &env, sizeof(env));
setReturnS32(ctx, 8);
return;
}
setReturnS32(ctx, 0);
uint64_t *const words = reinterpret_cast<uint64_t *>(ptr);
words[0] = 0x1000000000008008ULL;
words[1] = 0x000000000000000EULL;
words[2] = (static_cast<uint64_t>(fbw) << 16) | (static_cast<uint64_t>(psm) << 24);
words[3] = 0x4c;
words[4] = (static_cast<uint64_t>(zbuf) & 0xFFFFULL) | (static_cast<uint64_t>(param_6 & 0xF) << 24) |
(param_5 == 0 ? 0x100000000ULL : 0ULL);
words[5] = 0x4e;
int32_t off_x = 0x800 - (w >> 1);
int32_t off_y = 0x800 - (h >> 1);
words[6] = (static_cast<uint64_t>(static_cast<uint32_t>(off_y) & 0xFFFF) << 36) |
(static_cast<uint32_t>(off_x) & 0xFFFF) * 16ULL;
words[7] = 0x18;
words[8] = (static_cast<uint64_t>(static_cast<uint32_t>(h - 1) & 0xFFFF) << 48) |
(static_cast<uint64_t>(static_cast<uint32_t>(w - 1) & 0xFFFF) << 16);
words[9] = 0x40;
words[10] = 1;
words[11] = 0x1a;
words[12] = 1;
words[13] = 0x46;
words[14] = (param_2 & 2) ? 1ULL : 0ULL;
words[15] = 0x45;
words[16] = (param_5 == 0) ? 0x30000ULL : ((static_cast<uint64_t>(param_5 & 3) << 17) | 0x10000ULL);
words[17] = 0x47;
setReturnS32(ctx, 8);
}
void sceGsSetDefDrawEnv2(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -333,7 +503,6 @@ void sceGsSetDefStoreImage(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtim
void sceGsSwapDBuffDc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
// can we get away with that ? kkkk
static int cur = 0;
cur ^= 1;
setReturnS32(ctx, cur);
@@ -341,22 +510,156 @@ void sceGsSwapDBuffDc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void sceGsSyncPath(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, 0);
int32_t mode = static_cast<int32_t>(getRegU32(ctx, 4));
auto &mem = runtime->memory();
if (mode == 0)
{
mem.processPendingTransfers();
uint32_t count = 0;
constexpr uint32_t kTimeout = 0x1000000;
while ((mem.readIORegister(0x10009000) & 0x100) != 0)
{
if (++count > kTimeout)
{
setReturnS32(ctx, -1);
return;
}
}
while ((mem.readIORegister(0x1000A000) & 0x100) != 0)
{
if (++count > kTimeout)
{
setReturnS32(ctx, -1);
return;
}
}
while ((mem.readIORegister(0x10003C00) & 0x1F000003) != 0)
{
if (++count > kTimeout)
{
setReturnS32(ctx, -1);
return;
}
}
while ((mem.readIORegister(0x10003020) & 0xC00) != 0)
{
if (++count > kTimeout)
{
setReturnS32(ctx, -1);
return;
}
}
setReturnS32(ctx, 0);
}
else
{
uint32_t result = 0;
if ((mem.readIORegister(0x10009000) & 0x100) != 0)
result |= 1;
if ((mem.readIORegister(0x1000A000) & 0x100) != 0)
result |= 2;
if ((mem.readIORegister(0x10003C00) & 0x1F000003) != 0)
result |= 4;
if ((mem.readIORegister(0x10003020) & 0xC00) != 0)
result |= 0x10;
setReturnS32(ctx, result);
}
}
void sceGsSyncV(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
ps2_syscalls::WaitVSyncTick(rdram, runtime);
setReturnS32(ctx, 0);
}
void sceGsSyncVCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
ps2_syscalls::WaitVSyncTick(rdram, runtime);
setReturnS32(ctx, 0);
(void)rdram;
const uint32_t newCallback = getRegU32(ctx, 4);
const uint32_t callerPc = ctx ? ctx->pc : 0u;
const uint32_t callerRa = ctx ? getRegU32(ctx, 31) : 0u;
const uint32_t gp = getRegU32(ctx, 28);
const uint32_t sp = getRegU32(ctx, 29);
uint32_t oldCallback = 0u;
{
std::lock_guard<std::mutex> lock(g_gs_sync_v_callback_mutex);
oldCallback = g_gs_sync_v_callback_func;
g_gs_sync_v_callback_func = newCallback;
if (newCallback != 0u)
{
g_gs_sync_v_callback_gp = gp;
g_gs_sync_v_callback_sp = sp;
}
}
static uint32_t s_syncVCallbackLogCount = 0u;
if (s_syncVCallbackLogCount < 128u)
{
std::cout << "[sceGsSyncVCallback:set] new=0x" << std::hex << newCallback
<< " old=0x" << oldCallback
<< " callerPc=0x" << callerPc
<< " callerRa=0x" << callerRa
<< " gp=0x" << gp
<< " sp=0x" << sp
<< std::dec << std::endl;
++s_syncVCallbackLogCount;
}
setReturnU32(ctx, oldCallback);
}
void sceGszbufaddr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnU32(ctx, getRegU32(ctx, 4));
(void)rdram;
uint32_t param_1 = getRegU32(ctx, 4);
int32_t w = static_cast<int32_t>(static_cast<int16_t>(getRegU32(ctx, 6) & 0xFFFF));
int32_t h = static_cast<int32_t>(static_cast<int16_t>(getRegU32(ctx, 7) & 0xFFFF));
int32_t width_blocks = (w + 63) >> 6;
if (w + 63 < 0)
width_blocks = (w + 126) >> 6;
int32_t height_blocks;
if ((param_1 & 2) != 0)
{
int32_t v = (h + 63) >> 6;
if (h + 63 < 0)
v = (h + 126) >> 6;
height_blocks = v;
}
else
{
int32_t v = (h + 31) >> 5;
if (h + 31 < 0)
v = (h + 62) >> 5;
height_blocks = v;
}
int32_t product = width_blocks * height_blocks;
uint64_t gparam_val = 0;
if (runtime)
{
uint8_t *scratch = runtime->memory().getScratchpad();
if (scratch)
{
std::memcpy(&gparam_val, scratch + 0x100, sizeof(gparam_val));
}
}
if ((gparam_val & 0xFFFF0000FFFFULL) == 1ULL)
product = (product * 0x10000) >> 16;
else
product = (product * 0x20000) >> 16;
setReturnS32(ctx, product);
}
+136 -49
View File
@@ -1,3 +1,45 @@
namespace
{
uint32_t sanitizeMemTransferSize(uint32_t size, const char *op)
{
constexpr uint32_t kMaxTransfer = PS2_RAM_SIZE;
if (size <= kMaxTransfer)
{
return size;
}
static std::mutex s_warnMutex;
static std::unordered_map<std::string, uint32_t> s_warnCounts;
uint32_t warnCount = 0u;
{
std::lock_guard<std::mutex> lock(s_warnMutex);
warnCount = ++s_warnCounts[op ? op : "memop"];
}
if (warnCount <= 16u)
{
std::cerr << "[" << (op ? op : "memop") << "] size clamp from 0x"
<< std::hex << size << " to 0x" << kMaxTransfer
<< std::dec << std::endl;
}
return kMaxTransfer;
}
uint32_t guestContiguousBytes(uint32_t guestAddr)
{
uint32_t offset = 0u;
bool scratch = false;
if (!ps2ResolveGuestPointer(guestAddr, offset, scratch))
{
return 0u;
}
if (scratch)
{
return (offset < PS2_SCRATCHPAD_SIZE) ? (PS2_SCRATCHPAD_SIZE - offset) : 0u;
}
return (offset < PS2_RAM_SIZE) ? (PS2_RAM_SIZE - offset) : 0u;
}
}
void malloc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t size = getRegU32(ctx, 4); // $a0
@@ -34,22 +76,38 @@ void memcpy(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t destAddr = getRegU32(ctx, 4); // $a0
uint32_t srcAddr = getRegU32(ctx, 5); // $a1
size_t size = getRegU32(ctx, 6); // $a2
uint32_t size = getRegU32(ctx, 6); // $a2
size = sanitizeMemTransferSize(size, "memcpy");
uint8_t *hostDest = getMemPtr(rdram, destAddr);
const uint8_t *hostSrc = getConstMemPtr(rdram, srcAddr);
if (hostDest && hostSrc)
uint32_t copied = 0u;
uint32_t curDst = destAddr;
uint32_t curSrc = srcAddr;
while (copied < size)
{
::memcpy(hostDest, hostSrc, size);
ps2TraceGuestRangeWrite(rdram, destAddr, static_cast<uint32_t>(size), "memcpy", ctx);
uint8_t *hostDest = getMemPtr(rdram, curDst);
const uint8_t *hostSrc = getConstMemPtr(rdram, curSrc);
if (!hostDest || !hostSrc)
{
break;
}
uint32_t chunk = size - copied;
chunk = std::min(chunk, guestContiguousBytes(curDst));
chunk = std::min(chunk, guestContiguousBytes(curSrc));
if (chunk == 0u)
{
break;
}
::memcpy(hostDest, hostSrc, chunk);
copied += chunk;
curDst += chunk;
curSrc += chunk;
}
else
if (copied != 0u)
{
std::cerr << "memcpy error: Attempted copy involving non-RDRAM address (or invalid RDRAM address)."
<< " Dest: 0x" << std::hex << destAddr << " (host ptr valid: " << (hostDest != nullptr) << ")"
<< ", Src: 0x" << srcAddr << " (host ptr valid: " << (hostSrc != nullptr) << ")" << std::dec
<< ", Size: " << size << std::endl;
ps2TraceGuestRangeWrite(rdram, destAddr, copied, "memcpy", ctx);
}
// returns dest pointer ($v0 = $a0)
@@ -61,17 +119,33 @@ void memset(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
uint32_t destAddr = getRegU32(ctx, 4); // $a0
int value = (int)(getRegU32(ctx, 5) & 0xFF); // $a1 (char value)
uint32_t size = getRegU32(ctx, 6); // $a2
size = sanitizeMemTransferSize(size, "memset");
uint8_t *hostDest = getMemPtr(rdram, destAddr);
if (hostDest)
uint32_t written = 0u;
uint32_t curDst = destAddr;
while (written < size)
{
::memset(hostDest, value, size);
ps2TraceGuestRangeWrite(rdram, destAddr, size, "memset", ctx);
uint8_t *hostDest = getMemPtr(rdram, curDst);
if (!hostDest)
{
break;
}
uint32_t chunk = size - written;
chunk = std::min(chunk, guestContiguousBytes(curDst));
if (chunk == 0u)
{
break;
}
::memset(hostDest, value, chunk);
written += chunk;
curDst += chunk;
}
else
if (written != 0u)
{
std::cerr << "memset error: Invalid address provided." << std::endl;
ps2TraceGuestRangeWrite(rdram, destAddr, written, "memset", ctx);
}
// returns dest pointer ($v0 = $a0)
@@ -82,22 +156,36 @@ void memmove(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t destAddr = getRegU32(ctx, 4); // $a0
uint32_t srcAddr = getRegU32(ctx, 5); // $a1
size_t size = getRegU32(ctx, 6); // $a2
uint32_t size = getRegU32(ctx, 6); // $a2
size = sanitizeMemTransferSize(size, "memmove");
uint8_t *hostDest = getMemPtr(rdram, destAddr);
const uint8_t *hostSrc = getConstMemPtr(rdram, srcAddr);
if (hostDest && hostSrc)
uint32_t copied = 0u;
std::vector<uint8_t> tmp;
tmp.reserve(size);
for (uint32_t i = 0u; i < size; ++i)
{
::memmove(hostDest, hostSrc, size);
ps2TraceGuestRangeWrite(rdram, destAddr, static_cast<uint32_t>(size), "memmove", ctx);
const uint8_t *src = getConstMemPtr(rdram, srcAddr + i);
if (!src)
{
break;
}
tmp.push_back(*src);
}
else
for (uint32_t i = 0u; i < static_cast<uint32_t>(tmp.size()); ++i)
{
std::cerr << "memmove error: Attempted move involving potentially invalid RDRAM address."
<< " Dest: 0x" << std::hex << destAddr << " (host ptr valid: " << (hostDest != nullptr) << ")"
<< ", Src: 0x" << srcAddr << " (host ptr valid: " << (hostSrc != nullptr) << ")" << std::dec
<< ", Size: " << size << std::endl;
uint8_t *dst = getMemPtr(rdram, destAddr + i);
if (!dst)
{
break;
}
*dst = tmp[i];
++copied;
}
if (copied != 0u)
{
ps2TraceGuestRangeWrite(rdram, destAddr, copied, "memmove", ctx);
}
// returns dest pointer ($v0 = $a0)
@@ -109,25 +197,23 @@ void memcmp(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
uint32_t ptr1Addr = getRegU32(ctx, 4); // $a0
uint32_t ptr2Addr = getRegU32(ctx, 5); // $a1
uint32_t size = getRegU32(ctx, 6); // $a2
const uint8_t *hostPtr1 = getConstMemPtr(rdram, ptr1Addr);
const uint8_t *hostPtr2 = getConstMemPtr(rdram, ptr2Addr);
size = sanitizeMemTransferSize(size, "memcmp");
int result = 0;
if (hostPtr1 && hostPtr2)
for (uint32_t i = 0u; i < size; ++i)
{
result = ::memcmp(hostPtr1, hostPtr2, size);
}
else
{
std::cerr << "memcmp error: Invalid address provided."
<< " Ptr1: 0x" << std::hex << ptr1Addr << " (host ptr valid: " << (hostPtr1 != nullptr) << ")"
<< ", Ptr2: 0x" << ptr2Addr << " (host ptr valid: " << (hostPtr2 != nullptr) << ")" << std::dec
<< std::endl;
result = (hostPtr1 == nullptr) - (hostPtr2 == nullptr);
if (result == 0)
result = 1; // If both null, still different? Or 0?
const uint8_t *lhs = getConstMemPtr(rdram, ptr1Addr + i);
const uint8_t *rhs = getConstMemPtr(rdram, ptr2Addr + i);
if (!lhs || !rhs)
{
result = (!lhs && !rhs) ? 0 : (lhs ? 1 : -1);
break;
}
if (*lhs != *rhs)
{
result = static_cast<int>(*lhs) - static_cast<int>(*rhs);
break;
}
}
setReturnS32(ctx, result);
}
@@ -427,6 +513,7 @@ void sprintf(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t str_addr = getRegU32(ctx, 4); // $a0
uint32_t format_addr = getRegU32(ctx, 5); // $a1
constexpr size_t kSafeSprintfBytes = 256u; // Keep guest stack temporaries from being overwritten.
const std::string formatOwned = readPs2CStringBounded(rdram, runtime, format_addr, 1024);
int ret = -1;
@@ -454,9 +541,9 @@ void sprintf(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
std::string rendered = formatPs2StringWithArgs(rdram, ctx, runtime, formatOwned.c_str(), 2);
if (rendered.size() >= kMaxFormattedOutputBytes)
if (rendered.size() >= kSafeSprintfBytes)
{
rendered.resize(kMaxFormattedOutputBytes - 1);
rendered.resize(kSafeSprintfBytes - 1);
}
const size_t writeLen = rendered.size() + 1u;
if (writeGuestBytes(rdram, runtime, str_addr, reinterpret_cast<const uint8_t *>(rendered.c_str()), writeLen))
File diff suppressed because it is too large Load Diff
@@ -138,3 +138,13 @@ void builtin_set_imask(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, 0);
}
void InitThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
static int logCount = 0;
if (logCount < 8)
{
std::cout << "ps2_stub InitThread" << std::endl;
++logCount;
}
setReturnS32(ctx, 1); // success
}
@@ -271,18 +271,66 @@ static bool readStackU32(uint8_t *rdram, uint32_t sp, uint32_t offset, uint32_t
static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime,
uint32_t funcAddr, uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t *outV0)
{
if (!runtime || !funcAddr || !runtime->hasFunction(funcAddr))
if (!runtime || !ctx || !funcAddr || !runtime->hasFunction(funcAddr))
return false;
constexpr uint32_t kRpcInvokeStackSize = 0x4000u;
constexpr uint32_t kRpcInvokeReturnSentinel = 0x00FFF000u;
constexpr uint32_t kRpcInvokeMaxSteps = 0x8000u;
R5900Context tmp = *ctx;
setRegU32(&tmp, 4, a0);
setRegU32(&tmp, 5, a1);
setRegU32(&tmp, 6, a2);
setRegU32(&tmp, 7, a3);
thread_local uint32_t s_rpcInvokeStackBase = 0u;
thread_local uint32_t s_rpcInvokeStackTop = 0u;
if (s_rpcInvokeStackTop == 0u)
{
const uint32_t stackBase = runtime->guestMalloc(kRpcInvokeStackSize, 16u);
if (stackBase != 0u)
{
s_rpcInvokeStackBase = stackBase;
s_rpcInvokeStackTop = (stackBase + kRpcInvokeStackSize) & ~0xFu;
}
}
if (s_rpcInvokeStackTop != 0u)
{
setRegU32(&tmp, 29, s_rpcInvokeStackTop);
}
(void)s_rpcInvokeStackBase;
setRegU32(&tmp, 31, kRpcInvokeReturnSentinel);
tmp.pc = funcAddr;
PS2Runtime::RecompiledFunction func = runtime->lookupFunction(funcAddr);
func(rdram, &tmp, runtime);
uint32_t steps = 0u;
uint32_t lastPc = 0xFFFFFFFFu;
uint32_t samePcCount = 0u;
while (tmp.pc != 0u &&
tmp.pc != kRpcInvokeReturnSentinel &&
runtime->hasFunction(tmp.pc) &&
steps < kRpcInvokeMaxSteps)
{
const uint32_t pc = tmp.pc;
if (pc == lastPc)
{
++samePcCount;
if (samePcCount > 0x2000u)
{
break;
}
}
else
{
lastPc = pc;
samePcCount = 0u;
}
PS2Runtime::RecompiledFunction func = runtime->lookupFunction(pc);
func(rdram, &tmp, runtime);
++steps;
}
if (outV0)
{
@@ -201,6 +201,8 @@ static std::unordered_map<int, std::shared_ptr<ThreadInfo>> g_threads;
static int g_nextThreadId = 2; // Reserve 1 for the main thread
static thread_local int g_currentThreadId = 1;
static std::mutex g_thread_map_mutex;
static std::unordered_map<int, std::thread> g_hostThreads;
static std::mutex g_host_thread_mutex;
static std::unordered_map<int, std::shared_ptr<SemaInfo>> g_semas;
static int g_nextSemaId = 1;
@@ -216,6 +218,92 @@ static std::once_flag g_alarm_worker_once;
std::atomic<int> g_activeThreads{0};
static std::mutex g_fd_mutex;
static void registerHostThread(int tid, std::thread worker)
{
std::thread stale;
{
std::lock_guard<std::mutex> lock(g_host_thread_mutex);
auto it = g_hostThreads.find(tid);
if (it != g_hostThreads.end())
{
stale = std::move(it->second);
g_hostThreads.erase(it);
}
g_hostThreads.emplace(tid, std::move(worker));
}
if (stale.joinable())
{
if (stale.get_id() == std::this_thread::get_id())
{
stale.detach();
}
else
{
stale.join();
}
}
}
static void joinHostThreadById(int tid)
{
std::thread worker;
{
std::lock_guard<std::mutex> lock(g_host_thread_mutex);
auto it = g_hostThreads.find(tid);
if (it != g_hostThreads.end())
{
worker = std::move(it->second);
g_hostThreads.erase(it);
}
}
if (!worker.joinable())
{
return;
}
if (worker.get_id() == std::this_thread::get_id())
{
worker.detach();
}
else
{
worker.join();
}
}
static void joinAllHostThreads()
{
std::vector<std::thread> workers;
{
std::lock_guard<std::mutex> lock(g_host_thread_mutex);
workers.reserve(g_hostThreads.size());
const std::thread::id selfId = std::this_thread::get_id();
for (auto it = g_hostThreads.begin(); it != g_hostThreads.end();)
{
std::thread &worker = it->second;
if (worker.joinable() && worker.get_id() == selfId)
{
++it;
continue;
}
workers.push_back(std::move(worker));
it = g_hostThreads.erase(it);
}
}
for (auto &worker : workers)
{
if (!worker.joinable())
{
continue;
}
worker.join();
}
}
struct RpcServerState
{
uint32_t sid = 0;
@@ -232,6 +320,7 @@ struct RpcClientState
static std::unordered_map<uint32_t, RpcServerState> g_rpc_servers;
static std::unordered_map<uint32_t, RpcClientState> g_rpc_clients;
static std::mutex g_rpc_mutex;
static std::recursive_mutex g_sif_call_rpc_mutex;
static bool g_rpc_initialized = false;
static uint32_t g_rpc_next_id = 1;
static uint32_t g_rpc_packet_index = 0;
@@ -26,6 +26,15 @@ static void releasePs2Fd(int ps2Fd)
g_fileDescriptors.erase(ps2Fd);
}
struct VagAccumEntry
{
std::vector<uint8_t> data;
uint32_t firstBufAddr = 0;
};
static std::unordered_map<int, VagAccumEntry> g_vagAccum;
static std::mutex g_vagAccumMutex;
static constexpr size_t kVagAccumMaxBytes = 16 * 1024 * 1024;
static const char *translateFioMode(int ps2Flags)
{
bool read = (ps2Flags & PS2_FIO_O_RDONLY) || (ps2Flags & PS2_FIO_O_RDWR);
@@ -106,21 +115,47 @@ void fioOpen(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void fioClose(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
int ps2Fd = (int)getRegU32(ctx, 4); // $a0
std::cout << "fioClose: fd=" << ps2Fd << std::endl;
int ps2Fd = (int)getRegU32(ctx, 4);
FILE *fp = getHostFile(ps2Fd);
if (!fp)
{
std::cerr << "fioClose warning: Invalid PS2 file descriptor " << ps2Fd << std::endl;
setReturnS32(ctx, -1); // e.g., -EBADF
setReturnS32(ctx, -1);
return;
}
int ret = ::fclose(fp);
releasePs2Fd(ps2Fd);
// returns 0 on success, -1 on error
{
std::lock_guard<std::mutex> lock(g_vagAccumMutex);
auto it = g_vagAccum.find(ps2Fd);
if (it != g_vagAccum.end())
{
VagAccumEntry &e = it->second;
if (e.data.size() >= 48)
{
const uint32_t magic = (static_cast<uint32_t>(e.data[0]) << 24) |
(static_cast<uint32_t>(e.data[1]) << 16) |
(static_cast<uint32_t>(e.data[2]) << 8) |
static_cast<uint32_t>(e.data[3]);
const uint32_t magicLE = (static_cast<uint32_t>(e.data[3]) << 24) |
(static_cast<uint32_t>(e.data[2]) << 16) |
(static_cast<uint32_t>(e.data[1]) << 8) |
static_cast<uint32_t>(e.data[0]);
if (magic == 0x56414770u || magicLE == 0x56414770u)
{
if (runtime)
runtime->audioBackend().onVagTransferFromBuffer(
e.data.data(), static_cast<uint32_t>(e.data.size()),
e.firstBufAddr ? e.firstBufAddr : 0u);
}
}
g_vagAccum.erase(it);
}
}
setReturnS32(ctx, ret == 0 ? 0 : -1);
}
@@ -161,11 +196,39 @@ void fioRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
std::cerr << "fioRead error: fread failed for fd " << ps2Fd << ": " << strerror(errno) << std::endl;
clearerr(fp);
setReturnS32(ctx, -1); // -EIO or other appropriate error
setReturnS32(ctx, -1);
return;
}
// returns number of bytes read (can be 0 for EOF)
{
std::lock_guard<std::mutex> lock(g_vagAccumMutex);
auto it = g_vagAccum.find(ps2Fd);
if (it != g_vagAccum.end())
{
VagAccumEntry &e = it->second;
if (e.data.size() + bytesRead <= kVagAccumMaxBytes)
e.data.insert(e.data.end(), hostBuf, hostBuf + bytesRead);
}
else if (bytesRead >= 4)
{
const uint32_t magic = (static_cast<uint32_t>(hostBuf[0]) << 24) |
(static_cast<uint32_t>(hostBuf[1]) << 16) |
(static_cast<uint32_t>(hostBuf[2]) << 8) |
static_cast<uint32_t>(hostBuf[3]);
const uint32_t magicLE = (static_cast<uint32_t>(hostBuf[3]) << 24) |
(static_cast<uint32_t>(hostBuf[2]) << 16) |
(static_cast<uint32_t>(hostBuf[1]) << 8) |
static_cast<uint32_t>(hostBuf[0]);
if (magic == 0x56414770u || magicLE == 0x56414770u)
{
VagAccumEntry &e = g_vagAccum[ps2Fd];
e.firstBufAddr = bufAddr;
if (bytesRead <= kVagAccumMaxBytes)
e.data.assign(hostBuf, hostBuf + bytesRead);
}
}
}
setReturnS32(ctx, (int32_t)bytesRead);
}
@@ -232,8 +232,11 @@ void SignalSema(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
int ret = KE_OK;
int beforeCount = 0;
int afterCount = 0;
{
std::lock_guard<std::mutex> lock(sema->m);
beforeCount = sema->count;
if (sema->count >= sema->maxCount)
{
ret = KE_SEMA_OVF;
@@ -243,6 +246,18 @@ void SignalSema(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
sema->count++;
sema->cv.notify_one();
}
afterCount = sema->count;
}
static std::atomic<uint32_t> s_signalSemaLogs{0};
const uint32_t sigLog = s_signalSemaLogs.fetch_add(1, std::memory_order_relaxed);
if (sigLog < 256u)
{
std::cout << "[SignalSema] tid=" << g_currentThreadId
<< " sid=" << sid
<< " count=" << beforeCount << "->" << afterCount
<< " ret=" << ret
<< std::endl;
}
setReturnS32(ctx, ret);
@@ -270,6 +285,18 @@ void WaitSema(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
if (sema->count == 0)
{
static std::atomic<uint32_t> s_waitSemaBlockLogs{0};
const uint32_t blockLog = s_waitSemaBlockLogs.fetch_add(1, std::memory_order_relaxed);
if (blockLog < 256u)
{
std::cout << "[WaitSema:block] tid=" << g_currentThreadId
<< " sid=" << sid
<< " pc=0x" << std::hex << ctx->pc
<< " ra=0x" << getRegU32(ctx, 31)
<< std::dec
<< std::endl;
}
if (info)
{
std::lock_guard<std::mutex> tLock(info->m);
@@ -315,6 +342,17 @@ void WaitSema(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
sema->count--;
}
static std::atomic<uint32_t> s_waitSemaWakeLogs{0};
const uint32_t wakeLog = s_waitSemaWakeLogs.fetch_add(1, std::memory_order_relaxed);
if (wakeLog < 256u)
{
std::cout << "[WaitSema:wake] tid=" << g_currentThreadId
<< " sid=" << sid
<< " ret=" << ret
<< " count=" << sema->count
<< std::endl;
}
lock.unlock();
waitWhileSuspended(info);
setReturnS32(ctx, ret);
@@ -456,9 +494,22 @@ void SetEventFlag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
return;
}
uint32_t newBits = 0u;
{
std::lock_guard<std::mutex> lock(info->m);
info->bits |= bits;
newBits = info->bits;
}
static std::atomic<uint32_t> s_setEventFlagLogs{0};
const uint32_t setLog = s_setEventFlagLogs.fetch_add(1, std::memory_order_relaxed);
if (setLog < 256u)
{
std::cout << "[SetEventFlag] tid=" << g_currentThreadId
<< " eid=" << eid
<< " bits=0x" << std::hex << bits
<< " newBits=0x" << newBits
<< std::dec << std::endl;
}
info->cv.notify_all();
setReturnS32(ctx, 0);
@@ -551,6 +602,21 @@ void WaitEventFlag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
if (!satisfied())
{
static std::atomic<uint32_t> s_waitEventBlockLogs{0};
const uint32_t evBlockLog = s_waitEventBlockLogs.fetch_add(1, std::memory_order_relaxed);
if (evBlockLog < 256u)
{
std::cout << "[WaitEventFlag:block] tid=" << g_currentThreadId
<< " eid=" << eid
<< " waitBits=0x" << std::hex << waitBits
<< " mode=0x" << mode
<< " bits=0x" << info->bits
<< " pc=0x" << ctx->pc
<< " ra=0x" << getRegU32(ctx, 31)
<< std::dec
<< std::endl;
}
if (tInfo)
{
std::lock_guard<std::mutex> tLock(tInfo->m);
@@ -610,6 +676,18 @@ void WaitEventFlag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
}
static std::atomic<uint32_t> s_waitEventWakeLogs{0};
const uint32_t evWakeLog = s_waitEventWakeLogs.fetch_add(1, std::memory_order_relaxed);
if (evWakeLog < 256u)
{
std::cout << "[WaitEventFlag:wake] tid=" << g_currentThreadId
<< " eid=" << eid
<< " ret=" << ret
<< " bits=0x" << std::hex << info->bits
<< std::dec
<< std::endl;
}
lock.unlock();
waitWhileSuspended(tInfo);
setReturnS32(ctx, ret);
@@ -671,10 +749,14 @@ void PollEventFlag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
*resBitsPtr = info->bits;
}
if (mode & (WEF_CLEAR | WEF_CLEAR_ALL))
if (mode & WEF_CLEAR_ALL)
{
info->bits = 0;
}
else if (mode & WEF_CLEAR)
{
info->bits &= ~waitBits;
}
setReturnS32(ctx, KE_OK);
}
@@ -274,6 +274,11 @@ void EnableIntc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_OK);
}
void iEnableIntc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
EnableIntc(rdram, ctx, runtime);
}
void DisableIntc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t cause = getRegU32(ctx, 4);
@@ -285,6 +290,11 @@ void DisableIntc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_OK);
}
void iDisableIntc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
DisableIntc(rdram, ctx, runtime);
}
void AddIntcHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
IrqHandlerInfo info{};
@@ -309,6 +319,11 @@ void AddIntcHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, handlerId);
}
void AddIntcHandler2(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
AddIntcHandler(rdram, ctx, runtime);
}
void RemoveIntcHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t cause = getRegU32(ctx, 4);
@@ -347,6 +362,11 @@ void AddDmacHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, handlerId);
}
void AddDmacHandler2(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
AddDmacHandler(rdram, ctx, runtime);
}
void RemoveDmacHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t cause = getRegU32(ctx, 4);
@@ -426,6 +446,11 @@ void EnableDmac(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_OK);
}
void iEnableDmac(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
EnableDmac(rdram, ctx, runtime);
}
void DisableDmac(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
const uint32_t cause = getRegU32(ctx, 4);
@@ -436,3 +461,8 @@ void DisableDmac(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
setReturnS32(ctx, KE_OK);
}
void iDisableDmac(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
DisableDmac(rdram, ctx, runtime);
}
@@ -156,6 +156,8 @@ void SifBindRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
std::lock_guard<std::recursive_mutex> rpcCallLock(g_sif_call_rpc_mutex);
uint32_t clientPtr = getRegU32(ctx, 4);
uint32_t rpcNum = getRegU32(ctx, 5);
uint32_t mode = getRegU32(ctx, 6);
@@ -198,7 +200,7 @@ void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
auto looksLikeSize = [&](uint32_t v) -> bool
{
return v <= 0x100000u;
return v <= 0x2000000u;
};
auto looksLikeFunc = [&](uint32_t v) -> bool
@@ -211,10 +213,42 @@ void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
return looksLikeSize(sendSz) && looksLikeGuestPtr(rbuf) && looksLikeSize(rsz) && looksLikeFunc(endFn);
};
bool useRegConvention = true;
if (!plausiblePack(sendSizeReg, recvBufReg, recvSizeReg, endFuncReg))
const bool regPackPlausible = plausiblePack(sendSizeReg, recvBufReg, recvSizeReg, endFuncReg);
const bool stackPackPlausible = plausiblePack(sendSizeStk, recvBufStk, recvSizeStk, endFuncStk);
uint32_t boundSidHint = 0u;
{
if (plausiblePack(sendSizeStk, recvBufStk, recvSizeStk, endFuncStk))
std::lock_guard<std::mutex> lock(g_rpc_mutex);
auto it = g_rpc_clients.find(clientPtr);
if (it != g_rpc_clients.end())
{
boundSidHint = it->second.sid;
}
}
auto looksLikeDtxCreatePack = [&](uint32_t sendSz, uint32_t rbuf, uint32_t rsz) -> bool
{
return rbuf != 0u && rsz >= 4u && rsz <= 0x40u &&
sendSz >= 12u && sendSz <= 0x1000u;
};
const bool isDtxCreate34Call = (boundSidHint == kDtxRpcSid) && (rpcNum == 0x422u);
const bool forceStackForDtxCreate34 =
isDtxCreate34Call &&
stackPackPlausible &&
looksLikeDtxCreatePack(sendSizeStk, recvBufStk, recvSizeStk) &&
!looksLikeDtxCreatePack(sendSizeReg, recvBufReg, recvSizeReg);
bool useRegConvention = true;
if (forceStackForDtxCreate34)
{
useRegConvention = false;
}
else if (!regPackPlausible && stackPackPlausible)
{
const bool regHasValidCallback = (endFuncReg != 0u) && looksLikeFunc(endFuncReg);
const bool stackHasValidCallback = (endFuncStk != 0u) && looksLikeFunc(endFuncStk);
if (!(regHasValidCallback && !stackHasValidCallback))
{
useRegConvention = false;
}
@@ -226,6 +260,22 @@ void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
endFunc = useRegConvention ? endFuncReg : endFuncStk;
endParam = useRegConvention ? endParamReg : endParamStk;
const bool isDtxLikeRpc = (boundSidHint == kDtxRpcSid) || ((rpcNum & 0xFF00u) == 0x0400u);
static uint32_t dtxAbiLogCount = 0u;
if (isDtxLikeRpc && dtxAbiLogCount < 96u)
{
std::cout << "[SifCallRpc:ABI] client=0x" << std::hex << clientPtr
<< " rpc=0x" << rpcNum
<< " sidHint=0x" << boundSidHint
<< " useReg=" << (useRegConvention ? 1 : 0)
<< " reg=(" << sendSizeReg << "," << recvBufReg << "," << recvSizeReg << "," << endFuncReg << "," << endParamReg << ")"
<< " stk=(" << sendSizeStk << "," << recvBufStk << "," << recvSizeStk << "," << endFuncStk << "," << endParamStk << ")"
<< " plausible=(" << (regPackPlausible ? 1 : 0) << "," << (stackPackPlausible ? 1 : 0) << ")"
<< " force34=" << (forceStackForDtxCreate34 ? 1 : 0)
<< std::dec << std::endl;
++dtxAbiLogCount;
}
t_SifRpcClientData *client = reinterpret_cast<t_SifRpcClientData *>(getMemPtr(rdram, clientPtr));
if (!client)
@@ -321,6 +371,19 @@ void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
return true;
};
if (!handled && sid != 0 && runtime)
{
if (!runtime->iop().handleRPC(sid, rpcNum, sendBuf, sendSize, recvBuf, recvSize) &&
sid == IOP_SID_LIBSD)
{
const uint8_t *sendPtr = sendBuf ? getConstMemPtr(rdram, sendBuf) : nullptr;
uint8_t *recvPtr = recvBuf ? getMemPtr(rdram, recvBuf) : nullptr;
ps2_iop_audio::handleLibSdRpc(runtime, sid, rpcNum, sendPtr, sendSize, recvPtr, recvSize);
handled = true;
resultPtr = recvBuf;
}
}
const bool isDtxUrpc = (sid == kDtxRpcSid) && (rpcNum >= 0x400u) && (rpcNum < 0x500u);
uint32_t dtxUrpcCommand = isDtxUrpc ? (rpcNum & 0xFFu) : 0u;
uint32_t dtxUrpcFn = 0;
@@ -412,6 +475,16 @@ void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
rpcZeroRdram(rdram, recvBuf + sizeof(uint32_t), recvSize - sizeof(uint32_t));
}
static uint32_t dtxCreateLogCount = 0;
if (dtxCreateLogCount < 64u)
{
std::cout << "[SifCallRpc:DTX_CREATE] dtxId=0x" << std::hex << dtxId
<< " remote=0x" << remoteHandle
<< " recvBuf=0x" << recvBuf
<< " recvSize=0x" << recvSize
<< std::dec << std::endl;
++dtxCreateLogCount;
}
handled = true;
resultPtr = recvBuf;
}
@@ -805,24 +878,22 @@ void SifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
if (sid == 1u && (rpcNum == 0x12u || rpcNum == 0x13u))
{
uint32_t responseWord = 1u;
if (rpcNum == 0x13u)
{
static uint32_t sdrStateBlobAddr = 0u;
if (sdrStateBlobAddr == 0u)
{
sdrStateBlobAddr = rpcAllocPacketAddr(rdram);
if (sdrStateBlobAddr == 0u)
{
sdrStateBlobAddr = kRpcPacketPoolBase;
}
}
// RECVX snddrv expects:
// cmd 0x12 -> SND_STATUS* (get_adrs)
// cmd 0x13 -> int[16]* (iop_data_adr_top)
constexpr uint32_t kSdrStatusAddr = 0x00012000u;
constexpr uint32_t kSdrAddrTableAddr = 0x00012100u;
constexpr uint32_t kSdrHdBaseAddr = 0x00014000u;
constexpr uint32_t kSdrSqBaseAddr = 0x00018000u;
constexpr uint32_t kSdrDataBaseAddr = 0x00030000u;
rpcZeroRdram(rdram, sdrStateBlobAddr, 64u);
(void)writeRpcU32(sdrStateBlobAddr + 0u, 1u);
responseWord = sdrStateBlobAddr;
}
rpcZeroRdram(rdram, kSdrStatusAddr, 0x42u);
rpcZeroRdram(rdram, kSdrAddrTableAddr, 16u * sizeof(uint32_t));
(void)writeRpcU32(kSdrAddrTableAddr + (0u * sizeof(uint32_t)), kSdrHdBaseAddr);
(void)writeRpcU32(kSdrAddrTableAddr + (1u * sizeof(uint32_t)), kSdrSqBaseAddr);
(void)writeRpcU32(kSdrAddrTableAddr + (2u * sizeof(uint32_t)), kSdrDataBaseAddr);
const uint32_t responseWord = (rpcNum == 0x12u) ? kSdrStatusAddr : kSdrAddrTableAddr;
if (recvBuf && recvSize >= sizeof(uint32_t))
{
(void)writeRpcU32(recvBuf, responseWord);
@@ -9,6 +9,11 @@ void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
<< ", frameMode=" << frameMode << std::endl;
}
void SetGsCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
GsSetCrt(rdram, ctx, runtime);
}
void GsGetIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint64_t imr = 0;
@@ -22,6 +27,11 @@ void GsGetIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnU64(ctx, imr); // Return in $v0/$v1
}
void iGsGetIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
GsGetIMR(rdram, ctx, runtime);
}
void GsPutIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint64_t newImr = getRegU32(ctx, 4) | ((uint64_t)getRegU32(ctx, 5) << 32); // $a0 = lower 32 bits, $a1 = upper 32 bits
@@ -35,6 +45,11 @@ void GsPutIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnU64(ctx, oldImr);
}
void iGsPutIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
GsPutIMR(rdram, ctx, runtime);
}
void GsSetVideoMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
int mode = getRegU32(ctx, 4); // $a0 - video mode (various flags)
@@ -249,14 +264,54 @@ void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encod
setReturnS32(ctx, 0);
}
// 0x3C SetupThread: returns stack pointer (stack + stack_size)
// args: $a0 = stack base, $a1 = stack size, $a2 = gp, $a3 = entry point
// 0x3C SetupThread
// args: $a0 = gp, $a1 = stack, $a2 = stack_size, $a3 = args, $t0 = root_func
void SetupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t stackBase = getRegU32(ctx, 4);
uint32_t stackSize = getRegU32(ctx, 5);
uint32_t sp = stackBase + stackSize;
setReturnS32(ctx, sp);
const uint32_t gp = getRegU32(ctx, 4);
const uint32_t stack = getRegU32(ctx, 5);
const int32_t stackSizeSigned = static_cast<int32_t>(getRegU32(ctx, 6));
const uint32_t currentSp = getRegU32(ctx, 29);
if (gp != 0u)
{
setRegU32(ctx, 28, gp);
}
uint32_t sp = currentSp;
if (stack == 0xFFFFFFFFu)
{
if (stackSizeSigned > 0)
{
const uint32_t requestedSize = static_cast<uint32_t>(stackSizeSigned);
if (requestedSize < PS2_RAM_SIZE)
{
sp = PS2_RAM_SIZE - requestedSize;
}
else
{
sp = PS2_RAM_SIZE;
}
}
else
{
sp = PS2_RAM_SIZE;
}
}
else if (stack != 0u)
{
if (stackSizeSigned > 0)
{
sp = stack + static_cast<uint32_t>(stackSizeSigned);
}
else
{
sp = stack;
}
}
sp &= ~0xFu;
setReturnU32(ctx, sp);
}
// 0x3D SetupHeap: returns heap base/start pointer
@@ -293,6 +348,20 @@ void EndOfHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnU32(ctx, getRegU32(ctx, 4));
}
void GetMemorySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)rdram;
(void)runtime;
setReturnU32(ctx, PS2_RAM_SIZE);
}
void Deci2Call(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)rdram;
(void)runtime;
setReturnS32(ctx, KE_OK);
}
// 0x5A QueryBootMode (stub): return 0 for now
void QueryBootMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
@@ -48,10 +48,15 @@ void FlushCache(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_OK);
}
void iFlushCache(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
FlushCache(rdram, ctx, runtime);
}
void ResetEE(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
std::cerr << "Syscall: ResetEE - requesting runtime stop" << std::endl;
runtime->requestStop();
std::cerr << "Syscall: ResetEE - requesting runtime stop" << std::endl;
// runtime->requestStop();
setReturnS32(ctx, KE_OK);
}
@@ -60,6 +65,12 @@ void SetMemoryMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_OK);
}
void InitThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
// This is a common ps2sdk helper that some games link against.
setReturnS32(ctx, 1);
}
void CreateThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
uint32_t paramAddr = getRegU32(ctx, 4); // $a0 points to ThreadParam
@@ -251,6 +262,13 @@ void StartThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_ERROR);
return;
}
if (runtime->isStopRequested())
{
setReturnS32(ctx, KE_ERROR);
return;
}
joinHostThreadById(tid);
const uint32_t callerSp = getRegU32(ctx, 29);
const uint32_t callerGp = getRegU32(ctx, 28);
@@ -296,7 +314,8 @@ void StartThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
g_activeThreads.fetch_add(1, std::memory_order_relaxed);
try
{
std::thread worker([=]() mutable {
std::thread worker([=]() mutable
{
{
std::string name = "PS2Thread_" + std::to_string(tid);
ThreadNaming::SetCurrentThreadName(name);
@@ -342,10 +361,12 @@ void StartThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
uint32_t lastPc = 0xFFFFFFFFu;
uint32_t samePcCount = 0;
constexpr uint32_t kSamePcYieldMask = 0x3FFFu;
constexpr uint32_t kSamePcWarnInterval = 0x400000u;
constexpr uint32_t kSamePcWarnInterval = 0x20000u;
uint64_t stepCount = 0u;
while (runtime && !runtime->isStopRequested())
{
++stepCount;
if (info->terminated.load(std::memory_order_relaxed))
{
throw ThreadExitException();
@@ -359,6 +380,16 @@ void StartThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
break;
}
if ((stepCount & 0x1FFFFFu) == 0u)
{
std::cout << "[StartThread] id=" << tid
<< " heartbeat pc=0x" << std::hex << pc
<< " ra=0x" << GPR_U32(threadCtx, 31)
<< " sp=0x" << GPR_U32(threadCtx, 29)
<< " gp=0x" << GPR_U32(threadCtx, 28)
<< std::dec << std::endl;
}
if (pc == lastPc)
{
++samePcCount;
@@ -380,6 +411,33 @@ void StartThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
lastPc = pc;
}
thread_local uint32_t s_adxProbeLogs = 0u;
if (s_adxProbeLogs < 256u)
{
const uint32_t raProbe = GPR_U32(threadCtx, 31);
const bool probeAdxSetCmd = (pc == 0x2F22E0u) &&
((raProbe < 0x00100000u) || (raProbe == 0x2F45B0u));
const bool probeAdxUnlock = (pc == 0x2F45B0u) &&
(raProbe < 0x00100000u);
const bool probeLowPc = (pc < 0x00100000u);
if (probeAdxSetCmd || probeAdxUnlock || probeLowPc)
{
auto flags = std::cerr.flags();
std::cerr << "[StartThread:adx-probe] tid=" << tid
<< " pc=0x" << std::hex << pc
<< " ra=0x" << raProbe
<< " sp=0x" << GPR_U32(threadCtx, 29)
<< " gp=0x" << GPR_U32(threadCtx, 28)
<< " a0=0x" << GPR_U32(threadCtx, 4)
<< " a1=0x" << GPR_U32(threadCtx, 5)
<< " a2=0x" << GPR_U32(threadCtx, 6)
<< " a3=0x" << GPR_U32(threadCtx, 7)
<< std::dec << std::endl;
std::cerr.flags(flags);
++s_adxProbeLogs;
}
}
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(pc);
if (!step)
{
@@ -446,9 +504,8 @@ void StartThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
// Notify anybody waiting for termination (like TerminateThread)
info->cv.notify_all();
g_activeThreads.fetch_sub(1, std::memory_order_relaxed);
});
worker.detach();
g_activeThreads.fetch_sub(1, std::memory_order_relaxed); });
registerHostThread(tid, std::move(worker));
}
catch (const std::exception &e)
{
@@ -549,9 +606,8 @@ void TerminateThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
// Block until the target thread actually finishes unwinding and becomes dormant
std::unique_lock<std::mutex> lock(info->m);
info->cv.wait(lock, [&]() {
return !info->started && info->status == THS_DORMANT;
});
info->cv.wait(lock, [&]()
{ return !info->started && info->status == THS_DORMANT; });
}
setReturnS32(ctx, KE_OK);
@@ -684,6 +740,11 @@ void ReferThreadStatus(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
setReturnS32(ctx, KE_OK);
}
void iReferThreadStatus(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
ReferThreadStatus(rdram, ctx, runtime);
}
void SleepThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
auto info = ensureCurrentThreadInfo(ctx);
@@ -708,6 +769,16 @@ void SleepThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
else
{
static std::atomic<uint32_t> s_sleepBlockLogs{0};
const uint32_t sleepBlockLog = s_sleepBlockLogs.fetch_add(1, std::memory_order_relaxed);
if (sleepBlockLog < 256u)
{
std::cout << "[SleepThread:block] tid=" << g_currentThreadId
<< " pc=0x" << std::hex << ctx->pc
<< " ra=0x" << getRegU32(ctx, 31)
<< std::dec << std::endl;
}
info->status = THS_WAIT;
info->waitType = TSW_SLEEP;
info->waitId = 0;
@@ -738,6 +809,16 @@ void SleepThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
}
}
static std::atomic<uint32_t> s_sleepWakeLogs{0};
const uint32_t sleepWakeLog = s_sleepWakeLogs.fetch_add(1, std::memory_order_relaxed);
if (sleepWakeLog < 256u)
{
std::cout << "[SleepThread:wake] tid=" << g_currentThreadId
<< " ret=" << ret
<< " wakeupCount=" << info->wakeupCount
<< std::endl;
}
lock.unlock();
waitWhileSuspended(info);
setReturnS32(ctx, ret);
@@ -764,6 +845,8 @@ void WakeupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
return;
}
int newWakeupCount = 0;
int statusAfter = THS_DORMANT;
{
std::lock_guard<std::mutex> lock(info->m);
if (info->status == THS_DORMANT)
@@ -790,6 +873,19 @@ void WakeupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
info->wakeupCount++;
}
newWakeupCount = info->wakeupCount;
statusAfter = info->status;
}
static std::atomic<uint32_t> s_wakeupLogs{0};
const uint32_t wakeupLog = s_wakeupLogs.fetch_add(1, std::memory_order_relaxed);
if (wakeupLog < 256u)
{
std::cout << "[WakeupThread] tid=" << g_currentThreadId
<< " target=" << tid
<< " status=" << statusAfter
<< " wakeupCount=" << newWakeupCount
<< std::endl;
}
setReturnS32(ctx, KE_OK);
}
@@ -885,6 +981,11 @@ void ChangeThreadPriority(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime
setReturnS32(ctx, KE_OK);
}
void iChangeThreadPriority(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
ChangeThreadPriority(rdram, ctx, runtime);
}
void RotateThreadReadyQueue(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
static int logCount = 0;
@@ -914,6 +1015,11 @@ void RotateThreadReadyQueue(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runti
setReturnS32(ctx, KE_OK);
}
void iRotateThreadReadyQueue(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
RotateThreadReadyQueue(rdram, ctx, runtime);
}
void ReleaseWaitThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
int tid = static_cast<int>(getRegU32(ctx, 4));
+41 -2
View File
@@ -1,9 +1,30 @@
#include "ps2_runtime.h"
#include "register_functions.h"
#include "games_database.h"
#ifdef _DEBUG
#include "ps2_log.h"
#endif
#include <iostream>
#include <string>
#include <filesystem>
int main(int argc, char *argv[])
std::string normalizeGameId(const std::string& folderName)
{
std::string result = folderName;
size_t underscore = result.find('_');
if (underscore != std::string::npos)
result[underscore] = '-';
size_t dot = result.find('.');
if (dot != std::string::npos)
result.erase(dot, 1);
return result;
}
int main(int argc, char* argv[])
{
if (argc < 2)
{
@@ -12,9 +33,24 @@ int main(int argc, char *argv[])
}
std::string elfPath = argv[1];
std::filesystem::path pathObj(elfPath);
std::string folderName = pathObj.filename().string();
std::string normalizedId = normalizeGameId(folderName);
std::string windowTitle = "PS2-Recomp | ";
const char* gameName = getGameName(normalizedId);
if (gameName)
{
windowTitle += std::string(gameName) + " | " + folderName;
}
else
{
windowTitle += folderName;
}
PS2Runtime runtime;
if (!runtime.initialize("ps2xRuntime (Raylib host)"))
if (!runtime.initialize(windowTitle.c_str()))
{
std::cerr << "Failed to initialize PS2 runtime" << std::endl;
return 1;
@@ -30,5 +66,8 @@ int main(int argc, char *argv[])
runtime.run();
#ifdef _DEBUG
ps2_log::print_saved_location();
#endif
return 0;
}
+804
View File
@@ -0,0 +1,804 @@
#include "games_database.h"
#include <unordered_map>
static const std::unordered_map<std::string, std::string> gameDatabase =
{
{ "SLUS-20267", "hack Part 1 - Infection (USA)" },
{ "SLKA-25080", ".hack Vol. 1 - Infection (Korea)" },
{ "SLPS-25143", ".hack Vol. 2 - Mutation (Japan)" },
{ "SLPS-25158", ".hack Vol. 3 - Erosion Pollution (Japan)" },
{ "SLUS-20579", "007 - NightFire (USA)" },
{ "SLES-51258", "007 - Nightfire (Europe)" },
{ "SLES-51260", "007 - Nightfire (Europe)" },
{ "SLES-50214", "18 Wheeler - American Pro Trucker (Europe)" },
{ "SLUS-20210", "18 Wheeler - American Pro Trucker (USA)" },
{ "SLPS-25118", "2002 FIFA World Cup (Japan)" },
{ "SLUS-20404", "2002 FIFA World Cup (USA)" },
{ "SLES-50796", "2002 FIFA World Cup Korea Japan (Europe)" },
{ "SLES-50798", "2002 FIFA World Cup Korea Japan (Germany)" },
{ "SLES-50799", "2002 FIFA World Cup Korea Japan (Italy)" },
{ "SLES-50800", "2002 FIFA World Cup Korea Japan (Spain)" },
{ "SLPS-20214", "3D Fighting School 2 (Japan)" },
{ "SLUS-20091", "4x4 Evo (USA)" },
{ "SCES-50293", "ATV Offroad - All Terrain Vehicle (Europe)" },
{ "SCUS-97104", "ATV Offroad Fury (USA) (v1.00)" },
{ "SCUS-97104", "ATV Offroad Fury (USA) (v3.01)" },
{ "SLUS-20588", "Activision Anthology (USA)" },
{ "SLPM-65150", "Aero Dancing 4 - New Generation (Japan)" },
{ "SLUS-20614", "Aero Elite - Combat Academy (USA)" },
{ "SLPS-25224", "Ai yori Aoshi Limited Edition (Japan)" },
{ "SLES-50953", "Air Ranger - Rescue Helicopter (Europe)" },
{ "SLPM-65486", "AirForce Delta - Blue Wing Knights (Japan)" },
{ "SLUS-20703", "AirForce Delta Strike (USA)" },
{ "SLES-50919", "Akira Psycho Ball (Europe)" },
{ "SLPS-20150", "Akira Psycho Ball (Japan)" },
{ "SLES-50429", "Alex Ferguson’s Player Manager 2001 (Europe)" },
{ "SLES-51792", "Aliens Versus Predator - Extinction (Europe)" },
{ "SLUS-20147", "Aliens vs Predator - Extinction (USA)" },
{ "SLPS-20181", "Alpine Racer 3 (Japan)" },
{ "SLUS-21069", "American Chopper (USA)" },
{ "SLPM-65513", "Angel’s Feather (Japan)" },
{ "SLPM-65027", "Anime Eikaiwa - 15 Shounen Hyouryuuki - Hitomi no Naka no Shounen (Japan)" },
{ "SLPM-65029", "Anime Eikaiwa - Tondemo Nezumi Daikatsuyaku (Japan)" },
{ "SLPM-65028", "Anime Eikaiwa - Tottoi (Japan)" },
{ "SLUS-20217", "Arctic Thunder (USA)" },
{ "SLES-50191", "Army Men - Green Rogue (Europe)" },
{ "SLUS-20087", "Army Men - Green Rogue (USA)" },
{ "SLPM-62501", "Assault Suits Valken (Japan)" },
{ "SLES-51896", "Attheraces Presents Gallop Racer (Europe)" },
{ "SLES-51191", "Auto Modellista (Europe)" },
{ "SLUS-20498", "Auto Modellista (USA) (Volume 1.0) (Beta)" },
{ "SLUS-28031", "Auto Modellista (USA) (Volume 2.0) (Beta)" },
{ "SLUS-20642", "Auto Modellista (USA)" },
{ "SLPS-25140", "Baldur’s Gate - Dark Alliance (Japan)" },
{ "SLPM-62155", "Baseball 2002, The - Battle Ball Park Sengen (Japan)" },
{ "SLPM-65180", "Baseball 2003, The - Battle Ball Park Sengen - Perfect Play Pro Yakyuu (Japan) (v1.05)" },
{ "SLES-51756", "Batman - Rise of Sin Tzu (Europe)" },
{ "SLUS-20709", "Batman - Rise of Sin Tzu (USA)" },
{ "SLES-50355", "Batman - Vengeance (Europe)" },
{ "SLUS-20226", "Batman - Vengeance (USA)" },
{ "SLPM-62052", "Beatmania Da Da Da!! (Japan)" },
{ "SCPS-11004", "Bikkuri Mouse (Japan)" },
{ "SLPM-65059", "Biohazard - Gun Survivor 2 - CODE - Veronica (Japan)" },
{ "SLPS-20187", "Black-Matrix II (Japan)" },
{ "SLES-51013", "Blade II (Europe)" },
{ "SLUS-20360", "Blade II (USA)" },
{ "SLUS-20862", "BloodRayne 2 (USA)" },
{ "SLPM-65262", "Boboboubo Boubobo - Hajike Matsuri (Japan)" },
{ "SLUS-20499", "Breath of Fire - Dragon Quarter (USA)" },
{ "SLPM-65196", "Breath of Fire V - Dragon Quarter (Japan)" },
{ "SLPM-66410", "Brothers in Arms - Meiyo no Daishou (Japan)" },
{ "SLUS-20895", "Bujingai - The Forsaken City (USA)" },
{ "?", "Burnout Dominator SLAJ-25094 SLPM-66739 SLUS-21596 SLES-54627 SLES-54681" },
{ "SLUS-20141", "CART Fury - Championship Racing (USA)" },
{ "SLES-50541", "Capcom vs. SNK 2 - Mark of the Millennium 2001 (Europe)" },
{ "SLUS-20246", "Capcom vs. SNK 2 - Mark of the Millennium 2001 (USA)" },
{ "SLPM-62365", "Cardinal Arc - Konton no Fuusatsu (Japan)" },
{ "SLES-52143", "Carmen Sandiego - The Secret of the Stolen Drums (Europe) (En,Fr,De,Es)" },
{ "SLUS-20849", "Carmen Sandiego - The Secret of the Stolen Drums (USA)" },
{ "SLES-50636", "Centre Court - Hard Hitter (Europe)" },
{ "SLPM-65255", "Chobits - Chii dake no Hito (Japan)" },
{ "SLPS-25015", "Choro Q - High Grade (Japan)" },
{ "SLPS-25014", "Choro Q - High Grade Limited Edition (Japan)" },
{ "SLPS-25073", "Cinema Surfing - Youga Taizen (Japan)" },
{ "SLES-50935", "Circus Maximus - Chariot Wars (Europe)" },
{ "SLES-51619", "Clock Tower 3 (Europe)" },
{ "SLUS-20633", "Clock Tower 3 (USA)" },
{ "SLPS-20056", "Colorio Hagaki Print (Japan)" },
{ "SCUS-97108", "Cool Boarders 2001 (USA)" },
{ "SLUS-20238", "Crash Bandicoot - The Wrath of Cortex (USA) (v1.00)" },
{ "SLES-50215", "Crazy Taxi (Europe)" },
{ "SLUS-20202", "Crazy Taxi (USA)" },
{ "SLPM-65368", "D.N.Angel - TV Animation Series (Japan)" },
{ "PSXC-00203", "DESR-7000-DESR-5000-DESR-7100-DESR-5100 Senyou - PSX Update Disc Ver. 1.31 (Japan)" },
{ "SCES-51190", "Dark Chronicle (Europe)" },
{ "PAPX-90506", "Dark Chronicle (Japan) (Demo)" },
{ "SCES-50295", "Dark Cloud (Europe)" },
{ "SCPS-15004", "Dark Cloud (Japan)" },
{ "SCUS-97111", "Dark Cloud (USA)" },
{ "SCUS-97213", "Dark Cloud 2 (USA) (v2.00)" },
{ "SLES-52874", "Dark Wind (Europe)" },
{ "SLPM-65303", "Dennou Senki - Virtual-On Marz (Japan)" },
{ "SLPS-25321", "Derby Stallion 04 (Japan)" },
{ "SLED-50359", "Devil May Cry (Europe) (Demo)" },
{ "SLPM-61010", "Devil May Cry (Japan) (Demo)" },
{ "SLPM-65023", "Devil May Cry (Japan) (Demo)" },
{ "SLPM-65038", "Devil May Cry (Japan)" },
{ "SLUS-20216", "Devil May Cry (USA)" },
{ "SLES-51347", "Die Hard - Vendetta (Europe)" },
{ "SLES-51348", "Die Hard - Vendetta (Germany)" },
{ "SLES-51095", "Dino Stalker (France)" },
{ "SLES-51096", "Dino Stalker (Germany)" },
{ "SLUS-20485", "Dino Stalker (USA)" },
{ "SLES-55392", "Disney Sing It (Europe)" },
{ "SLES-55542", "Disney Sing It - Pop Hits (Europe)" },
{ "SLES-50042", "Disney’s Dinosaur (Europe)" },
{ "SLES-50043", "Disney’s Dinosaur (Europe)" },
{ "SLES-50048", "Disney’s Donald Duck - Quack Attack (Europe)" },
{ "SLES-50045", "Disney’s Jungle Book - Groove Party (Europe)" },
{ "SCES-50522", "Disney’s Peter Pan - The Legend of Never Land (Europe)" },
{ "SCES-50531", "Disney’s Peter Pan - The Legend of Never Land (Scandinavia)" },
{ "SLES-50350", "Disney’s Tarzan - Freeride (Europe)" },
{ "SCES-51176", "Disney’s Treasure Planet (Europe)" },
{ "SCUS-97146", "Disney’s Treasure Planet (USA)" },
{ "SCES-50600", "Disney-Pixar Die Monster AG - Schreckens-Insel (Germany)" },
{ "SCES-50597", "Disney-Pixar Monsters en Co. - Schrik Eiland (Netherlands)" },
{ "SCES-50595", "Disney-Pixar Monsters, Inc. - Scare Island (Europe)" },
{ "SCES-50604", "Disney-Pixar Monsters, Inc. - Skraemmaroen (Sweden)" },
{ "SCES-50603", "Disney-Pixar Monstruos, S.A. - Isla de los Sustos (Spain)" },
{ "SLPM-65703", "Double Reaction! Plus (Japan)" },
{ "SCPS-56010", "Downhill Racer (Korea)" },
{ "SLPM-62199", "Dragon Quest Characters - Torneko no Daibouken 3 (Japan)" },
{ "SLPM-62490", "Dragon Quest VIII Premium Disc (Japan)" },
{ "SLPS-20016", "Dream Audition (Japan)" },
{ "SLPS-20099", "Dream Audition 3 (Japan)" },
{ "SLPS-20140", "Dream Audition Super Hit Disc 1 (Japan)" },
{ "SLPS-20141", "Dream Audition Super Hit Disc 2 (Japan)" },
{ "SLUS-20239", "Driven (USA)" },
{ "SLUS-20113", "Driving Emotion Type-S (USA)" },
{ "SLES-51303", "Drome Racers (Europe)" },
{ "SLUS-20475", "Dual Hearts (USA)" },
{ "SLES-50057", "Dynasty Warriors 2 (Europe)" },
{ "SLES-50058", "Dynasty Warriors 2 (France)" },
{ "SLPM-69004", "EGBrowser Light for I-O Data Device, Inc. (Japan)" },
{ "SLES-50036", "ESPN International Track & Field (Europe)" },
{ "SLUS-20041", "ESPN International Track & Field (USA)" },
{ "SLUS-20320", "ESPN International Winter Sports 2002 (USA)" },
{ "SLUS-20128", "ESPN MLS ExtraTime (USA)" },
{ "SLUS-20089", "ESPN Winter X Games Snowboarding (USA)" },
{ "SLPM-62103", "EX Okuman Chouja Game - The Money Battle (Japan)" },
{ "SLUS-20169", "Ephemeral Fantasia (USA)" },
{ "SLES-51813", "European Tennis Pro (Europe)" },
{ "SCED-51728", "EverQuest - Online Adventures (Europe) (Demo)" },
{ "SLES-51392", "Evolution Snowboarding (Europe)" },
{ "SLUS-20546", "Evolution Snowboarding (USA)" },
{ "SLPS-25326", "Exciting Pro Wres 5 (Japan) (Limited Edition)" },
{ "SLPS-25083", "Exciting Pro Wrestling 3 (Japan) (Limited Edition)" },
{ "SLPS-25087", "Exciting Pro Wrestling 3 (Japan)" },
{ "SLPS-20223", "Exciting Pro Wrestling 4 (Japan) (Demo)" },
{ "SLPS-25210", "Exciting Pro Wrestling 4 (Japan)" },
{ "SLPS-25326", "Exciting Pro Wrestling 5 (Japan)" },
{ "SCES-51513", "EyeToy - Play (Europe, Australia)" },
{ "SCUS-97319", "EyeToy - Play (USA)" },
{ "SLES-50011", "FIFA 2001 (Europe)" },
{ "SLES-50012", "FIFA 2001 (France)" },
{ "SLES-50013", "FIFA 2001 (Germany)" },
{ "SLES-50015", "FIFA 2001 (Italy)" },
{ "SLES-50016", "FIFA 2001 (Spain)" },
{ "SLUS-20097", "FIFA 2001 (USA)" },
{ "SLPS-20054", "FIFA 2001 - World Championship (Japan)" },
{ "SLPS-25069", "FIFA 2002 - Road to FIFA World Cup (Japan)" },
{ "SLPS-25179", "FIFA 2003 - Europe Soccer (Japan)" },
{ "SLES-50464", "FIFA Football 2002 (Europe)" },
{ "SLES-50466", "FIFA Football 2002 (France)" },
{ "SLES-50467", "FIFA Football 2002 (Germany)" },
{ "SLES-50470", "FIFA Football 2002 (Italy)" },
{ "SLPM-67503", "FIFA Football 2002 (Korea)" },
{ "SLES-50471", "FIFA Football 2002 (Spain)" },
{ "SLES-51197", "FIFA Football 2003 (Europe)" },
{ "SLUS-20280", "FIFA Soccer 2002 (USA)" },
{ "SLUS-20580", "FIFA Soccer 2003 (USA)" },
{ "SLPS-20020", "FIFA Soccer World Championship (Japan)" },
{ "SLPS-25236", "Fantastic Fortune 2 (Japan)" },
{ "SLUS-20388", "Fatal Frame (USA)" },
{ "SLUS-20766", "Fatal Frame II - Crimson Butterfly (USA)" },
{ "SLPS-20298", "Fever 8 - Sankyo Koushiki Pachinko Simulation (Japan)" },
{ "SLUS-20524", "Fighter Maker 2 (USA)" },
{ "SLPM-62135", "Final Fantasy XI - Online (Japan) (Beta)" },
{ "SLPS-25200", "Final Fantasy XI - Online (Japan)" },
{ "SCUS-97271", "Final Fantasy XI - Online (USA) (Beta)" },
{ "SCUS-97266", "Final Fantasy XI - Online (USA)" },
{ "SLPM-65288", "Final Fantasy XI - Zilart no Gen’ei (Japan) (All in One Pack 2003)" },
{ "SLPM-65287", "Final Fantasy XI - Zilart no Gen’ei (Japan)" },
{ "SLES-51418", "Fisherman’s Challenge (Europe)" },
{ "SLUS-20553", "Fisherman’s Challenge (USA)" },
{ "SLES-50259", "Flintstones in Viva Rock Vegas, The (Europe)" },
{ "SLPS-25034", "Flower, Sun and Rain (Japan)" },
{ "SLED-52852", "Forgotten Realms - Demon Stone (Europe) (Demo)" },
{ "SLUS-29061", "Freaky Flyers (USA) (Demo)" },
{ "SLUS-20658", "Freedom Fighters (USA)" },
{ "SLES-50720", "Freestyle Metal X (Europe)" },
{ "SLUS-20494", "Freestyle Metal X (USA)" },
{ "SLES-50788", "Frogger - The Great Quest (Europe)" },
{ "SLUS-20257", "Frogger - The Great Quest (USA)" },
{ "SLPM-60102", "From Software First Previews (Japan)" },
{ "SLUS-20785", "Funkmaster Flex - Digital Hitz Factory (USA)" },
{ "SLUS-20859", "Future Tactics - The Uprising (USA)" },
{ "SLKA-25139", "Fuuun Shinsengumi (Korea)" },
{ "SCED-52094", "G-Con 2 Competition Demo (Germany)" },
{ "SLES-50584", "G1 Jockey (Europe)" },
{ "SLES-51357", "G1 Jockey 3 (Europe)" },
{ "SLUS-20690", "G1 Jockey 3 (USA)" },
{ "SLPM-62020", "GI Jockey 2 (Japan)" },
{ "SLPM-62059", "GI Jockey 2 2001 (Japan) (Super Value Set)" },
{ "SLPM-62061", "GI Jockey 2 2001 (Japan)" },
{ "SLPM-62279", "GI Jockey 3 (Japan) (Premium Pack)" },
{ "SLPM-62277", "GI Jockey 3 (Japan)" },
{ "SLES-50472", "GTC Africa (Europe)" },
{ "SLES-52845", "Gadget & the Gadgetinis (Europe)" },
{ "SLUS-20225", "Gadget Racers (USA)" },
{ "SLPS-25333", "Gallop Racer - Lucky 7 (Japan)" },
{ "SLUS-20255", "Gallop Racer 2001 (USA)" },
{ "SLUS-20662", "Gallop Racer 2003 - A New Breed (USA)" },
{ "SLUS-21031", "Gallop Racer 2004 (USA)" },
{ "SLPS-25036", "Gallop Racer 5 (Japan)" },
{ "SLPS-73415", "Gallop Racer 6 - Revolution (Japan) (PlayStation 2 the Best)" },
{ "SLPS-25177", "Gallop Racer 6 - Revolution (Japan)" },
{ "SLPM-62009", "Ganbare! Nippon! Olympic 2000 (Japan)" },
{ "SLES-50211", "Gauntlet - Dark Legacy (Europe)" },
{ "SLUS-20047", "Gauntlet - Dark Legacy (USA)" },
{ "SLPM-62235", "Get Bass Battle (Japan)" },
{ "?", "Ghost Master - The Gravenville Chronicles (2003 beta) [Emuparadise]" },
{ "SLPS-20052", "Global Folktale (Japan)" },
{ "SLUS-20395", "Global Touring Challenge - Africa (USA)" },
{ "SLES-52117", "Go Go Copter - Remote Control Helicopter (Europe)" },
{ "SLES-51055", "Go Go Golf (Europe)" },
{ "SCED-54680", "God of War II (Europe) (Demo)" },
{ "SLES-50433", "Godai - Elemental Force (Europe)" },
{ "SLUS-20288", "Godai - Elemental Force (USA)" },
{ "SLPM-60107", "Golf Paradise (Japan) (Demo)" },
{ "SLPS-20009", "Golf Paradise (Japan)" },
{ "SLES-51296", "Grand Prix Challenge (Europe)" },
{ "SLES-50793", "Grand Theft Auto III (Australia)" },
{ "SLES-50330", "Grand Theft Auto III (Europe) (v1.40)" },
{ "SLES-50330", "Grand Theft Auto III (Europe) (v1.60)" },
{ "SLUS-20062", "Grand Theft Auto III (USA)" },
{ "SLUS-20466", "Gravenville Ghost Master Chronicles" },
{ "SLUS-20310", "Gravity Games Bike - Street. Vert. Dirt. (USA)" },
{ "SCES-50246", "Gravity Sucks (Europe, Australia)" },
{ "SLES-51999", "Grooverider (Europe)" },
{ "SLPS-20106", "Growlanser II - The Sense of Justice (Japan)" },
{ "SLKA-15007", "Growlanser II - The Sense of Justice (Korea)" },
{ "SLPM-62108", "Growlanser III - The Dual Darkness (Japan)" },
{ "SLPM-65383", "Growlanser IV - Wayfarer of the Time (Japan) (Deluxe Pack)" },
{ "SLPM-65408", "Growlanser IV - Wayfarer of the Time (Japan)" },
{ "SLPM-65139", "Gun Survivor 3 - Dino Crisis (Japan)" },
{ "SLES-52620", "Guncom 2 (Europe)" },
{ "SLPM-65153", "Gungrave (Japan)" },
{ "SLUS-20493", "Gungrave (USA)" },
{ "SLUS-21020", "Gungrave - Overdose (USA)" },
{ "SLPM-65492", "Gungrave OD (Japan)" },
{ "SLES-50559", "Guy Roux Manager 2002 (France)" },
{ "SLPM-62273", "Haishin 3 (Japan)" },
{ "SLPS-20098", "Hard Hitter (Japan)" },
{ "SLES-51057", "Hard Hitter 2 (Europe)" },
{ "SLPS-20173", "Hard Hitter 2 (Japan)" },
{ "SLUS-20568", "Hard Hitter Tennis (USA)" },
{ "SLES-51254", "Herr der Ringe, Der - Die zwei Tuerme (Germany)" },
{ "SLES-50260", "Hidden Invasion (Europe)" },
{ "SLUS-20301", "Hidden Invasion (USA)" },
{ "SLPS-25111", "Higanbana (Japan)" },
{ "SLPS-20213", "Hissatsu Pachinko Station V4 - Drumtic Mahjong (Japan)" },
{ "SLES-53028", "Hitman - Blood Money (Europe)" },
{ "SLPS-25269", "Hitman 2 - Silent Assassin (Japan)" },
{ "SLUS-20374", "Hitman 2 - Silent Assassin (USA) (v1.01)" },
{ "SLPM-62072", "Horse Breaker (Japan)" },
{ "SLES-51063", "Hot Wheels - Velocity X - Maximum Justice (Europe)" },
{ "SLUS-20412", "Hot Wheels - Velocity X - Maximum Justice (USA)" },
{ "SLPM-65083", "Houshin Engi 2 (Japan)" },
{ "SLES-52102", "Hugo - Bukkazoom! (Europe)" },
{ "SLPM-62067", "Hunter x Hunter - Ryumyaku no Saidan (Japan)" },
{ "SLES-50266", "Hype - The Time Quest (Europe)" },
{ "SLES-50265", "Hype - The Time Quest (Germany)" },
{ "SLPM-65405", "Hyper Dimension Fortress Macross (Japan)" },
{ "SLPM-62126", "Hyper Sports 2002 Winter (Japan)" },
{ "SLUS-20586", "IHRA Drag Racing 2 (USA)" },
{ "SCES-50760", "Ico (Europe)" },
{ "SLPS-25182", "Idol Janshi R - Jan Guru Project (Japan)" },
{ "SLES-51255", "Il Signore degli Anelli - Le Due Torri (Italy)" },
{ "SLES-51397", "IndyCar Series (Europe)" },
{ "SLUS-20641", "IndyCar Series featuring The Indianapolis 500 (USA)" },
{ "SLUS-20830", "Intellivision Lives! (USA)" },
{ "SLES-51629", "International Pool Championship (Europe)" },
{ "SLES-50039", "International Superstar Soccer (Europe)" },
{ "SLPM-62075", "International Superstar Soccer 2 (Europe) (Beta)" },
{ "SLUS-20913", "Inuyasha - The Secret of the Cursed Mask (USA)" },
{ "SLPM-65530", "J. League Pro Soccer Club o Tsukurou! ‘04 (Japan)" },
{ "SLPM-62217", "J. League Winning Eleven 6 (Japan)" },
{ "SLES-50735", "Jade Cocoon 2 (Europe)" },
{ "SCED-52952", "Jak 3 (Europe) (Demo)" },
{ "SCKA-20010", "Jak II (Korea) (En,Ja,Fr,De,Es,It,Ko)" },
{ "SCUS-97273", "Jak II (USA) (Demo)" },
{ "SCUS-97265", "Jak II (USA) (En,Ja,Fr,De,Es,It,Ko) (v1.00)" },
{ "SCUS-97265", "Jak II (USA) (En,Ja,Fr,De,Es,It,Ko) (v2.01)" },
{ "SCPS-15057", "Jak II - Jak x Daxter 2 (Japan)" },
{ "SCED-51700", "Jak II - Renegade (Europe) (Demo)" },
{ "SCES-51608", "Jak II Renegade (Europe) (Preview)" },
{ "SCES-50361", "Jak and Daxter - The Precursor Legacy (Europe)" },
{ "SCUS-97124", "Jak and Daxter - The Precursor Legacy (USA) (Cingular Wireless Demo)" },
{ "SCUS-97124", "Jak and Daxter - The Precursor Legacy (USA) (En,Fr,De,Es,It) (Rev 1)" },
{ "SCUS-97124", "Jak and Daxter - The Precursor Legacy (USA) (En,Fr,De,Es,It)" },
{ "PAPX-90222", "Jak x Daxter - Kyuu Sekai no Isan (Japan) (Demo)" },
{ "SCPS-15021", "Jak x Daxter - Kyuu Sekai no Isan (Japan)" },
{ "SLES-50209", "Jeremy McGrath Supercross World (Europe)" },
{ "SLUS-20245", "Jeremy McGrath Supercross World (USA)" },
{ "SCUS-97239", "Jet X2O (USA) (Demo)" },
{ "SCUS-97173", "Jet X2O (USA)" },
{ "SLPM-62011", "Jikkyou GI Stable (Japan)" },
{ "SLPM-62075", "Jikkyou World Soccer 2001 (Japan)" },
{ "SLPM-65140", "Jojo no Kimyou na Bouken - Ougon no Kaze (Japan)" },
{ "SLPM-65336", "K-1 World Grand Prix - The Beast Attack! (Japan)" },
{ "SLPM-65075", "K-1 World Grand Prix 2001 (Japan)" },
{ "SLPM-65202", "K-1 World Grand Prix 2002 (Japan)" },
{ "SLPM-65433", "K-1 World Grand Prix 2003 (Japan)" },
{ "SLPS-25386", "KOF - Maximum Impact (Japan)" },
{ "SLUS-20923", "KOF - Maximum Impact (USA)" },
{ "SCPS-11009", "Ka (Japan)" },
{ "SCPS-15045", "Ka 2 - Let’s Go Hawaii (Japan)" },
{ "SLPM-62383", "Karaoke Revolution - Night Selection 2003 (Japan)" },
{ "SLPM-62528", "Karaoke Revolution Family Pack (Japan)" },
{ "SLES-52308", "Karaoke Stage (Europe)" },
{ "SLES-51200", "Kelly Slater’s Pro Surfer (Europe)" },
{ "SLES-51201", "Kelly Slater’s Pro Surfer (Europe)" },
{ "SLUS-20334", "Kelly Slater’s Pro Surfer (USA)" },
{ "SLES-50114", "Kengo - Master of Bushido (Europe)" },
{ "SLUS-20021", "Kengo - Master of Bushido (USA)" },
{ "SLPM-60177", "Kengou 2 (Japan) (Taikenban)" },
{ "SLPS-25107", "Kengou 2 (Japan)" },
{ "SLPS-25020", "Kidou Senshi Gundam (Japan)" },
{ "SLPS-25120", "Kidou Senshi Gundam - Gihren no Yabou - Zeon Dokuritsu Sensouki (Japan)" },
{ "SLPS-25212", "Kidou Senshi Gundam - Gihren no Yabou - Zeon Dokuritsu Sensouki - Kouryaku Shireisho (Japan)" },
{ "SLPM-65076", "Kidou Senshi Gundam - Renpou vs. Zeon DX (Japan)" },
{ "SLPS-25061", "Kidou Senshi Gundam - Ver. 1.5 (Japan)" },
{ "SLPS-25389", "Kidou Senshi Gundam Seed - Owaranai Ashita e (Japan)" },
{ "SLPS-25123", "Kidou Senshi Gundam Senki - Lost War Chronicles (Japan)" },
{ "SLPM-65033", "Kikou Heidan J-Phoenix (Japan)" },
{ "SLPM-65123", "Kikou Heidan J-Phoenix - Burst Tactics (Japan)" },
{ "SLPM-65199", "Kikou Heidan J-Phoenix - Cobalt Shoutai-hen (Japan)" },
{ "SLPS-20075", "Kikou Heidan J-Phoenix - Joshou-hen (Japan)" },
{ "SLPM-65343", "Kikou Heidan J-Phoenix 2 (Japan)" },
{ "SLUS-20834", "King of Fighters 2000, The (USA)" },
{ "SLPS-25266", "King of Fighters 2001, The (Japan)" },
{ "SLUS-20839", "King of Fighters 2001, The (USA)" },
{ "?", "Kingdom Hearts - Re Chain of Memories (Preview)" },
{ "SLPS-25248", "Kino no Tabi - The Beautiful World (Japan)" },
{ "SLPM-65491", "Kishin Houkou Demonbane (Japan)" },
{ "SLPM-65404", "Kita e. - Diamond Dust (Japan)" },
{ "SLPM-65569", "Kita e. - Diamond Dust+ - Kiss is Beginning. (Japan)" },
{ "SLES-50128", "Knockout Kings 2001 (Europe)" },
{ "SLES-50129", "Knockout Kings 2001 (France)" },
{ "SLES-50130", "Knockout Kings 2001 (Germany)" },
{ "SLUS-20150", "Knockout Kings 2001 (USA)" },
{ "SLPM-65554", "Korokke! Ban Ou no Kiki o Sukue (Japan)" },
{ "SLPM-65447", "Kunoichi (Japan)" },
{ "SLPS-25136", "Kuon no Kizuna - Sairinshou (Japan)" },
{ "SLPM-60127", "Kuri Kuri Mix (Japan) (Taikenban)" },
{ "SLES-50443", "LEGO Racers 2 (Europe)" },
{ "SLUS-20042", "LEGO Racers 2 (USA)" },
{ "SLPS-20165", "La Pucelle - Hikari no Seijo Densetsu (Japan)" },
{ "SLES-50709", "Le Maillon Faible (France)" },
{ "SLES-50131", "Le Mans 24 Hours (Europe)" },
{ "SLUS-20207", "Le Mans 24 Hours (USA) (En,Fr,Es)" },
{ "SLES-51415", "Legacy of Kain Defiance" },
{ "SLUS-20045", "Legend of Alon D’ar, The (USA)" },
{ "SLES-51045", "Legends of Wrestling II (Europe)" },
{ "SLUS-20507", "Legends of Wrestling II (USA)" },
{ "SLES-50892", "Lethal Skies - Elite Pilot - Team SW (Europe)" },
{ "SLUS-20386", "Lethal Skies - Elite Pilot - Team SW (USA)" },
{ "SLES-51886", "Lethal Skies II (Europe)" },
{ "SLUS-20735", "Lethal Skies II (USA)" },
{ "SLPS-29004", "Lord of the Rings, The - Futatsu no Tou (Japan)" },
{ "SLPM-65212", "Lord of the Rings, The - The Two Towers (Asia) (En,Zh)" },
{ "SLES-51252", "Lord of the Rings, The - The Two Towers (Europe)" },
{ "SLPM-67546", "Lord of the Rings, The - The Two Towers (Korea)" },
{ "SLUS-20578", "Lord of the Rings, The - The Two Towers (USA)" },
{ "SLES-50230", "Lotus Challenge (Europe)" },
{ "SLPM-60101", "Love Story (Japan) (Demo)" },
{ "SLPS-20245", "LowRider - Round the World (Japan)" },
{ "SLES-50248", "MDK2 - Armageddon (Europe)" },
{ "SLUS-20105", "MDK2 - Armageddon (USA)" },
{ "SLES-50182", "MTV Music Generator 2 (Europe)" },
{ "SLUS-20222", "MTV Music Generator 2 (USA)" },
{ "SLES-50428", "MX 2002 featuring Ricky Carmichael (Europe)" },
{ "SLUS-20072", "MX 2002 featuring Ricky Carmichael (USA)" },
{ "SLES-50132", "MX Rider (Europe)" },
{ "SLUS-20234", "MX Rider (USA)" },
{ "SLES-51038", "MX SuperFly (Europe)" },
{ "SLUS-20381", "MX SuperFly (USA)" },
{ "SLES-51653", "Mace Griffin - Bounty Hunter (Europe)" },
{ "SLES-51654", "Mace Griffin - Bounty Hunter (Germany)" },
{ "SLUS-20505", "Mace Griffin - Bounty Hunter (USA)" },
{ "SLPM-62077", "Maestromusic II, The (Japan) (Doukonban)" },
{ "SLPM-62078", "Maestromusic II, The (Japan)" },
{ "SLUS-20671", "Mafia (USA)" },
{ "SLPS-20037", "Magical Sports Go Go Golf (Japan)" },
{ "SLPS-20310", "Mahjong Hiryuu Densetsu - Tenpai (Japan)" },
{ "SLPM-65367", "Makai Eiyuuki Maximo - Machine Monster no Yabou (Japan)" },
{ "SLPS-25042", "Maken Shao (Japan)" },
{ "SLES-51058", "Maken Shao - Demon Sword (Europe)" },
{ "SLUS-20358", "Malice (USA)" },
{ "SCED-51406", "Mark of Kri, The (Europe) (Demo)" },
{ "SCES-51164", "Mark of Kri, The (Europe)" },
{ "SCUS-97140", "Mark of Kri, The (USA)" },
{ "SLUS-20722", "Maximo vs Army of Zin (USA)" },
{ "SCPS-11014", "McDonald’s Original Happy Disc (Japan)" },
{ "SLPS-20031", "MechSmith, The - Run=Dim (Japan)" },
{ "SLES-51873", "Medal of Honor - Rising Sun (Europe, Australia)" },
{ "SLES-51875", "Medal of Honor - Rising Sun (Germany)" },
{ "SLPM-65469", "Medal of Honor - Rising Sun (Japan)" },
{ "SLES-51876", "Medal of Honor - Rising Sun (Spain)" },
{ "SLUS-20753", "Medal of Honor - Rising Sun (USA)" },
{ "SLES-51874", "Medal of Honor - Soleil Levant (France)" },
{ "SLES-50903", "MegaRace 3 - Nanotech Disaster (Europe)" },
{ "SLPM-67535", "Memories Off (Korea) (Ja,Ko)" },
{ "SLES-50789", "Men in Black II - Alien Escape (Europe)" },
{ "SLUS-20373", "Men in Black II - Alien Escape (USA)" },
{ "SLES-52599", "Metal Slug 3 (Europe)" },
{ "SLPS-25209", "Metal Slug 3 (Japan)" },
{ "SLES-53383", "Metal Slug 5 (Europe)" },
{ "SLPM-65480", "Michigan (Japan)" },
{ "SLES-52001", "Mission - Impossible - Operation Surma (Europe)" },
{ "SLUS-20400", "Mission - Impossible - Operation Surma (USA)" },
{ "SLES-51271", "Mobile Suit Gundam - Federation vs. Zeon (Europe)" },
{ "SLUS-20382", "Mobile Suit Gundam - Federation vs. Zeon (USA)" },
{ "SLUS-20175", "Mobile Suit Gundam - Journey to Jaburo (USA)" },
{ "SLUS-20741", "Mojo! (USA)" },
{ "SLPS-20381", "Monkey Turn V (Japan)" },
{ "SCPS-12345", "Monster House (Europe)" },
{ "SCPS-12345", "Monster House (Italy)" },
{ "SLPM-65495", "Monster Hunter (Japan)" },
{ "SLES-50908", "Monster Jam - Maximum Destruction (Europe)" },
{ "SLUS-20186", "Monster Jam - Maximum Destruction (USA)" },
{ "SLES-50717", "Mortal Kombat - Deadly Alliance (Europe, Australia)" },
{ "SLES-51439", "Mortal Kombat - Deadly Alliance (Germany)" },
{ "SLPS-25242", "Motion Gravure Series - Kitagawa Tomomi (Japan)" },
{ "SLES-51605", "Motorsiege - Warriors of Primetime (Europe)" },
{ "SLES-51363", "Music 3000 (Europe)" },
{ "SCUS-97263", "My Street (USA) (Demo)" },
{ "SCUS-97212", "My Street (USA)" },
{ "SLES-50726", "Myst III - Exile (Europe)" },
{ "SLUS-20434", "Myst III - Exile (USA)" },
{ "SLES-50080", "NBA Hoopz (Europe)" },
{ "SLUS-20050", "NBA Hoopz (USA)" },
{ "SCUS-97114", "NBA ShootOut 2001 (USA)" },
{ "SLES-50219", "NBA Street (Europe)" },
{ "SLUS-20187", "NBA Street (USA)" },
{ "SCUS-97109", "NCAA Final Four 2001 (USA)" },
{ "SCUS-97136", "NCAA Final Four 2002 (USA)" },
{ "SCUS-97204", "NCAA Final Four 2003 (USA)" },
{ "SCUS-97278", "NCAA Final Four 2004 (USA)" },
{ "SCUS-97107", "NCAA GameBreaker 2001 (USA)" },
{ "SCUS-97106", "NFL GameDay 2001 (USA)" },
{ "SLUS-20308", "NFL Prime Time 2002 (USA)" },
{ "SLES-50213", "NFL QB Club 2002 (Europe)" },
{ "SLUS-20154", "NFL QB Club 2002 (USA)" },
{ "SLES-51341", "NHL 2K3 (Europe)" },
{ "SLUS-20477", "NHL 2K3 (USA)" },
{ "SLES-50451", "NHL Hitz 2002 (Europe)" },
{ "SLUS-20140", "NHL Hitz 2002 (USA) (v2.00)" },
{ "SLES-50712", "NHL Hitz 2003 (Europe)" },
{ "SLUS-20438", "NHL Hitz 2003 (USA)" },
{ "SLUS-20691", "NHL Hitz Pro (USA)" },
{ "SLPS-25276", "Natsu Yume Ya Wa - The Tale of a Midsummer Night’s Dream (Japan)" },
{ "SLPS-25314", "Nebula - Echo Night (Japan)" },
{ "?", "Need for Speed Most Wanted" },
{ "SLUS-20537", "Nickelodeon Jimmy Neutron - Boy Genius (USA)" },
{ "SLUS-20473", "Nickelodeon Rocket Power - Beach Bandits (USA)" },
{ "SLUS-20810", "Nightshade (USA)" },
{ "SLPM-65130", "Nihon Daihyou Senshu ni Narou! (Japan)" },
{ "SLPM-62082", "Nihon Pro Yakyuu Kikou Kounin - Pro Yakyuu Japan 2001 (Japan)" },
{ "SLPS-25324", "Nishikaze no Kyoushikyoku - The Rhapsody of Zephyr (Japan)" },
{ "SLES-50232", "Off-Road - Wide Open (Europe)" },
{ "SLPM-65010", "Onimusha (Japan)" },
{ "SLES-51913", "Onimusha - Blade Warriors (Europe)" },
{ "SLES-50247", "Onimusha - Warlords (Europe)" },
{ "SLUS-20018", "Onimusha - Warlords (USA) (En,Ja)" },
{ "SCPS-15038", "Operator’s Side (Japan)" },
{ "SLPM-65524", "Orange Pocket - Root (Japan)" },
{ "SLPM-65005", "Ore ga Kantoku da! Gekitou Pennant Race (Japan)" },
{ "SCPS-15017", "PaRappa the Rapper 2 (Japan) (En,Ja)" },
{ "SCES-50888", "Pac-Man World 2 (Europe)" },
{ "SLPS-25141", "Pac-Man World 2 (Japan)" },
{ "SLUS-20224", "Pac-Man World 2 (USA) (v1.00)" },
{ "SLUS-20224", "Pac-Man World 2 (USA) (v2.00)" },
{ "SLPS-20186", "Pachinko de Asobou! Fever Dodeka Saurus (Japan)" },
{ "SLES-50212", "Paris-Dakar Rally (Europe)" },
{ "SLUS-20324", "Paris-Dakar Rally (USA)" },
{ "SLES-50252", "Penny Racers (Europe)" },
{ "SLPS-25222", "Pia Carrot e Youkoso!! 3 - Round Summer (Japan)" },
{ "SCPS-11014", "Piposaru 2001 (Japan)" },
{ "SLPM-65611", "Pizzicato Polka - Ensa Gen’ya (Japan)" },
{ "SCPS-15063", "PoPoLoCrois - Tsuki no Okite no Bouken (Japan)" },
{ "SLPS-20323", "Pochi to Nyaa (Japan)" },
{ "SCES-51135", "Primal" },
{ "SLES-50637", "Pro Rally 2002 (Europe)" },
{ "SLPM-65543", "Pro Yakyuu Spirits 2004 (Japan)" },
{ "SLPM-65721", "Pro Yakyuu Spirits 2004 Climax (Japan)" },
{ "SLPM-65426", "Pro Yakyuu Team o Tsukurou! 2003 (Japan)" },
{ "SLES-50821", "Project Zero (Europe)" },
{ "SLPM-66235", "Psychic Force Complete (Japan)" },
{ "SLPM-64534", "Psyvariar - Complete Edition (Korea)" },
{ "SLPM-65532", "Puyo Puyo Fever (Japan) (En,Ja,Fr,De,Es,It)" },
{ "SLES-50126", "Quake III - Revolution (Europe)" },
{ "SLES-50127", "Quake III - Revolution (Germany)" },
{ "SLUS-20167", "Quake III - Revolution (USA)" },
{ "SLPM-62424", "Quiz & Variety - Suku Suku Inufuku (Japan)" },
{ "SLES-50981", "R-C Sports Copter Challenge (Europe)" },
{ "SLES-50077", "RC Revenge Pro (Europe)" },
{ "SLUS-20153", "RC Revenge Pro (USA)" },
{ "SLUS-20340", "RPG Maker II (USA)" },
{ "SLPS-20143", "RPG Tkool 5 (Japan)" },
{ "SLES-51391", "RTL Skispringen 2003 (Germany)" },
{ "SLES-51633", "Racing Simulation 3 (Europe)" },
{ "SLPS-20307", "Rakushou! Pachi-Slot Sengen (Japan)" },
{ "SLES-50763", "Rally Championship (Europe)" },
{ "SLPS-20305", "Real Sports Pro Yakyuu (Japan)" },
{ "SLPM-65004", "Reiselied - Ephemeral Fantasia (Japan) (v1.00)" },
{ "SLPM-65004", "Reiselied - Ephemeral Fantasia (Japan) (v2.01)" },
{ "SLES-50306", "Resident Evil - Code - Veronica X (Europe)" },
{ "SLUS-20184", "Resident Evil - Code - Veronica X (USA)" },
{ "SLES-50650", "Resident Evil - Survivor 2 - Code - Veronica (Europe)" },
{ "SLUS-21134", "Resident Evil 4" },
{ "SLPS-25094", "Reveal Fantasia - Mariel to Yousei Monogatari (Japan)" },
{ "SLES-50113", "Ring of Red (Europe)" },
{ "SLPM-60122", "Ring of Red (Japan) (Taikenban)" },
{ "SLPM-62013", "Ring of Red (Japan)" },
{ "SLUS-20145", "Ring of Red (USA)" },
{ "SLES-51374", "RoboCop (Europe)" },
{ "SLES-50136", "Robot Warlords (Europe)" },
{ "SLES-50137", "Robot Warlords (France)" },
{ "SLES-50138", "Robot Warlords (Germany)" },
{ "SLES-50572", "Robot Wars - Arenas of Destruction (UK)" },
{ "SLPS-25005", "Rock’n Megastage (Japan)" },
{ "SLPM-99999", "Rockman X - Command Mission (Japan)" },
{ "SLPM-65463", "Rocky (Japan)" },
{ "SLES-52002", "Rogue Ops (Europe)" },
{ "SLPM-65534", "Rogue Ops (Japan)" },
{ "SLUS-20746", "Rogue Ops (USA)" },
{ "SLES-52100", "Rugby League (Australia)" },
{ "SLUS-20174", "Rumble Racing" },
{ "SLES-50335", "Rune - Viking Warlord (Europe)" },
{ "SLES-50337", "Rune - Viking Warlord (France)" },
{ "SLES-50336", "Rune - Viking Warlord (Germany)" },
{ "SLES-50338", "Rune - Viking Warlord (Italy)" },
{ "SLES-50339", "Rune - Viking Warlord (Spain)" },
{ "SLUS-20109", "Rune - Viking Warlord (USA)" },
{ "SLPS-25316", "SNK vs. Capcom - SVC Chaos (Japan)" },
{ "SLUS-20433", "SWAT - Global Strike Team (USA)" },
{ "SLUS-20600", "SX Superstar" },
{ "SCPS-11005", "Sagashi ni Ikouyo (Japan)" },
{ "SLPS-20365", "Saikyou Ginsei Shougi 4 (Japan)" },
{ "SLPS-25081", "Saishuu Densha (Japan)" },
{ "SLPM-65275", "Saishuu Heiki Kanojo (Japan)" },
{ "SLPS-20391", "Saiyuuki Reload Gunlock (Japan)" },
{ "SLPM-65109", "Saka Tsuku 2002 - J. League Pro Soccer Club wo Tsukurou! (Japan)" },
{ "SLPM-65515", "Sakura Taisen Monogatari - Mysterious Paris (Japan)" },
{ "SLPS-25559", "Samurai Spirits - Tenkaichi Kenkakuden (Japan)" },
{ "SLPS-20203", "Sanyo Pachinko Paradise 7 - Edokko Gen-san (Japan)" },
{ "SLES-51883", "Scooby-Doo! Mystery Mayhem (Europe)" },
{ "SLUS-20701", "Scooby-Doo! Mystery Mayhem (USA) (En,Fr)" },
{ "SLUS-20424", "Scorpion King, The - Rise of the Akkadian (USA)" },
{ "SLPM-62079", "Se-Pa 2001 (Japan)" },
{ "SLUS-20606", "Seek and Destroy (USA)" },
{ "SLPM-62400", "Sega Ages 2500 Series Vol. 12 - Puyo Puyo Tsuu - Perfect Set (Japan)" },
{ "SLPM-62547", "Sega Ages 2500 Series Vol. 16 - Virtua Fighter 2 (Japan)" },
{ "SLPM-62366", "Sega Ages 2500 Series Vol. 3 - Fantasy Zone (Japan)" },
{ "SLPM-62385", "Sega Ages 2500 Series Vol. 5 - Golden Axe (Japan)" },
{ "SLES-51388", "Sega Bass Fishing Duel (Europe)" },
{ "SLUS-20339", "Sega Bass Fishing Duel (USA)" },
{ "SLES-53461", "Sega Classics Collection (Europe, Australia)" },
{ "SLES-51125", "Sega Soccer Slam (Europe)" },
{ "SLUS-20509", "Sega Soccer Slam (USA)" },
{ "SLES-51253", "Seigneur des Anneaux, Le - Les Deux Tours (France)" },
{ "SLES-51256", "Senor de los Anillos, El - Las Dos Torres (Spain)" },
{ "SLES-50822", "Shadow Hearts (Europe)" },
{ "SLPS-25041", "Shadow Hearts (Japan)" },
{ "SLUS-20347", "Shadow Hearts (USA)" },
{ "SLES-50446", "Shadow Man - 2econd Coming (Europe)" },
{ "SLES-50608", "Shadow Man - 2econd Coming (Germany)" },
{ "SLUS-20413", "Shadow Man - 2econd Coming (USA)" },
{ "?", "Shadow of the Colossus (Europe)" },
{ "SLES-50400", "Shaun Palmer’s Pro Snowboarder (Europe)" },
{ "SLES-50401", "Shaun Palmer’s Pro Snowboarder (France)" },
{ "SLES-50402", "Shaun Palmer’s Pro Snowboarder (Germany)" },
{ "SLUS-20199", "Shaun Palmer’s Pro Snowboarder (USA)" },
{ "SLPM-65334", "Shin Seiki Evangelion - Ayanami Ikusei Keikaku with Asuka Hokan Keikaku (Japan)" },
{ "SLPM-65867", "Shin Seiki Evangelion - Koutetsu no Girlfriend 2nd (Japan)" },
{ "SLPM-65391", "Shinki Gensou - Spectral Souls (Japan)" },
{ "SLPM-65200", "Shinobi (Japan)" },
{ "SLUS-20459", "Shinobi (USA) (En,Ja)" },
{ "SLPM-65328", "Shirachuu Tankenbu (Japan)" },
{ "SLES-52382", "Shrek 2 (Spain)" },
{ "SLPS-25076", "Sidewinder F (Japan)" },
{ "SLPS-25018", "Sidewinder Max (Japan)" },
{ "SLPS-25255", "Sidewinder V (Japan)" },
{ "SLES-51157", "Silent Scope 3 (Europe)" },
{ "SLUS-20514", "Silent Scope 3 (USA) (En,Ja,Es)" },
{ "SLUS-20624", "Simpsons Hit and Run" },
{ "SLES-50754", "Simpsons Skateboarding, The (Europe)" },
{ "SLES-50755", "Simpsons Skateboarding, The (France)" },
{ "SLES-51362", "Simpsons Skateboarding, The (Germany)" },
{ "SLES-51360", "Simpsons Skateboarding, The (Italy)" },
{ "SLES-51361", "Simpsons Skateboarding, The (Spain)" },
{ "SLUS-20114", "Simpsons Skateboarding, The (USA)" },
{ "SLES-51257", "Sims, The (Europe)" },
{ "SLUS-20573", "Sims, The (USA)" },
{ "SLES-50261", "Sky Surfer (Europe)" },
{ "SLPS-20012", "Sky Surfer (Japan)" },
{ "SLPS-20262", "Slot! Pro DX - Fujiko 2 (Japan)" },
{ "SLPS-20285", "Slotter Up Core - Enda! Kyojin no Hoshi (Japan)" },
{ "SLPS-20370", "Slotter Up Core 3 - Yuda! Doronjo ni Omakase (Japan)" },
{ "SLPS-20337", "Slotter Up Core Alpha - Shukko! Yuushou Panel! Shinka! Kyojin no Hoshi (Japan)" },
{ "SLPS-20278", "Slotter Up Mania - Chou Oki-Slot! Pioneer Special (Japan)" },
{ "SLPM-62615", "Slotter Up Mania 6 - Oki no Neppuu! Pioneer Special II (Japan)" },
{ "SLES-51800", "Smash Cars (Europe)" },
{ "SLUS-20620", "Smash Cars (USA)" },
{ "SLPM-65431", "Sonic Heroes (Japan) (En,Ja,Fr,De,Es,It)" },
{ "SLUS-20718", "Sonic Heroes (USA) (En,Ja,Fr,De,Es,It)" },
{ "SLPM-62310", "Soutenryuu - The Arcade (Japan)" },
{ "SLPM-62275", "Space Raiders (Japan)" },
{ "SLES-50486", "Splashdown (Europe)" },
{ "SLES-50268", "SpyHunter (Europe)" },
{ "SLUS-20056", "SpyHunter (USA)" },
{ "SLES-51043", "Spyro - Enter the Dragonfly (Europe)" },
{ "SLUS-20315", "Spyro - Enter the Dragonfly (USA)" },
{ "SLES-52545", "Star Wars - Battlefront (Europe)" },
{ "SLES-52546", "Star Wars - Battlefront (France)" },
{ "SLES-53503", "Star Wars - Battlefront (Germany)" },
{ "SLUS-29164", "Star Wars - Battlefront II (USA) (Beta)" },
{ "SLPS-25252", "Star Wars - Jango Fett (Japan)" },
{ "SLES-50204", "Star Wars - Super Bombad Racing (Europe)" },
{ "SLES-50205", "Star Wars - Super Bombad Racing (France)" },
{ "SLES-50206", "Star Wars - Super Bombad Racing (Germany)" },
{ "SLES-50207", "Star Wars - Super Bombad Racing (Italy)" },
{ "SLES-50208", "Star Wars - Super Bombad Racing (Spain)" },
{ "SLUS-20043", "Star Wars - Super Bombad Racing (USA) (En,Fr,De,Es,It)" },
{ "SLPS-20018", "Stepping Selection (Japan) (Disc 1)" },
{ "SLPS-20019", "Stepping Selection (Japan) (Disc 2)" },
{ "SLES-50072", "Street Fighter EX3 (Europe)" },
{ "SLPM-60105", "Street Fighter EX3 (Japan) (Taikenban)" },
{ "SLPS-20003", "Street Fighter EX3 (Japan)" },
{ "SLUS-20130", "Street Fighter EX3 (USA)" },
{ "SLES-50064", "Stunt GP (Europe)" },
{ "SLPS-20152", "Stunt GP (Japan)" },
{ "SLUS-20218", "Stunt GP (USA)" },
{ "SLES-51160", "Sub Rebellion (Europe)" },
{ "SLUS-20548", "Sub Rebellion (USA)" },
{ "SLPM-65751", "Suigetsu - Mayoi Gokoro (Japan)" },
{ "SLUS-20074", "Summoner (USA)" },
{ "SLES-50533", "Sunny Garcia Surfing (Europe)" },
{ "SLUS-20208", "Sunny Garcia Surfing (USA)" },
{ "SLPS-25070", "Sunrise Eiyuutan 2 (Japan)" },
{ "SLPS-25270", "Sunrise World War (Japan)" },
{ "SLPS-25104", "Super Robot Taisen Impact (Japan)" },
{ "SLES-50897", "Super Trucks (Europe)" },
{ "SLUS-20748", "Super Trucks Racing (USA)" },
{ "SLPM-62423", "SuperLite 2000 Vol. 13 - Tetris - Kiwame Michi (Japan) (v1.02)" },
{ "SLPM-65689", "SuperLite 2000 Vol. 23 - Never 7 - The End of Infinity (Japan)" },
{ "SLES-50419", "Supercar Street Challenge (Europe)" },
{ "SLES-50421", "Supercar Street Challenge (Germany)" },
{ "SLUS-20012", "Supercar Street Challenge (USA)" },
{ "SLES-50852", "Sven-Goeran Eriksson’s World Challenge (Europe)" },
{ "SLES-50794", "Sven-Goeran Eriksson’s World Manager 2002 (Europe)" },
{ "SLES-50033", "Swing Away Golf (Europe)" },
{ "SLUS-20096", "Swing Away Golf (USA)" },
{ "SLPM-65121", "Switch (Japan)" },
{ "SLES-51290", "Sword of the Samurai (Europe)" },
{ "SLPM-65261", "TBS All Star Kanshasai Vol. 1 - Chou Gouka! Quiz Ketteiban (Japan)" },
{ "SLES-50778", "TD Overdrive - The Brotherhood of Speed (Europe)" },
{ "SLPM-62105", "Taikou Risshiden IV (Japan)" },
{ "SLPM-65450", "Tantei Gakuen Q - Kioukan no Satsui (Japan)" },
{ "SLPM-60134", "Technictix (Japan) (Taikenban)" },
{ "SLPS-20055", "Technictix (Japan)" },
{ "SLUS-20981", "Teenage Mutant Ninja Turtles 2 - Battle Nexus (USA)" },
{ "SCAJ-20100", "Tenchu Kurenai (Asia)" },
{ "SLPS-25384", "Tenchu Kurenai (Japan)" },
{ "SLPM-65401", "Tengai Makyou II - Manji Maru (Japan) (Shokai Gentei Picture Label Shiyou)" },
{ "SLPM-65401", "Tengai Makyou II - Manji Maru (Japan)" },
{ "SLPM-65398", "Tennis no Oujisama - Kiss of Prince Flame (Japan)" },
{ "SLPM-65397", "Tennis no Oujisama - Kiss of Prince Ice (Japan)" },
{ "SLPM-65323", "Tennis no Oujisama - Smash Hit! (Japan)" },
{ "SLPM-62359", "Tennis no Oujisama - Smash Hit! Original Anime Game (Japan)" },
{ "SLPM-65371", "Tennis no Oujisama - Sweat & Tears 2 - Seishun Gakuen Teikyuusai ‘03 - Perfect Live (Japan)" },
{ "SLPS-20053", "Tenshi no Present - Marl Oukoku Monogatari (Japan) (Genteiban)" },
{ "SLPS-20066", "Tenshi no Present - Marl Oukoku Monogatari (Japan)" },
{ "SLPM-65598", "Tenshou Gakuen Gensouroku (Japan)" },
{ "SLPS-25298", "Tentama - 1st Sunny Side (Japan)" },
{ "SLUS-20213", "Test Drive (USA)" },
{ "SLUS-20177", "Test Drive Off-Road - Wide Open (USA)" },
{ "SLES-50551", "Tetris Worlds (Europe)" },
{ "SLUS-20099", "Theme Park Roller Coaster (USA)" },
{ "SLES-50078", "TimeSplitters (Europe)" },
{ "SLUS-20090", "TimeSplitters (USA) (v1.10)" },
{ "SLUS-20090", "TimeSplitters (USA) (v2.00)" },
{ "SLES-51181", "Tom Clancy’s Ghost Recon (Europe)" },
{ "SLES-51182", "Tom Clancy’s Ghost Recon (Germany)" },
{ "SLUS-20613", "Tom Clancy’s Ghost Recon (USA)" },
{ "SLED-51472", "Tom Clancy’s Splinter Cell (Europe) (Demo)" },
{ "SLUS-20652", "Tom Clancy’s Splinter Cell (USA)" },
{ "SLES-50400", "Tony Hawk’s Pro Skater 3 (Europe)" },
{ "SLES-50401", "Tony Hawk’s Pro Skater 3 (France)" },
{ "SLES-50402", "Tony Hawk’s Pro Skater 3 (Germany)" },
{ "SLUS-20199", "Tony Hawk’s Pro Skater 3 (USA) (Rev 1)" },
{ "SLUS-20199", "Tony Hawk’s Pro Skater 3 (USA)" },
{ "SLPS-99999", "Tony Hawk’s Pro Skater 4 (USA) (v1.02)" },
{ "SLPS-99999", "Tony Hawk’s Pro Skater 4 (USA) (v2.01)" },
{ "SCED-52441", "Transformers (Europe) (Demo)" },
{ "SLUS-20149", "Tribes - Aerial Assault (USA)" },
{ "SLUS-20931", "Trigger Man (USA)" },
{ "SLUS-20168", "Triple Play Baseball (USA)" },
{ "SLPS-20196", "Tsuki no Hikari - Shizumeru Kane no Satsujin (Japan)" },
{ "SCES-50360", "Twisted Metal - Black (Europe)" },
{ "SCUS-97101", "Twisted Metal - Black (USA)" },
{ "SLPS-20080", "Typing Namidabashi Ashita no Joe Touda (Japan) (USB Keyboard Doukonban)" },
{ "SLPS-20194", "U - Underwater Unit (Japan)" },
{ "SLES-50195", "UEFA Challenge (Europe)" },
{ "SLPS-25294", "Uchuu no Stellvia (Japan)" },
{ "SLPS-25364", "Ultraman (Japan)" },
{ "SLES-51606", "Unlimited Saga (Europe)" },
{ "SLPS-25185", "Unlimited Saga (Japan) (Limited Edition)" },
{ "SLPS-25199", "Unlimited Saga (Japan)" },
{ "SLUS-20678", "Unlimited Saga (USA)" },
{ "SLES-50725", "V-Rally 3 (Europe)" },
{ "SLPM-65191", "V-Rally 3 (Japan) (En,Ja)" },
{ "SCES-50411", "Vampire Night (Europe, Australia)" },
{ "SLPS-25077", "Vampire Night (Japan)" },
{ "SLUS-20221", "Vampire Night (USA)" },
{ "SLPS-20034", "Velvet File (Japan)" },
{ "SLPS-25012", "Victorious Boxers (Japan)" },
{ "SLPS-25129", "Victorious Boxers - Championship Version (Japan)" },
{ "SLES-50280", "Victorious Boxers - Ippo’s Road to Glory (Europe)" },
{ "SLUS-20282", "Victorious Boxers - Ippo’s Road to Glory (USA)" },
{ "SLPS-25287", "Victorious Boxers 2 - Ioop’s Road to Glory (Japan)" },
{ "SLUS-20951", "Viewtiful Joe (USA)" },
{ "SLES-51699", "Virtua Fighter - 10th Anniversary Edition (Europe)" },
{ "SLES-51616", "Virtua Fighter 4 - Evolution (Europe)" },
{ "SLPM-65270", "Virtua Fighter 4 - Evolution (Japan)" },
{ "SLKA-00000", "Virtua Fighter 4 - Evolution (Korea)" },
{ "SLUS-00000", "Virtua Fighter 4 - Evolution (USA)" },
{ "SLES-51600", "WWE Crush Hour (Europe)" },
{ "SLUS-20385", "WWE Crush Hour (USA)" },
{ "SLES-52036", "WWE SmackDown! Here Comes the Pain (Europe)" },
{ "SLUS-20787", "WWE SmackDown! Here Comes the Pain (USA)" },
{ "SLES-51283", "WWE SmackDown! Shut Your Mouth (Europe)" },
{ "SLUS-20483", "WWE SmackDown! Shut Your Mouth (USA)" },
{ "SLES-50183", "Wacky Races Starring Dastardly & Muttley (Europe)" },
{ "SLES-51272", "Wakeboarding Unleashed featuring Shaun Murray (Europe)" },
{ "SLES-51273", "Wakeboarding Unleashed featuring Shaun Murray (France)" },
{ "SLUS-20418", "Wakeboarding Unleashed featuring Shaun Murray (USA)" },
{ "SLUS-20075", "Walt Disney’s The Jungle Book - Rhythm n’ Groove (USA)" },
{ "SLES-51973", "War Chess (Europe)" },
{ "SCUS-97197", "War of the Monsters (USA)" },
{ "SLES-50503", "Weakest Link, The (Europe)" },
{ "SLPM-62019", "Winning Post 4 Maximum (Japan)" },
{ "SLPM-62058", "Winning Post 4 Maximum 2001 (Japan) (Super Value Set)" },
{ "SLPM-62123", "Winning Post 5 (Japan)" },
{ "SLPM-62280", "Winning Post 5 Maximum 2002 (Japan) (Premium Pack)" },
{ "SLPM-62221", "Winning Post 5 Maximum 2002 (Japan)" },
{ "SLES-50035", "Winter X Games Snowboarding (Europe)" },
{ "SLES-50670", "Winter X Games Snowboarding 2 (Europe)" },
{ "SLUS-20321", "Winter X Games Snowboarding 2002 (USA)" },
{ "SLES-50170", "World Destruction League - Thunder Tanks (Europe)" },
{ "SLUS-20005", "World Destruction League - Thunder Tanks (USA)" },
{ "SLES-50262", "World Destruction League - WarJetz (Europe)" },
{ "SLUS-20007", "World Destruction League - WarJetz (USA)" },
{ "SLUS-20611", "World Series Baseball 2K3 (USA)" },
{ "SLPM-62268", "World Soccer Winning Eleven 6 - Final Evolution (Japan)" },
{ "SLES-51843", "Worms 3D (Europe)" },
{ "SLES-51202", "Wreckless - The Yakuza Missions (Europe)" },
{ "SLUS-20431", "Wreckless - The Yakuza Missions (USA)" },
{ "SLES-50430", "X Games Skateboarding (Europe)" },
{ "SLES-50031", "X Squad (Europe)" },
{ "SLUS-20094", "X Squad (USA)" },
{ "SLES-50210", "XGIII - Extreme G Racing (Europe) (v1.02)" },
{ "SLES-50210", "XGIII - Extreme G Racing (Europe) (v2.00)" },
{ "SLUS-20302", "XGIII - Extreme G Racing (USA)" },
{ "SLPS-29002", "Xenosaga Episode I - Der Wille zur Macht (Japan) (Premium Box)" },
{ "SLPS-29002", "Xenosaga Episode I - Der Wille zur Macht (Japan)" },
{ "SLUS-20469", "Xenosaga Episode I - Der Wille zur Macht (USA)" },
{ "SLPS-25048", "Zeonic Front - Kidou Senshi Gundam 0079 (Japan)" },
{ "SLPS-25074", "Zero (Japan)" },
{ "SLPS-25303", "Zero - Akai Chou (Japan)" },
{ "SLPM-65019", "Zone of the Enders - Z.O.E (Japan)" },
{ "SLES-50933", "eJay ClubWorld - The Music Making Experience (Europe)" },
{ "SLUS-20525", "eJay Clubworld - The Music Making Experience (USA)" }
};
const char* getGameName(const std::string& gameId)
{
auto it = gameDatabase.find(gameId);
if (it != gameDatabase.end())
return it->second.c_str();
return nullptr;
}
+7
View File
@@ -11,7 +11,14 @@ add_executable(ps2x_tests
src/r5900_decoder_tests.cpp
src/elf_analyzer_tests.cpp
src/ps2_runtime_io_tests.cpp
src/ps2_runtime_kernel_tests.cpp
src/ps2_runtime_interrupt_tests.cpp
src/ps2_memory_tests.cpp
src/ps2_gs_tests.cpp
src/ps2_sif_rpc_tests.cpp
src/ps2_sif_dma_tests.cpp
src/ps2_recompiler_tests.cpp
src/ps2_runtime_expansion_tests.cpp
)
option(PRINT_GENERATED_CODE "Print generated code in tests" OFF)
+333
View File
@@ -112,6 +112,46 @@ void register_code_generator_tests()
{
MiniTest::Case("CodeGenerator", [](TestCase &tc)
{
tc.Run("R5900 MULT writes rd when rd is non-zero", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction mult{};
mult.opcode = OPCODE_SPECIAL;
mult.function = SPECIAL_MULT;
mult.rs = 4;
mult.rt = 5;
mult.rd = 3;
std::string generated = gen.translateInstruction(mult);
printGeneratedCode("R5900 MULT writes rd when rd is non-zero", generated);
t.IsTrue(generated.find("SET_GPR_S32(ctx, 3, (int32_t)result);") != std::string::npos,
"MULT should write low product to rd on R5900");
mult.rd = 0;
generated = gen.translateInstruction(mult);
t.IsTrue(generated.find("SET_GPR_S32(") == std::string::npos,
"MULT should not write rd when rd is zero");
});
tc.Run("R5900 MMI MULT1 writes rd when rd is non-zero", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction mult1{};
mult1.opcode = OPCODE_MMI;
mult1.isMMI = true;
mult1.function = MMI_MULT1;
mult1.rs = 8;
mult1.rt = 9;
mult1.rd = 10;
std::string generated = gen.translateInstruction(mult1);
printGeneratedCode("R5900 MMI MULT1 writes rd when rd is non-zero", generated);
t.IsTrue(generated.find("SET_GPR_S32(ctx, 10, (int32_t)result);") != std::string::npos,
"MULT1 should write low product to rd on R5900");
});
tc.Run("emits labels and gotos for internal branches", [](TestCase &t) {
Function func;
func.name = "test_func";
@@ -393,6 +433,158 @@ void register_code_generator_tests()
t.IsTrue(ctc2Code.find("Unimplemented CTC2 VU CReg") == std::string::npos, "CTC2 should not hit unimplemented CReg path");
});
tc.Run("scalar logical immediates emit low64 operations", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction andi{};
andi.opcode = OPCODE_ANDI;
andi.rs = 4;
andi.rt = 5;
andi.immediate = 0xABCD;
std::string andiCode = gen.translateInstruction(andi);
t.IsTrue(andiCode.find("SET_GPR_U64(ctx, 5, GPR_U64(ctx, 4) & (uint64_t)(uint16_t)43981);") != std::string::npos,
"ANDI should use low64 scalar emission");
t.IsTrue(andiCode.find("SET_GPR_VEC") == std::string::npos,
"ANDI should not use vector emission");
Instruction ori{};
ori.opcode = OPCODE_ORI;
ori.rs = 6;
ori.rt = 7;
ori.immediate = 0x1234;
std::string oriCode = gen.translateInstruction(ori);
t.IsTrue(oriCode.find("SET_GPR_U64(ctx, 7, GPR_U64(ctx, 6) | (uint64_t)(uint16_t)4660);") != std::string::npos,
"ORI should use low64 scalar emission");
t.IsTrue(oriCode.find("SET_GPR_VEC") == std::string::npos,
"ORI should not use vector emission");
Instruction xori{};
xori.opcode = OPCODE_XORI;
xori.rs = 8;
xori.rt = 9;
xori.immediate = 0x00FF;
std::string xoriCode = gen.translateInstruction(xori);
t.IsTrue(xoriCode.find("SET_GPR_U64(ctx, 9, GPR_U64(ctx, 8) ^ (uint64_t)(uint16_t)255);") != std::string::npos,
"XORI should use low64 scalar emission");
t.IsTrue(xoriCode.find("SET_GPR_VEC") == std::string::npos,
"XORI should not use vector emission");
});
tc.Run("scalar logical register ops emit low64 operations", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction andInst{};
andInst.opcode = OPCODE_SPECIAL;
andInst.function = SPECIAL_AND;
andInst.rs = 2;
andInst.rt = 3;
andInst.rd = 1;
std::string andCode = gen.translateInstruction(andInst);
t.IsTrue(andCode.find("SET_GPR_U64(ctx, 1, GPR_U64(ctx, 2) & GPR_U64(ctx, 3));") != std::string::npos,
"AND should use low64 scalar emission");
Instruction orInst{};
orInst.opcode = OPCODE_SPECIAL;
orInst.function = SPECIAL_OR;
orInst.rs = 4;
orInst.rt = 5;
orInst.rd = 6;
std::string orCode = gen.translateInstruction(orInst);
t.IsTrue(orCode.find("SET_GPR_U64(ctx, 6, GPR_U64(ctx, 4) | GPR_U64(ctx, 5));") != std::string::npos,
"OR should use low64 scalar emission");
Instruction xorInst{};
xorInst.opcode = OPCODE_SPECIAL;
xorInst.function = SPECIAL_XOR;
xorInst.rs = 7;
xorInst.rt = 8;
xorInst.rd = 9;
std::string xorCode = gen.translateInstruction(xorInst);
t.IsTrue(xorCode.find("SET_GPR_U64(ctx, 9, GPR_U64(ctx, 7) ^ GPR_U64(ctx, 8));") != std::string::npos,
"XOR should use low64 scalar emission");
Instruction norInst{};
norInst.opcode = OPCODE_SPECIAL;
norInst.function = SPECIAL_NOR;
norInst.rs = 10;
norInst.rt = 11;
norInst.rd = 12;
std::string norCode = gen.translateInstruction(norInst);
t.IsTrue(norCode.find("SET_GPR_U64(ctx, 12, ~(GPR_U64(ctx, 10) | GPR_U64(ctx, 11)));") != std::string::npos,
"NOR should use low64 scalar emission");
t.IsTrue(norCode.find("SET_GPR_VEC") == std::string::npos,
"SPECIAL logical ops should not use vector emission");
});
tc.Run("SC requires matching LL reservation address", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction sc{};
sc.opcode = OPCODE_SC;
sc.rs = 9;
sc.rt = 10;
sc.simmediate = static_cast<uint32_t>(static_cast<int16_t>(4));
std::string out = gen.translateInstruction(sc);
t.IsTrue(out.find("ctx->llbit && ctx->lladdr == addr") != std::string::npos,
"SC must require both llbit and matching lladdr");
t.IsTrue(out.find("ctx->llbit = 0; ctx->lladdr = 0;") != std::string::npos,
"SC must clear reservation state after attempting the store");
});
tc.Run("QFSRV translation uses runtime helper macro", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction qfsrv{};
qfsrv.isMMI = true;
qfsrv.opcode = OPCODE_MMI;
qfsrv.function = MMI_MMI1;
qfsrv.sa = MMI1_QFSRV;
qfsrv.rd = 3;
qfsrv.rs = 4;
qfsrv.rt = 5;
std::string out = gen.translateInstruction(qfsrv);
t.IsTrue(out.find("PS2_QFSRV(GPR_VEC(ctx, 4), GPR_VEC(ctx, 5), ctx->sa & 0x7F)") != std::string::npos,
"QFSRV should map to PS2_QFSRV with rs/rt ordering");
});
tc.Run("PCPYLD and PEXEW use runtime helper macros", [](TestCase &t) {
CodeGenerator gen({}, {});
Instruction pcpyld{};
pcpyld.isMMI = true;
pcpyld.opcode = OPCODE_MMI;
pcpyld.function = MMI_MMI2;
pcpyld.sa = MMI2_PCPYLD;
pcpyld.rd = 6;
pcpyld.rs = 7;
pcpyld.rt = 8;
std::string pcpyldOut = gen.translateInstruction(pcpyld);
t.IsTrue(pcpyldOut.find("PS2_PCPYLD(GPR_VEC(ctx, 7), GPR_VEC(ctx, 8))") != std::string::npos,
"PCPYLD should use PS2_PCPYLD helper");
Instruction pexew{};
pexew.isMMI = true;
pexew.opcode = OPCODE_MMI;
pexew.function = MMI_MMI2;
pexew.sa = MMI2_PEXEW;
pexew.rd = 9;
pexew.rs = 10;
std::string pexewOut = gen.translateInstruction(pexew);
t.IsTrue(pexewOut.find("PS2_PEXEW(GPR_VEC(ctx, 10))") != std::string::npos,
"PEXEW should use PS2_PEXEW helper");
});
tc.Run("VU0 macro mappings cover all S1/S2 enums", [](TestCase &t) {
const std::vector<std::string> candidates = {
"ps2xRecomp/include/ps2recomp/instructions.h",
@@ -737,6 +929,82 @@ void register_code_generator_tests()
"switch should include other in-function labels");
});
tc.Run("configured jump table addresses drive JR dispatch targets", [](TestCase &t) {
Function func;
func.name = "jr_configured_jump_table";
func.start = 0x1600;
func.end = 0x1640;
func.isRecompiled = true;
func.isStub = false;
constexpr uint32_t tableAddress = 0x00200000u;
Instruction lui{};
lui.address = 0x1600;
lui.opcode = OPCODE_LUI;
lui.rt = 9;
lui.immediate = static_cast<uint16_t>((tableAddress >> 16) & 0xFFFFu);
Instruction addiu{};
addiu.address = 0x1604;
addiu.opcode = OPCODE_ADDIU;
addiu.rs = 9;
addiu.rt = 9;
addiu.immediate = static_cast<uint16_t>(tableAddress & 0xFFFFu);
addiu.simmediate = addiu.immediate;
Instruction sll{};
sll.address = 0x1608;
sll.opcode = OPCODE_SPECIAL;
sll.function = SPECIAL_SLL;
sll.rd = 8;
sll.rt = 4;
sll.sa = 2;
Instruction addu{};
addu.address = 0x160C;
addu.opcode = OPCODE_SPECIAL;
addu.function = SPECIAL_ADDU;
addu.rs = 9;
addu.rt = 8;
addu.rd = 9;
Instruction lw{};
lw.address = 0x1610;
lw.opcode = OPCODE_LW;
lw.rs = 9;
lw.rt = 10;
lw.immediate = 0;
lw.simmediate = 0;
Instruction jr = makeJr(0x1614, 10);
Instruction jrDelay = makeNop(0x1618);
Instruction target0 = makeNop(0x1620);
Instruction target1 = makeNop(0x1630);
JumpTable configured{};
configured.address = tableAddress;
configured.entries.push_back({0u, 0x1620u});
configured.entries.push_back({1u, 0x1630u});
CodeGenerator gen({}, {});
gen.setConfiguredJumpTables({configured});
std::string generated = gen.generateFunction(
func,
{lui, addiu, sll, addu, lw, jr, jrDelay, target0, target1},
false);
printGeneratedCode("configured jump table addresses drive JR dispatch targets", generated);
t.IsTrue(generated.find("switch (jumpTarget)") != std::string::npos,
"JR should emit a switch");
t.IsTrue(generated.find("case 0x1620u: goto label_1620;") != std::string::npos,
"configured table target 0x1620 should be emitted");
t.IsTrue(generated.find("case 0x1630u: goto label_1630;") != std::string::npos,
"configured table target 0x1630 should be emitted");
t.IsTrue(generated.find("case 0x1600u: goto label_1600;") == std::string::npos,
"configured table should avoid broad JR fallback labels");
});
tc.Run("JALR includes switch and fallback/guard pair", [](TestCase &t) {
Function func;
func.name = "jalr_switch_and_fallback";
@@ -767,6 +1035,71 @@ void register_code_generator_tests()
"JALR should retain non-fallthrough guard");
});
tc.Run("JALR fallback should not expose epilogue tail-jump labels", [](TestCase &t) {
Function func;
func.name = "jalr_epilogue_guard";
func.start = 0x2000;
func.end = 0x2030;
func.isRecompiled = true;
func.isStub = false;
Instruction prolog{};
prolog.address = 0x2000;
prolog.opcode = OPCODE_ADDIU;
prolog.rs = 29;
prolog.rt = 29;
prolog.simmediate = static_cast<uint32_t>(static_cast<int32_t>(-0x20));
prolog.raw = 0;
Instruction saveRa{};
saveRa.address = 0x2004;
saveRa.opcode = OPCODE_SD;
saveRa.rs = 29;
saveRa.rt = 31;
saveRa.simmediate = 0x10;
saveRa.raw = 0;
// Dynamic callback entry point.
Instruction jalr = makeJalr(0x2008, 2, 31);
Instruction jalrDelay = makeNop(0x200C);
Instruction restoreRa{};
restoreRa.address = 0x2010;
restoreRa.opcode = OPCODE_LD;
restoreRa.rs = 29;
restoreRa.rt = 31;
restoreRa.simmediate = 0x10;
restoreRa.raw = 0;
// Tail jump sequence that must not be reachable from jalr fallback dispatch.
Instruction tailJump{};
tailJump.address = 0x2014;
tailJump.opcode = OPCODE_J;
tailJump.target = (0x3000u >> 2) & 0x3FFFFFFu;
tailJump.hasDelaySlot = true;
tailJump.raw = 0;
Instruction tailDelay{};
tailDelay.address = 0x2018;
tailDelay.opcode = OPCODE_ADDIU;
tailDelay.rs = 29;
tailDelay.rt = 29;
tailDelay.simmediate = 0x20;
tailDelay.raw = 0;
CodeGenerator gen({}, {});
std::string generated = gen.generateFunction(
func,
{prolog, saveRa, jalr, jalrDelay, restoreRa, tailJump, tailDelay},
false);
printGeneratedCode("JALR fallback should not expose epilogue tail-jump labels", generated);
t.IsTrue(generated.find("case 0x2014u: goto label_2014;") == std::string::npos,
"jalr fallback should not dispatch directly to epilogue tail-jump block");
t.IsTrue(generated.find("case 0x2018u: goto label_2018;") == std::string::npos,
"jalr fallback should not dispatch directly to tail-jump delay slot");
});
tc.Run("resolveStubTarget allows leading underscore alias", [](TestCase &t) {
t.Equals(PS2Recompiler::resolveStubTarget("_rand"), StubTarget::Stub,
"_rand should resolve via rand stub alias");
+26
View File
@@ -4,6 +4,7 @@
#include "ps2recomp/types.h"
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace ps2recomp;
@@ -33,6 +34,16 @@ void register_elf_analyzer_tests()
"_printf should be classified as library");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("sceCdRead"),
"sce-prefixed PS2 API should be classified as library");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("SetSyscall"),
"SetSyscall kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("SetTLBEntry"),
"SetTLBEntry kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("InitTLB"),
"InitTLB kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("AddIntcHandler2"),
"AddIntcHandler2 kernel wrapper should be classified as library/runtime");
t.IsTrue(analyzer.isLibrarySymbolNameForHeuristics("SetGsCrt"),
"SetGsCrt kernel wrapper should be classified as library/runtime");
t.IsFalse(analyzer.isLibrarySymbolNameForHeuristics("bhEne13_Brain"),
"named game function should not be classified as library");
@@ -75,6 +86,21 @@ void register_elf_analyzer_tests()
t.IsFalse(ElfAnalyzer::isSystemSymbolNameForHeuristics("sub_00100C00"),
"unreliable names should not be considered system by this classifier"); });
tc.Run("system skip keeps forced entry names recompiled", [](TestCase &t)
{
std::unordered_set<std::string> forcedNames{"_start", "_init"};
t.IsFalse(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("_start", forcedNames),
"forced entry name _start should not be skipped");
t.IsFalse(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("_init", forcedNames),
"forced entry name _init should not be skipped");
t.IsTrue(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("__main", forcedNames),
"system symbol not marked as forced should still be skipped");
t.IsTrue(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("__divdi3", {}),
"compiler helper __divdi3 should be skippable as system/runtime");
t.IsFalse(ElfAnalyzer::shouldSkipSystemSymbolForHeuristics("ps2___divdi3", {}),
"generated ps2_ wrapper names should not be treated as system"); });
tc.Run("entry-point mapping handles exact inside and fallback", [](TestCase &t)
{
Function f1;
+14
View File
@@ -4,7 +4,14 @@ void register_code_generator_tests();
void register_r5900_decoder_tests();
void register_elf_analyzer_tests();
void register_ps2_runtime_io_tests();
void register_ps2_runtime_kernel_tests();
void register_ps2_runtime_interrupt_tests();
void register_ps2_memory_tests();
void register_ps2_gs_tests();
void register_ps2_sif_rpc_tests();
void register_ps2_sif_dma_tests();
void register_ps2_recompiler_tests();
void register_ps2_runtime_expansion_tests();
int main()
{
@@ -12,6 +19,13 @@ int main()
register_r5900_decoder_tests();
register_elf_analyzer_tests();
register_ps2_runtime_io_tests();
register_ps2_runtime_kernel_tests();
register_ps2_runtime_interrupt_tests();
register_ps2_memory_tests();
register_ps2_gs_tests();
register_ps2_sif_rpc_tests();
register_ps2_sif_dma_tests();
register_ps2_recompiler_tests();
register_ps2_runtime_expansion_tests();
return MiniTest::Run();
}
+452
View File
@@ -0,0 +1,452 @@
#include "MiniTest.h"
#include "ps2_memory.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "ps2_gs_gpu.h"
#include <cstdint>
#include <cstring>
#include <vector>
using namespace ps2_syscalls;
namespace
{
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
uint32_t getRegU32Test(const R5900Context &ctx, int reg)
{
return ::getRegU32(&ctx, reg);
}
uint64_t getReturnU64(const R5900Context &ctx)
{
const uint64_t lo = static_cast<uint64_t>(getRegU32Test(ctx, 2));
const uint64_t hi = static_cast<uint64_t>(getRegU32Test(ctx, 3));
return lo | (hi << 32);
}
uint64_t makeGifTag(uint16_t nloop, uint8_t flg, uint8_t nreg, bool eop = true)
{
uint64_t tag = static_cast<uint64_t>(nloop & 0x7FFFu);
if (eop)
tag |= (1ull << 15);
tag |= (static_cast<uint64_t>(flg & 0x3u) << 58);
tag |= (static_cast<uint64_t>(nreg & 0xFu) << 60);
return tag;
}
void appendU64(std::vector<uint8_t> &dst, uint64_t value)
{
const size_t pos = dst.size();
dst.resize(pos + sizeof(uint64_t));
std::memcpy(dst.data() + pos, &value, sizeof(uint64_t));
}
}
void register_ps2_gs_tests()
{
MiniTest::Case("PS2GS", [](TestCase &tc)
{
tc.Run("GS CSR/IMR support coherent 64-bit and 32-bit access", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kGsCsr = 0x12001000u;
constexpr uint32_t kGsImr = 0x12001010u;
const uint64_t csrPattern = 0xA1B2C3D4E5F60718ull;
mem.write64(kGsCsr, csrPattern);
t.Equals(mem.read64(kGsCsr), csrPattern, "64-bit CSR read should match prior 64-bit write");
t.Equals(mem.read32(kGsCsr), static_cast<uint32_t>(csrPattern & 0xFFFFFFFFull), "CSR low dword read should match");
t.Equals(mem.read32(kGsCsr + 4u), static_cast<uint32_t>(csrPattern >> 32), "CSR high dword read should match");
mem.write32(kGsCsr, 0x11223344u);
t.Equals(mem.read64(kGsCsr), 0xA1B2C3D411223344ull, "32-bit low write should preserve CSR high dword");
mem.write32(kGsCsr + 4u, 0x55667788u);
t.Equals(mem.read64(kGsCsr), 0x5566778811223344ull, "32-bit high write should preserve CSR low dword");
const uint64_t imrPattern = 0x0123456789ABCDEFull;
mem.write64(kGsImr, imrPattern);
t.Equals(mem.read64(kGsImr), imrPattern, "IMR 64-bit read should match prior write");
t.Equals(mem.read32(kGsImr), 0x89ABCDEFu, "IMR low dword should match");
t.Equals(mem.read32(kGsImr + 4u), 0x01234567u, "IMR high dword should match");
});
tc.Run("unknown GS privileged offsets are no-op and read as zero", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kKnownBusdir = 0x12001040u;
constexpr uint32_t kUnknown = 0x12001008u; // inside GS priv range, but not mapped by gsRegPtr.
mem.write64(kKnownBusdir, 0xCAFEBABE12345678ull);
const uint64_t before = mem.read64(kKnownBusdir);
mem.write32(kUnknown, 0xDEADBEEFu);
t.Equals(mem.read32(kUnknown), 0u, "unknown GS offset should read as zero");
t.Equals(mem.read64(kKnownBusdir), before, "unknown GS writes should not corrupt mapped GS registers");
});
tc.Run("GS writeIORegister increments GS write counter", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
constexpr uint32_t kGsPmode = 0x12000000u;
constexpr uint32_t kGsImr = 0x12001010u;
const uint64_t countBefore = mem.gsWriteCount();
t.IsTrue(mem.writeIORegister(kGsPmode, 0x11u), "writeIORegister PMODE should succeed");
t.IsTrue(mem.writeIORegister(kGsImr, 0x22u), "writeIORegister IMR should succeed");
t.Equals(mem.gsWriteCount(), countBefore + 2ull, "GS IO writes should increment GS write counter");
t.Equals(mem.readIORegister(kGsPmode), 0x11u, "writeIORegister PMODE value should be readable");
t.Equals(mem.readIORegister(kGsImr), 0x22u, "writeIORegister IMR value should be readable");
});
tc.Run("GsPutIMR and GsGetIMR roundtrip old and new values", [](TestCase &t)
{
PS2Runtime runtime;
t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed");
runtime.memory().gs().imr = 0xAAAABBBBCCCCDDDDull;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
setRegU32(ctx, 4, 0x11112222u); // new IMR low
setRegU32(ctx, 5, 0x33334444u); // new IMR high
GsPutIMR(rdram.data(), &ctx, &runtime);
const uint64_t oldImr = getReturnU64(ctx);
t.Equals(oldImr, 0xAAAABBBBCCCCDDDDull, "GsPutIMR should return previous IMR");
t.Equals(runtime.memory().gs().imr, 0x3333444411112222ull, "GsPutIMR should update GS IMR");
std::memset(&ctx, 0, sizeof(ctx));
GsGetIMR(rdram.data(), &ctx, &runtime);
const uint64_t currentImr = getReturnU64(ctx);
t.Equals(currentImr, 0x3333444411112222ull, "GsGetIMR should return current GS IMR");
});
tc.Run("GIF PACKED A+D writes DISPFB1 and DISPLAY1 privileged registers", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GSRegisters regs{};
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), &regs);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(2u, GIF_FMT_PACKED, 1u, true));
appendU64(packet, 0x0Eull); // REGS[0] = A+D
const uint64_t dispfb1 = 0x0123456789ABCDEFull;
const uint64_t display1 = 0x1111222233334444ull;
appendU64(packet, dispfb1);
appendU64(packet, 0x59ull); // DISPFB1
appendU64(packet, display1);
appendU64(packet, 0x5Aull); // DISPLAY1
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
t.Equals(regs.dispfb1, dispfb1, "A+D should write GS DISPFB1");
t.Equals(regs.display1, display1, "A+D should write GS DISPLAY1");
});
tc.Run("GIF REGLIST with odd register count consumes 128-bit padding before next tag", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) |
(static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(0u) << 24) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(0u) << 56);
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32));
gs.writeRegister(GS_REG_TRXDIR, 0ull);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(1u, GIF_FMT_REGLIST, 1u, false));
appendU64(packet, 0x0ull); // REGS[0] = PRIM
appendU64(packet, 0x0000000000000006ull); // PRIM write
appendU64(packet, 0xDEADBEEFCAFEBABEull); // required REGLIST pad qword
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
const uint8_t payload[16] = {
0x31u, 0x32u, 0x33u, 0x34u,
0x35u, 0x36u, 0x37u, 0x38u,
0x39u, 0x3Au, 0x3Bu, 0x3Cu,
0x3Du, 0x3Eu, 0x3Fu, 0x40u,
};
packet.insert(packet.end(), payload, payload + sizeof(payload));
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
bool imageOk = true;
for (uint32_t i = 0; i < 16u; ++i)
{
if (vram[i] != payload[i])
{
imageOk = false;
break;
}
}
t.IsTrue(imageOk, "odd REGLIST payload should not corrupt alignment of the following IMAGE tag");
});
tc.Run("GIF REGLIST NREG=0 is treated as sixteen descriptors", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) |
(static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(0u) << 24) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(0u) << 56);
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32));
gs.writeRegister(GS_REG_TRXDIR, 0ull);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(1u, GIF_FMT_REGLIST, 0u, false)); // NREG=0 -> 16 regs
appendU64(packet, 0ull); // 16x PRIM descriptors
for (uint32_t i = 0; i < 16u; ++i)
{
appendU64(packet, static_cast<uint64_t>(i));
}
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
const uint8_t payload[16] = {
0x51u, 0x52u, 0x53u, 0x54u,
0x55u, 0x56u, 0x57u, 0x58u,
0x59u, 0x5Au, 0x5Bu, 0x5Cu,
0x5Du, 0x5Eu, 0x5Fu, 0x60u,
};
packet.insert(packet.end(), payload, payload + sizeof(payload));
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
bool imageOk = true;
for (uint32_t i = 0; i < 16u; ++i)
{
if (vram[i] != payload[i])
{
imageOk = false;
break;
}
}
t.IsTrue(imageOk, "NREG=0 REGLIST should consume 16 data words and keep following tag aligned");
});
tc.Run("GS SIGNAL and FINISH set CSR bits that clear by CSR write-one acknowledge", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
GS gs;
gs.init(mem.getGSVRAM(), static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &mem.gs());
const uint64_t signalValue = (0xFFFFFFFFull << 32) | 0x11223344ull;
gs.writeRegister(GS_REG_SIGNAL, signalValue);
gs.writeRegister(GS_REG_FINISH, 0u);
t.IsTrue((mem.gs().csr & 0x1ull) != 0ull, "SIGNAL should raise CSR.SIGNAL");
t.IsTrue((mem.gs().csr & 0x2ull) != 0ull, "FINISH should raise CSR.FINISH");
t.Equals(static_cast<uint32_t>(mem.gs().siglblid & 0xFFFFFFFFull), 0x11223344u, "SIGNAL should update SIGLBLID low dword");
mem.write64(0x12001000u, 0x1ull);
t.IsTrue((mem.gs().csr & 0x1ull) == 0ull, "writing CSR bit0 should acknowledge SIGNAL");
t.IsTrue((mem.gs().csr & 0x2ull) != 0ull, "acknowledging SIGNAL should not clear FINISH");
mem.write32(0x12001000u, 0x2u);
t.IsTrue((mem.gs().csr & 0x2ull) == 0ull, "writing CSR bit1 should acknowledge FINISH");
});
tc.Run("GIF IMAGE packet writes host-to-local data into GS VRAM", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
// Setup for host->local transfer to DBP=0, DBW=1, PSMCT32, rect 2x2.
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) | // SBP
(static_cast<uint64_t>(1u) << 16) | // SBW
(static_cast<uint64_t>(0u) << 24) | // SPSM
(static_cast<uint64_t>(0u) << 32) | // DBP
(static_cast<uint64_t>(1u) << 48) | // DBW
(static_cast<uint64_t>(0u) << 56); // DPSM (CT32)
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (2ull << 0) | (2ull << 32));
gs.writeRegister(GS_REG_TRXDIR, 0ull);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
const uint8_t payload[16] = {
0x10u, 0x11u, 0x12u, 0x13u,
0x20u, 0x21u, 0x22u, 0x23u,
0x30u, 0x31u, 0x32u, 0x33u,
0x40u, 0x41u, 0x42u, 0x43u,
};
packet.insert(packet.end(), payload, payload + sizeof(payload));
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
bool same = true;
for (size_t i = 0; i < 8u; ++i)
{
if (vram[i] != payload[i] || vram[256u + i] != payload[8u + i])
{
same = false;
break;
}
}
t.IsTrue(same, "GIF IMAGE transfer should write payload bytes into GS VRAM");
});
tc.Run("GS local-to-host transfer supports partial incremental reads", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
for (uint32_t i = 0; i < 16u; ++i)
{
vram[i] = static_cast<uint8_t>(0xA0u + i);
}
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) | // SBP
(static_cast<uint64_t>(1u) << 16) | // SBW
(static_cast<uint64_t>(0u) << 24) | // SPSM (CT32)
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(0u) << 56);
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); // 4 pixels, 1 row -> 16 bytes
gs.writeRegister(GS_REG_TRXDIR, 1ull);
uint8_t bufA[8] = {};
uint8_t bufB[16] = {};
const uint32_t nA = gs.consumeLocalToHostBytes(bufA, 6u);
const uint32_t nB = gs.consumeLocalToHostBytes(bufB, 16u);
const uint32_t nC = gs.consumeLocalToHostBytes(bufB, 4u);
t.Equals(nA, 6u, "first partial read should consume requested bytes");
t.Equals(nB, 10u, "second read should consume the remaining bytes");
t.Equals(nC, 0u, "buffer should be empty after all bytes are consumed");
bool bytesOk = true;
for (uint32_t i = 0; i < 6u; ++i)
{
if (bufA[i] != static_cast<uint8_t>(0xA0u + i))
bytesOk = false;
}
for (uint32_t i = 0; i < 10u; ++i)
{
if (bufB[i] != static_cast<uint8_t>(0xA6u + i))
bytesOk = false;
}
t.IsTrue(bytesOk, "partial reads should return local->host data in-order");
});
tc.Run("GS CT24 host-local-host transfer preserves 24-bit RGB payload", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) | // SBP
(static_cast<uint64_t>(1u) << 16) | // SBW
(static_cast<uint64_t>(1u) << 24) | // SPSM CT24
(static_cast<uint64_t>(0u) << 32) | // DBP
(static_cast<uint64_t>(1u) << 48) | // DBW
(static_cast<uint64_t>(1u) << 56); // DPSM CT24
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (2ull << 0) | (1ull << 32)); // 2 pixels
gs.writeRegister(GS_REG_TRXDIR, 0ull);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
const uint8_t rgbData[16] = {
0x11u, 0x22u, 0x33u,
0x44u, 0x55u, 0x66u,
0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u
};
packet.insert(packet.end(), rgbData, rgbData + sizeof(rgbData));
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
// Read back from local to host in CT24.
gs.writeRegister(GS_REG_TRXDIR, 1ull);
uint8_t out[16] = {};
const uint32_t outBytes = gs.consumeLocalToHostBytes(out, sizeof(out));
t.Equals(outBytes, 6u, "CT24 local->host read should output 3 bytes per pixel");
t.Equals(out[0], static_cast<uint8_t>(0x11u), "pixel0 R should roundtrip");
t.Equals(out[1], static_cast<uint8_t>(0x22u), "pixel0 G should roundtrip");
t.Equals(out[2], static_cast<uint8_t>(0x33u), "pixel0 B should roundtrip");
t.Equals(out[3], static_cast<uint8_t>(0x44u), "pixel1 R should roundtrip");
t.Equals(out[4], static_cast<uint8_t>(0x55u), "pixel1 G should roundtrip");
t.Equals(out[5], static_cast<uint8_t>(0x66u), "pixel1 B should roundtrip");
});
tc.Run("GS PSMT4 host-local-host keeps nibble packing stable", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t bitblt =
(static_cast<uint64_t>(0u) << 0) | // SBP
(static_cast<uint64_t>(1u) << 16) | // SBW
(static_cast<uint64_t>(20u) << 24) | // SPSM PSMT4
(static_cast<uint64_t>(0u) << 32) | // DBP
(static_cast<uint64_t>(1u) << 48) | // DBW
(static_cast<uint64_t>(20u) << 56); // DPSM PSMT4
gs.writeRegister(GS_REG_BITBLTBUF, bitblt);
gs.writeRegister(GS_REG_TRXPOS, 0ull);
gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); // 4 texels => 2 bytes
gs.writeRegister(GS_REG_TRXDIR, 0ull);
std::vector<uint8_t> packet;
appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true));
appendU64(packet, 0ull);
const uint8_t nibbleData[16] = {0x21u, 0x43u};
packet.insert(packet.end(), nibbleData, nibbleData + sizeof(nibbleData));
gs.processGIFPacket(packet.data(), static_cast<uint32_t>(packet.size()));
gs.writeRegister(GS_REG_TRXDIR, 1ull);
uint8_t out[8] = {};
const uint32_t outBytes = gs.consumeLocalToHostBytes(out, sizeof(out));
t.Equals(outBytes, 2u, "PSMT4 local->host should return packed nibble bytes");
t.Equals(out[0], static_cast<uint8_t>(0x21u), "packed nibble byte 0 should roundtrip");
t.Equals(out[1], static_cast<uint8_t>(0x43u), "packed nibble byte 1 should roundtrip");
});
});
}
File diff suppressed because it is too large Load Diff
+220
View File
@@ -1,8 +1,14 @@
#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 <elfio/elfio.hpp>
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <unordered_map>
#include <vector>
@@ -29,6 +35,18 @@ static Instruction makeAbsJump(uint32_t address, uint32_t target, uint32_t opcod
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{};
@@ -41,6 +59,65 @@ static Function makeFunction(const std::string &name, uint32_t start, uint32_t e
return fn;
}
static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesystem::path &elfPath)
{
ELFIO::elfio writer;
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
writer.set_type(ELFIO::ET_EXEC);
writer.set_machine(ELFIO::EM_MIPS);
writer.set_entry(0x00100000u);
ELFIO::section *text = writer.sections.add(".text");
text->set_type(ELFIO::SHT_PROGBITS);
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
text->set_addr_align(4);
text->set_address(0x00100000u);
const char textBytes[] = {0x08, 0x00, static_cast<char>(0xE0), 0x03, 0x00, 0x00, 0x00, 0x00};
text->set_data(textBytes, sizeof(textBytes));
ELFIO::section *data = writer.sections.add(".data");
data->set_type(ELFIO::SHT_PROGBITS);
data->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE);
data->set_addr_align(4);
data->set_address(0x00200000u);
const char dataBytes[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, static_cast<char>(0x88)};
data->set_data(dataBytes, sizeof(dataBytes));
ELFIO::section *strtab = writer.sections.add(".strtab");
strtab->set_type(ELFIO::SHT_STRTAB);
strtab->set_addr_align(1);
ELFIO::section *symtab = writer.sections.add(".symtab");
symtab->set_type(ELFIO::SHT_SYMTAB);
symtab->set_info(1);
symtab->set_link(strtab->get_index());
symtab->set_addr_align(4);
symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB));
ELFIO::symbol_section_accessor symbols(writer, symtab);
ELFIO::string_section_accessor strings(strtab);
symbols.add_symbol(strings, "", 0, 0, ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF);
symbols.add_symbol(strings, "code_func", text->get_address(), text->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
symbols.add_symbol(strings, "data_func", data->get_address(), data->get_size(),
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, data->get_index());
ELFIO::segment *textSegment = writer.segments.add();
textSegment->set_type(ELFIO::PT_LOAD);
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
textSegment->set_align(0x1000);
textSegment->add_section_index(text->get_index(), text->get_addr_align());
ELFIO::segment *dataSegment = writer.segments.add();
dataSegment->set_type(ELFIO::PT_LOAD);
dataSegment->set_flags(ELFIO::PF_R | ELFIO::PF_W);
dataSegment->set_align(0x1000);
dataSegment->add_section_index(data->get_index(), data->get_addr_align());
return writer.save(elfPath.string());
}
void register_ps2_recompiler_tests()
{
MiniTest::Case("PS2Recompiler", [](TestCase &tc)
@@ -293,5 +370,148 @@ void register_ps2_recompiler_tests()
const bool hasDataEntry = std::any_of(functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x3004u; });
t.IsFalse(hasDataEntry, "target in data section must not produce entry wrapper");
});
tc.Run("entry starting at jr ra is capped to return thunk", [](TestCase &t) {
std::vector<Section> sections = {
{".text", 0x1000u, 0x2000u, 0u, true, false, false, true, nullptr}
};
std::vector<Function> functions = {
makeFunction("container", 0x1000u, 0x1200u),
makeFunction("caller", 0x1300u, 0x1310u)
};
std::unordered_map<uint32_t, std::vector<Instruction>> decodedFunctions;
decodedFunctions[0x1000u] = {
makeNopLike(0x1000u),
makeNopLike(0x1004u),
makeNopLike(0x1008u),
makeJrRa(0x10A0u),
makeNopLike(0x10A4u),
makeNopLike(0x10A8u),
makeNopLike(0x10ACu)
};
decodedFunctions[0x1300u] = {
makeAbsJump(0x1300u, 0x10A0u, OPCODE_J),
makeNopLike(0x1304u)
};
size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints(
functions, decodedFunctions, sections);
t.Equals(discovered, static_cast<size_t>(1),
"expected one additional entry from cross-function jump");
auto entryIt = std::find_if(functions.begin(), functions.end(),
[](const Function &fn) { return fn.start == 0x10A0u; });
t.IsTrue(entryIt != functions.end(), "entry wrapper at 0x10A0 should exist");
if (entryIt != functions.end())
{
t.Equals(entryIt->end, 0x10A8u,
"jr ra entry should end after delay slot, not at container end");
}
auto decodedEntryIt = decodedFunctions.find(0x10A0u);
t.IsTrue(decodedEntryIt != decodedFunctions.end(),
"decoded entry slice for 0x10A0 should exist");
if (decodedEntryIt != decodedFunctions.end())
{
t.Equals(decodedEntryIt->second.size(), static_cast<size_t>(2),
"jr ra entry slice should contain exactly jr+delay");
if (!decodedEntryIt->second.empty())
{
t.Equals(decodedEntryIt->second.front().address, 0x10A0u,
"entry slice should start at 0x10A0");
}
}
});
tc.Run("config manager parses jump_tables table entries", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path configPath =
std::filesystem::temp_directory_path() / ("ps2recomp-jump-table-" + uniqueSuffix + ".toml");
std::ofstream configFile(configPath);
t.IsTrue(static_cast<bool>(configFile), "temp config file should be writable");
if (!configFile)
{
return;
}
configFile << "[general]\n";
configFile << "input = \"dummy.elf\"\n";
configFile << "output = \"out\"\n\n";
configFile << "[jump_tables]\n";
configFile << "[[jump_tables.table]]\n";
configFile << "address = \"0x200000\"\n";
configFile << "base_register = 9\n";
configFile << "entries = [\n";
configFile << " { index = 0, target = \"0x1620\" },\n";
configFile << " { index = 1, target = \"0x1630\" },\n";
configFile << "]\n";
configFile.close();
ConfigManager manager(configPath.string());
RecompilerConfig config = manager.loadConfig();
t.Equals(config.jumpTables.size(), static_cast<size_t>(1),
"one configured jump table should be loaded");
if (!config.jumpTables.empty())
{
const JumpTable &table = config.jumpTables.front();
t.Equals(table.address, 0x200000u, "table address should parse from hex string");
t.Equals(table.baseRegister, 9u, "base register should parse");
t.Equals(table.entries.size(), static_cast<size_t>(2),
"two jump table entries should parse");
if (table.entries.size() >= 2)
{
t.Equals(table.entries[0].index, 0u, "first entry index should parse");
t.Equals(table.entries[0].target, 0x1620u, "first entry target should parse");
t.Equals(table.entries[1].index, 1u, "second entry index should parse");
t.Equals(table.entries[1].target, 0x1630u, "second entry target should parse");
}
}
std::error_code removeError;
std::filesystem::remove(configPath, removeError);
});
tc.Run("elf parser ignores STT_FUNC symbols in non-executable sections", [](TestCase &t) {
const auto uniqueSuffix = std::to_string(
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
const std::filesystem::path elfPath =
std::filesystem::temp_directory_path() / ("ps2recomp-parser-" + uniqueSuffix + ".elf");
const bool writeOk = writeMinimalMipsElfWithCodeAndDataFunctionSymbols(elfPath);
t.IsTrue(writeOk, "temporary ELF should be generated");
if (!writeOk)
{
return;
}
ElfParser parser(elfPath.string());
const bool parseOk = parser.parse();
t.IsTrue(parseOk, "generated ELF should parse");
if (!parseOk)
{
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
return;
}
const auto functions = parser.extractFunctions();
const bool hasCodeFunction = std::any_of(functions.begin(), functions.end(),
[](const Function &fn)
{ return fn.start == 0x00100000u; });
const bool hasDataFunction = std::any_of(functions.begin(), functions.end(),
[](const Function &fn)
{ return fn.start == 0x00200000u; });
t.IsTrue(hasCodeFunction, "function in executable section should be retained");
t.IsFalse(hasDataFunction, "STT_FUNC symbol in .data must be ignored");
std::error_code removeError;
std::filesystem::remove(elfPath, removeError);
}); });
}
@@ -0,0 +1,631 @@
#include "MiniTest.h"
#include "ps2recomp/code_generator.h"
#include "ps2recomp/instructions.h"
#include "ps2recomp/r5900_decoder.h"
#include "ps2recomp/types.h"
#include "ps2_runtime.h"
#include "ps2_memory.h"
#include "ps2_syscalls.h"
#include "ps2_gs_gpu.h"
#include "ps2_runtime_macros.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <exception>
#include <string>
#include <thread>
#include <vector>
using namespace ps2recomp;
using namespace ps2_syscalls;
namespace
{
constexpr uint32_t COP0_CAUSE_BD = 0x80000000u;
constexpr uint32_t COP0_CAUSE_EXCCODE_MASK = 0x0000007Cu;
constexpr uint32_t COP0_STATUS_EXL = 0x00000002u;
constexpr uint32_t COP0_STATUS_BEV = 0x00400000u;
constexpr uint32_t EXCEPTION_VECTOR_GENERAL = 0x80000080u;
constexpr uint32_t EXCEPTION_VECTOR_BOOT = 0xBFC00200u;
constexpr int KE_OK = 0;
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
int32_t getRegS32(const R5900Context &ctx, int reg)
{
return static_cast<int32_t>(::getRegU32(&ctx, reg));
}
uint32_t makeVifCmd(uint8_t opcode, uint8_t num, uint16_t imm)
{
return (static_cast<uint32_t>(opcode) << 24) |
(static_cast<uint32_t>(num) << 16) |
static_cast<uint32_t>(imm);
}
bool hasSignedRdWrite(const std::string &generated, uint8_t rd)
{
if (rd == 0u)
{
return false;
}
const std::string needle = "SET_GPR_S32(ctx, " + std::to_string(rd) + ",";
return generated.find(needle) != std::string::npos;
}
template <typename Predicate>
bool waitUntil(Predicate pred, std::chrono::milliseconds timeout)
{
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline)
{
if (pred())
{
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return pred();
}
uint32_t frameOffsetBytes(uint32_t x, uint32_t y, uint32_t fbw)
{
const uint32_t stride = fbw * 64u * 4u; // CT32
return y * stride + x * 4u;
}
void testRuntimeWorkerLoop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
if (!ctx || !runtime)
{
return;
}
// Keep touching guest memory so teardown races are easier to catch.
(void)Ps2FastRead64(rdram, static_cast<uint32_t>(0x01FFFFF8u + (ctx->insn_count & 0x7u)));
++ctx->insn_count;
if (runtime->isStopRequested())
{
ctx->pc = 0u;
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void register_ps2_runtime_expansion_tests()
{
MiniTest::Case("PS2RuntimeExpansion", [](TestCase &tc)
{
tc.Run("differential decoder/codegen gpr-write contract for MULT and DIV families", [](TestCase &t)
{
R5900Decoder decoder;
CodeGenerator generator({}, {});
const struct
{
const char *name;
uint32_t raw;
} cases[] = {
{"MULT rd!=0", (OPCODE_SPECIAL << 26) | (4u << 21) | (5u << 16) | (3u << 11) | SPECIAL_MULT},
{"MULT rd==0", (OPCODE_SPECIAL << 26) | (4u << 21) | (5u << 16) | (0u << 11) | SPECIAL_MULT},
{"DIV rd!=0", (OPCODE_SPECIAL << 26) | (6u << 21) | (7u << 16) | (9u << 11) | SPECIAL_DIV},
{"MMI MULT1 rd!=0", (OPCODE_MMI << 26) | (8u << 21) | (9u << 16) | (10u << 11) | MMI_MULT1},
{"MMI DIV1 rd!=0", (OPCODE_MMI << 26) | (8u << 21) | (9u << 16) | (10u << 11) | MMI_DIV1},
};
for (size_t i = 0; i < std::size(cases); ++i)
{
const Instruction inst = decoder.decodeInstruction(0x1000u + static_cast<uint32_t>(i * 4u), cases[i].raw);
const std::string generated = generator.translateInstruction(inst);
const bool emittedRdWrite = hasSignedRdWrite(generated, inst.rd);
t.Equals(emittedRdWrite, inst.modificationInfo.modifiesGPR,
std::string("decoder/codegen mismatch for ") + cases[i].name);
t.IsTrue(inst.modificationInfo.modifiesControl,
std::string("HI/LO control side-effect missing for ") + cases[i].name);
}
});
tc.Run("multiply-add matrix writes rd only when R5900 requires it", [](TestCase &t)
{
R5900Decoder decoder;
CodeGenerator generator({}, {});
const struct
{
const char *name;
uint32_t raw;
bool expectedRdWrite;
} cases[] = {
{"MULTU rd!=0", (OPCODE_SPECIAL << 26) | (2u << 21) | (3u << 16) | (11u << 11) | SPECIAL_MULTU, true},
{"MMI MADD rd!=0", (OPCODE_MMI << 26) | (2u << 21) | (3u << 16) | (12u << 11) | MMI_MADD, true},
{"MMI MADDU rd!=0", (OPCODE_MMI << 26) | (2u << 21) | (3u << 16) | (13u << 11) | MMI_MADDU, true},
{"MMI MADD1 rd!=0", (OPCODE_MMI << 26) | (2u << 21) | (3u << 16) | (14u << 11) | MMI_MADD1, true},
{"MMI MADDU1 rd!=0", (OPCODE_MMI << 26) | (2u << 21) | (3u << 16) | (15u << 11) | MMI_MADDU1, true},
{"MMI DIVU1 rd!=0", (OPCODE_MMI << 26) | (2u << 21) | (3u << 16) | (16u << 11) | MMI_DIVU1, false},
};
for (size_t i = 0; i < std::size(cases); ++i)
{
const Instruction inst = decoder.decodeInstruction(0x2000u + static_cast<uint32_t>(i * 4u), cases[i].raw);
const std::string generated = generator.translateInstruction(inst);
const bool emittedRdWrite = hasSignedRdWrite(generated, inst.rd);
t.Equals(inst.modificationInfo.modifiesGPR, cases[i].expectedRdWrite,
std::string("decoder rd-write metadata mismatch for ") + cases[i].name);
t.Equals(emittedRdWrite, cases[i].expectedRdWrite,
std::string("codegen rd-write mismatch for ") + cases[i].name);
}
});
tc.Run("SignalException marks EPC and BD for delay-slot exceptions", [](TestCase &t)
{
PS2Runtime runtime;
R5900Context ctx{};
ctx.pc = 0x2000u;
ctx.branch_pc = 0x1FFCu;
ctx.in_delay_slot = true;
ctx.cop0_status = 0u;
ctx.cop0_cause = 0u;
runtime.SignalException(&ctx, EXCEPTION_ADDRESS_ERROR_LOAD);
t.Equals(ctx.cop0_epc, 0x1FFCu, "delay-slot exception should capture branch_pc in EPC");
t.IsTrue((ctx.cop0_cause & COP0_CAUSE_BD) != 0u, "delay-slot exception should set CAUSE.BD");
t.Equals(ctx.cop0_cause & COP0_CAUSE_EXCCODE_MASK,
(static_cast<uint32_t>(EXCEPTION_ADDRESS_ERROR_LOAD) << 2) & COP0_CAUSE_EXCCODE_MASK,
"CAUSE.EXCCODE should match exception");
t.IsTrue((ctx.cop0_status & COP0_STATUS_EXL) != 0u, "exception should set STATUS.EXL");
t.Equals(ctx.pc, EXCEPTION_VECTOR_GENERAL, "exception should jump to general vector when BEV=0");
t.IsFalse(ctx.in_delay_slot, "exception delivery should clear delay-slot state");
});
tc.Run("SignalException uses current pc without BD and honors BEV vector", [](TestCase &t)
{
PS2Runtime runtime;
R5900Context ctx{};
ctx.pc = 0x3000u;
ctx.in_delay_slot = false;
ctx.cop0_status = COP0_STATUS_BEV;
ctx.cop0_cause = COP0_CAUSE_BD;
runtime.SignalException(&ctx, EXCEPTION_ADDRESS_ERROR_STORE);
t.Equals(ctx.cop0_epc, 0x3000u, "non-delay exception should capture current pc in EPC");
t.IsTrue((ctx.cop0_cause & COP0_CAUSE_BD) == 0u, "non-delay exception should clear CAUSE.BD");
t.Equals(ctx.pc, EXCEPTION_VECTOR_BOOT, "BEV=1 should route exception to boot vector");
});
tc.Run("handleSyscall rejects invocation in delay slot", [](TestCase &t)
{
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
ctx.in_delay_slot = true;
bool threw = false;
try
{
runtime.handleSyscall(rdram.data(), &ctx, 0x3Cu);
}
catch (const std::runtime_error &)
{
threw = true;
}
t.IsTrue(threw, "syscall from delay slot should throw to preserve block atomicity");
});
tc.Run("VIF MSCAL and MSCNT toggle DBF and keep TOPS/ITOPS coherent", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed");
mem.vif1_regs.base = 4u;
mem.vif1_regs.ofst = 2u;
mem.vif1_regs.itop = 0x21u;
mem.vif1_regs.stat &= ~(1u << 7); // DBF = 0
uint32_t callbackPc = 0xFFFFFFFFu;
uint32_t callbackItop = 0xFFFFFFFFu;
uint32_t callbackCount = 0u;
mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t itop)
{
callbackPc = startPC;
callbackItop = itop;
callbackCount++;
});
const uint32_t mscal = makeVifCmd(0x14u, 0u, 3u); // start PC = 3 * 8
mem.processVIF1Data(reinterpret_cast<const uint8_t *>(&mscal), sizeof(mscal));
t.Equals(callbackCount, 1u, "MSCAL should invoke VU1 callback exactly once");
t.Equals(callbackPc, 24u, "MSCAL should pass startPC=imm*8");
t.Equals(callbackItop, 0x21u, "MSCAL callback should receive current ITOP");
t.Equals(mem.vif1_regs.itops, 0x21u, "MSCAL should latch ITOPS from ITOP");
t.IsTrue((mem.vif1_regs.stat & (1u << 7)) != 0u, "MSCAL should toggle DBF on");
t.Equals(mem.vif1_regs.tops, 6u, "DBF=1 should make TOPS=BASE+OFST");
const uint32_t mscnt = makeVifCmd(0x17u, 0u, 0u);
mem.processVIF1Data(reinterpret_cast<const uint8_t *>(&mscnt), sizeof(mscnt));
t.Equals(callbackCount, 1u, "MSCNT should not invoke MSCAL callback");
t.IsTrue((mem.vif1_regs.stat & (1u << 7)) == 0u, "MSCNT should toggle DBF back off");
t.Equals(mem.vif1_regs.tops, 4u, "DBF=0 should make TOPS=BASE");
t.Equals(mem.vif1_regs.itops, 0x21u, "MSCNT should refresh ITOPS from ITOP");
});
tc.Run("GS sprite draw applies XYOFFSET and fully-outside scissor should not render", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t frame1 =
(0ull << 0) | // FBP
(1ull << 16) | // FBW
(0ull << 24) | // PSM CT32
(0ull << 32); // FBMSK
gs.writeRegister(GS_REG_FRAME_1, frame1);
// XYOFFSET=1,1 pixels (16.4 fixed point).
const uint64_t xyoffset = (16ull) | (16ull << 32);
gs.writeRegister(GS_REG_XYOFFSET_1, xyoffset);
// Scissor initially includes pixel (1,1).
const uint64_t scissorInside = (0ull) | (3ull << 16) | (0ull << 32) | (3ull << 48);
gs.writeRegister(GS_REG_SCISSOR_1, scissorInside);
gs.writeRegister(GS_REG_PRIM, static_cast<uint64_t>(GS_PRIM_SPRITE));
gs.writeRegister(GS_REG_RGBAQ, 0xFF3214C8ull); // RGBA=(200,20,50,255)
// With XYOFFSET=(1,1), vertex at (2,2) draws to pixel (1,1).
const uint64_t xyz = (32ull) | (32ull << 16) | (0ull << 32);
gs.writeRegister(GS_REG_XYZ2, xyz);
gs.writeRegister(GS_REG_XYZ2, xyz);
const uint32_t insideOff = frameOffsetBytes(1u, 1u, 1u);
t.Equals(vram[insideOff + 0u], static_cast<uint8_t>(200u), "inside draw should write R");
t.Equals(vram[insideOff + 1u], static_cast<uint8_t>(20u), "inside draw should write G");
t.Equals(vram[insideOff + 2u], static_cast<uint8_t>(50u), "inside draw should write B");
t.Equals(vram[insideOff + 3u], static_cast<uint8_t>(255u), "inside draw should write A");
std::memset(vram.data(), 0, 1024u);
// Move scissor so target pixel is fully outside.
const uint64_t scissorOutside = (3ull) | (4ull << 16) | (3ull << 32) | (4ull << 48);
gs.writeRegister(GS_REG_SCISSOR_1, scissorOutside);
gs.writeRegister(GS_REG_XYZ2, xyz);
gs.writeRegister(GS_REG_XYZ2, xyz);
bool anyWrite = false;
for (size_t i = 0; i < 1024u; ++i)
{
if (vram[i] != 0u)
{
anyWrite = true;
break;
}
}
t.IsFalse(anyWrite, "fully-outside sprite should not render any pixel");
});
tc.Run("GS alpha blend uses ALPHA register FIX factor", [](TestCase &t)
{
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
const uint64_t frame1 =
(0ull << 0) | // FBP
(1ull << 16) | // FBW
(0ull << 24) | // PSM CT32
(0ull << 32); // FBMSK
gs.writeRegister(GS_REG_FRAME_1, frame1);
gs.writeRegister(GS_REG_SCISSOR_1, (0ull) | (4ull << 16) | (0ull << 32) | (4ull << 48));
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
const uint32_t pxOff = frameOffsetBytes(1u, 1u, 1u);
vram[pxOff + 0u] = 40u;
vram[pxOff + 1u] = 40u;
vram[pxOff + 2u] = 40u;
vram[pxOff + 3u] = 255u;
// ABE on sprite prim.
gs.writeRegister(GS_REG_PRIM, static_cast<uint64_t>(GS_PRIM_SPRITE) | (1ull << 6));
// ALPHA: (A-B)*FIX/128 + D
// A=Cs(0), B=Cd(1), C=FIX(2), D=Cd(1), FIX=64.
const uint64_t alpha = (0ull << 0) | (1ull << 2) | (2ull << 4) | (1ull << 6) | (64ull << 32);
gs.writeRegister(GS_REG_ALPHA_1, alpha);
gs.writeRegister(GS_REG_RGBAQ, 0xFFC8C8C8ull); // src RGB = 200
const uint64_t xyz = (16ull) | (16ull << 16) | (0ull << 32); // pixel (1,1)
gs.writeRegister(GS_REG_XYZ2, xyz);
gs.writeRegister(GS_REG_XYZ2, xyz);
// ((200 - 40) * 64 >> 7) + 40 = 120
t.Equals(vram[pxOff + 0u], static_cast<uint8_t>(120u), "alpha blend should update R with FIX factor");
t.Equals(vram[pxOff + 1u], static_cast<uint8_t>(120u), "alpha blend should update G with FIX factor");
t.Equals(vram[pxOff + 2u], static_cast<uint8_t>(120u), "alpha blend should update B with FIX factor");
});
tc.Run("notifyRuntimeStop joins guest worker threads before teardown", [](TestCase &t)
{
notifyRuntimeStop();
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
constexpr uint32_t kEntry = 0x250000u;
constexpr uint32_t kThreadParamAddr = 0x2600u;
const uint32_t threadParam[7] = {
0u, // attr
kEntry, // entry
0x00100000u, // stack
0x00000400u, // stack size
0x00110000u, // gp
8u, // priority
0u // option
};
runtime.registerFunction(kEntry, &testRuntimeWorkerLoop);
std::memcpy(rdram.data() + kThreadParamAddr, threadParam, sizeof(threadParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kThreadParamAddr);
CreateThread(rdram.data(), &createCtx, &runtime);
const int32_t tid = getRegS32(createCtx, 2);
t.IsTrue(tid > 0, "CreateThread should succeed for teardown-join test");
R5900Context startCtx{};
setRegU32(startCtx, 4, static_cast<uint32_t>(tid));
setRegU32(startCtx, 5, 0u);
StartThread(rdram.data(), &startCtx, &runtime);
t.Equals(getRegS32(startCtx, 2), KE_OK, "StartThread should launch worker");
const bool started = waitUntil([&]()
{
return g_activeThreads.load(std::memory_order_relaxed) > 0;
}, std::chrono::milliseconds(500));
t.IsTrue(started, "worker thread should become active");
runtime.requestStop();
const bool drained = waitUntil([&]()
{
return g_activeThreads.load(std::memory_order_relaxed) == 0;
}, std::chrono::milliseconds(2000));
t.IsTrue(drained, "requestStop should drain all guest worker threads");
notifyRuntimeStop();
});
tc.Run("Semaphore poll/signal remains stable under host-thread contention", [](TestCase &t)
{
notifyRuntimeStop();
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
constexpr uint32_t kParamAddr = 0x2000u;
const uint32_t semaParam[6] = {
0u, // count
1u, // max_count
1u, // init_count
0u, // wait_threads
0u, // attr
0u // option
};
std::memcpy(rdram.data() + kParamAddr, semaParam, sizeof(semaParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kParamAddr);
CreateSema(rdram.data(), &createCtx, &runtime);
const int32_t sid = getRegS32(createCtx, 2);
t.IsTrue(sid > 0, "CreateSema should return a valid sid");
std::atomic<int32_t> pollOkCount{0};
std::atomic<int32_t> signalOkCount{0};
std::atomic<bool> pollerThrew{false};
std::atomic<bool> signalerThrew{false};
std::thread poller([&]()
{
try
{
for (int i = 0; i < 64; ++i)
{
R5900Context pollCtx{};
setRegU32(pollCtx, 4, static_cast<uint32_t>(sid));
PollSema(rdram.data(), &pollCtx, &runtime);
if (getRegS32(pollCtx, 2) == KE_OK)
{
pollOkCount.fetch_add(1, std::memory_order_relaxed);
}
}
}
catch (...)
{
pollerThrew.store(true, std::memory_order_release);
}
});
std::thread signaler([&]()
{
try
{
for (int i = 0; i < 64; ++i)
{
R5900Context signalCtx{};
setRegU32(signalCtx, 4, static_cast<uint32_t>(sid));
SignalSema(rdram.data(), &signalCtx, &runtime);
if (getRegS32(signalCtx, 2) == KE_OK)
{
signalOkCount.fetch_add(1, std::memory_order_relaxed);
}
}
}
catch (...)
{
signalerThrew.store(true, std::memory_order_release);
}
});
if (poller.joinable())
{
poller.join();
}
if (signaler.joinable())
{
signaler.join();
}
t.IsFalse(pollerThrew.load(std::memory_order_acquire),
"PollSema worker thread should not throw");
t.IsFalse(signalerThrew.load(std::memory_order_acquire),
"SignalSema worker thread should not throw");
t.IsTrue(pollOkCount.load(std::memory_order_relaxed) > 0,
"contended PollSema should observe at least one successful acquire");
t.IsTrue(signalOkCount.load(std::memory_order_relaxed) > 0,
"contended SignalSema should observe successful releases");
constexpr uint32_t kStatusAddr = 0x2100u;
R5900Context referCtx{};
setRegU32(referCtx, 4, static_cast<uint32_t>(sid));
setRegU32(referCtx, 5, kStatusAddr);
ReferSemaStatus(rdram.data(), &referCtx, &runtime);
t.Equals(getRegS32(referCtx, 2), KE_OK, "ReferSemaStatus should succeed after contention");
int32_t finalCount = 0;
std::memcpy(&finalCount, rdram.data() + kStatusAddr + 0u, sizeof(finalCount));
t.IsTrue(finalCount >= 0 && finalCount <= 1, "semaphore count should remain within [0, max_count]");
runtime.requestStop();
notifyRuntimeStop();
});
tc.Run("WaitEventFlag AND-mode is stable under concurrent setters", [](TestCase &t)
{
notifyRuntimeStop();
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
constexpr uint32_t kEventParamAddr = 0x2400u;
constexpr uint32_t kResBitsAddr = 0x2410u;
const uint32_t eventParam[3] = {0u, 0u, 0u};
std::memcpy(rdram.data() + kEventParamAddr, eventParam, sizeof(eventParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kEventParamAddr);
CreateEventFlag(rdram.data(), &createCtx, &runtime);
const int32_t eid = getRegS32(createCtx, 2);
t.IsTrue(eid > 0, "CreateEventFlag should return a valid id");
std::atomic<bool> waiterDone{false};
std::atomic<int32_t> waiterRet{-9999};
std::atomic<uint32_t> waiterBits{0u};
std::atomic<bool> waiterThrew{false};
std::atomic<bool> setterAThrew{false};
std::atomic<bool> setterBThrew{false};
std::thread waiter([&]()
{
try
{
R5900Context waitCtx{};
setRegU32(waitCtx, 4, static_cast<uint32_t>(eid));
setRegU32(waitCtx, 5, 0x3u); // wait for bit0 and bit1 (AND mode)
setRegU32(waitCtx, 6, 0u); // AND, no clear
setRegU32(waitCtx, 7, kResBitsAddr);
WaitEventFlag(rdram.data(), &waitCtx, &runtime);
waiterRet.store(getRegS32(waitCtx, 2), std::memory_order_relaxed);
uint32_t bits = 0u;
std::memcpy(&bits, rdram.data() + kResBitsAddr, sizeof(bits));
waiterBits.store(bits, std::memory_order_relaxed);
}
catch (...)
{
waiterThrew.store(true, std::memory_order_release);
}
waiterDone.store(true, std::memory_order_release);
});
std::thread setterA([&]()
{
try
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
R5900Context setCtx{};
setRegU32(setCtx, 4, static_cast<uint32_t>(eid));
setRegU32(setCtx, 5, 0x1u);
SetEventFlag(rdram.data(), &setCtx, &runtime);
}
catch (...)
{
setterAThrew.store(true, std::memory_order_release);
}
});
std::thread setterB([&]()
{
try
{
std::this_thread::sleep_for(std::chrono::milliseconds(15));
R5900Context setCtx{};
setRegU32(setCtx, 4, static_cast<uint32_t>(eid));
setRegU32(setCtx, 5, 0x2u);
SetEventFlag(rdram.data(), &setCtx, &runtime);
}
catch (...)
{
setterBThrew.store(true, std::memory_order_release);
}
});
const bool woke = waitUntil([&]()
{
return waiterDone.load(std::memory_order_acquire);
}, std::chrono::milliseconds(500));
if (setterA.joinable())
{
setterA.join();
}
if (setterB.joinable())
{
setterB.join();
}
if (waiter.joinable())
{
waiter.join();
}
t.IsFalse(waiterThrew.load(std::memory_order_acquire),
"WaitEventFlag waiter thread should not throw");
t.IsFalse(setterAThrew.load(std::memory_order_acquire),
"SetEventFlag setterA thread should not throw");
t.IsFalse(setterBThrew.load(std::memory_order_acquire),
"SetEventFlag setterB thread should not throw");
t.IsTrue(woke, "WaitEventFlag AND waiter should wake after both bits are published");
t.Equals(waiterRet.load(std::memory_order_relaxed), KE_OK, "WaitEventFlag should return KE_OK");
t.IsTrue((waiterBits.load(std::memory_order_relaxed) & 0x3u) == 0x3u,
"WaitEventFlag result bits should include both concurrently-set bits");
R5900Context deleteCtx{};
setRegU32(deleteCtx, 4, static_cast<uint32_t>(eid));
DeleteEventFlag(rdram.data(), &deleteCtx, &runtime);
runtime.requestStop();
notifyRuntimeStop();
});
});
}
@@ -0,0 +1,458 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <exception>
#include <thread>
#include <vector>
using namespace ps2_syscalls;
namespace
{
constexpr int KE_OK = 0;
constexpr int KE_EVF_COND = -421;
constexpr uint32_t WEF_OR = 1u;
constexpr uint32_t WEF_CLEAR = 0x10u;
constexpr uint32_t WEF_CLEAR_ALL = 0x20u;
struct Ps2EventFlagInfo
{
uint32_t attr;
uint32_t option;
uint32_t initBits;
uint32_t currBits;
int32_t numThreads;
int32_t reserved1;
int32_t reserved2;
};
static_assert(sizeof(Ps2EventFlagInfo) == 28u, "Unexpected Ps2EventFlagInfo layout.");
struct TestEnv
{
std::vector<uint8_t> rdram;
PS2Runtime runtime;
TestEnv() : rdram(PS2_RAM_SIZE, 0u)
{
}
};
std::atomic<uint32_t> g_vblankStartHits{0u};
std::atomic<uint32_t> g_vblankEndHits{0u};
std::atomic<uint32_t> g_lastIntcArg{0u};
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
int32_t getRegS32(const R5900Context &ctx, int reg)
{
return static_cast<int32_t>(::getRegU32(&ctx, reg));
}
bool callSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
return dispatchNumericSyscall(syscallNumber, rdram, ctx, runtime);
}
void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
uint32_t readGuestU32(const uint8_t *rdram, uint32_t addr)
{
uint32_t value = 0;
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
uint64_t readGuestU64(const uint8_t *rdram, uint32_t addr)
{
uint64_t value = 0;
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
template <typename Predicate>
bool waitUntil(Predicate pred, std::chrono::milliseconds timeout)
{
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline)
{
if (pred())
{
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return pred();
}
void cleanupRuntime(TestEnv &env)
{
env.runtime.requestStop();
notifyRuntimeStop();
}
void testIntcHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)rdram;
(void)runtime;
const uint32_t cause = getRegU32(ctx, 4);
const uint32_t arg = getRegU32(ctx, 5);
g_lastIntcArg.store(arg, std::memory_order_relaxed);
if (cause == 2u)
{
g_vblankStartHits.fetch_add(1u, std::memory_order_relaxed);
}
else if (cause == 3u)
{
g_vblankEndHits.fetch_add(1u, std::memory_order_relaxed);
}
ctx->pc = 0u;
}
}
void register_ps2_runtime_interrupt_tests()
{
MiniTest::Case("PS2RuntimeInterrupt", [](TestCase &tc)
{
tc.Run("SetVSyncFlag updates guest flag and monotonic tick", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kFlagAddr = 0x1000u;
constexpr uint32_t kTickAddr = 0x1010u;
writeGuestU32(env.rdram.data(), kFlagAddr, 0xDEADBEEFu);
writeGuestU32(env.rdram.data(), kTickAddr + 0u, 0xAAAAAAAAu);
writeGuestU32(env.rdram.data(), kTickAddr + 4u, 0xBBBBBBBBu);
R5900Context ctx{};
setRegU32(ctx, 4, kFlagAddr);
setRegU32(ctx, 5, kTickAddr);
t.IsTrue(callSyscall(0x73u, env.rdram.data(), &ctx, &env.runtime), "SetVSyncFlag syscall should dispatch");
t.Equals(getRegS32(ctx, 2), KE_OK, "SetVSyncFlag should return KE_OK");
t.Equals(readGuestU32(env.rdram.data(), kFlagAddr), 0u, "SetVSyncFlag should reset flag to zero");
t.Equals(readGuestU64(env.rdram.data(), kTickAddr), 0ull, "SetVSyncFlag should reset tick counter to zero");
const bool firstTickSeen = waitUntil([&]() {
return readGuestU64(env.rdram.data(), kTickAddr) > 0u;
}, std::chrono::milliseconds(300));
t.IsTrue(firstTickSeen, "VSync worker should update tick value");
const uint64_t firstTick = readGuestU64(env.rdram.data(), kTickAddr);
t.IsTrue(firstTick > 0u, "First observed VSync tick should be positive");
t.Equals(readGuestU32(env.rdram.data(), kFlagAddr), 1u, "VSync worker should set flag to one");
const bool secondTickSeen = waitUntil([&]() {
return readGuestU64(env.rdram.data(), kTickAddr) > firstTick;
}, std::chrono::milliseconds(300));
t.IsTrue(secondTickSeen, "VSync tick should continue to advance");
t.IsTrue(readGuestU64(env.rdram.data(), kTickAddr) > firstTick, "tick should be monotonic");
cleanupRuntime(env);
});
tc.Run("INTC VBLANK handlers respect EnableIntc and DisableIntc masks", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
g_vblankStartHits.store(0u, std::memory_order_relaxed);
g_vblankEndHits.store(0u, std::memory_order_relaxed);
g_lastIntcArg.store(0u, std::memory_order_relaxed);
constexpr uint32_t kFlagAddr = 0x1100u;
constexpr uint32_t kTickAddr = 0x1110u;
constexpr uint32_t kHandlerAddr = 0x00ABC100u;
env.runtime.registerFunction(kHandlerAddr, &testIntcHandler);
R5900Context addStart{};
setRegU32(addStart, 4, 2u); // VBLANK start
setRegU32(addStart, 5, kHandlerAddr);
setRegU32(addStart, 6, 0u);
setRegU32(addStart, 7, 0xCAFE0002u);
setRegU32(addStart, 28, 0x12340000u);
setRegU32(addStart, 29, 0x001FFFE0u);
t.IsTrue(callSyscall(0x10u, env.rdram.data(), &addStart, &env.runtime), "AddIntcHandler syscall should dispatch");
t.IsTrue(getRegS32(addStart, 2) > 0, "AddIntcHandler for cause 2 should return handler id");
R5900Context addEnd{};
setRegU32(addEnd, 4, 3u); // VBLANK end
setRegU32(addEnd, 5, kHandlerAddr);
setRegU32(addEnd, 6, 0u);
setRegU32(addEnd, 7, 0xCAFE0003u);
setRegU32(addEnd, 28, 0x12340000u);
setRegU32(addEnd, 29, 0x001FFFE0u);
t.IsTrue(callSyscall(0x10u, env.rdram.data(), &addEnd, &env.runtime), "AddIntcHandler syscall should dispatch");
t.IsTrue(getRegS32(addEnd, 2) > 0, "AddIntcHandler for cause 3 should return handler id");
R5900Context vsyncCtx{};
setRegU32(vsyncCtx, 4, kFlagAddr);
setRegU32(vsyncCtx, 5, kTickAddr);
t.IsTrue(callSyscall(0x73u, env.rdram.data(), &vsyncCtx, &env.runtime), "SetVSyncFlag syscall should dispatch");
t.Equals(getRegS32(vsyncCtx, 2), KE_OK, "SetVSyncFlag should succeed");
const bool startSeen = waitUntil([&]() {
return g_vblankStartHits.load(std::memory_order_relaxed) > 0u;
}, std::chrono::milliseconds(400));
const bool endSeen = waitUntil([&]() {
return g_vblankEndHits.load(std::memory_order_relaxed) > 0u;
}, std::chrono::milliseconds(400));
t.IsTrue(startSeen, "VBLANK start handler should fire while cause 2 is enabled");
t.IsTrue(endSeen, "VBLANK end handler should fire while cause 3 is enabled");
R5900Context disableStart{};
setRegU32(disableStart, 4, 2u);
t.IsTrue(callSyscall(0x15u, env.rdram.data(), &disableStart, &env.runtime), "DisableIntc syscall should dispatch");
t.Equals(getRegS32(disableStart, 2), KE_OK, "DisableIntc should return KE_OK");
std::this_thread::sleep_for(std::chrono::milliseconds(40));
const uint32_t startAfterDisable = g_vblankStartHits.load(std::memory_order_relaxed);
const uint32_t endAfterDisable = g_vblankEndHits.load(std::memory_order_relaxed);
std::this_thread::sleep_for(std::chrono::milliseconds(80));
const uint32_t startLater = g_vblankStartHits.load(std::memory_order_relaxed);
const uint32_t endLater = g_vblankEndHits.load(std::memory_order_relaxed);
t.Equals(startLater, startAfterDisable, "cause 2 handler count should stop increasing while cause 2 is disabled");
t.IsTrue(endLater > endAfterDisable, "cause 3 handler should keep firing while still enabled");
R5900Context enableStart{};
setRegU32(enableStart, 4, 2u);
t.IsTrue(callSyscall(0x14u, env.rdram.data(), &enableStart, &env.runtime), "EnableIntc syscall should dispatch");
t.Equals(getRegS32(enableStart, 2), KE_OK, "EnableIntc should return KE_OK");
const bool startResumed = waitUntil([&]() {
return g_vblankStartHits.load(std::memory_order_relaxed) > startLater;
}, std::chrono::milliseconds(300));
t.IsTrue(startResumed, "cause 2 handler should resume after re-enable");
const uint32_t lastArg = g_lastIntcArg.load(std::memory_order_relaxed);
t.IsTrue(lastArg == 0xCAFE0002u || lastArg == 0xCAFE0003u,
"handler should receive configured argument value");
cleanupRuntime(env);
});
tc.Run("WaitEventFlag blocks and wakes when SetEventFlag publishes bits", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kParamAddr = 0x1200u;
constexpr uint32_t kResBitsAddr = 0x1300u;
const uint32_t eventParam[3] = {
0u, // attr
0u, // option
0u // init bits
};
std::memcpy(env.rdram.data() + kParamAddr, eventParam, sizeof(eventParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kParamAddr);
CreateEventFlag(env.rdram.data(), &createCtx, &env.runtime);
const int32_t eid = getRegS32(createCtx, 2);
t.IsTrue(eid > 0, "CreateEventFlag should return a valid id");
writeGuestU32(env.rdram.data(), kResBitsAddr, 0u);
std::atomic<bool> waiterDone{false};
std::atomic<bool> waiterThrew{false};
std::atomic<int32_t> waiterRet{0x7FFFFFFF};
std::atomic<uint32_t> waiterResBits{0u};
std::thread waiter([&]()
{
try
{
R5900Context waitCtx{};
setRegU32(waitCtx, 4, static_cast<uint32_t>(eid));
setRegU32(waitCtx, 5, 0x4u); // wait bits
setRegU32(waitCtx, 6, WEF_OR); // OR mode
setRegU32(waitCtx, 7, kResBitsAddr);
WaitEventFlag(env.rdram.data(), &waitCtx, &env.runtime);
waiterRet.store(getRegS32(waitCtx, 2), std::memory_order_relaxed);
waiterResBits.store(readGuestU32(env.rdram.data(), kResBitsAddr), std::memory_order_relaxed);
}
catch (...)
{
waiterThrew.store(true, std::memory_order_release);
}
waiterDone.store(true, std::memory_order_release);
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
t.IsFalse(waiterDone.load(std::memory_order_acquire), "WaitEventFlag should block before matching bits are set");
R5900Context signalCtx{};
setRegU32(signalCtx, 4, static_cast<uint32_t>(eid));
setRegU32(signalCtx, 5, 0x4u);
SetEventFlag(env.rdram.data(), &signalCtx, &env.runtime);
t.Equals(getRegS32(signalCtx, 2), KE_OK, "SetEventFlag should succeed");
const bool woke = waitUntil([&]() {
return waiterDone.load(std::memory_order_acquire);
}, std::chrono::milliseconds(300));
if (!woke)
{
// Force unblock for deterministic test cleanup.
R5900Context deleteCtx{};
setRegU32(deleteCtx, 4, static_cast<uint32_t>(eid));
DeleteEventFlag(env.rdram.data(), &deleteCtx, &env.runtime);
}
if (waiter.joinable())
{
waiter.join();
}
t.IsFalse(waiterThrew.load(std::memory_order_acquire),
"WaitEventFlag waiter thread should not throw");
t.IsTrue(woke, "WaitEventFlag should wake after SetEventFlag publishes matching bits");
t.Equals(waiterRet.load(std::memory_order_relaxed), KE_OK, "waiter should return KE_OK");
t.IsTrue((waiterResBits.load(std::memory_order_relaxed) & 0x4u) != 0u,
"waiter result bits should include published bit");
R5900Context deleteCtx{};
setRegU32(deleteCtx, 4, static_cast<uint32_t>(eid));
DeleteEventFlag(env.rdram.data(), &deleteCtx, &env.runtime);
cleanupRuntime(env);
});
tc.Run("PollEventFlag WEF_CLEAR clears only matched bits", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
constexpr uint32_t kParamAddr = 0x1400u;
constexpr uint32_t kResBitsAddr = 0x1410u;
constexpr uint32_t kStatusAddr = 0x1420u;
const uint32_t eventParam[3] = {
0u, // attr
0u, // option
0x7u // init bits: 0b111
};
std::memcpy(env.rdram.data() + kParamAddr, eventParam, sizeof(eventParam));
R5900Context createCtx{};
setRegU32(createCtx, 4, kParamAddr);
CreateEventFlag(env.rdram.data(), &createCtx, &env.runtime);
const int32_t eid = getRegS32(createCtx, 2);
t.IsTrue(eid > 0, "CreateEventFlag should return a valid id");
R5900Context pollCtx{};
setRegU32(pollCtx, 4, static_cast<uint32_t>(eid));
setRegU32(pollCtx, 5, 0x1u);
setRegU32(pollCtx, 6, WEF_OR | WEF_CLEAR);
setRegU32(pollCtx, 7, kResBitsAddr);
PollEventFlag(env.rdram.data(), &pollCtx, &env.runtime);
t.Equals(getRegS32(pollCtx, 2), KE_OK, "PollEventFlag should succeed when condition is met");
t.Equals(readGuestU32(env.rdram.data(), kResBitsAddr), 0x7u, "PollEventFlag should report bits before clear");
R5900Context referCtx{};
setRegU32(referCtx, 4, static_cast<uint32_t>(eid));
setRegU32(referCtx, 5, kStatusAddr);
ReferEventFlagStatus(env.rdram.data(), &referCtx, &env.runtime);
t.Equals(getRegS32(referCtx, 2), KE_OK, "ReferEventFlagStatus should succeed");
Ps2EventFlagInfo info{};
std::memcpy(&info, env.rdram.data() + kStatusAddr, sizeof(info));
t.Equals(info.currBits, 0x6u, "WEF_CLEAR should clear only requested bits, not all bits");
R5900Context pollMissCtx{};
setRegU32(pollMissCtx, 4, static_cast<uint32_t>(eid));
setRegU32(pollMissCtx, 5, 0x1u);
setRegU32(pollMissCtx, 6, WEF_OR);
setRegU32(pollMissCtx, 7, 0u);
PollEventFlag(env.rdram.data(), &pollMissCtx, &env.runtime);
t.Equals(getRegS32(pollMissCtx, 2), KE_EVF_COND,
"after clearing bit 0, polling for bit 0 should fail condition");
R5900Context deleteCtx{};
setRegU32(deleteCtx, 4, static_cast<uint32_t>(eid));
DeleteEventFlag(env.rdram.data(), &deleteCtx, &env.runtime);
t.Equals(getRegS32(deleteCtx, 2), KE_OK, "DeleteEventFlag should succeed");
cleanupRuntime(env);
});
tc.Run("WaitVSyncTick returns when runtime stop is requested", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
std::atomic<bool> waiterDone{false};
std::atomic<bool> waiterThrew{false};
std::thread waiter([&]()
{
try
{
WaitVSyncTick(env.rdram.data(), &env.runtime);
}
catch (...)
{
waiterThrew.store(true, std::memory_order_release);
}
waiterDone.store(true, std::memory_order_release);
});
std::this_thread::sleep_for(std::chrono::milliseconds(2));
env.runtime.requestStop();
bool wokeOnStop = waitUntil([&]() {
return waiterDone.load(std::memory_order_acquire);
}, std::chrono::milliseconds(80));
if (!wokeOnStop)
{
// Fallback wake-up for deterministic cleanup: one extra tick on fresh runtime.
TestEnv wakeEnv;
R5900Context setCtx{};
constexpr uint32_t kWakeFlagAddr = 0x1500u;
constexpr uint32_t kWakeTickAddr = 0x1510u;
setRegU32(setCtx, 4, kWakeFlagAddr);
setRegU32(setCtx, 5, kWakeTickAddr);
(void)callSyscall(0x73u, wakeEnv.rdram.data(), &setCtx, &wakeEnv.runtime);
(void)waitUntil([&]() {
return readGuestU64(wakeEnv.rdram.data(), kWakeTickAddr) > 0u;
}, std::chrono::milliseconds(300));
wakeEnv.runtime.requestStop();
wokeOnStop = waitUntil([&]() {
return waiterDone.load(std::memory_order_acquire);
}, std::chrono::milliseconds(80));
}
if (waiter.joinable())
{
waiter.join();
}
t.IsFalse(waiterThrew.load(std::memory_order_acquire),
"WaitVSyncTick waiter thread should not throw");
t.IsTrue(wokeOnStop, "WaitVSyncTick waiter should unblock when runtime is stopping");
cleanupRuntime(env);
});
});
}
+22
View File
@@ -1,6 +1,7 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include <filesystem>
#include <fstream>
@@ -274,5 +275,26 @@ void register_ps2_runtime_io_tests()
t.IsFalse(std::filesystem::exists(test.paths.cdRoot / "ISOLATED"),
"mc0: directory should NOT exist under cdRoot");
});
tc.Run("sceIoctl cmd1 updates wait flag state", [](TestCase &t)
{
TestContext test;
constexpr uint32_t statusAddr = GUEST_BUFFER_AREA_START + 0x1800;
const uint32_t busy = 1u;
std::memcpy(test.rdram.data() + statusAddr, &busy, sizeof(busy));
setRegU32(test.ctx, 4, 3u); // fd
setRegU32(test.ctx, 5, 1u); // cmd
setRegU32(test.ctx, 6, statusAddr); // arg
ps2_stubs::sceIoctl(test.rdram.data(), &test.ctx, nullptr);
t.Equals(getRegS32(&test.ctx, 2), 0, "sceIoctl cmd1 should return success");
uint32_t state = 0xFFFFFFFFu;
std::memcpy(&state, test.rdram.data() + statusAddr, sizeof(state));
t.Equals(state, 0u, "sceIoctl cmd1 should clear wait state from busy to ready");
});
});
}
+390
View File
@@ -0,0 +1,390 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include <array>
#include <cstdint>
#include <cstring>
#include <vector>
using namespace ps2_syscalls;
namespace
{
constexpr uint32_t K_PARAM_ADDR = 0x1000u;
constexpr uint32_t K_STATUS_ADDR = 0x1400u;
constexpr int KE_OK = 0;
constexpr int KE_ERROR = -1;
constexpr int KE_ILLEGAL_THID = -406;
constexpr int KE_UNKNOWN_THID = -407;
constexpr int KE_UNKNOWN_SEMID = -408;
constexpr int KE_DORMANT = -413;
constexpr int KE_SEMA_ZERO = -419;
constexpr int KE_SEMA_OVF = -420;
constexpr int THS_DORMANT = 0x10;
struct EeThreadStatus
{
int32_t status;
uint32_t func;
uint32_t stack;
int32_t stack_size;
uint32_t gp_reg;
int32_t initial_priority;
int32_t current_priority;
uint32_t attr;
uint32_t option;
uint32_t waitType;
uint32_t waitId;
uint32_t wakeupCount;
};
struct EeSemaStatus
{
int32_t count;
int32_t max_count;
int32_t init_count;
int32_t wait_threads;
uint32_t attr;
uint32_t option;
};
static_assert(sizeof(EeThreadStatus) == 0x30u, "Unexpected ee_thread_status_t size.");
static_assert(sizeof(EeSemaStatus) == 0x18u, "Unexpected ee_sema_t size.");
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
int32_t getRegS32(const R5900Context &ctx, int reg)
{
return static_cast<int32_t>(::getRegU32(&ctx, reg));
}
void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
void writeGuestWords(uint8_t *rdram, uint32_t addr, const uint32_t *words, size_t count)
{
for (size_t i = 0; i < count; ++i)
{
writeGuestU32(rdram, addr + static_cast<uint32_t>(i * sizeof(uint32_t)), words[i]);
}
}
bool callSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
return dispatchNumericSyscall(syscallNumber, rdram, ctx, runtime);
}
struct TestEnv
{
std::vector<uint8_t> rdram;
R5900Context ctx{};
PS2Runtime runtime;
TestEnv() : rdram(PS2_RAM_SIZE, 0)
{
std::memset(&ctx, 0, sizeof(ctx));
}
};
}
void register_ps2_runtime_kernel_tests()
{
MiniTest::Case("PS2RuntimeKernel", [](TestCase &tc)
{
tc.Run("thread create/refer/delete follows EE status layout", [](TestCase &t)
{
TestEnv env;
const uint32_t threadParam[7] = {
0x00000002u, // attr
0x00200000u, // entry
0x00300000u, // stack
0x00000800u, // stack size
0x00120000u, // gp
5u, // initial priority
0xABCD0001u // option
};
writeGuestWords(env.rdram.data(), K_PARAM_ADDR, threadParam, std::size(threadParam));
setRegU32(env.ctx, 4, K_PARAM_ADDR);
CreateThread(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t tid = getRegS32(env.ctx, 2);
t.IsTrue(tid >= 2, "CreateThread should return a valid non-main thread id");
setRegU32(env.ctx, 4, static_cast<uint32_t>(tid));
setRegU32(env.ctx, 5, K_STATUS_ADDR);
ReferThreadStatus(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "ReferThreadStatus should succeed for created thread");
EeThreadStatus status{};
std::memcpy(&status, env.rdram.data() + K_STATUS_ADDR, sizeof(status));
t.Equals(status.status, THS_DORMANT, "new thread should be dormant before StartThread");
t.Equals(status.func, threadParam[1], "status.func should match entry");
t.Equals(status.stack, threadParam[2], "status.stack should match configured stack");
t.Equals(status.stack_size, static_cast<int32_t>(threadParam[3]), "status.stack_size should match thread param");
t.Equals(status.gp_reg, threadParam[4], "status.gp_reg should match configured gp");
t.Equals(status.initial_priority, 5, "status.initial_priority should match thread param");
t.Equals(status.current_priority, 5, "status.current_priority should start at initial priority");
t.Equals(status.attr, threadParam[0], "status.attr should match thread param");
t.Equals(status.option, threadParam[6], "status.option should match thread param");
setRegU32(env.ctx, 4, static_cast<uint32_t>(tid));
DeleteThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteThread should succeed for dormant thread");
setRegU32(env.ctx, 4, static_cast<uint32_t>(tid));
setRegU32(env.ctx, 5, K_STATUS_ADDR);
ReferThreadStatus(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_UNKNOWN_THID, "deleted thread id should no longer be referable");
});
tc.Run("start thread validates target and entry registration", [](TestCase &t)
{
TestEnv env;
const uint32_t threadParam[7] = {
0u,
0x00250000u, // entry not registered in runtime
0x00300000u,
0x00000400u,
0x00110000u,
8u,
0u
};
writeGuestWords(env.rdram.data(), K_PARAM_ADDR, threadParam, std::size(threadParam));
setRegU32(env.ctx, 4, K_PARAM_ADDR);
CreateThread(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t tid = getRegS32(env.ctx, 2);
t.IsTrue(tid >= 2, "CreateThread should return an id before StartThread check");
setRegU32(env.ctx, 4, static_cast<uint32_t>(tid));
setRegU32(env.ctx, 5, 0x12345678u);
StartThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_ERROR, "StartThread should fail when entry is not registered");
setRegU32(env.ctx, 4, static_cast<uint32_t>(tid));
setRegU32(env.ctx, 5, K_STATUS_ADDR);
ReferThreadStatus(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "ReferThreadStatus should still succeed after failed StartThread");
EeThreadStatus status{};
std::memcpy(&status, env.rdram.data() + K_STATUS_ADDR, sizeof(status));
t.Equals(status.status, THS_DORMANT, "thread should remain dormant when StartThread fails early");
setRegU32(env.ctx, 4, static_cast<uint32_t>(tid));
DeleteThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteThread should clean up failed-start thread");
});
tc.Run("thread id and wakeup guard rails match kernel-style errors", [](TestCase &t)
{
TestEnv env;
GetThreadId(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t selfTid = getRegS32(env.ctx, 2);
t.IsTrue(selfTid > 0, "GetThreadId should return a positive thread id");
setRegU32(env.ctx, 4, 0u);
WakeupThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_ILLEGAL_THID, "WakeupThread(TH_SELF/0) should be illegal");
setRegU32(env.ctx, 4, static_cast<uint32_t>(selfTid));
WakeupThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_ILLEGAL_THID, "WakeupThread(self) should be illegal");
setRegU32(env.ctx, 4, 0u);
iCancelWakeupThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_ILLEGAL_THID, "iCancelWakeupThread(0) should be illegal");
setRegU32(env.ctx, 4, 0u);
CancelWakeupThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "CancelWakeupThread(TH_SELF) should return previous count (0)");
});
tc.Run("semaphore EE layout covers poll, signal overflow, and status", [](TestCase &t)
{
TestEnv env;
const uint32_t semaParam[6] = {
0u, // count (unused by runtime decode)
2u, // max_count
1u, // init_count
0u, // wait_threads
0x11u, // attr
0x00202020u // option
};
writeGuestWords(env.rdram.data(), K_PARAM_ADDR, semaParam, std::size(semaParam));
setRegU32(env.ctx, 4, K_PARAM_ADDR);
CreateSema(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t sid = getRegS32(env.ctx, 2);
t.IsTrue(sid > 0, "CreateSema should return positive semaphore id");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
setRegU32(env.ctx, 5, K_STATUS_ADDR);
ReferSemaStatus(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "ReferSemaStatus should succeed for valid semaphore");
EeSemaStatus semaStatus{};
std::memcpy(&semaStatus, env.rdram.data() + K_STATUS_ADDR, sizeof(semaStatus));
t.Equals(semaStatus.count, 1, "initial semaphore count should match init_count");
t.Equals(semaStatus.max_count, 2, "max_count should match CreateSema params");
t.Equals(semaStatus.init_count, 1, "init_count should be preserved");
t.Equals(semaStatus.attr, semaParam[4], "attr should be preserved");
t.Equals(semaStatus.option, semaParam[5], "option should be preserved");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "PollSema should consume one available token");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_SEMA_ZERO, "PollSema should fail when count is zero");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
SignalSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SignalSema should increment count when below max");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
SignalSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SignalSema should allow increment up to max");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
SignalSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_SEMA_OVF, "SignalSema should report overflow at max_count");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
DeleteSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteSema should succeed for existing semaphore");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_UNKNOWN_SEMID, "deleted semaphore id should be rejected");
});
tc.Run("semaphore legacy layout decode remains supported", [](TestCase &t)
{
TestEnv env;
const uint32_t legacyParam[6] = {
0x7u, // attr
0x1234u, // legacy option / ee max_count
3u, // init
4u, // max
0u, // ee attr (ignored if legacy selected)
0x1FFFFFFFu // ee option (invalid guest pointer to bias decode toward legacy)
};
writeGuestWords(env.rdram.data(), K_PARAM_ADDR, legacyParam, std::size(legacyParam));
setRegU32(env.ctx, 4, K_PARAM_ADDR);
CreateSema(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t sid = getRegS32(env.ctx, 2);
t.IsTrue(sid > 0, "CreateSema should still accept legacy-style parameter blocks");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
setRegU32(env.ctx, 5, K_STATUS_ADDR);
ReferSemaStatus(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "ReferSemaStatus should succeed for legacy-decoded semaphore");
EeSemaStatus semaStatus{};
std::memcpy(&semaStatus, env.rdram.data() + K_STATUS_ADDR, sizeof(semaStatus));
t.Equals(semaStatus.count, 3, "legacy init_count should map to runtime count");
t.Equals(semaStatus.max_count, 4, "legacy max_count should map to runtime max");
t.Equals(semaStatus.attr, 0x7u, "legacy attr should be preserved");
t.Equals(semaStatus.option, 0x1234u, "legacy option should be preserved");
setRegU32(env.ctx, 4, static_cast<uint32_t>(sid));
DeleteSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteSema should clean up legacy-decoded semaphore");
});
tc.Run("setup heap and allocator primitives track end-of-heap", [](TestCase &t)
{
TestEnv env;
setRegU32(env.ctx, 4, 0x00180010u);
setRegU32(env.ctx, 5, 0x00001000u);
t.IsTrue(callSyscall(0x3Du, env.rdram.data(), &env.ctx, &env.runtime), "SetupHeap syscall should dispatch");
const uint32_t heapBase = static_cast<uint32_t>(getRegS32(env.ctx, 2));
t.Equals(heapBase, 0x00180010u, "SetupHeap should return configured base");
t.IsTrue(callSyscall(0x3Eu, env.rdram.data(), &env.ctx, &env.runtime), "EndOfHeap syscall should dispatch");
const uint32_t heapEndBefore = static_cast<uint32_t>(getRegS32(env.ctx, 2));
t.Equals(heapEndBefore, heapBase, "EndOfHeap should start at heap base before allocation");
const uint32_t alignedAlloc = env.runtime.guestMalloc(0x20u, 64u);
t.IsTrue(alignedAlloc != 0u, "guestMalloc should allocate inside configured heap");
t.Equals(alignedAlloc & 0x3Fu, 0u, "guestMalloc should honor 64-byte alignment");
t.IsTrue(callSyscall(0x3Eu, env.rdram.data(), &env.ctx, &env.runtime), "EndOfHeap syscall should dispatch");
const uint32_t heapEndAfter = static_cast<uint32_t>(getRegS32(env.ctx, 2));
t.IsTrue(heapEndAfter >= alignedAlloc + 0x20u, "EndOfHeap should advance after allocation");
env.runtime.guestFree(alignedAlloc);
const uint32_t a = env.runtime.guestMalloc(0x100u, 16u);
const uint32_t b = env.runtime.guestMalloc(0x100u, 16u);
t.IsTrue(a != 0u && b != 0u, "guestMalloc should provide two adjacent blocks in this heap window");
env.runtime.guestFree(b);
const uint32_t grown = env.runtime.guestRealloc(a, 0x180u, 16u);
t.Equals(grown, a, "guestRealloc should grow in place when adjacent free space is available");
env.runtime.guestFree(grown);
const uint32_t reused = env.runtime.guestMalloc(0x80u, 16u);
t.Equals(reused, heapBase, "guestFree should make the head block reusable");
});
tc.Run("setup heap and thread invalid ids use documented kernel errors", [](TestCase &t)
{
TestEnv env;
setRegU32(env.ctx, 4, 0u);
CreateThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_ERROR, "CreateThread with null param should fail");
setRegU32(env.ctx, 4, 0u);
DeleteThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_ILLEGAL_THID, "DeleteThread(0) should be KE_ILLEGAL_THID");
setRegU32(env.ctx, 4, 0x7FFFu);
StartThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_UNKNOWN_THID, "StartThread should reject unknown thread ids");
setRegU32(env.ctx, 4, 0x7FFFu);
WakeupThread(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_UNKNOWN_THID, "WakeupThread should reject unknown thread ids");
setRegU32(env.ctx, 4, 0x7FFFu);
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_UNKNOWN_SEMID, "PollSema should reject unknown semaphore ids");
setRegU32(env.ctx, 4, 0xFFFFFFFFu);
t.IsTrue(callSyscall(0x3Du, env.rdram.data(), &env.ctx, &env.runtime), "SetupHeap syscall should dispatch");
const uint32_t clampedBase = static_cast<uint32_t>(getRegS32(env.ctx, 2));
t.IsTrue(clampedBase < PS2_RAM_SIZE, "SetupHeap should normalize out-of-range base into guest RAM");
t.IsTrue(callSyscall(0x3Eu, env.rdram.data(), &env.ctx, &env.runtime), "EndOfHeap syscall should dispatch");
const uint32_t heapEnd = static_cast<uint32_t>(getRegS32(env.ctx, 2));
t.IsTrue(heapEnd >= clampedBase, "EndOfHeap should be at or above normalized heap base");
setRegU32(env.ctx, 4, 1u);
setRegU32(env.ctx, 5, 0u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 29, 0x0010FFF0u);
t.IsTrue(callSyscall(0x3Cu, env.rdram.data(), &env.ctx, &env.runtime), "SetupThread syscall should dispatch");
const uint32_t setupSp = static_cast<uint32_t>(getRegS32(env.ctx, 2));
t.Equals(setupSp & 0xFu, 0u, "SetupThread should always return a 16-byte aligned stack pointer");
});
});
}
+232
View File
@@ -0,0 +1,232 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_stubs.h"
#include <array>
#include <cstdint>
#include <cstring>
#include <vector>
namespace
{
struct TestEnv
{
std::vector<uint8_t> rdram;
R5900Context ctx{};
PS2Runtime runtime;
TestEnv() : rdram(PS2_RAM_SIZE, 0u)
{
std::memset(&ctx, 0, sizeof(ctx));
}
};
#pragma pack(push, 1)
struct Ps2SifDmaTransfer
{
uint32_t src;
uint32_t dest;
int32_t size;
int32_t attr;
};
struct SifRpcHeader
{
uint32_t pkt_addr;
uint32_t rpc_id;
int32_t sema_id;
uint32_t mode;
};
struct SifRpcReceiveData
{
SifRpcHeader hdr;
uint32_t src;
uint32_t dest;
int32_t size;
};
#pragma pack(pop)
static_assert(sizeof(Ps2SifDmaTransfer) == 16u, "Unexpected Ps2SifDmaTransfer size.");
static_assert(sizeof(SifRpcReceiveData) == 28u, "Unexpected SifRpcReceiveData size.");
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
int32_t getRegS32(const R5900Context &ctx, int reg)
{
return static_cast<int32_t>(::getRegU32(&ctx, reg));
}
void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
uint32_t readGuestU32(const uint8_t *rdram, uint32_t addr)
{
uint32_t value = 0;
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
}
void register_ps2_sif_dma_tests()
{
MiniTest::Case("PS2SifDma", [](TestCase &tc)
{
tc.Run("sceSifSetDma copies payload and sceSifDmaStat reports complete", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kDescAddr = 0x00020000u;
constexpr uint32_t kSrcAddr = 0x00020100u;
constexpr uint32_t kDstAddr = 0x00020200u;
std::array<uint8_t, 16> payload{};
for (size_t i = 0; i < payload.size(); ++i)
{
payload[i] = static_cast<uint8_t>(0x30u + i);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kDstAddr, 0, payload.size());
const Ps2SifDmaTransfer desc{
kSrcAddr,
kDstAddr,
static_cast<int32_t>(payload.size()),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t dmaId = getRegS32(env.ctx, 2);
t.IsTrue(dmaId > 0, "sceSifSetDma should return a positive transfer id on success");
t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, payload.data(), payload.size()) == 0,
"sceSifSetDma should copy transfer payload to destination");
setRegU32(env.ctx, 4, static_cast<uint32_t>(dmaId));
ps2_stubs::sceSifDmaStat(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) < 0, "sceSifDmaStat should be negative when transfer is complete");
});
tc.Run("sceSifSetDma rejects invalid descriptors without partial writes", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kDescAddr = 0x00021000u;
constexpr uint32_t kSrcA = 0x00021100u;
constexpr uint32_t kDstA = 0x00021200u;
constexpr uint32_t kSrcB = 0x00021300u;
constexpr uint32_t kInvalidDstB = 0xE0000100u; // unsupported guest segment
std::array<uint8_t, 8> payloadA{};
for (size_t i = 0; i < payloadA.size(); ++i)
{
payloadA[i] = static_cast<uint8_t>(0x70u + i);
}
std::array<uint8_t, 8> payloadB{};
for (size_t i = 0; i < payloadB.size(); ++i)
{
payloadB[i] = static_cast<uint8_t>(0x90u + i);
}
std::memcpy(env.rdram.data() + kSrcA, payloadA.data(), payloadA.size());
std::memcpy(env.rdram.data() + kSrcB, payloadB.data(), payloadB.size());
std::memset(env.rdram.data() + kDstA, 0x5Au, payloadA.size());
const Ps2SifDmaTransfer descs[2] = {
{kSrcA, kDstA, static_cast<int32_t>(payloadA.size()), 0},
{kSrcB, kInvalidDstB, static_cast<int32_t>(payloadB.size()), 0}};
std::memcpy(env.rdram.data() + kDescAddr, descs, sizeof(descs));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 2u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "sceSifSetDma should fail when any descriptor is invalid");
const std::array<uint8_t, 8> expectedUnchanged{
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A};
t.IsTrue(std::memcmp(env.rdram.data() + kDstA, expectedUnchanged.data(), expectedUnchanged.size()) == 0,
"failed multi-descriptor sceSifSetDma should not partially write earlier descriptors");
});
tc.Run("sceSifSetDma enforces descriptor count limit", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kDescAddr = 0x00022000u;
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 33u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "sceSifSetDma should reject count > 32");
});
tc.Run("sceSifGetOtherData copies payload and writes receive metadata", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kRdAddr = 0x00023000u;
constexpr uint32_t kSrcAddr = 0x00023100u;
constexpr uint32_t kDstAddr = 0x00023200u;
constexpr uint32_t kSize = 20u;
std::array<uint8_t, kSize> payload{};
for (size_t i = 0; i < payload.size(); ++i)
{
payload[i] = static_cast<uint8_t>((i * 7u) & 0xFFu);
}
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kDstAddr, 0, payload.size());
std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData));
setRegU32(env.ctx, 4, kRdAddr);
setRegU32(env.ctx, 5, kSrcAddr);
setRegU32(env.ctx, 6, kDstAddr);
setRegU32(env.ctx, 7, kSize);
ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "sceSifGetOtherData should succeed for valid transfer");
t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, payload.data(), payload.size()) == 0,
"sceSifGetOtherData should copy payload");
const SifRpcReceiveData rd = *reinterpret_cast<const SifRpcReceiveData *>(env.rdram.data() + kRdAddr);
t.Equals(rd.src, kSrcAddr, "receive metadata src should be populated");
t.Equals(rd.dest, kDstAddr, "receive metadata dest should be populated");
t.Equals(static_cast<uint32_t>(rd.size), kSize, "receive metadata size should be populated");
});
tc.Run("sceSifGetOtherData rejects unsupported guest segments", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kRdAddr = 0x00024000u;
constexpr uint32_t kDstAddr = 0x00024100u;
constexpr uint32_t kInvalidSrcAddr = 0xE0000200u;
constexpr uint32_t kSize = 16u;
std::memset(env.rdram.data() + kDstAddr, 0xA5, kSize);
writeGuestU32(env.rdram.data(), kRdAddr + 0x10u, 0x11111111u);
writeGuestU32(env.rdram.data(), kRdAddr + 0x14u, 0x22222222u);
writeGuestU32(env.rdram.data(), kRdAddr + 0x18u, 0x33333333u);
setRegU32(env.ctx, 4, kRdAddr);
setRegU32(env.ctx, 5, kInvalidSrcAddr);
setRegU32(env.ctx, 6, kDstAddr);
setRegU32(env.ctx, 7, kSize);
ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), -1, "sceSifGetOtherData should fail for unsupported source segment");
std::array<uint8_t, kSize> expected{};
expected.fill(0xA5u);
t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, expected.data(), expected.size()) == 0,
"failed sceSifGetOtherData should not modify destination");
t.Equals(readGuestU32(env.rdram.data(), kRdAddr + 0x10u), 0x11111111u,
"failed sceSifGetOtherData should not overwrite rd metadata");
});
});
}
+509
View File
@@ -0,0 +1,509 @@
#include "MiniTest.h"
#include "ps2_runtime.h"
#include "ps2_syscalls.h"
#include <array>
#include <cstdint>
#include <cstring>
#include <vector>
using namespace ps2_syscalls;
namespace
{
constexpr int KE_OK = 0;
constexpr int KE_SEMA_ZERO = -419;
constexpr uint32_t K_SIF_RPC_MODE_NOWAIT = 0x01u;
constexpr uint32_t K_STACK_ADDR = 0x00100000u;
#pragma pack(push, 1)
struct SifRpcHeader
{
uint32_t pkt_addr;
uint32_t rpc_id;
int32_t sema_id;
uint32_t mode;
};
struct SifRpcClientData
{
SifRpcHeader hdr;
uint32_t command;
uint32_t buf;
uint32_t cbuf;
uint32_t end_function;
uint32_t end_param;
uint32_t server;
};
struct SifRpcServerData
{
int32_t sid;
uint32_t func;
uint32_t buf;
int32_t size;
uint32_t cfunc;
uint32_t cbuf;
int32_t size2;
uint32_t client;
uint32_t pkt_addr;
int32_t rpc_number;
uint32_t recvbuf;
int32_t rsize;
int32_t rmode;
int32_t rid;
uint32_t link;
uint32_t next;
uint32_t base;
};
struct SifRpcDataQueue
{
int32_t thread_id;
int32_t active;
uint32_t link;
uint32_t start;
uint32_t end;
uint32_t next;
};
#pragma pack(pop)
static_assert(sizeof(SifRpcHeader) == 0x10u, "Unexpected SifRpcHeader size.");
static_assert(sizeof(SifRpcClientData) == 0x28u, "Unexpected SifRpcClientData size.");
static_assert(sizeof(SifRpcServerData) == 0x44u, "Unexpected SifRpcServerData size.");
static_assert(sizeof(SifRpcDataQueue) == 0x18u, "Unexpected SifRpcDataQueue size.");
struct TestEnv
{
std::vector<uint8_t> rdram;
R5900Context ctx{};
PS2Runtime runtime;
TestEnv() : rdram(PS2_RAM_SIZE, 0)
{
std::memset(&ctx, 0, sizeof(ctx));
}
};
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
}
int32_t getRegS32(const R5900Context &ctx, int reg)
{
return static_cast<int32_t>(::getRegU32(&ctx, reg));
}
uint32_t getRegU32Result(const R5900Context &ctx, int reg)
{
return ::getRegU32(&ctx, reg);
}
void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
template <typename T>
void writeGuestStruct(uint8_t *rdram, uint32_t addr, const T &value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
template <typename T>
T readGuestStruct(const uint8_t *rdram, uint32_t addr)
{
T value{};
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
}
void register_ps2_sif_rpc_tests()
{
MiniTest::Case("PS2SifRpc", [](TestCase &tc)
{
tc.Run("register bind call updates descriptors and payload", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kQdAddr = 0x00022000u;
constexpr uint32_t kSdAddr = 0x00022100u;
constexpr uint32_t kClientAddr = 0x00022200u;
constexpr uint32_t kServerBufAddr = 0x00022300u;
constexpr uint32_t kClientCbufAddr = 0x00022400u;
constexpr uint32_t kSendAddr = 0x00022500u;
constexpr uint32_t kRecvAddr = 0x00022600u;
constexpr uint32_t kSid = 0x20000111u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kQdAddr);
setRegU32(env.ctx, 5, 0x33u);
SifSetRpcQueue(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifSetRpcQueue should succeed");
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, 0x9000u); // cfunc
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, kClientCbufAddr); // cbuf
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, kQdAddr); // qd
setRegU32(env.ctx, 4, kSdAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u); // no server callback
setRegU32(env.ctx, 7, kServerBufAddr);
SifRegisterRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifRegisterRpc should succeed");
const SifRpcDataQueue qdAfterRegister = readGuestStruct<SifRpcDataQueue>(env.rdram.data(), kQdAddr);
const SifRpcServerData sdAfterRegister = readGuestStruct<SifRpcServerData>(env.rdram.data(), kSdAddr);
t.Equals(qdAfterRegister.link, kSdAddr, "queue link should point at registered server");
t.Equals(static_cast<uint32_t>(sdAfterRegister.sid), kSid, "server sid should match registered sid");
t.Equals(sdAfterRegister.buf, kServerBufAddr, "server buf should match register arg");
t.Equals(sdAfterRegister.cbuf, kClientCbufAddr, "server cbuf should match stack arg");
t.Equals(sdAfterRegister.base, kQdAddr, "server base should point to queue");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed");
const SifRpcClientData clientAfterBind = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
t.Equals(clientAfterBind.server, kSdAddr, "client should bind to registered server");
t.Equals(clientAfterBind.buf, kServerBufAddr, "client buf should mirror server buf");
t.Equals(clientAfterBind.cbuf, kClientCbufAddr, "client cbuf should mirror server cbuf");
std::array<uint8_t, 16> payload{};
for (size_t i = 0; i < payload.size(); ++i)
{
payload[i] = static_cast<uint8_t>(0x50u + i);
}
std::memcpy(env.rdram.data() + kSendAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kServerBufAddr, 0, payload.size());
std::memset(env.rdram.data() + kRecvAddr, 0, payload.size());
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x55u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, static_cast<uint32_t>(payload.size()));
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, static_cast<uint32_t>(payload.size()));
setRegU32(env.ctx, 11, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u); // endParam
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed");
const SifRpcServerData sdAfterCall = readGuestStruct<SifRpcServerData>(env.rdram.data(), kSdAddr);
t.Equals(sdAfterCall.client, kClientAddr, "server should record caller client pointer");
t.Equals(static_cast<uint32_t>(sdAfterCall.rpc_number), 0x55u, "server rpc_number should match request");
t.Equals(static_cast<uint32_t>(sdAfterCall.size), static_cast<uint32_t>(payload.size()), "server size should match sendSize");
t.Equals(sdAfterCall.recvbuf, kRecvAddr, "server recvbuf should match request recv pointer");
t.Equals(static_cast<uint32_t>(sdAfterCall.rsize), static_cast<uint32_t>(payload.size()), "server rsize should match recvSize");
t.Equals(static_cast<uint32_t>(sdAfterCall.rmode), 1u, "blocking call should set rmode to 1");
t.IsTrue(std::memcmp(env.rdram.data() + kServerBufAddr, payload.data(), payload.size()) == 0,
"send payload should be copied into server buffer");
t.IsTrue(std::memcmp(env.rdram.data() + kRecvAddr, payload.data(), payload.size()) == 0,
"unhandled RPC should copy payload into recv buffer");
setRegU32(env.ctx, 4, kClientAddr);
SifCheckStatRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "SifCheckStatRpc should report not busy after synchronous completion");
});
tc.Run("bind before register creates placeholder then remaps", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kQdAddr = 0x00024000u;
constexpr uint32_t kSdAddr = 0x00024100u;
constexpr uint32_t kClientAddr = 0x00024200u;
constexpr uint32_t kServerBufAddr = 0x00024300u;
constexpr uint32_t kServerCbufAddr = 0x00024400u;
constexpr uint32_t kSid = 0x20000122u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
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");
setRegU32(env.ctx, 4, kQdAddr);
setRegU32(env.ctx, 5, 0x44u);
SifSetRpcQueue(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifSetRpcQueue should succeed");
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, kServerCbufAddr);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, kQdAddr);
setRegU32(env.ctx, 4, kSdAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kServerBufAddr);
SifRegisterRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifRegisterRpc should succeed");
const SifRpcClientData clientAfterRegister = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
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");
setRegU32(env.ctx, 4, kSdAddr);
setRegU32(env.ctx, 5, kQdAddr);
SifRemoveRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegU32Result(env.ctx, 2), kSdAddr, "SifRemoveRpc should return removed server pointer");
const SifRpcDataQueue qdAfterRemove = readGuestStruct<SifRpcDataQueue>(env.rdram.data(), kQdAddr);
const SifRpcServerData sdAfterRemove = readGuestStruct<SifRpcServerData>(env.rdram.data(), kSdAddr);
t.Equals(qdAfterRemove.link, 0u, "queue link should detach removed server");
t.Equals(sdAfterRemove.link, 0u, "removed server link should be cleared");
});
tc.Run("SifSetRpcQueue remove roundtrip is stable", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kQdAddr = 0x00026000u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kQdAddr);
setRegU32(env.ctx, 5, 0x55u);
SifSetRpcQueue(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifSetRpcQueue should succeed");
const SifRpcDataQueue qd = readGuestStruct<SifRpcDataQueue>(env.rdram.data(), kQdAddr);
t.Equals(static_cast<uint32_t>(qd.thread_id), 0x55u, "queue thread id should match argument");
setRegU32(env.ctx, 4, kQdAddr);
SifRemoveRpcQueue(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegU32Result(env.ctx, 2), kQdAddr, "SifRemoveRpcQueue should return removed queue pointer");
setRegU32(env.ctx, 4, kQdAddr);
SifRemoveRpcQueue(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegU32Result(env.ctx, 2), 0u, "removing the same queue twice should return 0");
});
tc.Run("sid1 nowait RPC 0x12/0x13 returns expected pointers and signals sema", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kClientAddr = 0x00028000u;
constexpr uint32_t kSemaParamAddr = 0x00028100u;
constexpr uint32_t kRecvAddr = 0x00028200u;
constexpr uint32_t kSid = 1u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t semaParam[6] = {
0u, // count (unused by runtime decode)
1u, // max_count
0u, // init_count
0u, // wait_threads
0u, // attr
0u // option
};
std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam));
setRegU32(env.ctx, 4, kSemaParamAddr);
CreateSema(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t semaId = getRegS32(env.ctx, 2);
t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for sid 1");
SifRpcClientData client = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
client.hdr.sema_id = semaId;
writeGuestStruct(env.rdram.data(), kClientAddr, client);
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_SEMA_ZERO, "semaphore should start at zero before nowait rpc");
std::memset(env.rdram.data() + kRecvAddr, 0, 16u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x12u);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc(0x12) should succeed");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr), 0x00012000u, "rpc 0x12 should return SND_STATUS pointer");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "nowait rpc should signal completion sema");
std::memset(env.rdram.data() + kRecvAddr, 0, 16u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x13u);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc(0x13) should succeed");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr), 0x00012100u, "rpc 0x13 should return address-table pointer");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "each nowait rpc should signal completion sema");
});
tc.Run("SifCallRpc falls back to stack ABI when register pack is implausible", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kQdAddr = 0x0002A000u;
constexpr uint32_t kSdAddr = 0x0002A100u;
constexpr uint32_t kClientAddr = 0x0002A200u;
constexpr uint32_t kServerBufAddr = 0x0002A300u;
constexpr uint32_t kSendAddr = 0x0002A400u;
constexpr uint32_t kRecvAddr = 0x0002A500u;
constexpr uint32_t kSid = 0x20000133u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kQdAddr);
setRegU32(env.ctx, 5, 0x66u);
SifSetRpcQueue(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifSetRpcQueue should succeed");
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, kQdAddr);
setRegU32(env.ctx, 4, kSdAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kServerBufAddr);
SifRegisterRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifRegisterRpc should succeed");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed");
std::array<uint8_t, 12> payload{};
for (size_t i = 0; i < payload.size(); ++i)
{
payload[i] = static_cast<uint8_t>(0xA0u + i);
}
std::memcpy(env.rdram.data() + kSendAddr, payload.data(), payload.size());
std::memset(env.rdram.data() + kRecvAddr, 0, payload.size());
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, static_cast<uint32_t>(payload.size()));
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, kRecvAddr);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, static_cast<uint32_t>(payload.size()));
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x1Cu, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x20u, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x99u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 0x03000000u); // implausible size (> 0x02000000 threshold)
setRegU32(env.ctx, 9, 0x00000004u); // implausible guest pointer
setRegU32(env.ctx, 10, 0x03000001u);
setRegU32(env.ctx, 11, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed with stack ABI fallback");
const SifRpcServerData sdAfterCall = readGuestStruct<SifRpcServerData>(env.rdram.data(), kSdAddr);
t.Equals(static_cast<uint32_t>(sdAfterCall.size), static_cast<uint32_t>(payload.size()),
"stack ABI sendSize should be selected when register ABI is implausible");
t.Equals(sdAfterCall.recvbuf, kRecvAddr, "stack ABI recvBuf should be selected");
t.Equals(static_cast<uint32_t>(sdAfterCall.rsize), static_cast<uint32_t>(payload.size()),
"stack ABI recvSize should be selected");
t.IsTrue(std::memcmp(env.rdram.data() + kRecvAddr, payload.data(), payload.size()) == 0,
"recv payload should match stack-selected transfer size");
});
tc.Run("SifCallRpc prefers stack ABI for DTX URPC when both packs look plausible", [](TestCase &t)
{
TestEnv env;
constexpr uint32_t kClientAddr = 0x0002B000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kSendAddr = 0x0002B100u;
constexpr uint32_t kRecvStackAddr = 0x0002B200u;
constexpr uint32_t kRecvRegAddr = 0x0002B300u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for DTX sid");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); // mode
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0x1E21440u); // wk addr
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 0x100u); // wk size
writeGuestU32(env.rdram.data(), kRecvStackAddr, 0u);
writeGuestU32(env.rdram.data(), kRecvRegAddr, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, 12u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, kRecvStackAddr);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, 4u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x1Cu, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x20u, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x422u); // DTX URPC command 34 (SJUNI create)
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
// Plausible but intentionally wrong register-side packed args.
setRegU32(env.ctx, 8, 4u);
setRegU32(env.ctx, 9, kRecvRegAddr);
setRegU32(env.ctx, 10, 12u);
setRegU32(env.ctx, 11, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed for DTX URPC");
const uint32_t stackHandle = readGuestStruct<uint32_t>(env.rdram.data(), kRecvStackAddr);
const uint32_t regHandle = readGuestStruct<uint32_t>(env.rdram.data(), kRecvRegAddr);
t.IsTrue(stackHandle != 0u, "DTX handle should be written to stack-selected recv buffer");
t.Equals(regHandle, 0u, "register recv buffer should remain untouched when stack ABI is preferred");
});
});
}
+25
View File
@@ -117,6 +117,31 @@ void register_r5900_decoder_tests()
t.IsTrue(inst.modificationInfo.modifiesGPR, "jalr with rd!=0 should mark GPR modification");
});
tc.Run("R5900 MULT marks rd modification when rd is non-zero", [](TestCase &t) {
uint32_t address = 0x5800;
uint32_t rawWithRd = (OPCODE_SPECIAL << 26) | (4 << 21) | (5 << 16) | (3 << 11) | SPECIAL_MULT;
uint32_t rawRdZero = (OPCODE_SPECIAL << 26) | (4 << 21) | (5 << 16) | (0 << 11) | SPECIAL_MULT;
R5900Decoder decoder;
Instruction withRd = decoder.decodeInstruction(address, rawWithRd);
Instruction rdZero = decoder.decodeInstruction(address + 4, rawRdZero);
t.IsTrue(withRd.modificationInfo.modifiesControl, "MULT should modify HI/LO");
t.IsTrue(withRd.modificationInfo.modifiesGPR, "MULT should mark rd modification when rd!=0");
t.IsFalse(rdZero.modificationInfo.modifiesGPR, "MULT should not mark rd modification when rd==0");
});
tc.Run("R5900 MMI MULT1 marks rd modification when rd is non-zero", [](TestCase &t) {
uint32_t address = 0x5900;
uint32_t raw = (OPCODE_MMI << 26) | (6 << 21) | (7 << 16) | (8 << 11) | MMI_MULT1;
R5900Decoder decoder;
Instruction inst = decoder.decodeInstruction(address, raw);
t.IsTrue(inst.modificationInfo.modifiesControl, "MULT1 should modify HI1/LO1");
t.IsTrue(inst.modificationInfo.modifiesGPR, "MULT1 should mark rd modification when rd!=0");
});
tc.Run("MMI instruction sets MMI flags", [](TestCase &t) {
uint32_t address = 0x6000;
// Use opcode 0x1C (MMI), rs=1, rt=2, rd=3, sa=MMI0_PADDW (0)