mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 16:59:35 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ca6a866c9 | |||
| 9e4fd794c1 | |||
| 493522a769 | |||
| da10073cb9 | |||
| ccd17e5244 | |||
| 765cbcca0f | |||
| b3be2650b0 |
@@ -49,3 +49,4 @@ add_subdirectory("ps2xRuntime")
|
|||||||
|
|
||||||
add_subdirectory("ps2xAnalyzer")
|
add_subdirectory("ps2xAnalyzer")
|
||||||
add_subdirectory("ps2xTest")
|
add_subdirectory("ps2xTest")
|
||||||
|
add_subdirectory("ps2xStudio")
|
||||||
|
|||||||
@@ -61,27 +61,35 @@ cmake --build out/build --config Debug
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
1. Analyze ELF and generate config:
|
Preferred workflow for retail or stripped games:
|
||||||
|
|
||||||
```bash
|
1. Open the ELF in Ghidra.
|
||||||
./ps2_analyzer your_game.elf config.toml
|
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||||
```
|
3. Use the exported TOML and CSV map.
|
||||||
*For better results on retail games, see the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-recommended-for-complex-games).*
|
4. Recompile with the exported TOML:
|
||||||
|
|
||||||
2. Recompile using generated TOML:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./ps2_recomp config.toml
|
./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
|
### Configuration
|
||||||
|
|
||||||
Main fields in `config.toml`:
|
Main fields in `config.toml`:
|
||||||
|
|
||||||
* `general.input`: source ELF path.
|
* `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.output`: generated C++ output folder.
|
||||||
* `general.single_file_output`: one combined cpp or one file per function.
|
* `general.single_file_output`: one combined cpp or one file per function.
|
||||||
* `general.patch_syscalls`: apply configured patches to `SYSCALL` instructions (`false` recommended).
|
* `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.
|
* 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(...)`.
|
* Example: `sceCdRead@0x00123456` binds function start `0x00123456` to `ps2_stubs::sceCdRead(...)`.
|
||||||
* Generic temporary handlers are available: `ret0@0xADDR`, `ret1@0xADDR`, `reta0@0xADDR`.
|
* 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.
|
* The address must be the function start in that exact ELF build.
|
||||||
* Addresses are not portable across different games/regions/builds.
|
* 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`).
|
* 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
|
### 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.
|
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":
|
For commercial games where symbols are stripped, the analyzer uses a "JAL Scanner":
|
||||||
* It scans executable sections for `JAL` (Jump and Link) instructions.
|
* It scans executable sections for `JAL` (Jump and Link) instructions.
|
||||||
* It infers function start points based on jump targets.
|
* It infers function start points based on jump targets.
|
||||||
* It generates names like `sub_XXXXXXXX`.
|
* It generates names like `sub_XXXXXXXX`.
|
||||||
|
|
||||||
### 3. Ghidra Integration (For Complex Games)
|
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.
|
||||||
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`.
|
### 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.
|
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"`.
|
3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`.
|
||||||
4. The recompiler will prioritize Ghidra's boundaries over its own heuristics.
|
4. Run the recompiler with that exported TOML.
|
||||||
|
5. The recompiler will prioritize Ghidra's boundaries over its own heuristics.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
@@ -41,12 +44,16 @@ ps2_analyzer <input_elf> <output_toml>
|
|||||||
* `output_toml`: Path where the generated TOML configuration will be saved.
|
* `output_toml`: Path where the generated TOML configuration will be saved.
|
||||||
|
|
||||||
## Example Workflow
|
## Example Workflow
|
||||||
1. Run the analyzer on your game:
|
1. Open `game.elf` in Ghidra.
|
||||||
`ps2_analyzer game.elf config.toml`
|
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||||
2. (Optional) Open `game.elf` in Ghidra, run the export script, and update `config.toml` with the CSV path.
|
3. Use the exported TOML and CSV.
|
||||||
3. Run the recompiler:
|
4. Run the recompiler:
|
||||||
`ps2recomp config.toml`
|
`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
|
## Generated Configuration
|
||||||
The tool creates a TOML file with the following sections:
|
The tool creates a TOML file with the following sections:
|
||||||
* `[general]`: Paths to ELF and Ghidra maps.
|
* `[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.
|
* Self-modifying code is flagged but requires manual review.
|
||||||
* Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries.
|
* 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).
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ namespace ps2recomp
|
|||||||
|
|
||||||
bool analyze();
|
bool analyze();
|
||||||
bool generateToml(const std::string &outputPath);
|
bool generateToml(const std::string &outputPath);
|
||||||
|
bool importGhidraMap(const std::string &csvPath);
|
||||||
|
const std::vector<Function>& getFunctions() const;
|
||||||
bool isLibrarySymbolNameForHeuristics(const std::string &name) const;
|
bool isLibrarySymbolNameForHeuristics(const std::string &name) const;
|
||||||
static bool isReliableSymbolNameForHeuristics(const std::string &name);
|
static bool isReliableSymbolNameForHeuristics(const std::string &name);
|
||||||
static bool isSystemSymbolNameForHeuristics(const std::string &name);
|
static bool isSystemSymbolNameForHeuristics(const std::string &name);
|
||||||
|
|||||||
@@ -2542,4 +2542,88 @@ namespace ps2recomp
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ElfAnalyzer::importGhidraMap(const std::string &csvPath)
|
||||||
|
{
|
||||||
|
std::ifstream file(csvPath);
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
std::cerr << "Failed to open Ghidra CSV file: " << csvPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
int lineNum = 0;
|
||||||
|
int importedCount = 0;
|
||||||
|
|
||||||
|
while (std::getline(file, line))
|
||||||
|
{
|
||||||
|
lineNum++;
|
||||||
|
|
||||||
|
// Skip header line
|
||||||
|
if (lineNum == 1 && line.find("Name") != std::string::npos)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CSV line: Name,Start,End,Size
|
||||||
|
std::stringstream ss(line);
|
||||||
|
std::string name, startStr, endStr, sizeStr;
|
||||||
|
|
||||||
|
if (!std::getline(ss, name, ',') ||
|
||||||
|
!std::getline(ss, startStr, ',') ||
|
||||||
|
!std::getline(ss, endStr, ','))
|
||||||
|
{
|
||||||
|
continue; // Skip malformed lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse hex addresses (e.g., "0x00123456")
|
||||||
|
uint32_t startAddr = 0;
|
||||||
|
uint32_t endAddr = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
startAddr = std::stoul(startStr, nullptr, 16);
|
||||||
|
endAddr = std::stoul(endStr, nullptr, 16);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
continue; // Skip lines with invalid addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing function or create new one
|
||||||
|
bool found = false;
|
||||||
|
for (auto &func : m_functions)
|
||||||
|
{
|
||||||
|
if (func.start == startAddr)
|
||||||
|
{
|
||||||
|
// Update with Ghidra's more accurate boundaries
|
||||||
|
func.name = name;
|
||||||
|
func.end = endAddr;
|
||||||
|
found = true;
|
||||||
|
importedCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, create new function from Ghidra data
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
Function newFunc;
|
||||||
|
newFunc.name = name;
|
||||||
|
newFunc.start = startAddr;
|
||||||
|
newFunc.end = endAddr;
|
||||||
|
m_functions.push_back(newFunc);
|
||||||
|
importedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Imported " << importedCount << " functions from Ghidra CSV: " << csvPath << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Function>& ElfAnalyzer::getFunctions() const
|
||||||
|
{
|
||||||
|
return m_functions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace ps2recomp
|
namespace ps2recomp
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,8 @@ namespace ps2recomp
|
|||||||
std::vector<Symbol> m_symbols;
|
std::vector<Symbol> m_symbols;
|
||||||
std::vector<Relocation> m_relocations;
|
std::vector<Relocation> m_relocations;
|
||||||
std::vector<Function> m_extraFunctions;
|
std::vector<Function> m_extraFunctions;
|
||||||
|
bool m_hasLoadedGhidraMap = false;
|
||||||
|
std::unordered_set<uint32_t> m_ghidraMapStarts;
|
||||||
|
|
||||||
void loadSections();
|
void loadSections();
|
||||||
void loadSymbols();
|
void loadSymbols();
|
||||||
@@ -56,4 +59,4 @@ namespace ps2recomp
|
|||||||
|
|
||||||
} // namespace ps2recomp
|
} // namespace ps2recomp
|
||||||
|
|
||||||
#endif // PS2RECOMP_ELF_PARSER_H
|
#endif // PS2RECOMP_ELF_PARSER_H
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ namespace ps2recomp
|
|||||||
std::vector<Function> &functions,
|
std::vector<Function> &functions,
|
||||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions);
|
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions);
|
||||||
|
|
||||||
|
static std::string ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ConfigManager m_configManager;
|
ConfigManager m_configManager;
|
||||||
std::unique_ptr<ElfParser> m_elfParser;
|
std::unique_ptr<ElfParser> m_elfParser;
|
||||||
@@ -70,6 +72,7 @@ namespace ps2recomp
|
|||||||
bool generateStubHeader();
|
bool generateStubHeader();
|
||||||
bool writeToFile(const std::string &path, const std::string &content);
|
bool writeToFile(const std::string &path, const std::string &content);
|
||||||
std::filesystem::path getOutputPath(const Function &function) const;
|
std::filesystem::path getOutputPath(const Function &function) const;
|
||||||
|
static std::string clampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength);
|
||||||
std::string sanitizeFunctionName(const std::string &name) const;
|
std::string sanitizeFunctionName(const std::string &name) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ namespace ps2recomp
|
|||||||
return ((address + 4) & 0xF0000000u) | (target << 2);
|
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)
|
static std::string formatFloatLiteral(float value)
|
||||||
{
|
{
|
||||||
if (!std::isfinite(value))
|
if (!std::isfinite(value))
|
||||||
@@ -927,18 +937,35 @@ namespace ps2recomp
|
|||||||
|
|
||||||
try
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -2949,18 +2976,18 @@ namespace ps2recomp
|
|||||||
std::string CodeGenerator::translateVU_VRNEXT(const Instruction &inst)
|
std::string CodeGenerator::translateVU_VRNEXT(const Instruction &inst)
|
||||||
{
|
{
|
||||||
return fmt::format(
|
return fmt::format(
|
||||||
"{{ "
|
"{{\n"
|
||||||
" uint32_t r_vals[4]; "
|
" uint32_t r_vals[4];\n"
|
||||||
" _mm_storeu_si128((__m128i*)r_vals, _mm_castps_si128(ctx->vu0_r)); "
|
" _mm_storeu_si128((__m128i*)r_vals, _mm_castps_si128(ctx->vu0_r));\n"
|
||||||
" "
|
"\n"
|
||||||
" // Simple LFSR-based random number generation (PS2-like behavior) "
|
" // 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); "
|
" 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]; "
|
" r_vals[0] = r_vals[1];\n"
|
||||||
" r_vals[1] = r_vals[2]; "
|
" r_vals[1] = r_vals[2];\n"
|
||||||
" r_vals[2] = r_vals[3]; "
|
" r_vals[2] = r_vals[3];\n"
|
||||||
" r_vals[3] = feedback; "
|
" r_vals[3] = feedback;\n"
|
||||||
" "
|
"\n"
|
||||||
" ctx->vu0_r = _mm_castsi128_ps(_mm_loadu_si128((__m128i*)r_vals)); \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;
|
uint8_t fsf = inst.vectorInfo.fsf;
|
||||||
|
|
||||||
return fmt::format(
|
return fmt::format(
|
||||||
"{{ "
|
"{{\n"
|
||||||
" float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{}))); "
|
" 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)); "
|
" uint32_t seed; std::memcpy(&seed, &src, sizeof(seed));\n"
|
||||||
" "
|
"\n"
|
||||||
" // PS2 uses a specific LFSR initialization pattern "
|
" // PS2 uses a specific LFSR initialization pattern\n"
|
||||||
" if (seed == 0) seed = 1; " // Prevent zero seed
|
" if (seed == 0) seed = 1;\n"
|
||||||
" "
|
"\n"
|
||||||
" uint32_t r0 = seed; "
|
" uint32_t r0 = seed;\n"
|
||||||
" uint32_t r1 = seed * 0x41C64E6D + 0x3039; " // PS2-like LCG constants
|
" uint32_t r1 = seed * 0x41C64E6D + 0x3039;\n"
|
||||||
" uint32_t r2 = r1 * 0x41C64E6D + 0x3039; "
|
" uint32_t r2 = r1 * 0x41C64E6D + 0x3039;\n"
|
||||||
" uint32_t r3 = r2 * 0x41C64E6D + 0x3039; "
|
" uint32_t r3 = r2 * 0x41C64E6D + 0x3039;\n"
|
||||||
" "
|
"\n"
|
||||||
" ctx->vu0_r = _mm_castsi128_ps(_mm_set_epi32(r3, r2, r1, r0)); \n "
|
" ctx->vu0_r = _mm_castsi128_ps(_mm_set_epi32(r3, r2, r1, r0));\n"
|
||||||
"}}",
|
"}}",
|
||||||
fs_reg, fs_reg, fsf);
|
fs_reg, fs_reg, fsf);
|
||||||
}
|
}
|
||||||
@@ -3631,20 +3658,20 @@ namespace ps2recomp
|
|||||||
uint8_t fsf = inst.vectorInfo.fsf;
|
uint8_t fsf = inst.vectorInfo.fsf;
|
||||||
|
|
||||||
return fmt::format(
|
return fmt::format(
|
||||||
"{{ "
|
"{{\n"
|
||||||
" float src = _mm_cvtss_f32(_mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,{}))); "
|
" 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)); "
|
" uint32_t src_bits; std::memcpy(&src_bits, &src, sizeof(src_bits));\n"
|
||||||
" __m128i r_current = _mm_castps_si128(ctx->vu0_r); "
|
" __m128i r_current = _mm_castps_si128(ctx->vu0_r);\n"
|
||||||
" __m128i fs_data = _mm_set1_epi32((int)src_bits); "
|
" __m128i fs_data = _mm_set1_epi32((int)src_bits);\n"
|
||||||
" "
|
"\n"
|
||||||
" // XOR the current random value with the data from the VU vector register "
|
" // XOR the current random value with the data from the VU vector register\n"
|
||||||
" __m128i xored = _mm_xor_si128(r_current, fs_data); "
|
" __m128i xored = _mm_xor_si128(r_current, fs_data);\n"
|
||||||
" "
|
"\n"
|
||||||
" // Apply a simple mixing function similar to PS2's LFSR "
|
" // Apply a simple mixing function similar to PS2's LFSR\n"
|
||||||
" __m128i mixed = _mm_xor_si128(xored, _mm_slli_epi32(xored, 7)); "
|
" __m128i mixed = _mm_xor_si128(xored, _mm_slli_epi32(xored, 7));\n"
|
||||||
" mixed = _mm_xor_si128(mixed, _mm_srli_epi32(mixed, 9)); "
|
" mixed = _mm_xor_si128(mixed, _mm_srli_epi32(mixed, 9));\n"
|
||||||
" "
|
"\n"
|
||||||
" ctx->vu0_r = _mm_castsi128_ps(mixed);"
|
" ctx->vu0_r = _mm_castsi128_ps(mixed);\n"
|
||||||
"}}",
|
"}}",
|
||||||
fs_reg, fs_reg, fsf);
|
fs_reg, fs_reg, fsf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ namespace
|
|||||||
{
|
{
|
||||||
bool IsAutoGeneratedName(const std::string &name)
|
bool IsAutoGeneratedName(const std::string &name)
|
||||||
{
|
{
|
||||||
return name.rfind("sub_", 0) == 0;
|
return name.rfind("sub_", 0) == 0 ||
|
||||||
|
name.rfind("FUN_", 0) == 0 ||
|
||||||
|
name.rfind("LAB_", 0) == 0 ||
|
||||||
|
name.rfind("DAT_", 0) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppendLoadSegmentsAsSections(const ELFIO::elfio &elf, std::vector<ps2recomp::Section> §ions)
|
void AppendLoadSegmentsAsSections(const ELFIO::elfio &elf, std::vector<ps2recomp::Section> §ions)
|
||||||
@@ -566,6 +569,13 @@ namespace ps2recomp
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_hasLoadedGhidraMap &&
|
||||||
|
IsAutoGeneratedName(symbol.name) &&
|
||||||
|
!m_ghidraMapStarts.contains(symbol.address))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const Section *functionSection = FindFunctionSectionByAddress(m_sections, symbol.address);
|
const Section *functionSection = FindFunctionSectionByAddress(m_sections, symbol.address);
|
||||||
if (!functionSection)
|
if (!functionSection)
|
||||||
{
|
{
|
||||||
@@ -947,6 +957,9 @@ namespace ps2recomp
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_hasLoadedGhidraMap = false;
|
||||||
|
m_ghidraMapStarts.clear();
|
||||||
|
|
||||||
std::ifstream file(mapPath);
|
std::ifstream file(mapPath);
|
||||||
if (!file.is_open())
|
if (!file.is_open())
|
||||||
{
|
{
|
||||||
@@ -963,6 +976,7 @@ namespace ps2recomp
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
int skippedNonExecutable = 0;
|
int skippedNonExecutable = 0;
|
||||||
int skippedInvalidRange = 0;
|
int skippedInvalidRange = 0;
|
||||||
|
std::unordered_set<uint32_t> mapStarts;
|
||||||
while (std::getline(file, line))
|
while (std::getline(file, line))
|
||||||
{
|
{
|
||||||
if (line.empty())
|
if (line.empty())
|
||||||
@@ -1007,6 +1021,7 @@ namespace ps2recomp
|
|||||||
func.isSkipped = false;
|
func.isSkipped = false;
|
||||||
|
|
||||||
m_extraFunctions.push_back(std::move(func));
|
m_extraFunctions.push_back(std::move(func));
|
||||||
|
mapStarts.insert(start);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
@@ -1017,6 +1032,8 @@ namespace ps2recomp
|
|||||||
|
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
|
m_hasLoadedGhidraMap = true;
|
||||||
|
m_ghidraMapStarts = mapStarts;
|
||||||
std::cout << "Loaded " << count << " functions from Ghidra map" << std::endl;
|
std::cout << "Loaded " << count << " functions from Ghidra map" << std::endl;
|
||||||
if (skippedNonExecutable > 0)
|
if (skippedNonExecutable > 0)
|
||||||
{
|
{
|
||||||
@@ -1029,9 +1046,36 @@ namespace ps2recomp
|
|||||||
<< " Ghidra function(s) with invalid ranges after section clamping." << std::endl;
|
<< " Ghidra function(s) with invalid ranges after section clamping." << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_extraFunctions.erase(
|
||||||
|
std::remove_if(m_extraFunctions.begin(), m_extraFunctions.end(),
|
||||||
|
[&](const Function &func)
|
||||||
|
{
|
||||||
|
return IsAutoGeneratedName(func.name) && !mapStarts.contains(func.start);
|
||||||
|
}),
|
||||||
|
m_extraFunctions.end());
|
||||||
|
|
||||||
std::sort(m_extraFunctions.begin(), m_extraFunctions.end(),
|
std::sort(m_extraFunctions.begin(), m_extraFunctions.end(),
|
||||||
[](const Function &a, const Function &b)
|
[](const Function &a, const Function &b)
|
||||||
{ return a.start < b.start; });
|
{
|
||||||
|
if (a.start != b.start)
|
||||||
|
{
|
||||||
|
return a.start < b.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool aAuto = IsAutoGeneratedName(a.name);
|
||||||
|
const bool bAuto = IsAutoGeneratedName(b.name);
|
||||||
|
if (aAuto != bAuto)
|
||||||
|
{
|
||||||
|
return !aAuto;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.end != b.end)
|
||||||
|
{
|
||||||
|
return a.end > b.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.name < b.name;
|
||||||
|
});
|
||||||
|
|
||||||
m_extraFunctions.erase(
|
m_extraFunctions.erase(
|
||||||
std::unique(m_extraFunctions.begin(), m_extraFunctions.end(),
|
std::unique(m_extraFunctions.begin(), m_extraFunctions.end(),
|
||||||
@@ -1267,6 +1311,8 @@ namespace ps2recomp
|
|||||||
void ElfParser::loadDebugFunctions()
|
void ElfParser::loadDebugFunctions()
|
||||||
{
|
{
|
||||||
m_extraFunctions.clear();
|
m_extraFunctions.clear();
|
||||||
|
m_hasLoadedGhidraMap = false;
|
||||||
|
m_ghidraMapStarts.clear();
|
||||||
|
|
||||||
if (HasDwarfSections(*m_elf))
|
if (HasDwarfSections(*m_elf))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -241,6 +241,12 @@ namespace ps2recomp
|
|||||||
size_t passCount = 0;
|
size_t passCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StaticEntryTarget
|
||||||
|
{
|
||||||
|
uint32_t target = 0u;
|
||||||
|
bool isCall = false;
|
||||||
|
};
|
||||||
|
|
||||||
EntryDiscoveryStats discoverAdditionalEntryPointsImpl(
|
EntryDiscoveryStats discoverAdditionalEntryPointsImpl(
|
||||||
std::vector<Function> &functions,
|
std::vector<Function> &functions,
|
||||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||||
@@ -269,11 +275,14 @@ namespace ps2recomp
|
|||||||
return false;
|
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)
|
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 &&
|
if (inst.opcode == OPCODE_SPECIAL &&
|
||||||
@@ -369,7 +378,8 @@ namespace ps2recomp
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t target = targetOpt.value();
|
const StaticEntryTarget staticTarget = targetOpt.value();
|
||||||
|
const uint32_t target = staticTarget.target;
|
||||||
|
|
||||||
if ((target & 0x3) != 0 || !isExecutableAddress(target))
|
if ((target & 0x3) != 0 || !isExecutableAddress(target))
|
||||||
{
|
{
|
||||||
@@ -381,10 +391,25 @@ namespace ps2recomp
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Function *containingFunction = findContainingFunction(target);
|
const bool targetInCurrentFunction = std::any_of(
|
||||||
if (containingFunction && containingFunction->start == function.start)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,11 +1476,46 @@ namespace ps2recomp
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path outputPath = m_config.outputPath;
|
std::filesystem::path outputPath = m_config.outputPath;
|
||||||
outputPath /= safeName + ".cpp";
|
outputPath /= clampFilenameLength(safeName, ".cpp", 100);
|
||||||
|
|
||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string PS2Recompiler::clampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
|
||||||
|
{
|
||||||
|
if (maxLength == 0)
|
||||||
|
{
|
||||||
|
std::cerr << "clampFilenameLength::maxLength must be greater than 0" << std::endl;
|
||||||
|
//Better go over the limit than create files with an empty path
|
||||||
|
return baseName + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseName.size() + extension.size() <= maxLength)
|
||||||
|
return baseName + extension;
|
||||||
|
|
||||||
|
std::string namePart = baseName;
|
||||||
|
std::string preservedSuffix;
|
||||||
|
|
||||||
|
auto suffixPos = namePart.rfind("_0x");
|
||||||
|
if (suffixPos != std::string::npos)
|
||||||
|
{
|
||||||
|
preservedSuffix = namePart.substr(suffixPos);
|
||||||
|
namePart = namePart.substr(0, suffixPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t available = maxLength - extension.size() - preservedSuffix.size();
|
||||||
|
|
||||||
|
if (available == 0)
|
||||||
|
{
|
||||||
|
return preservedSuffix + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namePart.size() > available)
|
||||||
|
namePart = namePart.substr(0, available);
|
||||||
|
|
||||||
|
return namePart + preservedSuffix + extension;
|
||||||
|
}
|
||||||
|
|
||||||
std::string PS2Recompiler::sanitizeFunctionName(const std::string &name) const
|
std::string PS2Recompiler::sanitizeFunctionName(const std::string &name) const
|
||||||
{
|
{
|
||||||
std::string sanitized = sanitizeIdentifierBody(name);
|
std::string sanitized = sanitizeIdentifierBody(name);
|
||||||
@@ -1510,4 +1570,9 @@ namespace ps2recomp
|
|||||||
}
|
}
|
||||||
return StubTarget::Unknown;
|
return StubTarget::Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string PS2Recompiler::ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
|
||||||
|
{
|
||||||
|
return clampFilenameLength(baseName, extension, maxLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,21 @@
|
|||||||
// @category PS2Recomp
|
// @category PS2Recomp
|
||||||
|
|
||||||
import ghidra.app.script.GhidraScript;
|
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.address.AddressSetView;
|
||||||
|
import ghidra.program.model.listing.Instruction;
|
||||||
import ghidra.program.model.listing.Function;
|
import ghidra.program.model.listing.Function;
|
||||||
import ghidra.program.model.listing.FunctionIterator;
|
import ghidra.program.model.listing.FunctionIterator;
|
||||||
import ghidra.program.model.listing.FunctionManager;
|
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.File;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
@@ -91,6 +102,7 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
long start;
|
long start;
|
||||||
long endExclusive;
|
long endExclusive;
|
||||||
long size;
|
long size;
|
||||||
|
boolean syntheticEntry = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ClassificationKind {
|
private enum ClassificationKind {
|
||||||
@@ -120,6 +132,23 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ghidra on Windows can return executable paths like "/D:/path/to/elf".
|
||||||
|
// TOML expects "D:/path/to/elf" for this field.
|
||||||
|
private static String normalizeWindowsDrivePath(String value) {
|
||||||
|
if (value == null || value.length() < 4) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.charAt(0) == '/' &&
|
||||||
|
Character.isLetter(value.charAt(1)) &&
|
||||||
|
value.charAt(2) == ':' &&
|
||||||
|
(value.charAt(3) == '/' || value.charAt(3) == '\\')) {
|
||||||
|
return value.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private static String normalizeOptionalLeadingUnderscore(String value) {
|
private static String normalizeOptionalLeadingUnderscore(String value) {
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
@@ -314,6 +343,178 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
return selectors;
|
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
|
@Override
|
||||||
public void run() throws Exception {
|
public void run() throws Exception {
|
||||||
File tomlFile = askFile("Choose output TOML config file", "Save");
|
File tomlFile = askFile("Choose output TOML config file", "Save");
|
||||||
@@ -323,11 +524,9 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
|
|
||||||
boolean exportCsv = askYesNo("Export CSV", "Also export compatibility CSV function map?");
|
boolean exportCsv = askYesNo("Export CSV", "Also export compatibility CSV function map?");
|
||||||
File csvFile = null;
|
File csvFile = null;
|
||||||
if (exportCsv) {
|
csvFile = askFile("Choose output CSV file", "Save");
|
||||||
csvFile = askFile("Choose output CSV file", "Save");
|
if (csvFile == null) {
|
||||||
if (csvFile == null) {
|
return;
|
||||||
exportCsv = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionManager fm = currentProgram.getFunctionManager();
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
@@ -363,31 +562,35 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> stubSelectors = collectFunctionSelectors(stubNames, functionRecords, true);
|
final int functionCount = functionRecords.size();
|
||||||
List<String> skipSelectors = collectFunctionSelectors(skipNames, functionRecords, true);
|
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) {
|
List<String> stubSelectors = collectFunctionSelectors(stubNames, exportRecords, true);
|
||||||
try (PrintWriter writer = new PrintWriter(csvFile)) {
|
List<String> skipSelectors = collectFunctionSelectors(skipNames, exportRecords, true);
|
||||||
writer.println("Name,Start,End,Size");
|
|
||||||
functionRecords.sort(Comparator.comparingLong(r -> r.start));
|
try (PrintWriter writer = new PrintWriter(csvFile)) {
|
||||||
for (FunctionRecord record : functionRecords) {
|
writer.println("Name,Start,End,Size");
|
||||||
writer.printf("%s,0x%08X,0x%08X,%d%n",
|
for (FunctionRecord record : exportRecords) {
|
||||||
record.name,
|
writer.printf("%s,0x%08X,0x%08X,%d%n",
|
||||||
record.start,
|
record.name,
|
||||||
record.endExclusive,
|
record.start,
|
||||||
record.size
|
record.endExclusive,
|
||||||
);
|
record.size
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String programPath = currentProgram.getExecutablePath();
|
String programPath = currentProgram.getExecutablePath();
|
||||||
if (programPath == null) {
|
if (programPath == null) {
|
||||||
programPath = "";
|
programPath = "";
|
||||||
}
|
}
|
||||||
|
programPath = normalizeWindowsDrivePath(programPath);
|
||||||
|
|
||||||
File outputDir = tomlFile.getParentFile() == null ? new File("output") : new File(tomlFile.getParentFile(), "output");
|
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)) {
|
try (PrintWriter writer = new PrintWriter(tomlFile)) {
|
||||||
writer.println("# Auto-generated by ExportPS2Functions.java");
|
writer.println("# Auto-generated by ExportPS2Functions.java");
|
||||||
@@ -419,7 +622,9 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
writer.println();
|
writer.println();
|
||||||
|
|
||||||
writer.println("[ghidra_export]");
|
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("stub_count = " + stubSelectors.size());
|
||||||
writer.println("skip_count = " + skipSelectors.size());
|
writer.println("skip_count = " + skipSelectors.size());
|
||||||
writer.println("uncategorized_count = " + uncategorizedCount);
|
writer.println("uncategorized_count = " + uncategorizedCount);
|
||||||
@@ -427,9 +632,7 @@ public class ExportPS2Functions extends GhidraScript {
|
|||||||
writer.println("runtime_call_source = \"regex_only\"");
|
writer.println("runtime_call_source = \"regex_only\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exportCsv && csvFile != null) {
|
println(String.format("Exported %d functions and %d executable labels to %s", functionCount, labelRecords.size(), csvFile.getAbsolutePath()));
|
||||||
println(String.format("Exported %d functions to %s", functionRecords.size(), csvFile.getAbsolutePath()));
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Using regex-only runtime/library classification (no ps2_call_list.h).");
|
println("Using regex-only runtime/library classification (no ps2_call_list.h).");
|
||||||
println(String.format("Exported TOML config to %s", tomlFile.getAbsolutePath()));
|
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()
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
// I know ugly, but will work for now.
|
// I know ugly, but will work for now.
|
||||||
|
|
||||||
#define PS2_SYSCALL_LIST(X) \
|
#define PS2_SYSCALL_LIST(X) \
|
||||||
@@ -105,6 +107,7 @@
|
|||||||
X(GsPutIMR) \
|
X(GsPutIMR) \
|
||||||
X(iGsPutIMR) \
|
X(iGsPutIMR) \
|
||||||
X(SetVSyncFlag) \
|
X(SetVSyncFlag) \
|
||||||
|
X(SetSyscall) \
|
||||||
X(GsSetVideoMode) \
|
X(GsSetVideoMode) \
|
||||||
\
|
\
|
||||||
X(GetOsdConfigParam) \
|
X(GetOsdConfigParam) \
|
||||||
@@ -644,3 +647,9 @@
|
|||||||
X(syRtcInit) \
|
X(syRtcInit) \
|
||||||
X(InitThread) \
|
X(InitThread) \
|
||||||
/* Game/middleware */
|
/* Game/middleware */
|
||||||
|
|
||||||
|
// Test hooks: override pad input for scePadRead.
|
||||||
|
#define PS2_TEST_HOOK_LIST(X) \
|
||||||
|
X(setPadOverrideState, (uint16_t buttons, uint8_t lx, uint8_t ly, \
|
||||||
|
uint8_t rx, uint8_t ry)) \
|
||||||
|
X(clearPadOverrideState, (void))
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ namespace ps2_stubs
|
|||||||
PS2_STUB_LIST(PS2_DECLARE_STUB)
|
PS2_STUB_LIST(PS2_DECLARE_STUB)
|
||||||
#undef PS2_DECLARE_STUB
|
#undef PS2_DECLARE_STUB
|
||||||
|
|
||||||
|
#define PS2_DECLARE_TEST_HOOK(name, signature) void name signature;
|
||||||
|
PS2_TEST_HOOK_LIST(PS2_DECLARE_TEST_HOOK)
|
||||||
|
#undef PS2_DECLARE_TEST_HOOK
|
||||||
void resetGsSyncVCallbackState();
|
void resetGsSyncVCallbackState();
|
||||||
void dispatchGsSyncVCallback(uint8_t *rdram, PS2Runtime *runtime);
|
void dispatchGsSyncVCallback(uint8_t *rdram, PS2Runtime *runtime);
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ namespace ps2_stubs
|
|||||||
|
|
||||||
void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||||
void TODO_NAMED(const char *name, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
void TODO_NAMED(const char *name, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // PS2_STUBS_H
|
#endif // PS2_STUBS_H
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ namespace ps2_syscalls
|
|||||||
#undef PS2_DECLARE_SYSCALL
|
#undef PS2_DECLARE_SYSCALL
|
||||||
|
|
||||||
bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
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 TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encodedSyscallId);
|
||||||
void notifyRuntimeStop();
|
void notifyRuntimeStop();
|
||||||
void WaitVSyncTick(uint8_t *rdram, PS2Runtime *runtime);
|
void WaitVSyncTick(uint8_t *rdram, PS2Runtime *runtime);
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
#include "ps2_gs_gpu.h"
|
#include "ps2_gs_gpu.h"
|
||||||
#include <ThreadNaming.h>
|
#include <ThreadNaming.h>
|
||||||
|
|
||||||
|
namespace ps2_stubs
|
||||||
|
{
|
||||||
|
void resetSifState();
|
||||||
|
}
|
||||||
|
|
||||||
#define ELF_MAGIC 0x464C457F // "\x7FELF" in little endian
|
#define ELF_MAGIC 0x464C457F // "\x7FELF" in little endian
|
||||||
#define ET_EXEC 2 // Executable file
|
#define ET_EXEC 2 // Executable file
|
||||||
#define EM_MIPS 8 // MIPS architecture
|
#define EM_MIPS 8 // MIPS architecture
|
||||||
@@ -1701,7 +1706,9 @@ void PS2Runtime::HandleIntegerOverflow(R5900Context *ctx)
|
|||||||
void PS2Runtime::run()
|
void PS2Runtime::run()
|
||||||
{
|
{
|
||||||
m_stopRequested.store(false, std::memory_order_relaxed);
|
m_stopRequested.store(false, std::memory_order_relaxed);
|
||||||
|
ps2_stubs::resetSifState();
|
||||||
ps2_stubs::resetGsSyncVCallbackState();
|
ps2_stubs::resetGsSyncVCallbackState();
|
||||||
|
ps2_syscalls::initializeGuestKernelState(m_memory.getRDRAM());
|
||||||
m_cpuContext.r[4] = _mm_setzero_si128();
|
m_cpuContext.r[4] = _mm_setzero_si128();
|
||||||
m_cpuContext.r[5] = _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));
|
m_cpuContext.r[29] = _mm_set_epi64x(0, static_cast<int64_t>(PS2_RAM_SIZE - 0x10u));
|
||||||
|
|||||||
@@ -18,12 +18,200 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
|
#include "raylib.h"
|
||||||
#include "stubs/helpers/ps2_stubs_helpers.inl"
|
#include "stubs/helpers/ps2_stubs_helpers.inl"
|
||||||
|
|
||||||
namespace ps2_stubs
|
namespace ps2_stubs
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint8_t kPadModeDualShock = 0x73;
|
||||||
|
constexpr uint8_t kPadAnalogCenter = 0x80;
|
||||||
|
|
||||||
|
constexpr uint16_t kPadBtnSelect = 1u << 0;
|
||||||
|
constexpr uint16_t kPadBtnL3 = 1u << 1;
|
||||||
|
constexpr uint16_t kPadBtnR3 = 1u << 2;
|
||||||
|
constexpr uint16_t kPadBtnStart = 1u << 3;
|
||||||
|
constexpr uint16_t kPadBtnUp = 1u << 4;
|
||||||
|
constexpr uint16_t kPadBtnRight = 1u << 5;
|
||||||
|
constexpr uint16_t kPadBtnDown = 1u << 6;
|
||||||
|
constexpr uint16_t kPadBtnLeft = 1u << 7;
|
||||||
|
constexpr uint16_t kPadBtnL2 = 1u << 8;
|
||||||
|
constexpr uint16_t kPadBtnR2 = 1u << 9;
|
||||||
|
constexpr uint16_t kPadBtnL1 = 1u << 10;
|
||||||
|
constexpr uint16_t kPadBtnR1 = 1u << 11;
|
||||||
|
constexpr uint16_t kPadBtnTriangle = 1u << 12;
|
||||||
|
constexpr uint16_t kPadBtnCircle = 1u << 13;
|
||||||
|
constexpr uint16_t kPadBtnCross = 1u << 14;
|
||||||
|
constexpr uint16_t kPadBtnSquare = 1u << 15;
|
||||||
|
|
||||||
|
struct PadInputState
|
||||||
|
{
|
||||||
|
uint16_t buttons = 0xFFFF; // active-low
|
||||||
|
uint8_t rx = kPadAnalogCenter;
|
||||||
|
uint8_t ry = kPadAnalogCenter;
|
||||||
|
uint8_t lx = kPadAnalogCenter;
|
||||||
|
uint8_t ly = kPadAnalogCenter;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::mutex g_padOverrideMutex;
|
||||||
|
bool g_padOverrideEnabled = false;
|
||||||
|
PadInputState g_padOverrideState{};
|
||||||
|
bool g_padDebugCached = false;
|
||||||
|
bool g_padDebugEnabled = false;
|
||||||
|
|
||||||
|
uint8_t axisToByte(float axis)
|
||||||
|
{
|
||||||
|
axis = std::clamp(axis, -1.0f, 1.0f);
|
||||||
|
const float mapped = (axis + 1.0f) * 127.5f;
|
||||||
|
return static_cast<uint8_t>(std::lround(mapped));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool padDebugEnabled()
|
||||||
|
{
|
||||||
|
if (!g_padDebugCached)
|
||||||
|
{
|
||||||
|
const char *env = std::getenv("PS2_PAD_DEBUG");
|
||||||
|
g_padDebugEnabled = (env && *env && std::strcmp(env, "0") != 0);
|
||||||
|
g_padDebugCached = true;
|
||||||
|
}
|
||||||
|
return g_padDebugEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setButton(PadInputState &state, uint16_t mask, bool pressed)
|
||||||
|
{
|
||||||
|
if (pressed)
|
||||||
|
{
|
||||||
|
state.buttons = static_cast<uint16_t>(state.buttons & ~mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int findFirstGamepad()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
if (IsGamepadAvailable(i))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyGamepadState(PadInputState &state)
|
||||||
|
{
|
||||||
|
if (!IsWindowReady())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int gamepad = findFirstGamepad();
|
||||||
|
if (gamepad < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raylib mapping (PS2 -> raylib buttons/axes):
|
||||||
|
// D-Pad -> LEFT_FACE_*, Cross/Circle/Square/Triangle -> RIGHT_FACE_*
|
||||||
|
// L1/R1 -> TRIGGER_1, L2/R2 -> TRIGGER_2, L3/R3 -> THUMB
|
||||||
|
// Select/Start -> MIDDLE_LEFT/MIDDLE_RIGHT
|
||||||
|
state.lx = axisToByte(GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X));
|
||||||
|
state.ly = axisToByte(GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_Y));
|
||||||
|
state.rx = axisToByte(GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_X));
|
||||||
|
state.ry = axisToByte(GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_Y));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnUp, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_UP));
|
||||||
|
setButton(state, kPadBtnDown, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_DOWN));
|
||||||
|
setButton(state, kPadBtnLeft, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT));
|
||||||
|
setButton(state, kPadBtnRight, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnCross, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN));
|
||||||
|
setButton(state, kPadBtnCircle, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT));
|
||||||
|
setButton(state, kPadBtnSquare, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_LEFT));
|
||||||
|
setButton(state, kPadBtnTriangle, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_UP));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnL1, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_TRIGGER_1));
|
||||||
|
setButton(state, kPadBtnR1, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_TRIGGER_1));
|
||||||
|
setButton(state, kPadBtnL2, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_TRIGGER_2));
|
||||||
|
setButton(state, kPadBtnR2, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_TRIGGER_2));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnL3, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_THUMB));
|
||||||
|
setButton(state, kPadBtnR3, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_THUMB));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnSelect, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_MIDDLE_LEFT));
|
||||||
|
setButton(state, kPadBtnStart, IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_MIDDLE_RIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyKeyboardState(PadInputState &state, bool allowAnalog)
|
||||||
|
{
|
||||||
|
if (!IsWindowReady())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard mapping (PS2 -> keys):
|
||||||
|
// D-Pad: arrows, Square/Cross/Circle/Triangle: Z/X/C/V
|
||||||
|
// L1/R1: Q/E, L2/R2: 1/3, Start/Select: Enter/RightShift
|
||||||
|
// L3/R3: LeftCtrl/RightCtrl, Analog left: WASD
|
||||||
|
setButton(state, kPadBtnUp, IsKeyDown(KEY_UP));
|
||||||
|
setButton(state, kPadBtnDown, IsKeyDown(KEY_DOWN));
|
||||||
|
setButton(state, kPadBtnLeft, IsKeyDown(KEY_LEFT));
|
||||||
|
setButton(state, kPadBtnRight, IsKeyDown(KEY_RIGHT));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnSquare, IsKeyDown(KEY_Z));
|
||||||
|
setButton(state, kPadBtnCross, IsKeyDown(KEY_X));
|
||||||
|
setButton(state, kPadBtnCircle, IsKeyDown(KEY_C));
|
||||||
|
setButton(state, kPadBtnTriangle, IsKeyDown(KEY_V));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnL1, IsKeyDown(KEY_Q));
|
||||||
|
setButton(state, kPadBtnR1, IsKeyDown(KEY_E));
|
||||||
|
setButton(state, kPadBtnL2, IsKeyDown(KEY_ONE));
|
||||||
|
setButton(state, kPadBtnR2, IsKeyDown(KEY_THREE));
|
||||||
|
|
||||||
|
setButton(state, kPadBtnStart, IsKeyDown(KEY_ENTER));
|
||||||
|
setButton(state, kPadBtnSelect, IsKeyDown(KEY_RIGHT_SHIFT));
|
||||||
|
setButton(state, kPadBtnL3, IsKeyDown(KEY_LEFT_CONTROL));
|
||||||
|
setButton(state, kPadBtnR3, IsKeyDown(KEY_RIGHT_CONTROL));
|
||||||
|
|
||||||
|
if (!allowAnalog)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ax = 0.0f;
|
||||||
|
float ay = 0.0f;
|
||||||
|
if (IsKeyDown(KEY_D))
|
||||||
|
ax += 1.0f;
|
||||||
|
if (IsKeyDown(KEY_A))
|
||||||
|
ax -= 1.0f;
|
||||||
|
if (IsKeyDown(KEY_S))
|
||||||
|
ay += 1.0f;
|
||||||
|
if (IsKeyDown(KEY_W))
|
||||||
|
ay -= 1.0f;
|
||||||
|
|
||||||
|
if (ax != 0.0f || ay != 0.0f)
|
||||||
|
{
|
||||||
|
state.lx = axisToByte(ax);
|
||||||
|
state.ly = axisToByte(ay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillPadStatus(uint8_t *data, const PadInputState &state)
|
||||||
|
{
|
||||||
|
std::memset(data, 0, 32);
|
||||||
|
data[1] = kPadModeDualShock;
|
||||||
|
data[2] = static_cast<uint8_t>(state.buttons & 0xFFu);
|
||||||
|
data[3] = static_cast<uint8_t>((state.buttons >> 8) & 0xFFu);
|
||||||
|
data[4] = state.rx;
|
||||||
|
data[5] = state.ry;
|
||||||
|
data[6] = state.lx;
|
||||||
|
data[7] = state.ly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#include "stubs/ps2_stubs_libc.inl"
|
#include "stubs/ps2_stubs_libc.inl"
|
||||||
#include "stubs/ps2_stubs_ps2.inl"
|
#include "stubs/ps2_stubs_ps2.inl"
|
||||||
#include "stubs/ps2_stubs_misc.inl"
|
#include "stubs/ps2_stubs_misc.inl"
|
||||||
@@ -74,4 +262,22 @@ namespace ps2_stubs
|
|||||||
setReturnS32(ctx, -1); // Return error
|
setReturnS32(ctx, -1); // Return error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPadOverrideState(uint16_t buttons, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_padOverrideMutex);
|
||||||
|
g_padOverrideEnabled = true;
|
||||||
|
g_padOverrideState.buttons = buttons;
|
||||||
|
g_padOverrideState.lx = lx;
|
||||||
|
g_padOverrideState.ly = ly;
|
||||||
|
g_padOverrideState.rx = rx;
|
||||||
|
g_padOverrideState.ry = ry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPadOverrideState()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_padOverrideMutex);
|
||||||
|
g_padOverrideEnabled = false;
|
||||||
|
g_padOverrideState = PadInputState{};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -42,6 +43,11 @@ namespace ps2_syscalls
|
|||||||
|
|
||||||
bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
bool dispatchNumericSyscall(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
if (dispatchSyscallOverride(syscallNumber, rdram, ctx, runtime))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (syscallNumber)
|
switch (syscallNumber)
|
||||||
{
|
{
|
||||||
case 0x01:
|
case 0x01:
|
||||||
@@ -272,7 +278,7 @@ namespace ps2_syscalls
|
|||||||
SetVSyncFlag(rdram, ctx, runtime);
|
SetVSyncFlag(rdram, ctx, runtime);
|
||||||
return true;
|
return true;
|
||||||
case 0x74:
|
case 0x74:
|
||||||
RegisterExitHandler(rdram, ctx, runtime);
|
SetSyscall(rdram, ctx, runtime);
|
||||||
return true;
|
return true;
|
||||||
case 0x76:
|
case 0x76:
|
||||||
case static_cast<uint32_t>(-0x76):
|
case static_cast<uint32_t>(-0x76):
|
||||||
@@ -286,6 +292,9 @@ namespace ps2_syscalls
|
|||||||
case static_cast<uint32_t>(-0x78):
|
case static_cast<uint32_t>(-0x78):
|
||||||
ps2_stubs::sceSifSetDChain(rdram, ctx, runtime);
|
ps2_stubs::sceSifSetDChain(rdram, ctx, runtime);
|
||||||
return true;
|
return true;
|
||||||
|
case 0x83:
|
||||||
|
FindAddress(rdram, ctx, runtime);
|
||||||
|
return true;
|
||||||
case 0x85:
|
case 0x85:
|
||||||
SetMemoryMode(rdram, ctx, runtime);
|
SetMemoryMode(rdram, ctx, runtime);
|
||||||
return true;
|
return true;
|
||||||
@@ -388,5 +397,14 @@ namespace ps2_syscalls
|
|||||||
g_alarms.clear();
|
g_alarms.clear();
|
||||||
}
|
}
|
||||||
g_alarm_cv.notify_all();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2212,33 +2212,46 @@ void sceOpen(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
|
|
||||||
void scePadEnd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadEnd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadEnterPressMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadEnterPressMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadExitPressMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadExitPressMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadGetButtonMask(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadGetButtonMask(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
|
// Report all buttons supported.
|
||||||
setReturnS32(ctx, static_cast<int32_t>(0xFFFFu));
|
setReturnS32(ctx, static_cast<int32_t>(0xFFFFu));
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadGetDmaStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadGetDmaStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnU32(ctx, getRegU32(ctx, 6));
|
setReturnU32(ctx, getRegU32(ctx, 6));
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadGetFrameCount(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadGetFrameCount(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
static uint32_t frameCount = 0u;
|
(void)rdram;
|
||||||
setReturnU32(ctx, ++frameCount);
|
(void)runtime;
|
||||||
|
static std::atomic<uint32_t> frameCount{0};
|
||||||
|
setReturnU32(ctx, frameCount++);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadGetModVersion(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadGetModVersion(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
@@ -2293,6 +2306,9 @@ void scePadInfoAct(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
|
|
||||||
void scePadInfoComb(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadInfoComb(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
|
// No combined modes reported.
|
||||||
setReturnS32(ctx, 0);
|
setReturnS32(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2384,90 +2400,145 @@ void scePadRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
setReturnS32(ctx, 0);
|
setReturnS32(ctx, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (runtime && runtime->padBackend().readState(port, slot, data, 32))
|
|
||||||
|
PadInputState state;
|
||||||
|
bool useOverride = false;
|
||||||
{
|
{
|
||||||
setReturnS32(ctx, 1);
|
std::lock_guard<std::mutex> lock(g_padOverrideMutex);
|
||||||
return;
|
if (g_padOverrideEnabled)
|
||||||
|
{
|
||||||
|
state = g_padOverrideState;
|
||||||
|
useOverride = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
std::memset(data, 0, 32);
|
|
||||||
data[1] = 0x73;
|
if (!useOverride)
|
||||||
data[2] = data[3] = 0xFF;
|
{
|
||||||
data[4] = data[5] = data[6] = data[7] = 0x80;
|
if (runtime && runtime->padBackend().readState(port, slot, data, 32))
|
||||||
|
{
|
||||||
|
setReturnS32(ctx, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyGamepadState(state);
|
||||||
|
applyKeyboardState(state, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillPadStatus(data, state);
|
||||||
|
|
||||||
|
if (padDebugEnabled())
|
||||||
|
{
|
||||||
|
static uint32_t logCounter = 0;
|
||||||
|
if ((logCounter++ % 60u) == 0u)
|
||||||
|
{
|
||||||
|
std::cout << "[pad] buttons=0x" << std::hex << state.buttons << std::dec
|
||||||
|
<< " lx=" << static_cast<int>(state.lx)
|
||||||
|
<< " ly=" << static_cast<int>(state.ly)
|
||||||
|
<< " rx=" << static_cast<int>(state.rx)
|
||||||
|
<< " ry=" << static_cast<int>(state.ry)
|
||||||
|
<< (useOverride ? " (override)" : "") << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadReqIntToStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadReqIntToStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
const uint32_t outAddr = getRegU32(ctx, 5);
|
(void)runtime;
|
||||||
if (uint8_t *out = getMemPtr(rdram, outAddr))
|
const uint32_t state = getRegU32(ctx, 4);
|
||||||
|
const uint32_t strAddr = getRegU32(ctx, 5);
|
||||||
|
char *buf = reinterpret_cast<char *>(getMemPtr(rdram, strAddr));
|
||||||
|
if (!buf)
|
||||||
{
|
{
|
||||||
constexpr const char *kReq = "COMPLETE";
|
setReturnS32(ctx, -1);
|
||||||
std::memcpy(out, kReq, std::strlen(kReq) + 1u);
|
|
||||||
setReturnU32(ctx, outAddr);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setReturnU32(ctx, 0u);
|
|
||||||
|
const char *text = (state == 0) ? "COMPLETE" : "BUSY";
|
||||||
|
std::strncpy(buf, text, 31);
|
||||||
|
buf[31] = '\0';
|
||||||
|
setReturnS32(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetActAlign(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetActAlign(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetActDirect(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetActDirect(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetButtonInfo(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetButtonInfo(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetMainMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetMainMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetReqState(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetReqState(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetVrefParam(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetVrefParam(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
setReturnS32(ctx, 1);
|
setReturnS32(ctx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadSetWarningLevel(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadSetWarningLevel(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
setReturnS32(ctx, 1);
|
(void)rdram;
|
||||||
|
(void)runtime;
|
||||||
|
setReturnS32(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePadStateIntToStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePadStateIntToStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
const int32_t state = static_cast<int32_t>(getRegU32(ctx, 4));
|
(void)runtime;
|
||||||
const uint32_t outAddr = getRegU32(ctx, 5);
|
const uint32_t state = getRegU32(ctx, 4);
|
||||||
const char *label = "UNKNOWN";
|
const uint32_t strAddr = getRegU32(ctx, 5);
|
||||||
switch (state)
|
char *buf = reinterpret_cast<char *>(getMemPtr(rdram, strAddr));
|
||||||
|
if (!buf)
|
||||||
{
|
{
|
||||||
case 0:
|
setReturnS32(ctx, -1);
|
||||||
label = "DISCONN";
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
label = "STABLE";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uint8_t *out = getMemPtr(rdram, outAddr))
|
|
||||||
{
|
|
||||||
std::memcpy(out, label, std::strlen(label) + 1u);
|
|
||||||
setReturnU32(ctx, outAddr);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setReturnU32(ctx, 0u);
|
|
||||||
|
const char *text = "UNKNOWN";
|
||||||
|
if (state == 6)
|
||||||
|
{
|
||||||
|
text = "STABLE";
|
||||||
|
}
|
||||||
|
else if (state == 1)
|
||||||
|
{
|
||||||
|
text = "FINDPAD";
|
||||||
|
}
|
||||||
|
else if (state == 0)
|
||||||
|
{
|
||||||
|
text = "DISCONNECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::strncpy(buf, text, 31);
|
||||||
|
buf[31] = '\0';
|
||||||
|
setReturnS32(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scePrintf(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void scePrintf(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
@@ -2556,6 +2627,55 @@ namespace
|
|||||||
uint32_t g_sifCmdBuffer = 0u;
|
uint32_t g_sifCmdBuffer = 0u;
|
||||||
uint32_t g_sifSysCmdBuffer = 0u;
|
uint32_t g_sifSysCmdBuffer = 0u;
|
||||||
bool g_sifCmdInitialized = false;
|
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()
|
uint32_t allocateSifDmaTransferId()
|
||||||
{
|
{
|
||||||
@@ -2673,6 +2793,12 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetSifState()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||||
|
seedDefaultSifRegsLocked();
|
||||||
|
}
|
||||||
|
|
||||||
void sceSifAddCmdHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void sceSifAddCmdHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
const uint32_t cid = getRegU32(ctx, 4);
|
const uint32_t cid = getRegU32(ctx, 4);
|
||||||
@@ -2725,8 +2851,7 @@ void sceSifExecRequest(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
void sceSifExitCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void sceSifExitCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||||
g_sifCmdInitialized = false;
|
seedDefaultSifRegsLocked();
|
||||||
g_sifCmdHandlers.clear();
|
|
||||||
setReturnS32(ctx, 0);
|
setReturnS32(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2983,6 +3108,7 @@ void sceSifGetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
{
|
{
|
||||||
const uint32_t reg = getRegU32(ctx, 4);
|
const uint32_t reg = getRegU32(ctx, 4);
|
||||||
uint32_t value = 0u;
|
uint32_t value = 0u;
|
||||||
|
bool shouldLog = false;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||||
auto it = g_sifRegs.find(reg);
|
auto it = g_sifRegs.find(reg);
|
||||||
@@ -2990,6 +3116,21 @@ void sceSifGetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
{
|
{
|
||||||
value = it->second;
|
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);
|
setReturnU32(ctx, value);
|
||||||
}
|
}
|
||||||
@@ -3188,6 +3329,8 @@ void sceSifSetDma(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps2_syscalls::dispatchDmacHandlersForCause(rdram, runtime, 5u);
|
||||||
|
|
||||||
setReturnS32(ctx, static_cast<int32_t>(allocateSifDmaTransferId()));
|
setReturnS32(ctx, static_cast<int32_t>(allocateSifDmaTransferId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3201,6 +3344,7 @@ void sceSifSetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
const uint32_t reg = getRegU32(ctx, 4);
|
const uint32_t reg = getRegU32(ctx, 4);
|
||||||
const uint32_t value = getRegU32(ctx, 5);
|
const uint32_t value = getRegU32(ctx, 5);
|
||||||
uint32_t prev = 0u;
|
uint32_t prev = 0u;
|
||||||
|
bool shouldLog = false;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||||
auto it = g_sifRegs.find(reg);
|
auto it = g_sifRegs.find(reg);
|
||||||
@@ -3209,6 +3353,22 @@ void sceSifSetReg(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
prev = it->second;
|
prev = it->second;
|
||||||
}
|
}
|
||||||
g_sifRegs[reg] = value;
|
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);
|
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)
|
static void setRegU32(R5900Context *ctx, int reg, uint32_t value)
|
||||||
{
|
{
|
||||||
if (reg < 0 || reg > 31)
|
if (!ctx || reg < 0 || reg > 31)
|
||||||
return;
|
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)
|
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;
|
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,
|
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)
|
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 steps = 0u;
|
||||||
uint32_t lastPc = 0xFFFFFFFFu;
|
uint32_t lastPc = 0xFFFFFFFFu;
|
||||||
uint32_t samePcCount = 0u;
|
uint32_t samePcCount = 0u;
|
||||||
|
RpcInvokeExitReason exitReason = RpcInvokeExitReason::MissingFunction;
|
||||||
while (tmp.pc != 0u &&
|
while (tmp.pc != 0u &&
|
||||||
tmp.pc != kRpcInvokeReturnSentinel &&
|
tmp.pc != kRpcInvokeReturnSentinel &&
|
||||||
runtime->hasFunction(tmp.pc) &&
|
runtime->hasFunction(tmp.pc) &&
|
||||||
@@ -318,6 +347,7 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run
|
|||||||
++samePcCount;
|
++samePcCount;
|
||||||
if (samePcCount > 0x2000u)
|
if (samePcCount > 0x2000u)
|
||||||
{
|
{
|
||||||
|
exitReason = RpcInvokeExitReason::SamePcLimit;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +366,41 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run
|
|||||||
{
|
{
|
||||||
*outV0 = getRegU32(&tmp, 2);
|
*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)
|
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 uint32_t g_bootmode_pool_offset = 0;
|
||||||
static std::unordered_map<uint8_t, uint32_t> g_bootmode_addresses;
|
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 std::mutex g_tls_mutex;
|
||||||
static uint32_t g_tls_index = 0;
|
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)
|
static uint64_t signalVSyncFlag(uint8_t *rdram)
|
||||||
{
|
{
|
||||||
VSyncFlagRegistration reg{};
|
VSyncFlagRegistration reg{};
|
||||||
|
|||||||
@@ -264,6 +264,239 @@ void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encod
|
|||||||
setReturnS32(ctx, 0);
|
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
|
// 0x3C SetupThread
|
||||||
// args: $a0 = gp, $a1 = stack, $a2 = stack_size, $a3 = args, $t0 = root_func
|
// args: $a0 = gp, $a1 = stack, $a2 = stack_size, $a3 = args, $t0 = root_func
|
||||||
void SetupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void SetupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
@@ -355,6 +588,292 @@ void GetMemorySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
|||||||
setReturnU32(ctx, PS2_RAM_SIZE);
|
setReturnU32(ctx, PS2_RAM_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline uint32_t normalizeKernelAlias(uint32_t addr)
|
||||||
|
{
|
||||||
|
if (addr >= 0x80000000u && addr < 0xC0000000u)
|
||||||
|
{
|
||||||
|
return addr & 0x1FFFFFFFu;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
// - a2: target address to locate inside the table (word entries)
|
||||||
|
// Returns the guest address of the matching word entry, or 0 if not found.
|
||||||
|
void FindAddress(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
|
{
|
||||||
|
(void)runtime;
|
||||||
|
|
||||||
|
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;
|
||||||
|
end &= ~0x3u;
|
||||||
|
|
||||||
|
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));
|
||||||
|
++scannedWords;
|
||||||
|
|
||||||
|
if (firstWordCount < kFindAddressWordSamples)
|
||||||
|
{
|
||||||
|
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};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
void Deci2Call(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
(void)rdram;
|
(void)rdram;
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Blackline Interactive
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
project(ps2xStudio)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
imgui
|
||||||
|
GIT_REPOSITORY https://github.com/ocornut/imgui.git
|
||||||
|
GIT_TAG docking
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
imgui_colortextedit
|
||||||
|
GIT_REPOSITORY https://github.com/BalazsJako/ImGuiColorTextEdit.git
|
||||||
|
GIT_TAG master
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
imgui_club
|
||||||
|
GIT_REPOSITORY https://github.com/ocornut/imgui_club.git
|
||||||
|
GIT_TAG main
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
imgui_file_dialog
|
||||||
|
GIT_REPOSITORY https://github.com/aiekick/ImGuiFileDialog.git
|
||||||
|
GIT_TAG master
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
SDL2
|
||||||
|
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
|
||||||
|
GIT_TAG release-2.30.9
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SDL_SHARED ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_STATIC OFF CACHE BOOL "" FORCE)
|
||||||
|
|
||||||
|
FetchContent_MakeAvailable(imgui imgui_colortextedit imgui_club SDL2)
|
||||||
|
|
||||||
|
FetchContent_GetProperties(imgui_file_dialog)
|
||||||
|
if(NOT imgui_file_dialog_POPULATED)
|
||||||
|
FetchContent_Populate(imgui_file_dialog)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(OpenGL REQUIRED)
|
||||||
|
|
||||||
|
set(IMGUI_SOURCES
|
||||||
|
${imgui_SOURCE_DIR}/imgui.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_draw.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_tables.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_widgets.cpp
|
||||||
|
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
||||||
|
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl2.cpp
|
||||||
|
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(imgui_lib STATIC ${IMGUI_SOURCES})
|
||||||
|
|
||||||
|
target_include_directories(imgui_lib PUBLIC
|
||||||
|
${imgui_SOURCE_DIR}
|
||||||
|
${imgui_SOURCE_DIR}/backends
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(imgui_lib PUBLIC SDL2::SDL2 OpenGL::GL)
|
||||||
|
|
||||||
|
add_library(imgui_colortextedit_lib STATIC
|
||||||
|
${imgui_colortextedit_SOURCE_DIR}/TextEditor.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(imgui_colortextedit_lib PUBLIC ${imgui_colortextedit_SOURCE_DIR})
|
||||||
|
|
||||||
|
target_link_libraries(imgui_colortextedit_lib PUBLIC imgui_lib)
|
||||||
|
|
||||||
|
add_library(imgui_memory_editor_lib INTERFACE)
|
||||||
|
|
||||||
|
target_include_directories(imgui_memory_editor_lib INTERFACE
|
||||||
|
${imgui_club_SOURCE_DIR}/imgui_memory_editor
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(imgui_memory_editor_lib INTERFACE imgui_lib)
|
||||||
|
|
||||||
|
add_library(imgui_filedialog_lib STATIC
|
||||||
|
${imgui_file_dialog_SOURCE_DIR}/ImGuiFileDialog.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(imgui_filedialog_lib PUBLIC
|
||||||
|
${imgui_file_dialog_SOURCE_DIR}
|
||||||
|
${imgui_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(imgui_filedialog_lib PUBLIC imgui_lib)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE STUDIO_SOURCES "src/*.cpp")
|
||||||
|
file(GLOB_RECURSE STUDIO_HEADERS "include/*.hpp")
|
||||||
|
|
||||||
|
add_executable(ps2xStudio ${STUDIO_SOURCES} ${STUDIO_HEADERS})
|
||||||
|
|
||||||
|
target_include_directories(ps2xStudio PRIVATE
|
||||||
|
include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ps2xAnalyzer/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ps2xRecomp/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../ps2xTest/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(ps2xStudio PRIVATE
|
||||||
|
imgui_lib
|
||||||
|
imgui_colortextedit_lib
|
||||||
|
imgui_memory_editor_lib
|
||||||
|
imgui_filedialog_lib
|
||||||
|
ps2_recomp_lib
|
||||||
|
ps2_analyzer_lib
|
||||||
|
ps2_test_lib
|
||||||
|
SDL2::SDL2
|
||||||
|
OpenGL::GL
|
||||||
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(ps2xStudio PRIVATE SDL2::SDL2main)
|
||||||
|
endif()
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "StudioState.hpp"
|
||||||
|
|
||||||
|
namespace GUI {
|
||||||
|
void DrawStudio(StudioState& state);
|
||||||
|
void ApplySettings(StudioState& state);
|
||||||
|
|
||||||
|
// Called from main loop BEFORE NewFrame when fonts need rebuilding
|
||||||
|
void RebuildFontsIfNeeded(StudioState& state);
|
||||||
|
|
||||||
|
// Notify GUI that config editor should resync
|
||||||
|
void SyncConfigEditor(StudioState& state);
|
||||||
|
|
||||||
|
// Returns true when user requested exit (File > Exit)
|
||||||
|
bool WantsQuit();
|
||||||
|
}
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <future>
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "ps2recomp/elf_analyzer.h"
|
||||||
|
#include "ps2recomp/config_manager.h"
|
||||||
|
#include "ps2recomp/ps2_recompiler.h"
|
||||||
|
#include "ps2recomp/types.h"
|
||||||
|
|
||||||
|
#include "../../ps2xTest/include/MiniTest.h"
|
||||||
|
extern void register_code_generator_tests();
|
||||||
|
extern void register_r5900_decoder_tests();
|
||||||
|
|
||||||
|
enum class OverrideStatus {
|
||||||
|
Default, Stub, Skip, ForceRecompile
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ThemeMode {
|
||||||
|
Dark, Light, Custom
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppSettings {
|
||||||
|
ThemeMode theme = ThemeMode::Dark;
|
||||||
|
float fontSize = 15.0f;
|
||||||
|
float uiScale = 1.0f;
|
||||||
|
std::string selectedFont = "Font_1.ttf";
|
||||||
|
int windowWidth = 1280;
|
||||||
|
int windowHeight = 720;
|
||||||
|
bool maximized = true;
|
||||||
|
|
||||||
|
// Custom theme colors
|
||||||
|
float customBgBase[4] = {0.08f, 0.08f, 0.08f, 1.00f};
|
||||||
|
float customAccent[4] = {0.00f, 0.48f, 0.80f, 1.00f};
|
||||||
|
};
|
||||||
|
|
||||||
|
class StreamRedirector : public std::stringbuf {
|
||||||
|
public:
|
||||||
|
StreamRedirector(std::vector<std::string>& target, std::mutex& mtx, std::atomic<size_t>& ver)
|
||||||
|
: logs(target), mutex(mtx), version(ver) {}
|
||||||
|
int sync() override {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
std::string line = this->str();
|
||||||
|
if (!line.empty() && line.back() == '\n') line.pop_back();
|
||||||
|
if (!line.empty()) {
|
||||||
|
logs.push_back(line);
|
||||||
|
version.fetch_add(1);
|
||||||
|
}
|
||||||
|
this->str("");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
std::vector<std::string>& logs;
|
||||||
|
std::mutex& mutex;
|
||||||
|
std::atomic<size_t>& version;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIState {
|
||||||
|
std::string elfPath;
|
||||||
|
std::string outputPath = "output";
|
||||||
|
std::string customOutputPath;
|
||||||
|
std::string ghidraCSVPath;
|
||||||
|
std::string ghidraCSVContent;
|
||||||
|
std::string configTomlContent;
|
||||||
|
std::vector<uint8_t> rawElfData;
|
||||||
|
std::map<std::string, OverrideStatus> funcOverrides;
|
||||||
|
std::shared_ptr<ps2recomp::ElfAnalyzer> analyzer;
|
||||||
|
bool isAnalysisComplete = false;
|
||||||
|
|
||||||
|
// NEW: Support for multiple imported CSV files
|
||||||
|
std::vector<std::string> importedCSVFiles;
|
||||||
|
int selectedCSVIndex = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
class StudioState {
|
||||||
|
public:
|
||||||
|
UIState data;
|
||||||
|
AppSettings settings;
|
||||||
|
int selectedFuncIndex = -1;
|
||||||
|
std::atomic<bool> isBusy = false;
|
||||||
|
std::vector<std::string> logs;
|
||||||
|
std::mutex stateMutex;
|
||||||
|
std::streambuf* oldCout = nullptr;
|
||||||
|
std::streambuf* oldCerr = nullptr;
|
||||||
|
std::unique_ptr<StreamRedirector> redirector;
|
||||||
|
std::future<void> workerThread;
|
||||||
|
std::vector<std::string> availableFonts;
|
||||||
|
|
||||||
|
// Deferred font rebuild flag - fonts are rebuilt in the main loop BEFORE NewFrame
|
||||||
|
std::atomic<bool> pendingFontRebuild{false};
|
||||||
|
|
||||||
|
// Resolved absolute path to config.toml
|
||||||
|
std::string configTomlPath;
|
||||||
|
|
||||||
|
// Track log changes for efficient UI updates
|
||||||
|
std::atomic<size_t> logVersion{0};
|
||||||
|
|
||||||
|
// Thread-safe status message
|
||||||
|
void SetStatus(const std::string& msg) {
|
||||||
|
std::lock_guard<std::mutex> lock(statusMutex_);
|
||||||
|
statusMessage_ = msg;
|
||||||
|
}
|
||||||
|
std::string GetStatus() const {
|
||||||
|
std::lock_guard<std::mutex> lock(statusMutex_);
|
||||||
|
return statusMessage_;
|
||||||
|
}
|
||||||
|
|
||||||
|
StudioState() {
|
||||||
|
redirector = std::make_unique<StreamRedirector>(logs, stateMutex, logVersion);
|
||||||
|
oldCout = std::cout.rdbuf(redirector.get());
|
||||||
|
oldCerr = std::cerr.rdbuf(redirector.get());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!std::filesystem::exists("output")) {
|
||||||
|
std::filesystem::create_directory("output");
|
||||||
|
}
|
||||||
|
data.outputPath = std::filesystem::absolute("output").string();
|
||||||
|
} catch(...) {}
|
||||||
|
|
||||||
|
LoadSettings();
|
||||||
|
ScanFonts();
|
||||||
|
|
||||||
|
// Load config.toml immediately and resolve its path
|
||||||
|
LoadConfigToml();
|
||||||
|
}
|
||||||
|
|
||||||
|
~StudioState() {
|
||||||
|
std::cout.rdbuf(oldCout);
|
||||||
|
std::cerr.rdbuf(oldCerr);
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Log(const std::string& msg) {
|
||||||
|
std::lock_guard<std::mutex> lock(stateMutex);
|
||||||
|
logs.push_back("[Studio] " + msg);
|
||||||
|
logVersion.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogRaw(const std::string& msg) {
|
||||||
|
std::lock_guard<std::mutex> lock(stateMutex);
|
||||||
|
logs.push_back(msg);
|
||||||
|
logVersion.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanFonts() {
|
||||||
|
availableFonts.clear();
|
||||||
|
try {
|
||||||
|
std::filesystem::path fontDir = "external/Font";
|
||||||
|
if (std::filesystem::exists(fontDir)) {
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(fontDir)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
std::string ext = entry.path().extension().string();
|
||||||
|
if (ext == ".ttf" || ext == ".TTF") {
|
||||||
|
availableFonts.push_back(entry.path().filename().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(...) {}
|
||||||
|
|
||||||
|
if (availableFonts.empty()) {
|
||||||
|
availableFonts.push_back("Default");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadSettings() {
|
||||||
|
try {
|
||||||
|
std::ifstream file("studio_settings.ini");
|
||||||
|
if (file) {
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
size_t pos = line.find('=');
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
std::string key = line.substr(0, pos);
|
||||||
|
std::string value = line.substr(pos + 1);
|
||||||
|
|
||||||
|
if (key == "theme") {
|
||||||
|
int t = std::stoi(value);
|
||||||
|
settings.theme = static_cast<ThemeMode>(t);
|
||||||
|
} else if (key == "fontSize") {
|
||||||
|
float newSize = std::stof(value);
|
||||||
|
// Validate font size
|
||||||
|
if (newSize >= 10.0f && newSize <= 48.0f) {
|
||||||
|
settings.fontSize = newSize;
|
||||||
|
}
|
||||||
|
} else if (key == "uiScale") {
|
||||||
|
float newScale = std::stof(value);
|
||||||
|
if (newScale >= 0.5f && newScale <= 3.0f) {
|
||||||
|
settings.uiScale = newScale;
|
||||||
|
}
|
||||||
|
} else if (key == "selectedFont") {
|
||||||
|
settings.selectedFont = value;
|
||||||
|
} else if (key == "windowWidth") {
|
||||||
|
settings.windowWidth = std::stoi(value);
|
||||||
|
} else if (key == "windowHeight") {
|
||||||
|
settings.windowHeight = std::stoi(value);
|
||||||
|
} else if (key == "maximized") {
|
||||||
|
settings.maximized = (value == "1");
|
||||||
|
} else if (key == "customOutputPath") {
|
||||||
|
data.customOutputPath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(...) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveSettings() {
|
||||||
|
try {
|
||||||
|
std::ofstream file("studio_settings.ini");
|
||||||
|
file << "theme=" << static_cast<int>(settings.theme) << "\n";
|
||||||
|
file << "fontSize=" << settings.fontSize << "\n";
|
||||||
|
file << "uiScale=" << settings.uiScale << "\n";
|
||||||
|
file << "selectedFont=" << settings.selectedFont << "\n";
|
||||||
|
file << "windowWidth=" << settings.windowWidth << "\n";
|
||||||
|
file << "windowHeight=" << settings.windowHeight << "\n";
|
||||||
|
file << "maximized=" << (settings.maximized ? "1" : "0") << "\n";
|
||||||
|
if (!data.customOutputPath.empty()) {
|
||||||
|
file << "customOutputPath=" << data.customOutputPath << "\n";
|
||||||
|
}
|
||||||
|
} catch(...) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadELF(const std::string& path) {
|
||||||
|
data.elfPath = path;
|
||||||
|
data.isAnalysisComplete = false;
|
||||||
|
data.analyzer.reset();
|
||||||
|
data.funcOverrides.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (file) {
|
||||||
|
data.rawElfData.assign((std::istreambuf_iterator<char>(file)),
|
||||||
|
(std::istreambuf_iterator<char>()));
|
||||||
|
} else {
|
||||||
|
data.rawElfData.clear();
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
data.rawElfData.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string filename = std::filesystem::path(path).filename().string();
|
||||||
|
SetStatus("Loaded: " + filename);
|
||||||
|
Log("File loaded: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImportGhidraCSV(const std::string& csvPath) {
|
||||||
|
if (!data.analyzer) {
|
||||||
|
Log("Error: Analyze ELF first before importing symbols.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ghidraCSVPath = csvPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream file(csvPath);
|
||||||
|
if (file) {
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
data.ghidraCSVContent = buffer.str();
|
||||||
|
}
|
||||||
|
} catch(...) {}
|
||||||
|
|
||||||
|
data.analyzer->importGhidraMap(csvPath);
|
||||||
|
Log("Ghidra symbols imported successfully from: " + csvPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetOutputDir(const std::string& path) {
|
||||||
|
if (path.empty()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data.customOutputPath = std::filesystem::absolute(path).string();
|
||||||
|
if (!std::filesystem::exists(data.customOutputPath)) {
|
||||||
|
std::filesystem::create_directories(data.customOutputPath);
|
||||||
|
}
|
||||||
|
Log("Custom output directory set to: " + data.customOutputPath);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Log(std::string("Error setting output dir: ") + e.what());
|
||||||
|
data.customOutputPath = std::filesystem::absolute("output").string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StartAnalysis(std::string path = "") {
|
||||||
|
if (isBusy) return;
|
||||||
|
if (!path.empty()) data.elfPath = path;
|
||||||
|
if (data.elfPath.empty()) {
|
||||||
|
Log("Error: No file selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBusy = true;
|
||||||
|
SetStatus("Analyzing...");
|
||||||
|
std::string currentPath = data.elfPath;
|
||||||
|
|
||||||
|
workerThread = std::async(std::launch::async, [this, currentPath]() {
|
||||||
|
try {
|
||||||
|
auto newAnalyzer = std::make_shared<ps2recomp::ElfAnalyzer>(currentPath);
|
||||||
|
if (newAnalyzer->analyze()) {
|
||||||
|
std::lock_guard<std::mutex> lock(stateMutex);
|
||||||
|
data.analyzer = newAnalyzer;
|
||||||
|
data.isAnalysisComplete = true;
|
||||||
|
SetStatus("Analysis Complete");
|
||||||
|
logVersion.fetch_add(1);
|
||||||
|
} else {
|
||||||
|
SetStatus("Analysis Failed");
|
||||||
|
Log("Analysis failed for: " + currentPath);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
SetStatus("Error");
|
||||||
|
Log(std::string("Analysis exception: ") + e.what());
|
||||||
|
}
|
||||||
|
isBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoveOutputFiles() {
|
||||||
|
if (data.customOutputPath.empty() || data.customOutputPath == data.outputPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::filesystem::path srcDir = data.outputPath;
|
||||||
|
std::filesystem::path dstDir = data.customOutputPath;
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(dstDir)) {
|
||||||
|
std::filesystem::create_directories(dstDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(srcDir)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
std::filesystem::path dst = dstDir / entry.path().filename();
|
||||||
|
std::filesystem::copy_file(entry.path(), dst,
|
||||||
|
std::filesystem::copy_options::overwrite_existing);
|
||||||
|
Log("Moved: " + entry.path().filename().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("All output files moved to: " + data.customOutputPath);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Log(std::string("Error moving files: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StartRecompilation() {
|
||||||
|
if (isBusy || !data.isAnalysisComplete) return;
|
||||||
|
|
||||||
|
// Set the effective output path BEFORE saving config
|
||||||
|
std::string effectiveOutputPath = data.customOutputPath.empty()
|
||||||
|
? data.outputPath : data.customOutputPath;
|
||||||
|
|
||||||
|
if (effectiveOutputPath.empty()) {
|
||||||
|
SetOutputDir("output");
|
||||||
|
effectiveOutputPath = data.outputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
try {
|
||||||
|
if (!std::filesystem::exists(effectiveOutputPath)) {
|
||||||
|
std::filesystem::create_directories(effectiveOutputPath);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Log(std::string("Error creating output dir: ") + e.what());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveConfigTOML();
|
||||||
|
isBusy = true;
|
||||||
|
SetStatus("Recompiling...");
|
||||||
|
|
||||||
|
// Use the resolved config path
|
||||||
|
std::string resolvedConfigPath = configTomlPath.empty() ? "config.toml" : configTomlPath;
|
||||||
|
|
||||||
|
workerThread = std::async(std::launch::async, [this, resolvedConfigPath]() {
|
||||||
|
try {
|
||||||
|
ps2recomp::PS2Recompiler recompiler(resolvedConfigPath);
|
||||||
|
if (recompiler.initialize() && recompiler.recompile()) {
|
||||||
|
recompiler.generateOutput();
|
||||||
|
SetStatus("Recompilation Success");
|
||||||
|
Log("Recompilation completed successfully");
|
||||||
|
LoadConfigToml();
|
||||||
|
} else {
|
||||||
|
SetStatus("Recompilation Failed");
|
||||||
|
Log("Recompilation failed");
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
SetStatus("Recompilation Error");
|
||||||
|
Log(std::string("Recompilation error: ") + e.what());
|
||||||
|
}
|
||||||
|
isBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunDiagnostics() {
|
||||||
|
if (isBusy) return;
|
||||||
|
isBusy = true;
|
||||||
|
SetStatus("Running Diagnostics...");
|
||||||
|
|
||||||
|
workerThread = std::async(std::launch::async, [this]() {
|
||||||
|
try {
|
||||||
|
register_code_generator_tests();
|
||||||
|
register_r5900_decoder_tests();
|
||||||
|
std::cout << "\n--- DIAGNOSTICS ---\n" << std::endl;
|
||||||
|
int failed = MiniTest::Run();
|
||||||
|
if (failed == 0) {
|
||||||
|
SetStatus("System Healthy");
|
||||||
|
std::cout << "\n[SUCCESS] All systems operational." << std::endl;
|
||||||
|
} else {
|
||||||
|
SetStatus("Issues Found");
|
||||||
|
std::cerr << "\n[WARNING] " << failed << " tests failed!" << std::endl;
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
SetStatus("Diagnostics Error");
|
||||||
|
std::cerr << "Critical failure: " << e.what() << std::endl;
|
||||||
|
}
|
||||||
|
isBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve and load config.toml with proper path tracking
|
||||||
|
void LoadConfigToml() {
|
||||||
|
bool loaded = false;
|
||||||
|
|
||||||
|
// Build search paths - prioritize custom output path and previously resolved path
|
||||||
|
std::vector<std::string> configPaths;
|
||||||
|
if (!configTomlPath.empty()) {
|
||||||
|
configPaths.push_back(configTomlPath); // previously resolved
|
||||||
|
}
|
||||||
|
configPaths.push_back("config.toml");
|
||||||
|
if (!data.customOutputPath.empty()) {
|
||||||
|
configPaths.push_back(data.customOutputPath + "/config.toml");
|
||||||
|
}
|
||||||
|
if (!data.outputPath.empty()) {
|
||||||
|
configPaths.push_back(data.outputPath + "/config.toml");
|
||||||
|
}
|
||||||
|
configPaths.push_back("../config.toml");
|
||||||
|
|
||||||
|
for (const auto& path : configPaths) {
|
||||||
|
if (path.empty()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (file && file.good()) {
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
std::string content = buffer.str();
|
||||||
|
if (!content.empty()) {
|
||||||
|
data.configTomlContent = content;
|
||||||
|
configTomlPath = std::filesystem::absolute(path).string();
|
||||||
|
Log("Config loaded from: " + configTomlPath);
|
||||||
|
loaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(...) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
// Create default config in CWD
|
||||||
|
configTomlPath = std::filesystem::absolute("config.toml").string();
|
||||||
|
CreateDefaultConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Create default config if none exists
|
||||||
|
void CreateDefaultConfig() {
|
||||||
|
try {
|
||||||
|
std::string defaultConfig = R"(# PS2Recomp Configuration File
|
||||||
|
# Generated by PS2Recomp Studio
|
||||||
|
|
||||||
|
[input]
|
||||||
|
path = ""
|
||||||
|
|
||||||
|
[output]
|
||||||
|
path = "output"
|
||||||
|
single_file = false
|
||||||
|
|
||||||
|
[functions]
|
||||||
|
stub = []
|
||||||
|
skip = []
|
||||||
|
force_recompile = []
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
optimize = true
|
||||||
|
debug_info = false
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string savePath = configTomlPath.empty() ? "config.toml" : configTomlPath;
|
||||||
|
std::ofstream file(savePath);
|
||||||
|
if (file) {
|
||||||
|
file << defaultConfig;
|
||||||
|
file.flush();
|
||||||
|
data.configTomlContent = defaultConfig;
|
||||||
|
if (configTomlPath.empty()) {
|
||||||
|
configTomlPath = std::filesystem::absolute(savePath).string();
|
||||||
|
}
|
||||||
|
Log("Created default config at: " + configTomlPath);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Log(std::string("Error creating default config: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveConfigTOML() {
|
||||||
|
if (!data.isAnalysisComplete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ps2recomp::RecompilerConfig config;
|
||||||
|
config.inputPath = data.elfPath;
|
||||||
|
// Use the effective output path (custom if set, default otherwise)
|
||||||
|
config.outputPath = data.customOutputPath.empty()
|
||||||
|
? data.outputPath : data.customOutputPath;
|
||||||
|
config.singleFileOutput = false;
|
||||||
|
|
||||||
|
for (const auto& [name, status] : data.funcOverrides) {
|
||||||
|
if (status == OverrideStatus::Stub) {
|
||||||
|
config.stubImplementations.push_back(name);
|
||||||
|
} else if (status == OverrideStatus::Skip) {
|
||||||
|
config.skipFunctions.push_back(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string savePath = configTomlPath.empty() ? "config.toml" : configTomlPath;
|
||||||
|
ps2recomp::ConfigManager mgr(savePath);
|
||||||
|
mgr.saveConfig(config);
|
||||||
|
Log("Config saved to " + savePath);
|
||||||
|
|
||||||
|
LoadConfigToml();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Log(std::string("Error saving config: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveConfigTomlFromEditor(const std::string& newContent) {
|
||||||
|
try {
|
||||||
|
std::string savePath = configTomlPath.empty() ? "config.toml" : configTomlPath;
|
||||||
|
std::ofstream file(savePath);
|
||||||
|
if (file) {
|
||||||
|
file << newContent;
|
||||||
|
file.flush();
|
||||||
|
data.configTomlContent = newContent;
|
||||||
|
Log("config.toml saved to: " + savePath);
|
||||||
|
} else {
|
||||||
|
Log("Error: Cannot open " + savePath + " for writing");
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Log(std::string("Error saving config.toml: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the effective output directory
|
||||||
|
std::string GetEffectiveOutputPath() const {
|
||||||
|
if (!data.customOutputPath.empty()) return data.customOutputPath;
|
||||||
|
return data.outputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex statusMutex_;
|
||||||
|
std::string statusMessage_ = "Ready";
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "../StudioState.hpp"
|
||||||
|
|
||||||
|
namespace StyleManager {
|
||||||
|
void ApplyTheme(ThemeMode mode, AppSettings& settings);
|
||||||
|
void ApplyDarkTheme();
|
||||||
|
void ApplyLightTheme();
|
||||||
|
void ApplyCustomTheme(AppSettings& settings);
|
||||||
|
void SetupFonts(AppSettings& settings);
|
||||||
|
}
|
||||||
@@ -0,0 +1,742 @@
|
|||||||
|
#include "GUI.hpp"
|
||||||
|
#include "ui/StyleManager.hpp"
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
#include "imgui_memory_editor.h"
|
||||||
|
#include "TextEditor.h"
|
||||||
|
#include "ImGuiFileDialog.h"
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
static MemoryEditor mem_edit;
|
||||||
|
static TextEditor code_editor;
|
||||||
|
static TextEditor config_editor;
|
||||||
|
static TextEditor ghidra_editor;
|
||||||
|
static TextEditor log_editor; // TextEditor for logs - supports text selection
|
||||||
|
static bool editors_initialized = false;
|
||||||
|
static bool show_settings_window = false;
|
||||||
|
static bool config_editor_needs_sync = false;
|
||||||
|
static size_t last_log_version = 0;
|
||||||
|
static bool s_wantsQuit = false;
|
||||||
|
|
||||||
|
// HEX view color highlighting
|
||||||
|
struct HexHighlightRange {
|
||||||
|
size_t start;
|
||||||
|
size_t end;
|
||||||
|
ImU32 color;
|
||||||
|
std::string label;
|
||||||
|
};
|
||||||
|
static std::vector<HexHighlightRange> hex_highlight_ranges;
|
||||||
|
|
||||||
|
// Global pointer for BgColorFn callback
|
||||||
|
static std::vector<HexHighlightRange>* g_hex_highlights = &hex_highlight_ranges;
|
||||||
|
|
||||||
|
std::string FormatHex(uint32_t val) {
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "0x" << std::hex << std::setw(8) << std::setfill('0') << val;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// BgColorFn callback for MemoryEditor - provides per-byte background color
|
||||||
|
static ImU32 HexBgColorCallback(const ImU8* /*mem*/, size_t off, void* /*user_data*/) {
|
||||||
|
if (g_hex_highlights) {
|
||||||
|
for (const auto& hl : *g_hex_highlights) {
|
||||||
|
if (off >= hl.start && off < hl.end) {
|
||||||
|
return hl.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0; // no color (transparent)
|
||||||
|
}
|
||||||
|
|
||||||
|
void GUI::ApplySettings(StudioState& state) {
|
||||||
|
StyleManager::SetupFonts(state.settings);
|
||||||
|
StyleManager::ApplyTheme(state.settings.theme, state.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GUI::RebuildFontsIfNeeded(StudioState& state) {
|
||||||
|
if (state.pendingFontRebuild.exchange(false)) {
|
||||||
|
StyleManager::SetupFonts(state.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GUI::SyncConfigEditor(StudioState& state) {
|
||||||
|
config_editor_needs_sync = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GUI::WantsQuit() {
|
||||||
|
return s_wantsQuit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Settings Window ----
|
||||||
|
static void DrawSettingsWindow(StudioState& state) {
|
||||||
|
if (!show_settings_window) return;
|
||||||
|
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
|
||||||
|
if (ImGui::Begin("Settings", &show_settings_window)) {
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Theme", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
const char* themes[] = { "Dark", "Light", "Custom" };
|
||||||
|
int current = static_cast<int>(state.settings.theme);
|
||||||
|
if (ImGui::Combo("Theme Mode", ¤t, themes, 3)) {
|
||||||
|
state.settings.theme = static_cast<ThemeMode>(current);
|
||||||
|
StyleManager::ApplyTheme(state.settings.theme, state.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.settings.theme == ThemeMode::Custom) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::ColorEdit4("Background", state.settings.customBgBase);
|
||||||
|
ImGui::ColorEdit4("Accent", state.settings.customAccent);
|
||||||
|
if (ImGui::Button("Apply Custom Colors")) {
|
||||||
|
StyleManager::ApplyTheme(state.settings.theme, state.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Font & Size", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
if (ImGui::BeginCombo("Font", state.settings.selectedFont.c_str())) {
|
||||||
|
for (const auto& font : state.availableFonts) {
|
||||||
|
bool selected = (state.settings.selectedFont == font);
|
||||||
|
if (ImGui::Selectable(font.c_str(), selected)) {
|
||||||
|
state.settings.selectedFont = font;
|
||||||
|
// Defer font rebuild to main loop (prevents crash)
|
||||||
|
state.pendingFontRebuild = true;
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font size slider - changes are deferred to prevent crash
|
||||||
|
if (ImGui::SliderFloat("Font Size", &state.settings.fontSize, 10.0f, 24.0f, "%.1f")) {
|
||||||
|
// Clamp to safe range
|
||||||
|
state.settings.fontSize = std::clamp(state.settings.fontSize, 10.0f, 48.0f);
|
||||||
|
// Defer font rebuild to main loop - NOT during frame rendering
|
||||||
|
state.pendingFontRebuild = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("UI Scale", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
if (ImGui::SliderFloat("Scale", &state.settings.uiScale, 0.5f, 2.0f, "%.2f")) {
|
||||||
|
state.settings.uiScale = std::clamp(state.settings.uiScale, 0.5f, 3.0f);
|
||||||
|
StyleManager::ApplyTheme(state.settings.theme, state.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Window", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
ImGui::Checkbox("Start Maximized", &state.settings.maximized);
|
||||||
|
ImGui::SliderInt("Default Width", &state.settings.windowWidth, 800, 3840);
|
||||||
|
ImGui::SliderInt("Default Height", &state.settings.windowHeight, 600, 2160);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (ImGui::Button("Rescan Fonts", ImVec2(150, 0))) {
|
||||||
|
state.ScanFonts();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Reset to Defaults", ImVec2(150, 0))) {
|
||||||
|
state.settings = AppSettings();
|
||||||
|
state.pendingFontRebuild = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Close", ImVec2(150, 0))) {
|
||||||
|
show_settings_window = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Main Draw ----
|
||||||
|
void GUI::DrawStudio(StudioState& state) {
|
||||||
|
if (!editors_initialized) {
|
||||||
|
code_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
|
||||||
|
code_editor.SetPalette(TextEditor::GetDarkPalette());
|
||||||
|
code_editor.SetReadOnly(true);
|
||||||
|
|
||||||
|
config_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
|
||||||
|
config_editor.SetPalette(TextEditor::GetDarkPalette());
|
||||||
|
config_editor.SetReadOnly(false);
|
||||||
|
config_editor.SetText(state.data.configTomlContent);
|
||||||
|
|
||||||
|
ghidra_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
|
||||||
|
ghidra_editor.SetPalette(TextEditor::GetDarkPalette());
|
||||||
|
ghidra_editor.SetReadOnly(true);
|
||||||
|
|
||||||
|
// Log editor: read-only TextEditor that supports text selection (Ctrl+A, Ctrl+C, mouse drag)
|
||||||
|
log_editor.SetPalette(TextEditor::GetDarkPalette());
|
||||||
|
log_editor.SetReadOnly(true);
|
||||||
|
log_editor.SetShowWhitespaces(false);
|
||||||
|
|
||||||
|
mem_edit.ReadOnly = true;
|
||||||
|
mem_edit.OptShowAscii = true;
|
||||||
|
mem_edit.OptGreyOutZeroes = true;
|
||||||
|
mem_edit.OptUpperCaseHex = false;
|
||||||
|
// Set BgColorFn for highlighting selected functions in hex view
|
||||||
|
mem_edit.BgColorFn = HexBgColorCallback;
|
||||||
|
mem_edit.UserData = nullptr;
|
||||||
|
|
||||||
|
editors_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync config editor if requested
|
||||||
|
if (config_editor_needs_sync) {
|
||||||
|
config_editor.SetText(state.data.configTomlContent);
|
||||||
|
config_editor_needs_sync = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update log editor when logs change
|
||||||
|
{
|
||||||
|
size_t currentVersion = state.logVersion.load();
|
||||||
|
if (currentVersion != last_log_version) {
|
||||||
|
std::lock_guard<std::mutex> lock(state.stateMutex);
|
||||||
|
std::stringstream ss;
|
||||||
|
for (const auto& logLine : state.logs) {
|
||||||
|
ss << logLine << "\n";
|
||||||
|
}
|
||||||
|
log_editor.SetText(ss.str());
|
||||||
|
|
||||||
|
// Set error markers for lines containing Error/Failed
|
||||||
|
TextEditor::ErrorMarkers markers;
|
||||||
|
for (size_t i = 0; i < state.logs.size(); i++) {
|
||||||
|
const auto& line = state.logs[i];
|
||||||
|
if (line.find("Error") != std::string::npos ||
|
||||||
|
line.find("Failed") != std::string::npos ||
|
||||||
|
line.find("error") != std::string::npos) {
|
||||||
|
markers.insert(std::make_pair(static_cast<int>(i + 1), line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_editor.SetErrorMarkers(markers);
|
||||||
|
|
||||||
|
last_log_version = currentVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dockspace
|
||||||
|
ImGuiID dockspace_id = ImGui::GetID("StudioDock");
|
||||||
|
ImGui::DockSpaceOverViewport(dockspace_id, ImGui::GetMainViewport());
|
||||||
|
|
||||||
|
static bool first_run = true;
|
||||||
|
if (first_run) {
|
||||||
|
first_run = false;
|
||||||
|
ImGui::DockBuilderRemoveNode(dockspace_id);
|
||||||
|
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
|
||||||
|
ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->Size);
|
||||||
|
|
||||||
|
ImGuiID dock_left = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.22f, nullptr, &dockspace_id);
|
||||||
|
ImGuiID dock_right = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.28f, nullptr, &dockspace_id);
|
||||||
|
ImGuiID dock_down = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Down, 0.25f, nullptr, &dockspace_id);
|
||||||
|
|
||||||
|
ImGui::DockBuilderDockWindow("Explorer", dock_left);
|
||||||
|
ImGui::DockBuilderDockWindow("Inspector", dock_right);
|
||||||
|
ImGui::DockBuilderDockWindow("Logs", dock_down);
|
||||||
|
ImGui::DockBuilderDockWindow("Workspace", dockspace_id);
|
||||||
|
ImGui::DockBuilderFinish(dockspace_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File dialogs
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(900, 600), ImGuiCond_FirstUseEver);
|
||||||
|
|
||||||
|
if (ImGuiFileDialog::Instance()->Display("ChooseFileDlgKey")) {
|
||||||
|
if (ImGuiFileDialog::Instance()->IsOk()) {
|
||||||
|
state.LoadELF(ImGuiFileDialog::Instance()->GetFilePathName());
|
||||||
|
}
|
||||||
|
ImGuiFileDialog::Instance()->Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGuiFileDialog::Instance()->Display("ImportGhidraKey")) {
|
||||||
|
if (ImGuiFileDialog::Instance()->IsOk()) {
|
||||||
|
std::string csvPath = ImGuiFileDialog::Instance()->GetFilePathName();
|
||||||
|
state.ImportGhidraCSV(csvPath);
|
||||||
|
// Add to imported CSV list (avoid duplicates)
|
||||||
|
bool alreadyImported = false;
|
||||||
|
for (const auto& existing : state.data.importedCSVFiles) {
|
||||||
|
if (existing == csvPath) { alreadyImported = true; break; }
|
||||||
|
}
|
||||||
|
if (!alreadyImported) {
|
||||||
|
state.data.importedCSVFiles.push_back(csvPath);
|
||||||
|
}
|
||||||
|
state.data.selectedCSVIndex = static_cast<int>(state.data.importedCSVFiles.size()) - 1;
|
||||||
|
ghidra_editor.SetText(state.data.ghidraCSVContent);
|
||||||
|
}
|
||||||
|
ImGuiFileDialog::Instance()->Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGuiFileDialog::Instance()->Display("ChooseDirDlgKey")) {
|
||||||
|
if (ImGuiFileDialog::Instance()->IsOk()) {
|
||||||
|
state.SetOutputDir(ImGuiFileDialog::Instance()->GetFilePathName());
|
||||||
|
}
|
||||||
|
ImGuiFileDialog::Instance()->Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Main Menu Bar ----
|
||||||
|
if (ImGui::BeginMainMenuBar()) {
|
||||||
|
if (ImGui::BeginMenu("File")) {
|
||||||
|
if (ImGui::MenuItem("Open ELF...", "Ctrl+O")) {
|
||||||
|
IGFD::FileDialogConfig config;
|
||||||
|
config.path = ".";
|
||||||
|
ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Choose File", ".*,.elf", config);
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Import Ghidra CSV...")) {
|
||||||
|
IGFD::FileDialogConfig config;
|
||||||
|
config.path = ".";
|
||||||
|
ImGuiFileDialog::Instance()->OpenDialog("ImportGhidraKey", "Choose CSV", ".csv", config);
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Set Output Directory...")) {
|
||||||
|
IGFD::FileDialogConfig config;
|
||||||
|
config.path = ".";
|
||||||
|
ImGuiFileDialog::Instance()->OpenDialog("ChooseDirDlgKey", "Choose Output Dir", nullptr, config);
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem("Save Config", "Ctrl+S", false, state.data.isAnalysisComplete)) {
|
||||||
|
state.SaveConfigTOML();
|
||||||
|
config_editor_needs_sync = true;
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Reload Config from Disk")) {
|
||||||
|
state.LoadConfigToml();
|
||||||
|
config_editor_needs_sync = true;
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem("Exit")) s_wantsQuit = true;
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu("Tools")) {
|
||||||
|
bool busy = state.isBusy.load();
|
||||||
|
if (ImGui::MenuItem("Analyze", "F5", false, !busy && !state.data.elfPath.empty())) {
|
||||||
|
state.StartAnalysis();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Recompile", "F7", false, !busy && state.data.isAnalysisComplete)) {
|
||||||
|
state.StartRecompilation();
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem("Run System Diagnostics", nullptr, false, !busy)) {
|
||||||
|
state.RunDiagnostics();
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu("Settings")) {
|
||||||
|
if (ImGui::MenuItem("Open Settings Window")) {
|
||||||
|
show_settings_window = true;
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show output path in menu bar
|
||||||
|
std::string pathDisplay = state.GetEffectiveOutputPath();
|
||||||
|
float pathWidth = ImGui::CalcTextSize(pathDisplay.c_str()).x;
|
||||||
|
if (ImGui::GetWindowWidth() > pathWidth + 300) {
|
||||||
|
ImGui::SameLine(ImGui::GetWindowWidth() - pathWidth - 20);
|
||||||
|
ImGui::TextDisabled("Output: %s", pathDisplay.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndMainMenuBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Keyboard Shortcuts ----
|
||||||
|
{
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
bool noPopup = !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId);
|
||||||
|
if (noPopup) {
|
||||||
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
||||||
|
IGFD::FileDialogConfig config;
|
||||||
|
config.path = ".";
|
||||||
|
ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Choose File", ".*,.elf", config);
|
||||||
|
}
|
||||||
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S) && state.data.isAnalysisComplete) {
|
||||||
|
state.SaveConfigTOML();
|
||||||
|
config_editor_needs_sync = true;
|
||||||
|
}
|
||||||
|
bool busy = state.isBusy.load();
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_F5) && !busy && !state.data.elfPath.empty()) {
|
||||||
|
state.StartAnalysis();
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_F7) && !busy && state.data.isAnalysisComplete) {
|
||||||
|
state.StartRecompilation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawSettingsWindow(state);
|
||||||
|
|
||||||
|
// ---- Explorer Panel ----
|
||||||
|
ImGui::Begin("Explorer");
|
||||||
|
ImGui::SetNextItemWidth(-1);
|
||||||
|
static char filterBuf[128] = "";
|
||||||
|
ImGui::InputTextWithHint("##Search", "Search functions...", filterBuf, 128);
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (state.data.isAnalysisComplete && state.data.analyzer) {
|
||||||
|
ImGui::BeginChild("FuncList", ImVec2(0, 0), false);
|
||||||
|
const auto& funcs = state.data.analyzer->getFunctions();
|
||||||
|
|
||||||
|
// Build filtered index list so clipper works correctly
|
||||||
|
std::string filterStr(filterBuf);
|
||||||
|
static std::vector<int> filteredIndices;
|
||||||
|
filteredIndices.clear();
|
||||||
|
filteredIndices.reserve(funcs.size());
|
||||||
|
for (int idx = 0; idx < static_cast<int>(funcs.size()); idx++) {
|
||||||
|
if (filterStr.empty() || funcs[idx].name.find(filterStr) != std::string::npos) {
|
||||||
|
filteredIndices.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiListClipper clipper;
|
||||||
|
clipper.Begin(static_cast<int>(filteredIndices.size()));
|
||||||
|
while (clipper.Step()) {
|
||||||
|
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) {
|
||||||
|
if (row < 0 || row >= static_cast<int>(filteredIndices.size())) continue;
|
||||||
|
int i = filteredIndices[row];
|
||||||
|
|
||||||
|
const auto& func = funcs[i];
|
||||||
|
|
||||||
|
OverrideStatus status = OverrideStatus::Default;
|
||||||
|
auto it = state.data.funcOverrides.find(func.name);
|
||||||
|
if (it != state.data.funcOverrides.end()) {
|
||||||
|
status = it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec4 color = ImGui::GetStyle().Colors[ImGuiCol_Text];
|
||||||
|
if (status == OverrideStatus::Stub) {
|
||||||
|
color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
|
||||||
|
} else if (status == OverrideStatus::Skip) {
|
||||||
|
color = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||||
|
} else if (status == OverrideStatus::ForceRecompile) {
|
||||||
|
color = ImVec4(0.4f, 1.0f, 0.4f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
||||||
|
bool isSelected = state.selectedFuncIndex == i;
|
||||||
|
if (ImGui::Selectable(func.name.c_str(), isSelected)) {
|
||||||
|
state.selectedFuncIndex = i;
|
||||||
|
std::stringstream codeSS;
|
||||||
|
codeSS << "// Function: " << func.name << "\n";
|
||||||
|
codeSS << "// Address: " << FormatHex(func.start) << "\n";
|
||||||
|
codeSS << "// Size: " << (func.end - func.start) << " bytes\n\n";
|
||||||
|
codeSS << "void " << func.name << "() {\n";
|
||||||
|
for (const auto& inst : func.instructions) {
|
||||||
|
codeSS << " // " << FormatHex(inst.address)
|
||||||
|
<< ": [0x" << std::hex << inst.opcode << "]\n";
|
||||||
|
}
|
||||||
|
codeSS << "}\n";
|
||||||
|
code_editor.SetText(codeSS.str());
|
||||||
|
|
||||||
|
// Update hex view to highlight and jump to this function
|
||||||
|
if (!state.data.rawElfData.empty() &&
|
||||||
|
func.start < state.data.rawElfData.size()) {
|
||||||
|
mem_edit.GotoAddrAndHighlight(func.start,
|
||||||
|
std::min(static_cast<size_t>(func.end), state.data.rawElfData.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::Text("Start: %s", FormatHex(func.start).c_str());
|
||||||
|
ImGui::Text("Size: %d bytes", (func.end - func.start));
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
} else {
|
||||||
|
float winW = ImGui::GetWindowSize().x;
|
||||||
|
float winH = ImGui::GetWindowSize().y;
|
||||||
|
if (state.data.elfPath.empty()) {
|
||||||
|
const char* txt = "No ELF loaded";
|
||||||
|
ImGui::SetCursorPos(ImVec2((winW - ImGui::CalcTextSize(txt).x) * 0.5f, winH * 0.4f));
|
||||||
|
ImGui::TextDisabled("%s", txt);
|
||||||
|
} else {
|
||||||
|
const char* txt = "Ready to Analyze";
|
||||||
|
ImGui::SetCursorPos(ImVec2((winW - ImGui::CalcTextSize(txt).x) * 0.5f, winH * 0.4f));
|
||||||
|
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s", txt);
|
||||||
|
ImGui::SetCursorPosX((winW - 120) * 0.5f);
|
||||||
|
if (ImGui::Button("Run Analysis", ImVec2(120, 30))) {
|
||||||
|
state.StartAnalysis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
// ---- Inspector Panel ----
|
||||||
|
ImGui::Begin("Inspector");
|
||||||
|
if (state.data.analyzer && state.selectedFuncIndex >= 0) {
|
||||||
|
const auto& funcs = state.data.analyzer->getFunctions();
|
||||||
|
if (state.selectedFuncIndex < static_cast<int>(funcs.size())) {
|
||||||
|
const auto& func = funcs[state.selectedFuncIndex];
|
||||||
|
|
||||||
|
if (ImGui::GetIO().Fonts->Fonts.Size > 0) {
|
||||||
|
ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
|
||||||
|
ImGui::TextColored(ImVec4(0.0f, 0.6f, 1.0f, 1.0f), "%s", func.name.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
} else {
|
||||||
|
ImGui::TextColored(ImVec4(0.0f, 0.6f, 1.0f, 1.0f), "%s", func.name.c_str());
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
OverrideStatus current = OverrideStatus::Default;
|
||||||
|
auto it = state.data.funcOverrides.find(func.name);
|
||||||
|
if (it != state.data.funcOverrides.end()) {
|
||||||
|
current = it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* options[] = { "Auto (Default)", "Stub (Plug)", "Skip (Ignore)", "Force Recompile" };
|
||||||
|
int idx = static_cast<int>(current);
|
||||||
|
ImGui::TextDisabled("Strategy:");
|
||||||
|
ImGui::SetNextItemWidth(-1);
|
||||||
|
if (ImGui::Combo("##Action", &idx, options, 4)) {
|
||||||
|
state.data.funcOverrides[func.name] = static_cast<OverrideStatus>(idx);
|
||||||
|
state.Log("Strategy changed for " + func.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (ImGui::BeginTable("MetaTable", 2)) {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextDisabled("Address:");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%s", FormatHex(func.start).c_str());
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextDisabled("Size:");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%d bytes", (func.end - func.start));
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextDisabled("Instructions:");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%zu", func.instructions.size());
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.selectedFuncIndex = -1;
|
||||||
|
ImGui::TextDisabled("Select a function to inspect");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled("Select a function to inspect");
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
// ---- Workspace Panel (Tabs) ----
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 1.00f));
|
||||||
|
ImGui::Begin("Workspace", nullptr, ImGuiWindowFlags_NoTitleBar);
|
||||||
|
|
||||||
|
if (ImGui::BeginTabBar("Tabs", ImGuiTabBarFlags_Reorderable)) {
|
||||||
|
// C++ Preview Tab
|
||||||
|
if (ImGui::BeginTabItem(" C++ Preview ")) {
|
||||||
|
if (ImGui::BeginPopupContextItem("CppContext")) {
|
||||||
|
if (ImGui::MenuItem("Copy All")) {
|
||||||
|
ImGui::SetClipboardText(code_editor.GetText().c_str());
|
||||||
|
state.Log("C++ code copied to clipboard");
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
code_editor.Render("Editor");
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex View Tab - with highlighting
|
||||||
|
if (ImGui::BeginTabItem(" Hex View ")) {
|
||||||
|
if (state.data.rawElfData.empty()) {
|
||||||
|
ImGui::TextDisabled("No binary data available (File not loaded or read error)");
|
||||||
|
} else {
|
||||||
|
// Setup hex highlights based on selected function
|
||||||
|
hex_highlight_ranges.clear();
|
||||||
|
if (state.data.analyzer && state.selectedFuncIndex >= 0) {
|
||||||
|
const auto& funcs = state.data.analyzer->getFunctions();
|
||||||
|
if (state.selectedFuncIndex < static_cast<int>(funcs.size())) {
|
||||||
|
const auto& func = funcs[state.selectedFuncIndex];
|
||||||
|
if (func.start < state.data.rawElfData.size()) {
|
||||||
|
HexHighlightRange hl;
|
||||||
|
hl.start = func.start;
|
||||||
|
hl.end = std::min(static_cast<size_t>(func.end), state.data.rawElfData.size());
|
||||||
|
hl.color = IM_COL32(0, 120, 215, 80);
|
||||||
|
hl.label = "Function: " + func.name;
|
||||||
|
hex_highlight_ranges.push_back(hl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also highlight ELF header (first 52 bytes for 32-bit ELF)
|
||||||
|
if (state.data.rawElfData.size() >= 52) {
|
||||||
|
HexHighlightRange elfHeader;
|
||||||
|
elfHeader.start = 0;
|
||||||
|
elfHeader.end = 52;
|
||||||
|
elfHeader.color = IM_COL32(100, 180, 80, 40);
|
||||||
|
elfHeader.label = "ELF Header";
|
||||||
|
hex_highlight_ranges.push_back(elfHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
mem_edit.DrawContents(state.data.rawElfData.data(), state.data.rawElfData.size());
|
||||||
|
}
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
// config.toml Tab
|
||||||
|
if (ImGui::BeginTabItem(" config.toml ")) {
|
||||||
|
// Toolbar for config editor
|
||||||
|
if (ImGui::Button("Save")) {
|
||||||
|
state.SaveConfigTomlFromEditor(config_editor.GetText());
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Reload from Disk")) {
|
||||||
|
state.LoadConfigToml();
|
||||||
|
config_editor.SetText(state.data.configTomlContent);
|
||||||
|
state.Log("config.toml reloaded from disk");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Copy All")) {
|
||||||
|
ImGui::SetClipboardText(config_editor.GetText().c_str());
|
||||||
|
state.Log("config.toml copied to clipboard");
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (!state.configTomlPath.empty()) {
|
||||||
|
ImGui::TextDisabled("Path: %s", state.configTomlPath.c_str());
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
config_editor.Render("ConfigEditor");
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imported CSV Files Tab - ALWAYS visible
|
||||||
|
if (ImGui::BeginTabItem(" Ghidra CSV ")) {
|
||||||
|
// Import button always available
|
||||||
|
if (ImGui::Button("Import CSV...")) {
|
||||||
|
IGFD::FileDialogConfig config;
|
||||||
|
config.path = ".";
|
||||||
|
ImGuiFileDialog::Instance()->OpenDialog("ImportGhidraKey", "Choose CSV", ".csv", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.data.importedCSVFiles.empty()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("No CSV files imported yet");
|
||||||
|
} else {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("(%zu files)", state.data.importedCSVFiles.size());
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// File list
|
||||||
|
if (!state.data.importedCSVFiles.empty()) {
|
||||||
|
ImGui::BeginChild("CSVList", ImVec2(0, 120), true);
|
||||||
|
for (size_t i = 0; i < state.data.importedCSVFiles.size(); i++) {
|
||||||
|
const auto& csvPath = state.data.importedCSVFiles[i];
|
||||||
|
std::string filename = std::filesystem::path(csvPath).filename().string();
|
||||||
|
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
bool isSelected = state.data.selectedCSVIndex == static_cast<int>(i);
|
||||||
|
if (ImGui::Selectable(filename.c_str(), isSelected)) {
|
||||||
|
state.data.selectedCSVIndex = static_cast<int>(i);
|
||||||
|
try {
|
||||||
|
std::ifstream file(csvPath);
|
||||||
|
if (file) {
|
||||||
|
std::stringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
std::string content = buffer.str();
|
||||||
|
ghidra_editor.SetText(content);
|
||||||
|
state.data.ghidraCSVPath = csvPath;
|
||||||
|
state.data.ghidraCSVContent = content;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
state.Log("Error reading CSV: " + csvPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("%s", csvPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("X")) {
|
||||||
|
state.data.importedCSVFiles.erase(state.data.importedCSVFiles.begin() + i);
|
||||||
|
if (state.data.selectedCSVIndex == static_cast<int>(i)) {
|
||||||
|
state.data.selectedCSVIndex = -1;
|
||||||
|
ghidra_editor.SetText("");
|
||||||
|
} else if (state.data.selectedCSVIndex > static_cast<int>(i)) {
|
||||||
|
state.data.selectedCSVIndex--;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
break; // restart iteration next frame
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// Show content of selected CSV
|
||||||
|
if (state.data.selectedCSVIndex >= 0 &&
|
||||||
|
state.data.selectedCSVIndex < static_cast<int>(state.data.importedCSVFiles.size())) {
|
||||||
|
std::string selFileName = std::filesystem::path(
|
||||||
|
state.data.importedCSVFiles[state.data.selectedCSVIndex]).filename().string();
|
||||||
|
ImGui::TextDisabled("Content: %s", selFileName.c_str());
|
||||||
|
ImGui::Separator();
|
||||||
|
ghidra_editor.Render("GhidraEditor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
// ---- Logs Panel ----
|
||||||
|
ImGui::Begin("Logs");
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
{
|
||||||
|
std::string currentStatus = state.GetStatus();
|
||||||
|
if (state.isBusy.load()) {
|
||||||
|
float progress = std::fmod((float)ImGui::GetTime(), 1.0f);
|
||||||
|
ImGui::ProgressBar(progress, ImVec2(-1, 6), "");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("%s", currentStatus.c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled("Status: %s", currentStatus.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log toolbar - right-aligned
|
||||||
|
{
|
||||||
|
float btnWidth = ImGui::CalcTextSize("Copy All").x + ImGui::CalcTextSize("Clear").x
|
||||||
|
+ ImGui::GetStyle().FramePadding.x * 4 + ImGui::GetStyle().ItemSpacing.x * 2 + 20;
|
||||||
|
ImGui::SameLine(std::max(0.0f, ImGui::GetWindowWidth() - btnWidth));
|
||||||
|
}
|
||||||
|
if (ImGui::SmallButton("Copy All")) {
|
||||||
|
std::lock_guard<std::mutex> lock(state.stateMutex);
|
||||||
|
std::stringstream allLogs;
|
||||||
|
for (const auto& log : state.logs) {
|
||||||
|
allLogs << log << "\n";
|
||||||
|
}
|
||||||
|
ImGui::SetClipboardText(allLogs.str().c_str());
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("Clear")) {
|
||||||
|
std::lock_guard<std::mutex> lock(state.stateMutex);
|
||||||
|
state.logs.clear();
|
||||||
|
state.logVersion.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// Log viewer using TextEditor - supports text selection, Ctrl+A, Ctrl+C, mouse drag
|
||||||
|
log_editor.Render("LogEditor");
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_impl_sdl2.h"
|
||||||
|
#include "imgui_impl_opengl3.h"
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <SDL_opengl.h>
|
||||||
|
#include "GUI.hpp"
|
||||||
|
#include "StudioState.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) return -1;
|
||||||
|
|
||||||
|
SDL_DisplayMode displayMode;
|
||||||
|
SDL_GetCurrentDisplayMode(0, &displayMode);
|
||||||
|
|
||||||
|
int screenWidth = displayMode.w;
|
||||||
|
int screenHeight = displayMode.h;
|
||||||
|
|
||||||
|
int windowWidth = (int)(screenWidth * 0.8f);
|
||||||
|
int windowHeight = (int)(screenHeight * 0.8f);
|
||||||
|
|
||||||
|
SDL_WindowFlags window_flags = (SDL_WindowFlags)(
|
||||||
|
SDL_WINDOW_OPENGL |
|
||||||
|
SDL_WINDOW_RESIZABLE
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pre-load window settings before StudioState construction
|
||||||
|
AppSettings tempSettings;
|
||||||
|
try {
|
||||||
|
std::ifstream file("studio_settings.ini");
|
||||||
|
if (file) {
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
size_t pos = line.find('=');
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
std::string key = line.substr(0, pos);
|
||||||
|
std::string value = line.substr(pos + 1);
|
||||||
|
|
||||||
|
if (key == "maximized") {
|
||||||
|
tempSettings.maximized = (value == "1");
|
||||||
|
} else if (key == "windowWidth") {
|
||||||
|
tempSettings.windowWidth = std::stoi(value);
|
||||||
|
} else if (key == "windowHeight") {
|
||||||
|
tempSettings.windowHeight = std::stoi(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(...) {}
|
||||||
|
|
||||||
|
if (tempSettings.maximized) {
|
||||||
|
window_flags = (SDL_WindowFlags)(window_flags | SDL_WINDOW_MAXIMIZED);
|
||||||
|
} else {
|
||||||
|
windowWidth = tempSettings.windowWidth;
|
||||||
|
windowHeight = tempSettings.windowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Window* window = SDL_CreateWindow(
|
||||||
|
"PS2Recomp Studio",
|
||||||
|
SDL_WINDOWPOS_CENTERED,
|
||||||
|
SDL_WINDOWPOS_CENTERED,
|
||||||
|
windowWidth,
|
||||||
|
windowHeight,
|
||||||
|
window_flags
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!window) {
|
||||||
|
SDL_Quit();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
|
||||||
|
SDL_GL_MakeCurrent(window, gl_context);
|
||||||
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
ImGui::CreateContext();
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||||
|
|
||||||
|
// Create StudioState AFTER ImGui context
|
||||||
|
StudioState state;
|
||||||
|
|
||||||
|
// Initial font and theme setup
|
||||||
|
GUI::ApplySettings(state);
|
||||||
|
|
||||||
|
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
|
||||||
|
ImGui_ImplOpenGL3_Init("#version 130");
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
state.StartAnalysis(argv[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
while (!done) {
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||||
|
if (event.type == SDL_QUIT) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
if (event.type == SDL_WINDOWEVENT) {
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||||
|
state.settings.windowWidth = event.window.data1;
|
||||||
|
state.settings.windowHeight = event.window.data2;
|
||||||
|
}
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_MAXIMIZED) {
|
||||||
|
state.settings.maximized = true;
|
||||||
|
}
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_RESTORED) {
|
||||||
|
state.settings.maximized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check GUI quit request (File > Exit)
|
||||||
|
if (GUI::WantsQuit()) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Handle deferred font rebuild BEFORE NewFrame
|
||||||
|
// This is the safe place to modify fonts - outside of the rendering frame
|
||||||
|
if (state.pendingFontRebuild.load()) {
|
||||||
|
// Rebuild fonts (clears atlas, adds fonts with new size)
|
||||||
|
GUI::RebuildFontsIfNeeded(state);
|
||||||
|
// Build the font atlas; the OpenGL3 backend will detect the
|
||||||
|
// change and upload the new texture automatically on NewFrame.
|
||||||
|
io.Fonts->Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
|
ImGui_ImplSDL2_NewFrame();
|
||||||
|
ImGui::NewFrame();
|
||||||
|
|
||||||
|
GUI::DrawStudio(state);
|
||||||
|
|
||||||
|
ImGui::Render();
|
||||||
|
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
|
||||||
|
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
SDL_GL_SwapWindow(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings before shutdown
|
||||||
|
state.SaveSettings();
|
||||||
|
|
||||||
|
if (state.workerThread.valid()) {
|
||||||
|
state.workerThread.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui_ImplOpenGL3_Shutdown();
|
||||||
|
ImGui_ImplSDL2_Shutdown();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
SDL_GL_DeleteContext(gl_context);
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
SDL_Quit();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#include "ui/StyleManager.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace StyleManager {
|
||||||
|
|
||||||
|
void SetupFonts(AppSettings& settings) {
|
||||||
|
if (ImGui::GetCurrentContext() == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// Validate and clamp font size
|
||||||
|
float safeFontSize = std::clamp(settings.fontSize, 10.0f, 48.0f);
|
||||||
|
settings.fontSize = safeFontSize;
|
||||||
|
|
||||||
|
// Clear existing fonts
|
||||||
|
io.Fonts->Clear();
|
||||||
|
|
||||||
|
std::string fontPath = "external/Font/" + settings.selectedFont;
|
||||||
|
|
||||||
|
ImFont* loadedFont = nullptr;
|
||||||
|
|
||||||
|
if (settings.selectedFont != "Default" && std::filesystem::exists(fontPath)) {
|
||||||
|
loadedFont = io.Fonts->AddFontFromFileTTF(fontPath.c_str(), safeFontSize);
|
||||||
|
if (!loadedFont) {
|
||||||
|
// TTF loading failed, use default
|
||||||
|
ImFontConfig config;
|
||||||
|
config.SizePixels = safeFontSize;
|
||||||
|
loadedFont = io.Fonts->AddFontDefault(&config);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImFontConfig config;
|
||||||
|
config.SizePixels = safeFontSize;
|
||||||
|
loadedFont = io.Fonts->AddFontDefault(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font atlas will be built automatically by the backend
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyDarkTheme() {
|
||||||
|
if (ImGui::GetCurrentContext() == nullptr) return;
|
||||||
|
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
ImVec4* colors = style.Colors;
|
||||||
|
|
||||||
|
const ImVec4 bgBase = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
|
||||||
|
const ImVec4 bgPanel = ImVec4(0.13f, 0.13f, 0.14f, 1.00f);
|
||||||
|
const ImVec4 bgInput = ImVec4(0.18f, 0.18f, 0.19f, 1.00f);
|
||||||
|
const ImVec4 accentMain = ImVec4(0.00f, 0.48f, 0.80f, 1.00f);
|
||||||
|
const ImVec4 accentHover = ImVec4(0.10f, 0.55f, 0.90f, 1.00f);
|
||||||
|
const ImVec4 accentActive = ImVec4(0.00f, 0.40f, 0.70f, 1.00f);
|
||||||
|
const ImVec4 textBright = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
|
||||||
|
const ImVec4 textDim = ImVec4(0.70f, 0.70f, 0.70f, 1.00f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_Text] = textBright;
|
||||||
|
colors[ImGuiCol_TextDisabled] = textDim;
|
||||||
|
colors[ImGuiCol_WindowBg] = bgBase;
|
||||||
|
colors[ImGuiCol_ChildBg] = bgPanel;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.11f, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = ImVec4(0.30f, 0.30f, 0.30f, 0.60f);
|
||||||
|
colors[ImGuiCol_TitleBg] = ImVec4(0.06f, 0.06f, 0.06f, 1.00f);
|
||||||
|
colors[ImGuiCol_TitleBgActive] = ImVec4(0.05f, 0.05f, 0.05f, 1.00f);
|
||||||
|
colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.15f, 0.16f, 1.00f);
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = bgBase;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f);
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.45f, 0.45f, 0.45f, 1.00f);
|
||||||
|
colors[ImGuiCol_CheckMark] = accentMain;
|
||||||
|
colors[ImGuiCol_SliderGrab] = accentMain;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = accentActive;
|
||||||
|
colors[ImGuiCol_Button] = ImVec4(0.24f, 0.24f, 0.25f, 1.00f);
|
||||||
|
colors[ImGuiCol_ButtonHovered] = accentMain;
|
||||||
|
colors[ImGuiCol_ButtonActive] = accentActive;
|
||||||
|
colors[ImGuiCol_Header] = ImVec4(0.24f, 0.24f, 0.25f, 1.00f);
|
||||||
|
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.28f, 0.29f, 1.00f);
|
||||||
|
colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.20f, 0.21f, 1.00f);
|
||||||
|
colors[ImGuiCol_FrameBg] = bgInput;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.22f, 0.23f, 1.00f);
|
||||||
|
colors[ImGuiCol_FrameBgActive] = ImVec4(0.24f, 0.24f, 0.25f, 1.00f);
|
||||||
|
colors[ImGuiCol_Tab] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
|
||||||
|
colors[ImGuiCol_TabHovered] = bgPanel;
|
||||||
|
colors[ImGuiCol_TabActive] = bgPanel;
|
||||||
|
colors[ImGuiCol_TabUnfocused] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = bgPanel;
|
||||||
|
colors[ImGuiCol_DockingPreview] = accentMain;
|
||||||
|
colors[ImGuiCol_DockingEmptyBg] = bgBase;
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.48f, 0.80f, 0.40f);
|
||||||
|
colors[ImGuiCol_NavHighlight] = accentMain;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyLightTheme() {
|
||||||
|
if (ImGui::GetCurrentContext() == nullptr) return;
|
||||||
|
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
ImVec4* colors = style.Colors;
|
||||||
|
|
||||||
|
const ImVec4 bgBase = ImVec4(0.95f, 0.95f, 0.95f, 1.00f);
|
||||||
|
const ImVec4 bgPanel = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
|
||||||
|
const ImVec4 bgInput = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
|
||||||
|
const ImVec4 accentMain = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
|
||||||
|
const ImVec4 accentHover = ImVec4(0.26f, 0.59f, 0.98f, 0.80f);
|
||||||
|
const ImVec4 accentActive = ImVec4(0.06f, 0.53f, 0.98f, 1.00f);
|
||||||
|
const ImVec4 textBright = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
|
||||||
|
const ImVec4 textDim = ImVec4(0.40f, 0.40f, 0.40f, 1.00f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_Text] = textBright;
|
||||||
|
colors[ImGuiCol_TextDisabled] = textDim;
|
||||||
|
colors[ImGuiCol_WindowBg] = bgBase;
|
||||||
|
colors[ImGuiCol_ChildBg] = bgPanel;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = ImVec4(0.70f, 0.70f, 0.70f, 0.60f);
|
||||||
|
colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f);
|
||||||
|
colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f);
|
||||||
|
colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f);
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = bgBase;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 1.00f);
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.59f, 0.59f, 0.59f, 1.00f);
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f);
|
||||||
|
colors[ImGuiCol_CheckMark] = accentMain;
|
||||||
|
colors[ImGuiCol_SliderGrab] = accentMain;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = accentActive;
|
||||||
|
colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f);
|
||||||
|
colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
|
||||||
|
colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f);
|
||||||
|
colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f);
|
||||||
|
colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f);
|
||||||
|
colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
|
||||||
|
colors[ImGuiCol_FrameBg] = bgInput;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f);
|
||||||
|
colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f);
|
||||||
|
colors[ImGuiCol_Tab] = ImVec4(0.76f, 0.80f, 0.84f, 0.93f);
|
||||||
|
colors[ImGuiCol_TabHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f);
|
||||||
|
colors[ImGuiCol_TabActive] = ImVec4(0.60f, 0.73f, 0.88f, 1.00f);
|
||||||
|
colors[ImGuiCol_TabUnfocused] = ImVec4(0.92f, 0.93f, 0.94f, 0.98f);
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.74f, 0.82f, 0.91f, 1.00f);
|
||||||
|
colors[ImGuiCol_DockingPreview] = accentMain;
|
||||||
|
colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f);
|
||||||
|
colors[ImGuiCol_NavHighlight] = accentMain;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyCustomTheme(AppSettings& settings) {
|
||||||
|
if (ImGui::GetCurrentContext() == nullptr) return;
|
||||||
|
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
ImVec4* colors = style.Colors;
|
||||||
|
|
||||||
|
ImVec4 bgBase(settings.customBgBase[0], settings.customBgBase[1],
|
||||||
|
settings.customBgBase[2], settings.customBgBase[3]);
|
||||||
|
ImVec4 accent(settings.customAccent[0], settings.customAccent[1],
|
||||||
|
settings.customAccent[2], settings.customAccent[3]);
|
||||||
|
|
||||||
|
colors[ImGuiCol_WindowBg] = bgBase;
|
||||||
|
colors[ImGuiCol_Button] = accent;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = ImVec4(accent.x + 0.1f, accent.y + 0.1f, accent.z + 0.1f, accent.w);
|
||||||
|
colors[ImGuiCol_ButtonActive] = ImVec4(accent.x - 0.1f, accent.y - 0.1f, accent.z - 0.1f, accent.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyTheme(ThemeMode mode, AppSettings& settings) {
|
||||||
|
if (ImGui::GetCurrentContext() == nullptr) return;
|
||||||
|
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
|
||||||
|
float safeScale = std::clamp(settings.uiScale, 0.5f, 3.0f);
|
||||||
|
|
||||||
|
style.WindowPadding = ImVec2(8 * safeScale, 8 * safeScale);
|
||||||
|
style.FramePadding = ImVec2(5 * safeScale, 4 * safeScale);
|
||||||
|
style.CellPadding = ImVec2(4 * safeScale, 4 * safeScale);
|
||||||
|
style.ItemSpacing = ImVec2(8 * safeScale, 6 * safeScale);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6 * safeScale, 6 * safeScale);
|
||||||
|
style.IndentSpacing = 20 * safeScale;
|
||||||
|
style.ScrollbarSize = 12 * safeScale;
|
||||||
|
style.GrabMinSize = 10 * safeScale;
|
||||||
|
style.WindowBorderSize = 1 * safeScale;
|
||||||
|
style.ChildBorderSize = 1 * safeScale;
|
||||||
|
style.PopupBorderSize = 1 * safeScale;
|
||||||
|
style.FrameBorderSize = 0;
|
||||||
|
style.TabBorderSize = 0;
|
||||||
|
style.WindowRounding = 5.0f * safeScale;
|
||||||
|
style.ChildRounding = 5.0f * safeScale;
|
||||||
|
style.FrameRounding = 3.0f * safeScale;
|
||||||
|
style.PopupRounding = 5.0f * safeScale;
|
||||||
|
style.ScrollbarRounding = 9.0f * safeScale;
|
||||||
|
style.GrabRounding = 3.0f * safeScale;
|
||||||
|
style.TabRounding = 5.0f * safeScale;
|
||||||
|
|
||||||
|
switch(mode) {
|
||||||
|
case ThemeMode::Dark:
|
||||||
|
ApplyDarkTheme();
|
||||||
|
break;
|
||||||
|
case ThemeMode::Light:
|
||||||
|
ApplyLightTheme();
|
||||||
|
break;
|
||||||
|
case ThemeMode::Custom:
|
||||||
|
ApplyDarkTheme();
|
||||||
|
ApplyCustomTheme(settings);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace StyleManager
|
||||||
+23
-2
@@ -5,11 +5,12 @@ project(ps2xTest LANGUAGES CXX)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
add_executable(ps2x_tests
|
# Static library with test logic (no main), used by ps2xStudio
|
||||||
src/main.cpp
|
add_library(ps2_test_lib STATIC
|
||||||
src/code_generator_tests.cpp
|
src/code_generator_tests.cpp
|
||||||
src/r5900_decoder_tests.cpp
|
src/r5900_decoder_tests.cpp
|
||||||
src/elf_analyzer_tests.cpp
|
src/elf_analyzer_tests.cpp
|
||||||
|
src/pad_input_tests.cpp
|
||||||
src/ps2_runtime_io_tests.cpp
|
src/ps2_runtime_io_tests.cpp
|
||||||
src/ps2_runtime_kernel_tests.cpp
|
src/ps2_runtime_kernel_tests.cpp
|
||||||
src/ps2_runtime_interrupt_tests.cpp
|
src/ps2_runtime_interrupt_tests.cpp
|
||||||
@@ -21,9 +22,28 @@ add_executable(ps2x_tests
|
|||||||
src/ps2_runtime_expansion_tests.cpp
|
src/ps2_runtime_expansion_tests.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories(ps2_test_lib PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
${CMAKE_SOURCE_DIR}/ps2xRecomp/include
|
||||||
|
${CMAKE_SOURCE_DIR}/ps2xAnalyzer/include
|
||||||
|
${CMAKE_SOURCE_DIR}/ps2xRuntime/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(ps2_test_lib PRIVATE
|
||||||
|
ps2_recomp_lib
|
||||||
|
ps2_analyzer_lib
|
||||||
|
ps2_runtime
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(ps2x_tests
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
option(PRINT_GENERATED_CODE "Print generated code in tests" OFF)
|
option(PRINT_GENERATED_CODE "Print generated code in tests" OFF)
|
||||||
if(PRINT_GENERATED_CODE)
|
if(PRINT_GENERATED_CODE)
|
||||||
|
|
||||||
target_compile_definitions(ps2x_tests PRIVATE PRINT_GENERATED_CODE)
|
target_compile_definitions(ps2x_tests PRIVATE PRINT_GENERATED_CODE)
|
||||||
|
target_compile_definitions(ps2_test_lib PRIVATE PRINT_GENERATED_CODE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(ps2x_tests PRIVATE
|
target_include_directories(ps2x_tests PRIVATE
|
||||||
@@ -34,6 +54,7 @@ target_include_directories(ps2x_tests PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(ps2x_tests PRIVATE
|
target_link_libraries(ps2x_tests PRIVATE
|
||||||
|
ps2_test_lib
|
||||||
ps2_recomp_lib
|
ps2_recomp_lib
|
||||||
ps2_analyzer_lib
|
ps2_analyzer_lib
|
||||||
ps2_runtime
|
ps2_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");
|
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) {
|
tc.Run("JAL to internal target becomes goto", [](TestCase &t) {
|
||||||
Function func;
|
Function func;
|
||||||
func.name = "jal_internal";
|
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");
|
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) {
|
tc.Run("JR non-RA emits switch for in-function jump targets", [](TestCase &t) {
|
||||||
Function func;
|
Function func;
|
||||||
func.name = "jr_non_ra_switch";
|
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");
|
"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) {
|
tc.Run("resolveStubTarget allows leading underscore alias", [](TestCase &t) {
|
||||||
t.Equals(PS2Recompiler::resolveStubTarget("_rand"), StubTarget::Stub,
|
t.Equals(PS2Recompiler::resolveStubTarget("_rand"), StubTarget::Stub,
|
||||||
"_rand should resolve via rand stub alias");
|
"_rand should resolve via rand stub alias");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
void register_code_generator_tests();
|
void register_code_generator_tests();
|
||||||
void register_r5900_decoder_tests();
|
void register_r5900_decoder_tests();
|
||||||
void register_elf_analyzer_tests();
|
void register_elf_analyzer_tests();
|
||||||
|
void register_pad_input_tests();
|
||||||
void register_ps2_runtime_io_tests();
|
void register_ps2_runtime_io_tests();
|
||||||
void register_ps2_runtime_kernel_tests();
|
void register_ps2_runtime_kernel_tests();
|
||||||
void register_ps2_runtime_interrupt_tests();
|
void register_ps2_runtime_interrupt_tests();
|
||||||
@@ -18,6 +19,7 @@ int main()
|
|||||||
register_code_generator_tests();
|
register_code_generator_tests();
|
||||||
register_r5900_decoder_tests();
|
register_r5900_decoder_tests();
|
||||||
register_elf_analyzer_tests();
|
register_elf_analyzer_tests();
|
||||||
|
register_pad_input_tests();
|
||||||
register_ps2_runtime_io_tests();
|
register_ps2_runtime_io_tests();
|
||||||
register_ps2_runtime_kernel_tests();
|
register_ps2_runtime_kernel_tests();
|
||||||
register_ps2_runtime_interrupt_tests();
|
register_ps2_runtime_interrupt_tests();
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#include "MiniTest.h"
|
||||||
|
#include "ps2_stubs.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint32_t kPadDataAddr = 0x1000;
|
||||||
|
|
||||||
|
constexpr uint16_t kPadBtnSelect = 1u << 0;
|
||||||
|
constexpr uint16_t kPadBtnL3 = 1u << 1;
|
||||||
|
constexpr uint16_t kPadBtnR3 = 1u << 2;
|
||||||
|
constexpr uint16_t kPadBtnStart = 1u << 3;
|
||||||
|
constexpr uint16_t kPadBtnUp = 1u << 4;
|
||||||
|
constexpr uint16_t kPadBtnRight = 1u << 5;
|
||||||
|
constexpr uint16_t kPadBtnDown = 1u << 6;
|
||||||
|
constexpr uint16_t kPadBtnLeft = 1u << 7;
|
||||||
|
constexpr uint16_t kPadBtnL2 = 1u << 8;
|
||||||
|
constexpr uint16_t kPadBtnR2 = 1u << 9;
|
||||||
|
constexpr uint16_t kPadBtnL1 = 1u << 10;
|
||||||
|
constexpr uint16_t kPadBtnR1 = 1u << 11;
|
||||||
|
constexpr uint16_t kPadBtnTriangle = 1u << 12;
|
||||||
|
constexpr uint16_t kPadBtnCircle = 1u << 13;
|
||||||
|
constexpr uint16_t kPadBtnCross = 1u << 14;
|
||||||
|
constexpr uint16_t kPadBtnSquare = 1u << 15;
|
||||||
|
|
||||||
|
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
|
||||||
|
{
|
||||||
|
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void runPadRead(R5900Context &ctx, std::vector<uint8_t> &rdram)
|
||||||
|
{
|
||||||
|
setRegU32(ctx, 6, kPadDataAddr); // a2
|
||||||
|
ps2_stubs::scePadRead(rdram.data(), &ctx, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t readButtons(const std::vector<uint8_t> &rdram)
|
||||||
|
{
|
||||||
|
const uint8_t *data = rdram.data() + kPadDataAddr;
|
||||||
|
return static_cast<uint16_t>(data[2] | (data[3] << 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_pad_input_tests()
|
||||||
|
{
|
||||||
|
MiniTest::Case("PadInput", [](TestCase &tc)
|
||||||
|
{
|
||||||
|
tc.Run("scePadRead uses override state", [](TestCase &t)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0);
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
const uint16_t buttons = static_cast<uint16_t>(0xFFFFu & ~kPadBtnCross & ~kPadBtnStart);
|
||||||
|
ps2_stubs::setPadOverrideState(buttons, 0x00, 0xFF, 0x10, 0xEE);
|
||||||
|
|
||||||
|
runPadRead(ctx, rdram);
|
||||||
|
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadRead should return 1");
|
||||||
|
t.Equals(readButtons(rdram), buttons, "button bitmask should match override state");
|
||||||
|
const uint8_t *data = rdram.data() + kPadDataAddr;
|
||||||
|
t.Equals(data[4], static_cast<uint8_t>(0x10), "rx should match override");
|
||||||
|
t.Equals(data[5], static_cast<uint8_t>(0xEE), "ry should match override");
|
||||||
|
t.Equals(data[6], static_cast<uint8_t>(0x00), "lx should match override");
|
||||||
|
t.Equals(data[7], static_cast<uint8_t>(0xFF), "ly should match override");
|
||||||
|
|
||||||
|
ps2_stubs::clearPadOverrideState();
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("scePadRead button bits are active-low", [](TestCase &t)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0);
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
struct ButtonCase
|
||||||
|
{
|
||||||
|
uint16_t mask;
|
||||||
|
const char *name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonCase cases[] = {
|
||||||
|
{kPadBtnSelect, "select"},
|
||||||
|
{kPadBtnL3, "l3"},
|
||||||
|
{kPadBtnR3, "r3"},
|
||||||
|
{kPadBtnStart, "start"},
|
||||||
|
{kPadBtnUp, "up"},
|
||||||
|
{kPadBtnRight, "right"},
|
||||||
|
{kPadBtnDown, "down"},
|
||||||
|
{kPadBtnLeft, "left"},
|
||||||
|
{kPadBtnL2, "l2"},
|
||||||
|
{kPadBtnR2, "r2"},
|
||||||
|
{kPadBtnL1, "l1"},
|
||||||
|
{kPadBtnR1, "r1"},
|
||||||
|
{kPadBtnTriangle, "triangle"},
|
||||||
|
{kPadBtnCircle, "circle"},
|
||||||
|
{kPadBtnCross, "cross"},
|
||||||
|
{kPadBtnSquare, "square"}};
|
||||||
|
|
||||||
|
for (const auto &entry : cases)
|
||||||
|
{
|
||||||
|
const uint16_t buttons = static_cast<uint16_t>(0xFFFFu & ~entry.mask);
|
||||||
|
ps2_stubs::setPadOverrideState(buttons, 0x80, 0x80, 0x80, 0x80);
|
||||||
|
runPadRead(ctx, rdram);
|
||||||
|
|
||||||
|
const uint16_t mask = readButtons(rdram);
|
||||||
|
t.IsTrue((mask & entry.mask) == 0, std::string("button should be active-low: ").append(entry.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
ps2_stubs::clearPadOverrideState();
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("scePadGetButtonMask returns all buttons", [](TestCase &t)
|
||||||
|
{
|
||||||
|
R5900Context ctx;
|
||||||
|
ps2_stubs::scePadGetButtonMask(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0xFFFF), "button mask should be 0xFFFF");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("basic pad init/port/state functions return expected values", [](TestCase &t)
|
||||||
|
{
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
ps2_stubs::scePadInit(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadInit should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadInit2(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadInit2 should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadPortOpen(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadPortOpen should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadPortClose(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadPortClose should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadGetState(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(6), "scePadGetState should return STABLE");
|
||||||
|
|
||||||
|
ps2_stubs::scePadGetReqState(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0), "scePadGetReqState should return completed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadGetPortMax(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(2), "scePadGetPortMax should be 2");
|
||||||
|
|
||||||
|
ps2_stubs::scePadGetSlotMax(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadGetSlotMax should be 1");
|
||||||
|
|
||||||
|
ps2_stubs::scePadGetModVersion(nullptr, &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0x0200), "scePadGetModVersion should be 0x0200");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("pad info and mode helpers return consistent values", [](TestCase &t)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0);
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
ps2_stubs::scePadInfoAct(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0), "scePadInfoAct should return 0");
|
||||||
|
|
||||||
|
ps2_stubs::scePadInfoComb(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0), "scePadInfoComb should return 0");
|
||||||
|
|
||||||
|
setRegU32(ctx, 6, 1);
|
||||||
|
setRegU32(ctx, 7, 0);
|
||||||
|
ps2_stubs::scePadInfoMode(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(7), "scePadInfoMode CURID should return DualShock");
|
||||||
|
|
||||||
|
setRegU32(ctx, 6, 4);
|
||||||
|
setRegU32(ctx, 7, static_cast<uint32_t>(-1));
|
||||||
|
ps2_stubs::scePadInfoMode(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadInfoMode table count should be 1");
|
||||||
|
|
||||||
|
ps2_stubs::scePadInfoPressMode(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0), "scePadInfoPressMode should be 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("pad setters return success", [](TestCase &t)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0);
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetActAlign(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadSetActAlign should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetActDirect(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadSetActDirect should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetButtonInfo(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadSetButtonInfo should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetMainMode(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadSetMainMode should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetReqState(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadSetReqState should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetVrefParam(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadSetVrefParam should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadSetWarningLevel(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(0), "scePadSetWarningLevel should return 0");
|
||||||
|
|
||||||
|
ps2_stubs::scePadEnd(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadEnd should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadEnterPressMode(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadEnterPressMode should succeed");
|
||||||
|
|
||||||
|
ps2_stubs::scePadExitPressMode(rdram.data(), &ctx, nullptr);
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegU32(&ctx, 2)), static_cast<uint32_t>(1), "scePadExitPressMode should succeed");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("pad string helpers map state codes", [](TestCase &t)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0);
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
setRegU32(ctx, 4, 1);
|
||||||
|
setRegU32(ctx, 5, kPadDataAddr);
|
||||||
|
ps2_stubs::scePadStateIntToStr(rdram.data(), &ctx, nullptr);
|
||||||
|
t.IsTrue(std::string(reinterpret_cast<const char *>(rdram.data() + kPadDataAddr)).find("FINDPAD") != std::string::npos,
|
||||||
|
"state 1 should map to FINDPAD");
|
||||||
|
|
||||||
|
setRegU32(ctx, 4, 0);
|
||||||
|
setRegU32(ctx, 5, kPadDataAddr + 64);
|
||||||
|
ps2_stubs::scePadStateIntToStr(rdram.data(), &ctx, nullptr);
|
||||||
|
t.IsTrue(std::string(reinterpret_cast<const char *>(rdram.data() + kPadDataAddr + 64)).find("DISCONNECTED") != std::string::npos,
|
||||||
|
"state 0 should map to DISCONNECTED");
|
||||||
|
|
||||||
|
setRegU32(ctx, 4, 1);
|
||||||
|
setRegU32(ctx, 5, kPadDataAddr + 128);
|
||||||
|
ps2_stubs::scePadReqIntToStr(rdram.data(), &ctx, nullptr);
|
||||||
|
t.IsTrue(std::string(reinterpret_cast<const char *>(rdram.data() + kPadDataAddr + 128)).find("BUSY") != std::string::npos,
|
||||||
|
"req state 1 should map to BUSY");
|
||||||
|
});
|
||||||
|
tc.Run("scePadGetFrameCount increments", [](TestCase &t)
|
||||||
|
{
|
||||||
|
R5900Context ctx;
|
||||||
|
ps2_stubs::scePadGetFrameCount(nullptr, &ctx, nullptr);
|
||||||
|
const uint32_t first = getRegU32(&ctx, 2);
|
||||||
|
ps2_stubs::scePadGetFrameCount(nullptr, &ctx, nullptr);
|
||||||
|
const uint32_t second = getRegU32(&ctx, 2);
|
||||||
|
t.Equals(second, first + 1, "frame count should increment");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("scePadStateIntToStr and scePadReqIntToStr write strings", [](TestCase &t)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0);
|
||||||
|
R5900Context ctx;
|
||||||
|
|
||||||
|
setRegU32(ctx, 4, 6);
|
||||||
|
setRegU32(ctx, 5, kPadDataAddr);
|
||||||
|
ps2_stubs::scePadStateIntToStr(rdram.data(), &ctx, nullptr);
|
||||||
|
const char *stateStr = reinterpret_cast<const char *>(rdram.data() + kPadDataAddr);
|
||||||
|
t.IsTrue(std::string(stateStr).find("STABLE") != std::string::npos, "state string should include STABLE");
|
||||||
|
|
||||||
|
setRegU32(ctx, 4, 0);
|
||||||
|
setRegU32(ctx, 5, kPadDataAddr + 64);
|
||||||
|
ps2_stubs::scePadReqIntToStr(rdram.data(), &ctx, nullptr);
|
||||||
|
const char *reqStr = reinterpret_cast<const char *>(rdram.data() + kPadDataAddr + 64);
|
||||||
|
t.IsTrue(std::string(reqStr).find("COMPLETE") != std::string::npos, "req string should include COMPLETE");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ps2recomp/types.h"
|
#include "ps2recomp/types.h"
|
||||||
#include <elfio/elfio.hpp>
|
#include <elfio/elfio.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -118,6 +119,41 @@ static bool writeMinimalMipsElfWithCodeAndDataFunctionSymbols(const std::filesys
|
|||||||
return writer.save(elfPath.string());
|
return writer.save(elfPath.string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path &elfPath)
|
||||||
|
{
|
||||||
|
ELFIO::elfio writer;
|
||||||
|
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
|
||||||
|
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
|
||||||
|
writer.set_type(ELFIO::ET_EXEC);
|
||||||
|
writer.set_machine(ELFIO::EM_MIPS);
|
||||||
|
writer.set_entry(0x00100000u);
|
||||||
|
|
||||||
|
ELFIO::section *text = writer.sections.add(".text");
|
||||||
|
text->set_type(ELFIO::SHT_PROGBITS);
|
||||||
|
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
|
||||||
|
text->set_addr_align(4);
|
||||||
|
text->set_address(0x00100000u);
|
||||||
|
|
||||||
|
const std::array<uint32_t, 6> textWords = {
|
||||||
|
0x0C040004u, // jal 0x00100010
|
||||||
|
0x00000000u, // nop
|
||||||
|
0x03E00008u, // jr $ra
|
||||||
|
0x00000000u, // nop
|
||||||
|
0x03E00008u, // jr $ra
|
||||||
|
0x00000000u // nop
|
||||||
|
};
|
||||||
|
text->set_data(reinterpret_cast<const char *>(textWords.data()),
|
||||||
|
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
|
||||||
|
|
||||||
|
ELFIO::segment *textSegment = writer.segments.add();
|
||||||
|
textSegment->set_type(ELFIO::PT_LOAD);
|
||||||
|
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
|
||||||
|
textSegment->set_align(0x1000);
|
||||||
|
textSegment->add_section_index(text->get_index(), text->get_addr_align());
|
||||||
|
|
||||||
|
return writer.save(elfPath.string());
|
||||||
|
}
|
||||||
|
|
||||||
void register_ps2_recompiler_tests()
|
void register_ps2_recompiler_tests()
|
||||||
{
|
{
|
||||||
MiniTest::Case("PS2Recompiler", [](TestCase &tc)
|
MiniTest::Case("PS2Recompiler", [](TestCase &tc)
|
||||||
@@ -275,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) {
|
tc.Run("entry reslice handles entries without containing function", [](TestCase &t) {
|
||||||
std::vector<Function> functions = {
|
std::vector<Function> functions = {
|
||||||
makeFunction("entry_1008", 0x1008u, 0x1018u),
|
makeFunction("entry_1008", 0x1008u, 0x1018u),
|
||||||
@@ -513,5 +586,85 @@ void register_ps2_recompiler_tests()
|
|||||||
|
|
||||||
std::error_code removeError;
|
std::error_code removeError;
|
||||||
std::filesystem::remove(elfPath, removeError);
|
std::filesystem::remove(elfPath, removeError);
|
||||||
}); });
|
});
|
||||||
|
|
||||||
|
tc.Run("ghidra map replaces JAL fallback-only auto starts", [](TestCase &t) {
|
||||||
|
const auto uniqueSuffix = std::to_string(
|
||||||
|
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||||
|
const std::filesystem::path elfPath =
|
||||||
|
std::filesystem::temp_directory_path() / ("ps2recomp-ghidra-merge-" + uniqueSuffix + ".elf");
|
||||||
|
const std::filesystem::path mapPath =
|
||||||
|
std::filesystem::temp_directory_path() / ("ps2recomp-ghidra-merge-" + uniqueSuffix + ".csv");
|
||||||
|
|
||||||
|
const bool writeOk = writeMinimalMipsElfWithJalFallbackTarget(elfPath);
|
||||||
|
t.IsTrue(writeOk, "temporary ELF should be generated");
|
||||||
|
if (!writeOk)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElfParser parser(elfPath.string());
|
||||||
|
const bool parseOk = parser.parse();
|
||||||
|
t.IsTrue(parseOk, "generated ELF should parse");
|
||||||
|
if (!parseOk)
|
||||||
|
{
|
||||||
|
std::error_code removeError;
|
||||||
|
std::filesystem::remove(elfPath, removeError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fallbackExtras = parser.extractExtraFunctions();
|
||||||
|
const bool hasFallbackStart = std::any_of(
|
||||||
|
fallbackExtras.begin(), fallbackExtras.end(),
|
||||||
|
[](const Function &fn)
|
||||||
|
{ return fn.start == 0x00100010u; });
|
||||||
|
t.IsTrue(hasFallbackStart, "JAL fallback should discover secondary start before map load");
|
||||||
|
|
||||||
|
std::ofstream mapFile(mapPath);
|
||||||
|
t.IsTrue(static_cast<bool>(mapFile), "ghidra map file should be writable");
|
||||||
|
if (!mapFile)
|
||||||
|
{
|
||||||
|
std::error_code removeError;
|
||||||
|
std::filesystem::remove(elfPath, removeError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapFile << "name,start,end,size\n";
|
||||||
|
mapFile << "FUN_00100000,0x00100000,0x00100010,0x10\n";
|
||||||
|
mapFile.close();
|
||||||
|
|
||||||
|
const bool mapLoaded = parser.loadGhidraFunctionMap(mapPath.string());
|
||||||
|
t.IsTrue(mapLoaded, "ghidra map should load");
|
||||||
|
|
||||||
|
const auto functions = parser.extractFunctions();
|
||||||
|
const auto entryIt = std::find_if(
|
||||||
|
functions.begin(), functions.end(),
|
||||||
|
[](const Function &fn)
|
||||||
|
{ return fn.start == 0x00100000u; });
|
||||||
|
t.IsTrue(entryIt != functions.end(), "ghidra entry should exist");
|
||||||
|
if (entryIt != functions.end())
|
||||||
|
{
|
||||||
|
t.Equals(entryIt->name, std::string("FUN_00100000"),
|
||||||
|
"ghidra name should win over fallback auto-name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool stillHasFallbackOnlyStart = std::any_of(
|
||||||
|
functions.begin(), functions.end(),
|
||||||
|
[](const Function &fn)
|
||||||
|
{ return fn.start == 0x00100010u; });
|
||||||
|
t.IsFalse(stillHasFallbackOnlyStart,
|
||||||
|
"fallback-only function starts should be removed once ghidra map is loaded");
|
||||||
|
|
||||||
|
std::error_code removeError;
|
||||||
|
std::filesystem::remove(elfPath, removeError);
|
||||||
|
std::filesystem::remove(mapPath, removeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("respect max length for .cpp filenames", [](TestCase& t) {
|
||||||
|
|
||||||
|
t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678",".cpp",50).length() <= 50,"Function name must be max 50 characters");
|
||||||
|
|
||||||
|
t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678", ".cpp", 50).rfind("0x12345678") != std::string::npos, "Function name must mantain the function address at the end, if present");
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "MiniTest.h"
|
#include "MiniTest.h"
|
||||||
#include "ps2_runtime.h"
|
#include "ps2_runtime.h"
|
||||||
|
#include "ps2_runtime_macros.h"
|
||||||
#include "ps2_syscalls.h"
|
#include "ps2_syscalls.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -56,7 +57,7 @@ namespace
|
|||||||
|
|
||||||
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
|
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)
|
int32_t getRegS32(const R5900Context &ctx, int reg)
|
||||||
@@ -82,6 +83,61 @@ namespace
|
|||||||
return dispatchNumericSyscall(syscallNumber, rdram, ctx, runtime);
|
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
|
struct TestEnv
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> rdram;
|
std::vector<uint8_t> rdram;
|
||||||
@@ -386,5 +442,304 @@ void register_ps2_runtime_kernel_tests()
|
|||||||
const uint32_t setupSp = static_cast<uint32_t>(getRegS32(env.ctx, 2));
|
const uint32_t setupSp = static_cast<uint32_t>(getRegS32(env.ctx, 2));
|
||||||
t.Equals(setupSp & 0xFu, 0u, "SetupThread should always return a 16-byte aligned stack pointer");
|
t.Equals(setupSp & 0xFu, 0u, "SetupThread should always return a 16-byte aligned stack pointer");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tc.Run("numeric syscall 0x83 finds matching table entry", [](TestCase &t)
|
||||||
|
{
|
||||||
|
TestEnv env;
|
||||||
|
constexpr uint32_t kTableBase = 0x00002000u;
|
||||||
|
constexpr uint32_t kValues[] = {
|
||||||
|
0x11111111u,
|
||||||
|
0x11223344u,
|
||||||
|
0x55555555u,
|
||||||
|
0x89ABCDEFu
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
"syscall 0x83 should dispatch");
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
|
||||||
|
kTableBase + 4u,
|
||||||
|
"FindAddress should return address of first matching word");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("numeric syscall 0x83 supports KSEG aliases", [](TestCase &t)
|
||||||
|
{
|
||||||
|
TestEnv env;
|
||||||
|
constexpr uint32_t kTableBasePhys = 0x00003000u;
|
||||||
|
constexpr uint32_t kTableBaseKseg = 0x80003000u;
|
||||||
|
constexpr uint32_t kValues[] = {
|
||||||
|
0x00123456u,
|
||||||
|
0x8000AAAAu
|
||||||
|
};
|
||||||
|
|
||||||
|
writeGuestWords(env.rdram.data(), kTableBasePhys, kValues, std::size(kValues));
|
||||||
|
setRegU32(env.ctx, 4, kTableBaseKseg);
|
||||||
|
setRegU32(env.ctx, 5, kTableBaseKseg + static_cast<uint32_t>(sizeof(kValues)));
|
||||||
|
setRegU32(env.ctx, 6, 0x80123456u); // Alias of first table value
|
||||||
|
|
||||||
|
t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime),
|
||||||
|
"syscall 0x83 should dispatch");
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
|
||||||
|
kTableBaseKseg,
|
||||||
|
"FindAddress should match KSEG aliases and preserve guest segment in return value");
|
||||||
|
});
|
||||||
|
|
||||||
|
tc.Run("numeric syscall 0x83 returns 0 when entry is absent", [](TestCase &t)
|
||||||
|
{
|
||||||
|
TestEnv env;
|
||||||
|
constexpr uint32_t kTableBase = 0x00004000u;
|
||||||
|
constexpr uint32_t kValues[] = {
|
||||||
|
0x00000001u,
|
||||||
|
0x00000002u,
|
||||||
|
0x00000003u
|
||||||
|
};
|
||||||
|
|
||||||
|
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, 0xDEADBEEFu);
|
||||||
|
|
||||||
|
t.IsTrue(callSyscall(0x83u, env.rdram.data(), &env.ctx, &env.runtime),
|
||||||
|
"syscall 0x83 should dispatch");
|
||||||
|
t.Equals(static_cast<uint32_t>(getRegS32(env.ctx, 2)),
|
||||||
|
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 "MiniTest.h"
|
||||||
#include "ps2_runtime.h"
|
#include "ps2_runtime.h"
|
||||||
|
#include "ps2_syscalls.h"
|
||||||
#include "ps2_stubs.h"
|
#include "ps2_stubs.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -7,6 +8,11 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ps2_stubs
|
||||||
|
{
|
||||||
|
void resetSifState();
|
||||||
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
struct TestEnv
|
struct TestEnv
|
||||||
@@ -17,6 +23,7 @@ namespace
|
|||||||
|
|
||||||
TestEnv() : rdram(PS2_RAM_SIZE, 0u)
|
TestEnv() : rdram(PS2_RAM_SIZE, 0u)
|
||||||
{
|
{
|
||||||
|
ps2_stubs::resetSifState();
|
||||||
std::memset(&ctx, 0, sizeof(ctx));
|
std::memset(&ctx, 0, sizeof(ctx));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -71,6 +78,23 @@ namespace
|
|||||||
std::memcpy(&value, rdram + addr, sizeof(value));
|
std::memcpy(&value, rdram + addr, sizeof(value));
|
||||||
return 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()
|
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");
|
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)
|
tc.Run("sceSifSetDma rejects invalid descriptors without partial writes", [](TestCase &t)
|
||||||
{
|
{
|
||||||
TestEnv env;
|
TestEnv env;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "MiniTest.h"
|
#include "MiniTest.h"
|
||||||
#include "ps2_runtime.h"
|
#include "ps2_runtime.h"
|
||||||
#include "ps2_syscalls.h"
|
#include "ps2_syscalls.h"
|
||||||
|
#include "ps2_stubs.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -9,6 +10,11 @@
|
|||||||
|
|
||||||
using namespace ps2_syscalls;
|
using namespace ps2_syscalls;
|
||||||
|
|
||||||
|
namespace ps2_stubs
|
||||||
|
{
|
||||||
|
void resetSifState();
|
||||||
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int KE_OK = 0;
|
constexpr int KE_OK = 0;
|
||||||
@@ -82,6 +88,7 @@ namespace
|
|||||||
|
|
||||||
TestEnv() : rdram(PS2_RAM_SIZE, 0)
|
TestEnv() : rdram(PS2_RAM_SIZE, 0)
|
||||||
{
|
{
|
||||||
|
ps2_stubs::resetSifState();
|
||||||
std::memset(&ctx, 0, sizeof(ctx));
|
std::memset(&ctx, 0, sizeof(ctx));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user