From 7ca6a866c92f812fff44110a0602c2eac0b2a8e2 Mon Sep 17 00:00:00 2001 From: Ranieri Date: Mon, 9 Mar 2026 00:41:51 -0300 Subject: [PATCH] Feature/ghidra export and syscall fixes (#100) * docs: deprecate the local analyzer workflow in favor of Ghidra feat: improve the Ghidra exporter for stripped games and internal entry points fix: correct FindAddress behavior in the runtime fix: emit missing delay-slot code in recompiler edge cases feat: add SetSyscall support from @Whoneon feat: add dispatchSyscallOverride support from @Whoneon fix: fix Unmatched '{' due to missing newlines from issue #96 feat: delete python ghidra script I never updated it anyway feat: added a lot more of regression test feat: added a lot of logs to help debug on runtime fix: fix wrong syscall ID on runtime --- README.md | 28 +- ps2xAnalyzer/Readme.md | 29 +- ps2xRecomp/src/lib/code_generator.cpp | 117 +++-- ps2xRecomp/src/lib/ps2_recompiler.cpp | 37 +- .../tools/ghidra/ExportPS2Functions.java | 235 ++++++++- ps2xRecomp/tools/ghidra/ExportPS2Functions.py | 43 -- ps2xRuntime/include/ps2_call_list.h | 1 + ps2xRuntime/include/ps2_syscalls.h | 2 + ps2xRuntime/src/lib/ps2_runtime.cpp | 7 + ps2xRuntime/src/lib/ps2_syscalls.cpp | 17 +- ps2xRuntime/src/lib/stubs/ps2_stubs_misc.inl | 93 +++- .../helpers/ps2_syscalls_helpers_runtime.inl | 70 ++- .../helpers/ps2_syscalls_helpers_state.inl | 9 + .../lib/syscalls/ps2_syscalls_interrupt.inl | 84 +++ .../src/lib/syscalls/ps2_syscalls_system.inl | 478 +++++++++++++++++- ps2xTest/src/code_generator_tests.cpp | 86 ++++ ps2xTest/src/ps2_recompiler_tests.cpp | 37 ++ ps2xTest/src/ps2_runtime_kernel_tests.cpp | 290 ++++++++++- ps2xTest/src/ps2_sif_dma_tests.cpp | 121 +++++ ps2xTest/src/ps2_sif_rpc_tests.cpp | 7 + 20 files changed, 1638 insertions(+), 153 deletions(-) delete mode 100644 ps2xRecomp/tools/ghidra/ExportPS2Functions.py diff --git a/README.md b/README.md index 89f1cc4..a5ef4f4 100644 --- a/README.md +++ b/README.md @@ -61,27 +61,35 @@ cmake --build out/build --config Debug ### Usage -1. Analyze ELF and generate config: +Preferred workflow for retail or stripped games: -```bash -./ps2_analyzer your_game.elf config.toml -``` -*For better results on retail games, see the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-recommended-for-complex-games).* - -2. Recompile using generated TOML: +1. Open the ELF in Ghidra. +2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`. +3. Use the exported TOML and CSV map. +4. Recompile with the exported TOML: ```bash ./ps2_recomp config.toml ``` -3. Build generated output and link with `ps2xRuntime`. +Fallback workflow for quick local experiments or ELFs with debug symbol : + +```bash +./ps2_analyzer your_game.elf config.toml +``` + +Use this only when you do not have a Ghidra project yet. The native analyzer is faster to start, but it is less accurate on stripped retail games and more likely to miss internal callable entry points. + +See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for the recommended path. + +Then build generated output and link with `ps2xRuntime`. ### Configuration Main fields in `config.toml`: * `general.input`: source ELF path. -* `general.ghidra_output`: optional function map CSV. +* `general.ghidra_output`: recommended function map CSV exported from Ghidra. * `general.output`: generated C++ output folder. * `general.single_file_output`: one combined cpp or one file per function. * `general.patch_syscalls`: apply configured patches to `SYSCALL` instructions (`false` recommended). @@ -96,7 +104,7 @@ Address binding for stripped ELFs: * Use `handler@0xADDRESS` inside `general.stubs` to map a stripped function start directly to a runtime handler. * Example: `sceCdRead@0x00123456` binds function start `0x00123456` to `ps2_stubs::sceCdRead(...)`. * Generic temporary handlers are available: `ret0@0xADDR`, `ret1@0xADDR`, `reta0@0xADDR`. -* Before manual binding, try plain recompilation first: if ELF relocation symbols are present for calls, runtime handler routing can be inferred automatically. +* Before manual binding, prefer recompilation from a Ghidra-exported TOML/CSV first. The extra boundaries and synthetic entry points are usually more important than manual early triage. * The address must be the function start in that exact ELF build. * Addresses are not portable across different games/regions/builds. * The handler name must exist in runtime call lists (`PS2_SYSCALL_LIST` or `PS2_STUB_LIST`). diff --git a/ps2xAnalyzer/Readme.md b/ps2xAnalyzer/Readme.md index 27be1eb..e735c01 100644 --- a/ps2xAnalyzer/Readme.md +++ b/ps2xAnalyzer/Readme.md @@ -9,18 +9,21 @@ The analyzer supports three distinct paths for discovering code within a PS2 bin ### 1. DWARF Debug Information If the ELF was compiled with debug symbols (`-g`), the analyzer uses `libdwarf` to extract perfect function names and exact start/end addresses. This is common in homebrew or early development builds. -### 2. Native Heuristic Scanner (Retail/Stripped) +### 2. Native Heuristic Scanner (Test only) For commercial games where symbols are stripped, the analyzer uses a "JAL Scanner": * It scans executable sections for `JAL` (Jump and Link) instructions. * It infers function start points based on jump targets. * It generates names like `sub_XXXXXXXX`. -### 3. Ghidra Integration (For Complex Games) -For the highest accuracy in stripped games, you can use Ghidra's superior analysis engine: -1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.py` or `.java`. +Use this path only as a quick fallback when you do not yet have a Ghidra project. It is not the preferred workflow for retail games. + +### 3. Ghidra Integration (For Retail and Stripped Games, Preferred) +This is the recommended workflow for almost every commercial game: +1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`. 2. Run it in Ghidra to export a CSV map of all functions. -3. Add the CSV path to your TOML: `ghidra_output = "path/to/map.csv"`. -4. The recompiler will prioritize Ghidra's boundaries over its own heuristics. +3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`. +4. Run the recompiler with that exported TOML. +5. The recompiler will prioritize Ghidra's boundaries over its own heuristics. ## Key Features @@ -41,12 +44,16 @@ ps2_analyzer * `output_toml`: Path where the generated TOML configuration will be saved. ## Example Workflow -1. Run the analyzer on your game: - `ps2_analyzer game.elf config.toml` -2. (Optional) Open `game.elf` in Ghidra, run the export script, and update `config.toml` with the CSV path. -3. Run the recompiler: +1. Open `game.elf` in Ghidra. +2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`. +3. Use the exported TOML and CSV. +4. Run the recompiler: `ps2recomp config.toml` +Fallback: +1. Run `ps2_analyzer game.elf config.toml`. +2. Use that TOML only for quick bring-up or symbol-rich builds. + ## Generated Configuration The tool creates a TOML file with the following sections: * `[general]`: Paths to ELF and Ghidra maps. @@ -60,4 +67,4 @@ The tool creates a TOML file with the following sections: * Self-modifying code is flagged but requires manual review. * Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries. -For more details on the recompilation process, see the [Main README](../README.md). \ No newline at end of file +For more details on the recompilation process, see the [Main README](../README.md). diff --git a/ps2xRecomp/src/lib/code_generator.cpp b/ps2xRecomp/src/lib/code_generator.cpp index 6f02e2f..3fc9148 100644 --- a/ps2xRecomp/src/lib/code_generator.cpp +++ b/ps2xRecomp/src/lib/code_generator.cpp @@ -37,6 +37,16 @@ namespace ps2recomp return ((address + 4) & 0xF0000000u) | (target << 2); } + static Instruction makeSyntheticDelaySlot(uint32_t address) + { + Instruction inst{}; + inst.address = address; + inst.raw = 0; + inst.opcode = OPCODE_SPECIAL; + inst.function = SPECIAL_SLL; + return inst; + } + static std::string formatFloatLiteral(float value) { if (!std::isfinite(value)) @@ -927,18 +937,35 @@ namespace ps2recomp try { - if (inst.hasDelaySlot && i + 1 < instructions.size()) + if (inst.hasDelaySlot) { - const Instruction &delaySlot = instructions[i + 1]; + const bool hasDecodedDelaySlot = + i + 1 < instructions.size() && + instructions[i + 1].address == inst.address + 4u; - if (internalTargets.contains(delaySlot.address)) + Instruction syntheticDelaySlot{}; + const Instruction *delaySlot = nullptr; + if (hasDecodedDelaySlot) { - ss << "label_" << std::hex << delaySlot.address << std::dec << ":\n"; + delaySlot = &instructions[i + 1]; + } + else + { + syntheticDelaySlot = makeSyntheticDelaySlot(inst.address + 4u); + delaySlot = &syntheticDelaySlot; } - ss << handleBranchDelaySlots(inst, delaySlot, function, analysisResult); + if (hasDecodedDelaySlot && internalTargets.contains(delaySlot->address)) + { + ss << "label_" << std::hex << delaySlot->address << std::dec << ":\n"; + } - ++i; // Skip delay slot instruction (handled inside branch logic) + ss << handleBranchDelaySlots(inst, *delaySlot, function, analysisResult); + + if (hasDecodedDelaySlot) + { + ++i; // Skip delay slot instruction (handled inside branch logic) + } } else { @@ -2949,18 +2976,18 @@ namespace ps2recomp std::string CodeGenerator::translateVU_VRNEXT(const Instruction &inst) { return fmt::format( - "{{ " - " uint32_t r_vals[4]; " - " _mm_storeu_si128((__m128i*)r_vals, _mm_castps_si128(ctx->vu0_r)); " - " " - " // Simple LFSR-based random number generation (PS2-like behavior) " - " uint32_t feedback = r_vals[0] ^ (r_vals[0] << 13) ^ (r_vals[1] >> 19) ^ (r_vals[2] << 7); " - " r_vals[0] = r_vals[1]; " - " r_vals[1] = r_vals[2]; " - " r_vals[2] = r_vals[3]; " - " r_vals[3] = feedback; " - " " - " ctx->vu0_r = _mm_castsi128_ps(_mm_loadu_si128((__m128i*)r_vals)); \n" + "{{\n" + " uint32_t r_vals[4];\n" + " _mm_storeu_si128((__m128i*)r_vals, _mm_castps_si128(ctx->vu0_r));\n" + "\n" + " // Simple LFSR-based random number generation (PS2-like behavior)\n" + " uint32_t feedback = r_vals[0] ^ (r_vals[0] << 13) ^ (r_vals[1] >> 19) ^ (r_vals[2] << 7);\n" + " r_vals[0] = r_vals[1];\n" + " r_vals[1] = r_vals[2];\n" + " r_vals[2] = r_vals[3];\n" + " r_vals[3] = feedback;\n" + "\n" + " ctx->vu0_r = _mm_castsi128_ps(_mm_loadu_si128((__m128i*)r_vals));\n" "}}"); } @@ -3608,19 +3635,19 @@ namespace ps2recomp uint8_t fsf = inst.vectorInfo.fsf; return fmt::format( - "{{ " - " float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{}))); " - " uint32_t seed; std::memcpy(&seed, &src, sizeof(seed)); " - " " - " // PS2 uses a specific LFSR initialization pattern " - " if (seed == 0) seed = 1; " // Prevent zero seed - " " - " uint32_t r0 = seed; " - " uint32_t r1 = seed * 0x41C64E6D + 0x3039; " // PS2-like LCG constants - " uint32_t r2 = r1 * 0x41C64E6D + 0x3039; " - " uint32_t r3 = r2 * 0x41C64E6D + 0x3039; " - " " - " ctx->vu0_r = _mm_castsi128_ps(_mm_set_epi32(r3, r2, r1, r0)); \n " + "{{\n" + " float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{})));\n" + " uint32_t seed; std::memcpy(&seed, &src, sizeof(seed));\n" + "\n" + " // PS2 uses a specific LFSR initialization pattern\n" + " if (seed == 0) seed = 1;\n" + "\n" + " uint32_t r0 = seed;\n" + " uint32_t r1 = seed * 0x41C64E6D + 0x3039;\n" + " uint32_t r2 = r1 * 0x41C64E6D + 0x3039;\n" + " uint32_t r3 = r2 * 0x41C64E6D + 0x3039;\n" + "\n" + " ctx->vu0_r = _mm_castsi128_ps(_mm_set_epi32(r3, r2, r1, r0));\n" "}}", fs_reg, fs_reg, fsf); } @@ -3631,20 +3658,20 @@ namespace ps2recomp uint8_t fsf = inst.vectorInfo.fsf; return fmt::format( - "{{ " - " float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{}))); " - " uint32_t src_bits; std::memcpy(&src_bits, &src, sizeof(src_bits)); " - " __m128i r_current = _mm_castps_si128(ctx->vu0_r); " - " __m128i fs_data = _mm_set1_epi32((int)src_bits); " - " " - " // XOR the current random value with the data from the VU vector register " - " __m128i xored = _mm_xor_si128(r_current, fs_data); " - " " - " // Apply a simple mixing function similar to PS2's LFSR " - " __m128i mixed = _mm_xor_si128(xored, _mm_slli_epi32(xored, 7)); " - " mixed = _mm_xor_si128(mixed, _mm_srli_epi32(mixed, 9)); " - " " - " ctx->vu0_r = _mm_castsi128_ps(mixed);" + "{{\n" + " float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{})));\n" + " uint32_t src_bits; std::memcpy(&src_bits, &src, sizeof(src_bits));\n" + " __m128i r_current = _mm_castps_si128(ctx->vu0_r);\n" + " __m128i fs_data = _mm_set1_epi32((int)src_bits);\n" + "\n" + " // XOR the current random value with the data from the VU vector register\n" + " __m128i xored = _mm_xor_si128(r_current, fs_data);\n" + "\n" + " // Apply a simple mixing function similar to PS2's LFSR\n" + " __m128i mixed = _mm_xor_si128(xored, _mm_slli_epi32(xored, 7));\n" + " mixed = _mm_xor_si128(mixed, _mm_srli_epi32(mixed, 9));\n" + "\n" + " ctx->vu0_r = _mm_castsi128_ps(mixed);\n" "}}", fs_reg, fs_reg, fsf); } diff --git a/ps2xRecomp/src/lib/ps2_recompiler.cpp b/ps2xRecomp/src/lib/ps2_recompiler.cpp index 01c5082..2ea7d54 100644 --- a/ps2xRecomp/src/lib/ps2_recompiler.cpp +++ b/ps2xRecomp/src/lib/ps2_recompiler.cpp @@ -241,6 +241,12 @@ namespace ps2recomp size_t passCount = 0; }; + struct StaticEntryTarget + { + uint32_t target = 0u; + bool isCall = false; + }; + EntryDiscoveryStats discoverAdditionalEntryPointsImpl( std::vector &functions, std::unordered_map> &decodedFunctions, @@ -269,11 +275,14 @@ namespace ps2recomp return false; }; - auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional + auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional { if (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL) { - return decodeAbsoluteJumpTarget(inst.address, inst.target); + StaticEntryTarget target{}; + target.target = decodeAbsoluteJumpTarget(inst.address, inst.target); + target.isCall = (inst.opcode == OPCODE_JAL); + return target; } if (inst.opcode == OPCODE_SPECIAL && @@ -369,7 +378,8 @@ namespace ps2recomp continue; } - uint32_t target = targetOpt.value(); + const StaticEntryTarget staticTarget = targetOpt.value(); + const uint32_t target = staticTarget.target; if ((target & 0x3) != 0 || !isExecutableAddress(target)) { @@ -381,10 +391,25 @@ namespace ps2recomp continue; } - const Function *containingFunction = findContainingFunction(target); - if (containingFunction && containingFunction->start == function.start) + const bool targetInCurrentFunction = std::any_of( + instructions.begin(), instructions.end(), + [&](const Instruction &candidate) + { return candidate.address == target; }); + if (targetInCurrentFunction && !staticTarget.isCall) { - // Internal branches within the same function are handled as labels/gotos and should not produce separate entry wrappers. + // jumps with the current decoded function remain labels/gotos. + continue; + } + + const Function *containingFunction = findContainingFunction(target); + if (targetInCurrentFunction) + { + PendingEntry pending{}; + pending.target = target; + pending.containingStart = function.start; + pending.containingEnd = function.end; + pendingEntries.push_back(pending); + pendingStarts.insert(target); continue; } diff --git a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java index 54b4608..af9b980 100644 --- a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java +++ b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java @@ -2,10 +2,21 @@ // @category PS2Recomp import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Instruction; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.FunctionIterator; import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.listing.InstructionIterator; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceIterator; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.model.symbol.SymbolIterator; +import ghidra.program.model.symbol.SymbolType; import java.io.File; import java.io.PrintWriter; @@ -91,6 +102,7 @@ public class ExportPS2Functions extends GhidraScript { long start; long endExclusive; long size; + boolean syntheticEntry = false; } private enum ClassificationKind { @@ -331,6 +343,178 @@ public class ExportPS2Functions extends GhidraScript { return selectors; } + private boolean isExecutableAddress(Address address) { + if (address == null) { + return false; + } + + MemoryBlock block = currentProgram.getMemory().getBlock(address); + return block != null && block.isExecute(); + } + + private boolean hasCallableLabelReference(Address address) { + if (address == null) { + return false; + } + + ReferenceIterator refs = currentProgram.getReferenceManager().getReferencesTo(address); + while (refs.hasNext()) { + Reference ref = refs.next(); + if (ref == null) { + continue; + } + + RefType type = ref.getReferenceType(); + if (type != null && type.isCall()) { + return true; + } + + Address from = ref.getFromAddress(); + if (from == null) { + continue; + } + + MemoryBlock fromBlock = currentProgram.getMemory().getBlock(from); + if (fromBlock == null || !fromBlock.isExecute()) { + return true; + } + } + + return false; + } + + private static String makeAnonymousEntryName(long start) { + return String.format("entry_%08x", start & 0xFFFFFFFFL); + } + + private List collectExecutableLabelRecords(List functionRecords) { + List labelRecords = new ArrayList<>(); + Set existingStarts = new HashSet<>(); + for (FunctionRecord record : functionRecords) { + existingStarts.add(record.start); + } + + SymbolIterator symbols = currentProgram.getSymbolTable().getSymbolIterator(true); + while (symbols.hasNext() && !monitor.isCancelled()) { + Symbol symbol = symbols.next(); + if (symbol == null || !symbol.isPrimary()) { + continue; + } + + if (symbol.getSymbolType() == SymbolType.FUNCTION) { + continue; + } + + Address address = symbol.getAddress(); + if (!isExecutableAddress(address)) { + continue; + } + + long start = address.getOffset(); + if (existingStarts.contains(start)) { + continue; + } + + Instruction instruction = currentProgram.getListing().getInstructionAt(address); + if (instruction == null) { + continue; + } + + if (!hasCallableLabelReference(address)) { + continue; + } + + FunctionRecord record = new FunctionRecord(); + record.name = symbol.getName(); + record.start = start; + record.syntheticEntry = true; + labelRecords.add(record); + existingStarts.add(start); + } + + AddressSet executableAddresses = new AddressSet(); + for (MemoryBlock block : currentProgram.getMemory().getBlocks()) { + if (block != null && block.isExecute()) { + executableAddresses.addRange(block.getStart(), block.getEnd()); + } + } + + InstructionIterator instructions = currentProgram.getListing().getInstructions(executableAddresses, true); + while (instructions.hasNext() && !monitor.isCancelled()) { + Instruction instruction = instructions.next(); + if (instruction == null) { + continue; + } + + Address address = instruction.getAddress(); + long start = address.getOffset(); + if (existingStarts.contains(start)) { + continue; + } + + if (!hasCallableLabelReference(address)) { + continue; + } + + FunctionRecord record = new FunctionRecord(); + record.name = makeAnonymousEntryName(start); + record.start = start; + record.syntheticEntry = true; + labelRecords.add(record); + existingStarts.add(start); + } + + if (labelRecords.isEmpty()) { + return labelRecords; + } + + List boundaries = new ArrayList<>(); + for (FunctionRecord record : functionRecords) { + boundaries.add(record.start); + } + for (FunctionRecord record : labelRecords) { + boundaries.add(record.start); + } + Collections.sort(boundaries); + + functionRecords.sort(Comparator.comparingLong(r -> r.start)); + for (FunctionRecord record : labelRecords) { + long endExclusive = 0L; + + for (FunctionRecord functionRecord : functionRecords) { + if (record.start > functionRecord.start && record.start < functionRecord.endExclusive) { + endExclusive = functionRecord.endExclusive; + break; + } + } + + if (endExclusive == 0L) { + Address startAddress = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(record.start); + MemoryBlock block = currentProgram.getMemory().getBlock(startAddress); + if (block != null) { + endExclusive = block.getEnd().getOffset() + 1L; + } + } + + for (Long boundary : boundaries) { + if (boundary > record.start && (endExclusive == 0L || boundary < endExclusive)) { + endExclusive = boundary; + break; + } + } + + if (endExclusive <= record.start) { + endExclusive = record.start + 4L; + } + + record.endExclusive = endExclusive; + record.size = record.endExclusive - record.start; + } + + labelRecords.sort(Comparator.comparingLong(r -> r.start)); + return labelRecords; + } + @Override public void run() throws Exception { File tomlFile = askFile("Choose output TOML config file", "Save"); @@ -340,11 +524,9 @@ public class ExportPS2Functions extends GhidraScript { 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; - } + csvFile = askFile("Choose output CSV file", "Save"); + if (csvFile == null) { + return; } FunctionManager fm = currentProgram.getFunctionManager(); @@ -380,23 +562,26 @@ public class ExportPS2Functions extends GhidraScript { } } - List stubSelectors = collectFunctionSelectors(stubNames, functionRecords, true); - List skipSelectors = collectFunctionSelectors(skipNames, functionRecords, true); + final int functionCount = functionRecords.size(); + List labelRecords = collectExecutableLabelRecords(functionRecords); + List exportRecords = new ArrayList<>(functionRecords); + exportRecords.addAll(labelRecords); + exportRecords.sort(Comparator.comparingLong(r -> r.start)); - 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 - ); - } + List stubSelectors = collectFunctionSelectors(stubNames, exportRecords, true); + List skipSelectors = collectFunctionSelectors(skipNames, exportRecords, true); + + try (PrintWriter writer = new PrintWriter(csvFile)) { + writer.println("Name,Start,End,Size"); + for (FunctionRecord record : exportRecords) { + writer.printf("%s,0x%08X,0x%08X,%d%n", + record.name, + record.start, + record.endExclusive, + record.size + ); } - } + } String programPath = currentProgram.getExecutablePath(); if (programPath == null) { @@ -405,7 +590,7 @@ public class ExportPS2Functions extends GhidraScript { programPath = normalizeWindowsDrivePath(programPath); File outputDir = tomlFile.getParentFile() == null ? new File("output") : new File(tomlFile.getParentFile(), "output"); - String ghidraCsvPath = (exportCsv && csvFile != null) ? csvFile.getAbsolutePath() : ""; + String ghidraCsvPath = csvFile.getAbsolutePath(); try (PrintWriter writer = new PrintWriter(tomlFile)) { writer.println("# Auto-generated by ExportPS2Functions.java"); @@ -437,7 +622,9 @@ public class ExportPS2Functions extends GhidraScript { writer.println(); writer.println("[ghidra_export]"); - writer.println("function_count = " + functionRecords.size()); + writer.println("function_count = " + functionCount); + writer.println("code_label_count = " + labelRecords.size()); + writer.println("csv_record_count = " + exportRecords.size()); writer.println("stub_count = " + stubSelectors.size()); writer.println("skip_count = " + skipSelectors.size()); writer.println("uncategorized_count = " + uncategorizedCount); @@ -445,9 +632,7 @@ public class ExportPS2Functions extends GhidraScript { writer.println("runtime_call_source = \"regex_only\""); } - if (exportCsv && csvFile != null) { - println(String.format("Exported %d functions to %s", functionRecords.size(), csvFile.getAbsolutePath())); - } + println(String.format("Exported %d functions and %d executable labels to %s", functionCount, labelRecords.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())); diff --git a/ps2xRecomp/tools/ghidra/ExportPS2Functions.py b/ps2xRecomp/tools/ghidra/ExportPS2Functions.py deleted file mode 100644 index b0e7ed8..0000000 --- a/ps2xRecomp/tools/ghidra/ExportPS2Functions.py +++ /dev/null @@ -1,43 +0,0 @@ -# Exports function addresses and names to CSV for PS2Recomp -# @category PS2Recomp - -import csv -import os - -from ghidra.program.model.symbol import SourceType - -def run(): - f = askFile("Choose output CSV file", "Save") - - if f is None: - return - - with open(f.getAbsolutePath(), 'w') as csvfile: - writer = csv.writer(csvfile) - writer.writerow(['Name', 'Start', 'End', 'Size']) - - fm = currentProgram.getFunctionManager() - functions = fm.getFunctions(True) # True iterates forward wtf kkkkk - - count = 0 - for func in functions: - name = func.getName() - start = func.getEntryPoint().getOffset() - - body = func.getBody() - max_addr = body.getMaxAddress().getOffset() - - size = body.getNumAddresses() - - writer.writerow([ - name, - "0x{:08X}".format(start), - "0x{:08X}".format(max_addr + 1), # End address is exclusive - size - ]) - count += 1 - - print("Exported {} functions to {}".format(count, f.getAbsolutePath())) - -if __name__ == "__main__": - run() diff --git a/ps2xRuntime/include/ps2_call_list.h b/ps2xRuntime/include/ps2_call_list.h index e8a5165..818b5ea 100644 --- a/ps2xRuntime/include/ps2_call_list.h +++ b/ps2xRuntime/include/ps2_call_list.h @@ -107,6 +107,7 @@ X(GsPutIMR) \ X(iGsPutIMR) \ X(SetVSyncFlag) \ + X(SetSyscall) \ X(GsSetVideoMode) \ \ X(GetOsdConfigParam) \ diff --git a/ps2xRuntime/include/ps2_syscalls.h b/ps2xRuntime/include/ps2_syscalls.h index 6a78c84..f6122ae 100644 --- a/ps2xRuntime/include/ps2_syscalls.h +++ b/ps2xRuntime/include/ps2_syscalls.h @@ -22,6 +22,8 @@ namespace ps2_syscalls #undef PS2_DECLARE_SYSCALL bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); + void dispatchDmacHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause); + void initializeGuestKernelState(uint8_t *rdram); void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encodedSyscallId); void notifyRuntimeStop(); void WaitVSyncTick(uint8_t *rdram, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 713d6ec..c08d821 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -19,6 +19,11 @@ #include "ps2_gs_gpu.h" #include +namespace ps2_stubs +{ + void resetSifState(); +} + #define ELF_MAGIC 0x464C457F // "\x7FELF" in little endian #define ET_EXEC 2 // Executable file #define EM_MIPS 8 // MIPS architecture @@ -1701,7 +1706,9 @@ void PS2Runtime::HandleIntegerOverflow(R5900Context *ctx) void PS2Runtime::run() { m_stopRequested.store(false, std::memory_order_relaxed); + ps2_stubs::resetSifState(); ps2_stubs::resetGsSyncVCallbackState(); + ps2_syscalls::initializeGuestKernelState(m_memory.getRDRAM()); m_cpuContext.r[4] = _mm_setzero_si128(); m_cpuContext.r[5] = _mm_setzero_si128(); m_cpuContext.r[29] = _mm_set_epi64x(0, static_cast(PS2_RAM_SIZE - 0x10u)); diff --git a/ps2xRuntime/src/lib/ps2_syscalls.cpp b/ps2xRuntime/src/lib/ps2_syscalls.cpp index 0e3950e..04fe125 100644 --- a/ps2xRuntime/src/lib/ps2_syscalls.cpp +++ b/ps2xRuntime/src/lib/ps2_syscalls.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,11 @@ namespace ps2_syscalls bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + if (dispatchSyscallOverride(syscallNumber, rdram, ctx, runtime)) + { + return true; + } + switch (syscallNumber) { case 0x01: @@ -272,7 +278,7 @@ namespace ps2_syscalls SetVSyncFlag(rdram, ctx, runtime); return true; case 0x74: - RegisterExitHandler(rdram, ctx, runtime); + SetSyscall(rdram, ctx, runtime); return true; case 0x76: case static_cast(-0x76): @@ -391,5 +397,14 @@ namespace ps2_syscalls g_alarms.clear(); } g_alarm_cv.notify_all(); + + { + std::lock_guard lock(g_exit_handler_mutex); + g_exit_handlers.clear(); + } + { + std::lock_guard lock(g_syscall_override_mutex); + g_syscall_overrides.clear(); + } } } diff --git a/ps2xRuntime/src/lib/stubs/ps2_stubs_misc.inl b/ps2xRuntime/src/lib/stubs/ps2_stubs_misc.inl index 51cd30d..8c9dcbf 100644 --- a/ps2xRuntime/src/lib/stubs/ps2_stubs_misc.inl +++ b/ps2xRuntime/src/lib/stubs/ps2_stubs_misc.inl @@ -2627,6 +2627,55 @@ namespace uint32_t g_sifCmdBuffer = 0u; uint32_t g_sifSysCmdBuffer = 0u; bool g_sifCmdInitialized = false; + uint32_t g_sifGetRegLogCount = 0u; + uint32_t g_sifSetRegLogCount = 0u; + + constexpr uint32_t kSifRegBootStatus = 0x4u; + constexpr uint32_t kSifRegMainAddr = 0x80000000u; + constexpr uint32_t kSifRegSubAddr = 0x80000001u; + constexpr uint32_t kSifRegMsCom = 0x80000002u; + constexpr uint32_t kSifBootReadyMask = 0x00020000u; + + void seedDefaultSifRegsLocked() + { + g_sifRegs.clear(); + g_sifSregs.clear(); + g_sifCmdHandlers.clear(); + g_sifCmdBuffer = 0u; + g_sifSysCmdBuffer = 0u; + g_sifCmdInitialized = false; + g_sifGetRegLogCount = 0u; + g_sifSetRegLogCount = 0u; + + g_sifRegs[kSifRegBootStatus] = kSifBootReadyMask; + g_sifRegs[kSifRegMainAddr] = 0u; + g_sifRegs[kSifRegSubAddr] = 0u; + g_sifRegs[kSifRegMsCom] = 0u; + } + + bool shouldTraceSifReg(uint32_t reg) + { + switch (reg) + { + case 0x2u: + case 0x4u: + case 0x80000000u: + case 0x80000001u: + case 0x80000002u: + return true; + default: + return false; + } + } + + struct SifStateInitializer + { + SifStateInitializer() + { + std::lock_guard lock(g_sifCmdStateMutex); + seedDefaultSifRegsLocked(); + } + } g_sifStateInitializer; uint32_t allocateSifDmaTransferId() { @@ -2744,6 +2793,12 @@ namespace } } +void resetSifState() +{ + std::lock_guard lock(g_sifCmdStateMutex); + seedDefaultSifRegsLocked(); +} + void sceSifAddCmdHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t cid = getRegU32(ctx, 4); @@ -2796,8 +2851,7 @@ void sceSifExecRequest(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) void sceSifExitCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { std::lock_guard lock(g_sifCmdStateMutex); - g_sifCmdInitialized = false; - g_sifCmdHandlers.clear(); + seedDefaultSifRegsLocked(); setReturnS32(ctx, 0); } @@ -3054,6 +3108,7 @@ void sceSifGetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t reg = getRegU32(ctx, 4); uint32_t value = 0u; + bool shouldLog = false; { std::lock_guard lock(g_sifCmdStateMutex); auto it = g_sifRegs.find(reg); @@ -3061,6 +3116,21 @@ void sceSifGetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { value = it->second; } + shouldLog = shouldTraceSifReg(reg) && g_sifGetRegLogCount < 128u; + if (shouldLog) + { + ++g_sifGetRegLogCount; + } + } + if (shouldLog) + { + auto flags = std::cerr.flags(); + std::cerr << "[sceSifGetReg] reg=0x" << std::hex << reg + << " value=0x" << value + << " pc=0x" << (ctx ? ctx->pc : 0u) + << " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u) + << std::dec << std::endl; + std::cerr.flags(flags); } setReturnU32(ctx, value); } @@ -3259,6 +3329,8 @@ void sceSifSetDma(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) return; } + ps2_syscalls::dispatchDmacHandlersForCause(rdram, runtime, 5u); + setReturnS32(ctx, static_cast(allocateSifDmaTransferId())); } @@ -3272,6 +3344,7 @@ void sceSifSetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) const uint32_t reg = getRegU32(ctx, 4); const uint32_t value = getRegU32(ctx, 5); uint32_t prev = 0u; + bool shouldLog = false; { std::lock_guard lock(g_sifCmdStateMutex); auto it = g_sifRegs.find(reg); @@ -3280,6 +3353,22 @@ void sceSifSetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) prev = it->second; } g_sifRegs[reg] = value; + shouldLog = shouldTraceSifReg(reg) && g_sifSetRegLogCount < 128u; + if (shouldLog) + { + ++g_sifSetRegLogCount; + } + } + if (shouldLog) + { + auto flags = std::cerr.flags(); + std::cerr << "[sceSifSetReg] reg=0x" << std::hex << reg + << " prev=0x" << prev + << " value=0x" << value + << " pc=0x" << (ctx ? ctx->pc : 0u) + << " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u) + << std::dec << std::endl; + std::cerr.flags(flags); } setReturnU32(ctx, prev); } diff --git a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl index 4e4ab48..4878f8c 100644 --- a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl +++ b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl @@ -101,9 +101,9 @@ static std::shared_ptr lookupEventFlagInfo(int eid) static void setRegU32(R5900Context *ctx, int reg, uint32_t value) { - if (reg < 0 || reg > 31) + if (!ctx || reg < 0 || reg > 31) return; - ctx->r[reg] = _mm_set_epi32(0, 0, 0, value); + SET_GPR_U32(ctx, reg, value); } static std::chrono::microseconds alarmTicksToDuration(uint16_t ticks) @@ -268,6 +268,34 @@ static bool readStackU32(uint8_t *rdram, uint32_t sp, uint32_t offset, uint32_t return true; } +enum class RpcInvokeExitReason +{ + Returned, + NullPc, + MissingFunction, + StepLimit, + SamePcLimit +}; + +static const char *rpcInvokeExitReasonName(RpcInvokeExitReason reason) +{ + switch (reason) + { + case RpcInvokeExitReason::Returned: + return "returned"; + case RpcInvokeExitReason::NullPc: + return "null-pc"; + case RpcInvokeExitReason::MissingFunction: + return "missing-function"; + case RpcInvokeExitReason::StepLimit: + return "step-limit"; + case RpcInvokeExitReason::SamePcLimit: + return "same-pc-limit"; + default: + return "unknown"; + } +} + 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) { @@ -307,6 +335,7 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run uint32_t steps = 0u; uint32_t lastPc = 0xFFFFFFFFu; uint32_t samePcCount = 0u; + RpcInvokeExitReason exitReason = RpcInvokeExitReason::MissingFunction; while (tmp.pc != 0u && tmp.pc != kRpcInvokeReturnSentinel && runtime->hasFunction(tmp.pc) && @@ -318,6 +347,7 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run ++samePcCount; if (samePcCount > 0x2000u) { + exitReason = RpcInvokeExitReason::SamePcLimit; break; } } @@ -336,7 +366,41 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run { *outV0 = getRegU32(&tmp, 2); } - return true; + + if (tmp.pc == kRpcInvokeReturnSentinel) + { + return true; + } + + if (tmp.pc == 0u) + { + exitReason = RpcInvokeExitReason::NullPc; + } + else if (steps >= kRpcInvokeMaxSteps) + { + exitReason = RpcInvokeExitReason::StepLimit; + } + else if (!runtime->hasFunction(tmp.pc)) + { + exitReason = RpcInvokeExitReason::MissingFunction; + } + + static std::atomic s_rpcInvokeFailureLogs{0u}; + constexpr uint32_t kMaxRpcInvokeFailureLogs = 64u; + const uint32_t logIndex = s_rpcInvokeFailureLogs.fetch_add(1u, std::memory_order_relaxed); + if (logIndex < kMaxRpcInvokeFailureLogs) + { + std::cerr << "[SyscallOverride:invoke-failed]" + << " func=0x" << std::hex << funcAddr + << " exitPc=0x" << tmp.pc + << " ra=0x" << getRegU32(&tmp, 31) + << std::dec + << " steps=" << steps + << " reason=" << rpcInvokeExitReasonName(exitReason) + << std::endl; + } + + return false; } static uint32_t rpcAllocPacketAddr(uint8_t *rdram) diff --git a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_state.inl b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_state.inl index 12d4f78..341c121 100644 --- a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_state.inl +++ b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_state.inl @@ -416,6 +416,15 @@ static bool g_bootmode_initialized = false; static uint32_t g_bootmode_pool_offset = 0; static std::unordered_map g_bootmode_addresses; +static std::mutex g_syscall_override_mutex; +static std::unordered_map g_syscall_overrides; +static std::unordered_set g_syscall_mirror_addrs; + +static constexpr uint32_t kGuestSyscallTableGuestBase = 0x80011F80u; +static constexpr uint32_t kGuestSyscallTablePhysBase = kGuestSyscallTableGuestBase & 0x1FFFFFFFu; +static constexpr uint32_t kGuestSyscallMirrorLimit = 0x00080000u; +static constexpr uint32_t kGuestSyscallTableProbeBase = 0x000002F0u; + static std::mutex g_tls_mutex; static uint32_t g_tls_index = 0; diff --git a/ps2xRuntime/src/lib/syscalls/ps2_syscalls_interrupt.inl b/ps2xRuntime/src/lib/syscalls/ps2_syscalls_interrupt.inl index 6f482e2..13ea66f 100644 --- a/ps2xRuntime/src/lib/syscalls/ps2_syscalls_interrupt.inl +++ b/ps2xRuntime/src/lib/syscalls/ps2_syscalls_interrupt.inl @@ -138,6 +138,90 @@ static void dispatchIntcHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, ui } } +void dispatchDmacHandlersForCause(uint8_t *rdram, PS2Runtime *runtime, uint32_t cause) +{ + if (!rdram || !runtime) + { + return; + } + + std::vector handlers; + { + std::lock_guard lock(g_irq_handler_mutex); + if (cause < 32u && (g_enabled_dmac_mask & (1u << cause)) == 0u) + { + return; + } + + handlers.reserve(g_dmacHandlers.size()); + for (const auto &[id, info] : g_dmacHandlers) + { + (void)id; + if (!info.enabled) + { + continue; + } + if (info.cause != cause) + { + continue; + } + if (info.handler == 0u) + { + continue; + } + handlers.push_back(info); + } + std::sort(handlers.begin(), handlers.end(), [](const IrqHandlerInfo &a, const IrqHandlerInfo &b) { + return a.order < b.order; + }); + } + + for (const IrqHandlerInfo &info : handlers) + { + if (!runtime->hasFunction(info.handler)) + { + continue; + } + + try + { + R5900Context irqCtx{}; + const uint32_t sp = (info.sp != 0u) ? info.sp : (PS2_RAM_SIZE - 0x10u); + SET_GPR_U32(&irqCtx, 28, info.gp); + SET_GPR_U32(&irqCtx, 29, sp); + SET_GPR_U32(&irqCtx, 31, 0u); + SET_GPR_U32(&irqCtx, 4, cause); + SET_GPR_U32(&irqCtx, 5, info.arg); + SET_GPR_U32(&irqCtx, 6, 0u); + SET_GPR_U32(&irqCtx, 7, 0u); + irqCtx.pc = info.handler; + + while (irqCtx.pc != 0u && runtime && !runtime->isStopRequested()) + { + PS2Runtime::RecompiledFunction step = runtime->lookupFunction(irqCtx.pc); + if (!step) + { + break; + } + step(rdram, &irqCtx, runtime); + } + } + catch (const ThreadExitException &) + { + } + catch (const std::exception &e) + { + static uint32_t warnCount = 0; + if (warnCount < 8u) + { + std::cerr << "[DMAC] handler 0x" << std::hex << info.handler + << " threw exception: " << e.what() << std::dec << std::endl; + ++warnCount; + } + } + } +} + static uint64_t signalVSyncFlag(uint8_t *rdram) { VSyncFlagRegistration reg{}; diff --git a/ps2xRuntime/src/lib/syscalls/ps2_syscalls_system.inl b/ps2xRuntime/src/lib/syscalls/ps2_syscalls_system.inl index e835807..3035932 100644 --- a/ps2xRuntime/src/lib/syscalls/ps2_syscalls_system.inl +++ b/ps2xRuntime/src/lib/syscalls/ps2_syscalls_system.inl @@ -264,6 +264,239 @@ void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encod setReturnS32(ctx, 0); } +static uint32_t computeBuiltinFindAddressResult(uint8_t *rdram, + uint32_t originalStart, + uint32_t originalEnd, + uint32_t target); + +static bool dispatchSyscallOverride(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) +{ + uint32_t handler = 0u; + { + std::lock_guard lock(g_syscall_override_mutex); + auto it = g_syscall_overrides.find(syscallNumber); + if (it == g_syscall_overrides.end()) + { + return false; + } + handler = it->second; + } + + if (!runtime || !ctx || handler == 0u) + { + return false; + } + + const uint32_t overrideA0 = getRegU32(ctx, 4); + const uint32_t overrideA1 = getRegU32(ctx, 5); + const uint32_t overrideA2 = getRegU32(ctx, 6); + const uint32_t overrideA3 = getRegU32(ctx, 7); + const uint32_t overridePc = ctx->pc; + const uint32_t overrideRa = getRegU32(ctx, 31); + + thread_local std::vector s_activeSyscallOverrides; + if (std::find(s_activeSyscallOverrides.begin(), s_activeSyscallOverrides.end(), syscallNumber) != s_activeSyscallOverrides.end()) + { + static std::atomic s_reentrantLogs{0u}; + constexpr uint32_t kMaxReentrantLogs = 32u; + const uint32_t logIndex = s_reentrantLogs.fetch_add(1u, std::memory_order_relaxed); + if (logIndex < kMaxReentrantLogs) + { + std::cerr << "[SyscallOverride:reentrant]" + << " syscall=0x" << std::hex << syscallNumber + << " handler=0x" << handler + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl; + } + return false; + } + + s_activeSyscallOverrides.push_back(syscallNumber); + struct ScopedActiveOverride + { + std::vector &active; + ~ScopedActiveOverride() + { + if (!active.empty()) + { + active.pop_back(); + } + } + } scopedActiveOverride{s_activeSyscallOverrides}; + + uint32_t retV0 = 0u; + const bool invoked = rpcInvokeFunction(rdram, + ctx, + runtime, + handler, + getRegU32(ctx, 4), + getRegU32(ctx, 5), + getRegU32(ctx, 6), + getRegU32(ctx, 7), + &retV0); + + if (syscallNumber == 0x83u) + { + const uint32_t builtinRet = computeBuiltinFindAddressResult(rdram, overrideA0, overrideA1, overrideA2); + const bool mismatch = (retV0 != builtinRet); + + static std::atomic s_findAddressOverrideLogs{0u}; + static std::atomic s_findAddressOverrideMismatchLogs{0u}; + constexpr uint32_t kMaxFindAddressOverrideLogs = 64u; + constexpr uint32_t kMaxFindAddressOverrideMismatchLogs = 128u; + + const uint32_t logIndex = s_findAddressOverrideLogs.fetch_add(1u, std::memory_order_relaxed); + const uint32_t mismatchIndex = mismatch + ? s_findAddressOverrideMismatchLogs.fetch_add(1u, std::memory_order_relaxed) + : 0u; + if (logIndex < kMaxFindAddressOverrideLogs || + (mismatch && mismatchIndex < kMaxFindAddressOverrideMismatchLogs)) + { + const uint32_t guestMinus20c = (retV0 != 0u) ? (retV0 - 0x20Cu) : 0u; + const uint32_t guestMinus168 = (retV0 != 0u) ? (retV0 - 0x168u) : 0u; + const uint32_t builtinMinus20c = (builtinRet != 0u) ? (builtinRet - 0x20Cu) : 0u; + const uint32_t builtinMinus168 = (builtinRet != 0u) ? (builtinRet - 0x168u) : 0u; + + std::cerr << "[Syscall83:override]" + << " handler=0x" << std::hex << handler + << " invoked=" << (invoked ? "true" : "false") + << " pc=0x" << overridePc + << " ra=0x" << overrideRa + << " a0=0x" << overrideA0 + << " a1=0x" << overrideA1 + << " a2=0x" << overrideA2 + << " a3=0x" << overrideA3 + << " guestRet=0x" << retV0 + << " builtinRet=0x" << builtinRet + << " guest-20c=0x" << guestMinus20c + << " builtin-20c=0x" << builtinMinus20c + << " guest-168=0x" << guestMinus168 + << " builtin-168=0x" << builtinMinus168 + << " match=" << (mismatch ? "false" : "true") + << std::dec << std::endl; + } + } + + if (!invoked) + { + static std::atomic s_fallbackLogs{0u}; + constexpr uint32_t kMaxFallbackLogs = 64u; + const uint32_t logIndex = s_fallbackLogs.fetch_add(1u, std::memory_order_relaxed); + if (logIndex < kMaxFallbackLogs) + { + std::cerr << "[SyscallOverride:fallback]" + << " syscall=0x" << std::hex << syscallNumber + << " handler=0x" << handler + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl; + } + return false; + } + + setReturnU32(ctx, retV0); + return true; +} + +static bool tryResolveGuestSyscallMirrorAddr(uint32_t syscallIndex, uint32_t &guestAddr) +{ + const int64_t offsetBytes = + static_cast(static_cast(syscallIndex)) * static_cast(sizeof(uint32_t)); + const int64_t guestAddr64 = static_cast(kGuestSyscallTablePhysBase) + offsetBytes; + if (guestAddr64 < 0 || (guestAddr64 + static_cast(sizeof(uint32_t))) > static_cast(kGuestSyscallMirrorLimit)) + { + return false; + } + + guestAddr = static_cast(guestAddr64); + return true; +} + +static void writeGuestKernelWord(uint8_t *rdram, uint32_t guestAddr, uint32_t value) +{ + if (!rdram) + { + return; + } + + if (uint8_t *ptr = getMemPtr(rdram, guestAddr)) + { + std::memcpy(ptr, &value, sizeof(value)); + } +} + +static void seedGuestSyscallTableProbeLocked(uint8_t *rdram) +{ + writeGuestKernelWord(rdram, kGuestSyscallTableProbeBase + 0u, kGuestSyscallTableGuestBase >> 16); + writeGuestKernelWord(rdram, kGuestSyscallTableProbeBase + 8u, kGuestSyscallTableGuestBase & 0xFFFFu); + g_syscall_mirror_addrs.insert(kGuestSyscallTableProbeBase + 0u); + g_syscall_mirror_addrs.insert(kGuestSyscallTableProbeBase + 8u); +} + +static void mirrorGuestSyscallEntryLocked(uint8_t *rdram, uint32_t syscallIndex, uint32_t handler) +{ + uint32_t guestAddr = 0u; + if (!tryResolveGuestSyscallMirrorAddr(syscallIndex, guestAddr)) + { + return; + } + + writeGuestKernelWord(rdram, guestAddr, handler); + if (handler == 0u) + { + g_syscall_mirror_addrs.erase(guestAddr); + return; + } + + g_syscall_mirror_addrs.insert(guestAddr); +} + +void initializeGuestKernelState(uint8_t *rdram) +{ + if (!rdram) + { + return; + } + + std::lock_guard lock(g_syscall_override_mutex); + for (uint32_t guestAddr : g_syscall_mirror_addrs) + { + writeGuestKernelWord(rdram, guestAddr, 0u); + } + g_syscall_mirror_addrs.clear(); + + seedGuestSyscallTableProbeLocked(rdram); + + for (const auto &entry : g_syscall_overrides) + { + mirrorGuestSyscallEntryLocked(rdram, entry.first, entry.second); + } +} + +void SetSyscall(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) +{ + (void)runtime; + const uint32_t syscallIndex = getRegU32(ctx, 4); + const uint32_t handler = getRegU32(ctx, 5); + + { + std::lock_guard lock(g_syscall_override_mutex); + if (handler == 0u) + { + g_syscall_overrides.erase(syscallIndex); + } + else + { + g_syscall_overrides[syscallIndex] = handler; + } + + mirrorGuestSyscallEntryLocked(rdram, syscallIndex, handler); + } + + setReturnS32(ctx, 0); +} + // 0x3C SetupThread // args: $a0 = gp, $a1 = stack, $a2 = stack_size, $a3 = args, $t0 = root_func void SetupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -364,6 +597,153 @@ static inline uint32_t normalizeKernelAlias(uint32_t addr) return addr; } +static uint32_t computeBuiltinFindAddressResult(uint8_t *rdram, + uint32_t originalStart, + uint32_t originalEnd, + uint32_t target) +{ + uint32_t start = (originalStart + 3u) & ~0x3u; + uint32_t end = originalEnd & ~0x3u; + if (start >= end) + { + return 0u; + } + + const uint32_t targetNorm = normalizeKernelAlias(target); + for (uint32_t addr = start; addr < end; addr += sizeof(uint32_t)) + { + const uint8_t *entryPtr = getConstMemPtr(rdram, addr); + if (!entryPtr) + { + break; + } + + uint32_t entry = 0u; + std::memcpy(&entry, entryPtr, sizeof(entry)); + if (entry == target || normalizeKernelAlias(entry) == targetNorm) + { + return addr; + } + } + + return 0u; +} + +struct FindAddressWordSample +{ + uint32_t addr = 0u; + uint32_t value = 0u; +}; + +struct FindAddressMatchSample +{ + uint32_t addr = 0u; + uint32_t value = 0u; + bool aliasOnly = false; +}; + +static void logFindAddressDiagnostics(uint32_t callerPc, + uint32_t originalStart, + uint32_t originalEnd, + uint32_t alignedStart, + uint32_t alignedEnd, + uint32_t target, + uint32_t targetNorm, + bool found, + uint32_t resultAddr, + uint32_t scannedWords, + bool allZero, + bool aborted, + uint32_t abortedAddr, + const FindAddressWordSample *firstWords, + uint32_t firstWordCount, + const FindAddressWordSample *nonZeroWords, + uint32_t nonZeroWordCount, + const FindAddressMatchSample *matches, + uint32_t matchCount) +{ + static std::atomic s_findAddressHitLogs{0u}; + static std::atomic s_findAddressMissLogs{0u}; + constexpr uint32_t kMaxFindAddressHitLogs = 16u; + constexpr uint32_t kMaxFindAddressMissLogs = 128u; + + std::atomic &counter = found ? s_findAddressHitLogs : s_findAddressMissLogs; + const uint32_t logIndex = counter.fetch_add(1u, std::memory_order_relaxed); + const uint32_t logLimit = found ? kMaxFindAddressHitLogs : kMaxFindAddressMissLogs; + if (logIndex >= logLimit) + { + return; + } + + std::cerr << "[FindAddress:" << (found ? "hit" : "miss") << "]" + << " pc=0x" << std::hex << callerPc + << " start=0x" << originalStart + << " end=0x" << originalEnd + << " alignedStart=0x" << alignedStart + << " alignedEnd=0x" << alignedEnd + << " target=0x" << target + << " targetNorm=0x" << targetNorm + << " result=0x" << resultAddr + << std::dec + << " scannedWords=" << scannedWords + << " allZero=" << (allZero ? "true" : "false") + << " aborted=" << (aborted ? "true" : "false"); + if (aborted) + { + std::cerr << " abortedAddr=0x" << std::hex << abortedAddr << std::dec; + } + std::cerr << std::endl; + + std::cerr << " firstWords:"; + if (firstWordCount == 0u) + { + std::cerr << " none"; + } + else + { + for (uint32_t i = 0; i < firstWordCount; ++i) + { + std::cerr << " [0x" << std::hex << firstWords[i].addr + << "]=0x" << firstWords[i].value; + } + std::cerr << std::dec; + } + std::cerr << std::endl; + + std::cerr << " nonZeroSample:"; + if (nonZeroWordCount == 0u) + { + std::cerr << " none"; + } + else + { + for (uint32_t i = 0; i < nonZeroWordCount; ++i) + { + std::cerr << " [0x" << std::hex << nonZeroWords[i].addr + << "]=0x" << nonZeroWords[i].value; + } + std::cerr << std::dec; + } + std::cerr << std::endl; + + std::cerr << " matches:"; + if (matchCount == 0u) + { + std::cerr << " none"; + } + else + { + for (uint32_t i = 0; i < matchCount; ++i) + { + std::cerr << " [0x" << std::hex << matches[i].addr + << "]=0x" << matches[i].value + << (matches[i].aliasOnly ? "(alias)" : "(exact)"); + } + std::cerr << std::dec; + } + std::cerr << std::endl; +} + // 0x83 FindAddress: // - a0: table start (inclusive) // - a1: table end (exclusive) @@ -373,10 +753,17 @@ void FindAddress(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)runtime; - uint32_t start = getRegU32(ctx, 4); - uint32_t end = getRegU32(ctx, 5); + constexpr uint32_t kFindAddressWordSamples = 8u; + constexpr uint32_t kFindAddressMatchSamples = 4u; + + const uint32_t originalStart = getRegU32(ctx, 4); + const uint32_t originalEnd = getRegU32(ctx, 5); const uint32_t target = getRegU32(ctx, 6); const uint32_t targetNorm = normalizeKernelAlias(target); + const uint32_t callerPc = ctx->pc; + + uint32_t start = originalStart; + uint32_t end = originalEnd; // Word-scan semantics: align the search window to uint32 boundaries. start = (start + 3u) & ~0x3u; @@ -384,28 +771,107 @@ void FindAddress(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) if (start >= end) { + logFindAddressDiagnostics(callerPc, + originalStart, + originalEnd, + start, + end, + target, + targetNorm, + false, + 0u, + 0u, + true, + false, + 0u, + nullptr, + 0u, + nullptr, + 0u, + nullptr, + 0u); setReturnU32(ctx, 0u); return; } + FindAddressWordSample firstWords[kFindAddressWordSamples]{}; + FindAddressWordSample nonZeroWords[kFindAddressWordSamples]{}; + FindAddressMatchSample matches[kFindAddressMatchSamples]{}; + uint32_t firstWordCount = 0u; + uint32_t nonZeroWordCount = 0u; + uint32_t matchCount = 0u; + uint32_t scannedWords = 0u; + uint32_t resultAddr = 0u; + uint32_t abortedAddr = 0u; + bool aborted = false; + bool allZero = true; + bool foundMatch = false; + for (uint32_t addr = start; addr < end; addr += sizeof(uint32_t)) { const uint8_t *entryPtr = getConstMemPtr(rdram, addr); if (!entryPtr) { + aborted = true; + abortedAddr = addr; break; } uint32_t entry = 0; std::memcpy(&entry, entryPtr, sizeof(entry)); - if (entry == target || normalizeKernelAlias(entry) == targetNorm) + ++scannedWords; + + if (firstWordCount < kFindAddressWordSamples) { - setReturnU32(ctx, addr); - return; + firstWords[firstWordCount++] = {addr, entry}; + } + + if (entry != 0u) + { + allZero = false; + if (nonZeroWordCount < kFindAddressWordSamples) + { + nonZeroWords[nonZeroWordCount++] = {addr, entry}; + } + } + + const bool exactMatch = (entry == target); + const bool aliasMatch = !exactMatch && (normalizeKernelAlias(entry) == targetNorm); + if (exactMatch || aliasMatch) + { + if (!foundMatch) + { + resultAddr = addr; + foundMatch = true; + } + if (matchCount < kFindAddressMatchSamples) + { + matches[matchCount++] = {addr, entry, aliasMatch}; + } } } - setReturnU32(ctx, 0u); + logFindAddressDiagnostics(callerPc, + originalStart, + originalEnd, + start, + end, + target, + targetNorm, + foundMatch, + resultAddr, + scannedWords, + allZero, + aborted, + abortedAddr, + firstWords, + firstWordCount, + nonZeroWords, + nonZeroWordCount, + matches, + matchCount); + + setReturnU32(ctx, resultAddr); } void Deci2Call(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index f2e62f9..689684b 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -757,6 +757,35 @@ void register_code_generator_tests() t.IsTrue(generated.find("if (ctx->pc != 0xA008u) { return; }") != std::string::npos, "JAL should check return PC"); }); + tc.Run("trailing JAL without decoded delay slot still emits call flow", [](TestCase &t) { + Function func; + func.name = "jal_truncated"; + func.start = 0xA100; + func.end = 0xA108; + func.isRecompiled = true; + func.isStub = false; + + Symbol targetSym; + targetSym.name = "some_func"; + targetSym.address = 0xB000; + targetSym.isFunction = true; + + Instruction jal = makeJal(0xA100, 0xB000); + + CodeGenerator gen({targetSym}, {}); + std::string generated = gen.generateFunction(func, {jal}, false); + printGeneratedCode("trailing JAL without decoded delay slot still emits call flow", generated); + + t.IsTrue(generated.find("SET_GPR_U32(ctx, 31, 0xA108u);") != std::string::npos, + "truncated trailing JAL should still set RA"); + t.IsTrue(generated.find("some_func(rdram, ctx, runtime);") != std::string::npos, + "truncated trailing JAL should still emit the call"); + t.IsTrue(generated.find("if (ctx->pc != 0xA108u) { return; }") != std::string::npos, + "truncated trailing JAL should still enforce fallthrough"); + t.IsTrue(generated.find("// JAL 0xB000 - Handled by branch logic") == std::string::npos, + "truncated trailing JAL must not degrade to comment-only output"); + }); + tc.Run("JAL to internal target becomes goto", [](TestCase &t) { Function func; func.name = "jal_internal"; @@ -900,6 +929,34 @@ void register_code_generator_tests() t.IsTrue(generated.find("case 0x1308u: goto label_1308;") != std::string::npos, "switch should include return address from internal JAL"); }); + tc.Run("trailing JR $31 without decoded delay slot still emits return flow", [](TestCase &t) { + Function func; + func.name = "jr_ra_truncated"; + func.start = 0x1500; + func.end = 0x1540; + func.isRecompiled = true; + func.isStub = false; + + Instruction jal = makeJal(0x1500, 0x1510); + Instruction jalDelay = makeNop(0x1504); + Instruction atReturn = makeNop(0x1508); + Instruction atTarget = makeNop(0x1510); + Instruction jr = makeJr(0x1514, 31); + + CodeGenerator gen({}, {}); + std::string generated = gen.generateFunction(func, {jal, jalDelay, atReturn, atTarget, jr}, false); + printGeneratedCode("trailing JR $31 without decoded delay slot still emits return flow", generated); + + t.IsTrue(generated.find("uint32_t jumpTarget = GPR_U32(ctx, 31);") != std::string::npos, + "truncated trailing JR should still read the return target"); + t.IsTrue(generated.find("switch (jumpTarget)") != std::string::npos, + "truncated trailing JR should still emit the return-target switch"); + t.IsTrue(generated.find("case 0x1508u: goto label_1508;") != std::string::npos, + "truncated trailing JR should still include internal return targets"); + t.IsTrue(generated.find("// JR $31 - Handled by branch logic") == std::string::npos, + "truncated trailing JR must not degrade to comment-only output"); + }); + tc.Run("JR non-RA emits switch for in-function jump targets", [](TestCase &t) { Function func; func.name = "jr_non_ra_switch"; @@ -1100,6 +1157,35 @@ void register_code_generator_tests() "jalr fallback should not dispatch directly to tail-jump delay slot"); }); + tc.Run("VU random helpers emit line comments on separate lines", [](TestCase &t) { + CodeGenerator gen({}, {}); + + Instruction inst{}; + inst.rd = 7; + inst.vectorInfo.fsf = 2; + + std::string vrnext = gen.translateVU_VRNEXT(inst); + printGeneratedCode("VU random helpers emit line comments on separate lines - VRNEXT", vrnext); + t.IsTrue(vrnext.find("// Simple LFSR-based random number generation (PS2-like behavior)\n" + " uint32_t feedback") != std::string::npos, + "VRNEXT should place the generated line comment on its own line"); + + std::string vrinit = gen.translateVU_VRINIT(inst); + printGeneratedCode("VU random helpers emit line comments on separate lines - VRINIT", vrinit); + t.IsTrue(vrinit.find("// PS2 uses a specific LFSR initialization pattern\n" + " if (seed == 0) seed = 1;") != std::string::npos, + "VRINIT should place the generated line comment on its own line"); + + std::string vrxor = gen.translateVU_VRXOR(inst); + printGeneratedCode("VU random helpers emit line comments on separate lines - VRXOR", vrxor); + t.IsTrue(vrxor.find("// XOR the current random value with the data from the VU vector register\n" + " __m128i xored") != std::string::npos, + "VRXOR should keep the XOR comment on its own line"); + t.IsTrue(vrxor.find("// Apply a simple mixing function similar to PS2's LFSR\n" + " __m128i mixed") != std::string::npos, + "VRXOR should keep the LFSR comment on its own line"); + }); + tc.Run("resolveStubTarget allows leading underscore alias", [](TestCase &t) { t.Equals(PS2Recompiler::resolveStubTarget("_rand"), StubTarget::Stub, "_rand should resolve via rand stub alias"); diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index a218622..bbaaacd 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -311,6 +311,43 @@ void register_ps2_recompiler_tests() } }); + tc.Run("same-function JAL targets get entry wrappers but J targets stay labels", [](TestCase &t) { + std::vector
sections = { + {".text", 0x1000u, 0x40u, 0u, true, false, false, true, nullptr} + }; + + std::vector functions = { + makeFunction("container", 0x1000u, 0x101Cu) + }; + + std::unordered_map> decodedFunctions; + decodedFunctions[0x1000u] = { + makeAbsJump(0x1000u, 0x100Cu, OPCODE_JAL), + makeNopLike(0x1004u), + makeAbsJump(0x1008u, 0x1014u, OPCODE_J), + makeNopLike(0x100Cu), + makeNopLike(0x1010u), + makeNopLike(0x1014u), + makeJrRa(0x1018u) + }; + + size_t discovered = PS2Recompiler::DiscoverAdditionalEntryPoints( + functions, decodedFunctions, sections); + + t.Equals(discovered, static_cast(1), + "same-function JAL should create one entry while plain J stays internal"); + + const bool hasCallEntry = std::any_of( + functions.begin(), functions.end(), + [](const Function &fn) { return fn.start == 0x100Cu; }); + const bool hasJumpEntry = std::any_of( + functions.begin(), functions.end(), + [](const Function &fn) { return fn.start == 0x1014u && fn.name.rfind("entry_", 0) == 0; }); + + t.IsTrue(hasCallEntry, "same-function JAL target should be promoted to an entry wrapper"); + t.IsFalse(hasJumpEntry, "same-function J target should remain an internal label only"); + }); + tc.Run("entry reslice handles entries without containing function", [](TestCase &t) { std::vector functions = { makeFunction("entry_1008", 0x1008u, 0x1018u), diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index 1ddbfd5..a3d13c5 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -1,5 +1,6 @@ #include "MiniTest.h" #include "ps2_runtime.h" +#include "ps2_runtime_macros.h" #include "ps2_syscalls.h" #include @@ -56,7 +57,7 @@ namespace void setRegU32(R5900Context &ctx, int reg, uint32_t value) { - ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); + SET_GPR_U32(&ctx, reg, value); } int32_t getRegS32(const R5900Context &ctx, int reg) @@ -82,6 +83,61 @@ namespace return dispatchNumericSyscall(syscallNumber, rdram, ctx, runtime); } + void overrideReturnHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + setReturnU32(ctx, ::getRegU32(ctx, 4) + ::getRegU32(ctx, 5)); + ctx->pc = ::getRegU32(ctx, 31); + } + + void overrideBrokenHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + setReturnU32(ctx, 0xDEADBEEFu); + ctx->pc = 0x12345678u; + } + + void overrideRecursiveFindAddressHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + runtime->handleSyscall(rdram, ctx, 0x83u); + ctx->pc = ::getRegU32(ctx, 31); + } + + void overrideKsegCompareHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + auto getLowU64 = [](const R5900Context *cpu, int reg) -> uint64_t + { + return (reg == 0) ? 0u : static_cast(_mm_extract_epi64(cpu->r[reg], 0)); + }; + auto setLowS32 = [](R5900Context *cpu, int reg, uint32_t value) + { + SET_GPR_S32(cpu, reg, value); + }; + auto setLowU64 = [](R5900Context *cpu, int reg, uint64_t value) + { + SET_GPR_U64(cpu, reg, value); + }; + + const uint32_t nextA0 = static_cast(::getRegU32(ctx, 4) + 4u); + setLowS32(ctx, 4, nextA0); + setLowU64(ctx, 2, (getLowU64(ctx, 4) < getLowU64(ctx, 5)) ? 1u : 0u); + if (getLowU64(ctx, 2) == 0u) + { + ctx->r[4] = _mm_setzero_si128(); + } + setLowU64(ctx, 2, getLowU64(ctx, 4)); + ctx->pc = ::getRegU32(ctx, 31); + } + + constexpr uint64_t K_EXPECTED_UPPER64 = 0x1122334455667788ull; + + void overridePreserveUpper64Handler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + const uint64_t hi = static_cast(_mm_extract_epi64(ctx->r[4], 1)); + const uint64_t low = static_cast(_mm_extract_epi64(ctx->r[4], 0)); + const uint64_t expectedLow = static_cast(static_cast(static_cast(0x80000000u))); + setReturnU32(ctx, (hi == K_EXPECTED_UPPER64 && low == expectedLow) ? 1u : 0u); + ctx->pc = ::getRegU32(ctx, 31); + } + struct TestEnv { std::vector rdram; @@ -453,5 +509,237 @@ void register_ps2_runtime_kernel_tests() 0u, "FindAddress should return 0 when no matching word exists"); }); + + tc.Run("SetSyscall mirrors guest kernel table entries into low memory", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + initializeGuestKernelState(env.rdram.data()); + + constexpr uint32_t kGuestSyscallTableGuestBase = 0x80011F80u; + constexpr uint32_t kSyscallIndex = 0x83u; + constexpr uint32_t kHandler = 0x00383548u; + constexpr uint32_t kExpectedGuestAddr = kGuestSyscallTableGuestBase + (kSyscallIndex * 4u); + constexpr uint32_t kExpectedPhysAddr = kExpectedGuestAddr & 0x1FFFFFFFu; + + setRegU32(env.ctx, 4, kSyscallIndex); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + uint32_t mirrored = 0u; + std::memcpy(&mirrored, env.rdram.data() + kExpectedPhysAddr, sizeof(mirrored)); + t.Equals(mirrored, + kHandler, + "SetSyscall should mirror handler pointers into the guest kernel syscall table"); + + setRegU32(env.ctx, 4, 0x80000000u); + setRegU32(env.ctx, 5, 0x80080000u); + setRegU32(env.ctx, 6, kHandler); + t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime), + "FindAddress syscall should dispatch"); + t.Equals(static_cast(getRegS32(env.ctx, 2)), + kExpectedGuestAddr, + "FindAddress should discover mirrored SetSyscall entries in low guest memory"); + + notifyRuntimeStop(); + }); + + tc.Run("SetSyscall honors signed kernel-table offsets", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + initializeGuestKernelState(env.rdram.data()); + + constexpr uint32_t kPatchIndex = 0xFFFFC402u; + constexpr uint32_t kHandler = 0xDEADBEEFu; + constexpr uint32_t kExpectedGuestAddr = 0x80002F88u; + constexpr uint32_t kExpectedPhysAddr = kExpectedGuestAddr & 0x1FFFFFFFu; + + setRegU32(env.ctx, 4, kPatchIndex); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch for signed offsets"); + + uint32_t mirrored = 0u; + std::memcpy(&mirrored, env.rdram.data() + kExpectedPhysAddr, sizeof(mirrored)); + t.Equals(mirrored, + kHandler, + "SetSyscall should treat the syscall index as a signed offset from the kernel table base"); + + notifyRuntimeStop(); + }); + + tc.Run("guest kernel syscall mirror resets between runs", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + initializeGuestKernelState(env.rdram.data()); + + constexpr uint32_t kGuestSyscallTableGuestBase = 0x80011F80u; + constexpr uint32_t kGuestSyscallTableProbeBase = 0x000002F0u; + constexpr uint32_t kSyscallIndex = 0x5Au; + constexpr uint32_t kHandler = 0x00383510u; + constexpr uint32_t kEntryPhysAddr = (kGuestSyscallTableGuestBase + (kSyscallIndex * 4u)) & 0x1FFFFFFFu; + + setRegU32(env.ctx, 4, kSyscallIndex); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + notifyRuntimeStop(); + initializeGuestKernelState(env.rdram.data()); + + uint32_t mirrored = 1u; + std::memcpy(&mirrored, env.rdram.data() + kEntryPhysAddr, sizeof(mirrored)); + t.Equals(mirrored, + 0u, + "Initializing guest kernel state should clear stale mirrored syscall entries"); + + uint32_t probeHi = 0u; + uint32_t probeLo = 0u; + std::memcpy(&probeHi, env.rdram.data() + kGuestSyscallTableProbeBase + 0u, sizeof(probeHi)); + std::memcpy(&probeLo, env.rdram.data() + kGuestSyscallTableProbeBase + 8u, sizeof(probeLo)); + t.Equals(probeHi, + kGuestSyscallTableGuestBase >> 16, + "Guest kernel initialization should seed the syscall table probe high word"); + t.Equals(probeLo, + kGuestSyscallTableGuestBase & 0xFFFFu, + "Guest kernel initialization should seed the syscall table probe low word"); + }); + + tc.Run("SetSyscall override dispatches guest handlers that return through the sentinel", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + constexpr uint32_t kSyscallIndex = 0x91u; + constexpr uint32_t kHandler = 0x00200000u; + + env.runtime.registerFunction(kHandler, overrideReturnHandler); + setRegU32(env.ctx, 4, kSyscallIndex); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + setRegU32(env.ctx, 4, 7u); + setRegU32(env.ctx, 5, 5u); + t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime), + "Overridden syscall should dispatch through guest handler"); + t.Equals(static_cast(getRegS32(env.ctx, 2)), + 12u, + "Successful override dispatch should propagate guest handler return value"); + + notifyRuntimeStop(); + }); + + tc.Run("SetSyscall override preserves KSEG argument sign extension", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + constexpr uint32_t kSyscallIndex = 0x92u; + constexpr uint32_t kHandler = 0x00200030u; + + env.runtime.registerFunction(kHandler, overrideKsegCompareHandler); + setRegU32(env.ctx, 4, kSyscallIndex); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + setRegU32(env.ctx, 4, 0x80000000u); + setRegU32(env.ctx, 5, 0x80080000u); + t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime), + "Override syscall should invoke the guest handler"); + t.Equals(static_cast(getRegS32(env.ctx, 2)), + 0x80000004u, + "Override invocation should preserve KSEG ordering after 32-bit guest writes"); + + notifyRuntimeStop(); + }); + + tc.Run("SetSyscall override preserves upper 64 bits when writing 32-bit args", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + constexpr uint32_t kSyscallIndex = 0x93u; + constexpr uint32_t kHandler = 0x00200040u; + + env.runtime.registerFunction(kHandler, overridePreserveUpper64Handler); + setRegU32(env.ctx, 4, kSyscallIndex); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + env.ctx.r[4] = _mm_set_epi64x(static_cast(K_EXPECTED_UPPER64), + static_cast(static_cast(0x80000000u))); + t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime), + "Override syscall should invoke the guest handler"); + t.Equals(static_cast(getRegS32(env.ctx, 2)), + 1u, + "Override invocation should preserve the upper 64 bits of 128-bit GPRs when setting 32-bit args"); + + notifyRuntimeStop(); + }); + + tc.Run("broken syscall overrides fall back to builtin handlers", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + constexpr uint32_t kHandler = 0x00200010u; + constexpr uint32_t kTableBase = 0x00002000u; + constexpr uint32_t kValues[] = { + 0x11111111u, + 0x11223344u, + 0x55555555u + }; + + env.runtime.registerFunction(kHandler, overrideBrokenHandler); + setRegU32(env.ctx, 4, 0x83u); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + writeGuestWords(env.rdram.data(), kTableBase, kValues, std::size(kValues)); + setRegU32(env.ctx, 4, kTableBase); + setRegU32(env.ctx, 5, kTableBase + static_cast(sizeof(kValues))); + setRegU32(env.ctx, 6, 0x11223344u); + t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime), + "Builtin syscall should still dispatch when override exits abnormally"); + t.Equals(static_cast(getRegS32(env.ctx, 2)), + kTableBase + 4u, + "Abnormal override exits should fall back to the builtin syscall implementation"); + + notifyRuntimeStop(); + }); + + tc.Run("reentrant syscall overrides fall back to builtin handlers", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + constexpr uint32_t kHandler = 0x00200020u; + constexpr uint32_t kTableBase = 0x00003000u; + constexpr uint32_t kValues[] = { + 0xCAFEBABEu, + 0x11223344u, + 0x55667788u + }; + + env.runtime.registerFunction(kHandler, overrideRecursiveFindAddressHandler); + setRegU32(env.ctx, 4, 0x83u); + setRegU32(env.ctx, 5, kHandler); + t.IsTrue(callSyscall(0x74u, env.rdram.data(), &env.ctx, &env.runtime), + "SetSyscall syscall should dispatch"); + + writeGuestWords(env.rdram.data(), kTableBase, kValues, std::size(kValues)); + setRegU32(env.ctx, 4, kTableBase); + setRegU32(env.ctx, 5, kTableBase + static_cast(sizeof(kValues))); + setRegU32(env.ctx, 6, 0x11223344u); + t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime), + "Reentrant override should resolve through builtin fallback"); + t.Equals(static_cast(getRegS32(env.ctx, 2)), + kTableBase + 4u, + "Reentrant override dispatch should use builtin syscall implementation"); + + notifyRuntimeStop(); + }); }); } diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 9cd103c..a173590 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -1,5 +1,6 @@ #include "MiniTest.h" #include "ps2_runtime.h" +#include "ps2_syscalls.h" #include "ps2_stubs.h" #include @@ -7,6 +8,11 @@ #include #include +namespace ps2_stubs +{ + void resetSifState(); +} + namespace { struct TestEnv @@ -17,6 +23,7 @@ namespace TestEnv() : rdram(PS2_RAM_SIZE, 0u) { + ps2_stubs::resetSifState(); std::memset(&ctx, 0, sizeof(ctx)); } }; @@ -71,6 +78,23 @@ namespace std::memcpy(&value, rdram + addr, sizeof(value)); return value; } + + uint32_t g_dmacHandlerWriteAddr = 0u; + uint32_t g_dmacHandlerValue = 0u; + uint32_t g_dmacHandlerLastCause = 0u; + uint32_t g_dmacHandlerLastArg = 0u; + + void testDmacHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + (void)runtime; + g_dmacHandlerLastCause = ::getRegU32(ctx, 4); + g_dmacHandlerLastArg = ::getRegU32(ctx, 5); + if (g_dmacHandlerWriteAddr != 0u) + { + writeGuestU32(rdram, g_dmacHandlerWriteAddr, g_dmacHandlerValue); + } + ctx->pc = 0u; + } } void register_ps2_sif_dma_tests() @@ -114,6 +138,103 @@ void register_ps2_sif_dma_tests() t.IsTrue(getRegS32(env.ctx, 2) < 0, "sceSifDmaStat should be negative when transfer is complete"); }); + tc.Run("sceSifSetDma dispatches enabled DMAC handlers for cause 5", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kDescAddr = 0x00020300u; + constexpr uint32_t kSrcAddr = 0x00020400u; + constexpr uint32_t kDstAddr = 0x00020500u; + constexpr uint32_t kHandlerAddr = 0x00100000u; + constexpr uint32_t kHandlerWriteAddr = 0x00020600u; + constexpr uint32_t kHandlerArg = 0x12345678u; + + g_dmacHandlerWriteAddr = kHandlerWriteAddr; + g_dmacHandlerValue = 0xCAFEBABEu; + g_dmacHandlerLastCause = 0u; + g_dmacHandlerLastArg = 0u; + env.runtime.registerFunction(kHandlerAddr, &testDmacHandler); + + setRegU32(env.ctx, 4, 5u); + setRegU32(env.ctx, 5, kHandlerAddr); + setRegU32(env.ctx, 6, 0u); + setRegU32(env.ctx, 7, kHandlerArg); + ps2_syscalls::AddDmacHandler(env.rdram.data(), &env.ctx, &env.runtime); + const int32_t handlerId = getRegS32(env.ctx, 2); + t.IsTrue(handlerId > 0, "AddDmacHandler should register a handler"); + + setRegU32(env.ctx, 4, 5u); + ps2_syscalls::EnableDmac(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), 0, "EnableDmac should succeed"); + + std::array payload{}; + for (size_t i = 0; i < payload.size(); ++i) + { + payload[i] = static_cast(0x40u + i); + } + std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size()); + + const Ps2SifDmaTransfer desc{ + kSrcAddr, + kDstAddr, + static_cast(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); + + t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should still report success"); + t.Equals(readGuestU32(env.rdram.data(), kHandlerWriteAddr), g_dmacHandlerValue, + "sceSifSetDma should invoke registered DMAC handlers"); + t.Equals(g_dmacHandlerLastCause, 5u, "DMAC handler should observe cause 5"); + t.Equals(g_dmacHandlerLastArg, kHandlerArg, "DMAC handler should receive registered argument"); + }); + + tc.Run("resetSifState seeds boot-ready SIF registers", [](TestCase &t) + { + TestEnv env; + + auto getReg = [&](uint32_t reg) -> uint32_t + { + setRegU32(env.ctx, 4, reg); + ps2_stubs::sceSifGetReg(env.rdram.data(), &env.ctx, &env.runtime); + return ::getRegU32(&env.ctx, 2); + }; + + t.Equals(getReg(0x4u), 0x00020000u, "SIF boot status register should expose ready bit by default"); + t.Equals(getReg(0x80000000u), 0u, "SIF main-address register should default to zero"); + t.Equals(getReg(0x80000001u), 0u, "SIF sub-address register should default to zero"); + t.Equals(getReg(0x80000002u), 0u, "SIF mscom register should default to zero"); + }); + + tc.Run("sceSifExitCmd restores default boot-ready SIF registers", [](TestCase &t) + { + TestEnv env; + + setRegU32(env.ctx, 4, 0x4u); + setRegU32(env.ctx, 5, 0x12340000u); + ps2_stubs::sceSifSetReg(env.rdram.data(), &env.ctx, &env.runtime); + + setRegU32(env.ctx, 4, 0x80000002u); + setRegU32(env.ctx, 5, 0x89ABCDEFu); + ps2_stubs::sceSifSetReg(env.rdram.data(), &env.ctx, &env.runtime); + + ps2_stubs::sceSifExitCmd(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), 0, "sceSifExitCmd should succeed"); + + auto getReg = [&](uint32_t reg) -> uint32_t + { + setRegU32(env.ctx, 4, reg); + ps2_stubs::sceSifGetReg(env.rdram.data(), &env.ctx, &env.runtime); + return ::getRegU32(&env.ctx, 2); + }; + + t.Equals(getReg(0x4u), 0x00020000u, "sceSifExitCmd should restore the boot-ready status bit"); + t.Equals(getReg(0x80000002u), 0u, "sceSifExitCmd should clear transient mscom state"); + }); + tc.Run("sceSifSetDma rejects invalid descriptors without partial writes", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index e067b00..3aede2f 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -1,6 +1,7 @@ #include "MiniTest.h" #include "ps2_runtime.h" #include "ps2_syscalls.h" +#include "ps2_stubs.h" #include #include @@ -9,6 +10,11 @@ using namespace ps2_syscalls; +namespace ps2_stubs +{ + void resetSifState(); +} + namespace { constexpr int KE_OK = 0; @@ -82,6 +88,7 @@ namespace TestEnv() : rdram(PS2_RAM_SIZE, 0) { + ps2_stubs::resetSifState(); std::memset(&ctx, 0, sizeof(ctx)); } };