mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
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
This commit is contained in:
@@ -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`).
|
||||
|
||||
+18
-11
@@ -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 <input_elf> <output_toml>
|
||||
* `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).
|
||||
For more details on the recompilation process, see the [Main README](../README.md).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -241,6 +241,12 @@ namespace ps2recomp
|
||||
size_t passCount = 0;
|
||||
};
|
||||
|
||||
struct StaticEntryTarget
|
||||
{
|
||||
uint32_t target = 0u;
|
||||
bool isCall = false;
|
||||
};
|
||||
|
||||
EntryDiscoveryStats discoverAdditionalEntryPointsImpl(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
@@ -269,11 +275,14 @@ namespace ps2recomp
|
||||
return false;
|
||||
};
|
||||
|
||||
auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional<uint32_t>
|
||||
auto getStaticEntryTarget = [](const Instruction &inst) -> std::optional<StaticEntryTarget>
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FunctionRecord> collectExecutableLabelRecords(List<FunctionRecord> functionRecords) {
|
||||
List<FunctionRecord> labelRecords = new ArrayList<>();
|
||||
Set<Long> 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<Long> 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<String> stubSelectors = collectFunctionSelectors(stubNames, functionRecords, true);
|
||||
List<String> skipSelectors = collectFunctionSelectors(skipNames, functionRecords, true);
|
||||
final int functionCount = functionRecords.size();
|
||||
List<FunctionRecord> labelRecords = collectExecutableLabelRecords(functionRecords);
|
||||
List<FunctionRecord> 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<String> stubSelectors = collectFunctionSelectors(stubNames, exportRecords, true);
|
||||
List<String> 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()));
|
||||
|
||||
@@ -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()
|
||||
@@ -107,6 +107,7 @@
|
||||
X(GsPutIMR) \
|
||||
X(iGsPutIMR) \
|
||||
X(SetVSyncFlag) \
|
||||
X(SetSyscall) \
|
||||
X(GsSetVideoMode) \
|
||||
\
|
||||
X(GetOsdConfigParam) \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
#include "ps2_gs_gpu.h"
|
||||
#include <ThreadNaming.h>
|
||||
|
||||
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<int64_t>(PS2_RAM_SIZE - 0x10u));
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
@@ -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<uint32_t>(-0x76):
|
||||
@@ -391,5 +397,14 @@ namespace ps2_syscalls
|
||||
g_alarms.clear();
|
||||
}
|
||||
g_alarm_cv.notify_all();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_exit_handler_mutex);
|
||||
g_exit_handlers.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_syscall_override_mutex);
|
||||
g_syscall_overrides.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<std::mutex> lock(g_sifCmdStateMutex);
|
||||
seedDefaultSifRegsLocked();
|
||||
}
|
||||
} g_sifStateInitializer;
|
||||
|
||||
uint32_t allocateSifDmaTransferId()
|
||||
{
|
||||
@@ -2744,6 +2793,12 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void resetSifState()
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<int32_t>(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<std::mutex> 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);
|
||||
}
|
||||
|
||||
@@ -101,9 +101,9 @@ static std::shared_ptr<EventFlagInfo> 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<uint32_t> 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)
|
||||
|
||||
@@ -416,6 +416,15 @@ static bool g_bootmode_initialized = false;
|
||||
static uint32_t g_bootmode_pool_offset = 0;
|
||||
static std::unordered_map<uint8_t, uint32_t> g_bootmode_addresses;
|
||||
|
||||
static std::mutex g_syscall_override_mutex;
|
||||
static std::unordered_map<uint32_t, uint32_t> g_syscall_overrides;
|
||||
static std::unordered_set<uint32_t> 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;
|
||||
|
||||
|
||||
@@ -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<IrqHandlerInfo> handlers;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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{};
|
||||
|
||||
@@ -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<std::mutex> 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<uint32_t> s_activeSyscallOverrides;
|
||||
if (std::find(s_activeSyscallOverrides.begin(), s_activeSyscallOverrides.end(), syscallNumber) != s_activeSyscallOverrides.end())
|
||||
{
|
||||
static std::atomic<uint32_t> 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<uint32_t> &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<uint32_t> s_findAddressOverrideLogs{0u};
|
||||
static std::atomic<uint32_t> 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<uint32_t> 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<int64_t>(static_cast<int32_t>(syscallIndex)) * static_cast<int64_t>(sizeof(uint32_t));
|
||||
const int64_t guestAddr64 = static_cast<int64_t>(kGuestSyscallTablePhysBase) + offsetBytes;
|
||||
if (guestAddr64 < 0 || (guestAddr64 + static_cast<int64_t>(sizeof(uint32_t))) > static_cast<int64_t>(kGuestSyscallMirrorLimit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
guestAddr = static_cast<uint32_t>(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<std::mutex> 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<std::mutex> 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<uint32_t> s_findAddressHitLogs{0u};
|
||||
static std::atomic<uint32_t> s_findAddressMissLogs{0u};
|
||||
constexpr uint32_t kMaxFindAddressHitLogs = 16u;
|
||||
constexpr uint32_t kMaxFindAddressMissLogs = 128u;
|
||||
|
||||
std::atomic<uint32_t> &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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<Section> sections = {
|
||||
{".text", 0x1000u, 0x40u, 0u, true, false, false, true, nullptr}
|
||||
};
|
||||
|
||||
std::vector<Function> functions = {
|
||||
makeFunction("container", 0x1000u, 0x101Cu)
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> 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<size_t>(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<Function> functions = {
|
||||
makeFunction("entry_1008", 0x1008u, 0x1018u),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "MiniTest.h"
|
||||
#include "ps2_runtime.h"
|
||||
#include "ps2_runtime_macros.h"
|
||||
#include "ps2_syscalls.h"
|
||||
|
||||
#include <array>
|
||||
@@ -56,7 +57,7 @@ namespace
|
||||
|
||||
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
|
||||
{
|
||||
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(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<uint64_t>(_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<uint32_t>(::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<uint64_t>(_mm_extract_epi64(ctx->r[4], 1));
|
||||
const uint64_t low = static_cast<uint64_t>(_mm_extract_epi64(ctx->r[4], 0));
|
||||
const uint64_t expectedLow = static_cast<uint64_t>(static_cast<int64_t>(static_cast<int32_t>(0x80000000u)));
|
||||
setReturnU32(ctx, (hi == K_EXPECTED_UPPER64 && low == expectedLow) ? 1u : 0u);
|
||||
ctx->pc = ::getRegU32(ctx, 31);
|
||||
}
|
||||
|
||||
struct TestEnv
|
||||
{
|
||||
std::vector<uint8_t> 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<uint32_t>(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<uint32_t>(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<uint32_t>(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<int64_t>(K_EXPECTED_UPPER64),
|
||||
static_cast<int64_t>(static_cast<int32_t>(0x80000000u)));
|
||||
t.IsTrue(callSyscall(kSyscallIndex, env.rdram.data(), &env.ctx, &env.runtime),
|
||||
"Override syscall should invoke the guest handler");
|
||||
t.Equals(static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(getRegS32(env.ctx, 2)),
|
||||
kTableBase + 4u,
|
||||
"Reentrant override dispatch should use builtin syscall implementation");
|
||||
|
||||
notifyRuntimeStop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "MiniTest.h"
|
||||
#include "ps2_runtime.h"
|
||||
#include "ps2_syscalls.h"
|
||||
#include "ps2_stubs.h"
|
||||
|
||||
#include <array>
|
||||
@@ -7,6 +8,11 @@
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
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<uint8_t, 16> payload{};
|
||||
for (size_t i = 0; i < payload.size(); ++i)
|
||||
{
|
||||
payload[i] = static_cast<uint8_t>(0x40u + i);
|
||||
}
|
||||
std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), 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);
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "MiniTest.h"
|
||||
#include "ps2_runtime.h"
|
||||
#include "ps2_syscalls.h"
|
||||
#include "ps2_stubs.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user