mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
feat: remove LLE IOPs
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,635 +0,0 @@
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class ClFileService final : public IopService
|
||||
{
|
||||
public:
|
||||
ClFileService(IopHost &host, ClFileBindings bindings)
|
||||
: m_host(host),
|
||||
m_bindings(std::move(bindings)),
|
||||
m_sids{m_bindings.sid},
|
||||
m_nextLoadHandle(m_bindings.rpc.firstLoadHandle)
|
||||
{
|
||||
}
|
||||
|
||||
~ClFileService() override
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override
|
||||
{
|
||||
return m_bindings.serviceName;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto &[handle, entry] : m_fileHandles)
|
||||
{
|
||||
(void)handle;
|
||||
if (entry.handle != 0u)
|
||||
{
|
||||
m_host.closeHostFile(entry.handle);
|
||||
entry.handle = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
m_fileHandles.clear();
|
||||
m_loads.clear();
|
||||
m_root.clear();
|
||||
m_nextFileHandle = 1u;
|
||||
m_nextLoadHandle = m_bindings.rpc.firstLoadHandle;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult result;
|
||||
if (request.sid != m_bindings.sid)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
const Operation operation = decodeFunction(request.function);
|
||||
if (operation == Operation::Unknown &&
|
||||
!m_bindings.rpc.acknowledgeUnknownFunctions)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
if (request.receive.address != 0u && request.receive.size != 0u)
|
||||
{
|
||||
(void)m_host.zeroGuest(request.receive.address,
|
||||
std::min(request.receive.size,
|
||||
m_bindings.rpc.responseClearBytes));
|
||||
}
|
||||
|
||||
const auto writeRpcResult = [&](int32_t status, uint32_t value)
|
||||
{
|
||||
writeResult(request.receive, status, value);
|
||||
};
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case Operation::DirectLoad:
|
||||
{
|
||||
const uint32_t stringBytes = request.send.size != 0u
|
||||
? std::min(request.send.size,
|
||||
m_bindings.rpc.pathBytes)
|
||||
: m_bindings.rpc.pathBytes;
|
||||
const std::string guestPath = readGuestString(request.send.address, stringBytes);
|
||||
uint32_t requestedBytes = 0u;
|
||||
uint32_t destinationAddress = 0u;
|
||||
(void)readGuestU32(request.send.address + m_bindings.rpc.directLoadSizeOffset,
|
||||
requestedBytes);
|
||||
(void)readGuestU32(request.send.address + m_bindings.rpc.directLoadDestinationOffset,
|
||||
destinationAddress);
|
||||
|
||||
uint32_t status = m_bindings.rpc.loadStatusFailed;
|
||||
uint32_t fileSize = 0u;
|
||||
const std::string hostPath = resolvePath(guestPath);
|
||||
if (!hostPath.empty())
|
||||
{
|
||||
const uint64_t file = m_host.openHostFile(hostPath);
|
||||
uint64_t hostFileSize = 0u;
|
||||
if (file != 0u && m_host.hostFileSize(file, hostFileSize))
|
||||
{
|
||||
fileSize = static_cast<uint32_t>(
|
||||
std::min<uint64_t>(hostFileSize, 0xFFFFFFFFull));
|
||||
const uint64_t maxRequestedBytes = requestedBytes != 0u
|
||||
? requestedBytes
|
||||
: hostFileSize;
|
||||
const uint64_t bytesToCopy = std::min(hostFileSize, maxRequestedBytes);
|
||||
|
||||
status = m_bindings.rpc.loadStatusComplete;
|
||||
if (destinationAddress != 0u && bytesToCopy != 0u)
|
||||
{
|
||||
if (!copyFileToGuest(file, destinationAddress, bytesToCopy))
|
||||
{
|
||||
status = m_bindings.rpc.loadStatusFailed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file != 0u)
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t loadHandle = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
loadHandle = allocateLoadLocked(status, fileSize);
|
||||
}
|
||||
writeRpcResult(static_cast<int32_t>(m_bindings.rpc.loadResultQueued), loadHandle);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Initialize:
|
||||
writeRpcResult(0, 1u);
|
||||
return result;
|
||||
|
||||
case Operation::Wait:
|
||||
case Operation::SecondaryWait:
|
||||
writeRpcResult(0, 0u);
|
||||
return result;
|
||||
|
||||
case Operation::SetRoot:
|
||||
{
|
||||
const std::string root = readGuestString(request.send.address,
|
||||
request.send.size != 0u
|
||||
? request.send.size
|
||||
: m_bindings.rpc.pathBytes);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_root = root;
|
||||
}
|
||||
writeRpcResult(0, 1u);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Open:
|
||||
{
|
||||
const std::string guestPath = readGuestString(request.send.address,
|
||||
request.send.size != 0u
|
||||
? request.send.size
|
||||
: m_bindings.rpc.pathBytes);
|
||||
const std::string hostPath = resolvePath(guestPath);
|
||||
if (hostPath.empty())
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
const uint64_t file = m_host.openHostFile(hostPath);
|
||||
if (file == 0u)
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t hostFileSize = 0u;
|
||||
if (!m_host.hostFileSize(file, hostFileSize))
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
const uint32_t fileSize = static_cast<uint32_t>(
|
||||
std::min<uint64_t>(hostFileSize, 0x7FFFFFFFull));
|
||||
|
||||
uint32_t handle = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
handle = allocateFileHandleLocked(file, fileSize);
|
||||
}
|
||||
if (handle == 0u)
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
writeRpcResult(0, handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Close:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
(void)readGuestU32(request.send.address, handle);
|
||||
|
||||
uint64_t file = 0u;
|
||||
bool closedLoad = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto fileIt = m_fileHandles.find(handle);
|
||||
if (fileIt != m_fileHandles.end())
|
||||
{
|
||||
file = fileIt->second.handle;
|
||||
m_fileHandles.erase(fileIt);
|
||||
}
|
||||
|
||||
const auto loadIt = m_loads.find(handle);
|
||||
if (loadIt != m_loads.end())
|
||||
{
|
||||
m_loads.erase(loadIt);
|
||||
closedLoad = true;
|
||||
}
|
||||
}
|
||||
if (file != 0u)
|
||||
{
|
||||
m_host.closeHostFile(file);
|
||||
}
|
||||
|
||||
const bool closed = file != 0u || closedLoad;
|
||||
writeRpcResult(closed ? 0 : -1, closed ? 1u : 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Read:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
uint32_t requestedBytes = 0u;
|
||||
uint32_t destinationAddress = 0u;
|
||||
(void)readGuestU32(request.send.address + 0u, handle);
|
||||
(void)readGuestU32(request.send.address + 4u, requestedBytes);
|
||||
(void)readGuestU32(request.send.address + 8u, destinationAddress);
|
||||
|
||||
if (destinationAddress == 0u)
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> bytes(std::min(requestedBytes,
|
||||
m_bindings.rpc.maximumReadBytes));
|
||||
size_t bytesRead = 0u;
|
||||
bool readFailed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto fileIt = m_fileHandles.find(handle);
|
||||
if (fileIt == m_fileHandles.end() || fileIt->second.handle == 0u)
|
||||
{
|
||||
readFailed = true;
|
||||
}
|
||||
else if (!bytes.empty())
|
||||
{
|
||||
size_t hostBytesRead = 0u;
|
||||
if (!m_host.readHostFile(fileIt->second.handle,
|
||||
fileIt->second.position,
|
||||
bytes.data(),
|
||||
bytes.size(),
|
||||
hostBytesRead))
|
||||
{
|
||||
readFailed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesRead = hostBytesRead;
|
||||
fileIt->second.position += hostBytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (readFailed ||
|
||||
(bytesRead != 0u &&
|
||||
!m_host.writeGuest(destinationAddress, bytes.data(), bytesRead)))
|
||||
{
|
||||
writeRpcResult(-1, 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
writeRpcResult(0, static_cast<uint32_t>(bytesRead));
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::GetStatus:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
(void)readGuestU32(request.send.address, handle);
|
||||
|
||||
bool loadFound = false;
|
||||
uint32_t loadStatus = 0u;
|
||||
bool fileFound = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto loadIt = m_loads.find(handle);
|
||||
if (loadIt != m_loads.end())
|
||||
{
|
||||
loadFound = true;
|
||||
loadStatus = loadIt->second.status;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileFound = m_fileHandles.find(handle) != m_fileHandles.end();
|
||||
}
|
||||
}
|
||||
|
||||
if (loadFound)
|
||||
{
|
||||
writeRpcResult(static_cast<int32_t>(loadStatus), 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
writeRpcResult(0, fileFound ? 0u : m_bindings.rpc.invalidHandleStatus);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::GetSize:
|
||||
{
|
||||
uint32_t handle = 0u;
|
||||
(void)readGuestU32(request.send.address, handle);
|
||||
|
||||
bool found = false;
|
||||
uint32_t size = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto loadIt = m_loads.find(handle);
|
||||
if (loadIt != m_loads.end())
|
||||
{
|
||||
found = true;
|
||||
size = loadIt->second.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto fileIt = m_fileHandles.find(handle);
|
||||
if (fileIt != m_fileHandles.end())
|
||||
{
|
||||
found = true;
|
||||
size = fileIt->second.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeRpcResult(found ? 0 : -1, found ? size : 0u);
|
||||
return result;
|
||||
}
|
||||
|
||||
case Operation::Unknown:
|
||||
writeRpcResult(0, 0u);
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"open_files", m_fileHandles.size(), false});
|
||||
metrics.push_back({"load_records", m_loads.size(), false});
|
||||
metrics.push_back({"next_file_handle", m_nextFileHandle, true});
|
||||
metrics.push_back({"next_load_handle", m_nextLoadHandle, true});
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Operation
|
||||
{
|
||||
DirectLoad,
|
||||
GetStatus,
|
||||
Initialize,
|
||||
Wait,
|
||||
GetSize,
|
||||
Open,
|
||||
Close,
|
||||
Read,
|
||||
SecondaryWait,
|
||||
SetRoot,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
[[nodiscard]] Operation decodeFunction(uint32_t function) const
|
||||
{
|
||||
const ClFileRpcLayout &rpc = m_bindings.rpc;
|
||||
if (function == rpc.directLoadFunction) return Operation::DirectLoad;
|
||||
if (function == rpc.getStatusFunction) return Operation::GetStatus;
|
||||
if (function == rpc.initializeFunction) return Operation::Initialize;
|
||||
if (function == rpc.waitFunction) return Operation::Wait;
|
||||
if (function == rpc.getSizeFunction) return Operation::GetSize;
|
||||
if (function == rpc.openFunction) return Operation::Open;
|
||||
if (function == rpc.closeFunction) return Operation::Close;
|
||||
if (function == rpc.readFunction) return Operation::Read;
|
||||
if (function == rpc.secondaryWaitFunction) return Operation::SecondaryWait;
|
||||
if (function == rpc.setRootFunction) return Operation::SetRoot;
|
||||
return Operation::Unknown;
|
||||
}
|
||||
|
||||
struct ClFileHandle
|
||||
{
|
||||
uint64_t handle = 0u;
|
||||
uint32_t size = 0u;
|
||||
uint64_t position = 0u;
|
||||
};
|
||||
|
||||
struct ClFileLoad
|
||||
{
|
||||
uint32_t status = 0u;
|
||||
uint32_t size = 0u;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool readGuestU32(uint32_t address, uint32_t &value) const
|
||||
{
|
||||
value = 0u;
|
||||
return m_host.readGuest(address, &value, sizeof(value));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string readGuestString(uint32_t address, uint32_t maxBytes) const
|
||||
{
|
||||
if (address == 0u || maxBytes == 0u)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<char> bytes(maxBytes);
|
||||
if (!m_host.readGuest(address, bytes.data(), bytes.size()))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t length = 0u;
|
||||
while (length < bytes.size() && bytes[length] != '\0')
|
||||
{
|
||||
++length;
|
||||
}
|
||||
return std::string(bytes.data(), length);
|
||||
}
|
||||
|
||||
[[nodiscard]] static bool hasDevice(std::string_view path)
|
||||
{
|
||||
return path.find(':') != std::string_view::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::string joinGuestPath(const std::string &root,
|
||||
const std::string &leaf)
|
||||
{
|
||||
if (root.empty() || leaf.empty() || hasDevice(leaf))
|
||||
{
|
||||
return leaf;
|
||||
}
|
||||
|
||||
const char tail = root.back();
|
||||
if (tail == '/' || tail == '\\' || tail == ':')
|
||||
{
|
||||
return root + leaf;
|
||||
}
|
||||
return root + "/" + leaf;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string resolvePath(const std::string &path) const
|
||||
{
|
||||
std::string root;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
root = m_root;
|
||||
}
|
||||
|
||||
const std::string translated = m_host.translateGuestPath(joinGuestPath(root, path));
|
||||
return translated;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t allocateFileHandleLocked(uint64_t file, uint32_t size)
|
||||
{
|
||||
if (file == 0u)
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
|
||||
for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt)
|
||||
{
|
||||
uint32_t handle = m_nextFileHandle++;
|
||||
if (handle == 0u)
|
||||
{
|
||||
handle = m_nextFileHandle++;
|
||||
}
|
||||
if (m_fileHandles.find(handle) == m_fileHandles.end() &&
|
||||
m_loads.find(handle) == m_loads.end())
|
||||
{
|
||||
m_fileHandles.emplace(handle, ClFileHandle{file, size, 0u});
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] uint32_t allocateLoadLocked(uint32_t status, uint32_t size)
|
||||
{
|
||||
for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt)
|
||||
{
|
||||
uint32_t handle = m_nextLoadHandle++;
|
||||
if (handle < 3u)
|
||||
{
|
||||
handle = m_bindings.rpc.firstLoadHandle;
|
||||
m_nextLoadHandle = m_bindings.rpc.firstLoadHandle + 1u;
|
||||
}
|
||||
if (m_loads.find(handle) == m_loads.end() &&
|
||||
m_fileHandles.find(handle) == m_fileHandles.end())
|
||||
{
|
||||
m_loads.emplace(handle, ClFileLoad{status, size});
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
void writeResult(GuestBuffer receive, int32_t status, uint32_t value)
|
||||
{
|
||||
if (receive.address != 0u &&
|
||||
receive.size >= m_bindings.rpc.responseStatusOffset + sizeof(uint32_t))
|
||||
{
|
||||
const uint32_t encodedStatus = static_cast<uint32_t>(status);
|
||||
(void)m_host.writeGuest(receive.address + m_bindings.rpc.responseStatusOffset,
|
||||
&encodedStatus,
|
||||
sizeof(encodedStatus));
|
||||
}
|
||||
if (receive.address != 0u &&
|
||||
receive.size >= m_bindings.rpc.responseValueOffset + sizeof(uint32_t))
|
||||
{
|
||||
(void)m_host.writeGuest(receive.address + m_bindings.rpc.responseValueOffset,
|
||||
&value,
|
||||
sizeof(value));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool copyFileToGuest(uint64_t file,
|
||||
uint32_t destinationAddress,
|
||||
uint64_t bytesToCopy)
|
||||
{
|
||||
constexpr size_t kChunkBytes = 16u * 1024u;
|
||||
if (bytesToCopy > 0xFFFFFFFFull - static_cast<uint64_t>(destinationAddress) + 1ull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> chunk(kChunkBytes);
|
||||
uint64_t copied = 0u;
|
||||
while (copied < bytesToCopy)
|
||||
{
|
||||
const size_t wanted = static_cast<size_t>(
|
||||
std::min<uint64_t>(chunk.size(), bytesToCopy - copied));
|
||||
size_t received = 0u;
|
||||
if (!m_host.readHostFile(file,
|
||||
copied,
|
||||
chunk.data(),
|
||||
wanted,
|
||||
received) ||
|
||||
received != wanted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t chunkAddress = destinationAddress + static_cast<uint32_t>(copied);
|
||||
if (!m_host.writeGuest(chunkAddress, chunk.data(), received))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
copied += received;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
ClFileBindings m_bindings;
|
||||
std::array<uint32_t, 1> m_sids;
|
||||
mutable std::mutex m_mutex;
|
||||
std::unordered_map<uint32_t, ClFileHandle> m_fileHandles;
|
||||
std::unordered_map<uint32_t, ClFileLoad> m_loads;
|
||||
uint32_t m_nextFileHandle = 1u;
|
||||
uint32_t m_nextLoadHandle = 0u;
|
||||
std::string m_root;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createClFileService(IopHost &host,
|
||||
ClFileBindings bindings)
|
||||
{
|
||||
const ClFileRpcLayout &rpc = bindings.rpc;
|
||||
const std::array<uint32_t, 10> functions = {
|
||||
rpc.directLoadFunction,
|
||||
rpc.getStatusFunction,
|
||||
rpc.initializeFunction,
|
||||
rpc.waitFunction,
|
||||
rpc.getSizeFunction,
|
||||
rpc.openFunction,
|
||||
rpc.closeFunction,
|
||||
rpc.readFunction,
|
||||
rpc.secondaryWaitFunction,
|
||||
rpc.setRootFunction,
|
||||
};
|
||||
std::unordered_set<uint32_t> uniqueFunctions;
|
||||
for (const uint32_t function : functions)
|
||||
{
|
||||
if (!uniqueFunctions.emplace(function).second)
|
||||
{
|
||||
throw std::invalid_argument("duplicate CLFILE RPC function binding");
|
||||
}
|
||||
}
|
||||
if (bindings.serviceName.empty() || bindings.sid == 0u ||
|
||||
rpc.pathBytes == 0u || rpc.maximumReadBytes == 0u ||
|
||||
rpc.firstLoadHandle < 3u)
|
||||
{
|
||||
throw std::invalid_argument("invalid CLFILE bindings");
|
||||
}
|
||||
return std::make_unique<ClFileService>(host, std::move(bindings));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,335 +0,0 @@
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kEeRamSize = 32u * 1024u * 1024u;
|
||||
|
||||
class SdrdrvService final : public IopService
|
||||
{
|
||||
public:
|
||||
SdrdrvService(IopHost &host, SdrdrvBindings bindings)
|
||||
: m_host(host), m_bindings(std::move(bindings)), m_sids{m_bindings.sid}
|
||||
{
|
||||
}
|
||||
|
||||
std::string_view name() const override { return m_bindings.serviceName; }
|
||||
std::span<const uint32_t> sids() const override { return m_sids; }
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_headerWarnCount = 0;
|
||||
m_bodyWarnCount = 0;
|
||||
}
|
||||
|
||||
RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult result;
|
||||
if (request.sid != m_bindings.sid)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
if (m_bindings.clearReceiveBeforeDispatch &&
|
||||
request.receive.address && request.receive.size)
|
||||
{
|
||||
(void)m_host.zeroGuest(request.receive.address, request.receive.size);
|
||||
}
|
||||
|
||||
if (request.function == m_bindings.initFunction)
|
||||
{
|
||||
if (!loadImageHeader())
|
||||
{
|
||||
warnHeader();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (request.function == m_bindings.shutdownFunction)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
if (request.function != m_bindings.submitFunction)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const uint32_t count = std::min(request.send.size / m_bindings.commandBytes,
|
||||
m_bindings.maxCommands);
|
||||
for (uint32_t commandIndex = 0; commandIndex < count; ++commandIndex)
|
||||
{
|
||||
std::vector<uint32_t> words(m_bindings.commandBytes / sizeof(uint32_t));
|
||||
const uint32_t commandAddress = request.send.address +
|
||||
commandIndex * m_bindings.commandBytes;
|
||||
if (!m_host.readGuest(commandAddress,
|
||||
words.data(),
|
||||
m_bindings.commandBytes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (words[0] == m_bindings.headerCommand)
|
||||
{
|
||||
if (!loadImageHeader())
|
||||
{
|
||||
warnHeader();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (words[0] != m_bindings.loadCommand)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t lbn = words[m_bindings.lbnWord];
|
||||
const uint32_t byteCount = words[m_bindings.byteCountWord];
|
||||
const uint32_t destination = words[m_bindings.destinationWord];
|
||||
const bool eeLoad = words[m_bindings.destinationKindWord] ==
|
||||
m_bindings.eeDestinationKind;
|
||||
const uint32_t loadId = words[m_bindings.loadIdWord];
|
||||
const bool loaded = eeLoad
|
||||
? readBody(lbn, byteCount, destination)
|
||||
: m_bindings.pretendNonEeLoadsComplete;
|
||||
if (eeLoad && !loaded)
|
||||
{
|
||||
(void)m_host.zeroGuest(destination, byteCount);
|
||||
bool shouldWarn = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_bodyWarnCount < m_bindings.bodyWarningLimit)
|
||||
{
|
||||
++m_bodyWarnCount;
|
||||
shouldWarn = true;
|
||||
}
|
||||
}
|
||||
if (shouldWarn)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << '[' << m_bindings.serviceName
|
||||
<< "] failed data read lbn=0x" << std::hex << lbn
|
||||
<< " bytes=0x" << byteCount << " dst=0x" << destination;
|
||||
m_host.log(LogLevel::Warning, message.str());
|
||||
}
|
||||
}
|
||||
if (loaded || m_bindings.completeFailedLoads)
|
||||
{
|
||||
markLoadComplete(request.receive, loadId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"header_warnings", m_headerWarnCount, false});
|
||||
metrics.push_back({"body_warnings", m_bodyWarnCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t openSiblingFile(const std::string &lowerName,
|
||||
const std::string &upperName)
|
||||
{
|
||||
const std::array<std::string, 2> roots = {
|
||||
m_host.hostPath(HostPathKind::CdRoot),
|
||||
m_host.hostPath(HostPathKind::ElfDirectory),
|
||||
};
|
||||
for (const std::string &rootValue : roots)
|
||||
{
|
||||
if (rootValue.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::filesystem::path root(rootValue);
|
||||
for (const std::string *name : {&lowerName, &upperName})
|
||||
{
|
||||
if (name->empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::filesystem::path candidate = root / *name;
|
||||
const uint64_t handle = m_host.openHostFile(candidate.string());
|
||||
if (handle != 0u)
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
bool copyHostRange(uint64_t handle,
|
||||
uint64_t offset,
|
||||
uint32_t destination,
|
||||
uint64_t byteCount)
|
||||
{
|
||||
if (byteCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::array<uint8_t, 16 * 1024> chunk{};
|
||||
uint64_t copied = 0;
|
||||
while (copied < byteCount)
|
||||
{
|
||||
const size_t wanted = static_cast<size_t>(std::min<uint64_t>(chunk.size(), byteCount - copied));
|
||||
std::fill(chunk.begin(), chunk.begin() + static_cast<std::ptrdiff_t>(wanted), 0u);
|
||||
size_t got = 0u;
|
||||
if (!m_host.readHostFile(handle,
|
||||
offset + copied,
|
||||
chunk.data(),
|
||||
wanted,
|
||||
got) ||
|
||||
got > wanted ||
|
||||
!m_host.writeGuest(destination + static_cast<uint32_t>(copied),
|
||||
chunk.data(),
|
||||
wanted))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
copied += wanted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadImageHeader()
|
||||
{
|
||||
const uint64_t handle = openSiblingFile(m_bindings.imageHeaderLowerName,
|
||||
m_bindings.imageHeaderUpperName);
|
||||
if (handle == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
uint64_t fileSize = 0u;
|
||||
if (!m_host.hostFileSize(handle, fileSize))
|
||||
{
|
||||
m_host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
uint32_t normalized = 0;
|
||||
if (!m_host.normalizeGuestAddress(m_bindings.imageHeaderAddress, normalized) ||
|
||||
normalized >= kEeRamSize)
|
||||
{
|
||||
m_host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
const bool copied = copyHostRange(handle,
|
||||
0u,
|
||||
m_bindings.imageHeaderAddress,
|
||||
std::min<uint64_t>(fileSize,
|
||||
kEeRamSize - normalized));
|
||||
m_host.closeHostFile(handle);
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool readBody(uint32_t lbn, uint32_t byteCount, uint32_t destination)
|
||||
{
|
||||
uint32_t normalized = 0;
|
||||
if (!m_host.normalizeGuestAddress(destination, normalized) || normalized >= kEeRamSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
uint64_t handle = openSiblingFile(m_bindings.imageBodyLowerName,
|
||||
m_bindings.imageBodyUpperName);
|
||||
if (handle == 0u && m_bindings.fallbackBodyToCdImage)
|
||||
{
|
||||
handle = m_host.openHostFile(m_host.hostPath(HostPathKind::CdImage));
|
||||
}
|
||||
if (handle == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const uint64_t bytes = std::min<uint64_t>(byteCount, kEeRamSize - normalized);
|
||||
const bool copied = copyHostRange(handle,
|
||||
static_cast<uint64_t>(lbn) * m_bindings.sectorSize,
|
||||
destination,
|
||||
bytes);
|
||||
m_host.closeHostFile(handle);
|
||||
return copied;
|
||||
}
|
||||
|
||||
void markLoadComplete(GuestBuffer receive, uint32_t loadId)
|
||||
{
|
||||
const uint32_t offset = m_bindings.statusOffset +
|
||||
((loadId & m_bindings.statusSlotMask) *
|
||||
m_bindings.statusStride);
|
||||
if (receive.address && offset < receive.size)
|
||||
{
|
||||
const uint8_t complete = m_bindings.completeValue;
|
||||
(void)m_host.writeGuest(receive.address + offset, &complete, sizeof(complete));
|
||||
}
|
||||
}
|
||||
|
||||
void warnHeader()
|
||||
{
|
||||
bool shouldWarn = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_headerWarnCount < m_bindings.headerWarningLimit)
|
||||
{
|
||||
++m_headerWarnCount;
|
||||
shouldWarn = true;
|
||||
}
|
||||
}
|
||||
if (shouldWarn)
|
||||
{
|
||||
m_host.log(LogLevel::Warning,
|
||||
'[' + m_bindings.serviceName + "] failed to load image header");
|
||||
}
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
SdrdrvBindings m_bindings;
|
||||
std::array<uint32_t, 1> m_sids;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_headerWarnCount = 0;
|
||||
uint32_t m_bodyWarnCount = 0;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createSdrdrvService(IopHost &host,
|
||||
SdrdrvBindings bindings)
|
||||
{
|
||||
const uint32_t largestWord = std::max({bindings.lbnWord,
|
||||
bindings.byteCountWord,
|
||||
bindings.destinationWord,
|
||||
bindings.destinationKindWord,
|
||||
bindings.loadIdWord});
|
||||
if (bindings.serviceName.empty() ||
|
||||
bindings.sid == 0u ||
|
||||
bindings.imageHeaderAddress == 0u ||
|
||||
bindings.commandBytes == 0u ||
|
||||
(bindings.commandBytes % sizeof(uint32_t)) != 0u ||
|
||||
largestWord >= bindings.commandBytes / sizeof(uint32_t) ||
|
||||
bindings.maxCommands == 0u ||
|
||||
bindings.sectorSize == 0u ||
|
||||
bindings.statusStride == 0u ||
|
||||
bindings.initFunction == bindings.submitFunction ||
|
||||
bindings.initFunction == bindings.shutdownFunction ||
|
||||
bindings.submitFunction == bindings.shutdownFunction ||
|
||||
bindings.headerCommand == bindings.loadCommand ||
|
||||
(bindings.imageHeaderLowerName.empty() &&
|
||||
bindings.imageHeaderUpperName.empty()) ||
|
||||
(bindings.imageBodyLowerName.empty() &&
|
||||
bindings.imageBodyUpperName.empty() &&
|
||||
!bindings.fallbackBodyToCdImage))
|
||||
{
|
||||
throw std::invalid_argument("invalid SDRDRV bindings");
|
||||
}
|
||||
return std::make_unique<SdrdrvService>(host, std::move(bindings));
|
||||
}
|
||||
}
|
||||
@@ -1,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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,956 +0,0 @@
|
||||
#include "plugin_loader.h"
|
||||
|
||||
#include "ps2x/iop/plugin_api.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr size_t kMaxPluginProfiles = 256u;
|
||||
constexpr size_t kMaxPluginSids = 256u;
|
||||
constexpr size_t kMaxPluginStringBytes = 4096u;
|
||||
|
||||
bool validStringView(ps2x_iop_string_view_v1 value)
|
||||
{
|
||||
return value.size <= kMaxPluginStringBytes && (value.size == 0u || value.data != nullptr);
|
||||
}
|
||||
|
||||
std::string copyString(ps2x_iop_string_view_v1 value)
|
||||
{
|
||||
if (!validStringView(value) || value.size == 0u)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return std::string(value.data, value.size);
|
||||
}
|
||||
|
||||
ps2x_iop_string_view_v1 makeStringView(std::string_view value)
|
||||
{
|
||||
return {value.data(), value.size()};
|
||||
}
|
||||
|
||||
int32_t copyHostString(const std::string &value,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *requiredSize)
|
||||
{
|
||||
const size_t required = value.size() + 1;
|
||||
if (requiredSize)
|
||||
{
|
||||
*requiredSize = required;
|
||||
}
|
||||
if (!destination || capacity < required)
|
||||
{
|
||||
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
|
||||
}
|
||||
std::memcpy(destination, value.c_str(), required);
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
|
||||
IopHandleKind toHandleKind(uint32_t kind)
|
||||
{
|
||||
return kind == PS2X_IOP_HANDLE_RPC_PACKET_V1
|
||||
? IopHandleKind::RpcPacket
|
||||
: IopHandleKind::RpcServer;
|
||||
}
|
||||
|
||||
HostPathKind toHostPathKind(uint32_t kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case PS2X_IOP_PATH_CD_ROOT_V1:
|
||||
return HostPathKind::CdRoot;
|
||||
case PS2X_IOP_PATH_CD_IMAGE_V1:
|
||||
return HostPathKind::CdImage;
|
||||
case PS2X_IOP_PATH_HOST_ROOT_V1:
|
||||
return HostPathKind::HostRoot;
|
||||
case PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1:
|
||||
return HostPathKind::MemoryCardRoot;
|
||||
default:
|
||||
return HostPathKind::ElfDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
MemoryCardOperation toMemoryCardOperation(uint32_t operation)
|
||||
{
|
||||
const uint32_t last = static_cast<uint32_t>(MemoryCardOperation::Mkdir);
|
||||
if (operation > last)
|
||||
{
|
||||
throw std::out_of_range("invalid memory-card operation");
|
||||
}
|
||||
return static_cast<MemoryCardOperation>(operation);
|
||||
}
|
||||
|
||||
class HostApiBridge
|
||||
{
|
||||
public:
|
||||
explicit HostApiBridge(IopHost &hostRef)
|
||||
: host(hostRef)
|
||||
{
|
||||
api.abi_version = PS2X_IOP_ABI_VERSION_V1;
|
||||
api.struct_size = sizeof(api);
|
||||
api.userdata = this;
|
||||
api.read_guest = &readGuest;
|
||||
api.write_guest = &writeGuest;
|
||||
api.zero_guest = &zeroGuest;
|
||||
api.normalize_guest_address = &normalizeGuestAddress;
|
||||
api.allocate_iop_handle = &allocateIopHandle;
|
||||
api.allocate_guest = &allocateGuest;
|
||||
api.free_guest = &freeGuest;
|
||||
api.audio_command = &audioCommand;
|
||||
api.get_host_path = &getHostPath;
|
||||
api.translate_guest_path = &translateGuestPath;
|
||||
api.open_host_file = &openHostFile;
|
||||
api.host_file_size = &hostFileSize;
|
||||
api.read_host_file = &readHostFile;
|
||||
api.close_host_file = &closeHostFile;
|
||||
api.memory_card = &memoryCard;
|
||||
api.has_guest_function = &hasGuestFunction;
|
||||
api.invoke_guest_function = &invokeGuestFunction;
|
||||
api.log = &log;
|
||||
}
|
||||
|
||||
ps2x_iop_host_api_v1 api{};
|
||||
IopHost &host;
|
||||
|
||||
private:
|
||||
static HostApiBridge *self(void *userdata)
|
||||
{
|
||||
return static_cast<HostApiBridge *>(userdata);
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
static int32_t guardedStatus(Callback &&callback) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return static_cast<int32_t>(
|
||||
std::forward<Callback>(callback)());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return PS2X_IOP_STATUS_FAILED_V1;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Value, typename Callback>
|
||||
static Value guardedValue(Value fallback, Callback &&callback) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
return static_cast<Value>(
|
||||
std::forward<Callback>(callback)());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
static void guardedVoid(Callback &&callback) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
std::forward<Callback>(callback)();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t readGuest(void *userdata, uint32_t address, void *destination, size_t size)
|
||||
{
|
||||
if (!userdata || (!destination && size != 0))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.readGuest(address, destination, size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t writeGuest(void *userdata, uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
if (!userdata || (!source && size != 0))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.writeGuest(address, source, size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t zeroGuest(void *userdata, uint32_t address, size_t size)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.zeroGuest(address, size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t normalizeGuestAddress(void *userdata, uint32_t address, uint32_t *normalized)
|
||||
{
|
||||
if (!userdata || !normalized)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.normalizeGuestAddress(address, *normalized)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static uint32_t allocateIopHandle(void *userdata, uint32_t kind)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return guardedValue<uint32_t>(0u, [&]()
|
||||
{ return self(userdata)->host.allocateIopHandle(toHandleKind(kind)); });
|
||||
}
|
||||
|
||||
static uint32_t allocateGuest(void *userdata, uint32_t size, uint32_t alignment)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return guardedValue<uint32_t>(0u, [&]()
|
||||
{ return self(userdata)->host.allocateGuest(size, alignment); });
|
||||
}
|
||||
|
||||
static void freeGuest(void *userdata, uint32_t address)
|
||||
{
|
||||
if (userdata && address)
|
||||
{
|
||||
guardedVoid([&]()
|
||||
{ self(userdata)->host.freeGuest(address); });
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t audioCommand(void *userdata,
|
||||
uint32_t sid,
|
||||
uint32_t function,
|
||||
ps2x_iop_guest_buffer_v1 send,
|
||||
ps2x_iop_guest_buffer_v1 receive)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{
|
||||
self(userdata)->host.audioCommand(sid,
|
||||
function,
|
||||
{send.address, send.size},
|
||||
{receive.address, receive.size});
|
||||
return PS2X_IOP_STATUS_OK_V1; });
|
||||
}
|
||||
|
||||
static int32_t getHostPath(void *userdata,
|
||||
uint32_t kind,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *requiredSize)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return copyHostString(self(userdata)->host.hostPath(toHostPathKind(kind)),
|
||||
destination,
|
||||
capacity,
|
||||
requiredSize); });
|
||||
}
|
||||
|
||||
static int32_t translateGuestPath(void *userdata,
|
||||
ps2x_iop_string_view_v1 path,
|
||||
char *destination,
|
||||
size_t capacity,
|
||||
size_t *requiredSize)
|
||||
{
|
||||
if (!userdata || (!path.data && path.size != 0))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{
|
||||
const std::string translated = self(userdata)->host.translateGuestPath(
|
||||
std::string_view(path.data ? path.data : "", path.size));
|
||||
return copyHostString(translated, destination, capacity, requiredSize); });
|
||||
}
|
||||
|
||||
static uint64_t openHostFile(void *userdata,
|
||||
ps2x_iop_string_view_v1 path)
|
||||
{
|
||||
if (!userdata || (!path.data && path.size != 0u))
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
return guardedValue<uint64_t>(0u, [&]()
|
||||
{ return self(userdata)->host.openHostFile(
|
||||
std::string_view(path.data ? path.data : "", path.size)); });
|
||||
}
|
||||
|
||||
static int32_t hostFileSize(void *userdata,
|
||||
uint64_t handle,
|
||||
uint64_t *size)
|
||||
{
|
||||
if (!userdata || handle == 0u || !size)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.hostFileSize(handle, *size)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static int32_t readHostFile(void *userdata,
|
||||
uint64_t handle,
|
||||
uint64_t offset,
|
||||
void *destination,
|
||||
size_t size,
|
||||
size_t *bytesRead)
|
||||
{
|
||||
if (!userdata || handle == 0u || !bytesRead ||
|
||||
(!destination && size != 0u))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.readHostFile(handle,
|
||||
offset,
|
||||
destination,
|
||||
size,
|
||||
*bytesRead)
|
||||
? PS2X_IOP_STATUS_OK_V1
|
||||
: PS2X_IOP_STATUS_FAILED_V1; });
|
||||
}
|
||||
|
||||
static void closeHostFile(void *userdata, uint64_t handle)
|
||||
{
|
||||
if (userdata && handle != 0u)
|
||||
{
|
||||
guardedVoid([&]()
|
||||
{ self(userdata)->host.closeHostFile(handle); });
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t memoryCard(void *userdata,
|
||||
const ps2x_iop_memory_card_request_v1 *request,
|
||||
int32_t *result)
|
||||
{
|
||||
if (!userdata || !request || !result || request->struct_size < sizeof(*request))
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
try
|
||||
{
|
||||
MemoryCardRequest converted;
|
||||
converted.operation = toMemoryCardOperation(request->operation);
|
||||
std::copy(std::begin(request->arguments), std::end(request->arguments), converted.arguments.begin());
|
||||
*result = self(userdata)->host.memoryCard(converted);
|
||||
return PS2X_IOP_STATUS_OK_V1;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return PS2X_IOP_STATUS_FAILED_V1;
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t hasGuestFunction(void *userdata, uint32_t address)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.hasGuestFunction(address) ? 1 : 0; });
|
||||
}
|
||||
|
||||
static int32_t invokeGuestFunction(void *userdata,
|
||||
uint64_t callToken,
|
||||
uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t *resultAddress)
|
||||
{
|
||||
if (!userdata)
|
||||
{
|
||||
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
|
||||
}
|
||||
return guardedStatus([&]()
|
||||
{ return self(userdata)->host.invokeGuestFunction(callToken,
|
||||
address,
|
||||
a0,
|
||||
a1,
|
||||
a2,
|
||||
a3,
|
||||
resultAddress)
|
||||
? 1
|
||||
: 0; });
|
||||
}
|
||||
|
||||
static void log(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message)
|
||||
{
|
||||
if (!userdata || (!message.data && message.size != 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const uint32_t maxLevel = static_cast<uint32_t>(LogLevel::Error);
|
||||
const auto converted = static_cast<LogLevel>(std::min(level, maxLevel));
|
||||
guardedVoid([&]()
|
||||
{ self(userdata)->host.log(
|
||||
converted,
|
||||
std::string_view(message.data ? message.data : "", message.size)); });
|
||||
}
|
||||
};
|
||||
|
||||
ps2x_iop_rpc_candidate_v1 toPluginCandidate(const RpcCallCandidate &candidate)
|
||||
{
|
||||
return {
|
||||
candidate.sendSize,
|
||||
candidate.receiveAddress,
|
||||
candidate.receiveSize,
|
||||
candidate.endFunction,
|
||||
candidate.endParameter,
|
||||
candidate.plausible ? 1u : 0u,
|
||||
};
|
||||
}
|
||||
|
||||
class PluginService final : public IopService
|
||||
{
|
||||
public:
|
||||
PluginService(IopHost &host,
|
||||
std::shared_ptr<void> libraryKeepAlive,
|
||||
ps2x_iop_profile_api_v1 profileApi,
|
||||
std::string serviceName,
|
||||
std::vector<uint32_t> serviceSids,
|
||||
const GameIdentity &identity)
|
||||
: m_libraryKeepAlive(std::move(libraryKeepAlive)),
|
||||
m_api(profileApi),
|
||||
m_name(std::move(serviceName)),
|
||||
m_sids(std::move(serviceSids)),
|
||||
m_host(host)
|
||||
{
|
||||
const ps2x_iop_game_identity_v1 pluginIdentity{
|
||||
sizeof(ps2x_iop_game_identity_v1),
|
||||
makeStringView(identity.elfName),
|
||||
identity.entryPoint,
|
||||
identity.crc32,
|
||||
};
|
||||
try
|
||||
{
|
||||
m_instance = m_api.create(&m_host.api, &pluginIdentity);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
throw std::runtime_error("plugin profile create threw an exception");
|
||||
}
|
||||
if (!m_instance)
|
||||
{
|
||||
throw std::runtime_error("plugin profile create returned null");
|
||||
}
|
||||
}
|
||||
|
||||
~PluginService() override
|
||||
{
|
||||
if (m_instance && m_api.destroy)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_api.destroy(m_instance);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_host.host.log(LogLevel::Error,
|
||||
"IOP plugin destroy threw for " + m_name);
|
||||
}
|
||||
}
|
||||
m_instance = nullptr;
|
||||
}
|
||||
|
||||
std::string_view name() const override
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
if (!m_api.reset)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.reset(m_instance);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1)
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning, "IOP plugin reset failed for " + m_name);
|
||||
}
|
||||
}
|
||||
|
||||
RpcAbi selectRpcAbi(const RpcAbiRequest &request) const override
|
||||
{
|
||||
if (!m_api.select_rpc_abi)
|
||||
{
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
const ps2x_iop_rpc_abi_request_v1 converted{
|
||||
sizeof(ps2x_iop_rpc_abi_request_v1),
|
||||
request.boundSid,
|
||||
request.function,
|
||||
toPluginCandidate(request.registers),
|
||||
toPluginCandidate(request.stack),
|
||||
};
|
||||
uint32_t result = PS2X_IOP_RPC_ABI_DEFAULT_V1;
|
||||
try
|
||||
{
|
||||
result = m_api.select_rpc_abi(m_instance, &converted);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning,
|
||||
"IOP plugin ABI selector threw for " + m_name);
|
||||
}
|
||||
if (result == PS2X_IOP_RPC_ABI_REGISTERS_V1)
|
||||
{
|
||||
return RpcAbi::Registers;
|
||||
}
|
||||
if (result == PS2X_IOP_RPC_ABI_STACK_V1)
|
||||
{
|
||||
return RpcAbi::Stack;
|
||||
}
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
const ps2x_iop_rpc_request_v1 converted{
|
||||
sizeof(ps2x_iop_rpc_request_v1),
|
||||
request.callToken,
|
||||
request.clientAddress,
|
||||
request.serverAddress,
|
||||
request.serverFunction,
|
||||
request.serverBuffer,
|
||||
request.sid,
|
||||
request.function,
|
||||
request.mode,
|
||||
{request.send.address, request.send.size},
|
||||
{request.receive.address, request.receive.size},
|
||||
request.endFunction,
|
||||
request.endParameter,
|
||||
};
|
||||
ps2x_iop_rpc_result_v1 result{};
|
||||
result.struct_size = sizeof(result);
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.handle_rpc(m_instance, &converted, &result);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1 ||
|
||||
result.struct_size < sizeof(result))
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning, "IOP plugin RPC failed for " + m_name);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
result.handled != 0,
|
||||
result.result_address,
|
||||
result.signal_nowait_completion != 0,
|
||||
result.signal_completion != 0,
|
||||
result.callback_policy == PS2X_IOP_CALLBACK_SUPPRESS_V1
|
||||
? CallbackPolicy::Suppress
|
||||
: CallbackPolicy::RuntimeDefault,
|
||||
result.server_dispatch_policy == PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1
|
||||
? ServerDispatchPolicy::Suppress
|
||||
: ServerDispatchPolicy::RuntimeDefault,
|
||||
};
|
||||
}
|
||||
|
||||
void onSifTransfer(const SifTransfer &transfer) override
|
||||
{
|
||||
if (!m_api.on_sif_transfer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const ps2x_iop_sif_transfer_v1 converted{
|
||||
sizeof(ps2x_iop_sif_transfer_v1),
|
||||
static_cast<uint32_t>(transfer.kind),
|
||||
static_cast<uint32_t>(transfer.phase),
|
||||
transfer.sourceAddress,
|
||||
transfer.destinationAddress,
|
||||
transfer.size,
|
||||
};
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.on_sif_transfer(m_instance, &converted);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1)
|
||||
{
|
||||
m_host.host.log(LogLevel::Warning, "IOP plugin transfer hook failed for " + m_name);
|
||||
}
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
if (!m_api.debug_metric_count || !m_api.debug_metric)
|
||||
{
|
||||
return;
|
||||
}
|
||||
size_t rawCount = 0u;
|
||||
try
|
||||
{
|
||||
rawCount = m_api.debug_metric_count(m_instance);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const size_t count = std::min<size_t>(rawCount, 256);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
ps2x_iop_debug_metric_v1 metric{};
|
||||
metric.struct_size = sizeof(metric);
|
||||
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
status = m_api.debug_metric(m_instance, i, &metric);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
if (status != PS2X_IOP_STATUS_OK_V1 ||
|
||||
metric.struct_size < sizeof(metric))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
metrics.push_back({copyString(metric.name), metric.value, metric.hexadecimal != 0});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<void> m_libraryKeepAlive;
|
||||
ps2x_iop_profile_api_v1 m_api{};
|
||||
std::string m_name;
|
||||
std::vector<uint32_t> m_sids;
|
||||
HostApiBridge m_host;
|
||||
void *m_instance = nullptr;
|
||||
};
|
||||
|
||||
bool hasPluginExtension(const std::filesystem::path &path)
|
||||
{
|
||||
std::string extension = path.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value)
|
||||
{ return static_cast<char>(std::tolower(value)); });
|
||||
#if defined(_WIN32)
|
||||
return extension == ".dll";
|
||||
#elif defined(__linux__)
|
||||
return extension == ".so";
|
||||
#else
|
||||
(void)extension;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string formatPluginDiagnostic(const std::filesystem::path &path, std::string_view reason)
|
||||
{
|
||||
return "IOP plugin '" + path.string() + "': " + std::string(reason);
|
||||
}
|
||||
}
|
||||
|
||||
class PluginCatalog::DynamicLibrary
|
||||
{
|
||||
public:
|
||||
explicit DynamicLibrary(std::filesystem::path sourcePath)
|
||||
: path(std::move(sourcePath))
|
||||
{
|
||||
}
|
||||
|
||||
~DynamicLibrary()
|
||||
{
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
if (handle)
|
||||
{
|
||||
FreeLibrary(static_cast<HMODULE>(handle));
|
||||
}
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
if (handle)
|
||||
{
|
||||
dlclose(handle);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool open(std::string &error)
|
||||
{
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
handle = LoadLibraryW(path.c_str());
|
||||
if (!handle)
|
||||
{
|
||||
error = "LoadLibraryW failed with code " + std::to_string(GetLastError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!handle)
|
||||
{
|
||||
const char *message = dlerror();
|
||||
error = message ? message : "dlopen failed";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
error = "dynamic IOP plugins are disabled on this platform";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void *symbol(const char *name) const
|
||||
{
|
||||
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
|
||||
return handle ? reinterpret_cast<void *>(GetProcAddress(static_cast<HMODULE>(handle), name)) : nullptr;
|
||||
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
|
||||
return handle ? dlsym(handle, name) : nullptr;
|
||||
#else
|
||||
(void)name;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path path;
|
||||
void *handle = nullptr;
|
||||
};
|
||||
|
||||
PluginCatalog::PluginCatalog(IopHost &host)
|
||||
: m_host(host)
|
||||
{
|
||||
}
|
||||
|
||||
PluginCatalog::~PluginCatalog() = default;
|
||||
|
||||
// TODO I never test this one
|
||||
bool PluginCatalog::load(const std::vector<std::filesystem::path> &searchPaths,
|
||||
std::vector<ProfileDefinition> &profiles,
|
||||
std::vector<std::string> &diagnostics,
|
||||
std::string *error)
|
||||
{
|
||||
(void)error;
|
||||
#if !PS2X_IOP_ENABLE_PLUGINS
|
||||
if (!searchPaths.empty())
|
||||
{
|
||||
diagnostics.push_back("dynamic IOP plugins are disabled on this platform");
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
for (const auto &searchPath : searchPaths)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(searchPath, ec) || ec)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!std::filesystem::is_directory(searchPath, ec) || ec)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(searchPath, "search path is not a directory"));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (std::filesystem::directory_iterator iterator(searchPath, ec), end; !ec && iterator != end; iterator.increment(ec))
|
||||
{
|
||||
const std::filesystem::directory_entry &entry = *iterator;
|
||||
if (!entry.is_regular_file(ec) || ec || !hasPluginExtension(entry.path()))
|
||||
{
|
||||
ec.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(entry.path(), ec);
|
||||
if (ec)
|
||||
{
|
||||
ec.clear();
|
||||
canonicalPath = entry.path().lexically_normal();
|
||||
}
|
||||
const std::string pathKey = canonicalPath.generic_string();
|
||||
if (!m_loadedPaths.insert(pathKey).second)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto library = std::make_shared<DynamicLibrary>(canonicalPath);
|
||||
std::string openError;
|
||||
if (!library->open(openError))
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, openError));
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto query = reinterpret_cast<ps2x_iop_query_v1_fn>(
|
||||
library->symbol(PS2X_IOP_QUERY_SYMBOL_V1));
|
||||
if (!query)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "missing " PS2X_IOP_QUERY_SYMBOL_V1));
|
||||
continue;
|
||||
}
|
||||
|
||||
ps2x_iop_plugin_api_v1 plugin{};
|
||||
plugin.struct_size = sizeof(plugin);
|
||||
int32_t queryStatus = PS2X_IOP_STATUS_FAILED_V1;
|
||||
try
|
||||
{
|
||||
queryStatus = query(PS2X_IOP_ABI_VERSION_V1, &plugin);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"query entry threw an exception"));
|
||||
continue;
|
||||
}
|
||||
if (queryStatus != PS2X_IOP_STATUS_OK_V1 ||
|
||||
plugin.abi_version != PS2X_IOP_ABI_VERSION_V1 ||
|
||||
plugin.struct_size < sizeof(plugin))
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "incompatible ABI or invalid descriptor"));
|
||||
continue;
|
||||
}
|
||||
if (plugin.profile_count > 0 && !plugin.profiles)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "profile table is null"));
|
||||
continue;
|
||||
}
|
||||
if (plugin.profile_count > kMaxPluginProfiles)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "too many profiles"));
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string provider = copyString(plugin.name).empty()
|
||||
? canonicalPath.filename().string()
|
||||
: copyString(plugin.name);
|
||||
size_t acceptedProfiles = 0;
|
||||
for (size_t index = 0; index < plugin.profile_count; ++index)
|
||||
{
|
||||
const ps2x_iop_profile_api_v1 &profile = plugin.profiles[index];
|
||||
if (profile.abi_version != PS2X_IOP_ABI_VERSION_V1 ||
|
||||
profile.struct_size < sizeof(profile) ||
|
||||
profile.matcher.struct_size < sizeof(profile.matcher))
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"ignored invalid profile at index " + std::to_string(index)));
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string profileId = copyString(profile.id);
|
||||
const bool validMatcherName = validStringView(profile.matcher.elf_name);
|
||||
const bool matcherPresent = profile.matcher.elf_name.size != 0 ||
|
||||
profile.matcher.entry_point != 0 ||
|
||||
profile.matcher.crc32 != 0;
|
||||
if (!validStringView(profile.id) || !validMatcherName ||
|
||||
profileId.empty() || !matcherPresent ||
|
||||
profile.sid_count == 0 || !profile.sids ||
|
||||
!profile.create || !profile.destroy || !profile.reset ||
|
||||
!profile.handle_rpc)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"ignored invalid profile at index " + std::to_string(index)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (profile.sid_count > kMaxPluginSids)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"ignored profile with too many SIDs: " + profileId));
|
||||
continue;
|
||||
}
|
||||
std::vector<uint32_t> sids(profile.sids, profile.sids + profile.sid_count);
|
||||
|
||||
ProfileDefinition definition;
|
||||
definition.id = profileId;
|
||||
definition.provider = provider;
|
||||
definition.matcher.elfName = copyString(profile.matcher.elf_name);
|
||||
definition.matcher.entryPoint = profile.matcher.entry_point;
|
||||
definition.matcher.crc32 = profile.matcher.crc32;
|
||||
const std::shared_ptr<void> keepAlive = library;
|
||||
definition.factory = [keepAlive, profile, profileId, sids = std::move(sids)](
|
||||
IopHost &host,
|
||||
const GameIdentity &identity) mutable
|
||||
{
|
||||
ServiceList services;
|
||||
services.push_back(std::make_unique<PluginService>(host,
|
||||
keepAlive,
|
||||
profile,
|
||||
profileId,
|
||||
sids,
|
||||
identity));
|
||||
return services;
|
||||
};
|
||||
profiles.push_back(std::move(definition));
|
||||
++acceptedProfiles;
|
||||
}
|
||||
|
||||
if (acceptedProfiles == 0)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "no valid profiles"));
|
||||
continue;
|
||||
}
|
||||
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
|
||||
"loaded " + std::to_string(acceptedProfiles) + " profile(s)"));
|
||||
m_libraries.push_back(std::move(library));
|
||||
}
|
||||
|
||||
if (ec)
|
||||
{
|
||||
diagnostics.push_back(formatPluginDiagnostic(searchPath, ec.message()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "iop_service.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class PluginCatalog
|
||||
{
|
||||
public:
|
||||
explicit PluginCatalog(IopHost &host);
|
||||
~PluginCatalog();
|
||||
|
||||
PluginCatalog(const PluginCatalog &) = delete;
|
||||
PluginCatalog &operator=(const PluginCatalog &) = delete;
|
||||
|
||||
bool load(const std::vector<std::filesystem::path> &searchPaths,
|
||||
std::vector<ProfileDefinition> &profiles,
|
||||
std::vector<std::string> &diagnostics,
|
||||
std::string *error);
|
||||
|
||||
private:
|
||||
class DynamicLibrary;
|
||||
|
||||
IopHost &m_host;
|
||||
std::vector<std::shared_ptr<DynamicLibrary>> m_libraries;
|
||||
std::unordered_set<std::string> m_loadedPaths;
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user