mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-27 09:05:28 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75d729ce40 |
@@ -12,7 +12,7 @@ This project statically recompiles PS2 ELF binaries into C++ and provides a runt
|
||||
* `ps2xAnalyzer`: scans ELF/functions and writes TOML config (`stubs`, `skip`, instruction patches).
|
||||
* `ps2xRecomp`: reads TOML + ELF, decodes R5900 instructions, and generates C++ output.
|
||||
* `ps2xRuntime`: hosts memory, function registration, syscall dispatch, and hardware stubs.
|
||||
* `ps2xIOP`: portable, instance-owned IOP HLE services, game profiles, and the C plugin ABI.
|
||||
* `ps2xIOP`: R3000A IRX execution, a virtual IOP kernel, and generic HLE fallbacks.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -79,9 +79,7 @@ Fallback workflow for quick local experiments or ELFs with debug symbol :
|
||||
./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.
|
||||
See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for ghdira instructions.
|
||||
|
||||
Then build generated output and link with `ps2xRuntime`.
|
||||
|
||||
@@ -133,15 +131,15 @@ To execute the recompiled code.
|
||||
* Some syscall dispatcher with common kernel IDs.
|
||||
* Basic GS/VU/file/system stubs.
|
||||
* Foundation to expand and port your game.
|
||||
* `ps2xIOP` profile selection and optional `.dll`/`.so` discovery for game-specific IOP HLE.
|
||||
* `ps2xIOP` execution of original IRX modules with generic HLE fallbacks.
|
||||
|
||||
See [IOP HLE profiles and plugins](ps2xIOP/README.md) for the service boundary and external plugin workflow.
|
||||
See [IOP emulation](ps2xIOP/README.md) for module execution and the service boundary.
|
||||
|
||||
### Game Override Hooks
|
||||
|
||||
Game overrides are runtime-side, build-scoped patch modules.
|
||||
|
||||
A game override is C++ code that runs during `loadELF` and can replace EE function bindings by address for one specific game build. IOP RPC/DMA behavior belongs in a `ps2xIOP` profile instead. This is separate from recompilation output and separate from global runtime stubs/syscalls.
|
||||
A game override is C++ code that runs during `loadELF` and can replace EE function bindings by address for one specific game build. IOP RPC/DMA behavior is handled by the `ps2xIOP` emulator and its runtime transport. This is separate from recompilation output and separate from global runtime stubs/syscalls.
|
||||
|
||||
API:
|
||||
|
||||
@@ -164,9 +162,8 @@ Use Game Override modules when:
|
||||
6. Re-test from cold boot after each batch.
|
||||
|
||||
### Limitations
|
||||
|
||||
* Graphics Synthesizer and other hardware components need external implementation
|
||||
* VU1 microcode is not complete.
|
||||
|
||||
* Performance is very bad for VU and GS
|
||||
* Hardware emulation is partial and many paths are stubbed.
|
||||
|
||||
### Acknowledgments
|
||||
@@ -175,3 +172,4 @@ Use Game Override modules when:
|
||||
* Uses ELFIO for ELF parsing
|
||||
* Uses toml11 for TOML parsing
|
||||
* Uses fmt for string formatting
|
||||
* Reference for runtime PCSX2
|
||||
@@ -32,8 +32,7 @@ Japanese set, and depends on samples that retained relocations. Treat the result
|
||||
high-confidence hint rather than a complete SDK catalog: it can miss SDK variants that
|
||||
were not present in the sampled games, and ambiguous matches are intentionally ignored.
|
||||
|
||||
### 4. Ghidra Integration (For Retail and Stripped Games, Preferred)
|
||||
This is the recommended workflow for almost every commercial game:
|
||||
### 4. Ghidra Integration
|
||||
1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
2. Run it in Ghidra to export a CSV map of all functions.
|
||||
3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`.
|
||||
@@ -63,8 +62,7 @@ ps2_analyzer <input_elf> <output_toml> [sce_symbol_db_dir]
|
||||
1. Open `game.elf` in Ghidra.
|
||||
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
3. Use the exported TOML and CSV.
|
||||
4. Run the recompiler:
|
||||
`ps2recomp config.toml`
|
||||
4. Run the recompiler: `ps2recomp config.toml`
|
||||
|
||||
Fallback:
|
||||
1. Run `ps2_analyzer game.elf config.toml`.
|
||||
@@ -74,8 +72,8 @@ Fallback:
|
||||
The tool creates a TOML file with the following sections:
|
||||
* `[general]`: Paths to ELF and Ghidra maps.
|
||||
* `stubs`: Runtime-known functions to be replaced by C++ stubs or syscall handlers.
|
||||
* `untracked_stubs`: Detected library-like functions without runtime handlers. This is
|
||||
informational only and is ignored by the recompiler.
|
||||
* `untracked_stubs`: Detected library-like functions without runtime handlers. This is informational only and is ignored by the recompiler.
|
||||
* `entry_points`: Guest functions without runtime handlers that may be referenced by address.
|
||||
* `skip`: Legacy compatibility field. The analyzer no longer auto-populates it.
|
||||
* `[patches]`: Individual instructions that need to be replaced (SYSCALLs, COP0, etc.).
|
||||
|
||||
@@ -83,6 +81,5 @@ The tool creates a TOML file with the following sections:
|
||||
|
||||
* Heuristics may not catch all special cases in highly optimized code.
|
||||
* Self-modifying code is flagged but requires manual review.
|
||||
* Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries.
|
||||
|
||||
For more details on the recompilation process, see the [Main README](../README.md).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ps2recomp/elf_analyzer.h"
|
||||
#include "ps2recomp/gif_dma_kick_analyzer.h"
|
||||
#include "ps2recomp/analysis_passes.h"
|
||||
#include "ps2recomp/elf_parser.h"
|
||||
#include "ps2recomp/r5900_decoder.h"
|
||||
@@ -358,9 +359,11 @@ namespace ps2recomp
|
||||
}
|
||||
|
||||
const auto &instructions = getDecodedInstructions(func);
|
||||
ConstantRegisterState constantRegisters;
|
||||
|
||||
for (const auto &inst : instructions)
|
||||
{
|
||||
const MemoryAccessHint directAddress = resolveMemoryAccessHint(inst, constantRegisters);
|
||||
if (inst.opcode == OPCODE_LW || inst.opcode == OPCODE_SW ||
|
||||
inst.opcode == OPCODE_LB || inst.opcode == OPCODE_SB ||
|
||||
inst.opcode == OPCODE_LH || inst.opcode == OPCODE_SH ||
|
||||
@@ -420,65 +423,46 @@ namespace ps2recomp
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check for direct addressing with LUI+ADDIU combinations
|
||||
else if (inst.opcode == OPCODE_LW || inst.opcode == OPCODE_SW)
|
||||
|
||||
else if ((inst.opcode == OPCODE_LW || inst.opcode == OPCODE_SW) && directAddress.hasAddress)
|
||||
{
|
||||
// Look for the LUI instruction that sets up the high bits
|
||||
uint32_t baseAddr = 0;
|
||||
for (int i = 1; i <= 5 && static_cast<int>(inst.address) - i * 4 >= static_cast<int>(func.start); i++)
|
||||
{
|
||||
uint32_t prevAddr = inst.address - i * 4;
|
||||
uint32_t prevInst = 0;
|
||||
if (!tryReadWord(m_elfParser.get(), prevAddr, prevInst))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const uint32_t targetAddr = directAddress.address;
|
||||
|
||||
// Check if it's a LUI instruction for the same register
|
||||
if (OPCODE(prevInst) == OPCODE_LUI && RT(prevInst) == inst.rs)
|
||||
{
|
||||
baseAddr = IMMEDIATE(prevInst) << 16;
|
||||
break;
|
||||
}
|
||||
// Detect MMIO accesses
|
||||
if (
|
||||
(targetAddr >= 0x10000000 && targetAddr < 0x14000000) || // I/O
|
||||
(targetAddr >= 0x70000000 && targetAddr < 0x70004000) // Scratchpad
|
||||
)
|
||||
{
|
||||
m_mmioByInstructionAddress[inst.address] = targetAddr;
|
||||
std::cout << "Detected MMIO access at " << std::hex << inst.address << " -> " << targetAddr << std::dec << std::endl;
|
||||
}
|
||||
|
||||
if (baseAddr != 0)
|
||||
for (const auto §ion : m_context.sections)
|
||||
{
|
||||
uint32_t targetAddr = baseAddr + static_cast<int16_t>(inst.immediate);
|
||||
|
||||
// Detect MMIO accesses
|
||||
if ((targetAddr >= 0x10000000 && targetAddr < 0x14000000) || // I/O
|
||||
(targetAddr >= 0x70000000 && targetAddr < 0x70004000)) // Scratchpad
|
||||
if (targetAddr >= section.address && targetAddr < section.address + section.size)
|
||||
{
|
||||
m_mmioByInstructionAddress[inst.address] = targetAddr;
|
||||
std::cout << "Detected MMIO access at " << std::hex << inst.address
|
||||
<< " -> " << targetAddr << std::dec << std::endl;
|
||||
}
|
||||
auto symIt = std::find_if(m_context.symbols.begin(), m_context.symbols.end(),
|
||||
[targetAddr](const Symbol &s)
|
||||
{ return !s.isFunction && s.address <= targetAddr &&
|
||||
s.address + s.size > targetAddr; });
|
||||
|
||||
for (const auto §ion : m_context.sections)
|
||||
{
|
||||
if (targetAddr >= section.address && targetAddr < section.address + section.size)
|
||||
if (symIt != m_context.symbols.end())
|
||||
{
|
||||
auto symIt = std::find_if(m_context.symbols.begin(), m_context.symbols.end(),
|
||||
[targetAddr](const Symbol &s)
|
||||
{ return !s.isFunction && s.address <= targetAddr &&
|
||||
s.address + s.size > targetAddr; });
|
||||
std::cout << "Function " << func.name << " directly accesses "
|
||||
<< (inst.opcode == OPCODE_LW ? "reads from" : "writes to")
|
||||
<< " data symbol " << symIt->name
|
||||
<< " at 0x" << std::hex << targetAddr << std::dec << std::endl;
|
||||
|
||||
if (symIt != m_context.symbols.end())
|
||||
{
|
||||
std::cout << "Function " << func.name << " directly accesses "
|
||||
<< (inst.opcode == OPCODE_LW ? "reads from" : "writes to")
|
||||
<< " data symbol " << symIt->name
|
||||
<< " at 0x" << std::hex << targetAddr << std::dec << std::endl;
|
||||
|
||||
m_functionDataUsage[func.name].insert(symIt->name);
|
||||
}
|
||||
break;
|
||||
m_functionDataUsage[func.name].insert(symIt->name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateConstantRegisters(inst, constantRegisters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ namespace ps2recomp
|
||||
}
|
||||
file << "]\n\n";
|
||||
|
||||
file << "# Detected library-like functions without runtime handlers.\n";
|
||||
file << "# This is informational only; PS2Recomp ignores this list and recompiles them normally.\n";
|
||||
file << "untracked_stubs = [\n";
|
||||
file << "# Guest functions without runtime handlers that may be referenced by address.\n";
|
||||
file << "# PS2Recomp keeps their guest implementation and exposes exact callable entries.\n";
|
||||
file << "entry_points = [\n";
|
||||
for (const auto &func : untrackedStubEntries)
|
||||
{
|
||||
file << " \"" << func << "\",\n";
|
||||
|
||||
+39
-13
@@ -2,20 +2,32 @@ cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(ps2xIOP LANGUAGES CXX)
|
||||
|
||||
option(PS2X_IOP_ENABLE_PLUGINS "Enable dynamic ps2xIOP plugins" OFF)
|
||||
option(PS2X_IOP_BUILD_TESTS "Build ps2xIOP emulator smoke tests" OFF)
|
||||
|
||||
add_library(ps2_iop STATIC
|
||||
src/ps2_path.cpp
|
||||
src/iop_module_manager.cpp
|
||||
src/iop_subsystem.cpp
|
||||
src/builtin_profiles.cpp
|
||||
src/plugin_loader.cpp
|
||||
src/emulator/iop_emulator.cpp
|
||||
src/emulator/core/iop_cpu.cpp
|
||||
src/emulator/core/iop_kernel.cpp
|
||||
src/emulator/core/iop_memory.cpp
|
||||
src/emulator/services/iop_module_loader.cpp
|
||||
src/emulator/services/iop_rpc.cpp
|
||||
src/emulator/imports/iop_cdvd.cpp
|
||||
src/emulator/imports/iop_heaplib.cpp
|
||||
src/emulator/imports/iop_imports.cpp
|
||||
src/emulator/imports/iop_intrman.cpp
|
||||
src/emulator/imports/iop_ioman.cpp
|
||||
src/emulator/imports/iop_loadcore.cpp
|
||||
src/emulator/imports/iop_stdio.cpp
|
||||
src/emulator/imports/iop_sysclib.cpp
|
||||
src/emulator/imports/iop_sysmem.cpp
|
||||
src/emulator/imports/iop_timrman.cpp
|
||||
src/emulator/imports/iop_vblank.cpp
|
||||
src/modules/dbcman.cpp
|
||||
src/modules/libsd.cpp
|
||||
src/modules/mcserv.cpp
|
||||
src/modules/tsnddrv.cpp
|
||||
src/modules/cri_dtx.cpp
|
||||
src/modules/clfile.cpp
|
||||
src/modules/sound_update_stub.cpp
|
||||
src/modules/sdrdrv.cpp
|
||||
)
|
||||
|
||||
target_compile_features(ps2_iop PUBLIC cxx_std_20)
|
||||
@@ -30,12 +42,26 @@ target_include_directories(ps2_iop
|
||||
|
||||
add_library(ps2x::iop ALIAS ps2_iop)
|
||||
|
||||
target_compile_definitions(ps2_iop PUBLIC
|
||||
PS2X_IOP_ENABLE_PLUGINS=$<BOOL:${PS2X_IOP_ENABLE_PLUGINS}>
|
||||
)
|
||||
if(PS2X_IOP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(ps2_iop_emulator_tests tests/iop_emulator_tests.cpp)
|
||||
target_link_libraries(ps2_iop_emulator_tests PRIVATE ps2_iop)
|
||||
add_test(NAME ps2_iop_emulator_tests COMMAND ps2_iop_emulator_tests)
|
||||
|
||||
add_executable(ps2_iop_import_tests tests/iop_import_tests.cpp)
|
||||
target_link_libraries(ps2_iop_import_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_import_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_import_tests COMMAND ps2_iop_import_tests)
|
||||
|
||||
add_executable(ps2_iop_compatibility_tests tests/iop_compatibility_tests.cpp)
|
||||
target_link_libraries(ps2_iop_compatibility_tests PRIVATE ps2_iop)
|
||||
add_test(NAME ps2_iop_compatibility_tests COMMAND ps2_iop_compatibility_tests)
|
||||
|
||||
add_executable(ps2_iop_import_version_tests tests/iop_import_version_tests.cpp)
|
||||
target_link_libraries(ps2_iop_import_version_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests)
|
||||
|
||||
if(PS2X_IOP_ENABLE_PLUGINS AND UNIX AND NOT APPLE)
|
||||
target_link_libraries(ps2_iop PRIVATE ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
|
||||
install(TARGETS ps2_iop
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
# Minimal plugin
|
||||
|
||||
This plugin matches one ELF basename and handles one function on a synthetic
|
||||
SID. It is synchronous: it signals NOWAIT completion and suppresses a second
|
||||
dispatch through a registered EE server.
|
||||
|
||||
```c
|
||||
#include <ps2x/iop/plugin_api.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#define STRING_VIEW(literal) { (literal), sizeof(literal) - 1u }
|
||||
|
||||
enum
|
||||
{
|
||||
MY_SID = 0x6D795349u,
|
||||
MY_FUNCTION = 1u,
|
||||
};
|
||||
|
||||
struct my_state
|
||||
{
|
||||
const ps2x_iop_host_api_v1 *host;
|
||||
};
|
||||
|
||||
static void *my_create(const ps2x_iop_host_api_v1 *host,
|
||||
const ps2x_iop_game_identity_v1 *identity)
|
||||
{
|
||||
struct my_state *state;
|
||||
(void)identity;
|
||||
|
||||
if (!host ||
|
||||
host->abi_version != PS2X_IOP_ABI_VERSION_V1 ||
|
||||
host->struct_size < sizeof(*host))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
state = (struct my_state *)calloc(1u, sizeof(*state));
|
||||
if (state)
|
||||
{
|
||||
state->host = host;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
static void my_destroy(void *instance)
|
||||
{
|
||||
free(instance);
|
||||
}
|
||||
|
||||
static int32_t my_reset(void *instance)
|
||||
{
|
||||
return instance ? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
|
||||
static int32_t my_handle_rpc(void *instance,
|
||||
const ps2x_iop_rpc_request_v1 *request,
|
||||
ps2x_iop_rpc_result_v1 *result)
|
||||
{
|
||||
struct my_state *state = (struct my_state *)instance;
|
||||
const uint32_t value = 1u;
|
||||
int32_t status;
|
||||
|
||||
if (!state || !request || !result ||
|
||||
request->struct_size < sizeof(*request) ||
|
||||
result->struct_size < sizeof(*result))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
|
||||
result->handled = 0u;
|
||||
result->result_address = 0u;
|
||||
result->signal_nowait_completion = 0u;
|
||||
result->signal_completion = 0u;
|
||||
result->callback_policy = PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1;
|
||||
result->server_dispatch_policy =
|
||||
PS2X_IOP_SERVER_DISPATCH_RUNTIME_DEFAULT_V1;
|
||||
|
||||
if (request->sid != MY_SID || request->function != MY_FUNCTION)
|
||||
{
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
if (request->receive.size < sizeof(value))
|
||||
{
|
||||
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
|
||||
}
|
||||
if (!state->host->write_guest)
|
||||
{
|
||||
return PS2X_IOP_STATUS_UNSUPPORTED_V1;
|
||||
}
|
||||
|
||||
status = state->host->write_guest(state->host->userdata,
|
||||
request->receive.address,
|
||||
&value,
|
||||
sizeof(value));
|
||||
if (status != PS2X_IOP_STATUS_OK_V1)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
result->handled = 1u;
|
||||
result->result_address = request->receive.address;
|
||||
result->signal_nowait_completion = 1u;
|
||||
result->server_dispatch_policy = PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1;
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
|
||||
static const uint32_t my_sids[] = { MY_SID };
|
||||
|
||||
static const ps2x_iop_profile_api_v1 my_profiles[] = {
|
||||
{
|
||||
PS2X_IOP_ABI_VERSION_V1,
|
||||
sizeof(ps2x_iop_profile_api_v1),
|
||||
STRING_VIEW("my-game-profile"),
|
||||
{
|
||||
sizeof(ps2x_iop_game_matcher_v1),
|
||||
STRING_VIEW("SLUS_000.00"),
|
||||
0u,
|
||||
0u,
|
||||
},
|
||||
1u,
|
||||
my_sids,
|
||||
my_create,
|
||||
my_destroy,
|
||||
my_reset,
|
||||
NULL,
|
||||
my_handle_rpc,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
},
|
||||
};
|
||||
|
||||
PS2X_IOP_PLUGIN_EXPORT int32_t
|
||||
ps2x_iop_query_v1(uint32_t host_abi_version,
|
||||
ps2x_iop_plugin_api_v1 *out)
|
||||
{
|
||||
static const ps2x_iop_plugin_api_v1 plugin = {
|
||||
PS2X_IOP_ABI_VERSION_V1,
|
||||
sizeof(ps2x_iop_plugin_api_v1),
|
||||
STRING_VIEW("my-iop-plugin"),
|
||||
STRING_VIEW("1.0.0"),
|
||||
1u,
|
||||
my_profiles,
|
||||
};
|
||||
|
||||
if (host_abi_version != PS2X_IOP_ABI_VERSION_V1)
|
||||
{
|
||||
return PS2X_IOP_STATUS_UNSUPPORTED_V1;
|
||||
}
|
||||
if (!out)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
if (out->struct_size < sizeof(*out))
|
||||
{
|
||||
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
|
||||
}
|
||||
|
||||
*out = plugin;
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone CMake target
|
||||
|
||||
The plugin consumes only the public ABI header; it does not link to `ps2xRuntime` or `ps2_iop`.
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(my_iop_plugin LANGUAGES C)
|
||||
|
||||
set(PS2X_IOP_INCLUDE_DIR "" CACHE PATH
|
||||
"Directory containing ps2x/iop/plugin_api.h"
|
||||
)
|
||||
if(NOT EXISTS "${PS2X_IOP_INCLUDE_DIR}/ps2x/iop/plugin_api.h")
|
||||
message(FATAL_ERROR
|
||||
"Set PS2X_IOP_INCLUDE_DIR to PS2Recomp/ps2xIOP/include"
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(my_iop_plugin MODULE my_iop_plugin.c)
|
||||
target_include_directories(my_iop_plugin PRIVATE
|
||||
"${PS2X_IOP_INCLUDE_DIR}"
|
||||
)
|
||||
set_target_properties(my_iop_plugin PROPERTIES
|
||||
PREFIX ""
|
||||
C_STANDARD 11
|
||||
C_STANDARD_REQUIRED YES
|
||||
C_EXTENSIONS NO
|
||||
)
|
||||
install(TARGETS my_iop_plugin
|
||||
RUNTIME DESTINATION .
|
||||
LIBRARY DESTINATION .
|
||||
)
|
||||
```
|
||||
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
cmake -S . -B build -A x64 `
|
||||
-DPS2X_IOP_INCLUDE_DIR="C:/path/to/PS2Recomp/ps2xIOP/include"
|
||||
cmake --build build --config Release
|
||||
cmake --install build --config Release `
|
||||
--prefix "C:/path/to/ps2EntryRunner/iop_plugins"
|
||||
```
|
||||
|
||||
On Linux:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
|
||||
-DPS2X_IOP_INCLUDE_DIR=/path/to/PS2Recomp/ps2xIOP/include
|
||||
cmake --build build -j
|
||||
cmake --install build --prefix /path/to/ps2EntryRunner/iop_plugins
|
||||
```
|
||||
|
||||
Build or install the resulting `.dll`/`.so` before the runtime calls
|
||||
`initialize()`. If it is not installed directly, copy it into the executable's
|
||||
`iop_plugins/` directory.
|
||||
+41
-203
@@ -1,219 +1,57 @@
|
||||
# ps2xIOP
|
||||
|
||||
`ps2xIOP` is the IOP high-level emulation (HLE) subsystem used by
|
||||
`ps2xRuntime`. It implements the behavior that games expect from IOP services
|
||||
exposed through SIF RPC and DMA.
|
||||
`ps2xIOP` runs original IRX modules on an R3000A interpreter, with a virtual
|
||||
IOP kernel providing imports without a PS2 BIOS. The C++20 static library
|
||||
`ps2_iop` / `ps2x::iop` is linked into `ps2xRuntime`.
|
||||
|
||||
This subsystem does not emulate the IOP's R3000A CPU and does not load or
|
||||
execute IRX binaries. Its scope is the RPC/DMA behavior needed by recompiled
|
||||
games.
|
||||
## Execution policy
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `ps2_iop`/`ps2x::iop` is a C++20 static library linked into the runtime.
|
||||
> Optional `.dll` and `.so` files are native profile plugins loaded by that
|
||||
> library. They extend the profile catalog; they do not replace `ps2_iop`, its
|
||||
> registry, its host bridge, the SIF transport, or execute PS2 IRX code.
|
||||
Game-specific IOP code executes from IRX modules. There is no game-profile
|
||||
selection or native profile-plugin loader. A physical IRX RPC server is
|
||||
authoritative for its SID.
|
||||
|
||||
## Architecture
|
||||
Generic HLE services remain available when no loaded IRX provides an endpoint:
|
||||
|
||||
```text
|
||||
EE game
|
||||
|
|
||||
| SIF RPC / DMA
|
||||
v
|
||||
ps2xRuntime transport
|
||||
|
|
||||
| RpcRequest / RpcResult / SifTransfer
|
||||
v
|
||||
ps2x::iop::IopSubsystem
|
||||
|-- selected game profile services
|
||||
|-- core services
|
||||
|
|
||||
v
|
||||
IopHost bridge -> validated guest memory, files, audio, memory card,
|
||||
logging, and EE function invocation
|
||||
```
|
||||
|
||||
### Modules, bindings, and profiles
|
||||
|
||||
These terms describe different layers:
|
||||
|
||||
- A **module implementation** is a reusable protocol engine, such as TSNDDRV,
|
||||
CRI DTX, CLFILE, or SDRDRV.
|
||||
- A **binding** contains build-specific values: SIDs, absolute EE addresses,
|
||||
callback addresses, guest arenas, archive names, and protocol variants.
|
||||
- A **profile** matches one game build and creates the required module
|
||||
implementations with that build's bindings.
|
||||
|
||||
For example, `cri_dtx.cpp` contains the reusable CRI DTX engine, while the
|
||||
`recvx-us` profile supplies Code: Veronica X addresses. A second game should
|
||||
reuse that engine only after its wire protocol has been compared with the
|
||||
characterized variant; normally only its profile bindings should change.
|
||||
Parameterized does not mean universally protocol-compatible. In particular,
|
||||
`sound_update_stub.cpp` is a narrow LotR compatibility shim, not a complete
|
||||
generic SOUND driver.
|
||||
|
||||
## Built-in services and profiles
|
||||
|
||||
Core services are created for every `IopSubsystem`:
|
||||
|
||||
| Service | SID | Availability |
|
||||
| Service | SID | Activation |
|
||||
| --- | --- | --- |
|
||||
| MCSERV | `0x80000400`, `0x80000480` | Always active |
|
||||
| LIBSD | `0x80000701` | Always active |
|
||||
| DBCMAN | `0x80001300` | Always active |
|
||||
| MCSERV | `0x80000400`, `0x80000480` | Recognized module load |
|
||||
| LIBSD | `0x80000701` | Recognized module load |
|
||||
| DBCMAN | `0x80001300` | Recognized module load |
|
||||
|
||||
The current built-in game profiles are:
|
||||
These services are dormant before module load and after reset or the final
|
||||
module stop. Unknown modules fail to load; unknown RPC SIDs remain unhandled.
|
||||
Games previously using TSNDDRV, CRI DTX, CLFILE, SOUND or SDRDRV profiles now
|
||||
require their IRX modules and support for the imports and hardware they use.
|
||||
|
||||
| Profile | Matcher | Services |
|
||||
| --- | --- | --- |
|
||||
| `recvx-us` | `slus_201.84` | TSNDDRV and CRI DTX |
|
||||
| `lotr-two-towers-us` | `SLUS_205.78` | CLFILE and SOUND update compatibility |
|
||||
| `fatal-frame-us` | `SLUS_203.88` | SDRDRV |
|
||||
## Lifecycle and transport
|
||||
|
||||
All current built-ins declare only the ELF basename; they do not yet constrain
|
||||
the entry point or CRC32. Basename matching is case-insensitive.
|
||||
- `reset()` clears loaded modules, HLE service state and emulator state.
|
||||
- `loadModule(...)` / `loadModuleBuffer(...)` load and start an IRX.
|
||||
- `stopModule(...)` releases a module and its owned state.
|
||||
- `runEeCycles(...)` advances the IOP from EE cycle accounting.
|
||||
- `selectRpcAbi(...)`, `handleRpc(...)` and `onSifTransfer(...)` connect SIF transport.
|
||||
|
||||
If no profile matches, the subsystem still has MCSERV, LIBSD, and DBCMAN. It
|
||||
does not create any game-specific service. An unknown SID remains unhandled so
|
||||
the SIF transport can apply its normal fallback behavior and report it in the
|
||||
debugger.
|
||||
IOP RAM is separate from EE RAM. The transport copies data through the IOP
|
||||
memory accessors; SIF notifications do not mirror bytes into equal-numbered EE
|
||||
addresses. `RpcResult` describes completion and dispatch actions for the runtime.
|
||||
|
||||
## Profile selection
|
||||
|
||||
When an ELF is loaded, the runtime calculates one `GameIdentity`:
|
||||
|
||||
- ELF basename;
|
||||
- entry point;
|
||||
- CRC-32/IEEE (the common ZIP CRC-32) over the complete ELF file.
|
||||
|
||||
A profile matcher may declare any combination of those fields. Every declared
|
||||
field must match. The matcher with the greatest number of declared fields wins.
|
||||
Two matching profiles with equal specificity are an error; `loadELF()` fails
|
||||
instead of silently choosing one.
|
||||
|
||||
A duplicate SID within the same service layer is an
|
||||
error. Routing selects one service per SID: if a profile shadows a core SID and
|
||||
then returns `handled = 0`, the subsystem does not make a second attempt through
|
||||
the shadowed core service.
|
||||
|
||||
## Dispatch and transfer flow
|
||||
|
||||
`IopSubsystem` exposes five operations used by the runtime:
|
||||
|
||||
1. `configure(GameIdentity)` selects and creates the active profile.
|
||||
2. `reset()` resets core and profile services.
|
||||
3. `selectRpcAbi(...)` lets a service choose the register or stack RPC layout when the default decoder is not sufficient.
|
||||
4. `handleRpc(...)` routes a request by SID and returns both the payload result and the transport policy.
|
||||
5. `onSifTransfer(...)` notifies services before and after SetDma and GetOtherData copies.
|
||||
|
||||
`RpcResult::handled` indicates whether a service consumed the request. The
|
||||
result can also request completion semaphore signals and can suppress the
|
||||
runtime's default EE callback or registered-server dispatch. The transport
|
||||
executes those actions; the service never reaches into runtime internals.
|
||||
|
||||
The transfer hook is deliberately generic. TSNDDRV uses it for compatibility
|
||||
backfill and CRI DTX uses it to observe DMA, but the SIF transport contains no
|
||||
game names, game addresses, or branches for those modules.
|
||||
|
||||
RPC ABI selection is offered to every active profile service before the core
|
||||
services, and every active service receives each SIF transfer notification.
|
||||
Implementations must filter the relevant SID/function or transfer
|
||||
kind/phase/address range themselves.
|
||||
|
||||
## Linking the static library
|
||||
|
||||
```cmake
|
||||
target_link_libraries(my_runtime PRIVATE ps2x::iop)
|
||||
```
|
||||
|
||||
The public C++ API is
|
||||
[`iop_subsystem.h`](include/ps2x/iop/iop_subsystem.h). Applications using
|
||||
`PS2Runtime` normally do not construct it directly; the runtime creates the
|
||||
subsystem and its `IopHost` adapter.
|
||||
|
||||
## Dynamic profile plugins
|
||||
|
||||
Dynamic plugins are optional and disabled by default. Enable them on Windows or
|
||||
Linux with `PS2X_IOP_ENABLE_PLUGINS=ON`.
|
||||
|
||||
| Platform | Plugin format | Status |
|
||||
| --- | --- | --- |
|
||||
| Windows | `.dll` | Supported |
|
||||
| Linux | `.so` | Supported |
|
||||
|
||||
When enable By default, the runtime scans `iop_plugins/` next to the executable. Discovery
|
||||
is non-recursive. An embedding application can replace the search directories
|
||||
before calling `initialize()`:
|
||||
|
||||
```cpp
|
||||
runtime.setIopPluginSearchPaths({
|
||||
std::filesystem::path{"path/to/my/iop_plugins"},
|
||||
});
|
||||
```
|
||||
|
||||
Each native module can publish one or more profiles. Missing query symbols,
|
||||
incompatible ABI versions, malformed descriptors, and unsupported modules are
|
||||
ignored with a diagnostic. Profile ambiguity, an active-layer SID conflict, or
|
||||
failure to create the selected profile makes `loadELF()` fail with a clear
|
||||
error.
|
||||
|
||||
The v1 loader accepts at most 256 profiles per plugin and 256 SIDs per profile.
|
||||
A profile needs a non-empty ID, at least one matcher field, at least one SID,
|
||||
and valid `create`, `destroy`, `reset`, and `handle_rpc` callbacks.
|
||||
|
||||
## Plugin ABI v1
|
||||
|
||||
Plugins include
|
||||
[`plugin_api.h`](include/ps2x/iop/plugin_api.h) and export exactly one C entry
|
||||
point:
|
||||
|
||||
```c
|
||||
PS2X_IOP_PLUGIN_EXPORT int32_t
|
||||
ps2x_iop_query_v1(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api);
|
||||
```
|
||||
|
||||
The ABI uses only fixed C function tables and POD data:
|
||||
|
||||
- validate `abi_version` and `struct_size` before accessing a structure;
|
||||
- use pointer-plus-length string and buffer views;
|
||||
- keep the profile instance behind an opaque `void *` handle;
|
||||
- implement `create`, `destroy`, `reset`, and `handle_rpc`;
|
||||
- optionally implement RPC ABI selection, SIF transfer hooks, and debug metrics;
|
||||
- use host callbacks for guest memory, files, audio, memory cards, logging, and
|
||||
EE function invocation;
|
||||
- never retain request/result pointers after a callback returns;
|
||||
- never pass STL types, C++ classes, exceptions, runtime objects, allocators, or
|
||||
raw guest-memory pointers across the ABI.
|
||||
|
||||
The plugin itself may be implemented in C or C++, but exceptions must not cross
|
||||
the exported C boundary. Guest buffer fields are PS2 addresses, not host
|
||||
pointers.
|
||||
|
||||
The `host` function table passed to `create` may be retained until `destroy`.
|
||||
The identity and its strings, RPC request/result, transfer, and metric pointers
|
||||
are callback-scoped and must not be retained. `invoke_guest_function` is valid
|
||||
only during `handle_rpc` and must use that request's `call_token`. Close file
|
||||
handles and release guest allocations in `reset`/`destroy`.
|
||||
|
||||
Most `int32_t`-returning host callbacks return a `PS2X_IOP_STATUS_*_V1` code.
|
||||
Two are intentionally boolean-style: `has_guest_function` and
|
||||
`invoke_guest_function` return `1` for yes/success, `0` for no/failure, and a
|
||||
negative value for an API error. Do not compare their successful result with
|
||||
`PS2X_IOP_STATUS_OK_V1`, which is zero.
|
||||
|
||||
When compiling as C++, keep the exported query function under `extern "C"`
|
||||
linkage. Including `plugin_api.h` provides the matching C declaration.
|
||||
|
||||
FOr learn more you can check [PluginExample](./PluginExample.md)
|
||||
Link with `target_link_libraries(my_runtime PRIVATE ps2x::iop)`. The public API
|
||||
is [iop_subsystem.h](include/ps2x/iop/iop_subsystem.h); `PS2Runtime` owns its
|
||||
subsystem and host adapter.
|
||||
|
||||
## Diagnostics and tests
|
||||
|
||||
`debugSnapshot()` exposes the active profile, its provider, registered core and
|
||||
profile services, service metrics, loader diagnostics, and the last selection
|
||||
error. The runtime debugger renders this data in the **IOP/SIF** tab.
|
||||
`debugSnapshot()` exposes emulator cycle/instruction counts, loaded module,
|
||||
thread and RPC-server counts, generic service metrics and load diagnostics.
|
||||
The runtime debugger renders these in the **IOP/SIF** tab.
|
||||
|
||||
Registry behavior, instance isolation, reset, built-in services, profile
|
||||
precedence, plugin discovery, ABI rejection, ambiguity, dispatch, destruction,
|
||||
and module lifetime are covered by
|
||||
[`ps2_iop_tests.cpp`](../ps2xTest/src/ps2_iop_tests.cpp).
|
||||
Build standalone tests with:
|
||||
|
||||
```sh
|
||||
cmake -S ps2xIOP -B out/build/iop-tests -DPS2X_IOP_BUILD_TESTS=ON
|
||||
cmake --build out/build/iop-tests
|
||||
ctest --test-dir out/build/iop-tests --output-on-failure
|
||||
```
|
||||
|
||||
The suites cover IRX execution, RPC, imports, version resolution and generic
|
||||
HLE compatibility. `ps2x_tests` also covers runtime SIF RPC/DMA integration.
|
||||
|
||||
@@ -62,6 +62,34 @@ namespace ps2x::iop
|
||||
virtual bool writeGuest(uint32_t address, const void *source, size_t size) = 0;
|
||||
virtual bool zeroGuest(uint32_t address, size_t size) = 0;
|
||||
virtual bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const = 0;
|
||||
|
||||
// IOP RAM is a distinct address space from the EE guest. TODO remove this later
|
||||
virtual bool readIopMemory(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
(void)address;
|
||||
(void)destination;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual bool writeIopMemory(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
(void)address;
|
||||
(void)source;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual bool zeroIopMemory(uint32_t address, size_t size)
|
||||
{
|
||||
(void)address;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const
|
||||
{
|
||||
(void)address;
|
||||
normalized = 0u;
|
||||
return false;
|
||||
}
|
||||
virtual uint32_t allocateIopHandle(IopHandleKind kind) = 0;
|
||||
virtual uint32_t allocateGuest(uint32_t size, uint32_t alignment) = 0;
|
||||
virtual void freeGuest(uint32_t address) = 0;
|
||||
@@ -90,6 +118,18 @@ namespace ps2x::iop
|
||||
uint32_t a3,
|
||||
uint32_t *resultAddress) = 0;
|
||||
|
||||
// Deliver an IOP -> EE SIF command packet. The default keeps hosts
|
||||
// which do not emulate the EE command dispatcher source-compatible.
|
||||
virtual bool sendSifCommand(uint32_t commandId,
|
||||
const void *packet,
|
||||
size_t packetSize)
|
||||
{
|
||||
(void)commandId;
|
||||
(void)packet;
|
||||
(void)packetSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void log(LogLevel level, std::string_view message) = 0;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
@@ -21,16 +21,26 @@ namespace ps2x::iop
|
||||
IopSubsystem(IopSubsystem &&) noexcept;
|
||||
IopSubsystem &operator=(IopSubsystem &&) noexcept;
|
||||
|
||||
void setPluginSearchPaths(std::vector<std::filesystem::path> paths);
|
||||
bool loadPlugins(std::string *error = nullptr);
|
||||
|
||||
bool configure(const GameIdentity &identity, std::string *error = nullptr);
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result = nullptr);
|
||||
void runEeCycles(uint64_t eeCycles) noexcept;
|
||||
|
||||
[[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const;
|
||||
[[nodiscard]] bool canBindRpc(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
|
||||
// Physical IOP RAM access shared by the emulator, SIF DMA, and HLE services. Addresses are IOP addresses.
|
||||
[[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u);
|
||||
[[nodiscard]] bool freeMemory(uint32_t address);
|
||||
[[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroMemory(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const;
|
||||
|
||||
[[nodiscard]] DebugSnapshot debugSnapshot() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace ps2x::iop
|
||||
uint32_t crc32 = 0;
|
||||
};
|
||||
|
||||
struct ModuleLoadResult
|
||||
{
|
||||
bool handled = false;
|
||||
int32_t moduleId = -1;
|
||||
int32_t startResult = -1;
|
||||
};
|
||||
|
||||
enum class RpcAbi : uint32_t
|
||||
{
|
||||
RuntimeDefault = 0,
|
||||
@@ -131,14 +138,17 @@ namespace ps2x::iop
|
||||
{
|
||||
std::string name;
|
||||
std::vector<uint32_t> sids;
|
||||
bool profileSpecific = false;
|
||||
bool active = true;
|
||||
std::vector<DebugMetric> metrics;
|
||||
};
|
||||
|
||||
struct DebugSnapshot
|
||||
{
|
||||
std::string activeProfile;
|
||||
std::string activeProvider;
|
||||
uint64_t emulatorCycles = 0;
|
||||
uint64_t emulatorInstructions = 0;
|
||||
uint32_t emulatorLoadedModules = 0;
|
||||
uint32_t emulatorThreads = 0;
|
||||
uint32_t emulatorRpcServers = 0;
|
||||
std::vector<DebugService> services;
|
||||
std::vector<std::string> diagnostics;
|
||||
};
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
#ifndef PS2X_IOP_PLUGIN_API_H
|
||||
#define PS2X_IOP_PLUGIN_API_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define PS2X_IOP_PLUGIN_EXPORT __declspec(dllexport)
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
#define PS2X_IOP_PLUGIN_EXPORT __attribute__((visibility("default")))
|
||||
#else
|
||||
#define PS2X_IOP_PLUGIN_EXPORT
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
#define PS2X_IOP_ABI_VERSION_V1 1u
|
||||
#define PS2X_IOP_QUERY_SYMBOL_V1 "ps2x_iop_query_v1"
|
||||
|
||||
enum ps2x_iop_status_v1
|
||||
{
|
||||
PS2X_IOP_STATUS_OK_V1 = 0,
|
||||
PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1 = 1,
|
||||
PS2X_IOP_STATUS_INVALID_ARGUMENT_V1 = -1,
|
||||
PS2X_IOP_STATUS_UNSUPPORTED_V1 = -2,
|
||||
PS2X_IOP_STATUS_FAILED_V1 = -3,
|
||||
};
|
||||
|
||||
enum ps2x_iop_rpc_abi_v1
|
||||
{
|
||||
PS2X_IOP_RPC_ABI_DEFAULT_V1 = 0,
|
||||
PS2X_IOP_RPC_ABI_REGISTERS_V1 = 1,
|
||||
PS2X_IOP_RPC_ABI_STACK_V1 = 2,
|
||||
};
|
||||
|
||||
enum ps2x_iop_callback_policy_v1
|
||||
{
|
||||
PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1 = 0,
|
||||
PS2X_IOP_CALLBACK_SUPPRESS_V1 = 1,
|
||||
};
|
||||
|
||||
enum ps2x_iop_server_dispatch_policy_v1
|
||||
{
|
||||
PS2X_IOP_SERVER_DISPATCH_RUNTIME_DEFAULT_V1 = 0,
|
||||
PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1 = 1,
|
||||
};
|
||||
|
||||
enum ps2x_iop_transfer_kind_v1
|
||||
{
|
||||
PS2X_IOP_TRANSFER_SET_DMA_V1 = 0,
|
||||
PS2X_IOP_TRANSFER_GET_OTHER_DATA_V1 = 1,
|
||||
};
|
||||
|
||||
enum ps2x_iop_transfer_phase_v1
|
||||
{
|
||||
PS2X_IOP_TRANSFER_BEFORE_COPY_V1 = 0,
|
||||
PS2X_IOP_TRANSFER_AFTER_COPY_V1 = 1,
|
||||
};
|
||||
|
||||
enum ps2x_iop_host_path_kind_v1
|
||||
{
|
||||
PS2X_IOP_PATH_ELF_DIRECTORY_V1 = 0,
|
||||
PS2X_IOP_PATH_CD_ROOT_V1 = 1,
|
||||
PS2X_IOP_PATH_CD_IMAGE_V1 = 2,
|
||||
PS2X_IOP_PATH_HOST_ROOT_V1 = 3,
|
||||
PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1 = 4,
|
||||
};
|
||||
|
||||
enum ps2x_iop_handle_kind_v1
|
||||
{
|
||||
PS2X_IOP_HANDLE_RPC_SERVER_V1 = 0,
|
||||
PS2X_IOP_HANDLE_RPC_PACKET_V1 = 1,
|
||||
};
|
||||
|
||||
enum ps2x_iop_log_level_v1
|
||||
{
|
||||
PS2X_IOP_LOG_DEBUG_V1 = 0,
|
||||
PS2X_IOP_LOG_INFO_V1 = 1,
|
||||
PS2X_IOP_LOG_WARNING_V1 = 2,
|
||||
PS2X_IOP_LOG_ERROR_V1 = 3,
|
||||
};
|
||||
|
||||
enum ps2x_iop_memory_card_operation_v1
|
||||
{
|
||||
PS2X_IOP_MC_INIT_V1 = 0,
|
||||
PS2X_IOP_MC_GET_INFO_V1 = 1,
|
||||
PS2X_IOP_MC_OPEN_V1 = 2,
|
||||
PS2X_IOP_MC_CLOSE_V1 = 3,
|
||||
PS2X_IOP_MC_SEEK_V1 = 4,
|
||||
PS2X_IOP_MC_READ_V1 = 5,
|
||||
PS2X_IOP_MC_WRITE_V1 = 6,
|
||||
PS2X_IOP_MC_FLUSH_V1 = 7,
|
||||
PS2X_IOP_MC_CHDIR_V1 = 8,
|
||||
PS2X_IOP_MC_GET_DIR_V1 = 9,
|
||||
PS2X_IOP_MC_SET_FILE_INFO_V1 = 10,
|
||||
PS2X_IOP_MC_DELETE_V1 = 11,
|
||||
PS2X_IOP_MC_FORMAT_V1 = 12,
|
||||
PS2X_IOP_MC_UNFORMAT_V1 = 13,
|
||||
PS2X_IOP_MC_MKDIR_V1 = 14,
|
||||
};
|
||||
|
||||
typedef struct ps2x_iop_string_view_v1
|
||||
{
|
||||
const char *data;
|
||||
size_t size;
|
||||
} ps2x_iop_string_view_v1;
|
||||
|
||||
typedef struct ps2x_iop_guest_buffer_v1
|
||||
{
|
||||
uint32_t address;
|
||||
uint32_t size;
|
||||
} ps2x_iop_guest_buffer_v1;
|
||||
|
||||
typedef struct ps2x_iop_game_identity_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
ps2x_iop_string_view_v1 elf_name;
|
||||
uint32_t entry_point;
|
||||
uint32_t crc32;
|
||||
} ps2x_iop_game_identity_v1;
|
||||
|
||||
typedef struct ps2x_iop_game_matcher_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
ps2x_iop_string_view_v1 elf_name;
|
||||
uint32_t entry_point;
|
||||
uint32_t crc32;
|
||||
} ps2x_iop_game_matcher_v1;
|
||||
|
||||
typedef struct ps2x_iop_rpc_candidate_v1
|
||||
{
|
||||
uint32_t send_size;
|
||||
uint32_t receive_address;
|
||||
uint32_t receive_size;
|
||||
uint32_t end_function;
|
||||
uint32_t end_parameter;
|
||||
uint32_t plausible;
|
||||
} ps2x_iop_rpc_candidate_v1;
|
||||
|
||||
typedef struct ps2x_iop_rpc_abi_request_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
uint32_t bound_sid;
|
||||
uint32_t function;
|
||||
ps2x_iop_rpc_candidate_v1 registers;
|
||||
ps2x_iop_rpc_candidate_v1 stack;
|
||||
} ps2x_iop_rpc_abi_request_v1;
|
||||
|
||||
typedef struct ps2x_iop_rpc_request_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
uint64_t call_token;
|
||||
uint32_t client_address;
|
||||
uint32_t server_address;
|
||||
uint32_t server_function;
|
||||
uint32_t server_buffer;
|
||||
uint32_t sid;
|
||||
uint32_t function;
|
||||
uint32_t mode;
|
||||
ps2x_iop_guest_buffer_v1 send;
|
||||
ps2x_iop_guest_buffer_v1 receive;
|
||||
uint32_t end_function;
|
||||
uint32_t end_parameter;
|
||||
} ps2x_iop_rpc_request_v1;
|
||||
|
||||
typedef struct ps2x_iop_rpc_result_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
uint32_t handled;
|
||||
uint32_t result_address;
|
||||
uint32_t signal_nowait_completion;
|
||||
uint32_t signal_completion;
|
||||
uint32_t callback_policy;
|
||||
uint32_t server_dispatch_policy;
|
||||
} ps2x_iop_rpc_result_v1;
|
||||
|
||||
typedef struct ps2x_iop_sif_transfer_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
uint32_t kind;
|
||||
uint32_t phase;
|
||||
uint32_t source_address;
|
||||
uint32_t destination_address;
|
||||
uint32_t size;
|
||||
} ps2x_iop_sif_transfer_v1;
|
||||
|
||||
typedef struct ps2x_iop_debug_metric_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
ps2x_iop_string_view_v1 name;
|
||||
uint64_t value;
|
||||
uint32_t hexadecimal;
|
||||
} ps2x_iop_debug_metric_v1;
|
||||
|
||||
typedef struct ps2x_iop_memory_card_request_v1
|
||||
{
|
||||
uint32_t struct_size;
|
||||
uint32_t operation;
|
||||
uint32_t arguments[5];
|
||||
} ps2x_iop_memory_card_request_v1;
|
||||
|
||||
typedef struct ps2x_iop_host_api_v1
|
||||
{
|
||||
uint32_t abi_version;
|
||||
uint32_t struct_size;
|
||||
void *userdata;
|
||||
|
||||
int32_t (*read_guest)(void *userdata, uint32_t address, void *destination, size_t size);
|
||||
int32_t (*write_guest)(void *userdata, uint32_t address, const void *source, size_t size);
|
||||
int32_t (*zero_guest)(void *userdata, uint32_t address, size_t size);
|
||||
int32_t (*normalize_guest_address)(void *userdata, uint32_t address, uint32_t *normalized);
|
||||
uint32_t (*allocate_iop_handle)(void *userdata, uint32_t kind);
|
||||
uint32_t (*allocate_guest)(void *userdata, uint32_t size, uint32_t alignment);
|
||||
void (*free_guest)(void *userdata, uint32_t address);
|
||||
|
||||
int32_t (*audio_command)(void *userdata,
|
||||
uint32_t sid,
|
||||
uint32_t function,
|
||||
ps2x_iop_guest_buffer_v1 send,
|
||||
ps2x_iop_guest_buffer_v1 receive);
|
||||
|
||||
int32_t (*get_host_path)(void *userdata,
|
||||
uint32_t kind,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *required_size);
|
||||
int32_t (*translate_guest_path)(void *userdata,
|
||||
ps2x_iop_string_view_v1 path,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *required_size);
|
||||
uint64_t (*open_host_file)(void *userdata, ps2x_iop_string_view_v1 path);
|
||||
int32_t (*host_file_size)(void *userdata, uint64_t handle, uint64_t *size);
|
||||
int32_t (*read_host_file)(void *userdata,
|
||||
uint64_t handle,
|
||||
uint64_t offset,
|
||||
void *destination,
|
||||
size_t size,
|
||||
size_t *bytes_read);
|
||||
void (*close_host_file)(void *userdata, uint64_t handle);
|
||||
|
||||
int32_t (*memory_card)(void *userdata, const ps2x_iop_memory_card_request_v1 *request, int32_t *result);
|
||||
|
||||
int32_t (*has_guest_function)(void *userdata, uint32_t address);
|
||||
int32_t (*invoke_guest_function)(void *userdata,
|
||||
uint64_t call_token,
|
||||
uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t *result_address);
|
||||
void (*log)(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message);
|
||||
} ps2x_iop_host_api_v1;
|
||||
|
||||
typedef void *(*ps2x_iop_profile_create_v1)(const ps2x_iop_host_api_v1 *host,
|
||||
const ps2x_iop_game_identity_v1 *identity);
|
||||
typedef void (*ps2x_iop_profile_destroy_v1)(void *instance);
|
||||
typedef int32_t (*ps2x_iop_profile_reset_v1)(void *instance);
|
||||
typedef uint32_t (*ps2x_iop_profile_select_rpc_abi_v1)(void *instance, const ps2x_iop_rpc_abi_request_v1 *request);
|
||||
typedef int32_t (*ps2x_iop_profile_handle_rpc_v1)(void *instance,
|
||||
const ps2x_iop_rpc_request_v1 *request,
|
||||
ps2x_iop_rpc_result_v1 *result);
|
||||
typedef int32_t (*ps2x_iop_profile_on_sif_transfer_v1)(void *instance, const ps2x_iop_sif_transfer_v1 *transfer);
|
||||
typedef size_t (*ps2x_iop_profile_debug_metric_count_v1)(void *instance);
|
||||
typedef int32_t (*ps2x_iop_profile_debug_metric_v1)(void *instance, size_t index, ps2x_iop_debug_metric_v1 *metric);
|
||||
|
||||
typedef struct ps2x_iop_profile_api_v1
|
||||
{
|
||||
uint32_t abi_version;
|
||||
uint32_t struct_size;
|
||||
ps2x_iop_string_view_v1 id;
|
||||
ps2x_iop_game_matcher_v1 matcher;
|
||||
size_t sid_count;
|
||||
const uint32_t *sids;
|
||||
ps2x_iop_profile_create_v1 create;
|
||||
ps2x_iop_profile_destroy_v1 destroy;
|
||||
ps2x_iop_profile_reset_v1 reset;
|
||||
ps2x_iop_profile_select_rpc_abi_v1 select_rpc_abi;
|
||||
ps2x_iop_profile_handle_rpc_v1 handle_rpc;
|
||||
ps2x_iop_profile_on_sif_transfer_v1 on_sif_transfer;
|
||||
ps2x_iop_profile_debug_metric_count_v1 debug_metric_count;
|
||||
ps2x_iop_profile_debug_metric_v1 debug_metric;
|
||||
} ps2x_iop_profile_api_v1;
|
||||
|
||||
typedef struct ps2x_iop_plugin_api_v1
|
||||
{
|
||||
uint32_t abi_version;
|
||||
uint32_t struct_size;
|
||||
ps2x_iop_string_view_v1 name;
|
||||
ps2x_iop_string_view_v1 version;
|
||||
size_t profile_count;
|
||||
const ps2x_iop_profile_api_v1 *profiles;
|
||||
} ps2x_iop_plugin_api_v1;
|
||||
|
||||
typedef int32_t (*ps2x_iop_query_v1_fn)(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api);
|
||||
|
||||
PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
enum class Ps2PathDevice
|
||||
{
|
||||
Invalid,
|
||||
Host,
|
||||
Cdrom,
|
||||
MemoryCard0,
|
||||
Rom0,
|
||||
NativeHost,
|
||||
};
|
||||
|
||||
struct ParsedPs2Path
|
||||
{
|
||||
Ps2PathDevice device = Ps2PathDevice::Invalid;
|
||||
std::string deviceName;
|
||||
std::string path;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const noexcept
|
||||
{
|
||||
return device != Ps2PathDevice::Invalid;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] ParsedPs2Path parsePs2Path(std::string_view path);
|
||||
|
||||
// Returns a lower-case module/file leaf without an optional .irx suffix.
|
||||
[[nodiscard]] std::string ps2PathLeafKey(const ParsedPs2Path &path);
|
||||
[[nodiscard]] std::string ps2PathLeafKey(std::string_view path);
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
#include "iop_service.h"
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
TsnddrvBindings recvxTsnddrvBindings()
|
||||
{
|
||||
return {
|
||||
.serviceName = "TSNDDRV",
|
||||
.protocol = TsnddrvProtocolVariant::SndQueueV1,
|
||||
.arena = {
|
||||
.base = 0x00120000u,
|
||||
.limit = 0x00200000u,
|
||||
.statusAlignment = 0x100u,
|
||||
.tableAlignment = 0x100u,
|
||||
.storageAlignment = 0x1000u,
|
||||
.hdBytes = 0x4000u,
|
||||
.sqBytes = 0x18000u,
|
||||
.dataBytes = 0x40000u,
|
||||
},
|
||||
.checksumCandidates = {
|
||||
{0x01E0EF10u, 0x01E0EF20u},
|
||||
{0x01E1EF10u, 0x01E1EF20u},
|
||||
},
|
||||
.busyFlagAddress = 0x01E212C8u,
|
||||
.completionRules = {
|
||||
{0x002EAC20u, true, true, false},
|
||||
{0x002EAC30u, true, true, true},
|
||||
{0x002FAC20u, true, true, false},
|
||||
{0x002FAC30u, true, true, true},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
CriDtxBindings recvxCriDtxBindings()
|
||||
{
|
||||
return {
|
||||
.serviceName = "CRI DTX",
|
||||
.sid = 0x7D000000u,
|
||||
.urpcObjectBase = 0x01F18000u,
|
||||
.urpcObjectLimit = 0x01F1FF00u,
|
||||
.urpcObjectStride = 0x20u,
|
||||
.urpcFunctionTableBase = 0x0033FED0u,
|
||||
.urpcObjectTableBase = 0x0033FFD0u,
|
||||
.dispatcherFunctionAddress = 0x002FABC0u,
|
||||
.rpcServerPoolBase = 0x01F10000u,
|
||||
.rpcServerStride = 0x80u,
|
||||
};
|
||||
}
|
||||
|
||||
ClFileBindings lotrClFileBindings()
|
||||
{
|
||||
return {
|
||||
.serviceName = "CLFILE",
|
||||
.sid = 0x0000FF01u,
|
||||
.rpc = {},
|
||||
};
|
||||
}
|
||||
|
||||
SoundUpdateStubBindings lotrSoundBindings()
|
||||
{
|
||||
return {
|
||||
.serviceName = "SOUND update compatibility stub",
|
||||
.sid = 0x00012345u,
|
||||
.activeStreamCountOffset = 0u,
|
||||
.responseCounterOffset = 4u,
|
||||
.zeroReceiveBuffer = true,
|
||||
.signalNowaitCompletion = true,
|
||||
.completeQueuedPlayStreams = true,
|
||||
.suppressedCompletionCallbacks = {},
|
||||
};
|
||||
}
|
||||
|
||||
SdrdrvBindings fatalFrameSdrdrvBindings()
|
||||
{
|
||||
return {
|
||||
.serviceName = "SDRDRV",
|
||||
.sid = 0x19740512u,
|
||||
.imageHeaderAddress = 0x012F0000u,
|
||||
.sectorSize = 2048u,
|
||||
.statusOffset = 0x6Cu,
|
||||
.statusStride = 8u,
|
||||
.statusSlotMask = 0x1Fu,
|
||||
.completeValue = 0u,
|
||||
.imageHeaderLowerName = "img_hd.bin",
|
||||
.imageHeaderUpperName = "IMG_HD.BIN",
|
||||
.imageBodyLowerName = "img_bd.bin",
|
||||
.imageBodyUpperName = "IMG_BD.BIN",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ServiceList createCoreServices(IopHost &host)
|
||||
{
|
||||
ServiceList services;
|
||||
services.emplace_back(createMcservService(host));
|
||||
services.emplace_back(createDbcmanService(host));
|
||||
services.emplace_back(createLibSdService(host));
|
||||
return services;
|
||||
}
|
||||
|
||||
std::vector<ProfileDefinition> createBuiltinProfiles()
|
||||
{
|
||||
std::vector<ProfileDefinition> profiles;
|
||||
|
||||
profiles.push_back({
|
||||
"recvx-us",
|
||||
"builtin",
|
||||
{.elfName = "slus_201.84"},
|
||||
[](IopHost &host, const GameIdentity &)
|
||||
{
|
||||
ServiceList services;
|
||||
services.emplace_back(createTsnddrvService(host, recvxTsnddrvBindings()));
|
||||
services.emplace_back(createCriDtxService(host, recvxCriDtxBindings()));
|
||||
return services;
|
||||
},
|
||||
});
|
||||
|
||||
profiles.push_back({
|
||||
"lotr-two-towers-us",
|
||||
"builtin",
|
||||
{.elfName = "SLUS_205.78"},
|
||||
[](IopHost &host, const GameIdentity &)
|
||||
{
|
||||
ServiceList services;
|
||||
services.emplace_back(createClFileService(host, lotrClFileBindings()));
|
||||
services.emplace_back(createSoundUpdateStubService(host, lotrSoundBindings()));
|
||||
return services;
|
||||
},
|
||||
});
|
||||
|
||||
profiles.push_back({
|
||||
"fatal-frame-us",
|
||||
"builtin",
|
||||
{.elfName = "SLUS_203.88"},
|
||||
[](IopHost &host, const GameIdentity &)
|
||||
{
|
||||
ServiceList services;
|
||||
services.emplace_back(createSdrdrvService(host, fatalFrameSdrdrvBindings()));
|
||||
return services;
|
||||
},
|
||||
});
|
||||
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
#include "iop_cpu.h"
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopCpuCore::IopCpuCore(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopCpuCore::writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg)
|
||||
{
|
||||
if (reg == 0u)
|
||||
return;
|
||||
cpu.gpr[reg] = value;
|
||||
writtenReg = reg;
|
||||
}
|
||||
|
||||
void IopCpuCore::scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue)
|
||||
{
|
||||
if (reg == 0u)
|
||||
return;
|
||||
scheduled = true;
|
||||
scheduledReg = reg;
|
||||
scheduledValue = value;
|
||||
}
|
||||
|
||||
void IopCpuCore::raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress) const
|
||||
{
|
||||
uint32_t cause = cpu.cop0[13] & ~0x7Cu;
|
||||
cause |= (code & 0x1Fu) << 2u;
|
||||
if (delaySlot)
|
||||
{
|
||||
cause |= 0x80000000u;
|
||||
cpu.cop0[14] = faultPc - 4u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cause &= ~0x80000000u;
|
||||
cpu.cop0[14] = faultPc;
|
||||
}
|
||||
cpu.cop0[13] = cause;
|
||||
if (badAddress)
|
||||
cpu.cop0[8] = *badAddress;
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
cpu.cop0[12] = (status & ~0x3Fu) | ((status << 2u) & 0x3Fu);
|
||||
cpu.pc = (status & (1u << 22u)) ? 0xBFC00180u : 0x80000080u;
|
||||
cpu.branchPending = false;
|
||||
cpu.pendingLoad = false;
|
||||
cpu.exception = true;
|
||||
}
|
||||
|
||||
bool IopCpuCore::executeInstruction(IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t pc = cpu.pc;
|
||||
const uint32_t instruction = m_memory.read32(pc);
|
||||
const bool wasDelaySlot = cpu.branchPending;
|
||||
const uint32_t priorBranchTarget = cpu.branchTarget;
|
||||
|
||||
cpu.branchPending = false;
|
||||
cpu.exception = false;
|
||||
cpu.yielded = false;
|
||||
|
||||
const uint32_t opcode = instruction >> 26u;
|
||||
const uint32_t rs = (instruction >> 21u) & 31u;
|
||||
const uint32_t rt = (instruction >> 16u) & 31u;
|
||||
const uint32_t rd = (instruction >> 11u) & 31u;
|
||||
const uint32_t sa = (instruction >> 6u) & 31u;
|
||||
const uint32_t funct = instruction & 63u;
|
||||
const uint32_t imm = instruction & 0xFFFFu;
|
||||
const int32_t simm = static_cast<int16_t>(imm);
|
||||
const uint32_t nextPc = pc + 4u;
|
||||
|
||||
uint32_t writtenReg = 0u;
|
||||
bool scheduledLoad = false;
|
||||
uint32_t scheduledReg = 0u;
|
||||
uint32_t scheduledValue = 0u;
|
||||
bool newBranch = false;
|
||||
uint32_t newBranchTarget = 0u;
|
||||
|
||||
auto branch = [&](bool condition)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
newBranch = true;
|
||||
newBranchTarget = nextPc + (static_cast<uint32_t>(simm) << 2u);
|
||||
}
|
||||
};
|
||||
auto write = [&](uint32_t reg, uint32_t value)
|
||||
{
|
||||
writeRegister(cpu, reg, value, writtenReg);
|
||||
};
|
||||
auto load = [&](uint32_t reg, uint32_t value)
|
||||
{
|
||||
scheduleLoad(reg, value, scheduledLoad, scheduledReg, scheduledValue);
|
||||
};
|
||||
auto overflowAdd = [&](int32_t lhs, int32_t rhs, uint32_t reg)
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(lhs) + rhs;
|
||||
if (result > std::numeric_limits<int32_t>::max() || result < std::numeric_limits<int32_t>::min())
|
||||
raiseException(cpu, 12u, pc, wasDelaySlot);
|
||||
else
|
||||
write(reg, static_cast<uint32_t>(static_cast<int32_t>(result)));
|
||||
};
|
||||
auto overflowSub = [&](int32_t lhs, int32_t rhs, uint32_t reg)
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(lhs) - rhs;
|
||||
if (result > std::numeric_limits<int32_t>::max() || result < std::numeric_limits<int32_t>::min())
|
||||
raiseException(cpu, 12u, pc, wasDelaySlot);
|
||||
else
|
||||
write(reg, static_cast<uint32_t>(static_cast<int32_t>(result)));
|
||||
};
|
||||
|
||||
// TODO kill this magic number and make it a constant somewhere
|
||||
switch (opcode)
|
||||
{
|
||||
case 0x00:
|
||||
switch (funct)
|
||||
{
|
||||
case 0x00:
|
||||
write(rd, cpu.gpr[rt] << sa);
|
||||
break;
|
||||
case 0x02:
|
||||
write(rd, cpu.gpr[rt] >> sa);
|
||||
break;
|
||||
case 0x03:
|
||||
write(rd, static_cast<uint32_t>(static_cast<int32_t>(cpu.gpr[rt]) >> sa));
|
||||
break;
|
||||
case 0x04:
|
||||
write(rd, cpu.gpr[rt] << (cpu.gpr[rs] & 31u));
|
||||
break;
|
||||
case 0x06:
|
||||
write(rd, cpu.gpr[rt] >> (cpu.gpr[rs] & 31u));
|
||||
break;
|
||||
case 0x07:
|
||||
write(rd, static_cast<uint32_t>(static_cast<int32_t>(cpu.gpr[rt]) >> (cpu.gpr[rs] & 31u)));
|
||||
break;
|
||||
case 0x08:
|
||||
newBranch = true;
|
||||
newBranchTarget = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x09:
|
||||
write(rd ? rd : 31u, pc + 8u);
|
||||
newBranch = true;
|
||||
newBranchTarget = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x0C:
|
||||
raiseException(cpu, 8u, pc, wasDelaySlot);
|
||||
break;
|
||||
case 0x0D:
|
||||
raiseException(cpu, 9u, pc, wasDelaySlot);
|
||||
break;
|
||||
case 0x10:
|
||||
write(rd, cpu.hi);
|
||||
break;
|
||||
case 0x11:
|
||||
cpu.hi = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x12:
|
||||
write(rd, cpu.lo);
|
||||
break;
|
||||
case 0x13:
|
||||
cpu.lo = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x18:
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(static_cast<int32_t>(cpu.gpr[rs])) * static_cast<int64_t>(static_cast<int32_t>(cpu.gpr[rt]));
|
||||
cpu.lo = static_cast<uint32_t>(result);
|
||||
cpu.hi = static_cast<uint32_t>(static_cast<uint64_t>(result) >> 32u);
|
||||
break;
|
||||
}
|
||||
case 0x19:
|
||||
{
|
||||
const uint64_t result = static_cast<uint64_t>(cpu.gpr[rs]) * cpu.gpr[rt];
|
||||
cpu.lo = static_cast<uint32_t>(result);
|
||||
cpu.hi = static_cast<uint32_t>(result >> 32u);
|
||||
break;
|
||||
}
|
||||
case 0x1A:
|
||||
{
|
||||
const int32_t lhs = static_cast<int32_t>(cpu.gpr[rs]);
|
||||
const int32_t rhs = static_cast<int32_t>(cpu.gpr[rt]);
|
||||
if (rhs == 0)
|
||||
{
|
||||
cpu.lo = lhs >= 0 ? 0xFFFFFFFFu : 1u;
|
||||
cpu.hi = static_cast<uint32_t>(lhs);
|
||||
}
|
||||
else if (lhs == std::numeric_limits<int32_t>::min() && rhs == -1)
|
||||
{
|
||||
cpu.lo = static_cast<uint32_t>(lhs);
|
||||
cpu.hi = 0u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.lo = static_cast<uint32_t>(lhs / rhs);
|
||||
cpu.hi = static_cast<uint32_t>(lhs % rhs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x1B:
|
||||
if (cpu.gpr[rt] == 0u)
|
||||
{
|
||||
cpu.lo = 0xFFFFFFFFu;
|
||||
cpu.hi = cpu.gpr[rs];
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.lo = cpu.gpr[rs] / cpu.gpr[rt];
|
||||
cpu.hi = cpu.gpr[rs] % cpu.gpr[rt];
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
overflowAdd(static_cast<int32_t>(cpu.gpr[rs]), static_cast<int32_t>(cpu.gpr[rt]), rd);
|
||||
break;
|
||||
case 0x21:
|
||||
write(rd, cpu.gpr[rs] + cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x22:
|
||||
overflowSub(static_cast<int32_t>(cpu.gpr[rs]), static_cast<int32_t>(cpu.gpr[rt]), rd);
|
||||
break;
|
||||
case 0x23:
|
||||
write(rd, cpu.gpr[rs] - cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x24:
|
||||
write(rd, cpu.gpr[rs] & cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x25:
|
||||
write(rd, cpu.gpr[rs] | cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x26:
|
||||
write(rd, cpu.gpr[rs] ^ cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x27:
|
||||
write(rd, ~(cpu.gpr[rs] | cpu.gpr[rt]));
|
||||
break;
|
||||
case 0x2A:
|
||||
write(rd, static_cast<int32_t>(cpu.gpr[rs]) < static_cast<int32_t>(cpu.gpr[rt]) ? 1u : 0u);
|
||||
break;
|
||||
case 0x2B:
|
||||
write(rd, cpu.gpr[rs] < cpu.gpr[rt] ? 1u : 0u);
|
||||
break;
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x01:
|
||||
switch (rt)
|
||||
{
|
||||
case 0x00:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) < 0);
|
||||
break;
|
||||
case 0x01:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) >= 0);
|
||||
break;
|
||||
case 0x10:
|
||||
write(31u, pc + 8u);
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) < 0);
|
||||
break;
|
||||
case 0x11:
|
||||
write(31u, pc + 8u);
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) >= 0);
|
||||
break;
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x02:
|
||||
newBranch = true;
|
||||
newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u);
|
||||
break;
|
||||
case 0x03:
|
||||
write(31u, pc + 8u);
|
||||
newBranch = true;
|
||||
newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u);
|
||||
break;
|
||||
case 0x04:
|
||||
branch(cpu.gpr[rs] == cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x05:
|
||||
branch(cpu.gpr[rs] != cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x06:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) <= 0);
|
||||
break;
|
||||
case 0x07:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) > 0);
|
||||
break;
|
||||
case 0x08:
|
||||
overflowAdd(static_cast<int32_t>(cpu.gpr[rs]), simm, rt);
|
||||
break;
|
||||
case 0x09:
|
||||
write(rt, cpu.gpr[rs] + static_cast<uint32_t>(simm));
|
||||
break;
|
||||
case 0x0A:
|
||||
write(rt, static_cast<int32_t>(cpu.gpr[rs]) < simm ? 1u : 0u);
|
||||
break;
|
||||
case 0x0B:
|
||||
write(rt, cpu.gpr[rs] < static_cast<uint32_t>(simm) ? 1u : 0u);
|
||||
break;
|
||||
case 0x0C:
|
||||
write(rt, cpu.gpr[rs] & imm);
|
||||
break;
|
||||
case 0x0D:
|
||||
write(rt, cpu.gpr[rs] | imm);
|
||||
break;
|
||||
case 0x0E:
|
||||
write(rt, cpu.gpr[rs] ^ imm);
|
||||
break;
|
||||
case 0x0F:
|
||||
write(rt, imm << 16u);
|
||||
break;
|
||||
case 0x10:
|
||||
{
|
||||
const uint32_t copRs = rs;
|
||||
if (copRs == 0x00)
|
||||
load(rt, cpu.cop0[rd]);
|
||||
else if (copRs == 0x04)
|
||||
cpu.cop0[rd] = cpu.gpr[rt];
|
||||
else if (copRs == 0x10 && funct == 0x10)
|
||||
{
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
cpu.cop0[12] = (status & ~0x0Fu) | ((status >> 2u) & 0x0Fu);
|
||||
}
|
||||
else
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
case 0x20:
|
||||
case 0x24:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint8_t value = m_memory.read8(address);
|
||||
load(rt, opcode == 0x20
|
||||
? static_cast<uint32_t>(static_cast<int32_t>(static_cast<int8_t>(value)))
|
||||
: value);
|
||||
break;
|
||||
}
|
||||
case 0x21:
|
||||
case 0x25:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 1u)
|
||||
{
|
||||
raiseException(cpu, 4u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
const uint16_t value = m_memory.read16(address);
|
||||
load(rt, opcode == 0x21 ? static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(value))) : value);
|
||||
break;
|
||||
}
|
||||
case 0x22:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t memory = m_memory.read32(address & ~3u);
|
||||
const uint32_t old = cpu.gpr[rt];
|
||||
static constexpr uint32_t masks[4] = {0x00FFFFFFu, 0x0000FFFFu, 0x000000FFu, 0x00000000u};
|
||||
static constexpr uint32_t shifts[4] = {24u, 16u, 8u, 0u};
|
||||
load(rt, (old & masks[address & 3u]) | (memory << shifts[address & 3u]));
|
||||
break;
|
||||
}
|
||||
case 0x23:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 3u)
|
||||
{
|
||||
raiseException(cpu, 4u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
load(rt, m_memory.read32(address));
|
||||
break;
|
||||
}
|
||||
case 0x26:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t memory = m_memory.read32(address & ~3u);
|
||||
const uint32_t old = cpu.gpr[rt];
|
||||
static constexpr uint32_t masks[4] = {0x00000000u, 0xFF000000u, 0xFFFF0000u, 0xFFFFFF00u};
|
||||
static constexpr uint32_t shifts[4] = {0u, 8u, 16u, 24u};
|
||||
load(rt, (old & masks[address & 3u]) | (memory >> shifts[address & 3u]));
|
||||
break;
|
||||
}
|
||||
case 0x28:
|
||||
m_memory.write8(cpu.gpr[rs] + static_cast<uint32_t>(simm), static_cast<uint8_t>(cpu.gpr[rt]));
|
||||
break;
|
||||
case 0x29:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 1u)
|
||||
{
|
||||
raiseException(cpu, 5u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
m_memory.write16(address, static_cast<uint16_t>(cpu.gpr[rt]));
|
||||
break;
|
||||
}
|
||||
case 0x2A:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t aligned = address & ~3u;
|
||||
const uint32_t old = m_memory.read32(aligned);
|
||||
const uint32_t value = cpu.gpr[rt];
|
||||
uint32_t result = old;
|
||||
switch (address & 3u)
|
||||
{
|
||||
case 0u:
|
||||
result = (old & 0xFFFFFF00u) | (value >> 24u);
|
||||
break;
|
||||
case 1u:
|
||||
result = (old & 0xFFFF0000u) | (value >> 16u);
|
||||
break;
|
||||
case 2u:
|
||||
result = (old & 0xFF000000u) | (value >> 8u);
|
||||
break;
|
||||
case 3u:
|
||||
result = value;
|
||||
break;
|
||||
}
|
||||
m_memory.write32(aligned, result);
|
||||
break;
|
||||
}
|
||||
case 0x2B:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 3u)
|
||||
{
|
||||
raiseException(cpu, 5u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
m_memory.write32(address, cpu.gpr[rt]);
|
||||
break;
|
||||
}
|
||||
case 0x2E:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t aligned = address & ~3u;
|
||||
const uint32_t old = m_memory.read32(aligned);
|
||||
const uint32_t value = cpu.gpr[rt];
|
||||
uint32_t result = old;
|
||||
switch (address & 3u)
|
||||
{
|
||||
case 0u:
|
||||
result = value;
|
||||
break;
|
||||
case 1u:
|
||||
result = (old & 0x000000FFu) | (value << 8u);
|
||||
break;
|
||||
case 2u:
|
||||
result = (old & 0x0000FFFFu) | (value << 16u);
|
||||
break;
|
||||
case 3u:
|
||||
result = (old & 0x00FFFFFFu) | (value << 24u);
|
||||
break;
|
||||
}
|
||||
m_memory.write32(aligned, result);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
|
||||
cpu.gpr[0] = 0u;
|
||||
if (cpu.exception)
|
||||
return !cpu.stopped;
|
||||
|
||||
if (cpu.pendingLoad)
|
||||
{
|
||||
if (cpu.pendingLoadReg != 0u && cpu.pendingLoadReg != writtenReg)
|
||||
cpu.gpr[cpu.pendingLoadReg] = cpu.pendingLoadValue;
|
||||
cpu.pendingLoad = false;
|
||||
}
|
||||
if (scheduledLoad)
|
||||
{
|
||||
cpu.pendingLoad = true;
|
||||
cpu.pendingLoadReg = scheduledReg;
|
||||
cpu.pendingLoadValue = scheduledValue;
|
||||
}
|
||||
cpu.gpr[0] = 0u;
|
||||
|
||||
if (wasDelaySlot)
|
||||
{
|
||||
cpu.pc = priorBranchTarget;
|
||||
cpu.branchPending = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.pc = nextPc;
|
||||
cpu.branchPending = newBranch;
|
||||
cpu.branchTarget = newBranchTarget;
|
||||
}
|
||||
return !cpu.stopped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
struct IopCpuState
|
||||
{
|
||||
std::array<uint32_t, 32> gpr{};
|
||||
uint32_t hi = 0;
|
||||
uint32_t lo = 0;
|
||||
uint32_t pc = 0;
|
||||
std::array<uint32_t, 32> cop0{};
|
||||
uint32_t pendingLoadReg = 0;
|
||||
uint32_t pendingLoadValue = 0;
|
||||
bool pendingLoad = false;
|
||||
bool branchPending = false;
|
||||
uint32_t branchTarget = 0;
|
||||
bool stopped = false;
|
||||
bool yielded = false;
|
||||
bool exception = false;
|
||||
};
|
||||
|
||||
class IopCpuCore
|
||||
{
|
||||
public:
|
||||
explicit IopCpuCore(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool executeInstruction(IopCpuState &cpu);
|
||||
void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress = std::nullopt) const;
|
||||
|
||||
private:
|
||||
static void writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg);
|
||||
static void scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue);
|
||||
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,744 @@
|
||||
#include "iop_kernel.h"
|
||||
|
||||
#include "iop_memory.h"
|
||||
#include "../iop_emulator_const.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment <= 1u)
|
||||
return value;
|
||||
const uint32_t mask = alignment - 1u;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
IopKernel::IopKernel(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopKernel::reset()
|
||||
{
|
||||
m_threads.clear();
|
||||
m_semaphores.clear();
|
||||
m_eventFlags.clear();
|
||||
m_nextThreadId = 1;
|
||||
m_nextSemaphoreId = 1;
|
||||
m_nextEventFlagId = 1;
|
||||
m_currentThread = nullptr;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // CreateThread
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
IopThread thread;
|
||||
thread.id = static_cast<int>(m_nextThreadId++);
|
||||
thread.attr = m_memory.read32(descriptor + 0u);
|
||||
thread.option = m_memory.read32(descriptor + 4u);
|
||||
thread.entry = m_memory.read32(descriptor + 8u);
|
||||
thread.stackSize = std::max<uint32_t>(m_memory.read32(descriptor + 12u), 0x100u);
|
||||
thread.priority = std::clamp<uint32_t>(m_memory.read32(descriptor + 16u), 1u, 126u);
|
||||
thread.initialPriority = thread.priority;
|
||||
thread.stackBase = m_memory.allocate(thread.stackSize + kStackGuardBytes, 16u);
|
||||
if (thread.stackBase == 0u)
|
||||
{
|
||||
setV0(-400);
|
||||
return true;
|
||||
}
|
||||
const int id = thread.id;
|
||||
m_threads.emplace(id, std::move(thread));
|
||||
setV0(id);
|
||||
return true;
|
||||
}
|
||||
case 5: // DeleteThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.stackBase != 0u)
|
||||
(void)m_memory.freeAllocation(it->second.stackBase);
|
||||
m_threads.erase(it);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 6: // StartThread
|
||||
case 7: // StartThreadArgs
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
IopThread &thread = it->second;
|
||||
thread.cpu = {};
|
||||
thread.cpu.pc = thread.entry;
|
||||
thread.cpu.gpr[4] = cpu.gpr[5];
|
||||
thread.cpu.gpr[5] = ordinal == 7 ? cpu.gpr[6] : 0u;
|
||||
thread.cpu.gpr[28] = cpu.gpr[28];
|
||||
thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u;
|
||||
thread.cpu.gpr[31] = kThreadReturnSentinel;
|
||||
thread.state = IopThreadState::Ready;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8: // ExitThread
|
||||
case 9: // ExitDeleteThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
m_currentThread->state = ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant;
|
||||
cpu.stopped = true;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 10:
|
||||
case 11: // TerminateThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.state = IopThreadState::Dormant;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 12:
|
||||
case 13:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 14:
|
||||
case 15:
|
||||
{
|
||||
int id = static_cast<int>(cpu.gpr[4]);
|
||||
if (id == 0 && m_currentThread != nullptr)
|
||||
id = m_currentThread->id;
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.priority = std::clamp<uint32_t>(cpu.gpr[5], 1u, 126u);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 16:
|
||||
case 17:
|
||||
setV0(0);
|
||||
cpu.yielded = true;
|
||||
return true;
|
||||
case 18:
|
||||
case 19:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Sleep ||
|
||||
it->second.state == IopThreadState::Delay ||
|
||||
it->second.state == IopThreadState::Semaphore ||
|
||||
it->second.state == IopThreadState::EventFlag)
|
||||
{
|
||||
it->second.state = IopThreadState::Ready;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 20:
|
||||
setV0(m_currentThread != nullptr ? m_currentThread->id : 0);
|
||||
return true;
|
||||
case 21:
|
||||
setV0(0x1000);
|
||||
return true;
|
||||
case 22:
|
||||
case 23:
|
||||
setV0(referThreadStatus(static_cast<int>(cpu.gpr[4]), cpu.gpr[5]) ? 0 : -1);
|
||||
return true;
|
||||
case 24: // SleepThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
if (m_currentThread->wakeupCount > 0)
|
||||
--m_currentThread->wakeupCount;
|
||||
else
|
||||
sleepCurrent(cpu);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 25:
|
||||
case 26:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Sleep)
|
||||
it->second.state = IopThreadState::Ready;
|
||||
else
|
||||
++it->second.wakeupCount;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 27:
|
||||
case 28:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const int old = it->second.wakeupCount;
|
||||
it->second.wakeupCount = 0;
|
||||
setV0(old);
|
||||
return true;
|
||||
}
|
||||
case 29:
|
||||
case 30: // SuspendThread / iSuspendThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.state = IopThreadState::Suspended;
|
||||
if (m_currentThread == &it->second)
|
||||
cpu.yielded = true;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 31:
|
||||
case 32: // ResumeThread / iResumeThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Suspended)
|
||||
it->second.state = IopThreadState::Ready;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 33: // DelayThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
const uint64_t delayCycles = (static_cast<uint64_t>(cpu.gpr[4]) * kIopClockHz + 999'999ull) / 1'000'000ull;
|
||||
delayCurrentUntil(currentCycle + std::max<uint64_t>(delayCycles, 1u), cpu);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 34: // GetSystemTime
|
||||
m_memory.write32(cpu.gpr[4], static_cast<uint32_t>(currentCycle));
|
||||
m_memory.write32(cpu.gpr[4] + 4u, static_cast<uint32_t>(currentCycle >> 32u));
|
||||
setV0(0);
|
||||
return true;
|
||||
case 35:
|
||||
case 36:
|
||||
case 37:
|
||||
case 38:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 39: // USec2SysClock
|
||||
{
|
||||
const uint64_t cycles = (static_cast<uint64_t>(cpu.gpr[4]) * kIopClockHz) / 1'000'000ull;
|
||||
m_memory.write32(cpu.gpr[5], static_cast<uint32_t>(cycles));
|
||||
m_memory.write32(cpu.gpr[5] + 4u, static_cast<uint32_t>(cycles >> 32u));
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 40:
|
||||
{
|
||||
const uint64_t cycles = static_cast<uint64_t>(m_memory.read32(cpu.gpr[4])) | (static_cast<uint64_t>(m_memory.read32(cpu.gpr[4] + 4u)) << 32u);
|
||||
const uint64_t usec = (cycles * 1'000'000ull) / kIopClockHz;
|
||||
if (cpu.gpr[5] != 0u)
|
||||
m_memory.write32(cpu.gpr[5], static_cast<uint32_t>(usec / 1'000'000ull));
|
||||
if (cpu.gpr[6] != 0u)
|
||||
m_memory.write32(cpu.gpr[6], static_cast<uint32_t>(usec % 1'000'000ull));
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 41:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::referThreadStatus(int id, uint32_t outputAddress)
|
||||
{
|
||||
if (id == 0 && m_currentThread != nullptr)
|
||||
id = m_currentThread->id;
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end() || outputAddress == 0u)
|
||||
return false;
|
||||
|
||||
const IopThread &thread = it->second;
|
||||
uint32_t status = 0x10u;
|
||||
switch (thread.state)
|
||||
{
|
||||
case IopThreadState::Running:
|
||||
status = 0x01u;
|
||||
break;
|
||||
case IopThreadState::Ready:
|
||||
status = 0x02u;
|
||||
break;
|
||||
case IopThreadState::Sleep:
|
||||
case IopThreadState::Delay:
|
||||
case IopThreadState::Semaphore:
|
||||
case IopThreadState::EventFlag:
|
||||
status = 0x04u;
|
||||
break;
|
||||
case IopThreadState::Suspended:
|
||||
status = 0x08u;
|
||||
break;
|
||||
default:
|
||||
status = 0x10u;
|
||||
break;
|
||||
}
|
||||
|
||||
m_memory.write32(outputAddress + 0u, thread.attr);
|
||||
m_memory.write32(outputAddress + 4u, thread.option);
|
||||
m_memory.write32(outputAddress + 8u, status);
|
||||
m_memory.write32(outputAddress + 12u, thread.entry);
|
||||
m_memory.write32(outputAddress + 16u, thread.stackBase);
|
||||
m_memory.write32(outputAddress + 20u, thread.stackSize);
|
||||
m_memory.write32(outputAddress + 24u, thread.cpu.gpr[28]);
|
||||
m_memory.write32(outputAddress + 28u, thread.initialPriority);
|
||||
m_memory.write32(outputAddress + 32u, thread.priority);
|
||||
m_memory.write32(outputAddress + 36u, thread.state == IopThreadState::Sleep ? 1u : thread.state == IopThreadState::Delay ? 2u
|
||||
: thread.state == IopThreadState::Semaphore ? 3u
|
||||
: thread.state == IopThreadState::EventFlag ? 4u
|
||||
: 0u);
|
||||
m_memory.write32(outputAddress + 40u, static_cast<uint32_t>(thread.waitId));
|
||||
m_memory.write32(outputAddress + 44u, static_cast<uint32_t>(thread.wakeupCount));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4:
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
Semaphore semaphore;
|
||||
semaphore.id = static_cast<int>(m_nextSemaphoreId++);
|
||||
semaphore.attr = m_memory.read32(descriptor + 0u);
|
||||
semaphore.option = m_memory.read32(descriptor + 4u);
|
||||
semaphore.current = static_cast<int>(m_memory.read32(descriptor + 8u));
|
||||
semaphore.maximum = std::max(1, static_cast<int>(m_memory.read32(descriptor + 12u)));
|
||||
m_semaphores.emplace(semaphore.id, semaphore);
|
||||
setV0(semaphore.id);
|
||||
return true;
|
||||
}
|
||||
case 5:
|
||||
setV0(m_semaphores.erase(static_cast<int>(cpu.gpr[4])) != 0u ? 0 : -1);
|
||||
return true;
|
||||
case 6:
|
||||
case 7:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.current < it->second.maximum)
|
||||
++it->second.current;
|
||||
wakeOneSemaphore(id);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8:
|
||||
case 9:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.current > 0)
|
||||
{
|
||||
--it->second.current;
|
||||
setV0(0);
|
||||
}
|
||||
else if (ordinal == 9)
|
||||
setV0(-419);
|
||||
else if (m_currentThread != nullptr)
|
||||
{
|
||||
m_currentThread->state = IopThreadState::Semaphore;
|
||||
m_currentThread->waitId = id;
|
||||
cpu.yielded = true;
|
||||
setV0(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case 11:
|
||||
case 12:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const uint32_t outputAddress = cpu.gpr[5];
|
||||
if (outputAddress != 0u)
|
||||
{
|
||||
m_memory.write32(outputAddress + 0u, it->second.attr);
|
||||
m_memory.write32(outputAddress + 4u, it->second.option);
|
||||
m_memory.write32(outputAddress + 8u, 0u);
|
||||
m_memory.write32(outputAddress + 12u, static_cast<uint32_t>(it->second.maximum));
|
||||
m_memory.write32(outputAddress + 16u, static_cast<uint32_t>(it->second.current));
|
||||
uint32_t waiters = 0u;
|
||||
for (const auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Semaphore && thread.waitId == id)
|
||||
++waiters;
|
||||
}
|
||||
m_memory.write32(outputAddress + 20u, waiters);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::wakeOneSemaphore(int id)
|
||||
{
|
||||
IopThread *best = nullptr;
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::Semaphore || thread.waitId != id)
|
||||
continue;
|
||||
if (best == nullptr || thread.priority < best->priority)
|
||||
best = &thread;
|
||||
}
|
||||
|
||||
const auto semaphore = m_semaphores.find(id);
|
||||
if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0)
|
||||
{
|
||||
--semaphore->second.current;
|
||||
best->state = IopThreadState::Ready;
|
||||
best->waitId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode)
|
||||
{
|
||||
if (bits == 0u)
|
||||
return false;
|
||||
return (mode & 1u) != 0u ? (event.bits & bits) != 0u : (event.bits & bits) == bits;
|
||||
}
|
||||
|
||||
void IopKernel::wakeEventWaiters(EventFlag &event)
|
||||
{
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::EventFlag || thread.waitId != event.id)
|
||||
continue;
|
||||
if (!eventSatisfied(event, thread.waitBits, thread.waitMode))
|
||||
continue;
|
||||
|
||||
if (thread.waitResultAddress != 0u)
|
||||
m_memory.write32(thread.waitResultAddress, event.bits);
|
||||
thread.cpu.gpr[2] = 0u;
|
||||
if ((thread.waitMode & 0x10u) != 0u)
|
||||
event.bits = 0u;
|
||||
thread.state = IopThreadState::Ready;
|
||||
thread.waitId = 0;
|
||||
thread.waitBits = 0;
|
||||
thread.waitMode = 0;
|
||||
thread.waitResultAddress = 0;
|
||||
if (event.bits == 0u)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int IopKernel::createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits)
|
||||
{
|
||||
EventFlag event;
|
||||
event.id = static_cast<int>(m_nextEventFlagId++);
|
||||
event.attr = attr;
|
||||
event.option = option;
|
||||
event.bits = bits;
|
||||
const int id = event.id;
|
||||
m_eventFlags.emplace(id, event);
|
||||
return id;
|
||||
}
|
||||
|
||||
bool IopKernel::setInternalEventFlag(int id, uint32_t bits)
|
||||
{
|
||||
const auto event = m_eventFlags.find(id);
|
||||
if (event == m_eventFlags.end())
|
||||
return false;
|
||||
event->second.bits |= bits;
|
||||
wakeEventWaiters(event->second);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchEventImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4:
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
setV0(createInternalEventFlag(m_memory.read32(descriptor + 0u),
|
||||
m_memory.read32(descriptor + 4u),
|
||||
m_memory.read32(descriptor + 8u)));
|
||||
return true;
|
||||
}
|
||||
case 5:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
if (m_eventFlags.erase(id) == 0u)
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == id)
|
||||
{
|
||||
thread.state = IopThreadState::Ready;
|
||||
thread.waitId = 0;
|
||||
thread.cpu.gpr[2] = static_cast<uint32_t>(-1);
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 6:
|
||||
case 7:
|
||||
{
|
||||
if (!setInternalEventFlag(static_cast<int>(cpu.gpr[4]), cpu.gpr[5]))
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8:
|
||||
case 9:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
// IOP ClearEventFlag applies a mask: callers pass ~bitsToClear.
|
||||
event->second.bits &= cpu.gpr[5];
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 10: // WaitEventFlag
|
||||
case 11: // PollEventFlag
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const uint32_t bits = cpu.gpr[5];
|
||||
const uint32_t mode = cpu.gpr[6];
|
||||
if (eventSatisfied(event->second, bits, mode))
|
||||
{
|
||||
if (cpu.gpr[7] != 0u)
|
||||
m_memory.write32(cpu.gpr[7], event->second.bits);
|
||||
if ((mode & 0x10u) != 0u)
|
||||
event->second.bits = 0u;
|
||||
setV0(0);
|
||||
}
|
||||
else if (ordinal == 11)
|
||||
setV0(-418);
|
||||
else if (m_currentThread != nullptr)
|
||||
{
|
||||
m_currentThread->state = IopThreadState::EventFlag;
|
||||
m_currentThread->waitId = event->second.id;
|
||||
m_currentThread->waitBits = bits;
|
||||
m_currentThread->waitMode = mode;
|
||||
m_currentThread->waitResultAddress = cpu.gpr[7];
|
||||
setV0(0);
|
||||
cpu.yielded = true;
|
||||
}
|
||||
else
|
||||
setV0(-418);
|
||||
return true;
|
||||
}
|
||||
case 13:
|
||||
case 14:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (cpu.gpr[5] != 0u)
|
||||
{
|
||||
uint32_t waiters = 0u;
|
||||
for (const auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == event->second.id)
|
||||
++waiters;
|
||||
}
|
||||
m_memory.write32(cpu.gpr[5] + 0u, event->second.attr);
|
||||
m_memory.write32(cpu.gpr[5] + 4u, event->second.option);
|
||||
m_memory.write32(cpu.gpr[5] + 8u, event->second.bits);
|
||||
m_memory.write32(cpu.gpr[5] + 12u, event->second.bits);
|
||||
m_memory.write32(cpu.gpr[5] + 16u, waiters);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::sleepCurrent(IopCpuState &cpu)
|
||||
{
|
||||
if (m_currentThread == nullptr)
|
||||
return;
|
||||
m_currentThread->state = IopThreadState::Sleep;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
|
||||
void IopKernel::delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu)
|
||||
{
|
||||
if (m_currentThread == nullptr)
|
||||
return;
|
||||
m_currentThread->wakeCycle = wakeCycle;
|
||||
m_currentThread->state = IopThreadState::Delay;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
|
||||
IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
|
||||
{
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Delay && thread.wakeCycle <= currentCycle)
|
||||
thread.state = IopThreadState::Ready;
|
||||
}
|
||||
|
||||
IopThread *next = nullptr;
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::Ready)
|
||||
continue;
|
||||
if (next == nullptr || thread.priority < next->priority ||
|
||||
(thread.priority == next->priority && thread.id < next->id))
|
||||
next = &thread;
|
||||
}
|
||||
if (next == nullptr)
|
||||
return nullptr;
|
||||
|
||||
m_currentThread = next;
|
||||
next->state = IopThreadState::Running;
|
||||
next->cpu.stopped = false;
|
||||
next->cpu.yielded = false;
|
||||
return next;
|
||||
}
|
||||
|
||||
uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const
|
||||
{
|
||||
uint64_t nextWake = fallback;
|
||||
for (const auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Delay)
|
||||
nextWake = std::min(nextWake, thread.wakeCycle);
|
||||
}
|
||||
return nextWake;
|
||||
}
|
||||
|
||||
void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
|
||||
{
|
||||
if (thread.cpu.pc == returnSentinel || thread.cpu.stopped)
|
||||
thread.state = IopThreadState::Dormant;
|
||||
else if (thread.state == IopThreadState::Running)
|
||||
thread.state = IopThreadState::Ready;
|
||||
m_currentThread = nullptr;
|
||||
cleanupDeadThreads();
|
||||
}
|
||||
|
||||
void IopKernel::cleanupDeadThreads()
|
||||
{
|
||||
for (auto thread = m_threads.begin(); thread != m_threads.end();)
|
||||
{
|
||||
if (thread->second.state != IopThreadState::Dead)
|
||||
{
|
||||
++thread;
|
||||
continue;
|
||||
}
|
||||
if (thread->second.stackBase != 0u)
|
||||
(void)m_memory.freeAllocation(thread->second.stackBase);
|
||||
thread = m_threads.erase(thread);
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::terminateThreadsInRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
|
||||
if (pc >= base && pc < base + size)
|
||||
thread.state = IopThreadState::Dead;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include "iop_cpu.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
enum class IopThreadState : uint8_t
|
||||
{
|
||||
Dormant,
|
||||
Ready,
|
||||
Running,
|
||||
Sleep,
|
||||
Delay,
|
||||
Semaphore,
|
||||
EventFlag,
|
||||
Suspended,
|
||||
Dead,
|
||||
};
|
||||
|
||||
struct IopThread
|
||||
{
|
||||
int id = 0;
|
||||
IopThreadState state = IopThreadState::Dormant;
|
||||
IopCpuState cpu;
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackBase = 0;
|
||||
uint32_t stackSize = 0;
|
||||
uint32_t priority = 0x40;
|
||||
uint32_t initialPriority = 0x40;
|
||||
uint32_t option = 0;
|
||||
uint32_t attr = 0;
|
||||
uint64_t wakeCycle = 0;
|
||||
int waitId = 0;
|
||||
uint32_t waitBits = 0;
|
||||
uint32_t waitMode = 0;
|
||||
uint32_t waitResultAddress = 0;
|
||||
int wakeupCount = 0;
|
||||
};
|
||||
|
||||
class IopKernel
|
||||
{
|
||||
public:
|
||||
explicit IopKernel(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] bool dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
[[nodiscard]] bool dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] bool dispatchEventImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
[[nodiscard]] int createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits);
|
||||
[[nodiscard]] bool setInternalEventFlag(int id, uint32_t bits);
|
||||
|
||||
void sleepCurrent(IopCpuState &cpu);
|
||||
void delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu);
|
||||
|
||||
[[nodiscard]] IopThread *beginNextReady(uint64_t currentCycle);
|
||||
[[nodiscard]] uint64_t nextWakeCycle(uint64_t fallback) const;
|
||||
void endTimeslice(IopThread &thread, uint32_t returnSentinel);
|
||||
void cleanupDeadThreads();
|
||||
void terminateThreadsInRange(uint32_t base, uint32_t size);
|
||||
|
||||
[[nodiscard]] size_t threadCount() const noexcept { return m_threads.size(); }
|
||||
|
||||
private:
|
||||
struct Semaphore
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
int current = 0;
|
||||
int maximum = 1;
|
||||
};
|
||||
|
||||
struct EventFlag
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t bits = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool referThreadStatus(int id, uint32_t outputAddress);
|
||||
void wakeOneSemaphore(int id);
|
||||
[[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode);
|
||||
void wakeEventWaiters(EventFlag &event);
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<int, IopThread> m_threads;
|
||||
std::map<int, Semaphore> m_semaphores;
|
||||
std::map<int, EventFlag> m_eventFlags;
|
||||
uint32_t m_nextThreadId = 1;
|
||||
uint32_t m_nextSemaphoreId = 1;
|
||||
uint32_t m_nextEventFlagId = 1;
|
||||
IopThread *m_currentThread = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kDmaSpu0Chcr = 0x1F8010C8u;
|
||||
constexpr uint32_t kDmaSpu1Chcr = 0x1F801508u;
|
||||
constexpr uint32_t kDmaStart = 1u << 24u;
|
||||
constexpr int kDmaSpu0Irq = 0x24;
|
||||
constexpr int kDmaSpu1Irq = 0x28;
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
return (value + alignment - 1u) & ~(alignment - 1u);
|
||||
}
|
||||
}
|
||||
|
||||
IopMemory::IopMemory()
|
||||
: m_ram(RamSize), m_owned(RamSize), m_scratch(ScratchSize)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void IopMemory::reset()
|
||||
{
|
||||
std::fill(m_ram.begin(), m_ram.end(), uint8_t{0});
|
||||
std::fill(m_owned.begin(), m_owned.end(), uint8_t{0});
|
||||
std::fill(m_scratch.begin(), m_scratch.end(), uint8_t{0});
|
||||
m_hardware.clear();
|
||||
m_allocations.clear();
|
||||
m_heapCursor = HeapBase;
|
||||
m_interruptStatus = 0;
|
||||
m_interruptMask = 0;
|
||||
m_interruptControl = 1;
|
||||
m_dmaStart.reset();
|
||||
}
|
||||
|
||||
uint32_t IopMemory::physicalAddress(uint32_t address) noexcept
|
||||
{
|
||||
return address & 0x1FFFFFFFu;
|
||||
}
|
||||
|
||||
uint8_t IopMemory::read8(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys < RamSize)
|
||||
return m_ram[phys];
|
||||
if (phys >= ScratchBase && phys < ScratchBase + ScratchSize)
|
||||
return m_scratch[phys - ScratchBase];
|
||||
const uint32_t value = readHardware32(phys & ~3u);
|
||||
return static_cast<uint8_t>(value >> ((phys & 3u) * 8u));
|
||||
}
|
||||
|
||||
uint16_t IopMemory::read16(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys + 1u < RamSize)
|
||||
{
|
||||
uint16_t value;
|
||||
std::memcpy(&value, m_ram.data() + phys, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
return static_cast<uint16_t>(read8(address) | (static_cast<uint16_t>(read8(address + 1u)) << 8u));
|
||||
}
|
||||
|
||||
uint32_t IopMemory::read32(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((phys & 3u) == 0u && phys + 3u < RamSize)
|
||||
{
|
||||
uint32_t value;
|
||||
std::memcpy(&value, m_ram.data() + phys, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize)
|
||||
{
|
||||
uint32_t value;
|
||||
std::memcpy(&value, m_scratch.data() + (phys - ScratchBase), sizeof(value));
|
||||
return value;
|
||||
}
|
||||
if ((phys & 3u) == 0u && isHardwareAddress(phys))
|
||||
return readHardware32(phys);
|
||||
|
||||
return static_cast<uint32_t>(read8(address)) |
|
||||
(static_cast<uint32_t>(read8(address + 1u)) << 8u) |
|
||||
(static_cast<uint32_t>(read8(address + 2u)) << 16u) |
|
||||
(static_cast<uint32_t>(read8(address + 3u)) << 24u);
|
||||
}
|
||||
|
||||
void IopMemory::write8(uint32_t address, uint8_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys < RamSize)
|
||||
{
|
||||
m_ram[phys] = value;
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if (phys >= ScratchBase && phys < ScratchBase + ScratchSize)
|
||||
{
|
||||
m_scratch[phys - ScratchBase] = value;
|
||||
return;
|
||||
}
|
||||
const uint32_t aligned = phys & ~3u;
|
||||
uint32_t current = readHardware32(aligned);
|
||||
const uint32_t shift = (phys & 3u) * 8u;
|
||||
current = (current & ~(0xFFu << shift)) | (static_cast<uint32_t>(value) << shift);
|
||||
writeHardware32(aligned, current);
|
||||
}
|
||||
|
||||
void IopMemory::write16(uint32_t address, uint16_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys + 1u < RamSize)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, &value, sizeof(value));
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
write8(address, static_cast<uint8_t>(value));
|
||||
write8(address + 1u, static_cast<uint8_t>(value >> 8u));
|
||||
}
|
||||
|
||||
void IopMemory::write32(uint32_t address, uint32_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((phys & 3u) == 0u && phys + 3u < RamSize)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, &value, sizeof(value));
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize)
|
||||
{
|
||||
std::memcpy(m_scratch.data() + (phys - ScratchBase), &value, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if ((phys & 3u) == 0u)
|
||||
{
|
||||
writeHardware32(phys, value);
|
||||
return;
|
||||
}
|
||||
write8(address, static_cast<uint8_t>(value));
|
||||
write8(address + 1u, static_cast<uint8_t>(value >> 8u));
|
||||
write8(address + 2u, static_cast<uint8_t>(value >> 16u));
|
||||
write8(address + 3u, static_cast<uint8_t>(value >> 24u));
|
||||
}
|
||||
|
||||
bool IopMemory::readRam(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((!destination && size != 0u) || phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(destination, m_ram.data() + phys, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::writeRam(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((!source && size != 0u) || phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, source, size);
|
||||
markOwned(phys, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::zeroRam(uint32_t address, size_t size)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memset(m_ram.data() + phys, 0, size);
|
||||
markOwned(phys, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::ownsRamRange(uint32_t address, size_t size) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
return std::all_of(m_owned.begin() + phys, m_owned.begin() + phys + size,
|
||||
[](uint8_t value)
|
||||
{ return value != 0u; });
|
||||
}
|
||||
|
||||
void IopMemory::markOwned(uint32_t address, size_t size)
|
||||
{
|
||||
if (address > RamSize || size > RamSize - address)
|
||||
return;
|
||||
std::fill(m_owned.begin() + address, m_owned.begin() + address + size, uint8_t{1});
|
||||
}
|
||||
|
||||
bool IopMemory::isHardwareAddress(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
return (phys >= HardwareBase && phys < HardwareEnd) ||
|
||||
(phys >= Spu2Base && phys < Spu2End) ||
|
||||
(phys >= SifBase && phys < SifEnd);
|
||||
}
|
||||
|
||||
uint32_t IopMemory::readHardware32(uint32_t address) const
|
||||
{
|
||||
const auto value = m_hardware.find(address);
|
||||
if (value != m_hardware.end())
|
||||
return value->second;
|
||||
switch (address)
|
||||
{
|
||||
case 0x1F801070u:
|
||||
return m_interruptStatus;
|
||||
case 0x1F801074u:
|
||||
return m_interruptMask;
|
||||
case 0x1F801078u:
|
||||
return m_interruptControl;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
void IopMemory::writeHardware32(uint32_t address, uint32_t value)
|
||||
{
|
||||
switch (address)
|
||||
{
|
||||
case 0x1F801070u:
|
||||
m_interruptStatus &= value;
|
||||
return;
|
||||
case 0x1F801074u:
|
||||
m_interruptMask = value;
|
||||
return;
|
||||
case 0x1F801078u:
|
||||
m_interruptControl = value & 1u;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
m_hardware[address] = value;
|
||||
if ((address != kDmaSpu0Chcr && address != kDmaSpu1Chcr) || (value & kDmaStart) == 0u)
|
||||
return;
|
||||
|
||||
const bool secondCore = address == kDmaSpu1Chcr;
|
||||
m_hardware[address] = value & ~kDmaStart;
|
||||
|
||||
const uint32_t statusAddress = 0x1F900344u + (secondCore ? 0x400u : 0u);
|
||||
const uint32_t alignedStatus = statusAddress & ~3u;
|
||||
const uint32_t shift = (statusAddress & 2u) * 8u;
|
||||
uint32_t status = 0u;
|
||||
if (const auto current = m_hardware.find(alignedStatus); current != m_hardware.end())
|
||||
status = current->second;
|
||||
status |= 0x80u << shift;
|
||||
m_hardware[alignedStatus] = status;
|
||||
|
||||
const uint32_t blockControlAddress = address - sizeof(uint32_t);
|
||||
uint32_t blockControl = 0u;
|
||||
if (const auto current = m_hardware.find(blockControlAddress); current != m_hardware.end())
|
||||
blockControl = current->second;
|
||||
const uint32_t wordsPerBlock = std::max<uint32_t>(blockControl & 0xFFFFu, 1u);
|
||||
const uint32_t blockCount = std::max<uint32_t>(blockControl >> 16u, 1u);
|
||||
const uint64_t transferWords = static_cast<uint64_t>(wordsPerBlock) * blockCount;
|
||||
m_dmaStart = DmaStart{
|
||||
secondCore ? kDmaSpu1Irq : kDmaSpu0Irq,
|
||||
std::max<uint64_t>(transferWords * 2u, 64u),
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<IopMemory::DmaStart> IopMemory::takeDmaStart() noexcept
|
||||
{
|
||||
std::optional<DmaStart> result = m_dmaStart;
|
||||
m_dmaStart.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t IopMemory::allocate(uint32_t size, uint32_t alignment, std::optional<uint32_t> fixed)
|
||||
{
|
||||
size = alignUp(std::max(size, 1u), 16u);
|
||||
alignment = std::max<uint32_t>(alignment, 4u);
|
||||
if (fixed)
|
||||
{
|
||||
const uint32_t address = *fixed;
|
||||
if (address < HeapBase || address + size > HeapLimit)
|
||||
return 0u;
|
||||
for (const auto &block : m_allocations)
|
||||
if (address < block.address + block.size && block.address < address + size)
|
||||
return 0u;
|
||||
m_allocations.push_back({address, size});
|
||||
markOwned(address, size);
|
||||
return address;
|
||||
}
|
||||
|
||||
uint32_t candidate = alignUp(m_heapCursor, alignment);
|
||||
for (;;)
|
||||
{
|
||||
bool overlap = false;
|
||||
for (const auto &block : m_allocations)
|
||||
{
|
||||
if (candidate < block.address + block.size && block.address < candidate + size)
|
||||
{
|
||||
candidate = alignUp(block.address + block.size, alignment);
|
||||
overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!overlap)
|
||||
break;
|
||||
}
|
||||
if (candidate > HeapLimit || size > HeapLimit - candidate)
|
||||
return 0u;
|
||||
m_allocations.push_back({candidate, size});
|
||||
markOwned(candidate, size);
|
||||
m_heapCursor = std::max(m_heapCursor, candidate + size);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
bool IopMemory::freeAllocation(uint32_t address)
|
||||
{
|
||||
const auto block = std::find_if(m_allocations.begin(), m_allocations.end(),
|
||||
[&](const Allocation &candidate)
|
||||
{ return candidate.address == address; });
|
||||
if (block == m_allocations.end())
|
||||
return false;
|
||||
std::fill(m_owned.begin() + block->address,
|
||||
m_owned.begin() + block->address + block->size,
|
||||
uint8_t{0});
|
||||
m_allocations.erase(block);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t IopMemory::maxFreeMemory() const
|
||||
{
|
||||
return m_heapCursor < HeapLimit ? HeapLimit - m_heapCursor : 0u;
|
||||
}
|
||||
|
||||
std::optional<IopMemory::Allocation> IopMemory::allocationContaining(uint32_t address) const
|
||||
{
|
||||
const auto block = std::find_if(m_allocations.begin(), m_allocations.end(),
|
||||
[&](const Allocation &candidate)
|
||||
{
|
||||
return address >= candidate.address &&
|
||||
address < candidate.address + candidate.size;
|
||||
});
|
||||
if (block == m_allocations.end())
|
||||
return std::nullopt;
|
||||
return *block;
|
||||
}
|
||||
|
||||
std::string IopMemory::readString(uint32_t address, size_t limit) const
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(std::min<size_t>(limit, 64u));
|
||||
for (size_t i = 0; i < limit; ++i)
|
||||
{
|
||||
const char ch = static_cast<char>(read8(address + static_cast<uint32_t>(i)));
|
||||
if (ch == '\0')
|
||||
break;
|
||||
result.push_back(ch);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory
|
||||
{
|
||||
public:
|
||||
static constexpr uint32_t RamSize = 2u * 1024u * 1024u;
|
||||
static constexpr uint32_t ScratchBase = 0x1F800000u;
|
||||
static constexpr uint32_t ScratchSize = 0x400u;
|
||||
static constexpr uint32_t HardwareBase = 0x1F801000u;
|
||||
static constexpr uint32_t HardwareEnd = 0x1F900000u;
|
||||
static constexpr uint32_t Spu2Base = 0x1F900000u;
|
||||
static constexpr uint32_t Spu2End = 0x1FA00000u;
|
||||
static constexpr uint32_t SifBase = 0x1D000000u;
|
||||
static constexpr uint32_t SifEnd = 0x1D001000u;
|
||||
static constexpr uint32_t HeapBase = 0x00120000u;
|
||||
static constexpr uint32_t HeapLimit = 0x001F0000u;
|
||||
|
||||
struct Allocation
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
struct DmaStart
|
||||
{
|
||||
int irq = 0;
|
||||
uint64_t delayCycles = 0;
|
||||
};
|
||||
|
||||
IopMemory();
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] uint8_t read8(uint32_t address) const;
|
||||
[[nodiscard]] uint16_t read16(uint32_t address) const;
|
||||
[[nodiscard]] uint32_t read32(uint32_t address) const;
|
||||
void write8(uint32_t address, uint8_t value);
|
||||
void write16(uint32_t address, uint16_t value);
|
||||
void write32(uint32_t address, uint32_t value);
|
||||
|
||||
[[nodiscard]] bool readRam(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeRam(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroRam(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool ownsRamRange(uint32_t address, size_t size) const;
|
||||
[[nodiscard]] bool isHardwareAddress(uint32_t address) const;
|
||||
[[nodiscard]] std::string readString(uint32_t address, size_t limit = 1024u) const;
|
||||
|
||||
[[nodiscard]] uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional<uint32_t> fixed = std::nullopt);
|
||||
[[nodiscard]] bool freeAllocation(uint32_t address);
|
||||
[[nodiscard]] uint32_t maxFreeMemory() const;
|
||||
[[nodiscard]] std::optional<Allocation> allocationContaining(uint32_t address) const;
|
||||
|
||||
[[nodiscard]] uint32_t interruptStatus() const noexcept { return m_interruptStatus; }
|
||||
[[nodiscard]] uint32_t interruptMask() const noexcept { return m_interruptMask; }
|
||||
[[nodiscard]] uint32_t interruptControl() const noexcept { return m_interruptControl; }
|
||||
void setInterruptStatus(uint32_t value) noexcept { m_interruptStatus = value; }
|
||||
void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; }
|
||||
void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; }
|
||||
|
||||
[[nodiscard]] std::optional<DmaStart> takeDmaStart() noexcept;
|
||||
[[nodiscard]] std::span<const uint8_t> ram() const noexcept { return m_ram; }
|
||||
|
||||
[[nodiscard]] static uint32_t physicalAddress(uint32_t address) noexcept;
|
||||
|
||||
private:
|
||||
[[nodiscard]] uint32_t readHardware32(uint32_t address) const;
|
||||
void writeHardware32(uint32_t address, uint32_t value);
|
||||
void markOwned(uint32_t address, size_t size);
|
||||
|
||||
std::vector<uint8_t> m_ram;
|
||||
std::vector<uint8_t> m_owned;
|
||||
std::vector<uint8_t> m_scratch;
|
||||
std::unordered_map<uint32_t, uint32_t> m_hardware;
|
||||
std::vector<Allocation> m_allocations;
|
||||
uint32_t m_heapCursor = HeapBase;
|
||||
uint32_t m_interruptStatus = 0;
|
||||
uint32_t m_interruptMask = 0;
|
||||
uint32_t m_interruptControl = 1;
|
||||
std::optional<DmaStart> m_dmaStart;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
#include "iop_cdvd.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_kernel.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kSectorSize = 2048u;
|
||||
constexpr uint32_t kPrimaryVolumeDescriptorLsn = 16u;
|
||||
constexpr uint32_t kVolumeDescriptorTerminatorLsn = 17u;
|
||||
constexpr uint32_t kFirstDirectoryLsn = 20u;
|
||||
constexpr uint32_t kCdvdErrorNone = 0u;
|
||||
constexpr uint32_t kCdvdErrorRead = 0x30u;
|
||||
constexpr uint32_t kCdvdTypePs2Dvd = 0x14u;
|
||||
constexpr uint32_t kCdvdReadyComplete = 2u;
|
||||
constexpr uint32_t kCdvdStatusPause = 0x0Au;
|
||||
constexpr uint32_t kCdvdInitExit = 5u;
|
||||
constexpr uint32_t kCdvdCallbackRead = 1u;
|
||||
constexpr uint32_t kCdvdCallbackSeek = 4u;
|
||||
constexpr uint32_t kCdvdInterruptReadyBits = 0x29u;
|
||||
constexpr uint32_t kEventFlagMulti = 2u;
|
||||
constexpr uint32_t kCdvdStreamTimeout = 5000u;
|
||||
constexpr uint32_t kCdvdSyncTimeout = 15000u;
|
||||
constexpr uint32_t kCdvdmanVersion = 0x0226u;
|
||||
|
||||
uint32_t alignSectors(uint64_t bytes)
|
||||
{
|
||||
return static_cast<uint32_t>((bytes + kSectorSize - 1u) / kSectorSize);
|
||||
}
|
||||
|
||||
void writeLe16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
destination[0] = static_cast<uint8_t>(value);
|
||||
destination[1] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
|
||||
void writeBe16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
destination[0] = static_cast<uint8_t>(value >> 8u);
|
||||
destination[1] = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
void writeLe32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
destination[i] = static_cast<uint8_t>(value >> (i * 8u));
|
||||
}
|
||||
|
||||
void writeBe32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
destination[i] = static_cast<uint8_t>(value >> ((3u - i) * 8u));
|
||||
}
|
||||
|
||||
void writeBoth16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
writeLe16(destination, value);
|
||||
writeBe16(destination + 2u, value);
|
||||
}
|
||||
|
||||
void writeBoth32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
writeLe32(destination, value);
|
||||
writeBe32(destination + 4u, value);
|
||||
}
|
||||
|
||||
std::string isoName(const std::filesystem::path &path, bool directory)
|
||||
{
|
||||
std::string name = path.filename().string();
|
||||
for (char &character : name)
|
||||
{
|
||||
const unsigned char byte = static_cast<unsigned char>(character);
|
||||
character = byte < 0x80u ? static_cast<char>(std::toupper(byte)) : '_';
|
||||
}
|
||||
if (name.size() > 200u)
|
||||
name.resize(200u);
|
||||
if (!directory && name.find(';') == std::string::npos)
|
||||
name += ";1";
|
||||
return name;
|
||||
}
|
||||
|
||||
size_t directoryRecordSize(size_t identifierSize)
|
||||
{
|
||||
const size_t unpadded = 33u + identifierSize;
|
||||
return unpadded + (unpadded & 1u);
|
||||
}
|
||||
|
||||
uint32_t directoryBytesFor(const std::vector<size_t> &identifierSizes)
|
||||
{
|
||||
uint64_t cursor = 0u;
|
||||
for (const size_t identifierSize : identifierSizes)
|
||||
{
|
||||
const uint64_t recordSize = directoryRecordSize(identifierSize);
|
||||
const uint64_t sectorOffset = cursor % kSectorSize;
|
||||
if (sectorOffset + recordSize > kSectorSize)
|
||||
cursor += kSectorSize - sectorOffset;
|
||||
cursor += recordSize;
|
||||
}
|
||||
return static_cast<uint32_t>(std::max<uint64_t>(kSectorSize, alignSectors(cursor) * kSectorSize));
|
||||
}
|
||||
|
||||
std::string normalizedIsoComponent(std::string_view value)
|
||||
{
|
||||
std::string result(value);
|
||||
const size_t semicolon = result.rfind(';');
|
||||
if (semicolon != std::string::npos && semicolon + 1u < result.size() &&
|
||||
std::all_of(result.begin() + static_cast<std::ptrdiff_t>(semicolon + 1u), result.end(),
|
||||
[](unsigned char ch)
|
||||
{ return std::isdigit(ch) != 0; }))
|
||||
{
|
||||
result.resize(semicolon);
|
||||
}
|
||||
std::transform(result.begin(), result.end(), result.begin(),
|
||||
[](unsigned char ch)
|
||||
{ return static_cast<char>(std::toupper(ch)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t writeDirectoryRecord(uint8_t *destination,
|
||||
uint32_t lsn,
|
||||
uint32_t size,
|
||||
bool directory,
|
||||
const uint8_t *identifier,
|
||||
size_t identifierSize)
|
||||
{
|
||||
const size_t recordSize = directoryRecordSize(identifierSize);
|
||||
std::memset(destination, 0, recordSize);
|
||||
destination[0] = static_cast<uint8_t>(recordSize);
|
||||
writeBoth32(destination + 2u, lsn);
|
||||
writeBoth32(destination + 10u, size);
|
||||
destination[25] = directory ? 2u : 0u;
|
||||
writeBoth16(destination + 28u, 1u);
|
||||
destination[32] = static_cast<uint8_t>(identifierSize);
|
||||
std::memcpy(destination + 33u, identifier, identifierSize);
|
||||
return recordSize;
|
||||
}
|
||||
}
|
||||
|
||||
class IopCdvd::Impl
|
||||
{
|
||||
public:
|
||||
struct Callback
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
struct IsoNode
|
||||
{
|
||||
std::filesystem::path hostPath;
|
||||
std::string identifier;
|
||||
size_t parent = 0u;
|
||||
bool directory = false;
|
||||
uint32_t lsn = 0u;
|
||||
uint32_t size = 0u;
|
||||
uint32_t sectors = 0u;
|
||||
uint64_t handle = 0u;
|
||||
std::vector<size_t> children;
|
||||
};
|
||||
|
||||
Impl(IopHost &hostRef, IopMemory &memoryRef, IopKernel &kernelRef)
|
||||
: host(hostRef), memory(memoryRef), kernel(kernelRef)
|
||||
{
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
closeFiles();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
closeFiles();
|
||||
callback = {};
|
||||
initialized = false;
|
||||
mediaMode = 0u;
|
||||
currentLsn = 0u;
|
||||
lastError = kCdvdErrorNone;
|
||||
streamFlag = 0u;
|
||||
lastReadTimeout = 0u;
|
||||
interruptEventFlagId = 0;
|
||||
virtualIsoBuilt = false;
|
||||
virtualIsoValid = false;
|
||||
completionCallback.reset();
|
||||
nodes.clear();
|
||||
metadataSectors.clear();
|
||||
imageHandle = 0u;
|
||||
}
|
||||
|
||||
bool dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // sceCdInit
|
||||
initialized = a0 != kCdvdInitExit;
|
||||
if (initialized)
|
||||
{
|
||||
callback = {};
|
||||
completionCallback.reset();
|
||||
}
|
||||
lastError = kCdvdErrorNone;
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 5: // sceCdStandby
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 6: // sceCdRead
|
||||
if (readSectors(a0, a1, a2))
|
||||
{
|
||||
signalCommandComplete();
|
||||
if (callback.address != 0u)
|
||||
{
|
||||
completionCallback = CompletionCallback{
|
||||
callback.address,
|
||||
callback.gp,
|
||||
kCdvdCallbackRead,
|
||||
};
|
||||
}
|
||||
cpu.gpr[2] = 1u;
|
||||
}
|
||||
else
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
|
||||
case 7: // sceCdSeek
|
||||
currentLsn = a0;
|
||||
lastError = kCdvdErrorNone;
|
||||
signalCommandComplete();
|
||||
if (callback.address != 0u)
|
||||
{
|
||||
completionCallback = CompletionCallback{
|
||||
callback.address,
|
||||
callback.gp,
|
||||
kCdvdCallbackSeek,
|
||||
};
|
||||
}
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 8: // sceCdGetError
|
||||
cpu.gpr[2] = lastError;
|
||||
return true;
|
||||
|
||||
case 10: // sceCdSearchFile
|
||||
cpu.gpr[2] = searchFile(a0, a1) ? 1u : 0u;
|
||||
return true;
|
||||
|
||||
case 11: // sceCdSync
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
|
||||
case 12: // sceCdGetDiskType
|
||||
cpu.gpr[2] = kCdvdTypePs2Dvd;
|
||||
return true;
|
||||
|
||||
case 13: // sceCdDiskReady
|
||||
cpu.gpr[2] = kCdvdReadyComplete;
|
||||
return true;
|
||||
|
||||
case 28: // sceCdStatus
|
||||
cpu.gpr[2] = kCdvdStatusPause;
|
||||
return true;
|
||||
|
||||
case 37: // sceCdCallback
|
||||
{
|
||||
const uint32_t previous = callback.address;
|
||||
callback = {a0, cpu.gpr[28]};
|
||||
cpu.gpr[2] = previous;
|
||||
return true;
|
||||
}
|
||||
|
||||
case 50: // sceCdSC
|
||||
{
|
||||
const int32_t code = static_cast<int32_t>(a0);
|
||||
switch (code)
|
||||
{
|
||||
case -23: // Translate a logical sector for a dual-layer disc.
|
||||
// The host image exposes one continuous LSN space, so no layer offset is required.
|
||||
cpu.gpr[2] = a1 != 0u ? memory.read32(a1) : 0u;
|
||||
return true;
|
||||
case -18:
|
||||
lastReadTimeout = a1 != 0u ? memory.read32(a1) : 0u;
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
case -17:
|
||||
cpu.gpr[2] = kCdvdStreamTimeout;
|
||||
return true;
|
||||
case -15:
|
||||
cpu.gpr[2] = kCdvdSyncTimeout;
|
||||
return true;
|
||||
case -11:
|
||||
cpu.gpr[2] = static_cast<uint32_t>(ensureInterruptEventFlag());
|
||||
return true;
|
||||
case -9:
|
||||
cpu.gpr[2] = kCdvdmanVersion;
|
||||
return true;
|
||||
case -2:
|
||||
lastError = a1 != 0u ? memory.read8(a1) : kCdvdErrorNone;
|
||||
cpu.gpr[2] = lastError;
|
||||
return true;
|
||||
case -1:
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
if (a1 != 0u)
|
||||
memory.write32(a1, lastError & 0xFFu);
|
||||
if (code != -1)
|
||||
streamFlag = static_cast<uint32_t>(code);
|
||||
cpu.gpr[2] = streamFlag;
|
||||
return true;
|
||||
default:
|
||||
// sceCdSC is intentionally extensible; unsupported controls are no-ops in cdvdman.
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
case 75: // sceCdMmode
|
||||
mediaMode = a0;
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<CompletionCallback> takeCompletionCallback() noexcept
|
||||
{
|
||||
std::optional<CompletionCallback> result = completionCallback;
|
||||
completionCallback.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
int ensureInterruptEventFlag()
|
||||
{
|
||||
if (interruptEventFlagId == 0)
|
||||
{
|
||||
interruptEventFlagId = kernel.createInternalEventFlag(
|
||||
kEventFlagMulti, 0u, kCdvdInterruptReadyBits);
|
||||
}
|
||||
return interruptEventFlagId;
|
||||
}
|
||||
|
||||
void signalCommandComplete()
|
||||
{
|
||||
if (interruptEventFlagId != 0)
|
||||
(void)kernel.setInternalEventFlag(interruptEventFlagId, kCdvdInterruptReadyBits);
|
||||
}
|
||||
|
||||
void closeFiles()
|
||||
{
|
||||
if (imageHandle != 0u)
|
||||
host.closeHostFile(imageHandle);
|
||||
imageHandle = 0u;
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (node.handle != 0u)
|
||||
host.closeHostFile(node.handle);
|
||||
node.handle = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool addDirectory(size_t parent, const std::filesystem::path &path)
|
||||
{
|
||||
std::error_code error;
|
||||
std::vector<std::filesystem::directory_entry> entries;
|
||||
for (
|
||||
std::filesystem::directory_iterator iterator(path, std::filesystem::directory_options::skip_permission_denied, error),
|
||||
end;
|
||||
!error && iterator != end;
|
||||
iterator.increment(error))
|
||||
{
|
||||
const std::filesystem::directory_entry &entry = *iterator;
|
||||
if (entry.is_symlink(error))
|
||||
{
|
||||
error.clear();
|
||||
continue;
|
||||
}
|
||||
error.clear();
|
||||
if (entry.is_directory(error) || entry.is_regular_file(error))
|
||||
entries.push_back(entry);
|
||||
error.clear();
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const auto &lhs, const auto &rhs)
|
||||
{
|
||||
return isoName(lhs.path(), lhs.is_directory()) < isoName(rhs.path(), rhs.is_directory());
|
||||
});
|
||||
|
||||
for (const auto &entry : entries)
|
||||
{
|
||||
error.clear();
|
||||
const bool directory = entry.is_directory(error);
|
||||
if (error)
|
||||
continue;
|
||||
IsoNode node;
|
||||
node.hostPath = entry.path();
|
||||
node.identifier = isoName(entry.path(), directory);
|
||||
node.parent = parent;
|
||||
node.directory = directory;
|
||||
if (!directory)
|
||||
{
|
||||
const uint64_t fileSize = entry.file_size(error);
|
||||
if (error)
|
||||
continue;
|
||||
node.size = static_cast<uint32_t>(std::min<uint64_t>(fileSize, std::numeric_limits<uint32_t>::max()));
|
||||
}
|
||||
const size_t index = nodes.size();
|
||||
nodes.push_back(std::move(node));
|
||||
nodes[parent].children.push_back(index);
|
||||
if (directory && !addDirectory(index, entry.path()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool buildVirtualIso()
|
||||
{
|
||||
if (virtualIsoBuilt)
|
||||
return virtualIsoValid;
|
||||
virtualIsoBuilt = true;
|
||||
|
||||
const std::string rootValue = host.hostPath(HostPathKind::CdRoot);
|
||||
if (rootValue.empty())
|
||||
return false;
|
||||
const std::filesystem::path rootPath(rootValue);
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_directory(rootPath, error) || error)
|
||||
return false;
|
||||
|
||||
nodes.clear();
|
||||
IsoNode root;
|
||||
root.hostPath = rootPath;
|
||||
root.identifier.clear();
|
||||
root.parent = 0u;
|
||||
root.directory = true;
|
||||
nodes.push_back(std::move(root));
|
||||
if (!addDirectory(0u, rootPath))
|
||||
return false;
|
||||
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory)
|
||||
continue;
|
||||
std::vector<size_t> identifierSizes = {1u, 1u};
|
||||
identifierSizes.reserve(node.children.size() + 2u);
|
||||
for (const size_t child : node.children)
|
||||
identifierSizes.push_back(nodes[child].identifier.size());
|
||||
node.size = directoryBytesFor(identifierSizes);
|
||||
node.sectors = node.size / kSectorSize;
|
||||
}
|
||||
|
||||
uint32_t cursor = kFirstDirectoryLsn;
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory)
|
||||
continue;
|
||||
node.lsn = cursor;
|
||||
cursor += node.sectors;
|
||||
}
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (node.directory)
|
||||
continue;
|
||||
node.lsn = cursor;
|
||||
node.sectors = alignSectors(node.size);
|
||||
cursor += node.sectors;
|
||||
}
|
||||
volumeSectors = std::max<uint32_t>(cursor, 32u);
|
||||
|
||||
std::array<uint8_t, kSectorSize> primary{};
|
||||
primary[0] = 1u;
|
||||
std::memcpy(primary.data() + 1u, "CD001", 5u);
|
||||
primary[6] = 1u;
|
||||
std::memset(primary.data() + 8u, ' ', 32u);
|
||||
std::memcpy(primary.data() + 8u, "PS2XRECOMP", 10u);
|
||||
std::memset(primary.data() + 40u, ' ', 32u);
|
||||
std::memcpy(primary.data() + 40u, "PS2X VIRTUAL DISC", 17u);
|
||||
writeBoth32(primary.data() + 80u, volumeSectors);
|
||||
writeBoth16(primary.data() + 120u, 1u);
|
||||
writeBoth16(primary.data() + 124u, 1u);
|
||||
writeBoth16(primary.data() + 128u, static_cast<uint16_t>(kSectorSize));
|
||||
const uint8_t rootIdentifier = 0u;
|
||||
(void)writeDirectoryRecord(primary.data() + 156u, nodes[0].lsn, nodes[0].size, true, &rootIdentifier, 1u);
|
||||
primary[881] = 1u;
|
||||
metadataSectors[kPrimaryVolumeDescriptorLsn] = primary;
|
||||
|
||||
std::array<uint8_t, kSectorSize> terminator{};
|
||||
terminator[0] = 255u;
|
||||
std::memcpy(terminator.data() + 1u, "CD001", 5u);
|
||||
terminator[6] = 1u;
|
||||
metadataSectors[kVolumeDescriptorTerminatorLsn] = terminator;
|
||||
|
||||
for (size_t nodeIndex = 0u; nodeIndex < nodes.size(); ++nodeIndex)
|
||||
{
|
||||
const IsoNode &node = nodes[nodeIndex];
|
||||
if (!node.directory)
|
||||
continue;
|
||||
std::vector<uint8_t> bytes(node.size, 0u);
|
||||
size_t offset = 0u;
|
||||
const auto appendRecord = [&](const IsoNode &entry, const uint8_t *identifier, size_t identifierSize)
|
||||
{
|
||||
const size_t recordSize = directoryRecordSize(identifierSize);
|
||||
const size_t sectorOffset = offset % kSectorSize;
|
||||
if (sectorOffset + recordSize > kSectorSize)
|
||||
offset += kSectorSize - sectorOffset;
|
||||
offset += writeDirectoryRecord(bytes.data() + offset,
|
||||
entry.lsn,
|
||||
entry.size,
|
||||
entry.directory,
|
||||
identifier,
|
||||
identifierSize);
|
||||
};
|
||||
const uint8_t selfIdentifier = 0u;
|
||||
const uint8_t parentIdentifier = 1u;
|
||||
appendRecord(node, &selfIdentifier, 1u);
|
||||
appendRecord(nodes[node.parent], &parentIdentifier, 1u);
|
||||
for (const size_t childIndex : node.children)
|
||||
{
|
||||
const IsoNode &child = nodes[childIndex];
|
||||
appendRecord(child, reinterpret_cast<const uint8_t *>(child.identifier.data()), child.identifier.size());
|
||||
}
|
||||
for (uint32_t sector = 0u; sector < node.sectors; ++sector)
|
||||
{
|
||||
std::array<uint8_t, kSectorSize> contents{};
|
||||
std::memcpy(contents.data(), bytes.data() + sector * kSectorSize, kSectorSize);
|
||||
metadataSectors[node.lsn + sector] = contents;
|
||||
}
|
||||
}
|
||||
|
||||
virtualIsoValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
IsoNode *findVirtualIsoNode(std::string_view guestPath)
|
||||
{
|
||||
if (!buildVirtualIso())
|
||||
return nullptr;
|
||||
|
||||
const ParsedPs2Path parsed = parsePs2Path(guestPath);
|
||||
if (!parsed || parsed.device != Ps2PathDevice::Cdrom)
|
||||
return nullptr;
|
||||
|
||||
size_t current = 0u;
|
||||
size_t begin = 0u;
|
||||
while (begin <= parsed.path.size())
|
||||
{
|
||||
const size_t end = parsed.path.find('/', begin);
|
||||
const size_t length = (end == std::string::npos) ? parsed.path.size() - begin : end - begin;
|
||||
const std::string_view component(parsed.path.data() + begin, length);
|
||||
begin = (end == std::string::npos) ? parsed.path.size() + 1u : end + 1u;
|
||||
|
||||
if (component.empty() || component == ".")
|
||||
continue;
|
||||
if (component == "..")
|
||||
return nullptr;
|
||||
|
||||
const std::string wanted = normalizedIsoComponent(component);
|
||||
const auto child = std::find_if(nodes[current].children.begin(), nodes[current].children.end(),
|
||||
[&](size_t childIndex)
|
||||
{
|
||||
return normalizedIsoComponent(nodes[childIndex].identifier) == wanted;
|
||||
});
|
||||
if (child == nodes[current].children.end())
|
||||
return nullptr;
|
||||
current = *child;
|
||||
}
|
||||
return &nodes[current];
|
||||
}
|
||||
|
||||
bool searchFile(uint32_t resultAddress, uint32_t nameAddress)
|
||||
{
|
||||
if (resultAddress == 0u || nameAddress == 0u)
|
||||
return false;
|
||||
|
||||
const std::string guestPath = memory.readString(nameAddress, 1024u);
|
||||
IsoNode *node = findVirtualIsoNode(guestPath);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
// sceCdlFILE: lsn, size, name[16], date/flags[8].
|
||||
std::array<uint8_t, 32u> result{};
|
||||
writeLe32(result.data(), node->lsn);
|
||||
writeLe32(result.data() + 4u, node->size);
|
||||
const std::string leaf = normalizedIsoComponent(node->identifier);
|
||||
std::memcpy(result.data() + 8u, leaf.data(), std::min<size_t>(16u, leaf.size()));
|
||||
result[24u] = node->directory ? 2u : 0u;
|
||||
return memory.writeRam(resultAddress, result.data(), result.size());
|
||||
}
|
||||
|
||||
IsoNode *fileForSector(uint32_t lsn)
|
||||
{
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory && lsn >= node.lsn && lsn < node.lsn + node.sectors)
|
||||
return &node;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool readVirtualSector(uint32_t lsn, uint8_t *destination)
|
||||
{
|
||||
const auto metadata = metadataSectors.find(lsn);
|
||||
if (metadata != metadataSectors.end())
|
||||
{
|
||||
std::memcpy(destination, metadata->second.data(), kSectorSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
IsoNode *node = fileForSector(lsn);
|
||||
if (!node)
|
||||
{
|
||||
std::memset(destination, 0, kSectorSize);
|
||||
return lsn < volumeSectors;
|
||||
}
|
||||
if (node->handle == 0u)
|
||||
node->handle = host.openHostFile(node->hostPath.string());
|
||||
if (node->handle == 0u)
|
||||
return false;
|
||||
|
||||
std::memset(destination, 0, kSectorSize);
|
||||
const uint64_t offset = static_cast<uint64_t>(lsn - node->lsn) * kSectorSize;
|
||||
const size_t wanted = static_cast<size_t>(std::min<uint64_t>(kSectorSize, static_cast<uint64_t>(node->size) - offset));
|
||||
size_t bytesRead = 0u;
|
||||
return host.readHostFile(node->handle, offset, destination, wanted, bytesRead) && bytesRead == wanted;
|
||||
}
|
||||
|
||||
bool readSectors(uint32_t lsn, uint32_t sectors, uint32_t destination)
|
||||
{
|
||||
if (sectors == 0u)
|
||||
{
|
||||
lastError = kCdvdErrorNone;
|
||||
return true;
|
||||
}
|
||||
const uint64_t byteCount64 = static_cast<uint64_t>(sectors) * kSectorSize;
|
||||
if (byteCount64 > IopMemory::RamSize || !memory.ownsRamRange(destination, static_cast<size_t>(byteCount64)))
|
||||
{
|
||||
lastError = kCdvdErrorRead;
|
||||
return false;
|
||||
}
|
||||
const size_t byteCount = static_cast<size_t>(byteCount64);
|
||||
std::vector<uint8_t> bytes(byteCount, 0u);
|
||||
|
||||
bool read = false;
|
||||
const std::string imagePath = host.hostPath(HostPathKind::CdImage);
|
||||
if (!imagePath.empty())
|
||||
{
|
||||
if (imageHandle == 0u)
|
||||
imageHandle = host.openHostFile(imagePath);
|
||||
if (imageHandle != 0u)
|
||||
{
|
||||
size_t bytesRead = 0u;
|
||||
read = host.readHostFile(imageHandle,
|
||||
static_cast<uint64_t>(lsn) * kSectorSize,
|
||||
bytes.data(),
|
||||
byteCount,
|
||||
bytesRead) &&
|
||||
bytesRead == byteCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (!read && buildVirtualIso())
|
||||
{
|
||||
read = true;
|
||||
for (uint32_t sector = 0u; sector < sectors; ++sector)
|
||||
{
|
||||
if (!readVirtualSector(lsn + sector, bytes.data() + static_cast<size_t>(sector) * kSectorSize))
|
||||
{
|
||||
read = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!read || !memory.writeRam(destination, bytes.data(), bytes.size()))
|
||||
{
|
||||
lastError = kCdvdErrorRead;
|
||||
return false;
|
||||
}
|
||||
lastError = kCdvdErrorNone;
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
IopMemory &memory;
|
||||
IopKernel &kernel;
|
||||
Callback callback;
|
||||
std::optional<CompletionCallback> completionCallback;
|
||||
bool initialized = false;
|
||||
uint32_t mediaMode = 0u;
|
||||
uint32_t currentLsn = 0u;
|
||||
uint32_t lastError = kCdvdErrorNone;
|
||||
uint32_t streamFlag = 0u;
|
||||
uint32_t lastReadTimeout = 0u;
|
||||
int interruptEventFlagId = 0;
|
||||
uint64_t imageHandle = 0u;
|
||||
bool virtualIsoBuilt = false;
|
||||
bool virtualIsoValid = false;
|
||||
uint32_t volumeSectors = 0u;
|
||||
std::vector<IsoNode> nodes;
|
||||
std::unordered_map<uint32_t, std::array<uint8_t, kSectorSize>> metadataSectors;
|
||||
};
|
||||
|
||||
IopCdvd::IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel)
|
||||
: m_impl(std::make_unique<Impl>(host, memory, kernel))
|
||||
{
|
||||
}
|
||||
|
||||
IopCdvd::~IopCdvd() = default;
|
||||
|
||||
void IopCdvd::reset() noexcept
|
||||
{
|
||||
m_impl->reset();
|
||||
}
|
||||
|
||||
bool IopCdvd::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
return m_impl->dispatchImport(ordinal, cpu);
|
||||
}
|
||||
|
||||
std::optional<IopCdvd::CompletionCallback> IopCdvd::takeCompletionCallback() noexcept
|
||||
{
|
||||
return m_impl->takeCompletionCallback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
class IopMemory;
|
||||
|
||||
class IopCdvd
|
||||
{
|
||||
public:
|
||||
struct CompletionCallback
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
uint32_t reason = 0u;
|
||||
};
|
||||
|
||||
IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel);
|
||||
~IopCdvd();
|
||||
|
||||
IopCdvd(const IopCdvd &) = delete;
|
||||
IopCdvd &operator=(const IopCdvd &) = delete;
|
||||
|
||||
void reset() noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] std::optional<CompletionCallback> takeCompletionCallback() noexcept;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "iop_heaplib.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopHeaplib::IopHeaplib(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopHeaplib::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // CreateHeap
|
||||
setV0(m_memory.allocate(16u, 16u));
|
||||
return true;
|
||||
case 5: // DeleteHeap
|
||||
if (a0 != 0u)
|
||||
(void)m_memory.freeAllocation(a0);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 6:
|
||||
setV0(m_memory.allocate(a1, 16u));
|
||||
return true;
|
||||
case 7:
|
||||
setV0(m_memory.freeAllocation(a1) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 8:
|
||||
setV0(m_memory.maxFreeMemory());
|
||||
return true;
|
||||
case 11:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 15:
|
||||
if (const auto block = m_memory.allocationContaining(a0))
|
||||
setV0(block->size);
|
||||
else
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopHeaplib
|
||||
{
|
||||
public:
|
||||
explicit IopHeaplib(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "iop_imports.h"
|
||||
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kImportMagic = 0x41E00000u;
|
||||
constexpr uint32_t kExportMagic = 0x41C00000u;
|
||||
|
||||
bool equalsIgnoreCase(std::string_view lhs, std::string_view rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(rhs[i])))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string trimLibraryName(const char *name)
|
||||
{
|
||||
size_t length = 0u;
|
||||
while (length < 8u && name[length] != '\0')
|
||||
++length;
|
||||
return std::string(name, length);
|
||||
}
|
||||
}
|
||||
|
||||
IopImportRegistry::IopImportRegistry(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopImportRegistry::reset()
|
||||
{
|
||||
m_libraries.clear();
|
||||
}
|
||||
|
||||
std::optional<IopImportCall> IopImportRegistry::decode(uint32_t pc) const
|
||||
{
|
||||
if (m_memory.read32(pc) != 0x03E00008u)
|
||||
return std::nullopt;
|
||||
const uint32_t delay = m_memory.read32(pc + 4u);
|
||||
if ((delay & 0xFFFF0000u) != 0x24000000u)
|
||||
return std::nullopt;
|
||||
|
||||
const uint32_t physicalPc = IopMemory::physicalAddress(pc);
|
||||
const uint32_t searchBegin = physicalPc > 0x10000u ? physicalPc - 0x10000u : 0u;
|
||||
for (uint32_t candidate = physicalPc & ~3u; candidate >= searchBegin + 20u; candidate -= 4u)
|
||||
{
|
||||
const uint32_t table = candidate - 20u;
|
||||
if (m_memory.read32(table) != kImportMagic)
|
||||
{
|
||||
if (candidate == searchBegin + 20u)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
char name[9]{};
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
name[i] = static_cast<char>(m_memory.read8(table + 12u + i));
|
||||
const uint32_t stubs = table + 20u;
|
||||
if (physicalPc < stubs || ((physicalPc - stubs) & 7u) != 0u)
|
||||
continue;
|
||||
|
||||
bool valid = false;
|
||||
for (uint32_t stub = stubs;
|
||||
stub + 7u < IopMemory::RamSize && stub <= physicalPc;
|
||||
stub += 8u)
|
||||
{
|
||||
const uint32_t first = m_memory.read32(stub);
|
||||
const uint32_t second = m_memory.read32(stub + 4u);
|
||||
if (first == 0u && second == 0u)
|
||||
break;
|
||||
if (stub == physicalPc)
|
||||
{
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
return IopImportCall{
|
||||
trimLibraryName(name),
|
||||
static_cast<uint16_t>(delay & 0xFFFFu),
|
||||
m_memory.read16(table + 8u),
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool IopImportRegistry::registerExportTable(uint32_t address)
|
||||
{
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
if (physical + 20u > IopMemory::RamSize ||
|
||||
m_memory.read32(physical) != kExportMagic)
|
||||
return false;
|
||||
|
||||
char name[9]{};
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
name[i] = static_cast<char>(m_memory.read8(physical + 12u + i));
|
||||
|
||||
ExportLibrary library;
|
||||
library.tableAddress = physical;
|
||||
library.version = m_memory.read16(physical + 8u);
|
||||
library.name = trimLibraryName(name);
|
||||
for (uint32_t cursor = physical + 20u; cursor + 3u < IopMemory::RamSize; cursor += 4u)
|
||||
{
|
||||
const uint32_t function = m_memory.read32(cursor);
|
||||
if (function == 0u)
|
||||
break;
|
||||
library.functions.push_back(function);
|
||||
if (library.functions.size() > 1024u)
|
||||
return false;
|
||||
}
|
||||
m_libraries[physical] = std::move(library);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopImportRegistry::releaseExportTable(uint32_t address)
|
||||
{
|
||||
return m_libraries.erase(IopMemory::physicalAddress(address)) != 0u;
|
||||
}
|
||||
|
||||
const IopImportRegistry::ExportLibrary *IopImportRegistry::findLibrary(std::string_view name, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *selected = nullptr;
|
||||
for (const auto &[address, library] : m_libraries)
|
||||
{
|
||||
(void)address;
|
||||
if (!equalsIgnoreCase(library.name, name) ||
|
||||
(version && (library.version >> 8u) != (*version >> 8u)))
|
||||
continue;
|
||||
// LOADCORE links by major version; a newer minor supersedes older exports.
|
||||
if (!selected || library.version > selected->version)
|
||||
selected = &library;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::findTable(std::string_view library, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *found = findLibrary(library, version);
|
||||
return found ? found->tableAddress : 0u;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::resolve(std::string_view library, uint16_t ordinal, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *found = findLibrary(library, version);
|
||||
if (!found || ordinal >= found->functions.size())
|
||||
return 0u;
|
||||
return found->functions[ordinal];
|
||||
}
|
||||
|
||||
int32_t IopImportRegistry::setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode)
|
||||
{
|
||||
constexpr int32_t kLibraryNotFound = -213;
|
||||
constexpr int32_t kIllegalLibrary = -214;
|
||||
|
||||
if (address == 0u)
|
||||
return kIllegalLibrary;
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
if (physical + 12u > IopMemory::RamSize)
|
||||
return kLibraryNotFound;
|
||||
|
||||
const bool registered = m_libraries.find(physical) != m_libraries.end();
|
||||
if (!registered && m_memory.read32(physical) != kExportMagic)
|
||||
return kLibraryNotFound;
|
||||
|
||||
const uint16_t oldMode = m_memory.read16(physical + 10u);
|
||||
m_memory.write16(physical + 10u, static_cast<uint16_t>((oldMode & ~6u) | (mode & 6u)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void IopImportRegistry::eraseRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto library = m_libraries.begin(); library != m_libraries.end();)
|
||||
{
|
||||
if (library->first >= base && library->first < base + size)
|
||||
library = m_libraries.erase(library);
|
||||
else
|
||||
++library;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
struct IopImportCall
|
||||
{
|
||||
std::string library;
|
||||
uint16_t ordinal = 0;
|
||||
uint16_t version = 0;
|
||||
};
|
||||
|
||||
class IopImportRegistry
|
||||
{
|
||||
public:
|
||||
explicit IopImportRegistry(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc) const;
|
||||
[[nodiscard]] bool registerExportTable(uint32_t address);
|
||||
[[nodiscard]] bool releaseExportTable(uint32_t address);
|
||||
[[nodiscard]] uint32_t findTable(std::string_view library, std::optional<uint16_t> version = std::nullopt) const;
|
||||
[[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal, std::optional<uint16_t> version = std::nullopt) const;
|
||||
[[nodiscard]] int32_t setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode);
|
||||
void eraseRange(uint32_t base, uint32_t size);
|
||||
|
||||
private:
|
||||
struct ExportLibrary
|
||||
{
|
||||
uint32_t tableAddress = 0;
|
||||
uint16_t version = 0;
|
||||
std::string name;
|
||||
std::vector<uint32_t> functions;
|
||||
};
|
||||
|
||||
[[nodiscard]] const ExportLibrary *findLibrary(std::string_view name, std::optional<uint16_t> version) const;
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<uint32_t, ExportLibrary> m_libraries;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "iop_intrman.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "../services/iop_rpc.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopIntrman::IopIntrman(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopIntrman::reset()
|
||||
{
|
||||
m_handlers.clear();
|
||||
m_enabled.clear();
|
||||
}
|
||||
|
||||
bool IopIntrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const uint32_t a3 = cpu.gpr[7];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 3:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 4: // RegisterIntrHandler
|
||||
m_handlers[static_cast<int>(a0)] = {a2, a3, cpu.gpr[28]};
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 5: // ReleaseIntrHandler
|
||||
m_handlers.erase(static_cast<int>(a0));
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 6: // EnableIntr
|
||||
m_enabled[static_cast<int>(a0)] = true;
|
||||
if (a0 < 32u)
|
||||
m_memory.setInterruptMask(m_memory.interruptMask() | (1u << a0));
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 7: // DisableIntr
|
||||
if (a1 != 0u)
|
||||
m_memory.write32(a1, a0);
|
||||
if (a0 < 32u)
|
||||
m_memory.setInterruptMask(m_memory.interruptMask() & ~(1u << a0));
|
||||
m_enabled[static_cast<int>(a0)] = false;
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 8: // CpuDisableIntr
|
||||
m_memory.setInterruptControl(0u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 9: // CpuEnableIntr
|
||||
m_memory.setInterruptControl(1u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 14:
|
||||
setV0(a0 != 0u
|
||||
? executor.executeGuestFunctionWithBudget(a0, a1, a2, a3, 0u, cpu.gpr[28], 100000u)
|
||||
: 0u);
|
||||
return true;
|
||||
case 15:
|
||||
case 16:
|
||||
case 23:
|
||||
case 24:
|
||||
case 25:
|
||||
case 28:
|
||||
case 30:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 17:
|
||||
if (a0 != 0u)
|
||||
m_memory.write32(a0, m_memory.interruptControl());
|
||||
m_memory.setInterruptControl(0u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 18:
|
||||
m_memory.setInterruptControl(a0 != 0u ? 1u : 0u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopIntrman::dispatchInterrupt(int irq, IopGuestExecutor &executor) const
|
||||
{
|
||||
const auto enabled = m_enabled.find(irq);
|
||||
if (enabled == m_enabled.end() || !enabled->second)
|
||||
return false;
|
||||
const auto handler = m_handlers.find(irq);
|
||||
if (handler == m_handlers.end() || handler->second.function == 0u)
|
||||
return false;
|
||||
|
||||
(void)executor.executeGuestFunctionWithBudget(handler->second.function,
|
||||
handler->second.argument,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
handler->second.gp,
|
||||
100000u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopGuestExecutor;
|
||||
class IopMemory;
|
||||
|
||||
class IopIntrman
|
||||
{
|
||||
public:
|
||||
explicit IopIntrman(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor);
|
||||
[[nodiscard]] bool dispatchInterrupt(int irq, IopGuestExecutor &executor) const;
|
||||
|
||||
private:
|
||||
struct Handler
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t argument = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<int, Handler> m_handlers;
|
||||
std::map<int, bool> m_enabled;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "iop_ioman.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "../services/iop_rpc.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopIoman::IopIoman(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopIoman::reset()
|
||||
{
|
||||
m_devices.clear();
|
||||
}
|
||||
|
||||
bool IopIoman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor)
|
||||
{
|
||||
constexpr size_t kMaxDevices = 16u;
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 20: // AddDrv
|
||||
{
|
||||
if (a0 == 0u || m_devices.size() >= kMaxDevices)
|
||||
{
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t nameAddress = m_memory.read32(a0);
|
||||
const uint32_t operations = m_memory.read32(a0 + 16u);
|
||||
const std::string name = m_memory.readString(nameAddress, 64u);
|
||||
if (nameAddress == 0u || operations == 0u || name.empty())
|
||||
{
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_devices.push_back({a0, cpu.gpr[28], name});
|
||||
const uint32_t init = m_memory.read32(operations);
|
||||
if (init != 0u)
|
||||
{
|
||||
const int32_t result = static_cast<int32_t>(
|
||||
executor.executeGuestFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28]));
|
||||
if (result < 0)
|
||||
{
|
||||
m_devices.pop_back();
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 21: // DelDrv
|
||||
{
|
||||
const std::string name = m_memory.readString(a0, 64u);
|
||||
const auto device = std::find_if(
|
||||
m_devices.begin(), m_devices.end(),
|
||||
[&](const Device &candidate)
|
||||
{ return candidate.name == name; });
|
||||
if (device == m_devices.end())
|
||||
{
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t operations = m_memory.read32(device->address + 16u);
|
||||
const uint32_t deinit = operations != 0u ? m_memory.read32(operations + 4u) : 0u;
|
||||
if (deinit != 0u)
|
||||
(void)executor.executeGuestFunction(deinit, device->address, 0u, 0u, 0u, device->gp);
|
||||
m_devices.erase(device);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopGuestExecutor;
|
||||
class IopMemory;
|
||||
|
||||
class IopIoman
|
||||
{
|
||||
public:
|
||||
explicit IopIoman(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor);
|
||||
|
||||
private:
|
||||
struct Device
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::vector<Device> m_devices;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "iop_loadcore.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "iop_imports.h"
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopLoadcore::IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept
|
||||
: m_memory(memory), m_imports(imports)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopLoadcore::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 8:
|
||||
case 9:
|
||||
case 12:
|
||||
case 13:
|
||||
case 14:
|
||||
case 15:
|
||||
case 16:
|
||||
case 17:
|
||||
case 20:
|
||||
case 21:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 6:
|
||||
case 10:
|
||||
setV0(m_imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 7:
|
||||
setV0(m_imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 11: // QueryLibraryEntryTable returns the function array, not the export header.
|
||||
{
|
||||
const uint32_t address = IopMemory::physicalAddress(a0);
|
||||
if (a0 == 0u || address > IopMemory::RamSize - 20u)
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
const uint32_t table = m_imports.findTable(m_memory.readString(address + 12u, 8u), m_memory.read16(address + 8u));
|
||||
setV0(table != 0u ? table + 20u : 0u);
|
||||
return true;
|
||||
}
|
||||
case 27: // SetRebootTimeLibraryHandlingMode
|
||||
setV0(static_cast<uint32_t>(m_imports.setRebootTimeLibraryHandlingMode(a0, cpu.gpr[5])));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopImportRegistry;
|
||||
class IopMemory;
|
||||
|
||||
class IopLoadcore
|
||||
{
|
||||
public:
|
||||
IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
IopImportRegistry &m_imports;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "iop_stdio.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopStdio::IopStdio(IopHost &host, IopMemory &memory) noexcept
|
||||
: m_host(host), m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopStdio::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
const auto logString = [&](std::string_view prefix, uint32_t address, uint32_t resultBias = 0u)
|
||||
{
|
||||
const std::string text = m_memory.readString(address, 2048u);
|
||||
m_host.log(LogLevel::Info, std::string(prefix) + text);
|
||||
setV0(static_cast<uint32_t>(text.size()) + resultBias);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // printf
|
||||
logString("[IOP printf] ", a0);
|
||||
return true;
|
||||
case 5: // getchar
|
||||
case 10:
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 6: // putchar
|
||||
m_host.log(LogLevel::Info, std::string("[IOP putchar] ") + static_cast<char>(a0 & 0xFFu));
|
||||
setV0(a0 & 0xFFu);
|
||||
return true;
|
||||
case 7: // puts
|
||||
logString("[IOP puts] ", a0, 1u);
|
||||
return true;
|
||||
case 8: // gets
|
||||
case 13:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 9: // fdprintf
|
||||
logString("[IOP fdprintf] ", a1);
|
||||
return true;
|
||||
case 11:
|
||||
setV0(a0 & 0xFFu);
|
||||
return true;
|
||||
case 12: // fdputs
|
||||
logString("[IOP fdputs] ", a0);
|
||||
return true;
|
||||
case 14: // vfdprintf
|
||||
logString("[IOP vfdprintf] ", a1);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopStdio
|
||||
{
|
||||
public:
|
||||
IopStdio(IopHost &host, IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
#include "iop_sysclib.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopSysclib::IopSysclib(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopSysclib::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
const auto compare = [&](uint32_t lhs, uint32_t rhs, uint32_t count) -> int32_t
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(lhs + i);
|
||||
const uint8_t right = m_memory.read8(rhs + i);
|
||||
if (left != right)
|
||||
return static_cast<int32_t>(left) - static_cast<int32_t>(right);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const auto copy = [&](uint32_t destination, uint32_t source, uint32_t count)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
m_memory.write8(destination + i, m_memory.read8(source + i));
|
||||
};
|
||||
const auto appendString = [&](uint32_t destination, uint32_t source, std::optional<uint32_t> maxAppend = std::nullopt)
|
||||
{
|
||||
uint32_t destinationOffset = 0;
|
||||
while (m_memory.read8(destination + destinationOffset) != 0u && destinationOffset < (1u << 20))
|
||||
++destinationOffset;
|
||||
|
||||
uint32_t sourceOffset = 0;
|
||||
while (sourceOffset < (1u << 20) && (!maxAppend || sourceOffset < *maxAppend))
|
||||
{
|
||||
const uint8_t character = m_memory.read8(source + sourceOffset);
|
||||
m_memory.write8(destination + destinationOffset + sourceOffset, character);
|
||||
++sourceOffset;
|
||||
if (character == 0u)
|
||||
return;
|
||||
}
|
||||
m_memory.write8(destination + destinationOffset + sourceOffset, 0u);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // setjmp - enough for callers which only test the initial return.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 5: // longjmp, TODO bc w can do it without the BIOS jmp_buf ABI.
|
||||
setV0(a1 == 0u ? 1u : a1);
|
||||
return true;
|
||||
case 6:
|
||||
setV0(static_cast<uint32_t>(std::toupper(static_cast<unsigned char>(a0))));
|
||||
return true;
|
||||
case 7:
|
||||
setV0(static_cast<uint32_t>(std::tolower(static_cast<unsigned char>(a0))));
|
||||
return true;
|
||||
case 8:
|
||||
case 9: // ctype table is optional for most IRXs.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 10: // memchr
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
if (m_memory.read8(a0 + i) == static_cast<uint8_t>(a1))
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 11:
|
||||
setV0(static_cast<uint32_t>(compare(a0, a1, a2)));
|
||||
return true;
|
||||
case 12:
|
||||
copy(a0, a1, a2);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 13:
|
||||
{
|
||||
std::vector<uint8_t> temporary(a2);
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
temporary[i] = m_memory.read8(a1 + i);
|
||||
(void)m_memory.writeRam(a0, temporary.data(), temporary.size());
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 14:
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write8(a0 + i, static_cast<uint8_t>(a1));
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 15: // bcmp
|
||||
setV0(static_cast<uint32_t>(compare(a0, a1, a2)));
|
||||
return true;
|
||||
case 16: // bcopy(src,dst,n)
|
||||
copy(a1, a0, a2);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 17:
|
||||
for (uint32_t i = 0; i < a1; ++i)
|
||||
m_memory.write8(a0 + i, 0u);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 18: // prnt
|
||||
setV0(0);
|
||||
return true;
|
||||
case 19: // sprintf: preserve useful literal formats even before full vararg formatting.
|
||||
case 42: // vsprintf fallback: copy format literal.
|
||||
{
|
||||
const std::string format = m_memory.readString(a1, 4096u);
|
||||
for (size_t i = 0; i <= format.size(); ++i)
|
||||
{
|
||||
m_memory.write8(a0 + static_cast<uint32_t>(i), i < format.size() ? static_cast<uint8_t>(format[i]) : 0u);
|
||||
}
|
||||
setV0(static_cast<uint32_t>(format.size()));
|
||||
return true;
|
||||
}
|
||||
case 20:
|
||||
appendString(a0, a1);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 21: // strchr
|
||||
case 25: // index
|
||||
{
|
||||
const uint8_t needle = static_cast<uint8_t>(a1);
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a0 + i);
|
||||
if (character == needle)
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 22: // strcmp
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(a0 + i);
|
||||
const uint8_t right = m_memory.read8(a1 + i);
|
||||
if (left != right)
|
||||
{
|
||||
setV0(static_cast<uint32_t>(static_cast<int32_t>(left) - static_cast<int32_t>(right)));
|
||||
return true;
|
||||
}
|
||||
if (left == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 23: // strcpy
|
||||
{
|
||||
uint32_t i = 0;
|
||||
for (;; ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a1 + i);
|
||||
m_memory.write8(a0 + i, character);
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 24: // strcspn
|
||||
{
|
||||
const std::string reject = m_memory.readString(a1, 4096u);
|
||||
uint32_t count = 0;
|
||||
for (; count < (1u << 20); ++count)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + count));
|
||||
if (character == 0 || reject.find(character) != std::string::npos)
|
||||
break;
|
||||
}
|
||||
setV0(count);
|
||||
return true;
|
||||
}
|
||||
case 26: // rindex
|
||||
case 32: // strrchr
|
||||
{
|
||||
const uint8_t needle = static_cast<uint8_t>(a1);
|
||||
uint32_t found = 0u;
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a0 + i);
|
||||
if (character == needle)
|
||||
found = a0 + i;
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(found);
|
||||
return true;
|
||||
}
|
||||
case 27:
|
||||
setV0(static_cast<uint32_t>(m_memory.readString(a0, 1u << 20).size()));
|
||||
return true;
|
||||
case 28:
|
||||
appendString(a0, a1, a2);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 29: // strncmp
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(a0 + i);
|
||||
const uint8_t right = m_memory.read8(a1 + i);
|
||||
if (left != right)
|
||||
{
|
||||
setV0(static_cast<uint32_t>(static_cast<int32_t>(left) - static_cast<int32_t>(right)));
|
||||
return true;
|
||||
}
|
||||
if (left == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 30: // strncpy
|
||||
{
|
||||
bool ended = false;
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
const uint8_t character = ended ? 0u : m_memory.read8(a1 + i);
|
||||
if (character == 0u)
|
||||
ended = true;
|
||||
m_memory.write8(a0 + i, character);
|
||||
}
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 31: // strpbrk
|
||||
{
|
||||
const std::string accept = m_memory.readString(a1, 4096u);
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + i));
|
||||
if (character == 0)
|
||||
break;
|
||||
if (accept.find(character) != std::string::npos)
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 33: // strspn
|
||||
{
|
||||
const std::string accept = m_memory.readString(a1, 4096u);
|
||||
uint32_t count = 0;
|
||||
for (; count < (1u << 20); ++count)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + count));
|
||||
if (character == 0 || accept.find(character) == std::string::npos)
|
||||
break;
|
||||
}
|
||||
setV0(count);
|
||||
return true;
|
||||
}
|
||||
case 34: // strstr
|
||||
{
|
||||
const std::string needle = m_memory.readString(a1, 4096u);
|
||||
if (needle.empty())
|
||||
{
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
const std::string haystack = m_memory.readString(a0, 1u << 20);
|
||||
const size_t position = haystack.find(needle);
|
||||
setV0(position == std::string::npos
|
||||
? 0u
|
||||
: a0 + static_cast<uint32_t>(position));
|
||||
return true;
|
||||
}
|
||||
case 35: // strtok state is intentionally not shared across modules yet.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 36:
|
||||
case 38: // strtol / strtoul
|
||||
{
|
||||
const std::string value = m_memory.readString(a0, 4096u);
|
||||
char *end = nullptr;
|
||||
const int base = static_cast<int>(a2);
|
||||
const unsigned long parsed = ordinal == 36
|
||||
? static_cast<unsigned long>(std::strtol(value.c_str(), &end, base))
|
||||
: std::strtoul(value.c_str(), &end, base);
|
||||
if (a1 != 0u)
|
||||
{
|
||||
m_memory.write32(a1, a0 + static_cast<uint32_t>(end - value.c_str()));
|
||||
}
|
||||
setV0(static_cast<uint32_t>(parsed));
|
||||
return true;
|
||||
}
|
||||
case 37: // atob
|
||||
setV0(0);
|
||||
return true;
|
||||
case 40: // _wmemcopy, count is 32-bit words
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write32(a0 + i * 4u, m_memory.read32(a1 + i * 4u));
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 41:
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write32(a0 + i * 4u, a1);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 43:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopSysclib
|
||||
{
|
||||
public:
|
||||
explicit IopSysclib(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "iop_sysmem.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopSysmem::IopSysmem(IopHost &host, IopMemory &memory) noexcept
|
||||
: m_host(host), m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopSysmem::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // AllocSysMemory
|
||||
{
|
||||
const uint32_t address = a0 == 2u
|
||||
? m_memory.allocate(a1, 16u, a2)
|
||||
: m_memory.allocate(a1, 16u);
|
||||
setV0(address);
|
||||
return true;
|
||||
}
|
||||
case 5: // FreeSysMemory
|
||||
setV0(m_memory.freeAllocation(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 6: // QueryMemSize
|
||||
setV0(IopMemory::RamSize);
|
||||
return true;
|
||||
case 7: // QueryMaxFreeMemSize
|
||||
case 8: // QueryTotalFreeMemSize
|
||||
setV0(m_memory.maxFreeMemory());
|
||||
return true;
|
||||
case 9: // QueryBlockTopAddress
|
||||
if (const auto block = m_memory.allocationContaining(a0))
|
||||
setV0(block->address);
|
||||
else
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 10: // QueryBlockSize
|
||||
if (const auto block = m_memory.allocationContaining(a0))
|
||||
setV0(block->size);
|
||||
else
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 14: // Kprintf
|
||||
{
|
||||
const std::string format = m_memory.readString(a0, 512u);
|
||||
m_host.log(LogLevel::Info, std::string("[IOP Kprintf] ") + format);
|
||||
setV0(static_cast<uint32_t>(format.size()));
|
||||
return true;
|
||||
}
|
||||
case 15:
|
||||
setV0(0u);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopSysmem
|
||||
{
|
||||
public:
|
||||
IopSysmem(IopHost &host, IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
#include "iop_timrman.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../services/iop_rpc.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr int32_t kNoTimer = -150;
|
||||
constexpr int32_t kIllegalTimerId = -151;
|
||||
constexpr int32_t kIllegalSource = -152;
|
||||
constexpr int32_t kIllegalPrescale = -153;
|
||||
constexpr int32_t kTimerBusy = -154;
|
||||
constexpr int32_t kTimerNotConfigured = -155;
|
||||
constexpr int32_t kTimerNotRunning = -156;
|
||||
constexpr int32_t kIllegalMode = -405;
|
||||
constexpr uint64_t kIopClockHz = 36'864'000ull;
|
||||
constexpr uint64_t kPixelClockHz = 13'500'000ull;
|
||||
constexpr uint64_t kHlineClockHz = 15'734ull;
|
||||
|
||||
constexpr std::array<size_t, 6> kAllocationOrder{2u, 5u, 4u, 3u, 0u, 1u};
|
||||
constexpr std::array<uint32_t, 6> kAddresses{
|
||||
0xBF801100u,
|
||||
0xBF801110u,
|
||||
0xBF801120u,
|
||||
0xBF801480u,
|
||||
0xBF801490u,
|
||||
0xBF8014A0u,
|
||||
};
|
||||
constexpr std::array<uint8_t, 6> kSources{0x0Bu, 0x0Du, 0x01u, 0x05u, 0x01u, 0x01u};
|
||||
constexpr std::array<uint8_t, 6> kWidths{16u, 16u, 16u, 32u, 32u, 32u};
|
||||
constexpr std::array<uint16_t, 6> kMaxPrescales{1u, 1u, 8u, 1u, 256u, 256u};
|
||||
constexpr std::array<uint8_t, 6> kIrqs{4u, 5u, 6u, 14u, 15u, 16u};
|
||||
|
||||
uint32_t errorValue(int32_t error) noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(error);
|
||||
}
|
||||
}
|
||||
|
||||
void IopTimrman::reset() noexcept
|
||||
{
|
||||
for (size_t i = 0u; i < m_timers.size(); ++i)
|
||||
{
|
||||
m_timers[i] = {};
|
||||
m_timers[i].address = kAddresses[i];
|
||||
m_timers[i].sources = kSources[i];
|
||||
m_timers[i].width = kWidths[i];
|
||||
m_timers[i].maxPrescale = kMaxPrescales[i];
|
||||
m_timers[i].irq = kIrqs[i];
|
||||
}
|
||||
m_holdMode = 0u;
|
||||
m_servicing = false;
|
||||
}
|
||||
|
||||
uint32_t IopTimrman::timerId(size_t index) noexcept
|
||||
{
|
||||
return (static_cast<uint32_t>(index + 1u) << 28u) | (kAddresses[index] >> 4u);
|
||||
}
|
||||
|
||||
IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) noexcept
|
||||
{
|
||||
const uint32_t encoded = id >> 28u;
|
||||
if (encoded == 0u || encoded > m_timers.size())
|
||||
return nullptr;
|
||||
Timer &timer = m_timers[encoded - 1u];
|
||||
return timer.users != 0u && (id & 0x0FFFFFFFu) == (timer.address >> 4u)
|
||||
? &timer
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) const noexcept
|
||||
{
|
||||
return const_cast<IopTimrman *>(this)->timerFromId(id);
|
||||
}
|
||||
|
||||
uint64_t IopTimrman::ticksToCycles(const Timer &timer, uint64_t ticks) noexcept
|
||||
{
|
||||
const uint64_t prescale = std::max<uint64_t>(timer.prescale, 1u);
|
||||
const uint64_t sourceHz = timer.source == 2u
|
||||
? kPixelClockHz
|
||||
: (timer.source == 4u ? kHlineClockHz : kIopClockHz);
|
||||
if (ticks == 0u)
|
||||
ticks = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
|
||||
const unsigned long long scaled = ticks * prescale;
|
||||
if (sourceHz == kIopClockHz)
|
||||
return std::max<uint64_t>(scaled, 1u);
|
||||
const uint64_t whole = (scaled / sourceHz) * kIopClockHz;
|
||||
const uint64_t remainder = scaled % sourceHz;
|
||||
return std::max<uint64_t>(1u, whole + (remainder * kIopClockHz + sourceHz - 1u) / sourceHz);
|
||||
}
|
||||
|
||||
uint64_t IopTimrman::elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
if (!timer.running || currentCycle <= timer.counterBaseCycle)
|
||||
return 0u;
|
||||
const uint64_t elapsed = currentCycle - timer.counterBaseCycle;
|
||||
const uint64_t sourceHz = timer.source == 2u
|
||||
? kPixelClockHz
|
||||
: (timer.source == 4u ? kHlineClockHz : kIopClockHz);
|
||||
return (elapsed * sourceHz) / (kIopClockHz * std::max<uint64_t>(timer.prescale, 1u));
|
||||
}
|
||||
|
||||
uint32_t IopTimrman::counterValue(const Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
const uint64_t value = static_cast<uint64_t>(timer.counterBase) + elapsedTicks(timer, currentCycle);
|
||||
return timer.width == 16u ? static_cast<uint32_t>(value & 0xFFFFu) : static_cast<uint32_t>(value);
|
||||
}
|
||||
|
||||
void IopTimrman::schedule(Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
timer.compareCycle = UINT64_MAX;
|
||||
timer.overflowCycle = UINT64_MAX;
|
||||
if (!timer.running)
|
||||
return;
|
||||
|
||||
const uint64_t current = counterValue(timer, currentCycle);
|
||||
timer.counterBase = static_cast<uint32_t>(current);
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
|
||||
if (timer.compareCallback.function != 0u)
|
||||
{
|
||||
const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
|
||||
const uint64_t compare = timer.width == 16u ? (timer.compare & 0xFFFFu) : timer.compare;
|
||||
uint64_t delta = (compare + modulus - current) % modulus;
|
||||
if (delta == 0u)
|
||||
delta = modulus;
|
||||
timer.compareCycle = currentCycle + ticksToCycles(timer, delta);
|
||||
}
|
||||
|
||||
if (timer.overflowCallback.function != 0u)
|
||||
{
|
||||
const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
|
||||
uint64_t delta = modulus - current;
|
||||
if (delta == 0u)
|
||||
delta = modulus;
|
||||
timer.overflowCycle = currentCycle + ticksToCycles(timer, delta);
|
||||
}
|
||||
}
|
||||
|
||||
void IopTimrman::stop(Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
timer.counterBase = counterValue(timer, currentCycle);
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
timer.running = false;
|
||||
timer.liveMode = 0u;
|
||||
timer.compareCycle = UINT64_MAX;
|
||||
timer.overflowCycle = UINT64_MAX;
|
||||
}
|
||||
|
||||
bool IopTimrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const uint32_t a3 = cpu.gpr[7];
|
||||
const auto setV0 = [&](uint32_t value) { cpu.gpr[2] = value; };
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 3: // GetTimersTable
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 4: // AllocHardTimer
|
||||
for (const size_t index : kAllocationOrder)
|
||||
{
|
||||
Timer &timer = m_timers[index];
|
||||
if (timer.users != 0u || (timer.sources & a0) == 0u || timer.width != a1 || timer.maxPrescale < a2)
|
||||
continue;
|
||||
timer.users = 1u;
|
||||
timer.source = a0;
|
||||
timer.prescale = std::max(a2, 1u);
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
setV0(timerId(index));
|
||||
return true;
|
||||
}
|
||||
setV0(errorValue(kNoTimer));
|
||||
return true;
|
||||
case 5: // ReferHardTimer
|
||||
for (size_t index = 0u; index < m_timers.size(); ++index)
|
||||
{
|
||||
Timer &timer = m_timers[index];
|
||||
if (timer.users == 0u || timer.liveMode == 0u || (timer.sources & a0) == 0u ||
|
||||
timer.width != a1 || (timer.liveMode & a3) != a2)
|
||||
continue;
|
||||
++timer.users;
|
||||
setV0(timerId(index));
|
||||
return true;
|
||||
}
|
||||
setV0(errorValue(kNoTimer));
|
||||
return true;
|
||||
case 6: // FreeHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (--timer->users == 0u)
|
||||
{
|
||||
const uint32_t address = timer->address;
|
||||
const uint8_t sources = timer->sources;
|
||||
const uint8_t width = timer->width;
|
||||
const uint16_t maxPrescale = timer->maxPrescale;
|
||||
const uint8_t irq = timer->irq;
|
||||
*timer = {};
|
||||
timer->address = address;
|
||||
timer->sources = sources;
|
||||
timer->width = width;
|
||||
timer->maxPrescale = maxPrescale;
|
||||
timer->irq = irq;
|
||||
}
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 7: // SetTimerMode
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (a1 == 0u)
|
||||
stop(*timer, currentCycle);
|
||||
else
|
||||
{
|
||||
timer->liveMode = a1;
|
||||
timer->running = true;
|
||||
timer->counterBaseCycle = currentCycle;
|
||||
schedule(*timer, currentCycle);
|
||||
}
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 8: // GetTimerStatus
|
||||
case 17: // GetTimerMode
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? timer->liveMode : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 9: // SetTimerCounter
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
timer->counterBase = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
|
||||
timer->counterBaseCycle = currentCycle;
|
||||
schedule(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 10: // GetTimerCounter
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? counterValue(*timer, currentCycle) : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 11: // SetTimerCompare
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
|
||||
schedule(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 12: // GetTimerCompare
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? timer->compare : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 13: // SetHoldMode
|
||||
m_holdMode = (m_holdMode & ~(0xFu << ((a0 & 7u) * 4u))) | ((a1 & 0xFu) << ((a0 & 7u) * 4u));
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 14: // GetHoldMode
|
||||
setV0((m_holdMode >> ((a0 & 7u) * 4u)) & 0xFu);
|
||||
return true;
|
||||
case 15: // GetHoldReg
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 16: // GetHardTimerIntrCode
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? timer->irq : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 18: // GetTimerReadFunc
|
||||
// Returning a host-side register reader as a guest function is not meaningful.
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 20: // SetTimerHandler
|
||||
case 21: // SetOverflowHandler
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerNotRunning));
|
||||
return true;
|
||||
}
|
||||
if (ordinal == 20u)
|
||||
{
|
||||
timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
|
||||
timer->compareCallback = {a2, a3, cpu.gpr[28]};
|
||||
}
|
||||
else
|
||||
{
|
||||
timer->overflowCallback = {a1, a2, cpu.gpr[28]};
|
||||
}
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 22: // SetupHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerBusy));
|
||||
return true;
|
||||
}
|
||||
if ((a2 != 0u && a2 != 1u && a2 != 3u && a2 != 5u && a2 != 7u))
|
||||
{
|
||||
setV0(errorValue(kIllegalMode));
|
||||
return true;
|
||||
}
|
||||
if ((timer->sources & a1) == 0u)
|
||||
{
|
||||
setV0(errorValue(kIllegalSource));
|
||||
return true;
|
||||
}
|
||||
if (a3 == 0u || a3 > timer->maxPrescale)
|
||||
{
|
||||
setV0(errorValue(kIllegalPrescale));
|
||||
return true;
|
||||
}
|
||||
timer->source = a1;
|
||||
timer->setupMode = a2;
|
||||
timer->prescale = a3;
|
||||
timer->configured = true;
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 23: // StartHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerBusy));
|
||||
return true;
|
||||
}
|
||||
if (!timer->configured)
|
||||
{
|
||||
setV0(errorValue(kTimerNotConfigured));
|
||||
return true;
|
||||
}
|
||||
timer->counterBase = 0u;
|
||||
timer->counterBaseCycle = currentCycle;
|
||||
timer->liveMode = 0x80000000u | timer->setupMode;
|
||||
timer->running = true;
|
||||
schedule(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 24: // StopHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (!timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerNotRunning));
|
||||
return true;
|
||||
}
|
||||
stop(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopTimrman::serviceDue(uint64_t currentCycle, IopGuestExecutor &executor)
|
||||
{
|
||||
if (m_servicing)
|
||||
return;
|
||||
m_servicing = true;
|
||||
struct ServiceGuard
|
||||
{
|
||||
bool &flag;
|
||||
~ServiceGuard() { flag = false; }
|
||||
} guard{m_servicing};
|
||||
|
||||
for (Timer &timer : m_timers)
|
||||
{
|
||||
if (!timer.running)
|
||||
continue;
|
||||
|
||||
const bool compareDue = timer.compareCycle <= currentCycle;
|
||||
const bool overflowDue = timer.overflowCycle <= currentCycle;
|
||||
if (!compareDue && !overflowDue)
|
||||
continue;
|
||||
|
||||
const Callback callback = compareDue ? timer.compareCallback : timer.overflowCallback;
|
||||
timer.compareCycle = UINT64_MAX;
|
||||
timer.overflowCycle = UINT64_MAX;
|
||||
const uint32_t result = callback.function != 0u
|
||||
? executor.executeGuestFunctionWithBudget(callback.function,
|
||||
callback.common,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
callback.gp,
|
||||
100000u)
|
||||
: 0u;
|
||||
if (!timer.running)
|
||||
continue;
|
||||
if (result == 0u)
|
||||
{
|
||||
stop(timer, currentCycle);
|
||||
continue;
|
||||
}
|
||||
if (compareDue)
|
||||
timer.compare = timer.width == 16u ? (result & 0xFFFFu) : result;
|
||||
timer.counterBase = 0u;
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
schedule(timer, currentCycle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint64_t IopTimrman::nextEventCycle(uint64_t fallback) const noexcept
|
||||
{
|
||||
uint64_t next = fallback;
|
||||
for (const Timer &timer : m_timers)
|
||||
{
|
||||
next = std::min(next, timer.compareCycle);
|
||||
next = std::min(next, timer.overflowCycle);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopGuestExecutor;
|
||||
|
||||
class IopTimrman
|
||||
{
|
||||
public:
|
||||
void reset() noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
void serviceDue(uint64_t currentCycle, IopGuestExecutor &executor);
|
||||
[[nodiscard]] uint64_t nextEventCycle(uint64_t fallback) const noexcept;
|
||||
|
||||
private:
|
||||
struct Callback
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t common = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
struct Timer
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint8_t sources = 0u;
|
||||
uint8_t width = 0u;
|
||||
uint16_t maxPrescale = 0u;
|
||||
uint8_t irq = 0u;
|
||||
uint8_t users = 0u;
|
||||
|
||||
uint32_t source = 1u;
|
||||
uint32_t prescale = 1u;
|
||||
uint32_t setupMode = 0u;
|
||||
uint32_t liveMode = 0u;
|
||||
uint32_t counterBase = 0u;
|
||||
uint32_t compare = 0u;
|
||||
uint64_t counterBaseCycle = 0u;
|
||||
uint64_t compareCycle = UINT64_MAX;
|
||||
uint64_t overflowCycle = UINT64_MAX;
|
||||
bool configured = false;
|
||||
bool running = false;
|
||||
|
||||
Callback compareCallback;
|
||||
Callback overflowCallback;
|
||||
};
|
||||
|
||||
[[nodiscard]] Timer *timerFromId(uint32_t timerId) noexcept;
|
||||
[[nodiscard]] const Timer *timerFromId(uint32_t timerId) const noexcept;
|
||||
[[nodiscard]] static uint32_t timerId(size_t index) noexcept;
|
||||
[[nodiscard]] static uint64_t ticksToCycles(const Timer &timer, uint64_t ticks) noexcept;
|
||||
[[nodiscard]] static uint64_t elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept;
|
||||
[[nodiscard]] static uint32_t counterValue(const Timer &timer, uint64_t currentCycle) noexcept;
|
||||
static void schedule(Timer &timer, uint64_t currentCycle) noexcept;
|
||||
static void stop(Timer &timer, uint64_t currentCycle) noexcept;
|
||||
|
||||
std::array<Timer, 6> m_timers{};
|
||||
uint32_t m_holdMode = 0u;
|
||||
bool m_servicing = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "iop_vblank.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../iop_emulator_const.h"
|
||||
#include "../core/iop_kernel.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopVblank::IopVblank(IopKernel &kernel) noexcept
|
||||
: m_kernel(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopVblank::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // WaitVblankStart
|
||||
case 5: // WaitVblankEnd
|
||||
case 6: // WaitVblank
|
||||
case 7: // WaitNonVblank
|
||||
{
|
||||
const bool waitForEnd = ordinal == 5u || ordinal == 7u;
|
||||
const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u;
|
||||
const uint64_t fieldStart = currentCycle - (currentCycle % kVblankPeriodCycles);
|
||||
uint64_t wakeCycle = fieldStart + phase;
|
||||
if (wakeCycle <= currentCycle)
|
||||
wakeCycle += kVblankPeriodCycles;
|
||||
m_kernel.delayCurrentUntil(wakeCycle, cpu);
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
}
|
||||
case 8: // RegisterVblankHandler
|
||||
case 9: // ReleaseVblankHandler
|
||||
// Callback delivery is not required by the scheduler wait ABI yet.
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
|
||||
class IopVblank
|
||||
{
|
||||
public:
|
||||
explicit IopVblank(IopKernel &kernel) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
|
||||
private:
|
||||
IopKernel &m_kernel;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
#include "iop_emulator.h"
|
||||
#include "imports/iop_cdvd.h"
|
||||
#include "core/iop_cpu.h"
|
||||
#include "imports/iop_heaplib.h"
|
||||
#include "imports/iop_imports.h"
|
||||
#include "imports/iop_intrman.h"
|
||||
#include "imports/iop_ioman.h"
|
||||
#include "core/iop_kernel.h"
|
||||
#include "imports/iop_loadcore.h"
|
||||
#include "core/iop_memory.h"
|
||||
#include "services/iop_module_loader.h"
|
||||
#include "services/iop_rpc.h"
|
||||
#include "imports/iop_stdio.h"
|
||||
#include "imports/iop_sysclib.h"
|
||||
#include "imports/iop_sysmem.h"
|
||||
#include "imports/iop_timrman.h"
|
||||
#include "imports/iop_vblank.h"
|
||||
#include "iop_emulator_const.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kRamSize = IopMemory::RamSize;
|
||||
constexpr uint32_t kKernelHeapBase = IopMemory::HeapBase;
|
||||
constexpr uint32_t kKernelHeapLimit = IopMemory::HeapLimit;
|
||||
constexpr uint32_t kCallStackBase = kKernelHeapLimit;
|
||||
constexpr uint32_t kCallStackLimit = 0x001FFF00u;
|
||||
constexpr uint32_t kCallStackSize = 0x2000u;
|
||||
constexpr uint32_t kCallStackCapacity = (kCallStackLimit - kCallStackBase) / kCallStackSize;
|
||||
constexpr uint64_t kCdvdCompletionCycles = 128u;
|
||||
|
||||
uint32_t physicalAddress(uint32_t address)
|
||||
{
|
||||
return IopMemory::physicalAddress(address);
|
||||
}
|
||||
|
||||
int32_t sign16(uint32_t value)
|
||||
{
|
||||
return static_cast<int16_t>(value & 0xFFFFu);
|
||||
}
|
||||
|
||||
bool iequals(std::string_view lhs, std::string_view rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(rhs[i])))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class IopEmulator::Impl final : public IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
using CpuState = IopCpuState;
|
||||
|
||||
struct Module
|
||||
{
|
||||
int id = 0;
|
||||
std::string path;
|
||||
std::string name;
|
||||
uint32_t base = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t gp = 0;
|
||||
bool resident = false;
|
||||
};
|
||||
|
||||
struct GuestCallback
|
||||
{
|
||||
uint32_t function = 0;
|
||||
uint32_t gp = 0;
|
||||
};
|
||||
|
||||
struct ScheduledGuestCallback
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t gp = 0u;
|
||||
uint32_t argument = 0u;
|
||||
};
|
||||
|
||||
explicit Impl(IopHost &hostRef)
|
||||
: host(hostRef),
|
||||
sysmem(host, memory),
|
||||
kernel(memory),
|
||||
cdvd(host, memory, kernel),
|
||||
vblank(kernel),
|
||||
rpc(host, memory, kernel),
|
||||
sysclib(memory),
|
||||
stdio(host, memory),
|
||||
heaplib(memory),
|
||||
intrman(memory),
|
||||
timrman(),
|
||||
ioman(memory),
|
||||
cpuCore(memory),
|
||||
imports(memory),
|
||||
loadcore(memory, imports)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
memory.reset();
|
||||
kernel.reset();
|
||||
modules.clear();
|
||||
imports.reset();
|
||||
rpc.reset();
|
||||
cdvd.reset();
|
||||
intrman.reset();
|
||||
timrman.reset();
|
||||
ioman.reset();
|
||||
pendingDmaInterrupts.clear();
|
||||
pendingGuestCallbacks.clear();
|
||||
nextModuleId = 1;
|
||||
moduleCursor = kModuleLoadBase;
|
||||
totalCycles = 0;
|
||||
totalInstructions = 0;
|
||||
eeCycleCarry = 0;
|
||||
activeCpu = nullptr;
|
||||
lastError.clear();
|
||||
servicingDmaInterrupts = false;
|
||||
servicingGuestCallbacks = false;
|
||||
callDepth = 0u;
|
||||
secrMcCommandHandler = {};
|
||||
secrMcDevIdHandler = {};
|
||||
checkKelfPathCallback = {};
|
||||
}
|
||||
|
||||
uint8_t read8(uint32_t address) const
|
||||
{
|
||||
return memory.read8(address);
|
||||
}
|
||||
|
||||
uint16_t read16(uint32_t address) const
|
||||
{
|
||||
return memory.read16(address);
|
||||
}
|
||||
|
||||
uint32_t read32(uint32_t address) const
|
||||
{
|
||||
return memory.read32(address);
|
||||
}
|
||||
|
||||
void write8(uint32_t address, uint8_t value)
|
||||
{
|
||||
memory.write8(address, value);
|
||||
schedulePendingDma();
|
||||
}
|
||||
|
||||
void write16(uint32_t address, uint16_t value)
|
||||
{
|
||||
memory.write16(address, value);
|
||||
schedulePendingDma();
|
||||
}
|
||||
|
||||
void write32(uint32_t address, uint32_t value)
|
||||
{
|
||||
memory.write32(address, value);
|
||||
schedulePendingDma();
|
||||
}
|
||||
|
||||
void schedulePendingDma()
|
||||
{
|
||||
if (const auto dma = memory.takeDmaStart())
|
||||
pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles;
|
||||
}
|
||||
|
||||
bool readRam(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
return memory.readRam(address, destination, size);
|
||||
}
|
||||
|
||||
bool writeRam(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
return memory.writeRam(address, source, size);
|
||||
}
|
||||
|
||||
bool zeroRam(uint32_t address, size_t size)
|
||||
{
|
||||
return memory.zeroRam(address, size);
|
||||
}
|
||||
|
||||
bool isHardwareAddress(uint32_t phys) const
|
||||
{
|
||||
return memory.isHardwareAddress(phys);
|
||||
}
|
||||
|
||||
uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional<uint32_t> fixed = std::nullopt)
|
||||
{
|
||||
return memory.allocate(size, alignment, fixed);
|
||||
}
|
||||
|
||||
bool freeAllocation(uint32_t address)
|
||||
{
|
||||
return memory.freeAllocation(address);
|
||||
}
|
||||
|
||||
void log(LogLevel level, std::string_view text)
|
||||
{
|
||||
host.log(level, text);
|
||||
}
|
||||
|
||||
bool checkInterrupt(CpuState &cpu)
|
||||
{
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
if ((status & 1u) == 0u)
|
||||
return false;
|
||||
if ((status & 0x2u) != 0u)
|
||||
return false;
|
||||
const bool pending = memory.interruptControl() != 0u && (memory.interruptStatus() & memory.interruptMask()) != 0u;
|
||||
if (!pending)
|
||||
return false;
|
||||
cpu.cop0[13] |= 0x400u;
|
||||
cpuCore.raiseException(cpu, 0u, cpu.pc, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class ImportDisposition
|
||||
{
|
||||
Handled,
|
||||
JumpToGuest,
|
||||
Missing,
|
||||
};
|
||||
|
||||
ImportDisposition dispatchImport(const IopImportCall &call, CpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
if (iequals(call.library, "sysmem") && sysmem.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
if (iequals(call.library, "cdvdman") && cdvd.dispatchImport(call.ordinal, cpu))
|
||||
{
|
||||
if (const auto callback = cdvd.takeCompletionCallback())
|
||||
{
|
||||
pendingGuestCallbacks.emplace(
|
||||
totalCycles + kCdvdCompletionCycles,
|
||||
ScheduledGuestCallback{
|
||||
callback->address,
|
||||
callback->gp,
|
||||
callback->reason,
|
||||
});
|
||||
}
|
||||
return ImportDisposition::Handled;
|
||||
}
|
||||
|
||||
if (iequals(call.library, "loadcore") && loadcore.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
if (iequals(call.library, "thbase") || iequals(call.library, "threadman"))
|
||||
{
|
||||
return kernel.dispatchThreadImport(call.ordinal, cpu, totalCycles)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "thsemap"))
|
||||
{
|
||||
return kernel.dispatchSemaphoreImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "thevent"))
|
||||
{
|
||||
return kernel.dispatchEventImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "sifcmd"))
|
||||
{
|
||||
return rpc.dispatchSifCmdImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "intrman") && intrman.dispatchImport(call.ordinal, cpu, *this))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "secrman"))
|
||||
{
|
||||
switch (call.ordinal)
|
||||
{
|
||||
case 4: // SecrSetMcCommandHandler
|
||||
secrMcCommandHandler = {a0, cpu.gpr[28]};
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
case 5: // SecrSetMcDevIDHandler
|
||||
secrMcDevIdHandler = {a0, cpu.gpr[28]};
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (iequals(call.library, "modload") && call.ordinal == 13u)
|
||||
{
|
||||
checkKelfPathCallback = {a0, cpu.gpr[28]};
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
}
|
||||
if (iequals(call.library, "ioman") && ioman.dispatchImport(call.ordinal, cpu, *this))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "sifman"))
|
||||
{
|
||||
return rpc.dispatchSifManImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "vblank") && vblank.dispatchImport(call.ordinal, cpu, totalCycles))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "timrman") && timrman.dispatchImport(call.ordinal, cpu, totalCycles))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "dmacman"))
|
||||
{
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
}
|
||||
if (iequals(call.library, "stdio") && stdio.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "sysclib"))
|
||||
{
|
||||
return sysclib.dispatchImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
const uint32_t target = imports.resolve(call.library, call.ordinal, call.version);
|
||||
if (target != 0u)
|
||||
{
|
||||
cpu.pc = target;
|
||||
cpu.branchPending = false;
|
||||
return ImportDisposition::JumpToGuest;
|
||||
}
|
||||
|
||||
std::ostringstream out;
|
||||
out << "[IOP] unhandled import " << call.library << ':' << call.ordinal
|
||||
<< " version=0x" << std::hex << call.version << " pc=0x" << cpu.pc;
|
||||
log(LogLevel::Warning, out.str());
|
||||
setV0(0);
|
||||
return ImportDisposition::Missing;
|
||||
}
|
||||
|
||||
bool step(CpuState &cpu)
|
||||
{
|
||||
if (cpu.stopped)
|
||||
return false;
|
||||
if (cpu.pc == kThreadReturnSentinel || cpu.pc == kCallReturnSentinel)
|
||||
{
|
||||
cpu.stopped = true;
|
||||
return false;
|
||||
}
|
||||
if (physicalAddress(cpu.pc) >= kRamSize)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "[IOP] execution outside RAM pc=0x" << std::hex << cpu.pc;
|
||||
log(LogLevel::Error, out.str());
|
||||
cpu.stopped = true;
|
||||
return false;
|
||||
}
|
||||
if (checkInterrupt(cpu))
|
||||
return true;
|
||||
|
||||
if (const auto import = imports.decode(cpu.pc))
|
||||
{
|
||||
const ImportDisposition disposition = dispatchImport(*import, cpu);
|
||||
++totalInstructions;
|
||||
++totalCycles;
|
||||
if (disposition == ImportDisposition::JumpToGuest)
|
||||
return true;
|
||||
cpu.pc = cpu.gpr[31];
|
||||
cpu.branchPending = false;
|
||||
return !cpu.stopped;
|
||||
}
|
||||
|
||||
const bool running = cpuCore.executeInstruction(cpu);
|
||||
schedulePendingDma();
|
||||
++totalInstructions;
|
||||
++totalCycles;
|
||||
return running;
|
||||
}
|
||||
|
||||
uint32_t runCpu(CpuState &cpu, uint32_t instructionBudget)
|
||||
{
|
||||
CpuState *previous = activeCpu;
|
||||
activeCpu = &cpu;
|
||||
const uint64_t start = totalInstructions;
|
||||
while (!cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget)
|
||||
{
|
||||
if (!step(cpu))
|
||||
break;
|
||||
if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty())
|
||||
servicePendingDmaInterrupts();
|
||||
if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty())
|
||||
servicePendingGuestCallbacks();
|
||||
}
|
||||
activeCpu = previous;
|
||||
return static_cast<uint32_t>(totalInstructions - start);
|
||||
}
|
||||
|
||||
uint32_t callFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp,
|
||||
uint32_t budget = kMaxCallInstructions)
|
||||
{
|
||||
struct CallDepthGuard
|
||||
{
|
||||
uint32_t &depth;
|
||||
~CallDepthGuard() { --depth; }
|
||||
};
|
||||
|
||||
const uint32_t depth = callDepth++;
|
||||
const CallDepthGuard depthGuard{callDepth};
|
||||
CpuState cpu{};
|
||||
cpu.pc = address;
|
||||
cpu.gpr[4] = a0;
|
||||
cpu.gpr[5] = a1;
|
||||
cpu.gpr[6] = a2;
|
||||
cpu.gpr[7] = a3;
|
||||
cpu.gpr[28] = gp;
|
||||
if (depth < kCallStackCapacity)
|
||||
{
|
||||
const uint32_t stackTop = kCallStackLimit - depth * kCallStackSize;
|
||||
cpu.gpr[29] = stackTop - 32u;
|
||||
}
|
||||
else if (activeCpu && activeCpu->gpr[29] > kCallStackBase + kStackGuardBytes)
|
||||
{
|
||||
// Extremely deep re-entrancy borrows unused space below the
|
||||
// suspended caller's live frame. Stack growth remains away
|
||||
// from the caller, so its saved registers stay intact.
|
||||
cpu.gpr[29] = (activeCpu->gpr[29] - kStackGuardBytes) & ~15u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.gpr[29] = kCallStackBase - 32u;
|
||||
}
|
||||
cpu.gpr[31] = kCallReturnSentinel;
|
||||
runCpu(cpu, budget);
|
||||
return cpu.gpr[2];
|
||||
}
|
||||
|
||||
uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp) override
|
||||
{
|
||||
return callFunction(address, a0, a1, a2, a3, gp);
|
||||
}
|
||||
|
||||
uint32_t executeGuestFunctionWithBudget(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp,
|
||||
uint32_t instructionBudget) override
|
||||
{
|
||||
return callFunction(address, a0, a1, a2, a3, gp, instructionBudget);
|
||||
}
|
||||
|
||||
// Not that good to use exception handling for control flow but will do for now
|
||||
void servicePendingDmaInterrupts()
|
||||
{
|
||||
if (servicingDmaInterrupts || pendingDmaInterrupts.empty())
|
||||
return;
|
||||
|
||||
servicingDmaInterrupts = true;
|
||||
|
||||
std::vector<int> completed;
|
||||
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
|
||||
{
|
||||
if (it->second > totalCycles)
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
completed.push_back(it->first);
|
||||
it = pendingDmaInterrupts.erase(it);
|
||||
}
|
||||
try
|
||||
{
|
||||
for (const int irq : completed)
|
||||
(void)intrman.dispatchInterrupt(irq, *this);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
servicingDmaInterrupts = false;
|
||||
throw;
|
||||
}
|
||||
servicingDmaInterrupts = false;
|
||||
}
|
||||
|
||||
void servicePendingGuestCallbacks()
|
||||
{
|
||||
if (servicingGuestCallbacks || pendingGuestCallbacks.empty())
|
||||
return;
|
||||
|
||||
std::vector<ScheduledGuestCallback> callbacks;
|
||||
for (auto it = pendingGuestCallbacks.begin(); it != pendingGuestCallbacks.end();)
|
||||
{
|
||||
if (it->first > totalCycles)
|
||||
break;
|
||||
callbacks.push_back(it->second);
|
||||
it = pendingGuestCallbacks.erase(it);
|
||||
}
|
||||
if (callbacks.empty())
|
||||
return;
|
||||
|
||||
servicingGuestCallbacks = true;
|
||||
try
|
||||
{
|
||||
for (const ScheduledGuestCallback &callback : callbacks)
|
||||
{
|
||||
if (callback.function != 0u)
|
||||
{
|
||||
(void)callFunction(callback.function,
|
||||
callback.argument,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
callback.gp,
|
||||
100000u);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
servicingGuestCallbacks = false;
|
||||
throw;
|
||||
}
|
||||
servicingGuestCallbacks = false;
|
||||
}
|
||||
|
||||
void runCycles(uint64_t cycles) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
const uint64_t target = totalCycles + cycles;
|
||||
while (totalCycles < target)
|
||||
{
|
||||
servicePendingDmaInterrupts();
|
||||
servicePendingGuestCallbacks();
|
||||
timrman.serviceDue(totalCycles, *this);
|
||||
IopThread *next = kernel.beginNextReady(totalCycles);
|
||||
if (!next)
|
||||
{
|
||||
uint64_t nextWake = kernel.nextWakeCycle(target);
|
||||
for (const auto &[irq, completionCycle] : pendingDmaInterrupts)
|
||||
nextWake = std::min(nextWake, completionCycle);
|
||||
if (!pendingGuestCallbacks.empty())
|
||||
nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first);
|
||||
nextWake = timrman.nextEventCycle(nextWake);
|
||||
totalCycles = std::max(totalCycles + 1u, std::min(target, nextWake));
|
||||
continue;
|
||||
}
|
||||
const uint64_t before = totalCycles;
|
||||
runCpu(next->cpu, static_cast<uint32_t>(std::min<uint64_t>(kDefaultSlice, target - totalCycles)));
|
||||
kernel.endTimeslice(*next, kThreadReturnSentinel);
|
||||
if (totalCycles == before)
|
||||
++totalCycles;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Runtime scheduling must never throw through EeScheduler::accountCycles().
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadResult loadImage(std::string path, std::span<const uint8_t> image, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
ModuleLoadResult result{true, -1, -1};
|
||||
const IopImageLoadResult loaded = IopModuleLoader::load(image, memory, moduleCursor);
|
||||
moduleCursor = loaded.nextModuleCursor;
|
||||
if (!loaded)
|
||||
{
|
||||
if (loaded.error == IopImageLoadError::InvalidElf)
|
||||
log(LogLevel::Error, "[IOP] rejected invalid/non-MIPS IRX ELF");
|
||||
else if (loaded.error == IopImageLoadError::ArenaExhausted)
|
||||
log(LogLevel::Error, "[IOP] module arena exhausted");
|
||||
return result;
|
||||
}
|
||||
if (!loaded.relocationsComplete)
|
||||
log(LogLevel::Warning, "[IOP] one or more IRX relocations were unsupported");
|
||||
|
||||
Module module;
|
||||
module.id = nextModuleId++;
|
||||
module.path = std::move(path);
|
||||
const size_t slash = module.path.find_last_of("/\\:");
|
||||
module.name = slash == std::string::npos ? module.path : module.path.substr(slash + 1u);
|
||||
module.base = loaded.base;
|
||||
module.size = loaded.size;
|
||||
module.entry = loaded.entry;
|
||||
module.gp = loaded.gp;
|
||||
|
||||
uint32_t args = 0u;
|
||||
if (arguments && argumentSize)
|
||||
{
|
||||
args = allocate(argumentSize + 1u, 16u);
|
||||
if (args)
|
||||
{
|
||||
writeRam(args, arguments, argumentSize);
|
||||
write8(args + argumentSize, 0u);
|
||||
}
|
||||
}
|
||||
const uint32_t startResult = callFunction(module.entry, argumentSize, args, 0u, 0u, module.gp);
|
||||
if (args)
|
||||
freeAllocation(args);
|
||||
module.resident = startResult == 0u || startResult == 2u;
|
||||
result.moduleId = module.id;
|
||||
result.startResult = static_cast<int32_t>(startResult);
|
||||
modules[module.id] = std::move(module);
|
||||
|
||||
std::ostringstream out;
|
||||
out << "[IOP] loaded IRX id=" << result.moduleId
|
||||
<< " entry=0x" << std::hex << modules[result.moduleId].entry
|
||||
<< " base=0x" << modules[result.moduleId].base
|
||||
<< " start=" << std::dec << result.startResult;
|
||||
log(LogLevel::Info, out.str());
|
||||
return result;
|
||||
}
|
||||
|
||||
ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
std::vector<uint8_t> image;
|
||||
if (!IopModuleLoader::readWholeHostFile(host, path, image))
|
||||
{
|
||||
log(LogLevel::Warning, std::string("[IOP] failed to open IRX '") + std::string(path) + "'");
|
||||
return {true, -1, -1};
|
||||
}
|
||||
return loadImage(std::string(path), image, arguments, argumentSize);
|
||||
}
|
||||
|
||||
ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
std::vector<uint8_t> image;
|
||||
if (!IopModuleLoader::readElfFromGuest(host, guestAddress, image))
|
||||
return {true, -1, -1};
|
||||
std::ostringstream tag;
|
||||
tag << "buffer@0x" << std::hex << guestAddress;
|
||||
return loadImage(tag.str(), image, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
auto it = modules.find(moduleId);
|
||||
if (it == modules.end())
|
||||
return false;
|
||||
// A removable IRX normally exposes a stop entry through module metadata. We do not guess it; terminate owned execution and release the image cleanly.
|
||||
kernel.terminateThreadsInRange(it->second.base, it->second.size);
|
||||
rpc.removeServersInRange(it->second.base, it->second.size);
|
||||
imports.eraseRange(it->second.base, it->second.size);
|
||||
modules.erase(it);
|
||||
kernel.cleanupDeadThreads();
|
||||
if (result)
|
||||
*result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
IopMemory memory;
|
||||
IopSysmem sysmem;
|
||||
IopKernel kernel;
|
||||
IopCdvd cdvd;
|
||||
IopVblank vblank;
|
||||
IopRpcBridge rpc;
|
||||
IopSysclib sysclib;
|
||||
IopStdio stdio;
|
||||
IopHeaplib heaplib;
|
||||
IopIntrman intrman;
|
||||
IopTimrman timrman;
|
||||
IopIoman ioman;
|
||||
IopCpuCore cpuCore;
|
||||
IopImportRegistry imports;
|
||||
IopLoadcore loadcore;
|
||||
std::map<int, Module> modules;
|
||||
std::map<int, uint64_t> pendingDmaInterrupts;
|
||||
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
|
||||
uint32_t nextModuleId = 1;
|
||||
uint32_t moduleCursor = kModuleLoadBase;
|
||||
uint64_t totalCycles = 0;
|
||||
uint64_t totalInstructions = 0;
|
||||
uint64_t eeCycleCarry = 0;
|
||||
CpuState *activeCpu = nullptr;
|
||||
std::string lastError;
|
||||
bool servicingDmaInterrupts = false;
|
||||
bool servicingGuestCallbacks = false;
|
||||
uint32_t callDepth = 0u;
|
||||
GuestCallback secrMcCommandHandler;
|
||||
GuestCallback secrMcDevIdHandler;
|
||||
GuestCallback checkKelfPathCallback;
|
||||
};
|
||||
|
||||
IopEmulator::IopEmulator(IopHost &host)
|
||||
: m_impl(std::make_unique<Impl>(host))
|
||||
{
|
||||
}
|
||||
|
||||
IopEmulator::~IopEmulator() = default;
|
||||
|
||||
void IopEmulator::reset()
|
||||
{
|
||||
m_impl->reset();
|
||||
}
|
||||
|
||||
ModuleLoadResult IopEmulator::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->loadModule(path, arguments, argumentSize);
|
||||
}
|
||||
|
||||
ModuleLoadResult IopEmulator::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->loadModuleBuffer(guestAddress, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool IopEmulator::stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
return m_impl->stopModule(moduleId, result);
|
||||
}
|
||||
|
||||
void IopEmulator::runEeCycles(uint64_t eeCycles) noexcept
|
||||
{
|
||||
const uint64_t total = m_impl->eeCycleCarry + eeCycles;
|
||||
const uint64_t iopCycles = total / 8u;
|
||||
m_impl->eeCycleCarry = total % 8u;
|
||||
if (iopCycles)
|
||||
m_impl->runCycles(iopCycles);
|
||||
}
|
||||
|
||||
RpcResult IopEmulator::handleRpc(const RpcRequest &request)
|
||||
{
|
||||
return m_impl->rpc.handleRpc(request, *m_impl);
|
||||
}
|
||||
|
||||
bool IopEmulator::hasRpcServer(uint32_t sid) const noexcept
|
||||
{
|
||||
return m_impl->rpc.hasServer(sid);
|
||||
}
|
||||
|
||||
void IopEmulator::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
m_impl->rpc.onSifTransfer(transfer);
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::allocateMemory(uint32_t size, uint32_t alignment)
|
||||
{
|
||||
return m_impl->memory.allocate(size, alignment);
|
||||
}
|
||||
|
||||
bool IopEmulator::freeMemory(uint32_t address)
|
||||
{
|
||||
return m_impl->memory.freeAllocation(address);
|
||||
}
|
||||
|
||||
bool IopEmulator::readMemory(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
return isMemoryRange(address, size) &&
|
||||
m_impl->memory.readRam(address, destination, size);
|
||||
}
|
||||
|
||||
bool IopEmulator::writeMemory(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
return isMemoryRange(address, size) &&
|
||||
m_impl->memory.writeRam(address, source, size);
|
||||
}
|
||||
|
||||
bool IopEmulator::zeroMemory(uint32_t address, size_t size)
|
||||
{
|
||||
return isMemoryRange(address, size) &&
|
||||
m_impl->memory.zeroRam(address, size);
|
||||
}
|
||||
|
||||
bool IopEmulator::isMemoryRange(uint32_t address, size_t size) const
|
||||
{
|
||||
const bool physicalSegment = address < IopMemory::RamSize;
|
||||
const bool cachedSegment = address >= 0x80000000u && address < 0x80200000u;
|
||||
const bool uncachedSegment = address >= 0xA0000000u && address < 0xA0200000u;
|
||||
if (!physicalSegment && !cachedSegment && !uncachedSegment)
|
||||
return false;
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
return physical <= IopMemory::RamSize && size <= IopMemory::RamSize - physical;
|
||||
}
|
||||
|
||||
uint64_t IopEmulator::cycles() const noexcept
|
||||
{
|
||||
return m_impl->totalCycles;
|
||||
}
|
||||
|
||||
uint64_t IopEmulator::instructions() const noexcept
|
||||
{
|
||||
return m_impl->totalInstructions;
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::loadedModuleCount() const noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(m_impl->modules.size());
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::threadCount() const noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(m_impl->kernel.threadCount());
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::rpcServerCount() const noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(m_impl->rpc.serverCount());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopEmulator
|
||||
{
|
||||
public:
|
||||
explicit IopEmulator(IopHost &host);
|
||||
~IopEmulator();
|
||||
|
||||
IopEmulator(const IopEmulator &) = delete;
|
||||
IopEmulator &operator=(const IopEmulator &) = delete;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize);
|
||||
[[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize);
|
||||
[[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result);
|
||||
void runEeCycles(uint64_t eeCycles) noexcept;
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
|
||||
[[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept;
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
|
||||
[[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u);
|
||||
[[nodiscard]] bool freeMemory(uint32_t address);
|
||||
[[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroMemory(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const;
|
||||
|
||||
[[nodiscard]] uint64_t cycles() const noexcept;
|
||||
[[nodiscard]] uint64_t instructions() const noexcept;
|
||||
[[nodiscard]] uint32_t loadedModuleCount() const noexcept;
|
||||
[[nodiscard]] uint32_t threadCount() const noexcept;
|
||||
[[nodiscard]] uint32_t rpcServerCount() const noexcept;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
constexpr uint32_t kThreadReturnSentinel = 0x1FFFFF00u;
|
||||
constexpr uint32_t kCallReturnSentinel = 0x1FFFFF04u;
|
||||
constexpr uint64_t kIopClockHz = 36'864'000ull;
|
||||
// NTSC field cadence (approximately 59.94 Hz). VBlank imports are
|
||||
// scheduler waits, not no-op timing hints: returning immediately lets
|
||||
// high-priority IRX threads busy-loop and starve RPC server threads.
|
||||
constexpr uint64_t kVblankPeriodCycles = (kIopClockHz * 1001ull + 30'000ull) / 60'000ull;
|
||||
constexpr uint64_t kVblankEndPhaseCycles = kVblankPeriodCycles / 16ull;
|
||||
constexpr uint32_t kDefaultSlice = 256u;
|
||||
constexpr uint32_t kMaxCallInstructions = 2'000'000u;
|
||||
constexpr uint32_t kModuleLoadBase = 0x00010000u;
|
||||
constexpr uint32_t kStackGuardBytes = 64u;
|
||||
@@ -0,0 +1,561 @@
|
||||
#include "iop_module_loader.h"
|
||||
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kMaxImageSize = 64u * 1024u * 1024u;
|
||||
constexpr uint32_t kModuleLoadBase = 0x00010000u;
|
||||
|
||||
constexpr uint16_t ET_EXEC = 2;
|
||||
constexpr uint16_t ET_SCE_IOPRELEXEC = 0xFF80u;
|
||||
constexpr uint16_t ET_SCE_IOPRELEXEC2 = 0xFF81u;
|
||||
constexpr uint16_t EM_MIPS = 8;
|
||||
constexpr uint32_t PT_LOAD = 1;
|
||||
constexpr uint32_t PT_SCE_IOPMOD = 0x70000080u;
|
||||
constexpr uint32_t PT_MIPS_REGINFO = 0x70000000u;
|
||||
constexpr uint32_t SHT_SYMTAB = 2;
|
||||
constexpr uint32_t SHT_MIPS_REGINFO = 0x70000006u;
|
||||
constexpr uint32_t SHT_RELA = 4;
|
||||
constexpr uint32_t SHT_NOBITS = 8;
|
||||
constexpr uint32_t SHT_REL = 9;
|
||||
constexpr uint32_t SHF_ALLOC = 0x2;
|
||||
constexpr uint32_t R_MIPS_NONE = 0;
|
||||
constexpr uint32_t R_MIPS_16 = 1;
|
||||
constexpr uint32_t R_MIPS_32 = 2;
|
||||
constexpr uint32_t R_MIPS_REL32 = 3;
|
||||
constexpr uint32_t R_MIPS_26 = 4;
|
||||
constexpr uint32_t R_MIPS_HI16 = 5;
|
||||
constexpr uint32_t R_MIPS_LO16 = 6;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct Elf32Ehdr
|
||||
{
|
||||
unsigned char ident[16];
|
||||
uint16_t type;
|
||||
uint16_t machine;
|
||||
uint32_t version;
|
||||
uint32_t entry;
|
||||
uint32_t phoff;
|
||||
uint32_t shoff;
|
||||
uint32_t flags;
|
||||
uint16_t ehsize;
|
||||
uint16_t phentsize;
|
||||
uint16_t phnum;
|
||||
uint16_t shentsize;
|
||||
uint16_t shnum;
|
||||
uint16_t shstrndx;
|
||||
};
|
||||
|
||||
struct Elf32Phdr
|
||||
{
|
||||
uint32_t type;
|
||||
uint32_t offset;
|
||||
uint32_t vaddr;
|
||||
uint32_t paddr;
|
||||
uint32_t filesz;
|
||||
uint32_t memsz;
|
||||
uint32_t flags;
|
||||
uint32_t align;
|
||||
};
|
||||
|
||||
struct Elf32Shdr
|
||||
{
|
||||
uint32_t name;
|
||||
uint32_t type;
|
||||
uint32_t flags;
|
||||
uint32_t addr;
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
uint32_t link;
|
||||
uint32_t info;
|
||||
uint32_t addralign;
|
||||
uint32_t entsize;
|
||||
};
|
||||
|
||||
struct Elf32Sym
|
||||
{
|
||||
uint32_t name;
|
||||
uint32_t value;
|
||||
uint32_t size;
|
||||
uint8_t info;
|
||||
uint8_t other;
|
||||
uint16_t shndx;
|
||||
};
|
||||
|
||||
struct Elf32Rel
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t info;
|
||||
};
|
||||
|
||||
struct Elf32Rela
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t info;
|
||||
int32_t addend;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(Elf32Ehdr) == 52);
|
||||
static_assert(sizeof(Elf32Phdr) == 32);
|
||||
static_assert(sizeof(Elf32Shdr) == 40);
|
||||
static_assert(sizeof(Elf32Sym) == 16);
|
||||
|
||||
struct PendingHi16
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t symbolValue = 0;
|
||||
uint32_t symbolIndex = 0;
|
||||
};
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment <= 1u)
|
||||
return value;
|
||||
const uint32_t mask = alignment - 1u;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
|
||||
bool checkedRange(size_t total, uint32_t offset, uint32_t size)
|
||||
{
|
||||
return offset <= total && size <= total - offset;
|
||||
}
|
||||
|
||||
bool validElfHeader(const Elf32Ehdr &header)
|
||||
{
|
||||
return header.ident[0] == 0x7Fu &&
|
||||
header.ident[1] == 'E' &&
|
||||
header.ident[2] == 'L' &&
|
||||
header.ident[3] == 'F' &&
|
||||
header.ident[4] == 1 &&
|
||||
header.ident[5] == 1 &&
|
||||
header.machine == EM_MIPS &&
|
||||
header.ehsize >= sizeof(Elf32Ehdr);
|
||||
}
|
||||
|
||||
bool applyRelocations(std::span<const uint8_t> image,
|
||||
const std::vector<Elf32Shdr> §ions,
|
||||
int64_t delta,
|
||||
uint32_t loadBase,
|
||||
bool isIopRelocatable,
|
||||
IopMemory &memory)
|
||||
{
|
||||
if (sections.empty())
|
||||
return true;
|
||||
|
||||
bool allSupported = true;
|
||||
std::vector<PendingHi16> hi16;
|
||||
for (size_t sectionIndex = 0; sectionIndex < sections.size(); ++sectionIndex)
|
||||
{
|
||||
const Elf32Shdr &relsec = sections[sectionIndex];
|
||||
if (relsec.type != SHT_REL && relsec.type != SHT_RELA)
|
||||
continue;
|
||||
if (relsec.info >= sections.size())
|
||||
continue;
|
||||
|
||||
const Elf32Shdr &targetSection = sections[relsec.info];
|
||||
const uint32_t targetBase = static_cast<uint32_t>(static_cast<int64_t>(targetSection.addr) + delta);
|
||||
|
||||
std::span<const Elf32Sym> symbols;
|
||||
std::vector<Elf32Sym> symbolStorage;
|
||||
if (relsec.link < sections.size())
|
||||
{
|
||||
const Elf32Shdr &symsec = sections[relsec.link];
|
||||
if (symsec.type == SHT_SYMTAB && symsec.entsize >= sizeof(Elf32Sym) && checkedRange(image.size(), symsec.offset, symsec.size))
|
||||
{
|
||||
const size_t count = symsec.size / symsec.entsize;
|
||||
symbolStorage.resize(count);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
std::memcpy(&symbolStorage[i], image.data() + symsec.offset + i * symsec.entsize, sizeof(Elf32Sym));
|
||||
}
|
||||
symbols = symbolStorage;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t entrySize = relsec.type == SHT_RELA
|
||||
? std::max<uint32_t>(relsec.entsize, sizeof(Elf32Rela))
|
||||
: std::max<uint32_t>(relsec.entsize, sizeof(Elf32Rel));
|
||||
if (entrySize == 0u || !checkedRange(image.size(), relsec.offset, relsec.size))
|
||||
continue;
|
||||
|
||||
for (uint32_t offset = 0; offset + entrySize <= relsec.size; offset += entrySize)
|
||||
{
|
||||
uint32_t relocationOffset = 0u;
|
||||
uint32_t relocationInfo = 0u;
|
||||
int32_t explicitAddend = 0;
|
||||
if (relsec.type == SHT_RELA)
|
||||
{
|
||||
Elf32Rela relocation{};
|
||||
std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation));
|
||||
relocationOffset = relocation.offset;
|
||||
relocationInfo = relocation.info;
|
||||
explicitAddend = relocation.addend;
|
||||
}
|
||||
else
|
||||
{
|
||||
Elf32Rel relocation{};
|
||||
std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation));
|
||||
relocationOffset = relocation.offset;
|
||||
relocationInfo = relocation.info;
|
||||
}
|
||||
|
||||
const uint32_t type = relocationInfo & 0xFFu;
|
||||
const uint32_t symbolIndex = relocationInfo >> 8u;
|
||||
uint32_t symbolValue = isIopRelocatable ? loadBase : 0u;
|
||||
if (symbolIndex < symbols.size())
|
||||
{
|
||||
const Elf32Sym &symbol = symbols[symbolIndex];
|
||||
if (!isIopRelocatable || symbolIndex != 0u)
|
||||
{
|
||||
symbolValue = symbol.value;
|
||||
if (symbol.shndx != 0u)
|
||||
{
|
||||
symbolValue = static_cast<uint32_t>(static_cast<int64_t>(symbolValue) + delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sony IOP relocatable executables use absolute image offsets
|
||||
// and symbol index zero. loadcore applies them as loadBase +
|
||||
// r_offset; normal ELF REL sections use a section-relative offset.
|
||||
const uint64_t place64 = isIopRelocatable
|
||||
? static_cast<uint64_t>(loadBase) + relocationOffset
|
||||
: static_cast<uint64_t>(targetBase) + relocationOffset;
|
||||
if (place64 > std::numeric_limits<uint32_t>::max())
|
||||
{
|
||||
allSupported = false;
|
||||
continue;
|
||||
}
|
||||
const uint32_t place = static_cast<uint32_t>(place64);
|
||||
if (place + 3u >= IopMemory::RamSize)
|
||||
{
|
||||
allSupported = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t word = memory.read32(place);
|
||||
const int32_t addend = relsec.type == SHT_RELA
|
||||
? explicitAddend
|
||||
: static_cast<int32_t>(word);
|
||||
switch (type)
|
||||
{
|
||||
case R_MIPS_NONE:
|
||||
break;
|
||||
case R_MIPS_32:
|
||||
case R_MIPS_REL32:
|
||||
memory.write32(place, static_cast<uint32_t>(static_cast<int64_t>(addend) + symbolValue));
|
||||
break;
|
||||
case R_MIPS_26:
|
||||
{
|
||||
const uint32_t target = ((word & 0x03FFFFFFu) << 2u) + symbolValue;
|
||||
memory.write32(place, (word & 0xFC000000u) | ((target >> 2u) & 0x03FFFFFFu));
|
||||
break;
|
||||
}
|
||||
case R_MIPS_HI16:
|
||||
hi16.push_back({place, symbolValue, symbolIndex});
|
||||
break;
|
||||
case R_MIPS_LO16:
|
||||
{
|
||||
const int32_t lo = static_cast<int16_t>(word & 0xFFFFu);
|
||||
for (auto pending = hi16.begin(); pending != hi16.end();)
|
||||
{
|
||||
if (pending->symbolIndex != symbolIndex)
|
||||
{
|
||||
++pending;
|
||||
continue;
|
||||
}
|
||||
const uint32_t hiWord = memory.read32(pending->address);
|
||||
const int32_t hi = static_cast<int16_t>(hiWord & 0xFFFFu) << 16u;
|
||||
const int64_t full = static_cast<int64_t>(hi) + lo + pending->symbolValue;
|
||||
const uint32_t relocatedHi = static_cast<uint32_t>((full + 0x8000) >> 16u) & 0xFFFFu;
|
||||
memory.write32(pending->address, (hiWord & 0xFFFF0000u) | relocatedHi);
|
||||
pending = hi16.erase(pending);
|
||||
}
|
||||
const int64_t full = static_cast<int64_t>(lo) + symbolValue;
|
||||
memory.write32(place, (word & 0xFFFF0000u) | (static_cast<uint32_t>(full) & 0xFFFFu));
|
||||
break;
|
||||
}
|
||||
case R_MIPS_16:
|
||||
memory.write32(place, (word & 0xFFFF0000u) | (static_cast<uint32_t>(addend + symbolValue) & 0xFFFFu));
|
||||
break;
|
||||
default:
|
||||
allSupported = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSupported;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopModuleLoader::readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector<uint8_t> &bytes)
|
||||
{
|
||||
const std::string translated = host.translateGuestPath(guestPath);
|
||||
const std::string_view path = translated.empty() ? guestPath : std::string_view(translated);
|
||||
const uint64_t handle = host.openHostFile(path);
|
||||
if (handle == 0u)
|
||||
return false;
|
||||
|
||||
uint64_t size = 0u;
|
||||
if (!host.hostFileSize(handle, size) || size == 0u || size > kMaxImageSize)
|
||||
{
|
||||
host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes.resize(static_cast<size_t>(size));
|
||||
size_t bytesRead = 0u;
|
||||
const bool ok = host.readHostFile(handle, 0u, bytes.data(), bytes.size(), bytesRead) && bytesRead == bytes.size();
|
||||
host.closeHostFile(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IopModuleLoader::readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector<uint8_t> &bytes)
|
||||
{
|
||||
Elf32Ehdr header{};
|
||||
if (!host.readGuest(guestAddress, &header, sizeof(header)) || !validElfHeader(header))
|
||||
return false;
|
||||
|
||||
uint64_t required = sizeof(header);
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(header.phoff) + static_cast<uint64_t>(header.phentsize) * header.phnum);
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(header.shoff) + static_cast<uint64_t>(header.shentsize) * header.shnum);
|
||||
|
||||
if (required > kMaxImageSize)
|
||||
return false; // Should we log an error here? TODO check later
|
||||
|
||||
bytes.resize(static_cast<size_t>(required));
|
||||
if (!host.readGuest(guestAddress, bytes.data(), bytes.size()))
|
||||
return false;
|
||||
|
||||
if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr))
|
||||
{
|
||||
for (uint16_t i = 0; i < header.shnum; ++i)
|
||||
{
|
||||
Elf32Shdr section{};
|
||||
const size_t offset = static_cast<size_t>(header.shoff) + static_cast<size_t>(i) * header.shentsize;
|
||||
std::memcpy(§ion, bytes.data() + offset, sizeof(section));
|
||||
if (section.type != SHT_NOBITS)
|
||||
{
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(section.offset) + section.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr))
|
||||
{
|
||||
for (uint16_t i = 0; i < header.phnum; ++i)
|
||||
{
|
||||
Elf32Phdr program{};
|
||||
const size_t offset = static_cast<size_t>(header.phoff) + static_cast<size_t>(i) * header.phentsize;
|
||||
std::memcpy(&program, bytes.data() + offset, sizeof(program));
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(program.offset) + program.filesz);
|
||||
}
|
||||
}
|
||||
if (required > kMaxImageSize)
|
||||
return false;
|
||||
|
||||
bytes.resize(static_cast<size_t>(required));
|
||||
return host.readGuest(guestAddress, bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
IopImageLoadResult IopModuleLoader::load(std::span<const uint8_t> image, IopMemory &memory, uint32_t moduleCursor)
|
||||
{
|
||||
IopImageLoadResult result;
|
||||
result.nextModuleCursor = moduleCursor;
|
||||
if (image.size() < sizeof(Elf32Ehdr))
|
||||
return result;
|
||||
|
||||
Elf32Ehdr header{};
|
||||
std::memcpy(&header, image.data(), sizeof(header));
|
||||
if (!validElfHeader(header))
|
||||
{
|
||||
result.error = IopImageLoadError::InvalidElf;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t minVaddr = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t maxVaddr = 0u;
|
||||
bool hasLoad = false;
|
||||
std::vector<Elf32Phdr> programHeaders;
|
||||
if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr) && checkedRange(image.size(), header.phoff, static_cast<uint32_t>(header.phentsize) * header.phnum))
|
||||
{
|
||||
programHeaders.reserve(header.phnum);
|
||||
for (uint16_t i = 0; i < header.phnum; ++i)
|
||||
{
|
||||
Elf32Phdr program{};
|
||||
std::memcpy(&program, image.data() + header.phoff + static_cast<size_t>(i) * header.phentsize, sizeof(program));
|
||||
programHeaders.push_back(program);
|
||||
if (program.type == PT_LOAD && program.memsz != 0u)
|
||||
{
|
||||
hasLoad = true;
|
||||
minVaddr = std::min(minVaddr, program.vaddr);
|
||||
maxVaddr = std::max(maxVaddr, program.vaddr + program.memsz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Elf32Shdr> sectionHeaders;
|
||||
if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr) && checkedRange(image.size(), header.shoff, static_cast<uint32_t>(header.shentsize) * header.shnum))
|
||||
{
|
||||
sectionHeaders.reserve(header.shnum);
|
||||
for (uint16_t i = 0; i < header.shnum; ++i)
|
||||
{
|
||||
Elf32Shdr section{};
|
||||
std::memcpy(§ion, image.data() + header.shoff + static_cast<size_t>(i) * header.shentsize, sizeof(section));
|
||||
sectionHeaders.push_back(section);
|
||||
if (!hasLoad && (section.flags & SHF_ALLOC) != 0u && section.size != 0u)
|
||||
{
|
||||
minVaddr = std::min(minVaddr, section.addr);
|
||||
maxVaddr = std::max(maxVaddr, section.addr + section.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minVaddr == std::numeric_limits<uint32_t>::max())
|
||||
minVaddr = 0u;
|
||||
uint32_t span = maxVaddr > minVaddr ? maxVaddr - minVaddr : 0x1000u;
|
||||
span = alignUp(span, 0x100u);
|
||||
const bool relocate = header.type != ET_EXEC ||
|
||||
maxVaddr > IopMemory::RamSize ||
|
||||
(minVaddr < kModuleLoadBase && minVaddr != 0u);
|
||||
uint32_t base = 0u;
|
||||
int64_t delta = 0;
|
||||
if (relocate)
|
||||
{
|
||||
base = alignUp(moduleCursor, 0x100u);
|
||||
if (base + span >= IopMemory::HeapBase)
|
||||
{
|
||||
result.error = IopImageLoadError::ArenaExhausted;
|
||||
return result;
|
||||
}
|
||||
delta = static_cast<int64_t>(base) - minVaddr;
|
||||
result.nextModuleCursor = base + span;
|
||||
}
|
||||
else
|
||||
{
|
||||
base = minVaddr;
|
||||
}
|
||||
|
||||
if (hasLoad)
|
||||
{
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (program.type != PT_LOAD || program.memsz == 0u)
|
||||
continue;
|
||||
if (!checkedRange(image.size(), program.offset, program.filesz) ||
|
||||
program.memsz < program.filesz)
|
||||
return result;
|
||||
const uint32_t destination = static_cast<uint32_t>(static_cast<int64_t>(program.vaddr) + delta);
|
||||
if (destination >= IopMemory::RamSize || program.memsz > IopMemory::RamSize - destination)
|
||||
return result;
|
||||
if (!memory.writeRam(destination, image.data() + program.offset, program.filesz))
|
||||
return result;
|
||||
if (program.memsz > program.filesz && !memory.zeroRam(destination + program.filesz, program.memsz - program.filesz))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t sectionCursor = base;
|
||||
for (auto §ion : sectionHeaders)
|
||||
{
|
||||
if ((section.flags & SHF_ALLOC) == 0u || section.size == 0u)
|
||||
continue;
|
||||
uint32_t destination = 0u;
|
||||
if (section.addr != 0u)
|
||||
{
|
||||
destination = static_cast<uint32_t>(static_cast<int64_t>(section.addr) + delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
sectionCursor = alignUp(sectionCursor, std::max<uint32_t>(section.addralign, 4u));
|
||||
destination = sectionCursor;
|
||||
section.addr = static_cast<uint32_t>(static_cast<int64_t>(destination) - delta);
|
||||
sectionCursor += section.size;
|
||||
}
|
||||
if (destination >= IopMemory::RamSize || section.size > IopMemory::RamSize - destination)
|
||||
return result;
|
||||
if (section.type == SHT_NOBITS)
|
||||
{
|
||||
if (!memory.zeroRam(destination, section.size))
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!checkedRange(image.size(), section.offset, section.size) ||
|
||||
!memory.writeRam(destination, image.data() + section.offset, section.size))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool isIopRelocatable = header.type == ET_SCE_IOPRELEXEC || header.type == ET_SCE_IOPRELEXEC2;
|
||||
result.relocationsComplete = applyRelocations(image,
|
||||
sectionHeaders,
|
||||
delta,
|
||||
base,
|
||||
isIopRelocatable,
|
||||
memory);
|
||||
|
||||
result.base = base;
|
||||
result.size = span;
|
||||
result.entry = static_cast<uint32_t>(static_cast<int64_t>(header.entry) + delta);
|
||||
result.gp = 0u;
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (program.type == PT_SCE_IOPMOD && program.filesz >= 12u && checkedRange(image.size(), program.offset, 12u))
|
||||
{
|
||||
uint32_t entry = 0u;
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&entry, image.data() + program.offset + 4u, sizeof(entry));
|
||||
std::memcpy(&gp, image.data() + program.offset + 8u, sizeof(gp));
|
||||
result.entry = static_cast<uint32_t>(static_cast<int64_t>(entry) + delta);
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (result.gp != 0u)
|
||||
break;
|
||||
if (program.type == PT_MIPS_REGINFO && program.filesz >= 24u && checkedRange(image.size(), program.offset, 24u))
|
||||
{
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&gp, image.data() + program.offset + 20u, sizeof(gp));
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result.gp == 0u)
|
||||
{
|
||||
for (const auto §ion : sectionHeaders)
|
||||
{
|
||||
if (section.type == SHT_MIPS_REGINFO && section.size >= 24u && checkedRange(image.size(), section.offset, 24u))
|
||||
{
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&gp, image.data() + section.offset + 20u, sizeof(gp));
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.error = IopImageLoadError::None;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
enum class IopImageLoadError : uint8_t
|
||||
{
|
||||
None,
|
||||
InvalidElf,
|
||||
ArenaExhausted,
|
||||
MalformedImage,
|
||||
};
|
||||
|
||||
struct IopImageLoadResult
|
||||
{
|
||||
IopImageLoadError error = IopImageLoadError::MalformedImage;
|
||||
uint32_t base = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t nextModuleCursor = 0;
|
||||
bool relocationsComplete = true;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const noexcept
|
||||
{
|
||||
return error == IopImageLoadError::None;
|
||||
}
|
||||
};
|
||||
|
||||
class IopModuleLoader
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] static bool readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector<uint8_t> &bytes);
|
||||
[[nodiscard]] static bool readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector<uint8_t> &bytes);
|
||||
[[nodiscard]] static IopImageLoadResult load(std::span<const uint8_t> image, IopMemory &memory, uint32_t moduleCursor);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
#include "iop_rpc.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_kernel.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopRpcBridge::IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept
|
||||
: m_host(host), m_memory(memory), m_kernel(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
void IopRpcBridge::reset()
|
||||
{
|
||||
m_servers.clear();
|
||||
m_nextDmaId = 1u;
|
||||
m_sifInitialized = false;
|
||||
}
|
||||
|
||||
bool IopRpcBridge::dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // sceSifDma2Init
|
||||
case 5: // sceSifInit
|
||||
m_sifInitialized = true;
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 7: // sceSifSetDma
|
||||
{
|
||||
constexpr uint32_t kDescriptorSize = 16u;
|
||||
constexpr uint32_t kMaxDescriptors = 32u;
|
||||
const uint32_t descriptorAddress = cpu.gpr[4];
|
||||
const uint32_t descriptorCount = cpu.gpr[5];
|
||||
if (descriptorAddress == 0u || descriptorCount == 0u || descriptorCount > kMaxDescriptors)
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct PendingTransfer
|
||||
{
|
||||
uint32_t source = 0u;
|
||||
uint32_t destination = 0u;
|
||||
uint32_t size = 0u;
|
||||
};
|
||||
|
||||
std::array<uint32_t, kMaxDescriptors * 4u> descriptorWords{};
|
||||
const size_t descriptorBytes = static_cast<size_t>(descriptorCount) * kDescriptorSize;
|
||||
if (!m_memory.readRam(descriptorAddress, descriptorWords.data(), descriptorBytes))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<PendingTransfer, kMaxDescriptors> pending{};
|
||||
uint32_t pendingCount = 0u;
|
||||
uint32_t largestTransfer = 0u;
|
||||
for (uint32_t i = 0u; i < descriptorCount; ++i)
|
||||
{
|
||||
const uint32_t source = descriptorWords[i * 4u + 0u];
|
||||
const uint32_t destination = descriptorWords[i * 4u + 1u];
|
||||
const int32_t signedSize = static_cast<int32_t>(descriptorWords[i * 4u + 2u]);
|
||||
if (signedSize <= 0)
|
||||
continue;
|
||||
|
||||
const uint32_t size = static_cast<uint32_t>(signedSize);
|
||||
if (!m_memory.ownsRamRange(source, size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
pending[pendingCount++] = {source, destination, size};
|
||||
largestTransfer = std::max(largestTransfer, size);
|
||||
}
|
||||
|
||||
// IOP-side sceSifSetDma sends IOP RAM to the EE. Validate all EE
|
||||
// destinations before committing any write so a bad chain cannot
|
||||
// partially update guest memory, but maybe we could skip this check if we trust the EE-side SIF driver to validate the chain ?!
|
||||
// TODO check later
|
||||
std::vector<uint8_t> scratch(largestTransfer);
|
||||
for (uint32_t i = 0u; i < pendingCount; ++i)
|
||||
{
|
||||
const PendingTransfer &transfer = pending[i];
|
||||
if (!m_host.readGuest(transfer.destination, scratch.data(), transfer.size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t i = 0u; i < pendingCount; ++i)
|
||||
{
|
||||
const PendingTransfer &transfer = pending[i];
|
||||
if (!m_memory.readRam(transfer.source, scratch.data(), transfer.size) || !m_host.writeGuest(transfer.destination, scratch.data(), transfer.size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t dmaId = m_nextDmaId++;
|
||||
if (m_nextDmaId == 0u || m_nextDmaId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))
|
||||
{
|
||||
m_nextDmaId = 1u;
|
||||
}
|
||||
setV0(dmaId);
|
||||
return true;
|
||||
}
|
||||
case 8: // sceSifDmaStat
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 29: // sceSifCheckInit
|
||||
setV0(m_sifInitialized ? 1u : 0u);
|
||||
return true;
|
||||
default:
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopRpcBridge::dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // InitCmd
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
case 8:
|
||||
case 9:
|
||||
case 10:
|
||||
case 11:
|
||||
case 14: // InitRpc
|
||||
case 15:
|
||||
case 16:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 12: // sceSifSendCmd
|
||||
case 13: // isceSifSendCmd
|
||||
{
|
||||
constexpr uint32_t kHeaderSize = 16u;
|
||||
constexpr uint32_t kMaxPacketSize = 112u;
|
||||
const uint32_t commandId = cpu.gpr[4];
|
||||
const uint32_t packetAddress = cpu.gpr[5];
|
||||
const uint32_t packetSize = cpu.gpr[6];
|
||||
const uint32_t extraSource = cpu.gpr[7];
|
||||
const uint32_t stackPointer = cpu.gpr[29];
|
||||
const uint32_t extraDestination = m_memory.read32(stackPointer + 16u);
|
||||
const int32_t signedExtraSize = static_cast<int32_t>(m_memory.read32(stackPointer + 20u));
|
||||
|
||||
if (packetAddress == 0u || packetSize < kHeaderSize || packetSize > kMaxPacketSize ||
|
||||
!m_memory.ownsRamRange(packetAddress, packetSize))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kMaxPacketSize> packet{};
|
||||
if (!m_memory.readRam(packetAddress, packet.data(), packetSize))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t extraSize = 0u;
|
||||
if (signedExtraSize > 0)
|
||||
{
|
||||
extraSize = static_cast<uint32_t>(signedExtraSize);
|
||||
if (extraSource == 0u || extraDestination == 0u ||
|
||||
!m_memory.ownsRamRange(extraSource, extraSize) ||
|
||||
!m_host.writeGuest(extraDestination, m_memory.ram().data() + IopMemory::physicalAddress(extraSource), extraSize))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t sizeWord = packetSize | (extraSize << 8u);
|
||||
std::memcpy(packet.data() + 0u, &sizeWord, sizeof(sizeWord));
|
||||
std::memcpy(packet.data() + 4u, &extraDestination, sizeof(extraDestination));
|
||||
std::memcpy(packet.data() + 8u, &commandId, sizeof(commandId));
|
||||
|
||||
if (!m_host.sendSifCommand(commandId, packet.data(), packetSize))
|
||||
{
|
||||
// A command without an EE handler is still a completed DMA on real hardware. Only malformed packets fail above.
|
||||
}
|
||||
|
||||
const uint32_t dmaId = m_nextDmaId++;
|
||||
if (m_nextDmaId == 0u || m_nextDmaId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))
|
||||
m_nextDmaId = 1u;
|
||||
setV0(dmaId);
|
||||
return true;
|
||||
}
|
||||
case 17: // sceSifRegisterRpc
|
||||
{
|
||||
RpcServer server;
|
||||
server.serverData = cpu.gpr[4];
|
||||
server.sid = cpu.gpr[5];
|
||||
server.function = cpu.gpr[6];
|
||||
server.gp = cpu.gpr[28];
|
||||
server.buffer = cpu.gpr[7];
|
||||
const uint32_t stackPointer = cpu.gpr[29];
|
||||
server.callback = m_memory.read32(stackPointer + 16u);
|
||||
server.callbackBuffer = m_memory.read32(stackPointer + 20u);
|
||||
server.queue = m_memory.read32(stackPointer + 24u);
|
||||
m_servers[server.sid] = server;
|
||||
if (server.serverData != 0u)
|
||||
{
|
||||
m_memory.write32(server.serverData + 0x20u, server.sid);
|
||||
m_memory.write32(server.serverData + 0x28u, server.function);
|
||||
m_memory.write32(server.serverData + 0x2Cu, server.buffer);
|
||||
}
|
||||
setV0(server.serverData);
|
||||
return true;
|
||||
}
|
||||
case 18:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 19: // SetRpcQueue
|
||||
setV0(cpu.gpr[4]);
|
||||
return true;
|
||||
case 20:
|
||||
case 21:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 22: // RpcLoop
|
||||
m_kernel.sleepCurrent(cpu);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 23:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 24: // RemoveRpc
|
||||
{
|
||||
const uint32_t serverData = cpu.gpr[4];
|
||||
for (auto server = m_servers.begin(); server != m_servers.end(); ++server)
|
||||
{
|
||||
if (server->second.serverData == serverData)
|
||||
{
|
||||
m_servers.erase(server);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 25:
|
||||
case 26:
|
||||
case 27:
|
||||
case 28:
|
||||
case 29:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RpcResult IopRpcBridge::handleRpc(const RpcRequest &request, IopGuestExecutor &executor)
|
||||
{
|
||||
RpcResult result{};
|
||||
const auto serverIt = m_servers.find(request.sid);
|
||||
if (serverIt == m_servers.end() || serverIt->second.function == 0u)
|
||||
return result;
|
||||
|
||||
RpcServer &server = serverIt->second;
|
||||
if (request.send.size != 0u && server.buffer != 0u)
|
||||
{
|
||||
const uint32_t copySize = std::min<uint32_t>(request.send.size, IopMemory::RamSize - std::min(server.buffer, IopMemory::RamSize));
|
||||
if (copySize != 0u)
|
||||
{
|
||||
std::vector<uint8_t> payload(copySize);
|
||||
if (m_host.readGuest(request.send.address, payload.data(), payload.size()))
|
||||
(void)m_memory.writeRam(server.buffer, payload.data(), payload.size());
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t returnPointer = executor.executeGuestFunction(server.function,
|
||||
request.function,
|
||||
server.buffer,
|
||||
request.send.size,
|
||||
0u,
|
||||
server.gp);
|
||||
if (returnPointer == 0u)
|
||||
returnPointer = server.buffer;
|
||||
if (request.receive.address != 0u && request.receive.size != 0u && returnPointer != 0u)
|
||||
{
|
||||
const uint32_t physical = IopMemory::physicalAddress(returnPointer);
|
||||
if (physical < IopMemory::RamSize)
|
||||
{
|
||||
const uint32_t copySize = std::min<uint32_t>(request.receive.size, IopMemory::RamSize - physical);
|
||||
(void)m_host.writeGuest(request.receive.address, m_memory.ram().data() + physical, copySize);
|
||||
if (copySize < request.receive.size)
|
||||
(void)m_host.zeroGuest(request.receive.address + copySize, request.receive.size - copySize);
|
||||
}
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
result.serverDispatchPolicy = ServerDispatchPolicy::Suppress;
|
||||
result.signalNowaitCompletion = true;
|
||||
result.signalCompletion = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
void IopRpcBridge::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
// The EE SIF transport owns the actual directional memory movement.
|
||||
// Services still receive both phases through IopSubsystem, but mirroring
|
||||
// IOP bytes through an equal-numbered EE address would alias two distinct
|
||||
// PS2 address spaces and can overwrite live game data.
|
||||
(void)transfer;
|
||||
}
|
||||
|
||||
void IopRpcBridge::removeServersInRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto server = m_servers.begin(); server != m_servers.end();)
|
||||
{
|
||||
const uint32_t function = IopMemory::physicalAddress(server->second.function);
|
||||
if (function >= base && function < base + size)
|
||||
server = m_servers.erase(server);
|
||||
else
|
||||
++server;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopRpcBridge::hasServer(uint32_t sid) const noexcept
|
||||
{
|
||||
const auto server = m_servers.find(sid);
|
||||
return server != m_servers.end() && server->second.function != 0u;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
class IopMemory;
|
||||
|
||||
class IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
virtual ~IopGuestExecutor() = default;
|
||||
|
||||
[[nodiscard]] virtual uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp) = 0;
|
||||
[[nodiscard]] virtual uint32_t executeGuestFunctionWithBudget(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp,
|
||||
uint32_t instructionBudget)
|
||||
{
|
||||
return executeGuestFunction(address, a0, a1, a2, a3, gp);
|
||||
}
|
||||
};
|
||||
|
||||
class IopRpcBridge
|
||||
{
|
||||
public:
|
||||
IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] bool dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request, IopGuestExecutor &executor);
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
void removeServersInRange(uint32_t base, uint32_t size);
|
||||
|
||||
[[nodiscard]] bool hasServer(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] size_t serverCount() const noexcept { return m_servers.size(); }
|
||||
|
||||
private:
|
||||
struct RpcServer
|
||||
{
|
||||
uint32_t sid = 0;
|
||||
uint32_t serverData = 0;
|
||||
uint32_t function = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t buffer = 0;
|
||||
uint32_t callback = 0;
|
||||
uint32_t callbackBuffer = 0;
|
||||
uint32_t queue = 0;
|
||||
};
|
||||
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
IopKernel &m_kernel;
|
||||
std::unordered_map<uint32_t, RpcServer> m_servers;
|
||||
uint32_t m_nextDmaId = 1u;
|
||||
bool m_sifInitialized = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "iop_module_manager.h"
|
||||
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopModuleManager::IopModuleManager()
|
||||
{
|
||||
// ROM modules that the no-BIOS HLE environment can legitimately provide.
|
||||
// Entries with RPC services become routable only after load.
|
||||
constexpr std::string_view modules[] = {
|
||||
"sysmem",
|
||||
"loadcore",
|
||||
"intrman",
|
||||
"sifman",
|
||||
"sifcmd",
|
||||
"sifinit",
|
||||
"ioman",
|
||||
"iomanx",
|
||||
"modload",
|
||||
"stdio",
|
||||
"sysclib",
|
||||
"thbase",
|
||||
"thevent",
|
||||
"thsemap",
|
||||
"thmsgbx",
|
||||
"timrman",
|
||||
"vblank",
|
||||
"secrman",
|
||||
"sio2man",
|
||||
"xsio2man",
|
||||
"sio2d",
|
||||
"padman",
|
||||
"xpadman",
|
||||
"mcman",
|
||||
"xmcman",
|
||||
"mcserv",
|
||||
"libsd",
|
||||
"cdvdman",
|
||||
"cdvdfsv",
|
||||
"dev9",
|
||||
"usbd",
|
||||
"usbhdfsd",
|
||||
"udnl",
|
||||
"fileio",
|
||||
"poweroff",
|
||||
"netman",
|
||||
"ps2ip",
|
||||
"dbcman",
|
||||
"dbcm",
|
||||
};
|
||||
for (const std::string_view module : modules)
|
||||
m_builtinKeys.emplace(module);
|
||||
}
|
||||
|
||||
void IopModuleManager::reset()
|
||||
{
|
||||
m_records.clear();
|
||||
m_hleIdsByKey.clear();
|
||||
m_loadedKeyReferences.clear();
|
||||
m_nextHleId = 0x40000000;
|
||||
}
|
||||
|
||||
void IopModuleManager::setServiceModuleKeys(std::vector<std::string> keys)
|
||||
{
|
||||
m_serviceKeys.clear();
|
||||
for (std::string &key : keys)
|
||||
{
|
||||
const std::string normalized = ps2PathLeafKey(key);
|
||||
if (!normalized.empty())
|
||||
m_serviceKeys.emplace(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadResult IopModuleManager::loadHle(std::string_view path)
|
||||
{
|
||||
ModuleLoadResult result{true, -1, -1};
|
||||
const std::string key = ps2PathLeafKey(path);
|
||||
if (key.empty() || (!m_builtinKeys.contains(key) && !m_serviceKeys.contains(key)))
|
||||
return result;
|
||||
|
||||
const auto existing = m_hleIdsByKey.find(key);
|
||||
if (existing != m_hleIdsByKey.end())
|
||||
{
|
||||
Record &record = m_records[existing->second];
|
||||
++record.references;
|
||||
addLoadedKey(key);
|
||||
result.moduleId = existing->second;
|
||||
result.startResult = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (m_nextHleId <= 0)
|
||||
return result;
|
||||
const int32_t id = m_nextHleId++;
|
||||
m_records.emplace(id, Record{key, 1u, false});
|
||||
m_hleIdsByKey.emplace(key, id);
|
||||
addLoadedKey(key);
|
||||
result.moduleId = id;
|
||||
result.startResult = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
void IopModuleManager::observePhysicalLoad(int32_t moduleId, std::string_view path)
|
||||
{
|
||||
if (moduleId <= 0)
|
||||
return;
|
||||
const std::string key = ps2PathLeafKey(path);
|
||||
if (key.empty())
|
||||
return;
|
||||
m_records[moduleId] = Record{key, 1u, true};
|
||||
addLoadedKey(key);
|
||||
}
|
||||
|
||||
bool IopModuleManager::stopHle(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
const auto found = m_records.find(moduleId);
|
||||
if (found == m_records.end() || found->second.physical)
|
||||
return false;
|
||||
|
||||
Record &record = found->second;
|
||||
removeLoadedKey(record.key);
|
||||
if (record.references > 1u)
|
||||
{
|
||||
--record.references;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_hleIdsByKey.erase(record.key);
|
||||
m_records.erase(found);
|
||||
}
|
||||
if (result)
|
||||
*result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IopModuleManager::observePhysicalStop(int32_t moduleId)
|
||||
{
|
||||
const auto found = m_records.find(moduleId);
|
||||
if (found == m_records.end() || !found->second.physical)
|
||||
return;
|
||||
removeLoadedKey(found->second.key);
|
||||
m_records.erase(found);
|
||||
}
|
||||
|
||||
bool IopModuleManager::isLoaded(std::span<const std::string_view> aliases) const
|
||||
{
|
||||
if (aliases.empty())
|
||||
return true;
|
||||
return std::any_of(aliases.begin(), aliases.end(), [&](std::string_view alias)
|
||||
{
|
||||
const std::string key = ps2PathLeafKey(alias);
|
||||
const auto found = m_loadedKeyReferences.find(key);
|
||||
return found != m_loadedKeyReferences.end() && found->second != 0u; });
|
||||
}
|
||||
|
||||
bool IopModuleManager::recognizes(std::string_view path) const
|
||||
{
|
||||
const std::string key = ps2PathLeafKey(path);
|
||||
return m_builtinKeys.contains(key) || m_serviceKeys.contains(key);
|
||||
}
|
||||
|
||||
void IopModuleManager::addLoadedKey(std::string_view key)
|
||||
{
|
||||
++m_loadedKeyReferences[std::string(key)];
|
||||
}
|
||||
|
||||
void IopModuleManager::removeLoadedKey(std::string_view key)
|
||||
{
|
||||
const auto found = m_loadedKeyReferences.find(std::string(key));
|
||||
if (found == m_loadedKeyReferences.end())
|
||||
return;
|
||||
if (found->second > 1u)
|
||||
--found->second;
|
||||
else
|
||||
m_loadedKeyReferences.erase(found);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopModuleManager
|
||||
{
|
||||
public:
|
||||
IopModuleManager();
|
||||
|
||||
void reset();
|
||||
void setServiceModuleKeys(std::vector<std::string> keys);
|
||||
|
||||
[[nodiscard]] ModuleLoadResult loadHle(std::string_view path);
|
||||
void observePhysicalLoad(int32_t moduleId, std::string_view path);
|
||||
[[nodiscard]] bool stopHle(int32_t moduleId, int32_t *result);
|
||||
void observePhysicalStop(int32_t moduleId);
|
||||
|
||||
[[nodiscard]] bool isLoaded(std::span<const std::string_view> aliases) const;
|
||||
[[nodiscard]] bool recognizes(std::string_view path) const;
|
||||
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
std::string key;
|
||||
uint32_t references = 0u;
|
||||
bool physical = false;
|
||||
};
|
||||
|
||||
void addLoadedKey(std::string_view key);
|
||||
void removeLoadedKey(std::string_view key);
|
||||
|
||||
std::unordered_set<std::string> m_builtinKeys;
|
||||
std::unordered_set<std::string> m_serviceKeys;
|
||||
std::unordered_map<int32_t, Record> m_records;
|
||||
std::unordered_map<std::string, int32_t> m_hleIdsByKey;
|
||||
std::unordered_map<std::string, uint32_t> m_loadedKeyReferences;
|
||||
int32_t m_nextHleId = 0x40000000;
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
@@ -18,6 +17,12 @@ namespace ps2x::iop::detail
|
||||
|
||||
[[nodiscard]] virtual std::string_view name() const = 0;
|
||||
[[nodiscard]] virtual std::span<const uint32_t> sids() const = 0;
|
||||
// A service with aliases is dormant until one of these IOP modules is
|
||||
// actually loaded.
|
||||
[[nodiscard]] virtual std::span<const std::string_view> moduleAliases() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
virtual void reset() = 0;
|
||||
|
||||
[[nodiscard]] virtual RpcAbi selectRpcAbi(const RpcAbiRequest &request) const
|
||||
@@ -40,16 +45,4 @@ namespace ps2x::iop::detail
|
||||
};
|
||||
|
||||
using ServiceList = std::vector<std::unique_ptr<IopService>>;
|
||||
using ProfileFactory = std::function<ServiceList(IopHost &, const GameIdentity &)>;
|
||||
|
||||
struct ProfileDefinition
|
||||
{
|
||||
std::string id;
|
||||
std::string provider = "builtin";
|
||||
GameMatcher matcher;
|
||||
ProfileFactory factory;
|
||||
};
|
||||
|
||||
ServiceList createCoreServices(IopHost &host);
|
||||
std::vector<ProfileDefinition> createBuiltinProfiles();
|
||||
}
|
||||
|
||||
+175
-237
@@ -1,123 +1,91 @@
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include "iop_service.h"
|
||||
#include "plugin_loader.h"
|
||||
#include "iop_module_manager.h"
|
||||
#include "emulator/iop_emulator.h"
|
||||
#include "module_factories.h"
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool equalsIgnoreCaseAscii(std::string_view lhs, std::string_view rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
const auto left = static_cast<unsigned char>(lhs[i]);
|
||||
const auto right = static_cast<unsigned char>(rhs[i]);
|
||||
if (std::tolower(left) != std::tolower(right))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int matchSpecificity(const GameMatcher &matcher, const GameIdentity &identity)
|
||||
{
|
||||
int specificity = 0;
|
||||
if (!matcher.elfName.empty())
|
||||
{
|
||||
if (!equalsIgnoreCaseAscii(matcher.elfName, identity.elfName))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
++specificity;
|
||||
}
|
||||
if (matcher.entryPoint != 0)
|
||||
{
|
||||
if (matcher.entryPoint != identity.entryPoint)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
++specificity;
|
||||
}
|
||||
if (matcher.crc32 != 0)
|
||||
{
|
||||
if (matcher.crc32 != identity.crc32)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
++specificity;
|
||||
}
|
||||
return specificity;
|
||||
}
|
||||
}
|
||||
|
||||
class IopSubsystem::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl(IopHost &hostRef)
|
||||
: host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles())
|
||||
: host(hostRef),
|
||||
emulator(hostRef)
|
||||
{
|
||||
coreServices.emplace_back(detail::createMcservService(host));
|
||||
coreServices.emplace_back(detail::createDbcmanService(host));
|
||||
coreServices.emplace_back(detail::createLibSdService(host));
|
||||
refreshServiceModuleKeys();
|
||||
rebuildRoutes();
|
||||
}
|
||||
|
||||
bool serviceActive(const detail::IopService &service) const
|
||||
{
|
||||
return moduleManager.isLoaded(service.moduleAliases());
|
||||
}
|
||||
|
||||
void refreshServiceModuleKeys()
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (const auto &service : coreServices)
|
||||
{
|
||||
for (std::string_view alias : service->moduleAliases())
|
||||
keys.emplace_back(alias);
|
||||
}
|
||||
moduleManager.setServiceModuleKeys(std::move(keys));
|
||||
}
|
||||
|
||||
void rebuildRoutes()
|
||||
{
|
||||
routes.clear();
|
||||
auto addLayer = [&](detail::ServiceList &services, bool profileSpecific) -> bool
|
||||
lastError.clear();
|
||||
for (const auto &service : coreServices)
|
||||
{
|
||||
std::unordered_map<uint32_t, detail::IopService *> layer;
|
||||
for (const auto &service : services)
|
||||
if (!serviceActive(*service))
|
||||
continue;
|
||||
for (const uint32_t sid : service->sids())
|
||||
{
|
||||
if (!service)
|
||||
if (!routes.emplace(sid, service.get()).second)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const uint32_t sid : service->sids())
|
||||
{
|
||||
if (!layer.emplace(sid, service.get()).second)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "duplicate IOP SID 0x" << std::hex << sid << " in " << (profileSpecific ? "profile" : "core") << " layer";
|
||||
lastError = out.str();
|
||||
return false;
|
||||
}
|
||||
std::ostringstream out;
|
||||
out << "duplicate IOP SID 0x" << std::hex << sid << " in core services";
|
||||
lastError = out.str();
|
||||
routes.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const auto &[sid, service] : layer)
|
||||
{
|
||||
routes[sid] = service;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
routesValid = addLayer(coreServices, false) && addLayer(profileServices, true);
|
||||
void recordLoadOutcome(std::string_view path, bool hle)
|
||||
{
|
||||
constexpr size_t maxOutcomes = 32u;
|
||||
if (loadOutcomes.size() >= maxOutcomes || !loggedLoadPaths.emplace(path).second)
|
||||
return;
|
||||
std::string message = hle ? "[IOP:HLE] fallback module='" : "[IOP:load-failed] module='";
|
||||
message.append(path);
|
||||
message += hle ? "' physical IRX unavailable; using registered HLE provider"
|
||||
: "' no HLE provider accepted the module; physical IRX was not loaded";
|
||||
loadOutcomes.push_back(message);
|
||||
host.log(hle ? LogLevel::Info : LogLevel::Warning, message);
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
detail::PluginCatalog pluginCatalog;
|
||||
detail::ServiceList coreServices;
|
||||
detail::ServiceList profileServices;
|
||||
std::vector<detail::ProfileDefinition> profiles;
|
||||
std::unordered_map<uint32_t, detail::IopService *> routes;
|
||||
std::vector<std::filesystem::path> pluginSearchPaths;
|
||||
std::vector<std::string> diagnostics;
|
||||
std::string activeProfile;
|
||||
std::string activeProvider;
|
||||
std::vector<std::string> loadOutcomes;
|
||||
std::unordered_set<std::string> loggedLoadPaths;
|
||||
std::string lastError;
|
||||
bool routesValid = true;
|
||||
detail::IopModuleManager moduleManager;
|
||||
detail::IopEmulator emulator;
|
||||
};
|
||||
|
||||
IopSubsystem::IopSubsystem(IopHost &host)
|
||||
@@ -129,111 +97,11 @@ namespace ps2x::iop
|
||||
IopSubsystem::IopSubsystem(IopSubsystem &&) noexcept = default;
|
||||
IopSubsystem &IopSubsystem::operator=(IopSubsystem &&) noexcept = default;
|
||||
|
||||
void IopSubsystem::setPluginSearchPaths(std::vector<std::filesystem::path> paths)
|
||||
{
|
||||
m_impl->pluginSearchPaths = std::move(paths);
|
||||
}
|
||||
|
||||
bool IopSubsystem::loadPlugins(std::string *error)
|
||||
{
|
||||
return m_impl->pluginCatalog.load(m_impl->pluginSearchPaths, m_impl->profiles, m_impl->diagnostics, error);
|
||||
}
|
||||
|
||||
bool IopSubsystem::configure(const GameIdentity &identity, std::string *error)
|
||||
{
|
||||
m_impl->profileServices.clear();
|
||||
m_impl->activeProfile.clear();
|
||||
m_impl->activeProvider.clear();
|
||||
m_impl->lastError.clear();
|
||||
|
||||
const detail::ProfileDefinition *selected = nullptr;
|
||||
const detail::ProfileDefinition *selectedTie = nullptr;
|
||||
int selectedSpecificity = -1;
|
||||
for (const auto &profile : m_impl->profiles)
|
||||
{
|
||||
const int specificity = matchSpecificity(profile.matcher, identity);
|
||||
if (specificity < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (specificity > selectedSpecificity)
|
||||
{
|
||||
selected = &profile;
|
||||
selectedTie = nullptr;
|
||||
selectedSpecificity = specificity;
|
||||
continue;
|
||||
}
|
||||
if (specificity == selectedSpecificity && selected)
|
||||
{
|
||||
selectedTie = &profile;
|
||||
}
|
||||
}
|
||||
|
||||
if (selected && selectedTie)
|
||||
{
|
||||
m_impl->lastError = "ambiguous IOP profiles '" + selected->provider + ":" +
|
||||
selected->id + "' and '" + selectedTie->provider + ":" +
|
||||
selectedTie->id + "'";
|
||||
if (error)
|
||||
{
|
||||
*error = m_impl->lastError;
|
||||
}
|
||||
m_impl->rebuildRoutes();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selected)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_impl->profileServices = selected->factory(m_impl->host, identity);
|
||||
m_impl->activeProfile = selected->id;
|
||||
m_impl->activeProvider = selected->provider;
|
||||
}
|
||||
catch (const std::exception &exception)
|
||||
{
|
||||
m_impl->lastError = "failed to create IOP profile '" + selected->id + "': " + exception.what();
|
||||
if (error)
|
||||
{
|
||||
*error = m_impl->lastError;
|
||||
}
|
||||
m_impl->rebuildRoutes();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_impl->lastError = "failed to create IOP profile '" + selected->id + "': unknown plugin exception";
|
||||
if (error)
|
||||
{
|
||||
*error = m_impl->lastError;
|
||||
}
|
||||
m_impl->rebuildRoutes();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_impl->rebuildRoutes();
|
||||
if (!m_impl->routesValid)
|
||||
{
|
||||
const std::string routeError = m_impl->lastError;
|
||||
m_impl->profileServices.clear();
|
||||
m_impl->activeProfile.clear();
|
||||
m_impl->activeProvider.clear();
|
||||
m_impl->rebuildRoutes();
|
||||
m_impl->lastError = routeError;
|
||||
if (error)
|
||||
{
|
||||
*error = m_impl->lastError;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IopSubsystem::reset()
|
||||
{
|
||||
m_impl->moduleManager.reset();
|
||||
m_impl->loadOutcomes.clear();
|
||||
m_impl->loggedLoadPaths.clear();
|
||||
for (auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service)
|
||||
@@ -241,31 +109,71 @@ namespace ps2x::iop
|
||||
service->reset();
|
||||
}
|
||||
}
|
||||
for (auto &service : m_impl->profileServices)
|
||||
m_impl->emulator.reset();
|
||||
m_impl->refreshServiceModuleKeys();
|
||||
m_impl->rebuildRoutes();
|
||||
}
|
||||
|
||||
ModuleLoadResult IopSubsystem::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
const ParsedPs2Path parsed = parsePs2Path(path);
|
||||
if (!parsed)
|
||||
return {true, -1, -1};
|
||||
|
||||
if (parsed.device != Ps2PathDevice::Rom0)
|
||||
{
|
||||
if (service)
|
||||
ModuleLoadResult physical = m_impl->emulator.loadModule(path, arguments, argumentSize);
|
||||
if (physical.moduleId > 0)
|
||||
{
|
||||
service->reset();
|
||||
m_impl->moduleManager.observePhysicalLoad(physical.moduleId, path);
|
||||
m_impl->rebuildRoutes();
|
||||
return physical;
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadResult hle = m_impl->moduleManager.loadHle(path);
|
||||
if (hle.moduleId > 0)
|
||||
{
|
||||
m_impl->rebuildRoutes();
|
||||
if (parsed.device != Ps2PathDevice::Rom0)
|
||||
m_impl->recordLoadOutcome(path, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_impl->recordLoadOutcome(path, false);
|
||||
}
|
||||
return hle;
|
||||
}
|
||||
|
||||
ModuleLoadResult IopSubsystem::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->emulator.loadModuleBuffer(guestAddress, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool IopSubsystem::stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
if (m_impl->moduleManager.stopHle(moduleId, result))
|
||||
{
|
||||
m_impl->rebuildRoutes();
|
||||
return true;
|
||||
}
|
||||
if (!m_impl->emulator.stopModule(moduleId, result))
|
||||
return false;
|
||||
m_impl->moduleManager.observePhysicalStop(moduleId);
|
||||
m_impl->rebuildRoutes();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IopSubsystem::runEeCycles(uint64_t eeCycles) noexcept
|
||||
{
|
||||
m_impl->emulator.runEeCycles(eeCycles);
|
||||
}
|
||||
|
||||
RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const
|
||||
{
|
||||
for (const auto &service : m_impl->profileServices)
|
||||
{
|
||||
if (service)
|
||||
{
|
||||
const RpcAbi selected = service->selectRpcAbi(request);
|
||||
if (selected != RpcAbi::RuntimeDefault)
|
||||
{
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service)
|
||||
if (service && m_impl->serviceActive(*service))
|
||||
{
|
||||
const RpcAbi selected = service->selectRpcAbi(request);
|
||||
if (selected != RpcAbi::RuntimeDefault)
|
||||
@@ -277,63 +185,93 @@ namespace ps2x::iop
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
bool IopSubsystem::canBindRpc(uint32_t sid) const noexcept
|
||||
{
|
||||
if (m_impl->routes.find(sid) != m_impl->routes.end())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return m_impl->emulator.hasRpcServer(sid);
|
||||
}
|
||||
|
||||
RpcResult IopSubsystem::handleRpc(const RpcRequest &request)
|
||||
{
|
||||
const auto it = m_impl->routes.find(request.sid);
|
||||
if (it == m_impl->routes.end() || !it->second)
|
||||
const auto route = m_impl->routes.find(request.sid);
|
||||
detail::IopService *hle = route != m_impl->routes.end() ? route->second : nullptr;
|
||||
|
||||
RpcResult emulated = m_impl->emulator.handleRpc(request);
|
||||
if (emulated.handled || !hle)
|
||||
{
|
||||
return {};
|
||||
return emulated;
|
||||
}
|
||||
return it->second->handleRpc(request);
|
||||
return hle->handleRpc(request);
|
||||
}
|
||||
|
||||
void IopSubsystem::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
for (auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service)
|
||||
{
|
||||
service->onSifTransfer(transfer);
|
||||
}
|
||||
}
|
||||
for (auto &service : m_impl->profileServices)
|
||||
{
|
||||
if (service)
|
||||
if (service && m_impl->serviceActive(*service))
|
||||
{
|
||||
service->onSifTransfer(transfer);
|
||||
}
|
||||
}
|
||||
m_impl->emulator.onSifTransfer(transfer);
|
||||
}
|
||||
|
||||
uint32_t IopSubsystem::allocateMemory(uint32_t size, uint32_t alignment)
|
||||
{
|
||||
return m_impl->emulator.allocateMemory(size, alignment);
|
||||
}
|
||||
|
||||
bool IopSubsystem::freeMemory(uint32_t address)
|
||||
{
|
||||
return m_impl->emulator.freeMemory(address);
|
||||
}
|
||||
|
||||
bool IopSubsystem::readMemory(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
return m_impl->emulator.readMemory(address, destination, size);
|
||||
}
|
||||
|
||||
bool IopSubsystem::writeMemory(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
return m_impl->emulator.writeMemory(address, source, size);
|
||||
}
|
||||
|
||||
bool IopSubsystem::zeroMemory(uint32_t address, size_t size)
|
||||
{
|
||||
return m_impl->emulator.zeroMemory(address, size);
|
||||
}
|
||||
|
||||
bool IopSubsystem::isMemoryRange(uint32_t address, size_t size) const
|
||||
{
|
||||
return m_impl->emulator.isMemoryRange(address, size);
|
||||
}
|
||||
|
||||
DebugSnapshot IopSubsystem::debugSnapshot() const
|
||||
{
|
||||
DebugSnapshot snapshot;
|
||||
snapshot.activeProfile = m_impl->activeProfile;
|
||||
snapshot.activeProvider = m_impl->activeProvider;
|
||||
snapshot.diagnostics = m_impl->diagnostics;
|
||||
snapshot.emulatorCycles = m_impl->emulator.cycles();
|
||||
snapshot.emulatorInstructions = m_impl->emulator.instructions();
|
||||
snapshot.emulatorLoadedModules = m_impl->emulator.loadedModuleCount();
|
||||
snapshot.emulatorThreads = m_impl->emulator.threadCount();
|
||||
snapshot.emulatorRpcServers = m_impl->emulator.rpcServerCount();
|
||||
snapshot.diagnostics = m_impl->loadOutcomes;
|
||||
if (!m_impl->lastError.empty())
|
||||
{
|
||||
snapshot.diagnostics.push_back(m_impl->lastError);
|
||||
}
|
||||
|
||||
auto append = [&](const detail::ServiceList &services, bool profileSpecific)
|
||||
for (const auto &service : m_impl->coreServices)
|
||||
{
|
||||
for (const auto &service : services)
|
||||
{
|
||||
if (!service)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
DebugService row;
|
||||
row.name = service->name();
|
||||
row.sids.assign(service->sids().begin(), service->sids().end());
|
||||
row.profileSpecific = profileSpecific;
|
||||
service->appendDebugMetrics(row.metrics);
|
||||
snapshot.services.push_back(std::move(row));
|
||||
}
|
||||
};
|
||||
append(m_impl->coreServices, false);
|
||||
append(m_impl->profileServices, true);
|
||||
DebugService row;
|
||||
row.name = service->name();
|
||||
row.sids.assign(service->sids().begin(), service->sids().end());
|
||||
row.active = m_impl->serviceActive(*service);
|
||||
service->appendDebugMetrics(row.metrics);
|
||||
snapshot.services.push_back(std::move(row));
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,156 +2,9 @@
|
||||
|
||||
#include "iop_service.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct CriDtxBindings
|
||||
{
|
||||
std::string serviceName;
|
||||
uint32_t sid = 0u;
|
||||
uint32_t urpcObjectBase = 0u;
|
||||
uint32_t urpcObjectLimit = 0u;
|
||||
uint32_t urpcObjectStride = 0u;
|
||||
uint32_t urpcFunctionTableBase = 0u;
|
||||
uint32_t urpcObjectTableBase = 0u;
|
||||
uint32_t dispatcherFunctionAddress = 0u;
|
||||
uint32_t rpcServerPoolBase = 0u;
|
||||
uint32_t rpcServerStride = 0u;
|
||||
};
|
||||
|
||||
enum class TsnddrvProtocolVariant
|
||||
{
|
||||
SndQueueV1,
|
||||
};
|
||||
|
||||
struct TsnddrvGuestArena
|
||||
{
|
||||
uint32_t base = 0u;
|
||||
uint32_t limit = 0u;
|
||||
uint32_t statusAlignment = 0u;
|
||||
uint32_t tableAlignment = 0u;
|
||||
uint32_t storageAlignment = 0u;
|
||||
uint32_t hdBytes = 0u;
|
||||
uint32_t sqBytes = 0u;
|
||||
uint32_t dataBytes = 0u;
|
||||
};
|
||||
|
||||
struct TsnddrvChecksumTables
|
||||
{
|
||||
uint32_t seAddress = 0u;
|
||||
uint32_t midiAddress = 0u;
|
||||
};
|
||||
|
||||
struct TsnddrvCompletionRule
|
||||
{
|
||||
uint32_t eeFunction = 0u;
|
||||
bool suppressGuestCallback = false;
|
||||
bool signalCompletion = false;
|
||||
bool clearBusy = false;
|
||||
};
|
||||
|
||||
struct TsnddrvBindings
|
||||
{
|
||||
std::string serviceName;
|
||||
TsnddrvProtocolVariant protocol = TsnddrvProtocolVariant::SndQueueV1;
|
||||
TsnddrvGuestArena arena;
|
||||
std::vector<TsnddrvChecksumTables> checksumCandidates;
|
||||
uint32_t busyFlagAddress = 0u;
|
||||
std::vector<TsnddrvCompletionRule> completionRules;
|
||||
};
|
||||
|
||||
struct ClFileRpcLayout
|
||||
{
|
||||
uint32_t directLoadFunction = 0x01u;
|
||||
uint32_t getStatusFunction = 0x03u;
|
||||
uint32_t initializeFunction = 0x04u;
|
||||
uint32_t waitFunction = 0x05u;
|
||||
uint32_t getSizeFunction = 0x06u;
|
||||
uint32_t openFunction = 0x08u;
|
||||
uint32_t closeFunction = 0x09u;
|
||||
uint32_t readFunction = 0x0Au;
|
||||
uint32_t secondaryWaitFunction = 0x15u;
|
||||
uint32_t setRootFunction = 0x16u;
|
||||
uint32_t pathBytes = 0x100u;
|
||||
uint32_t directLoadSizeOffset = 0x100u;
|
||||
uint32_t directLoadDestinationOffset = 0x104u;
|
||||
uint32_t responseStatusOffset = 0u;
|
||||
uint32_t responseValueOffset = 4u;
|
||||
uint32_t responseClearBytes = 0x40u;
|
||||
uint32_t maximumReadBytes = 0x2000u;
|
||||
uint32_t loadResultQueued = 5u;
|
||||
uint32_t loadStatusFailed = 3u;
|
||||
uint32_t loadStatusComplete = 7u;
|
||||
uint32_t invalidHandleStatus = 9u;
|
||||
uint32_t firstLoadHandle = 0x00010000u;
|
||||
bool acknowledgeUnknownFunctions = true;
|
||||
};
|
||||
|
||||
struct ClFileBindings
|
||||
{
|
||||
std::string serviceName;
|
||||
uint32_t sid = 0u;
|
||||
ClFileRpcLayout rpc;
|
||||
};
|
||||
|
||||
// TODO This is for the lord of the rings better name for that one
|
||||
struct SoundUpdateStubBindings
|
||||
{
|
||||
std::string serviceName;
|
||||
uint32_t sid = 0u;
|
||||
uint32_t activeStreamCountOffset = 0u;
|
||||
uint32_t responseCounterOffset = 0u;
|
||||
bool zeroReceiveBuffer = true;
|
||||
bool signalNowaitCompletion = false;
|
||||
bool completeQueuedPlayStreams = false;
|
||||
std::vector<uint32_t> suppressedCompletionCallbacks;
|
||||
};
|
||||
|
||||
struct SdrdrvBindings
|
||||
{
|
||||
std::string serviceName;
|
||||
uint32_t sid = 0u;
|
||||
uint32_t imageHeaderAddress = 0u;
|
||||
uint32_t sectorSize = 0u;
|
||||
uint32_t statusOffset = 0u;
|
||||
uint32_t statusStride = 0u;
|
||||
uint32_t statusSlotMask = 0u;
|
||||
uint8_t completeValue = 0u;
|
||||
uint32_t initFunction = 0u;
|
||||
uint32_t submitFunction = 1u;
|
||||
uint32_t shutdownFunction = 2u;
|
||||
uint32_t headerCommand = 0x0Cu;
|
||||
uint32_t loadCommand = 0x0Eu;
|
||||
uint32_t commandBytes = 32u;
|
||||
uint32_t maxCommands = 32u;
|
||||
uint32_t lbnWord = 2u;
|
||||
uint32_t byteCountWord = 3u;
|
||||
uint32_t destinationWord = 4u;
|
||||
uint32_t destinationKindWord = 5u;
|
||||
uint32_t loadIdWord = 6u;
|
||||
uint32_t eeDestinationKind = 0u;
|
||||
bool fallbackBodyToCdImage = true;
|
||||
bool clearReceiveBeforeDispatch = true;
|
||||
bool completeFailedLoads = true;
|
||||
bool pretendNonEeLoadsComplete = true;
|
||||
uint32_t headerWarningLimit = 4u;
|
||||
uint32_t bodyWarningLimit = 8u;
|
||||
std::string imageHeaderLowerName;
|
||||
std::string imageHeaderUpperName;
|
||||
std::string imageBodyLowerName;
|
||||
std::string imageBodyUpperName;
|
||||
};
|
||||
|
||||
std::unique_ptr<IopService> createDbcmanService(IopHost &host);
|
||||
std::unique_ptr<IopService> createLibSdService(IopHost &host);
|
||||
std::unique_ptr<IopService> createMcservService(IopHost &host);
|
||||
std::unique_ptr<IopService> createTsnddrvService(IopHost &host, TsnddrvBindings bindings);
|
||||
std::unique_ptr<IopService> createCriDtxService(IopHost &host, CriDtxBindings bindings);
|
||||
std::unique_ptr<IopService> createClFileService(IopHost &host, ClFileBindings bindings);
|
||||
std::unique_ptr<IopService> createSoundUpdateStubService(IopHost &host, SoundUpdateStubBindings bindings);
|
||||
std::unique_ptr<IopService> createSdrdrvService(IopHost &host, SdrdrvBindings bindings);
|
||||
}
|
||||
|
||||
@@ -1,635 +0,0 @@
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class ClFileService final : public IopService
|
||||
{
|
||||
public:
|
||||
ClFileService(IopHost &host, ClFileBindings bindings)
|
||||
: m_host(host),
|
||||
m_bindings(std::move(bindings)),
|
||||
m_sids{m_bindings.sid},
|
||||
m_nextLoadHandle(m_bindings.rpc.firstLoadHandle)
|
||||
{
|
||||
}
|
||||
|
||||
~ClFileService() override
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override
|
||||
{
|
||||
return m_bindings.serviceName;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto &[handle, entry] : m_fileHandles)
|
||||
{
|
||||
(void)handle;
|
||||
if (entry.handle != 0u)
|
||||
{
|
||||
m_host.closeHostFile(entry.handle);
|
||||
entry.handle = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
m_fileHandles.clear();
|
||||
m_loads.clear();
|
||||
m_root.clear();
|
||||
m_nextFileHandle = 1u;
|
||||
m_nextLoadHandle = m_bindings.rpc.firstLoadHandle;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult result;
|
||||
if (request.sid != m_bindings.sid)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
const Operation operation = decodeFunction(request.function);
|
||||
if (operation == Operation::Unknown &&
|
||||
!m_bindings.rpc.acknowledgeUnknownFunctions)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
if (request.receive.address != 0u && request.receive.size != 0u)
|
||||
{
|
||||
(void)m_host.zeroGuest(request.receive.address,
|
||||
std::min(request.receive.size,
|
||||
m_bindings.rpc.responseClearBytes));
|
||||
}
|
||||
|
||||
const auto writeRpcResult = [&](int32_t status, uint32_t value)
|
||||
{
|
||||
writeResult(request.receive, status, value);
|
||||
};
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case Operation::DirectLoad:
|
||||
{
|
||||
const uint32_t stringBytes = request.send.size != 0u
|
||||
? std::min(request.send.size,
|
||||
m_bindings.rpc.pathBytes)
|
||||
: m_bindings.rpc.pathBytes;
|
||||
const std::string guestPath = readGuestString(request.send.address, stringBytes);
|
||||
uint32_t requestedBytes = 0u;
|
||||
uint32_t destinationAddress = 0u;
|
||||
(void)readGuestU32(request.send.address + m_bindings.rpc.directLoadSizeOffset,
|
||||
requestedBytes);
|
||||
(void)readGuestU32(request.send.address + m_bindings.rpc.directLoadDestinationOffset,
|
||||
destinationAddress);
|
||||
|
||||
uint32_t status = m_bindings.rpc.loadStatusFailed;
|
||||
uint32_t fileSize = 0u;
|
||||
const std::string hostPath = resolvePath(guestPath);
|
||||
if (!hostPath.empty())
|
||||
{
|
||||
const uint64_t file = m_host.openHostFile(hostPath);
|
||||
uint64_t hostFileSize = 0u;
|
||||
if (file != 0u && m_host.hostFileSize(file, hostFileSize))
|
||||
{
|
||||
fileSize = static_cast<uint32_t>(
|
||||
std::min<uint64_t>(hostFileSize, 0xFFFFFFFFull));
|
||||
const uint64_t maxRequestedBytes = requestedBytes != 0u
|
||||
? requestedBytes
|
||||
: hostFileSize;
|
||||
const uint64_t bytesToCopy = std::min(hostFileSize, maxRequestedBytes);
|
||||
|
||||
status = m_bindings.rpc.loadStatusComplete;
|
||||
if (destinationAddress != 0u && bytesToCopy != 0u)
|
||||
{
|
||||
if (!copyFileToGuest(file, destinationAddress, bytesToCopy))
|
||||
{
|
||||
status = m_bindings.rpc.loadStatusFailed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file != 0u)
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t loadHandle = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
loadHandle = allocateLoadLocked(status, fileSize);
|
||||
}
|
||||
writeRpcResult(static_cast<int32_t>(m_bindings.rpc.loadResultQueued), loadHandle);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Initialize:
|
||||
writeRpcResult(0, 1u);
|
||||
return result;
|
||||
|
||||
case Operation::Wait:
|
||||
case Operation::SecondaryWait:
|
||||
writeRpcResult(0, 0u);
|
||||
return result;
|
||||
|
||||
case Operation::SetRoot:
|
||||
{
|
||||
const std::string root = readGuestString(request.send.address,
|
||||
request.send.size != 0u
|
||||
? request.send.size
|
||||
: m_bindings.rpc.pathBytes);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_root = root;
|
||||
}
|
||||
writeRpcResult(0, 1u);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Open:
|
||||
{
|
||||
const std::string guestPath = readGuestString(request.send.address,
|
||||
request.send.size != 0u
|
||||
? request.send.size
|
||||
: m_bindings.rpc.pathBytes);
|
||||
const std::string hostPath = resolvePath(guestPath);
|
||||
if (hostPath.empty())
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
const uint64_t file = m_host.openHostFile(hostPath);
|
||||
if (file == 0u)
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t hostFileSize = 0u;
|
||||
if (!m_host.hostFileSize(file, hostFileSize))
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
const uint32_t fileSize = static_cast<uint32_t>(
|
||||
std::min<uint64_t>(hostFileSize, 0x7FFFFFFFull));
|
||||
|
||||
uint32_t handle = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
handle = allocateFileHandleLocked(file, fileSize);
|
||||
}
|
||||
if (handle == 0u)
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
writeRpcResult(0, handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Close:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
(void)readGuestU32(request.send.address, handle);
|
||||
|
||||
uint64_t file = 0u;
|
||||
bool closedLoad = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto fileIt = m_fileHandles.find(handle);
|
||||
if (fileIt != m_fileHandles.end())
|
||||
{
|
||||
file = fileIt->second.handle;
|
||||
m_fileHandles.erase(fileIt);
|
||||
}
|
||||
|
||||
const auto loadIt = m_loads.find(handle);
|
||||
if (loadIt != m_loads.end())
|
||||
{
|
||||
m_loads.erase(loadIt);
|
||||
closedLoad = true;
|
||||
}
|
||||
}
|
||||
if (file != 0u)
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
}
|
||||
|
||||
const bool closed = file != 0u || closedLoad;
|
||||
writeRpcResult(closed ? 0 : -1, closed ? 1u : 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Read:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
uint32_t requestedBytes = 0u;
|
||||
uint32_t destinationAddress = 0u;
|
||||
(void)readGuestU32(request.send.address + 0u, handle);
|
||||
(void)readGuestU32(request.send.address + 4u, requestedBytes);
|
||||
(void)readGuestU32(request.send.address + 8u, destinationAddress);
|
||||
|
||||
if (destinationAddress == 0u)
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bytes(std::min(requestedBytes,
|
||||
m_bindings.rpc.maximumReadBytes));
|
||||
size_t bytesRead = 0u;
|
||||
bool readFailed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto fileIt = m_fileHandles.find(handle);
|
||||
if (fileIt == m_fileHandles.end() || fileIt->second.handle == 0u)
|
||||
{
|
||||
readFailed = true;
|
||||
}
|
||||
else if (!bytes.empty())
|
||||
{
|
||||
size_t hostBytesRead = 0u;
|
||||
if (!m_host.readHostFile(fileIt->second.handle,
|
||||
fileIt->second.position,
|
||||
bytes.data(),
|
||||
bytes.size(),
|
||||
hostBytesRead))
|
||||
{
|
||||
readFailed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesRead = hostBytesRead;
|
||||
fileIt->second.position += hostBytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (readFailed ||
|
||||
(bytesRead != 0u &&
|
||||
!m_host.writeGuest(destinationAddress, bytes.data(), bytesRead)))
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
writeRpcResult(0, static_cast<uint32_t>(bytesRead));
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::GetStatus:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
(void)readGuestU32(request.send.address, handle);
|
||||
|
||||
bool loadFound = false;
|
||||
uint32_t loadStatus = 0u;
|
||||
bool fileFound = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto loadIt = m_loads.find(handle);
|
||||
if (loadIt != m_loads.end())
|
||||
{
|
||||
loadFound = true;
|
||||
loadStatus = loadIt->second.status;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileFound = m_fileHandles.find(handle) != m_fileHandles.end();
|
||||
}
|
||||
}
|
||||
|
||||
if (loadFound)
|
||||
{
|
||||
writeRpcResult(static_cast<int32_t>(loadStatus), 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
writeRpcResult(0, fileFound ? 0u : m_bindings.rpc.invalidHandleStatus);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::GetSize:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
(void)readGuestU32(request.send.address, handle);
|
||||
|
||||
bool found = false;
|
||||
uint32_t size = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto loadIt = m_loads.find(handle);
|
||||
if (loadIt != m_loads.end())
|
||||
{
|
||||
found = true;
|
||||
size = loadIt->second.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto fileIt = m_fileHandles.find(handle);
|
||||
if (fileIt != m_fileHandles.end())
|
||||
{
|
||||
found = true;
|
||||
size = fileIt->second.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeRpcResult(found ? 0 : -1, found ? size : 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Unknown:
|
||||
writeRpcResult(0, 0u);
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"open_files", m_fileHandles.size(), false});
|
||||
metrics.push_back({"load_records", m_loads.size(), false});
|
||||
metrics.push_back({"next_file_handle", m_nextFileHandle, true});
|
||||
metrics.push_back({"next_load_handle", m_nextLoadHandle, true});
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Operation
|
||||
{
|
||||
DirectLoad,
|
||||
GetStatus,
|
||||
Initialize,
|
||||
Wait,
|
||||
GetSize,
|
||||
Open,
|
||||
Close,
|
||||
Read,
|
||||
SecondaryWait,
|
||||
SetRoot,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
[[nodiscard]] Operation decodeFunction(uint32_t function) const
|
||||
{
|
||||
const ClFileRpcLayout &rpc = m_bindings.rpc;
|
||||
if (function == rpc.directLoadFunction) return Operation::DirectLoad;
|
||||
if (function == rpc.getStatusFunction) return Operation::GetStatus;
|
||||
if (function == rpc.initializeFunction) return Operation::Initialize;
|
||||
if (function == rpc.waitFunction) return Operation::Wait;
|
||||
if (function == rpc.getSizeFunction) return Operation::GetSize;
|
||||
if (function == rpc.openFunction) return Operation::Open;
|
||||
if (function == rpc.closeFunction) return Operation::Close;
|
||||
if (function == rpc.readFunction) return Operation::Read;
|
||||
if (function == rpc.secondaryWaitFunction) return Operation::SecondaryWait;
|
||||
if (function == rpc.setRootFunction) return Operation::SetRoot;
|
||||
return Operation::Unknown;
|
||||
}
|
||||
|
||||
struct ClFileHandle
|
||||
{
|
||||
uint64_t handle = 0u;
|
||||
uint32_t size = 0u;
|
||||
uint64_t position = 0u;
|
||||
};
|
||||
|
||||
struct ClFileLoad
|
||||
{
|
||||
uint32_t status = 0u;
|
||||
uint32_t size = 0u;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool readGuestU32(uint32_t address, uint32_t &value) const
|
||||
{
|
||||
value = 0u;
|
||||
return m_host.readGuest(address, &value, sizeof(value));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string readGuestString(uint32_t address, uint32_t maxBytes) const
|
||||
{
|
||||
if (address == 0u || maxBytes == 0u)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<char> bytes(maxBytes);
|
||||
if (!m_host.readGuest(address, bytes.data(), bytes.size()))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t length = 0u;
|
||||
while (length < bytes.size() && bytes[length] != '\0')
|
||||
{
|
||||
++length;
|
||||
}
|
||||
return std::string(bytes.data(), length);
|
||||
}
|
||||
|
||||
[[nodiscard]] static bool hasDevice(std::string_view path)
|
||||
{
|
||||
return path.find(':') != std::string_view::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::string joinGuestPath(const std::string &root,
|
||||
const std::string &leaf)
|
||||
{
|
||||
if (root.empty() || leaf.empty() || hasDevice(leaf))
|
||||
{
|
||||
return leaf;
|
||||
}
|
||||
|
||||
const char tail = root.back();
|
||||
if (tail == '/' || tail == '\\' || tail == ':')
|
||||
{
|
||||
return root + leaf;
|
||||
}
|
||||
return root + "/" + leaf;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string resolvePath(const std::string &path) const
|
||||
{
|
||||
std::string root;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
root = m_root;
|
||||
}
|
||||
|
||||
const std::string translated = m_host.translateGuestPath(joinGuestPath(root, path));
|
||||
return translated;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t allocateFileHandleLocked(uint64_t file, uint32_t size)
|
||||
{
|
||||
if (file == 0u)
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
|
||||
for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt)
|
||||
{
|
||||
uint32_t handle = m_nextFileHandle++;
|
||||
if (handle == 0u)
|
||||
{
|
||||
handle = m_nextFileHandle++;
|
||||
}
|
||||
if (m_fileHandles.find(handle) == m_fileHandles.end() &&
|
||||
m_loads.find(handle) == m_loads.end())
|
||||
{
|
||||
m_fileHandles.emplace(handle, ClFileHandle{file, size, 0u});
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t allocateLoadLocked(uint32_t status, uint32_t size)
|
||||
{
|
||||
for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt)
|
||||
{
|
||||
uint32_t handle = m_nextLoadHandle++;
|
||||
if (handle < 3u)
|
||||
{
|
||||
handle = m_bindings.rpc.firstLoadHandle;
|
||||
m_nextLoadHandle = m_bindings.rpc.firstLoadHandle + 1u;
|
||||
}
|
||||
if (m_loads.find(handle) == m_loads.end() &&
|
||||
m_fileHandles.find(handle) == m_fileHandles.end())
|
||||
{
|
||||
m_loads.emplace(handle, ClFileLoad{status, size});
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
void writeResult(GuestBuffer receive, int32_t status, uint32_t value)
|
||||
{
|
||||
if (receive.address != 0u &&
|
||||
receive.size >= m_bindings.rpc.responseStatusOffset + sizeof(uint32_t))
|
||||
{
|
||||
const uint32_t encodedStatus = static_cast<uint32_t>(status);
|
||||
(void)m_host.writeGuest(receive.address + m_bindings.rpc.responseStatusOffset,
|
||||
&encodedStatus,
|
||||
sizeof(encodedStatus));
|
||||
}
|
||||
if (receive.address != 0u &&
|
||||
receive.size >= m_bindings.rpc.responseValueOffset + sizeof(uint32_t))
|
||||
{
|
||||
(void)m_host.writeGuest(receive.address + m_bindings.rpc.responseValueOffset,
|
||||
&value,
|
||||
sizeof(value));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool copyFileToGuest(uint64_t file,
|
||||
uint32_t destinationAddress,
|
||||
uint64_t bytesToCopy)
|
||||
{
|
||||
constexpr size_t kChunkBytes = 16u * 1024u;
|
||||
if (bytesToCopy > 0xFFFFFFFFull - static_cast<uint64_t>(destinationAddress) + 1ull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> chunk(kChunkBytes);
|
||||
uint64_t copied = 0u;
|
||||
while (copied < bytesToCopy)
|
||||
{
|
||||
const size_t wanted = static_cast<size_t>(
|
||||
std::min<uint64_t>(chunk.size(), bytesToCopy - copied));
|
||||
size_t received = 0u;
|
||||
if (!m_host.readHostFile(file,
|
||||
copied,
|
||||
chunk.data(),
|
||||
wanted,
|
||||
received) ||
|
||||
received != wanted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t chunkAddress = destinationAddress + static_cast<uint32_t>(copied);
|
||||
if (!m_host.writeGuest(chunkAddress, chunk.data(), received))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
copied += received;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
ClFileBindings m_bindings;
|
||||
std::array<uint32_t, 1> m_sids;
|
||||
mutable std::mutex m_mutex;
|
||||
std::unordered_map<uint32_t, ClFileHandle> m_fileHandles;
|
||||
std::unordered_map<uint32_t, ClFileLoad> m_loads;
|
||||
uint32_t m_nextFileHandle = 1u;
|
||||
uint32_t m_nextLoadHandle = 0u;
|
||||
std::string m_root;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createClFileService(IopHost &host,
|
||||
ClFileBindings bindings)
|
||||
{
|
||||
const ClFileRpcLayout &rpc = bindings.rpc;
|
||||
const std::array<uint32_t, 10> functions = {
|
||||
rpc.directLoadFunction,
|
||||
rpc.getStatusFunction,
|
||||
rpc.initializeFunction,
|
||||
rpc.waitFunction,
|
||||
rpc.getSizeFunction,
|
||||
rpc.openFunction,
|
||||
rpc.closeFunction,
|
||||
rpc.readFunction,
|
||||
rpc.secondaryWaitFunction,
|
||||
rpc.setRootFunction,
|
||||
};
|
||||
std::unordered_set<uint32_t> uniqueFunctions;
|
||||
for (const uint32_t function : functions)
|
||||
{
|
||||
if (!uniqueFunctions.emplace(function).second)
|
||||
{
|
||||
throw std::invalid_argument("duplicate CLFILE RPC function binding");
|
||||
}
|
||||
}
|
||||
if (bindings.serviceName.empty() || bindings.sid == 0u ||
|
||||
rpc.pathBytes == 0u || rpc.maximumReadBytes == 0u ||
|
||||
rpc.firstLoadHandle < 3u)
|
||||
{
|
||||
throw std::invalid_argument("invalid CLFILE bindings");
|
||||
}
|
||||
return std::make_unique<ClFileService>(host, std::move(bindings));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
#include "module_factories.h"
|
||||
#include "rpc_reply.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -12,9 +13,11 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
constexpr uint32_t kDbcManSid = 0x80001300u;
|
||||
constexpr uint32_t kRpcCheckVersion = 0x80001363u;
|
||||
constexpr uint32_t kDbcManVersion = 0x0320u;
|
||||
constexpr uint32_t kMaxUnknownRpcLogs = 32u;
|
||||
|
||||
constexpr std::array<uint16_t, 2> kSupportedVersions{0x0310u, 0x0320u};
|
||||
constexpr uint16_t kReportedVersion = kSupportedVersions.front();
|
||||
|
||||
class DbcmanService final : public IopService
|
||||
{
|
||||
public:
|
||||
@@ -33,10 +36,17 @@ namespace ps2x::iop::detail
|
||||
return kSids;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override
|
||||
{
|
||||
return kModuleAliases;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_unknownRpcLogCount = 0u;
|
||||
m_versionQueryCount = 0u;
|
||||
m_failedVersionReplies = 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
@@ -56,12 +66,21 @@ namespace ps2x::iop::detail
|
||||
|
||||
if (request.function == kRpcCheckVersion)
|
||||
{
|
||||
const uint32_t wordCount = request.receive.size / sizeof(uint32_t);
|
||||
const uint32_t count = wordCount < 4u ? wordCount : 4u;
|
||||
for (uint32_t index = 0u; index < count; ++index)
|
||||
const uint32_t version = kReportedVersion;
|
||||
const std::array<uint32_t, 4> reply{version, version, version, version};
|
||||
const bool written = writeRpcWords(m_host, request.receive, reply);
|
||||
bool firstQuery = false;
|
||||
{
|
||||
const uint32_t address = request.receive.address + index * sizeof(uint32_t);
|
||||
(void)m_host.writeGuest(address, &kDbcManVersion, sizeof(kDbcManVersion));
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
firstQuery = m_versionQueryCount++ == 0u;
|
||||
if (!written)
|
||||
++m_failedVersionReplies;
|
||||
}
|
||||
if (firstQuery)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "[DBCMAN:HLE] check-version reply=0x" << std::hex << version;
|
||||
m_host.log(LogLevel::Info, message.str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -94,15 +113,21 @@ namespace ps2x::iop::detail
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"reported_version", kReportedVersion, true});
|
||||
metrics.push_back({"version_queries", m_versionQueryCount, false});
|
||||
metrics.push_back({"failed_version_replies", m_failedVersionReplies, false});
|
||||
metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
inline static constexpr std::array<uint32_t, 1> kSids{kDbcManSid};
|
||||
inline static constexpr std::array<std::string_view, 3> kModuleAliases{"dbcman", "dbcm", "dbcmserv"};
|
||||
|
||||
IopHost &m_host;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_unknownRpcLogCount = 0u;
|
||||
uint64_t m_versionQueryCount = 0u;
|
||||
uint64_t m_failedVersionReplies = 0u;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ namespace ps2x::iop::detail
|
||||
return kSids;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override
|
||||
{
|
||||
return kModuleAliases;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
}
|
||||
@@ -51,6 +56,7 @@ namespace ps2x::iop::detail
|
||||
|
||||
private:
|
||||
inline static constexpr std::array<uint32_t, 1> kSids{kLibSdSid};
|
||||
inline static constexpr std::array<std::string_view, 1> kModuleAliases{"libsd"};
|
||||
|
||||
IopHost &m_host;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../iop_service.h"
|
||||
#include "../rpc_reply.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -82,43 +83,75 @@ namespace ps2x::iop::detail
|
||||
flavor = Flavor::NewXmcserv;
|
||||
switch (function)
|
||||
{
|
||||
case 0xFEu: return Operation::Init;
|
||||
case 0x01u: return Operation::GetInfo;
|
||||
case 0x02u: return Operation::Open;
|
||||
case 0x03u: return Operation::Close;
|
||||
case 0x04u: return Operation::Seek;
|
||||
case 0x05u: return Operation::Read;
|
||||
case 0x06u: return Operation::Write;
|
||||
case 0x0Au: return Operation::Flush;
|
||||
case 0x0Cu: return Operation::Chdir;
|
||||
case 0x0Du: return Operation::GetDir;
|
||||
case 0x0Eu: return Operation::SetInfo;
|
||||
case 0x0Fu: return Operation::Delete;
|
||||
case 0x10u: return Operation::Format;
|
||||
case 0x11u: return Operation::Unformat;
|
||||
case 0x12u: return Operation::GetEnt;
|
||||
case 0x14u: return Operation::ChangePriority;
|
||||
default: break;
|
||||
case 0xFEu:
|
||||
return Operation::Init;
|
||||
case 0x01u:
|
||||
return Operation::GetInfo;
|
||||
case 0x02u:
|
||||
return Operation::Open;
|
||||
case 0x03u:
|
||||
return Operation::Close;
|
||||
case 0x04u:
|
||||
return Operation::Seek;
|
||||
case 0x05u:
|
||||
return Operation::Read;
|
||||
case 0x06u:
|
||||
return Operation::Write;
|
||||
case 0x0Au:
|
||||
return Operation::Flush;
|
||||
case 0x0Cu:
|
||||
return Operation::Chdir;
|
||||
case 0x0Du:
|
||||
return Operation::GetDir;
|
||||
case 0x0Eu:
|
||||
return Operation::SetInfo;
|
||||
case 0x0Fu:
|
||||
return Operation::Delete;
|
||||
case 0x10u:
|
||||
return Operation::Format;
|
||||
case 0x11u:
|
||||
return Operation::Unformat;
|
||||
case 0x12u:
|
||||
return Operation::GetEnt;
|
||||
case 0x14u:
|
||||
return Operation::ChangePriority;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
flavor = Flavor::OldMcserv;
|
||||
switch (function)
|
||||
{
|
||||
case 0x70u: return Operation::Init;
|
||||
case 0x71u: return Operation::Open;
|
||||
case 0x72u: return Operation::Close;
|
||||
case 0x73u: return Operation::Read;
|
||||
case 0x74u: return Operation::Write;
|
||||
case 0x75u: return Operation::Seek;
|
||||
case 0x76u: return Operation::GetDir;
|
||||
case 0x77u: return Operation::Format;
|
||||
case 0x78u: return Operation::GetInfo;
|
||||
case 0x79u: return Operation::Delete;
|
||||
case 0x7Au: return Operation::Flush;
|
||||
case 0x7Bu: return Operation::Chdir;
|
||||
case 0x7Cu: return Operation::SetInfo;
|
||||
case 0x80u: return Operation::Unformat;
|
||||
default: return Operation::Unknown;
|
||||
case 0x70u:
|
||||
return Operation::Init;
|
||||
case 0x71u:
|
||||
return Operation::Open;
|
||||
case 0x72u:
|
||||
return Operation::Close;
|
||||
case 0x73u:
|
||||
return Operation::Read;
|
||||
case 0x74u:
|
||||
return Operation::Write;
|
||||
case 0x75u:
|
||||
return Operation::Seek;
|
||||
case 0x76u:
|
||||
return Operation::GetDir;
|
||||
case 0x77u:
|
||||
return Operation::Format;
|
||||
case 0x78u:
|
||||
return Operation::GetInfo;
|
||||
case 0x79u:
|
||||
return Operation::Delete;
|
||||
case 0x7Au:
|
||||
return Operation::Flush;
|
||||
case 0x7Bu:
|
||||
return Operation::Chdir;
|
||||
case 0x7Cu:
|
||||
return Operation::SetInfo;
|
||||
case 0x80u:
|
||||
return Operation::Unformat;
|
||||
default:
|
||||
return Operation::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +169,7 @@ namespace ps2x::iop::detail
|
||||
|
||||
[[nodiscard]] std::string_view name() const override { return "MCSERV"; }
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override { return m_sids; }
|
||||
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override { return m_moduleAliases; }
|
||||
|
||||
void reset() override
|
||||
{
|
||||
@@ -156,8 +190,8 @@ namespace ps2x::iop::detail
|
||||
const Operation operation = decodeOperation(request.function, flavor);
|
||||
if (operation == Operation::Init)
|
||||
{
|
||||
(void)call(MemoryCardOperation::Init);
|
||||
writeInitResult(request.receive);
|
||||
const int32_t result = call(MemoryCardOperation::Init);
|
||||
writeInitResult(request.receive, flavor, result);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -173,7 +207,7 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
NameParameter parameter{};
|
||||
if (request.send.address != 0u &&
|
||||
request.send.size >= offsetof(NameParameter, name) &&
|
||||
request.send.size >= sizeof(parameter) &&
|
||||
m_host.readGuest(request.send.address, ¶meter, sizeof(parameter)))
|
||||
{
|
||||
result = handleNameOperation(operation, request.send.address, parameter);
|
||||
@@ -189,22 +223,15 @@ namespace ps2x::iop::detail
|
||||
if (operation == Operation::Write && parameter.origin > 0 &&
|
||||
parameter.origin <= static_cast<int32_t>(sizeof(parameter.data)))
|
||||
{
|
||||
const uint32_t inlineAddress =
|
||||
request.send.address + static_cast<uint32_t>(offsetof(DescriptorParameter, data));
|
||||
const int32_t prefix = call(MemoryCardOperation::Write,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
inlineAddress,
|
||||
static_cast<uint32_t>(parameter.origin));
|
||||
const uint32_t inlineAddress = request.send.address + static_cast<uint32_t>(offsetof(DescriptorParameter, data));
|
||||
const int32_t prefix = call(MemoryCardOperation::Write, static_cast<uint32_t>(parameter.fd), inlineAddress, static_cast<uint32_t>(parameter.origin));
|
||||
if (prefix < 0)
|
||||
{
|
||||
result = prefix;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int32_t body = call(MemoryCardOperation::Write,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
parameter.buffer,
|
||||
static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
const int32_t body = call(MemoryCardOperation::Write, static_cast<uint32_t>(parameter.fd), parameter.buffer, static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
result = body < 0 ? body : prefix + body;
|
||||
}
|
||||
}
|
||||
@@ -238,40 +265,20 @@ namespace ps2x::iop::detail
|
||||
|
||||
void writeResult(GuestBuffer receive, int32_t result)
|
||||
{
|
||||
if (receive.address == 0u || receive.size < sizeof(result))
|
||||
{
|
||||
return;
|
||||
}
|
||||
(void)m_host.writeGuest(receive.address, &result, sizeof(result));
|
||||
if (receive.size > sizeof(result))
|
||||
{
|
||||
(void)m_host.zeroGuest(receive.address + sizeof(result),
|
||||
receive.size - sizeof(result));
|
||||
}
|
||||
const std::array<uint32_t, 1> values{static_cast<uint32_t>(result)};
|
||||
(void)writeRpcWords(m_host, receive, values);
|
||||
}
|
||||
|
||||
void writeInitResult(GuestBuffer receive)
|
||||
void writeInitResult(GuestBuffer receive, Flavor flavor, int32_t result)
|
||||
{
|
||||
if (receive.address == 0u || receive.size < sizeof(int32_t))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::array<uint32_t, 3> values = {
|
||||
static_cast<uint32_t>(kSucceeded), kMcservVersion, kMcmanVersion};
|
||||
const uint32_t bytes = std::min<uint32_t>(receive.size, sizeof(values));
|
||||
(void)m_host.writeGuest(receive.address, values.data(), bytes);
|
||||
if (receive.size > bytes)
|
||||
{
|
||||
(void)m_host.zeroGuest(receive.address + bytes, receive.size - bytes);
|
||||
}
|
||||
const std::array<uint32_t, 3> values = {static_cast<uint32_t>(result), kMcservVersion, kMcmanVersion};
|
||||
const size_t count = flavor == Flavor::NewXmcserv ? values.size() : 1u;
|
||||
(void)writeRpcWords(m_host, receive, std::span<const uint32_t>(values.data(), count));
|
||||
}
|
||||
|
||||
int32_t handleNameOperation(Operation operation,
|
||||
uint32_t sendAddress,
|
||||
const NameParameter ¶meter)
|
||||
int32_t handleNameOperation(Operation operation, uint32_t sendAddress, const NameParameter ¶meter)
|
||||
{
|
||||
const uint32_t nameAddress =
|
||||
sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
|
||||
const uint32_t nameAddress = sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
|
||||
const uint32_t port = static_cast<uint32_t>(parameter.port);
|
||||
const uint32_t slot = static_cast<uint32_t>(parameter.slot);
|
||||
switch (operation)
|
||||
@@ -281,12 +288,9 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
return call(MemoryCardOperation::Mkdir, port, slot, nameAddress);
|
||||
}
|
||||
return call(MemoryCardOperation::Open,
|
||||
port, slot, nameAddress,
|
||||
static_cast<uint32_t>(parameter.flags));
|
||||
return call(MemoryCardOperation::Open, port, slot, nameAddress, static_cast<uint32_t>(parameter.flags));
|
||||
case Operation::Chdir:
|
||||
return call(MemoryCardOperation::Chdir,
|
||||
port, slot, nameAddress, parameter.pointer);
|
||||
return call(MemoryCardOperation::Chdir, port, slot, nameAddress, parameter.pointer);
|
||||
case Operation::SetInfo:
|
||||
return call(MemoryCardOperation::SetFileInfo, port, slot, nameAddress);
|
||||
case Operation::Delete:
|
||||
@@ -302,9 +306,7 @@ namespace ps2x::iop::detail
|
||||
}
|
||||
}
|
||||
|
||||
int32_t handleDescriptorOperation(Operation operation,
|
||||
Flavor flavor,
|
||||
const DescriptorParameter ¶meter)
|
||||
int32_t handleDescriptorOperation(Operation operation, Flavor flavor, const DescriptorParameter ¶meter)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
@@ -341,8 +343,7 @@ namespace ps2x::iop::detail
|
||||
case Operation::Read:
|
||||
if (parameter.parameter != 0u)
|
||||
{
|
||||
(void)m_host.zeroGuest(parameter.parameter,
|
||||
flavor == Flavor::NewXmcserv ? 192u : 64u);
|
||||
(void)m_host.zeroGuest(parameter.parameter, flavor == Flavor::NewXmcserv ? 192u : 64u);
|
||||
}
|
||||
return call(MemoryCardOperation::Read,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
@@ -392,6 +393,7 @@ namespace ps2x::iop::detail
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_unknownRpcLogCount = 0u;
|
||||
const std::array<uint32_t, 2> m_sids = {kMcservSid, kMcservDev9Sid};
|
||||
const std::array<std::string_view, 2> m_moduleAliases = {"mcserv", "xmcserv"};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kEeRamSize = 32u * 1024u * 1024u;
|
||||
|
||||
class SdrdrvService final : public IopService
|
||||
{
|
||||
public:
|
||||
SdrdrvService(IopHost &host, SdrdrvBindings bindings)
|
||||
: m_host(host), m_bindings(std::move(bindings)), m_sids{m_bindings.sid}
|
||||
{
|
||||
}
|
||||
|
||||
std::string_view name() const override { return m_bindings.serviceName; }
|
||||
std::span<const uint32_t> sids() const override { return m_sids; }
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_headerWarnCount = 0;
|
||||
m_bodyWarnCount = 0;
|
||||
}
|
||||
|
||||
RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult result;
|
||||
if (request.sid != m_bindings.sid)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
if (m_bindings.clearReceiveBeforeDispatch &&
|
||||
request.receive.address && request.receive.size)
|
||||
{
|
||||
(void)m_host.zeroGuest(request.receive.address, request.receive.size);
|
||||
}
|
||||
|
||||
if (request.function == m_bindings.initFunction)
|
||||
{
|
||||
if (!loadImageHeader())
|
||||
{
|
||||
warnHeader();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (request.function == m_bindings.shutdownFunction)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
if (request.function != m_bindings.submitFunction)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const uint32_t count = std::min(request.send.size / m_bindings.commandBytes,
|
||||
m_bindings.maxCommands);
|
||||
for (uint32_t commandIndex = 0; commandIndex < count; ++commandIndex)
|
||||
{
|
||||
std::vector<uint32_t> words(m_bindings.commandBytes / sizeof(uint32_t));
|
||||
const uint32_t commandAddress = request.send.address +
|
||||
commandIndex * m_bindings.commandBytes;
|
||||
if (!m_host.readGuest(commandAddress,
|
||||
words.data(),
|
||||
m_bindings.commandBytes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (words[0] == m_bindings.headerCommand)
|
||||
{
|
||||
if (!loadImageHeader())
|
||||
{
|
||||
warnHeader();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (words[0] != m_bindings.loadCommand)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t lbn = words[m_bindings.lbnWord];
|
||||
const uint32_t byteCount = words[m_bindings.byteCountWord];
|
||||
const uint32_t destination = words[m_bindings.destinationWord];
|
||||
const bool eeLoad = words[m_bindings.destinationKindWord] ==
|
||||
m_bindings.eeDestinationKind;
|
||||
const uint32_t loadId = words[m_bindings.loadIdWord];
|
||||
const bool loaded = eeLoad
|
||||
? readBody(lbn, byteCount, destination)
|
||||
: m_bindings.pretendNonEeLoadsComplete;
|
||||
if (eeLoad && !loaded)
|
||||
{
|
||||
(void)m_host.zeroGuest(destination, byteCount);
|
||||
bool shouldWarn = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_bodyWarnCount < m_bindings.bodyWarningLimit)
|
||||
{
|
||||
++m_bodyWarnCount;
|
||||
shouldWarn = true;
|
||||
}
|
||||
}
|
||||
if (shouldWarn)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << '[' << m_bindings.serviceName
|
||||
<< "] failed data read lbn=0x" << std::hex << lbn
|
||||
<< " bytes=0x" << byteCount << " dst=0x" << destination;
|
||||
m_host.log(LogLevel::Warning, message.str());
|
||||
}
|
||||
}
|
||||
if (loaded || m_bindings.completeFailedLoads)
|
||||
{
|
||||
markLoadComplete(request.receive, loadId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"header_warnings", m_headerWarnCount, false});
|
||||
metrics.push_back({"body_warnings", m_bodyWarnCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t openSiblingFile(const std::string &lowerName,
|
||||
const std::string &upperName)
|
||||
{
|
||||
const std::array<std::string, 2> roots = {
|
||||
m_host.hostPath(HostPathKind::CdRoot),
|
||||
m_host.hostPath(HostPathKind::ElfDirectory),
|
||||
};
|
||||
for (const std::string &rootValue : roots)
|
||||
{
|
||||
if (rootValue.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::filesystem::path root(rootValue);
|
||||
for (const std::string *name : {&lowerName, &upperName})
|
||||
{
|
||||
if (name->empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::filesystem::path candidate = root / *name;
|
||||
const uint64_t handle = m_host.openHostFile(candidate.string());
|
||||
if (handle != 0u)
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
bool copyHostRange(uint64_t handle,
|
||||
uint64_t offset,
|
||||
uint32_t destination,
|
||||
uint64_t byteCount)
|
||||
{
|
||||
if (byteCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::array<uint8_t, 16 * 1024> chunk{};
|
||||
uint64_t copied = 0;
|
||||
while (copied < byteCount)
|
||||
{
|
||||
const size_t wanted = static_cast<size_t>(std::min<uint64_t>(chunk.size(), byteCount - copied));
|
||||
std::fill(chunk.begin(), chunk.begin() + static_cast<std::ptrdiff_t>(wanted), 0u);
|
||||
size_t got = 0u;
|
||||
if (!m_host.readHostFile(handle,
|
||||
offset + copied,
|
||||
chunk.data(),
|
||||
wanted,
|
||||
got) ||
|
||||
got > wanted ||
|
||||
!m_host.writeGuest(destination + static_cast<uint32_t>(copied),
|
||||
chunk.data(),
|
||||
wanted))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
copied += wanted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadImageHeader()
|
||||
{
|
||||
const uint64_t handle = openSiblingFile(m_bindings.imageHeaderLowerName,
|
||||
m_bindings.imageHeaderUpperName);
|
||||
if (handle == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
uint64_t fileSize = 0u;
|
||||
if (!m_host.hostFileSize(handle, fileSize))
|
||||
{
|
||||
m_host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
uint32_t normalized = 0;
|
||||
if (!m_host.normalizeGuestAddress(m_bindings.imageHeaderAddress, normalized) ||
|
||||
normalized >= kEeRamSize)
|
||||
{
|
||||
m_host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
const bool copied = copyHostRange(handle,
|
||||
0u,
|
||||
m_bindings.imageHeaderAddress,
|
||||
std::min<uint64_t>(fileSize,
|
||||
kEeRamSize - normalized));
|
||||
m_host.closeHostFile(handle);
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool readBody(uint32_t lbn, uint32_t byteCount, uint32_t destination)
|
||||
{
|
||||
uint32_t normalized = 0;
|
||||
if (!m_host.normalizeGuestAddress(destination, normalized) || normalized >= kEeRamSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
uint64_t handle = openSiblingFile(m_bindings.imageBodyLowerName,
|
||||
m_bindings.imageBodyUpperName);
|
||||
if (handle == 0u && m_bindings.fallbackBodyToCdImage)
|
||||
{
|
||||
handle = m_host.openHostFile(m_host.hostPath(HostPathKind::CdImage));
|
||||
}
|
||||
if (handle == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const uint64_t bytes = std::min<uint64_t>(byteCount, kEeRamSize - normalized);
|
||||
const bool copied = copyHostRange(handle,
|
||||
static_cast<uint64_t>(lbn) * m_bindings.sectorSize,
|
||||
destination,
|
||||
bytes);
|
||||
m_host.closeHostFile(handle);
|
||||
return copied;
|
||||
}
|
||||
|
||||
void markLoadComplete(GuestBuffer receive, uint32_t loadId)
|
||||
{
|
||||
const uint32_t offset = m_bindings.statusOffset +
|
||||
((loadId & m_bindings.statusSlotMask) *
|
||||
m_bindings.statusStride);
|
||||
if (receive.address && offset < receive.size)
|
||||
{
|
||||
const uint8_t complete = m_bindings.completeValue;
|
||||
(void)m_host.writeGuest(receive.address + offset, &complete, sizeof(complete));
|
||||
}
|
||||
}
|
||||
|
||||
void warnHeader()
|
||||
{
|
||||
bool shouldWarn = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_headerWarnCount < m_bindings.headerWarningLimit)
|
||||
{
|
||||
++m_headerWarnCount;
|
||||
shouldWarn = true;
|
||||
}
|
||||
}
|
||||
if (shouldWarn)
|
||||
{
|
||||
m_host.log(LogLevel::Warning,
|
||||
'[' + m_bindings.serviceName + "] failed to load image header");
|
||||
}
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
SdrdrvBindings m_bindings;
|
||||
std::array<uint32_t, 1> m_sids;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_headerWarnCount = 0;
|
||||
uint32_t m_bodyWarnCount = 0;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createSdrdrvService(IopHost &host,
|
||||
SdrdrvBindings bindings)
|
||||
{
|
||||
const uint32_t largestWord = std::max({bindings.lbnWord,
|
||||
bindings.byteCountWord,
|
||||
bindings.destinationWord,
|
||||
bindings.destinationKindWord,
|
||||
bindings.loadIdWord});
|
||||
if (bindings.serviceName.empty() ||
|
||||
bindings.sid == 0u ||
|
||||
bindings.imageHeaderAddress == 0u ||
|
||||
bindings.commandBytes == 0u ||
|
||||
(bindings.commandBytes % sizeof(uint32_t)) != 0u ||
|
||||
largestWord >= bindings.commandBytes / sizeof(uint32_t) ||
|
||||
bindings.maxCommands == 0u ||
|
||||
bindings.sectorSize == 0u ||
|
||||
bindings.statusStride == 0u ||
|
||||
bindings.initFunction == bindings.submitFunction ||
|
||||
bindings.initFunction == bindings.shutdownFunction ||
|
||||
bindings.submitFunction == bindings.shutdownFunction ||
|
||||
bindings.headerCommand == bindings.loadCommand ||
|
||||
(bindings.imageHeaderLowerName.empty() &&
|
||||
bindings.imageHeaderUpperName.empty()) ||
|
||||
(bindings.imageBodyLowerName.empty() &&
|
||||
bindings.imageBodyUpperName.empty() &&
|
||||
!bindings.fallbackBodyToCdImage))
|
||||
{
|
||||
throw std::invalid_argument("invalid SDRDRV bindings");
|
||||
}
|
||||
return std::make_unique<SdrdrvService>(host, std::move(bindings));
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint16_t kPlayStreamCommand = 1u;
|
||||
constexpr uint32_t kResponseRecordStride = 0x20u;
|
||||
constexpr uint32_t kPackedStreamOffset = 4u;
|
||||
constexpr uint32_t kStreamSlotMask = 0x3Fu;
|
||||
constexpr uint32_t kStreamSlotCount = 48u;
|
||||
constexpr uint32_t kCommandStreamSlotShift = 8u;
|
||||
constexpr uint32_t kResponseStreamSlotShift = 4u;
|
||||
|
||||
class SoundUpdateStubService final : public IopService
|
||||
{
|
||||
public:
|
||||
SoundUpdateStubService(IopHost &host, SoundUpdateStubBindings bindings)
|
||||
: m_host(host), m_bindings(std::move(bindings)), m_sids{m_bindings.sid}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override
|
||||
{
|
||||
return m_bindings.serviceName;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_updateCounter = 0u;
|
||||
m_completedStreamCount = 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
if (request.sid != m_bindings.sid)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
RpcResult result;
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
result.signalNowaitCompletion = m_bindings.signalNowaitCompletion;
|
||||
|
||||
if (std::find(m_bindings.suppressedCompletionCallbacks.begin(),
|
||||
m_bindings.suppressedCompletionCallbacks.end(),
|
||||
request.endFunction) != m_bindings.suppressedCompletionCallbacks.end())
|
||||
{
|
||||
result.signalCompletion = true;
|
||||
result.callbackPolicy = CallbackPolicy::Suppress;
|
||||
}
|
||||
|
||||
if (m_bindings.zeroReceiveBuffer &&
|
||||
request.receive.address != 0u && request.receive.size != 0u)
|
||||
{
|
||||
(void)m_host.zeroGuest(request.receive.address, request.receive.size);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> activeStreamSlots;
|
||||
if (m_bindings.completeQueuedPlayStreams && request.receive.address != 0u)
|
||||
{
|
||||
// PlayStream leaves the EE slot in state 2. One active record moves it
|
||||
// to state 1; the following empty update lets SOUND_CopyIOPBuffer clear it.
|
||||
activeStreamSlots = findQueuedPlayStreams(request);
|
||||
trimToReceiveCapacity(activeStreamSlots, request.receive.size);
|
||||
}
|
||||
|
||||
uint32_t counter = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
counter = ++m_updateCounter;
|
||||
m_completedStreamCount += activeStreamSlots.size();
|
||||
}
|
||||
|
||||
const uint32_t activeStreams = static_cast<uint32_t>(activeStreamSlots.size());
|
||||
if (request.receive.address != 0u &&
|
||||
request.receive.size >= m_bindings.activeStreamCountOffset + sizeof(activeStreams))
|
||||
{
|
||||
const uint32_t address = request.receive.address + m_bindings.activeStreamCountOffset;
|
||||
(void)m_host.writeGuest(address, &activeStreams, sizeof(activeStreams));
|
||||
}
|
||||
|
||||
for (size_t index = 0u; index < activeStreamSlots.size(); ++index)
|
||||
{
|
||||
const uint32_t packedStream = activeStreamSlots[index] << kResponseStreamSlotShift;
|
||||
const uint32_t offset = m_bindings.activeStreamCountOffset + static_cast<uint32_t>(index) * kResponseRecordStride + kPackedStreamOffset;
|
||||
const uint32_t address = request.receive.address + offset;
|
||||
(void)m_host.writeGuest(address, &packedStream, sizeof(packedStream));
|
||||
}
|
||||
|
||||
const uint32_t counterOffset = m_bindings.responseCounterOffset +
|
||||
activeStreams * kResponseRecordStride;
|
||||
if (request.receive.address != 0u &&
|
||||
request.receive.size >= counterOffset + sizeof(counter))
|
||||
{
|
||||
const uint32_t address = request.receive.address + counterOffset;
|
||||
(void)m_host.writeGuest(address, &counter, sizeof(counter));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"update_counter", m_updateCounter, false});
|
||||
metrics.push_back({"completed_streams", m_completedStreamCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::vector<uint32_t> findQueuedPlayStreams(const RpcRequest &request) const
|
||||
{
|
||||
std::vector<uint32_t> slots;
|
||||
if (request.send.address == 0u || request.send.size < sizeof(uint16_t))
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
uint16_t commandCount = 0u;
|
||||
if (!m_host.readGuest(request.send.address, &commandCount, sizeof(commandCount)))
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
uint32_t offset = sizeof(commandCount);
|
||||
for (uint32_t commandIndex = 0u; commandIndex < commandCount; ++commandIndex)
|
||||
{
|
||||
constexpr uint32_t headerSize = sizeof(uint16_t) * 2u;
|
||||
if (offset > request.send.size || request.send.size - offset < headerSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
std::array<uint16_t, 2> header{};
|
||||
if (!m_host.readGuest(request.send.address + offset,
|
||||
header.data(),
|
||||
sizeof(header)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
offset += headerSize;
|
||||
|
||||
const uint32_t argumentBytes =
|
||||
static_cast<uint32_t>(header[1]) * sizeof(uint16_t);
|
||||
if (argumentBytes > request.send.size - offset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (header[0] == kPlayStreamCommand && header[1] >= 2u)
|
||||
{
|
||||
uint16_t encodedSlot = 0u;
|
||||
if (m_host.readGuest(request.send.address + offset + sizeof(uint16_t),
|
||||
&encodedSlot,
|
||||
sizeof(encodedSlot)))
|
||||
{
|
||||
const uint32_t slot =
|
||||
(encodedSlot >> kCommandStreamSlotShift) & kStreamSlotMask;
|
||||
if (slot < kStreamSlotCount &&
|
||||
std::find(slots.begin(), slots.end(), slot) == slots.end())
|
||||
{
|
||||
slots.push_back(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += argumentBytes;
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
void trimToReceiveCapacity(std::vector<uint32_t> &slots, uint32_t receiveSize) const
|
||||
{
|
||||
size_t count = 0u;
|
||||
for (; count < slots.size(); ++count)
|
||||
{
|
||||
const uint64_t recordOffset =
|
||||
static_cast<uint64_t>(m_bindings.activeStreamCountOffset) +
|
||||
static_cast<uint64_t>(count) * kResponseRecordStride +
|
||||
kPackedStreamOffset;
|
||||
const uint64_t counterOffset =
|
||||
static_cast<uint64_t>(m_bindings.responseCounterOffset) +
|
||||
static_cast<uint64_t>(count + 1u) * kResponseRecordStride;
|
||||
if (recordOffset + sizeof(uint32_t) > receiveSize ||
|
||||
counterOffset + sizeof(uint32_t) > receiveSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
slots.resize(count);
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
SoundUpdateStubBindings m_bindings;
|
||||
std::array<uint32_t, 1> m_sids;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_updateCounter = 0u;
|
||||
uint64_t m_completedStreamCount = 0u;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createSoundUpdateStubService(IopHost &host,
|
||||
SoundUpdateStubBindings bindings)
|
||||
{
|
||||
if (bindings.serviceName.empty() || bindings.sid == 0u ||
|
||||
bindings.activeStreamCountOffset == bindings.responseCounterOffset)
|
||||
{
|
||||
throw std::invalid_argument("invalid SOUND update stub bindings");
|
||||
}
|
||||
std::unordered_set<uint32_t> callbacks;
|
||||
for (const uint32_t callback : bindings.suppressedCompletionCallbacks)
|
||||
{
|
||||
if (callback == 0u || !callbacks.emplace(callback).second)
|
||||
{
|
||||
throw std::invalid_argument("invalid SOUND update callback binding");
|
||||
}
|
||||
}
|
||||
return std::make_unique<SoundUpdateStubService>(host, std::move(bindings));
|
||||
}
|
||||
}
|
||||
@@ -1,628 +0,0 @@
|
||||
#include "../module_factories.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kCommandSid = 0x00000000u;
|
||||
constexpr uint32_t kStateSid = 0x00000001u;
|
||||
constexpr uint32_t kSubmitFunction = 0x00000000u;
|
||||
constexpr uint32_t kGetStatusAddressFunction = 0x00000012u;
|
||||
constexpr uint32_t kGetAddressTableFunction = 0x00000013u;
|
||||
|
||||
constexpr uint32_t kStatusSize = 0x42u;
|
||||
constexpr uint32_t kSeInfoOffset = 0x00u;
|
||||
constexpr uint32_t kMidiInfoOffset = 0x0Cu;
|
||||
constexpr uint32_t kMidiSumOffset = 0x1Eu;
|
||||
constexpr uint32_t kSeSumOffset = 0x26u;
|
||||
constexpr uint32_t kAddressTableEntries = 16u;
|
||||
constexpr uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment == 0u)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return (value + (alignment - 1u)) & ~(alignment - 1u);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readGuestPod(const IopHost &host, uint32_t address, T &value)
|
||||
{
|
||||
value = {};
|
||||
return host.readGuest(address, &value, sizeof(value));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool writeGuestPod(IopHost &host, uint32_t address, const T &value)
|
||||
{
|
||||
return host.writeGuest(address, &value, sizeof(value));
|
||||
}
|
||||
|
||||
template <typename T, size_t Size>
|
||||
bool hasAnyNonZero(const std::array<T, Size> &values)
|
||||
{
|
||||
return std::any_of(values.begin(), values.end(), [](const T value)
|
||||
{ return value != static_cast<T>(0); });
|
||||
}
|
||||
|
||||
size_t commandLength(uint8_t command)
|
||||
{
|
||||
const uint8_t hi = static_cast<uint8_t>(command & 0xF0u);
|
||||
switch (hi)
|
||||
{
|
||||
case 0x00u:
|
||||
{
|
||||
size_t length = 4u;
|
||||
if ((command & 0x01u) != 0u)
|
||||
{
|
||||
++length;
|
||||
}
|
||||
if ((command & 0x02u) != 0u)
|
||||
{
|
||||
++length;
|
||||
}
|
||||
if ((command & 0x04u) != 0u)
|
||||
{
|
||||
length += 2u;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
case 0x10u:
|
||||
return command == 0x11u ? 3u : 1u;
|
||||
case 0x20u:
|
||||
if (command == 0x22u || command == 0x23u || command == 0x24u || command == 0x25u)
|
||||
{
|
||||
return 3u;
|
||||
}
|
||||
if (command == 0x26u)
|
||||
{
|
||||
return 4u;
|
||||
}
|
||||
if (command == 0x20u)
|
||||
{
|
||||
return 5u;
|
||||
}
|
||||
if (command == 0x27u || command == 0x28u || command == 0x29u ||
|
||||
command == 0x2Cu || command == 0x2Du)
|
||||
{
|
||||
return 8u;
|
||||
}
|
||||
return 2u;
|
||||
case 0x40u:
|
||||
if (command == 0x47u || command == 0x48u || command == 0x49u || command == 0x4Au ||
|
||||
command == 0x41u || command == 0x42u)
|
||||
{
|
||||
return 2u;
|
||||
}
|
||||
if (command == 0x4Bu)
|
||||
{
|
||||
return 3u;
|
||||
}
|
||||
if (command == 0x45u || command == 0x4Cu)
|
||||
{
|
||||
return 4u;
|
||||
}
|
||||
if (command == 0x44u)
|
||||
{
|
||||
return 6u;
|
||||
}
|
||||
if (command == 0x4Du || command == 0x4Eu)
|
||||
{
|
||||
return 3u;
|
||||
}
|
||||
if (command == 0x4Fu)
|
||||
{
|
||||
return 6u;
|
||||
}
|
||||
return 1u;
|
||||
case 0x50u:
|
||||
case 0x60u:
|
||||
if (command == 0x51u || command == 0x52u || command == 0x53u || command == 0x54u)
|
||||
{
|
||||
return 8u;
|
||||
}
|
||||
return 2u;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
class TsnddrvService final : public IopService
|
||||
{
|
||||
public:
|
||||
TsnddrvService(IopHost &host, TsnddrvBindings bindings)
|
||||
: m_host(host), m_bindings(std::move(bindings))
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override
|
||||
{
|
||||
return m_bindings.serviceName;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_state = {};
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult result{};
|
||||
|
||||
if (request.sid == kCommandSid && request.function == kSubmitFunction)
|
||||
{
|
||||
handleCommandBuffer(request.send);
|
||||
result.handled = true;
|
||||
}
|
||||
else if (request.sid == kStateSid &&
|
||||
(request.function == kGetStatusAddressFunction ||
|
||||
request.function == kGetAddressTableFunction))
|
||||
{
|
||||
uint32_t responseAddress = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!ensureMemoryLocked())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
responseAddress = request.function == kGetStatusAddressFunction
|
||||
? m_state.statusAddress
|
||||
: m_state.addressTableAddress;
|
||||
}
|
||||
|
||||
if (request.receive.address != 0u && request.receive.size >= sizeof(uint32_t))
|
||||
{
|
||||
(void)writeGuestPod(m_host, request.receive.address, responseAddress);
|
||||
if (request.receive.size > sizeof(uint32_t))
|
||||
{
|
||||
(void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t),
|
||||
request.receive.size - sizeof(uint32_t));
|
||||
}
|
||||
result.resultAddress = request.receive.address;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.signalNowaitCompletion = true;
|
||||
}
|
||||
|
||||
if (result.handled)
|
||||
{
|
||||
const auto rule = std::find_if(
|
||||
m_bindings.completionRules.begin(),
|
||||
m_bindings.completionRules.end(),
|
||||
[&](const TsnddrvCompletionRule &candidate) {
|
||||
return candidate.eeFunction == request.endFunction;
|
||||
});
|
||||
if (rule != m_bindings.completionRules.end())
|
||||
{
|
||||
if (rule->suppressGuestCallback)
|
||||
{
|
||||
result.callbackPolicy = CallbackPolicy::Suppress;
|
||||
}
|
||||
result.signalCompletion = rule->signalCompletion;
|
||||
if (rule->clearBusy)
|
||||
{
|
||||
constexpr uint32_t idle = 0u;
|
||||
(void)writeGuestPod(m_host,
|
||||
m_bindings.busyFlagAddress,
|
||||
idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void onSifTransfer(const SifTransfer &transfer) override
|
||||
{
|
||||
if (transfer.kind != SifTransferKind::GetOtherData ||
|
||||
transfer.phase != SifTransferPhase::BeforeCopy ||
|
||||
transfer.size != kStatusSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!m_state.initialized || transfer.sourceAddress != m_state.statusAddress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
backfillStatusLocked();
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"initialized", m_state.initialized ? 1u : 0u, false});
|
||||
metrics.push_back({"status_address", m_state.statusAddress, true});
|
||||
metrics.push_back({"address_table", m_state.addressTableAddress, true});
|
||||
metrics.push_back({"hd_base", m_state.hdBaseAddress, true});
|
||||
metrics.push_back({"sq_base", m_state.sqBaseAddress, true});
|
||||
metrics.push_back({"data_base", m_state.dataBaseAddress, true});
|
||||
}
|
||||
|
||||
private:
|
||||
struct State
|
||||
{
|
||||
bool initialized = false;
|
||||
uint32_t storageBaseAddress = 0u;
|
||||
uint32_t storageSize = 0u;
|
||||
uint32_t statusAddress = 0u;
|
||||
uint32_t addressTableAddress = 0u;
|
||||
uint32_t hdBaseAddress = 0u;
|
||||
uint32_t sqBaseAddress = 0u;
|
||||
uint32_t dataBaseAddress = 0u;
|
||||
};
|
||||
|
||||
bool ensureMemoryLocked()
|
||||
{
|
||||
if (m_state.statusAddress == 0u)
|
||||
{
|
||||
const TsnddrvGuestArena &arena = m_bindings.arena;
|
||||
const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment);
|
||||
const uint32_t addressTableAddress =
|
||||
alignUp(statusAddress + kStatusSize, arena.tableAlignment);
|
||||
const uint32_t hdBaseAddress =
|
||||
alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)),
|
||||
arena.storageAlignment);
|
||||
const uint32_t sqBaseAddress =
|
||||
alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment);
|
||||
const uint32_t dataBaseAddress =
|
||||
alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment);
|
||||
const uint32_t storageEnd = dataBaseAddress + arena.dataBytes;
|
||||
if (storageEnd > arena.limit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state.statusAddress = statusAddress;
|
||||
m_state.addressTableAddress = addressTableAddress;
|
||||
m_state.hdBaseAddress = hdBaseAddress;
|
||||
m_state.sqBaseAddress = sqBaseAddress;
|
||||
m_state.dataBaseAddress = dataBaseAddress;
|
||||
m_state.storageBaseAddress = hdBaseAddress;
|
||||
m_state.storageSize = storageEnd - hdBaseAddress;
|
||||
}
|
||||
|
||||
if (m_state.statusAddress == 0u ||
|
||||
m_state.addressTableAddress == 0u ||
|
||||
m_state.storageBaseAddress == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_state.initialized)
|
||||
{
|
||||
if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) ||
|
||||
!m_host.zeroGuest(m_state.addressTableAddress,
|
||||
kAddressTableEntries * sizeof(uint32_t)) ||
|
||||
!m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writeGuestPod(m_host,
|
||||
m_state.addressTableAddress + (0u * sizeof(uint32_t)),
|
||||
m_state.hdBaseAddress) ||
|
||||
!writeGuestPod(m_host,
|
||||
m_state.addressTableAddress + (1u * sizeof(uint32_t)),
|
||||
m_state.sqBaseAddress) ||
|
||||
!writeGuestPod(m_host,
|
||||
m_state.addressTableAddress + (2u * sizeof(uint32_t)),
|
||||
m_state.dataBaseAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_state.initialized = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int16_t checkValue(bool seTable,
|
||||
uint32_t index,
|
||||
uint32_t count) const
|
||||
{
|
||||
if (index >= count)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates)
|
||||
{
|
||||
const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress;
|
||||
int16_t value = 0;
|
||||
if (readGuestPod(m_host,
|
||||
base + (index * sizeof(int16_t)),
|
||||
value) &&
|
||||
value != 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool selectCompatChecks(uint32_t &seBase, uint32_t &midiBase) const
|
||||
{
|
||||
const TsnddrvChecksumTables *firstReadable = nullptr;
|
||||
for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates)
|
||||
{
|
||||
std::array<int16_t, 5> seValues{};
|
||||
std::array<int16_t, 4> midiValues{};
|
||||
const bool seReadable = m_host.readGuest(candidate.seAddress,
|
||||
seValues.data(),
|
||||
sizeof(seValues));
|
||||
const bool midiReadable = m_host.readGuest(candidate.midiAddress,
|
||||
midiValues.data(),
|
||||
sizeof(midiValues));
|
||||
if (seReadable && midiReadable && !firstReadable)
|
||||
{
|
||||
firstReadable = &candidate;
|
||||
}
|
||||
const bool looksLive =
|
||||
(seReadable && hasAnyNonZero(seValues)) ||
|
||||
(midiReadable && hasAnyNonZero(midiValues));
|
||||
if (seReadable && midiReadable && looksLive)
|
||||
{
|
||||
seBase = candidate.seAddress;
|
||||
midiBase = candidate.midiAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstReadable)
|
||||
{
|
||||
seBase = firstReadable->seAddress;
|
||||
midiBase = firstReadable->midiAddress;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void backfillStatusLocked()
|
||||
{
|
||||
uint32_t seBase = 0u;
|
||||
uint32_t midiBase = 0u;
|
||||
if (!selectCompatChecks(seBase, midiBase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto backfillSlots = [&](uint32_t statusOffset,
|
||||
uint32_t compatBase,
|
||||
uint32_t slotCount)
|
||||
{
|
||||
for (uint32_t slot = 0u; slot < slotCount; ++slot)
|
||||
{
|
||||
int16_t liveValue = 0;
|
||||
if (!readGuestPod(m_host,
|
||||
m_state.statusAddress + statusOffset +
|
||||
(slot * sizeof(int16_t)),
|
||||
liveValue) ||
|
||||
liveValue != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int16_t compatValue = 0;
|
||||
if (!readGuestPod(m_host,
|
||||
compatBase + (slot * sizeof(int16_t)),
|
||||
compatValue) ||
|
||||
compatValue == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)writeGuestPod(m_host,
|
||||
m_state.statusAddress + statusOffset +
|
||||
(slot * sizeof(int16_t)),
|
||||
compatValue);
|
||||
}
|
||||
};
|
||||
|
||||
backfillSlots(kSeSumOffset, seBase, 5u);
|
||||
backfillSlots(kMidiSumOffset, midiBase, 4u);
|
||||
}
|
||||
|
||||
void applyCommandLocked(const std::array<uint8_t, 8> &command)
|
||||
{
|
||||
if (m_state.statusAddress == 0u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command[0])
|
||||
{
|
||||
case 0x20u: // SdrBgmReq
|
||||
{
|
||||
const uint32_t port = command[1] & 0x0Fu;
|
||||
uint16_t midiInfo = 0u;
|
||||
(void)readGuestPod(m_host,
|
||||
m_state.statusAddress + kMidiInfoOffset,
|
||||
midiInfo);
|
||||
midiInfo = static_cast<uint16_t>(midiInfo |
|
||||
static_cast<uint16_t>(1u << port));
|
||||
(void)writeGuestPod(m_host,
|
||||
m_state.statusAddress + kMidiInfoOffset,
|
||||
midiInfo);
|
||||
break;
|
||||
}
|
||||
case 0x21u: // SdrBgmStop
|
||||
{
|
||||
const uint32_t port = command[1] & 0x0Fu;
|
||||
uint16_t midiInfo = 0u;
|
||||
(void)readGuestPod(m_host,
|
||||
m_state.statusAddress + kMidiInfoOffset,
|
||||
midiInfo);
|
||||
midiInfo = static_cast<uint16_t>(midiInfo &
|
||||
~static_cast<uint16_t>(1u << port));
|
||||
(void)writeGuestPod(m_host,
|
||||
m_state.statusAddress + kMidiInfoOffset,
|
||||
midiInfo);
|
||||
break;
|
||||
}
|
||||
case 0x28u: // SdrHDDataSet
|
||||
{
|
||||
const uint32_t port = command[1] & 0x0Fu;
|
||||
if (port >= 4u)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const int16_t checksum = checkValue(false, port, 4u);
|
||||
(void)writeGuestPod(m_host,
|
||||
m_state.statusAddress + kMidiSumOffset +
|
||||
(port * sizeof(int16_t)),
|
||||
checksum);
|
||||
break;
|
||||
}
|
||||
case 0x29u: // SdrHDDataSet2
|
||||
{
|
||||
const uint32_t port = command[1] & 0x0Fu;
|
||||
if (port >= 5u)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const int16_t checksum = checkValue(true, port, 5u);
|
||||
(void)writeGuestPod(m_host,
|
||||
m_state.statusAddress + kSeSumOffset +
|
||||
(port * sizeof(int16_t)),
|
||||
checksum);
|
||||
break;
|
||||
}
|
||||
case 0x10u: // SdrSeAllStop
|
||||
(void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset,
|
||||
6u * sizeof(uint16_t));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handleCommandBuffer(GuestBuffer send)
|
||||
{
|
||||
if (send.address == 0u || send.size == 0u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (!ensureMemoryLocked())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t offset = 0u; offset < send.size;)
|
||||
{
|
||||
uint8_t operation = 0u;
|
||||
if (!m_host.readGuest(send.address + offset, &operation, sizeof(operation)) ||
|
||||
operation == 0xFFu)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t length = commandLength(operation);
|
||||
if (length == 0u ||
|
||||
static_cast<uint64_t>(offset) + length > send.size)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 8> command{};
|
||||
if (!m_host.readGuest(send.address + offset, command.data(), length))
|
||||
{
|
||||
break;
|
||||
}
|
||||
applyCommandLocked(command);
|
||||
offset += static_cast<uint32_t>(length);
|
||||
}
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
TsnddrvBindings m_bindings;
|
||||
mutable std::mutex m_mutex;
|
||||
State m_state;
|
||||
const std::array<uint32_t, 2> m_sids = {kCommandSid, kStateSid};
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createTsnddrvService(IopHost &host,
|
||||
TsnddrvBindings bindings)
|
||||
{
|
||||
const auto isPowerOfTwo = [](uint32_t value) {
|
||||
return value != 0u && (value & (value - 1u)) == 0u;
|
||||
};
|
||||
const auto alignUp64 = [](uint64_t value, uint32_t alignment) {
|
||||
return (value + (alignment - 1u)) &
|
||||
~static_cast<uint64_t>(alignment - 1u);
|
||||
};
|
||||
|
||||
const TsnddrvGuestArena &arena = bindings.arena;
|
||||
if (bindings.serviceName.empty() ||
|
||||
arena.base >= arena.limit ||
|
||||
!isPowerOfTwo(arena.statusAlignment) ||
|
||||
!isPowerOfTwo(arena.tableAlignment) ||
|
||||
!isPowerOfTwo(arena.storageAlignment) ||
|
||||
arena.hdBytes == 0u || arena.sqBytes == 0u || arena.dataBytes == 0u ||
|
||||
bindings.checksumCandidates.empty())
|
||||
{
|
||||
throw std::invalid_argument("invalid TSNDDRV bindings");
|
||||
}
|
||||
|
||||
uint64_t end = alignUp64(arena.base, arena.statusAlignment) + kStatusSize;
|
||||
end = alignUp64(end, arena.tableAlignment) +
|
||||
(kAddressTableEntries * sizeof(uint32_t));
|
||||
end = alignUp64(end, arena.storageAlignment) + arena.hdBytes;
|
||||
end = alignUp64(end, arena.storageAlignment) + arena.sqBytes;
|
||||
end = alignUp64(end, arena.storageAlignment) + arena.dataBytes;
|
||||
if (end > arena.limit || end > std::numeric_limits<uint32_t>::max())
|
||||
{
|
||||
throw std::invalid_argument("TSNDDRV guest arena is too small");
|
||||
}
|
||||
|
||||
for (const TsnddrvChecksumTables &candidate : bindings.checksumCandidates)
|
||||
{
|
||||
if (candidate.seAddress == 0u || candidate.midiAddress == 0u)
|
||||
{
|
||||
throw std::invalid_argument("incomplete TSNDDRV checksum binding");
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> callbacks;
|
||||
for (const TsnddrvCompletionRule &rule : bindings.completionRules)
|
||||
{
|
||||
if (rule.eeFunction == 0u || !callbacks.emplace(rule.eeFunction).second ||
|
||||
(rule.clearBusy && bindings.busyFlagAddress == 0u))
|
||||
{
|
||||
throw std::invalid_argument("invalid TSNDDRV completion rule");
|
||||
}
|
||||
}
|
||||
|
||||
switch (bindings.protocol)
|
||||
{
|
||||
case TsnddrvProtocolVariant::SndQueueV1:
|
||||
break;
|
||||
}
|
||||
return std::make_unique<TsnddrvService>(host, std::move(bindings));
|
||||
}
|
||||
}
|
||||
@@ -1,956 +0,0 @@
|
||||
#include "plugin_loader.h"
|
||||
|
||||
#include "ps2x/iop/plugin_api.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr size_t kMaxPluginProfiles = 256u;
|
||||
constexpr size_t kMaxPluginSids = 256u;
|
||||
constexpr size_t kMaxPluginStringBytes = 4096u;
|
||||
|
||||
bool validStringView(ps2x_iop_string_view_v1 value)
|
||||
{
|
||||
return value.size <= kMaxPluginStringBytes && (value.size == 0u || value.data != nullptr);
|
||||
}
|
||||
|
||||
std::string copyString(ps2x_iop_string_view_v1 value)
|
||||
{
|
||||
if (!validStringView(value) || value.size == 0u)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return std::string(value.data, value.size);
|
||||
}
|
||||
|
||||
ps2x_iop_string_view_v1 makeStringView(std::string_view value)
|
||||
{
|
||||
return {value.data(), value.size()};
|
||||
}
|
||||
|
||||
int32_t copyHostString(const std::string &value,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *requiredSize)
|
||||
{
|
||||
const size_t required = value.size() + 1;
|
||||
if (requiredSize)
|
||||
{
|
||||
*requiredSize = required;
|
||||
}
|
||||
if (!destination || capacity < required)
|
||||
{
|
||||
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
|
||||
}
|
||||
std::memcpy(destination, value.c_str(), required);
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
|
||||
IopHandleKind toHandleKind(uint32_t kind)
|
||||
{
|
||||
return kind == PS2X_IOP_HANDLE_RPC_PACKET_V1
|
||||
? IopHandleKind::RpcPacket
|
||||
: IopHandleKind::RpcServer;
|
||||
}
|
||||
|
||||
HostPathKind toHostPathKind(uint32_t kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case PS2X_IOP_PATH_CD_ROOT_V1:
|
||||
return HostPathKind::CdRoot;
|
||||
case PS2X_IOP_PATH_CD_IMAGE_V1:
|
||||
return HostPathKind::CdImage;
|
||||
case PS2X_IOP_PATH_HOST_ROOT_V1:
|
||||
return HostPathKind::HostRoot;
|
||||
case PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1:
|
||||
return HostPathKind::MemoryCardRoot;
|
||||
default:
|
||||
return HostPathKind::ElfDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
MemoryCardOperation toMemoryCardOperation(uint32_t operation)
|
||||
{
|
||||
const uint32_t last = static_cast<uint32_t>(MemoryCardOperation::Mkdir);
|
||||
if (operation > last)
|
||||
{
|
||||
throw std::out_of_range("invalid memory-card operation");
|
||||
}
|
||||
return static_cast<MemoryCardOperation>(operation);
|
||||
}
|
||||
|
||||
class HostApiBridge
|
||||
{
|
||||
public:
|
||||
explicit HostApiBridge(IopHost &hostRef)
|
||||
: host(hostRef)
|
||||
{
|
||||
api.abi_version = PS2X_IOP_ABI_VERSION_V1;
|
||||
api.struct_size = sizeof(api);
|
||||
api.userdata = this;
|
||||
api.read_guest = &readGuest;
|
||||
api.write_guest = &writeGuest;
|
||||
api.zero_guest = &zeroGuest;
|
||||
api.normalize_guest_address = &normalizeGuestAddress;
|
||||
api.allocate_iop_handle = &allocateIopHandle;
|
||||
api.allocate_guest = &allocateGuest;
|
||||
api.free_guest = &freeGuest;
|
||||
api.audio_command = &audioCommand;
|
||||
api.get_host_path = &getHostPath;
|
||||
api.translate_guest_path = &translateGuestPath;
|
||||
api.open_host_file = &openHostFile;
|
||||
api.host_file_size = &hostFileSize;
|
||||
api.read_host_file = &readHostFile;
|
||||
api.close_host_file = &closeHostFile;
|
||||
api.memory_card = &memoryCard;
|
||||
api.has_guest_function = &hasGuestFunction;
|
||||
api.invoke_guest_function = &invokeGuestFunction;
|
||||
api.log = &log;
|
||||
}
|
||||
|
||||
ps2x_iop_host_api_v1 api{};
|
||||
IopHost &host;
|
||||
|
||||
private:
|
||||
static HostApiBridge *self(void *userdata)
|
||||
{
|
||||
return static_cast<HostApiBridge *>(userdata);
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
static int32_t guardedStatus(Callback &&callback) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return static_cast<int32_t>(
|
||||
std::forward<Callback>(callback)());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return PS2X_IOP_STATUS_FAILED_V1;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Value, typename Callback>
|
||||
static Value guardedValue(Value fallback, Callback &&callback) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return static_cast<Value>(
|
||||
std::forward<Callback>(callback)());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
static void guardedVoid(Callback &&callback) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
std::forward<Callback>(callback)();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t readGuest(void *userdata, uint32_t address, void *destination, size_t size)
|
||||
{
|
||||
if (!userdata || (!destination && size != 0))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.readGuest(address, destination, size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t writeGuest(void *userdata, uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
if (!userdata || (!source && size != 0))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.writeGuest(address, source, size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t zeroGuest(void *userdata, uint32_t address, size_t size)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.zeroGuest(address, size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t normalizeGuestAddress(void *userdata, uint32_t address, uint32_t *normalized)
|
||||
{
|
||||
if (!userdata || !normalized)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.normalizeGuestAddress(address, *normalized)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static uint32_t allocateIopHandle(void *userdata, uint32_t kind)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return guardedValue<uint32_t>(0u, [&]()
|
||||
{ return self(userdata)->host.allocateIopHandle(toHandleKind(kind)); });
|
||||
}
|
||||
|
||||
static uint32_t allocateGuest(void *userdata, uint32_t size, uint32_t alignment)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return guardedValue<uint32_t>(0u, [&]()
|
||||
{ return self(userdata)->host.allocateGuest(size, alignment); });
|
||||
}
|
||||
|
||||
static void freeGuest(void *userdata, uint32_t address)
|
||||
{
|
||||
if (userdata && address)
|
||||
{
|
||||
guardedVoid([&]()
|
||||
{ self(userdata)->host.freeGuest(address); });
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t audioCommand(void *userdata,
|
||||
uint32_t sid,
|
||||
uint32_t function,
|
||||
ps2x_iop_guest_buffer_v1 send,
|
||||
ps2x_iop_guest_buffer_v1 receive)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{
|
||||
self(userdata)->host.audioCommand(sid,
|
||||
function,
|
||||
{send.address, send.size},
|
||||
{receive.address, receive.size});
|
||||
return PS2X_IOP_STATUS_OK_V1; });
|
||||
}
|
||||
|
||||
static int32_t getHostPath(void *userdata,
|
||||
uint32_t kind,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *requiredSize)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return copyHostString(self(userdata)->host.hostPath(toHostPathKind(kind)),
|
||||
destination,
|
||||
capacity,
|
||||
requiredSize); });
|
||||
}
|
||||
|
||||
static int32_t translateGuestPath(void *userdata,
|
||||
ps2x_iop_string_view_v1 path,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *requiredSize)
|
||||
{
|
||||
if (!userdata || (!path.data && path.size != 0))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{
|
||||
const std::string translated = self(userdata)->host.translateGuestPath(
|
||||
std::string_view(path.data ? path.data : "", path.size));
|
||||
return copyHostString(translated, destination, capacity, requiredSize); });
|
||||
}
|
||||
|
||||
static uint64_t openHostFile(void *userdata,
|
||||
ps2x_iop_string_view_v1 path)
|
||||
{
|
||||
if (!userdata || (!path.data && path.size != 0u))
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
return guardedValue<uint64_t>(0u, [&]()
|
||||
{ return self(userdata)->host.openHostFile(
|
||||
std::string_view(path.data ? path.data : "", path.size)); });
|
||||
}
|
||||
|
||||
static int32_t hostFileSize(void *userdata,
|
||||
uint64_t handle,
|
||||
uint64_t *size)
|
||||
{
|
||||
if (!userdata || handle == 0u || !size)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.hostFileSize(handle, *size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t readHostFile(void *userdata,
|
||||
uint64_t handle,
|
||||
uint64_t offset,
|
||||
void *destination,
|
||||
size_t size,
|
||||
size_t *bytesRead)
|
||||
{
|
||||
if (!userdata || handle == 0u || !bytesRead ||
|
||||
(!destination && size != 0u))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.readHostFile(handle,
|
||||
offset,
|
||||
destination,
|
||||
size,
|
||||
*bytesRead)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static void closeHostFile(void *userdata, uint64_t handle)
|
||||
{
|
||||
if (userdata && handle != 0u)
|
||||
{
|
||||
guardedVoid([&]()
|
||||
{ self(userdata)->host.closeHostFile(handle); });
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t memoryCard(void *userdata,
|
||||
const ps2x_iop_memory_card_request_v1 *request,
|
||||
int32_t *result)
|
||||
{
|
||||
if (!userdata || !request || !result || request->struct_size < sizeof(*request))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
try
|
||||
{
|
||||
MemoryCardRequest converted;
|
||||
converted.operation = toMemoryCardOperation(request->operation);
|
||||
std::copy(std::begin(request->arguments), std::end(request->arguments), converted.arguments.begin());
|
||||
*result = self(userdata)->host.memoryCard(converted);
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return PS2X_IOP_STATUS_FAILED_V1;
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t hasGuestFunction(void *userdata, uint32_t address)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.hasGuestFunction(address) ? 1 : 0; });
|
||||
}
|
||||
|
||||
static int32_t invokeGuestFunction(void *userdata,
|
||||
uint64_t callToken,
|
||||
uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t *resultAddress)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.invokeGuestFunction(callToken,
|
||||
address,
|
||||
a0,
|
||||
a1,
|
||||
a2,
|
||||
a3,
|
||||
resultAddress)
|
||||
? 1
|
||||
: 0; });
|
||||
}
|
||||
|
||||
static void log(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message)
|
||||
{
|
||||
if (!userdata || (!message.data && message.size != 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const uint32_t maxLevel = static_cast<uint32_t>(LogLevel::Error);
|
||||
const auto converted = static_cast<LogLevel>(std::min(level, maxLevel));
|
||||
guardedVoid([&]()
|
||||
{ self(userdata)->host.log(
|
||||
converted,
|
||||
std::string_view(message.data ? message.data : "", message.size)); });
|
||||
}
|
||||
};
|
||||
|
||||
ps2x_iop_rpc_candidate_v1 toPluginCandidate(const RpcCallCandidate &candidate)
|
||||
{
|
||||
return {
|
||||
candidate.sendSize,
|
||||
candidate.receiveAddress,
|
||||
candidate.receiveSize,
|
||||
candidate.endFunction,
|
||||
candidate.endParameter,
|
||||
candidate.plausible ? 1u : 0u,
|
||||
};
|
||||
}
|
||||
|
||||
class PluginService final : public IopService
|
||||
{
|
||||
public:
|
||||
PluginService(IopHost &host,
|
||||
std::shared_ptr<void> libraryKeepAlive,
|
||||
ps2x_iop_profile_api_v1 profileApi,
|
||||
std::string serviceName,
|
||||
std::vector<uint32_t> serviceSids,
|
||||
const GameIdentity &identity)
|
||||
: m_libraryKeepAlive(std::move(libraryKeepAlive)),
|
||||
m_api(profileApi),
|
||||
m_name(std::move(serviceName)),
|
||||
m_sids(std::move(serviceSids)),
|
||||
m_host(host)
|
||||
{
|
||||
const ps2x_iop_game_identity_v1 pluginIdentity{
|
||||
sizeof(ps2x_iop_game_identity_v1),
|
||||
makeStringView(identity.elfName),
|
||||
identity.entryPoint,
|
||||
identity.crc32,
|
||||
};
|
||||
try
|
||||
{
|
||||
m_instance = m_api.create(&m_host.api, &pluginIdentity);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
throw std::runtime_error("plugin profile create threw an exception");
|
||||
}
|
||||
if (!m_instance)
|
||||
{
|
||||
throw std::runtime_error("plugin profile create returned null");
|
||||
}
|
||||
}
|
||||
|
||||
~PluginService() override
|
||||
{
|
||||
if (m_instance && m_api.destroy)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_api.destroy(m_instance);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_host.host.log(LogLevel::Error,
|
||||
"IOP plugin destroy threw for " + m_name);
|
||||
}
|
||||
}
|
||||
m_instance = nullptr;
|
||||
}
|
||||
|
||||
std::string_view name() const override
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
if (!m_api.reset)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.reset(m_instance);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1)
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning, "IOP plugin reset failed for " + m_name);
|
||||
}
|
||||
}
|
||||
|
||||
RpcAbi selectRpcAbi(const RpcAbiRequest &request) const override
|
||||
{
|
||||
if (!m_api.select_rpc_abi)
|
||||
{
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
const ps2x_iop_rpc_abi_request_v1 converted{
|
||||
sizeof(ps2x_iop_rpc_abi_request_v1),
|
||||
request.boundSid,
|
||||
request.function,
|
||||
toPluginCandidate(request.registers),
|
||||
toPluginCandidate(request.stack),
|
||||
};
|
||||
uint32_t result = PS2X_IOP_RPC_ABI_DEFAULT_V1;
|
||||
try
|
||||
{
|
||||
result = m_api.select_rpc_abi(m_instance, &converted);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning,
|
||||
"IOP plugin ABI selector threw for " + m_name);
|
||||
}
|
||||
if (result == PS2X_IOP_RPC_ABI_REGISTERS_V1)
|
||||
{
|
||||
return RpcAbi::Registers;
|
||||
}
|
||||
if (result == PS2X_IOP_RPC_ABI_STACK_V1)
|
||||
{
|
||||
return RpcAbi::Stack;
|
||||
}
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
const ps2x_iop_rpc_request_v1 converted{
|
||||
sizeof(ps2x_iop_rpc_request_v1),
|
||||
request.callToken,
|
||||
request.clientAddress,
|
||||
request.serverAddress,
|
||||
request.serverFunction,
|
||||
request.serverBuffer,
|
||||
request.sid,
|
||||
request.function,
|
||||
request.mode,
|
||||
{request.send.address, request.send.size},
|
||||
{request.receive.address, request.receive.size},
|
||||
request.endFunction,
|
||||
request.endParameter,
|
||||
};
|
||||
ps2x_iop_rpc_result_v1 result{};
|
||||
result.struct_size = sizeof(result);
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.handle_rpc(m_instance, &converted, &result);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1 ||
|
||||
result.struct_size < sizeof(result))
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning, "IOP plugin RPC failed for " + m_name);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
result.handled != 0,
|
||||
result.result_address,
|
||||
result.signal_nowait_completion != 0,
|
||||
result.signal_completion != 0,
|
||||
result.callback_policy == PS2X_IOP_CALLBACK_SUPPRESS_V1
|
||||
? CallbackPolicy::Suppress
|
||||
: CallbackPolicy::RuntimeDefault,
|
||||
result.server_dispatch_policy == PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1
|
||||
? ServerDispatchPolicy::Suppress
|
||||
: ServerDispatchPolicy::RuntimeDefault,
|
||||
};
|
||||
}
|
||||
|
||||
void onSifTransfer(const SifTransfer &transfer) override
|
||||
{
|
||||
if (!m_api.on_sif_transfer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const ps2x_iop_sif_transfer_v1 converted{
|
||||
sizeof(ps2x_iop_sif_transfer_v1),
|
||||
static_cast<uint32_t>(transfer.kind),
|
||||
static_cast<uint32_t>(transfer.phase),
|
||||
transfer.sourceAddress,
|
||||
transfer.destinationAddress,
|
||||
transfer.size,
|
||||
};
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.on_sif_transfer(m_instance, &converted);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1)
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning, "IOP plugin transfer hook failed for " + m_name);
|
||||
}
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
if (!m_api.debug_metric_count || !m_api.debug_metric)
|
||||
{
|
||||
return;
|
||||
}
|
||||
size_t rawCount = 0u;
|
||||
try
|
||||
{
|
||||
rawCount = m_api.debug_metric_count(m_instance);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const size_t count = std::min<size_t>(rawCount, 256);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
ps2x_iop_debug_metric_v1 metric{};
|
||||
metric.struct_size = sizeof(metric);
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.debug_metric(m_instance, i, &metric);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1 ||
|
||||
metric.struct_size < sizeof(metric))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
metrics.push_back({copyString(metric.name), metric.value, metric.hexadecimal != 0});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<void> m_libraryKeepAlive;
|
||||
ps2x_iop_profile_api_v1 m_api{};
|
||||
std::string m_name;
|
||||
std::vector<uint32_t> m_sids;
|
||||
HostApiBridge m_host;
|
||||
void *m_instance = nullptr;
|
||||
};
|
||||
|
||||
bool hasPluginExtension(const std::filesystem::path &path)
|
||||
{
|
||||
std::string extension = path.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value)
|
||||
{ return static_cast<char>(std::tolower(value)); });
|
||||
#if defined(_WIN32)
|
||||
return extension == ".dll";
|
||||
#elif defined(__linux__)
|
||||
return extension == ".so";
|
||||
#else
|
||||
(void)extension;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string formatPluginDiagnostic(const std::filesystem::path &path, std::string_view reason)
|
||||
{
|
||||
return "IOP plugin '" + path.string() + "': " + std::string(reason);
|
||||
}
|
||||
}
|
||||
|
||||
class PluginCatalog::DynamicLibrary
|
||||
{
|
||||
public:
|
||||
explicit DynamicLibrary(std::filesystem::path sourcePath)
|
||||
: path(std::move(sourcePath))
|
||||
{
|
||||
}
|
||||
|
||||
~DynamicLibrary()
|
||||
{
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
if (handle)
|
||||
{
|
||||
FreeLibrary(static_cast<HMODULE>(handle));
|
||||
}
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
if (handle)
|
||||
{
|
||||
dlclose(handle);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool open(std::string &error)
|
||||
{
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
handle = LoadLibraryW(path.c_str());
|
||||
if (!handle)
|
||||
{
|
||||
error = "LoadLibraryW failed with code " + std::to_string(GetLastError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!handle)
|
||||
{
|
||||
const char *message = dlerror();
|
||||
error = message ? message : "dlopen failed";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
error = "dynamic IOP plugins are disabled on this platform";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void *symbol(const char *name) const
|
||||
{
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
return handle ? reinterpret_cast<void *>(GetProcAddress(static_cast<HMODULE>(handle), name)) : nullptr;
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
return handle ? dlsym(handle, name) : nullptr;
|
||||
#else
|
||||
(void)name;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path path;
|
||||
void *handle = nullptr;
|
||||
};
|
||||
|
||||
PluginCatalog::PluginCatalog(IopHost &host)
|
||||
: m_host(host)
|
||||
{
|
||||
}
|
||||
|
||||
PluginCatalog::~PluginCatalog() = default;
|
||||
|
||||
// TODO I never test this one
|
||||
bool PluginCatalog::load(const std::vector<std::filesystem::path> &searchPaths,
|
||||
std::vector<ProfileDefinition> &profiles,
|
||||
std::vector<std::string> &diagnostics,
|
||||
std::string *error)
|
||||
{
|
||||
(void)error;
|
||||
#if !PS2X_IOP_ENABLE_PLUGINS
|
||||
if (!searchPaths.empty())
|
||||
{
|
||||
diagnostics.push_back("dynamic IOP plugins are disabled on this platform");
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
for (const auto &searchPath : searchPaths)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(searchPath, ec) || ec)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!std::filesystem::is_directory(searchPath, ec) || ec)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(searchPath, "search path is not a directory"));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (std::filesystem::directory_iterator iterator(searchPath, ec), end; !ec && iterator != end; iterator.increment(ec))
|
||||
{
|
||||
const std::filesystem::directory_entry &entry = *iterator;
|
||||
if (!entry.is_regular_file(ec) || ec || !hasPluginExtension(entry.path()))
|
||||
{
|
||||
ec.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(entry.path(), ec);
|
||||
if (ec)
|
||||
{
|
||||
ec.clear();
|
||||
canonicalPath = entry.path().lexically_normal();
|
||||
}
|
||||
const std::string pathKey = canonicalPath.generic_string();
|
||||
if (!m_loadedPaths.insert(pathKey).second)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto library = std::make_shared<DynamicLibrary>(canonicalPath);
|
||||
std::string openError;
|
||||
if (!library->open(openError))
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, openError));
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto query = reinterpret_cast<ps2x_iop_query_v1_fn>(
|
||||
library->symbol(PS2X_IOP_QUERY_SYMBOL_V1));
|
||||
if (!query)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "missing " PS2X_IOP_QUERY_SYMBOL_V1));
|
||||
continue;
|
||||
}
|
||||
|
||||
ps2x_iop_plugin_api_v1 plugin{};
|
||||
plugin.struct_size = sizeof(plugin);
|
||||
int32_t queryStatus = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
queryStatus = query(PS2X_IOP_ABI_VERSION_V1, &plugin);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"query entry threw an exception"));
|
||||
continue;
|
||||
}
|
||||
if (queryStatus != PS2X_IOP_STATUS_OK_V1 ||
|
||||
plugin.abi_version != PS2X_IOP_ABI_VERSION_V1 ||
|
||||
plugin.struct_size < sizeof(plugin))
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "incompatible ABI or invalid descriptor"));
|
||||
continue;
|
||||
}
|
||||
if (plugin.profile_count > 0 && !plugin.profiles)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "profile table is null"));
|
||||
continue;
|
||||
}
|
||||
if (plugin.profile_count > kMaxPluginProfiles)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "too many profiles"));
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string provider = copyString(plugin.name).empty()
|
||||
? canonicalPath.filename().string()
|
||||
: copyString(plugin.name);
|
||||
size_t acceptedProfiles = 0;
|
||||
for (size_t index = 0; index < plugin.profile_count; ++index)
|
||||
{
|
||||
const ps2x_iop_profile_api_v1 &profile = plugin.profiles[index];
|
||||
if (profile.abi_version != PS2X_IOP_ABI_VERSION_V1 ||
|
||||
profile.struct_size < sizeof(profile) ||
|
||||
profile.matcher.struct_size < sizeof(profile.matcher))
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"ignored invalid profile at index " + std::to_string(index)));
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string profileId = copyString(profile.id);
|
||||
const bool validMatcherName = validStringView(profile.matcher.elf_name);
|
||||
const bool matcherPresent = profile.matcher.elf_name.size != 0 ||
|
||||
profile.matcher.entry_point != 0 ||
|
||||
profile.matcher.crc32 != 0;
|
||||
if (!validStringView(profile.id) || !validMatcherName ||
|
||||
profileId.empty() || !matcherPresent ||
|
||||
profile.sid_count == 0 || !profile.sids ||
|
||||
!profile.create || !profile.destroy || !profile.reset ||
|
||||
!profile.handle_rpc)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"ignored invalid profile at index " + std::to_string(index)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (profile.sid_count > kMaxPluginSids)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"ignored profile with too many SIDs: " + profileId));
|
||||
continue;
|
||||
}
|
||||
std::vector<uint32_t> sids(profile.sids, profile.sids + profile.sid_count);
|
||||
|
||||
ProfileDefinition definition;
|
||||
definition.id = profileId;
|
||||
definition.provider = provider;
|
||||
definition.matcher.elfName = copyString(profile.matcher.elf_name);
|
||||
definition.matcher.entryPoint = profile.matcher.entry_point;
|
||||
definition.matcher.crc32 = profile.matcher.crc32;
|
||||
const std::shared_ptr<void> keepAlive = library;
|
||||
definition.factory = [keepAlive, profile, profileId, sids = std::move(sids)](
|
||||
IopHost &host,
|
||||
const GameIdentity &identity) mutable
|
||||
{
|
||||
ServiceList services;
|
||||
services.push_back(std::make_unique<PluginService>(host,
|
||||
keepAlive,
|
||||
profile,
|
||||
profileId,
|
||||
sids,
|
||||
identity));
|
||||
return services;
|
||||
};
|
||||
profiles.push_back(std::move(definition));
|
||||
++acceptedProfiles;
|
||||
}
|
||||
|
||||
if (acceptedProfiles == 0)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "no valid profiles"));
|
||||
continue;
|
||||
}
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"loaded " + std::to_string(acceptedProfiles) + " profile(s)"));
|
||||
m_libraries.push_back(std::move(library));
|
||||
}
|
||||
|
||||
if (ec)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(searchPath, ec.message()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "iop_service.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class PluginCatalog
|
||||
{
|
||||
public:
|
||||
explicit PluginCatalog(IopHost &host);
|
||||
~PluginCatalog();
|
||||
|
||||
PluginCatalog(const PluginCatalog &) = delete;
|
||||
PluginCatalog &operator=(const PluginCatalog &) = delete;
|
||||
|
||||
bool load(const std::vector<std::filesystem::path> &searchPaths,
|
||||
std::vector<ProfileDefinition> &profiles,
|
||||
std::vector<std::string> &diagnostics,
|
||||
std::string *error);
|
||||
|
||||
private:
|
||||
class DynamicLibrary;
|
||||
|
||||
IopHost &m_host;
|
||||
std::vector<std::shared_ptr<DynamicLibrary>> m_libraries;
|
||||
std::unordered_set<std::string> m_loadedPaths;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string lowerAscii(std::string_view value)
|
||||
{
|
||||
std::string result(value);
|
||||
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch)
|
||||
{ return static_cast<char>(std::tolower(ch)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
void normalizeSuffix(std::string &suffix)
|
||||
{
|
||||
std::replace(suffix.begin(), suffix.end(), '\\', '/');
|
||||
while (!suffix.empty() && suffix.front() == '/')
|
||||
suffix.erase(suffix.begin());
|
||||
|
||||
const size_t semicolon = suffix.rfind(';');
|
||||
if (semicolon == std::string::npos || semicolon + 1u == suffix.size())
|
||||
return;
|
||||
|
||||
const bool numeric = std::all_of(suffix.begin() + static_cast<std::ptrdiff_t>(semicolon + 1u),
|
||||
suffix.end(),
|
||||
[](unsigned char ch)
|
||||
{ return std::isdigit(ch) != 0; });
|
||||
if (numeric)
|
||||
suffix.erase(semicolon);
|
||||
}
|
||||
}
|
||||
|
||||
ParsedPs2Path parsePs2Path(std::string_view value)
|
||||
{
|
||||
ParsedPs2Path result;
|
||||
if (value.empty())
|
||||
return result;
|
||||
|
||||
const std::string lower = lowerAscii(value);
|
||||
size_t prefixLength = 0u;
|
||||
if (lower.rfind("host0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Host;
|
||||
result.deviceName = "host0";
|
||||
prefixLength = 6u;
|
||||
}
|
||||
else if (lower.rfind("host:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Host;
|
||||
result.deviceName = "host";
|
||||
prefixLength = 5u;
|
||||
}
|
||||
else if (lower.rfind("cdrom0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Cdrom;
|
||||
result.deviceName = "cdrom0";
|
||||
prefixLength = 7u;
|
||||
}
|
||||
else if (lower.rfind("cdrom:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Cdrom;
|
||||
result.deviceName = "cdrom";
|
||||
prefixLength = 6u;
|
||||
}
|
||||
else if (lower.rfind("mc0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::MemoryCard0;
|
||||
result.deviceName = "mc0";
|
||||
prefixLength = 4u;
|
||||
}
|
||||
else if (lower.rfind("rom0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Rom0;
|
||||
result.deviceName = "rom0";
|
||||
prefixLength = 5u;
|
||||
}
|
||||
else if (value.size() > 2u && std::isalpha(static_cast<unsigned char>(value[0])) &&
|
||||
value[1] == ':' && (value[2] == '/' || value[2] == '\\'))
|
||||
{
|
||||
result.device = Ps2PathDevice::NativeHost;
|
||||
result.deviceName = "native";
|
||||
}
|
||||
else if (value.find(':') != std::string_view::npos)
|
||||
{
|
||||
// TODO maybe log an error here, but don't fail the parse. This is a non-standard device name.
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.device = Ps2PathDevice::Cdrom;
|
||||
result.deviceName = "cdrom0";
|
||||
}
|
||||
|
||||
result.path.assign(value.substr(prefixLength));
|
||||
if (result.device != Ps2PathDevice::NativeHost)
|
||||
normalizeSuffix(result.path);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ps2PathLeafKey(const ParsedPs2Path &parsed)
|
||||
{
|
||||
if (!parsed)
|
||||
return {};
|
||||
std::string path = parsed.path;
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
const size_t slash = path.find_last_of('/');
|
||||
if (slash != std::string::npos)
|
||||
path.erase(0u, slash + 1u);
|
||||
path = lowerAscii(path);
|
||||
if (path.size() > 4u && path.ends_with(".irx"))
|
||||
path.resize(path.size() - 4u);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string ps2PathLeafKey(std::string_view path)
|
||||
{
|
||||
return ps2PathLeafKey(parsePs2Path(path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
[[nodiscard]] inline bool writeRpcWords(IopHost &host, GuestBuffer receive, std::span<const uint32_t> words)
|
||||
{
|
||||
const size_t count = std::min<size_t>(receive.size / sizeof(uint32_t), words.size());
|
||||
const size_t bytes = count * sizeof(uint32_t);
|
||||
if (receive.address == 0u || bytes == 0u)
|
||||
return false;
|
||||
if (bytes - 1u > std::numeric_limits<uint32_t>::max() - receive.address)
|
||||
return false;
|
||||
return host.writeGuest(receive.address, words.data(), bytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace iop_test
|
||||
{
|
||||
using namespace ps2x::iop;
|
||||
|
||||
inline void require(bool condition, const char *message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
class Host final : public IopHost
|
||||
{
|
||||
public:
|
||||
explicit Host(size_t bytes = 0x20000u) : guest(bytes, 0xCCu) {}
|
||||
|
||||
bool readGuest(uint32_t address, void *destination, size_t size) const override
|
||||
{
|
||||
if ((size != 0u && !destination) || address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(destination, guest.data() + address, size);
|
||||
++guestReads;
|
||||
return true;
|
||||
}
|
||||
bool writeGuest(uint32_t address, const void *source, size_t size) override
|
||||
{
|
||||
if ((size != 0u && !source) || address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(guest.data() + address, source, size);
|
||||
++guestWrites;
|
||||
return true;
|
||||
}
|
||||
bool zeroGuest(uint32_t address, size_t size) override
|
||||
{
|
||||
if (address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
std::fill_n(guest.begin() + address, size, uint8_t{0});
|
||||
++guestWrites;
|
||||
return true;
|
||||
}
|
||||
bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override
|
||||
{
|
||||
normalized = address;
|
||||
return address < guest.size();
|
||||
}
|
||||
uint32_t allocateIopHandle(IopHandleKind) override { return nextHandle += 0x80u; }
|
||||
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
|
||||
void freeGuest(uint32_t) override {}
|
||||
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override { ++audioCalls; }
|
||||
std::string hostPath(HostPathKind) const override { return {}; }
|
||||
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
|
||||
uint64_t openHostFile(std::string_view) override { return file.empty() ? 0u : 1u; }
|
||||
bool hostFileSize(uint64_t handle, uint64_t &size) const override
|
||||
{
|
||||
size = file.size();
|
||||
return handle == 1u && !file.empty();
|
||||
}
|
||||
bool readHostFile(uint64_t handle, uint64_t offset, void *destination, size_t size,
|
||||
size_t &bytesRead) override
|
||||
{
|
||||
bytesRead = 0u;
|
||||
if (handle != 1u || offset > file.size())
|
||||
return false;
|
||||
bytesRead = std::min(size, file.size() - static_cast<size_t>(offset));
|
||||
if (bytesRead != 0u)
|
||||
std::memcpy(destination, file.data() + offset, bytesRead);
|
||||
return true;
|
||||
}
|
||||
void closeHostFile(uint64_t) override {}
|
||||
int32_t memoryCard(const MemoryCardRequest &request) override
|
||||
{
|
||||
cardCalls.push_back(request);
|
||||
return request.operation == MemoryCardOperation::Init ? initResult : 0;
|
||||
}
|
||||
bool hasGuestFunction(uint32_t) const override { return false; }
|
||||
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void log(LogLevel, std::string_view message) override { logs.emplace_back(message); }
|
||||
|
||||
uint32_t word(uint32_t address) const
|
||||
{
|
||||
uint32_t value = 0u;
|
||||
require(readGuest(address, &value, sizeof(value)), "test read outside guest RAM");
|
||||
return value;
|
||||
}
|
||||
void fill(uint32_t address, size_t size, uint8_t value = 0xCCu)
|
||||
{
|
||||
require(address <= guest.size() && size <= guest.size() - address, "test fill outside RAM");
|
||||
std::fill_n(guest.begin() + address, size, value);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> guest;
|
||||
std::vector<uint8_t> file;
|
||||
std::vector<std::string> logs;
|
||||
std::vector<MemoryCardRequest> cardCalls;
|
||||
mutable size_t guestReads = 0u;
|
||||
size_t guestWrites = 0u;
|
||||
size_t audioCalls = 0u;
|
||||
int32_t initResult = 0;
|
||||
uint32_t nextHandle = 0x1000u;
|
||||
};
|
||||
|
||||
inline RpcRequest request(uint32_t sid, uint32_t function, uint32_t size = 16u)
|
||||
{
|
||||
RpcRequest result{};
|
||||
result.sid = sid;
|
||||
result.function = function;
|
||||
result.receive = {0x800u, size};
|
||||
return result;
|
||||
}
|
||||
|
||||
inline uint64_t metric(const IopSubsystem &iop, std::string_view service, std::string_view name)
|
||||
{
|
||||
for (const auto &row : iop.debugSnapshot().services)
|
||||
if (row.name == service)
|
||||
for (const auto &entry : row.metrics)
|
||||
if (entry.name == name)
|
||||
return entry.value;
|
||||
throw std::runtime_error("missing debug metric");
|
||||
}
|
||||
|
||||
class Irx
|
||||
{
|
||||
public:
|
||||
explicit Irx(uint32_t base = 0x10000u, uint32_t imageBytes = 0x500u)
|
||||
: bytes(0x100u + imageBytes, 0u)
|
||||
{
|
||||
put32(0u, 0x464C457Fu);
|
||||
bytes[4] = bytes[5] = bytes[6] = 1u;
|
||||
put16(16u, 2u);
|
||||
put16(18u, 8u);
|
||||
put32(20u, 1u);
|
||||
put32(24u, base);
|
||||
put32(28u, 52u);
|
||||
put16(40u, 52u);
|
||||
put16(42u, 32u);
|
||||
put16(44u, 1u);
|
||||
put32(52u, 1u);
|
||||
put32(56u, 0x100u);
|
||||
put32(60u, base);
|
||||
put32(64u, base);
|
||||
put32(68u, imageBytes);
|
||||
put32(72u, imageBytes);
|
||||
put32(76u, 7u);
|
||||
put32(80u, 4u);
|
||||
}
|
||||
void words(uint32_t offset, std::initializer_list<uint32_t> values)
|
||||
{
|
||||
for (uint32_t value : values)
|
||||
{
|
||||
put32(0x100u + offset, value);
|
||||
offset += 4u;
|
||||
}
|
||||
}
|
||||
void install(Host &host, uint32_t address = 0x1000u) const
|
||||
{
|
||||
require(host.writeGuest(address, bytes.data(), bytes.size()), "synthetic IRX does not fit");
|
||||
}
|
||||
std::vector<uint8_t> bytes;
|
||||
|
||||
private:
|
||||
void put16(uint32_t offset, uint16_t value)
|
||||
{
|
||||
require(offset + 2u <= bytes.size(), "IRX builder overflow");
|
||||
bytes[offset] = static_cast<uint8_t>(value);
|
||||
bytes[offset + 1u] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
void put32(uint32_t offset, uint32_t value)
|
||||
{
|
||||
put16(offset, static_cast<uint16_t>(value));
|
||||
put16(offset + 2u, static_cast<uint16_t>(value >> 16u));
|
||||
}
|
||||
};
|
||||
|
||||
inline Irx rpcServer(uint32_t sid, uint32_t reply)
|
||||
{
|
||||
Irx image;
|
||||
image.words(0u, {
|
||||
0x27BDFFE0u, 0xAFBF001Cu, // save ra
|
||||
0x3C040001u, 0x34840200u,
|
||||
0x3C050000u | (sid >> 16u), 0x34A50000u | (sid & 0xFFFFu),
|
||||
0x3C060001u, 0x34C60300u,
|
||||
0x3C070001u, 0x34E70400u,
|
||||
0xAFA00010u, 0xAFA00014u, 0xAFA00018u,
|
||||
0x0C00401Du, 0u, // jal 0x10074: sceSifRegisterRpc
|
||||
0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u,
|
||||
});
|
||||
image.words(0x60u, {0x41E00000u, 0u, 0x0101u, 0x63666973u, 0x0000646Du,
|
||||
0x03E00008u, 0x24000011u, 0u, 0u});
|
||||
image.words(0x300u, {0x3C020001u, 0x34420400u, 0x03E00008u, 0u});
|
||||
image.words(0x400u, {reply, reply, reply, reply});
|
||||
return image;
|
||||
}
|
||||
|
||||
struct Test
|
||||
{
|
||||
const char *name;
|
||||
void (*function)();
|
||||
};
|
||||
|
||||
inline int run(std::span<const Test> tests)
|
||||
{
|
||||
size_t failures = 0u;
|
||||
for (const Test &test : tests)
|
||||
{
|
||||
try
|
||||
{
|
||||
test.function();
|
||||
std::cout << "PASS " << test.name << '\n';
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
++failures;
|
||||
std::cerr << "FAIL " << test.name << ": " << error.what() << '\n';
|
||||
}
|
||||
}
|
||||
std::cout << tests.size() - failures << '/' << tests.size() << " cases passed\n";
|
||||
return failures == 0u ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
constexpr uint32_t dbcSid = 0x80001300u;
|
||||
constexpr uint32_t dbcVersion = 0x80001363u;
|
||||
constexpr uint32_t mcSid = 0x80000400u;
|
||||
|
||||
void dbcDefault()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(!iop.canBindRpc(dbcSid), "unloaded DBCMAN must stay dormant");
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "DBCMAN load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
require(iop.handleRpc(query).handled, "version RPC not handled");
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
require(host.word(0x800u + i * 4u) == 0x0310u, "DBCMAN target version changed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
void dbcResetAndReconfigure()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "unexpected DBCMAN version");
|
||||
iop.reset();
|
||||
require(!iop.canBindRpc(dbcSid), "reset retained a module route");
|
||||
require(metric(iop, "dbcman", "version_queries") == 0u, "query counter not reset");
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "reload failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "IOP reboot changed target version");
|
||||
}
|
||||
|
||||
|
||||
void dbcReplyBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
for (uint32_t size = 0u; size <= 24u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 40u);
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion, size)).handled, "RPC failed");
|
||||
const uint32_t written = std::min(size / 4u, 4u) * 4u;
|
||||
for (uint32_t offset = written; offset < 32u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "reply wrote past whole-word payload");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "reply underflow");
|
||||
}
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
query.receive.address = 0u;
|
||||
const size_t writes = host.guestWrites;
|
||||
require(iop.handleRpc(query).handled && host.guestWrites == writes, "null reply was written");
|
||||
}
|
||||
|
||||
void dbcNoAddressWrap()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
query.receive.address = 0xFFFFFFF8u;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu,
|
||||
"overflowed reply corrupted low guest addresses");
|
||||
require(metric(iop, "dbcman", "failed_version_replies") == 1u, "invalid reply not recorded");
|
||||
}
|
||||
|
||||
void dbcNoRequestGuessing()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
const std::array<uint32_t, 4> randomArguments{0x0310u, 0x00010000u, 0u, 0xFFFFu};
|
||||
require(host.writeGuest(0x600u, randomArguments.data(), sizeof(randomArguments)), "write failed");
|
||||
query.send = {0x600u, sizeof(randomArguments)};
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "send buffer was guessed to be a requested version");
|
||||
}
|
||||
|
||||
void dbcPhysicalServerWins()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "HLE load failed");
|
||||
auto image = rpcServer(dbcSid, 0xDEADBEEFu);
|
||||
image.install(host);
|
||||
auto physical = iop.loadModuleBuffer(0x1000u);
|
||||
require(physical.moduleId > 0 && physical.startResult == 0, "physical IRX failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "physical RPC not handled");
|
||||
require(host.word(0x800u) == 0xDEADBEEFu, "HLE overwrote physical server version");
|
||||
require(metric(iop, "dbcman", "version_queries") == 0u, "HLE ran after physical service");
|
||||
require(iop.stopModule(physical.moduleId), "physical stop failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "HLE fallback not restored");
|
||||
require(host.word(0x800u) == 0x0310u, "wrong HLE version after physical stop");
|
||||
}
|
||||
|
||||
void mcNewInit()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:XMCSERV").moduleId > 0, "XMCSERV load failed");
|
||||
require(iop.handleRpc(request(mcSid, 0xFEu, 16u)).handled, "init RPC failed");
|
||||
require(host.word(0x800u) == 0u && host.word(0x804u) == 0x0205u && host.word(0x808u) == 0x0206u,
|
||||
"new memory-card init layout changed");
|
||||
require(host.word(0x80Cu) == 0xCCCCCCCCu, "new init wrote beyond 12-byte response");
|
||||
}
|
||||
|
||||
void mcOldInit()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
require(iop.handleRpc(request(mcSid, 0x70u, 16u)).handled, "init RPC failed");
|
||||
require(host.word(0x800u) == 0u && host.word(0x804u) == 0xCCCCCCCCu,
|
||||
"old init leaked extended protocol versions");
|
||||
}
|
||||
|
||||
void mcInitFailure()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
host.initResult = -5;
|
||||
for (uint32_t operation : {0x70u, 0xFEu})
|
||||
{
|
||||
require(iop.handleRpc(request(mcSid, operation)).handled, "init RPC failed");
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "init failure reported as success");
|
||||
}
|
||||
}
|
||||
|
||||
void mcReplyBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
for (uint32_t operation : {0x70u, 0xFEu})
|
||||
for (uint32_t size = 0u; size <= 20u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 32u);
|
||||
require(iop.handleRpc(request(mcSid, operation, size)).handled, "init RPC failed");
|
||||
const uint32_t words = operation == 0xFEu ? 3u : 1u;
|
||||
const uint32_t written = std::min(size / 4u, words) * 4u;
|
||||
for (uint32_t offset = written; offset < 24u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "init clobbered response tail");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "init underflowed buffer");
|
||||
}
|
||||
auto query = request(mcSid, 0xFEu);
|
||||
query.receive.address = 0xFFFFFFF8u;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu, "init overflowed guest address");
|
||||
}
|
||||
|
||||
void mcShortNamePacket()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
auto query = request(mcSid, 0x02u, 4u);
|
||||
query.send = {0x1000u, 20u};
|
||||
host.fill(0x1000u, 1044u, 0u);
|
||||
const size_t calls = host.cardCalls.size();
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.cardCalls.size() == calls, "short packet read a filename beyond send.size");
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "short packet not rejected");
|
||||
}
|
||||
|
||||
void mcFullNamePacket()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
const std::array<uint32_t, 5> header{1u, 0u, 1u, 0u, 0u};
|
||||
host.fill(0x1000u, 1044u, 0u);
|
||||
require(host.writeGuest(0x1000u, header.data(), sizeof(header)), "packet header write failed");
|
||||
constexpr char name[] = "/save.dat";
|
||||
require(host.writeGuest(0x1014u, name, sizeof(name)), "packet filename write failed");
|
||||
for (uint32_t operation : {0x02u, 0x71u})
|
||||
{
|
||||
auto query = request(mcSid, operation, 4u);
|
||||
query.send = {0x1000u, 1044u};
|
||||
const size_t before = host.cardCalls.size();
|
||||
require(iop.handleRpc(query).handled, "open RPC failed");
|
||||
require(host.cardCalls.size() == before + 1u, "valid packet not dispatched");
|
||||
const auto &call = host.cardCalls.back();
|
||||
require(call.operation == MemoryCardOperation::Open &&
|
||||
call.arguments[0] == 1u && call.arguments[1] == 0u &&
|
||||
call.arguments[2] == 0x1014u && call.arguments[3] == 1u,
|
||||
"valid name packet decoded incorrectly");
|
||||
}
|
||||
}
|
||||
|
||||
void mcStatusBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
for (uint32_t size = 0u; size <= 20u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 32u);
|
||||
require(iop.handleRpc(request(mcSid, 0xFFFFFFFFu, size)).handled, "RPC failed");
|
||||
const uint32_t written = size >= 4u ? 4u : 0u;
|
||||
if (written != 0u)
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "missing error status");
|
||||
for (uint32_t offset = written; offset < 24u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "status clobbered receive tail");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "status underflowed receive buffer");
|
||||
}
|
||||
auto query = request(mcSid, 0xFFFFFFFFu, 16u);
|
||||
query.receive.address = 0xFFFFFFFCu;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu,
|
||||
"status reply wrapped and zeroed low guest memory");
|
||||
}
|
||||
|
||||
void moduleAliases()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
for (const char *path : {"rom0:XSIO2MAN", "rom0:XPADMAN", "rom0:XMCMAN"})
|
||||
{
|
||||
auto result = iop.loadModule(path);
|
||||
require(result.moduleId > 0 && result.startResult == 0, "known extended module rejected");
|
||||
}
|
||||
require(!iop.canBindRpc(mcSid), "XMCMAN alone enabled a memory-card RPC server");
|
||||
const auto module = iop.loadModule("CDROM0:\\IOP\\xMcSeRv.IrX;1");
|
||||
require(module.moduleId > 0 && iop.canBindRpc(mcSid), "normalized XMCSERV alias not activated");
|
||||
require(iop.stopModule(module.moduleId) && !iop.canBindRpc(mcSid), "stopped alias remained active");
|
||||
}
|
||||
|
||||
void unknownModulesStayUnknown()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
for (const char *name : {"MC2_D.IRX", "DS2U_D.IRX", "CDVDSTM.IRX", "SDRDRV.IRX", "EZPCM.IRX", "ANYTHING_D.IRX"})
|
||||
{
|
||||
const auto result = iop.loadModule(std::string("host0:IOPModules/") + name);
|
||||
require(result.moduleId < 0 && result.startResult < 0, "unsupported module got a fake success");
|
||||
}
|
||||
require(!iop.canBindRpc(0x19740512u), "game-specific SDRDRV activated globally");
|
||||
}
|
||||
|
||||
void moduleLifetime()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
const auto a = iop.loadModule("rom0:DBCMAN");
|
||||
const auto b = iop.loadModule("rom0:dbcman.irx");
|
||||
const auto alias = iop.loadModule("rom0:DBCM");
|
||||
require(a.moduleId > 0 && a.moduleId == b.moduleId && alias.moduleId > 0, "module IDs unstable");
|
||||
require(iop.stopModule(a.moduleId) && iop.canBindRpc(dbcSid), "first release removed shared route");
|
||||
require(iop.stopModule(b.moduleId) && iop.canBindRpc(dbcSid), "remaining alias not honored");
|
||||
require(iop.stopModule(alias.moduleId) && !iop.canBindRpc(dbcSid), "last release retained route");
|
||||
}
|
||||
|
||||
void loaderDiagnostics()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("host0:LIBSD.IRX").moduleId > 0, "LIBSD fallback failed");
|
||||
require(iop.loadModule("host0:MISSING.IRX").moduleId < 0, "unknown load accepted");
|
||||
for (unsigned i = 0u; i < 100u; ++i)
|
||||
(void)iop.loadModule("host0:MISSING.IRX");
|
||||
auto snapshot = iop.debugSnapshot();
|
||||
require(snapshot.diagnostics.size() == 2u, "final loader outcomes not deduplicated");
|
||||
require(snapshot.diagnostics[0].find("[IOP:HLE]") != std::string::npos, "no fallback diagnostic");
|
||||
require(snapshot.diagnostics[1].find("no HLE provider") != std::string::npos, "no final failure diagnostic");
|
||||
for (unsigned i = 0u; i < 100u; ++i)
|
||||
(void)iop.loadModule("rom0:missing" + std::to_string(i));
|
||||
require(iop.debugSnapshot().diagnostics.size() <= 32u, "unbounded module diagnostics");
|
||||
iop.reset();
|
||||
require(iop.debugSnapshot().diagnostics.empty(), "stale load outcomes survived reset");
|
||||
}
|
||||
|
||||
void libsdUnchanged()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:LIBSD").moduleId > 0, "LIBSD load failed");
|
||||
require(iop.handleRpc(request(0x80000701u, 0x8010u)).handled, "LIBSD RPC not handled");
|
||||
require(host.audioCalls == 1u, "DBCMAN option intercepted LIBSD RPC");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"DBCMAN default and dormant route", dbcDefault},
|
||||
{"DBCMAN reboot and reconfiguration", dbcResetAndReconfigure},
|
||||
{"DBCMAN bounded whole-word response", dbcReplyBounds},
|
||||
{"DBCMAN rejects wrapping reply addresses", dbcNoAddressWrap},
|
||||
{"DBCMAN does not infer version from arbitrary RPC payload", dbcNoRequestGuessing},
|
||||
{"Physical DBCMAN server wins over configured HLE", dbcPhysicalServerWins},
|
||||
{"XMCSERV init status and two version fields", mcNewInit},
|
||||
{"Old MCSERV init is status only", mcOldInit},
|
||||
{"MCSERV propagates initialization failure", mcInitFailure},
|
||||
{"MCSERV response bounds for both dialects", mcReplyBounds},
|
||||
{"MCSERV rejects truncated name packet", mcShortNamePacket},
|
||||
{"MCSERV accepts complete name packets in both dialects", mcFullNamePacket},
|
||||
{"MCSERV status replies preserve bounds and cannot wrap", mcStatusBounds},
|
||||
{"Extended module aliases and activation", moduleAliases},
|
||||
{"Unsupported debug and game IRX stay unsupported", unknownModulesStayUnknown},
|
||||
{"HLE repeated loads and alias lifetime", moduleLifetime},
|
||||
{"Loader outcomes are bounded and resettable", loaderDiagnostics},
|
||||
{"LIBSD audio dispatch is unchanged", libsdUnchanged},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
#include "emulator/core/iop_cpu.h"
|
||||
#include "emulator/core/iop_kernel.h"
|
||||
#include "emulator/core/iop_memory.h"
|
||||
#include "emulator/imports/iop_cdvd.h"
|
||||
#include "emulator/imports/iop_imports.h"
|
||||
#include "emulator/imports/iop_loadcore.h"
|
||||
#include "emulator/imports/iop_timrman.h"
|
||||
#include "emulator/services/iop_rpc.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace ps2x::iop;
|
||||
using namespace ps2x::iop::detail;
|
||||
|
||||
constexpr uint32_t kExportMagic = 0x41C00000u;
|
||||
constexpr int32_t kLibraryNotFound = -213;
|
||||
constexpr int32_t kIllegalLibrary = -214;
|
||||
|
||||
class NullHost : public IopHost
|
||||
{
|
||||
public:
|
||||
bool readGuest(uint32_t, void *, size_t) const override { return false; }
|
||||
bool writeGuest(uint32_t, const void *, size_t) override { return false; }
|
||||
bool zeroGuest(uint32_t, size_t) override { return false; }
|
||||
bool normalizeGuestAddress(uint32_t, uint32_t &) const override { return false; }
|
||||
uint32_t allocateIopHandle(IopHandleKind) override { return 1u; }
|
||||
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
|
||||
void freeGuest(uint32_t) override {}
|
||||
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {}
|
||||
std::string hostPath(HostPathKind) const override { return {}; }
|
||||
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
|
||||
uint64_t openHostFile(std::string_view) override { return 0u; }
|
||||
bool hostFileSize(uint64_t, uint64_t &) const override { return false; }
|
||||
bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; }
|
||||
void closeHostFile(uint64_t) override {}
|
||||
int32_t memoryCard(const MemoryCardRequest &) override { return 0; }
|
||||
bool hasGuestFunction(uint32_t) const override { return false; }
|
||||
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; }
|
||||
void log(LogLevel, std::string_view) override {}
|
||||
};
|
||||
|
||||
class CdRootHost final : public NullHost
|
||||
{
|
||||
public:
|
||||
explicit CdRootHost(std::filesystem::path rootPath)
|
||||
: root(std::move(rootPath))
|
||||
{
|
||||
}
|
||||
|
||||
std::string hostPath(HostPathKind kind) const override
|
||||
{
|
||||
return kind == HostPathKind::CdRoot ? root.string() : std::string{};
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path root;
|
||||
};
|
||||
|
||||
class RecordingExecutor final : public IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t,
|
||||
uint32_t,
|
||||
uint32_t,
|
||||
uint32_t gp) override
|
||||
{
|
||||
++calls;
|
||||
lastAddress = address;
|
||||
lastArgument = a0;
|
||||
lastGp = gp;
|
||||
return callbackResult;
|
||||
}
|
||||
|
||||
uint32_t callbackResult = 0u;
|
||||
uint32_t calls = 0u;
|
||||
uint32_t lastAddress = 0u;
|
||||
uint32_t lastArgument = 0u;
|
||||
uint32_t lastGp = 0u;
|
||||
};
|
||||
|
||||
bool expect(bool condition, std::string_view message)
|
||||
{
|
||||
if (condition)
|
||||
return true;
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
bool testLoadcoreRebootLibraryMode()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
IopLoadcore loadcore(memory, imports);
|
||||
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = 0u;
|
||||
cpu.gpr[5] = 2u;
|
||||
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 was not handled") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kIllegalLibrary,
|
||||
"loadcore:27 did not reject a null export table"))
|
||||
return false;
|
||||
|
||||
constexpr uint32_t table = 0x1000u;
|
||||
memory.write32(table, kExportMagic);
|
||||
memory.write16(table + 8u, 0x0101u);
|
||||
memory.write16(table + 10u, 0x1234u);
|
||||
const char name[8] = {'t', 'e', 's', 't', 'l', 'i', 'b', '\0'};
|
||||
(void)memory.writeRam(table + 12u, name, sizeof(name));
|
||||
memory.write32(table + 20u, 0u);
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = table;
|
||||
cpu.gpr[5] = 2u;
|
||||
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a valid export table") ||
|
||||
!expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a valid export table") ||
|
||||
!expect(memory.read16(table + 10u) == 0x1232u,
|
||||
"loadcore:27 did not replace only export mode bits 1 and 2"))
|
||||
return false;
|
||||
|
||||
constexpr uint32_t invalidTable = 0x1100u;
|
||||
memory.write32(invalidTable, 0xDEADBEEFu);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = invalidTable;
|
||||
cpu.gpr[5] = 6u;
|
||||
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 did not consume an invalid-table call") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kLibraryNotFound,
|
||||
"loadcore:27 returned the wrong invalid-table error"))
|
||||
return false;
|
||||
|
||||
if (!expect(imports.registerExportTable(table), "test export table did not register"))
|
||||
return false;
|
||||
memory.write32(table, 0u);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = table;
|
||||
cpu.gpr[5] = 6u;
|
||||
return expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a registered table") &&
|
||||
expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a registered table") &&
|
||||
expect(memory.read16(table + 10u) == 0x1236u,
|
||||
"loadcore:27 did not update a registered table's mode");
|
||||
}
|
||||
|
||||
bool pollEvent(IopKernel &kernel, int eventId, uint32_t bits, uint32_t resultAddress, int32_t expected)
|
||||
{
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(eventId);
|
||||
cpu.gpr[5] = bits;
|
||||
cpu.gpr[6] = 0u; // WEF_AND
|
||||
cpu.gpr[7] = resultAddress;
|
||||
return expect(kernel.dispatchEventImport(11u, cpu), "PollEventFlag was not handled") &&
|
||||
expect(static_cast<int32_t>(cpu.gpr[2]) == expected, "PollEventFlag returned an unexpected result");
|
||||
}
|
||||
|
||||
bool testCdvdSpecialControl()
|
||||
{
|
||||
NullHost host;
|
||||
IopMemory memory;
|
||||
IopKernel kernel(memory);
|
||||
kernel.reset();
|
||||
IopCdvd cdvd(host, memory, kernel);
|
||||
cdvd.reset();
|
||||
|
||||
constexpr uint32_t param = 0x2000u;
|
||||
constexpr uint32_t eventResult = 0x2010u;
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-11); // sceCdSC: return cdvdman interrupt event flag
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "cdvdman:50 was not handled") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "sceCdSC(-11) did not return a valid event flag"))
|
||||
return false;
|
||||
const int eventId = static_cast<int>(cpu.gpr[2]);
|
||||
|
||||
if (!pollEvent(kernel, eventId, 0x29u, eventResult, 0) ||
|
||||
!expect(memory.read32(eventResult) == 0x29u, "cdvdman event flag did not start with bits 0x29"))
|
||||
return false;
|
||||
|
||||
IopCpuState clear{};
|
||||
clear.gpr[4] = static_cast<uint32_t>(eventId);
|
||||
clear.gpr[5] = ~0x29u;
|
||||
if (!expect(kernel.dispatchEventImport(8u, clear), "ClearEventFlag was not handled") ||
|
||||
!pollEvent(kernel, eventId, 0x29u, eventResult, -418))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = 0x12345u;
|
||||
if (!expect(cdvd.dispatchImport(7u, cpu), "sceCdSeek was not handled") ||
|
||||
!pollEvent(kernel, eventId, 0x29u, eventResult, 0))
|
||||
return false;
|
||||
|
||||
memory.write8(param, 0x30u);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-2);
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-2) was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0x30u, "sceCdSC(-2) did not store the low-byte error"))
|
||||
return false;
|
||||
|
||||
memory.write32(param, 0u);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-1);
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-1) was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "sceCdSC(-1) returned the wrong initial stream state") ||
|
||||
!expect(memory.read32(param) == 0x30u, "sceCdSC(-1) did not publish the last error"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = 2u;
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(2) was not handled") ||
|
||||
!expect(cpu.gpr[2] == 2u, "sceCdSC(2) did not update the stream state"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-1);
|
||||
cpu.gpr[5] = param;
|
||||
return expect(cdvd.dispatchImport(50u, cpu), "second sceCdSC(-1) was not handled") &&
|
||||
expect(cpu.gpr[2] == 2u, "sceCdSC(-1) did not preserve the stream state");
|
||||
}
|
||||
|
||||
bool testCdvdSearchFile()
|
||||
{
|
||||
const auto suffix = std::to_string(
|
||||
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
const std::filesystem::path root =
|
||||
std::filesystem::temp_directory_path() / ("ps2x-iop-cdvd-search-" + suffix);
|
||||
const std::filesystem::path movieDirectory = root / "MOVIE";
|
||||
const std::filesystem::path moviePath = movieDirectory / "OPENING.PSS";
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(movieDirectory, error);
|
||||
if (!expect(!error, "could not create the temporary CD root"))
|
||||
return false;
|
||||
{
|
||||
std::ofstream movie(moviePath, std::ios::binary);
|
||||
movie.write("PSS!", 4);
|
||||
}
|
||||
|
||||
CdRootHost host(root);
|
||||
IopMemory memory;
|
||||
IopKernel kernel(memory);
|
||||
kernel.reset();
|
||||
IopCdvd cdvd(host, memory, kernel);
|
||||
cdvd.reset();
|
||||
|
||||
constexpr uint32_t resultAddress = 0x2400u;
|
||||
constexpr uint32_t pathAddress = 0x2480u;
|
||||
const char path[] = "cdrom0:\\movie\\opening.pss;1";
|
||||
(void)memory.writeRam(pathAddress, path, sizeof(path));
|
||||
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = resultAddress;
|
||||
cpu.gpr[5] = pathAddress;
|
||||
const bool handled = cdvd.dispatchImport(10u, cpu);
|
||||
const bool passed =
|
||||
expect(handled, "cdvdman:10 was not handled") &&
|
||||
expect(cpu.gpr[2] == 1u, "sceCdSearchFile did not find a case-insensitive ISO path") &&
|
||||
expect(memory.read32(resultAddress) >= 20u, "sceCdSearchFile returned an invalid LSN") &&
|
||||
expect(memory.read32(resultAddress + 4u) == 4u, "sceCdSearchFile returned the wrong size") &&
|
||||
expect(memory.readString(resultAddress + 8u, 16u) == "OPENING.PSS",
|
||||
"sceCdSearchFile returned the wrong file name");
|
||||
|
||||
std::filesystem::remove_all(root, error);
|
||||
return passed;
|
||||
}
|
||||
|
||||
bool testTimrmanPeriodicCallback()
|
||||
{
|
||||
IopTimrman timrman;
|
||||
timrman.reset();
|
||||
IopCpuState cpu{};
|
||||
|
||||
cpu.gpr[4] = 1u; // SYSCLK
|
||||
cpu.gpr[5] = 32u;
|
||||
cpu.gpr[6] = 1u;
|
||||
if (!expect(timrman.dispatchImport(4u, cpu, 100u), "AllocHardTimer was not handled") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "AllocHardTimer did not allocate a 32-bit timer"))
|
||||
return false;
|
||||
const uint32_t timerId = cpu.gpr[2];
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = timerId;
|
||||
cpu.gpr[5] = 100u;
|
||||
cpu.gpr[6] = 0x12340u;
|
||||
cpu.gpr[7] = 0x45670u;
|
||||
cpu.gpr[28] = 0x89AB0u;
|
||||
if (!expect(timrman.dispatchImport(20u, cpu, 100u), "SetTimerHandler was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "SetTimerHandler failed"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = timerId;
|
||||
cpu.gpr[5] = 1u;
|
||||
cpu.gpr[6] = 0u;
|
||||
cpu.gpr[7] = 1u;
|
||||
if (!expect(timrman.dispatchImport(22u, cpu, 100u), "SetupHardTimer was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "SetupHardTimer failed"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = timerId;
|
||||
if (!expect(timrman.dispatchImport(23u, cpu, 100u), "StartHardTimer was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "StartHardTimer failed") ||
|
||||
!expect(timrman.nextEventCycle(1000u) == 200u, "timer compare was scheduled at the wrong cycle"))
|
||||
return false;
|
||||
|
||||
RecordingExecutor executor;
|
||||
executor.callbackResult = 100u;
|
||||
timrman.serviceDue(199u, executor);
|
||||
if (!expect(executor.calls == 0u, "timer callback ran too early"))
|
||||
return false;
|
||||
timrman.serviceDue(200u, executor);
|
||||
return expect(executor.calls == 1u, "timer callback did not run") &&
|
||||
expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") &&
|
||||
expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") &&
|
||||
expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") &&
|
||||
expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
|
||||
!testTimrmanPeriodicCallback())
|
||||
return 1;
|
||||
std::cout << "ps2xIOP import tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
|
||||
#include "emulator/core/iop_cpu.h"
|
||||
#include "emulator/core/iop_memory.h"
|
||||
#include "emulator/imports/iop_imports.h"
|
||||
#include "emulator/imports/iop_loadcore.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
using namespace ps2x::iop::detail;
|
||||
|
||||
void addExport(IopMemory &memory, IopImportRegistry &imports, uint32_t address,
|
||||
uint16_t version, uint32_t target, uint32_t count = 4u)
|
||||
{
|
||||
require(memory.zeroRam(address, 128u), "export table does not fit");
|
||||
memory.write32(address, 0x41C00000u);
|
||||
memory.write16(address + 8u, version);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(address + 12u, name, sizeof(name)), "export name does not fit");
|
||||
for (uint32_t i = 0u; i < count; ++i)
|
||||
memory.write32(address + 20u + 4u * i, target);
|
||||
require(imports.registerExportTable(address), "export registration failed");
|
||||
}
|
||||
|
||||
void importTable(IopMemory &memory, uint32_t address, uint16_t version)
|
||||
{
|
||||
require(memory.zeroRam(address, 64u), "import table does not fit");
|
||||
memory.write32(address, 0x41E00000u);
|
||||
memory.write16(address + 8u, version);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(address + 12u, name, sizeof(name)), "import name does not fit");
|
||||
memory.write32(address + 20u, 0x03E00008u);
|
||||
memory.write32(address + 24u, 0x24000003u);
|
||||
}
|
||||
|
||||
void decodeVersion()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
importTable(memory, 0x1000u, 0x0310u);
|
||||
const auto call = imports.decode(0x1014u);
|
||||
require(call && call->library == "tstlib" && call->ordinal == 3u && call->version == 0x0310u,
|
||||
"decoder dropped the import library version");
|
||||
const auto alias = imports.decode(0x80001014u);
|
||||
require(alias && alias->version == 0x0310u, "cached alias lost import version");
|
||||
}
|
||||
|
||||
void majorIsolation()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u);
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "linked to wrong library major");
|
||||
require(imports.resolve("tstlib", 3u, 0x0201u) == 0x2100u, "second major unavailable");
|
||||
require(imports.resolve("tstlib", 3u, 0x0300u) == 0u, "incompatible major silently linked");
|
||||
require(imports.findTable("tstlib", 0x0300u) == 0u, "query ignored requested major");
|
||||
}
|
||||
|
||||
void newestMinor()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0104u, 0x3100u);
|
||||
addExport(memory, imports, 0x1400u, 0x0103u, 0x4100u);
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "selected lowest address, not newest minor");
|
||||
require(imports.resolve("tstlib", 3u, 0x017Fu) == 0x3100u,
|
||||
"invented a minimum-minor rule absent from LOADCORE linking");
|
||||
require(imports.releaseExportTable(0x1800u), "unregister failed");
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x4100u, "unregistered library remained selected");
|
||||
}
|
||||
|
||||
void missingOrdinal()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u, 8u);
|
||||
addExport(memory, imports, 0x1800u, 0x0102u, 0x3100u, 4u);
|
||||
require(imports.resolve("tstlib", 7u, 0x0101u) == 0u,
|
||||
"missing ordinal fell back to a different export table");
|
||||
require(imports.resolve("missing", 0u, 0x0101u) == 0u, "missing library resolved");
|
||||
imports.reset();
|
||||
require(imports.resolve("tstlib", 0u, 0x0101u) == 0u, "registry reset left exports");
|
||||
}
|
||||
|
||||
void queryFunctionArray()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
IopLoadcore loadcore(memory, imports);
|
||||
addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u);
|
||||
importTable(memory, 0x800u, 0x0102u);
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = 0x800u;
|
||||
require(loadcore.dispatchImport(11u, cpu), "QueryLibraryEntryTable unhandled");
|
||||
require(cpu.gpr[2] == 0x1814u && memory.read32(cpu.gpr[2]) == 0x3100u,
|
||||
"query returned an export header instead of function array");
|
||||
memory.write16(0x808u, 0x0300u);
|
||||
require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "query accepted wrong major");
|
||||
for (uint32_t address : {0u, 0xFFFFFFF8u, IopMemory::RamSize - 4u})
|
||||
{
|
||||
cpu.gpr[4] = address;
|
||||
require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "invalid query pointer accepted");
|
||||
}
|
||||
}
|
||||
|
||||
Irx provider(uint32_t base, uint16_t version, uint32_t result)
|
||||
{
|
||||
Irx image(base);
|
||||
const uint32_t table = base + 0x80u;
|
||||
const uint32_t importStub = base + 0xC0u + 20u;
|
||||
image.words(0u, {0x27BDFFE0u, 0xAFBF001Cu,
|
||||
0x3C040000u | (table >> 16u), 0x34840000u | (table & 0xFFFFu),
|
||||
0x0C000000u | (importStub >> 2u), 0u,
|
||||
0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u});
|
||||
image.words(0x60u, {0x03E00008u, 0x24020000u | result});
|
||||
image.words(0x80u, {0x41C00000u, 0u, version, 0x6C747374u, 0x00006269u,
|
||||
base, base, base, base + 0x60u, 0u});
|
||||
image.words(0xC0u, {0x41E00000u, 0u, 0x0101u, 0x64616F6Cu, 0x65726F63u,
|
||||
0x03E00008u, 0x24000006u, 0u, 0u});
|
||||
return image;
|
||||
}
|
||||
|
||||
Irx consumer(uint16_t version)
|
||||
{
|
||||
constexpr uint32_t base = 0x13000u;
|
||||
Irx image(base);
|
||||
image.words(0u, {0x27BDFFF0u, 0xAFBF000Cu,
|
||||
0x0C000000u | ((base + 0x54u) >> 2u), 0u,
|
||||
0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0u});
|
||||
image.words(0x40u, {0x41E00000u, 0u, version, 0x6C747374u, 0x00006269u,
|
||||
0x03E00008u, 0x24000003u, 0u, 0u});
|
||||
return image;
|
||||
}
|
||||
|
||||
void physicalImportsEndToEnd()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
auto wrongMajor = provider(0x10000u, 0x0201u, 0x22u);
|
||||
wrongMajor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 2 failed");
|
||||
auto oldMinor = provider(0x11000u, 0x0101u, 0x11u);
|
||||
oldMinor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1 failed");
|
||||
auto newMinor = provider(0x12000u, 0x0103u, 0x13u);
|
||||
newMinor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1.3 failed");
|
||||
auto client = consumer(0x0101u);
|
||||
client.install(host);
|
||||
const auto result = iop.loadModuleBuffer(0x1000u);
|
||||
require(result.moduleId > 0 && result.startResult == 0x13, "R3000A called wrong export version");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"Import decoder preserves library ABI version", decodeVersion},
|
||||
{"Different major versions cannot cross-link", majorIsolation},
|
||||
{"Newest registered minor wins within the requested major", newestMinor},
|
||||
{"Ordinal lookup stays in the selected table", missingOrdinal},
|
||||
{"LOADCORE query returns function array and honors major", queryFunctionArray},
|
||||
{"Physical IRX consumer links correct version end to end", physicalImportsEndToEnd},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
@@ -5,6 +5,48 @@
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
inline constexpr uint32_t MIPS_INSTRUCTION_SIZE = sizeof(uint32_t);
|
||||
inline constexpr uint16_t MIPS_IMMEDIATE_SIGN_BIT = 0x8000u;
|
||||
inline constexpr uint32_t MIPS_JUMP_TARGET_SHIFT = 2u;
|
||||
inline constexpr uint32_t MIPS_JUMP_REGION_MASK = 0xF0000000u;
|
||||
|
||||
// R5900 general-purpose register indices used by the encoded RS/RT/RD fields.
|
||||
enum GprRegisters : uint32_t
|
||||
{
|
||||
GPR_ZERO = 0,
|
||||
GPR_AT = 1,
|
||||
GPR_V0 = 2,
|
||||
GPR_V1 = 3,
|
||||
GPR_A0 = 4,
|
||||
GPR_A1 = 5,
|
||||
GPR_A2 = 6,
|
||||
GPR_A3 = 7,
|
||||
GPR_T0 = 8,
|
||||
GPR_T1 = 9,
|
||||
GPR_T2 = 10,
|
||||
GPR_T3 = 11,
|
||||
GPR_T4 = 12,
|
||||
GPR_T5 = 13,
|
||||
GPR_T6 = 14,
|
||||
GPR_T7 = 15,
|
||||
GPR_S0 = 16,
|
||||
GPR_S1 = 17,
|
||||
GPR_S2 = 18,
|
||||
GPR_S3 = 19,
|
||||
GPR_S4 = 20,
|
||||
GPR_S5 = 21,
|
||||
GPR_S6 = 22,
|
||||
GPR_S7 = 23,
|
||||
GPR_T8 = 24,
|
||||
GPR_T9 = 25,
|
||||
GPR_K0 = 26,
|
||||
GPR_K1 = 27,
|
||||
GPR_GP = 28,
|
||||
GPR_SP = 29,
|
||||
GPR_FP = 30,
|
||||
GPR_RA = 31,
|
||||
};
|
||||
|
||||
// Basic MIPS opcodes (shared with R4300i)
|
||||
enum MipsOpcodes
|
||||
{
|
||||
|
||||
@@ -42,9 +42,12 @@ namespace ps2recomp
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::vector<Section> §ions);
|
||||
static size_t ResliceEntryFunctions(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions);
|
||||
static size_t ResliceEntryFunctions(std::vector<Function> &functions, std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions);
|
||||
static size_t CollectInternalEntryTargets(
|
||||
const std::vector<Function> &functions,
|
||||
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::unordered_set<uint32_t> &entryAddresses,
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner);
|
||||
|
||||
static std::string ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength);
|
||||
|
||||
@@ -67,6 +70,7 @@ namespace ps2recomp
|
||||
std::unordered_set<std::string> m_stubFunctions;
|
||||
std::unordered_set<uint32_t> m_stubFunctionStarts;
|
||||
std::unordered_map<uint32_t, std::string> m_stubHandlerBindingsByStart;
|
||||
std::unordered_set<uint32_t> m_entryPointHintStarts;
|
||||
std::unordered_set<uint32_t> m_correctnessCriticalFunctionStarts;
|
||||
std::map<uint32_t, std::string> m_generatedStubs;
|
||||
std::unordered_map<uint32_t, std::string> m_functionRenames;
|
||||
|
||||
@@ -187,6 +187,7 @@ namespace ps2recomp
|
||||
std::vector<std::string> skipFunctions;
|
||||
std::unordered_map<uint32_t, std::string> patches;
|
||||
std::vector<std::string> stubImplementations;
|
||||
std::vector<std::string> entryPointHints;
|
||||
std::unordered_map<uint32_t, uint32_t> mmioByInstructionAddress;
|
||||
std::vector<JumpTable> jumpTables;
|
||||
};
|
||||
|
||||
@@ -74,6 +74,27 @@ namespace ps2recomp
|
||||
config.stubImplementations = toml::find<std::vector<std::string>>(data, "stubs");
|
||||
}
|
||||
|
||||
auto appendEntryPointHints = [&](const toml::value &table, const char *key)
|
||||
{
|
||||
if (!table.contains(key) || !table.at(key).is_array())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const auto values = toml::find<std::vector<std::string>>(table, key);
|
||||
config.entryPointHints.insert(
|
||||
config.entryPointHints.end(), values.begin(), values.end());
|
||||
};
|
||||
appendEntryPointHints(general, "entry_points");
|
||||
appendEntryPointHints(data, "entry_points");
|
||||
// Backward compatibility
|
||||
appendEntryPointHints(general, "untracked_stubs");
|
||||
appendEntryPointHints(data, "untracked_stubs");
|
||||
|
||||
std::sort(config.entryPointHints.begin(), config.entryPointHints.end());
|
||||
config.entryPointHints.erase(
|
||||
std::unique(config.entryPointHints.begin(), config.entryPointHints.end()),
|
||||
config.entryPointHints.end());
|
||||
|
||||
if (general.contains("skip") && general.at("skip").is_array())
|
||||
{
|
||||
config.skipFunctions = toml::find<std::vector<std::string>>(general, "skip");
|
||||
@@ -276,6 +297,7 @@ namespace ps2recomp
|
||||
general["patch_cache"] = config.patchCache;
|
||||
general["skip"] = config.skipFunctions;
|
||||
general["stubs"] = config.stubImplementations;
|
||||
general["entry_points"] = config.entryPointHints;
|
||||
data["general"] = general;
|
||||
|
||||
if (!config.mmioByInstructionAddress.empty())
|
||||
|
||||
+1196
-31
File diff suppressed because it is too large
Load Diff
@@ -45,8 +45,8 @@ namespace ps2recomp
|
||||
ss << "#include <stdexcept>\n";
|
||||
ss << "#include \"ps2_runtime_macros.h\"\n";
|
||||
ss << "#include \"ps2_runtime.h\"\n";
|
||||
ss << "#include \"ps2_recompiled_functions.h\"\n";
|
||||
ss << "#include \"ps2_recompiled_stubs.h\"\n\n";
|
||||
ss << "#include <ps2_recompiled_functions.h>\n";
|
||||
ss << "#include <ps2_recompiled_stubs.h>\n\n";
|
||||
ss << "#include \"ps2_syscalls.h\"\n";
|
||||
ss << "#include \"ps2_stubs.h\"\n\n";
|
||||
ss << "#ifdef PS2_FUNCTION_LOG_TRACKER\n";
|
||||
|
||||
@@ -147,9 +147,9 @@ namespace ps2recomp
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "#include \"ps2_runtime.h\"\n";
|
||||
ss << "#include \"ps2_recompiled_functions.h\"\n";
|
||||
ss << "#include <ps2_recompiled_functions.h>\n";
|
||||
ss << "#include \"ps2_stubs.h\"\n";
|
||||
ss << "#include \"ps2_recompiled_stubs.h\"//this will give duplicated erros because runtime maybe has it define already, just delete the TODOS ones\n";
|
||||
ss << "#include <ps2_recompiled_stubs.h>\n";
|
||||
ss << "#include \"ps2_syscalls.h\"\n\n";
|
||||
|
||||
ss << "extern const uint32_t g_ps2RecompiledFunctionTableBase = 0x" << std::hex << tableBase << "u;\n";
|
||||
|
||||
@@ -69,14 +69,8 @@ namespace ps2recomp
|
||||
|
||||
MemoryAccessHint InstructionTranslator::effectiveMemoryHintFor(const Instruction &inst, const MemoryAccessHint &memoryHint) const
|
||||
{
|
||||
MemoryAccessHint effectiveMemoryHint = memoryHint;
|
||||
if (inst.isMmio)
|
||||
{
|
||||
effectiveMemoryHint.hasAddress = true;
|
||||
effectiveMemoryHint.address = inst.mmioAddress;
|
||||
}
|
||||
|
||||
return effectiveMemoryHint;
|
||||
// TODO disable for now since it causing issues with some games.
|
||||
return memoryHint;
|
||||
}
|
||||
|
||||
std::string InstructionTranslator::translateMemoryRead(const Instruction &inst,
|
||||
@@ -190,11 +184,11 @@ namespace ps2recomp
|
||||
case OPCODE_LW:
|
||||
return fmt::format("SET_GPR_S32(ctx, {}, (int32_t){});", inst.rt, genRead(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
case OPCODE_LBU:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, (uint8_t){});", inst.rt, genRead(8, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
return fmt::format("SET_GPR_ZE32(ctx, {}, (uint8_t){});", inst.rt, genRead(8, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
case OPCODE_LHU:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, (uint16_t){});", inst.rt, genRead(16, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
return fmt::format("SET_GPR_ZE32(ctx, {}, (uint16_t){});", inst.rt, genRead(16, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
case OPCODE_LWU:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, {});", inst.rt, genRead(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
return fmt::format("SET_GPR_ZE32(ctx, {}, {});", inst.rt, genRead(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)));
|
||||
case OPCODE_SB:
|
||||
return genWrite(8, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("(uint8_t)GPR_U32(ctx, {})", inst.rt)) + ";";
|
||||
case OPCODE_SH:
|
||||
|
||||
@@ -102,10 +102,10 @@ namespace ps2recomp
|
||||
void writeCombinedOutputPreamble(std::ostream &output)
|
||||
{
|
||||
output << "#include <stdexcept>\n";
|
||||
output << "#include \"ps2_recompiled_functions.h\"\n\n";
|
||||
output << "#include <ps2_recompiled_functions.h>\n\n";
|
||||
output << "#include \"ps2_runtime_macros.h\"\n";
|
||||
output << "#include \"ps2_runtime.h\"\n";
|
||||
output << "#include \"ps2_recompiled_stubs.h\"\n";
|
||||
output << "#include <ps2_recompiled_stubs.h>\n";
|
||||
output << "#include \"ps2_syscalls.h\"\n";
|
||||
output << "#include \"ps2_stubs.h\"\n";
|
||||
output << "#ifdef _DEBUG\n";
|
||||
@@ -288,7 +288,8 @@ namespace ps2recomp
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::vector<Section> §ions,
|
||||
CodeGenerator *codeGenerator,
|
||||
const std::function<bool(Function &)> &decodeExternalFunction)
|
||||
const std::function<bool(Function &)> &decodeExternalFunction,
|
||||
const std::unordered_set<uint32_t> &seedEntryAddresses = {})
|
||||
{
|
||||
std::unordered_set<uint32_t> existingStarts;
|
||||
for (const auto &function : functions)
|
||||
@@ -312,6 +313,19 @@ namespace ps2recomp
|
||||
return false;
|
||||
};
|
||||
|
||||
auto executableSectionEnd = [&](uint32_t address) -> std::optional<uint32_t>
|
||||
{
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (!section.isCode || address < section.address || address >= section.address + section.size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return section.address + section.size;
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
auto isSimpleReturnThunkStart = [](const Instruction &inst) -> bool
|
||||
{
|
||||
return inst.opcode == OPCODE_SPECIAL &&
|
||||
@@ -397,6 +411,14 @@ namespace ps2recomp
|
||||
pendingStarts.insert(target);
|
||||
};
|
||||
|
||||
if (stats.passCount == 1u)
|
||||
{
|
||||
for (uint32_t target : seedEntryAddresses)
|
||||
{
|
||||
queuePendingEntry(target);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &function : functions)
|
||||
{
|
||||
if (!function.isRecompiled || function.isStub || function.isSkipped)
|
||||
@@ -534,13 +556,24 @@ namespace ps2recomp
|
||||
}
|
||||
else
|
||||
{
|
||||
auto nextStartOpt = findNextBoundaryStart(target);
|
||||
if (!nextStartOpt.has_value() || nextStartOpt.value() <= target)
|
||||
const auto sectionEndOpt = executableSectionEnd(target);
|
||||
if (!sectionEndOpt.has_value())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entryFunction.end = nextStartOpt.value();
|
||||
uint32_t entryEnd = sectionEndOpt.value();
|
||||
auto nextStartOpt = findNextBoundaryStart(target);
|
||||
if (nextStartOpt.has_value() && nextStartOpt.value() < entryEnd)
|
||||
{
|
||||
entryEnd = nextStartOpt.value();
|
||||
}
|
||||
if (entryEnd <= target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entryFunction.end = entryEnd;
|
||||
if (!decodeExternalFunction(entryFunction))
|
||||
{
|
||||
continue;
|
||||
@@ -725,6 +758,71 @@ namespace ps2recomp
|
||||
|
||||
return reslicedCount;
|
||||
}
|
||||
|
||||
size_t collectInternalEntryTargetsImpl(
|
||||
const std::vector<Function> &functions,
|
||||
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::unordered_set<uint32_t> &entryAddresses,
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner)
|
||||
{
|
||||
std::unordered_set<uint32_t> functionStarts;
|
||||
functionStarts.reserve(functions.size());
|
||||
for (const auto &function : functions)
|
||||
{
|
||||
functionStarts.insert(function.start);
|
||||
}
|
||||
|
||||
size_t addedCount = 0u;
|
||||
for (uint32_t entryAddress : entryAddresses)
|
||||
{
|
||||
if (functionStarts.contains(entryAddress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const Function *owner = nullptr;
|
||||
for (const auto &function : functions)
|
||||
{
|
||||
if (!function.isRecompiled || function.isStub || function.isSkipped ||
|
||||
entryAddress <= function.start || entryAddress >= function.end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto decodedIt = decodedFunctions.find(function.start);
|
||||
if (decodedIt == decodedFunctions.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool containsInstruction = std::any_of(decodedIt->second.begin(), decodedIt->second.end(), [entryAddress](const Instruction &instruction)
|
||||
{ return instruction.address == entryAddress; });
|
||||
if (!containsInstruction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!owner || function.start > owner->start)
|
||||
{
|
||||
owner = &function;
|
||||
}
|
||||
}
|
||||
|
||||
if (!owner)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto &targets = targetsByOwner[owner->start];
|
||||
if (std::find(targets.begin(), targets.end(), entryAddress) == targets.end())
|
||||
{
|
||||
targets.push_back(entryAddress);
|
||||
++addedCount;
|
||||
}
|
||||
}
|
||||
|
||||
return addedCount;
|
||||
}
|
||||
}
|
||||
|
||||
PS2Recompiler::PS2Recompiler(const std::string &configPath)
|
||||
@@ -751,6 +849,7 @@ namespace ps2recomp
|
||||
m_stubFunctions.clear();
|
||||
m_stubFunctionStarts.clear();
|
||||
m_stubHandlerBindingsByStart.clear();
|
||||
m_entryPointHintStarts.clear();
|
||||
m_correctnessCriticalFunctionStarts.clear();
|
||||
|
||||
for (const auto &name : m_config.skipFunctions)
|
||||
@@ -792,6 +891,14 @@ namespace ps2recomp
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &hint : m_config.entryPointHints)
|
||||
{
|
||||
const FunctionSelector selector = parseFunctionSelector(hint);
|
||||
if (selector.start.has_value())
|
||||
{
|
||||
m_entryPointHintStarts.insert(*selector.start);
|
||||
}
|
||||
}
|
||||
|
||||
m_reporter.progress("parsing ELF");
|
||||
m_elfParser = std::make_unique<ElfParser>(m_config.inputPath);
|
||||
@@ -983,7 +1090,7 @@ namespace ps2recomp
|
||||
|
||||
if (isStubFunction(function))
|
||||
{
|
||||
if (!correctnessCritical || hasResolvedStubHandler(function))
|
||||
if (hasResolvedStubHandler(function))
|
||||
{
|
||||
function.isStub = true;
|
||||
function.isSkipped = false;
|
||||
@@ -991,12 +1098,15 @@ namespace ps2recomp
|
||||
continue;
|
||||
}
|
||||
|
||||
m_reporter.recordCorrectnessCriticalGuestFallback();
|
||||
if (correctnessCritical)
|
||||
{
|
||||
m_reporter.recordCorrectnessCriticalGuestFallback();
|
||||
}
|
||||
m_reporter.warningAt(
|
||||
"correctness-critical",
|
||||
"stub",
|
||||
function.name,
|
||||
function.start,
|
||||
"Unresolved initializer stub ignored; recompiling the original guest function");
|
||||
"Configured stub has no runtime handler; recompiling the original guest function");
|
||||
}
|
||||
|
||||
if (shouldSkipFunction(function))
|
||||
@@ -1792,6 +1902,63 @@ namespace ps2recomp
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> guestFallbackEntryAddresses = m_entryPointHintStarts;
|
||||
for (uint32_t address : m_stubFunctionStarts)
|
||||
{
|
||||
const auto bindingIt = m_stubHandlerBindingsByStart.find(address);
|
||||
if (bindingIt == m_stubHandlerBindingsByStart.end() ||
|
||||
resolveStubTarget(bindingIt->second) == StubTarget::Unknown)
|
||||
{
|
||||
guestFallbackEntryAddresses.insert(address);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer the existing wrapper when a configured entry lies inside a
|
||||
// decoded function. If Ghidra/analyzer omitted the whole routine,
|
||||
// synthesize a standalone guest function bounded by the next known
|
||||
// function instead of leaving a valid executable target unregistered.
|
||||
collectInternalEntryTargetsImpl(m_functions, m_decodedFunctions, guestFallbackEntryAddresses, m_resumeEntryTargetsByOwner);
|
||||
|
||||
std::unordered_set<uint32_t> coveredEntryAddresses;
|
||||
coveredEntryAddresses.reserve(m_functions.size() + guestFallbackEntryAddresses.size());
|
||||
for (const auto &function : m_functions)
|
||||
{
|
||||
coveredEntryAddresses.insert(function.start);
|
||||
}
|
||||
for (const auto &[owner, targets] : m_resumeEntryTargetsByOwner)
|
||||
{
|
||||
coveredEntryAddresses.insert(targets.begin(), targets.end());
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> standaloneEntryAddresses;
|
||||
for (uint32_t address : guestFallbackEntryAddresses)
|
||||
{
|
||||
if (!coveredEntryAddresses.contains(address))
|
||||
{
|
||||
standaloneEntryAddresses.insert(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (!standaloneEntryAddresses.empty())
|
||||
{
|
||||
const EntryDiscoveryStats configuredStats = discoverAdditionalEntryPointsImpl(
|
||||
m_functions,
|
||||
m_decodedFunctions,
|
||||
m_sections,
|
||||
nullptr,
|
||||
[this](Function &function)
|
||||
{ return decodeFunction(function); },
|
||||
standaloneEntryAddresses);
|
||||
if (configuredStats.discoveredCount > 0u)
|
||||
{
|
||||
m_reporter.recordAdditionalEntryPoints(configuredStats.discoveredCount);
|
||||
std::ostringstream msg;
|
||||
msg << "synthesized " << configuredStats.discoveredCount
|
||||
<< " standalone configured guest entry point(s)";
|
||||
m_reporter.progress(msg.str());
|
||||
}
|
||||
}
|
||||
|
||||
auto findContainingFunction = [&](uint32_t address) -> const Function *
|
||||
{
|
||||
const Function *best = nullptr;
|
||||
@@ -2152,12 +2319,12 @@ namespace ps2recomp
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
std::string PS2Recompiler::clampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
|
||||
std::string PS2Recompiler::clampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength)
|
||||
{
|
||||
if (maxLength == 0)
|
||||
{
|
||||
// Keep this static helper side-effect free; callers validate arguments.
|
||||
//Better go over the limit than create files with an empty path
|
||||
// Better go over the limit than create files with an empty path
|
||||
return baseName + extension;
|
||||
}
|
||||
|
||||
@@ -2224,13 +2391,20 @@ namespace ps2recomp
|
||||
return stats.discoveredCount;
|
||||
}
|
||||
|
||||
size_t PS2Recompiler::ResliceEntryFunctions(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
|
||||
size_t PS2Recompiler::ResliceEntryFunctions(std::vector<Function> &functions, std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
|
||||
{
|
||||
return resliceEntryFunctionsImpl(functions, decodedFunctions);
|
||||
}
|
||||
|
||||
size_t PS2Recompiler::CollectInternalEntryTargets(
|
||||
const std::vector<Function> &functions,
|
||||
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::unordered_set<uint32_t> &entryAddresses,
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner)
|
||||
{
|
||||
return collectInternalEntryTargetsImpl(functions, decodedFunctions, entryAddresses, targetsByOwner);
|
||||
}
|
||||
|
||||
StubTarget PS2Recompiler::resolveStubTarget(const std::string &name)
|
||||
{
|
||||
if (!ps2_runtime_calls::resolveSyscallName(name).empty())
|
||||
@@ -2244,7 +2418,7 @@ namespace ps2recomp
|
||||
return StubTarget::Unknown;
|
||||
}
|
||||
|
||||
std::string PS2Recompiler::ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
|
||||
std::string PS2Recompiler::ClampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength)
|
||||
{
|
||||
return clampFilenameLength(baseName, extension, maxLength);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,70 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class ExportPS2Functions extends GhidraScript {
|
||||
|
||||
// Names and values mirror ps2recomp::MipsOpcodes/SpecialFunctions/GprRegisters.
|
||||
// would be amazing cmake create this script coping the register from the header file
|
||||
private static final int MIPS_INSTRUCTION_SIZE = 4;
|
||||
private static final int MIPS_IMMEDIATE_BITS = 16;
|
||||
private static final int MIPS_IMMEDIATE_SIGN_BIT = 0x8000;
|
||||
private static final long UINT32_MASK = 0xFFFFFFFFL;
|
||||
private static final long OPCODE_MASK = 0x3FL;
|
||||
private static final long REGISTER_MASK = 0x1FL;
|
||||
private static final long IMMEDIATE_MASK = 0xFFFFL;
|
||||
private static final int OPCODE_SHIFT = 26;
|
||||
private static final int RS_SHIFT = 21;
|
||||
private static final int RT_SHIFT = 16;
|
||||
private static final int RD_SHIFT = 11;
|
||||
|
||||
private static final int GPR_ZERO = 0;
|
||||
private static final int GPR_A0 = 4;
|
||||
private static final int GPR_A3 = 7;
|
||||
private static final int GPR_SP = 29;
|
||||
private static final int GPR_RA = 31;
|
||||
|
||||
private static final int OPCODE_SPECIAL = 0x00;
|
||||
private static final int OPCODE_REGIMM = 0x01;
|
||||
private static final int OPCODE_J = 0x02;
|
||||
private static final int OPCODE_JAL = 0x03;
|
||||
private static final int OPCODE_BEQ = 0x04;
|
||||
private static final int OPCODE_BNE = 0x05;
|
||||
private static final int OPCODE_BLEZ = 0x06;
|
||||
private static final int OPCODE_BGTZ = 0x07;
|
||||
private static final int OPCODE_ADDI = 0x08;
|
||||
private static final int OPCODE_ADDIU = 0x09;
|
||||
private static final int OPCODE_SLTI = 0x0A;
|
||||
private static final int OPCODE_SLTIU = 0x0B;
|
||||
private static final int OPCODE_ANDI = 0x0C;
|
||||
private static final int OPCODE_ORI = 0x0D;
|
||||
private static final int OPCODE_XORI = 0x0E;
|
||||
private static final int OPCODE_LUI = 0x0F;
|
||||
private static final int OPCODE_BEQL = 0x14;
|
||||
private static final int OPCODE_BNEL = 0x15;
|
||||
private static final int OPCODE_BLEZL = 0x16;
|
||||
private static final int OPCODE_BGTZL = 0x17;
|
||||
private static final int OPCODE_DADDI = 0x18;
|
||||
private static final int OPCODE_DADDIU = 0x19;
|
||||
private static final int OPCODE_LDL = 0x1A;
|
||||
private static final int OPCODE_LDR = 0x1B;
|
||||
private static final int OPCODE_MMI = 0x1C;
|
||||
private static final int OPCODE_LQ = 0x1E;
|
||||
private static final int OPCODE_SQ = 0x1F;
|
||||
private static final int OPCODE_LB = 0x20;
|
||||
private static final int OPCODE_LH = 0x21;
|
||||
private static final int OPCODE_LWL = 0x22;
|
||||
private static final int OPCODE_LW = 0x23;
|
||||
private static final int OPCODE_LBU = 0x24;
|
||||
private static final int OPCODE_LHU = 0x25;
|
||||
private static final int OPCODE_LWR = 0x26;
|
||||
private static final int OPCODE_LWU = 0x27;
|
||||
private static final int OPCODE_SW = 0x2B;
|
||||
private static final int OPCODE_LL = 0x30;
|
||||
private static final int OPCODE_LLD = 0x34;
|
||||
private static final int OPCODE_LD = 0x37;
|
||||
private static final int OPCODE_SD = 0x3F;
|
||||
|
||||
private static final int SPECIAL_JR = 0x08;
|
||||
private static final int SPECIAL_JALR = 0x09;
|
||||
|
||||
// For now I have to copy all functions from the runtime handler list
|
||||
private static final Set<String> RUNTIME_HANDLER_NAMES = new HashSet<>(Arrays.asList(
|
||||
"FlushCache", "iFlushCache", "ResetEE", "SetMemoryMode", "InitThread", "CreateThread",
|
||||
@@ -51,7 +115,7 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
"fioWrite", "fioLseek", "fioMkdir", "fioChdir", "fioRmdir", "fioGetstat",
|
||||
"fioRemove", "SetGsCrt", "GsSetCrt", "GsGetIMR", "iGsGetIMR", "GsPutIMR",
|
||||
"iGsPutIMR", "SetVSyncFlag", "SetSyscall", "GsSetVideoMode", "GetOsdConfigParam", "SetOsdConfigParam",
|
||||
"EnableCache", "DisableCache", "GetRomName", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart",
|
||||
"EnableCache", "DisableCache", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart",
|
||||
"sceSifLoadModule", "sceSifLoadModuleBuffer", "SetupThread", "EndOfHeap", "GetMemorySize", "Deci2Call",
|
||||
"QueryBootMode", "GetThreadTLS", "Copy", "GetEntryAddress", "RegisterExitHandler", "ret0", "ret1", "reta0",
|
||||
"calloc_r", "free_r", "realloc_r", "memalign_r", "malloc_r", "malloc_extend_top", "malloc_trim_r", "mbtowc_r", "printf_r",
|
||||
@@ -207,6 +271,16 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
boolean syntheticEntry = false;
|
||||
}
|
||||
|
||||
private static final class AddressTakenCandidate {
|
||||
long sourceOffset;
|
||||
long target;
|
||||
|
||||
AddressTakenCandidate(long sourceOffset, long target) {
|
||||
this.sourceOffset = sourceOffset;
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ClassificationKind {
|
||||
STUB,
|
||||
UNTRACKED_STUB,
|
||||
@@ -224,7 +298,31 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
}
|
||||
|
||||
private static String hex(long value) {
|
||||
return String.format("0x%08X", value & 0xFFFFFFFFL);
|
||||
return String.format("0x%08X", value & UINT32_MASK);
|
||||
}
|
||||
|
||||
private static int opcode(long raw) {
|
||||
return (int) ((raw >>> OPCODE_SHIFT) & OPCODE_MASK);
|
||||
}
|
||||
|
||||
private static int rs(long raw) {
|
||||
return (int) ((raw >>> RS_SHIFT) & REGISTER_MASK);
|
||||
}
|
||||
|
||||
private static int rt(long raw) {
|
||||
return (int) ((raw >>> RT_SHIFT) & REGISTER_MASK);
|
||||
}
|
||||
|
||||
private static int rd(long raw) {
|
||||
return (int) ((raw >>> RD_SHIFT) & REGISTER_MASK);
|
||||
}
|
||||
|
||||
private static int function(long raw) {
|
||||
return (int) (raw & OPCODE_MASK);
|
||||
}
|
||||
|
||||
private static int immediate(long raw) {
|
||||
return (int) (raw & IMMEDIATE_MASK);
|
||||
}
|
||||
|
||||
private static String tomlString(String value) {
|
||||
@@ -466,8 +564,8 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
}
|
||||
|
||||
MemoryBlock fromBlock = currentProgram.getMemory().getBlock(from);
|
||||
if (fromBlock == null || !fromBlock.isExecute()) {
|
||||
continue; // lets ignore DATA/non-code refs
|
||||
if (fromBlock != null && fromBlock.isExecute()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +573,311 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
}
|
||||
|
||||
private static String makeAnonymousEntryName(long start) {
|
||||
return String.format("entry_%08x", start & 0xFFFFFFFFL);
|
||||
return String.format("entry_%08x", start & UINT32_MASK);
|
||||
}
|
||||
|
||||
private Long readWord(Address address) {
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return ((long) currentProgram.getMemory().getInt(address)) & UINT32_MASK;
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Address addressFromOffset(long offset) {
|
||||
try {
|
||||
return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset & UINT32_MASK);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean looksLikeCallableEntry(long target, boolean allowLeafThunk) {
|
||||
if ((target % MIPS_INSTRUCTION_SIZE) != 0L) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Address address = addressFromOffset(target);
|
||||
if (!isExecutableAddress(address) || currentProgram.getListing().getInstructionAt(address) == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int index = 0; index < 8; ++index) {
|
||||
Address probe;
|
||||
try {
|
||||
probe = address.add(index * (long) MIPS_INSTRUCTION_SIZE);
|
||||
} catch (Exception ignored) {
|
||||
break;
|
||||
}
|
||||
|
||||
Long rawValue = readWord(probe);
|
||||
if (rawValue == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
long raw = rawValue;
|
||||
int opcode = opcode(raw);
|
||||
int rs = rs(raw);
|
||||
int rt = rt(raw);
|
||||
int immediate = immediate(raw);
|
||||
|
||||
if (index < 4 && (opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU) && rs == GPR_SP && rt == GPR_SP && (immediate & MIPS_IMMEDIATE_SIGN_BIT) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) &&
|
||||
rs == GPR_SP && rt == GPR_RA) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowLeafThunk && opcode == OPCODE_SPECIAL &&
|
||||
function(raw) == SPECIAL_JR && rs == GPR_RA) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean writesGpr(long raw, int register) {
|
||||
if (register == GPR_ZERO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int opcode = opcode(raw);
|
||||
int rt = rt(raw);
|
||||
int rd = rd(raw);
|
||||
|
||||
if (opcode == OPCODE_SPECIAL || opcode == OPCODE_MMI) {
|
||||
return rd == register;
|
||||
}
|
||||
if (opcode == OPCODE_JAL) {
|
||||
return register == GPR_RA;
|
||||
}
|
||||
|
||||
boolean writesRt;
|
||||
switch (opcode) {
|
||||
case OPCODE_ADDI:
|
||||
case OPCODE_ADDIU:
|
||||
case OPCODE_SLTI:
|
||||
case OPCODE_SLTIU:
|
||||
case OPCODE_ANDI:
|
||||
case OPCODE_ORI:
|
||||
case OPCODE_XORI:
|
||||
case OPCODE_LUI:
|
||||
case OPCODE_DADDI:
|
||||
case OPCODE_DADDIU:
|
||||
case OPCODE_LDL:
|
||||
case OPCODE_LDR:
|
||||
case OPCODE_LQ:
|
||||
case OPCODE_LB:
|
||||
case OPCODE_LH:
|
||||
case OPCODE_LWL:
|
||||
case OPCODE_LW:
|
||||
case OPCODE_LBU:
|
||||
case OPCODE_LHU:
|
||||
case OPCODE_LWR:
|
||||
case OPCODE_LWU:
|
||||
case OPCODE_LL:
|
||||
case OPCODE_LLD:
|
||||
case OPCODE_LD:
|
||||
writesRt = true;
|
||||
break;
|
||||
default:
|
||||
writesRt = false;
|
||||
break;
|
||||
}
|
||||
return writesRt && rt == register;
|
||||
}
|
||||
|
||||
private static boolean isControlTransfer(long raw) {
|
||||
int opcode = opcode(raw);
|
||||
switch (opcode) {
|
||||
case OPCODE_REGIMM:
|
||||
case OPCODE_J:
|
||||
case OPCODE_JAL:
|
||||
case OPCODE_BEQ:
|
||||
case OPCODE_BNE:
|
||||
case OPCODE_BLEZ:
|
||||
case OPCODE_BGTZ:
|
||||
case OPCODE_BEQL:
|
||||
case OPCODE_BNEL:
|
||||
case OPCODE_BLEZL:
|
||||
case OPCODE_BGTZL:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (opcode != OPCODE_SPECIAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int function = function(raw);
|
||||
return function == SPECIAL_JR || function == SPECIAL_JALR;
|
||||
}
|
||||
|
||||
private static boolean isCallInstruction(long raw) {
|
||||
int opcode = opcode(raw);
|
||||
return opcode == OPCODE_JAL ||
|
||||
(opcode == OPCODE_SPECIAL && function(raw) == SPECIAL_JALR);
|
||||
}
|
||||
|
||||
private void addSyntheticEntry(List<FunctionRecord> records, Set<Long> existingStarts, long target) {
|
||||
target &= UINT32_MASK;
|
||||
if (!existingStarts.add(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FunctionRecord record = new FunctionRecord();
|
||||
record.name = makeAnonymousEntryName(target);
|
||||
record.start = target;
|
||||
record.syntheticEntry = true;
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
private void collectMaterializedCodeEntries(List<FunctionRecord> records, Set<Long> existingStarts) {
|
||||
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;
|
||||
}
|
||||
|
||||
Long upperRawValue = readWord(instruction.getAddress());
|
||||
if (upperRawValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long upperRaw = upperRawValue;
|
||||
if (opcode(upperRaw) != OPCODE_LUI) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int upperRegister = rt(upperRaw);
|
||||
if (upperRegister == GPR_ZERO) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long upperValue = ((long) immediate(upperRaw)) << MIPS_IMMEDIATE_BITS;
|
||||
boolean sawControlTransfer = false;
|
||||
boolean sawCallTransfer = false;
|
||||
|
||||
for (int lookahead = 1; lookahead <= 4; ++lookahead) {
|
||||
Address lowAddress;
|
||||
try {
|
||||
lowAddress = instruction.getAddress().add(lookahead * (long) MIPS_INSTRUCTION_SIZE);
|
||||
} catch (Exception ignored) {
|
||||
break;
|
||||
}
|
||||
|
||||
Long lowRawValue = readWord(lowAddress);
|
||||
if (lowRawValue == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
long lowRaw = lowRawValue;
|
||||
int opcode = opcode(lowRaw);
|
||||
int rs = rs(lowRaw);
|
||||
int rt = rt(lowRaw);
|
||||
|
||||
if ((opcode == OPCODE_ADDIU || opcode == OPCODE_ORI || opcode == OPCODE_DADDIU) &&
|
||||
rs == upperRegister) {
|
||||
int immediate = immediate(lowRaw);
|
||||
long target;
|
||||
if (opcode == OPCODE_ORI) {
|
||||
target = upperValue | immediate;
|
||||
} else {
|
||||
target = (upperValue + (short) immediate) & UINT32_MASK;
|
||||
}
|
||||
|
||||
Long nextRaw = readWord(addressFromOffset(
|
||||
lowAddress.getOffset() + MIPS_INSTRUCTION_SIZE));
|
||||
boolean followedByCall = nextRaw != null && isCallInstruction(nextRaw);
|
||||
boolean materializedAsCallArgument =
|
||||
rt >= GPR_A0 && rt <= GPR_A3 && (sawCallTransfer || followedByCall);
|
||||
|
||||
if (looksLikeCallableEntry(target, materializedAsCallArgument)) {
|
||||
addSyntheticEntry(records, existingStarts, target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sawControlTransfer) {
|
||||
break;
|
||||
}
|
||||
if (writesGpr(lowRaw, upperRegister)) {
|
||||
break;
|
||||
}
|
||||
if (isControlTransfer(lowRaw)) {
|
||||
sawControlTransfer = true;
|
||||
sawCallTransfer = isCallInstruction(lowRaw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDedicatedFunctionPointerBlock(String name) {
|
||||
return ".ctors".equals(name) || ".dtors".equals(name) ||
|
||||
".init_array".equals(name) || ".fini_array".equals(name);
|
||||
}
|
||||
|
||||
private void collectDataFunctionPointerEntries(List<FunctionRecord> records, Set<Long> existingStarts) {
|
||||
final long clusterDistanceBytes = 32L;
|
||||
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
if (block == null || block.isExecute() || !block.isInitialized() ||
|
||||
block.getSize() < MIPS_INSTRUCTION_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<AddressTakenCandidate> candidates = new ArrayList<>();
|
||||
for (long offset = 0;
|
||||
offset + MIPS_INSTRUCTION_SIZE <= block.getSize() && !monitor.isCancelled();
|
||||
offset += MIPS_INSTRUCTION_SIZE) {
|
||||
Address source;
|
||||
try {
|
||||
source = block.getStart().add(offset);
|
||||
} catch (Exception ignored) {
|
||||
break;
|
||||
}
|
||||
|
||||
Long target = readWord(source);
|
||||
if (target != null && looksLikeCallableEntry(target, true)) {
|
||||
candidates.add(new AddressTakenCandidate(offset, target));
|
||||
}
|
||||
}
|
||||
|
||||
boolean dedicatedPointerBlock = isDedicatedFunctionPointerBlock(block.getName());
|
||||
for (int index = 0; index < candidates.size(); ++index) {
|
||||
AddressTakenCandidate candidate = candidates.get(index);
|
||||
boolean clustered = dedicatedPointerBlock;
|
||||
|
||||
if (index > 0 &&
|
||||
candidate.sourceOffset - candidates.get(index - 1).sourceOffset <= clusterDistanceBytes) {
|
||||
clustered = true;
|
||||
}
|
||||
if (index + 1 < candidates.size() &&
|
||||
candidates.get(index + 1).sourceOffset - candidate.sourceOffset <= clusterDistanceBytes) {
|
||||
clustered = true;
|
||||
}
|
||||
|
||||
if (clustered) {
|
||||
addSyntheticEntry(records, existingStarts, candidate.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<FunctionRecord> collectExecutableLabelRecords(List<FunctionRecord> functionRecords) {
|
||||
@@ -555,6 +957,13 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
existingStarts.add(start);
|
||||
}
|
||||
|
||||
// Ghidra does not always promote function pointers to CALL references,
|
||||
// especially when the low half is produced in a MIPS delay slot. Mirror
|
||||
// the stripped-ELF fallback used by ElfParser so the CSV still contains
|
||||
// callback and vtable entries that are only address-taken.
|
||||
collectMaterializedCodeEntries(labelRecords, existingStarts);
|
||||
collectDataFunctionPointerEntries(labelRecords, existingStarts);
|
||||
|
||||
if (labelRecords.isEmpty()) {
|
||||
return labelRecords;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 32 CACHE STRING "Unity build batch size f
|
||||
option(PS2X_ENABLE_RUNNER_PCH "Precompile the heavy runtime headers for ps2EntryRunner" ON)
|
||||
option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON)
|
||||
|
||||
option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF)
|
||||
option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" OFF)
|
||||
option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" ON)
|
||||
option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" ON)
|
||||
option(PS2X_ENABLE_IOP_RPC_TRACE "Log unhandled IOP/SIF RPC trace suggestions" ON)
|
||||
option(PS2X_STRICT_RETURN_DIAGNOSTICS "Route generated JR $ra returns through runtime branch diagnostics" OFF)
|
||||
option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON)
|
||||
@@ -386,6 +386,8 @@ add_library(ps2_runtime STATIC
|
||||
src/lib/ps2_iop_host.cpp
|
||||
src/lib/ps2_memory.cpp
|
||||
src/lib/ps2_pad.cpp
|
||||
src/lib/ps2_rom_device.cpp
|
||||
src/lib/ps2_vfs.cpp
|
||||
src/lib/ps2_runtime.cpp
|
||||
src/lib/ps2_vif1_interpreter.cpp
|
||||
src/lib/vu/ps2_vu1_core.cpp
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
X(SetOsdConfigParam2) \
|
||||
X(EnableCache) \
|
||||
X(DisableCache) \
|
||||
X(GetRomName) \
|
||||
X(SifLoadElfPart) \
|
||||
X(sceSifLoadElf) \
|
||||
X(sceSifLoadElfPart) \
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <functional>
|
||||
#if defined(_MSC_VER)
|
||||
#include <intrin.h>
|
||||
@@ -30,6 +31,8 @@
|
||||
#include "runtime/ps2_vu1.h"
|
||||
#include "runtime/ps2_audio.h"
|
||||
#include "runtime/ps2_pad.h"
|
||||
#include "runtime/ps2_rom_device.h"
|
||||
#include "runtime/ps2_vfs.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
namespace ps2x::iop
|
||||
@@ -287,8 +290,16 @@ public:
|
||||
bool loadELF(const std::string &elfPath);
|
||||
void run();
|
||||
|
||||
void setIopPluginSearchPaths(std::vector<std::filesystem::path> paths);
|
||||
[[nodiscard]] ps2x::iop::ModuleLoadResult loadIopModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] ps2x::iop::ModuleLoadResult loadIopModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] bool stopIopModule(int32_t moduleId, int32_t *result = nullptr);
|
||||
[[nodiscard]] ps2x::iop::DebugSnapshot iopDebugSnapshot() const;
|
||||
uint32_t allocateIopMemory(uint32_t size, uint32_t alignment = 16u);
|
||||
bool freeIopMemory(uint32_t address);
|
||||
bool readIopMemory(uint32_t address, void *destination, size_t size) const;
|
||||
bool writeIopMemory(uint32_t address, const void *source, size_t size);
|
||||
bool zeroIopMemory(uint32_t address, size_t size);
|
||||
bool isIopMemoryRange(uint32_t address, size_t size) const;
|
||||
|
||||
using DebugUiCallback = void (*)(PS2Runtime &runtime, void *userData);
|
||||
void setDebugUiCallbacks(DebugUiCallback initCallback,
|
||||
@@ -440,6 +451,10 @@ public:
|
||||
inline const PS2AudioBackend &audioBackend() const { return m_audioBackend; }
|
||||
inline PSPadBackend &padBackend() { return m_padBackend; }
|
||||
inline const PSPadBackend &padBackend() const { return m_padBackend; }
|
||||
inline PS2RomDevice &romDevice() { return m_romDevice; }
|
||||
inline const PS2RomDevice &romDevice() const { return m_romDevice; }
|
||||
inline PS2Vfs &vfs() { return m_vfs; }
|
||||
inline const PS2Vfs &vfs() const { return m_vfs; }
|
||||
|
||||
private:
|
||||
struct GuestHeapBlock
|
||||
@@ -463,8 +478,10 @@ private:
|
||||
void HandleIntegerOverflow(R5900Context *ctx);
|
||||
|
||||
[[nodiscard]] ps2x::iop::RpcAbi selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const;
|
||||
[[nodiscard]] bool canBindIopRpc(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] ps2x::iop::RpcResult handleIopRpc(uint8_t *rdram, R5900Context *ctx, ps2x::iop::RpcRequest request);
|
||||
void notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransfer &transfer);
|
||||
void advanceIopEeCycles(uint64_t eeCycles) noexcept;
|
||||
void resetIop();
|
||||
|
||||
friend class PS2IopTransport;
|
||||
@@ -478,6 +495,8 @@ private:
|
||||
std::unique_ptr<ps2x::iop::IopSubsystem> m_iopSubsystem;
|
||||
PS2AudioBackend m_audioBackend;
|
||||
PSPadBackend m_padBackend;
|
||||
PS2RomDevice m_romDevice;
|
||||
PS2Vfs m_vfs;
|
||||
VU1Interpreter m_vu0{VU1Interpreter::Unit::VU0};
|
||||
VU1Interpreter m_vu1{VU1Interpreter::Unit::VU1};
|
||||
R5900Context m_cpuContext;
|
||||
|
||||
@@ -758,6 +758,17 @@ static inline void Ps2SetGprLow64(R5900Context *ctx, int reg, __m128i new_low)
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
|
||||
#define SET_GPR_ZE32(ctx_ptr, reg_idx, val) \
|
||||
do \
|
||||
{ \
|
||||
if ((reg_idx) != 0) \
|
||||
{ \
|
||||
__m128i _newVal = _mm_cvtsi64_si128((int64_t)(uint32_t)(val)); \
|
||||
Ps2SetGprLow64(ctx_ptr, reg_idx, _newVal); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define SET_GPR_S32(ctx_ptr, reg_idx, val) \
|
||||
do \
|
||||
{ \
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
std::string translatePs2Path(const char *ps2Path);
|
||||
|
||||
inline std::mutex g_sys_fd_mutex;
|
||||
|
||||
namespace ps2_syscalls
|
||||
{
|
||||
#define PS2_DECLARE_SYSCALL(name) void name(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||
|
||||
@@ -90,6 +90,7 @@ enum class GuestInvocationKind : uint8_t
|
||||
SyscallOverride,
|
||||
ExitHandler,
|
||||
HleCall,
|
||||
SifCommand,
|
||||
};
|
||||
|
||||
struct GuestInvocation
|
||||
@@ -181,6 +182,9 @@ struct EeThreadSnapshot
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t pc = 0;
|
||||
uint32_t ra = 0;
|
||||
uint32_t sp = 0;
|
||||
uint32_t contextGp = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t stack = 0;
|
||||
uint32_t stackSize = 0;
|
||||
@@ -192,6 +196,7 @@ struct EeThreadSnapshot
|
||||
int waitId = 0;
|
||||
int suspendCount = 0;
|
||||
uint32_t wakeupCount = 0;
|
||||
uint32_t invocationDepth = 0;
|
||||
};
|
||||
|
||||
struct EeSemaphoreSnapshot
|
||||
@@ -390,6 +395,8 @@ private:
|
||||
[[nodiscard]] bool hasReadyAtOrAbovePriority(int priority) const;
|
||||
void renewTimeSlice();
|
||||
void copyMainContextToRuntime();
|
||||
void publishDebugContext(const R5900Context &context);
|
||||
void publishIdleDebugContext();
|
||||
|
||||
PS2Runtime &m_runtime;
|
||||
uint8_t *m_rdram = nullptr;
|
||||
|
||||
@@ -14,6 +14,7 @@ public:
|
||||
virtual void Reset() = 0;
|
||||
|
||||
virtual void Submit(const GSPrimitiveBatch &batch) = 0;
|
||||
virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0;
|
||||
|
||||
virtual void BeginTransfer(const GSTransferCommand &command) = 0;
|
||||
virtual void UploadImage(const uint8_t *data, uint32_t sizeBytes) = 0;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime/gs/gs_backend.h"
|
||||
#include "runtime/gs/gs_texture_page_cache.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
void Reset() override;
|
||||
|
||||
void Submit(const GSPrimitiveBatch &batch) override;
|
||||
void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) override;
|
||||
void BeginTransfer(const GSTransferCommand &command) override;
|
||||
void UploadImage(const uint8_t *data, uint32_t sizeBytes) override;
|
||||
|
||||
@@ -34,7 +35,9 @@ public:
|
||||
|
||||
private:
|
||||
void ResetUnlocked();
|
||||
void LoadClutUnlocked(const GSTex0Reg &tex0, const GSTexClutReg &texclut);
|
||||
uint32_t ReadVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y) const;
|
||||
uint32_t ReadTextureVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y);
|
||||
void WriteVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value);
|
||||
|
||||
void DrawPrimitive(const GSPrimitiveBatch &batch);
|
||||
@@ -43,7 +46,7 @@ private:
|
||||
void DrawLine(const GSPrimitiveBatch &batch);
|
||||
void WritePixel(const GSDrawState &state, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t fog);
|
||||
uint32_t SampleTexture(const GSDrawState &state, float s, float t, float q, uint16_t u, uint16_t v);
|
||||
uint32_t LookupCLUT(const GSDrawState &state, uint8_t index, uint32_t cbp, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm);
|
||||
uint32_t LookupCLUT(const GSDrawState &state, uint8_t index, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm);
|
||||
|
||||
void PerformLocalToLocalTransfer();
|
||||
void PerformLocalToHostTransfer();
|
||||
@@ -58,8 +61,8 @@ private:
|
||||
uint32_t sourceOriginX,
|
||||
uint32_t sourceOriginY) const;
|
||||
|
||||
using WriteVramFunc = std::function<void(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)>;
|
||||
using ReadVramFunc = std::function<uint32_t(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t)>;
|
||||
using WriteVramFunc = void (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
using ReadVramFunc = uint32_t (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
|
||||
static constexpr size_t kPsmHandlerCount = 1u << 6u;
|
||||
mutable std::mutex m_mutex;
|
||||
@@ -67,6 +70,9 @@ private:
|
||||
uint32_t m_vramSize = 0;
|
||||
std::array<ReadVramFunc, kPsmHandlerCount> m_readVramFuncs{};
|
||||
std::array<WriteVramFunc, kPsmHandlerCount> m_writeVramFuncs{};
|
||||
std::array<uint16_t, 512> m_clut{};
|
||||
std::array<uint32_t, 2> m_clutCbp{};
|
||||
GSMem::TexturePageCache m_texturePageCache;
|
||||
|
||||
GSTransferCommand m_transfer{};
|
||||
GSTransferSnapshot m_transferState{};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace GSMem
|
||||
{
|
||||
class TexturePageCache
|
||||
{
|
||||
public:
|
||||
static constexpr uint32_t kPageSize = 8192u;
|
||||
|
||||
void Invalidate() noexcept
|
||||
{
|
||||
m_pageBase = UINT32_MAX;
|
||||
}
|
||||
|
||||
// byteAddress is the wrapped, swizzled VRAM address. The returned
|
||||
// pointer is valid only until the next miss or invalidation.
|
||||
const uint8_t* Resolve(const uint8_t* vram, uint32_t byteAddress) noexcept
|
||||
{
|
||||
const uint32_t pageBase = byteAddress & ~(kPageSize - 1u);
|
||||
if (m_pageBase != pageBase)
|
||||
{
|
||||
std::memcpy(m_bytes.data(), vram + pageBase, kPageSize);
|
||||
m_pageBase = pageBase;
|
||||
}
|
||||
return m_bytes.data() + (byteAddress & (kPageSize - 1u));
|
||||
}
|
||||
|
||||
private:
|
||||
alignas(64) std::array<uint8_t, kPageSize> m_bytes{};
|
||||
uint32_t m_pageBase = UINT32_MAX;
|
||||
};
|
||||
}
|
||||
@@ -7,11 +7,12 @@
|
||||
#include <span>
|
||||
|
||||
#include "types.h"
|
||||
#include "runtime/gs/gs_texture_page_cache.h"
|
||||
|
||||
namespace GSMem
|
||||
{
|
||||
constexpr usz MEMORY_SIZE = 4_mb;
|
||||
constexpr usz GS_PAGE_SIZE = 8_kb;
|
||||
constexpr usz GS_PAGE_SIZE = TexturePageCache::kPageSize;
|
||||
|
||||
// these are all the same regardless of storage mode
|
||||
constexpr usz BLOCKS_PER_PAGE = 32;
|
||||
@@ -261,7 +262,7 @@ namespace GSMem
|
||||
static constexpr void Write(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y, PackedT value);
|
||||
|
||||
// reads the pixel
|
||||
static constexpr auto Read(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y) -> PackedT;
|
||||
static constexpr auto Read(const PageLookupTableT& table, const u8* data, u32 block, u32 bw, u32 x, u32 y, TexturePageCache* cache = nullptr) -> PackedT;
|
||||
|
||||
static_assert(BlocksPerPage() == BLOCKS_PER_PAGE);
|
||||
static_assert(IsValidPsm(psm));
|
||||
@@ -501,15 +502,16 @@ namespace GSMem
|
||||
}
|
||||
|
||||
template<PixelStorageMode psm>
|
||||
constexpr auto PixelStorageTraits<psm>::Read(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y) -> PackedT
|
||||
constexpr auto PixelStorageTraits<psm>::Read(const PageLookupTableT& table, const u8* data, u32 block, u32 bw, u32 x, u32 y, TexturePageCache* cache) -> PackedT
|
||||
{
|
||||
const u32 pixel_addr = Address(table, block, bw, x, y);
|
||||
const u32 bits = pixel_addr * UnpackedBitWidth(psm) + BitOffset();
|
||||
const u32 byte_addr = (bits / 8) & (MEMORY_SIZE - sizeof(PackedT));
|
||||
const u32 shift = bits % 8;
|
||||
|
||||
const u8* source = cache ? cache->Resolve(data, byte_addr) : data + byte_addr;
|
||||
PackedT v;
|
||||
std::memcpy(&v, &data[byte_addr], sizeof(PackedT));
|
||||
std::memcpy(&v, source, sizeof(PackedT));
|
||||
|
||||
switch (psm)
|
||||
{
|
||||
@@ -533,11 +535,14 @@ namespace GSMem
|
||||
break;
|
||||
}
|
||||
|
||||
return 0xFFFF00FFu;
|
||||
return static_cast<PackedT>(0xFFFF00FFu);
|
||||
}
|
||||
|
||||
void InitLookupTables();
|
||||
|
||||
// Shares swizzle, VRAM wrapping, and lane extraction with the direct reads.
|
||||
u32 ReadTexture(TexturePageCache& cache, const u8* data, u32 psm, u32 bp, u32 bw, u32 x, u32 y);
|
||||
|
||||
void WriteCT32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value);
|
||||
void WriteZ32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value);
|
||||
|
||||
|
||||
@@ -449,6 +449,8 @@ public:
|
||||
};
|
||||
|
||||
std::array<EeTimer, 4> m_eeTimers{};
|
||||
bool tryProcessScratchpadDma(uint32_t channelBase, uint32_t chcr);
|
||||
void completeDmacChannel(uint32_t channelBase, uint32_t cause);
|
||||
void queueCompletedDmacCause(uint32_t cause);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct PS2RomProfile
|
||||
{
|
||||
std::string id;
|
||||
std::string provider = "application";
|
||||
ps2x::iop::GameMatcher matcher;
|
||||
std::unordered_map<std::string, std::vector<uint8_t>> files;
|
||||
};
|
||||
|
||||
class PS2RomDevice
|
||||
{
|
||||
public:
|
||||
PS2RomDevice();
|
||||
|
||||
static void registerProfile(PS2RomProfile profile);
|
||||
|
||||
bool configure(const ps2x::iop::GameIdentity &identity, std::string *error = nullptr);
|
||||
[[nodiscard]] bool readFile(std::string_view ps2Path, std::vector<uint8_t> &bytes) const;
|
||||
[[nodiscard]] bool fileSize(std::string_view ps2Path, uint64_t &size) const;
|
||||
[[nodiscard]] bool contains(std::string_view ps2Path) const;
|
||||
[[nodiscard]] std::string_view activeProfile() const noexcept { return m_activeProfile; }
|
||||
[[nodiscard]] std::string_view activeProvider() const noexcept { return m_activeProvider; }
|
||||
|
||||
private:
|
||||
static std::string normalizePath(std::string_view path);
|
||||
void mountBaseProfile();
|
||||
void mountFiles(const std::unordered_map<std::string, std::vector<uint8_t>> &files);
|
||||
|
||||
std::unordered_map<std::string, std::vector<uint8_t>> m_files;
|
||||
std::string m_activeProfile;
|
||||
std::string m_activeProvider;
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class PS2RomDevice;
|
||||
|
||||
struct PS2VfsMounts
|
||||
{
|
||||
std::filesystem::path hostRoot;
|
||||
std::filesystem::path cdRoot;
|
||||
std::filesystem::path memoryCard0Root;
|
||||
};
|
||||
|
||||
struct PS2VfsStat
|
||||
{
|
||||
bool directory = false;
|
||||
bool readOnly = false;
|
||||
uint64_t size = 0u;
|
||||
std::time_t created = 0;
|
||||
std::time_t accessed = 0;
|
||||
std::time_t modified = 0;
|
||||
};
|
||||
|
||||
struct PS2VfsDescriptorInfo
|
||||
{
|
||||
int32_t descriptor = -1;
|
||||
std::string device;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
class IPS2OpenFile
|
||||
{
|
||||
public:
|
||||
virtual ~IPS2OpenFile() = default;
|
||||
|
||||
[[nodiscard]] virtual int64_t read(void *destination, size_t size) = 0;
|
||||
[[nodiscard]] virtual int64_t write(const void *source, size_t size) = 0;
|
||||
[[nodiscard]] virtual int64_t seek(int64_t offset, int whence) = 0;
|
||||
};
|
||||
|
||||
class PS2Vfs
|
||||
{
|
||||
public:
|
||||
PS2Vfs() = default;
|
||||
~PS2Vfs();
|
||||
|
||||
PS2Vfs(const PS2Vfs &) = delete;
|
||||
PS2Vfs &operator=(const PS2Vfs &) = delete;
|
||||
|
||||
[[nodiscard]] int32_t open(std::string_view path, uint32_t flags, const PS2VfsMounts &mounts, const PS2RomDevice &rom);
|
||||
[[nodiscard]] int32_t close(int32_t descriptor);
|
||||
[[nodiscard]] int64_t read(int32_t descriptor, void *destination, size_t size);
|
||||
[[nodiscard]] int64_t write(int32_t descriptor, const void *source, size_t size);
|
||||
[[nodiscard]] int64_t seek(int32_t descriptor, int64_t offset, int whence);
|
||||
|
||||
[[nodiscard]] bool stat(std::string_view path, const PS2VfsMounts &mounts, const PS2RomDevice &rom, PS2VfsStat &result) const;
|
||||
[[nodiscard]] bool resolveHostPath(std::string_view path, const PS2VfsMounts &mounts, std::filesystem::path &result) const;
|
||||
[[nodiscard]] std::vector<PS2VfsDescriptorInfo> descriptors() const;
|
||||
|
||||
private:
|
||||
struct OpenDescriptor
|
||||
{
|
||||
std::unique_ptr<IPS2OpenFile> file;
|
||||
std::string device;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::unordered_map<int32_t, OpenDescriptor> m_descriptors;
|
||||
int32_t m_nextDescriptor = 3;
|
||||
};
|
||||
@@ -170,6 +170,8 @@ void EeScheduler::run()
|
||||
GuestThread *next = selectReady();
|
||||
if (!next && m_pendingInvocations.empty())
|
||||
{
|
||||
copyMainContextToRuntime();
|
||||
publishIdleDebugContext();
|
||||
publishSnapshot();
|
||||
waitForEvent();
|
||||
continue;
|
||||
@@ -224,10 +226,7 @@ void EeScheduler::run()
|
||||
--m_debugPublishCountdown;
|
||||
}
|
||||
|
||||
m_runtime.m_debugPc.store(context.pc, std::memory_order_relaxed);
|
||||
m_runtime.m_debugRa.store(getRegU32(&context, 31), std::memory_order_relaxed);
|
||||
m_runtime.m_debugSp.store(getRegU32(&context, 29), std::memory_order_relaxed);
|
||||
m_runtime.m_debugGp.store(getRegU32(&context, 28), std::memory_order_relaxed);
|
||||
publishDebugContext(context);
|
||||
|
||||
if (context.pc == 0u)
|
||||
{
|
||||
@@ -249,10 +248,13 @@ void EeScheduler::run()
|
||||
}
|
||||
makeDormant(*running);
|
||||
m_currentThreadId = 0;
|
||||
copyMainContextToRuntime();
|
||||
publishIdleDebugContext();
|
||||
publishSnapshot();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!m_pendingInvocations.empty())
|
||||
if (!m_pendingInvocations.empty() && running->invocations.empty())
|
||||
{
|
||||
GuestInvocation invocation = std::move(m_pendingInvocations.front());
|
||||
m_pendingInvocations.pop_front();
|
||||
@@ -391,6 +393,7 @@ void EeScheduler::accountCycles(uint32_t cycles) noexcept
|
||||
const uint64_t elapsed = std::max<uint64_t>(1u, cycles);
|
||||
m_eeCycle += elapsed;
|
||||
m_pendingEeTimerInterrupts |= m_runtime.memory().advanceEeTimers(elapsed);
|
||||
m_runtime.advanceIopEeCycles(elapsed);
|
||||
if (m_pendingEeTimerInterrupts != 0u)
|
||||
{
|
||||
m_checkpointPending.store(true, std::memory_order_release);
|
||||
@@ -1278,7 +1281,7 @@ void EeScheduler::dispatchIrq(bool dmac, uint32_t cause)
|
||||
SET_GPR_U32(&invocation.context, 4, cause);
|
||||
SET_GPR_U32(&invocation.context, 5, handler.argument);
|
||||
SET_GPR_U32(&invocation.context, 28, handler.gp);
|
||||
SET_GPR_U32(&invocation.context, 29, handler.sp);
|
||||
SET_GPR_U32(&invocation.context, 29, 0u);
|
||||
SET_GPR_U32(&invocation.context, 31, 0u);
|
||||
queueInvocation(std::move(invocation));
|
||||
}
|
||||
@@ -1496,7 +1499,11 @@ void EeScheduler::publishSnapshot()
|
||||
}
|
||||
EeThreadSnapshot snapshot{};
|
||||
snapshot.id = id;
|
||||
snapshot.pc = item.activeContext().pc;
|
||||
const R5900Context &context = item.activeContext();
|
||||
snapshot.pc = context.pc;
|
||||
snapshot.ra = getRegU32(&context, 31);
|
||||
snapshot.sp = getRegU32(&context, 29);
|
||||
snapshot.contextGp = getRegU32(&context, 28);
|
||||
snapshot.entry = item.entry;
|
||||
snapshot.stack = item.stack;
|
||||
snapshot.stackSize = item.stackSize;
|
||||
@@ -1508,6 +1515,7 @@ void EeScheduler::publishSnapshot()
|
||||
snapshot.waitId = waitObjectId(item.wait);
|
||||
snapshot.suspendCount = item.suspendCount;
|
||||
snapshot.wakeupCount = item.wakeupCount;
|
||||
snapshot.invocationDepth = static_cast<uint32_t>(item.invocations.size());
|
||||
next.threads.push_back(snapshot);
|
||||
}
|
||||
std::sort(next.threads.begin(), next.threads.end(), [](const auto &left, const auto &right)
|
||||
@@ -1918,7 +1926,7 @@ void EeScheduler::processEvent(const EeEvent &event)
|
||||
SET_GPR_U32(&invocation.context, 5, static_cast<uint32_t>(alarm.ticks));
|
||||
SET_GPR_U32(&invocation.context, 6, alarm.argument);
|
||||
SET_GPR_U32(&invocation.context, 28, alarm.gp);
|
||||
SET_GPR_U32(&invocation.context, 29, alarm.sp);
|
||||
SET_GPR_U32(&invocation.context, 29, 0u);
|
||||
SET_GPR_U32(&invocation.context, 31, 0u);
|
||||
queueInvocation(std::move(invocation));
|
||||
break;
|
||||
@@ -2104,3 +2112,35 @@ void EeScheduler::copyMainContextToRuntime()
|
||||
m_runtime.m_cpuContext = main->context;
|
||||
}
|
||||
}
|
||||
|
||||
void EeScheduler::publishDebugContext(const R5900Context &context)
|
||||
{
|
||||
m_runtime.m_debugPc.store(context.pc, std::memory_order_relaxed);
|
||||
m_runtime.m_debugRa.store(getRegU32(&context, 31), std::memory_order_relaxed);
|
||||
m_runtime.m_debugSp.store(getRegU32(&context, 29), std::memory_order_relaxed);
|
||||
m_runtime.m_debugGp.store(getRegU32(&context, 28), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void EeScheduler::publishIdleDebugContext()
|
||||
{
|
||||
// Temporary IRQ/RPC/alarm invocations deliberately return to PC=0. Once
|
||||
// the scheduler is idle, show a real EE thread context instead of leaving
|
||||
// the debugger pinned to that completed dispatcher frame.
|
||||
const GuestThread *selected = thread(kMainThreadId);
|
||||
if (!selected)
|
||||
{
|
||||
for (const auto &[id, candidate] : m_threads)
|
||||
{
|
||||
if (id > 0 && candidate.status != EeThreadStatus::Dormant)
|
||||
{
|
||||
selected = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selected)
|
||||
{
|
||||
publishDebugContext(selected->activeContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,6 @@ namespace
|
||||
uint32_t g_cdStreamingEndLbn = 0xFFFFFFFFu;
|
||||
bool g_cdInitialized = false;
|
||||
|
||||
constexpr uint32_t kIopHeapBase = 0x04000000;
|
||||
constexpr uint32_t kIopHeapLimit = 0x04500000;
|
||||
constexpr uint32_t kIopHeapAlign = 64;
|
||||
uint32_t g_iopHeapNext = kIopHeapBase;
|
||||
|
||||
std::string toLowerAscii(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(),
|
||||
@@ -1360,7 +1355,11 @@ namespace
|
||||
uint32_t madr = 0;
|
||||
uint32_t qwc = 0;
|
||||
uint32_t tadr = payloadPhys;
|
||||
uint32_t chcr = 0x00000181u; // DIR=1, TIE=1, STR=1 (normal mode).
|
||||
PS2Memory &mem = runtime->memory();
|
||||
|
||||
const uint32_t configuredChcr = mem.readIORegister(channelBase + 0x00u);
|
||||
const uint32_t transferTagEnable = configuredChcr & 0x00000040u;
|
||||
uint32_t chcr = 0x00000181u | transferTagEnable; // DIR=1, TIE=1, STR=1 (normal mode).
|
||||
|
||||
if (preferNormalCount)
|
||||
{
|
||||
@@ -1369,10 +1368,9 @@ namespace
|
||||
}
|
||||
else
|
||||
{
|
||||
chcr = 0x00000185u; // MODE=1 chain, DIR=1, TIE=1, STR=1.
|
||||
chcr = 0x00000185u | transferTagEnable; // MODE=1 chain, DIR=1, TIE=1, STR=1.
|
||||
}
|
||||
|
||||
PS2Memory &mem = runtime->memory();
|
||||
mem.writeIORegister(channelBase + 0x20u, qwc & 0xFFFFu);
|
||||
mem.writeIORegister(channelBase + 0x10u, madr);
|
||||
mem.writeIORegister(channelBase + 0x30u, tadr);
|
||||
@@ -1402,10 +1400,10 @@ namespace
|
||||
if (g_dmaStubLogCount < kMaxDmaStubLogs)
|
||||
{
|
||||
RUNTIME_LOG("[sceDmaSend] ch=0x" << std::hex << channelBase
|
||||
<< " madr=0x" << madr
|
||||
<< " qwc=0x" << qwc
|
||||
<< " tadr=0x" << tadr
|
||||
<< " chcr=0x" << chcr << std::dec << std::endl);
|
||||
<< " madr=0x" << madr
|
||||
<< " qwc=0x" << qwc
|
||||
<< " tadr=0x" << tadr
|
||||
<< " chcr=0x" << chcr << std::dec << std::endl);
|
||||
|
||||
if (!preferNormalCount && (channelBase == 0x10009000u || channelBase == 0x1000A000u))
|
||||
{
|
||||
@@ -1418,13 +1416,13 @@ namespace
|
||||
std::memcpy(&w2, tagPtr + 8u, sizeof(w2));
|
||||
std::memcpy(&w3, tagPtr + 12u, sizeof(w3));
|
||||
RUNTIME_LOG("[sceDmaSend:head] ch=0x" << std::hex << channelBase
|
||||
<< " tagQwc=0x" << static_cast<uint32_t>(tagLo & 0xFFFFu)
|
||||
<< " id=0x" << static_cast<uint32_t>((tagLo >> 28u) & 0x7u)
|
||||
<< " irq=0x" << static_cast<uint32_t>((tagLo >> 31u) & 0x1u)
|
||||
<< " addr=0x" << static_cast<uint32_t>((tagLo >> 32u) & 0x7FFFFFFFu)
|
||||
<< " w2=0x" << w2
|
||||
<< " w3=0x" << w3
|
||||
<< std::dec << std::endl);
|
||||
<< " tagQwc=0x" << static_cast<uint32_t>(tagLo & 0xFFFFu)
|
||||
<< " id=0x" << static_cast<uint32_t>((tagLo >> 28u) & 0x7u)
|
||||
<< " irq=0x" << static_cast<uint32_t>((tagLo >> 31u) & 0x1u)
|
||||
<< " addr=0x" << static_cast<uint32_t>((tagLo >> 32u) & 0x7FFFFFFFu)
|
||||
<< " w2=0x" << w2
|
||||
<< " w3=0x" << w3
|
||||
<< std::dec << std::endl);
|
||||
}
|
||||
}
|
||||
++g_dmaStubLogCount;
|
||||
@@ -1838,9 +1836,9 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool readGsDBuff(uint8_t* rdram, uint32_t addr, GsDBuffMem& out)
|
||||
static bool readGsDBuff(uint8_t *rdram, uint32_t addr, GsDBuffMem &out)
|
||||
{
|
||||
const uint8_t* ptr = getConstMemPtr(rdram, addr);
|
||||
const uint8_t *ptr = getConstMemPtr(rdram, addr);
|
||||
if (!ptr)
|
||||
return false;
|
||||
std::memcpy(&out, ptr, sizeof(out));
|
||||
@@ -1856,9 +1854,9 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writeGsDBuff(uint8_t* rdram, uint32_t addr, const GsDBuffMem& db)
|
||||
static bool writeGsDBuff(uint8_t *rdram, uint32_t addr, const GsDBuffMem &db)
|
||||
{
|
||||
uint8_t* ptr = getMemPtr(rdram, addr);
|
||||
uint8_t *ptr = getMemPtr(rdram, addr);
|
||||
if (!ptr)
|
||||
return false;
|
||||
std::memcpy(ptr, &db, sizeof(db));
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
#include "../Syscalls/RPC.h"
|
||||
#include "../../ps2_iop_transport.h"
|
||||
#include "runtime/ps2_address.h"
|
||||
#include "runtime/ee_scheduler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2_stubs
|
||||
@@ -28,15 +29,22 @@ namespace ps2_stubs
|
||||
const uint32_t size = readStackU32(rdram, ctx, 20);
|
||||
if (size != 0u && srcAddr != 0u && dstAddr != 0u)
|
||||
{
|
||||
std::vector<uint8_t> payload(size);
|
||||
bool valid = runtime != nullptr;
|
||||
for (uint32_t i = 0; i < size; ++i)
|
||||
{
|
||||
const uint8_t *src = getConstMemPtr(rdram, srcAddr + i);
|
||||
uint8_t *dst = getMemPtr(rdram, dstAddr + i);
|
||||
if (!src || !dst)
|
||||
if (!src)
|
||||
{
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
*dst = *src;
|
||||
payload[i] = *src;
|
||||
}
|
||||
if (!valid || !runtime->writeIopMemory(dstAddr, payload.data(), payload.size()))
|
||||
{
|
||||
setReturnS32(ctx, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +65,15 @@ namespace ps2_stubs
|
||||
std::mutex g_sifDmaTransferMutex;
|
||||
uint32_t g_nextSifDmaTransferId = 1u;
|
||||
std::mutex g_sifCmdStateMutex;
|
||||
std::mutex g_sifHeapMutex;
|
||||
std::unordered_map<uint32_t, uint32_t> g_sifRegs;
|
||||
std::unordered_map<uint32_t, uint32_t> g_sifSregs;
|
||||
std::unordered_map<uint32_t, uint32_t> g_sifCmdHandlers;
|
||||
std::map<uint32_t, uint32_t> g_sifHeapAllocations;
|
||||
std::array<uint8_t, kIopHeapLimit - kIopHeapBase> g_sifHeapStorage{};
|
||||
struct SifCmdHandler
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t argument = 0u;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, SifCmdHandler> g_sifCmdHandlers;
|
||||
uint32_t g_sifCmdBuffer = 0u;
|
||||
uint32_t g_sifSysCmdBuffer = 0u;
|
||||
bool g_sifCmdInitialized = false;
|
||||
@@ -127,92 +138,6 @@ namespace ps2_stubs
|
||||
return id;
|
||||
}
|
||||
|
||||
uint32_t alignIopHeapSize(uint32_t size)
|
||||
{
|
||||
return (size + (kIopHeapAlign - 1u)) & ~(kIopHeapAlign - 1u);
|
||||
}
|
||||
|
||||
uint32_t allocateSifHeapBlock(uint32_t requestSize)
|
||||
{
|
||||
const uint32_t alignedSize = alignIopHeapSize(requestSize);
|
||||
if (alignedSize == 0u)
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
uint32_t candidate = kIopHeapBase;
|
||||
for (const auto &[addr, size] : g_sifHeapAllocations)
|
||||
{
|
||||
if (candidate + alignedSize <= addr)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t blockEnd = alignIopHeapSize(addr + size);
|
||||
if (blockEnd > candidate)
|
||||
{
|
||||
candidate = blockEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate < kIopHeapBase || candidate + alignedSize > kIopHeapLimit)
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
|
||||
g_sifHeapAllocations[candidate] = alignedSize;
|
||||
std::fill_n(g_sifHeapStorage.data() + (candidate - kIopHeapBase),
|
||||
alignedSize,
|
||||
uint8_t{0});
|
||||
g_iopHeapNext = candidate + alignedSize;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
bool freeSifHeapBlock(uint32_t addr)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
const auto it = g_sifHeapAllocations.find(addr);
|
||||
if (it == g_sifHeapAllocations.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
g_sifHeapAllocations.erase(it);
|
||||
if (g_sifHeapAllocations.empty())
|
||||
{
|
||||
g_iopHeapNext = kIopHeapBase;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void resetSifHeapState()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
g_sifHeapAllocations.clear();
|
||||
g_sifHeapStorage.fill(0u);
|
||||
g_iopHeapNext = kIopHeapBase;
|
||||
}
|
||||
|
||||
bool isAllocatedSifHeapRangeLocked(uint32_t address, size_t size)
|
||||
{
|
||||
if (address < kIopHeapBase || address >= kIopHeapLimit || size > static_cast<size_t>(kIopHeapLimit - address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto it = g_sifHeapAllocations.upper_bound(address);
|
||||
if (it == g_sifHeapAllocations.begin())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
--it;
|
||||
|
||||
const uint64_t allocationEnd = static_cast<uint64_t>(it->first) + it->second;
|
||||
const uint64_t rangeEnd = static_cast<uint64_t>(address) + size;
|
||||
return address >= it->first && rangeEnd <= allocationEnd;
|
||||
}
|
||||
|
||||
bool isCopyableGuestAddress(uint32_t addr)
|
||||
{
|
||||
if (Ps2AddressInRange(addr, PS2_SCRATCHPAD_BASE, PS2_SCRATCHPAD_SIZE))
|
||||
@@ -238,13 +163,9 @@ namespace ps2_stubs
|
||||
return false;
|
||||
}
|
||||
|
||||
bool canCopyAddressRange(const uint8_t *rdram, uint32_t address, uint32_t sizeBytes)
|
||||
bool canAccessEeRange(const uint8_t *rdram, uint32_t address, uint32_t sizeBytes)
|
||||
{
|
||||
if (isSifIopHeapRange(address, sizeBytes))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (isSifIopHeapAddress(address) || !rdram)
|
||||
if (!rdram)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -259,7 +180,7 @@ namespace ps2_stubs
|
||||
for (uint32_t i = 0u; i < sizeBytes; ++i)
|
||||
{
|
||||
const uint32_t byteAddress = address + i;
|
||||
if (!isCopyableGuestAddress(byteAddress) ||getConstMemPtr(rdram, byteAddress) == nullptr)
|
||||
if (!isCopyableGuestAddress(byteAddress) || getConstMemPtr(rdram, byteAddress) == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -267,200 +188,129 @@ namespace ps2_stubs
|
||||
return true;
|
||||
}
|
||||
|
||||
bool canCopyGuestByteRange(const uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes)
|
||||
bool readEeRange(const uint8_t *rdram, uint32_t address, void *destination, uint32_t sizeBytes)
|
||||
{
|
||||
return canCopyAddressRange(rdram, srcAddr, sizeBytes) && canCopyAddressRange(rdram, dstAddr, sizeBytes);
|
||||
}
|
||||
|
||||
bool copyGuestByteRange(uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes)
|
||||
{
|
||||
if (!canCopyGuestByteRange(rdram, dstAddr, srcAddr, sizeBytes))
|
||||
{
|
||||
if ((!destination && sizeBytes != 0u) || !canAccessEeRange(rdram, address, sizeBytes))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sizeBytes == 0u)
|
||||
auto *bytes = static_cast<uint8_t *>(destination);
|
||||
for (uint32_t i = 0u; i < sizeBytes; ++i)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool sourceIsIop = isSifIopHeapRange(srcAddr, sizeBytes);
|
||||
const bool destinationIsIop = isSifIopHeapRange(dstAddr, sizeBytes);
|
||||
if (sourceIsIop || destinationIsIop)
|
||||
{
|
||||
std::vector<uint8_t> payload(sizeBytes);
|
||||
if (sourceIsIop)
|
||||
{
|
||||
if (!readSifIopHeap(srcAddr, payload.data(), payload.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint32_t i = 0u; i < sizeBytes; ++i)
|
||||
{
|
||||
const uint8_t *src = getConstMemPtr(rdram, srcAddr + i);
|
||||
if (!src)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
payload[i] = *src;
|
||||
}
|
||||
}
|
||||
|
||||
if (destinationIsIop)
|
||||
{
|
||||
return writeSifIopHeap(dstAddr, payload.data(), payload.size());
|
||||
}
|
||||
|
||||
ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr);
|
||||
for (uint32_t i = 0u; i < sizeBytes; ++i)
|
||||
{
|
||||
uint8_t *dst = getMemPtr(rdram, dstAddr + i);
|
||||
if (!dst)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*dst = payload[i];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr);
|
||||
|
||||
const uint64_t srcBegin = srcAddr;
|
||||
const uint64_t srcEnd = srcBegin + static_cast<uint64_t>(sizeBytes);
|
||||
const uint64_t dstBegin = dstAddr;
|
||||
const bool copyBackward = (dstBegin > srcBegin) && (dstBegin < srcEnd);
|
||||
|
||||
if (copyBackward)
|
||||
{
|
||||
for (uint32_t i = sizeBytes; i > 0u; --i)
|
||||
{
|
||||
const uint32_t index = i - 1u;
|
||||
const uint8_t *src = getConstMemPtr(rdram, srcAddr + index);
|
||||
uint8_t *dst = getMemPtr(rdram, dstAddr + index);
|
||||
if (!src || !dst)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*dst = *src;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < sizeBytes; ++i)
|
||||
{
|
||||
const uint8_t *src = getConstMemPtr(rdram, srcAddr + i);
|
||||
uint8_t *dst = getMemPtr(rdram, dstAddr + i);
|
||||
if (!src || !dst)
|
||||
{
|
||||
const uint8_t *source = getConstMemPtr(rdram, address + i);
|
||||
if (!source)
|
||||
return false;
|
||||
}
|
||||
*dst = *src;
|
||||
bytes[i] = *source;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool isSifIopHeapAddress(uint32_t address)
|
||||
{
|
||||
return address >= kIopHeapBase && address < kIopHeapLimit;
|
||||
}
|
||||
|
||||
bool isSifIopHeapRange(uint32_t address, size_t size)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
return isAllocatedSifHeapRangeLocked(address, size);
|
||||
}
|
||||
|
||||
bool readSifIopHeap(uint32_t address, void *destination, size_t size)
|
||||
{
|
||||
if (!destination && size != 0u)
|
||||
bool writeEeRange(uint8_t *rdram, uint32_t address, const void *source, uint32_t sizeBytes)
|
||||
{
|
||||
return false;
|
||||
if ((!source && sizeBytes != 0u) || !canAccessEeRange(rdram, address, sizeBytes))
|
||||
return false;
|
||||
ps2TraceGuestRangeWrite(rdram, address, sizeBytes, "SIF IOP-to-EE DMA", nullptr);
|
||||
const auto *bytes = static_cast<const uint8_t *>(source);
|
||||
for (uint32_t i = 0u; i < sizeBytes; ++i)
|
||||
{
|
||||
uint8_t *destination = getMemPtr(rdram, address + i);
|
||||
if (!destination)
|
||||
return false;
|
||||
*destination = bytes[i];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
if (!isAllocatedSifHeapRangeLocked(address, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memcpy(destination,
|
||||
g_sifHeapStorage.data() + (address - kIopHeapBase),
|
||||
size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSifIopHeap(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
if (!source && size != 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
if (!isAllocatedSifHeapRangeLocked(address, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memcpy(g_sifHeapStorage.data() + (address - kIopHeapBase),
|
||||
source,
|
||||
size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool zeroSifIopHeap(uint32_t address, size_t size)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sifHeapMutex);
|
||||
if (!isAllocatedSifHeapRangeLocked(address, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memset(g_sifHeapStorage.data() + (address - kIopHeapBase), 0, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void resetSifState()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||
seedDefaultSifRegsLocked();
|
||||
resetSifHeapState();
|
||||
}
|
||||
|
||||
bool dispatchSifCommand(uint8_t *rdram,
|
||||
PS2Runtime *runtime,
|
||||
uint32_t commandId,
|
||||
const void *packet,
|
||||
size_t packetSize) noexcept
|
||||
{
|
||||
if (!rdram || !runtime || !packet || packetSize < 16u || packetSize > 112u)
|
||||
return false;
|
||||
|
||||
SifCmdHandler registered{};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||
const auto handler = g_sifCmdHandlers.find(commandId);
|
||||
if (handler == g_sifCmdHandlers.end() || handler->second.function == 0u)
|
||||
return false;
|
||||
registered = handler->second;
|
||||
}
|
||||
|
||||
if (!runtime->hasFunction(registered.function))
|
||||
return false;
|
||||
|
||||
const uint32_t packetAddress = runtime->guestMalloc(static_cast<uint32_t>(packetSize), 16u);
|
||||
if (packetAddress == 0u)
|
||||
return false;
|
||||
|
||||
uint8_t *const first = getMemPtr(rdram, packetAddress);
|
||||
uint8_t *const last = getMemPtr(rdram, packetAddress + static_cast<uint32_t>(packetSize - 1u));
|
||||
if (!first || !last || last < first || static_cast<size_t>(last - first) != packetSize - 1u)
|
||||
{
|
||||
runtime->guestFree(packetAddress);
|
||||
return false;
|
||||
}
|
||||
|
||||
ps2TraceGuestRangeWrite(rdram, packetAddress, static_cast<uint32_t>(packetSize), "SIF command packet", nullptr);
|
||||
std::memcpy(first, packet, packetSize);
|
||||
|
||||
try
|
||||
{
|
||||
GuestInvocation invocation{};
|
||||
invocation.kind = GuestInvocationKind::SifCommand;
|
||||
invocation.tag = commandId;
|
||||
invocation.context = runtime->cpu();
|
||||
invocation.context.pc = registered.function;
|
||||
SET_GPR_U32(&invocation.context, 4, packetAddress);
|
||||
SET_GPR_U32(&invocation.context, 5, registered.argument);
|
||||
SET_GPR_U32(&invocation.context, 6, 0u);
|
||||
SET_GPR_U32(&invocation.context, 7, 0u);
|
||||
SET_GPR_U32(&invocation.context, 29, 0u);
|
||||
SET_GPR_U32(&invocation.context, 31, 0u);
|
||||
invocation.onComplete = [runtime, packetAddress](const R5900Context &, R5900Context &)
|
||||
{
|
||||
runtime->guestFree(packetAddress);
|
||||
};
|
||||
runtime->eeScheduler().queueInvocation(std::move(invocation));
|
||||
return true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
runtime->guestFree(packetAddress);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void sceSifAddCmdHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
const uint32_t cid = getRegU32(ctx, 4);
|
||||
const uint32_t handler = getRegU32(ctx, 5);
|
||||
const uint32_t argument = getRegU32(ctx, 6);
|
||||
std::lock_guard<std::mutex> lock(g_sifCmdStateMutex);
|
||||
g_sifCmdHandlers[cid] = handler;
|
||||
g_sifCmdHandlers[cid] = SifCmdHandler{handler, argument};
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceSifAllocIopHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
(void)rdram;
|
||||
(void)runtime;
|
||||
|
||||
const uint32_t reqSize = getRegU32(ctx, 4);
|
||||
setReturnU32(ctx, allocateSifHeapBlock(reqSize));
|
||||
setReturnU32(ctx, runtime ? runtime->allocateIopMemory(reqSize, 64u) : 0u);
|
||||
}
|
||||
|
||||
void sceSifAllocSysMemory(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
(void)rdram;
|
||||
(void)runtime;
|
||||
|
||||
const uint32_t size = getRegU32(ctx, 5);
|
||||
setReturnU32(ctx, allocateSifHeapBlock(size));
|
||||
setReturnU32(ctx, runtime ? runtime->allocateIopMemory(size, 64u) : 0u);
|
||||
}
|
||||
|
||||
void sceSifBindRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -503,19 +353,15 @@ namespace ps2_stubs
|
||||
void sceSifFreeIopHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
(void)rdram;
|
||||
(void)runtime;
|
||||
|
||||
const uint32_t addr = getRegU32(ctx, 4);
|
||||
setReturnS32(ctx, freeSifHeapBlock(addr) ? 0 : -1);
|
||||
setReturnS32(ctx, runtime && runtime->freeIopMemory(addr) ? 0 : -1);
|
||||
}
|
||||
|
||||
void sceSifFreeSysMemory(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
(void)rdram;
|
||||
(void)runtime;
|
||||
|
||||
const uint32_t addr = getRegU32(ctx, 4);
|
||||
setReturnS32(ctx, freeSifHeapBlock(addr) ? 0 : -1);
|
||||
setReturnS32(ctx, runtime && runtime->freeIopMemory(addr) ? 0 : -1);
|
||||
}
|
||||
|
||||
void sceSifGetDataTable(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -564,15 +410,19 @@ namespace ps2_stubs
|
||||
if (runtime)
|
||||
{
|
||||
PS2IopTransport::notifyTransfer(runtime, rdram, {
|
||||
ps2x::iop::SifTransferKind::GetOtherData,
|
||||
ps2x::iop::SifTransferPhase::BeforeCopy,
|
||||
srcAddr,
|
||||
dstAddr,
|
||||
size,
|
||||
});
|
||||
ps2x::iop::SifTransferKind::GetOtherData,
|
||||
ps2x::iop::SifTransferPhase::BeforeCopy,
|
||||
srcAddr,
|
||||
dstAddr,
|
||||
size,
|
||||
});
|
||||
}
|
||||
|
||||
if (!copyGuestByteRange(rdram, dstAddr, srcAddr, size))
|
||||
std::vector<uint8_t> payload(size);
|
||||
if (!runtime || !runtime->isIopMemoryRange(srcAddr, size) ||
|
||||
!canAccessEeRange(rdram, dstAddr, size) ||
|
||||
!runtime->readIopMemory(srcAddr, payload.data(), payload.size()) ||
|
||||
!writeEeRange(rdram, dstAddr, payload.data(), size))
|
||||
{
|
||||
static uint32_t warnCount = 0;
|
||||
if (warnCount < 32u)
|
||||
@@ -600,12 +450,12 @@ namespace ps2_stubs
|
||||
if (runtime)
|
||||
{
|
||||
PS2IopTransport::notifyTransfer(runtime, rdram, {
|
||||
ps2x::iop::SifTransferKind::GetOtherData,
|
||||
ps2x::iop::SifTransferPhase::AfterCopy,
|
||||
srcAddr,
|
||||
dstAddr,
|
||||
size,
|
||||
});
|
||||
ps2x::iop::SifTransferKind::GetOtherData,
|
||||
ps2x::iop::SifTransferPhase::AfterCopy,
|
||||
srcAddr,
|
||||
dstAddr,
|
||||
size,
|
||||
});
|
||||
}
|
||||
|
||||
setReturnS32(ctx, 0);
|
||||
@@ -668,7 +518,7 @@ namespace ps2_stubs
|
||||
|
||||
void sceSifInitIopHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
resetSifHeapState();
|
||||
// The physical IOP allocator is initialized by IopSubsystem::reset().
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
@@ -709,6 +559,7 @@ namespace ps2_stubs
|
||||
|
||||
void sceSifRebootIop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
PS2IopTransport::reset(runtime);
|
||||
setReturnS32(ctx, 1);
|
||||
}
|
||||
|
||||
@@ -737,6 +588,7 @@ namespace ps2_stubs
|
||||
|
||||
void sceSifResetIop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
PS2IopTransport::reset(runtime);
|
||||
setReturnS32(ctx, 1);
|
||||
}
|
||||
|
||||
@@ -838,7 +690,7 @@ namespace ps2_stubs
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (!canCopyGuestByteRange(rdram, xfer.dest, xfer.src, sizeBytes))
|
||||
if (!runtime || !canAccessEeRange(rdram, xfer.src, sizeBytes) || !runtime->isIopMemoryRange(xfer.dest, sizeBytes))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
@@ -855,14 +707,16 @@ namespace ps2_stubs
|
||||
if (runtime)
|
||||
{
|
||||
PS2IopTransport::notifyTransfer(runtime, rdram, {
|
||||
ps2x::iop::SifTransferKind::SetDma,
|
||||
ps2x::iop::SifTransferPhase::BeforeCopy,
|
||||
xfer.src,
|
||||
xfer.dest,
|
||||
static_cast<uint32_t>(xfer.size),
|
||||
});
|
||||
ps2x::iop::SifTransferKind::SetDma,
|
||||
ps2x::iop::SifTransferPhase::BeforeCopy,
|
||||
xfer.src,
|
||||
xfer.dest,
|
||||
static_cast<uint32_t>(xfer.size),
|
||||
});
|
||||
}
|
||||
if (!copyGuestByteRange(rdram, xfer.dest, xfer.src, static_cast<uint32_t>(xfer.size)))
|
||||
const uint32_t sizeBytes = static_cast<uint32_t>(xfer.size);
|
||||
std::vector<uint8_t> payload(sizeBytes);
|
||||
if (!readEeRange(rdram, xfer.src, payload.data(), sizeBytes) || !runtime->writeIopMemory(xfer.dest, payload.data(), payload.size()))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
@@ -870,12 +724,12 @@ namespace ps2_stubs
|
||||
if (runtime)
|
||||
{
|
||||
PS2IopTransport::notifyTransfer(runtime, rdram, {
|
||||
ps2x::iop::SifTransferKind::SetDma,
|
||||
ps2x::iop::SifTransferPhase::AfterCopy,
|
||||
xfer.src,
|
||||
xfer.dest,
|
||||
static_cast<uint32_t>(xfer.size),
|
||||
});
|
||||
ps2x::iop::SifTransferKind::SetDma,
|
||||
ps2x::iop::SifTransferPhase::AfterCopy,
|
||||
xfer.src,
|
||||
xfer.dest,
|
||||
static_cast<uint32_t>(xfer.size),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,9 @@
|
||||
|
||||
#include "ps2_stubs.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace ps2_stubs
|
||||
{
|
||||
bool isSifIopHeapAddress(uint32_t address);
|
||||
bool isSifIopHeapRange(uint32_t address, size_t size);
|
||||
bool readSifIopHeap(uint32_t address, void *destination, size_t size);
|
||||
bool writeSifIopHeap(uint32_t address, const void *source, size_t size);
|
||||
bool zeroSifIopHeap(uint32_t address, size_t size);
|
||||
|
||||
bool dispatchSifCommand(uint8_t *rdram, PS2Runtime *runtime, uint32_t commandId, const void *packet, size_t packetSize) noexcept;
|
||||
void sceSifCmdIntrHdlr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||
void sceSifLoadModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||
void sceSifSendCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "runtime/ee_scheduler.h"
|
||||
#include "ps2_runtime_macros.h"
|
||||
#include "ps2_stubs.h"
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
@@ -253,6 +253,9 @@ namespace ps2_syscalls
|
||||
case 0x64:
|
||||
FlushCache(rdram, ctx, runtime);
|
||||
return true;
|
||||
case static_cast<uint32_t>(-0x68):
|
||||
iFlushCache(rdram, ctx, runtime);
|
||||
return true;
|
||||
case 0x6E:
|
||||
SetOsdConfigParam2(rdram, ctx, runtime);
|
||||
return true;
|
||||
|
||||
@@ -3,32 +3,10 @@
|
||||
|
||||
namespace ps2_syscalls
|
||||
{
|
||||
static int allocatePs2Fd(FILE *file)
|
||||
static PS2VfsMounts currentVfsMounts()
|
||||
{
|
||||
if (!file)
|
||||
return -1;
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_fd_mutex);
|
||||
int fd = g_nextFd++;
|
||||
g_fileDescriptors[fd] = file;
|
||||
return fd;
|
||||
}
|
||||
|
||||
static FILE *getHostFile(int ps2Fd)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_fd_mutex);
|
||||
auto it = g_fileDescriptors.find(ps2Fd);
|
||||
if (it != g_fileDescriptors.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void releasePs2Fd(int ps2Fd)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_fd_mutex);
|
||||
g_fileDescriptors.erase(ps2Fd);
|
||||
const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths();
|
||||
return {paths.hostRoot, paths.cdRoot, paths.mcRoot};
|
||||
}
|
||||
|
||||
struct VagAccumEntry
|
||||
@@ -40,39 +18,6 @@ namespace ps2_syscalls
|
||||
static std::mutex g_vagAccumMutex;
|
||||
static constexpr size_t kVagAccumMaxBytes = 16 * 1024 * 1024;
|
||||
|
||||
static const char *translateFioMode(int ps2Flags)
|
||||
{
|
||||
bool read = (ps2Flags & PS2_FIO_O_RDONLY) || (ps2Flags & PS2_FIO_O_RDWR);
|
||||
bool write = (ps2Flags & PS2_FIO_O_WRONLY) || (ps2Flags & PS2_FIO_O_RDWR);
|
||||
bool append = (ps2Flags & PS2_FIO_O_APPEND);
|
||||
bool create = (ps2Flags & PS2_FIO_O_CREAT);
|
||||
bool truncate = (ps2Flags & PS2_FIO_O_TRUNC);
|
||||
|
||||
if (read && write)
|
||||
{
|
||||
if (create && truncate)
|
||||
return "w+b";
|
||||
if (create)
|
||||
return "a+b";
|
||||
return "r+b";
|
||||
}
|
||||
else if (write)
|
||||
{
|
||||
if (append)
|
||||
return "ab";
|
||||
if (create && truncate)
|
||||
return "wb";
|
||||
if (create)
|
||||
return "wx";
|
||||
return "r+b";
|
||||
}
|
||||
else if (read)
|
||||
{
|
||||
return "rb";
|
||||
}
|
||||
return "rb";
|
||||
}
|
||||
|
||||
void fioOpen(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
uint32_t pathAddr = getRegU32(ctx, 4); // $a0
|
||||
@@ -86,52 +31,32 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
std::string hostPath = translatePs2Path(ps2Path);
|
||||
if (hostPath.empty())
|
||||
if (!runtime)
|
||||
{
|
||||
std::cerr << "fioOpen error: Failed to translate path '" << ps2Path << "'" << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *mode = translateFioMode(flags);
|
||||
RUNTIME_LOG("fioOpen: '" << hostPath << "' flags=0x" << std::hex << flags << std::dec << " mode='" << mode << "'");
|
||||
|
||||
FILE *fp = ::fopen(hostPath.c_str(), mode);
|
||||
if (!fp)
|
||||
{
|
||||
std::cerr << "fioOpen error: fopen failed for '" << hostPath << "': " << strerror(errno) << std::endl;
|
||||
setReturnS32(ctx, -1); // e.g., -ENOENT, -EACCES
|
||||
return;
|
||||
}
|
||||
|
||||
int ps2Fd = allocatePs2Fd(fp);
|
||||
if (ps2Fd < 0)
|
||||
{
|
||||
std::cerr << "fioOpen error: Failed to allocate PS2 file descriptor" << std::endl;
|
||||
::fclose(fp);
|
||||
setReturnS32(ctx, -1); // e.g., -EMFILE
|
||||
return;
|
||||
}
|
||||
|
||||
// returns the PS2 file descriptor
|
||||
setReturnS32(ctx, ps2Fd);
|
||||
const int32_t descriptor = runtime->vfs().open(ps2Path, static_cast<uint32_t>(flags), currentVfsMounts(), runtime->romDevice());
|
||||
setReturnS32(ctx, descriptor);
|
||||
}
|
||||
|
||||
void fioClose(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
int ps2Fd = (int)getRegU32(ctx, 4);
|
||||
|
||||
FILE *fp = getHostFile(ps2Fd);
|
||||
if (!fp)
|
||||
if (!runtime)
|
||||
{
|
||||
std::cerr << "fioClose warning: Invalid PS2 file descriptor " << ps2Fd << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
int ret = ::fclose(fp);
|
||||
releasePs2Fd(ps2Fd);
|
||||
const int32_t ret = runtime->vfs().close(ps2Fd);
|
||||
if (ret < 0)
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_vagAccumMutex);
|
||||
@@ -161,7 +86,7 @@ namespace ps2_syscalls
|
||||
}
|
||||
}
|
||||
|
||||
setReturnS32(ctx, ret == 0 ? 0 : -1);
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void fioRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -171,15 +96,13 @@ namespace ps2_syscalls
|
||||
size_t size = getRegU32(ctx, 6); // $a2
|
||||
|
||||
uint8_t *hostBuf = getMemPtr(rdram, bufAddr);
|
||||
FILE *fp = getHostFile(ps2Fd);
|
||||
|
||||
if (!hostBuf)
|
||||
{
|
||||
std::cerr << "fioRead error: Invalid buffer address for fd " << ps2Fd << std::endl;
|
||||
setReturnS32(ctx, -1); // -EFAULT
|
||||
return;
|
||||
}
|
||||
if (!fp)
|
||||
if (!runtime)
|
||||
{
|
||||
std::cerr << "fioRead error: Invalid file descriptor " << ps2Fd << std::endl;
|
||||
setReturnS32(ctx, -1); // -EBADF
|
||||
@@ -191,24 +114,18 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytesRead = 0;
|
||||
const int64_t readResult = runtime->vfs().read(ps2Fd, hostBuf, size);
|
||||
if (readResult < 0)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sys_fd_mutex);
|
||||
bytesRead = fread(hostBuf, 1, size, fp);
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
const size_t bytesRead = static_cast<size_t>(readResult);
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
ps2TraceGuestRangeWrite(rdram, bufAddr, static_cast<uint32_t>(bytesRead), "fioRead", ctx);
|
||||
}
|
||||
|
||||
if (bytesRead < size && ferror(fp))
|
||||
{
|
||||
std::cerr << "fioRead error: fread failed for fd " << ps2Fd << ": " << strerror(errno) << std::endl;
|
||||
clearerr(fp);
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_vagAccumMutex);
|
||||
auto it = g_vagAccum.find(ps2Fd);
|
||||
@@ -254,8 +171,7 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *fp = getHostFile(ps2Fd);
|
||||
if (!fp)
|
||||
if (!runtime)
|
||||
{
|
||||
setReturnS32(ctx, -1); // -EFAULT
|
||||
return;
|
||||
@@ -267,20 +183,15 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytesWritten = 0;
|
||||
const int64_t writeResult = runtime->vfs().write(ps2Fd, hostBuf, size);
|
||||
if (writeResult < 0)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_sys_fd_mutex);
|
||||
bytesWritten = ::fwrite(hostBuf, 1, size, fp);
|
||||
if (bytesWritten < size && ferror(fp))
|
||||
{
|
||||
clearerr(fp);
|
||||
setReturnS32(ctx, -1); // -EIO, -ENOSPC etc.
|
||||
return;
|
||||
}
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
// returns number of bytes written
|
||||
setReturnS32(ctx, (int32_t)bytesWritten);
|
||||
setReturnS32(ctx, static_cast<int32_t>(writeResult));
|
||||
}
|
||||
|
||||
void fioLseek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -289,8 +200,7 @@ namespace ps2_syscalls
|
||||
int32_t offset = getRegU32(ctx, 5); // $a1 (PS2 seems to use 32-bit offset here commonly)
|
||||
int whence = (int)getRegU32(ctx, 6); // $a2 (PS2 FIO_SEEK constants)
|
||||
|
||||
FILE *fp = getHostFile(ps2Fd);
|
||||
if (!fp)
|
||||
if (!runtime)
|
||||
{
|
||||
std::cerr << "fioLseek error: Invalid file descriptor " << ps2Fd << std::endl;
|
||||
setReturnS32(ctx, -1); // -EBADF
|
||||
@@ -315,22 +225,14 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
if (::fseek(fp, static_cast<long>(offset), hostWhence) != 0)
|
||||
{
|
||||
std::cerr << "fioLseek error: fseek failed for fd " << ps2Fd << ": " << strerror(errno) << std::endl;
|
||||
setReturnS32(ctx, -1); // Return error code
|
||||
return;
|
||||
}
|
||||
|
||||
long newPos = ::ftell(fp);
|
||||
const int64_t newPos = runtime->vfs().seek(ps2Fd, offset, hostWhence);
|
||||
if (newPos < 0)
|
||||
{
|
||||
std::cerr << "fioLseek error: ftell failed after fseek for fd " << ps2Fd << ": " << strerror(errno) << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (newPos > 0xFFFFFFFFL)
|
||||
if (static_cast<uint64_t>(newPos) > 0x7FFFFFFFu)
|
||||
{
|
||||
std::cerr << "fioLseek warning: New position exceeds 32-bit for fd " << ps2Fd << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
@@ -354,8 +256,8 @@ namespace ps2_syscalls
|
||||
setReturnS32(ctx, -1); // -EFAULT
|
||||
return;
|
||||
}
|
||||
std::string hostPath = translatePs2Path(ps2Path);
|
||||
if (hostPath.empty())
|
||||
std::filesystem::path hostPath;
|
||||
if (!runtime || !runtime->vfs().resolveHostPath(ps2Path, currentVfsMounts(), hostPath))
|
||||
{
|
||||
std::cerr << "fioMkdir error: Failed to translate path '" << ps2Path << "'" << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
@@ -366,13 +268,13 @@ namespace ps2_syscalls
|
||||
|
||||
if (!success && ec)
|
||||
{
|
||||
std::cerr << "fioMkdir error: create_directory failed for '" << hostPath
|
||||
std::cerr << "fioMkdir error: create_directory failed for '" << hostPath.string()
|
||||
<< "': " << ec.message() << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
RUNTIME_LOG("fioMkdir: Created directory '" << hostPath << "'");
|
||||
RUNTIME_LOG("fioMkdir: Created directory '" << hostPath.string() << "'");
|
||||
setReturnS32(ctx, 0); // Success
|
||||
}
|
||||
}
|
||||
@@ -388,27 +290,14 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
std::string hostPath = translatePs2Path(ps2Path);
|
||||
if (hostPath.empty())
|
||||
PS2VfsStat status;
|
||||
if (!runtime || !runtime->vfs().stat(ps2Path, currentVfsMounts(), runtime->romDevice(), status) || !status.directory)
|
||||
{
|
||||
std::cerr << "fioChdir error: Failed to translate path '" << ps2Path << "'" << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::current_path(hostPath, ec);
|
||||
|
||||
if (ec)
|
||||
{
|
||||
std::cerr << "fioChdir error: current_path failed for '" << hostPath
|
||||
<< "': " << ec.message() << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
RUNTIME_LOG("fioChdir: Changed directory to '" << hostPath << "'");
|
||||
setReturnS32(ctx, 0); // Success
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,8 +311,8 @@ namespace ps2_syscalls
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
std::string hostPath = translatePs2Path(ps2Path);
|
||||
if (hostPath.empty())
|
||||
std::filesystem::path hostPath;
|
||||
if (!runtime || !runtime->vfs().resolveHostPath(ps2Path, currentVfsMounts(), hostPath))
|
||||
{
|
||||
std::cerr << "fioRmdir error: Failed to translate path '" << ps2Path << "'" << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
@@ -435,20 +324,18 @@ namespace ps2_syscalls
|
||||
|
||||
if (!success || ec)
|
||||
{
|
||||
std::cerr << "fioRmdir error: remove failed for '" << hostPath
|
||||
<< "': " << ec.message() << std::endl;
|
||||
std::cerr << "fioRmdir error: remove failed for '" << hostPath.string() << "': " << ec.message() << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
RUNTIME_LOG("fioRmdir: Removed directory '" << hostPath << "'");
|
||||
RUNTIME_LOG("fioRmdir: Removed directory '" << hostPath.string() << "'");
|
||||
setReturnS32(ctx, 0); // Success
|
||||
}
|
||||
}
|
||||
|
||||
void fioGetstat(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
// we wont implement this for now.
|
||||
uint32_t pathAddr = getRegU32(ctx, 4); // $a0
|
||||
uint32_t statBufAddr = getRegU32(ctx, 5); // $a1
|
||||
|
||||
@@ -468,15 +355,29 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
std::string hostPath = translatePs2Path(ps2Path);
|
||||
if (hostPath.empty())
|
||||
if (!runtime)
|
||||
{
|
||||
std::cerr << "fioGetstat error: Bad path translate" << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
setReturnS32(ctx, -1);
|
||||
PS2VfsStat status;
|
||||
if (!runtime->vfs().stat(ps2Path, currentVfsMounts(), runtime->romDevice(), status))
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
io_stat_t guest{};
|
||||
guest.mode = (status.directory ? kFioSoIfDir : kFioSoIfReg) | kFioSoIROth | kFioSoIXOth | (status.readOnly ? 0u : kFioSoIWOth);
|
||||
guest.size = static_cast<uint32_t>(status.size & 0xFFFFFFFFu);
|
||||
guest.hisize = static_cast<uint32_t>(status.size >> 32u);
|
||||
encodePs2Time(status.created, guest.ctime);
|
||||
encodePs2Time(status.accessed, guest.atime);
|
||||
encodePs2Time(status.modified, guest.mtime);
|
||||
std::memcpy(ps2StatBuf, &guest, sizeof(guest));
|
||||
ps2TraceGuestRangeWrite(rdram, statBufAddr, sizeof(guest), "fioGetstat", ctx);
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void fioRemove(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -490,8 +391,8 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
std::string hostPath = translatePs2Path(ps2Path);
|
||||
if (hostPath.empty())
|
||||
std::filesystem::path hostPath;
|
||||
if (!runtime || !runtime->vfs().resolveHostPath(ps2Path, currentVfsMounts(), hostPath))
|
||||
{
|
||||
std::cerr << "fioRemove error: Path translate fail" << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
@@ -503,13 +404,12 @@ namespace ps2_syscalls
|
||||
|
||||
if (!success || ec)
|
||||
{
|
||||
std::cerr << "fioRemove error: remove failed for '" << hostPath
|
||||
<< "': " << ec.message() << std::endl;
|
||||
std::cerr << "fioRemove error: remove failed for '" << hostPath.string() << "': " << ec.message() << std::endl;
|
||||
setReturnS32(ctx, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
RUNTIME_LOG("fioRemove: Removed file '" << hostPath << "'");
|
||||
RUNTIME_LOG("fioRemove: Removed file '" << hostPath.string() << "'");
|
||||
setReturnS32(ctx, 0); // Success
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user