From ee149581aac999ab22791b3900377c0b028a5840 Mon Sep 17 00:00:00 2001 From: Ranieri Date: Wed, 15 Jul 2026 01:44:06 -0300 Subject: [PATCH] Refactor IOP system (#170) * feat: implement memory card IOP * feat: IOP trace * feat: move IOP logic to ps2xIOP refactor: small refactor on audio api on runtime --- CMakeLists.txt | 1 + README.md | 10 +- ps2xIOP/CMakeLists.txt | 49 + ps2xIOP/PluginExample.md | 220 ++ ps2xIOP/README.md | 219 ++ ps2xIOP/include/ps2x/iop/iop_host.h | 95 + ps2xIOP/include/ps2x/iop/iop_subsystem.h | 40 + ps2xIOP/include/ps2x/iop/iop_types.h | 142 + ps2xIOP/include/ps2x/iop/plugin_api.h | 307 ++ ps2xIOP/src/builtin_profiles.cpp | 149 + ps2xIOP/src/iop_service.h | 55 + ps2xIOP/src/iop_subsystem.cpp | 339 +++ ps2xIOP/src/module_factories.h | 156 ++ ps2xIOP/src/modules/clfile.cpp | 635 +++++ ps2xIOP/src/modules/cri_dtx.cpp | 1324 +++++++++ ps2xIOP/src/modules/dbcman.cpp | 113 + ps2xIOP/src/modules/libsd.cpp | 63 + ps2xIOP/src/modules/mcserv.cpp | 402 +++ ps2xIOP/src/modules/sdrdrv.cpp | 335 +++ ps2xIOP/src/modules/sound_update_stub.cpp | 122 + ps2xIOP/src/modules/tsnddrv.cpp | 628 +++++ ps2xIOP/src/plugin_loader.cpp | 956 +++++++ ps2xIOP/src/plugin_loader.h | 34 + ps2xRuntime/CMakeLists.txt | 19 +- ps2xRuntime/include/game_overrides.h | 2 +- ps2xRuntime/include/ps2_runtime.h | 99 +- ps2xRuntime/include/ps2_syscalls.h | 5 - ps2xRuntime/include/runtime/ps2_iop.h | 39 - ps2xRuntime/include/runtime/ps2_iop_audio.h | 23 - ps2xRuntime/include/runtime/ps2_iop_cl.h | 27 - ps2xRuntime/include/runtime/ps2_iop_dbcman.h | 17 - ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h | 17 - ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp | 170 +- ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp | 53 +- ps2xRuntime/src/lib/Kernel/Syscalls/Common.h | 1 - .../src/lib/Kernel/Syscalls/Helpers/State.h | 156 +- ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp | 2494 +++-------------- ps2xRuntime/src/lib/Kernel/Syscalls/RPC.h | 9 - ps2xRuntime/src/lib/game_overrides.cpp | 132 +- ps2xRuntime/src/lib/ps2_debug_panel.cpp | 285 +- ps2xRuntime/src/lib/ps2_iop.cpp | 105 - ps2xRuntime/src/lib/ps2_iop_audio.cpp | 37 - ps2xRuntime/src/lib/ps2_iop_cl.cpp | 514 ---- ps2xRuntime/src/lib/ps2_iop_dbcman.cpp | 89 - ps2xRuntime/src/lib/ps2_iop_host.cpp | 503 ++++ ps2xRuntime/src/lib/ps2_iop_host.h | 103 + ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp | 325 --- ps2xRuntime/src/lib/ps2_iop_transport.h | 58 + ps2xRuntime/src/lib/ps2_runtime.cpp | 130 +- ps2xTest/CMakeLists.txt | 48 +- ps2xTest/src/fake_iop_bad_abi.c | 15 + ps2xTest/src/fake_iop_missing_symbol.cpp | 6 + ps2xTest/src/fake_iop_plugin.cpp | 350 +++ ps2xTest/src/main.cpp | 2 + ps2xTest/src/ps2_iop_tests.cpp | 816 ++++++ ps2xTest/src/ps2_runtime_expansion_tests.cpp | 124 +- ps2xTest/src/ps2_sif_dma_tests.cpp | 45 +- ps2xTest/src/ps2_sif_rpc_tests.cpp | 435 ++- 58 files changed, 9675 insertions(+), 3972 deletions(-) create mode 100644 ps2xIOP/CMakeLists.txt create mode 100644 ps2xIOP/PluginExample.md create mode 100644 ps2xIOP/README.md create mode 100644 ps2xIOP/include/ps2x/iop/iop_host.h create mode 100644 ps2xIOP/include/ps2x/iop/iop_subsystem.h create mode 100644 ps2xIOP/include/ps2x/iop/iop_types.h create mode 100644 ps2xIOP/include/ps2x/iop/plugin_api.h create mode 100644 ps2xIOP/src/builtin_profiles.cpp create mode 100644 ps2xIOP/src/iop_service.h create mode 100644 ps2xIOP/src/iop_subsystem.cpp create mode 100644 ps2xIOP/src/module_factories.h create mode 100644 ps2xIOP/src/modules/clfile.cpp create mode 100644 ps2xIOP/src/modules/cri_dtx.cpp create mode 100644 ps2xIOP/src/modules/dbcman.cpp create mode 100644 ps2xIOP/src/modules/libsd.cpp create mode 100644 ps2xIOP/src/modules/mcserv.cpp create mode 100644 ps2xIOP/src/modules/sdrdrv.cpp create mode 100644 ps2xIOP/src/modules/sound_update_stub.cpp create mode 100644 ps2xIOP/src/modules/tsnddrv.cpp create mode 100644 ps2xIOP/src/plugin_loader.cpp create mode 100644 ps2xIOP/src/plugin_loader.h delete mode 100644 ps2xRuntime/include/runtime/ps2_iop.h delete mode 100644 ps2xRuntime/include/runtime/ps2_iop_audio.h delete mode 100644 ps2xRuntime/include/runtime/ps2_iop_cl.h delete mode 100644 ps2xRuntime/include/runtime/ps2_iop_dbcman.h delete mode 100644 ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h delete mode 100644 ps2xRuntime/src/lib/ps2_iop.cpp delete mode 100644 ps2xRuntime/src/lib/ps2_iop_audio.cpp delete mode 100644 ps2xRuntime/src/lib/ps2_iop_cl.cpp delete mode 100644 ps2xRuntime/src/lib/ps2_iop_dbcman.cpp create mode 100644 ps2xRuntime/src/lib/ps2_iop_host.cpp create mode 100644 ps2xRuntime/src/lib/ps2_iop_host.h delete mode 100644 ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp create mode 100644 ps2xRuntime/src/lib/ps2_iop_transport.h create mode 100644 ps2xTest/src/fake_iop_bad_abi.c create mode 100644 ps2xTest/src/fake_iop_missing_symbol.cpp create mode 100644 ps2xTest/src/fake_iop_plugin.cpp create mode 100644 ps2xTest/src/ps2_iop_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index abe5bf5..c4ec8ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ if(PS2X_BUILD_RECOMP) endif() if(PS2X_BUILD_RUNTIME) + add_subdirectory("ps2xIOP") add_subdirectory("ps2xRuntime") endif() diff --git a/README.md b/README.md index b9d3ac8..b1e6d93 100644 --- a/README.md +++ b/README.md @@ -12,6 +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. ### Features @@ -132,13 +133,16 @@ 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. + +See [IOP HLE profiles and plugins](ps2xIOP/README.md) for the service boundary and external plugin workflow. ### 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 function bindings by address for one specific game build. 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 belongs in a `ps2xIOP` profile instead. This is separate from recompilation output and separate from global runtime stubs/syscalls. + API: * Header: `ps2xRuntime/include/game_overrides.h` diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt new file mode 100644 index 0000000..e17b472 --- /dev/null +++ b/ps2xIOP/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.21) + +project(ps2xIOP LANGUAGES CXX) + +option(PS2X_IOP_ENABLE_PLUGINS "Enable dynamic ps2xIOP plugins" OFF) + +add_library(ps2_iop STATIC + src/iop_subsystem.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) + +target_include_directories(ps2_iop + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +add_library(ps2x::iop ALIAS ps2_iop) + +target_compile_definitions(ps2_iop PUBLIC + PS2X_IOP_ENABLE_PLUGINS=$ +) + +if(PS2X_IOP_ENABLE_PLUGINS AND UNIX AND NOT APPLE) + target_link_libraries(ps2_iop PRIVATE ${CMAKE_DL_LIBS}) +endif() + +install(TARGETS ps2_iop + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(DIRECTORY include/ + DESTINATION include +) diff --git a/ps2xIOP/PluginExample.md b/ps2xIOP/PluginExample.md new file mode 100644 index 0000000..00d5f19 --- /dev/null +++ b/ps2xIOP/PluginExample.md @@ -0,0 +1,220 @@ +# 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 + +#include + +#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. \ No newline at end of file diff --git a/ps2xIOP/README.md b/ps2xIOP/README.md new file mode 100644 index 0000000..4538f50 --- /dev/null +++ b/ps2xIOP/README.md @@ -0,0 +1,219 @@ +# ps2xIOP + +`ps2xIOP` is the IOP high-level emulation (HLE) subsystem used by +`ps2xRuntime`. It implements the behavior that games expect from IOP services +exposed through SIF RPC and DMA. + +This subsystem does not emulate the IOP's R3000A CPU and does not load or +execute IRX binaries. Its scope is the RPC/DMA behavior needed by recompiled +games. + +> [!IMPORTANT] +> `ps2_iop`/`ps2x::iop` is a C++20 static library linked into the runtime. +> Optional `.dll` and `.so` files are native profile plugins loaded by that +> library. They extend the profile catalog; they do not replace `ps2_iop`, its +> registry, its host bridge, the SIF transport, or execute PS2 IRX code. + +## Architecture + +```text +EE game + | + | SIF RPC / DMA + v +ps2xRuntime transport + | + | RpcRequest / RpcResult / SifTransfer + v +ps2x::iop::IopSubsystem + |-- selected game profile services + |-- core services + | + v +IopHost bridge -> validated guest memory, files, audio, memory card, + logging, and EE function invocation +``` + +### Modules, bindings, and profiles + +These terms describe different layers: + +- A **module implementation** is a reusable protocol engine, such as TSNDDRV, + CRI DTX, CLFILE, or SDRDRV. +- A **binding** contains build-specific values: SIDs, absolute EE addresses, + callback addresses, guest arenas, archive names, and protocol variants. +- A **profile** matches one game build and creates the required module + implementations with that build's bindings. + +For example, `cri_dtx.cpp` contains the reusable CRI DTX engine, while the +`recvx-us` profile supplies Code: Veronica X addresses. A second game should +reuse that engine only after its wire protocol has been compared with the +characterized variant; normally only its profile bindings should change. +Parameterized does not mean universally protocol-compatible. In particular, +`sound_update_stub.cpp` is a narrow LotR compatibility shim, not a complete +generic SOUND driver. + +## Built-in services and profiles + +Core services are created for every `IopSubsystem`: + +| Service | SID | Availability | +| --- | --- | --- | +| MCSERV | `0x80000400`, `0x80000480` | Always active | +| LIBSD | `0x80000701` | Always active | +| DBCMAN | `0x80001300` | Always active | + +The current built-in game profiles are: + +| 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 | + +All current built-ins declare only the ELF basename; they do not yet constrain +the entry point or CRC32. Basename matching is case-insensitive. + +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. + +## Profile selection + +When an ELF is loaded, the runtime calculates one `GameIdentity`: + +- ELF basename; +- entry point; +- CRC-32/IEEE (the common ZIP CRC-32) over the complete ELF file. + +A profile matcher may declare any combination of those fields. Every declared +field must match. The matcher with the greatest number of declared fields wins. +Two matching profiles with equal specificity are an error; `loadELF()` fails +instead of silently choosing one. + +A duplicate SID within the same service layer is an +error. Routing selects one service per SID: if a profile shadows a core SID and +then returns `handled = 0`, the subsystem does not make a second attempt through +the shadowed core service. + +## Dispatch and transfer flow + +`IopSubsystem` exposes five operations used by the runtime: + +1. `configure(GameIdentity)` selects and creates the active profile. +2. `reset()` resets core and profile services. +3. `selectRpcAbi(...)` lets a service choose the register or stack RPC layout when the default decoder is not sufficient. +4. `handleRpc(...)` routes a request by SID and returns both the payload result and the transport policy. +5. `onSifTransfer(...)` notifies services before and after SetDma and GetOtherData copies. + +`RpcResult::handled` indicates whether a service consumed the request. The +result can also request completion semaphore signals and can suppress the +runtime's default EE callback or registered-server dispatch. The transport +executes those actions; the service never reaches into runtime internals. + +The transfer hook is deliberately generic. TSNDDRV uses it for compatibility +backfill and CRI DTX uses it to observe DMA, but the SIF transport contains no +game names, game addresses, or branches for those modules. + +RPC ABI selection is offered to every active profile service before the core +services, and every active service receives each SIF transfer notification. +Implementations must filter the relevant SID/function or transfer +kind/phase/address range themselves. + +## Linking the static library + +```cmake +target_link_libraries(my_runtime PRIVATE ps2x::iop) +``` + +The public C++ API is +[`iop_subsystem.h`](include/ps2x/iop/iop_subsystem.h). Applications using +`PS2Runtime` normally do not construct it directly; the runtime creates the +subsystem and its `IopHost` adapter. + +## Dynamic profile plugins + +Dynamic plugins are optional and disabled by default. Enable them on Windows or +Linux with `PS2X_IOP_ENABLE_PLUGINS=ON`. + +| Platform | Plugin format | Status | +| --- | --- | --- | +| Windows | `.dll` | Supported | +| Linux | `.so` | Supported | + +When enable By default, the runtime scans `iop_plugins/` next to the executable. Discovery +is non-recursive. An embedding application can replace the search directories +before calling `initialize()`: + +```cpp +runtime.setIopPluginSearchPaths({ + std::filesystem::path{"path/to/my/iop_plugins"}, +}); +``` + +Each native module can publish one or more profiles. Missing query symbols, +incompatible ABI versions, malformed descriptors, and unsupported modules are +ignored with a diagnostic. Profile ambiguity, an active-layer SID conflict, or +failure to create the selected profile makes `loadELF()` fail with a clear +error. + +The v1 loader accepts at most 256 profiles per plugin and 256 SIDs per profile. +A profile needs a non-empty ID, at least one matcher field, at least one SID, +and valid `create`, `destroy`, `reset`, and `handle_rpc` callbacks. + +## Plugin ABI v1 + +Plugins include +[`plugin_api.h`](include/ps2x/iop/plugin_api.h) and export exactly one C entry +point: + +```c +PS2X_IOP_PLUGIN_EXPORT int32_t +ps2x_iop_query_v1(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api); +``` + +The ABI uses only fixed C function tables and POD data: + +- validate `abi_version` and `struct_size` before accessing a structure; +- use pointer-plus-length string and buffer views; +- keep the profile instance behind an opaque `void *` handle; +- implement `create`, `destroy`, `reset`, and `handle_rpc`; +- optionally implement RPC ABI selection, SIF transfer hooks, and debug metrics; +- use host callbacks for guest memory, files, audio, memory cards, logging, and + EE function invocation; +- never retain request/result pointers after a callback returns; +- never pass STL types, C++ classes, exceptions, runtime objects, allocators, or + raw guest-memory pointers across the ABI. + +The plugin itself may be implemented in C or C++, but exceptions must not cross +the exported C boundary. Guest buffer fields are PS2 addresses, not host +pointers. + +The `host` function table passed to `create` may be retained until `destroy`. +The identity and its strings, RPC request/result, transfer, and metric pointers +are callback-scoped and must not be retained. `invoke_guest_function` is valid +only during `handle_rpc` and must use that request's `call_token`. Close file +handles and release guest allocations in `reset`/`destroy`. + +Most `int32_t`-returning host callbacks return a `PS2X_IOP_STATUS_*_V1` code. +Two are intentionally boolean-style: `has_guest_function` and +`invoke_guest_function` return `1` for yes/success, `0` for no/failure, and a +negative value for an API error. Do not compare their successful result with +`PS2X_IOP_STATUS_OK_V1`, which is zero. + +When compiling as C++, keep the exported query function under `extern "C"` +linkage. Including `plugin_api.h` provides the matching C declaration. + +FOr learn more you can check [PluginExample](./PluginExample.md) + +## Diagnostics and tests + +`debugSnapshot()` exposes the active profile, its provider, registered core and +profile services, service metrics, loader diagnostics, and the last selection +error. The runtime debugger renders this data in the **IOP/SIF** tab. + +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). diff --git a/ps2xIOP/include/ps2x/iop/iop_host.h b/ps2xIOP/include/ps2x/iop/iop_host.h new file mode 100644 index 0000000..14e2eb7 --- /dev/null +++ b/ps2xIOP/include/ps2x/iop/iop_host.h @@ -0,0 +1,95 @@ +#pragma once + +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop +{ + enum class HostPathKind : uint32_t + { + ElfDirectory = 0, + CdRoot = 1, + CdImage = 2, + HostRoot = 3, + MemoryCardRoot = 4, + }; + + enum class LogLevel : uint32_t + { + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + }; + + enum class MemoryCardOperation : uint32_t + { + Init, + GetInfo, + Open, + Close, + Seek, + Read, + Write, + Flush, + Chdir, + GetDir, + SetFileInfo, + Delete, + Format, + Unformat, + Mkdir, + }; + + struct MemoryCardRequest + { + MemoryCardOperation operation = MemoryCardOperation::Init; + // The fifth argument is carried at sp + 16 by the EE ABI + std::array arguments{}; + }; + + class IopHost + { + public: + virtual ~IopHost() = default; + + virtual bool readGuest(uint32_t address, void *destination, size_t size) const = 0; + virtual bool writeGuest(uint32_t address, const void *source, size_t size) = 0; + virtual bool zeroGuest(uint32_t address, size_t size) = 0; + virtual bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const = 0; + virtual uint32_t allocateIopHandle(IopHandleKind kind) = 0; + virtual uint32_t allocateGuest(uint32_t size, uint32_t alignment) = 0; + virtual void freeGuest(uint32_t address) = 0; + + virtual void audioCommand(uint32_t sid, uint32_t function, GuestBuffer send, GuestBuffer receive) = 0; + + virtual std::string hostPath(HostPathKind kind) const = 0; + virtual std::string translateGuestPath(std::string_view path) const = 0; + virtual uint64_t openHostFile(std::string_view path) = 0; + virtual bool hostFileSize(uint64_t handle, uint64_t &size) const = 0; + virtual bool readHostFile(uint64_t handle, + uint64_t offset, + void *destination, + size_t size, + size_t &bytesRead) = 0; + virtual void closeHostFile(uint64_t handle) = 0; + + virtual int32_t memoryCard(const MemoryCardRequest &request) = 0; + + virtual bool hasGuestFunction(uint32_t address) const = 0; + virtual bool invokeGuestFunction(uint64_t callToken, + uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t *resultAddress) = 0; + + virtual void log(LogLevel level, std::string_view message) = 0; + }; +} diff --git a/ps2xIOP/include/ps2x/iop/iop_subsystem.h b/ps2xIOP/include/ps2x/iop/iop_subsystem.h new file mode 100644 index 0000000..0d086a0 --- /dev/null +++ b/ps2xIOP/include/ps2x/iop/iop_subsystem.h @@ -0,0 +1,40 @@ +#pragma once + +#include "ps2x/iop/iop_host.h" +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include + +namespace ps2x::iop +{ + class IopSubsystem + { + public: + explicit IopSubsystem(IopHost &host); + ~IopSubsystem(); + + IopSubsystem(const IopSubsystem &) = delete; + IopSubsystem &operator=(const IopSubsystem &) = delete; + IopSubsystem(IopSubsystem &&) noexcept; + IopSubsystem &operator=(IopSubsystem &&) noexcept; + + void setPluginSearchPaths(std::vector paths); + bool loadPlugins(std::string *error = nullptr); + + bool configure(const GameIdentity &identity, std::string *error = nullptr); + void reset(); + + [[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const; + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request); + void onSifTransfer(const SifTransfer &transfer); + + [[nodiscard]] DebugSnapshot debugSnapshot() const; + + private: + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/ps2xIOP/include/ps2x/iop/iop_types.h b/ps2xIOP/include/ps2x/iop/iop_types.h new file mode 100644 index 0000000..f84da65 --- /dev/null +++ b/ps2xIOP/include/ps2x/iop/iop_types.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include + +namespace ps2x::iop +{ + struct GuestBuffer + { + uint32_t address = 0; + uint32_t size = 0; + }; + + struct GameIdentity + { + std::string elfName; + uint32_t entryPoint = 0; + uint32_t crc32 = 0; + }; + + struct GameMatcher + { + std::string elfName; + uint32_t entryPoint = 0; + uint32_t crc32 = 0; + }; + + enum class RpcAbi : uint32_t + { + RuntimeDefault = 0, + Registers = 1, + Stack = 2, + }; + + struct RpcCallCandidate + { + uint32_t sendSize = 0; + uint32_t receiveAddress = 0; + uint32_t receiveSize = 0; + uint32_t endFunction = 0; + uint32_t endParameter = 0; + bool plausible = false; + }; + + struct RpcAbiRequest + { + uint32_t boundSid = 0; + uint32_t function = 0; + RpcCallCandidate registers; + RpcCallCandidate stack; + }; + + struct RpcRequest + { + uint64_t callToken = 0; + uint32_t clientAddress = 0; + uint32_t serverAddress = 0; + uint32_t serverFunction = 0; + uint32_t serverBuffer = 0; + uint32_t sid = 0; + uint32_t function = 0; + uint32_t mode = 0; + GuestBuffer send; + GuestBuffer receive; + uint32_t endFunction = 0; + uint32_t endParameter = 0; + }; + + enum class IopHandleKind : uint32_t + { + RpcServer = 0, + RpcPacket = 1, + }; + + enum class CallbackPolicy : uint32_t + { + RuntimeDefault = 0, + Suppress = 1, + }; + + enum class ServerDispatchPolicy : uint32_t + { + RuntimeDefault = 0, + Suppress = 1, + }; + + struct RpcResult + { + bool handled = false; + uint32_t resultAddress = 0; + bool signalNowaitCompletion = false; + bool signalCompletion = false; + CallbackPolicy callbackPolicy = CallbackPolicy::RuntimeDefault; + ServerDispatchPolicy serverDispatchPolicy = ServerDispatchPolicy::RuntimeDefault; + }; + + enum class SifTransferKind : uint32_t + { + SetDma = 0, + GetOtherData = 1, + }; + + enum class SifTransferPhase : uint32_t + { + BeforeCopy = 0, + AfterCopy = 1, + }; + + struct SifTransfer + { + SifTransferKind kind = SifTransferKind::SetDma; + SifTransferPhase phase = SifTransferPhase::AfterCopy; + uint32_t sourceAddress = 0; + uint32_t destinationAddress = 0; + uint32_t size = 0; + }; + + struct DebugMetric + { + std::string name; + uint64_t value = 0; + bool hexadecimal = false; + }; + + struct DebugService + { + std::string name; + std::vector sids; + bool profileSpecific = false; + std::vector metrics; + }; + + struct DebugSnapshot + { + std::string activeProfile; + std::string activeProvider; + std::vector services; + std::vector diagnostics; + }; +} diff --git a/ps2xIOP/include/ps2x/iop/plugin_api.h b/ps2xIOP/include/ps2x/iop/plugin_api.h new file mode 100644 index 0000000..79748ac --- /dev/null +++ b/ps2xIOP/include/ps2x/iop/plugin_api.h @@ -0,0 +1,307 @@ +#ifndef PS2X_IOP_PLUGIN_API_H +#define PS2X_IOP_PLUGIN_API_H + +#include +#include + +#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 diff --git a/ps2xIOP/src/builtin_profiles.cpp b/ps2xIOP/src/builtin_profiles.cpp new file mode 100644 index 0000000..b4db00c --- /dev/null +++ b/ps2xIOP/src/builtin_profiles.cpp @@ -0,0 +1,149 @@ +#include "iop_service.h" +#include "module_factories.h" + +#include + +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, + .suppressedCompletionCallbacks = {0x001FFD70u}, + }; + } + + 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 createBuiltinProfiles() + { + std::vector profiles; + + profiles.push_back({ + "recvx-us", + "builtin", + {.elfName = "slus_201.84"}, + [](IopHost &host, const GameIdentity &) + { + ServiceList services; + services.emplace_back(createTsnddrvService(host, recvxTsnddrvBindings())); + services.emplace_back(createCriDtxService(host, recvxCriDtxBindings())); + return services; + }, + }); + + profiles.push_back({ + "lotr-two-towers-us", + "builtin", + {.elfName = "SLUS_205.78"}, + [](IopHost &host, const GameIdentity &) + { + ServiceList services; + services.emplace_back(createClFileService(host, lotrClFileBindings())); + services.emplace_back(createSoundUpdateStubService(host, lotrSoundBindings())); + return services; + }, + }); + + profiles.push_back({ + "fatal-frame-us", + "builtin", + {.elfName = "SLUS_203.88"}, + [](IopHost &host, const GameIdentity &) + { + ServiceList services; + services.emplace_back(createSdrdrvService(host, fatalFrameSdrdrvBindings())); + return services; + }, + }); + + return profiles; + } +} diff --git a/ps2xIOP/src/iop_service.h b/ps2xIOP/src/iop_service.h new file mode 100644 index 0000000..7e3667a --- /dev/null +++ b/ps2xIOP/src/iop_service.h @@ -0,0 +1,55 @@ +#pragma once + +#include "ps2x/iop/iop_host.h" +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopService + { + public: + virtual ~IopService() = default; + + [[nodiscard]] virtual std::string_view name() const = 0; + [[nodiscard]] virtual std::span sids() const = 0; + virtual void reset() = 0; + + [[nodiscard]] virtual RpcAbi selectRpcAbi(const RpcAbiRequest &request) const + { + (void)request; + return RpcAbi::RuntimeDefault; + } + + [[nodiscard]] virtual RpcResult handleRpc(const RpcRequest &request) = 0; + + virtual void onSifTransfer(const SifTransfer &transfer) + { + (void)transfer; + } + + virtual void appendDebugMetrics(std::vector &metrics) const + { + (void)metrics; + } + }; + + using ServiceList = std::vector>; + using ProfileFactory = std::function; + + struct ProfileDefinition + { + std::string id; + std::string provider = "builtin"; + GameMatcher matcher; + ProfileFactory factory; + }; + + ServiceList createCoreServices(IopHost &host); + std::vector createBuiltinProfiles(); +} diff --git a/ps2xIOP/src/iop_subsystem.cpp b/ps2xIOP/src/iop_subsystem.cpp new file mode 100644 index 0000000..d848f8d --- /dev/null +++ b/ps2xIOP/src/iop_subsystem.cpp @@ -0,0 +1,339 @@ +#include "ps2x/iop/iop_subsystem.h" + +#include "iop_service.h" +#include "plugin_loader.h" + +#include +#include +#include +#include +#include +#include + +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(lhs[i]); + const auto right = static_cast(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()) + { + rebuildRoutes(); + } + + void rebuildRoutes() + { + routes.clear(); + auto addLayer = [&](detail::ServiceList &services, bool profileSpecific) -> bool + { + std::unordered_map layer; + for (const auto &service : services) + { + if (!service) + { + 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; + } + } + } + for (const auto &[sid, service] : layer) + { + routes[sid] = service; + } + return true; + }; + + routesValid = addLayer(coreServices, false) && addLayer(profileServices, true); + } + + IopHost &host; + detail::PluginCatalog pluginCatalog; + detail::ServiceList coreServices; + detail::ServiceList profileServices; + std::vector profiles; + std::unordered_map routes; + std::vector pluginSearchPaths; + std::vector diagnostics; + std::string activeProfile; + std::string activeProvider; + std::string lastError; + bool routesValid = true; + }; + + IopSubsystem::IopSubsystem(IopHost &host) + : m_impl(std::make_unique(host)) + { + } + + IopSubsystem::~IopSubsystem() = default; + IopSubsystem::IopSubsystem(IopSubsystem &&) noexcept = default; + IopSubsystem &IopSubsystem::operator=(IopSubsystem &&) noexcept = default; + + void IopSubsystem::setPluginSearchPaths(std::vector paths) + { + m_impl->pluginSearchPaths = std::move(paths); + } + + bool IopSubsystem::loadPlugins(std::string *error) + { + return m_impl->pluginCatalog.load(m_impl->pluginSearchPaths, m_impl->profiles, m_impl->diagnostics, error); + } + + bool IopSubsystem::configure(const GameIdentity &identity, std::string *error) + { + m_impl->profileServices.clear(); + m_impl->activeProfile.clear(); + m_impl->activeProvider.clear(); + m_impl->lastError.clear(); + + const detail::ProfileDefinition *selected = nullptr; + const detail::ProfileDefinition *selectedTie = nullptr; + int selectedSpecificity = -1; + for (const auto &profile : m_impl->profiles) + { + const int specificity = matchSpecificity(profile.matcher, identity); + if (specificity < 0) + { + continue; + } + if (specificity > selectedSpecificity) + { + selected = &profile; + selectedTie = nullptr; + selectedSpecificity = specificity; + continue; + } + if (specificity == selectedSpecificity && selected) + { + selectedTie = &profile; + } + } + + if (selected && selectedTie) + { + m_impl->lastError = "ambiguous IOP profiles '" + selected->provider + ":" + + selected->id + "' and '" + selectedTie->provider + ":" + + selectedTie->id + "'"; + if (error) + { + *error = m_impl->lastError; + } + m_impl->rebuildRoutes(); + return false; + } + + if (selected) + { + try + { + m_impl->profileServices = selected->factory(m_impl->host, identity); + m_impl->activeProfile = selected->id; + m_impl->activeProvider = selected->provider; + } + catch (const std::exception &exception) + { + m_impl->lastError = "failed to create IOP profile '" + selected->id + "': " + exception.what(); + if (error) + { + *error = m_impl->lastError; + } + m_impl->rebuildRoutes(); + return false; + } + catch (...) + { + m_impl->lastError = "failed to create IOP profile '" + selected->id + "': unknown plugin exception"; + if (error) + { + *error = m_impl->lastError; + } + m_impl->rebuildRoutes(); + return false; + } + } + + m_impl->rebuildRoutes(); + if (!m_impl->routesValid) + { + const std::string routeError = m_impl->lastError; + m_impl->profileServices.clear(); + m_impl->activeProfile.clear(); + m_impl->activeProvider.clear(); + m_impl->rebuildRoutes(); + m_impl->lastError = routeError; + if (error) + { + *error = m_impl->lastError; + } + return false; + } + + reset(); + return true; + } + + void IopSubsystem::reset() + { + for (auto &service : m_impl->coreServices) + { + if (service) + { + service->reset(); + } + } + for (auto &service : m_impl->profileServices) + { + if (service) + { + service->reset(); + } + } + } + + RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const + { + for (const auto &service : m_impl->profileServices) + { + if (service) + { + const RpcAbi selected = service->selectRpcAbi(request); + if (selected != RpcAbi::RuntimeDefault) + { + return selected; + } + } + } + for (const auto &service : m_impl->coreServices) + { + if (service) + { + const RpcAbi selected = service->selectRpcAbi(request); + if (selected != RpcAbi::RuntimeDefault) + { + return selected; + } + } + } + return RpcAbi::RuntimeDefault; + } + + RpcResult IopSubsystem::handleRpc(const RpcRequest &request) + { + const auto it = m_impl->routes.find(request.sid); + if (it == m_impl->routes.end() || !it->second) + { + return {}; + } + return it->second->handleRpc(request); + } + + void IopSubsystem::onSifTransfer(const SifTransfer &transfer) + { + for (auto &service : m_impl->coreServices) + { + if (service) + { + service->onSifTransfer(transfer); + } + } + for (auto &service : m_impl->profileServices) + { + if (service) + { + service->onSifTransfer(transfer); + } + } + } + + DebugSnapshot IopSubsystem::debugSnapshot() const + { + DebugSnapshot snapshot; + snapshot.activeProfile = m_impl->activeProfile; + snapshot.activeProvider = m_impl->activeProvider; + snapshot.diagnostics = m_impl->diagnostics; + if (!m_impl->lastError.empty()) + { + snapshot.diagnostics.push_back(m_impl->lastError); + } + + auto append = [&](const detail::ServiceList &services, bool profileSpecific) + { + for (const auto &service : services) + { + if (!service) + { + continue; + } + DebugService row; + row.name = service->name(); + row.sids.assign(service->sids().begin(), service->sids().end()); + row.profileSpecific = profileSpecific; + service->appendDebugMetrics(row.metrics); + snapshot.services.push_back(std::move(row)); + } + }; + append(m_impl->coreServices, false); + append(m_impl->profileServices, true); + return snapshot; + } +} diff --git a/ps2xIOP/src/module_factories.h b/ps2xIOP/src/module_factories.h new file mode 100644 index 0000000..42b1c30 --- /dev/null +++ b/ps2xIOP/src/module_factories.h @@ -0,0 +1,156 @@ +#pragma once + +#include "iop_service.h" + +#include +#include +#include +#include + +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 checksumCandidates; + uint32_t busyFlagAddress = 0u; + std::vector 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; + std::vector 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 createDbcmanService(IopHost &host); + std::unique_ptr createLibSdService(IopHost &host); + std::unique_ptr createMcservService(IopHost &host); + std::unique_ptr createTsnddrvService(IopHost &host, TsnddrvBindings bindings); + std::unique_ptr createCriDtxService(IopHost &host, CriDtxBindings bindings); + std::unique_ptr createClFileService(IopHost &host, ClFileBindings bindings); + std::unique_ptr createSoundUpdateStubService(IopHost &host, SoundUpdateStubBindings bindings); + std::unique_ptr createSdrdrvService(IopHost &host, SdrdrvBindings bindings); +} diff --git a/ps2xIOP/src/modules/clfile.cpp b/ps2xIOP/src/modules/clfile.cpp new file mode 100644 index 0000000..93bd236 --- /dev/null +++ b/ps2xIOP/src/modules/clfile.cpp @@ -0,0 +1,635 @@ +#include "module_factories.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 sids() const override + { + return m_sids; + } + + void reset() override + { + std::lock_guard 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( + std::min(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 lock(m_mutex); + loadHandle = allocateLoadLocked(status, fileSize); + } + writeRpcResult(static_cast(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 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( + std::min(hostFileSize, 0x7FFFFFFFull)); + + uint32_t handle = 0u; + { + std::lock_guard 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 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 bytes(std::min(requestedBytes, + m_bindings.rpc.maximumReadBytes)); + size_t bytesRead = 0u; + bool readFailed = false; + { + std::lock_guard 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(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 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(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 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 &metrics) const override + { + std::lock_guard 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 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 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(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(destinationAddress) + 1ull) + { + return false; + } + + std::vector chunk(kChunkBytes); + uint64_t copied = 0u; + while (copied < bytesToCopy) + { + const size_t wanted = static_cast( + std::min(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(copied); + if (!m_host.writeGuest(chunkAddress, chunk.data(), received)) + { + return false; + } + copied += received; + } + return true; + } + + IopHost &m_host; + ClFileBindings m_bindings; + std::array m_sids; + mutable std::mutex m_mutex; + std::unordered_map m_fileHandles; + std::unordered_map m_loads; + uint32_t m_nextFileHandle = 1u; + uint32_t m_nextLoadHandle = 0u; + std::string m_root; + }; + } + + std::unique_ptr createClFileService(IopHost &host, + ClFileBindings bindings) + { + const ClFileRpcLayout &rpc = bindings.rpc; + const std::array functions = { + rpc.directLoadFunction, + rpc.getStatusFunction, + rpc.initializeFunction, + rpc.waitFunction, + rpc.getSizeFunction, + rpc.openFunction, + rpc.closeFunction, + rpc.readFunction, + rpc.secondaryWaitFunction, + rpc.setRootFunction, + }; + std::unordered_set 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(host, std::move(bindings)); + } +} diff --git a/ps2xIOP/src/modules/cri_dtx.cpp b/ps2xIOP/src/modules/cri_dtx.cpp new file mode 100644 index 0000000..839629b --- /dev/null +++ b/ps2xIOP/src/modules/cri_dtx.cpp @@ -0,0 +1,1324 @@ +#include "../module_factories.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kDtxHeaderSize = 16u; + constexpr uint32_t kDtxCommandSize = 16u; + constexpr uint32_t kMaxDtxCommands = 128u; + constexpr uint32_t kMinimumDtxWorkSize = 64u; + constexpr uint32_t kDefaultSjrmtCapacity = 0x4000u; + constexpr uint32_t kMaximumSjrmtCapacity = 0x01000000u; + + template + bool readGuestPod(const IopHost &host, uint32_t address, T &value) + { + value = {}; + return host.readGuest(address, &value, sizeof(value)); + } + + template + bool writeGuestPod(IopHost &host, uint32_t address, const T &value) + { + return host.writeGuest(address, &value, sizeof(value)); + } + + uint32_t normalizeSjrmtCapacity(uint32_t requestedBytes) + { + if (requestedBytes == 0u || requestedBytes > kMaximumSjrmtCapacity) + { + return kDefaultSjrmtCapacity; + } + return requestedBytes; + } + + bool looksLikeCreate34Candidate(const RpcCallCandidate &candidate) + { + return candidate.receiveAddress != 0u && + candidate.receiveSize >= sizeof(uint32_t) && + candidate.receiveSize <= 0x40u && + candidate.sendSize >= 12u && + candidate.sendSize <= 0x1000u; + } + + class CriDtxService final : public IopService + { + public: + CriDtxService(IopHost &host, CriDtxBindings bindings) + : m_host(host), + m_bindings(std::move(bindings)), + m_sids{m_bindings.sid} + { + reset(); + } + + [[nodiscard]] std::string_view name() const override + { + return m_bindings.serviceName; + } + + [[nodiscard]] std::span sids() const override + { + return m_sids; + } + + void reset() override + { + std::lock_guard lock(m_mutex); + m_remoteById.clear(); + m_transferById.clear(); + m_sjxByHandle.clear(); + m_ps2RnaByHandle.clear(); + m_sjrmtByHandle.clear(); + m_nextUrpcObject = m_bindings.urpcObjectBase; + m_dmaAcks = 0u; + m_dmaMisses = 0u; + m_urpcCalls = 0u; + } + + [[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const override + { + if (request.boundSid == m_bindings.sid && + request.function == 0x422u && + request.stack.plausible && + looksLikeCreate34Candidate(request.stack) && + !looksLikeCreate34Candidate(request.registers)) + { + return RpcAbi::Stack; + } + return RpcAbi::RuntimeDefault; + } + + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override + { + RpcResult result{}; + if (request.sid != m_bindings.sid) + { + return result; + } + + // DTX owns this SID. Its base protocol intentionally bypasses generic EE + // server dispatch, and URPC dispatch is gated by the guest function table. + result.serverDispatchPolicy = ServerDispatchPolicy::Suppress; + + const bool isUrpc = request.function >= 0x400u && request.function < 0x500u; + const uint32_t command = isUrpc ? (request.function & 0xFFu) : 0u; + uint32_t urpcFunction = 0u; + uint32_t urpcObject = 0u; + if (isUrpc && command < 64u) + { + (void)readGuestPod(m_host, + m_bindings.urpcFunctionTableBase + (command * sizeof(uint32_t)), + urpcFunction); + (void)readGuestPod(m_host, + m_bindings.urpcObjectTableBase + (command * sizeof(uint32_t)), + urpcObject); + } + (void)urpcObject; + + const bool hasUrpcHandler = isUrpc && command < 64u && urpcFunction != 0u; + if (hasUrpcHandler && request.serverFunction != 0u) + { + uint32_t serverResult = 0u; + if (m_host.invokeGuestFunction(request.callToken, + request.serverFunction, + request.function, + request.serverBuffer, + request.send.size, + 0u, + &serverResult)) + { + result.handled = true; + result.resultAddress = serverResult; + if (result.resultAddress == 0u && request.serverBuffer != 0u) + { + result.resultAddress = request.serverBuffer; + } + if (result.resultAddress == 0u && request.receive.address != 0u) + { + result.resultAddress = request.receive.address; + } + return result; + } + } + + if (hasUrpcHandler && + request.send.address != 0u && + request.send.size > 0u) + { + uint32_t dispatcherResult = 0u; + if (m_host.invokeGuestFunction(request.callToken, + m_bindings.dispatcherFunctionAddress, + request.function, + request.send.address, + request.send.size, + 0u, + &dispatcherResult)) + { + result.handled = true; + result.resultAddress = dispatcherResult != 0u + ? dispatcherResult + : request.send.address; + return result; + } + } + + if (request.function == 2u && + request.receive.address != 0u && + request.receive.size >= sizeof(uint32_t)) + { + return handleCreateTransport(request, result); + } + if (request.function == 3u) + { + return handleDestroyTransport(request, result); + } + if (isUrpc) + { + return emulateUrpc(request, command, result); + } + + // Unknown calls remain visibly unhandled, but must never escape to a + // generic registered server for this configured SID. + return result; + } + + void onSifTransfer(const SifTransfer &transfer) override + { + if (transfer.kind != SifTransferKind::SetDma || + transfer.phase != SifTransferPhase::AfterCopy || + transfer.size < kMinimumDtxWorkSize) + { + return; + } + + uint32_t normalizedSource = 0u; + uint32_t normalizedDestination = 0u; + if (!m_host.normalizeGuestAddress(transfer.sourceAddress, normalizedSource) || + !m_host.normalizeGuestAddress(transfer.destinationAddress, normalizedDestination)) + { + return; + } + + TransferState matched{}; + bool found = false; + { + std::lock_guard lock(m_mutex); + for (const auto &[id, state] : m_transferById) + { + (void)id; + if (matchesTransfer(state, + normalizedSource, + normalizedDestination, + transfer.size)) + { + matched = state; + found = true; + break; + } + } + + if (!found) + { + found = inferTransferFromPayloadLocked(normalizedSource, + normalizedDestination, + transfer.size, + matched); + } + + if (!found) + { + ++m_dmaMisses; + } + } + + if (!found) + { + return; + } + + const uint32_t footerAddress = + matched.eeWorkAddress + matched.workSize - sizeof(uint32_t); + uint32_t ticket = 0u; + if (!readGuestPod(m_host, footerAddress, ticket)) + { + return; + } + (void)writeGuestPod(m_host, footerAddress, ticket + 1u); + + if (matched.dtxId == 0u) + { + applySjxPayload(matched); + } + else if (matched.dtxId == 1u) + { + applyPs2RnaPayload(matched); + } + + std::lock_guard lock(m_mutex); + ++m_dmaAcks; + } + + void appendDebugMetrics(std::vector &metrics) const override + { + std::lock_guard lock(m_mutex); + metrics.push_back({"sid", m_bindings.sid, true}); + metrics.push_back({"remote_handles", m_remoteById.size(), false}); + metrics.push_back({"transfers", m_transferById.size(), false}); + metrics.push_back({"sjx_objects", m_sjxByHandle.size(), false}); + metrics.push_back({"ps2rna_objects", m_ps2RnaByHandle.size(), false}); + metrics.push_back({"sjrmt_objects", m_sjrmtByHandle.size(), false}); + metrics.push_back({"next_urpc_object", m_nextUrpcObject, true}); + metrics.push_back({"urpc_calls", m_urpcCalls, false}); + metrics.push_back({"dma_acks", m_dmaAcks, false}); + metrics.push_back({"dma_misses", m_dmaMisses, false}); + } + + private: + struct TransferState + { + uint32_t dtxId = 0u; + uint32_t remoteHandle = 0u; + uint32_t eeWorkAddress = 0u; + uint32_t iopWorkAddress = 0u; + uint32_t workSize = 0u; + }; + + struct SjxState + { + uint32_t handle = 0u; + uint32_t sourceSjHandle = 0u; + uint32_t destinationSjHandle = 0u; + uint32_t line = 0u; + uint32_t eeObjectAddress = 0u; + uint16_t xid = 0u; + }; + + struct Ps2RnaState + { + uint32_t handle = 0u; + uint32_t maxChannels = 0u; + uint32_t sjHandle0 = 0u; + uint32_t sjHandle1 = 0u; + uint32_t channelCount = 0u; + uint32_t sampleFrequency = 0u; + uint32_t volume = 0u; + bool playEnabled = false; + }; + + struct SjrmtState + { + uint32_t handle = 0u; + uint32_t mode = 0u; + uint32_t workAddress = 0u; + uint32_t workSize = 0u; + uint32_t readPosition = 0u; + uint32_t writePosition = 0u; + uint32_t roomBytes = 0u; + uint32_t dataBytes = 0u; + uint32_t uuid0 = 0u; + uint32_t uuid1 = 0u; + uint32_t uuid2 = 0u; + uint32_t uuid3 = 0u; + }; + + static bool matchesTransfer(const TransferState &state, + uint32_t sourceAddress, + uint32_t destinationAddress, + uint32_t size) + { + (void)destinationAddress; + return state.eeWorkAddress != 0u && + state.workSize >= kMinimumDtxWorkSize && + state.eeWorkAddress == sourceAddress && + state.workSize == size; + } + + uint32_t normalizeAddress(uint32_t address) const + { + uint32_t normalized = 0u; + (void)m_host.normalizeGuestAddress(address, normalized); + return normalized; + } + + RpcResult handleCreateTransport(const RpcRequest &request, RpcResult result) + { + uint32_t dtxId = 0u; + uint32_t eeWorkAddress = 0u; + uint32_t iopWorkAddress = 0u; + uint32_t workSize = 0u; + if (request.send.address != 0u && request.send.size >= sizeof(uint32_t)) + { + (void)readGuestPod(m_host, request.send.address, dtxId); + } + if (request.send.address != 0u && request.send.size >= 4u * sizeof(uint32_t)) + { + (void)readGuestPod(m_host, request.send.address + 4u, eeWorkAddress); + (void)readGuestPod(m_host, request.send.address + 8u, iopWorkAddress); + (void)readGuestPod(m_host, request.send.address + 12u, workSize); + } + + const uint32_t normalizedEeWorkAddress = normalizeAddress(eeWorkAddress); + const uint32_t normalizedIopWorkAddress = normalizeAddress(iopWorkAddress); + + uint32_t remoteHandle = 0u; + { + std::lock_guard lock(m_mutex); + const auto existing = m_remoteById.find(dtxId); + if (existing != m_remoteById.end()) + { + remoteHandle = existing->second; + } + + if (remoteHandle == 0u) + { + remoteHandle = m_host.allocateIopHandle(IopHandleKind::RpcServer); + if (remoteHandle == 0u) + { + remoteHandle = m_host.allocateIopHandle(IopHandleKind::RpcPacket); + } + if (remoteHandle == 0u) + { + remoteHandle = m_bindings.rpcServerPoolBase + + ((dtxId & 0xFFu) * m_bindings.rpcServerStride); + } + m_remoteById[dtxId] = remoteHandle; + } + + m_transferById[dtxId] = TransferState{ + dtxId, + remoteHandle, + normalizedEeWorkAddress, + normalizedIopWorkAddress, + workSize, + }; + } + + (void)writeGuestPod(m_host, request.receive.address, remoteHandle); + if (request.receive.size > sizeof(uint32_t)) + { + (void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t), + request.receive.size - sizeof(uint32_t)); + } + + result.handled = true; + result.resultAddress = request.receive.address; + return result; + } + + RpcResult handleDestroyTransport(const RpcRequest &request, RpcResult result) + { + uint32_t remoteHandle = 0u; + if (request.send.address != 0u && + request.send.size >= sizeof(uint32_t) && + readGuestPod(m_host, request.send.address, remoteHandle) && + remoteHandle != 0u) + { + std::lock_guard lock(m_mutex); + for (auto it = m_remoteById.begin(); it != m_remoteById.end(); ++it) + { + if (it->second == remoteHandle) + { + m_transferById.erase(it->first); + m_remoteById.erase(it); + break; + } + } + } + + if (request.receive.address != 0u && request.receive.size > 0u) + { + (void)m_host.zeroGuest(request.receive.address, request.receive.size); + } + result.handled = true; + result.resultAddress = request.receive.address; + return result; + } + + uint32_t allocateUrpcHandleLocked() + { + if (m_nextUrpcObject < m_bindings.urpcObjectBase || + m_nextUrpcObject >= m_bindings.urpcObjectLimit) + { + m_nextUrpcObject = m_bindings.urpcObjectBase; + } + + for (uint32_t attempt = 0u; attempt < 4096u; ++attempt) + { + const uint32_t candidate = m_nextUrpcObject; + m_nextUrpcObject += m_bindings.urpcObjectStride; + if (m_nextUrpcObject < m_bindings.urpcObjectBase || + m_nextUrpcObject >= m_bindings.urpcObjectLimit) + { + m_nextUrpcObject = m_bindings.urpcObjectBase; + } + + if (candidate < m_bindings.urpcObjectBase || + candidate >= m_bindings.urpcObjectLimit || + m_sjrmtByHandle.find(candidate) != m_sjrmtByHandle.end() || + m_sjxByHandle.find(candidate) != m_sjxByHandle.end() || + m_ps2RnaByHandle.find(candidate) != m_ps2RnaByHandle.end()) + { + continue; + } + + const bool usedByRemote = + std::any_of(m_remoteById.begin(), m_remoteById.end(), + [candidate](const auto &entry) + { return entry.second == candidate; }); + if (!usedByRemote) + { + return candidate; + } + } + + return m_bindings.urpcObjectBase; + } + + RpcResult emulateUrpc(const RpcRequest &request, + uint32_t command, + RpcResult result) + { + std::array output = {1u, 0u, 0u, 0u}; + uint32_t outputWordCount = 1u; + + auto readSendWord = [&](uint32_t index, uint32_t &value) + { + const uint64_t byteOffset = + static_cast(index) * sizeof(uint32_t); + if (request.send.address == 0u || + request.send.size < byteOffset + sizeof(uint32_t)) + { + value = 0u; + return false; + } + return readGuestPod(m_host, + request.send.address + static_cast(byteOffset), + value); + }; + + switch (command) + { + case 0u: // SJX_CREATE + { + uint32_t sourceSjHandle = 0u; + uint32_t destinationSjHandle = 0u; + uint32_t line = 0u; + uint32_t eeObjectAddress = 0u; + (void)readSendWord(0u, sourceSjHandle); + (void)readSendWord(1u, destinationSjHandle); + (void)readSendWord(2u, line); + (void)readSendWord(3u, eeObjectAddress); + + std::lock_guard lock(m_mutex); + const uint32_t handle = allocateUrpcHandleLocked(); + m_sjxByHandle[handle] = SjxState{ + handle, + sourceSjHandle, + destinationSjHandle, + line, + eeObjectAddress, + 0u, + }; + output[0] = handle != 0u ? handle : 1u; + break; + } + case 1u: // SJX_DESTROY + { + uint32_t handle = 0u; + (void)readSendWord(0u, handle); + std::lock_guard lock(m_mutex); + m_sjxByHandle.erase(handle); + output[0] = 1u; + break; + } + case 2u: // SJX_RESET + { + uint32_t handle = 0u; + uint32_t xid = 0u; + (void)readSendWord(0u, handle); + (void)readSendWord(1u, xid); + std::lock_guard lock(m_mutex); + const auto it = m_sjxByHandle.find(handle); + if (it != m_sjxByHandle.end()) + { + it->second.xid = static_cast(xid & 0xFFFFu); + } + output[0] = 1u; + break; + } + case 8u: // PS2RNA_CREATE + { + uint32_t maxChannels = 0u; + uint32_t sjHandle0 = 0u; + uint32_t sjHandle1 = 0u; + (void)readSendWord(0u, maxChannels); + (void)readSendWord(2u, sjHandle0); + (void)readSendWord(3u, sjHandle1); + + std::lock_guard lock(m_mutex); + const uint32_t handle = allocateUrpcHandleLocked(); + m_ps2RnaByHandle[handle] = Ps2RnaState{ + handle, + maxChannels, + sjHandle0, + sjHandle1, + maxChannels, + 0u, + 0u, + false, + }; + output[0] = handle != 0u ? handle : 1u; + break; + } + case 9u: // PS2RNA_DESTROY + { + uint32_t handle = 0u; + (void)readSendWord(0u, handle); + std::lock_guard lock(m_mutex); + m_ps2RnaByHandle.erase(handle); + output[0] = 1u; + break; + } + case 32u: // SJRMT_RBF_CREATE + case 33u: // SJRMT_MEM_CREATE + case 34u: // SJRMT_UNI_CREATE + { + uint32_t argument0 = 0u; + uint32_t argument1 = 0u; + uint32_t argument2 = 0u; + (void)readSendWord(0u, argument0); + (void)readSendWord(1u, argument1); + (void)readSendWord(2u, argument2); + + uint32_t mode = 0u; + uint32_t workAddress = 0u; + uint32_t workSize = 0u; + if (command == 34u) + { + mode = argument0; + workAddress = argument1; + workSize = argument2; + } + else if (command == 33u) + { + workAddress = argument0; + workSize = argument1; + } + else + { + workAddress = argument0; + workSize = argument1 != 0u ? argument1 : argument2; + } + workSize = normalizeSjrmtCapacity(workSize); + + std::lock_guard lock(m_mutex); + const uint32_t handle = allocateUrpcHandleLocked(); + m_sjrmtByHandle[handle] = SjrmtState{ + handle, + mode, + workAddress, + workSize, + 0u, + 0u, + workSize, + 0u, + 0x53524D54u, + handle, + workAddress, + workSize, + }; + output[0] = handle != 0u ? handle : 1u; + break; + } + case 35u: // SJRMT_DESTROY + { + uint32_t handle = 0u; + (void)readSendWord(0u, handle); + std::lock_guard lock(m_mutex); + m_sjrmtByHandle.erase(handle); + output[0] = 1u; + break; + } + case 36u: // SJRMT_GET_UUID + { + uint32_t handle = 0u; + (void)readSendWord(0u, handle); + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + if (it != m_sjrmtByHandle.end()) + { + output[0] = it->second.uuid0; + output[1] = it->second.uuid1; + output[2] = it->second.uuid2; + output[3] = it->second.uuid3; + } + else + { + output = {0u, 0u, 0u, 0u}; + } + outputWordCount = 4u; + break; + } + case 37u: // SJRMT_RESET + { + uint32_t handle = 0u; + (void)readSendWord(0u, handle); + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + if (it != m_sjrmtByHandle.end()) + { + const uint32_t capacity = it->second.workSize == 0u + ? kDefaultSjrmtCapacity + : it->second.workSize; + it->second.readPosition = 0u; + it->second.writePosition = 0u; + it->second.roomBytes = capacity; + it->second.dataBytes = 0u; + } + output[0] = 1u; + break; + } + case 38u: // SJRMT_GET_CHUNK + { + uint32_t handle = 0u; + uint32_t streamId = 0u; + uint32_t requestedBytes = 0u; + (void)readSendWord(0u, handle); + (void)readSendWord(1u, streamId); + (void)readSendWord(2u, requestedBytes); + + uint32_t pointer = 0u; + uint32_t length = 0u; + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + if (it != m_sjrmtByHandle.end()) + { + SjrmtState &state = it->second; + const uint32_t capacity = state.workSize == 0u + ? kDefaultSjrmtCapacity + : state.workSize; + if (streamId == 0u) + { + length = std::min(requestedBytes, state.roomBytes); + pointer = state.workAddress + + (capacity != 0u ? state.writePosition % capacity : 0u); + if (capacity != 0u) + { + state.writePosition = (state.writePosition + length) % capacity; + } + state.roomBytes -= length; + } + else if (streamId == 1u) + { + length = std::min(requestedBytes, state.dataBytes); + pointer = state.workAddress + + (capacity != 0u ? state.readPosition % capacity : 0u); + if (capacity != 0u) + { + state.readPosition = (state.readPosition + length) % capacity; + } + state.dataBytes -= length; + } + } + output[0] = pointer; + output[1] = length; + outputWordCount = 2u; + break; + } + case 39u: // SJRMT_UNGET_CHUNK + { + uint32_t handle = 0u; + uint32_t streamId = 0u; + uint32_t length = 0u; + (void)readSendWord(0u, handle); + (void)readSendWord(1u, streamId); + (void)readSendWord(3u, length); + + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + if (it != m_sjrmtByHandle.end()) + { + SjrmtState &state = it->second; + const uint32_t capacity = state.workSize == 0u + ? kDefaultSjrmtCapacity + : state.workSize; + const uint32_t delta = capacity == 0u ? 0u : length % capacity; + if (streamId == 0u) + { + if (capacity != 0u) + { + state.writePosition = + (state.writePosition + capacity - delta) % capacity; + } + state.roomBytes = std::min(capacity, state.roomBytes + length); + } + else if (streamId == 1u) + { + if (capacity != 0u) + { + state.readPosition = + (state.readPosition + capacity - delta) % capacity; + } + state.dataBytes = std::min(capacity, state.dataBytes + length); + } + } + output[0] = 1u; + break; + } + case 40u: // SJRMT_PUT_CHUNK + { + uint32_t handle = 0u; + uint32_t streamId = 0u; + uint32_t length = 0u; + (void)readSendWord(0u, handle); + (void)readSendWord(1u, streamId); + (void)readSendWord(3u, length); + + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + if (it != m_sjrmtByHandle.end()) + { + SjrmtState &state = it->second; + const uint32_t capacity = state.workSize == 0u + ? kDefaultSjrmtCapacity + : state.workSize; + if (streamId == 0u) + { + state.roomBytes = std::min(capacity, state.roomBytes + length); + } + else if (streamId == 1u) + { + state.dataBytes = std::min(capacity, state.dataBytes + length); + } + } + output[0] = 1u; + break; + } + case 41u: // SJRMT_GET_NUM_DATA + { + uint32_t handle = 0u; + uint32_t streamId = 0u; + (void)readSendWord(0u, handle); + (void)readSendWord(1u, streamId); + + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + output[0] = it != m_sjrmtByHandle.end() + ? (streamId == 0u ? it->second.roomBytes + : it->second.dataBytes) + : 0u; + break; + } + case 42u: // SJRMT_IS_GET_CHUNK + { + uint32_t handle = 0u; + uint32_t streamId = 0u; + uint32_t requestedBytes = 0u; + (void)readSendWord(0u, handle); + (void)readSendWord(1u, streamId); + (void)readSendWord(2u, requestedBytes); + + uint32_t available = 0u; + std::lock_guard lock(m_mutex); + const auto it = m_sjrmtByHandle.find(handle); + if (it != m_sjrmtByHandle.end()) + { + available = streamId == 0u ? it->second.roomBytes + : it->second.dataBytes; + } + output[0] = available >= requestedBytes ? 1u : 0u; + output[1] = available; + outputWordCount = 2u; + break; + } + case 43u: // SJRMT_INIT + case 44u: // SJRMT_FINISH + output[0] = 1u; + break; + default: + { + uint32_t value = 1u; + if (request.send.address != 0u && + request.send.size >= sizeof(uint32_t)) + { + (void)readGuestPod(m_host, request.send.address, value); + } + if (command == 0u) + { + std::lock_guard lock(m_mutex); + value = allocateUrpcHandleLocked(); + } + output[0] = value != 0u ? value : 1u; + break; + } + } + + if (request.receive.address != 0u && request.receive.size > 0u) + { + const uint32_t receiveWordCapacity = + request.receive.size / sizeof(uint32_t); + const uint32_t wordsToWrite = + std::min(outputWordCount, receiveWordCapacity); + for (uint32_t index = 0u; index < wordsToWrite; ++index) + { + (void)writeGuestPod(m_host, + request.receive.address + + (index * sizeof(uint32_t)), + output[index]); + } + + // The original HLE deliberately makes rbuf[1] available to command + // 42 callers even when their declared output word count is one. + if (command == 42u && outputWordCount > 1u) + { + (void)writeGuestPod(m_host, + request.receive.address + sizeof(uint32_t), + output[1]); + } + + const uint32_t writtenBytes = wordsToWrite * sizeof(uint32_t); + if (request.receive.size > writtenBytes) + { + (void)m_host.zeroGuest(request.receive.address + writtenBytes, + request.receive.size - writtenBytes); + } + } + + { + std::lock_guard lock(m_mutex); + ++m_urpcCalls; + } + result.handled = true; + result.resultAddress = request.receive.address; + return result; + } + + bool hasReadableRange(uint32_t address, uint32_t length) const + { + if (length == 0u) + { + return true; + } + const uint32_t lastAddress = address + length - 1u; + if (address > lastAddress) + { + return false; + } + + uint8_t byte = 0u; + return m_host.readGuest(address, &byte, sizeof(byte)) && + m_host.readGuest(lastAddress, &byte, sizeof(byte)); + } + + bool readValidCommandCount(uint32_t workAddress, + uint32_t workSize, + uint32_t &commandCount) const + { + commandCount = 0u; + if (workSize < kMinimumDtxWorkSize || + !readGuestPod(m_host, workAddress, commandCount) || + commandCount == 0u || + commandCount > kMaxDtxCommands) + { + return false; + } + + const uint64_t commandBytes = + static_cast(commandCount) * kDtxCommandSize; + const uint64_t requiredBytes = + kDtxHeaderSize + commandBytes + sizeof(uint32_t); + return requiredBytes <= workSize; + } + + bool looksLikeSjxPayloadLocked(uint32_t workAddress, uint32_t workSize) const + { + uint32_t commandCount = 0u; + if (!readValidCommandCount(workAddress, workSize, commandCount)) + { + return false; + } + + for (uint32_t index = 0u; index < commandCount; ++index) + { + const uint32_t commandAddress = + workAddress + kDtxHeaderSize + (index * kDtxCommandSize); + std::array command{}; + if (!m_host.readGuest(commandAddress, command.data(), command.size())) + { + break; + } + + const uint8_t commandNumber = command[0]; + const uint8_t line = command[1]; + uint16_t xid = 0u; + uint32_t sjxHandle = 0u; + uint32_t chunkDataAddress = 0u; + uint32_t chunkLength = 0u; + std::memcpy(&xid, command.data() + 2u, sizeof(xid)); + std::memcpy(&sjxHandle, command.data() + 4u, sizeof(sjxHandle)); + std::memcpy(&chunkDataAddress, command.data() + 8u, + sizeof(chunkDataAddress)); + std::memcpy(&chunkLength, command.data() + 12u, sizeof(chunkLength)); + + if (commandNumber != 0u || chunkLength == 0u || + !hasReadableRange(chunkDataAddress, chunkLength)) + { + continue; + } + + const auto sjx = m_sjxByHandle.find(sjxHandle); + if (sjx == m_sjxByHandle.end() || + (sjx->second.xid != 0u && sjx->second.xid != xid) || + line != sjx->second.line || + m_sjrmtByHandle.find(sjx->second.destinationSjHandle) == + m_sjrmtByHandle.end()) + { + continue; + } + return true; + } + return false; + } + + bool looksLikePs2RnaPayloadLocked(uint32_t workAddress, + uint32_t workSize) const + { + uint32_t commandCount = 0u; + if (!readValidCommandCount(workAddress, workSize, commandCount)) + { + return false; + } + + for (uint32_t index = 0u; index < commandCount; ++index) + { + const uint32_t commandAddress = + workAddress + kDtxHeaderSize + (index * kDtxCommandSize); + std::array command{}; + if (!m_host.readGuest(commandAddress, command.data(), command.size())) + { + break; + } + + uint16_t commandNumber = 0u; + uint32_t rnaHandle = 0u; + std::memcpy(&commandNumber, command.data(), sizeof(commandNumber)); + std::memcpy(&rnaHandle, command.data() + 4u, sizeof(rnaHandle)); + if (commandNumber <= 5u && + m_ps2RnaByHandle.find(rnaHandle) != m_ps2RnaByHandle.end()) + { + return true; + } + } + return false; + } + + bool inferTransferFromPayloadLocked(uint32_t sourceAddress, + uint32_t destinationAddress, + uint32_t size, + TransferState &result) const + { + if (m_transferById.empty()) + { + return false; + } + + if (looksLikeSjxPayloadLocked(sourceAddress, size)) + { + result = TransferState{0u, 0u, sourceAddress, destinationAddress, size}; + return true; + } + if (looksLikePs2RnaPayloadLocked(sourceAddress, size)) + { + result = TransferState{1u, 0u, sourceAddress, destinationAddress, size}; + return true; + } + return false; + } + + bool copyBytes(uint32_t destinationAddress, + uint32_t sourceAddress, + uint32_t length) + { + for (uint32_t index = 0u; index < length; ++index) + { + uint8_t value = 0u; + if (!m_host.readGuest(sourceAddress + index, &value, sizeof(value)) || + !m_host.writeGuest(destinationAddress + index, &value, sizeof(value))) + { + return false; + } + } + return true; + } + + void appendToSjrmtData(SjrmtState &state, + uint32_t sourceDataAddress, + uint32_t requestedLength) + { + const uint32_t capacity = state.workSize == 0u + ? kDefaultSjrmtCapacity + : state.workSize; + if (capacity == 0u || state.workAddress == 0u || + state.roomBytes == 0u || requestedLength == 0u) + { + return; + } + + const uint32_t requestedCopyLength = + std::min(requestedLength, state.roomBytes); + uint32_t copiedLength = 0u; + for (uint32_t index = 0u; index < requestedCopyLength; ++index) + { + const uint32_t writeOffset = (state.writePosition + index) % capacity; + if (!copyBytes(state.workAddress + writeOffset, + sourceDataAddress + index, + 1u)) + { + break; + } + ++copiedLength; + } + + state.writePosition = (state.writePosition + copiedLength) % capacity; + state.roomBytes -= copiedLength; + state.dataBytes = std::min(capacity, state.dataBytes + copiedLength); + } + + static void consumeSjrmtData(SjrmtState &state) + { + const uint32_t capacity = state.workSize == 0u + ? kDefaultSjrmtCapacity + : state.workSize; + if (capacity == 0u || state.dataBytes == 0u) + { + return; + } + + const uint32_t consumed = std::min(capacity, state.dataBytes); + state.readPosition = (state.readPosition + consumed) % capacity; + state.dataBytes -= consumed; + state.roomBytes = std::min(capacity, state.roomBytes + consumed); + } + + void consumeActivePs2RnaStreamsLocked() + { + for (const auto &[handle, rna] : m_ps2RnaByHandle) + { + (void)handle; + if (!rna.playEnabled) + { + continue; + } + + auto consumeHandle = [&](uint32_t sjHandle) + { + if (sjHandle == 0u) + { + return; + } + const auto it = m_sjrmtByHandle.find(sjHandle); + if (it != m_sjrmtByHandle.end()) + { + consumeSjrmtData(it->second); + } + }; + consumeHandle(rna.sjHandle0); + consumeHandle(rna.sjHandle1); + } + } + + void applySjxPayload(const TransferState &transfer) + { + if (transfer.eeWorkAddress == 0u || + transfer.workSize < kMinimumDtxWorkSize) + { + return; + } + + uint32_t commandCount = 0u; + if (!readGuestPod(m_host, transfer.eeWorkAddress, commandCount)) + { + return; + } + commandCount = std::min(commandCount, kMaxDtxCommands); + if (commandCount == 0u) + { + return; + } + + std::lock_guard lock(m_mutex); + for (uint32_t index = 0u; index < commandCount; ++index) + { + const uint32_t commandAddress = + transfer.eeWorkAddress + kDtxHeaderSize + + (index * kDtxCommandSize); + std::array command{}; + if (!m_host.readGuest(commandAddress, command.data(), command.size())) + { + break; + } + + const uint8_t commandNumber = command[0]; + const uint8_t line = command[1]; + uint16_t xid = 0u; + uint32_t sjxHandle = 0u; + uint32_t chunkDataAddress = 0u; + uint32_t chunkLength = 0u; + std::memcpy(&xid, command.data() + 2u, sizeof(xid)); + std::memcpy(&sjxHandle, command.data() + 4u, sizeof(sjxHandle)); + std::memcpy(&chunkDataAddress, command.data() + 8u, + sizeof(chunkDataAddress)); + std::memcpy(&chunkLength, command.data() + 12u, sizeof(chunkLength)); + + if (commandNumber != 0u || chunkLength == 0u) + { + continue; + } + + const auto sjx = m_sjxByHandle.find(sjxHandle); + if (sjx == m_sjxByHandle.end() || + (sjx->second.xid != 0u && sjx->second.xid != xid)) + { + continue; + } + const auto sjrmt = + m_sjrmtByHandle.find(sjx->second.destinationSjHandle); + if (sjrmt == m_sjrmtByHandle.end() || line != sjx->second.line) + { + continue; + } + + appendToSjrmtData(sjrmt->second, + chunkDataAddress, + chunkLength); + constexpr uint8_t roomLine = 0u; + (void)m_host.writeGuest(commandAddress + 1u, + &roomLine, + sizeof(roomLine)); + } + consumeActivePs2RnaStreamsLocked(); + } + + void applyPs2RnaPayload(const TransferState &transfer) + { + if (transfer.eeWorkAddress == 0u || + transfer.workSize < kMinimumDtxWorkSize) + { + return; + } + + uint32_t commandCount = 0u; + if (!readGuestPod(m_host, transfer.eeWorkAddress, commandCount)) + { + return; + } + commandCount = std::min(commandCount, kMaxDtxCommands); + if (commandCount == 0u) + { + return; + } + + std::lock_guard lock(m_mutex); + for (uint32_t index = 0u; index < commandCount; ++index) + { + const uint32_t commandAddress = + transfer.eeWorkAddress + kDtxHeaderSize + + (index * kDtxCommandSize); + std::array command{}; + if (!m_host.readGuest(commandAddress, command.data(), command.size())) + { + break; + } + + uint16_t commandNumber = 0u; + uint32_t rnaHandle = 0u; + uint32_t argument1 = 0u; + uint32_t argument2 = 0u; + std::memcpy(&commandNumber, command.data(), sizeof(commandNumber)); + std::memcpy(&rnaHandle, command.data() + 4u, sizeof(rnaHandle)); + std::memcpy(&argument1, command.data() + 8u, sizeof(argument1)); + std::memcpy(&argument2, command.data() + 12u, sizeof(argument2)); + + const auto it = m_ps2RnaByHandle.find(rnaHandle); + if (it == m_ps2RnaByHandle.end()) + { + continue; + } + + Ps2RnaState &state = it->second; + switch (commandNumber) + { + case 0u: + state.playEnabled = true; + break; + case 1u: + state.playEnabled = false; + break; + case 2u: + state.playEnabled = argument1 != 0u; + break; + case 3u: + state.channelCount = argument1; + break; + case 4u: + state.sampleFrequency = argument1; + break; + case 5u: + state.volume = argument2; + break; + default: + break; + } + } + consumeActivePs2RnaStreamsLocked(); + } + + IopHost &m_host; + CriDtxBindings m_bindings; + mutable std::mutex m_mutex; + std::unordered_map m_remoteById; + std::unordered_map m_transferById; + std::unordered_map m_sjxByHandle; + std::unordered_map m_ps2RnaByHandle; + std::unordered_map m_sjrmtByHandle; + uint32_t m_nextUrpcObject = 0u; + uint64_t m_dmaAcks = 0u; + uint64_t m_dmaMisses = 0u; + uint64_t m_urpcCalls = 0u; + const std::array m_sids; + }; + } + + std::unique_ptr createCriDtxService(IopHost &host, + CriDtxBindings bindings) + { + if (bindings.serviceName.empty() || bindings.sid == 0u || + bindings.urpcObjectBase == 0u || + bindings.urpcObjectLimit <= bindings.urpcObjectBase || + bindings.urpcObjectStride == 0u || + bindings.urpcFunctionTableBase == 0u || + bindings.urpcObjectTableBase == 0u || + bindings.dispatcherFunctionAddress == 0u || + bindings.rpcServerPoolBase == 0u || + bindings.rpcServerStride == 0u) + { + throw std::invalid_argument("invalid CRI DTX bindings"); + } + return std::make_unique(host, std::move(bindings)); + } +} diff --git a/ps2xIOP/src/modules/dbcman.cpp b/ps2xIOP/src/modules/dbcman.cpp new file mode 100644 index 0000000..6c5e63d --- /dev/null +++ b/ps2xIOP/src/modules/dbcman.cpp @@ -0,0 +1,113 @@ +#include "module_factories.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kDbcManSid = 0x80001300u; + constexpr uint32_t kRpcCheckVersion = 0x80001363u; + constexpr uint32_t kDbcManVersion = 0x0320u; + constexpr uint32_t kMaxUnknownRpcLogs = 32u; + + class DbcmanService final : public IopService + { + public: + explicit DbcmanService(IopHost &host) + : m_host(host) + { + } + + [[nodiscard]] std::string_view name() const override + { + return "dbcman"; + } + + [[nodiscard]] std::span sids() const override + { + return kSids; + } + + void reset() override + { + std::lock_guard lock(m_mutex); + m_unknownRpcLogCount = 0u; + } + + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override + { + RpcResult result; + if (request.sid != kDbcManSid) + { + return result; + } + + result.handled = true; + result.resultAddress = request.receive.address; + if (request.receive.address == 0u || request.receive.size == 0u) + { + return result; + } + + if (request.function == kRpcCheckVersion) + { + const uint32_t wordCount = request.receive.size / sizeof(uint32_t); + const uint32_t count = wordCount < 4u ? wordCount : 4u; + for (uint32_t index = 0u; index < count; ++index) + { + const uint32_t address = request.receive.address + index * sizeof(uint32_t); + (void)m_host.writeGuest(address, &kDbcManVersion, sizeof(kDbcManVersion)); + } + return result; + } + + bool shouldLog = false; + { + std::lock_guard lock(m_mutex); + if (m_unknownRpcLogCount < kMaxUnknownRpcLogs) + { + ++m_unknownRpcLogCount; + shouldLog = true; + } + } + + if (shouldLog) + { + std::ostringstream message; + message << "[DBCMAN:stub]" + << " sid=0x" << std::hex << request.sid + << " rpc=0x" << request.function + << " send=0x" << request.send.address + << " sendSize=0x" << request.send.size + << " recv=0x" << request.receive.address + << " recvSize=0x" << request.receive.size; + m_host.log(LogLevel::Info, message.str()); + } + return result; + } + + void appendDebugMetrics(std::vector &metrics) const override + { + std::lock_guard lock(m_mutex); + metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false}); + } + + private: + inline static constexpr std::array kSids{kDbcManSid}; + + IopHost &m_host; + mutable std::mutex m_mutex; + uint32_t m_unknownRpcLogCount = 0u; + }; + } + + std::unique_ptr createDbcmanService(IopHost &host) + { + return std::make_unique(host); + } +} diff --git a/ps2xIOP/src/modules/libsd.cpp b/ps2xIOP/src/modules/libsd.cpp new file mode 100644 index 0000000..56d9a5c --- /dev/null +++ b/ps2xIOP/src/modules/libsd.cpp @@ -0,0 +1,63 @@ +#include "module_factories.h" + +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kLibSdSid = 0x80000701u; + + class LibSdService final : public IopService + { + public: + explicit LibSdService(IopHost &host) + : m_host(host) + { + } + + [[nodiscard]] std::string_view name() const override + { + return "libsd"; + } + + [[nodiscard]] std::span sids() const override + { + return kSids; + } + + void reset() override + { + } + + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override + { + if (request.sid != kLibSdSid) + { + return {}; + } + + m_host.audioCommand(request.sid, + request.function, + request.send, + request.receive); + + RpcResult result; + result.handled = true; + result.resultAddress = request.receive.address; + return result; + } + + private: + inline static constexpr std::array kSids{kLibSdSid}; + + IopHost &m_host; + }; + } + + std::unique_ptr createLibSdService(IopHost &host) + { + return std::make_unique(host); + } +} diff --git a/ps2xIOP/src/modules/mcserv.cpp b/ps2xIOP/src/modules/mcserv.cpp new file mode 100644 index 0000000..b3a71c2 --- /dev/null +++ b/ps2xIOP/src/modules/mcserv.cpp @@ -0,0 +1,402 @@ +#include "../iop_service.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kMcservSid = 0x80000400u; + constexpr uint32_t kMcservDev9Sid = 0x80000480u; + constexpr int32_t kSucceeded = 0; + constexpr int32_t kDenied = -5; + constexpr uint32_t kMcservVersion = 0x0205u; + constexpr uint32_t kMcmanVersion = 0x0206u; + constexpr uint32_t kCreateDirectory = 0x0040u; + + enum class Operation + { + Init, + GetInfo, + Open, + Close, + Seek, + Read, + Write, + Flush, + Chdir, + GetDir, + SetInfo, + Delete, + Format, + Unformat, + GetEnt, + ChangePriority, + Unknown, + }; + + enum class Flavor + { + OldMcserv, + NewXmcserv, + }; + +#pragma pack(push, 1) + struct DescriptorParameter + { + int32_t fd; + int32_t port; + int32_t slot; + int32_t size; + int32_t offset; + int32_t origin; + uint32_t buffer; + uint32_t parameter; + uint8_t data[16]; + }; + + struct NameParameter + { + int32_t port; + int32_t slot; + int32_t flags; + int32_t maxEntries; + uint32_t pointer; + char name[1024]; + }; +#pragma pack(pop) + + static_assert(sizeof(DescriptorParameter) == 48u); + static_assert(sizeof(NameParameter) == 1044u); + + Operation decodeOperation(uint32_t function, Flavor &flavor) + { + flavor = Flavor::NewXmcserv; + switch (function) + { + case 0xFEu: return Operation::Init; + case 0x01u: return Operation::GetInfo; + case 0x02u: return Operation::Open; + case 0x03u: return Operation::Close; + case 0x04u: return Operation::Seek; + case 0x05u: return Operation::Read; + case 0x06u: return Operation::Write; + case 0x0Au: return Operation::Flush; + case 0x0Cu: return Operation::Chdir; + case 0x0Du: return Operation::GetDir; + case 0x0Eu: return Operation::SetInfo; + case 0x0Fu: return Operation::Delete; + case 0x10u: return Operation::Format; + case 0x11u: return Operation::Unformat; + case 0x12u: return Operation::GetEnt; + case 0x14u: return Operation::ChangePriority; + default: break; + } + + flavor = Flavor::OldMcserv; + switch (function) + { + case 0x70u: return Operation::Init; + case 0x71u: return Operation::Open; + case 0x72u: return Operation::Close; + case 0x73u: return Operation::Read; + case 0x74u: return Operation::Write; + case 0x75u: return Operation::Seek; + case 0x76u: return Operation::GetDir; + case 0x77u: return Operation::Format; + case 0x78u: return Operation::GetInfo; + case 0x79u: return Operation::Delete; + case 0x7Au: return Operation::Flush; + case 0x7Bu: return Operation::Chdir; + case 0x7Cu: return Operation::SetInfo; + case 0x80u: return Operation::Unformat; + default: return Operation::Unknown; + } + } + + bool usesNameParameter(Operation operation) + { + return operation == Operation::Open || operation == Operation::Chdir || + operation == Operation::GetDir || operation == Operation::SetInfo || + operation == Operation::Delete || operation == Operation::GetEnt; + } + + class McservService final : public IopService + { + public: + explicit McservService(IopHost &host) : m_host(host) {} + + [[nodiscard]] std::string_view name() const override { return "MCSERV"; } + [[nodiscard]] std::span sids() const override { return m_sids; } + + void reset() override + { + { + std::lock_guard lock(m_mutex); + m_unknownRpcLogCount = 0u; + } + (void)call(MemoryCardOperation::Init); + } + + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override + { + RpcResult response{}; + response.handled = true; + response.resultAddress = request.receive.address; + + Flavor flavor = Flavor::NewXmcserv; + const Operation operation = decodeOperation(request.function, flavor); + if (operation == Operation::Init) + { + (void)call(MemoryCardOperation::Init); + writeInitResult(request.receive); + return response; + } + + int32_t result = kDenied; + if (operation == Operation::Unknown) + { + logUnknown(request); + writeResult(request.receive, result); + return response; + } + + if (usesNameParameter(operation)) + { + NameParameter parameter{}; + if (request.send.address != 0u && + request.send.size >= offsetof(NameParameter, name) && + m_host.readGuest(request.send.address, ¶meter, sizeof(parameter))) + { + result = handleNameOperation(operation, request.send.address, parameter); + } + } + else + { + DescriptorParameter parameter{}; + if (request.send.address != 0u && + request.send.size >= sizeof(parameter) && + m_host.readGuest(request.send.address, ¶meter, sizeof(parameter))) + { + if (operation == Operation::Write && parameter.origin > 0 && + parameter.origin <= static_cast(sizeof(parameter.data))) + { + const uint32_t inlineAddress = + request.send.address + static_cast(offsetof(DescriptorParameter, data)); + const int32_t prefix = call(MemoryCardOperation::Write, + static_cast(parameter.fd), + inlineAddress, + static_cast(parameter.origin)); + if (prefix < 0) + { + result = prefix; + } + else + { + const int32_t body = call(MemoryCardOperation::Write, + static_cast(parameter.fd), + parameter.buffer, + static_cast(std::max(parameter.size, 0))); + result = body < 0 ? body : prefix + body; + } + } + else + { + result = handleDescriptorOperation(operation, flavor, parameter); + } + } + } + + writeResult(request.receive, result); + return response; + } + + void appendDebugMetrics(std::vector &metrics) const override + { + std::lock_guard lock(m_mutex); + metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false}); + } + + private: + int32_t call(MemoryCardOperation operation, + uint32_t a0 = 0u, + uint32_t a1 = 0u, + uint32_t a2 = 0u, + uint32_t a3 = 0u, + uint32_t stackArgument = 0u) + { + return m_host.memoryCard({operation, {a0, a1, a2, a3, stackArgument}}); + } + + void writeResult(GuestBuffer receive, int32_t result) + { + if (receive.address == 0u || receive.size < sizeof(result)) + { + return; + } + (void)m_host.writeGuest(receive.address, &result, sizeof(result)); + if (receive.size > sizeof(result)) + { + (void)m_host.zeroGuest(receive.address + sizeof(result), + receive.size - sizeof(result)); + } + } + + void writeInitResult(GuestBuffer receive) + { + if (receive.address == 0u || receive.size < sizeof(int32_t)) + { + return; + } + const std::array values = { + static_cast(kSucceeded), kMcservVersion, kMcmanVersion}; + const uint32_t bytes = std::min(receive.size, sizeof(values)); + (void)m_host.writeGuest(receive.address, values.data(), bytes); + if (receive.size > bytes) + { + (void)m_host.zeroGuest(receive.address + bytes, receive.size - bytes); + } + } + + int32_t handleNameOperation(Operation operation, + uint32_t sendAddress, + const NameParameter ¶meter) + { + const uint32_t nameAddress = + sendAddress + static_cast(offsetof(NameParameter, name)); + const uint32_t port = static_cast(parameter.port); + const uint32_t slot = static_cast(parameter.slot); + switch (operation) + { + case Operation::Open: + if ((static_cast(parameter.flags) & kCreateDirectory) != 0u) + { + return call(MemoryCardOperation::Mkdir, port, slot, nameAddress); + } + return call(MemoryCardOperation::Open, + port, slot, nameAddress, + static_cast(parameter.flags)); + case Operation::Chdir: + return call(MemoryCardOperation::Chdir, + port, slot, nameAddress, parameter.pointer); + case Operation::SetInfo: + return call(MemoryCardOperation::SetFileInfo, port, slot, nameAddress); + case Operation::Delete: + return call(MemoryCardOperation::Delete, port, slot, nameAddress); + case Operation::GetDir: + // MCSERV normally DMA-writes entries. The existing HLE returns + // zero entries instead of fabricating directory contents. + return kSucceeded; + case Operation::GetEnt: + return 1024; + default: + return kDenied; + } + } + + int32_t handleDescriptorOperation(Operation operation, + Flavor flavor, + const DescriptorParameter ¶meter) + { + switch (operation) + { + case Operation::GetInfo: + { + uint32_t typeAddress = 0u; + uint32_t freeAddress = 0u; + uint32_t formatAddress = 0u; + if (parameter.parameter != 0u) + { + const uint32_t outputSize = flavor == Flavor::NewXmcserv ? 192u : 64u; + (void)m_host.zeroGuest(parameter.parameter, outputSize); + typeAddress = parameter.parameter; + freeAddress = parameter.parameter + 4u; + if (flavor == Flavor::NewXmcserv) + { + formatAddress = parameter.parameter + 144u; + } + } + return call(MemoryCardOperation::GetInfo, + static_cast(parameter.port), + static_cast(parameter.slot), + typeAddress, + freeAddress, + formatAddress); + } + case Operation::Close: + return call(MemoryCardOperation::Close, static_cast(parameter.fd)); + case Operation::Seek: + return call(MemoryCardOperation::Seek, + static_cast(parameter.fd), + static_cast(parameter.offset), + static_cast(parameter.origin)); + case Operation::Read: + if (parameter.parameter != 0u) + { + (void)m_host.zeroGuest(parameter.parameter, + flavor == Flavor::NewXmcserv ? 192u : 64u); + } + return call(MemoryCardOperation::Read, + static_cast(parameter.fd), + parameter.buffer, + static_cast(std::max(parameter.size, 0))); + case Operation::Write: + return call(MemoryCardOperation::Write, + static_cast(parameter.fd), + parameter.buffer, + static_cast(std::max(parameter.size, 0))); + case Operation::Flush: + return call(MemoryCardOperation::Flush, static_cast(parameter.fd)); + case Operation::Format: + return call(MemoryCardOperation::Format, + static_cast(parameter.port), + static_cast(parameter.slot)); + case Operation::Unformat: + return call(MemoryCardOperation::Unformat, + static_cast(parameter.port), + static_cast(parameter.slot)); + case Operation::ChangePriority: + return kSucceeded; + default: + return kDenied; + } + } + + void logUnknown(const RpcRequest &request) + { + { + std::lock_guard lock(m_mutex); + if (m_unknownRpcLogCount >= 32u) + { + return; + } + ++m_unknownRpcLogCount; + } + std::ostringstream message; + message << "MCSERV unknown RPC sid=0x" << std::hex << request.sid + << " function=0x" << request.function + << " send=0x" << request.send.address + << " size=0x" << request.send.size; + m_host.log(LogLevel::Warning, message.str()); + } + + IopHost &m_host; + mutable std::mutex m_mutex; + uint32_t m_unknownRpcLogCount = 0u; + const std::array m_sids = {kMcservSid, kMcservDev9Sid}; + }; + } + + std::unique_ptr createMcservService(IopHost &host) + { + return std::make_unique(host); + } +} diff --git a/ps2xIOP/src/modules/sdrdrv.cpp b/ps2xIOP/src/modules/sdrdrv.cpp new file mode 100644 index 0000000..bd86e26 --- /dev/null +++ b/ps2xIOP/src/modules/sdrdrv.cpp @@ -0,0 +1,335 @@ +#include "module_factories.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 sids() const override { return m_sids; } + + void reset() override + { + std::lock_guard 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 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 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 &metrics) const override + { + std::lock_guard 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 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 chunk{}; + uint64_t copied = 0; + while (copied < byteCount) + { + const size_t wanted = static_cast(std::min(chunk.size(), byteCount - copied)); + std::fill(chunk.begin(), chunk.begin() + static_cast(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(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(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(byteCount, kEeRamSize - normalized); + const bool copied = copyHostRange(handle, + static_cast(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 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 m_sids; + mutable std::mutex m_mutex; + uint32_t m_headerWarnCount = 0; + uint32_t m_bodyWarnCount = 0; + }; + } + + std::unique_ptr 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(host, std::move(bindings)); + } +} diff --git a/ps2xIOP/src/modules/sound_update_stub.cpp b/ps2xIOP/src/modules/sound_update_stub.cpp new file mode 100644 index 0000000..933ebe5 --- /dev/null +++ b/ps2xIOP/src/modules/sound_update_stub.cpp @@ -0,0 +1,122 @@ +#include "module_factories.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + 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 sids() const override + { + return m_sids; + } + + void reset() override + { + std::lock_guard lock(m_mutex); + m_updateCounter = 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); + } + + uint32_t counter = 0u; + { + std::lock_guard lock(m_mutex); + counter = ++m_updateCounter; + } + + constexpr uint32_t activeStreams = 0u; + 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)); + } + + if (request.receive.address != 0u && + request.receive.size >= m_bindings.responseCounterOffset + sizeof(counter)) + { + const uint32_t address = request.receive.address + m_bindings.responseCounterOffset; + (void)m_host.writeGuest(address, &counter, sizeof(counter)); + } + + return result; + } + + void appendDebugMetrics(std::vector &metrics) const override + { + std::lock_guard lock(m_mutex); + metrics.push_back({"update_counter", m_updateCounter, false}); + } + + private: + IopHost &m_host; + SoundUpdateStubBindings m_bindings; + std::array m_sids; + mutable std::mutex m_mutex; + uint32_t m_updateCounter = 0u; + }; + } + + std::unique_ptr 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 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(host, std::move(bindings)); + } +} diff --git a/ps2xIOP/src/modules/tsnddrv.cpp b/ps2xIOP/src/modules/tsnddrv.cpp new file mode 100644 index 0000000..155970c --- /dev/null +++ b/ps2xIOP/src/modules/tsnddrv.cpp @@ -0,0 +1,628 @@ +#include "../module_factories.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 + bool readGuestPod(const IopHost &host, uint32_t address, T &value) + { + value = {}; + return host.readGuest(address, &value, sizeof(value)); + } + + template + bool writeGuestPod(IopHost &host, uint32_t address, const T &value) + { + return host.writeGuest(address, &value, sizeof(value)); + } + + template + bool hasAnyNonZero(const std::array &values) + { + return std::any_of(values.begin(), values.end(), [](const T value) + { return value != static_cast(0); }); + } + + size_t commandLength(uint8_t command) + { + const uint8_t hi = static_cast(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 sids() const override + { + return m_sids; + } + + void reset() override + { + std::lock_guard 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 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 lock(m_mutex); + if (!m_state.initialized || transfer.sourceAddress != m_state.statusAddress) + { + return; + } + backfillStatusLocked(); + } + + void appendDebugMetrics(std::vector &metrics) const override + { + std::lock_guard lock(m_mutex); + metrics.push_back({"initialized", m_state.initialized ? 1u : 0u, false}); + metrics.push_back({"status_address", m_state.statusAddress, true}); + metrics.push_back({"address_table", m_state.addressTableAddress, true}); + metrics.push_back({"hd_base", m_state.hdBaseAddress, true}); + metrics.push_back({"sq_base", m_state.sqBaseAddress, true}); + metrics.push_back({"data_base", m_state.dataBaseAddress, true}); + } + + private: + struct State + { + bool initialized = false; + uint32_t storageBaseAddress = 0u; + uint32_t storageSize = 0u; + uint32_t statusAddress = 0u; + uint32_t addressTableAddress = 0u; + uint32_t hdBaseAddress = 0u; + uint32_t sqBaseAddress = 0u; + uint32_t dataBaseAddress = 0u; + }; + + bool ensureMemoryLocked() + { + if (m_state.statusAddress == 0u) + { + const TsnddrvGuestArena &arena = m_bindings.arena; + const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment); + const uint32_t addressTableAddress = + alignUp(statusAddress + kStatusSize, arena.tableAlignment); + const uint32_t hdBaseAddress = + alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)), + arena.storageAlignment); + const uint32_t sqBaseAddress = + alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment); + const uint32_t dataBaseAddress = + alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment); + const uint32_t storageEnd = dataBaseAddress + arena.dataBytes; + if (storageEnd > arena.limit) + { + return false; + } + + m_state.statusAddress = statusAddress; + m_state.addressTableAddress = addressTableAddress; + m_state.hdBaseAddress = hdBaseAddress; + m_state.sqBaseAddress = sqBaseAddress; + m_state.dataBaseAddress = dataBaseAddress; + m_state.storageBaseAddress = hdBaseAddress; + m_state.storageSize = storageEnd - hdBaseAddress; + } + + if (m_state.statusAddress == 0u || + m_state.addressTableAddress == 0u || + m_state.storageBaseAddress == 0u) + { + return false; + } + + if (!m_state.initialized) + { + if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) || + !m_host.zeroGuest(m_state.addressTableAddress, + kAddressTableEntries * sizeof(uint32_t)) || + !m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize)) + { + return false; + } + + if (!writeGuestPod(m_host, + m_state.addressTableAddress + (0u * sizeof(uint32_t)), + m_state.hdBaseAddress) || + !writeGuestPod(m_host, + m_state.addressTableAddress + (1u * sizeof(uint32_t)), + m_state.sqBaseAddress) || + !writeGuestPod(m_host, + m_state.addressTableAddress + (2u * sizeof(uint32_t)), + m_state.dataBaseAddress)) + { + return false; + } + m_state.initialized = true; + } + + return true; + } + + int16_t checkValue(bool seTable, + uint32_t index, + uint32_t count) const + { + if (index >= count) + { + return 0; + } + + for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates) + { + const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress; + int16_t value = 0; + if (readGuestPod(m_host, + base + (index * sizeof(int16_t)), + value) && + value != 0) + { + return value; + } + } + return 0; + } + + bool selectCompatChecks(uint32_t &seBase, uint32_t &midiBase) const + { + const TsnddrvChecksumTables *firstReadable = nullptr; + for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates) + { + std::array seValues{}; + std::array midiValues{}; + const bool seReadable = m_host.readGuest(candidate.seAddress, + seValues.data(), + sizeof(seValues)); + const bool midiReadable = m_host.readGuest(candidate.midiAddress, + midiValues.data(), + sizeof(midiValues)); + if (seReadable && midiReadable && !firstReadable) + { + firstReadable = &candidate; + } + const bool looksLive = + (seReadable && hasAnyNonZero(seValues)) || + (midiReadable && hasAnyNonZero(midiValues)); + if (seReadable && midiReadable && looksLive) + { + seBase = candidate.seAddress; + midiBase = candidate.midiAddress; + return true; + } + } + + if (firstReadable) + { + seBase = firstReadable->seAddress; + midiBase = firstReadable->midiAddress; + return true; + } + return false; + } + + void backfillStatusLocked() + { + uint32_t seBase = 0u; + uint32_t midiBase = 0u; + if (!selectCompatChecks(seBase, midiBase)) + { + return; + } + + auto backfillSlots = [&](uint32_t statusOffset, + uint32_t compatBase, + uint32_t slotCount) + { + for (uint32_t slot = 0u; slot < slotCount; ++slot) + { + int16_t liveValue = 0; + if (!readGuestPod(m_host, + m_state.statusAddress + statusOffset + + (slot * sizeof(int16_t)), + liveValue) || + liveValue != 0) + { + continue; + } + + int16_t compatValue = 0; + if (!readGuestPod(m_host, + compatBase + (slot * sizeof(int16_t)), + compatValue) || + compatValue == 0) + { + continue; + } + + (void)writeGuestPod(m_host, + m_state.statusAddress + statusOffset + + (slot * sizeof(int16_t)), + compatValue); + } + }; + + backfillSlots(kSeSumOffset, seBase, 5u); + backfillSlots(kMidiSumOffset, midiBase, 4u); + } + + void applyCommandLocked(const std::array &command) + { + if (m_state.statusAddress == 0u) + { + return; + } + + switch (command[0]) + { + case 0x20u: // SdrBgmReq + { + const uint32_t port = command[1] & 0x0Fu; + uint16_t midiInfo = 0u; + (void)readGuestPod(m_host, + m_state.statusAddress + kMidiInfoOffset, + midiInfo); + midiInfo = static_cast(midiInfo | + static_cast(1u << port)); + (void)writeGuestPod(m_host, + m_state.statusAddress + kMidiInfoOffset, + midiInfo); + break; + } + case 0x21u: // SdrBgmStop + { + const uint32_t port = command[1] & 0x0Fu; + uint16_t midiInfo = 0u; + (void)readGuestPod(m_host, + m_state.statusAddress + kMidiInfoOffset, + midiInfo); + midiInfo = static_cast(midiInfo & + ~static_cast(1u << port)); + (void)writeGuestPod(m_host, + m_state.statusAddress + kMidiInfoOffset, + midiInfo); + break; + } + case 0x28u: // SdrHDDataSet + { + const uint32_t port = command[1] & 0x0Fu; + if (port >= 4u) + { + break; + } + const int16_t checksum = checkValue(false, port, 4u); + (void)writeGuestPod(m_host, + m_state.statusAddress + kMidiSumOffset + + (port * sizeof(int16_t)), + checksum); + break; + } + case 0x29u: // SdrHDDataSet2 + { + const uint32_t port = command[1] & 0x0Fu; + if (port >= 5u) + { + break; + } + const int16_t checksum = checkValue(true, port, 5u); + (void)writeGuestPod(m_host, + m_state.statusAddress + kSeSumOffset + + (port * sizeof(int16_t)), + checksum); + break; + } + case 0x10u: // SdrSeAllStop + (void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset, + 6u * sizeof(uint16_t)); + break; + default: + break; + } + } + + void handleCommandBuffer(GuestBuffer send) + { + if (send.address == 0u || send.size == 0u) + { + return; + } + + std::lock_guard 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(offset) + length > send.size) + { + break; + } + + std::array command{}; + if (!m_host.readGuest(send.address + offset, command.data(), length)) + { + break; + } + applyCommandLocked(command); + offset += static_cast(length); + } + } + + IopHost &m_host; + TsnddrvBindings m_bindings; + mutable std::mutex m_mutex; + State m_state; + const std::array m_sids = {kCommandSid, kStateSid}; + }; + } + + std::unique_ptr 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(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::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 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(host, std::move(bindings)); + } +} diff --git a/ps2xIOP/src/plugin_loader.cpp b/ps2xIOP/src/plugin_loader.cpp new file mode 100644 index 0000000..5490786 --- /dev/null +++ b/ps2xIOP/src/plugin_loader.cpp @@ -0,0 +1,956 @@ +#include "plugin_loader.h" + +#include "ps2x/iop/plugin_api.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__) +#include +#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(MemoryCardOperation::Mkdir); + if (operation > last) + { + throw std::out_of_range("invalid memory-card operation"); + } + return static_cast(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(userdata); + } + + template + static int32_t guardedStatus(Callback &&callback) noexcept + { + try + { + return static_cast( + std::forward(callback)()); + } + catch (...) + { + return PS2X_IOP_STATUS_FAILED_V1; + } + } + + template + static Value guardedValue(Value fallback, Callback &&callback) noexcept + { + try + { + return static_cast( + std::forward(callback)()); + } + catch (...) + { + return fallback; + } + } + + template + static void guardedVoid(Callback &&callback) noexcept + { + try + { + std::forward(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(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(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(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(LogLevel::Error); + const auto converted = static_cast(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 libraryKeepAlive, + ps2x_iop_profile_api_v1 profileApi, + std::string serviceName, + std::vector 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 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(transfer.kind), + static_cast(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 &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(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 m_libraryKeepAlive; + ps2x_iop_profile_api_v1 m_api{}; + std::string m_name; + std::vector 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(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(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(GetProcAddress(static_cast(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 &searchPaths, + std::vector &profiles, + std::vector &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(canonicalPath); + std::string openError; + if (!library->open(openError)) + { + diagnostics.push_back(formatPluginDiagnostic(canonicalPath, openError)); + continue; + } + + const auto query = reinterpret_cast( + 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 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 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(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 + } +} diff --git a/ps2xIOP/src/plugin_loader.h b/ps2xIOP/src/plugin_loader.h new file mode 100644 index 0000000..012bce1 --- /dev/null +++ b/ps2xIOP/src/plugin_loader.h @@ -0,0 +1,34 @@ +#pragma once + +#include "iop_service.h" + +#include +#include +#include +#include +#include + +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 &searchPaths, + std::vector &profiles, + std::vector &diagnostics, + std::string *error); + + private: + class DynamicLibrary; + + IopHost &m_host; + std::vector> m_libraries; + std::unordered_set m_loadedPaths; + }; +} diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 2684ee5..91a23c2 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -11,6 +11,7 @@ option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON) option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF) option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" OFF) +option(PS2X_ENABLE_IOP_RPC_TRACE "Log unhandled IOP/SIF RPC trace suggestions" ON) option(PS2X_STRICT_RETURN_DIAGNOSTICS "Route generated JR $ra returns through runtime branch diagnostics" OFF) option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON) @@ -349,11 +350,7 @@ add_library(ps2_runtime STATIC src/lib/ps2_gs_gpu.cpp src/lib/ps2_gs_memory.cpp src/lib/ps2_gs_rasterizer.cpp - src/lib/ps2_iop.cpp - src/lib/ps2_iop_audio.cpp - src/lib/ps2_iop_dbcman.cpp - src/lib/ps2_iop_sdrdrv.cpp - src/lib/ps2_iop_cl.cpp + src/lib/ps2_iop_host.cpp src/lib/ps2_memory.cpp src/lib/ps2_pad.cpp src/lib/ps2_runtime.cpp @@ -377,6 +374,16 @@ if(PS2X_ENABLE_AGRESSIVE_LOGS) ) endif() +if(PS2X_ENABLE_IOP_RPC_TRACE) + target_compile_definitions(ps2_runtime PUBLIC + PS2X_ENABLE_IOP_RPC_TRACE=1 + ) +else() + target_compile_definitions(ps2_runtime PUBLIC + PS2X_ENABLE_IOP_RPC_TRACE=0 + ) +endif() + if(PS2X_STRICT_RETURN_DIAGNOSTICS) target_compile_definitions(ps2_runtime PUBLIC PS2X_STRICT_RETURN_DIAGNOSTICS=1 @@ -430,7 +437,7 @@ target_include_directories(ps2_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/lib/Kernel ) -target_link_libraries(ps2_runtime PUBLIC ps2_host_backend ffmpeg) +target_link_libraries(ps2_runtime PUBLIC ps2_host_backend ffmpeg ps2_iop) target_link_libraries(ps2EntryRunner PRIVATE ps2_runtime diff --git a/ps2xRuntime/include/game_overrides.h b/ps2xRuntime/include/game_overrides.h index 13bcc01..3afaa31 100644 --- a/ps2xRuntime/include/game_overrides.h +++ b/ps2xRuntime/include/game_overrides.h @@ -27,7 +27,7 @@ namespace ps2_game_overrides }; void registerDescriptor(const Descriptor &descriptor); - void applyMatching(PS2Runtime &runtime, const std::string &elfPath, uint32_t entry); + void applyMatching(PS2Runtime &runtime, const std::string &elfPath, uint32_t entry, uint32_t crc32, bool crc32Valid); // Runtime helper for game-specific modules: // bind an address to an existing syscall/stub handler by name. diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index b58ddb8..19c97e2 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -21,16 +21,25 @@ #include #include #include +#include #include "ps2_log.h" #include "runtime/ps2_address.h" #include "runtime/ps2_gif_arbiter.h" #include "runtime/ps2_memory.h" #include "runtime/ps2_gs_gpu.h" -#include "runtime/ps2_iop.h" #include "runtime/ps2_vu1.h" #include "runtime/ps2_audio.h" #include "runtime/ps2_pad.h" +#include "ps2x/iop/iop_types.h" + +namespace ps2x::iop +{ + class IopSubsystem; +} + +class PS2IopHostAdapter; +class PS2IopTransport; enum PS2Exception { @@ -128,7 +137,7 @@ struct alignas(16) R5900Context // FPU registers (COP1) float f[32]; - float f_acc; // FPU accumulator + float f_acc; // FPU accumulator uint32_t fcr31; // Control/status register R5900Context() @@ -256,78 +265,6 @@ inline void ps2TraceGuestRangeWrite(uint8_t *rdram, // TODO we dont need this anymore so on next release it will be deleted } -struct PS2SoundDriverCompatLayout -{ - uint32_t primarySeCheckAddr = 0; - uint32_t primaryMidiCheckAddr = 0; - uint32_t fallbackSeCheckAddr = 0; - uint32_t fallbackMidiCheckAddr = 0; - uint32_t busyFlagAddr = 0; - std::array completionCallbacks{}; - std::array clearBusyCallbacks{}; - - [[nodiscard]] bool hasChecksumTables() const - { - return primarySeCheckAddr != 0u || primaryMidiCheckAddr != 0u || - fallbackSeCheckAddr != 0u || fallbackMidiCheckAddr != 0u; - } - - [[nodiscard]] bool matchesCompletionCallback(uint32_t addr) const - { - for (const uint32_t candidate : completionCallbacks) - { - if (candidate != 0u && candidate == addr) - { - return true; - } - } - return false; - } - - [[nodiscard]] bool matchesClearBusyCallback(uint32_t addr) const - { - for (const uint32_t candidate : clearBusyCallbacks) - { - if (candidate != 0u && candidate == addr) - { - return true; - } - } - return false; - } -}; - -struct PS2DtxCompatLayout -{ - uint32_t rpcSid = 0; - uint32_t urpcObjBase = 0; - uint32_t urpcObjLimit = 0; - uint32_t urpcObjStride = 0x20u; - uint32_t urpcFnTableBase = 0; - uint32_t urpcObjTableBase = 0; - uint32_t dispatcherFuncAddr = 0; - - [[nodiscard]] bool isConfigured() const - { - return rpcSid != 0u; - } - - [[nodiscard]] bool hasUrpcObjectRange() const - { - return urpcObjBase != 0u && urpcObjLimit > urpcObjBase && urpcObjStride != 0u; - } - - [[nodiscard]] bool hasUrpcTables() const - { - return urpcFnTableBase != 0u && urpcObjTableBase != 0u; - } - - [[nodiscard]] bool isUrpcRpc(uint32_t sid, uint32_t rpcNum) const - { - return isConfigured() && sid == rpcSid && rpcNum >= 0x400u && rpcNum < 0x500u; - } -}; - class PS2Runtime { public: @@ -349,6 +286,9 @@ public: bool loadELF(const std::string &elfPath); void run(); + void setIopPluginSearchPaths(std::vector paths); + [[nodiscard]] ps2x::iop::DebugSnapshot iopDebugSnapshot() const; + using DebugUiCallback = void (*)(PS2Runtime &runtime, void *userData); void setDebugUiCallbacks(DebugUiCallback initCallback, DebugUiCallback drawCallback, @@ -509,8 +449,6 @@ public: inline VU1Interpreter &vu1() { return m_vu1; } inline const VU1Interpreter &vu1() const { return m_vu1; } - inline ps2_iop &iop() { return m_iop; } - inline const ps2_iop &iop() const { return m_iop; } inline PS2AudioBackend &audioBackend() { return m_audioBackend; } inline const PS2AudioBackend &audioBackend() const { return m_audioBackend; } inline PSPadBackend &padBackend() { return m_padBackend; } @@ -543,14 +481,21 @@ private: void HandleIntegerOverflow(R5900Context *ctx); + [[nodiscard]] ps2x::iop::RpcAbi selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const; + [[nodiscard]] ps2x::iop::RpcResult handleIopRpc(uint8_t *rdram, R5900Context *ctx, ps2x::iop::RpcRequest request); + void notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransfer &transfer); + void resetIop(); + friend class GuestExecutionScope; friend class GuestExecutionReleaseScope; + friend class PS2IopTransport; private: PS2Memory m_memory; GifArbiter m_gifArbiter; GS m_gs; - ps2_iop m_iop; + std::unique_ptr m_iopHost; + std::unique_ptr m_iopSubsystem; PS2AudioBackend m_audioBackend; PSPadBackend m_padBackend; VU1Interpreter m_vu0; diff --git a/ps2xRuntime/include/ps2_syscalls.h b/ps2xRuntime/include/ps2_syscalls.h index 0411a2a..e23795b 100644 --- a/ps2xRuntime/include/ps2_syscalls.h +++ b/ps2xRuntime/include/ps2_syscalls.h @@ -34,11 +34,6 @@ namespace ps2_syscalls void notifyRuntimeStop(); void joinAllGuestHostThreads(); void detachAllGuestHostThreads(); - void resetSoundDriverRpcState(); - void setSoundDriverCompatLayout(const PS2SoundDriverCompatLayout &layout); - void clearSoundDriverCompatLayout(); - void setDtxCompatLayout(const PS2DtxCompatLayout &layout); - void clearDtxCompatLayout(); void EnsureVSyncWorkerRunning(uint8_t *rdram, PS2Runtime *runtime); uint64_t GetCurrentVSyncTick(); uint64_t WaitForNextVSyncTick(uint8_t *rdram, PS2Runtime *runtime); diff --git a/ps2xRuntime/include/runtime/ps2_iop.h b/ps2xRuntime/include/runtime/ps2_iop.h deleted file mode 100644 index e3fda43..0000000 --- a/ps2xRuntime/include/runtime/ps2_iop.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef PS2_IOP_H -#define PS2_IOP_H - -#include - -class PS2Runtime; - -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_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; - -class ps2_iop -{ -public: - ps2_iop(); - ~ps2_iop() = default; - - void init(uint8_t *rdram); - void reset(); - - bool handleRPC(PS2Runtime *runtime, - uint32_t sid, uint32_t rpcNum, - uint32_t sendBufAddr, uint32_t sendSize, - uint32_t recvBufAddr, uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion); - -private: - uint8_t *m_rdram = nullptr; -}; - -#endif diff --git a/ps2xRuntime/include/runtime/ps2_iop_audio.h b/ps2xRuntime/include/runtime/ps2_iop_audio.h deleted file mode 100644 index 4629528..0000000 --- a/ps2xRuntime/include/runtime/ps2_iop_audio.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef PS2_IOP_AUDIO_H -#define PS2_IOP_AUDIO_H - -#include - -class PS2Runtime; - -namespace ps2_iop_audio -{ - void reset(); - - bool handleLibSdRpc(uint8_t* rdram, - PS2Runtime* runtime, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t& resultPtr); -} - -#endif diff --git a/ps2xRuntime/include/runtime/ps2_iop_cl.h b/ps2xRuntime/include/runtime/ps2_iop_cl.h deleted file mode 100644 index 8e344d1..0000000 --- a/ps2xRuntime/include/runtime/ps2_iop_cl.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -namespace ps2_iop_cl -{ - void reset(); - - bool handleClFileRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr); - - bool handleSoundRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion); -} diff --git a/ps2xRuntime/include/runtime/ps2_iop_dbcman.h b/ps2xRuntime/include/runtime/ps2_iop_dbcman.h deleted file mode 100644 index 4becf40..0000000 --- a/ps2xRuntime/include/runtime/ps2_iop_dbcman.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -namespace ps2_iop_dbcman -{ - void reset(); - - bool handleDbcManRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr); -} diff --git a/ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h b/ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h deleted file mode 100644 index 84fa2a2..0000000 --- a/ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -namespace ps2_iop_sdrdrv -{ - void reset(); - - bool handleSdrdrvRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr); -} diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp index 71829cd..e1be7d2 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp @@ -6,21 +6,44 @@ namespace ps2_stubs namespace { constexpr uint32_t kLibSdCmdSetParam = 0x8010u; - constexpr uint32_t kLibSdCmdBlockTrans = 0x80D0u; - constexpr uint32_t kLibSdCmdBlockTransAlt = 0x80E0u; - constexpr uint32_t kLibSdCmdBlockTransStatus = 0x80F0u; + constexpr uint32_t kLibSdCmdVoiceTrans = 0x80D0u; + constexpr uint32_t kLibSdCmdBlockTrans = 0x80E0u; + constexpr uint32_t kLibSdCmdVoiceTransStatus = 0x80F0u; + constexpr uint32_t kLibSdCmdBlockTransStatus = 0x8100u; constexpr uint32_t kAudioPositionMask = 0x00FFFFFFu; constexpr uint32_t kAudioTransferUnit = 1024u; + constexpr uint32_t kLibSdCoreCount = 2u; + constexpr uint32_t kLibSdTransModeIo = 0x08u; + constexpr uint32_t kLibSdTransDirectionMask = 0x03u; + constexpr uint32_t kLibSdTransStop = 0x02u; + constexpr uint32_t kLibSdTransLoop = 0x10u; + + struct VoiceTransferState + { + uint32_t sourceAddress = 0u; + uint32_t destinationAddress = 0u; + uint32_t size = 0u; + uint16_t mode = 0u; + bool completed = true; + }; + + struct BlockTransferState + { + uint32_t base = 0u; + uint32_t size = 0u; + uint32_t pauseBase = 0u; + uint32_t offset = 0u; + uint32_t statusTraceCount = 0u; + uint16_t mode = 0u; + bool active = false; + bool loop = false; + }; struct AudioStubState { bool initialized = false; - uint32_t currentBlockBase = 0u; - uint32_t currentBlockSize = 0u; - uint32_t currentPauseBase = 0u; - uint32_t currentBlockOffset = 0u; - uint32_t blockStatusTraceCount = 0u; - bool blockTransferActive = false; + std::array voiceTransfers{}; + std::array blockTransfers{}; }; std::mutex g_audio_stub_mutex; @@ -31,10 +54,12 @@ namespace ps2_stubs g_audio_stub_state = {}; } - uint32_t currentBlockPosition() + uint32_t currentBlockStatus(const BlockTransferState &transfer) { - return (g_audio_stub_state.currentBlockBase + g_audio_stub_state.currentBlockOffset) & - kAudioPositionMask; + const uint32_t position = (transfer.base + transfer.offset) & kAudioPositionMask; + const uint32_t halfSize = transfer.size / 2u; + const uint32_t bank = (transfer.loop && halfSize != 0u && transfer.offset >= halfSize) ? 1u : 0u; + return (bank << 24u) | position; } } @@ -69,63 +94,104 @@ namespace ps2_stubs std::lock_guard lock(g_audio_stub_mutex); g_audio_stub_state.initialized = true; + const uint32_t core = cmdArg0 & (kLibSdCoreCount - 1u); + VoiceTransferState &voiceTransfer = g_audio_stub_state.voiceTransfers[core]; + BlockTransferState &blockTransfer = g_audio_stub_state.blockTransfers[core]; + uint32_t returnValue = 0u; - if (cmd == kLibSdCmdBlockTrans || cmd == kLibSdCmdBlockTransAlt) + if (cmd == kLibSdCmdVoiceTrans) { - if (arg4 != 0u && arg5 != 0u) - { - g_audio_stub_state.currentBlockBase = arg4 & kAudioPositionMask; - g_audio_stub_state.currentBlockSize = arg5; - g_audio_stub_state.currentPauseBase = - ((arg6 != 0u) ? arg6 : arg4) & kAudioPositionMask; + voiceTransfer.sourceAddress = arg4; + voiceTransfer.destinationAddress = arg5; + voiceTransfer.size = arg6; + voiceTransfer.mode = static_cast(cmdArg1); + voiceTransfer.completed = true; + + // sceSdVoiceTrans returns the transferred byte count for DMA + // mode and zero for its synchronous programmed-I/O mode. + returnValue = ((voiceTransfer.mode & kLibSdTransModeIo) != 0u) + ? 0u + : voiceTransfer.size; - const uint32_t pauseOffset = - (g_audio_stub_state.currentPauseBase - g_audio_stub_state.currentBlockBase) & - kAudioPositionMask; - g_audio_stub_state.currentBlockOffset = - (pauseOffset < g_audio_stub_state.currentBlockSize) ? pauseOffset : 0u; - g_audio_stub_state.blockStatusTraceCount = 0u; - g_audio_stub_state.blockTransferActive = true; - } - else - { - g_audio_stub_state.blockTransferActive = false; - } PS2_IF_AGRESSIVE_LOGS({ - std::cerr << "[Audio:BlockTrans] active=" << g_audio_stub_state.blockTransferActive - << " base=0x" << std::hex << g_audio_stub_state.currentBlockBase - << " size=0x" << g_audio_stub_state.currentBlockSize - << " pause=0x" << g_audio_stub_state.currentPauseBase - << " offset=0x" << g_audio_stub_state.currentBlockOffset + std::cerr << "[Audio:VoiceTrans] core=" << core + << " src=0x" << std::hex << voiceTransfer.sourceAddress + << " dest=0x" << voiceTransfer.destinationAddress + << " size=0x" << voiceTransfer.size + << " mode=0x" << voiceTransfer.mode << std::dec << std::endl; }); } - else if (cmd == kLibSdCmdBlockTransStatus && - g_audio_stub_state.blockTransferActive && - g_audio_stub_state.currentBlockSize != 0u) + else if (cmd == kLibSdCmdBlockTrans) { - g_audio_stub_state.currentBlockOffset = - (g_audio_stub_state.currentBlockOffset + kAudioTransferUnit) % - g_audio_stub_state.currentBlockSize; - if (g_audio_stub_state.blockStatusTraceCount < 32u) + const uint32_t direction = cmdArg1 & kLibSdTransDirectionMask; + if (direction == kLibSdTransStop) { - PS2_IF_AGRESSIVE_LOGS({ - std::cerr << "[Audio:BlockStatus] position=0x" << std::hex << currentBlockPosition() - << " offset=0x" << g_audio_stub_state.currentBlockOffset - << " size=0x" << g_audio_stub_state.currentBlockSize - << std::dec << std::endl; - }); - ++g_audio_stub_state.blockStatusTraceCount; + returnValue = currentBlockStatus(blockTransfer); + blockTransfer = {}; } + else if (arg4 != 0u && arg5 != 0u) + { + blockTransfer.base = arg4 & kAudioPositionMask; + blockTransfer.size = arg5; + blockTransfer.pauseBase = + ((arg6 != 0u) ? arg6 : arg4) & kAudioPositionMask; + blockTransfer.mode = static_cast(cmdArg1); + blockTransfer.loop = (cmdArg1 & kLibSdTransLoop) != 0u; + + const uint32_t pauseOffset = + (blockTransfer.pauseBase - blockTransfer.base) & kAudioPositionMask; + blockTransfer.offset = (pauseOffset < blockTransfer.size) ? pauseOffset : 0u; + blockTransfer.statusTraceCount = 0u; + blockTransfer.active = true; + returnValue = 0u; + } + else + { + returnValue = std::numeric_limits::max(); + } + + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[Audio:BlockTrans] core=" << core + << " active=" << blockTransfer.active + << " base=0x" << std::hex << blockTransfer.base + << " size=0x" << blockTransfer.size + << " pause=0x" << blockTransfer.pauseBase + << " offset=0x" << blockTransfer.offset + << std::dec << std::endl; + }); + } + else if (cmd == kLibSdCmdVoiceTransStatus) + { + returnValue = voiceTransfer.completed ? 1u : 0u; + } + else if (cmd == kLibSdCmdBlockTransStatus) + { + if (blockTransfer.active && blockTransfer.size != 0u) + { + blockTransfer.offset = (blockTransfer.offset + kAudioTransferUnit) % blockTransfer.size; + if (blockTransfer.statusTraceCount < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[Audio:BlockStatus] core=" << core + << " status=0x" << std::hex << currentBlockStatus(blockTransfer) + << " offset=0x" << blockTransfer.offset + << " size=0x" << blockTransfer.size + << std::dec << std::endl; + }); + ++blockTransfer.statusTraceCount; + } + } + returnValue = currentBlockStatus(blockTransfer); } else if (cmd == kLibSdCmdSetParam) { (void)cmdArg0; (void)cmdArg1; + returnValue = 0u; } - // Some games only sample the low 24 bits of the reported SPU transfer head. - setReturnU32(ctx, currentBlockPosition()); + setReturnU32(ctx, returnValue); } void sceSdRemoteInit(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp index feb6e8b..8d3818a 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp @@ -1,6 +1,7 @@ #include "Common.h" #include "SIF.h" #include "../Syscalls/RPC.h" +#include "../../ps2_iop_transport.h" #include "runtime/ps2_address.h" #include @@ -397,8 +398,6 @@ namespace ps2_stubs void sceSifGetOtherData(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - (void)runtime; - const uint32_t rdAddr = getRegU32(ctx, 4); const uint32_t srcAddr = getRegU32(ctx, 5); const uint32_t dstAddr = getRegU32(ctx, 6); @@ -424,7 +423,16 @@ namespace ps2_stubs return; } - ps2_syscalls::prepareSoundDriverStatusTransfer(rdram, srcAddr, size); + if (runtime) + { + PS2IopTransport::notifyTransfer(runtime, rdram, { + ps2x::iop::SifTransferKind::GetOtherData, + ps2x::iop::SifTransferPhase::BeforeCopy, + srcAddr, + dstAddr, + size, + }); + } if (!copyGuestByteRange(rdram, dstAddr, srcAddr, size)) { @@ -451,7 +459,16 @@ namespace ps2_stubs std::memcpy(rd + 0x18u, &size, sizeof(size)); } - ps2_syscalls::finalizeSoundDriverStatusTransfer(rdram, srcAddr, dstAddr, size); + if (runtime) + { + PS2IopTransport::notifyTransfer(runtime, rdram, { + ps2x::iop::SifTransferKind::GetOtherData, + ps2x::iop::SifTransferPhase::AfterCopy, + srcAddr, + dstAddr, + size, + }); + } setReturnS32(ctx, 0); } @@ -621,8 +638,6 @@ namespace ps2_stubs void sceSifSetDma(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - (void)runtime; - const uint32_t dmatAddr = getRegU32(ctx, 4); const uint32_t count = getRegU32(ctx, 5); @@ -699,17 +714,31 @@ namespace ps2_stubs for (uint32_t i = 0; i < pendingCount; ++i) { const Ps2SifDmaTransfer &xfer = pending[i]; + if (runtime) + { + PS2IopTransport::notifyTransfer(runtime, rdram, { + ps2x::iop::SifTransferKind::SetDma, + ps2x::iop::SifTransferPhase::BeforeCopy, + xfer.src, + xfer.dest, + static_cast(xfer.size), + }); + } if (!copyGuestByteRange(rdram, xfer.dest, xfer.src, static_cast(xfer.size))) { ok = false; break; } - - ps2_syscalls::noteDtxSifDmaTransfer( - rdram, - xfer.src, - xfer.dest, - static_cast(xfer.size)); + if (runtime) + { + PS2IopTransport::notifyTransfer(runtime, rdram, { + ps2x::iop::SifTransferKind::SetDma, + ps2x::iop::SifTransferPhase::AfterCopy, + xfer.src, + xfer.dest, + static_cast(xfer.size), + }); + } } } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h index 8ad6cdc..a0ba132 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h @@ -1,7 +1,6 @@ #include "ps2_syscalls.h" #include "ps2_log.h" #include "ps2_runtime.h" -#include "runtime/ps2_iop_audio.h" #include "ps2_runtime_macros.h" #include "ps2_stubs.h" #include diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h index c955176..7d1e340 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h @@ -367,29 +367,23 @@ struct SifRpcDebugEvent uint32_t endParam = 0; uint32_t semaId = 0; uint32_t flags = 0; + uint32_t sendPreviewSize = 0; + uint32_t recvPreviewSize = 0; + uint8_t sendPreview[16]{}; + uint8_t recvPreview[16]{}; int32_t result = 0; }; static constexpr size_t kSifRpcDebugHistoryCount = 256u; +static constexpr size_t kSifRpcDebugPreviewBytes = 16u; static constexpr uint32_t kSifRpcDebugFlagNowait = 1u << 0; static constexpr uint32_t kSifRpcDebugFlagHandledByHle = 1u << 1; static constexpr uint32_t kSifRpcDebugFlagCallback = 1u << 2; static constexpr uint32_t kSifRpcDebugFlagMissingClient = 1u << 3; static constexpr uint32_t kSifRpcDebugFlagServerDispatch = 1u << 4; -static constexpr uint32_t kSifRpcDebugFlagDtx = 1u << 5; - -struct SoundDriverRpcState -{ - uintptr_t ownerRuntime = 0; - bool initialized = false; - uint32_t storageBaseAddr = 0; - uint32_t storageSize = 0; - uint32_t statusAddr = 0; - uint32_t addrTableAddr = 0; - uint32_t hdBaseAddr = 0; - uint32_t sqBaseAddr = 0; - uint32_t dataBaseAddr = 0; -}; +static constexpr uint32_t kSifRpcDebugFlagUnhandled = 1u << 6; +static constexpr uint32_t kSifRpcDebugFlagFallbackCopy = 1u << 7; +static constexpr uint32_t kSifRpcDebugFlagFallbackZero = 1u << 8; inline std::unordered_map g_rpc_servers; inline std::unordered_map g_rpc_clients; @@ -402,138 +396,6 @@ inline uint32_t g_rpc_next_id = 1; inline uint32_t g_rpc_packet_index = 0; inline uint32_t g_rpc_server_index = 0; inline uint32_t g_rpc_active_queue = 0; -inline SoundDriverRpcState g_soundDriverRpcState; -inline PS2SoundDriverCompatLayout g_soundDriverCompatLayout; -inline PS2DtxCompatLayout g_dtxCompatLayout; -inline std::mutex g_dtx_rpc_mutex; -inline std::unordered_map g_dtx_remote_by_id; -inline uint32_t g_dtx_next_urpc_obj = 0u; - -struct DtxTransferState -{ - uint32_t dtxId = 0; - uint32_t remoteHandle = 0; - uint32_t eeWorkAddr = 0; - uint32_t iopWorkAddr = 0; - uint32_t wkSize = 0; -}; - -inline std::unordered_map g_dtx_transfer_by_id; - -struct DtxSjxState -{ - uint32_t handle = 0; - uint32_t srcSjHandle = 0; - uint32_t dstSjHandle = 0; - uint32_t line = 0; - uint32_t eeObjAddr = 0; - uint16_t xid = 0; -}; - -inline std::unordered_map g_dtx_sjx_by_handle; - -struct DtxPs2RnaState -{ - uint32_t handle = 0; - uint32_t maxChannels = 0; - uint32_t sjHandle0 = 0; - uint32_t sjHandle1 = 0; - uint32_t channelCount = 0; - uint32_t sampleFreq = 0; - uint32_t volume = 0; - bool playEnabled = false; -}; - -inline std::unordered_map g_dtx_ps2rna_by_handle; - -struct DtxSjrmtState -{ - uint32_t handle = 0; - uint32_t mode = 0; - uint32_t wkAddr = 0; - uint32_t wkSize = 0; - uint32_t readPos = 0; - uint32_t writePos = 0; - uint32_t roomBytes = 0; - uint32_t dataBytes = 0; - uint32_t uuid0 = 0; - uint32_t uuid1 = 0; - uint32_t uuid2 = 0; - uint32_t uuid3 = 0; -}; - -inline std::unordered_map g_dtx_sjrmt_by_handle; - -static uint32_t dtxNormalizeSjrmtCapacity(uint32_t requestedBytes) -{ - if (requestedBytes == 0u || requestedBytes > 0x01000000u) - { - return 0x4000u; - } - return requestedBytes; -} - -static uint32_t dtxAllocUrpcHandleLocked() -{ - const PS2DtxCompatLayout &layout = g_dtxCompatLayout; - if (!layout.hasUrpcObjectRange()) - { - return 0u; - } - - if (g_dtx_next_urpc_obj < layout.urpcObjBase || g_dtx_next_urpc_obj >= layout.urpcObjLimit) - { - g_dtx_next_urpc_obj = layout.urpcObjBase; - } - - for (uint32_t i = 0; i < 4096u; ++i) - { - uint32_t candidate = g_dtx_next_urpc_obj; - g_dtx_next_urpc_obj += layout.urpcObjStride; - if (g_dtx_next_urpc_obj < layout.urpcObjBase || g_dtx_next_urpc_obj >= layout.urpcObjLimit) - { - g_dtx_next_urpc_obj = layout.urpcObjBase; - } - - if (candidate < layout.urpcObjBase || candidate >= layout.urpcObjLimit) - { - continue; - } - - if (g_dtx_sjrmt_by_handle.find(candidate) != g_dtx_sjrmt_by_handle.end()) - { - continue; - } - - if (g_dtx_sjx_by_handle.find(candidate) != g_dtx_sjx_by_handle.end()) - { - continue; - } - - if (g_dtx_ps2rna_by_handle.find(candidate) != g_dtx_ps2rna_by_handle.end()) - { - continue; - } - - bool inUseByDtxRemote = false; - for (const auto &entry : g_dtx_remote_by_id) - { - if (entry.second == candidate) - { - inUseByDtxRemote = true; - break; - } - } - - if (!inUseByDtxRemote) - { - return candidate; - } - } - - return layout.urpcObjBase; -} - struct ExitHandlerEntry { uint32_t func = 0; @@ -682,4 +544,4 @@ inline std::mutex g_sif_module_mutex; inline std::unordered_map g_sif_modules_by_id; inline std::unordered_map g_sif_module_id_by_path; inline int32_t g_next_sif_module_id = 1; -inline uint32_t g_sif_module_log_count = 0; \ No newline at end of file +inline uint32_t g_sif_module_log_count = 0; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp index 4f0fc7f..4aeb00f 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp @@ -1,25 +1,11 @@ #include "Common.h" #include "RPC.h" +#include "../../ps2_iop_transport.h" namespace ps2_syscalls { namespace { - constexpr uint32_t kSoundDriverStatusSize = 0x42u; - constexpr uint32_t kSoundDriverSeInfoOffset = 0x00u; - constexpr uint32_t kSoundDriverMidiInfoOffset = 0x0Cu; - constexpr uint32_t kSoundDriverMidiSumOffset = 0x1Eu; - constexpr uint32_t kSoundDriverSeSumOffset = 0x26u; - constexpr uint32_t kSoundDriverAddrTableEntries = 16u; - constexpr uint32_t kSoundDriverHdRegionSize = 0x4000u; - constexpr uint32_t kSoundDriverSqRegionSize = 0x18000u; - constexpr uint32_t kSoundDriverDataRegionSize = 0x40000u; - constexpr uint32_t kSoundDriverStatusAlignment = 0x100u; - constexpr uint32_t kSoundDriverAddrTableAlignment = 0x100u; - constexpr uint32_t kSoundDriverStorageAlignment = 0x1000u; - constexpr uint32_t kSoundDriverGuestPoolBase = 0x00120000u; - constexpr uint32_t kSoundDriverGuestPoolLimit = 0x00200000u; - SifRpcDebugEvent makeRpcDebugEvent(const char *op, R5900Context *ctx) { SifRpcDebugEvent event{}; @@ -42,560 +28,116 @@ namespace ps2_syscalls pushSifRpcDebugEventLocked(event); } - void resetDtxRpcStateUnlocked() + void fillRpcDebugPreview(const uint8_t *rdram, uint32_t addr, uint32_t size, uint8_t *preview, uint32_t &previewSize) { - g_dtx_remote_by_id.clear(); - g_dtx_transfer_by_id.clear(); - g_dtx_sjx_by_handle.clear(); - g_dtx_ps2rna_by_handle.clear(); - g_dtx_sjrmt_by_handle.clear(); - g_dtx_next_urpc_obj = g_dtxCompatLayout.urpcObjBase; - } - - bool readGuestU16(const uint8_t *rdram, uint32_t addr, uint16_t &out) - { - const uint8_t *ptr = getConstMemPtr(rdram, addr); - if (!ptr) - { - out = 0u; - return false; - } - std::memcpy(&out, ptr, sizeof(out)); - return true; - } - - bool readGuestS16(const uint8_t *rdram, uint32_t addr, int16_t &out) - { - const uint8_t *ptr = getConstMemPtr(rdram, addr); - if (!ptr) - { - out = 0; - return false; - } - std::memcpy(&out, ptr, sizeof(out)); - return true; - } - - bool writeGuestU16(uint8_t *rdram, uint32_t addr, uint16_t value) - { - uint8_t *ptr = getMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - std::memcpy(ptr, &value, sizeof(value)); - return true; - } - - bool writeGuestS16(uint8_t *rdram, uint32_t addr, int16_t value) - { - uint8_t *ptr = getMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - std::memcpy(ptr, &value, sizeof(value)); - return true; - } - - bool readGuestU32(const uint8_t *rdram, uint32_t addr, uint32_t &out) - { - const uint8_t *ptr = getConstMemPtr(rdram, addr); - if (!ptr) - { - out = 0u; - return false; - } - std::memcpy(&out, ptr, sizeof(out)); - return true; - } - - bool writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value) - { - uint8_t *ptr = getMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - std::memcpy(ptr, &value, sizeof(value)); - return true; - } - - bool normalizeGuestLinearAddr(uint32_t addr, uint32_t &out) - { - uint32_t offset = 0u; - bool scratch = false; - if (!ps2ResolveGuestPointer(addr, offset, scratch) || scratch) - { - out = 0u; - return false; - } - out = offset; - return true; - } - - bool hasAnyNonZero(const uint8_t *ptr, size_t bytes) - { - if (!ptr) - { - return false; - } - for (size_t i = 0; i < bytes; ++i) - { - if (ptr[i] != 0u) - { - return true; - } - } - return false; - } - - uint32_t alignUp(uint32_t value, uint32_t alignment) - { - if (alignment == 0u) - { - return value; - } - return (value + (alignment - 1u)) & ~(alignment - 1u); - } - - void resetSoundDriverRpcStateUnlocked() - { - const PS2SoundDriverCompatLayout compat = g_soundDriverCompatLayout; - g_soundDriverRpcState = {}; - g_soundDriverCompatLayout = compat; - } - - void adoptSoundDriverRuntimeUnlocked(PS2Runtime *runtime) - { - const uintptr_t runtimeCookie = reinterpret_cast(runtime); - if (g_soundDriverRpcState.ownerRuntime != runtimeCookie) - { - resetSoundDriverRpcStateUnlocked(); - g_soundDriverRpcState.ownerRuntime = runtimeCookie; - } - } - - bool checkSoundDriverMemoryUnlocked(uint8_t *rdram, PS2Runtime *runtime) - { - if (!rdram || !runtime) - { - return false; - } - - adoptSoundDriverRuntimeUnlocked(runtime); - - if (g_soundDriverRpcState.statusAddr == 0u) - { - const uint32_t statusAddr = alignUp(kSoundDriverGuestPoolBase, kSoundDriverStatusAlignment); - const uint32_t addrTableAddr = - alignUp(statusAddr + kSoundDriverStatusSize, kSoundDriverAddrTableAlignment); - const uint32_t hdBaseAddr = - alignUp(addrTableAddr + (kSoundDriverAddrTableEntries * sizeof(uint32_t)), kSoundDriverStorageAlignment); - const uint32_t sqBaseAddr = alignUp(hdBaseAddr + kSoundDriverHdRegionSize, kSoundDriverStorageAlignment); - const uint32_t dataBaseAddr = alignUp(sqBaseAddr + kSoundDriverSqRegionSize, kSoundDriverStorageAlignment); - const uint32_t storageEnd = dataBaseAddr + kSoundDriverDataRegionSize; - if (storageEnd > kSoundDriverGuestPoolLimit) - { - return false; - } - - g_soundDriverRpcState.statusAddr = statusAddr; - g_soundDriverRpcState.addrTableAddr = addrTableAddr; - g_soundDriverRpcState.hdBaseAddr = hdBaseAddr; - g_soundDriverRpcState.sqBaseAddr = sqBaseAddr; - g_soundDriverRpcState.dataBaseAddr = dataBaseAddr; - g_soundDriverRpcState.storageBaseAddr = hdBaseAddr; - g_soundDriverRpcState.storageSize = - (dataBaseAddr + kSoundDriverDataRegionSize) - hdBaseAddr; - } - - if (g_soundDriverRpcState.statusAddr == 0u || - g_soundDriverRpcState.addrTableAddr == 0u || - g_soundDriverRpcState.storageBaseAddr == 0u) - { - return false; - } - - if (!g_soundDriverRpcState.initialized) - { - rpcZeroRdram(rdram, g_soundDriverRpcState.statusAddr, kSoundDriverStatusSize); - rpcZeroRdram(rdram, - g_soundDriverRpcState.addrTableAddr, - kSoundDriverAddrTableEntries * sizeof(uint32_t)); - rpcZeroRdram(rdram, g_soundDriverRpcState.storageBaseAddr, g_soundDriverRpcState.storageSize); - (void)writeGuestU32(rdram, g_soundDriverRpcState.addrTableAddr + (0u * sizeof(uint32_t)), g_soundDriverRpcState.hdBaseAddr); - (void)writeGuestU32(rdram, g_soundDriverRpcState.addrTableAddr + (1u * sizeof(uint32_t)), g_soundDriverRpcState.sqBaseAddr); - (void)writeGuestU32(rdram, g_soundDriverRpcState.addrTableAddr + (2u * sizeof(uint32_t)), g_soundDriverRpcState.dataBaseAddr); - g_soundDriverRpcState.initialized = true; - } - - return true; - } - - int16_t soundDriverCheckValue(const uint8_t *rdram, uint32_t primaryBase, uint32_t fallbackBase, uint32_t index, uint32_t count) - { - if (index >= count) - { - return 0; - } - - int16_t value = 0; - if (readGuestS16(rdram, primaryBase + (index * sizeof(int16_t)), value) && value != 0) - { - return value; - } - - (void)readGuestS16(rdram, fallbackBase + (index * sizeof(int16_t)), value); - return value; - } - - bool selectSoundDriverCompatChecks(const uint8_t *rdram, uint32_t &seBase, uint32_t &midiBase) - { - const PS2SoundDriverCompatLayout &compat = g_soundDriverCompatLayout; - if (!compat.hasChecksumTables()) - { - return false; - } - - seBase = compat.primarySeCheckAddr; - midiBase = compat.primaryMidiCheckAddr; - - const uint8_t *selectedSe = getConstMemPtr(rdram, seBase); - const uint8_t *selectedMidi = getConstMemPtr(rdram, midiBase); - const bool primaryLooksLive = - hasAnyNonZero(selectedSe, 5u * sizeof(int16_t)) || - hasAnyNonZero(selectedMidi, 4u * sizeof(int16_t)); - - if ((!selectedSe || !selectedMidi) || !primaryLooksLive) - { - const uint8_t *fallbackSe = getConstMemPtr(rdram, compat.fallbackSeCheckAddr); - const uint8_t *fallbackMidi = getConstMemPtr(rdram, compat.fallbackMidiCheckAddr); - const bool fallbackLooksLive = - hasAnyNonZero(fallbackSe, 5u * sizeof(int16_t)) || - hasAnyNonZero(fallbackMidi, 4u * sizeof(int16_t)); - if (fallbackLooksLive) - { - seBase = compat.fallbackSeCheckAddr; - midiBase = compat.fallbackMidiCheckAddr; - return true; - } - } - - return selectedSe != nullptr && selectedMidi != nullptr; - } - - void backfillSoundDriverStatusFromCompatUnlocked(uint8_t *rdram) - { - if (!rdram || g_soundDriverRpcState.statusAddr == 0u || !g_soundDriverCompatLayout.hasChecksumTables()) + previewSize = 0u; + if (!rdram || !addr || !preview || size == 0u) { return; } - uint32_t seBase = 0u; - uint32_t midiBase = 0u; - if (!selectSoundDriverCompatChecks(rdram, seBase, midiBase)) + const uint32_t count = std::min(size, static_cast(kSifRpcDebugPreviewBytes)); + for (uint32_t i = 0; i < count; ++i) { - return; - } - - const uint8_t *selectedSe = getConstMemPtr(rdram, seBase); - const uint8_t *selectedMidi = getConstMemPtr(rdram, midiBase); - uint8_t *status = getMemPtr(rdram, g_soundDriverRpcState.statusAddr); - if (!selectedSe || !selectedMidi || !status) - { - return; - } - - auto backfillZeroS16Slots = [&](uint32_t statusOffset, const uint8_t *compatBase, uint32_t slotCount) - { - for (uint32_t slot = 0u; slot < slotCount; ++slot) + const uint8_t *ptr = getConstMemPtr(rdram, addr + i); + if (!ptr) { - int16_t liveValue = 0; - if (!readGuestS16(rdram, - g_soundDriverRpcState.statusAddr + statusOffset + (slot * sizeof(int16_t)), - liveValue)) - { - continue; - } - - if (liveValue != 0) - { - continue; - } - - int16_t compatValue = 0; - std::memcpy(&compatValue, compatBase + (slot * sizeof(int16_t)), sizeof(compatValue)); - if (compatValue == 0) - { - continue; - } - - (void)writeGuestS16(rdram, - g_soundDriverRpcState.statusAddr + statusOffset + (slot * sizeof(int16_t)), - compatValue); + break; } + preview[i] = *ptr; + ++previewSize; + } + } + + std::string formatRpcTraceBytes(const uint8_t *bytes, uint32_t count) + { + std::string out; + if (!bytes || count == 0u) + { + return out; + } + + char item[4] = {}; + for (uint32_t i = 0; i < count; ++i) + { + std::snprintf(item, sizeof(item), "%02X", bytes[i]); + if (!out.empty()) + { + out.push_back(' '); + } + out += item; + } + return out; + } + +#ifndef PS2X_ENABLE_IOP_RPC_TRACE +#define PS2X_ENABLE_IOP_RPC_TRACE 1 +#endif + +#if PS2X_ENABLE_IOP_RPC_TRACE + std::string loadedIopModuleTraceSummary() + { + std::lock_guard lock(g_sif_module_mutex); + std::string out; + uint32_t count = 0u; + for (const auto &entry : g_sif_modules_by_id) + { + const SifModuleRecord &module = entry.second; + if (!module.loaded) + { + continue; + } + + if (!out.empty()) + { + out += "; "; + } + out += module.pathKey.empty() ? module.path : module.pathKey; + ++count; + if (count >= 6u) + { + break; + } + } + return out; + } + + void logUnhandledRpcTrace(const SifRpcDebugEvent &event) + { + static std::unordered_set loggedSignatures; + if (std::strcmp(event.op ? event.op : "", "CallRpc") != 0) + { + return; + } + + uint64_t signature = 1469598103934665603ull; + auto mixSignature = [&](uint32_t value) + { + signature ^= static_cast(value); + signature *= 1099511628211ull; }; - - backfillZeroS16Slots(kSoundDriverSeSumOffset, selectedSe, 5u); - backfillZeroS16Slots(kSoundDriverMidiSumOffset, selectedMidi, 4u); - } - - size_t soundDriverCommandLength(uint8_t command) - { - const uint8_t hi = static_cast(command & 0xF0u); - switch (hi) - { - case 0x00u: - { - size_t len = 4u; - if ((command & 0x01u) != 0u) - { - ++len; - } - if ((command & 0x02u) != 0u) - { - ++len; - } - if ((command & 0x04u) != 0u) - { - len += 2u; - } - return len; - } - 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; - } - } - - void applySoundDriverCommandUnlocked(uint8_t *rdram, const uint8_t *cmd) - { - if (!rdram || !cmd || g_soundDriverRpcState.statusAddr == 0u) + mixSignature(event.sid); + mixSignature(event.rpcNum); + mixSignature(event.sendSize); + mixSignature(event.recvSize); + if (!loggedSignatures.insert(signature).second || loggedSignatures.size() > 128u) { return; } - const uint8_t op = cmd[0]; - switch (op) - { - case 0x20u: // SdrBgmReq - { - const uint32_t port = cmd[1] & 0x0Fu; - uint16_t midiInfo = 0u; - (void)readGuestU16(rdram, g_soundDriverRpcState.statusAddr + kSoundDriverMidiInfoOffset, midiInfo); - midiInfo = static_cast(midiInfo | static_cast(1u << port)); - (void)writeGuestU16(rdram, g_soundDriverRpcState.statusAddr + kSoundDriverMidiInfoOffset, midiInfo); - break; - } - case 0x21u: // SdrBgmStop - { - const uint32_t port = cmd[1] & 0x0Fu; - uint16_t midiInfo = 0u; - (void)readGuestU16(rdram, g_soundDriverRpcState.statusAddr + kSoundDriverMidiInfoOffset, midiInfo); - midiInfo = static_cast(midiInfo & ~static_cast(1u << port)); - (void)writeGuestU16(rdram, g_soundDriverRpcState.statusAddr + kSoundDriverMidiInfoOffset, midiInfo); - break; - } - case 0x28u: // SdrHDDataSet - { - const uint32_t port = cmd[1] & 0x0Fu; - const PS2SoundDriverCompatLayout &compat = g_soundDriverCompatLayout; - if (compat.primaryMidiCheckAddr != 0u || compat.fallbackMidiCheckAddr != 0u) - { - const int16_t checksum = - soundDriverCheckValue(rdram, compat.primaryMidiCheckAddr, compat.fallbackMidiCheckAddr, port, 4u); - (void)writeGuestS16(rdram, - g_soundDriverRpcState.statusAddr + kSoundDriverMidiSumOffset + (port * sizeof(int16_t)), - checksum); - } - break; - } - case 0x29u: // SdrHDDataSet2 - { - const uint32_t port = cmd[1] & 0x0Fu; - const PS2SoundDriverCompatLayout &compat = g_soundDriverCompatLayout; - if (compat.primarySeCheckAddr != 0u || compat.fallbackSeCheckAddr != 0u) - { - const int16_t checksum = - soundDriverCheckValue(rdram, compat.primarySeCheckAddr, compat.fallbackSeCheckAddr, port, 5u); - (void)writeGuestS16(rdram, - g_soundDriverRpcState.statusAddr + kSoundDriverSeSumOffset + (port * sizeof(int16_t)), - checksum); - } - break; - } - case 0x10u: // SdrSeAllStop - rpcZeroRdram(rdram, g_soundDriverRpcState.statusAddr + kSoundDriverSeInfoOffset, 6u * sizeof(uint16_t)); - break; - default: - break; - } + const std::string modules = loadedIopModuleTraceSummary(); + std::cerr << "[IOP/RPC trace:unhandled]" + << " sid=0x" << std::hex << event.sid + << " rpc=0x" << event.rpcNum + << " pc=0x" << event.pc + << " ra=0x" << event.ra + << " send=0x" << event.sendBuf << "/" << std::dec << event.sendSize + << " recv=0x" << std::hex << event.recvBuf << "/" << std::dec << event.recvSize + << " sendBytes=[" << formatRpcTraceBytes(event.sendPreview, event.sendPreviewSize) << "]" + << " loadedModules=[" << modules << "]" + << std::dec << std::endl; } - - void handleSoundDriverCommandBuffer(uint8_t *rdram, PS2Runtime *runtime, uint32_t sendBuf, uint32_t sendSize) - { - if (!rdram || !runtime || !sendBuf || sendSize == 0u) - { - return; - } - - std::lock_guard lock(g_rpc_mutex); - if (!checkSoundDriverMemoryUnlocked(rdram, runtime)) - { - return; - } - - const uint8_t *packet = getConstMemPtr(rdram, sendBuf); - if (!packet) - { - return; - } - - for (uint32_t offset = 0u; offset < sendSize;) - { - const uint8_t op = packet[offset]; - if (op == 0xFFu) - { - break; - } - - const size_t len = soundDriverCommandLength(op); - if (len == 0u || (offset + len) > sendSize) - { - break; - } - - applySoundDriverCommandUnlocked(rdram, packet + offset); - offset += static_cast(len); - } - } - - bool handleSoundDriverRpcServiceImpl(uint8_t *rdram, PS2Runtime *runtime, - uint32_t sid, uint32_t rpcNum, - uint32_t sendBuf, uint32_t sendSize, - uint32_t recvBuf, uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion) - { - resultPtr = 0u; - signalNowaitCompletion = false; - - if (!runtime || !rdram) - { - return false; - } - - if (sid == IOP_SID_SNDDRV_COMMAND && rpcNum == IOP_RPC_SNDDRV_SUBMIT) - { - handleSoundDriverCommandBuffer(rdram, runtime, sendBuf, sendSize); - return true; - } - - if (sid == IOP_SID_SNDDRV_STATE && - (rpcNum == IOP_RPC_SNDDRV_GET_STATUS_ADDR || rpcNum == IOP_RPC_SNDDRV_GET_ADDR_TABLE)) - { - uint32_t responseWord = 0u; - { - std::lock_guard lock(g_rpc_mutex); - if (!checkSoundDriverMemoryUnlocked(rdram, runtime)) - { - return false; - } - responseWord = - (rpcNum == IOP_RPC_SNDDRV_GET_STATUS_ADDR) ? g_soundDriverRpcState.statusAddr - : g_soundDriverRpcState.addrTableAddr; - } - - if (recvBuf && recvSize >= sizeof(uint32_t)) - { - (void)writeGuestU32(rdram, recvBuf, responseWord); - if (recvSize > sizeof(uint32_t)) - { - rpcZeroRdram(rdram, recvBuf + sizeof(uint32_t), recvSize - sizeof(uint32_t)); - } - resultPtr = recvBuf; - } - - signalNowaitCompletion = true; - return true; - } - - return false; - } - - } // namespace - - bool handleSoundDriverRpcService(uint8_t *rdram, PS2Runtime *runtime, - uint32_t sid, uint32_t rpcNum, - uint32_t sendBuf, uint32_t sendSize, - uint32_t recvBuf, uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion) - { - return handleSoundDriverRpcServiceImpl(rdram, runtime, - sid, rpcNum, - sendBuf, sendSize, - recvBuf, recvSize, - resultPtr, - signalNowaitCompletion); - } - - namespace - { +#endif bool signalRpcCompletionSema(uint32_t semaId) { @@ -627,676 +169,8 @@ namespace ps2_syscalls return signaled; } - const char *dtxUrpcCommandName(uint32_t command) - { - switch (command) - { - case 0u: - return "SJX_CREATE"; - case 1u: - return "SJX_DESTROY"; - case 2u: - return "SJX_RESET"; - case 3u: - return "SJX_REINIT"; - case 8u: - return "PS2RNA_CREATE"; - case 9u: - return "PS2RNA_DESTROY"; - case 10u: - return "PS2RNA_REINIT"; - case 32u: - return "SJRMT_RBF_CREATE"; - case 33u: - return "SJRMT_MEM_CREATE"; - case 34u: - return "SJRMT_UNI_CREATE"; - case 35u: - return "SJRMT_DESTROY"; - case 36u: - return "SJRMT_GET_UUID"; - case 37u: - return "SJRMT_RESET"; - case 38u: - return "SJRMT_GET_CHUNK"; - case 39u: - return "SJRMT_UNGET_CHUNK"; - case 40u: - return "SJRMT_PUT_CHUNK"; - case 41u: - return "SJRMT_GET_NUM_DATA"; - case 42u: - return "SJRMT_IS_GET_CHUNK"; - case 43u: - return "SJRMT_INIT"; - case 44u: - return "SJRMT_FINISH"; - default: - return "UNKNOWN"; - } - } - - const char *dtxStreamName(uint32_t streamId) - { - switch (streamId) - { - case 0u: - return "room"; - case 1u: - return "data"; - default: - return "other"; - } - } - - bool dtxMatchesTransfer(const DtxTransferState &state, uint32_t srcAddr, uint32_t dstAddr, uint32_t sizeBytes) - { - (void)dstAddr; - return state.eeWorkAddr != 0u && - state.wkSize >= 64u && - state.eeWorkAddr == srcAddr && - state.wkSize == sizeBytes; - } - - bool dtxHasReadableRange(const uint8_t *rdram, uint32_t addr, uint32_t len) - { - if (len == 0u) - { - return true; - } - if (addr > (addr + len - 1u)) - { - return false; - } - return getConstMemPtr(rdram, addr) && getConstMemPtr(rdram, addr + len - 1u); - } - - bool dtxReadValidCommandCount(const uint8_t *rdram, uint32_t workAddr, uint32_t workSize, - uint32_t &commandCount) - { - constexpr uint32_t kDtxHeaderSize = 16u; - constexpr uint32_t kDtxCommandSize = 16u; - - commandCount = 0u; - if (workSize < 64u || !readGuestU32(rdram, workAddr, commandCount)) - { - return false; - } - if (commandCount == 0u || commandCount > 128u) - { - return false; - } - - const uint64_t commandBytes = static_cast(commandCount) * kDtxCommandSize; - const uint64_t requiredBytes = kDtxHeaderSize + commandBytes + sizeof(uint32_t); - return requiredBytes <= workSize; - } - - bool dtxLooksLikeSjxPayloadLocked(const uint8_t *rdram, uint32_t workAddr, uint32_t workSize) - { - constexpr uint32_t kSjxHeaderSize = 16u; - constexpr uint32_t kSjxCommandSize = 16u; - - uint32_t commandCount = 0u; - if (!dtxReadValidCommandCount(rdram, workAddr, workSize, commandCount)) - { - return false; - } - - for (uint32_t i = 0; i < commandCount; ++i) - { - const uint32_t cmdAddr = workAddr + kSjxHeaderSize + (i * kSjxCommandSize); - const uint8_t *cmdPtr = getConstMemPtr(rdram, cmdAddr); - if (!cmdPtr) - { - break; - } - - const uint8_t cmdNo = cmdPtr[0]; - const uint8_t cmdLine = cmdPtr[1]; - uint16_t cmdXid = 0u; - uint32_t sjxHandle = 0u; - uint32_t chunkDataAddr = 0u; - uint32_t chunkLen = 0u; - std::memcpy(&cmdXid, cmdPtr + 2u, sizeof(cmdXid)); - std::memcpy(&sjxHandle, cmdPtr + 4u, sizeof(sjxHandle)); - std::memcpy(&chunkDataAddr, cmdPtr + 8u, sizeof(chunkDataAddr)); - std::memcpy(&chunkLen, cmdPtr + 12u, sizeof(chunkLen)); - - if (cmdNo != 0u || chunkLen == 0u || !dtxHasReadableRange(rdram, chunkDataAddr, chunkLen)) - { - continue; - } - - auto sjxIt = g_dtx_sjx_by_handle.find(sjxHandle); - if (sjxIt == g_dtx_sjx_by_handle.end()) - { - continue; - } - - const DtxSjxState &sjx = sjxIt->second; - if (sjx.xid != 0u && sjx.xid != cmdXid) - { - continue; - } - if (cmdLine != sjx.line) - { - continue; - } - if (g_dtx_sjrmt_by_handle.find(sjx.dstSjHandle) == g_dtx_sjrmt_by_handle.end()) - { - continue; - } - - return true; - } - - return false; - } - - bool dtxLooksLikePs2RnaPayloadLocked(const uint8_t *rdram, uint32_t workAddr, uint32_t workSize) - { - constexpr uint32_t kPs2RnaHeaderSize = 16u; - constexpr uint32_t kPs2RnaCommandSize = 16u; - - uint32_t commandCount = 0u; - if (!dtxReadValidCommandCount(rdram, workAddr, workSize, commandCount)) - { - return false; - } - - for (uint32_t i = 0; i < commandCount; ++i) - { - const uint32_t cmdAddr = workAddr + kPs2RnaHeaderSize + (i * kPs2RnaCommandSize); - const uint8_t *cmdPtr = getConstMemPtr(rdram, cmdAddr); - if (!cmdPtr) - { - break; - } - - uint16_t cmdNo = 0u; - uint32_t rnaHandle = 0u; - std::memcpy(&cmdNo, cmdPtr, sizeof(cmdNo)); - std::memcpy(&rnaHandle, cmdPtr + 4u, sizeof(rnaHandle)); - - if (cmdNo <= 5u && g_dtx_ps2rna_by_handle.find(rnaHandle) != g_dtx_ps2rna_by_handle.end()) - { - return true; - } - } - - return false; - } - - bool dtxInferTransferFromPayloadLocked(const uint8_t *rdram, uint32_t srcAddr, uint32_t dstAddr, - uint32_t sizeBytes, DtxTransferState &out) - { - if (g_dtx_transfer_by_id.empty()) - { - return false; - } - - if (dtxLooksLikeSjxPayloadLocked(rdram, srcAddr, sizeBytes)) - { - out = {}; - out.dtxId = 0u; - out.eeWorkAddr = srcAddr; - out.iopWorkAddr = dstAddr; - out.wkSize = sizeBytes; - return true; - } - - if (dtxLooksLikePs2RnaPayloadLocked(rdram, srcAddr, sizeBytes)) - { - out = {}; - out.dtxId = 1u; - out.eeWorkAddr = srcAddr; - out.iopWorkAddr = dstAddr; - out.wkSize = sizeBytes; - return true; - } - - return false; - } - - bool dtxCopyBytes(uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t len) - { - for (uint32_t i = 0; i < len; ++i) - { - const uint8_t *src = getConstMemPtr(rdram, srcAddr + i); - uint8_t *dst = getMemPtr(rdram, dstAddr + i); - if (!src || !dst) - { - return false; - } - *dst = *src; - } - return true; - } - - void dtxAppendToSjrmtData(uint8_t *rdram, DtxSjrmtState &state, uint32_t srcDataAddr, uint32_t requestedLen) - { - if (!rdram) - { - return; - } - - const uint32_t cap = (state.wkSize == 0u) ? 0x4000u : state.wkSize; - if (cap == 0u || state.wkAddr == 0u || state.roomBytes == 0u || requestedLen == 0u) - { - return; - } - - const uint32_t requestedCopyLen = std::min(requestedLen, state.roomBytes); - uint32_t copiedLen = 0u; - for (uint32_t i = 0; i < requestedCopyLen; ++i) - { - const uint32_t writeOffset = (state.writePos + i) % cap; - if (!dtxCopyBytes(rdram, state.wkAddr + writeOffset, srcDataAddr + i, 1u)) - { - break; - } - ++copiedLen; - } - - state.writePos = (state.writePos + copiedLen) % cap; - state.roomBytes -= copiedLen; - state.dataBytes = std::min(cap, state.dataBytes + copiedLen); - } - - void dtxConsumeSjrmtDataLocked(DtxSjrmtState &state) - { - const uint32_t cap = (state.wkSize == 0u) ? 0x4000u : state.wkSize; - if (cap == 0u || state.dataBytes == 0u) - { - return; - } - - const uint32_t consumed = std::min(cap, state.dataBytes); - state.readPos = (state.readPos + consumed) % cap; - state.dataBytes -= consumed; - state.roomBytes = std::min(cap, state.roomBytes + consumed); - } - - void dtxConsumeActivePs2RnaStreamsLocked() - { - for (const auto &entry : g_dtx_ps2rna_by_handle) - { - const DtxPs2RnaState &rna = entry.second; - if (!rna.playEnabled) - { - continue; - } - - auto consumeHandle = [&](uint32_t sjHandle) - { - if (sjHandle == 0u) - { - return; - } - - auto it = g_dtx_sjrmt_by_handle.find(sjHandle); - if (it == g_dtx_sjrmt_by_handle.end()) - { - return; - } - - dtxConsumeSjrmtDataLocked(it->second); - }; - - consumeHandle(rna.sjHandle0); - consumeHandle(rna.sjHandle1); - } - } - - void dtxApplySjxPayload(uint8_t *rdram, const DtxTransferState &transfer) - { - if (!rdram || transfer.eeWorkAddr == 0u || transfer.wkSize < 64u) - { - return; - } - - uint32_t commandCount = 0u; - if (!readGuestU32(rdram, transfer.eeWorkAddr, commandCount)) - { - return; - } - - commandCount = std::min(commandCount, 128u); - if (commandCount == 0u) - { - return; - } - - constexpr uint32_t kSjxHeaderSize = 16u; - constexpr uint32_t kSjxCommandSize = 16u; - - std::lock_guard lock(g_dtx_rpc_mutex); - for (uint32_t i = 0; i < commandCount; ++i) - { - const uint32_t cmdAddr = transfer.eeWorkAddr + kSjxHeaderSize + (i * kSjxCommandSize); - uint8_t *cmdPtr = getMemPtr(rdram, cmdAddr); - if (!cmdPtr) - { - break; - } - - const uint8_t cmdNo = cmdPtr[0]; - const uint8_t cmdLine = cmdPtr[1]; - uint16_t cmdXid = 0u; - uint32_t sjxHandle = 0u; - uint32_t chunkDataAddr = 0u; - uint32_t chunkLen = 0u; - std::memcpy(&cmdXid, cmdPtr + 2u, sizeof(cmdXid)); - std::memcpy(&sjxHandle, cmdPtr + 4u, sizeof(sjxHandle)); - std::memcpy(&chunkDataAddr, cmdPtr + 8u, sizeof(chunkDataAddr)); - std::memcpy(&chunkLen, cmdPtr + 12u, sizeof(chunkLen)); - - if (cmdNo != 0u || chunkLen == 0u) - { - continue; - } - - auto sjxIt = g_dtx_sjx_by_handle.find(sjxHandle); - if (sjxIt == g_dtx_sjx_by_handle.end()) - { - continue; - } - - DtxSjxState &sjx = sjxIt->second; - if (sjx.xid != 0u && sjx.xid != cmdXid) - { - continue; - } - - auto sjrmtIt = g_dtx_sjrmt_by_handle.find(sjx.dstSjHandle); - if (sjrmtIt == g_dtx_sjrmt_by_handle.end()) - { - continue; - } - - if (cmdLine == sjx.line) - { - dtxAppendToSjrmtData(rdram, sjrmtIt->second, chunkDataAddr, chunkLen); - - // The IOP side consumes data from the remote SJ stream and returns the - // chunk to the EE-side room queue. Rewriting the ack packet to line 0 - // makes the EE sjx_rcvcbf recycle the chunk instead of requeueing it - // as playable data forever. - cmdPtr[1] = 0u; - } - } - - dtxConsumeActivePs2RnaStreamsLocked(); - } - - void dtxApplyPs2RnaPayload(uint8_t *rdram, const DtxTransferState &transfer) - { - if (!rdram || transfer.eeWorkAddr == 0u || transfer.wkSize < 64u) - { - return; - } - - uint32_t commandCount = 0u; - if (!readGuestU32(rdram, transfer.eeWorkAddr, commandCount)) - { - return; - } - - commandCount = std::min(commandCount, 128u); - if (commandCount == 0u) - { - return; - } - - constexpr uint32_t kPs2RnaHeaderSize = 16u; - constexpr uint32_t kPs2RnaCommandSize = 16u; - - std::lock_guard lock(g_dtx_rpc_mutex); - for (uint32_t i = 0; i < commandCount; ++i) - { - const uint32_t cmdAddr = transfer.eeWorkAddr + kPs2RnaHeaderSize + (i * kPs2RnaCommandSize); - const uint8_t *cmdPtr = getConstMemPtr(rdram, cmdAddr); - if (!cmdPtr) - { - break; - } - - uint16_t cmdNo = 0u; - uint32_t rnaHandle = 0u; - uint32_t arg1 = 0u; - uint32_t arg2 = 0u; - std::memcpy(&cmdNo, cmdPtr + 0u, sizeof(cmdNo)); - std::memcpy(&rnaHandle, cmdPtr + 4u, sizeof(rnaHandle)); - std::memcpy(&arg1, cmdPtr + 8u, sizeof(arg1)); - std::memcpy(&arg2, cmdPtr + 12u, sizeof(arg2)); - - auto it = g_dtx_ps2rna_by_handle.find(rnaHandle); - if (it == g_dtx_ps2rna_by_handle.end()) - { - continue; - } - - DtxPs2RnaState &state = it->second; - switch (cmdNo) - { - case 0u: // IOPRNA_CMD_START - state.playEnabled = true; - break; - case 1u: // IOPRNA_CMD_STOP - state.playEnabled = false; - break; - case 2u: // IOPRNA_CMD_SETPSW - state.playEnabled = (arg1 != 0u); - break; - case 3u: // IOPRNA_CMD_SETNCH - state.channelCount = arg1; - break; - case 4u: // IOPRNA_CMD_SETSFREQ - state.sampleFreq = arg1; - break; - case 5u: // IOPRNA_CMD_SETVOL - state.volume = arg2; - break; - default: - break; - } - } - - dtxConsumeActivePs2RnaStreamsLocked(); - } - } // namespace - void noteDtxSifDmaTransfer(uint8_t *rdram, uint32_t srcAddr, uint32_t dstAddr, uint32_t sizeBytes) - { - if (!rdram || sizeBytes < 64u) - { - return; - } - - uint32_t normalizedSrc = 0u; - uint32_t normalizedDst = 0u; - if (!normalizeGuestLinearAddr(srcAddr, normalizedSrc) || - !normalizeGuestLinearAddr(dstAddr, normalizedDst)) - { - return; - } - - DtxTransferState matched{}; - bool found = false; - bool inferredPayload = false; - { - std::lock_guard lock(g_dtx_rpc_mutex); - if (!g_dtxCompatLayout.isConfigured()) - { - return; - } - for (const auto &entry : g_dtx_transfer_by_id) - { - if (dtxMatchesTransfer(entry.second, normalizedSrc, normalizedDst, sizeBytes)) - { - matched = entry.second; - found = true; - break; - } - } - if (!found && dtxInferTransferFromPayloadLocked(rdram, normalizedSrc, normalizedDst, sizeBytes, matched)) - { - found = true; - inferredPayload = true; - } - } - - if (!found) - { - const uint8_t *missPtr = getConstMemPtr(rdram, normalizedSrc); - - static uint32_t dtxMissLogCount = 0u; - if (dtxMissLogCount < 64u) - { - uint32_t knownTransfers = 0u; - uint32_t sampleDtxId = 0u; - uint32_t sampleSrc = 0u; - uint32_t sampleSize = 0u; - - { - std::lock_guard lock(g_dtx_rpc_mutex); - knownTransfers = static_cast(g_dtx_transfer_by_id.size()); - if (!g_dtx_transfer_by_id.empty()) - { - const auto &sample = *g_dtx_transfer_by_id.begin(); - sampleDtxId = sample.second.dtxId; - sampleSrc = sample.second.eeWorkAddr; - sampleSize = sample.second.wkSize; - } - } - - if (missPtr) - { - RUNTIME_LOG("[sceSifSetDma:DTX_MISS_DUMP] src=0x" << std::hex << normalizedSrc - << " dst=0x" << normalizedDst - << " size=0x" << sizeBytes - << " known=" << std::dec << knownTransfers - << " sampleDtxId=0x" << std::hex << sampleDtxId - << " sampleSrc=0x" << sampleSrc - << " sampleSize=0x" << sampleSize - << " first=" - << std::setw(2) << std::setfill('0') << static_cast(missPtr[0]) << " " - << std::setw(2) << static_cast(missPtr[1]) << " " - << std::setw(2) << static_cast(missPtr[2]) << " " - << std::setw(2) << static_cast(missPtr[3]) << " " - << std::setw(2) << static_cast(missPtr[4]) << " " - << std::setw(2) << static_cast(missPtr[5]) << " " - << std::setw(2) << static_cast(missPtr[6]) << " " - << std::setw(2) << static_cast(missPtr[7]) - << std::setfill(' ') - << std::dec << std::endl); - } - - ++dtxMissLogCount; - } - - return; - } - - const uint32_t footerTicketAddr = matched.eeWorkAddr + matched.wkSize - sizeof(uint32_t); - uint32_t ticketNo = 0u; - if (!readGuestU32(rdram, footerTicketAddr, ticketNo)) - { - return; - } - - (void)writeGuestU32(rdram, footerTicketAddr, ticketNo + 1u); - - if (matched.dtxId == 0u) - { - dtxApplySjxPayload(rdram, matched); - } - else if (matched.dtxId == 1u) - { - dtxApplyPs2RnaPayload(rdram, matched); - } - - static uint32_t dtxAckLogCount = 0u; - if (dtxAckLogCount < 32u) - { - RUNTIME_LOG("[sceSifSetDma:DTX_ACK] dtxId=0x" << std::hex << matched.dtxId - << " ee=0x" << matched.eeWorkAddr - << " iop=0x" << matched.iopWorkAddr - << " size=0x" << matched.wkSize - << " ticket=0x" << ticketNo - << "->0x" << (ticketNo + 1u)); - if (inferredPayload) - { - RUNTIME_LOG(" inferred=1"); - } - if (matched.dtxId == 0u) - { - uint32_t totalData = 0u; - std::lock_guard lock(g_dtx_rpc_mutex); - for (const auto &entry : g_dtx_sjrmt_by_handle) - { - totalData += entry.second.dataBytes; - } - RUNTIME_LOG(" sjrmtData=0x" << totalData); - } - RUNTIME_LOG(std::dec << std::endl); - ++dtxAckLogCount; - } - } - - void resetSoundDriverRpcState() - { - std::lock_guard lock(g_rpc_mutex); - resetSoundDriverRpcStateUnlocked(); - } - - void setSoundDriverCompatLayout(const PS2SoundDriverCompatLayout &layout) - { - std::lock_guard lock(g_rpc_mutex); - g_soundDriverCompatLayout = layout; - } - - void clearSoundDriverCompatLayout() - { - std::lock_guard lock(g_rpc_mutex); - g_soundDriverCompatLayout = {}; - } - - void setDtxCompatLayout(const PS2DtxCompatLayout &layout) - { - std::scoped_lock lock(g_rpc_mutex, g_dtx_rpc_mutex); - g_dtxCompatLayout = layout; - resetDtxRpcStateUnlocked(); - } - - void clearDtxCompatLayout() - { - std::scoped_lock lock(g_rpc_mutex, g_dtx_rpc_mutex); - g_dtxCompatLayout = {}; - resetDtxRpcStateUnlocked(); - } - - void prepareSoundDriverStatusTransfer(uint8_t *rdram, uint32_t srcAddr, uint32_t size) - { - std::lock_guard lock(g_rpc_mutex); - if (srcAddr != g_soundDriverRpcState.statusAddr || size != kSoundDriverStatusSize) - { - return; - } - - backfillSoundDriverStatusFromCompatUnlocked(rdram); - } - - void finalizeSoundDriverStatusTransfer(uint8_t *rdram, uint32_t srcAddr, uint32_t dstAddr, uint32_t size) - { - (void)rdram; - (void)srcAddr; - (void)dstAddr; - (void)size; - } - void SifStopModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const int32_t moduleId = static_cast(getRegU32(ctx, 4)); // $a0 @@ -1365,10 +239,10 @@ namespace ps2_syscalls void SifInitRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - std::scoped_lock lock(g_rpc_mutex, g_dtx_rpc_mutex); + std::lock_guard lock(g_rpc_mutex); if (runtime) { - runtime->iop().init(rdram); + PS2IopTransport::reset(runtime); } if (!g_rpc_initialized) { @@ -1383,16 +257,9 @@ namespace ps2_syscalls { g_sif_rpc_debug_history[i] = SifRpcDebugEvent{}; } - resetDtxRpcStateUnlocked(); - g_soundDriverRpcState.initialized = false; g_rpc_initialized = true; RUNTIME_LOG("[SifInitRpc] Initialized"); } - else - { - resetDtxRpcStateUnlocked(); - g_soundDriverRpcState.initialized = false; - } SifRpcDebugEvent event = makeRpcDebugEvent("InitRpc", ctx); event.result = 0; @@ -1489,147 +356,134 @@ namespace ps2_syscalls { std::lock_guard rpcCallLock(g_sif_call_rpc_mutex); - uint32_t clientPtr = getRegU32(ctx, 4); - uint32_t rpcNum = getRegU32(ctx, 5); - uint32_t mode = getRegU32(ctx, 6); - uint32_t sendBuf = getRegU32(ctx, 7); - uint32_t sendSize = 0; - uint32_t recvBuf = 0; - uint32_t recvSize = 0; - uint32_t endFunc = 0; - uint32_t endParam = 0; + const uint32_t clientPtr = getRegU32(ctx, 4); + const uint32_t rpcNum = getRegU32(ctx, 5); + const uint32_t mode = getRegU32(ctx, 6); + const uint32_t sendBuf = getRegU32(ctx, 7); + const uint32_t stackPointer = getRegU32(ctx, 29); - // Decode both extended-reg convention (EE default) and standard O32 stack convention, - // picking REG whenever plausible, to avoid zero-collision on the stack. - uint32_t sp = getRegU32(ctx, 29); + const uint32_t sendSizeRegisters = getRegU32(ctx, 8); + const uint32_t receiveBufferRegisters = getRegU32(ctx, 9); + const uint32_t receiveSizeRegisters = getRegU32(ctx, 10); + const uint32_t endFunctionRegisters = getRegU32(ctx, 11); + uint32_t endParameterRegisters = 0u; + (void)readStackU32(rdram, stackPointer, 0x0u, endParameterRegisters); - uint32_t sendSizeReg = getRegU32(ctx, 8); - uint32_t recvBufReg = getRegU32(ctx, 9); - uint32_t recvSizeReg = getRegU32(ctx, 10); - uint32_t endFuncReg = getRegU32(ctx, 11); - uint32_t endParamReg = 0; - (void)readStackU32(rdram, sp, 0x0, endParamReg); + uint32_t sendSizeStack = 0u; + uint32_t receiveBufferStack = 0u; + uint32_t receiveSizeStack = 0u; + uint32_t endFunctionStack = 0u; + uint32_t endParameterStack = 0u; + (void)readStackU32(rdram, stackPointer, 0x10u, sendSizeStack); + (void)readStackU32(rdram, stackPointer, 0x14u, receiveBufferStack); + (void)readStackU32(rdram, stackPointer, 0x18u, receiveSizeStack); + (void)readStackU32(rdram, stackPointer, 0x1Cu, endFunctionStack); + (void)readStackU32(rdram, stackPointer, 0x20u, endParameterStack); - uint32_t sendSizeStk = 0; - uint32_t recvBufStk = 0; - uint32_t recvSizeStk = 0; - uint32_t endFuncStk = 0; - uint32_t endParamStk = 0; - (void)readStackU32(rdram, sp, 0x10, sendSizeStk); - (void)readStackU32(rdram, sp, 0x14, recvBufStk); - (void)readStackU32(rdram, sp, 0x18, recvSizeStk); - (void)readStackU32(rdram, sp, 0x1C, endFuncStk); - (void)readStackU32(rdram, sp, 0x20, endParamStk); - - auto looksLikeGuestPtr = [&](uint32_t v) -> bool + const auto looksLikeGuestPointer = [](uint32_t value) { - if (v == 0) + if (value == 0u) + { return true; - const uint32_t norm = v & 0x1FFFFFFFu; - return norm >= 0x10000u && norm < PS2_RAM_SIZE; + } + const uint32_t normalized = value & 0x1FFFFFFFu; + return normalized >= 0x10000u && normalized < PS2_RAM_SIZE; }; - - auto looksLikeSize = [&](uint32_t v) -> bool + const auto looksLikeSize = [](uint32_t value) { - return v <= 0x2000000u; + return value <= 0x02000000u; }; - - auto looksLikeFunc = [&](uint32_t v) -> bool + const auto plausiblePack = [&](uint32_t sendSize, + uint32_t receiveBuffer, + uint32_t receiveSize, + uint32_t endFunction) { - return v == 0 || looksLikeGuestPtr(v); + return looksLikeSize(sendSize) && + looksLikeGuestPointer(receiveBuffer) && + looksLikeSize(receiveSize) && + (endFunction == 0u || looksLikeGuestPointer(endFunction)); }; - auto plausiblePack = [&](uint32_t sendSz, uint32_t rbuf, uint32_t rsz, uint32_t endFn) -> bool - { - return looksLikeSize(sendSz) && looksLikeGuestPtr(rbuf) && looksLikeSize(rsz) && looksLikeFunc(endFn); - }; + const bool registerPackPlausible = + plausiblePack(sendSizeRegisters, + receiveBufferRegisters, + receiveSizeRegisters, + endFunctionRegisters); + const bool stackPackPlausible = + plausiblePack(sendSizeStack, + receiveBufferStack, + receiveSizeStack, + endFunctionStack); - const bool regPackPlausible = plausiblePack(sendSizeReg, recvBufReg, recvSizeReg, endFuncReg); - const bool stackPackPlausible = plausiblePack(sendSizeStk, recvBufStk, recvSizeStk, endFuncStk); - - uint32_t boundSidHint = 0u; + uint32_t sidHint = 0u; { std::lock_guard lock(g_rpc_mutex); - auto it = g_rpc_clients.find(clientPtr); - if (it != g_rpc_clients.end()) + const auto clientIt = g_rpc_clients.find(clientPtr); + if (clientIt != g_rpc_clients.end()) { - boundSidHint = it->second.sid; - } - } - PS2DtxCompatLayout dtxCompat{}; - { - std::lock_guard lock(g_dtx_rpc_mutex); - dtxCompat = g_dtxCompatLayout; - } - - auto looksLikeDtxCreatePack = [&](uint32_t sendSz, uint32_t rbuf, uint32_t rsz) -> bool - { - return rbuf != 0u && rsz >= 4u && rsz <= 0x40u && - sendSz >= 12u && sendSz <= 0x1000u; - }; - - const bool isDtxCreate34Call = dtxCompat.isConfigured() && - (boundSidHint == dtxCompat.rpcSid) && - (rpcNum == 0x422u); - const bool forceStackForDtxCreate34 = - isDtxCreate34Call && - stackPackPlausible && - looksLikeDtxCreatePack(sendSizeStk, recvBufStk, recvSizeStk) && - !looksLikeDtxCreatePack(sendSizeReg, recvBufReg, recvSizeReg); - - bool useRegConvention = true; - if (forceStackForDtxCreate34) - { - useRegConvention = false; - } - else if (!regPackPlausible && stackPackPlausible) - { - const bool regHasValidCallback = (endFuncReg != 0u) && looksLikeFunc(endFuncReg); - const bool stackHasValidCallback = (endFuncStk != 0u) && looksLikeFunc(endFuncStk); - if (!(regHasValidCallback && !stackHasValidCallback)) - { - useRegConvention = false; + sidHint = clientIt->second.sid; } } - sendSize = useRegConvention ? sendSizeReg : sendSizeStk; - recvBuf = useRegConvention ? recvBufReg : recvBufStk; - recvSize = useRegConvention ? recvSizeReg : recvSizeStk; - endFunc = useRegConvention ? endFuncReg : endFuncStk; - endParam = useRegConvention ? endParamReg : endParamStk; - - const bool isDtxLikeRpc = dtxCompat.isConfigured() && - ((boundSidHint == dtxCompat.rpcSid) || ((rpcNum & 0xFF00u) == 0x0400u)); - static uint32_t dtxAbiLogCount = 0u; - if (isDtxLikeRpc && dtxAbiLogCount < 96u) + ps2x::iop::RpcAbi selectedAbi = ps2x::iop::RpcAbi::RuntimeDefault; { - RUNTIME_LOG("[SifCallRpc:ABI] client=0x" << std::hex << clientPtr - << " rpc=0x" << rpcNum - << " sidHint=0x" << boundSidHint - << " useReg=" << (useRegConvention ? 1 : 0) - << " reg=(" << sendSizeReg << "," << recvBufReg << "," << recvSizeReg << "," << endFuncReg << "," << endParamReg << ")" - << " stk=(" << sendSizeStk << "," << recvBufStk << "," << recvSizeStk << "," << endFuncStk << "," << endParamStk << ")" - << " plausible=(" << (regPackPlausible ? 1 : 0) << "," << (stackPackPlausible ? 1 : 0) << ")" - << " force34=" << (forceStackForDtxCreate34 ? 1 : 0) - << std::dec << std::endl); - ++dtxAbiLogCount; + ps2x::iop::RpcAbiRequest request{}; + request.boundSid = sidHint; + request.function = rpcNum; + request.registers = { + sendSizeRegisters, + receiveBufferRegisters, + receiveSizeRegisters, + endFunctionRegisters, + endParameterRegisters, + registerPackPlausible, + }; + request.stack = { + sendSizeStack, + receiveBufferStack, + receiveSizeStack, + endFunctionStack, + endParameterStack, + stackPackPlausible, + }; + selectedAbi = PS2IopTransport::selectRpcAbi(runtime, request); } - t_SifRpcClientData *client = reinterpret_cast(getMemPtr(rdram, clientPtr)); + bool useRegisterConvention = selectedAbi != ps2x::iop::RpcAbi::Stack; + if (selectedAbi == ps2x::iop::RpcAbi::RuntimeDefault && !registerPackPlausible && stackPackPlausible) + { + const bool registersHaveCallback = endFunctionRegisters != 0u && looksLikeGuestPointer(endFunctionRegisters); + const bool stackHasCallback = endFunctionStack != 0u && looksLikeGuestPointer(endFunctionStack); + if (!(registersHaveCallback && !stackHasCallback)) + { + useRegisterConvention = false; + } + } + else if (selectedAbi == ps2x::iop::RpcAbi::Registers) + { + useRegisterConvention = true; + } + const uint32_t sendSize = useRegisterConvention ? sendSizeRegisters : sendSizeStack; + const uint32_t receiveBuffer = useRegisterConvention ? receiveBufferRegisters : receiveBufferStack; + const uint32_t receiveSize = useRegisterConvention ? receiveSizeRegisters : receiveSizeStack; + const uint32_t endFunction = useRegisterConvention ? endFunctionRegisters : endFunctionStack; + const uint32_t endParameter = useRegisterConvention ? endParameterRegisters : endParameterStack; + + auto *client = reinterpret_cast(getMemPtr(rdram, clientPtr)); if (!client) { SifRpcDebugEvent event = makeRpcDebugEvent("CallRpc", ctx); event.clientPtr = clientPtr; + event.sid = sidHint; event.rpcNum = rpcNum; - event.sid = boundSidHint; event.mode = mode; event.sendBuf = sendBuf; event.sendSize = sendSize; - event.recvBuf = recvBuf; - event.recvSize = recvSize; - event.endFunc = endFunc; - event.endParam = endParam; + event.recvBuf = receiveBuffer; + event.recvSize = receiveSize; + event.endFunc = endFunction; + event.endParam = endParameter; event.flags = kSifRpcDebugFlagMissingClient | ((mode & kSifRpcModeNowait) ? kSifRpcDebugFlagNowait : 0u); event.result = -1; pushSifRpcDebugEvent(event); @@ -1638,840 +492,198 @@ namespace ps2_syscalls } client->command = rpcNum; - client->end_function = endFunc; - client->end_param = endParam; + client->end_function = endFunction; + client->end_param = endParameter; client->hdr.mode = mode; + uint32_t sid = 0u; { std::lock_guard lock(g_rpc_mutex); - g_rpc_clients[clientPtr].busy = true; - g_rpc_clients[clientPtr].last_rpc = rpcNum; - uint32_t sid = g_rpc_clients[clientPtr].sid; - if (sid) + auto &state = g_rpc_clients[clientPtr]; + state.busy = true; + state.last_rpc = rpcNum; + sid = state.sid; + if (sid != 0u) { - auto it = g_rpc_servers.find(sid); - if (it != g_rpc_servers.end()) + const auto serverIt = g_rpc_servers.find(sid); + if (serverIt != g_rpc_servers.end() && + serverIt->second.sd_ptr != 0u) { - uint32_t mappedServer = it->second.sd_ptr; - if (mappedServer && client->server != mappedServer) - { - client->server = mappedServer; - } + client->server = serverIt->second.sd_ptr; } } } - uint32_t sid = 0; + const uint32_t serverPtr = client->server; + auto *server = serverPtr + ? reinterpret_cast(getMemPtr(rdram, serverPtr)) + : nullptr; + if (server) { - std::lock_guard lock(g_rpc_mutex); - auto it = g_rpc_clients.find(clientPtr); - if (it != g_rpc_clients.end()) + server->client = clientPtr; + server->pkt_addr = client->hdr.pkt_addr; + server->rpc_number = rpcNum; + server->size = static_cast(sendSize); + server->recvbuf = receiveBuffer; + server->rsize = static_cast(receiveSize); + server->rmode = ((mode & kSifRpcModeNowait) && endFunction == 0u) ? 0 : 1; + server->rid = 0; + + if (server->buf != 0u && sendBuf != 0u && sendSize != 0u) { - sid = it->second.sid; + rpcCopyToRdram(rdram, server->buf, sendBuf, sendSize); } } - uint32_t serverPtr = client->server; - t_SifRpcServerData *sd = serverPtr ? reinterpret_cast(getMemPtr(rdram, serverPtr)) : nullptr; - - if (sd) - { - sd->client = clientPtr; - sd->pkt_addr = client->hdr.pkt_addr; - sd->rpc_number = rpcNum; - sd->size = static_cast(sendSize); - sd->recvbuf = recvBuf; - sd->rsize = static_cast(recvSize); - sd->rmode = ((mode & kSifRpcModeNowait) && endFunc == 0) ? 0 : 1; - sd->rid = 0; - } - - if (sd && sd->buf && sendBuf && sendSize > 0) - { - rpcCopyToRdram(rdram, sd->buf, sendBuf, sendSize); - } - - uint32_t resultPtr = 0; + uint32_t resultPointer = 0u; bool handled = false; bool handledByIop = false; - bool callbackInvokedForDebug = false; + bool serverDispatched = false; + bool callbackCompleted = false; + bool copiedFallback = false; + bool zeroedFallback = false; + ps2x::iop::RpcResult iopResult{}; - auto readRpcU32 = [&](uint32_t addr, uint32_t &out) -> bool + const auto completionSemaphore = [&]() { - if (!addr) + uint32_t semaphore = static_cast(client->hdr.sema_id); + if (semaphore == 0xFFFFFFFFu || semaphore == 0u) { - return false; + semaphore = endParameter; } - const uint8_t *ptr = getConstMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - std::memcpy(&out, ptr, sizeof(out)); - return true; + return semaphore; }; - auto writeRpcU32 = [&](uint32_t addr, uint32_t value) -> bool { - if (!addr) - { - return false; - } - uint8_t *ptr = getMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - std::memcpy(ptr, &value, sizeof(value)); - return true; - }; + ps2x::iop::RpcRequest request{}; + request.clientAddress = clientPtr; + request.serverAddress = serverPtr; + request.serverFunction = server ? server->func : 0u; + request.serverBuffer = server ? server->buf : 0u; + request.sid = sid; + request.function = rpcNum; + request.mode = mode; + request.send = {sendBuf, sendSize}; + request.receive = {receiveBuffer, receiveSize}; + request.endFunction = endFunction; + request.endParameter = endParameter; - if (!handled && runtime) + iopResult = PS2IopTransport::handleRpc(runtime, rdram, ctx, request); + handled = iopResult.handled; + handledByIop = iopResult.handled; + resultPointer = iopResult.resultAddress; + + if (iopResult.signalNowaitCompletion && + (mode & kSifRpcModeNowait) != 0u) + { + (void)signalRpcCompletionSema(completionSemaphore()); + } + if (iopResult.signalCompletion) + { + (void)signalRpcCompletionSema(completionSemaphore()); + } + } + + if (server && server->func != 0u && iopResult.serverDispatchPolicy != ps2x::iop::ServerDispatchPolicy::Suppress) { - runtime->iop().init(rdram); - uint32_t iopResultPtr = 0u; - bool iopSignalNowaitCompletion = false; - if (runtime->iop().handleRPC(runtime, - sid, rpcNum, - sendBuf, sendSize, - recvBuf, recvSize, - iopResultPtr, - iopSignalNowaitCompletion)) + uint32_t serverResult = 0u; + serverDispatched = rpcInvokeFunction(rdram, + ctx, + runtime, + server->func, + rpcNum, + server->buf, + sendSize, + 0u, + &serverResult); + if (serverDispatched) { handled = true; - handledByIop = true; - resultPtr = iopResultPtr; - if (iopSignalNowaitCompletion && (mode & kSifRpcModeNowait) != 0u) + resultPointer = serverResult; + if (resultPointer == 0u && server->buf != 0u) { - uint32_t semaId = static_cast(client->hdr.sema_id); - if (semaId == 0xFFFFFFFFu || semaId == 0u) - { - semaId = endParam; - } - (void)signalRpcCompletionSema(semaId); + resultPointer = server->buf; + } + if (resultPointer == 0u) + { + resultPointer = receiveBuffer; } } } - const bool isDtxUrpc = dtxCompat.isUrpcRpc(sid, rpcNum); - uint32_t dtxUrpcCommand = isDtxUrpc ? (rpcNum & 0xFFu) : 0u; - uint32_t dtxUrpcFn = 0; - uint32_t dtxUrpcObj = 0; - uint32_t dtxUrpcSend0 = 0; - bool dtxUrpcDispatchAttempted = false; - bool dtxUrpcFallbackEmulated = false; - bool dtxUrpcFallbackCreate34 = false; - bool hasUrpcHandler = false; - if (isDtxUrpc) + if (receiveBuffer != 0u && receiveSize != 0u) { - if (sendBuf && sendSize >= sizeof(uint32_t)) + if (handled && resultPointer != 0u && resultPointer != receiveBuffer) { - (void)readRpcU32(sendBuf, dtxUrpcSend0); + rpcCopyToRdram(rdram, + receiveBuffer, + resultPointer, + receiveSize); } - if (dtxCompat.hasUrpcTables() && dtxUrpcCommand < 64u) + else if (!handled && sendBuf != 0u && sendSize != 0u && sendBuf != receiveBuffer) { - (void)readRpcU32(dtxCompat.urpcFnTableBase + (dtxUrpcCommand * 4u), dtxUrpcFn); - (void)readRpcU32(dtxCompat.urpcObjTableBase + (dtxUrpcCommand * 4u), dtxUrpcObj); - } - hasUrpcHandler = (dtxUrpcCommand < 64u) && (dtxUrpcFn != 0u); - } - const bool allowServerDispatch = !isDtxUrpc || hasUrpcHandler; - const bool isDtxSid = dtxCompat.isConfigured() && sid == dtxCompat.rpcSid; - - if (sd && sd->func && (!isDtxSid || isDtxUrpc) && allowServerDispatch) - { - dtxUrpcDispatchAttempted = dtxUrpcDispatchAttempted || isDtxUrpc; - handled = rpcInvokeFunction(rdram, ctx, runtime, sd->func, rpcNum, sd->buf, sendSize, 0, &resultPtr); - if (handled && resultPtr == 0 && sd->buf) - { - resultPtr = sd->buf; - } - if (handled && resultPtr == 0 && recvBuf) - { - resultPtr = recvBuf; - } - } - - if (!handled && isDtxUrpc && sendBuf && sendSize > 0) - { - // Only dispatch through dtx_rpc_func when a URPC handler is registered in the table. - // If the slot is empty, defer to the fallback emulation below. - if (hasUrpcHandler && dtxCompat.dispatcherFuncAddr != 0u) - { - dtxUrpcDispatchAttempted = true; - handled = rpcInvokeFunction(rdram, ctx, runtime, dtxCompat.dispatcherFuncAddr, rpcNum, sendBuf, sendSize, 0, &resultPtr); - if (handled && resultPtr == 0) - { - resultPtr = sendBuf; - } - } - } - - if (!handled && isDtxSid) - { - if (rpcNum == 2 && recvBuf && recvSize >= sizeof(uint32_t)) - { - uint32_t dtxId = 0; - uint32_t eeWorkAddr = 0u; - uint32_t iopWorkAddr = 0u; - uint32_t wkSize = 0u; - if (sendBuf && sendSize >= sizeof(uint32_t)) - { - (void)readRpcU32(sendBuf + 0u, dtxId); - } - if (sendBuf && sendSize >= (4u * sizeof(uint32_t))) - { - (void)readRpcU32(sendBuf + 4u, eeWorkAddr); - (void)readRpcU32(sendBuf + 8u, iopWorkAddr); - (void)readRpcU32(sendBuf + 12u, wkSize); - } - - uint32_t normalizedEeWorkAddr = 0u; - uint32_t normalizedIopWorkAddr = 0u; - (void)normalizeGuestLinearAddr(eeWorkAddr, normalizedEeWorkAddr); - (void)normalizeGuestLinearAddr(iopWorkAddr, normalizedIopWorkAddr); - - uint32_t remoteHandle = 0; - { - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_remote_by_id.find(dtxId); - if (it != g_dtx_remote_by_id.end()) - { - remoteHandle = it->second; - } - if (!remoteHandle) - { - remoteHandle = rpcAllocServerAddr(rdram); - if (!remoteHandle) - { - remoteHandle = rpcAllocPacketAddr(rdram); - } - if (!remoteHandle) - { - remoteHandle = kRpcServerPoolBase + ((dtxId & 0xFFu) * kRpcServerStride); - } - g_dtx_remote_by_id[dtxId] = remoteHandle; - } - - DtxTransferState state{}; - state.dtxId = dtxId; - state.remoteHandle = remoteHandle; - state.eeWorkAddr = normalizedEeWorkAddr; - state.iopWorkAddr = normalizedIopWorkAddr; - state.wkSize = wkSize; - g_dtx_transfer_by_id[dtxId] = state; - } - - (void)writeRpcU32(recvBuf, remoteHandle); - if (recvSize > sizeof(uint32_t)) - { - rpcZeroRdram(rdram, recvBuf + sizeof(uint32_t), recvSize - sizeof(uint32_t)); - } - static uint32_t dtxCreateLogCount = 0; - if (dtxCreateLogCount < 64u) - { - RUNTIME_LOG("[SifCallRpc:DTX_CREATE] dtxId=0x" << std::hex << dtxId - << " remote=0x" << remoteHandle - << " recvBuf=0x" << recvBuf - << " recvSize=0x" << recvSize - << std::dec << std::endl); - ++dtxCreateLogCount; - } - handled = true; - resultPtr = recvBuf; - } - else if (rpcNum == 3) - { - uint32_t remoteHandle = 0; - if (sendBuf && sendSize >= sizeof(uint32_t) && readRpcU32(sendBuf, remoteHandle) && remoteHandle) - { - std::lock_guard lock(g_dtx_rpc_mutex); - for (auto it = g_dtx_remote_by_id.begin(); it != g_dtx_remote_by_id.end(); ++it) - { - if (it->second == remoteHandle) - { - g_dtx_transfer_by_id.erase(it->first); - g_dtx_remote_by_id.erase(it); - break; - } - } - } - if (recvBuf && recvSize > 0) - { - rpcZeroRdram(rdram, recvBuf, recvSize); - } - handled = true; - resultPtr = recvBuf; - } - else if (rpcNum >= 0x400 && rpcNum < 0x500) - { - dtxUrpcFallbackEmulated = true; - const uint32_t urpcCommand = rpcNum & 0xFFu; - uint32_t outWords[4] = {1u, 0u, 0u, 0u}; - uint32_t outWordCount = 1u; - - auto readSendWord = [&](uint32_t index, uint32_t &out) -> bool - { - const uint64_t byteOffset = static_cast(index) * sizeof(uint32_t); - if (!sendBuf || sendSize < (byteOffset + sizeof(uint32_t))) - { - return false; - } - return readRpcU32(sendBuf + static_cast(byteOffset), out); - }; - - switch (urpcCommand) - { - case 0u: // SJX_CREATE - { - uint32_t srcSjHandle = 0u; - uint32_t dstSjHandle = 0u; - uint32_t line = 0u; - uint32_t eeObjAddr = 0u; - (void)readSendWord(0u, srcSjHandle); - (void)readSendWord(1u, dstSjHandle); - (void)readSendWord(2u, line); - (void)readSendWord(3u, eeObjAddr); - - std::lock_guard lock(g_dtx_rpc_mutex); - const uint32_t handle = dtxAllocUrpcHandleLocked(); - DtxSjxState state{}; - state.handle = handle; - state.srcSjHandle = srcSjHandle; - state.dstSjHandle = dstSjHandle; - state.line = line; - state.eeObjAddr = eeObjAddr; - g_dtx_sjx_by_handle[handle] = state; - outWords[0] = handle ? handle : 1u; - outWordCount = 1u; - break; - } - case 1u: // SJX_DESTROY - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(g_dtx_rpc_mutex); - g_dtx_sjx_by_handle.erase(handle); - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 2u: // SJX_RESET - { - uint32_t handle = 0u; - uint32_t xid = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, xid); - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjx_by_handle.find(handle); - if (it != g_dtx_sjx_by_handle.end()) - { - it->second.xid = static_cast(xid & 0xFFFFu); - } - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 8u: // PS2RNA_CREATE - { - uint32_t maxChannels = 0u; - uint32_t sjHandle0 = 0u; - uint32_t sjHandle1 = 0u; - (void)readSendWord(0u, maxChannels); - (void)readSendWord(2u, sjHandle0); - (void)readSendWord(3u, sjHandle1); - - std::lock_guard lock(g_dtx_rpc_mutex); - const uint32_t handle = dtxAllocUrpcHandleLocked(); - DtxPs2RnaState state{}; - state.handle = handle; - state.maxChannels = maxChannels; - state.sjHandle0 = sjHandle0; - state.sjHandle1 = sjHandle1; - state.channelCount = maxChannels; - g_dtx_ps2rna_by_handle[handle] = state; - outWords[0] = handle ? handle : 1u; - outWordCount = 1u; - break; - } - case 9u: // PS2RNA_DESTROY - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(g_dtx_rpc_mutex); - g_dtx_ps2rna_by_handle.erase(handle); - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 32u: // SJRMT_RBF_CREATE - case 33u: // SJRMT_MEM_CREATE - case 34u: // SJRMT_UNI_CREATE - { - uint32_t arg0 = 0; - uint32_t arg1 = 0; - uint32_t arg2 = 0; - (void)readSendWord(0u, arg0); - (void)readSendWord(1u, arg1); - (void)readSendWord(2u, arg2); - - uint32_t mode = 0; - uint32_t wkAddr = 0; - uint32_t wkSize = 0; - if (urpcCommand == 34u) - { - mode = arg0; - wkAddr = arg1; - wkSize = arg2; - dtxUrpcFallbackCreate34 = true; - } - else if (urpcCommand == 33u) - { - wkAddr = arg0; - wkSize = arg1; - } - else - { - wkAddr = arg0; - wkSize = (arg1 != 0u) ? arg1 : arg2; - } - - wkSize = dtxNormalizeSjrmtCapacity(wkSize); - - std::lock_guard lock(g_dtx_rpc_mutex); - const uint32_t handle = dtxAllocUrpcHandleLocked(); - DtxSjrmtState state{}; - state.handle = handle; - state.mode = mode; - state.wkAddr = wkAddr; - state.wkSize = wkSize; - state.readPos = 0u; - state.writePos = 0u; - state.roomBytes = wkSize; - state.dataBytes = 0u; - state.uuid0 = 0x53524D54u; // "SRMT" - state.uuid1 = handle; - state.uuid2 = wkAddr; - state.uuid3 = wkSize; - g_dtx_sjrmt_by_handle[handle] = state; - - outWords[0] = handle ? handle : 1u; - outWordCount = 1u; - break; - } - case 35u: // SJRMT_DESTROY - { - uint32_t handle = 0; - (void)readSendWord(0u, handle); - std::lock_guard lock(g_dtx_rpc_mutex); - g_dtx_sjrmt_by_handle.erase(handle); - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 36u: // SJRMT_GET_UUID - { - uint32_t handle = 0; - (void)readSendWord(0u, handle); - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - outWords[0] = it->second.uuid0; - outWords[1] = it->second.uuid1; - outWords[2] = it->second.uuid2; - outWords[3] = it->second.uuid3; - } - else - { - outWords[0] = 0u; - outWords[1] = 0u; - outWords[2] = 0u; - outWords[3] = 0u; - } - outWordCount = 4u; - break; - } - case 37u: // SJRMT_RESET - { - uint32_t handle = 0; - (void)readSendWord(0u, handle); - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - const uint32_t cap = (it->second.wkSize == 0u) ? 0x4000u : it->second.wkSize; - it->second.readPos = 0u; - it->second.writePos = 0u; - it->second.roomBytes = cap; - it->second.dataBytes = 0u; - } - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 38u: // SJRMT_GET_CHUNK - { - uint32_t handle = 0; - uint32_t streamId = 0; - uint32_t nbyte = 0; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(2u, nbyte); - - uint32_t ptr = 0u; - uint32_t len = 0u; - - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - DtxSjrmtState &state = it->second; - const uint32_t cap = (state.wkSize == 0u) ? 0x4000u : state.wkSize; - - if (streamId == 0u) - { - len = std::min(nbyte, state.roomBytes); - ptr = state.wkAddr + (cap ? (state.writePos % cap) : 0u); - if (cap != 0u) - { - state.writePos = (state.writePos + len) % cap; - } - state.roomBytes -= len; - } - else if (streamId == 1u) - { - len = std::min(nbyte, state.dataBytes); - ptr = state.wkAddr + (cap ? (state.readPos % cap) : 0u); - if (cap != 0u) - { - state.readPos = (state.readPos + len) % cap; - } - state.dataBytes -= len; - } - } - - outWords[0] = ptr; - outWords[1] = len; - outWordCount = 2u; - break; - } - case 39u: // SJRMT_UNGET_CHUNK - { - uint32_t handle = 0; - uint32_t streamId = 0; - uint32_t len = 0; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(3u, len); - - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - DtxSjrmtState &state = it->second; - const uint32_t cap = (state.wkSize == 0u) ? 0x4000u : state.wkSize; - if (streamId == 0u) - { - const uint32_t delta = (cap == 0u) ? 0u : (len % cap); - if (cap != 0u) - { - state.writePos = (state.writePos + cap - delta) % cap; - } - state.roomBytes = std::min(cap, state.roomBytes + len); - } - else if (streamId == 1u) - { - const uint32_t delta = (cap == 0u) ? 0u : (len % cap); - if (cap != 0u) - { - state.readPos = (state.readPos + cap - delta) % cap; - } - state.dataBytes = std::min(cap, state.dataBytes + len); - } - } - - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 40u: // SJRMT_PUT_CHUNK - { - uint32_t handle = 0; - uint32_t streamId = 0; - uint32_t len = 0; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(3u, len); - - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - DtxSjrmtState &state = it->second; - const uint32_t cap = (state.wkSize == 0u) ? 0x4000u : state.wkSize; - if (streamId == 0u) - { - state.roomBytes = std::min(cap, state.roomBytes + len); - } - else if (streamId == 1u) - { - state.dataBytes = std::min(cap, state.dataBytes + len); - } - } - - outWords[0] = 1u; - outWordCount = 1u; - break; - } - case 41u: // SJRMT_GET_NUM_DATA - { - uint32_t handle = 0; - uint32_t streamId = 0; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - outWords[0] = (streamId == 0u) ? it->second.roomBytes : it->second.dataBytes; - } - else - { - outWords[0] = 0u; - } - outWordCount = 1u; - break; - } - case 42u: // SJRMT_IS_GET_CHUNK - { - uint32_t handle = 0; - uint32_t streamId = 0; - uint32_t nbyte = 0; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(2u, nbyte); - - uint32_t available = 0u; - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(handle); - if (it != g_dtx_sjrmt_by_handle.end()) - { - available = (streamId == 0u) ? it->second.roomBytes : it->second.dataBytes; - } - outWords[0] = (available >= nbyte) ? 1u : 0u; - outWords[1] = available; - outWordCount = 2u; - break; - } - case 43u: // SJRMT_INIT - case 44u: // SJRMT_FINISH - { - outWords[0] = 1u; - outWordCount = 1u; - break; - } - default: - { - uint32_t urpcRet = 1u; - if (sendBuf && sendSize >= sizeof(uint32_t)) - { - (void)readRpcU32(sendBuf, urpcRet); - } - if (urpcCommand == 0u) - { - std::lock_guard lock(g_dtx_rpc_mutex); - urpcRet = dtxAllocUrpcHandleLocked(); - } - if (urpcRet == 0u) - { - urpcRet = 1u; - } - outWords[0] = urpcRet; - outWordCount = 1u; - break; - } - } - - if (recvBuf && recvSize > 0u) - { - const uint32_t recvWordCapacity = static_cast(recvSize / sizeof(uint32_t)); - const uint32_t wordsToWrite = std::min(outWordCount, recvWordCapacity); - for (uint32_t i = 0; i < wordsToWrite; ++i) - { - (void)writeRpcU32(recvBuf + (i * sizeof(uint32_t)), outWords[i]); - } - - // SJRMT_IsGetChunk callers read rbuf[1] even when nout==1. - if (urpcCommand == 42u && outWordCount > 1u) - { - (void)writeRpcU32(recvBuf + sizeof(uint32_t), outWords[1]); - } - - if (recvSize > (wordsToWrite * sizeof(uint32_t))) - { - rpcZeroRdram(rdram, recvBuf + (wordsToWrite * sizeof(uint32_t)), - recvSize - (wordsToWrite * sizeof(uint32_t))); - } - } - - handled = true; - resultPtr = recvBuf; - } - } - - if (recvBuf && recvSize > 0) - { - if (handled && resultPtr && resultPtr != recvBuf) - { - rpcCopyToRdram(rdram, recvBuf, resultPtr, recvSize); - } - else if (!handled && sendBuf && sendSize > 0 && sendBuf != recvBuf) - { - size_t copySize = (sendSize < recvSize) ? sendSize : recvSize; - rpcCopyToRdram(rdram, recvBuf, sendBuf, copySize); + const uint32_t copySize = std::min(sendSize, receiveSize); + rpcCopyToRdram(rdram, receiveBuffer, sendBuf, copySize); + copiedFallback = true; } else if (!handled) { - rpcZeroRdram(rdram, recvBuf, recvSize); + rpcZeroRdram(rdram, receiveBuffer, receiveSize); + zeroedFallback = true; } } - if (isDtxUrpc) + if (endFunction != 0u) { - static int dtxUrpcLogCount = 0; - if (dtxUrpcLogCount < 64) + if (iopResult.callbackPolicy == ps2x::iop::CallbackPolicy::Suppress) { - uint32_t dtxUrpcRecv0 = 0; - if (recvBuf && recvSize >= sizeof(uint32_t)) - { - (void)readRpcU32(recvBuf, dtxUrpcRecv0); - } - RUNTIME_LOG("[SifCallRpc:DTX] rpcNum=0x" << std::hex << rpcNum - << " cmd=0x" << dtxUrpcCommand - << " name=" << dtxUrpcCommandName(dtxUrpcCommand) - << " fn=0x" << dtxUrpcFn - << " obj=0x" << dtxUrpcObj - << " send0=0x" << dtxUrpcSend0 - << " recv0=0x" << dtxUrpcRecv0 - << " resultPtr=0x" << resultPtr - << " handled=" << std::dec << (handled ? 1 : 0) - << " dispatch=" << (dtxUrpcDispatchAttempted ? 1 : 0) - << " emu=" << (dtxUrpcFallbackEmulated ? 1 : 0) - << " emu34=" << (dtxUrpcFallbackCreate34 ? 1 : 0) - << std::endl); - - if (dtxUrpcCommand >= 37u && dtxUrpcCommand <= 42u) - { - std::lock_guard lock(g_dtx_rpc_mutex); - auto it = g_dtx_sjrmt_by_handle.find(dtxUrpcSend0); - if (it != g_dtx_sjrmt_by_handle.end()) - { - const DtxSjrmtState &state = it->second; - RUNTIME_LOG("[SifCallRpc:DTX_SJRMT] handle=0x" << std::hex << dtxUrpcSend0 - << " cmd=" << dtxUrpcCommandName(dtxUrpcCommand) - << " wk=0x" << state.wkAddr - << " size=0x" << state.wkSize - << " read=0x" << state.readPos - << " write=0x" << state.writePos - << std::dec - << " room=" << state.roomBytes - << " data=" << state.dataBytes); - if ((dtxUrpcCommand == 38u || dtxUrpcCommand == 42u) && sendBuf && sendSize >= 12u) - { - uint32_t streamId = 0u; - uint32_t nbyte = 0u; - (void)readRpcU32(sendBuf + 4u, streamId); - (void)readRpcU32(sendBuf + 8u, nbyte); - RUNTIME_LOG(" stream=" << dtxStreamName(streamId) - << " req=" << nbyte); - } - RUNTIME_LOG(std::endl); - } - } - ++dtxUrpcLogCount; + callbackCompleted = true; } - } - - if (endFunc) - { - PS2SoundDriverCompatLayout soundCompat{}; + else { - std::lock_guard lock(g_rpc_mutex); - soundCompat = g_soundDriverCompatLayout; - } - - const bool hleCompletedEndCallback = handledByIop && soundCompat.matchesCompletionCallback(endFunc); - bool callbackInvoked = hleCompletedEndCallback; - callbackInvokedForDebug = hleCompletedEndCallback; - - if (!hleCompletedEndCallback) - { - callbackInvoked = rpcInvokeFunction(rdram, ctx, runtime, endFunc, endParam, 0, 0, 0, nullptr); - callbackInvokedForDebug = callbackInvoked; - - if (!callbackInvoked && endFunc >= 0x10000u) + callbackCompleted = rpcInvokeFunction(rdram, + ctx, + runtime, + endFunction, + endParameter, + 0u, + 0u, + 0u, + nullptr); + if (!callbackCompleted && endFunction >= 0x10000u) { - const uint32_t normalizedEndFunc = endFunc - 0x10000u; - if (runtime->hasFunction(normalizedEndFunc)) + const uint32_t normalizedEndFunction = endFunction - 0x10000u; + if (runtime->hasFunction(normalizedEndFunction)) { - callbackInvoked = rpcInvokeFunction(rdram, ctx, runtime, normalizedEndFunc, endParam, 0, 0, 0, nullptr); - callbackInvokedForDebug = callbackInvokedForDebug || callbackInvoked; + callbackCompleted = rpcInvokeFunction( + rdram, + ctx, + runtime, + normalizedEndFunction, + endParameter, + 0u, + 0u, + 0u, + nullptr); } } } - if (soundCompat.matchesCompletionCallback(endFunc)) + if (!callbackCompleted) { - uint32_t semaId = static_cast(client->hdr.sema_id); - if (semaId == 0xFFFFFFFFu || semaId == 0u) + const bool signaled = signalRpcCompletionSema(completionSemaphore()); + static uint32_t unresolvedCallbackWarnings = 0u; + if (unresolvedCallbackWarnings < 32u) { - semaId = endParam; - } - (void)signalRpcCompletionSema(semaId); - if (rdram && soundCompat.busyFlagAddr != 0u && soundCompat.matchesClearBusyCallback(endFunc)) - { - if (uint32_t *busy = reinterpret_cast(getMemPtr(rdram, soundCompat.busyFlagAddr))) - { - *busy = 0u; - } + std::cerr + << "[SifCallRpc] unresolved end callback endFunc=0x" + << std::hex << endFunction + << " semaId=0x" << completionSemaphore() + << " fallbackSignal=" << std::dec + << (signaled ? 1 : 0) << std::endl; + ++unresolvedCallbackWarnings; } } - - if (!callbackInvoked) - { - uint32_t semaId = static_cast(client->hdr.sema_id); - if (semaId == 0xFFFFFFFFu || semaId == 0u) - { - semaId = endParam; - } - const bool fallbackSignaledSema = signalRpcCompletionSema(semaId); - - static uint32_t unresolvedEndFuncWarnCount = 0; - if (unresolvedEndFuncWarnCount < 32u) - { - std::cerr << "[SifCallRpc] unresolved end callback endFunc=0x" << std::hex << endFunc - << " semaId=0x" << semaId - << " fallbackSignal=" << std::dec << (fallbackSignaledSema ? 1 : 0) - << std::endl; - ++unresolvedEndFuncWarnCount; - } - } - } - - static int logCount = 0; - if (logCount < 10) - { - RUNTIME_LOG("[SifCallRpc] client=0x" << std::hex << clientPtr - << " sid=0x" << sid - << " rpcNum=0x" << rpcNum - << " mode=0x" << mode - << " sendBuf=0x" << sendBuf - << " recvBuf=0x" << recvBuf - << " recvSize=0x" << recvSize - << " size=" << std::dec << sendSize << std::endl); - ++logCount; } { @@ -2487,18 +699,38 @@ namespace ps2_syscalls event.mode = mode; event.sendBuf = sendBuf; event.sendSize = sendSize; - event.recvBuf = recvBuf; - event.recvSize = recvSize; - event.resultPtr = resultPtr; - event.endFunc = endFunc; - event.endParam = endParam; - event.semaId = client ? static_cast(client->hdr.sema_id) : 0u; - event.flags = ((mode & kSifRpcModeNowait) ? kSifRpcDebugFlagNowait : 0u) | - (handledByIop ? kSifRpcDebugFlagHandledByHle : 0u) | - (callbackInvokedForDebug ? kSifRpcDebugFlagCallback : 0u) | - ((sd && sd->func) ? kSifRpcDebugFlagServerDispatch : 0u) | - ((isDtxSid || isDtxUrpc) ? kSifRpcDebugFlagDtx : 0u); + event.recvBuf = receiveBuffer; + event.recvSize = receiveSize; + event.resultPtr = resultPointer; + event.endFunc = endFunction; + event.endParam = endParameter; + event.semaId = static_cast(client->hdr.sema_id); + event.flags = + ((mode & kSifRpcModeNowait) ? kSifRpcDebugFlagNowait : 0u) | + (handledByIop ? kSifRpcDebugFlagHandledByHle : 0u) | + (callbackCompleted ? kSifRpcDebugFlagCallback : 0u) | + (serverDispatched ? kSifRpcDebugFlagServerDispatch : 0u) | + (!handled ? kSifRpcDebugFlagUnhandled : 0u) | + (copiedFallback ? kSifRpcDebugFlagFallbackCopy : 0u) | + (zeroedFallback ? kSifRpcDebugFlagFallbackZero : 0u); + fillRpcDebugPreview(rdram, + sendBuf, + sendSize, + event.sendPreview, + event.sendPreviewSize); + fillRpcDebugPreview(rdram, + receiveBuffer, + receiveSize, + event.recvPreview, + event.recvPreviewSize); event.result = 0; + +#if PS2X_ENABLE_IOP_RPC_TRACE + if ((event.flags & kSifRpcDebugFlagUnhandled) != 0u) + { + logUnhandledRpcTrace(event); + } +#endif pushSifRpcDebugEvent(event); setReturnS32(ctx, 0); diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.h b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.h index a968a75..5e9a446 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.h @@ -14,15 +14,6 @@ namespace ps2_syscalls void SifSetRpcQueue(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void SifRemoveRpcQueue(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void SifRemoveRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); - void noteDtxSifDmaTransfer(uint8_t *rdram, uint32_t srcAddr, uint32_t dstAddr, uint32_t sizeBytes); - bool handleSoundDriverRpcService(uint8_t *rdram, PS2Runtime *runtime, - uint32_t sid, uint32_t rpcNum, - uint32_t sendBuf, uint32_t sendSize, - uint32_t recvBuf, uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion); - void prepareSoundDriverStatusTransfer(uint8_t *rdram, uint32_t srcAddr, uint32_t size); - void finalizeSoundDriverStatusTransfer(uint8_t *rdram, uint32_t srcAddr, uint32_t dstAddr, uint32_t size); void sceSifCallRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifSendCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceRpcGetPacket(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/game_overrides.cpp b/ps2xRuntime/src/lib/game_overrides.cpp index 821491e..1e38bd4 100644 --- a/ps2xRuntime/src/lib/game_overrides.cpp +++ b/ps2xRuntime/src/lib/game_overrides.cpp @@ -5,10 +5,8 @@ #include "ps2_syscalls.h" #include "ps2_log.h" #include -#include #include #include -#include #include #include #include @@ -60,57 +58,6 @@ namespace return leaf.string(); } - uint32_t crc32Update(uint32_t crc, const uint8_t *data, size_t size) - { - static std::array table = []() - { - std::array values{}; - for (uint32_t i = 0; i < 256u; ++i) - { - uint32_t c = i; - for (int bit = 0; bit < 8; ++bit) - { - c = (c & 1u) ? (0xEDB88320u ^ (c >> 1u)) : (c >> 1u); - } - values[i] = c; - } - return values; - }(); - - uint32_t out = crc; - for (size_t i = 0; i < size; ++i) - { - out = table[(out ^ data[i]) & 0xFFu] ^ (out >> 8u); - } - return out; - } - - bool computeFileCrc32(const std::string &path, uint32_t &crcOut) - { - std::ifstream file(path, std::ios::binary); - if (!file.is_open()) - { - return false; - } - - std::array chunk{}; - uint32_t crc = 0xFFFFFFFFu; - - while (file.good()) - { - file.read(reinterpret_cast(chunk.data()), static_cast(chunk.size())); - const std::streamsize got = file.gcount(); - if (got <= 0) - { - break; - } - crc = crc32Update(crc, chunk.data(), static_cast(got)); - } - - crcOut = ~crc; - return true; - } - std::optional resolveHandlerByName(std::string_view handlerName) { const std::string_view resolvedSyscall = ps2_runtime_calls::resolveSyscallName(handlerName); @@ -140,7 +87,6 @@ namespace return std::nullopt; } } - namespace ps2_game_overrides { AutoRegister::AutoRegister(const Descriptor &descriptor) @@ -173,10 +119,12 @@ namespace ps2_game_overrides return runtime.replaceFunction(address, resolved.value()); } - void applyMatching(PS2Runtime &runtime, const std::string &elfPath, uint32_t entry) + void applyMatching(PS2Runtime &runtime, + const std::string &elfPath, + uint32_t entry, + uint32_t fileCrc32, + bool fileCrcValid) { - ps2_syscalls::clearSoundDriverCompatLayout(); - ps2_syscalls::clearDtxCompatLayout(); std::vector descriptors; { @@ -190,10 +138,6 @@ namespace ps2_game_overrides } const std::string elfName = basenameFromPath(elfPath); - uint32_t fileCrc32 = 0u; - bool fileCrcComputed = false; - bool fileCrcValid = false; - size_t appliedCount = 0; for (const Descriptor &descriptor : descriptors) { @@ -217,16 +161,6 @@ namespace ps2_game_overrides if (descriptor.crc32 != 0u) { - if (!fileCrcComputed) - { - fileCrcComputed = true; - fileCrcValid = computeFileCrc32(elfPath, fileCrc32); - if (!fileCrcValid) - { - std::cerr << "[game_overrides] failed to compute CRC32 for '" << elfPath << "'" << std::endl; - } - } - if (!fileCrcValid || fileCrc32 != descriptor.crc32) { continue; @@ -247,59 +181,3 @@ namespace ps2_game_overrides } } } - -namespace -{ - void applyRecvxSoundDriverCompat(PS2Runtime &runtime) - { - (void)runtime; - - // Trying to explain a bit of Resident Evil Code: Veronica X sound-driver guest globals. - // Update these guest addresses/callback PCs when porting the override to another build: - // - checksum tables back the SE/MIDI status values mirrored through the snddrv RPC stubs - // - busyFlagAddr is the guest-side "work in progress" word cleared on completion - // - completion/clearBusy callbacks are guest PCs reached when async snddrv work finishes - PS2SoundDriverCompatLayout layout{}; - layout.primarySeCheckAddr = 0x01E0EF10u; - layout.primaryMidiCheckAddr = 0x01E0EF20u; - layout.fallbackSeCheckAddr = 0x01E1EF10u; - layout.fallbackMidiCheckAddr = 0x01E1EF20u; - layout.busyFlagAddr = 0x01E212C8u; - layout.completionCallbacks = {0x002EAC20u, 0x002EAC30u, 0x002FAC20u, 0x002FAC30u}; - layout.clearBusyCallbacks = {0x002EAC30u, 0x002FAC30u}; - ps2_syscalls::setSoundDriverCompatLayout(layout); - } - - void applyRecvxDtxCompat(PS2Runtime &runtime) - { - (void)runtime; - - // Trying to explain abit of Resident Evil Code: Veronica X DTX guest layout. - // Update these guest values when porting the middleware override to another build: - // - rpcSid identifies the DTX RPC service the guest binds/registers - // - urpc object/table addresses back the SJX/PS2RNA/SJRMT command tables - // - dispatcherFuncAddr is the guest-side DTX RPC handler used for URPC dispatch - PS2DtxCompatLayout layout{}; - layout.rpcSid = 0x7D000000u; - layout.urpcObjBase = 0x01F18000u; - layout.urpcObjLimit = 0x01F1FF00u; - layout.urpcObjStride = 0x20u; - layout.urpcFnTableBase = 0x0034FED0u; - layout.urpcObjTableBase = 0x0034FFD0u; - layout.dispatcherFuncAddr = 0x002FABC0u; - ps2_syscalls::setDtxCompatLayout(layout); - } - - void applyLotrSoundRpcCompat(PS2Runtime &runtime) - { - (void)runtime; - - PS2SoundDriverCompatLayout layout{}; - layout.completionCallbacks = {0x001FFD70u, 0u, 0u, 0u}; - ps2_syscalls::setSoundDriverCompatLayout(layout); - } - - PS2_REGISTER_GAME_OVERRIDE("RECVX sound-driver compat", "slus_201.84", 0u, 0u, &applyRecvxSoundDriverCompat); - PS2_REGISTER_GAME_OVERRIDE("RECVX DTX compat", "slus_201.84", 0u, 0u, &applyRecvxDtxCompat); - PS2_REGISTER_GAME_OVERRIDE("LotR sound RPC compat", "SLUS_205.78", 0u, 0u, &applyLotrSoundRpcCompat); -} diff --git a/ps2xRuntime/src/lib/ps2_debug_panel.cpp b/ps2xRuntime/src/lib/ps2_debug_panel.cpp index 2876e12..9e5e2fd 100644 --- a/ps2xRuntime/src/lib/ps2_debug_panel.cpp +++ b/ps2xRuntime/src/lib/ps2_debug_panel.cpp @@ -8,7 +8,6 @@ #include "Kernel/Stubs/CD.h" #include "Kernel/Stubs/MemoryCard.h" #include "Kernel/Stubs/Pad.h" -#include "runtime/ps2_iop.h" #if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) #include "imgui.h" @@ -71,27 +70,17 @@ namespace } } - const char *iopRpcSidName(uint32_t sid) + std::string iopRpcSidName(const ps2x::iop::DebugSnapshot &snapshot, uint32_t sid) { - switch (sid) + for (const ps2x::iop::DebugService &service : snapshot.services) { - case IOP_SID_SNDDRV_COMMAND: - return "SNDDRV command"; - case IOP_SID_SNDDRV_STATE: - return "SNDDRV state"; - case IOP_SID_LIBSD: - return "LIBSD"; - case IOP_SID_FATAL_FRAME_SDRDRV: - return "Fatal Frame SDRDRV"; - case IOP_SID_LOTR_CLFILE: - return "LOTR CL"; - case IOP_SID_LOTR_SOUND: - return "LOTR Sound"; - case 0x80001300u: - return "DBCMAN"; - default: - return ""; + if (std::find(service.sids.begin(), service.sids.end(), sid) != + service.sids.end()) + { + return service.name; + } } + return {}; } std::string pressedPadButtons(uint16_t activeLowButtons) @@ -148,7 +137,9 @@ namespace {kSifRpcDebugFlagCallback, "callback"}, {kSifRpcDebugFlagMissingClient, "bad-client"}, {kSifRpcDebugFlagServerDispatch, "server"}, - {kSifRpcDebugFlagDtx, "dtx"}, + {kSifRpcDebugFlagUnhandled, "unhandled"}, + {kSifRpcDebugFlagFallbackCopy, "fallback-copy"}, + {kSifRpcDebugFlagFallbackZero, "fallback-zero"}, }; std::string out; @@ -166,6 +157,27 @@ namespace return out; } + std::string rpcPreviewBytes(const uint8_t *bytes, uint32_t count) + { + if (!bytes || count == 0u) + { + return ""; + } + + std::string out; + char item[4] = {}; + for (uint32_t i = 0; i < count; ++i) + { + std::snprintf(item, sizeof(item), "%02X", bytes[i]); + if (!out.empty()) + { + out.push_back(' '); + } + out += item; + } + return out; + } + template bool readGuestObjectNoThrow(uint8_t *rdram, uint32_t addr, T &out) { @@ -923,6 +935,7 @@ namespace void drawIopTab(PS2Runtime &runtime) { uint8_t *rdram = runtime.memory().getRDRAM(); + const ps2x::iop::DebugSnapshot iopSnapshot = runtime.iopDebugSnapshot(); struct ModuleRow { @@ -980,7 +993,6 @@ namespace uint32_t rpcPacketIndex = 0; uint32_t rpcServerIndex = 0; uint32_t rpcActiveQueue = 0; - SoundDriverRpcState soundState{}; { std::lock_guard lock(g_rpc_mutex); rpcInitialized = g_rpc_initialized; @@ -988,8 +1000,6 @@ namespace rpcPacketIndex = g_rpc_packet_index; rpcServerIndex = g_rpc_server_index; rpcActiveQueue = g_rpc_active_queue; - soundState = g_soundDriverRpcState; - servers.reserve(g_rpc_servers.size()); for (const auto &[sid, state] : g_rpc_servers) { @@ -1017,24 +1027,6 @@ namespace std::sort(clients.begin(), clients.end(), [](const RpcClientRow &a, const RpcClientRow &b) { return a.clientPtr < b.clientPtr; }); - PS2DtxCompatLayout dtxLayout{}; - size_t dtxRemoteCount = 0; - size_t dtxTransferCount = 0; - size_t dtxSjxCount = 0; - size_t dtxRnaCount = 0; - size_t dtxSjrmtCount = 0; - uint32_t dtxNextUrpcObj = 0; - { - std::lock_guard lock(g_dtx_rpc_mutex); - dtxLayout = g_dtxCompatLayout; - dtxRemoteCount = g_dtx_remote_by_id.size(); - dtxTransferCount = g_dtx_transfer_by_id.size(); - dtxSjxCount = g_dtx_sjx_by_handle.size(); - dtxRnaCount = g_dtx_ps2rna_by_handle.size(); - dtxSjrmtCount = g_dtx_sjrmt_by_handle.size(); - dtxNextUrpcObj = g_dtx_next_urpc_obj; - } - auto hasServer = [&](uint32_t sid) { return std::any_of(servers.begin(), servers.end(), [sid](const RpcServerRow &row) @@ -1075,50 +1067,94 @@ namespace ImGui::EndTable(); } - ImGui::SeparatorText("Known HLE IOP RPC services"); - if (ImGui::BeginTable("iop_hle_services", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) - { - struct ServiceRow - { - const char *name; - uint32_t sid; - bool dynamic; - }; - const ServiceRow services[] = { - {"SNDDRV command", IOP_SID_SNDDRV_COMMAND, false}, - {"SNDDRV state", IOP_SID_SNDDRV_STATE, false}, - {"LIBSD", IOP_SID_LIBSD, false}, - {"Fatal Frame SDRDRV", IOP_SID_FATAL_FRAME_SDRDRV, false}, - {"LOTR SOUND", IOP_SID_LOTR_SOUND, false}, - {"LOTR CLFILE", IOP_SID_LOTR_CLFILE, false}, - {"DBCMAN", 0x80001300u, false}, - {"DTX compat", dtxLayout.rpcSid, true}, - }; + ImGui::SeparatorText("ps2xIOP HLE services"); + ImGui::Text("Profile: %s provider: %s", + iopSnapshot.activeProfile.empty() + ? "" + : iopSnapshot.activeProfile.c_str(), + iopSnapshot.activeProvider.empty() + ? "builtin" + : iopSnapshot.activeProvider.c_str()); + if (ImGui::BeginTable("iop_hle_services", + 5, + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_Resizable)) + { ImGui::TableSetupColumn("Service"); + ImGui::TableSetupColumn("Layer"); ImGui::TableSetupColumn("SID"); - ImGui::TableSetupColumn("Configured"); - ImGui::TableSetupColumn("EE server registered"); + ImGui::TableSetupColumn("EE server"); + ImGui::TableSetupColumn("Metrics"); ImGui::TableHeadersRow(); - for (const ServiceRow &service : services) + for (const ps2x::iop::DebugService &service : iopSnapshot.services) { - if (service.dynamic && service.sid == 0u) + if (service.sids.empty()) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(service.name.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core"); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("-"); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("-"); + ImGui::TableNextColumn(); + ImGui::Text("%zu", service.metrics.size()); continue; } - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::TextUnformatted(service.name); - ImGui::TableNextColumn(); - ImGui::Text("0x%08X", service.sid); - ImGui::TableNextColumn(); - ImGui::Text("%s", (!service.dynamic || service.sid != 0u) ? "yes" : "no"); - ImGui::TableNextColumn(); - ImGui::Text("%s", hasServer(service.sid) ? "yes" : "no"); + + for (const uint32_t sid : service.sids) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(service.name.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core"); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", sid); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(hasServer(sid) ? "yes" : "no"); + ImGui::TableNextColumn(); + if (service.metrics.empty()) + { + ImGui::TextUnformatted("-"); + } + else + { + std::string metrics; + for (const ps2x::iop::DebugMetric &metric : service.metrics) + { + if (!metrics.empty()) + { + metrics += ", "; + } + std::ostringstream value; + value << metric.name << "="; + if (metric.hexadecimal) + { + value << "0x" << std::hex << metric.value; + } + else + { + value << std::dec << metric.value; + } + metrics += value.str(); + } + ImGui::TextUnformatted(metrics.c_str()); + } + } } ImGui::EndTable(); } + for (const std::string &diagnostic : iopSnapshot.diagnostics) + { + ImGui::TextDisabled("%s", diagnostic.c_str()); + } + ImGui::SeparatorText("SIF RPC state"); ImGui::Text("initialized=%u servers=%zu clients=%zu nextId=%u packetIndex=%u serverIndex=%u activeQueue=0x%08X", rpcInitialized ? 1u : 0u, @@ -1148,7 +1184,8 @@ namespace ImGui::TableNextColumn(); ImGui::Text("0x%08X", row.sid); ImGui::TableNextColumn(); - ImGui::TextUnformatted(iopRpcSidName(row.sid)); + const std::string serviceName = iopRpcSidName(iopSnapshot, row.sid); + ImGui::TextUnformatted(serviceName.c_str()); ImGui::TableNextColumn(); ImGui::Text("0x%08X", row.sdPtr); ImGui::TableNextColumn(); @@ -1189,7 +1226,8 @@ namespace ImGui::TableNextColumn(); ImGui::Text("0x%08X", row.sid); ImGui::TableNextColumn(); - ImGui::TextUnformatted(iopRpcSidName(row.sid)); + const std::string serviceName = iopRpcSidName(iopSnapshot, row.sid); + ImGui::TextUnformatted(serviceName.c_str()); ImGui::TableNextColumn(); ImGui::Text("%u", row.busy ? 1u : 0u); ImGui::TableNextColumn(); @@ -1205,38 +1243,11 @@ namespace } ImGui::EndTable(); } - - ImGui::SeparatorText("Sound driver / DTX compat"); - ImGui::Text("SoundDriver initialized=%u owner=0x%p storage=0x%08X/%u status=0x%08X addrTable=0x%08X hd=0x%08X sq=0x%08X data=0x%08X", - soundState.initialized ? 1u : 0u, - reinterpret_cast(soundState.ownerRuntime), - soundState.storageBaseAddr, - soundState.storageSize, - soundState.statusAddr, - soundState.addrTableAddr, - soundState.hdBaseAddr, - soundState.sqBaseAddr, - soundState.dataBaseAddr); - ImGui::Text("DTX configured=%u sid=0x%08X obj=[0x%08X,0x%08X) stride=0x%X fnTable=0x%08X objTable=0x%08X dispatcher=0x%08X nextObj=0x%08X", - dtxLayout.isConfigured() ? 1u : 0u, - dtxLayout.rpcSid, - dtxLayout.urpcObjBase, - dtxLayout.urpcObjLimit, - dtxLayout.urpcObjStride, - dtxLayout.urpcFnTableBase, - dtxLayout.urpcObjTableBase, - dtxLayout.dispatcherFuncAddr, - dtxNextUrpcObj); - ImGui::Text("DTX states: remote=%zu transfer=%zu sjx=%zu ps2rna=%zu sjrmt=%zu", - dtxRemoteCount, - dtxTransferCount, - dtxSjxCount, - dtxRnaCount, - dtxSjrmtCount); } - void drawRpcHistoryTab() + void drawRpcHistoryTab(PS2Runtime &runtime) { + const ps2x::iop::DebugSnapshot iopSnapshot = runtime.iopDebugSnapshot(); std::vector events; uint64_t nextSeq = 0; { @@ -1264,6 +1275,66 @@ namespace ImGui::SameLine(); ImGui::Checkbox("Hide bind/register/init", &hideBindNoise); + ImGui::SeparatorText("IOP/RPC tracer"); + ImGui::TextDisabled("Unhandled rows are RPC calls that no HLE service or EE server callback consumed; the runtime used copy/zero fallback."); + static bool tracerOnlyUnhandled = true; + ImGui::Checkbox("Only unhandled", &tracerOnlyUnhandled); + if (ImGui::BeginTable("iop_rpc_tracer", 9, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, + ImVec2(0, 220))) + { + ImGui::TableSetupColumn("Seq"); + ImGui::TableSetupColumn("SID"); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Rpc#"); + ImGui::TableSetupColumn("PC"); + ImGui::TableSetupColumn("Send"); + ImGui::TableSetupColumn("Recv"); + ImGui::TableSetupColumn("Send[0..15]"); + ImGui::TableSetupColumn("Recv[0..15]"); + ImGui::TableHeadersRow(); + + for (const SifRpcDebugEvent &event : events) + { + const char *op = event.op ? event.op : ""; + if (std::strcmp(op, "CallRpc") != 0) + { + continue; + } + const bool unhandled = (event.flags & kSifRpcDebugFlagUnhandled) != 0u; + if (tracerOnlyUnhandled && !unhandled) + { + continue; + } + + const std::string sendPreview = rpcPreviewBytes(event.sendPreview, event.sendPreviewSize); + const std::string recvPreview = rpcPreviewBytes(event.recvPreview, event.recvPreviewSize); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(event.seq)); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.sid); + ImGui::TableNextColumn(); + const std::string serviceName = iopRpcSidName(iopSnapshot, event.sid); + ImGui::TextUnformatted(serviceName.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.rpcNum); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.pc); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X/%u", event.sendBuf, event.sendSize); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X/%u", event.recvBuf, event.recvSize); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(sendPreview.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(recvPreview.c_str()); + } + ImGui::EndTable(); + } + if (ImGui::BeginTable("rpc_history", 20, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, @@ -1318,7 +1389,8 @@ namespace ImGui::TableNextColumn(); ImGui::Text("0x%08X", event.sid); ImGui::TableNextColumn(); - ImGui::TextUnformatted(iopRpcSidName(event.sid)); + const std::string serviceName = iopRpcSidName(iopSnapshot, event.sid); + ImGui::TextUnformatted(serviceName.c_str()); ImGui::TableNextColumn(); ImGui::Text("0x%08X", event.rpcNum); ImGui::TableNextColumn(); @@ -2117,7 +2189,6 @@ namespace #endif } - void PS2DebugPanel::initialize() { #if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) @@ -2193,7 +2264,7 @@ void PS2DebugPanel::draw(PS2Runtime &runtime) } if (ImGui::BeginTabItem("RPC History")) { - drawRpcHistoryTab(); + drawRpcHistoryTab(runtime); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("PAD")) diff --git a/ps2xRuntime/src/lib/ps2_iop.cpp b/ps2xRuntime/src/lib/ps2_iop.cpp deleted file mode 100644 index f4db8ab..0000000 --- a/ps2xRuntime/src/lib/ps2_iop.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "runtime/ps2_iop.h" -#include "runtime/ps2_iop_audio.h" -#include "runtime/ps2_iop_cl.h" -#include "runtime/ps2_iop_dbcman.h" -#include "runtime/ps2_iop_sdrdrv.h" -#include "runtime/ps2_memory.h" -#include "ps2_runtime.h" -#include "Kernel/Syscalls/RPC.h" - -ps2_iop::ps2_iop() -{ - reset(); -} - -void ps2_iop::init(uint8_t *rdram) -{ - m_rdram = rdram; -} - -void ps2_iop::reset() -{ - ps2_iop_cl::reset(); - ps2_iop_dbcman::reset(); - ps2_iop_sdrdrv::reset(); -} - -bool ps2_iop::handleRPC(PS2Runtime *runtime, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion) -{ - resultPtr = 0u; - signalNowaitCompletion = false; - - if (ps2_syscalls::handleSoundDriverRpcService(m_rdram, - runtime, - sid, - rpcNum, - sendBufAddr, - sendSize, - recvBufAddr, - recvSize, - resultPtr, - signalNowaitCompletion)) - { - return true; - } - - if (ps2_iop_dbcman::handleDbcManRpc(m_rdram, - sid, - rpcNum, - sendBufAddr, - sendSize, - recvBufAddr, - recvSize, - resultPtr)) - { - return true; - } - - if (ps2_iop_audio::handleLibSdRpc(m_rdram, - runtime, - sid, - rpcNum, - sendBufAddr, - sendSize, - recvBufAddr, - recvSize, - resultPtr)) - { - return true; - } - - if (ps2_iop_cl::handleSoundRpc(m_rdram, sid, - rpcNum, sendBufAddr, - sendSize, recvBufAddr, - recvSize, resultPtr, - signalNowaitCompletion)) - { - return true; - } - - if (ps2_iop_cl::handleClFileRpc(m_rdram, sid, - rpcNum, sendBufAddr, - sendSize, recvBufAddr, - recvSize, resultPtr)) - { - return true; - } - - if (ps2_iop_sdrdrv::handleSdrdrvRpc(m_rdram, sid, - rpcNum, sendBufAddr, - sendSize, recvBufAddr, - recvSize, resultPtr)) - { - return true; - } - - return false; -} diff --git a/ps2xRuntime/src/lib/ps2_iop_audio.cpp b/ps2xRuntime/src/lib/ps2_iop_audio.cpp deleted file mode 100644 index 0268796..0000000 --- a/ps2xRuntime/src/lib/ps2_iop_audio.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "runtime/ps2_iop_audio.h" -#include "ps2_runtime.h" - -namespace ps2_iop_audio -{ - void reset() - { - // Noop. - } - - bool handleLibSdRpc(uint8_t *rdram, - PS2Runtime *runtime, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr) - { - if (sid != IOP_SID_LIBSD) - return false; - - const uint8_t *sendPtr = sendBufAddr ? getConstMemPtr(rdram, sendBufAddr) : nullptr; - uint8_t *recvPtr = recvBufAddr ? getMemPtr(rdram, recvBufAddr) : nullptr; - - runtime->audioBackend().onSoundCommand(sid, - rpcNum, - sendPtr, - sendSize, - recvPtr, - recvSize); - - resultPtr = recvBufAddr; - return true; - } -} diff --git a/ps2xRuntime/src/lib/ps2_iop_cl.cpp b/ps2xRuntime/src/lib/ps2_iop_cl.cpp deleted file mode 100644 index ff6fea6..0000000 --- a/ps2xRuntime/src/lib/ps2_iop_cl.cpp +++ /dev/null @@ -1,514 +0,0 @@ -#include "runtime/ps2_iop_cl.h" -#include "runtime/ps2_iop.h" -#include "runtime/ps2_memory.h" -#include "Kernel/Syscalls/Common.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - constexpr uint32_t kClFileBufferBytes = 0x2000u; - constexpr uint32_t kClFileLoadResultQueued = 5u; - constexpr uint32_t kClFileLoadStatusFailed = 3u; - constexpr uint32_t kClFileLoadStatusComplete = 7u; - constexpr uint32_t kClFileFirstLoadHandle = 0x00010000u; - constexpr uint32_t kLotrSoundResponseCounterOffset = 4u; - - struct ClFileHandle - { - FILE *file = nullptr; - uint32_t size = 0u; - }; - - struct ClFileLoad - { - uint32_t status = kClFileLoadStatusFailed; - uint32_t size = 0u; - }; - - std::mutex g_clFileMutex; - std::unordered_map g_clFileHandles; - std::unordered_map g_clFileLoads; - uint32_t g_nextFileHandle = 1u; - uint32_t g_nextLoadHandle = kClFileFirstLoadHandle; - std::string g_clFileRoot; - - std::mutex g_lotrSoundMutex; - uint32_t g_lotrSoundCounter = 0u; - - bool readGuestU32(const uint8_t *rdram, uint32_t addr, uint32_t &out) - { - const uint8_t *ptr = getConstMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - - std::memcpy(&out, ptr, sizeof(out)); - return true; - } - - bool writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value) - { - uint8_t *ptr = getMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - - std::memcpy(ptr, &value, sizeof(value)); - return true; - } - - std::string readGuestString(uint8_t *rdram, uint32_t addr, size_t maxBytes) - { - const char *ptr = reinterpret_cast(getConstMemPtr(rdram, addr)); - if (!ptr || maxBytes == 0u) - { - return {}; - } - - size_t length = 0u; - while (length < maxBytes && ptr[length] != '\0') - { - ++length; - } - - return std::string(ptr, length); - } - - bool hasPs2PathDevice(const std::string &path) - { - return path.find(':') != std::string::npos; - } - - std::string joinPs2Path(const std::string &root, const std::string &leaf) - { - if (root.empty() || leaf.empty() || hasPs2PathDevice(leaf)) - { - return leaf; - } - - const char tail = root.back(); - if (tail == '/' || tail == '\\' || tail == ':') - { - return root + leaf; - } - return root + "/" + leaf; - } - - std::filesystem::path resolveClFilePath(const std::string &path) - { - std::string root; - { - std::lock_guard lock(g_clFileMutex); - root = g_clFileRoot; - } - - const std::string rootedPath = joinPs2Path(root, path); - const std::string translated = translatePs2Path(rootedPath.c_str()); - return translated.empty() ? std::filesystem::path{} : std::filesystem::path(translated); - } - - uint32_t allocateClFileHandleLocked(FILE *file, uint32_t size) - { - if (!file) - { - return 0u; - } - - for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt) - { - uint32_t handle = g_nextFileHandle++; - if (handle == 0u) - { - handle = g_nextFileHandle++; - } - - if (g_clFileHandles.find(handle) == g_clFileHandles.end() && - g_clFileLoads.find(handle) == g_clFileLoads.end()) - { - g_clFileHandles.emplace(handle, ClFileHandle{file, size}); - return handle; - } - } - - return 0u; - } - - uint32_t allocateClFileLoadLocked(uint32_t status, uint32_t size) - { - for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt) - { - uint32_t handle = g_nextLoadHandle++; - if (handle < 3u) - { - handle = kClFileFirstLoadHandle; - g_nextLoadHandle = kClFileFirstLoadHandle + 1u; - } - - if (g_clFileLoads.find(handle) == g_clFileLoads.end() && - g_clFileHandles.find(handle) == g_clFileHandles.end()) - { - g_clFileLoads.emplace(handle, ClFileLoad{status, size}); - return handle; - } - } - - return 0u; - } -} - -namespace ps2_iop_cl -{ - void reset() - { - { - std::lock_guard lock(g_clFileMutex); - for (auto &entry : g_clFileHandles) - { - if (entry.second.file) - { - std::fclose(entry.second.file); - } - } - - g_clFileHandles.clear(); - g_clFileLoads.clear(); - g_clFileRoot.clear(); - g_nextFileHandle = 1u; - g_nextLoadHandle = kClFileFirstLoadHandle; - } - - { - std::lock_guard lock(g_lotrSoundMutex); - g_lotrSoundCounter = 0u; - } - } - - bool handleClFileRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr) - { - if (sid != IOP_SID_LOTR_CLFILE) - { - return false; - } - - resultPtr = recvBufAddr; - if (recvBufAddr && recvSize > 0u) - { - if (uint8_t *recv = getMemPtr(rdram, recvBufAddr)) - { - std::memset(recv, 0, std::min(recvSize, 0x40u)); - } - } - - auto writeResult = [&](int32_t status, uint32_t value) { - if (recvBufAddr && recvSize >= sizeof(uint32_t)) - { - writeGuestU32(rdram, recvBufAddr + 0u, static_cast(status)); - } - if (recvBufAddr && recvSize >= sizeof(uint32_t) * 2u) - { - writeGuestU32(rdram, recvBufAddr + 4u, value); - } - }; - - switch (rpcNum) - { - case 0x01u: - { - const std::string guestPath = readGuestString(rdram, sendBufAddr, sendSize ? std::min(sendSize, 0x100u) : 0x100u); - uint32_t requestedBytes = 0u; - uint32_t dstAddr = 0u; - (void)readGuestU32(rdram, sendBufAddr + 0x100u, requestedBytes); - (void)readGuestU32(rdram, sendBufAddr + 0x104u, dstAddr); - - uint32_t status = kClFileLoadStatusFailed; - uint32_t fileSize = 0u; - const std::filesystem::path hostPath = resolveClFilePath(guestPath); - - if (!hostPath.empty()) - { - std::ifstream file(hostPath, std::ios::binary | std::ios::ate); - if (file.is_open()) - { - const std::streampos end = file.tellg(); - if (end >= std::streampos(0)) - { - const uint64_t hostFileSize = static_cast(end); - fileSize = static_cast(std::min(hostFileSize, 0xFFFFFFFFull)); - - uint32_t dstOffset = 0u; - bool scratch = false; - if (ps2ResolveGuestPointer(dstAddr, dstOffset, scratch) && !scratch && dstOffset < PS2_RAM_SIZE) - { - uint8_t *dst = getMemPtr(rdram, dstAddr); - const uint64_t maxGuestBytes = static_cast(PS2_RAM_SIZE - dstOffset); - const uint64_t maxRequestedBytes = requestedBytes ? requestedBytes : hostFileSize; - const size_t bytesToCopy = static_cast( - std::min({hostFileSize, maxRequestedBytes, maxGuestBytes})); - - status = kClFileLoadStatusComplete; - if (dst && bytesToCopy > 0u) - { - file.seekg(0, std::ios::beg); - file.read(reinterpret_cast(dst), static_cast(bytesToCopy)); - if (static_cast(file.gcount()) != bytesToCopy) - { - status = kClFileLoadStatusFailed; - } - } - } - } - } - } - - uint32_t loadHandle = 0u; - { - std::lock_guard lock(g_clFileMutex); - loadHandle = allocateClFileLoadLocked(status, fileSize); - } - - writeResult(kClFileLoadResultQueued, loadHandle); - return true; - } - - case 0x04u: - writeResult(0, 1u); - return true; - - case 0x05u: - writeResult(0, 0u); - return true; - - case 0x15u: - writeResult(0, 0u); - return true; - - case 0x16u: - { - const std::string root = readGuestString(rdram, sendBufAddr, sendSize ? sendSize : 0x100u); - std::lock_guard lock(g_clFileMutex); - g_clFileRoot = root; - writeResult(0, 1u); - return true; - } - - case 0x08u: - { - const std::string guestPath = readGuestString(rdram, sendBufAddr, sendSize ? sendSize : 0x100u); - const std::filesystem::path hostPath = resolveClFilePath(guestPath); - if (hostPath.empty()) - { - writeResult(-1, 0u); - return true; - } - - FILE *file = std::fopen(hostPath.string().c_str(), "rb"); - if (!file) - { - writeResult(-1, 0u); - return true; - } - - uint32_t size = 0u; - if (std::fseek(file, 0, SEEK_END) == 0) - { - const long end = std::ftell(file); - if (end > 0) - { - size = static_cast(std::min(end, 0x7FFFFFFF)); - } - std::fseek(file, 0, SEEK_SET); - } - - uint32_t handle = 0u; - { - std::lock_guard lock(g_clFileMutex); - handle = allocateClFileHandleLocked(file, size); - } - if (!handle) - { - std::fclose(file); - writeResult(-1, 0u); - return true; - } - - writeResult(0, handle); - return true; - } - - case 0x09u: - { - uint32_t handle = 0u; - readGuestU32(rdram, sendBufAddr, handle); - - FILE *file = nullptr; - bool closedLoad = false; - { - std::lock_guard lock(g_clFileMutex); - auto it = g_clFileHandles.find(handle); - if (it != g_clFileHandles.end()) - { - file = it->second.file; - g_clFileHandles.erase(it); - } - - auto loadIt = g_clFileLoads.find(handle); - if (loadIt != g_clFileLoads.end()) - { - g_clFileLoads.erase(loadIt); - closedLoad = true; - } - } - if (file) - { - std::fclose(file); - } - - const bool closed = file || closedLoad; - writeResult(closed ? 0 : -1, closed ? 1u : 0u); - return true; - } - - case 0x0Au: - { - uint32_t handle = 0u; - uint32_t requested = 0u; - uint32_t dstAddr = 0u; - readGuestU32(rdram, sendBufAddr + 0u, handle); - readGuestU32(rdram, sendBufAddr + 4u, requested); - readGuestU32(rdram, sendBufAddr + 8u, dstAddr); - - FILE *file = nullptr; - { - std::lock_guard lock(g_clFileMutex); - auto it = g_clFileHandles.find(handle); - if (it != g_clFileHandles.end()) - { - file = it->second.file; - } - } - - uint8_t *dst = getMemPtr(rdram, dstAddr); - if (!file || !dst) - { - writeResult(-1, 0u); - return true; - } - - const uint32_t bytesToRead = std::min(requested, kClFileBufferBytes); - const size_t bytesRead = std::fread(dst, 1u, bytesToRead, file); - if (bytesRead < bytesToRead && std::ferror(file)) - { - std::clearerr(file); - writeResult(-1, 0u); - return true; - } - - writeResult(0, static_cast(bytesRead)); - return true; - } - - case 0x03u: - { - uint32_t handle = 0u; - readGuestU32(rdram, sendBufAddr, handle); - - std::lock_guard lock(g_clFileMutex); - auto loadIt = g_clFileLoads.find(handle); - if (loadIt != g_clFileLoads.end()) - { - writeResult(static_cast(loadIt->second.status), 0u); - return true; - } - - writeResult(0, g_clFileHandles.find(handle) != g_clFileHandles.end() ? 0u : 9u); - return true; - } - - case 0x06u: - { - uint32_t handle = 0u; - readGuestU32(rdram, sendBufAddr, handle); - - std::lock_guard lock(g_clFileMutex); - auto loadIt = g_clFileLoads.find(handle); - if (loadIt != g_clFileLoads.end()) - { - writeResult(0, loadIt->second.size); - return true; - } - - auto it = g_clFileHandles.find(handle); - writeResult(it != g_clFileHandles.end() ? 0 : -1, - it != g_clFileHandles.end() ? it->second.size : 0u); - return true; - } - - default: - writeResult(0, 0u); - return true; - } - } - - bool handleSoundRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr, - bool &signalNowaitCompletion) - { - if (sid != IOP_SID_LOTR_SOUND) - { - return false; - } - - (void)rpcNum; - (void)sendBufAddr; - (void)sendSize; - - resultPtr = recvBufAddr; - signalNowaitCompletion = true; - - if (recvBufAddr && recvSize > 0u) - { - if (uint8_t *recv = getMemPtr(rdram, recvBufAddr)) - { - std::memset(recv, 0, recvSize); - } - } - - uint32_t counter = 0u; - { - std::lock_guard lock(g_lotrSoundMutex); - counter = ++g_lotrSoundCounter; - } - - if (recvBufAddr && recvSize >= kLotrSoundResponseCounterOffset + sizeof(uint32_t)) - { - // LotR's SOUND_JP callback reads word 0 as the number of active - // stream records and word 1 as the IOP update counter. - writeGuestU32(rdram, recvBufAddr + kLotrSoundResponseCounterOffset, counter); - } - - return true; - } -} diff --git a/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp b/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp deleted file mode 100644 index e0f3b1d..0000000 --- a/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "runtime/ps2_iop_dbcman.h" -#include "runtime/ps2_memory.h" - -#include -#include - -namespace -{ - constexpr uint32_t kDbcManSid = 0x80001300u; - constexpr uint32_t kRpcCheckVersion = 0x80001363u; - constexpr uint32_t kDbcManVersion = 0x0320u; - constexpr uint32_t kMaxUnknownRpcLogs = 32u; - - uint32_t g_unknownRpcLogCount = 0u; - - bool writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value) - { - uint8_t *ptr = getMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - - std::memcpy(ptr, &value, sizeof(value)); - return true; - } - - void writeVersionResponse(uint8_t *rdram, uint32_t recvBufAddr, uint32_t recvSize) - { - const uint32_t words = recvSize / sizeof(uint32_t); - const uint32_t count = words < 4u ? words : 4u; - for (uint32_t index = 0; index < count; ++index) - { - writeGuestU32(rdram, recvBufAddr + (index * sizeof(uint32_t)), kDbcManVersion); - } - } -} - -namespace ps2_iop_dbcman -{ - void reset() - { - g_unknownRpcLogCount = 0u; - } - - bool handleDbcManRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr) - { - if (sid != kDbcManSid) - { - return false; - } - - resultPtr = recvBufAddr; - if (recvBufAddr == 0u || recvSize == 0u) - { - return true; - } - - switch (rpcNum) - { - case kRpcCheckVersion: - // libdbc expects dbcman.irx version 3.20. - writeVersionResponse(rdram, recvBufAddr, recvSize); - return true; - - default: - if (g_unknownRpcLogCount < kMaxUnknownRpcLogs) - { - std::cerr << "[DBCMAN:stub]" - << " sid=0x" << std::hex << sid - << " rpc=0x" << rpcNum - << " send=0x" << sendBufAddr - << " sendSize=0x" << sendSize - << " recv=0x" << recvBufAddr - << " recvSize=0x" << recvSize - << std::dec << std::endl; - ++g_unknownRpcLogCount; - } - return true; - } - } -} diff --git a/ps2xRuntime/src/lib/ps2_iop_host.cpp b/ps2xRuntime/src/lib/ps2_iop_host.cpp new file mode 100644 index 0000000..b89a3c2 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_iop_host.cpp @@ -0,0 +1,503 @@ +#include "ps2_iop_host.h" + +#include "ps2_runtime.h" +#include "ps2_stubs.h" +#include "runtime/ps2_memory.h" +#include "Kernel/Stubs/MemoryCard.h" +#include "Kernel/Syscalls/Common.h" + +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#endif + +PS2IopHostAdapter::CallScope::CallScope(PS2IopHostAdapter &owner, R5900Context *context, uint8_t *rdram) + : m_lock(owner.m_callMutex), + m_owner(&owner), + m_previousContext(owner.m_activeContext), + m_previousRdram(owner.m_activeRdram), + m_previousToken(owner.m_activeToken) +{ + m_token = owner.m_nextToken++; + if (m_token == 0) + { + m_token = owner.m_nextToken++; + } + owner.m_activeContext = context; + owner.m_activeRdram = rdram; + owner.m_activeToken = m_token; +} + +PS2IopHostAdapter::CallScope::~CallScope() +{ + release(); +} + +PS2IopHostAdapter::CallScope::CallScope(CallScope &&other) noexcept + : m_lock(std::move(other.m_lock)), + m_owner(std::exchange(other.m_owner, nullptr)), + m_previousContext(other.m_previousContext), + m_previousRdram(other.m_previousRdram), + m_previousToken(other.m_previousToken), + m_token(other.m_token) +{ +} + +PS2IopHostAdapter::CallScope &PS2IopHostAdapter::CallScope::operator=(CallScope &&other) noexcept +{ + if (this != &other) + { + release(); + m_lock = std::move(other.m_lock); + m_owner = std::exchange(other.m_owner, nullptr); + m_previousContext = other.m_previousContext; + m_previousRdram = other.m_previousRdram; + m_previousToken = other.m_previousToken; + m_token = other.m_token; + } + return *this; +} + +void PS2IopHostAdapter::CallScope::release() +{ + if (m_owner) + { + m_owner->m_activeContext = m_previousContext; + m_owner->m_activeRdram = m_previousRdram; + m_owner->m_activeToken = m_previousToken; + m_owner = nullptr; + } +} + +PS2IopHostAdapter::PS2IopHostAdapter(PS2Runtime &runtime) + : m_runtime(runtime) +{ +} + +PS2IopHostAdapter::~PS2IopHostAdapter() +{ + std::lock_guard lock(m_hostFileMutex); + for (auto &[handle, file] : m_hostFiles) + { + (void)handle; + if (file.stream) + { + std::fclose(file.stream); + } + } + m_hostFiles.clear(); +} + +PS2IopHostAdapter::CallScope PS2IopHostAdapter::enterCall(R5900Context *context, uint8_t *rdram) +{ + return CallScope(*this, context, rdram); +} + +bool PS2IopHostAdapter::guestRange(uint32_t address, size_t size, uint8_t *&begin) const +{ + begin = nullptr; + uint8_t *const rdram = m_activeRdram + ? m_activeRdram + : m_runtime.memory().getRDRAM(); + if (!rdram) + { + return false; + } + if (size == 0) + { + begin = getMemPtr(rdram, address); + return begin != nullptr; + } + if (size - 1 > std::numeric_limits::max() - address) + { + return false; + } + uint8_t *const first = getMemPtr(rdram, address); + uint8_t *const last = getMemPtr(rdram, address + static_cast(size - 1)); + if (!first || !last || last < first || static_cast(last - first) != size - 1) + { + return false; + } + begin = first; + return true; +} + +bool PS2IopHostAdapter::readGuest(uint32_t address, void *destination, size_t size) const +{ + if (!destination && size != 0) + { + return false; + } + uint8_t *source = nullptr; + if (!guestRange(address, size, source)) + { + return false; + } + if (size != 0) + { + std::memcpy(destination, source, size); + } + return true; +} + +bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t size) +{ + if (!source && size != 0) + { + return false; + } + uint8_t *destination = nullptr; + if (!guestRange(address, size, destination)) + { + return false; + } + if (size != 0) + { + std::memcpy(destination, source, size); + } + return true; +} + +bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size) +{ + uint8_t *destination = nullptr; + if (!guestRange(address, size, destination)) + { + return false; + } + if (size != 0) + { + std::memset(destination, 0, size); + } + return true; +} + +bool PS2IopHostAdapter::normalizeGuestAddress(uint32_t address, uint32_t &normalized) const +{ + bool scratchpad = false; + if (!ps2ResolveGuestPointer(address, normalized, scratchpad) || scratchpad) + { + normalized = 0; + return false; + } + return true; +} + +uint32_t PS2IopHostAdapter::allocateIopHandle(ps2x::iop::IopHandleKind kind) +{ + uint8_t *const rdram = m_activeRdram + ? m_activeRdram + : m_runtime.memory().getRDRAM(); + if (!rdram) + { + return 0; + } + return kind == ps2x::iop::IopHandleKind::RpcPacket + ? rpcAllocPacketAddr(rdram) + : rpcAllocServerAddr(rdram); +} + +uint32_t PS2IopHostAdapter::allocateGuest(uint32_t size, uint32_t alignment) +{ + return m_runtime.guestMalloc(size, alignment); +} + +void PS2IopHostAdapter::freeGuest(uint32_t address) +{ + m_runtime.guestFree(address); +} + +void PS2IopHostAdapter::audioCommand(uint32_t sid, + uint32_t function, + ps2x::iop::GuestBuffer send, + ps2x::iop::GuestBuffer receive) +{ + uint8_t *sendPointer = nullptr; + uint8_t *receivePointer = nullptr; + if (send.address && !guestRange(send.address, send.size, sendPointer)) + { + sendPointer = nullptr; + } + if (receive.address && !guestRange(receive.address, receive.size, receivePointer)) + { + receivePointer = nullptr; + } + m_runtime.audioBackend().onSoundCommand(sid, + function, + sendPointer, + send.size, + receivePointer, + receive.size); +} + +std::string PS2IopHostAdapter::hostPath(ps2x::iop::HostPathKind kind) const +{ + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + switch (kind) + { + case ps2x::iop::HostPathKind::CdRoot: + return paths.cdRoot.string(); + case ps2x::iop::HostPathKind::CdImage: + return paths.cdImage.string(); + case ps2x::iop::HostPathKind::HostRoot: + return paths.hostRoot.string(); + case ps2x::iop::HostPathKind::MemoryCardRoot: + return paths.mcRoot.string(); + case ps2x::iop::HostPathKind::ElfDirectory: + default: + return paths.elfDirectory.string(); + } +} + +std::string PS2IopHostAdapter::translateGuestPath(std::string_view path) const +{ + return translatePs2Path(std::string(path).c_str()); +} + +uint64_t PS2IopHostAdapter::openHostFile(std::string_view path) +{ + if (path.empty()) + { + return 0u; + } + + const std::filesystem::path hostPath{std::string(path)}; +#if defined(_WIN32) + std::FILE *stream = ::_wfopen(hostPath.c_str(), L"rb"); +#else + std::FILE *stream = std::fopen(hostPath.string().c_str(), "rb"); +#endif + if (!stream) + { + return 0u; + } + + std::error_code error; + const uint64_t size = std::filesystem::file_size(hostPath, error); + if (error) + { + std::fclose(stream); + return 0u; + } + + std::lock_guard lock(m_hostFileMutex); + uint64_t handle = m_nextHostFileHandle++; + if (handle == 0u) + { + handle = m_nextHostFileHandle++; + } + m_hostFiles.emplace(handle, HostFile{stream, size}); + return handle; +} + +bool PS2IopHostAdapter::hostFileSize(uint64_t handle, uint64_t &size) const +{ + size = 0u; + std::lock_guard lock(m_hostFileMutex); + const auto it = m_hostFiles.find(handle); + if (it == m_hostFiles.end() || !it->second.stream) + { + return false; + } + size = it->second.size; + return true; +} + +bool PS2IopHostAdapter::readHostFile(uint64_t handle, + uint64_t offset, + void *destination, + size_t size, + size_t &bytesRead) +{ + bytesRead = 0u; + if (!destination && size != 0u) + { + return false; + } + + std::lock_guard lock(m_hostFileMutex); + const auto it = m_hostFiles.find(handle); + if (it == m_hostFiles.end() || !it->second.stream) + { + return false; + } + +#if defined(_WIN32) + if (offset > static_cast(std::numeric_limits::max()) || + _fseeki64(it->second.stream, static_cast(offset), SEEK_SET) != 0) +#else + if (offset > static_cast(std::numeric_limits::max()) || + fseeko(it->second.stream, static_cast(offset), SEEK_SET) != 0) +#endif + { + return false; + } + + bytesRead = size == 0u + ? 0u + : std::fread(destination, 1u, size, it->second.stream); + if (bytesRead < size && std::ferror(it->second.stream)) + { + std::clearerr(it->second.stream); + return false; + } + return true; +} + +void PS2IopHostAdapter::closeHostFile(uint64_t handle) +{ + std::FILE *stream = nullptr; + { + std::lock_guard lock(m_hostFileMutex); + const auto it = m_hostFiles.find(handle); + if (it == m_hostFiles.end()) + { + return; + } + stream = it->second.stream; + m_hostFiles.erase(it); + } + if (stream) + { + std::fclose(stream); + } +} + +int32_t PS2IopHostAdapter::memoryCard(const ps2x::iop::MemoryCardRequest &request) +{ + using Handler = void (*)(uint8_t *, R5900Context *, PS2Runtime *); + Handler handler = nullptr; + switch (request.operation) + { + case ps2x::iop::MemoryCardOperation::Init: + handler = ps2_stubs::sceMcInit; + break; + case ps2x::iop::MemoryCardOperation::GetInfo: + handler = ps2_stubs::sceMcGetInfo; + break; + case ps2x::iop::MemoryCardOperation::Open: + handler = ps2_stubs::sceMcOpen; + break; + case ps2x::iop::MemoryCardOperation::Close: + handler = ps2_stubs::sceMcClose; + break; + case ps2x::iop::MemoryCardOperation::Seek: + handler = ps2_stubs::sceMcSeek; + break; + case ps2x::iop::MemoryCardOperation::Read: + handler = ps2_stubs::sceMcRead; + break; + case ps2x::iop::MemoryCardOperation::Write: + handler = ps2_stubs::sceMcWrite; + break; + case ps2x::iop::MemoryCardOperation::Flush: + handler = ps2_stubs::sceMcFlush; + break; + case ps2x::iop::MemoryCardOperation::Chdir: + handler = ps2_stubs::sceMcChdir; + break; + case ps2x::iop::MemoryCardOperation::GetDir: + handler = ps2_stubs::sceMcGetDir; + break; + case ps2x::iop::MemoryCardOperation::SetFileInfo: + handler = ps2_stubs::sceMcSetFileInfo; + break; + case ps2x::iop::MemoryCardOperation::Delete: + handler = ps2_stubs::sceMcDelete; + break; + case ps2x::iop::MemoryCardOperation::Format: + handler = ps2_stubs::sceMcFormat; + break; + case ps2x::iop::MemoryCardOperation::Unformat: + handler = ps2_stubs::sceMcUnformat; + break; + case ps2x::iop::MemoryCardOperation::Mkdir: + handler = ps2_stubs::sceMcMkdir; + break; + } + if (!handler) + { + return -1; + } + + R5900Context context{}; + for (size_t i = 0; i < 4u; ++i) + { + setRegU32(&context, static_cast(4 + i), request.arguments[i]); + } + + uint32_t stackAddress = 0u; + if (request.arguments[4] != 0u) + { + stackAddress = allocateGuest(32u, 16u); + if (stackAddress == 0u || + !writeGuest(stackAddress + 16u, &request.arguments[4], sizeof(uint32_t))) + { + if (stackAddress != 0u) + { + freeGuest(stackAddress); + } + return -1; + } + setRegU32(&context, 29, stackAddress); + } + + handler(m_activeRdram ? m_activeRdram : m_runtime.memory().getRDRAM(), + &context, + &m_runtime); + if (stackAddress != 0u) + { + freeGuest(stackAddress); + } + return ps2_stubs::getMemoryCardDebugSnapshot().lastResult; +} + +bool PS2IopHostAdapter::hasGuestFunction(uint32_t address) const +{ + return m_runtime.hasFunction(address); +} + +bool PS2IopHostAdapter::invokeGuestFunction(uint64_t callToken, + uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t *resultAddress) +{ + if (!m_activeContext || callToken == 0 || callToken != m_activeToken) + { + return false; + } + return rpcInvokeFunction(m_activeRdram + ? m_activeRdram + : m_runtime.memory().getRDRAM(), + m_activeContext, + &m_runtime, + address, + a0, + a1, + a2, + a3, + resultAddress); +} + +void PS2IopHostAdapter::log(ps2x::iop::LogLevel level, std::string_view message) +{ + const char *prefix = "[ps2xIOP]"; + if (level == ps2x::iop::LogLevel::Warning) + { + prefix = "[ps2xIOP:warning]"; + } + else if (level == ps2x::iop::LogLevel::Error) + { + prefix = "[ps2xIOP:error]"; + } + std::cerr << prefix << ' ' << message << std::endl; +} diff --git a/ps2xRuntime/src/lib/ps2_iop_host.h b/ps2xRuntime/src/lib/ps2_iop_host.h new file mode 100644 index 0000000..48c5b22 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_iop_host.h @@ -0,0 +1,103 @@ +#pragma once + +#include "ps2x/iop/iop_host.h" + +#include +#include +#include +#include + +class PS2Runtime; +struct R5900Context; + +class PS2IopHostAdapter final : public ps2x::iop::IopHost +{ +public: + class CallScope + { + public: + CallScope() = default; + CallScope(PS2IopHostAdapter &owner, R5900Context *context, uint8_t *rdram); + ~CallScope(); + + CallScope(const CallScope &) = delete; + CallScope &operator=(const CallScope &) = delete; + CallScope(CallScope &&other) noexcept; + CallScope &operator=(CallScope &&other) noexcept; + + [[nodiscard]] uint64_t token() const { return m_token; } + + private: + void release(); + + std::unique_lock m_lock; + PS2IopHostAdapter *m_owner = nullptr; + R5900Context *m_previousContext = nullptr; + uint8_t *m_previousRdram = nullptr; + uint64_t m_previousToken = 0; + uint64_t m_token = 0; + }; + + explicit PS2IopHostAdapter(PS2Runtime &runtime); + ~PS2IopHostAdapter() override; + + [[nodiscard]] CallScope enterCall(R5900Context *context, + uint8_t *rdram = nullptr); + + bool readGuest(uint32_t address, void *destination, size_t size) const override; + bool writeGuest(uint32_t address, const void *source, size_t size) override; + bool zeroGuest(uint32_t address, size_t size) override; + bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override; + uint32_t allocateIopHandle(ps2x::iop::IopHandleKind kind) override; + uint32_t allocateGuest(uint32_t size, uint32_t alignment) override; + void freeGuest(uint32_t address) override; + + void audioCommand(uint32_t sid, + uint32_t function, + ps2x::iop::GuestBuffer send, + ps2x::iop::GuestBuffer receive) override; + + std::string hostPath(ps2x::iop::HostPathKind kind) const override; + std::string translateGuestPath(std::string_view path) const override; + uint64_t openHostFile(std::string_view path) override; + bool hostFileSize(uint64_t handle, uint64_t &size) const override; + bool readHostFile(uint64_t handle, + uint64_t offset, + void *destination, + size_t size, + size_t &bytesRead) override; + void closeHostFile(uint64_t handle) override; + + int32_t memoryCard(const ps2x::iop::MemoryCardRequest &request) override; + + bool hasGuestFunction(uint32_t address) const override; + bool invokeGuestFunction(uint64_t callToken, + uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t *resultAddress) override; + + void log(ps2x::iop::LogLevel level, std::string_view message) override; + +private: + friend class CallScope; + + bool guestRange(uint32_t address, size_t size, uint8_t *&begin) const; + + PS2Runtime &m_runtime; + std::recursive_mutex m_callMutex; + R5900Context *m_activeContext = nullptr; + uint8_t *m_activeRdram = nullptr; + uint64_t m_activeToken = 0; + uint64_t m_nextToken = 1; + struct HostFile + { + std::FILE *stream = nullptr; + uint64_t size = 0u; + }; + mutable std::mutex m_hostFileMutex; + std::unordered_map m_hostFiles; + uint64_t m_nextHostFileHandle = 1u; +}; diff --git a/ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp b/ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp deleted file mode 100644 index 9dae347..0000000 --- a/ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp +++ /dev/null @@ -1,325 +0,0 @@ -#include "runtime/ps2_iop_sdrdrv.h" -#include "runtime/ps2_iop.h" -#include "runtime/ps2_memory.h" -#include "ps2_runtime.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ - constexpr uint32_t kImgHeaderAddr = 0x012F0000u; - constexpr uint32_t kSectorSize = 2048u; - constexpr uint32_t kStatusOffset = 0x6Cu; - constexpr uint32_t kStatusStride = 8u; - - uint32_t g_headerWarnCount = 0u; - uint32_t g_bodyWarnCount = 0u; - - bool readGuestU32(const uint8_t *rdram, uint32_t addr, uint32_t &out) - { - const uint8_t *ptr = getConstMemPtr(rdram, addr); - if (!ptr) - { - return false; - } - - std::memcpy(&out, ptr, sizeof(out)); - return true; - } - - bool tryExistingFile(const std::filesystem::path &path, std::filesystem::path &out) - { - std::error_code ec; - if (!path.empty() && std::filesystem::is_regular_file(path, ec) && !ec) - { - out = path; - return true; - } - return false; - } - - bool findSiblingFile(const char *lowerName, const char *upperName, std::filesystem::path &out) - { - const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); - const std::array roots = { - paths.cdRoot, - paths.elfDirectory, - }; - - for (const std::filesystem::path &root : roots) - { - if (tryExistingFile(root / lowerName, out) || - tryExistingFile(root / upperName, out)) - { - return true; - } - } - - return false; - } - - bool readHostRange(const std::filesystem::path &path, uint64_t offsetBytes, uint8_t *dst, size_t byteCount) - { - if (!dst) - { - return false; - } - if (byteCount == 0) - { - return true; - } - - std::memset(dst, 0, byteCount); - - std::ifstream file(path, std::ios::binary); - if (!file.is_open()) - { - return false; - } - - file.seekg(static_cast(offsetBytes), std::ios::beg); - if (!file.good()) - { - return false; - } - - file.read(reinterpret_cast(dst), static_cast(byteCount)); - return true; - } - - size_t clampGuestRdramBytes(uint32_t addr, uint32_t requested) - { - uint32_t offset = 0u; - bool scratch = false; - if (!ps2ResolveGuestPointer(addr, offset, scratch) || scratch || offset >= PS2_RAM_SIZE) - { - return 0u; - } - - return static_cast( - std::min(requested, static_cast(PS2_RAM_SIZE - offset))); - } - - bool loadImageHeader(uint8_t *rdram) - { - std::filesystem::path headerPath; - if (!findSiblingFile("img_hd.bin", "IMG_HD.BIN", headerPath)) - { - return false; - } - - std::error_code ec; - const uint64_t fileBytes = static_cast(std::filesystem::file_size(headerPath, ec)); - if (ec) - { - return false; - } - - uint8_t *dst = getMemPtr(rdram, kImgHeaderAddr); - if (!dst) - { - return false; - } - - const size_t bytes = static_cast(std::min(fileBytes, PS2_RAM_SIZE - (kImgHeaderAddr & PS2_RAM_MASK))); - return readHostRange(headerPath, 0u, dst, bytes); - } - - void warnImageHeaderLoadFailed() - { - if (g_headerWarnCount < 4u) - { - std::cerr << "[FatalFrame SDRDRV] failed to load img_hd.bin" << std::endl; - ++g_headerWarnCount; - } - } - - bool readBody(uint8_t *rdram, uint32_t lbn, uint32_t byteCount, uint32_t dstAddr) - { - if (byteCount == 0) - { - return true; - } - - uint32_t dstOffset = 0u; - bool scratch = false; - if (!ps2ResolveGuestPointer(dstAddr, dstOffset, scratch) || scratch || dstOffset >= PS2_RAM_SIZE) - { - return false; - } - - uint8_t *dst = getMemPtr(rdram, dstAddr); - if (!dst) - { - return false; - } - - const size_t bytes = static_cast( - std::min(byteCount, static_cast(PS2_RAM_SIZE - dstOffset))); - - std::filesystem::path bodyPath; - if (!findSiblingFile("img_bd.bin", "IMG_BD.BIN", bodyPath)) - { - bodyPath = PS2Runtime::getIoPaths().cdImage; - } - if (bodyPath.empty()) - { - return false; - } - - const uint64_t offsetBytes = static_cast(lbn) * kSectorSize; - return readHostRange(bodyPath, offsetBytes, dst, bytes); - } - - void clearRecv(uint8_t *rdram, uint32_t recvBufAddr, uint32_t recvSize) - { - if (recvBufAddr == 0u || recvSize == 0u) - { - return; - } - - if (uint8_t *recv = getMemPtr(rdram, recvBufAddr)) - { - const size_t bytes = clampGuestRdramBytes(recvBufAddr, recvSize); - std::memset(recv, 0, bytes); - } - } - - void markLoadComplete(uint8_t *rdram, uint32_t recvBufAddr, uint32_t recvSize, uint32_t loadId) - { - const uint32_t statusOffset = kStatusOffset + ((loadId & 0x1Fu) * kStatusStride); - if (recvBufAddr == 0u || statusOffset >= recvSize) - { - return; - } - - if (uint8_t *status = getMemPtr(rdram, recvBufAddr + statusOffset)) - { - *status = 0u; - } - } -} - -namespace ps2_iop_sdrdrv -{ - void reset() - { - g_headerWarnCount = 0u; - g_bodyWarnCount = 0u; - } - - bool handleSdrdrvRpc(uint8_t *rdram, - uint32_t sid, - uint32_t rpcNum, - uint32_t sendBufAddr, - uint32_t sendSize, - uint32_t recvBufAddr, - uint32_t recvSize, - uint32_t &resultPtr) - { - if (sid != IOP_SID_FATAL_FRAME_SDRDRV) - { - return false; - } - - resultPtr = recvBufAddr; - clearRecv(rdram, recvBufAddr, recvSize); - - if (rpcNum == 0u) - { - if (!loadImageHeader(rdram)) - { - warnImageHeaderLoadFailed(); - } - return true; - } - - if (rpcNum == 2u) - { - return true; - } - - if (rpcNum != 1u) - { - return true; - } - - constexpr uint32_t kCommandBytes = 32u; - const uint32_t commandCount = std::min(sendSize / kCommandBytes, 32u); - for (uint32_t commandIndex = 0; commandIndex < commandCount; ++commandIndex) - { - std::array words{}; - const uint32_t commandAddr = sendBufAddr + (commandIndex * kCommandBytes); - bool validCommand = true; - for (uint32_t wordIndex = 0; wordIndex < words.size(); ++wordIndex) - { - if (!readGuestU32(rdram, commandAddr + (wordIndex * sizeof(uint32_t)), words[wordIndex])) - { - validCommand = false; - break; - } - } - if (!validCommand) - { - continue; - } - - switch (words[0]) - { - case 0x0Cu: - if (!loadImageHeader(rdram)) - { - warnImageHeaderLoadFailed(); - } - break; - - case 0x0Eu: - { - const uint32_t lbn = words[2]; - const uint32_t byteCount = words[3]; - const uint32_t dstAddr = words[4]; - const uint32_t loadId = words[6]; - const bool eeLoad = words[5] == 0u; - const bool bodyLoaded = !eeLoad || readBody(rdram, lbn, byteCount, dstAddr); - - if (eeLoad && !bodyLoaded) - { - if (uint8_t *dst = getMemPtr(rdram, dstAddr)) - { - uint32_t dstOffset = 0u; - bool scratch = false; - (void)ps2ResolveGuestPointer(dstAddr, dstOffset, scratch); - if (!scratch && dstOffset < PS2_RAM_SIZE) - { - const size_t bytes = static_cast( - std::min(byteCount, static_cast(PS2_RAM_SIZE - dstOffset))); - std::memset(dst, 0, bytes); - } - } - - if (g_bodyWarnCount < 8u) - { - std::cerr << "[FatalFrame SDRDRV] failed data read lbn=0x" << std::hex << lbn - << " bytes=0x" << byteCount - << " dst=0x" << dstAddr << std::dec << std::endl; - ++g_bodyWarnCount; - } - } - - markLoadComplete(rdram, recvBufAddr, recvSize, loadId); - break; - } - - default: - break; - } - } - - return true; - } -} diff --git a/ps2xRuntime/src/lib/ps2_iop_transport.h b/ps2xRuntime/src/lib/ps2_iop_transport.h new file mode 100644 index 0000000..988d6b2 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_iop_transport.h @@ -0,0 +1,58 @@ +#pragma once + +#include "ps2_runtime.h" +#include "ps2x/iop/iop_subsystem.h" + +#include + +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) + { + return runtime + ? runtime->selectIopRpcAbi(request) + : ps2x::iop::RpcAbi::RuntimeDefault; + } + + [[nodiscard]] static ps2x::iop::RpcResult handleRpc( + PS2Runtime *runtime, + uint8_t *rdram, + R5900Context *context, + ps2x::iop::RpcRequest request) + { + return runtime + ? runtime->handleIopRpc(rdram, context, std::move(request)) + : ps2x::iop::RpcResult{}; + } + + static void notifyTransfer( + PS2Runtime *runtime, + uint8_t *rdram, + const ps2x::iop::SifTransfer &transfer) + { + if (runtime) + { + runtime->notifyIopSifTransfer(rdram, transfer); + } + } + + static void reset(PS2Runtime *runtime) + { + if (runtime) + { + runtime->resetIop(); + } + } +}; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 17bcb17..a2cc20c 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -10,6 +10,8 @@ #include "Kernel/Stubs/GS.h" #include "Kernel/Stubs/MPEG.h" #include "ps2_host_backend.h" +#include "ps2_iop_host.h" +#include "ps2x/iop/iop_subsystem.h" #include #include @@ -107,6 +109,48 @@ namespace thread_local DispatchHistory g_dispatchHistory; thread_local std::unordered_map g_guestExecutionDepths; + bool computeFileCrc32(const std::string &path, uint32_t &crcOut) + { + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) + { + return false; + } + + static const std::array table = [] + { + std::array values{}; + for (uint32_t i = 0; i < values.size(); ++i) + { + uint32_t value = i; + for (uint32_t bit = 0; bit < 8; ++bit) + { + value = (value & 1u) ? (0xEDB88320u ^ (value >> 1u)) : (value >> 1u); + } + values[i] = value; + } + return values; + }(); + + uint32_t crc = 0xFFFFFFFFu; + std::array buffer{}; + while (file.good()) + { + file.read(reinterpret_cast(buffer.data()), static_cast(buffer.size())); + const std::streamsize count = file.gcount(); + for (std::streamsize i = 0; i < count; ++i) + { + crc = table[(crc ^ buffer[static_cast(i)]) & 0xFFu] ^ (crc >> 8u); + } + } + if (file.bad()) + { + return false; + } + crcOut = ~crc; + return true; + } + void pushDispatchPc(uint32_t pc) { DispatchHistory &h = g_dispatchHistory; @@ -293,7 +337,6 @@ namespace return paths; } - std::string readGuestPrintableString(const uint8_t *rdram, uint32_t addr, size_t maxLen) { std::string out; @@ -463,6 +506,17 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint PS2Runtime::PS2Runtime() { + m_iopHost = std::make_unique(*this); + m_iopSubsystem = std::make_unique(*m_iopHost); +#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 + std::memset(&m_cpuContext, 0, sizeof(m_cpuContext)); // R0 is always zero in MIPS @@ -504,6 +558,8 @@ PS2Runtime::~PS2Runtime() { requestStop(); ps2_syscalls::detachAllGuestHostThreads(); + m_iopSubsystem.reset(); + m_iopHost.reset(); #if defined(PLATFORM_VITA) m_audioBackend.stopAll(); m_audioBackend.setAudioReady(false); @@ -526,7 +582,6 @@ PS2Runtime::~PS2Runtime() } m_loadedModules.clear(); - } catch (const std::exception &e) { @@ -538,6 +593,39 @@ PS2Runtime::~PS2Runtime() } } +void PS2Runtime::setIopPluginSearchPaths(std::vector paths) +{ + m_iopSubsystem->setPluginSearchPaths(std::move(paths)); +} + +ps2x::iop::RpcAbi PS2Runtime::selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const +{ + return m_iopSubsystem->selectRpcAbi(request); +} + +ps2x::iop::RpcResult PS2Runtime::handleIopRpc(uint8_t *rdram, R5900Context *ctx, ps2x::iop::RpcRequest request) +{ + auto scope = m_iopHost->enterCall(ctx, rdram); + request.callToken = scope.token(); + return m_iopSubsystem->handleRpc(request); +} + +void PS2Runtime::notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransfer &transfer) +{ + auto scope = m_iopHost->enterCall(nullptr, rdram); + m_iopSubsystem->onSifTransfer(transfer); +} + +void PS2Runtime::resetIop() +{ + m_iopSubsystem->reset(); +} + +ps2x::iop::DebugSnapshot PS2Runtime::iopDebugSnapshot() const +{ + return m_iopSubsystem->debugSnapshot(); +} + bool PS2Runtime::syncCoreSubsystems() { uint8_t *const rdram = m_memory.getRDRAM(); @@ -564,8 +652,7 @@ bool PS2Runtime::syncCoreSubsystems() { m_vu1.resume(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE, m_memory.getVU1Data(), PS2_VU1_DATA_SIZE, m_gs, &m_memory, top, itop, 65536); }); - m_iop.init(rdram); - m_iop.reset(); + resetIop(); m_vu0.reset(); m_vu1.reset(); @@ -589,7 +676,15 @@ 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 @@ -846,7 +941,28 @@ bool PS2Runtime::loadELF(const std::string &elfPath) m_loadedModules.push_back(module); - ps2_game_overrides::applyMatching(*this, elfPath, m_cpuContext.pc); + uint32_t elfCrc32 = 0u; + const bool elfCrc32Valid = computeFileCrc32(elfPath, elfCrc32); + if (!elfCrc32Valid) + { + std::cerr << "[ps2xIOP] failed to compute ELF CRC32 for '" << elfPath << "'" << std::endl; + } + ps2x::iop::GameIdentity identity; + identity.elfName = module.name; + identity.entryPoint = m_cpuContext.pc; + identity.crc32 = elfCrc32; + std::string iopError; + if (!m_iopSubsystem->configure(identity, &iopError)) + { + std::cerr << "[ps2xIOP] failed to configure profile: " << iopError << std::endl; + return false; + } + + ps2_game_overrides::applyMatching(*this, + elfPath, + m_cpuContext.pc, + elfCrc32, + elfCrc32Valid); RUNTIME_LOG("ELF file loaded successfully. Entry point: 0x" << std::hex << m_cpuContext.pc << std::dec); return true; @@ -2161,7 +2277,7 @@ void PS2Runtime::run() { m_stopRequested.store(false, std::memory_order_relaxed); ps2_stubs::resetSifState(); - ps2_syscalls::resetSoundDriverRpcState(); + resetIop(); ps2_stubs::resetAudioStubState(); ps2_stubs::resetGsSyncVCallbackState(); ps2_stubs::resetMpegStubState(); diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 917df6a..ebe5b90 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -1,10 +1,37 @@ cmake_minimum_required(VERSION 3.21) -project(ps2xTest LANGUAGES CXX) +project(ps2xTest LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +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 @@ -12,6 +39,7 @@ add_library(ps2_test_function_table OBJECT target_include_directories(ps2_test_function_table PRIVATE ${CMAKE_SOURCE_DIR}/ps2xRuntime/include + ${CMAKE_SOURCE_DIR}/ps2xIOP/include ) add_library(ps2_test_lib STATIC @@ -25,6 +53,7 @@ add_library(ps2_test_lib STATIC src/ps2_memory_tests.cpp src/ps2_vu1_tests.cpp src/ps2_gs_tests.cpp + src/ps2_iop_tests.cpp src/ps2_sif_rpc_tests.cpp src/ps2_sif_dma_tests.cpp src/ps2_recompiler_tests.cpp @@ -36,14 +65,31 @@ target_include_directories(ps2_test_lib PRIVATE ${CMAKE_SOURCE_DIR}/ps2xRecomp/include ${CMAKE_SOURCE_DIR}/ps2xAnalyzer/include ${CMAKE_SOURCE_DIR}/ps2xRuntime/include + ${CMAKE_SOURCE_DIR}/ps2xRuntime/src/lib ) target_link_libraries(ps2_test_lib PRIVATE ps2_recomp_lib ps2_analyzer_lib ps2_runtime + ps2_iop ) +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="$" + ) + 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 $ diff --git a/ps2xTest/src/fake_iop_bad_abi.c b/ps2xTest/src/fake_iop_bad_abi.c new file mode 100644 index 0000000..5d794b8 --- /dev/null +++ b/ps2xTest/src/fake_iop_bad_abi.c @@ -0,0 +1,15 @@ +#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; +} diff --git a/ps2xTest/src/fake_iop_missing_symbol.cpp b/ps2xTest/src/fake_iop_missing_symbol.cpp new file mode 100644 index 0000000..08701f2 --- /dev/null +++ b/ps2xTest/src/fake_iop_missing_symbol.cpp @@ -0,0 +1,6 @@ +#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; +} diff --git a/ps2xTest/src/fake_iop_plugin.cpp b/ps2xTest/src/fake_iop_plugin.cpp new file mode 100644 index 0000000..dc8b13b --- /dev/null +++ b/ps2xTest/src/fake_iop_plugin.cpp @@ -0,0 +1,350 @@ +#include "ps2x/iop/plugin_api.h" + +#include +#include +#include + +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 + 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(instance); + log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-destroy")); + delete state; + } + + int32_t resetProfile(void *instance) + { + auto *state = static_cast(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(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(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(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; +} diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 7e8c3c3..71d2cd0 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -12,6 +12,7 @@ void register_ps2_runtime_interrupt_tests(); void register_ps2_memory_tests(); void register_ps2_vu1_tests(); void register_ps2_gs_tests(); +void register_ps2_iop_tests(); void register_ps2_sif_rpc_tests(); void register_ps2_sif_dma_tests(); void register_ps2_recompiler_tests(); @@ -32,6 +33,7 @@ int main() register_ps2_memory_tests(); register_ps2_vu1_tests(); register_ps2_gs_tests(); + register_ps2_iop_tests(); register_ps2_sif_rpc_tests(); register_ps2_sif_dma_tests(); register_ps2_recompiler_tests(); diff --git a/ps2xTest/src/ps2_iop_tests.cpp b/ps2xTest/src/ps2_iop_tests.cpp new file mode 100644 index 0000000..59340ba --- /dev/null +++ b/ps2xTest/src/ps2_iop_tests.cpp @@ -0,0 +1,816 @@ +#include "MiniTest.h" +#include "ps2x/iop/iop_subsystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#elif defined(__linux__) +#include +#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) + { + } + + 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(); + } + + 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(nextGuestAddress) + effectiveAlignment - 1u) / + effectiveAlignment) * + effectiveAlignment; + if (aligned + size > memory.size()) + { + return 0u; + } + nextGuestAddress = static_cast(aligned + size); + guestAllocations.push_back(static_cast(aligned)); + return static_cast(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, file->second.size() - static_cast(offset)); + if (bytesRead != 0u) + { + std::memcpy(destination, + file->second.data() + static_cast(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 memory; + uint32_t nextHandle = 0x8000u; + uint32_t nextGuestAddress = 0x4000u; + std::vector guestAllocations; + std::vector 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 lastGuestArguments; + std::vector> logs; + std::unordered_map> hostFileContents; + std::unordered_map openHostFiles; + std::vector closedHostFileHandles; + uint64_t nextHostFileHandle = 1u; + + private: + bool contains(uint32_t address, size_t size) const + { + const uint64_t end = static_cast(address) + static_cast(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::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 + } +} + +void register_ps2_iop_tests() +{ + MiniTest::Case("PS2IopSubsystem", [](TestCase &tc) + { + tc.Run("unknown SID remains unhandled without a matching profile", [](TestCase &t) + { + FakeIopHost 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; + const ps2x::iop::RpcResult result = subsystem.handleRpc(request); + t.IsFalse(result.handled, "an unknown SID should not be claimed by the IOP subsystem"); + t.Equals(result.resultAddress, 0u, "an unknown SID should not return a guest result address"); + t.IsFalse(result.signalNowaitCompletion, "an unknown SID should not signal nowait completion"); + 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("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 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(command.size())}; + t.IsTrue(subsystem.handleRpc(commandRequest).handled, + "TSNDDRV should handle the characterized command queue"); + + int16_t writtenChecksum = 0; + t.IsTrue(host.readGuest(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.writeGuest(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.readGuest(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 + }); +} diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index 20fb73d..dcfe8ac 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -1208,18 +1208,18 @@ void register_ps2_runtime_expansion_tests() setRegU32(blockTransCtx, 4, 1u); setRegU32(blockTransCtx, 5, 0x80E0u); setRegU32(blockTransCtx, 6, 1u); - setRegU32(blockTransCtx, 7, 2u); + setRegU32(blockTransCtx, 7, 0x13u); std::memcpy(rdram.data() + blockTransSp + 0x10u, "\x40\x23\x01\x00", 4u); std::memcpy(rdram.data() + blockTransSp + 0x14u, "\x00\x30\x00\x00", 4u); std::memcpy(rdram.data() + blockTransSp + 0x18u, "\x40\x27\x01\x00", 4u); ps2_stubs::sceSdRemote(rdram.data(), &blockTransCtx, nullptr); - t.Equals(getRegU32(&blockTransCtx, 2), 0x00012740u, - "sceSdRemote block transfer should resume from the configured IOP pause position"); + t.Equals(getRegU32(&blockTransCtx, 2), 0u, + "sceSdRemote block-transfer start should report libsd success"); R5900Context statusCtx{}; setRegU32(statusCtx, 29, blockTransSp); setRegU32(statusCtx, 4, 1u); - setRegU32(statusCtx, 5, 0x80F0u); + setRegU32(statusCtx, 5, 0x8100u); setRegU32(statusCtx, 6, 1u); setRegU32(statusCtx, 7, 0u); std::memset(rdram.data() + blockTransSp + 0x10u, 0, 12u); @@ -1241,9 +1241,123 @@ void register_ps2_runtime_expansion_tests() setRegU32(setParamCtx, 6, 0x0F81u); setRegU32(setParamCtx, 7, 0u); ps2_stubs::sceSdRemote(rdram.data(), &setParamCtx, nullptr); - t.Equals(getRegU32(&setParamCtx, 2), 0x00012740u, + t.Equals(getRegU32(&setParamCtx, 2), 0u, "sceSdRemote set-param calls should not trap or disturb the movie audio state"); }); + + tc.Run("sceSdRemote isolates voice transfers from block streaming state", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0u); + constexpr uint32_t kStackAddr = 0x00100000u; + constexpr uint32_t kBlockBase = 0x00012340u; + constexpr uint32_t kBlockSize = 0x00003000u; + constexpr uint32_t kBlockPause = 0x00012740u; + + R5900Context initCtx{}; + ps2_stubs::sceSdRemoteInit(rdram.data(), &initCtx, nullptr); + + R5900Context blockCtx{}; + setRegU32(blockCtx, 29, kStackAddr); + setRegU32(blockCtx, 4, 1u); + setRegU32(blockCtx, 5, 0x80E0u); + setRegU32(blockCtx, 6, 1u); + setRegU32(blockCtx, 7, 0x13u); + setRegU32(blockCtx, 8, kBlockBase); + setRegU32(blockCtx, 9, kBlockSize); + setRegU32(blockCtx, 10, kBlockPause); + ps2_stubs::sceSdRemote(rdram.data(), &blockCtx, nullptr); + + R5900Context blockStatusCtx{}; + setRegU32(blockStatusCtx, 29, kStackAddr); + setRegU32(blockStatusCtx, 4, 1u); + setRegU32(blockStatusCtx, 5, 0x8100u); + setRegU32(blockStatusCtx, 6, 1u); + setRegU32(blockStatusCtx, 7, 0u); + ps2_stubs::sceSdRemote(rdram.data(), &blockStatusCtx, nullptr); + t.Equals(getRegU32(&blockStatusCtx, 2), 0x00012B40u, + "initial block-status poll should advance the streaming ring"); + + R5900Context voiceCtx{}; + setRegU32(voiceCtx, 29, kStackAddr); + setRegU32(voiceCtx, 4, 1u); + setRegU32(voiceCtx, 5, 0x80D0u); + setRegU32(voiceCtx, 6, 0u); + setRegU32(voiceCtx, 7, 0u); + setRegU32(voiceCtx, 8, 0x00022000u); + setRegU32(voiceCtx, 9, 0x00004000u); + setRegU32(voiceCtx, 10, 0x00000800u); + ps2_stubs::sceSdRemote(rdram.data(), &voiceCtx, nullptr); + t.Equals(getRegU32(&voiceCtx, 2), 0x00000800u, + "DMA voice transfer should report its transferred byte count"); + + R5900Context voiceStatusCtx{}; + setRegU32(voiceStatusCtx, 29, kStackAddr); + setRegU32(voiceStatusCtx, 4, 1u); + setRegU32(voiceStatusCtx, 5, 0x80F0u); + setRegU32(voiceStatusCtx, 6, 0u); + setRegU32(voiceStatusCtx, 7, 1u); + ps2_stubs::sceSdRemote(rdram.data(), &voiceStatusCtx, nullptr); + t.Equals(getRegU32(&voiceStatusCtx, 2), 1u, + "voice-transfer status should complete independently from block position"); + + ps2_stubs::sceSdRemote(rdram.data(), &blockStatusCtx, nullptr); + t.Equals(getRegU32(&blockStatusCtx, 2), 0x00012F40u, + "voice transfer should not replace or advance the block-streaming ring"); + }); + + tc.Run("sceSdRemote keeps block cursors and loop banks isolated per core", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0u); + constexpr uint32_t kStackAddr = 0x00100000u; + + R5900Context initCtx{}; + ps2_stubs::sceSdRemoteInit(rdram.data(), &initCtx, nullptr); + + auto remote = [&](uint32_t command, + uint32_t core, + uint32_t mode, + uint32_t arg4 = 0u, + uint32_t arg5 = 0u, + uint32_t arg6 = 0u) + { + R5900Context ctx{}; + setRegU32(ctx, 29, kStackAddr); + setRegU32(ctx, 4, 1u); + setRegU32(ctx, 5, command); + setRegU32(ctx, 6, core); + setRegU32(ctx, 7, mode); + setRegU32(ctx, 8, arg4); + setRegU32(ctx, 9, arg5); + setRegU32(ctx, 10, arg6); + ps2_stubs::sceSdRemote(rdram.data(), &ctx, nullptr); + return getRegU32(&ctx, 2); + }; + + t.Equals(remote(0x80E0u, 0u, 0x10u, 0x00010000u, 0x00001000u, 0x00010000u), 0u, + "core 0 block stream should start successfully"); + t.Equals(remote(0x80E0u, 1u, 0x13u, 0x00020000u, 0x00002000u, 0x00020800u), 0u, + "core 1 block stream should start independently"); + + t.Equals(remote(0x8100u, 0u, 0u), 0x00010400u, + "core 0 status should advance only the core 0 cursor"); + t.Equals(remote(0x8100u, 1u, 0u), 0x00020C00u, + "core 1 status should retain its independent pause position"); + t.Equals(remote(0x8100u, 0u, 0u), 0x01010800u, + "loop status should expose the second buffer in the high byte"); + + t.Equals(remote(0x80E0u, 0u, 0x02u), 0x01010800u, + "block STOP should return the final core 0 cursor"); + t.Equals(remote(0x8100u, 0u, 0u), 0u, + "stopped block status should no longer expose a live cursor"); + t.Equals(remote(0x8100u, 1u, 0u), 0x01021000u, + "stopping core 0 should not stop or advance core 1"); + + ps2_stubs::sceSdRemoteInit(rdram.data(), &initCtx, nullptr); + t.Equals(remote(0x8100u, 1u, 0u), 0u, + "sceSdRemoteInit should reset block state for both cores"); + t.Equals(remote(0x80F0u, 1u, 0u), 1u, + "sceSdRemoteInit should restore idle voice status to complete"); + }); tc.Run("IPU init skips missing optional helper instead of dispatching the default trap", [](TestCase &t) { diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 391205b..8520545 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -1,11 +1,15 @@ #include "MiniTest.h" #include "ps2_runtime.h" +#include "ps2_iop_transport.h" #include "ps2_syscalls.h" #include "ps2_stubs.h" #include #include #include +#include +#include +#include #include namespace ps2_stubs @@ -26,24 +30,19 @@ namespace TestEnv() : rdram(PS2_RAM_SIZE, 0u) { ps2_stubs::resetSifState(); - ps2_syscalls::resetSoundDriverRpcState(); - ps2_syscalls::clearSoundDriverCompatLayout(); - ps2_syscalls::clearDtxCompatLayout(); std::memset(&ctx, 0, sizeof(ctx)); } }; - void setRecvxDtxCompatLayout() + void configureProfile(TestEnv &env, std::string_view elfName) { - PS2DtxCompatLayout layout{}; - layout.rpcSid = 0x7D000000u; - layout.urpcObjBase = 0x01F18000u; - layout.urpcObjLimit = 0x01F1FF00u; - layout.urpcObjStride = 0x20u; - layout.urpcFnTableBase = 0x0034FED0u; - layout.urpcObjTableBase = 0x0034FFD0u; - layout.dispatcherFuncAddr = 0x002FABC0u; - ps2_syscalls::setDtxCompatLayout(layout); + 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) @@ -259,7 +258,7 @@ void register_ps2_sif_dma_tests() tc.Run("sceSifSetDma acknowledges DTX work-buffer transfers by advancing the EE footer ticket", [](TestCase &t) { TestEnv env; - setRecvxDtxCompatLayout(); + configureProfile(env, "slus_201.84"); constexpr uint32_t kClientAddr = 0x0002D000u; constexpr uint32_t kDtxSid = 0x7D000000u; @@ -321,7 +320,7 @@ void register_ps2_sif_dma_tests() tc.Run("sceSifSetDma applies SJX DTX payloads into the emulated SJRMT data ring", [](TestCase &t) { TestEnv env; - setRecvxDtxCompatLayout(); + configureProfile(env, "slus_201.84"); constexpr uint32_t kClientAddr = 0x0002E000u; constexpr uint32_t kDtxSid = 0x7D000000u; @@ -442,7 +441,7 @@ void register_ps2_sif_dma_tests() tc.Run("sceSifSetDma recognizes SJX DTX payloads from rotated EE work buffers", [](TestCase &t) { TestEnv env; - setRecvxDtxCompatLayout(); + configureProfile(env, "slus_201.84"); constexpr uint32_t kClientAddr = 0x00031000u; constexpr uint32_t kDtxSid = 0x7D000000u; @@ -568,7 +567,7 @@ void register_ps2_sif_dma_tests() tc.Run("sceSifSetDma lets active PS2RNA playback drain emulated SJRMT data", [](TestCase &t) { TestEnv env; - setRecvxDtxCompatLayout(); + configureProfile(env, "slus_201.84"); constexpr uint32_t kClientAddr = 0x0002F000u; constexpr uint32_t kDtxSid = 0x7D000000u; @@ -885,6 +884,7 @@ void register_ps2_sif_dma_tests() 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; @@ -895,11 +895,6 @@ void register_ps2_sif_dma_tests() constexpr uint32_t kSeSumOffset = 0x26u; constexpr uint32_t kBank = 1u; - PS2SoundDriverCompatLayout compat{}; - compat.primarySeCheckAddr = kPrimarySeCheckAddr; - compat.primaryMidiCheckAddr = kPrimaryMidiCheckAddr; - ps2_syscalls::setSoundDriverCompatLayout(compat); - constexpr uint32_t kClientAddr = 0x00023500u; constexpr uint32_t kRecvAddr = 0x00023600u; constexpr uint32_t kSid = 1u; @@ -950,6 +945,7 @@ void register_ps2_sif_dma_tests() 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; @@ -961,11 +957,6 @@ void register_ps2_sif_dma_tests() constexpr uint32_t kLiveBank = 0u; constexpr uint32_t kPendingBank = 1u; - PS2SoundDriverCompatLayout compat{}; - compat.primarySeCheckAddr = kPrimarySeCheckAddr; - compat.primaryMidiCheckAddr = kPrimaryMidiCheckAddr; - ps2_syscalls::setSoundDriverCompatLayout(compat); - constexpr uint32_t kClientAddr = 0x00023900u; constexpr uint32_t kRecvAddr = 0x00023A00u; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index bebafca..7a4371f 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -1,8 +1,8 @@ #include "MiniTest.h" #include "ps2_runtime.h" +#include "ps2_iop_transport.h" #include "ps2_syscalls.h" #include "ps2_stubs.h" -#include "runtime/ps2_iop.h" #include #include @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include using namespace ps2_syscalls; @@ -27,6 +29,16 @@ 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 @@ -78,12 +90,26 @@ namespace uint32_t end; uint32_t next; }; + + struct McDescParam + { + int32_t fd; + int32_t port; + int32_t slot; + int32_t size; + int32_t offset; + int32_t origin; + uint32_t buffer; + uint32_t param; + uint8_t data[16]; + }; #pragma pack(pop) static_assert(sizeof(SifRpcHeader) == 0x10u, "Unexpected SifRpcHeader size."); static_assert(sizeof(SifRpcClientData) == 0x28u, "Unexpected SifRpcClientData size."); static_assert(sizeof(SifRpcServerData) == 0x44u, "Unexpected SifRpcServerData size."); static_assert(sizeof(SifRpcDataQueue) == 0x18u, "Unexpected SifRpcDataQueue size."); + static_assert(sizeof(McDescParam) == 0x30u, "Unexpected McDescParam size."); struct TestEnv { @@ -94,27 +120,47 @@ namespace TestEnv() : rdram(PS2_RAM_SIZE, 0) { ps2_stubs::resetSifState(); - ps2_syscalls::resetSoundDriverRpcState(); - ps2_syscalls::clearSoundDriverCompatLayout(); - ps2_syscalls::clearDtxCompatLayout(); std::memset(&ctx, 0, sizeof(ctx)); } }; - void setRecvxDtxCompatLayout() + void configureProfile(TestEnv &env, std::string_view elfName) { - PS2DtxCompatLayout layout{}; - layout.rpcSid = 0x7D000000u; - layout.urpcObjBase = 0x01F18000u; - layout.urpcObjLimit = 0x01F1FF00u; - layout.urpcObjStride = 0x20u; - layout.urpcFnTableBase = 0x0034FED0u; - layout.urpcObjTableBase = 0x0034FFD0u; - layout.dispatcherFuncAddr = 0x002FABC0u; - ps2_syscalls::setDtxCompatLayout(layout); + 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, + uint32_t sendAddress, + uint32_t sendSize, + uint32_t receiveAddress, + uint32_t receiveSize) + { + ps2x::iop::RpcRequest request{}; + request.sid = sid; + request.function = function; + request.send = {sendAddress, sendSize}; + request.receive = {receiveAddress, receiveSize}; + return PS2IopTransport::handleRpc( + &env.runtime, env.rdram.data(), &env.ctx, std::move(request)); } std::atomic g_lotrSoundCallbackHits{0u}; + std::atomic g_recvxSoundCallbackHits{0u}; + std::atomic g_dtxDispatcherHits{0u}; + std::atomic g_dtxDispatcherRpcNum{0u}; + std::atomic g_dtxDispatcherSendBuf{0u}; + std::atomic g_dtxDispatcherSendSize{0u}; + + constexpr uint32_t K_DTX_DISPATCH_RESULT_ADDR = 0x0002D800u; + constexpr uint32_t K_DTX_DISPATCH_RESULT_MARKER = 0xD15CA7C1u; void lotrSoundEndCallbackShouldNotRun(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { @@ -124,6 +170,29 @@ namespace 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 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(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(value)); @@ -283,6 +352,7 @@ void register_ps2_sif_rpc_tests() 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 header(64u, 0); @@ -319,47 +389,146 @@ void register_ps2_sif_rpc_tests() std::memcpy(env.rdram.data() + kSendAddr, commands.data(), commands.size() * sizeof(uint32_t)); std::memset(env.rdram.data() + kRecvAddr, 0xCC, 0x180u); - env.runtime.iop().init(env.rdram.data()); - uint32_t resultPtr = 0u; - bool signalNowait = true; - const bool initHandled = env.runtime.iop().handleRPC(&env.runtime, - IOP_SID_FATAL_FRAME_SDRDRV, - 0u, - 0u, - 0u, - kRecvAddr, - 0x180u, - resultPtr, - signalNowait); - t.IsTrue(initHandled, "Fatal Frame SDRDRV init RPC should be handled"); + 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"); - signalNowait = true; - const bool handled = env.runtime.iop().handleRPC(&env.runtime, - IOP_SID_FATAL_FRAME_SDRDRV, - 1u, - kSendAddr, - static_cast(commands.size() * sizeof(uint32_t)), - kRecvAddr, - 0x180u, - resultPtr, - signalNowait); + const ps2x::iop::RpcResult result = + callIop(env, IOP_SID_FATAL_FRAME_SDRDRV, 1u, + kSendAddr, + static_cast(commands.size() * sizeof(uint32_t)), + kRecvAddr, 0x180u); PS2Runtime::setIoPaths(oldPaths); - t.IsTrue(handled, "Fatal Frame SDRDRV SID should be handled"); - t.Equals(resultPtr, kRecvAddr, "SDRDRV RPC should return recv buffer"); - t.IsFalse(signalNowait, "SDRDRV RPC should not request special nowait signaling"); + 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(0), "SDRDRV load status should report completed"); }); + tc.Run("MCSERV RPC init and get info report a formatted PS2 card", [](TestCase &t) + { + TestEnv env; + ScopedTempDir temp("mcserv_rpc"); + + 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 = 0x00034000u; + constexpr uint32_t kRecvAddr = 0x00035000u; + constexpr uint32_t kEndParamAddr = 0x00036000u; + + const ps2x::iop::RpcResult initResult = + callIop(env, IOP_SID_MCSERV, 0xFEu, + kSendAddr, sizeof(McDescParam), kRecvAddr, 12u); + t.IsTrue(initResult.handled, "MCSERV init RPC should be handled"); + t.Equals(initResult.resultAddress, kRecvAddr, "MCSERV init should return recv buffer"); + t.IsFalse(initResult.signalNowaitCompletion, "MCSERV init should not request special nowait signaling"); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 0, + "MCSERV init result should succeed"); + t.IsTrue(readGuestStruct(env.rdram.data(), kRecvAddr + 4u) >= 0x205u, + "MCSERV init should expose a supported mcserv version"); + t.IsTrue(readGuestStruct(env.rdram.data(), kRecvAddr + 8u) >= 0x206u, + "MCSERV init should expose a supported mcman version"); + + McDescParam getInfo{}; + getInfo.port = 0; + getInfo.slot = 0; + getInfo.size = 1; // formatted requested by XMCSERV libmc + getInfo.offset = 1; // free clusters requested by XMCSERV libmc + getInfo.origin = 1; // type requested by XMCSERV libmc + getInfo.param = kEndParamAddr; + writeGuestStruct(env.rdram.data(), kSendAddr, getInfo); + std::memset(env.rdram.data() + kRecvAddr, 0xCC, 12u); + std::memset(env.rdram.data() + kEndParamAddr, 0xCC, 192u); + + const ps2x::iop::RpcResult getInfoResult = + callIop(env, IOP_SID_MCSERV, 0x01u, + kSendAddr, sizeof(McDescParam), kRecvAddr, 4u); + + PS2Runtime::setIoPaths(oldPaths); + + t.IsTrue(getInfoResult.handled, "MCSERV get info RPC should be handled"); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr), 0, + "get info result should succeed"); + t.Equals(readGuestStruct(env.rdram.data(), kEndParamAddr + 0u), 2, + "get info should report PS2 card type"); + t.Equals(readGuestStruct(env.rdram.data(), kEndParamAddr + 4u), 0x2000, + "get info should report free clusters"); + t.Equals(readGuestStruct(env.rdram.data(), kEndParamAddr + 144u), 1, + "get info should report formatted card"); + }); + + tc.Run("DBCMAN version RPC returns the 3.20 compatibility response", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kDbcManSid = 0x80001300u; + constexpr uint32_t kCheckVersionRpc = 0x80001363u; + constexpr uint32_t kRecvAddr = 0x00035A00u; + constexpr uint32_t kDbcManVersion = 0x0320u; + + std::memset(env.rdram.data() + kRecvAddr, 0xCC, 16u); + const ps2x::iop::RpcResult result = + callIop(env, kDbcManSid, kCheckVersionRpc, + 0u, 0u, kRecvAddr, 16u); + + t.IsTrue(result.handled, "DBCMAN check-version RPC should be handled"); + t.Equals(result.resultAddress, kRecvAddr, "DBCMAN check-version RPC should return the receive buffer"); + t.IsFalse(result.signalNowaitCompletion, "DBCMAN check-version RPC should not request special nowait signaling"); + for (uint32_t index = 0u; index < 4u; ++index) + { + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + (index * 4u)), + kDbcManVersion, + "DBCMAN should repeat version 3.20 across the response words"); + } + }); + + tc.Run("LIBSD RPC routes through the IOP audio service", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kSetVoiceRpc = 0x8010u; + constexpr uint32_t kSendAddr = 0x00035B00u; + constexpr uint32_t kRecvAddr = 0x00035C00u; + + std::array command{}; + command[0] = 3u; // voice index + command[2] = 0x1000u; // neutral pitch + command[3] = 0x00120000u; // plausible sample address + writeGuestStruct(env.rdram.data(), kSendAddr, command); + std::memset(env.rdram.data() + kRecvAddr, 0xA5, 16u); + const ps2x::iop::RpcResult result = + callIop(env, IOP_SID_LIBSD, kSetVoiceRpc, + kSendAddr, static_cast(sizeof(command)), + kRecvAddr, 16u); + + t.IsTrue(result.handled, "LIBSD SID should be handled by the IOP audio service"); + t.Equals(result.resultAddress, kRecvAddr, "LIBSD should return the audio backend receive buffer"); + t.IsFalse(result.signalNowaitCompletion, "LIBSD should not request special nowait signaling"); + for (uint32_t index = 0u; index < 16u; ++index) + { + t.Equals(env.rdram[kRecvAddr + index], static_cast(0xA5), + "LIBSD should preserve the backend-owned response buffer"); + } + }); + 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 payload = {'a', 'b', 'c'}; @@ -379,23 +548,13 @@ void register_ps2_sif_rpc_tests() constexpr uint32_t kRecvAddr = 0x00037000u; constexpr uint32_t kDstAddr = 0x00038000u; - env.runtime.iop().init(env.rdram.data()); - auto callClFileRpc = [&](uint32_t rpcNum, uint32_t sendSize) { - uint32_t resultPtr = 0u; - bool signalNowait = true; - const bool handled = env.runtime.iop().handleRPC(&env.runtime, - IOP_SID_LOTR_CLFILE, - rpcNum, - kSendAddr, - sendSize, - kRecvAddr, - 0x40u, - resultPtr, - signalNowait); - t.IsTrue(handled, "LotR ClFile SID should be handled"); - t.Equals(resultPtr, kRecvAddr, "ClFile RPC should return recv buffer"); - t.IsFalse(signalNowait, "ClFile RPC should not request special nowait signaling"); + 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); @@ -481,6 +640,7 @@ void register_ps2_sif_rpc_tests() tc.Run("LotR sound RPC completes HLE callback without invoking guest loop", [](TestCase &t) { TestEnv env; + configureProfile(env, "SLUS_205.78"); constexpr uint32_t kClientAddr = 0x00039000u; constexpr uint32_t kSemaParamAddr = 0x00039100u; @@ -488,10 +648,6 @@ void register_ps2_sif_rpc_tests() constexpr uint32_t kRecvAddr = 0x0003C000u; constexpr uint32_t kEndFunc = 0x001FFD70u; - PS2SoundDriverCompatLayout layout{}; - layout.completionCallbacks = {kEndFunc, 0u, 0u, 0u}; - ps2_syscalls::setSoundDriverCompatLayout(layout); - env.runtime.registerFunction(kEndFunc, lotrSoundEndCallbackShouldNotRun); g_lotrSoundCallbackHits = 0u; @@ -537,6 +693,80 @@ void register_ps2_sif_rpc_tests() t.Equals(getRegS32(env.ctx, 2), 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(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(env.rdram.data(), kBusyFlagAddr), kBusyBeforeCompletion, + "completion-only callback should preserve the RECVX busy flag"); + setRegU32(env.ctx, 4, static_cast(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(env.rdram.data(), kBusyFlagAddr), 0u, + "designated RECVX callback should clear the guest busy flag"); + setRegU32(env.ctx, 4, static_cast(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("bind before register creates placeholder then remaps", [](TestCase &t) { TestEnv env; @@ -625,6 +855,7 @@ void register_ps2_sif_rpc_tests() 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; @@ -716,6 +947,7 @@ void register_ps2_sif_rpc_tests() 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; @@ -857,10 +1089,91 @@ 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(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); + callUrpc(); + + 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(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; - setRecvxDtxCompatLayout(); + configureProfile(env, "slus_201.84"); constexpr uint32_t kClientAddr = 0x0002B000u; constexpr uint32_t kDtxSid = 0x7D000000u;