feat: remove LLE IOPs

This commit is contained in:
Ran-j
2026-09-19 21:17:58 -03:00
parent 78ecbae377
commit 39251a05f4
31 changed files with 120 additions and 8222 deletions
+4 -4
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
@@ -131,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:
-16
View File
@@ -2,7 +2,6 @@ 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
@@ -26,16 +25,9 @@ add_library(ps2_iop STATIC
src/emulator/imports/iop_sysmem.cpp
src/emulator/imports/iop_timrman.cpp
src/emulator/imports/iop_vblank.cpp
src/builtin_profiles.cpp
src/plugin_loader.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)
@@ -50,14 +42,6 @@ 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_ENABLE_PLUGINS AND UNIX AND NOT APPLE)
target_link_libraries(ps2_iop PRIVATE ${CMAKE_DL_LIBS})
endif()
if(PS2X_IOP_BUILD_TESTS)
enable_testing()
add_executable(ps2_iop_emulator_tests tests/iop_emulator_tests.cpp)
-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.
+40 -169
View File
@@ -1,186 +1,57 @@
# ps2xIOP
`ps2xIOP` is the IOP subsystem used by `ps2xRuntime`. It combines high-level service/profile path with an R3000A-backed IRX execution path.
No PS2 BIOS is required by the emulator backend: IRX imports for the kernel-facing libraries are handled by a small virtual IOP kernel, while the IRX module itself executes as original MIPS code.
> [!IMPORTANT]
> `ps2_iop`/`ps2x::iop` is a C++20 static library linked into the runtime.
> Optional `.dll` and `.so` files are native profile plugins. They extend
> the HLE profile catalog; they are not PS2 IRX modules.
`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`.
## Execution policy
The subsystem is always hybrid. Original IRX modules execute on the R3000A
path, while core and game-profile HLE services are available for endpoints
that the loaded modules do not provide. A server registered by a physical IRX
is normally authoritative for its SID; HLE is the fallback. A profile service
may explicitly replace a physical endpoint when it is a compatibility stub.
There is no runtime mode
switch or environment variable to create divergent boot paths.
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.
## Built-in services and profiles
Generic HLE services remain available when no loaded IRX provides an endpoint:
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 the profile/HLE operations plus emulator lifecycle entry points:
1. `configure(GameIdentity)` selects the active compatibility profile.
2. `reset()` resets both active HLE services and the emulator state.
3. `loadModule(...)` / `loadModuleBuffer(...)` load and start an IRX.
4. `stopModule(...)` releases an emulated module and its owned runtime state.
5. `runEeCycles(...)` advances the IOP from EE cycle accounting.
6. `selectRpcAbi(...)`, `handleRpc(...)`, and `onSifTransfer(...)` provide the SIF transport bridge.
`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 enabled, 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 emulator cycle/instruction counts, loaded
IRX/thread/RPC-server counts, the active profile and 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.
-5
View File
@@ -3,7 +3,6 @@
#include "ps2x/iop/iop_host.h"
#include "ps2x/iop/iop_types.h"
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
@@ -22,10 +21,6 @@ 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);
-3
View File
@@ -138,7 +138,6 @@ namespace ps2x::iop
{
std::string name;
std::vector<uint32_t> sids;
bool profileSpecific = false;
bool active = true;
std::vector<DebugMetric> metrics;
};
@@ -150,8 +149,6 @@ namespace ps2x::iop
uint32_t emulatorLoadedModules = 0;
uint32_t emulatorThreads = 0;
uint32_t emulatorRpcServers = 0;
std::string activeProfile;
std::string activeProvider;
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
-151
View File
@@ -1,151 +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,
.overridePhysicalServer = 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;
},
});
// TODO remove this on next release
// 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;
}
}
+1 -20
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>
@@ -19,8 +18,7 @@ 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. Profile services can omit aliases when the profile
// itself is the explicit compatibility contract.
// actually loaded.
[[nodiscard]] virtual std::span<const std::string_view> moduleAliases() const
{
return {};
@@ -33,11 +31,6 @@ namespace ps2x::iop::detail
return RpcAbi::RuntimeDefault;
}
[[nodiscard]] virtual bool overridesPhysicalRpcServer() const noexcept
{
return false;
}
[[nodiscard]] virtual RpcResult handleRpc(const RpcRequest &request) = 0;
virtual void onSifTransfer(const SifTransfer &transfer)
@@ -52,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();
}
+30 -274
View File
@@ -3,81 +3,26 @@
#include "iop_service.h"
#include "iop_module_manager.h"
#include "emulator/iop_emulator.h"
#include "plugin_loader.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()),
emulator(hostRef)
{
coreServices.emplace_back(detail::createMcservService(host));
coreServices.emplace_back(detail::createDbcmanService(host));
coreServices.emplace_back(detail::createLibSdService(host));
refreshServiceModuleKeys();
rebuildRoutes();
}
@@ -90,52 +35,34 @@ namespace ps2x::iop
void refreshServiceModuleKeys()
{
std::vector<std::string> keys;
auto collect = [&](const detail::ServiceList &services)
for (const auto &service : coreServices)
{
for (const auto &service : services)
{
if (!service)
continue;
for (std::string_view alias : service->moduleAliases())
keys.emplace_back(alias);
}
};
collect(coreServices);
collect(profileServices);
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 || !serviceActive(*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)
@@ -152,19 +79,11 @@ namespace ps2x::iop
}
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::vector<std::string> loadOutcomes;
std::unordered_set<std::string> loggedLoadPaths;
std::string activeProfile;
std::string activeProvider;
std::string lastError;
bool routesValid = true;
detail::IopModuleManager moduleManager;
detail::IopEmulator emulator;
};
@@ -178,116 +97,6 @@ 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)
{
if (error)
error->clear();
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->refreshServiceModuleKeys();
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->refreshServiceModuleKeys();
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->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
return false;
}
}
m_impl->refreshServiceModuleKeys();
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->refreshServiceModuleKeys();
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();
@@ -300,13 +109,6 @@ namespace ps2x::iop
service->reset();
}
}
for (auto &service : m_impl->profileServices)
{
if (service)
{
service->reset();
}
}
m_impl->emulator.reset();
m_impl->refreshServiceModuleKeys();
m_impl->rebuildRoutes();
@@ -369,17 +171,6 @@ namespace ps2x::iop
RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const
{
for (const auto &service : m_impl->profileServices)
{
if (service && m_impl->serviceActive(*service))
{
const RpcAbi selected = service->selectRpcAbi(request);
if (selected != RpcAbi::RuntimeDefault)
{
return selected;
}
}
}
for (const auto &service : m_impl->coreServices)
{
if (service && m_impl->serviceActive(*service))
@@ -408,23 +199,8 @@ namespace ps2x::iop
const auto route = m_impl->routes.find(request.sid);
detail::IopService *hle = route != m_impl->routes.end() ? route->second : nullptr;
// A profile can deliberately replace a physical endpoint when running
// that IRX is outside the selected compatibility scope (for example,
// disabling a game's audio driver while keeping the rest of its IOP
// modules physical).
if (hle && hle->overridesPhysicalRpcServer())
{
RpcResult overridden = hle->handleRpc(request);
if (overridden.handled)
{
return overridden;
}
}
// Otherwise physical servers are authoritative and HLE remains a
// compatibility fallback for endpoints no loaded IRX provides.
RpcResult emulated = m_impl->emulator.handleRpc(request);
if (emulated.handled || !hle || hle->overridesPhysicalRpcServer())
if (emulated.handled || !hle)
{
return emulated;
}
@@ -440,13 +216,6 @@ namespace ps2x::iop
service->onSifTransfer(transfer);
}
}
for (auto &service : m_impl->profileServices)
{
if (service && m_impl->serviceActive(*service))
{
service->onSifTransfer(transfer);
}
}
m_impl->emulator.onSifTransfer(transfer);
}
@@ -488,34 +257,21 @@ namespace ps2x::iop
snapshot.emulatorLoadedModules = m_impl->emulator.loadedModuleCount();
snapshot.emulatorThreads = m_impl->emulator.threadCount();
snapshot.emulatorRpcServers = m_impl->emulator.rpcServerCount();
snapshot.activeProfile = m_impl->activeProfile;
snapshot.activeProvider = m_impl->activeProvider;
snapshot.diagnostics = m_impl->diagnostics;
snapshot.diagnostics.insert(snapshot.diagnostics.end(), m_impl->loadOutcomes.begin(), m_impl->loadOutcomes.end());
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;
row.active = m_impl->serviceActive(*service);
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;
}
}
-148
View File
@@ -2,157 +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;
bool overridePhysicalServer = 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
-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));
}
}
-241
View File
@@ -1,241 +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;
}
[[nodiscard]] bool overridesPhysicalRpcServer() const noexcept override
{
return m_bindings.overridePhysicalServer;
}
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));
}
}
-591
View File
@@ -1,591 +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>
bool readIopPod(const IopHost &host, uint32_t address, T &value)
{
value = {};
return host.readIopMemory(address, &value, sizeof(value));
}
template <typename T>
bool writeIopPod(IopHost &host, uint32_t address, const T &value)
{
return host.writeIopMemory(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.zeroIopMemory(m_state.statusAddress, kStatusSize) ||
!m_host.zeroIopMemory(m_state.addressTableAddress, kAddressTableEntries * sizeof(uint32_t)) ||
!m_host.zeroIopMemory(m_state.storageBaseAddress, m_state.storageSize))
{
return false;
}
if (
!writeIopPod(m_host, m_state.addressTableAddress + (0u * sizeof(uint32_t)), m_state.hdBaseAddress) ||
!writeIopPod(m_host, m_state.addressTableAddress + (1u * sizeof(uint32_t)), m_state.sqBaseAddress) ||
!writeIopPod(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 (!readIopPod(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)writeIopPod(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)readIopPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo |
static_cast<uint16_t>(1u << port));
(void)writeIopPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
break;
}
case 0x21u: // SdrBgmStop
{
const uint32_t port = command[1] & 0x0Fu;
uint16_t midiInfo = 0u;
(void)readIopPod(m_host, m_state.statusAddress + kMidiInfoOffset, midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo & ~static_cast<uint16_t>(1u << port));
(void)writeIopPod(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)writeIopPod(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)writeIopPod(m_host, m_state.statusAddress + kSeSumOffset + (port * sizeof(int16_t)), checksum);
break;
}
case 0x10u: // SdrSeAllStop
(void)m_host.zeroIopMemory(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;
};
}
+33 -29
View File
@@ -926,36 +926,33 @@ int main()
"IOP reset did not remove the registered RPC server")) return 1;
constexpr uint32_t lotrSoundSid = 0x00012345u;
GameIdentity lotrIdentity{};
lotrIdentity.elfName = "SLUS_205.78";
std::string profileError;
if (!expect(iop.configure(lotrIdentity, &profileError),
"Could not configure the LotR IOP profile")) return 1;
writeRpcServerIrx(host, 0x100u, lotrSoundSid);
const ModuleLoadResult physicalSoundModule = iop.loadModuleBuffer(0x100u);
if (!expect(physicalSoundModule.handled && physicalSoundModule.startResult == 0,
"Synthetic physical sound RPC server did not start")) return 1;
constexpr uint32_t soundSendAddress = 0x800u;
constexpr uint32_t soundReceiveAddress = 0x900u;
uint16_t emptySoundCommandCount = 0u;
std::memcpy(host.guest.data() + soundSendAddress,
&emptySoundCommandCount,
sizeof(emptySoundCommandCount));
std::fill_n(host.guest.data() + soundReceiveAddress, 32u, 0xA5u);
const uint32_t soundHandler[] = {
0x3C020001u, // lui v0, 1
0x34420400u, // ori v0, v0, 0x400 (registered server buffer)
0x03E00008u, // jr ra
0x00000000u, // nop
};
if (!expect(iop.writeMemory(0x00010300u, soundHandler, sizeof(soundHandler)),
"Could not install the physical sound RPC handler")) return 1;
constexpr uint32_t soundPayload = 0x1234ABCDu;
std::memcpy(host.guest.data() + 0x800u, &soundPayload, sizeof(soundPayload));
RpcRequest soundRequest{};
soundRequest.sid = lotrSoundSid;
soundRequest.send = {soundSendAddress, 32u};
soundRequest.receive = {soundReceiveAddress, 32u};
soundRequest.send = {0x800u, sizeof(soundPayload)};
soundRequest.receive = {0x900u, sizeof(soundPayload)};
const uint64_t soundInstructionsBefore = iop.debugSnapshot().emulatorInstructions;
const RpcResult soundResult = iop.handleRpc(soundRequest);
uint32_t soundResponseCounter = 0u;
std::memcpy(&soundResponseCounter,
host.guest.data() + soundReceiveAddress + sizeof(uint32_t),
sizeof(soundResponseCounter));
if (!expect(soundResult.handled,
"LotR sound RPC was not handled")) return 1;
if (!expect(soundResponseCounter == 1u,
"Physical sound server bypassed the LotR compatibility stub")) return 1;
uint32_t soundResponse = 0u;
std::memcpy(&soundResponse, host.guest.data() + 0x900u, sizeof(soundResponse));
if (!expect(soundResult.handled && soundResponse == soundPayload,
"Physical sound RPC did not return its own payload")) return 1;
if (!expect(iop.debugSnapshot().emulatorInstructions > soundInstructionsBefore,
"Sound RPC did not execute the physical IOP handler")) return 1;
constexpr uint32_t relocatableRpcSid = 0xA11CE001u;
writeRelocatableRpcServerIrx(host, 0x100u);
@@ -997,6 +994,8 @@ int main()
constexpr uint32_t eeDestination = 0x900u;
const uint32_t transferPayload = 0x53494621u; // "SIF!"
std::memcpy(host.guest.data() + eeSource, &transferPayload, sizeof(transferPayload));
if (!expect(iop.writeMemory(iopBuffer, &transferPayload, sizeof(transferPayload)),
"Could not initialize physical IOP memory")) return 1;
iop.onSifTransfer({
SifTransferKind::SetDma,
SifTransferPhase::AfterCopy,
@@ -1014,8 +1013,12 @@ int main()
});
uint32_t stagedIopPayload = 0u;
std::memcpy(&stagedIopPayload, host.guest.data() + iopBuffer, sizeof(stagedIopPayload));
if (!expect(stagedIopPayload == transferPayload,
"sceSifGetOtherData did not stage physical IOP memory for an EE copy")) return 1;
if (!expect(stagedIopPayload == 0u,
"SIF notification aliased physical IOP memory into EE RAM")) return 1;
uint32_t physicalIopPayload = 0u;
if (!expect(iop.readMemory(iopBuffer, &physicalIopPayload, sizeof(physicalIopPayload)) &&
physicalIopPayload == transferPayload,
"SIF notification corrupted physical IOP memory")) return 1;
iop.reset();
constexpr uint32_t sifDmaDestination = 0xC00u;
@@ -1043,7 +1046,8 @@ int main()
sizeof(uint32_t),
});
uint32_t lowPriorityMarker = 0u;
std::memcpy(&lowPriorityMarker, host.guest.data() + 0x00010400u, sizeof(lowPriorityMarker));
if (!expect(iop.readMemory(0x00010400u, &lowPriorityMarker, sizeof(lowPriorityMarker)),
"Could not read the IOP scheduling marker")) return 1;
if (!expect(lowPriorityMarker == 1u,
"WaitVblankEnd returned immediately and starved a lower-priority IOP thread")) return 1;
@@ -1103,8 +1107,8 @@ int main()
sizeof(uint32_t),
});
uint32_t cdvdCallbackReason = 0u;
std::memcpy(&cdvdCallbackReason, host.guest.data() + 0x000101C0u,
sizeof(cdvdCallbackReason));
if (!expect(iop.readMemory(0x000101C0u, &cdvdCallbackReason, sizeof(cdvdCallbackReason)),
"Could not read the physical CDVD callback result")) return 1;
host.cdRoot.clear();
std::filesystem::remove(virtualCdRoot, cdRootError);
if (!expect(cdvdPvdRead.handled && cdvdPvdRead.startResult == 0,
@@ -1125,8 +1129,8 @@ int main()
sizeof(uint32_t),
});
uint32_t cdvdSeekCallbackReason = 0u;
std::memcpy(&cdvdSeekCallbackReason, host.guest.data() + 0x000101C0u,
sizeof(cdvdSeekCallbackReason));
if (!expect(iop.readMemory(0x000101C0u, &cdvdSeekCallbackReason, sizeof(cdvdSeekCallbackReason)),
"Could not read the physical CDVD callback result")) return 1;
if (!expect(cdvdSeek.handled && cdvdSeek.startResult == 1,
"sceCdSeek did not accept a valid LBN")) return 1;
if (!expect(cdvdSeekCallbackReason == 4u,
@@ -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";
-1
View File
@@ -290,7 +290,6 @@ 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);
+1 -14
View File
@@ -1067,18 +1067,9 @@ namespace
}
ImGui::SeparatorText("ps2xIOP HLE services");
ImGui::Text("Profile: %s provider: %s",
iopSnapshot.activeProfile.empty()
? "<core only>"
: iopSnapshot.activeProfile.c_str(),
iopSnapshot.activeProvider.empty()
? "builtin"
: iopSnapshot.activeProvider.c_str());
if (ImGui::BeginTable("iop_hle_services", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
if (ImGui::BeginTable("iop_hle_services", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("Service");
ImGui::TableSetupColumn("Layer");
ImGui::TableSetupColumn("Active");
ImGui::TableSetupColumn("SID");
ImGui::TableSetupColumn("EE server");
@@ -1092,8 +1083,6 @@ namespace
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.name.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core");
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.active ? "yes" : "no");
ImGui::TableNextColumn();
ImGui::TextUnformatted("-");
@@ -1110,8 +1099,6 @@ namespace
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.name.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core");
ImGui::TableNextColumn();
ImGui::TextUnformatted(service.active ? "yes" : "no");
ImGui::TableNextColumn();
ImGui::Text("0x%08X", sid);
-9
View File
@@ -8,15 +8,6 @@
class PS2IopTransport
{
public:
static bool configureForTesting(
PS2Runtime *runtime,
const ps2x::iop::GameIdentity &identity,
std::string *error = nullptr)
{
return runtime && runtime->m_iopSubsystem &&
runtime->m_iopSubsystem->configure(identity, error);
}
[[nodiscard]] static ps2x::iop::RpcAbi selectRpcAbi(
const PS2Runtime *runtime,
const ps2x::iop::RpcAbiRequest &request)
+2 -28
View File
@@ -483,14 +483,6 @@ PS2Runtime::PS2Runtime()
m_iopSubsystem = std::make_unique<ps2x::iop::IopSubsystem>(*m_iopHost);
m_eeScheduler = std::make_unique<EeScheduler>(*this);
#if defined(PS2X_IOP_ENABLE_PLUGINS) && PS2X_IOP_ENABLE_PLUGINS && \
!defined(PLATFORM_VITA) && (defined(_WIN32) || defined(__linux__))
if (const char *applicationDirectory = GetApplicationDirectory();
applicationDirectory && applicationDirectory[0] != '\0')
{
m_iopSubsystem->setPluginSearchPaths({std::filesystem::path(applicationDirectory) / "iop_plugins"});
}
#endif
// Assign rather than memset: R5900Context's constructor zeroes itself and
// then applies the COP0 reset values, which a memset here would discard.
@@ -572,11 +564,6 @@ PS2Runtime::~PS2Runtime()
}
}
void PS2Runtime::setIopPluginSearchPaths(std::vector<std::filesystem::path> paths)
{
m_iopSubsystem->setPluginSearchPaths(std::move(paths));
}
ps2x::iop::ModuleLoadResult PS2Runtime::loadIopModule(std::string_view path, const void *arguments, uint32_t argumentSize)
{
auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM());
@@ -741,15 +728,6 @@ bool PS2Runtime::initialize(const char *title)
std::cerr << "Failed to bind runtime core subsystems" << std::endl;
return false;
}
#if defined(PS2X_IOP_ENABLE_PLUGINS) && PS2X_IOP_ENABLE_PLUGINS && \
!defined(PLATFORM_VITA) && (defined(_WIN32) || defined(__linux__))
std::string pluginError;
if (!m_iopSubsystem->loadPlugins(&pluginError))
{
std::cerr << "Failed to load IOP plugins: " << pluginError << std::endl;
return false;
}
#endif
#if defined(PLATFORM_VITA)
InitWindow(HOST_WINDOW_WIDTH, HOST_WINDOW_HEIGHT, title); // raylib vita does not support audio
#else
@@ -1022,12 +1000,8 @@ bool PS2Runtime::loadELF(const std::string &elfPath)
std::cerr << "[ROM0] failed to configure profile: " << romError << std::endl;
return false;
}
std::string iopError;
if (!m_iopSubsystem->configure(identity, &iopError))
{
std::cerr << "[ps2xIOP] failed to configure profile: " << iopError << std::endl;
return false;
}
m_iopSubsystem->reset();
ps2_game_overrides::applyMatching(*this,
elfPath,
-38
View File
@@ -10,33 +10,6 @@ if(BUILD_TESTING)
add_subdirectory(gs_cache)
endif()
if(PS2X_IOP_ENABLE_PLUGINS AND (WIN32 OR (UNIX AND NOT APPLE)))
add_library(ps2_iop_fake_plugin MODULE
src/fake_iop_plugin.cpp
)
add_library(ps2_iop_bad_abi_plugin MODULE
src/fake_iop_bad_abi.c
)
add_library(ps2_iop_missing_symbol_plugin MODULE
src/fake_iop_missing_symbol.cpp
)
foreach(plugin_target IN ITEMS
ps2_iop_fake_plugin
ps2_iop_bad_abi_plugin
ps2_iop_missing_symbol_plugin)
target_compile_features(${plugin_target} PRIVATE cxx_std_20)
target_include_directories(${plugin_target} PRIVATE
${CMAKE_SOURCE_DIR}/ps2xIOP/include
)
set_target_properties(${plugin_target} PROPERTIES
PREFIX ""
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/iop_test_plugins"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/iop_test_plugins"
)
endforeach()
endif()
# Static library with test logic (no main), used by ps2xStudio
add_library(ps2_test_function_table OBJECT
src/test_function_table.cpp
@@ -85,17 +58,6 @@ if(UNIX AND NOT APPLE)
target_link_libraries(ps2_test_lib PRIVATE ${CMAKE_DL_LIBS})
endif()
if(TARGET ps2_iop_fake_plugin)
target_compile_definitions(ps2_test_lib PRIVATE
PS2X_TEST_IOP_PLUGIN_DIR="$<TARGET_FILE_DIR:ps2_iop_fake_plugin>"
)
add_dependencies(ps2_test_lib
ps2_iop_fake_plugin
ps2_iop_bad_abi_plugin
ps2_iop_missing_symbol_plugin
)
endif()
add_executable(ps2x_tests
src/main.cpp
$<TARGET_OBJECTS:ps2_test_function_table>
-15
View File
@@ -1,15 +0,0 @@
#include "ps2x/iop/plugin_api.h"
PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1(
uint32_t host_abi_version,
ps2x_iop_plugin_api_v1 *plugin_api)
{
(void)host_abi_version;
if (!plugin_api || plugin_api->struct_size < sizeof(*plugin_api))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
plugin_api->abi_version = PS2X_IOP_ABI_VERSION_V1 + 1u;
plugin_api->struct_size = sizeof(*plugin_api);
return PS2X_IOP_STATUS_OK_V1;
}
-6
View File
@@ -1,6 +0,0 @@
#include "ps2x/iop/plugin_api.h"
extern "C" PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_not_the_query_symbol()
{
return PS2X_IOP_STATUS_OK_V1;
}
-350
View File
@@ -1,350 +0,0 @@
#include "ps2x/iop/plugin_api.h"
#include <cstdint>
#include <cstring>
#include <new>
namespace
{
constexpr uint32_t kSyntheticSid = 0xF00DCAFEu;
constexpr uint32_t kCoreCollisionSid = 0x80001300u;
constexpr uint32_t kSyntheticFunction = 0x42u;
constexpr uint32_t kCoreCollisionFunction = 0x99u;
constexpr uint32_t kSyntheticEntryPoint = 0x00123456u;
constexpr uint32_t kSpecificRecvXEntryPoint = kSyntheticEntryPoint + 0x100u;
constexpr uint32_t kSyntheticCrc32 = 0xA1B2C3D4u;
constexpr uint32_t kResponseXor = 0xA5A55A5Au;
constexpr uint32_t kCoreCollisionResponse = 0xC0DEF00Du;
template <size_t Size>
constexpr ps2x_iop_string_view_v1 stringView(const char (&value)[Size])
{
return {value, Size - 1u};
}
struct FakePluginState
{
const ps2x_iop_host_api_v1 *host = nullptr;
uint64_t resetGeneration = 0;
uint64_t rpcCalls = 0;
uint64_t transfers = 0;
};
void log(FakePluginState *state, uint32_t level, ps2x_iop_string_view_v1 message)
{
if (state && state->host && state->host->log)
{
state->host->log(state->host->userdata, level, message);
}
}
void *createProfile(const ps2x_iop_host_api_v1 *host,
const ps2x_iop_game_identity_v1 *identity)
{
if (!host || host->abi_version != PS2X_IOP_ABI_VERSION_V1 ||
host->struct_size < sizeof(*host) || !identity ||
identity->struct_size < sizeof(*identity) ||
(identity->entry_point != kSyntheticEntryPoint &&
identity->entry_point != kSpecificRecvXEntryPoint) ||
identity->crc32 != kSyntheticCrc32)
{
return nullptr;
}
auto *state = new (std::nothrow) FakePluginState{};
if (!state)
{
return nullptr;
}
state->host = host;
log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-create"));
return state;
}
void destroyProfile(void *instance)
{
auto *state = static_cast<FakePluginState *>(instance);
log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-destroy"));
delete state;
}
int32_t resetProfile(void *instance)
{
auto *state = static_cast<FakePluginState *>(instance);
if (!state)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
++state->resetGeneration;
state->rpcCalls = 0;
state->transfers = 0;
return PS2X_IOP_STATUS_OK_V1;
}
uint32_t selectRpcAbi(void *instance, const ps2x_iop_rpc_abi_request_v1 *request)
{
if (!instance || !request || request->struct_size < sizeof(*request))
{
return PS2X_IOP_RPC_ABI_DEFAULT_V1;
}
if (request->bound_sid == kSyntheticSid && request->function == kSyntheticFunction)
{
return PS2X_IOP_RPC_ABI_STACK_V1;
}
return PS2X_IOP_RPC_ABI_DEFAULT_V1;
}
int32_t handleRpc(void *instance,
const ps2x_iop_rpc_request_v1 *request,
ps2x_iop_rpc_result_v1 *result)
{
auto *state = static_cast<FakePluginState *>(instance);
if (!state || !request || request->struct_size < sizeof(*request) ||
!result || 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->callback_policy = PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1;
if (request->sid == kCoreCollisionSid &&
request->function == kCoreCollisionFunction)
{
if (request->receive.size < sizeof(kCoreCollisionResponse) ||
!state->host->write_guest ||
state->host->write_guest(state->host->userdata,
request->receive.address,
&kCoreCollisionResponse,
sizeof(kCoreCollisionResponse)) !=
PS2X_IOP_STATUS_OK_V1)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
++state->rpcCalls;
result->handled = 1u;
result->result_address = request->receive.address;
return PS2X_IOP_STATUS_OK_V1;
}
if (request->sid != kSyntheticSid || request->function != kSyntheticFunction)
{
return PS2X_IOP_STATUS_OK_V1;
}
uint32_t input = 0u;
if (request->send.size < sizeof(input) || !state->host->read_guest ||
state->host->read_guest(state->host->userdata,
request->send.address,
&input,
sizeof(input)) != PS2X_IOP_STATUS_OK_V1)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
const uint32_t output = input ^ kResponseXor;
if (request->receive.size < sizeof(output) || !state->host->write_guest ||
state->host->write_guest(state->host->userdata,
request->receive.address,
&output,
sizeof(output)) != PS2X_IOP_STATUS_OK_V1)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
++state->rpcCalls;
result->handled = 1u;
result->result_address = request->receive.address;
result->signal_nowait_completion = 1u;
result->callback_policy = PS2X_IOP_CALLBACK_SUPPRESS_V1;
return PS2X_IOP_STATUS_OK_V1;
}
int32_t onSifTransfer(void *instance, const ps2x_iop_sif_transfer_v1 *transfer)
{
auto *state = static_cast<FakePluginState *>(instance);
if (!state || !transfer || transfer->struct_size < sizeof(*transfer))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
++state->transfers;
return PS2X_IOP_STATUS_OK_V1;
}
size_t debugMetricCount(void *instance)
{
return instance ? 3u : 0u;
}
int32_t debugMetric(void *instance, size_t index, ps2x_iop_debug_metric_v1 *metric)
{
const auto *state = static_cast<const FakePluginState *>(instance);
if (!state || !metric || metric->struct_size < sizeof(*metric) || index >= 3u)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
metric->struct_size = sizeof(*metric);
metric->hexadecimal = 0u;
switch (index)
{
case 0u:
metric->name = stringView("reset_generation");
metric->value = state->resetGeneration;
break;
case 1u:
metric->name = stringView("rpc_calls");
metric->value = state->rpcCalls;
break;
case 2u:
metric->name = stringView("sif_transfers");
metric->value = state->transfers;
break;
default:
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return PS2X_IOP_STATUS_OK_V1;
}
constexpr uint32_t kSids[] = {kSyntheticSid, kCoreCollisionSid};
constexpr uint32_t kDuplicateSids[] = {kSyntheticSid, kSyntheticSid};
constexpr uint32_t kAmbiguousSids[] = {kSyntheticSid};
const ps2x_iop_profile_api_v1 kProfiles[] = {
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-test-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("synthetic_iop_test.elf"),
kSyntheticEntryPoint,
kSyntheticCrc32,
},
2u,
kSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-duplicate-sid-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("synthetic_duplicate.elf"),
kSyntheticEntryPoint,
kSyntheticCrc32,
},
2u,
kDuplicateSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-ambiguous-recvx-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("slus_201.84"),
0u,
0u,
},
1u,
kAmbiguousSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-specific-recvx-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("slus_201.84"),
kSpecificRecvXEntryPoint,
kSyntheticCrc32,
},
1u,
kAmbiguousSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-invalid-large-sid-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("synthetic_invalid.elf"),
kSyntheticEntryPoint,
kSyntheticCrc32,
},
257u,
kAmbiguousSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
};
const ps2x_iop_plugin_api_v1 kPlugin = {
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_plugin_api_v1),
stringView("ps2x-test-plugin"),
stringView("1.0.0"),
5u,
kProfiles,
};
}
extern "C" PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1(
uint32_t hostAbiVersion,
ps2x_iop_plugin_api_v1 *pluginApi)
{
if (hostAbiVersion != PS2X_IOP_ABI_VERSION_V1)
{
return PS2X_IOP_STATUS_UNSUPPORTED_V1;
}
if (!pluginApi)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
if (pluginApi->struct_size < sizeof(*pluginApi))
{
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
}
*pluginApi = kPlugin;
return PS2X_IOP_STATUS_OK_V1;
}
+4 -907
View File
@@ -1,395 +1,6 @@
#include "MiniTest.h"
#include "ps2x/iop/iop_subsystem.h"
#include "ps2x/iop/ps2_path.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <limits>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#elif defined(__linux__)
#include <dlfcn.h>
#endif
namespace
{
using namespace ps2x::iop;
constexpr uint32_t kSyntheticSid = 0xF00DCAFEu;
constexpr uint32_t kCoreCollisionSid = 0x80001300u;
constexpr uint32_t kSyntheticFunction = 0x42u;
constexpr uint32_t kCoreCollisionFunction = 0x99u;
constexpr uint32_t kSyntheticEntryPoint = 0x00123456u;
constexpr uint32_t kSpecificRecvXEntryPoint = kSyntheticEntryPoint + 0x100u;
constexpr uint32_t kSyntheticCrc32 = 0xA1B2C3D4u;
constexpr uint32_t kResponseXor = 0xA5A55A5Au;
constexpr uint32_t kCoreCollisionResponse = 0xC0DEF00Du;
class FakeIopHost final : public IopHost
{
public:
explicit FakeIopHost(size_t memorySize = 0x10000u)
: memory(memorySize, 0u), iopMemory(0x00200000u, 0u)
{
}
bool readGuest(uint32_t address, void *destination, size_t size) const override
{
if ((!destination && size != 0u) || !contains(address, size))
{
return false;
}
if (size != 0u)
{
std::memcpy(destination, memory.data() + address, size);
}
return true;
}
bool writeGuest(uint32_t address, const void *source, size_t size) override
{
if ((!source && size != 0u) || !contains(address, size))
{
return false;
}
if (size != 0u)
{
std::memcpy(memory.data() + address, source, size);
}
return true;
}
bool zeroGuest(uint32_t address, size_t size) override
{
if (!contains(address, size))
{
return false;
}
std::fill(memory.begin() + address, memory.begin() + address + size, 0u);
return true;
}
bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override
{
normalized = address & 0x1FFFFFFFu;
return normalized < memory.size();
}
bool readIopMemory(uint32_t address, void *destination, size_t size) const override
{
uint32_t normalized = 0u;
if ((!destination && size != 0u) || !normalizeIopAddress(address, normalized) ||
static_cast<uint64_t>(normalized) + size > iopMemory.size())
return false;
if (size != 0u)
std::memcpy(destination, iopMemory.data() + normalized, size);
return true;
}
bool writeIopMemory(uint32_t address, const void *source, size_t size) override
{
uint32_t normalized = 0u;
if ((!source && size != 0u) || !normalizeIopAddress(address, normalized) ||
static_cast<uint64_t>(normalized) + size > iopMemory.size())
return false;
if (size != 0u)
std::memcpy(iopMemory.data() + normalized, source, size);
return true;
}
bool zeroIopMemory(uint32_t address, size_t size) override
{
uint32_t normalized = 0u;
if (!normalizeIopAddress(address, normalized) ||
static_cast<uint64_t>(normalized) + size > iopMemory.size())
return false;
std::fill(iopMemory.begin() + normalized, iopMemory.begin() + normalized + size, 0u);
return true;
}
bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const override
{
const bool physical = address < 0x00200000u;
const bool cached = address >= 0x80000000u && address < 0x80200000u;
const bool uncached = address >= 0xA0000000u && address < 0xA0200000u;
if (!physical && !cached && !uncached)
{
normalized = 0u;
return false;
}
normalized = address & 0x1FFFFFFFu;
return normalized < iopMemory.size();
}
uint32_t allocateIopHandle(IopHandleKind kind) override
{
const uint32_t value = nextHandle;
nextHandle += (kind == IopHandleKind::RpcPacket) ? 0x40u : 0x80u;
return value;
}
uint32_t allocateGuest(uint32_t size, uint32_t alignment) override
{
if (size == 0u)
{
return 0u;
}
const uint64_t effectiveAlignment = alignment == 0u ? 1u : alignment;
const uint64_t aligned = ((static_cast<uint64_t>(nextGuestAddress) + effectiveAlignment - 1u) /
effectiveAlignment) *
effectiveAlignment;
if (aligned + size > memory.size())
{
return 0u;
}
nextGuestAddress = static_cast<uint32_t>(aligned + size);
guestAllocations.push_back(static_cast<uint32_t>(aligned));
return static_cast<uint32_t>(aligned);
}
void freeGuest(uint32_t address) override
{
freedGuestAddresses.push_back(address);
}
void audioCommand(uint32_t sid,
uint32_t function,
GuestBuffer send,
GuestBuffer receive) override
{
lastAudioSid = sid;
lastAudioFunction = function;
lastAudioSend = send;
lastAudioReceive = receive;
++audioCalls;
}
std::string hostPath(HostPathKind kind) const override
{
switch (kind)
{
case HostPathKind::CdRoot:
return "fake/cd";
case HostPathKind::CdImage:
return "fake/disc.iso";
case HostPathKind::HostRoot:
return "fake/host";
case HostPathKind::MemoryCardRoot:
return "fake/mc0";
default:
return "fake/elf";
}
}
std::string translateGuestPath(std::string_view path) const override
{
return "translated/" + std::string(path);
}
uint64_t openHostFile(std::string_view path) override
{
const auto file = hostFileContents.find(std::string(path));
if (file == hostFileContents.end())
{
return 0u;
}
const uint64_t handle = nextHostFileHandle++;
openHostFiles.emplace(handle, file->first);
return handle;
}
bool hostFileSize(uint64_t handle, uint64_t &size) const override
{
size = 0u;
const auto open = openHostFiles.find(handle);
if (open == openHostFiles.end())
{
return false;
}
const auto file = hostFileContents.find(open->second);
if (file == hostFileContents.end())
{
return false;
}
size = file->second.size();
return true;
}
bool readHostFile(uint64_t handle,
uint64_t offset,
void *destination,
size_t size,
size_t &bytesRead) override
{
bytesRead = 0u;
if (!destination && size != 0u)
{
return false;
}
const auto open = openHostFiles.find(handle);
if (open == openHostFiles.end())
{
return false;
}
const auto file = hostFileContents.find(open->second);
if (file == hostFileContents.end() || offset > file->second.size())
{
return false;
}
bytesRead = std::min<size_t>(size, file->second.size() - static_cast<size_t>(offset));
if (bytesRead != 0u)
{
std::memcpy(destination,
file->second.data() + static_cast<size_t>(offset),
bytesRead);
}
return true;
}
void closeHostFile(uint64_t handle) override
{
if (openHostFiles.erase(handle) != 0u)
{
closedHostFileHandles.push_back(handle);
}
}
int32_t memoryCard(const MemoryCardRequest &request) override
{
lastMemoryCardRequest = request;
++memoryCardCalls;
return 0;
}
bool hasGuestFunction(uint32_t address) const override
{
return address == guestFunctionAddress;
}
bool invokeGuestFunction(uint64_t callToken,
uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t *resultAddress) override
{
if (!hasGuestFunction(address))
{
return false;
}
lastCallToken = callToken;
lastGuestArguments = {a0, a1, a2, a3};
if (resultAddress)
{
*resultAddress = guestFunctionResult;
}
return true;
}
void log(LogLevel level, std::string_view message) override
{
logs.emplace_back(level, std::string(message));
}
bool writeWord(uint32_t address, uint32_t value)
{
return writeGuest(address, &value, sizeof(value));
}
uint32_t readWord(uint32_t address) const
{
uint32_t value = 0u;
(void)readGuest(address, &value, sizeof(value));
return value;
}
bool hasLog(std::string_view expected) const
{
return std::any_of(logs.begin(), logs.end(), [&](const auto &entry)
{ return entry.second == expected; });
}
std::vector<uint8_t> memory;
std::vector<uint8_t> iopMemory;
uint32_t nextHandle = 0x8000u;
uint32_t nextGuestAddress = 0x4000u;
std::vector<uint32_t> guestAllocations;
std::vector<uint32_t> freedGuestAddresses;
uint32_t audioCalls = 0u;
uint32_t lastAudioSid = 0u;
uint32_t lastAudioFunction = 0u;
GuestBuffer lastAudioSend{};
GuestBuffer lastAudioReceive{};
uint32_t memoryCardCalls = 0u;
MemoryCardRequest lastMemoryCardRequest{};
uint32_t guestFunctionAddress = 0x2000u;
uint32_t guestFunctionResult = 0x3000u;
uint64_t lastCallToken = 0u;
std::vector<uint32_t> lastGuestArguments;
std::vector<std::pair<LogLevel, std::string>> logs;
std::unordered_map<std::string, std::vector<uint8_t>> hostFileContents;
std::unordered_map<uint64_t, std::string> openHostFiles;
std::vector<uint64_t> closedHostFileHandles;
uint64_t nextHostFileHandle = 1u;
private:
bool contains(uint32_t address, size_t size) const
{
const uint64_t end = static_cast<uint64_t>(address) + static_cast<uint64_t>(size);
return end <= memory.size();
}
};
bool containsDiagnostic(const DebugSnapshot &snapshot, std::string_view text)
{
return std::any_of(snapshot.diagnostics.begin(), snapshot.diagnostics.end(), [&](const std::string &diagnostic)
{ return diagnostic.find(text) != std::string::npos; });
}
const DebugService *findService(const DebugSnapshot &snapshot, std::string_view name)
{
const auto it = std::find_if(snapshot.services.begin(), snapshot.services.end(), [&](const DebugService &service)
{ return service.name == name; });
return it == snapshot.services.end() ? nullptr : &*it;
}
uint64_t metricValue(const DebugService &service, std::string_view name)
{
const auto it = std::find_if(service.metrics.begin(), service.metrics.end(), [&](const DebugMetric &metric)
{ return metric.name == name; });
return it == service.metrics.end() ? std::numeric_limits<uint64_t>::max() : it->value;
}
bool pluginModuleIsLoaded(const std::filesystem::path &path)
{
#if defined(_WIN32)
return GetModuleHandleW(path.c_str()) != nullptr;
#elif defined(__linux__)
void *handle = dlopen(path.c_str(), RTLD_NOW | RTLD_NOLOAD);
if (!handle)
{
return false;
}
dlclose(handle);
return true;
#else
(void)path;
return false;
#endif
}
}
#include "../../ps2xIOP/tests/iop_compat_test_support.h"
void register_ps2_iop_tests()
{
@@ -414,11 +25,8 @@ void register_ps2_iop_tests()
tc.Run("HLE services activate only after a recognized module load", [](TestCase &t)
{
FakeIopHost host;
iop_test::Host host;
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"unmatched.elf", 0x100000u, 0u}, &error),
"core-only IOP configuration should succeed");
t.IsFalse(subsystem.canBindRpc(0x80000701u),
"LIBSD RPC must not exist before LIBSD is loaded");
@@ -446,15 +54,11 @@ void register_ps2_iop_tests()
"stopping LIBSD should deactivate its RPC endpoint");
});
tc.Run("unknown SID remains unhandled without a matching profile", [](TestCase &t)
tc.Run("unknown SID remains unhandled without a loaded IRX", [](TestCase &t)
{
FakeIopHost host;
iop_test::Host host;
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
const bool configured = subsystem.configure({"unmatched.elf", 0x100000u, 0x12345678u}, &error);
t.IsTrue(configured, "configuring an unmatched game should keep core-only IOP services available");
ps2x::iop::RpcRequest request{};
request.sid = 0xDEADC0DEu;
request.function = 0x99u;
@@ -465,514 +69,7 @@ void register_ps2_iop_tests()
t.Equals(result.callbackPolicy, ps2x::iop::CallbackPolicy::RuntimeDefault,
"an unknown SID should preserve runtime callback handling");
const ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot();
t.IsTrue(snapshot.activeProfile.empty(), "an unmatched game should not activate a profile");
t.IsTrue(snapshot.activeProvider.empty(), "an unmatched game should not report a profile provider");
});
tc.Run("built-in profiles select by ELF basename and keep core services active", [](TestCase &t)
{
FakeIopHost host;
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"SLUS_201.84", 0u, 0u}, &error),
"RECVX profile should match case-insensitively by basename");
ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot();
t.Equals(snapshot.activeProfile, std::string("recvx-us"),
"RECVX ELF should select its built-in profile");
t.IsNotNull(findService(snapshot, "TSNDDRV"),
"RECVX profile should register TSNDDRV");
t.IsNotNull(findService(snapshot, "CRI DTX"),
"RECVX profile should register CRI DTX");
t.IsNotNull(findService(snapshot, "dbcman"),
"core DBCMAN should remain active with a game profile");
t.IsNotNull(findService(snapshot, "libsd"),
"core LIBSD should remain active with a game profile");
t.IsNotNull(findService(snapshot, "MCSERV"),
"core MCSERV should remain active with a game profile");
error.clear();
t.IsTrue(subsystem.configure({"slus_203.88", 0u, 0u}, &error),
"Fatal Frame profile should configure after a different game");
snapshot = subsystem.debugSnapshot();
t.Equals(snapshot.activeProfile, std::string("fatal-frame-us"),
"reload should replace the active profile");
t.IsNull(findService(snapshot, "CRI DTX"),
"reload should destroy services from the previous profile");
t.IsNotNull(findService(snapshot, "SDRDRV"),
"Fatal Frame profile should expose SDRDRV");
});
tc.Run("two subsystem instances isolate profile state and reset deterministically", [](TestCase &t)
{
FakeIopHost hostA;
FakeIopHost hostB;
ps2x::iop::IopSubsystem subsystemA(hostA);
ps2x::iop::IopSubsystem subsystemB(hostB);
std::string error;
t.IsTrue(subsystemA.configure({"SLUS_205.78", 0u, 0u}, &error),
"first LotR instance should configure");
t.IsTrue(subsystemB.configure({"SLUS_205.78", 0u, 0u}, &error),
"second LotR instance should configure");
ps2x::iop::RpcRequest request{};
request.sid = 0x00012345u;
request.receive = {0x1000u, 8u};
t.IsTrue(subsystemA.handleRpc(request).handled,
"first instance should handle LotR sound RPC");
t.Equals(hostA.readWord(0x1004u), 1u,
"first instance should start its counter at one");
(void)subsystemA.handleRpc(request);
t.Equals(hostA.readWord(0x1004u), 2u,
"first instance should advance independently");
t.IsTrue(subsystemB.handleRpc(request).handled,
"second instance should handle LotR sound RPC");
t.Equals(hostB.readWord(0x1004u), 1u,
"second instance must not inherit the first counter");
subsystemA.reset();
(void)subsystemA.handleRpc(request);
t.Equals(hostA.readWord(0x1004u), 1u,
"reset should restore per-instance service state");
});
tc.Run("LotR sound update completes queued PlayStream slots", [](TestCase &t)
{
FakeIopHost host;
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"SLUS_205.78", 0u, 0u}, &error),
"LotR profile should configure");
constexpr uint32_t kSendAddress = 0x0800u;
constexpr uint32_t kReceiveAddress = 0x1000u;
constexpr uint16_t kStreamSlot = 7u;
const std::array<uint16_t, 10> playStreamPacket = {
1u, // command count
1u, // PlayStream
7u, // argument count
0u,
static_cast<uint16_t>(kStreamSlot << 8u),
0u,
0u,
0u,
0u,
0u,
};
t.IsTrue(host.writeGuest(kSendAddress,
playStreamPacket.data(),
sizeof(playStreamPacket)),
"PlayStream command packet should fit in guest memory");
ps2x::iop::RpcRequest request{};
request.sid = 0x00012345u;
request.send = {kSendAddress, sizeof(playStreamPacket)};
request.receive = {kReceiveAddress, 0x100u};
t.IsTrue(subsystem.handleRpc(request).handled,
"LotR sound service should handle PlayStream");
t.Equals(host.readWord(kReceiveAddress), 1u,
"PlayStream response should expose one active record");
const uint32_t packedStream = host.readWord(kReceiveAddress + 4u);
t.Equals((packedStream >> 4u) & 0x3Fu,
static_cast<uint32_t>(kStreamSlot),
"active record should identify the queued EE stream slot");
t.Equals(host.readWord(kReceiveAddress + 0x24u), 1u,
"response counter should follow the active record");
const std::array<uint16_t, 5> statusPacket = {
1u, // command count
9u, // GetStatus
2u, // argument count
kStreamSlot,
0u,
};
t.IsTrue(host.writeGuest(kSendAddress, statusPacket.data(), sizeof(statusPacket)),
"GetStatus command packet should fit in guest memory");
request.send.size = sizeof(statusPacket);
t.IsTrue(subsystem.handleRpc(request).handled,
"LotR sound service should handle the following status update");
t.Equals(host.readWord(kReceiveAddress), 0u,
"the update after PlayStream should report no active records");
t.Equals(host.readWord(kReceiveAddress + 4u), 2u,
"empty response counter should return to the base offset");
});
tc.Run("TSNDDRV uses profile checksum bindings without writing invalid ports", [](TestCase &t)
{
FakeIopHost host(0x02000000u);
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"slus_201.84", 0u, 0u}, &error),
"RECVX profile should configure for TSNDDRV command testing");
constexpr uint32_t kResponseAddress = 0x1000u;
ps2x::iop::RpcRequest stateRequest{};
stateRequest.sid = 1u;
stateRequest.function = 0x12u;
stateRequest.receive = {kResponseAddress, sizeof(uint32_t)};
t.IsTrue(subsystem.handleRpc(stateRequest).handled,
"TSNDDRV should return its configured status buffer");
const uint32_t statusAddress = host.readWord(kResponseAddress);
t.IsTrue(statusAddress != 0u, "TSNDDRV status buffer should be allocated");
constexpr int16_t kChecksum = 0x1234;
t.IsTrue(host.writeGuest(0x01E0EF10u, &kChecksum, sizeof(kChecksum)),
"RECVX primary checksum binding should be writable in the fake guest");
constexpr uint32_t kCommandAddress = 0x2000u;
std::array<uint8_t, 8> command{};
command[0] = 0x29u;
command[1] = 0u;
t.IsTrue(host.writeGuest(kCommandAddress, command.data(), command.size()),
"valid TSNDDRV command should be writable");
ps2x::iop::RpcRequest commandRequest{};
commandRequest.sid = 0u;
commandRequest.function = 0u;
commandRequest.send = {kCommandAddress, static_cast<uint32_t>(command.size())};
t.IsTrue(subsystem.handleRpc(commandRequest).handled,
"TSNDDRV should handle the characterized command queue");
int16_t writtenChecksum = 0;
t.IsTrue(host.readIopMemory(statusAddress + 0x26u,
&writtenChecksum,
sizeof(writtenChecksum)),
"TSNDDRV SE checksum slot should be readable");
t.Equals(writtenChecksum, kChecksum,
"valid port should mirror the profile-bound checksum table");
constexpr uint32_t kPastStatusAddress = 0x44u;
constexpr uint16_t kSentinel = 0xBEEFu;
t.IsTrue(host.writeIopMemory(statusAddress + kPastStatusAddress,
&kSentinel,
sizeof(kSentinel)),
"sentinel after the status structure should be writable");
command[1] = 0x0Fu;
(void)host.writeGuest(kCommandAddress, command.data(), command.size());
(void)subsystem.handleRpc(commandRequest);
uint16_t sentinelAfter = 0u;
(void)host.readIopMemory(statusAddress + kPastStatusAddress,
&sentinelAfter,
sizeof(sentinelAfter));
t.Equals(sentinelAfter, kSentinel,
"invalid port must not overwrite memory past the 0x42-byte status structure");
});
tc.Run("RECVX reset clears CRI object maps without global state", [](TestCase &t)
{
FakeIopHost host(0x02000000u);
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"slus_201.84", 0u, 0u}, &error),
"RECVX profile should configure");
constexpr uint32_t kSendAddress = 0x2000u;
constexpr uint32_t kReceiveAddress = 0x2100u;
host.writeWord(kSendAddress + 0u, 0u);
host.writeWord(kSendAddress + 4u, 0x4000u);
host.writeWord(kSendAddress + 8u, 0x100u);
ps2x::iop::RpcRequest request{};
request.sid = 0x7D000000u;
request.function = 0x422u;
request.send = {kSendAddress, 12u};
request.receive = {kReceiveAddress, 4u};
t.IsTrue(subsystem.handleRpc(request).handled,
"SJRMT create should be emulated by the RECVX profile");
ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot();
const ps2x::iop::DebugService *service =
findService(snapshot, "CRI DTX");
if (!service)
{
t.Fail("CRI DTX service should be visible in the debug snapshot");
return;
}
t.Equals(metricValue(*service, "sjrmt_objects"), uint64_t{1},
"created CRI object should be tracked by this instance");
subsystem.reset();
snapshot = subsystem.debugSnapshot();
service = findService(snapshot, "CRI DTX");
if (!service)
{
t.Fail("CRI DTX service should survive reset");
return;
}
t.Equals(metricValue(*service, "sjrmt_objects"), uint64_t{0},
"reset should clear CRI object maps");
});
tc.Run("reset closes profile-owned host file handles", [](TestCase &t)
{
FakeIopHost host;
host.hostFileContents["translated/test.bin"] = {0x10u, 0x20u, 0x30u};
ps2x::iop::IopSubsystem subsystem(host);
std::string error;
t.IsTrue(subsystem.configure({"SLUS_205.78", 0u, 0u}, &error),
"LotR profile should configure for file lifecycle testing");
constexpr uint32_t kPathAddress = 0x1000u;
constexpr uint32_t kReceiveAddress = 0x1100u;
constexpr char kPath[] = "test.bin";
t.IsTrue(host.writeGuest(kPathAddress, kPath, sizeof(kPath)),
"fake guest path should be writable");
ps2x::iop::RpcRequest request{};
request.sid = 0x0000FF01u;
request.function = 0x08u;
request.send = {kPathAddress, sizeof(kPath)};
request.receive = {kReceiveAddress, 8u};
t.IsTrue(subsystem.handleRpc(request).handled,
"LotR CLFILE open should be handled");
t.Equals(host.openHostFiles.size(), size_t{1},
"open RPC should retain one opaque host file handle");
ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot();
const ps2x::iop::DebugService *service =
findService(snapshot, "CLFILE");
if (!service)
{
t.Fail("LotR CLFILE service should be visible before reset");
return;
}
t.Equals(metricValue(*service, "open_files"), uint64_t{1},
"debug state should report the open file");
subsystem.reset();
t.IsTrue(host.openHostFiles.empty(),
"reset should release every retained host file handle");
t.Equals(host.closedHostFileHandles.size(), size_t{1},
"host close callback should run exactly once");
snapshot = subsystem.debugSnapshot();
service = findService(snapshot, "CLFILE");
if (!service)
{
t.Fail("LotR CLFILE service should survive reset");
return;
}
t.Equals(metricValue(*service, "open_files"), uint64_t{0},
"reset should clear the CLFILE handle registry");
});
#if defined(PS2X_TEST_IOP_PLUGIN_DIR)
tc.Run("plugin module remains loaded through instances and unloads after subsystem destruction", [](TestCase &t)
{
const std::filesystem::path pluginDirectory(PS2X_TEST_IOP_PLUGIN_DIR);
#if defined(_WIN32)
const std::filesystem::path pluginPath =
pluginDirectory / "ps2_iop_fake_plugin.dll";
#else
const std::filesystem::path pluginPath =
pluginDirectory / "ps2_iop_fake_plugin.so";
#endif
t.IsFalse(pluginModuleIsLoaded(pluginPath),
"synthetic plugin should not be loaded before discovery");
{
FakeIopHost host;
ps2x::iop::IopSubsystem subsystem(host);
subsystem.setPluginSearchPaths({pluginDirectory});
std::string error;
t.IsTrue(subsystem.loadPlugins(&error),
"synthetic plugins should load for lifetime testing");
t.IsTrue(subsystem.configure({"synthetic_iop_test.elf",
kSyntheticEntryPoint,
kSyntheticCrc32},
&error),
"synthetic plugin instance should be created");
t.IsTrue(pluginModuleIsLoaded(pluginPath),
"module must stay loaded while a profile instance exists");
}
t.IsFalse(pluginModuleIsLoaded(pluginPath),
"module should unload after profile destruction and catalog teardown");
});
tc.Run("plugin discovery matches all identity fields and dispatches through the host bridge", [](TestCase &t)
{
FakeIopHost host;
ps2x::iop::IopSubsystem subsystem(host);
const std::filesystem::path pluginDirectory(PS2X_TEST_IOP_PLUGIN_DIR);
t.IsTrue(std::filesystem::is_directory(pluginDirectory),
"the synthetic IOP plugin directory should be staged by the test build");
subsystem.setPluginSearchPaths({pluginDirectory});
std::string error;
t.IsTrue(subsystem.loadPlugins(&error), "synthetic IOP plugin discovery should succeed");
ps2x::iop::DebugSnapshot discoverySnapshot = subsystem.debugSnapshot();
t.IsTrue(containsDiagnostic(discoverySnapshot, "loaded 4 profile(s)"),
"plugin discovery diagnostics should report all accepted synthetic profiles");
t.IsTrue(containsDiagnostic(discoverySnapshot, "too many SIDs"),
"an invalid profile descriptor should be ignored with a diagnostic");
t.IsTrue(containsDiagnostic(discoverySnapshot, "bad_abi"),
"an ABI-incompatible plugin should be ignored with a diagnostic");
t.IsTrue(containsDiagnostic(discoverySnapshot, "incompatible ABI"),
"the incompatible-plugin diagnostic should explain the ABI failure");
t.IsTrue(containsDiagnostic(discoverySnapshot, "missing_symbol"),
"a plugin without the query symbol should be ignored with a diagnostic");
t.IsTrue(containsDiagnostic(discoverySnapshot, "missing ps2x_iop_query_v1"),
"the missing-symbol diagnostic should name the required entry point");
auto expectNoProfile = [&](const ps2x::iop::GameIdentity &identity, const std::string &reason) {
error.clear();
t.IsTrue(subsystem.configure(identity, &error), "mismatching plugin identity should configure core-only services");
const ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot();
t.IsTrue(snapshot.activeProfile.empty(), reason);
ps2x::iop::RpcRequest request{};
request.sid = kSyntheticSid;
request.function = kSyntheticFunction;
t.IsFalse(subsystem.handleRpc(request).handled,
"a mismatching profile must not expose its synthetic SID");
};
expectNoProfile({"different.elf", kSyntheticEntryPoint, kSyntheticCrc32},
"a different ELF basename should not match the plugin profile");
expectNoProfile({"synthetic_iop_test.elf", kSyntheticEntryPoint + 4u, kSyntheticCrc32},
"a different entry point should not match the plugin profile");
expectNoProfile({"synthetic_iop_test.elf", kSyntheticEntryPoint, kSyntheticCrc32 ^ 1u},
"a different CRC32 should not match the plugin profile");
error.clear();
t.IsTrue(subsystem.configure({"synthetic_iop_test.elf", kSyntheticEntryPoint, kSyntheticCrc32}, &error),
"the synthetic ELF identity should activate the plugin profile");
ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot();
t.Equals(snapshot.activeProfile, std::string("synthetic-test-profile"),
"debug snapshot should expose the active plugin profile id");
t.Equals(snapshot.activeProvider, std::string("ps2x-test-plugin"),
"debug snapshot should expose the plugin provider name");
const ps2x::iop::DebugService *service = findService(snapshot, "synthetic-test-profile");
if (!service)
{
t.Fail("debug snapshot should include the synthetic profile service");
return;
}
t.IsTrue(service->profileSpecific, "plugin service should be marked profile-specific");
t.IsTrue(std::find(service->sids.begin(), service->sids.end(), kSyntheticSid) != service->sids.end(),
"plugin service should advertise its synthetic SID");
t.Equals(metricValue(*service, "reset_generation"), uint64_t{1},
"profile configuration should reset a new plugin instance once");
ps2x::iop::RpcAbiRequest abiRequest{};
abiRequest.boundSid = kSyntheticSid;
abiRequest.function = kSyntheticFunction;
abiRequest.registers.plausible = true;
abiRequest.stack.plausible = true;
t.Equals(subsystem.selectRpcAbi(abiRequest), ps2x::iop::RpcAbi::Stack,
"plugin should be able to select the stack RPC ABI");
abiRequest.function = kSyntheticFunction + 1u;
t.Equals(subsystem.selectRpcAbi(abiRequest), ps2x::iop::RpcAbi::RuntimeDefault,
"plugin ABI selection should fall back for unrelated functions");
constexpr uint32_t kSendAddress = 0x1000u;
constexpr uint32_t kReceiveAddress = 0x1100u;
constexpr uint32_t kInput = 0x1234ABCDu;
t.IsTrue(host.writeWord(kSendAddress, kInput), "fake host should seed the plugin send buffer");
t.IsTrue(host.writeWord(kReceiveAddress, 0u), "fake host should clear the plugin receive buffer");
ps2x::iop::RpcRequest request{};
request.callToken = 0x1122334455667788ull;
request.sid = kSyntheticSid;
request.function = kSyntheticFunction;
request.send = {kSendAddress, sizeof(uint32_t)};
request.receive = {kReceiveAddress, sizeof(uint32_t)};
const ps2x::iop::RpcResult result = subsystem.handleRpc(request);
t.IsTrue(result.handled, "matching synthetic SID/function should dispatch to the plugin");
t.Equals(result.resultAddress, kReceiveAddress, "plugin should return its receive-buffer address");
t.IsTrue(result.signalNowaitCompletion, "plugin should request nowait completion signaling");
t.Equals(result.callbackPolicy, ps2x::iop::CallbackPolicy::Suppress,
"plugin should be able to suppress the runtime callback");
t.Equals(host.readWord(kReceiveAddress), kInput ^ kResponseXor,
"plugin should read and write guest memory through the IopHost bridge");
ps2x::iop::RpcRequest unknownRequest{};
unknownRequest.sid = 0xDEADC0DEu;
unknownRequest.function = kSyntheticFunction;
t.IsFalse(subsystem.handleRpc(unknownRequest).handled,
"unknown SID should remain unhandled while a plugin profile is active");
constexpr uint32_t kCoreCollisionReceiveAddress = 0x1200u;
ps2x::iop::RpcRequest collisionRequest{};
collisionRequest.sid = kCoreCollisionSid;
collisionRequest.function = kCoreCollisionFunction;
collisionRequest.receive = {kCoreCollisionReceiveAddress, sizeof(uint32_t)};
const ps2x::iop::RpcResult collisionResult = subsystem.handleRpc(collisionRequest);
t.IsTrue(collisionResult.handled,
"a profile service should take precedence over a core service for the same SID");
t.Equals(host.readWord(kCoreCollisionReceiveAddress), kCoreCollisionResponse,
"the profile collision route should reach the plugin implementation");
subsystem.onSifTransfer({ps2x::iop::SifTransferKind::SetDma,
ps2x::iop::SifTransferPhase::AfterCopy,
kSendAddress,
kReceiveAddress,
sizeof(uint32_t)});
snapshot = subsystem.debugSnapshot();
service = findService(snapshot, "synthetic-test-profile");
if (!service)
{
t.Fail("synthetic profile service should remain visible after dispatch");
return;
}
t.Equals(metricValue(*service, "rpc_calls"), uint64_t{2},
"plugin debug metrics should count dispatched RPCs");
t.Equals(metricValue(*service, "sif_transfers"), uint64_t{1},
"plugin debug metrics should count SIF transfer hooks");
subsystem.reset();
snapshot = subsystem.debugSnapshot();
service = findService(snapshot, "synthetic-test-profile");
if (!service)
{
t.Fail("synthetic profile service should remain visible after reset");
return;
}
t.Equals(metricValue(*service, "reset_generation"), uint64_t{2},
"explicit subsystem reset should reach the plugin instance");
t.Equals(metricValue(*service, "rpc_calls"), uint64_t{0},
"plugin reset should clear per-instance RPC state");
t.Equals(metricValue(*service, "sif_transfers"), uint64_t{0},
"plugin reset should clear per-instance transfer state");
error.clear();
t.IsFalse(subsystem.configure({"synthetic_duplicate.elf", kSyntheticEntryPoint, kSyntheticCrc32}, &error),
"duplicate SIDs inside one profile layer should reject configuration");
t.IsTrue(error.find("duplicate IOP SID") != std::string::npos,
"duplicate-SID failure should clearly identify the registry conflict");
error.clear();
t.IsFalse(subsystem.configure({"slus_201.84", kSyntheticEntryPoint, kSyntheticCrc32}, &error),
"equally specific built-in and plugin matchers should be ambiguous");
t.IsTrue(error.find("ambiguous IOP profiles") != std::string::npos,
"ambiguous profile selection should fail clearly");
error.clear();
t.IsTrue(subsystem.configure({"slus_201.84",
kSpecificRecvXEntryPoint,
kSyntheticCrc32},
&error),
"a more-specific matcher should win over a lower-specificity tie");
t.Equals(subsystem.debugSnapshot().activeProfile,
std::string("synthetic-specific-recvx-profile"),
"the most specific plugin profile should be selected");
error.clear();
t.IsTrue(subsystem.configure({"different.elf", kSyntheticEntryPoint, kSyntheticCrc32}, &error),
"switching to an unmatched ELF should destroy the active plugin profile");
t.IsTrue(host.hasLog("fake-plugin-destroy"),
"plugin profile destroy callback should run when the active profile is replaced");
t.IsTrue(subsystem.debugSnapshot().activeProfile.empty(),
"switching to an unmatched ELF should leave no active profile");
});
#endif
});
}
-659
View File
@@ -10,9 +10,6 @@
#include <array>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace ps2_stubs
@@ -37,17 +34,6 @@ namespace
}
};
void configureProfile(TestEnv &env, std::string_view elfName)
{
std::string error;
const bool configured = PS2IopTransport::configureForTesting(
&env.runtime, {std::string(elfName), 0u, 0u}, &error);
if (!configured)
{
throw std::runtime_error("failed to configure test IOP profile: " + error);
}
}
#pragma pack(push, 1)
struct Ps2SifDmaTransfer
{
@@ -99,24 +85,6 @@ namespace
return value;
}
void writeGuestS16(uint8_t *rdram, uint32_t addr, int16_t value)
{
std::memcpy(rdram + addr, &value, sizeof(value));
}
int16_t readGuestS16(const uint8_t *rdram, uint32_t addr)
{
int16_t value = 0;
std::memcpy(&value, rdram + addr, sizeof(value));
return value;
}
void writeIopS16(PS2Runtime &runtime, uint32_t addr, int16_t value)
{
if (!runtime.writeIopMemory(addr, &value, sizeof(value)))
throw std::runtime_error("failed to write IOP test memory");
}
uint32_t g_dmacHandlerWriteAddr = 0u;
uint32_t g_dmacHandlerValue = 0u;
uint32_t g_dmacHandlerLastCause = 0u;
@@ -357,505 +325,6 @@ void register_ps2_sif_dma_tests()
"DMAC handler should receive registered argument");
});
tc.Run("sceSifSetDma acknowledges DTX work-buffer transfers by advancing the EE footer ticket", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0002D000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kSendAddr = 0x0002D100u;
constexpr uint32_t kRecvAddr = 0x0002D200u;
constexpr uint32_t kDescAddr = 0x0002D300u;
constexpr uint32_t kEeWorkAddr = 0x0002D400u;
constexpr uint32_t kIopWorkAddr = 0x0002D800u;
constexpr uint32_t kDtxId = 3u;
constexpr uint32_t kWorkLen = 0x100u;
constexpr uint32_t kFooterTicketAddr = kEeWorkAddr + kWorkLen - sizeof(uint32_t);
ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for the DTX sid");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, kDtxId);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWorkAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWorkAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen);
writeGuestU32(env.rdram.data(), kRecvAddr + 0x00u, 0u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 2u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should create the DTX transport");
t.IsTrue(readGuestU32(env.rdram.data(), kRecvAddr) != 0u, "DTX create should return a remote handle");
std::memset(env.rdram.data() + kEeWorkAddr, 0x44, kWorkLen);
std::memset(env.rdram.data() + kIopWorkAddr, 0x00, kWorkLen);
writeGuestU32(env.rdram.data(), kFooterTicketAddr, 1u);
const Ps2SifDmaTransfer desc{
kEeWorkAddr,
kIopWorkAddr,
static_cast<int32_t>(kWorkLen),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the DTX transfer");
t.Equals(readGuestU32(env.rdram.data(), kFooterTicketAddr), 2u,
"sceSifSetDma should advance the EE footer ticket so DTX clears wait_flag");
});
tc.Run("sceSifSetDma applies SJX DTX payloads into the emulated SJRMT data ring", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0002E000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kRecvAddr = 0x0002E100u;
constexpr uint32_t kSendAddr = 0x0002E200u;
constexpr uint32_t kDescAddr = 0x0002E300u;
constexpr uint32_t kEeWorkAddr = 0x0002E400u;
constexpr uint32_t kIopWorkAddr = 0x0002E800u;
constexpr uint32_t kRingAddr = 0x0002EC00u;
constexpr uint32_t kChunkDataAddr = 0x0002ED00u;
constexpr uint32_t kWorkLen = 0x100u;
constexpr uint32_t kChunkLen = 8u;
ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x422u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 12u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0x12345678u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x400u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWorkAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWorkAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 2u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed");
std::memset(env.rdram.data() + kEeWorkAddr, 0, kWorkLen);
std::memset(env.rdram.data() + kIopWorkAddr, 0, kWorkLen);
std::memset(env.rdram.data() + kRingAddr, 0, kWorkLen);
for (uint32_t i = 0; i < kChunkLen; ++i)
{
env.rdram[kChunkDataAddr + i] = static_cast<uint8_t>(0xA0u + i);
}
writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x00u, 1u);
env.rdram[kEeWorkAddr + 0x10u] = 0u;
env.rdram[kEeWorkAddr + 0x11u] = 1u;
std::memcpy(env.rdram.data() + kEeWorkAddr + 0x12u, "\0\0", 2u);
writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x14u, sjxHandle);
writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x18u, kChunkDataAddr);
writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x1Cu, kChunkLen);
writeGuestU32(env.rdram.data(), kEeWorkAddr + kWorkLen - sizeof(uint32_t), 1u);
const Ps2SifDmaTransfer desc{
kEeWorkAddr,
kIopWorkAddr,
static_cast<int32_t>(kWorkLen),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the SJX transport");
t.Equals(env.rdram[kEeWorkAddr + 0x11u], static_cast<uint8_t>(0u),
"SJX DMA ack should rewrite the response line to room so EE recycles the chunk");
t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + 0x14u), 0x12345678u,
"SJX DMA ack should translate the remote handle back to the EE callback object");
t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + kWorkLen - sizeof(uint32_t)), 2u,
"SJX DMA ack should still advance the EE footer ticket");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x429u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 8u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kChunkLen,
"SJX DMA should make SJRMT report available data");
t.IsTrue(std::memcmp(env.rdram.data() + kRingAddr, env.rdram.data() + kChunkDataAddr, kChunkLen) == 0,
"SJX DMA should copy the chunk payload into the emulated SJRMT ring");
});
tc.Run("sceSifSetDma recognizes SJX DTX payloads from rotated EE work buffers", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x00031000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kRecvAddr = 0x00031100u;
constexpr uint32_t kSendAddr = 0x00031200u;
constexpr uint32_t kDescAddr = 0x00031300u;
constexpr uint32_t kRegisteredEeWorkAddr = 0x00031400u;
constexpr uint32_t kRegisteredIopWorkAddr = 0x00031800u;
constexpr uint32_t kAltEeWorkAddr = 0x00031C00u;
constexpr uint32_t kAltIopWorkAddr = 0x00032000u;
constexpr uint32_t kRingAddr = 0x00032400u;
constexpr uint32_t kChunkDataAddr = 0x00032500u;
constexpr uint32_t kRegisteredWorkLen = 0x100u;
constexpr uint32_t kAltWorkLen = 0x180u;
constexpr uint32_t kChunkLen = 12u;
ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kRegisteredWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x422u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 12u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0x87654321u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x400u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRegisteredEeWorkAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kRegisteredIopWorkAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kRegisteredWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 2u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed");
std::memset(env.rdram.data() + kRegisteredEeWorkAddr, 0, kRegisteredWorkLen);
std::memset(env.rdram.data() + kRegisteredIopWorkAddr, 0, kRegisteredWorkLen);
std::memset(env.rdram.data() + kAltEeWorkAddr, 0, kAltWorkLen);
std::memset(env.rdram.data() + kAltIopWorkAddr, 0, kAltWorkLen);
std::memset(env.rdram.data() + kRingAddr, 0, kRegisteredWorkLen);
for (uint32_t i = 0; i < kChunkLen; ++i)
{
env.rdram[kChunkDataAddr + i] = static_cast<uint8_t>(0xC0u + i);
}
writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x00u, 1u);
env.rdram[kAltEeWorkAddr + 0x10u] = 0u;
env.rdram[kAltEeWorkAddr + 0x11u] = 1u;
std::memcpy(env.rdram.data() + kAltEeWorkAddr + 0x12u, "\0\0", 2u);
writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x14u, sjxHandle);
writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x18u, kChunkDataAddr);
writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x1Cu, kChunkLen);
writeGuestU32(env.rdram.data(), kAltEeWorkAddr + kAltWorkLen - sizeof(uint32_t), 9u);
const Ps2SifDmaTransfer desc{
kAltEeWorkAddr,
kAltIopWorkAddr,
static_cast<int32_t>(kAltWorkLen),
0};
std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc));
setRegU32(env.ctx, 4, kDescAddr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the rotated SJX transport");
t.Equals(env.rdram[kAltEeWorkAddr + 0x11u], static_cast<uint8_t>(0u),
"rotated SJX DMA ack should rewrite the response line to room");
t.Equals(readGuestU32(env.rdram.data(), kAltEeWorkAddr + kAltWorkLen - sizeof(uint32_t)), 10u,
"rotated SJX DMA ack should advance the alternate EE footer ticket");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x429u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 8u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kChunkLen,
"rotated SJX DMA should make SJRMT report available data");
t.IsTrue(std::memcmp(env.rdram.data() + kRingAddr, env.rdram.data() + kChunkDataAddr, kChunkLen) == 0,
"rotated SJX DMA should copy the chunk payload into the emulated SJRMT ring");
});
tc.Run("sceSifSetDma lets active PS2RNA playback drain emulated SJRMT data", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0002F000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kRecvAddr = 0x0002F100u;
constexpr uint32_t kSendAddr = 0x0002F200u;
constexpr uint32_t kDesc0Addr = 0x0002F300u;
constexpr uint32_t kDesc1Addr = 0x0002F320u;
constexpr uint32_t kEeWork0Addr = 0x0002F400u;
constexpr uint32_t kIopWork0Addr = 0x0002F800u;
constexpr uint32_t kEeWork1Addr = 0x0002FC00u;
constexpr uint32_t kIopWork1Addr = 0x00030000u;
constexpr uint32_t kRingAddr = 0x00030400u;
constexpr uint32_t kChunkDataAddr = 0x00030500u;
constexpr uint32_t kWorkLen = 0x100u;
constexpr uint32_t kChunkLen = 8u;
ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x422u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 12u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0xCAFEBABEu);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x400u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x408u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t ps2RnaHandle = readGuestU32(env.rdram.data(), kRecvAddr);
t.IsTrue(ps2RnaHandle != 0u, "PS2RNA_CREATE should return a handle");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWork0Addr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWork0Addr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 2u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed for SJX transport");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWork1Addr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWork1Addr);
writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 2u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 16u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed for PS2RNA transport");
std::memset(env.rdram.data() + kEeWork0Addr, 0, kWorkLen);
std::memset(env.rdram.data() + kIopWork0Addr, 0, kWorkLen);
std::memset(env.rdram.data() + kEeWork1Addr, 0, kWorkLen);
std::memset(env.rdram.data() + kIopWork1Addr, 0, kWorkLen);
std::memset(env.rdram.data() + kRingAddr, 0, kWorkLen);
for (uint32_t i = 0; i < kChunkLen; ++i)
{
env.rdram[kChunkDataAddr + i] = static_cast<uint8_t>(0xB0u + i);
}
writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x00u, 1u);
writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x10u, 2u);
writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x14u, ps2RnaHandle);
writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x18u, 1u);
writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x1Cu, 0u);
writeGuestU32(env.rdram.data(), kEeWork1Addr + kWorkLen - sizeof(uint32_t), 1u);
const Ps2SifDmaTransfer desc1{
kEeWork1Addr,
kIopWork1Addr,
static_cast<int32_t>(kWorkLen),
0};
std::memcpy(env.rdram.data() + kDesc1Addr, &desc1, sizeof(desc1));
setRegU32(env.ctx, 4, kDesc1Addr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the PS2RNA control transport");
t.Equals(readGuestU32(env.rdram.data(), kEeWork1Addr + kWorkLen - sizeof(uint32_t)), 2u,
"PS2RNA control DMA should advance the EE footer ticket");
writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x00u, 1u);
env.rdram[kEeWork0Addr + 0x10u] = 0u;
env.rdram[kEeWork0Addr + 0x11u] = 1u;
std::memcpy(env.rdram.data() + kEeWork0Addr + 0x12u, "\0\0", 2u);
writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x14u, sjxHandle);
writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x18u, kChunkDataAddr);
writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x1Cu, kChunkLen);
writeGuestU32(env.rdram.data(), kEeWork0Addr + kWorkLen - sizeof(uint32_t), 1u);
const Ps2SifDmaTransfer desc0{
kEeWork0Addr,
kIopWork0Addr,
static_cast<int32_t>(kWorkLen),
0};
std::memcpy(env.rdram.data() + kDesc0Addr, &desc0, sizeof(desc0));
setRegU32(env.ctx, 4, kDesc0Addr);
setRegU32(env.ctx, 5, 1u);
ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime);
t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the SJX transport");
t.Equals(env.rdram[kEeWork0Addr + 0x11u], static_cast<uint8_t>(0u),
"SJX DMA ack should still rewrite the response line to room");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x429u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 8u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), 0u,
"active PS2RNA playback should drain remote SJRMT data instead of leaving it queued forever");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle);
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x429u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 8u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kWorkLen,
"drained PS2RNA playback should return remote SJRMT room to full capacity");
});
tc.Run("resetSifState seeds boot-ready SIF registers", [](TestCase &t)
{
TestEnv env;
@@ -986,134 +455,6 @@ void register_ps2_sif_dma_tests()
t.Equals(static_cast<uint32_t>(rd.size), kSize, "receive metadata size should be populated");
});
tc.Run("sceSifGetOtherData preserves live sound-status sums when compat backfill is enabled", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kRdAddr = 0x00023300u;
constexpr uint32_t kDstAddr = 0x00023400u;
constexpr uint32_t kSize = 0x42u;
constexpr uint32_t kPrimarySeCheckAddr = 0x01E0EF10u;
constexpr uint32_t kPrimaryMidiCheckAddr = 0x01E0EF20u;
constexpr uint32_t kMidiSumOffset = 0x1Eu;
constexpr uint32_t kSeSumOffset = 0x26u;
constexpr uint32_t kBank = 1u;
constexpr uint32_t kClientAddr = 0x00023500u;
constexpr uint32_t kRecvAddr = 0x00023600u;
constexpr uint32_t kSid = 1u;
ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for sound-driver sid");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x12u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t kSrcAddr = readGuestU32(env.rdram.data(), kRecvAddr);
std::memset(env.rdram.data() + kDstAddr, 0, kSize);
std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData));
writeIopS16(env.runtime, kSrcAddr + kSeSumOffset + (kBank * 2u), static_cast<int16_t>(0x1357));
writeIopS16(env.runtime, kSrcAddr + kMidiSumOffset + (kBank * 2u), static_cast<int16_t>(0x2468));
writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kBank * 2u), static_cast<int16_t>(0x7B7B));
writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kBank * 2u), static_cast<int16_t>(0x6A6A));
setRegU32(env.ctx, 4, kRdAddr);
setRegU32(env.ctx, 5, kSrcAddr);
setRegU32(env.ctx, 6, kDstAddr);
setRegU32(env.ctx, 7, kSize);
ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0,
"sceSifGetOtherData should succeed for sound-status transfer");
t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kBank * 2u)),
static_cast<int16_t>(0x1357),
"live se_sum for the active bank should not be clobbered by compat check arrays");
t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kBank * 2u)),
static_cast<int16_t>(0x2468),
"live midi_sum for the active bank should not be clobbered by compat check arrays");
});
tc.Run("sceSifGetOtherData backfills zero sound-status sums for later banks", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kRdAddr = 0x00023700u;
constexpr uint32_t kDstAddr = 0x00023800u;
constexpr uint32_t kSize = 0x42u;
constexpr uint32_t kPrimarySeCheckAddr = 0x01E0EF10u;
constexpr uint32_t kPrimaryMidiCheckAddr = 0x01E0EF20u;
constexpr uint32_t kMidiSumOffset = 0x1Eu;
constexpr uint32_t kSeSumOffset = 0x26u;
constexpr uint32_t kLiveBank = 0u;
constexpr uint32_t kPendingBank = 1u;
constexpr uint32_t kClientAddr = 0x00023900u;
constexpr uint32_t kRecvAddr = 0x00023A00u;
ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 1u);
setRegU32(env.ctx, 6, 0u);
ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for sound-driver sid");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x12u);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 4u);
setRegU32(env.ctx, 11, 0u);
ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t kSrcAddr = readGuestU32(env.rdram.data(), kRecvAddr);
std::memset(env.rdram.data() + kDstAddr, 0, kSize);
std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData));
writeIopS16(env.runtime, kSrcAddr + kSeSumOffset + (kLiveBank * 2u), static_cast<int16_t>(0x1111));
writeIopS16(env.runtime, kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast<int16_t>(0x2222));
writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kPendingBank * 2u), static_cast<int16_t>(0x3333));
writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kPendingBank * 2u), static_cast<int16_t>(0x4444));
setRegU32(env.ctx, 4, kRdAddr);
setRegU32(env.ctx, 5, kSrcAddr);
setRegU32(env.ctx, 6, kDstAddr);
setRegU32(env.ctx, 7, kSize);
ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0,
"sceSifGetOtherData should succeed for later-bank sound-status transfer");
t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kLiveBank * 2u)),
static_cast<int16_t>(0x1111),
"existing live se_sum values should remain intact");
t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kLiveBank * 2u)),
static_cast<int16_t>(0x2222),
"existing live midi_sum values should remain intact");
t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kPendingBank * 2u)),
static_cast<int16_t>(0x3333),
"zero se_sum slots should backfill from compat tables for later banks");
t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kPendingBank * 2u)),
static_cast<int16_t>(0x4444),
"zero midi_sum slots should backfill from compat tables for later banks");
});
tc.Run("sceSifGetOtherData rejects unsupported guest segments", [](TestCase &t)
{
TestEnv env;
+3 -749
View File
@@ -7,14 +7,10 @@
#include "runtime/ee_scheduler.h"
#include <array>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <stdexcept>
#include <vector>
using namespace ps2_syscalls;
@@ -31,16 +27,8 @@ namespace
constexpr uint32_t K_SIF_RPC_MODE_NOWAIT = 0x01u;
constexpr uint32_t K_STACK_ADDR = 0x00100000u;
constexpr uint32_t IOP_SID_SNDDRV_COMMAND = 0x00000000u;
constexpr uint32_t IOP_SID_SNDDRV_STATE = 0x00000001u;
constexpr uint32_t IOP_SID_LOTR_CLFILE = 0x0000FF01u;
constexpr uint32_t IOP_SID_LOTR_SOUND = 0x00012345u;
constexpr uint32_t IOP_SID_MCSERV = 0x80000400u;
constexpr uint32_t IOP_SID_LIBSD = 0x80000701u;
constexpr uint32_t IOP_SID_FATAL_FRAME_SDRDRV = 0x19740512u;
constexpr uint32_t IOP_RPC_SNDDRV_SUBMIT = 0x00000000u;
constexpr uint32_t IOP_RPC_SNDDRV_GET_STATUS_ADDR = 0x00000012u;
constexpr uint32_t IOP_RPC_SNDDRV_GET_ADDR_TABLE = 0x00000013u;
#pragma pack(push, 1)
struct SifRpcHeader
@@ -126,17 +114,6 @@ namespace
}
};
void configureProfile(TestEnv &env, std::string_view elfName)
{
std::string error;
const bool configured = PS2IopTransport::configureForTesting(
&env.runtime, {std::string(elfName), 0u, 0u}, &error);
if (!configured)
{
throw std::runtime_error("failed to configure test IOP profile: " + error);
}
}
ps2x::iop::RpcResult callIop(TestEnv &env,
uint32_t sid,
uint32_t function,
@@ -154,107 +131,6 @@ namespace
&env.runtime, env.rdram.data(), &env.ctx, std::move(request));
}
std::atomic<uint32_t> g_lotrSoundCallbackHits{0u};
std::atomic<uint32_t> g_recvxSoundCallbackHits{0u};
std::atomic<uint32_t> g_dtxDispatcherHits{0u};
std::atomic<uint32_t> g_dtxDispatcherRpcNum{0u};
std::atomic<uint32_t> g_dtxDispatcherSendBuf{0u};
std::atomic<uint32_t> g_dtxDispatcherSendSize{0u};
constexpr uint32_t K_DTX_DISPATCH_RESULT_ADDR = 0x0002D800u;
constexpr uint32_t K_DTX_DISPATCH_RESULT_MARKER = 0xD15CA7C1u;
constexpr uint32_t K_DTX_SCHEDULER_CALL = 0x00102000u;
constexpr uint32_t K_DTX_SCHEDULER_RESUME = 0x00102010u;
constexpr uint32_t K_LOTR_SOUND_SCHEDULER_CALL = 0x00102020u;
constexpr uint32_t K_LOTR_SOUND_SCHEDULER_RESUME = 0x00102030u;
uint32_t g_schedulerRpcClient = 0u;
uint32_t g_schedulerRpcNumber = 0u;
uint32_t g_schedulerRpcSend = 0u;
uint32_t g_schedulerRpcReceive = 0u;
uint32_t g_schedulerRpcResult = 0u;
uint32_t g_schedulerLotrSoundClient = 0u;
uint32_t g_schedulerLotrSoundSend = 0u;
uint32_t g_schedulerLotrSoundReceive = 0u;
uint32_t g_schedulerLotrSoundEndFunction = 0u;
uint32_t g_schedulerLotrSoundSemaphore = 0u;
uint32_t g_schedulerLotrSoundResult = 0u;
void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value);
void lotrSoundEndCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)rdram;
(void)runtime;
++g_lotrSoundCallbackHits;
ctx->pc = ::getRegU32(ctx, 31);
}
void recvxSoundEndCallbackShouldNotRun(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)rdram;
(void)runtime;
++g_recvxSoundCallbackHits;
ctx->pc = ::getRegU32(ctx, 31);
}
void schedulerLotrSoundRpcCall(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
SET_GPR_U32(ctx, 4, g_schedulerLotrSoundClient);
SET_GPR_U32(ctx, 5, 0u);
SET_GPR_U32(ctx, 6, K_SIF_RPC_MODE_NOWAIT);
SET_GPR_U32(ctx, 7, g_schedulerLotrSoundSend);
SET_GPR_U32(ctx, 8, 0x2000u);
SET_GPR_U32(ctx, 9, g_schedulerLotrSoundReceive);
SET_GPR_U32(ctx, 10, 0x2000u);
SET_GPR_U32(ctx, 11, g_schedulerLotrSoundEndFunction);
writeGuestU32(rdram, ::getRegU32(ctx, 29), g_schedulerLotrSoundSemaphore);
ctx->pc = K_LOTR_SOUND_SCHEDULER_RESUME;
SifCallRpc(rdram, ctx, runtime);
}
void schedulerLotrSoundRpcResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_schedulerLotrSoundResult = ::getRegU32(ctx, 2);
ctx->pc = 0u;
runtime->requestStop();
}
void schedulerDtxRpcCall(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
SET_GPR_U32(ctx, 4, g_schedulerRpcClient);
SET_GPR_U32(ctx, 5, g_schedulerRpcNumber);
SET_GPR_U32(ctx, 6, 0u);
SET_GPR_U32(ctx, 7, g_schedulerRpcSend);
SET_GPR_U32(ctx, 8, 8u);
SET_GPR_U32(ctx, 9, g_schedulerRpcReceive);
SET_GPR_U32(ctx, 10, sizeof(uint32_t));
SET_GPR_U32(ctx, 11, 0u);
ctx->pc = K_DTX_SCHEDULER_RESUME;
SifCallRpc(rdram, ctx, runtime);
}
void schedulerDtxRpcResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
g_schedulerRpcResult = ::getRegU32(ctx, 2);
ctx->pc = 0u;
runtime->requestStop();
}
void recvxDtxDispatcher(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
(void)runtime;
++g_dtxDispatcherHits;
g_dtxDispatcherRpcNum = ::getRegU32(ctx, 4);
g_dtxDispatcherSendBuf = ::getRegU32(ctx, 5);
g_dtxDispatcherSendSize = ::getRegU32(ctx, 6);
std::memcpy(rdram + K_DTX_DISPATCH_RESULT_ADDR,
&K_DTX_DISPATCH_RESULT_MARKER,
sizeof(K_DTX_DISPATCH_RESULT_MARKER));
ctx->r[2] = _mm_set_epi64x(0, static_cast<int64_t>(K_DTX_DISPATCH_RESULT_ADDR));
ctx->pc = ::getRegU32(ctx, 31);
}
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
@@ -294,12 +170,6 @@ namespace
}
};
void writeFile(const std::filesystem::path &path, const std::vector<uint8_t> &data)
{
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char *>(data.data()), static_cast<std::streamsize>(data.size()));
}
template <typename T>
void writeGuestStruct(uint8_t *rdram, uint32_t addr, const T &value)
{
@@ -479,70 +349,6 @@ void register_ps2_sif_rpc_tests()
t.Equals(getRegS32(env.ctx, 2), 0, "SifCheckStatRpc should report not busy after synchronous completion");
});
tc.Run("Fatal Frame SDRDRV RPC initializes header and loads body archives", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "SLUS_203.88");
ScopedTempDir temp("fatal_frame_sdrdrv");
std::vector<uint8_t> header(64u, 0);
const char headerPayload[] = "img-header";
std::memcpy(header.data(), headerPayload, sizeof(headerPayload) - 1u);
writeFile(temp.path / "img_hd.bin", header);
constexpr uint32_t kSectorSize = 2048u;
std::vector<uint8_t> body(kSectorSize * 2u, 0);
const char bodyPayload[] = "archive-data";
std::memcpy(body.data() + kSectorSize, bodyPayload, sizeof(bodyPayload) - 1u);
writeFile(temp.path / "img_bd.bin", body);
const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths();
PS2Runtime::IoPaths ioPaths;
ioPaths.elfDirectory = temp.path;
ioPaths.hostRoot = temp.path;
ioPaths.cdRoot = temp.path;
ioPaths.mcRoot = temp.path / "mc0";
PS2Runtime::setIoPaths(ioPaths);
constexpr uint32_t kSendAddr = 0x00030000u;
constexpr uint32_t kRecvAddr = 0x00031000u;
constexpr uint32_t kDstAddr = 0x00032000u;
constexpr uint32_t kImgHeaderAddr = 0x012F0000u;
constexpr uint32_t kLoadId = 3u;
std::array<uint32_t, 8> commands{};
commands[0] = 0x0Eu;
commands[2] = 1u; // lbn
commands[3] = sizeof(bodyPayload) - 1u;
commands[4] = kDstAddr;
commands[6] = kLoadId;
std::memcpy(env.rdram.data() + kSendAddr, commands.data(), commands.size() * sizeof(uint32_t));
std::memset(env.rdram.data() + kRecvAddr, 0xCC, 0x180u);
const ps2x::iop::RpcResult initResult =
callIop(env, IOP_SID_FATAL_FRAME_SDRDRV, 0u,
0u, 0u, kRecvAddr, 0x180u);
t.IsTrue(initResult.handled, "Fatal Frame SDRDRV init RPC should be handled");
t.Equals(std::memcmp(env.rdram.data() + kImgHeaderAddr, "img-header", 10), 0,
"SDRDRV init RPC should load img_hd.bin into the arrangement table");
const ps2x::iop::RpcResult result =
callIop(env, IOP_SID_FATAL_FRAME_SDRDRV, 1u,
kSendAddr,
static_cast<uint32_t>(commands.size() * sizeof(uint32_t)),
kRecvAddr, 0x180u);
PS2Runtime::setIoPaths(oldPaths);
t.IsTrue(result.handled, "Fatal Frame SDRDRV SID should be handled");
t.Equals(result.resultAddress, kRecvAddr, "SDRDRV RPC should return recv buffer");
t.IsFalse(result.signalNowaitCompletion, "SDRDRV RPC should not request special nowait signaling");
t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "archive-data", 12), 0,
"SDRDRV load command should copy bytes from img_bd.bin by LBN");
t.Equals(env.rdram[kRecvAddr + 0x6Cu + (kLoadId * 8u)], static_cast<uint8_t>(0),
"SDRDRV load status should report completed");
});
tc.Run("MCSERV RPC init and get info report a formatted PS2 card", [](TestCase &t)
{
TestEnv env;
@@ -603,7 +409,7 @@ void register_ps2_sif_rpc_tests()
"get info should report formatted card");
});
tc.Run("DBCMAN version RPC returns the 3.20 compatibility response", [](TestCase &t)
tc.Run("DBCMAN version RPC returns the 3.10 compatibility response", [](TestCase &t)
{
TestEnv env;
const auto dbcmanModule = env.runtime.loadIopModule("rom0:DBCMAN");
@@ -612,7 +418,7 @@ void register_ps2_sif_rpc_tests()
constexpr uint32_t kDbcManSid = 0x80001300u;
constexpr uint32_t kCheckVersionRpc = 0x80001363u;
constexpr uint32_t kRecvAddr = 0x00035A00u;
constexpr uint32_t kDbcManVersion = 0x0320u;
constexpr uint32_t kDbcManVersion = 0x0310u;
std::memset(env.rdram.data() + kRecvAddr, 0xCC, 16u);
const ps2x::iop::RpcResult result =
@@ -626,7 +432,7 @@ void register_ps2_sif_rpc_tests()
{
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + (index * 4u)),
kDbcManVersion,
"DBCMAN should repeat version 3.20 across the response words");
"DBCMAN should repeat version 3.10 across the response words");
}
});
@@ -661,245 +467,6 @@ void register_ps2_sif_rpc_tests()
}
});
tc.Run("LotR ClFile RPC opens reads and reports EOF", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "SLUS_205.78");
ScopedTempDir temp("lotr_clfile_rpc");
const std::vector<uint8_t> payload = {'a', 'b', 'c'};
const std::vector<uint8_t> loadPayload = {'x', 'y', 'z', '!'};
writeFile(temp.path / "boot.cfg", payload);
writeFile(temp.path / "load.bin", loadPayload);
const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths();
PS2Runtime::IoPaths ioPaths;
ioPaths.elfDirectory = temp.path;
ioPaths.hostRoot = temp.path;
ioPaths.cdRoot = temp.path;
ioPaths.mcRoot = temp.path / "mc0";
PS2Runtime::setIoPaths(ioPaths);
constexpr uint32_t kSendAddr = 0x00036000u;
constexpr uint32_t kRecvAddr = 0x00037000u;
constexpr uint32_t kDstAddr = 0x00038000u;
auto callClFileRpc = [&](uint32_t rpcNum, uint32_t sendSize) {
const ps2x::iop::RpcResult result =
callIop(env, IOP_SID_LOTR_CLFILE, rpcNum,
kSendAddr, sendSize, kRecvAddr, 0x40u);
t.IsTrue(result.handled, "LotR ClFile SID should be handled");
t.Equals(result.resultAddress, kRecvAddr, "ClFile RPC should return recv buffer");
t.IsFalse(result.signalNowaitCompletion, "ClFile RPC should not request special nowait signaling");
};
std::memset(env.rdram.data() + kSendAddr, 0, 0x100u);
std::memcpy(env.rdram.data() + kSendAddr, "boot.cfg", 9u);
std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x40u);
callClFileRpc(0x08u, 0x100u);
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 0u), 0u,
"open should report success status");
const uint32_t handle = readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u);
t.IsTrue(handle != 0u, "open should return a non-zero remote file handle");
std::memset(env.rdram.data() + kSendAddr, 0, 0x100u);
std::memcpy(env.rdram.data() + kSendAddr, "missing.cfg", 12u);
std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x40u);
callClFileRpc(0x08u, 0x100u);
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u), 0u,
"missing file open should not manufacture a handle from path bytes");
constexpr uint32_t kLoadDstAddr = 0x00039000u;
std::memset(env.rdram.data() + kSendAddr, 0, 0x110u);
std::memcpy(env.rdram.data() + kSendAddr, "load.bin", 9u);
writeGuestU32(env.rdram.data(), kSendAddr + 0x100u, static_cast<uint32_t>(loadPayload.size()));
writeGuestU32(env.rdram.data(), kSendAddr + 0x104u, kLoadDstAddr);
std::memset(env.rdram.data() + kLoadDstAddr, 0xCC, 0x20u);
callClFileRpc(0x01u, 0x110u);
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 0u), 5u,
"direct load should report a queued load result");
const uint32_t loadHandle = readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u);
t.IsTrue(loadHandle >= 3u, "direct load should return a status handle usable by getStatus");
t.Equals(std::memcmp(env.rdram.data() + kLoadDstAddr, loadPayload.data(), loadPayload.size()), 0,
"direct load should copy file bytes into the requested guest destination");
writeGuestU32(env.rdram.data(), kSendAddr, loadHandle);
callClFileRpc(0x05u, 0u);
writeGuestU32(env.rdram.data(), kSendAddr, loadHandle);
callClFileRpc(0x03u, sizeof(uint32_t));
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 0u), 7u,
"direct load getStatus should report completed");
writeGuestU32(env.rdram.data(), kSendAddr, loadHandle);
callClFileRpc(0x06u, sizeof(uint32_t));
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u),
static_cast<uint32_t>(loadPayload.size()),
"direct load getSize should report the loaded host file size");
writeGuestU32(env.rdram.data(), kSendAddr, loadHandle);
callClFileRpc(0x09u, sizeof(uint32_t));
if (handle != 0u)
{
std::array<uint32_t, 4> readPacket = {handle, 2u, kDstAddr, 0u};
writeGuestStruct(env.rdram.data(), kSendAddr, readPacket);
std::memset(env.rdram.data() + kDstAddr, 0, 8u);
callClFileRpc(0x0Au, static_cast<uint32_t>(readPacket.size() * sizeof(uint32_t)));
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u), 2u,
"first read should report actual byte count");
t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "ab", 2), 0,
"first read should copy file bytes into guest destination");
writeGuestStruct(env.rdram.data(), kSendAddr, readPacket);
std::memset(env.rdram.data() + kDstAddr, 0, 8u);
callClFileRpc(0x0Au, static_cast<uint32_t>(readPacket.size() * sizeof(uint32_t)));
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u), 1u,
"short read should report remaining byte count");
t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "c", 1), 0,
"short read should copy remaining file byte");
writeGuestStruct(env.rdram.data(), kSendAddr, readPacket);
std::memset(env.rdram.data() + kDstAddr, 0xCC, 8u);
callClFileRpc(0x0Au, static_cast<uint32_t>(readPacket.size() * sizeof(uint32_t)));
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u), 0u,
"EOF read should report zero bytes");
writeGuestU32(env.rdram.data(), kSendAddr, handle);
callClFileRpc(0x09u, sizeof(uint32_t));
}
PS2Runtime::setIoPaths(oldPaths);
});
tc.Run("LotR sound RPC invokes guest callback to consume HLE response", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "SLUS_205.78");
constexpr uint32_t kClientAddr = 0x00039000u;
constexpr uint32_t kSendAddr = 0x0003A000u;
constexpr uint32_t kRecvAddr = 0x0003C000u;
constexpr uint32_t kEndFunc = 0x001FFD70u;
env.runtime.registerFunction(kEndFunc, lotrSoundEndCallback);
g_lotrSoundCallbackHits = 0u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_SID_LOTR_SOUND);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for LotR sound SID");
writeGuestU32(env.rdram.data(), kSendAddr, 3u);
std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x2000u);
env.runtime.registerFunction(K_LOTR_SOUND_SCHEDULER_CALL, schedulerLotrSoundRpcCall);
env.runtime.registerFunction(K_LOTR_SOUND_SCHEDULER_RESUME, schedulerLotrSoundRpcResume);
g_schedulerLotrSoundClient = kClientAddr;
g_schedulerLotrSoundSend = kSendAddr;
g_schedulerLotrSoundReceive = kRecvAddr;
g_schedulerLotrSoundEndFunction = kEndFunc;
g_schedulerLotrSoundResult = static_cast<uint32_t>(-1);
R5900Context mainContext{};
mainContext.pc = K_LOTR_SOUND_SCHEDULER_CALL;
setRegU32(mainContext, 29, K_STACK_ADDR);
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
const int32_t semaId = env.runtime.eeScheduler().createSemaphore(0, 1, 0u, 0u);
t.IsTrue(semaId > 0, "scheduler should create a positive semaphore id");
g_schedulerLotrSoundSemaphore = static_cast<uint32_t>(semaId);
env.runtime.eeScheduler().run();
t.Equals(g_schedulerLotrSoundResult, static_cast<uint32_t>(KE_OK),
"SifCallRpc should resume with KE_OK for LotR sound RPC");
t.Equals(g_lotrSoundCallbackHits.load(), 1u,
"LotR SOUND_JP callback should consume the HLE response");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 0u), 0u,
"LotR sound response should report no active stream records");
t.IsTrue(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u) != 0u,
"LotR sound response should advance the IOP update counter");
t.Equals(env.runtime.eeScheduler().pollSemaphore(semaId), semaId,
"LotR sound callback completion should signal the sema");
});
tc.Run("RECVX sound callbacks complete in HLE and only clear busy on designated callbacks", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0003D000u;
constexpr uint32_t kSemaParamAddr = 0x0003D100u;
constexpr uint32_t kCompletionOnlyCallback = 0x002EAC20u;
constexpr uint32_t kClearBusyCallback = 0x002EAC30u;
constexpr uint32_t kBusyFlagAddr = 0x01E212C8u;
env.runtime.registerFunction(kCompletionOnlyCallback, recvxSoundEndCallbackShouldNotRun);
env.runtime.registerFunction(kClearBusyCallback, recvxSoundEndCallbackShouldNotRun);
g_recvxSoundCallbackHits = 0u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t semaParam[6] = {0u, 1u, 0u, 0u, 0u, 0u};
std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam));
setRegU32(env.ctx, 4, kSemaParamAddr);
CreateSema(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t semaId = getRegS32(env.ctx, 2);
t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_SID_SNDDRV_COMMAND);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for RECVX snddrv command SID");
auto callSoundDriver = [&](uint32_t endFunc) {
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_SUBMIT);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, 0u);
setRegU32(env.ctx, 10, 0u);
setRegU32(env.ctx, 11, endFunc);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast<uint32_t>(semaId));
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "RECVX snddrv submission should complete");
};
constexpr uint32_t kBusyBeforeCompletion = 0x11111111u;
writeGuestU32(env.rdram.data(), kBusyFlagAddr, kBusyBeforeCompletion);
callSoundDriver(kCompletionOnlyCallback);
t.Equals(g_recvxSoundCallbackHits.load(), 0u,
"recognized RECVX completion callback should not execute guest code");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kBusyFlagAddr), kBusyBeforeCompletion,
"completion-only callback should preserve the RECVX busy flag");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), semaId, "completion-only callback should signal its semaphore");
writeGuestU32(env.rdram.data(), kBusyFlagAddr, 0x22222222u);
callSoundDriver(kClearBusyCallback);
t.Equals(g_recvxSoundCallbackHits.load(), 0u,
"recognized RECVX clear-busy callback should not execute guest code");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kBusyFlagAddr), 0u,
"designated RECVX callback should clear the guest busy flag");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), semaId, "clear-busy callback should signal its semaphore");
setRegU32(env.ctx, 4, kClientAddr);
SifCheckStatRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "RECVX snddrv client should no longer be RPC-busy");
});
tc.Run("hybrid bind before register waits then remaps", [](TestCase &t)
{
TestEnv env;
@@ -983,172 +550,6 @@ void register_ps2_sif_rpc_tests()
t.Equals(getRegU32Result(env.ctx, 2), 0u, "removing the same queue twice should return 0");
});
tc.Run("snddrv state RPC returns stable buffers and signals sema", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x00028000u;
constexpr uint32_t kSemaParamAddr = 0x00028100u;
constexpr uint32_t kRecvAddr = 0x00028200u;
constexpr uint32_t kSid = IOP_SID_SNDDRV_STATE;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t semaParam[6] = {
0u, // count (unused by runtime decode)
1u, // max_count
0u, // init_count
0u, // wait_threads
0u, // attr
0u // option
};
std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam));
setRegU32(env.ctx, 4, kSemaParamAddr);
CreateSema(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t semaId = getRegS32(env.ctx, 2);
t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for snddrv state sid");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_SEMA_ZERO, "semaphore should start at zero before nowait rpc");
std::memset(env.rdram.data() + kRecvAddr, 0, 16u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast<uint32_t>(semaId));
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc(sound status) should succeed");
const uint32_t statusAddr = readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr);
t.IsTrue(statusAddr != 0u, "rpc 0x12 should return a nonzero sound-status pointer");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), semaId, "nowait rpc should signal completion sema");
std::memset(env.rdram.data() + kRecvAddr, 0, 16u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_ADDR_TABLE);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast<uint32_t>(semaId));
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc(sound addr table) should succeed");
const uint32_t addrTableAddr = readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr);
t.IsTrue(addrTableAddr != 0u, "rpc 0x13 should return a nonzero address-table pointer");
t.IsTrue(addrTableAddr != statusAddr, "sound-status and address-table pointers should be distinct");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), semaId, "each nowait rpc should signal completion sema");
std::memset(env.rdram.data() + kRecvAddr, 0, 16u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast<uint32_t>(semaId));
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr), statusAddr,
"sound-status pointer should remain stable across repeated rpc 0x12 calls");
});
tc.Run("snddrv state RPC returns low guest sound-driver addresses", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x00028300u;
constexpr uint32_t kSemaParamAddr = 0x00028400u;
constexpr uint32_t kRecvAddr = 0x00028500u;
constexpr uint32_t kSid = IOP_SID_SNDDRV_STATE;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t semaParam[6] = {0u, 1u, 0u, 0u, 0u, 0u};
std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam));
setRegU32(env.ctx, 4, kSemaParamAddr);
CreateSema(env.rdram.data(), &env.ctx, &env.runtime);
const int32_t semaId = getRegS32(env.ctx, 2);
t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for snddrv state sid");
SifRpcClientData client = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
client.hdr.sema_id = semaId;
writeGuestStruct(env.rdram.data(), kClientAddr, client);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast<uint32_t>(semaId));
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t statusAddr = readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr);
t.IsTrue(statusAddr > 0u && statusAddr < 0x00200000u,
"rpc 0x12 should return a low guest address like an IOP pointer");
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_ADDR_TABLE);
setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT);
setRegU32(env.ctx, 7, 0u);
setRegU32(env.ctx, 8, 0u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, 16u);
setRegU32(env.ctx, 11, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
const uint32_t addrTableAddr = readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr);
t.IsTrue(addrTableAddr > 0u && addrTableAddr < 0x00200000u,
"rpc 0x13 should return a low guest address like an IOP pointer");
std::array<uint32_t, 3> addressTable{};
t.IsTrue(env.runtime.readIopMemory(addrTableAddr,
addressTable.data(),
sizeof(addressTable)),
"the sound-driver address table should live in physical IOP RAM");
const uint32_t hdBaseAddr = addressTable[0];
const uint32_t sqBaseAddr = addressTable[1];
const uint32_t dataBaseAddr = addressTable[2];
t.IsTrue(hdBaseAddr > 0u && hdBaseAddr < 0x00200000u,
"sound-driver hd base should stay in low guest address space");
t.IsTrue(sqBaseAddr > hdBaseAddr && sqBaseAddr < 0x00200000u,
"sound-driver sq base should be a later low guest address");
t.IsTrue(dataBaseAddr > sqBaseAddr && dataBaseAddr < 0x00200000u,
"sound-driver data base should be a later low guest address");
});
tc.Run("SifCallRpc falls back to stack ABI when register pack is implausible", [](TestCase &t)
{
TestEnv env;
@@ -1225,152 +626,5 @@ void register_ps2_sif_rpc_tests()
"recv payload should match stack-selected transfer size");
});
tc.Run("DTX URPC uses the guest dispatcher only when its function-table slot is registered", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0002C000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kSendAddr = 0x0002C100u;
constexpr uint32_t kRecvAddr = 0x0002C200u;
constexpr uint32_t kUrpcCommand = 7u;
constexpr uint32_t kRpcNum = 0x400u | kUrpcCommand;
constexpr uint32_t kFnTableSlot = 0x0033FED0u + (kUrpcCommand * sizeof(uint32_t));
constexpr uint32_t kObjTableSlot = 0x0033FFD0u + (kUrpcCommand * sizeof(uint32_t));
constexpr uint32_t kWrongFnTableSlot = 0x0034FED0u + (kUrpcCommand * sizeof(uint32_t));
constexpr uint32_t kWrongObjTableSlot = 0x0034FFD0u + (kUrpcCommand * sizeof(uint32_t));
constexpr uint32_t kDispatcherAddr = 0x002FABC0u;
constexpr uint32_t kRegisteredHandlerAddr = 0x002FADE0u;
constexpr uint32_t kRegisteredObjectAddr = 0x01F18100u;
constexpr uint32_t kFallbackValue = 0x13579BDFu;
g_dtxDispatcherHits = 0u;
g_dtxDispatcherRpcNum = 0u;
g_dtxDispatcherSendBuf = 0u;
g_dtxDispatcherSendSize = 0u;
env.runtime.registerFunction(kDispatcherAddr, recvxDtxDispatcher);
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for DTX dispatcher test");
writeGuestU32(env.rdram.data(), kSendAddr + 0u, kFallbackValue);
writeGuestU32(env.rdram.data(), kSendAddr + 4u, 0x2468ACE0u);
auto callUrpc = [&]() {
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kRpcNum);
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
setRegU32(env.ctx, 8, 8u);
setRegU32(env.ctx, 9, kRecvAddr);
setRegU32(env.ctx, 10, sizeof(uint32_t));
setRegU32(env.ctx, 11, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX URPC should complete");
};
writeGuestU32(env.rdram.data(), kFnTableSlot, 0u);
writeGuestU32(env.rdram.data(), kObjTableSlot, kRegisteredObjectAddr);
writeGuestU32(env.rdram.data(), kWrongFnTableSlot, kRegisteredHandlerAddr);
writeGuestU32(env.rdram.data(), kWrongObjTableSlot, kRegisteredObjectAddr);
writeGuestU32(env.rdram.data(), kRecvAddr, 0u);
callUrpc();
t.Equals(g_dtxDispatcherHits.load(), 0u,
"empty DTX function-table slot should use fallback emulation");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr), kFallbackValue,
"fallback DTX emulation should return the first send word for an unknown command");
writeGuestU32(env.rdram.data(), kFnTableSlot, kRegisteredHandlerAddr);
writeGuestU32(env.rdram.data(), kRecvAddr, 0u);
env.runtime.registerFunction(K_DTX_SCHEDULER_CALL, schedulerDtxRpcCall);
env.runtime.registerFunction(K_DTX_SCHEDULER_RESUME, schedulerDtxRpcResume);
g_schedulerRpcClient = kClientAddr;
g_schedulerRpcNumber = kRpcNum;
g_schedulerRpcSend = kSendAddr;
g_schedulerRpcReceive = kRecvAddr;
g_schedulerRpcResult = static_cast<uint32_t>(-1);
R5900Context mainContext{};
mainContext.pc = K_DTX_SCHEDULER_CALL;
setRegU32(mainContext, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u);
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
env.runtime.eeScheduler().run();
t.Equals(g_schedulerRpcResult, static_cast<uint32_t>(KE_OK),
"DTX URPC should resume its base context with KE_OK");
t.Equals(g_dtxDispatcherHits.load(), 1u,
"registered DTX function-table slot should enter the guest dispatcher");
t.Equals(g_dtxDispatcherRpcNum.load(), kRpcNum,
"DTX dispatcher should receive the full URPC number");
t.Equals(g_dtxDispatcherSendBuf.load(), kSendAddr,
"DTX dispatcher should receive the send-buffer address");
t.Equals(g_dtxDispatcherSendSize.load(), 8u,
"DTX dispatcher should receive the send-buffer size");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr), K_DTX_DISPATCH_RESULT_MARKER,
"SifCallRpc should copy the dispatcher result into the receive buffer");
});
tc.Run("SifCallRpc prefers stack ABI for DTX URPC when both packs look plausible", [](TestCase &t)
{
TestEnv env;
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0002B000u;
constexpr uint32_t kDtxSid = 0x7D000000u;
constexpr uint32_t kSendAddr = 0x0002B100u;
constexpr uint32_t kRecvStackAddr = 0x0002B200u;
constexpr uint32_t kRecvRegAddr = 0x0002B300u;
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, kDtxSid);
setRegU32(env.ctx, 6, 0u);
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for DTX sid");
writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); // mode
writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0x1E21440u); // wk addr
writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 0x100u); // wk size
writeGuestU32(env.rdram.data(), kRecvStackAddr, 0u);
writeGuestU32(env.rdram.data(), kRecvRegAddr, 0u);
setRegU32(env.ctx, 29, K_STACK_ADDR);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, 12u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, kRecvStackAddr);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, 4u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x1Cu, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x20u, 0u);
writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u);
setRegU32(env.ctx, 4, kClientAddr);
setRegU32(env.ctx, 5, 0x422u); // DTX URPC command 34 (SJUNI create)
setRegU32(env.ctx, 6, 0u);
setRegU32(env.ctx, 7, kSendAddr);
// Plausible but intentionally wrong register-side packed args.
setRegU32(env.ctx, 8, 4u);
setRegU32(env.ctx, 9, kRecvRegAddr);
setRegU32(env.ctx, 10, 12u);
setRegU32(env.ctx, 11, 0u);
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed for DTX URPC");
const uint32_t stackHandle = readGuestStruct<uint32_t>(env.rdram.data(), kRecvStackAddr);
const uint32_t regHandle = readGuestStruct<uint32_t>(env.rdram.data(), kRecvRegAddr);
t.IsTrue(stackHandle != 0u, "DTX handle should be written to stack-selected recv buffer");
t.Equals(regHandle, 0u, "register recv buffer should remain untouched when stack ABI is preferred");
});
});
}