mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 669114f3f6 | |||
| 8d1f1c5672 | |||
| 1551d59fbc | |||
| c1e98c9398 | |||
| bac1fc9ef8 |
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -143,6 +143,69 @@ namespace
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool HasAnyExecutableSection(const std::vector<ps2recomp::Section> §ions)
|
||||
{
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (section.isCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ps2recomp::Section *FindFunctionSectionByAddress(const std::vector<ps2recomp::Section> §ions, 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
const char* getGameName(const std::string& gameId);
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
std::string translatePs2Path(const char *ps2Path);
|
||||
|
||||
extern std::atomic<int> g_activeThreads;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+876
-111
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 ®ion)
|
||||
return (address >= region.start && address < region.end);
|
||||
}
|
||||
|
||||
bool PS2Memory::isCodeAddress(uint32_t address) const
|
||||
{
|
||||
for (const auto ®ion : 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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()), ®s);
|
||||
|
||||
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
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user