1 Commits

Author SHA1 Message Date
Ranieri 75d729ce40 Feature/iop emulator (#244)
* refactor: from guest  threads to EE scheduler

* feat: bad wip mpeg fix for code veronica

* feat: cheap copy from host
feat: small perf o vsync tick

* feat: added EE clock Hz
fix: fix MPEG out of sync with new EE refactor

* fix: fix lotr tests

* fix: fix cri dtx loading
fix: fix wrong mmi instruction translation
fix: fix thread info params
feat: added EE  timers decoder and consumer
feat: split SFI and IOP memory to prevent collision and overrides

* feat: revert wrong changes

* refactor: change GS architecture

* feat: IOP emulator
refactor: codegen to catch callbacks on mips code
feat: added a lot of entries or IOP emulator

* feat: analyzer resolve the complete constant-producing sequence with five-instruction backward scan stopped at LUI and therefore

* feat: remove recompiled version of GetRomName
refactor: split IOP emulator logic
feat: added more HLE IOP modules
feat: added ps2_path

* eat: enhance ELF parser with improved callable entry detection and control flow analysis

* feat: update memory hint handling and enhance entry point discovery logic

* feat: add SET_GPR_ZE32 macro for zero-extending loads with unsigned semantics

* refactor: Refactor PS2 IOP Host Adapter and Memory Management
feat: Added PS2Vfs for virtual file system operations, including file opening, reading, writing, and path resolution.
feat: Improve VIF1 data processing to handle GIF image packets more efficiently.

* feat: added a lot of tests

* fix: fix texture caching
feat: wip multi version on dbcman

* feat: remove LLE IOPs
2026-09-19 21:31:44 -03:00
139 changed files with 15625 additions and 9178 deletions
+8 -10
View File
@@ -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
+4 -7
View File
@@ -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).
+29 -45
View File
@@ -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 &section : 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 &section : 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);
}
}
+3 -3
View File
@@ -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
View File
@@ -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
-220
View File
@@ -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
View File
@@ -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.
+40
View File
@@ -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;
};
}
+15 -5
View File
@@ -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:
+13 -3
View File
@@ -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;
};
-307
View File
@@ -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
+35
View File
@@ -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);
}
-150
View File
@@ -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;
}
}
+497
View File
@@ -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;
}
}
+42
View File
@@ -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;
};
}
+744
View File
@@ -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;
}
}
}
+103
View File
@@ -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;
};
}
+371
View File
@@ -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;
}
}
+91
View File
@@ -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;
};
}
+751
View File
@@ -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();
}
}
+43
View File
@@ -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;
}
}
}
+32
View File
@@ -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;
}
}
}
+26
View File
@@ -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;
}
}
}
+26
View File
@@ -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;
}
}
}
+20
View File
@@ -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;
};
}
+830
View File
@@ -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());
}
}
+49
View File
@@ -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;
};
}
+15
View File
@@ -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> &sections,
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(&section, 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(&section, 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 &section : 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 &section : 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);
};
}
+349
View File
@@ -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;
}
}
+78
View File
@@ -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;
};
}
+180
View File
@@ -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);
}
}
+49
View File
@@ -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;
};
}
+6 -13
View File
@@ -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
View File
@@ -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;
}
}
-147
View File
@@ -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);
}
-635
View File
@@ -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
+31 -6
View File
@@ -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;
};
}
+6
View File
@@ -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;
};
+85 -83
View File
@@ -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, &parameter, 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 &parameter)
int32_t handleNameOperation(Operation operation, uint32_t sendAddress, const NameParameter &parameter)
{
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 &parameter)
int32_t handleDescriptorOperation(Operation operation, Flavor flavor, const DescriptorParameter &parameter)
{
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"};
};
}
-335
View File
@@ -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));
}
}
-236
View File
@@ -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));
}
}
-628
View File
@@ -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));
}
}
-956
View File
@@ -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
}
}
-34
View File
@@ -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;
};
}
+123
View File
@@ -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));
}
}
+22
View File
@@ -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);
}
}
+237
View File
@@ -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;
}
}
+315
View File
@@ -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
+338
View File
@@ -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;
}
+170
View File
@@ -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> &sections);
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;
+1
View File
@@ -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;
};
+22
View File
@@ -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())
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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";
+5 -11
View File
@@ -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:
+190 -16
View File
@@ -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> &sections,
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 &section : 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);
}
+414 -5
View File
@@ -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;
}
+4 -2
View File
@@ -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
-1
View File
@@ -120,7 +120,6 @@
X(SetOsdConfigParam2) \
X(EnableCache) \
X(DisableCache) \
X(GetRomName) \
X(SifLoadElfPart) \
X(sceSifLoadElf) \
X(sceSifLoadElfPart) \
+20 -1
View File
@@ -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;
+11
View File
@@ -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 \
{ \
-2
View File
@@ -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;
};
}
+10 -5
View File
@@ -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);
+2
View File
@@ -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;
};
+82
View File
@@ -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;
};
+48 -8
View File
@@ -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));
+147 -293
View File
@@ -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),
});
}
}
}
+1 -8
View File
@@ -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;
+62 -162
View File
@@ -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