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
This commit is contained in:
Ranieri
2026-07-15 01:44:06 -03:00
committed by GitHub
parent 905b4edfa8
commit ee149581aa
58 changed files with 9675 additions and 3972 deletions
+1
View File
@@ -76,6 +76,7 @@ if(PS2X_BUILD_RECOMP)
endif()
if(PS2X_BUILD_RUNTIME)
add_subdirectory("ps2xIOP")
add_subdirectory("ps2xRuntime")
endif()
+7 -3
View File
@@ -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`
+49
View File
@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
add_library(ps2x::iop ALIAS ps2_iop)
target_compile_definitions(ps2_iop PUBLIC
PS2X_IOP_ENABLE_PLUGINS=$<BOOL:${PS2X_IOP_ENABLE_PLUGINS}>
)
if(PS2X_IOP_ENABLE_PLUGINS AND UNIX AND NOT APPLE)
target_link_libraries(ps2_iop PRIVATE ${CMAKE_DL_LIBS})
endif()
install(TARGETS ps2_iop
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/
DESTINATION include
)
+220
View File
@@ -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 <ps2x/iop/plugin_api.h>
#include <stdlib.h>
#define STRING_VIEW(literal) { (literal), sizeof(literal) - 1u }
enum
{
MY_SID = 0x6D795349u,
MY_FUNCTION = 1u,
};
struct my_state
{
const ps2x_iop_host_api_v1 *host;
};
static void *my_create(const ps2x_iop_host_api_v1 *host,
const ps2x_iop_game_identity_v1 *identity)
{
struct my_state *state;
(void)identity;
if (!host ||
host->abi_version != PS2X_IOP_ABI_VERSION_V1 ||
host->struct_size < sizeof(*host))
{
return NULL;
}
state = (struct my_state *)calloc(1u, sizeof(*state));
if (state)
{
state->host = host;
}
return state;
}
static void my_destroy(void *instance)
{
free(instance);
}
static int32_t my_reset(void *instance)
{
return instance ? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
static int32_t my_handle_rpc(void *instance,
const ps2x_iop_rpc_request_v1 *request,
ps2x_iop_rpc_result_v1 *result)
{
struct my_state *state = (struct my_state *)instance;
const uint32_t value = 1u;
int32_t status;
if (!state || !request || !result ||
request->struct_size < sizeof(*request) ||
result->struct_size < sizeof(*result))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
result->handled = 0u;
result->result_address = 0u;
result->signal_nowait_completion = 0u;
result->signal_completion = 0u;
result->callback_policy = PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1;
result->server_dispatch_policy =
PS2X_IOP_SERVER_DISPATCH_RUNTIME_DEFAULT_V1;
if (request->sid != MY_SID || request->function != MY_FUNCTION)
{
return PS2X_IOP_STATUS_OK_V1;
}
if (request->receive.size < sizeof(value))
{
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
}
if (!state->host->write_guest)
{
return PS2X_IOP_STATUS_UNSUPPORTED_V1;
}
status = state->host->write_guest(state->host->userdata,
request->receive.address,
&value,
sizeof(value));
if (status != PS2X_IOP_STATUS_OK_V1)
{
return status;
}
result->handled = 1u;
result->result_address = request->receive.address;
result->signal_nowait_completion = 1u;
result->server_dispatch_policy = PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1;
return PS2X_IOP_STATUS_OK_V1;
}
static const uint32_t my_sids[] = { MY_SID };
static const ps2x_iop_profile_api_v1 my_profiles[] = {
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
STRING_VIEW("my-game-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
STRING_VIEW("SLUS_000.00"),
0u,
0u,
},
1u,
my_sids,
my_create,
my_destroy,
my_reset,
NULL,
my_handle_rpc,
NULL,
NULL,
NULL,
},
};
PS2X_IOP_PLUGIN_EXPORT int32_t
ps2x_iop_query_v1(uint32_t host_abi_version,
ps2x_iop_plugin_api_v1 *out)
{
static const ps2x_iop_plugin_api_v1 plugin = {
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_plugin_api_v1),
STRING_VIEW("my-iop-plugin"),
STRING_VIEW("1.0.0"),
1u,
my_profiles,
};
if (host_abi_version != PS2X_IOP_ABI_VERSION_V1)
{
return PS2X_IOP_STATUS_UNSUPPORTED_V1;
}
if (!out)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
if (out->struct_size < sizeof(*out))
{
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
}
*out = plugin;
return PS2X_IOP_STATUS_OK_V1;
}
```
### Standalone CMake target
The plugin consumes only the public ABI header; it does not link to `ps2xRuntime` or `ps2_iop`.
```cmake
cmake_minimum_required(VERSION 3.21)
project(my_iop_plugin LANGUAGES C)
set(PS2X_IOP_INCLUDE_DIR "" CACHE PATH
"Directory containing ps2x/iop/plugin_api.h"
)
if(NOT EXISTS "${PS2X_IOP_INCLUDE_DIR}/ps2x/iop/plugin_api.h")
message(FATAL_ERROR
"Set PS2X_IOP_INCLUDE_DIR to PS2Recomp/ps2xIOP/include"
)
endif()
add_library(my_iop_plugin MODULE my_iop_plugin.c)
target_include_directories(my_iop_plugin PRIVATE
"${PS2X_IOP_INCLUDE_DIR}"
)
set_target_properties(my_iop_plugin PROPERTIES
PREFIX ""
C_STANDARD 11
C_STANDARD_REQUIRED YES
C_EXTENSIONS NO
)
install(TARGETS my_iop_plugin
RUNTIME DESTINATION .
LIBRARY DESTINATION .
)
```
On Windows:
```powershell
cmake -S . -B build -A x64 `
-DPS2X_IOP_INCLUDE_DIR="C:/path/to/PS2Recomp/ps2xIOP/include"
cmake --build build --config Release
cmake --install build --config Release `
--prefix "C:/path/to/ps2EntryRunner/iop_plugins"
```
On Linux:
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DPS2X_IOP_INCLUDE_DIR=/path/to/PS2Recomp/ps2xIOP/include
cmake --build build -j
cmake --install build --prefix /path/to/ps2EntryRunner/iop_plugins
```
Build or install the resulting `.dll`/`.so` before the runtime calls
`initialize()`. If it is not installed directly, copy it into the executable's
`iop_plugins/` directory.
+219
View File
@@ -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).
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include "ps2x/iop/iop_types.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
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<uint32_t, 5> 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;
};
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include "ps2x/iop/iop_host.h"
#include "ps2x/iop/iop_types.h"
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
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<std::filesystem::path> 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<Impl> m_impl;
};
}
+142
View File
@@ -0,0 +1,142 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
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<uint32_t> sids;
bool profileSpecific = false;
std::vector<DebugMetric> metrics;
};
struct DebugSnapshot
{
std::string activeProfile;
std::string activeProvider;
std::vector<DebugService> services;
std::vector<std::string> diagnostics;
};
}
+307
View File
@@ -0,0 +1,307 @@
#ifndef PS2X_IOP_PLUGIN_API_H
#define PS2X_IOP_PLUGIN_API_H
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32)
#define PS2X_IOP_PLUGIN_EXPORT __declspec(dllexport)
#elif defined(__GNUC__) || defined(__clang__)
#define PS2X_IOP_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
#define PS2X_IOP_PLUGIN_EXPORT
#endif
#ifdef __cplusplus
extern "C"
{
#endif
#define PS2X_IOP_ABI_VERSION_V1 1u
#define PS2X_IOP_QUERY_SYMBOL_V1 "ps2x_iop_query_v1"
enum ps2x_iop_status_v1
{
PS2X_IOP_STATUS_OK_V1 = 0,
PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1 = 1,
PS2X_IOP_STATUS_INVALID_ARGUMENT_V1 = -1,
PS2X_IOP_STATUS_UNSUPPORTED_V1 = -2,
PS2X_IOP_STATUS_FAILED_V1 = -3,
};
enum ps2x_iop_rpc_abi_v1
{
PS2X_IOP_RPC_ABI_DEFAULT_V1 = 0,
PS2X_IOP_RPC_ABI_REGISTERS_V1 = 1,
PS2X_IOP_RPC_ABI_STACK_V1 = 2,
};
enum ps2x_iop_callback_policy_v1
{
PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1 = 0,
PS2X_IOP_CALLBACK_SUPPRESS_V1 = 1,
};
enum ps2x_iop_server_dispatch_policy_v1
{
PS2X_IOP_SERVER_DISPATCH_RUNTIME_DEFAULT_V1 = 0,
PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1 = 1,
};
enum ps2x_iop_transfer_kind_v1
{
PS2X_IOP_TRANSFER_SET_DMA_V1 = 0,
PS2X_IOP_TRANSFER_GET_OTHER_DATA_V1 = 1,
};
enum ps2x_iop_transfer_phase_v1
{
PS2X_IOP_TRANSFER_BEFORE_COPY_V1 = 0,
PS2X_IOP_TRANSFER_AFTER_COPY_V1 = 1,
};
enum ps2x_iop_host_path_kind_v1
{
PS2X_IOP_PATH_ELF_DIRECTORY_V1 = 0,
PS2X_IOP_PATH_CD_ROOT_V1 = 1,
PS2X_IOP_PATH_CD_IMAGE_V1 = 2,
PS2X_IOP_PATH_HOST_ROOT_V1 = 3,
PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1 = 4,
};
enum ps2x_iop_handle_kind_v1
{
PS2X_IOP_HANDLE_RPC_SERVER_V1 = 0,
PS2X_IOP_HANDLE_RPC_PACKET_V1 = 1,
};
enum ps2x_iop_log_level_v1
{
PS2X_IOP_LOG_DEBUG_V1 = 0,
PS2X_IOP_LOG_INFO_V1 = 1,
PS2X_IOP_LOG_WARNING_V1 = 2,
PS2X_IOP_LOG_ERROR_V1 = 3,
};
enum ps2x_iop_memory_card_operation_v1
{
PS2X_IOP_MC_INIT_V1 = 0,
PS2X_IOP_MC_GET_INFO_V1 = 1,
PS2X_IOP_MC_OPEN_V1 = 2,
PS2X_IOP_MC_CLOSE_V1 = 3,
PS2X_IOP_MC_SEEK_V1 = 4,
PS2X_IOP_MC_READ_V1 = 5,
PS2X_IOP_MC_WRITE_V1 = 6,
PS2X_IOP_MC_FLUSH_V1 = 7,
PS2X_IOP_MC_CHDIR_V1 = 8,
PS2X_IOP_MC_GET_DIR_V1 = 9,
PS2X_IOP_MC_SET_FILE_INFO_V1 = 10,
PS2X_IOP_MC_DELETE_V1 = 11,
PS2X_IOP_MC_FORMAT_V1 = 12,
PS2X_IOP_MC_UNFORMAT_V1 = 13,
PS2X_IOP_MC_MKDIR_V1 = 14,
};
typedef struct ps2x_iop_string_view_v1
{
const char *data;
size_t size;
} ps2x_iop_string_view_v1;
typedef struct ps2x_iop_guest_buffer_v1
{
uint32_t address;
uint32_t size;
} ps2x_iop_guest_buffer_v1;
typedef struct ps2x_iop_game_identity_v1
{
uint32_t struct_size;
ps2x_iop_string_view_v1 elf_name;
uint32_t entry_point;
uint32_t crc32;
} ps2x_iop_game_identity_v1;
typedef struct ps2x_iop_game_matcher_v1
{
uint32_t struct_size;
ps2x_iop_string_view_v1 elf_name;
uint32_t entry_point;
uint32_t crc32;
} ps2x_iop_game_matcher_v1;
typedef struct ps2x_iop_rpc_candidate_v1
{
uint32_t send_size;
uint32_t receive_address;
uint32_t receive_size;
uint32_t end_function;
uint32_t end_parameter;
uint32_t plausible;
} ps2x_iop_rpc_candidate_v1;
typedef struct ps2x_iop_rpc_abi_request_v1
{
uint32_t struct_size;
uint32_t bound_sid;
uint32_t function;
ps2x_iop_rpc_candidate_v1 registers;
ps2x_iop_rpc_candidate_v1 stack;
} ps2x_iop_rpc_abi_request_v1;
typedef struct ps2x_iop_rpc_request_v1
{
uint32_t struct_size;
uint64_t call_token;
uint32_t client_address;
uint32_t server_address;
uint32_t server_function;
uint32_t server_buffer;
uint32_t sid;
uint32_t function;
uint32_t mode;
ps2x_iop_guest_buffer_v1 send;
ps2x_iop_guest_buffer_v1 receive;
uint32_t end_function;
uint32_t end_parameter;
} ps2x_iop_rpc_request_v1;
typedef struct ps2x_iop_rpc_result_v1
{
uint32_t struct_size;
uint32_t handled;
uint32_t result_address;
uint32_t signal_nowait_completion;
uint32_t signal_completion;
uint32_t callback_policy;
uint32_t server_dispatch_policy;
} ps2x_iop_rpc_result_v1;
typedef struct ps2x_iop_sif_transfer_v1
{
uint32_t struct_size;
uint32_t kind;
uint32_t phase;
uint32_t source_address;
uint32_t destination_address;
uint32_t size;
} ps2x_iop_sif_transfer_v1;
typedef struct ps2x_iop_debug_metric_v1
{
uint32_t struct_size;
ps2x_iop_string_view_v1 name;
uint64_t value;
uint32_t hexadecimal;
} ps2x_iop_debug_metric_v1;
typedef struct ps2x_iop_memory_card_request_v1
{
uint32_t struct_size;
uint32_t operation;
uint32_t arguments[5];
} ps2x_iop_memory_card_request_v1;
typedef struct ps2x_iop_host_api_v1
{
uint32_t abi_version;
uint32_t struct_size;
void *userdata;
int32_t (*read_guest)(void *userdata, uint32_t address, void *destination, size_t size);
int32_t (*write_guest)(void *userdata, uint32_t address, const void *source, size_t size);
int32_t (*zero_guest)(void *userdata, uint32_t address, size_t size);
int32_t (*normalize_guest_address)(void *userdata, uint32_t address, uint32_t *normalized);
uint32_t (*allocate_iop_handle)(void *userdata, uint32_t kind);
uint32_t (*allocate_guest)(void *userdata, uint32_t size, uint32_t alignment);
void (*free_guest)(void *userdata, uint32_t address);
int32_t (*audio_command)(void *userdata,
uint32_t sid,
uint32_t function,
ps2x_iop_guest_buffer_v1 send,
ps2x_iop_guest_buffer_v1 receive);
int32_t (*get_host_path)(void *userdata,
uint32_t kind,
char *destination,
size_t capacity,
size_t *required_size);
int32_t (*translate_guest_path)(void *userdata,
ps2x_iop_string_view_v1 path,
char *destination,
size_t capacity,
size_t *required_size);
uint64_t (*open_host_file)(void *userdata, ps2x_iop_string_view_v1 path);
int32_t (*host_file_size)(void *userdata, uint64_t handle, uint64_t *size);
int32_t (*read_host_file)(void *userdata,
uint64_t handle,
uint64_t offset,
void *destination,
size_t size,
size_t *bytes_read);
void (*close_host_file)(void *userdata, uint64_t handle);
int32_t (*memory_card)(void *userdata, const ps2x_iop_memory_card_request_v1 *request, int32_t *result);
int32_t (*has_guest_function)(void *userdata, uint32_t address);
int32_t (*invoke_guest_function)(void *userdata,
uint64_t call_token,
uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t *result_address);
void (*log)(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message);
} ps2x_iop_host_api_v1;
typedef void *(*ps2x_iop_profile_create_v1)(const ps2x_iop_host_api_v1 *host,
const ps2x_iop_game_identity_v1 *identity);
typedef void (*ps2x_iop_profile_destroy_v1)(void *instance);
typedef int32_t (*ps2x_iop_profile_reset_v1)(void *instance);
typedef uint32_t (*ps2x_iop_profile_select_rpc_abi_v1)(void *instance, const ps2x_iop_rpc_abi_request_v1 *request);
typedef int32_t (*ps2x_iop_profile_handle_rpc_v1)(void *instance,
const ps2x_iop_rpc_request_v1 *request,
ps2x_iop_rpc_result_v1 *result);
typedef int32_t (*ps2x_iop_profile_on_sif_transfer_v1)(void *instance, const ps2x_iop_sif_transfer_v1 *transfer);
typedef size_t (*ps2x_iop_profile_debug_metric_count_v1)(void *instance);
typedef int32_t (*ps2x_iop_profile_debug_metric_v1)(void *instance, size_t index, ps2x_iop_debug_metric_v1 *metric);
typedef struct ps2x_iop_profile_api_v1
{
uint32_t abi_version;
uint32_t struct_size;
ps2x_iop_string_view_v1 id;
ps2x_iop_game_matcher_v1 matcher;
size_t sid_count;
const uint32_t *sids;
ps2x_iop_profile_create_v1 create;
ps2x_iop_profile_destroy_v1 destroy;
ps2x_iop_profile_reset_v1 reset;
ps2x_iop_profile_select_rpc_abi_v1 select_rpc_abi;
ps2x_iop_profile_handle_rpc_v1 handle_rpc;
ps2x_iop_profile_on_sif_transfer_v1 on_sif_transfer;
ps2x_iop_profile_debug_metric_count_v1 debug_metric_count;
ps2x_iop_profile_debug_metric_v1 debug_metric;
} ps2x_iop_profile_api_v1;
typedef struct ps2x_iop_plugin_api_v1
{
uint32_t abi_version;
uint32_t struct_size;
ps2x_iop_string_view_v1 name;
ps2x_iop_string_view_v1 version;
size_t profile_count;
const ps2x_iop_profile_api_v1 *profiles;
} ps2x_iop_plugin_api_v1;
typedef int32_t (*ps2x_iop_query_v1_fn)(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api);
PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api);
#ifdef __cplusplus
}
#endif
#endif
+149
View File
@@ -0,0 +1,149 @@
#include "iop_service.h"
#include "module_factories.h"
#include <utility>
namespace ps2x::iop::detail
{
namespace
{
TsnddrvBindings recvxTsnddrvBindings()
{
return {
.serviceName = "TSNDDRV",
.protocol = TsnddrvProtocolVariant::SndQueueV1,
.arena = {
.base = 0x00120000u,
.limit = 0x00200000u,
.statusAlignment = 0x100u,
.tableAlignment = 0x100u,
.storageAlignment = 0x1000u,
.hdBytes = 0x4000u,
.sqBytes = 0x18000u,
.dataBytes = 0x40000u,
},
.checksumCandidates = {
{0x01E0EF10u, 0x01E0EF20u},
{0x01E1EF10u, 0x01E1EF20u},
},
.busyFlagAddress = 0x01E212C8u,
.completionRules = {
{0x002EAC20u, true, true, false},
{0x002EAC30u, true, true, true},
{0x002FAC20u, true, true, false},
{0x002FAC30u, true, true, true},
},
};
}
CriDtxBindings recvxCriDtxBindings()
{
return {
.serviceName = "CRI DTX",
.sid = 0x7D000000u,
.urpcObjectBase = 0x01F18000u,
.urpcObjectLimit = 0x01F1FF00u,
.urpcObjectStride = 0x20u,
.urpcFunctionTableBase = 0x0033FED0u,
.urpcObjectTableBase = 0x0033FFD0u,
.dispatcherFunctionAddress = 0x002FABC0u,
.rpcServerPoolBase = 0x01F10000u,
.rpcServerStride = 0x80u,
};
}
ClFileBindings lotrClFileBindings()
{
return {
.serviceName = "CLFILE",
.sid = 0x0000FF01u,
.rpc = {},
};
}
SoundUpdateStubBindings lotrSoundBindings()
{
return {
.serviceName = "SOUND update compatibility stub",
.sid = 0x00012345u,
.activeStreamCountOffset = 0u,
.responseCounterOffset = 4u,
.zeroReceiveBuffer = true,
.signalNowaitCompletion = true,
.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<ProfileDefinition> createBuiltinProfiles()
{
std::vector<ProfileDefinition> profiles;
profiles.push_back({
"recvx-us",
"builtin",
{.elfName = "slus_201.84"},
[](IopHost &host, const GameIdentity &)
{
ServiceList services;
services.emplace_back(createTsnddrvService(host, recvxTsnddrvBindings()));
services.emplace_back(createCriDtxService(host, recvxCriDtxBindings()));
return services;
},
});
profiles.push_back({
"lotr-two-towers-us",
"builtin",
{.elfName = "SLUS_205.78"},
[](IopHost &host, const GameIdentity &)
{
ServiceList services;
services.emplace_back(createClFileService(host, lotrClFileBindings()));
services.emplace_back(createSoundUpdateStubService(host, lotrSoundBindings()));
return services;
},
});
profiles.push_back({
"fatal-frame-us",
"builtin",
{.elfName = "SLUS_203.88"},
[](IopHost &host, const GameIdentity &)
{
ServiceList services;
services.emplace_back(createSdrdrvService(host, fatalFrameSdrdrvBindings()));
return services;
},
});
return profiles;
}
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include "ps2x/iop/iop_host.h"
#include "ps2x/iop/iop_types.h"
#include <functional>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace ps2x::iop::detail
{
class IopService
{
public:
virtual ~IopService() = default;
[[nodiscard]] virtual std::string_view name() const = 0;
[[nodiscard]] virtual std::span<const uint32_t> 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<DebugMetric> &metrics) const
{
(void)metrics;
}
};
using ServiceList = std::vector<std::unique_ptr<IopService>>;
using ProfileFactory = std::function<ServiceList(IopHost &, const GameIdentity &)>;
struct ProfileDefinition
{
std::string id;
std::string provider = "builtin";
GameMatcher matcher;
ProfileFactory factory;
};
ServiceList createCoreServices(IopHost &host);
std::vector<ProfileDefinition> createBuiltinProfiles();
}
+339
View File
@@ -0,0 +1,339 @@
#include "ps2x/iop/iop_subsystem.h"
#include "iop_service.h"
#include "plugin_loader.h"
#include <algorithm>
#include <cctype>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
#include <utility>
namespace ps2x::iop
{
namespace
{
bool equalsIgnoreCaseAscii(std::string_view lhs, std::string_view rhs)
{
if (lhs.size() != rhs.size())
{
return false;
}
for (size_t i = 0; i < lhs.size(); ++i)
{
const auto left = static_cast<unsigned char>(lhs[i]);
const auto right = static_cast<unsigned char>(rhs[i]);
if (std::tolower(left) != std::tolower(right))
{
return false;
}
}
return true;
}
int matchSpecificity(const GameMatcher &matcher, const GameIdentity &identity)
{
int specificity = 0;
if (!matcher.elfName.empty())
{
if (!equalsIgnoreCaseAscii(matcher.elfName, identity.elfName))
{
return -1;
}
++specificity;
}
if (matcher.entryPoint != 0)
{
if (matcher.entryPoint != identity.entryPoint)
{
return -1;
}
++specificity;
}
if (matcher.crc32 != 0)
{
if (matcher.crc32 != identity.crc32)
{
return -1;
}
++specificity;
}
return specificity;
}
}
class IopSubsystem::Impl
{
public:
explicit Impl(IopHost &hostRef)
: host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles())
{
rebuildRoutes();
}
void rebuildRoutes()
{
routes.clear();
auto addLayer = [&](detail::ServiceList &services, bool profileSpecific) -> bool
{
std::unordered_map<uint32_t, detail::IopService *> 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<detail::ProfileDefinition> profiles;
std::unordered_map<uint32_t, detail::IopService *> routes;
std::vector<std::filesystem::path> pluginSearchPaths;
std::vector<std::string> diagnostics;
std::string activeProfile;
std::string activeProvider;
std::string lastError;
bool routesValid = true;
};
IopSubsystem::IopSubsystem(IopHost &host)
: m_impl(std::make_unique<Impl>(host))
{
}
IopSubsystem::~IopSubsystem() = default;
IopSubsystem::IopSubsystem(IopSubsystem &&) noexcept = default;
IopSubsystem &IopSubsystem::operator=(IopSubsystem &&) noexcept = default;
void IopSubsystem::setPluginSearchPaths(std::vector<std::filesystem::path> paths)
{
m_impl->pluginSearchPaths = std::move(paths);
}
bool IopSubsystem::loadPlugins(std::string *error)
{
return m_impl->pluginCatalog.load(m_impl->pluginSearchPaths, m_impl->profiles, m_impl->diagnostics, error);
}
bool IopSubsystem::configure(const GameIdentity &identity, std::string *error)
{
m_impl->profileServices.clear();
m_impl->activeProfile.clear();
m_impl->activeProvider.clear();
m_impl->lastError.clear();
const detail::ProfileDefinition *selected = nullptr;
const detail::ProfileDefinition *selectedTie = nullptr;
int selectedSpecificity = -1;
for (const auto &profile : m_impl->profiles)
{
const int specificity = matchSpecificity(profile.matcher, identity);
if (specificity < 0)
{
continue;
}
if (specificity > selectedSpecificity)
{
selected = &profile;
selectedTie = nullptr;
selectedSpecificity = specificity;
continue;
}
if (specificity == selectedSpecificity && selected)
{
selectedTie = &profile;
}
}
if (selected && selectedTie)
{
m_impl->lastError = "ambiguous IOP profiles '" + selected->provider + ":" +
selected->id + "' and '" + selectedTie->provider + ":" +
selectedTie->id + "'";
if (error)
{
*error = m_impl->lastError;
}
m_impl->rebuildRoutes();
return false;
}
if (selected)
{
try
{
m_impl->profileServices = selected->factory(m_impl->host, identity);
m_impl->activeProfile = selected->id;
m_impl->activeProvider = selected->provider;
}
catch (const std::exception &exception)
{
m_impl->lastError = "failed to create IOP profile '" + selected->id + "': " + exception.what();
if (error)
{
*error = m_impl->lastError;
}
m_impl->rebuildRoutes();
return false;
}
catch (...)
{
m_impl->lastError = "failed to create IOP profile '" + selected->id + "': unknown plugin exception";
if (error)
{
*error = m_impl->lastError;
}
m_impl->rebuildRoutes();
return false;
}
}
m_impl->rebuildRoutes();
if (!m_impl->routesValid)
{
const std::string routeError = m_impl->lastError;
m_impl->profileServices.clear();
m_impl->activeProfile.clear();
m_impl->activeProvider.clear();
m_impl->rebuildRoutes();
m_impl->lastError = routeError;
if (error)
{
*error = m_impl->lastError;
}
return false;
}
reset();
return true;
}
void IopSubsystem::reset()
{
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;
}
}
+156
View File
@@ -0,0 +1,156 @@
#pragma once
#include "iop_service.h"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace ps2x::iop::detail
{
struct CriDtxBindings
{
std::string serviceName;
uint32_t sid = 0u;
uint32_t urpcObjectBase = 0u;
uint32_t urpcObjectLimit = 0u;
uint32_t urpcObjectStride = 0u;
uint32_t urpcFunctionTableBase = 0u;
uint32_t urpcObjectTableBase = 0u;
uint32_t dispatcherFunctionAddress = 0u;
uint32_t rpcServerPoolBase = 0u;
uint32_t rpcServerStride = 0u;
};
enum class TsnddrvProtocolVariant
{
SndQueueV1,
};
struct TsnddrvGuestArena
{
uint32_t base = 0u;
uint32_t limit = 0u;
uint32_t statusAlignment = 0u;
uint32_t tableAlignment = 0u;
uint32_t storageAlignment = 0u;
uint32_t hdBytes = 0u;
uint32_t sqBytes = 0u;
uint32_t dataBytes = 0u;
};
struct TsnddrvChecksumTables
{
uint32_t seAddress = 0u;
uint32_t midiAddress = 0u;
};
struct TsnddrvCompletionRule
{
uint32_t eeFunction = 0u;
bool suppressGuestCallback = false;
bool signalCompletion = false;
bool clearBusy = false;
};
struct TsnddrvBindings
{
std::string serviceName;
TsnddrvProtocolVariant protocol = TsnddrvProtocolVariant::SndQueueV1;
TsnddrvGuestArena arena;
std::vector<TsnddrvChecksumTables> checksumCandidates;
uint32_t busyFlagAddress = 0u;
std::vector<TsnddrvCompletionRule> completionRules;
};
struct ClFileRpcLayout
{
uint32_t directLoadFunction = 0x01u;
uint32_t getStatusFunction = 0x03u;
uint32_t initializeFunction = 0x04u;
uint32_t waitFunction = 0x05u;
uint32_t getSizeFunction = 0x06u;
uint32_t openFunction = 0x08u;
uint32_t closeFunction = 0x09u;
uint32_t readFunction = 0x0Au;
uint32_t secondaryWaitFunction = 0x15u;
uint32_t setRootFunction = 0x16u;
uint32_t pathBytes = 0x100u;
uint32_t directLoadSizeOffset = 0x100u;
uint32_t directLoadDestinationOffset = 0x104u;
uint32_t responseStatusOffset = 0u;
uint32_t responseValueOffset = 4u;
uint32_t responseClearBytes = 0x40u;
uint32_t maximumReadBytes = 0x2000u;
uint32_t loadResultQueued = 5u;
uint32_t loadStatusFailed = 3u;
uint32_t loadStatusComplete = 7u;
uint32_t invalidHandleStatus = 9u;
uint32_t firstLoadHandle = 0x00010000u;
bool acknowledgeUnknownFunctions = true;
};
struct ClFileBindings
{
std::string serviceName;
uint32_t sid = 0u;
ClFileRpcLayout rpc;
};
// TODO This is for the lord of the rings better name for that one
struct SoundUpdateStubBindings
{
std::string serviceName;
uint32_t sid = 0u;
uint32_t activeStreamCountOffset = 0u;
uint32_t responseCounterOffset = 0u;
bool zeroReceiveBuffer = true;
bool signalNowaitCompletion = false;
std::vector<uint32_t> suppressedCompletionCallbacks;
};
struct SdrdrvBindings
{
std::string serviceName;
uint32_t sid = 0u;
uint32_t imageHeaderAddress = 0u;
uint32_t sectorSize = 0u;
uint32_t statusOffset = 0u;
uint32_t statusStride = 0u;
uint32_t statusSlotMask = 0u;
uint8_t completeValue = 0u;
uint32_t initFunction = 0u;
uint32_t submitFunction = 1u;
uint32_t shutdownFunction = 2u;
uint32_t headerCommand = 0x0Cu;
uint32_t loadCommand = 0x0Eu;
uint32_t commandBytes = 32u;
uint32_t maxCommands = 32u;
uint32_t lbnWord = 2u;
uint32_t byteCountWord = 3u;
uint32_t destinationWord = 4u;
uint32_t destinationKindWord = 5u;
uint32_t loadIdWord = 6u;
uint32_t eeDestinationKind = 0u;
bool fallbackBodyToCdImage = true;
bool clearReceiveBeforeDispatch = true;
bool completeFailedLoads = true;
bool pretendNonEeLoadsComplete = true;
uint32_t headerWarningLimit = 4u;
uint32_t bodyWarningLimit = 8u;
std::string imageHeaderLowerName;
std::string imageHeaderUpperName;
std::string imageBodyLowerName;
std::string imageBodyUpperName;
};
std::unique_ptr<IopService> createDbcmanService(IopHost &host);
std::unique_ptr<IopService> createLibSdService(IopHost &host);
std::unique_ptr<IopService> createMcservService(IopHost &host);
std::unique_ptr<IopService> createTsnddrvService(IopHost &host, TsnddrvBindings bindings);
std::unique_ptr<IopService> createCriDtxService(IopHost &host, CriDtxBindings bindings);
std::unique_ptr<IopService> createClFileService(IopHost &host, ClFileBindings bindings);
std::unique_ptr<IopService> createSoundUpdateStubService(IopHost &host, SoundUpdateStubBindings bindings);
std::unique_ptr<IopService> createSdrdrvService(IopHost &host, SdrdrvBindings bindings);
}
+635
View File
@@ -0,0 +1,635 @@
#include "module_factories.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <mutex>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace ps2x::iop::detail
{
namespace
{
class ClFileService final : public IopService
{
public:
ClFileService(IopHost &host, ClFileBindings bindings)
: m_host(host),
m_bindings(std::move(bindings)),
m_sids{m_bindings.sid},
m_nextLoadHandle(m_bindings.rpc.firstLoadHandle)
{
}
~ClFileService() override
{
reset();
}
[[nodiscard]] std::string_view name() const override
{
return m_bindings.serviceName;
}
[[nodiscard]] std::span<const uint32_t> sids() const override
{
return m_sids;
}
void reset() override
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto &[handle, entry] : m_fileHandles)
{
(void)handle;
if (entry.handle != 0u)
{
m_host.closeHostFile(entry.handle);
entry.handle = 0u;
}
}
m_fileHandles.clear();
m_loads.clear();
m_root.clear();
m_nextFileHandle = 1u;
m_nextLoadHandle = m_bindings.rpc.firstLoadHandle;
}
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
{
RpcResult result;
if (request.sid != m_bindings.sid)
{
return result;
}
const Operation operation = decodeFunction(request.function);
if (operation == Operation::Unknown &&
!m_bindings.rpc.acknowledgeUnknownFunctions)
{
return result;
}
result.handled = true;
result.resultAddress = request.receive.address;
if (request.receive.address != 0u && request.receive.size != 0u)
{
(void)m_host.zeroGuest(request.receive.address,
std::min(request.receive.size,
m_bindings.rpc.responseClearBytes));
}
const auto writeRpcResult = [&](int32_t status, uint32_t value)
{
writeResult(request.receive, status, value);
};
switch (operation)
{
case Operation::DirectLoad:
{
const uint32_t stringBytes = request.send.size != 0u
? std::min(request.send.size,
m_bindings.rpc.pathBytes)
: m_bindings.rpc.pathBytes;
const std::string guestPath = readGuestString(request.send.address, stringBytes);
uint32_t requestedBytes = 0u;
uint32_t destinationAddress = 0u;
(void)readGuestU32(request.send.address + m_bindings.rpc.directLoadSizeOffset,
requestedBytes);
(void)readGuestU32(request.send.address + m_bindings.rpc.directLoadDestinationOffset,
destinationAddress);
uint32_t status = m_bindings.rpc.loadStatusFailed;
uint32_t fileSize = 0u;
const std::string hostPath = resolvePath(guestPath);
if (!hostPath.empty())
{
const uint64_t file = m_host.openHostFile(hostPath);
uint64_t hostFileSize = 0u;
if (file != 0u && m_host.hostFileSize(file, hostFileSize))
{
fileSize = static_cast<uint32_t>(
std::min<uint64_t>(hostFileSize, 0xFFFFFFFFull));
const uint64_t maxRequestedBytes = requestedBytes != 0u
? requestedBytes
: hostFileSize;
const uint64_t bytesToCopy = std::min(hostFileSize, maxRequestedBytes);
status = m_bindings.rpc.loadStatusComplete;
if (destinationAddress != 0u && bytesToCopy != 0u)
{
if (!copyFileToGuest(file, destinationAddress, bytesToCopy))
{
status = m_bindings.rpc.loadStatusFailed;
}
}
}
if (file != 0u)
{
m_host.closeHostFile(file);
}
}
uint32_t loadHandle = 0u;
{
std::lock_guard<std::mutex> lock(m_mutex);
loadHandle = allocateLoadLocked(status, fileSize);
}
writeRpcResult(static_cast<int32_t>(m_bindings.rpc.loadResultQueued), loadHandle);
return result;
}
case Operation::Initialize:
writeRpcResult(0, 1u);
return result;
case Operation::Wait:
case Operation::SecondaryWait:
writeRpcResult(0, 0u);
return result;
case Operation::SetRoot:
{
const std::string root = readGuestString(request.send.address,
request.send.size != 0u
? request.send.size
: m_bindings.rpc.pathBytes);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_root = root;
}
writeRpcResult(0, 1u);
return result;
}
case Operation::Open:
{
const std::string guestPath = readGuestString(request.send.address,
request.send.size != 0u
? request.send.size
: m_bindings.rpc.pathBytes);
const std::string hostPath = resolvePath(guestPath);
if (hostPath.empty())
{
writeRpcResult(-1, 0u);
return result;
}
const uint64_t file = m_host.openHostFile(hostPath);
if (file == 0u)
{
writeRpcResult(-1, 0u);
return result;
}
uint64_t hostFileSize = 0u;
if (!m_host.hostFileSize(file, hostFileSize))
{
m_host.closeHostFile(file);
writeRpcResult(-1, 0u);
return result;
}
const uint32_t fileSize = static_cast<uint32_t>(
std::min<uint64_t>(hostFileSize, 0x7FFFFFFFull));
uint32_t handle = 0u;
{
std::lock_guard<std::mutex> lock(m_mutex);
handle = allocateFileHandleLocked(file, fileSize);
}
if (handle == 0u)
{
m_host.closeHostFile(file);
writeRpcResult(-1, 0u);
return result;
}
writeRpcResult(0, handle);
return result;
}
case Operation::Close:
{
uint32_t handle = 0u;
(void)readGuestU32(request.send.address, handle);
uint64_t file = 0u;
bool closedLoad = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto fileIt = m_fileHandles.find(handle);
if (fileIt != m_fileHandles.end())
{
file = fileIt->second.handle;
m_fileHandles.erase(fileIt);
}
const auto loadIt = m_loads.find(handle);
if (loadIt != m_loads.end())
{
m_loads.erase(loadIt);
closedLoad = true;
}
}
if (file != 0u)
{
m_host.closeHostFile(file);
}
const bool closed = file != 0u || closedLoad;
writeRpcResult(closed ? 0 : -1, closed ? 1u : 0u);
return result;
}
case Operation::Read:
{
uint32_t handle = 0u;
uint32_t requestedBytes = 0u;
uint32_t destinationAddress = 0u;
(void)readGuestU32(request.send.address + 0u, handle);
(void)readGuestU32(request.send.address + 4u, requestedBytes);
(void)readGuestU32(request.send.address + 8u, destinationAddress);
if (destinationAddress == 0u)
{
writeRpcResult(-1, 0u);
return result;
}
std::vector<uint8_t> bytes(std::min(requestedBytes,
m_bindings.rpc.maximumReadBytes));
size_t bytesRead = 0u;
bool readFailed = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto fileIt = m_fileHandles.find(handle);
if (fileIt == m_fileHandles.end() || fileIt->second.handle == 0u)
{
readFailed = true;
}
else if (!bytes.empty())
{
size_t hostBytesRead = 0u;
if (!m_host.readHostFile(fileIt->second.handle,
fileIt->second.position,
bytes.data(),
bytes.size(),
hostBytesRead))
{
readFailed = true;
}
else
{
bytesRead = hostBytesRead;
fileIt->second.position += hostBytesRead;
}
}
}
if (readFailed ||
(bytesRead != 0u &&
!m_host.writeGuest(destinationAddress, bytes.data(), bytesRead)))
{
writeRpcResult(-1, 0u);
return result;
}
writeRpcResult(0, static_cast<uint32_t>(bytesRead));
return result;
}
case Operation::GetStatus:
{
uint32_t handle = 0u;
(void)readGuestU32(request.send.address, handle);
bool loadFound = false;
uint32_t loadStatus = 0u;
bool fileFound = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto loadIt = m_loads.find(handle);
if (loadIt != m_loads.end())
{
loadFound = true;
loadStatus = loadIt->second.status;
}
else
{
fileFound = m_fileHandles.find(handle) != m_fileHandles.end();
}
}
if (loadFound)
{
writeRpcResult(static_cast<int32_t>(loadStatus), 0u);
}
else
{
writeRpcResult(0, fileFound ? 0u : m_bindings.rpc.invalidHandleStatus);
}
return result;
}
case Operation::GetSize:
{
uint32_t handle = 0u;
(void)readGuestU32(request.send.address, handle);
bool found = false;
uint32_t size = 0u;
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto loadIt = m_loads.find(handle);
if (loadIt != m_loads.end())
{
found = true;
size = loadIt->second.size;
}
else
{
const auto fileIt = m_fileHandles.find(handle);
if (fileIt != m_fileHandles.end())
{
found = true;
size = fileIt->second.size;
}
}
}
writeRpcResult(found ? 0 : -1, found ? size : 0u);
return result;
}
case Operation::Unknown:
writeRpcResult(0, 0u);
return result;
}
return result;
}
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
{
std::lock_guard<std::mutex> lock(m_mutex);
metrics.push_back({"open_files", m_fileHandles.size(), false});
metrics.push_back({"load_records", m_loads.size(), false});
metrics.push_back({"next_file_handle", m_nextFileHandle, true});
metrics.push_back({"next_load_handle", m_nextLoadHandle, true});
}
private:
enum class Operation
{
DirectLoad,
GetStatus,
Initialize,
Wait,
GetSize,
Open,
Close,
Read,
SecondaryWait,
SetRoot,
Unknown,
};
[[nodiscard]] Operation decodeFunction(uint32_t function) const
{
const ClFileRpcLayout &rpc = m_bindings.rpc;
if (function == rpc.directLoadFunction) return Operation::DirectLoad;
if (function == rpc.getStatusFunction) return Operation::GetStatus;
if (function == rpc.initializeFunction) return Operation::Initialize;
if (function == rpc.waitFunction) return Operation::Wait;
if (function == rpc.getSizeFunction) return Operation::GetSize;
if (function == rpc.openFunction) return Operation::Open;
if (function == rpc.closeFunction) return Operation::Close;
if (function == rpc.readFunction) return Operation::Read;
if (function == rpc.secondaryWaitFunction) return Operation::SecondaryWait;
if (function == rpc.setRootFunction) return Operation::SetRoot;
return Operation::Unknown;
}
struct ClFileHandle
{
uint64_t handle = 0u;
uint32_t size = 0u;
uint64_t position = 0u;
};
struct ClFileLoad
{
uint32_t status = 0u;
uint32_t size = 0u;
};
[[nodiscard]] bool readGuestU32(uint32_t address, uint32_t &value) const
{
value = 0u;
return m_host.readGuest(address, &value, sizeof(value));
}
[[nodiscard]] std::string readGuestString(uint32_t address, uint32_t maxBytes) const
{
if (address == 0u || maxBytes == 0u)
{
return {};
}
std::vector<char> bytes(maxBytes);
if (!m_host.readGuest(address, bytes.data(), bytes.size()))
{
return {};
}
size_t length = 0u;
while (length < bytes.size() && bytes[length] != '\0')
{
++length;
}
return std::string(bytes.data(), length);
}
[[nodiscard]] static bool hasDevice(std::string_view path)
{
return path.find(':') != std::string_view::npos;
}
[[nodiscard]] static std::string joinGuestPath(const std::string &root,
const std::string &leaf)
{
if (root.empty() || leaf.empty() || hasDevice(leaf))
{
return leaf;
}
const char tail = root.back();
if (tail == '/' || tail == '\\' || tail == ':')
{
return root + leaf;
}
return root + "/" + leaf;
}
[[nodiscard]] std::string resolvePath(const std::string &path) const
{
std::string root;
{
std::lock_guard<std::mutex> lock(m_mutex);
root = m_root;
}
const std::string translated = m_host.translateGuestPath(joinGuestPath(root, path));
return translated;
}
[[nodiscard]] uint32_t allocateFileHandleLocked(uint64_t file, uint32_t size)
{
if (file == 0u)
{
return 0u;
}
for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt)
{
uint32_t handle = m_nextFileHandle++;
if (handle == 0u)
{
handle = m_nextFileHandle++;
}
if (m_fileHandles.find(handle) == m_fileHandles.end() &&
m_loads.find(handle) == m_loads.end())
{
m_fileHandles.emplace(handle, ClFileHandle{file, size, 0u});
return handle;
}
}
return 0u;
}
[[nodiscard]] uint32_t allocateLoadLocked(uint32_t status, uint32_t size)
{
for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt)
{
uint32_t handle = m_nextLoadHandle++;
if (handle < 3u)
{
handle = m_bindings.rpc.firstLoadHandle;
m_nextLoadHandle = m_bindings.rpc.firstLoadHandle + 1u;
}
if (m_loads.find(handle) == m_loads.end() &&
m_fileHandles.find(handle) == m_fileHandles.end())
{
m_loads.emplace(handle, ClFileLoad{status, size});
return handle;
}
}
return 0u;
}
void writeResult(GuestBuffer receive, int32_t status, uint32_t value)
{
if (receive.address != 0u &&
receive.size >= m_bindings.rpc.responseStatusOffset + sizeof(uint32_t))
{
const uint32_t encodedStatus = static_cast<uint32_t>(status);
(void)m_host.writeGuest(receive.address + m_bindings.rpc.responseStatusOffset,
&encodedStatus,
sizeof(encodedStatus));
}
if (receive.address != 0u &&
receive.size >= m_bindings.rpc.responseValueOffset + sizeof(uint32_t))
{
(void)m_host.writeGuest(receive.address + m_bindings.rpc.responseValueOffset,
&value,
sizeof(value));
}
}
[[nodiscard]] bool copyFileToGuest(uint64_t file,
uint32_t destinationAddress,
uint64_t bytesToCopy)
{
constexpr size_t kChunkBytes = 16u * 1024u;
if (bytesToCopy > 0xFFFFFFFFull - static_cast<uint64_t>(destinationAddress) + 1ull)
{
return false;
}
std::vector<uint8_t> chunk(kChunkBytes);
uint64_t copied = 0u;
while (copied < bytesToCopy)
{
const size_t wanted = static_cast<size_t>(
std::min<uint64_t>(chunk.size(), bytesToCopy - copied));
size_t received = 0u;
if (!m_host.readHostFile(file,
copied,
chunk.data(),
wanted,
received) ||
received != wanted)
{
return false;
}
const uint32_t chunkAddress = destinationAddress + static_cast<uint32_t>(copied);
if (!m_host.writeGuest(chunkAddress, chunk.data(), received))
{
return false;
}
copied += received;
}
return true;
}
IopHost &m_host;
ClFileBindings m_bindings;
std::array<uint32_t, 1> m_sids;
mutable std::mutex m_mutex;
std::unordered_map<uint32_t, ClFileHandle> m_fileHandles;
std::unordered_map<uint32_t, ClFileLoad> m_loads;
uint32_t m_nextFileHandle = 1u;
uint32_t m_nextLoadHandle = 0u;
std::string m_root;
};
}
std::unique_ptr<IopService> createClFileService(IopHost &host,
ClFileBindings bindings)
{
const ClFileRpcLayout &rpc = bindings.rpc;
const std::array<uint32_t, 10> functions = {
rpc.directLoadFunction,
rpc.getStatusFunction,
rpc.initializeFunction,
rpc.waitFunction,
rpc.getSizeFunction,
rpc.openFunction,
rpc.closeFunction,
rpc.readFunction,
rpc.secondaryWaitFunction,
rpc.setRootFunction,
};
std::unordered_set<uint32_t> uniqueFunctions;
for (const uint32_t function : functions)
{
if (!uniqueFunctions.emplace(function).second)
{
throw std::invalid_argument("duplicate CLFILE RPC function binding");
}
}
if (bindings.serviceName.empty() || bindings.sid == 0u ||
rpc.pathBytes == 0u || rpc.maximumReadBytes == 0u ||
rpc.firstLoadHandle < 3u)
{
throw std::invalid_argument("invalid CLFILE bindings");
}
return std::make_unique<ClFileService>(host, std::move(bindings));
}
}
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
#include "module_factories.h"
#include <array>
#include <cstdint>
#include <mutex>
#include <sstream>
#include <string>
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<const uint32_t> sids() const override
{
return kSids;
}
void reset() override
{
std::lock_guard<std::mutex> 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<std::mutex> 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<DebugMetric> &metrics) const override
{
std::lock_guard<std::mutex> lock(m_mutex);
metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false});
}
private:
inline static constexpr std::array<uint32_t, 1> kSids{kDbcManSid};
IopHost &m_host;
mutable std::mutex m_mutex;
uint32_t m_unknownRpcLogCount = 0u;
};
}
std::unique_ptr<IopService> createDbcmanService(IopHost &host)
{
return std::make_unique<DbcmanService>(host);
}
}
+63
View File
@@ -0,0 +1,63 @@
#include "module_factories.h"
#include <array>
#include <cstdint>
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<const uint32_t> 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<uint32_t, 1> kSids{kLibSdSid};
IopHost &m_host;
};
}
std::unique_ptr<IopService> createLibSdService(IopHost &host)
{
return std::make_unique<LibSdService>(host);
}
}
+402
View File
@@ -0,0 +1,402 @@
#include "../iop_service.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include <sstream>
#include <string_view>
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<const uint32_t> sids() const override { return m_sids; }
void reset() override
{
{
std::lock_guard<std::mutex> 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, &parameter, 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, &parameter, sizeof(parameter)))
{
if (operation == Operation::Write && parameter.origin > 0 &&
parameter.origin <= static_cast<int32_t>(sizeof(parameter.data)))
{
const uint32_t inlineAddress =
request.send.address + static_cast<uint32_t>(offsetof(DescriptorParameter, data));
const int32_t prefix = call(MemoryCardOperation::Write,
static_cast<uint32_t>(parameter.fd),
inlineAddress,
static_cast<uint32_t>(parameter.origin));
if (prefix < 0)
{
result = prefix;
}
else
{
const int32_t body = call(MemoryCardOperation::Write,
static_cast<uint32_t>(parameter.fd),
parameter.buffer,
static_cast<uint32_t>(std::max(parameter.size, 0)));
result = body < 0 ? body : prefix + body;
}
}
else
{
result = handleDescriptorOperation(operation, flavor, parameter);
}
}
}
writeResult(request.receive, result);
return response;
}
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
{
std::lock_guard<std::mutex> 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<uint32_t, 3> values = {
static_cast<uint32_t>(kSucceeded), kMcservVersion, kMcmanVersion};
const uint32_t bytes = std::min<uint32_t>(receive.size, sizeof(values));
(void)m_host.writeGuest(receive.address, values.data(), bytes);
if (receive.size > bytes)
{
(void)m_host.zeroGuest(receive.address + bytes, receive.size - bytes);
}
}
int32_t handleNameOperation(Operation operation,
uint32_t sendAddress,
const NameParameter &parameter)
{
const uint32_t nameAddress =
sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
const uint32_t port = static_cast<uint32_t>(parameter.port);
const uint32_t slot = static_cast<uint32_t>(parameter.slot);
switch (operation)
{
case Operation::Open:
if ((static_cast<uint32_t>(parameter.flags) & kCreateDirectory) != 0u)
{
return call(MemoryCardOperation::Mkdir, port, slot, nameAddress);
}
return call(MemoryCardOperation::Open,
port, slot, nameAddress,
static_cast<uint32_t>(parameter.flags));
case Operation::Chdir:
return call(MemoryCardOperation::Chdir,
port, slot, nameAddress, parameter.pointer);
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 &parameter)
{
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<uint32_t>(parameter.port),
static_cast<uint32_t>(parameter.slot),
typeAddress,
freeAddress,
formatAddress);
}
case Operation::Close:
return call(MemoryCardOperation::Close, static_cast<uint32_t>(parameter.fd));
case Operation::Seek:
return call(MemoryCardOperation::Seek,
static_cast<uint32_t>(parameter.fd),
static_cast<uint32_t>(parameter.offset),
static_cast<uint32_t>(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<uint32_t>(parameter.fd),
parameter.buffer,
static_cast<uint32_t>(std::max(parameter.size, 0)));
case Operation::Write:
return call(MemoryCardOperation::Write,
static_cast<uint32_t>(parameter.fd),
parameter.buffer,
static_cast<uint32_t>(std::max(parameter.size, 0)));
case Operation::Flush:
return call(MemoryCardOperation::Flush, static_cast<uint32_t>(parameter.fd));
case Operation::Format:
return call(MemoryCardOperation::Format,
static_cast<uint32_t>(parameter.port),
static_cast<uint32_t>(parameter.slot));
case Operation::Unformat:
return call(MemoryCardOperation::Unformat,
static_cast<uint32_t>(parameter.port),
static_cast<uint32_t>(parameter.slot));
case Operation::ChangePriority:
return kSucceeded;
default:
return kDenied;
}
}
void logUnknown(const RpcRequest &request)
{
{
std::lock_guard<std::mutex> 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<uint32_t, 2> m_sids = {kMcservSid, kMcservDev9Sid};
};
}
std::unique_ptr<IopService> createMcservService(IopHost &host)
{
return std::make_unique<McservService>(host);
}
}
+335
View File
@@ -0,0 +1,335 @@
#include "module_factories.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace ps2x::iop::detail
{
namespace
{
constexpr uint32_t kEeRamSize = 32u * 1024u * 1024u;
class SdrdrvService final : public IopService
{
public:
SdrdrvService(IopHost &host, SdrdrvBindings bindings)
: m_host(host), m_bindings(std::move(bindings)), m_sids{m_bindings.sid}
{
}
std::string_view name() const override { return m_bindings.serviceName; }
std::span<const uint32_t> sids() const override { return m_sids; }
void reset() override
{
std::lock_guard<std::mutex> lock(m_mutex);
m_headerWarnCount = 0;
m_bodyWarnCount = 0;
}
RpcResult handleRpc(const RpcRequest &request) override
{
RpcResult result;
if (request.sid != m_bindings.sid)
{
return result;
}
result.handled = true;
result.resultAddress = request.receive.address;
if (m_bindings.clearReceiveBeforeDispatch &&
request.receive.address && request.receive.size)
{
(void)m_host.zeroGuest(request.receive.address, request.receive.size);
}
if (request.function == m_bindings.initFunction)
{
if (!loadImageHeader())
{
warnHeader();
}
return result;
}
if (request.function == m_bindings.shutdownFunction)
{
return result;
}
if (request.function != m_bindings.submitFunction)
{
return result;
}
const uint32_t count = std::min(request.send.size / m_bindings.commandBytes,
m_bindings.maxCommands);
for (uint32_t commandIndex = 0; commandIndex < count; ++commandIndex)
{
std::vector<uint32_t> words(m_bindings.commandBytes / sizeof(uint32_t));
const uint32_t commandAddress = request.send.address +
commandIndex * m_bindings.commandBytes;
if (!m_host.readGuest(commandAddress,
words.data(),
m_bindings.commandBytes))
{
continue;
}
if (words[0] == m_bindings.headerCommand)
{
if (!loadImageHeader())
{
warnHeader();
}
continue;
}
if (words[0] != m_bindings.loadCommand)
{
continue;
}
const uint32_t lbn = words[m_bindings.lbnWord];
const uint32_t byteCount = words[m_bindings.byteCountWord];
const uint32_t destination = words[m_bindings.destinationWord];
const bool eeLoad = words[m_bindings.destinationKindWord] ==
m_bindings.eeDestinationKind;
const uint32_t loadId = words[m_bindings.loadIdWord];
const bool loaded = eeLoad
? readBody(lbn, byteCount, destination)
: m_bindings.pretendNonEeLoadsComplete;
if (eeLoad && !loaded)
{
(void)m_host.zeroGuest(destination, byteCount);
bool shouldWarn = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bodyWarnCount < m_bindings.bodyWarningLimit)
{
++m_bodyWarnCount;
shouldWarn = true;
}
}
if (shouldWarn)
{
std::ostringstream message;
message << '[' << m_bindings.serviceName
<< "] failed data read lbn=0x" << std::hex << lbn
<< " bytes=0x" << byteCount << " dst=0x" << destination;
m_host.log(LogLevel::Warning, message.str());
}
}
if (loaded || m_bindings.completeFailedLoads)
{
markLoadComplete(request.receive, loadId);
}
}
return result;
}
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
{
std::lock_guard<std::mutex> lock(m_mutex);
metrics.push_back({"header_warnings", m_headerWarnCount, false});
metrics.push_back({"body_warnings", m_bodyWarnCount, false});
}
private:
uint64_t openSiblingFile(const std::string &lowerName,
const std::string &upperName)
{
const std::array<std::string, 2> roots = {
m_host.hostPath(HostPathKind::CdRoot),
m_host.hostPath(HostPathKind::ElfDirectory),
};
for (const std::string &rootValue : roots)
{
if (rootValue.empty())
{
continue;
}
const std::filesystem::path root(rootValue);
for (const std::string *name : {&lowerName, &upperName})
{
if (name->empty())
{
continue;
}
const std::filesystem::path candidate = root / *name;
const uint64_t handle = m_host.openHostFile(candidate.string());
if (handle != 0u)
{
return handle;
}
}
}
return 0u;
}
bool copyHostRange(uint64_t handle,
uint64_t offset,
uint32_t destination,
uint64_t byteCount)
{
if (byteCount == 0)
{
return true;
}
std::array<uint8_t, 16 * 1024> chunk{};
uint64_t copied = 0;
while (copied < byteCount)
{
const size_t wanted = static_cast<size_t>(std::min<uint64_t>(chunk.size(), byteCount - copied));
std::fill(chunk.begin(), chunk.begin() + static_cast<std::ptrdiff_t>(wanted), 0u);
size_t got = 0u;
if (!m_host.readHostFile(handle,
offset + copied,
chunk.data(),
wanted,
got) ||
got > wanted ||
!m_host.writeGuest(destination + static_cast<uint32_t>(copied),
chunk.data(),
wanted))
{
return false;
}
copied += wanted;
}
return true;
}
bool loadImageHeader()
{
const uint64_t handle = openSiblingFile(m_bindings.imageHeaderLowerName,
m_bindings.imageHeaderUpperName);
if (handle == 0u)
{
return false;
}
uint64_t fileSize = 0u;
if (!m_host.hostFileSize(handle, fileSize))
{
m_host.closeHostFile(handle);
return false;
}
uint32_t normalized = 0;
if (!m_host.normalizeGuestAddress(m_bindings.imageHeaderAddress, normalized) ||
normalized >= kEeRamSize)
{
m_host.closeHostFile(handle);
return false;
}
const bool copied = copyHostRange(handle,
0u,
m_bindings.imageHeaderAddress,
std::min<uint64_t>(fileSize,
kEeRamSize - normalized));
m_host.closeHostFile(handle);
return copied;
}
bool readBody(uint32_t lbn, uint32_t byteCount, uint32_t destination)
{
uint32_t normalized = 0;
if (!m_host.normalizeGuestAddress(destination, normalized) || normalized >= kEeRamSize)
{
return false;
}
uint64_t handle = openSiblingFile(m_bindings.imageBodyLowerName,
m_bindings.imageBodyUpperName);
if (handle == 0u && m_bindings.fallbackBodyToCdImage)
{
handle = m_host.openHostFile(m_host.hostPath(HostPathKind::CdImage));
}
if (handle == 0u)
{
return false;
}
const uint64_t bytes = std::min<uint64_t>(byteCount, kEeRamSize - normalized);
const bool copied = copyHostRange(handle,
static_cast<uint64_t>(lbn) * m_bindings.sectorSize,
destination,
bytes);
m_host.closeHostFile(handle);
return copied;
}
void markLoadComplete(GuestBuffer receive, uint32_t loadId)
{
const uint32_t offset = m_bindings.statusOffset +
((loadId & m_bindings.statusSlotMask) *
m_bindings.statusStride);
if (receive.address && offset < receive.size)
{
const uint8_t complete = m_bindings.completeValue;
(void)m_host.writeGuest(receive.address + offset, &complete, sizeof(complete));
}
}
void warnHeader()
{
bool shouldWarn = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_headerWarnCount < m_bindings.headerWarningLimit)
{
++m_headerWarnCount;
shouldWarn = true;
}
}
if (shouldWarn)
{
m_host.log(LogLevel::Warning,
'[' + m_bindings.serviceName + "] failed to load image header");
}
}
IopHost &m_host;
SdrdrvBindings m_bindings;
std::array<uint32_t, 1> m_sids;
mutable std::mutex m_mutex;
uint32_t m_headerWarnCount = 0;
uint32_t m_bodyWarnCount = 0;
};
}
std::unique_ptr<IopService> createSdrdrvService(IopHost &host,
SdrdrvBindings bindings)
{
const uint32_t largestWord = std::max({bindings.lbnWord,
bindings.byteCountWord,
bindings.destinationWord,
bindings.destinationKindWord,
bindings.loadIdWord});
if (bindings.serviceName.empty() ||
bindings.sid == 0u ||
bindings.imageHeaderAddress == 0u ||
bindings.commandBytes == 0u ||
(bindings.commandBytes % sizeof(uint32_t)) != 0u ||
largestWord >= bindings.commandBytes / sizeof(uint32_t) ||
bindings.maxCommands == 0u ||
bindings.sectorSize == 0u ||
bindings.statusStride == 0u ||
bindings.initFunction == bindings.submitFunction ||
bindings.initFunction == bindings.shutdownFunction ||
bindings.submitFunction == bindings.shutdownFunction ||
bindings.headerCommand == bindings.loadCommand ||
(bindings.imageHeaderLowerName.empty() &&
bindings.imageHeaderUpperName.empty()) ||
(bindings.imageBodyLowerName.empty() &&
bindings.imageBodyUpperName.empty() &&
!bindings.fallbackBodyToCdImage))
{
throw std::invalid_argument("invalid SDRDRV bindings");
}
return std::make_unique<SdrdrvService>(host, std::move(bindings));
}
}
+122
View File
@@ -0,0 +1,122 @@
#include "module_factories.h"
#include <array>
#include <algorithm>
#include <cstdint>
#include <mutex>
#include <stdexcept>
#include <unordered_set>
#include <utility>
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<const uint32_t> sids() const override
{
return m_sids;
}
void reset() override
{
std::lock_guard<std::mutex> 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<std::mutex> 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<DebugMetric> &metrics) const override
{
std::lock_guard<std::mutex> lock(m_mutex);
metrics.push_back({"update_counter", m_updateCounter, false});
}
private:
IopHost &m_host;
SoundUpdateStubBindings m_bindings;
std::array<uint32_t, 1> m_sids;
mutable std::mutex m_mutex;
uint32_t m_updateCounter = 0u;
};
}
std::unique_ptr<IopService> createSoundUpdateStubService(IopHost &host,
SoundUpdateStubBindings bindings)
{
if (bindings.serviceName.empty() || bindings.sid == 0u ||
bindings.activeStreamCountOffset == bindings.responseCounterOffset)
{
throw std::invalid_argument("invalid SOUND update stub bindings");
}
std::unordered_set<uint32_t> callbacks;
for (const uint32_t callback : bindings.suppressedCompletionCallbacks)
{
if (callback == 0u || !callbacks.emplace(callback).second)
{
throw std::invalid_argument("invalid SOUND update callback binding");
}
}
return std::make_unique<SoundUpdateStubService>(host, std::move(bindings));
}
}
+628
View File
@@ -0,0 +1,628 @@
#include "../module_factories.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include <limits>
#include <span>
#include <stdexcept>
#include <string_view>
#include <unordered_set>
#include <utility>
namespace ps2x::iop::detail
{
namespace
{
constexpr uint32_t kCommandSid = 0x00000000u;
constexpr uint32_t kStateSid = 0x00000001u;
constexpr uint32_t kSubmitFunction = 0x00000000u;
constexpr uint32_t kGetStatusAddressFunction = 0x00000012u;
constexpr uint32_t kGetAddressTableFunction = 0x00000013u;
constexpr uint32_t kStatusSize = 0x42u;
constexpr uint32_t kSeInfoOffset = 0x00u;
constexpr uint32_t kMidiInfoOffset = 0x0Cu;
constexpr uint32_t kMidiSumOffset = 0x1Eu;
constexpr uint32_t kSeSumOffset = 0x26u;
constexpr uint32_t kAddressTableEntries = 16u;
constexpr uint32_t alignUp(uint32_t value, uint32_t alignment)
{
if (alignment == 0u)
{
return value;
}
return (value + (alignment - 1u)) & ~(alignment - 1u);
}
template <typename T>
bool readGuestPod(const IopHost &host, uint32_t address, T &value)
{
value = {};
return host.readGuest(address, &value, sizeof(value));
}
template <typename T>
bool writeGuestPod(IopHost &host, uint32_t address, const T &value)
{
return host.writeGuest(address, &value, sizeof(value));
}
template <typename T, size_t Size>
bool hasAnyNonZero(const std::array<T, Size> &values)
{
return std::any_of(values.begin(), values.end(), [](const T value)
{ return value != static_cast<T>(0); });
}
size_t commandLength(uint8_t command)
{
const uint8_t hi = static_cast<uint8_t>(command & 0xF0u);
switch (hi)
{
case 0x00u:
{
size_t length = 4u;
if ((command & 0x01u) != 0u)
{
++length;
}
if ((command & 0x02u) != 0u)
{
++length;
}
if ((command & 0x04u) != 0u)
{
length += 2u;
}
return length;
}
case 0x10u:
return command == 0x11u ? 3u : 1u;
case 0x20u:
if (command == 0x22u || command == 0x23u || command == 0x24u || command == 0x25u)
{
return 3u;
}
if (command == 0x26u)
{
return 4u;
}
if (command == 0x20u)
{
return 5u;
}
if (command == 0x27u || command == 0x28u || command == 0x29u ||
command == 0x2Cu || command == 0x2Du)
{
return 8u;
}
return 2u;
case 0x40u:
if (command == 0x47u || command == 0x48u || command == 0x49u || command == 0x4Au ||
command == 0x41u || command == 0x42u)
{
return 2u;
}
if (command == 0x4Bu)
{
return 3u;
}
if (command == 0x45u || command == 0x4Cu)
{
return 4u;
}
if (command == 0x44u)
{
return 6u;
}
if (command == 0x4Du || command == 0x4Eu)
{
return 3u;
}
if (command == 0x4Fu)
{
return 6u;
}
return 1u;
case 0x50u:
case 0x60u:
if (command == 0x51u || command == 0x52u || command == 0x53u || command == 0x54u)
{
return 8u;
}
return 2u;
default:
return 0u;
}
}
class TsnddrvService final : public IopService
{
public:
TsnddrvService(IopHost &host, TsnddrvBindings bindings)
: m_host(host), m_bindings(std::move(bindings))
{
}
[[nodiscard]] std::string_view name() const override
{
return m_bindings.serviceName;
}
[[nodiscard]] std::span<const uint32_t> sids() const override
{
return m_sids;
}
void reset() override
{
std::lock_guard<std::mutex> lock(m_mutex);
m_state = {};
}
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
{
RpcResult result{};
if (request.sid == kCommandSid && request.function == kSubmitFunction)
{
handleCommandBuffer(request.send);
result.handled = true;
}
else if (request.sid == kStateSid &&
(request.function == kGetStatusAddressFunction ||
request.function == kGetAddressTableFunction))
{
uint32_t responseAddress = 0u;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!ensureMemoryLocked())
{
return result;
}
responseAddress = request.function == kGetStatusAddressFunction
? m_state.statusAddress
: m_state.addressTableAddress;
}
if (request.receive.address != 0u && request.receive.size >= sizeof(uint32_t))
{
(void)writeGuestPod(m_host, request.receive.address, responseAddress);
if (request.receive.size > sizeof(uint32_t))
{
(void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t),
request.receive.size - sizeof(uint32_t));
}
result.resultAddress = request.receive.address;
}
result.handled = true;
result.signalNowaitCompletion = true;
}
if (result.handled)
{
const auto rule = std::find_if(
m_bindings.completionRules.begin(),
m_bindings.completionRules.end(),
[&](const TsnddrvCompletionRule &candidate) {
return candidate.eeFunction == request.endFunction;
});
if (rule != m_bindings.completionRules.end())
{
if (rule->suppressGuestCallback)
{
result.callbackPolicy = CallbackPolicy::Suppress;
}
result.signalCompletion = rule->signalCompletion;
if (rule->clearBusy)
{
constexpr uint32_t idle = 0u;
(void)writeGuestPod(m_host,
m_bindings.busyFlagAddress,
idle);
}
}
}
return result;
}
void onSifTransfer(const SifTransfer &transfer) override
{
if (transfer.kind != SifTransferKind::GetOtherData ||
transfer.phase != SifTransferPhase::BeforeCopy ||
transfer.size != kStatusSize)
{
return;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_state.initialized || transfer.sourceAddress != m_state.statusAddress)
{
return;
}
backfillStatusLocked();
}
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
{
std::lock_guard<std::mutex> lock(m_mutex);
metrics.push_back({"initialized", m_state.initialized ? 1u : 0u, false});
metrics.push_back({"status_address", m_state.statusAddress, true});
metrics.push_back({"address_table", m_state.addressTableAddress, true});
metrics.push_back({"hd_base", m_state.hdBaseAddress, true});
metrics.push_back({"sq_base", m_state.sqBaseAddress, true});
metrics.push_back({"data_base", m_state.dataBaseAddress, true});
}
private:
struct State
{
bool initialized = false;
uint32_t storageBaseAddress = 0u;
uint32_t storageSize = 0u;
uint32_t statusAddress = 0u;
uint32_t addressTableAddress = 0u;
uint32_t hdBaseAddress = 0u;
uint32_t sqBaseAddress = 0u;
uint32_t dataBaseAddress = 0u;
};
bool ensureMemoryLocked()
{
if (m_state.statusAddress == 0u)
{
const TsnddrvGuestArena &arena = m_bindings.arena;
const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment);
const uint32_t addressTableAddress =
alignUp(statusAddress + kStatusSize, arena.tableAlignment);
const uint32_t hdBaseAddress =
alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)),
arena.storageAlignment);
const uint32_t sqBaseAddress =
alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment);
const uint32_t dataBaseAddress =
alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment);
const uint32_t storageEnd = dataBaseAddress + arena.dataBytes;
if (storageEnd > arena.limit)
{
return false;
}
m_state.statusAddress = statusAddress;
m_state.addressTableAddress = addressTableAddress;
m_state.hdBaseAddress = hdBaseAddress;
m_state.sqBaseAddress = sqBaseAddress;
m_state.dataBaseAddress = dataBaseAddress;
m_state.storageBaseAddress = hdBaseAddress;
m_state.storageSize = storageEnd - hdBaseAddress;
}
if (m_state.statusAddress == 0u ||
m_state.addressTableAddress == 0u ||
m_state.storageBaseAddress == 0u)
{
return false;
}
if (!m_state.initialized)
{
if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) ||
!m_host.zeroGuest(m_state.addressTableAddress,
kAddressTableEntries * sizeof(uint32_t)) ||
!m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize))
{
return false;
}
if (!writeGuestPod(m_host,
m_state.addressTableAddress + (0u * sizeof(uint32_t)),
m_state.hdBaseAddress) ||
!writeGuestPod(m_host,
m_state.addressTableAddress + (1u * sizeof(uint32_t)),
m_state.sqBaseAddress) ||
!writeGuestPod(m_host,
m_state.addressTableAddress + (2u * sizeof(uint32_t)),
m_state.dataBaseAddress))
{
return false;
}
m_state.initialized = true;
}
return true;
}
int16_t checkValue(bool seTable,
uint32_t index,
uint32_t count) const
{
if (index >= count)
{
return 0;
}
for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates)
{
const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress;
int16_t value = 0;
if (readGuestPod(m_host,
base + (index * sizeof(int16_t)),
value) &&
value != 0)
{
return value;
}
}
return 0;
}
bool selectCompatChecks(uint32_t &seBase, uint32_t &midiBase) const
{
const TsnddrvChecksumTables *firstReadable = nullptr;
for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates)
{
std::array<int16_t, 5> seValues{};
std::array<int16_t, 4> midiValues{};
const bool seReadable = m_host.readGuest(candidate.seAddress,
seValues.data(),
sizeof(seValues));
const bool midiReadable = m_host.readGuest(candidate.midiAddress,
midiValues.data(),
sizeof(midiValues));
if (seReadable && midiReadable && !firstReadable)
{
firstReadable = &candidate;
}
const bool looksLive =
(seReadable && hasAnyNonZero(seValues)) ||
(midiReadable && hasAnyNonZero(midiValues));
if (seReadable && midiReadable && looksLive)
{
seBase = candidate.seAddress;
midiBase = candidate.midiAddress;
return true;
}
}
if (firstReadable)
{
seBase = firstReadable->seAddress;
midiBase = firstReadable->midiAddress;
return true;
}
return false;
}
void backfillStatusLocked()
{
uint32_t seBase = 0u;
uint32_t midiBase = 0u;
if (!selectCompatChecks(seBase, midiBase))
{
return;
}
auto backfillSlots = [&](uint32_t statusOffset,
uint32_t compatBase,
uint32_t slotCount)
{
for (uint32_t slot = 0u; slot < slotCount; ++slot)
{
int16_t liveValue = 0;
if (!readGuestPod(m_host,
m_state.statusAddress + statusOffset +
(slot * sizeof(int16_t)),
liveValue) ||
liveValue != 0)
{
continue;
}
int16_t compatValue = 0;
if (!readGuestPod(m_host,
compatBase + (slot * sizeof(int16_t)),
compatValue) ||
compatValue == 0)
{
continue;
}
(void)writeGuestPod(m_host,
m_state.statusAddress + statusOffset +
(slot * sizeof(int16_t)),
compatValue);
}
};
backfillSlots(kSeSumOffset, seBase, 5u);
backfillSlots(kMidiSumOffset, midiBase, 4u);
}
void applyCommandLocked(const std::array<uint8_t, 8> &command)
{
if (m_state.statusAddress == 0u)
{
return;
}
switch (command[0])
{
case 0x20u: // SdrBgmReq
{
const uint32_t port = command[1] & 0x0Fu;
uint16_t midiInfo = 0u;
(void)readGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo |
static_cast<uint16_t>(1u << port));
(void)writeGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
break;
}
case 0x21u: // SdrBgmStop
{
const uint32_t port = command[1] & 0x0Fu;
uint16_t midiInfo = 0u;
(void)readGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
midiInfo = static_cast<uint16_t>(midiInfo &
~static_cast<uint16_t>(1u << port));
(void)writeGuestPod(m_host,
m_state.statusAddress + kMidiInfoOffset,
midiInfo);
break;
}
case 0x28u: // SdrHDDataSet
{
const uint32_t port = command[1] & 0x0Fu;
if (port >= 4u)
{
break;
}
const int16_t checksum = checkValue(false, port, 4u);
(void)writeGuestPod(m_host,
m_state.statusAddress + kMidiSumOffset +
(port * sizeof(int16_t)),
checksum);
break;
}
case 0x29u: // SdrHDDataSet2
{
const uint32_t port = command[1] & 0x0Fu;
if (port >= 5u)
{
break;
}
const int16_t checksum = checkValue(true, port, 5u);
(void)writeGuestPod(m_host,
m_state.statusAddress + kSeSumOffset +
(port * sizeof(int16_t)),
checksum);
break;
}
case 0x10u: // SdrSeAllStop
(void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset,
6u * sizeof(uint16_t));
break;
default:
break;
}
}
void handleCommandBuffer(GuestBuffer send)
{
if (send.address == 0u || send.size == 0u)
{
return;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (!ensureMemoryLocked())
{
return;
}
for (uint32_t offset = 0u; offset < send.size;)
{
uint8_t operation = 0u;
if (!m_host.readGuest(send.address + offset, &operation, sizeof(operation)) ||
operation == 0xFFu)
{
break;
}
const size_t length = commandLength(operation);
if (length == 0u ||
static_cast<uint64_t>(offset) + length > send.size)
{
break;
}
std::array<uint8_t, 8> command{};
if (!m_host.readGuest(send.address + offset, command.data(), length))
{
break;
}
applyCommandLocked(command);
offset += static_cast<uint32_t>(length);
}
}
IopHost &m_host;
TsnddrvBindings m_bindings;
mutable std::mutex m_mutex;
State m_state;
const std::array<uint32_t, 2> m_sids = {kCommandSid, kStateSid};
};
}
std::unique_ptr<IopService> createTsnddrvService(IopHost &host,
TsnddrvBindings bindings)
{
const auto isPowerOfTwo = [](uint32_t value) {
return value != 0u && (value & (value - 1u)) == 0u;
};
const auto alignUp64 = [](uint64_t value, uint32_t alignment) {
return (value + (alignment - 1u)) &
~static_cast<uint64_t>(alignment - 1u);
};
const TsnddrvGuestArena &arena = bindings.arena;
if (bindings.serviceName.empty() ||
arena.base >= arena.limit ||
!isPowerOfTwo(arena.statusAlignment) ||
!isPowerOfTwo(arena.tableAlignment) ||
!isPowerOfTwo(arena.storageAlignment) ||
arena.hdBytes == 0u || arena.sqBytes == 0u || arena.dataBytes == 0u ||
bindings.checksumCandidates.empty())
{
throw std::invalid_argument("invalid TSNDDRV bindings");
}
uint64_t end = alignUp64(arena.base, arena.statusAlignment) + kStatusSize;
end = alignUp64(end, arena.tableAlignment) +
(kAddressTableEntries * sizeof(uint32_t));
end = alignUp64(end, arena.storageAlignment) + arena.hdBytes;
end = alignUp64(end, arena.storageAlignment) + arena.sqBytes;
end = alignUp64(end, arena.storageAlignment) + arena.dataBytes;
if (end > arena.limit || end > std::numeric_limits<uint32_t>::max())
{
throw std::invalid_argument("TSNDDRV guest arena is too small");
}
for (const TsnddrvChecksumTables &candidate : bindings.checksumCandidates)
{
if (candidate.seAddress == 0u || candidate.midiAddress == 0u)
{
throw std::invalid_argument("incomplete TSNDDRV checksum binding");
}
}
std::unordered_set<uint32_t> callbacks;
for (const TsnddrvCompletionRule &rule : bindings.completionRules)
{
if (rule.eeFunction == 0u || !callbacks.emplace(rule.eeFunction).second ||
(rule.clearBusy && bindings.busyFlagAddress == 0u))
{
throw std::invalid_argument("invalid TSNDDRV completion rule");
}
}
switch (bindings.protocol)
{
case TsnddrvProtocolVariant::SndQueueV1:
break;
}
return std::make_unique<TsnddrvService>(host, std::move(bindings));
}
}
+956
View File
@@ -0,0 +1,956 @@
#include "plugin_loader.h"
#include "ps2x/iop/plugin_api.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <system_error>
#include <utility>
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
#include <dlfcn.h>
#endif
namespace ps2x::iop::detail
{
namespace
{
constexpr size_t kMaxPluginProfiles = 256u;
constexpr size_t kMaxPluginSids = 256u;
constexpr size_t kMaxPluginStringBytes = 4096u;
bool validStringView(ps2x_iop_string_view_v1 value)
{
return value.size <= kMaxPluginStringBytes && (value.size == 0u || value.data != nullptr);
}
std::string copyString(ps2x_iop_string_view_v1 value)
{
if (!validStringView(value) || value.size == 0u)
{
return {};
}
return std::string(value.data, value.size);
}
ps2x_iop_string_view_v1 makeStringView(std::string_view value)
{
return {value.data(), value.size()};
}
int32_t copyHostString(const std::string &value,
char *destination,
size_t capacity,
size_t *requiredSize)
{
const size_t required = value.size() + 1;
if (requiredSize)
{
*requiredSize = required;
}
if (!destination || capacity < required)
{
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
}
std::memcpy(destination, value.c_str(), required);
return PS2X_IOP_STATUS_OK_V1;
}
IopHandleKind toHandleKind(uint32_t kind)
{
return kind == PS2X_IOP_HANDLE_RPC_PACKET_V1
? IopHandleKind::RpcPacket
: IopHandleKind::RpcServer;
}
HostPathKind toHostPathKind(uint32_t kind)
{
switch (kind)
{
case PS2X_IOP_PATH_CD_ROOT_V1:
return HostPathKind::CdRoot;
case PS2X_IOP_PATH_CD_IMAGE_V1:
return HostPathKind::CdImage;
case PS2X_IOP_PATH_HOST_ROOT_V1:
return HostPathKind::HostRoot;
case PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1:
return HostPathKind::MemoryCardRoot;
default:
return HostPathKind::ElfDirectory;
}
}
MemoryCardOperation toMemoryCardOperation(uint32_t operation)
{
const uint32_t last = static_cast<uint32_t>(MemoryCardOperation::Mkdir);
if (operation > last)
{
throw std::out_of_range("invalid memory-card operation");
}
return static_cast<MemoryCardOperation>(operation);
}
class HostApiBridge
{
public:
explicit HostApiBridge(IopHost &hostRef)
: host(hostRef)
{
api.abi_version = PS2X_IOP_ABI_VERSION_V1;
api.struct_size = sizeof(api);
api.userdata = this;
api.read_guest = &readGuest;
api.write_guest = &writeGuest;
api.zero_guest = &zeroGuest;
api.normalize_guest_address = &normalizeGuestAddress;
api.allocate_iop_handle = &allocateIopHandle;
api.allocate_guest = &allocateGuest;
api.free_guest = &freeGuest;
api.audio_command = &audioCommand;
api.get_host_path = &getHostPath;
api.translate_guest_path = &translateGuestPath;
api.open_host_file = &openHostFile;
api.host_file_size = &hostFileSize;
api.read_host_file = &readHostFile;
api.close_host_file = &closeHostFile;
api.memory_card = &memoryCard;
api.has_guest_function = &hasGuestFunction;
api.invoke_guest_function = &invokeGuestFunction;
api.log = &log;
}
ps2x_iop_host_api_v1 api{};
IopHost &host;
private:
static HostApiBridge *self(void *userdata)
{
return static_cast<HostApiBridge *>(userdata);
}
template <typename Callback>
static int32_t guardedStatus(Callback &&callback) noexcept
{
try
{
return static_cast<int32_t>(
std::forward<Callback>(callback)());
}
catch (...)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
}
template <typename Value, typename Callback>
static Value guardedValue(Value fallback, Callback &&callback) noexcept
{
try
{
return static_cast<Value>(
std::forward<Callback>(callback)());
}
catch (...)
{
return fallback;
}
}
template <typename Callback>
static void guardedVoid(Callback &&callback) noexcept
{
try
{
std::forward<Callback>(callback)();
}
catch (...)
{
}
}
static int32_t readGuest(void *userdata, uint32_t address, void *destination, size_t size)
{
if (!userdata || (!destination && size != 0))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.readGuest(address, destination, size)
? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_FAILED_V1; });
}
static int32_t writeGuest(void *userdata, uint32_t address, const void *source, size_t size)
{
if (!userdata || (!source && size != 0))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.writeGuest(address, source, size)
? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_FAILED_V1; });
}
static int32_t zeroGuest(void *userdata, uint32_t address, size_t size)
{
if (!userdata)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.zeroGuest(address, size)
? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_FAILED_V1; });
}
static int32_t normalizeGuestAddress(void *userdata, uint32_t address, uint32_t *normalized)
{
if (!userdata || !normalized)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.normalizeGuestAddress(address, *normalized)
? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_FAILED_V1; });
}
static uint32_t allocateIopHandle(void *userdata, uint32_t kind)
{
if (!userdata)
{
return 0;
}
return guardedValue<uint32_t>(0u, [&]()
{ return self(userdata)->host.allocateIopHandle(toHandleKind(kind)); });
}
static uint32_t allocateGuest(void *userdata, uint32_t size, uint32_t alignment)
{
if (!userdata)
{
return 0;
}
return guardedValue<uint32_t>(0u, [&]()
{ return self(userdata)->host.allocateGuest(size, alignment); });
}
static void freeGuest(void *userdata, uint32_t address)
{
if (userdata && address)
{
guardedVoid([&]()
{ self(userdata)->host.freeGuest(address); });
}
}
static int32_t audioCommand(void *userdata,
uint32_t sid,
uint32_t function,
ps2x_iop_guest_buffer_v1 send,
ps2x_iop_guest_buffer_v1 receive)
{
if (!userdata)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{
self(userdata)->host.audioCommand(sid,
function,
{send.address, send.size},
{receive.address, receive.size});
return PS2X_IOP_STATUS_OK_V1; });
}
static int32_t getHostPath(void *userdata,
uint32_t kind,
char *destination,
size_t capacity,
size_t *requiredSize)
{
if (!userdata)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return copyHostString(self(userdata)->host.hostPath(toHostPathKind(kind)),
destination,
capacity,
requiredSize); });
}
static int32_t translateGuestPath(void *userdata,
ps2x_iop_string_view_v1 path,
char *destination,
size_t capacity,
size_t *requiredSize)
{
if (!userdata || (!path.data && path.size != 0))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{
const std::string translated = self(userdata)->host.translateGuestPath(
std::string_view(path.data ? path.data : "", path.size));
return copyHostString(translated, destination, capacity, requiredSize); });
}
static uint64_t openHostFile(void *userdata,
ps2x_iop_string_view_v1 path)
{
if (!userdata || (!path.data && path.size != 0u))
{
return 0u;
}
return guardedValue<uint64_t>(0u, [&]()
{ return self(userdata)->host.openHostFile(
std::string_view(path.data ? path.data : "", path.size)); });
}
static int32_t hostFileSize(void *userdata,
uint64_t handle,
uint64_t *size)
{
if (!userdata || handle == 0u || !size)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.hostFileSize(handle, *size)
? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_FAILED_V1; });
}
static int32_t readHostFile(void *userdata,
uint64_t handle,
uint64_t offset,
void *destination,
size_t size,
size_t *bytesRead)
{
if (!userdata || handle == 0u || !bytesRead ||
(!destination && size != 0u))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.readHostFile(handle,
offset,
destination,
size,
*bytesRead)
? PS2X_IOP_STATUS_OK_V1
: PS2X_IOP_STATUS_FAILED_V1; });
}
static void closeHostFile(void *userdata, uint64_t handle)
{
if (userdata && handle != 0u)
{
guardedVoid([&]()
{ self(userdata)->host.closeHostFile(handle); });
}
}
static int32_t memoryCard(void *userdata,
const ps2x_iop_memory_card_request_v1 *request,
int32_t *result)
{
if (!userdata || !request || !result || request->struct_size < sizeof(*request))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
try
{
MemoryCardRequest converted;
converted.operation = toMemoryCardOperation(request->operation);
std::copy(std::begin(request->arguments), std::end(request->arguments), converted.arguments.begin());
*result = self(userdata)->host.memoryCard(converted);
return PS2X_IOP_STATUS_OK_V1;
}
catch (...)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
}
static int32_t hasGuestFunction(void *userdata, uint32_t address)
{
if (!userdata)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.hasGuestFunction(address) ? 1 : 0; });
}
static int32_t invokeGuestFunction(void *userdata,
uint64_t callToken,
uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t *resultAddress)
{
if (!userdata)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return guardedStatus([&]()
{ return self(userdata)->host.invokeGuestFunction(callToken,
address,
a0,
a1,
a2,
a3,
resultAddress)
? 1
: 0; });
}
static void log(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message)
{
if (!userdata || (!message.data && message.size != 0))
{
return;
}
const uint32_t maxLevel = static_cast<uint32_t>(LogLevel::Error);
const auto converted = static_cast<LogLevel>(std::min(level, maxLevel));
guardedVoid([&]()
{ self(userdata)->host.log(
converted,
std::string_view(message.data ? message.data : "", message.size)); });
}
};
ps2x_iop_rpc_candidate_v1 toPluginCandidate(const RpcCallCandidate &candidate)
{
return {
candidate.sendSize,
candidate.receiveAddress,
candidate.receiveSize,
candidate.endFunction,
candidate.endParameter,
candidate.plausible ? 1u : 0u,
};
}
class PluginService final : public IopService
{
public:
PluginService(IopHost &host,
std::shared_ptr<void> libraryKeepAlive,
ps2x_iop_profile_api_v1 profileApi,
std::string serviceName,
std::vector<uint32_t> serviceSids,
const GameIdentity &identity)
: m_libraryKeepAlive(std::move(libraryKeepAlive)),
m_api(profileApi),
m_name(std::move(serviceName)),
m_sids(std::move(serviceSids)),
m_host(host)
{
const ps2x_iop_game_identity_v1 pluginIdentity{
sizeof(ps2x_iop_game_identity_v1),
makeStringView(identity.elfName),
identity.entryPoint,
identity.crc32,
};
try
{
m_instance = m_api.create(&m_host.api, &pluginIdentity);
}
catch (...)
{
throw std::runtime_error("plugin profile create threw an exception");
}
if (!m_instance)
{
throw std::runtime_error("plugin profile create returned null");
}
}
~PluginService() override
{
if (m_instance && m_api.destroy)
{
try
{
m_api.destroy(m_instance);
}
catch (...)
{
m_host.host.log(LogLevel::Error,
"IOP plugin destroy threw for " + m_name);
}
}
m_instance = nullptr;
}
std::string_view name() const override
{
return m_name;
}
std::span<const uint32_t> sids() const override
{
return m_sids;
}
void reset() override
{
if (!m_api.reset)
{
return;
}
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
try
{
status = m_api.reset(m_instance);
}
catch (...)
{
}
if (status != PS2X_IOP_STATUS_OK_V1)
{
m_host.host.log(LogLevel::Warning, "IOP plugin reset failed for " + m_name);
}
}
RpcAbi selectRpcAbi(const RpcAbiRequest &request) const override
{
if (!m_api.select_rpc_abi)
{
return RpcAbi::RuntimeDefault;
}
const ps2x_iop_rpc_abi_request_v1 converted{
sizeof(ps2x_iop_rpc_abi_request_v1),
request.boundSid,
request.function,
toPluginCandidate(request.registers),
toPluginCandidate(request.stack),
};
uint32_t result = PS2X_IOP_RPC_ABI_DEFAULT_V1;
try
{
result = m_api.select_rpc_abi(m_instance, &converted);
}
catch (...)
{
m_host.host.log(LogLevel::Warning,
"IOP plugin ABI selector threw for " + m_name);
}
if (result == PS2X_IOP_RPC_ABI_REGISTERS_V1)
{
return RpcAbi::Registers;
}
if (result == PS2X_IOP_RPC_ABI_STACK_V1)
{
return RpcAbi::Stack;
}
return RpcAbi::RuntimeDefault;
}
RpcResult handleRpc(const RpcRequest &request) override
{
const ps2x_iop_rpc_request_v1 converted{
sizeof(ps2x_iop_rpc_request_v1),
request.callToken,
request.clientAddress,
request.serverAddress,
request.serverFunction,
request.serverBuffer,
request.sid,
request.function,
request.mode,
{request.send.address, request.send.size},
{request.receive.address, request.receive.size},
request.endFunction,
request.endParameter,
};
ps2x_iop_rpc_result_v1 result{};
result.struct_size = sizeof(result);
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
try
{
status = m_api.handle_rpc(m_instance, &converted, &result);
}
catch (...)
{
}
if (status != PS2X_IOP_STATUS_OK_V1 ||
result.struct_size < sizeof(result))
{
m_host.host.log(LogLevel::Warning, "IOP plugin RPC failed for " + m_name);
return {};
}
return {
result.handled != 0,
result.result_address,
result.signal_nowait_completion != 0,
result.signal_completion != 0,
result.callback_policy == PS2X_IOP_CALLBACK_SUPPRESS_V1
? CallbackPolicy::Suppress
: CallbackPolicy::RuntimeDefault,
result.server_dispatch_policy == PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1
? ServerDispatchPolicy::Suppress
: ServerDispatchPolicy::RuntimeDefault,
};
}
void onSifTransfer(const SifTransfer &transfer) override
{
if (!m_api.on_sif_transfer)
{
return;
}
const ps2x_iop_sif_transfer_v1 converted{
sizeof(ps2x_iop_sif_transfer_v1),
static_cast<uint32_t>(transfer.kind),
static_cast<uint32_t>(transfer.phase),
transfer.sourceAddress,
transfer.destinationAddress,
transfer.size,
};
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
try
{
status = m_api.on_sif_transfer(m_instance, &converted);
}
catch (...)
{
}
if (status != PS2X_IOP_STATUS_OK_V1)
{
m_host.host.log(LogLevel::Warning, "IOP plugin transfer hook failed for " + m_name);
}
}
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
{
if (!m_api.debug_metric_count || !m_api.debug_metric)
{
return;
}
size_t rawCount = 0u;
try
{
rawCount = m_api.debug_metric_count(m_instance);
}
catch (...)
{
return;
}
const size_t count = std::min<size_t>(rawCount, 256);
for (size_t i = 0; i < count; ++i)
{
ps2x_iop_debug_metric_v1 metric{};
metric.struct_size = sizeof(metric);
int32_t status = PS2X_IOP_STATUS_FAILED_V1;
try
{
status = m_api.debug_metric(m_instance, i, &metric);
}
catch (...)
{
}
if (status != PS2X_IOP_STATUS_OK_V1 ||
metric.struct_size < sizeof(metric))
{
continue;
}
metrics.push_back({copyString(metric.name), metric.value, metric.hexadecimal != 0});
}
}
private:
std::shared_ptr<void> m_libraryKeepAlive;
ps2x_iop_profile_api_v1 m_api{};
std::string m_name;
std::vector<uint32_t> m_sids;
HostApiBridge m_host;
void *m_instance = nullptr;
};
bool hasPluginExtension(const std::filesystem::path &path)
{
std::string extension = path.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value)
{ return static_cast<char>(std::tolower(value)); });
#if defined(_WIN32)
return extension == ".dll";
#elif defined(__linux__)
return extension == ".so";
#else
(void)extension;
return false;
#endif
}
std::string formatPluginDiagnostic(const std::filesystem::path &path, std::string_view reason)
{
return "IOP plugin '" + path.string() + "': " + std::string(reason);
}
}
class PluginCatalog::DynamicLibrary
{
public:
explicit DynamicLibrary(std::filesystem::path sourcePath)
: path(std::move(sourcePath))
{
}
~DynamicLibrary()
{
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
if (handle)
{
FreeLibrary(static_cast<HMODULE>(handle));
}
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
if (handle)
{
dlclose(handle);
}
#endif
}
bool open(std::string &error)
{
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
handle = LoadLibraryW(path.c_str());
if (!handle)
{
error = "LoadLibraryW failed with code " + std::to_string(GetLastError());
return false;
}
return true;
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!handle)
{
const char *message = dlerror();
error = message ? message : "dlopen failed";
return false;
}
return true;
#else
error = "dynamic IOP plugins are disabled on this platform";
return false;
#endif
}
void *symbol(const char *name) const
{
#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32)
return handle ? reinterpret_cast<void *>(GetProcAddress(static_cast<HMODULE>(handle), name)) : nullptr;
#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__)
return handle ? dlsym(handle, name) : nullptr;
#else
(void)name;
return nullptr;
#endif
}
std::filesystem::path path;
void *handle = nullptr;
};
PluginCatalog::PluginCatalog(IopHost &host)
: m_host(host)
{
}
PluginCatalog::~PluginCatalog() = default;
// TODO I never test this one
bool PluginCatalog::load(const std::vector<std::filesystem::path> &searchPaths,
std::vector<ProfileDefinition> &profiles,
std::vector<std::string> &diagnostics,
std::string *error)
{
(void)error;
#if !PS2X_IOP_ENABLE_PLUGINS
if (!searchPaths.empty())
{
diagnostics.push_back("dynamic IOP plugins are disabled on this platform");
}
return true;
#else
for (const auto &searchPath : searchPaths)
{
std::error_code ec;
if (!std::filesystem::exists(searchPath, ec) || ec)
{
continue;
}
if (!std::filesystem::is_directory(searchPath, ec) || ec)
{
diagnostics.push_back(formatPluginDiagnostic(searchPath, "search path is not a directory"));
continue;
}
for (std::filesystem::directory_iterator iterator(searchPath, ec), end; !ec && iterator != end; iterator.increment(ec))
{
const std::filesystem::directory_entry &entry = *iterator;
if (!entry.is_regular_file(ec) || ec || !hasPluginExtension(entry.path()))
{
ec.clear();
continue;
}
std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(entry.path(), ec);
if (ec)
{
ec.clear();
canonicalPath = entry.path().lexically_normal();
}
const std::string pathKey = canonicalPath.generic_string();
if (!m_loadedPaths.insert(pathKey).second)
{
continue;
}
auto library = std::make_shared<DynamicLibrary>(canonicalPath);
std::string openError;
if (!library->open(openError))
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, openError));
continue;
}
const auto query = reinterpret_cast<ps2x_iop_query_v1_fn>(
library->symbol(PS2X_IOP_QUERY_SYMBOL_V1));
if (!query)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "missing " PS2X_IOP_QUERY_SYMBOL_V1));
continue;
}
ps2x_iop_plugin_api_v1 plugin{};
plugin.struct_size = sizeof(plugin);
int32_t queryStatus = PS2X_IOP_STATUS_FAILED_V1;
try
{
queryStatus = query(PS2X_IOP_ABI_VERSION_V1, &plugin);
}
catch (...)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
"query entry threw an exception"));
continue;
}
if (queryStatus != PS2X_IOP_STATUS_OK_V1 ||
plugin.abi_version != PS2X_IOP_ABI_VERSION_V1 ||
plugin.struct_size < sizeof(plugin))
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "incompatible ABI or invalid descriptor"));
continue;
}
if (plugin.profile_count > 0 && !plugin.profiles)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "profile table is null"));
continue;
}
if (plugin.profile_count > kMaxPluginProfiles)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "too many profiles"));
continue;
}
const std::string provider = copyString(plugin.name).empty()
? canonicalPath.filename().string()
: copyString(plugin.name);
size_t acceptedProfiles = 0;
for (size_t index = 0; index < plugin.profile_count; ++index)
{
const ps2x_iop_profile_api_v1 &profile = plugin.profiles[index];
if (profile.abi_version != PS2X_IOP_ABI_VERSION_V1 ||
profile.struct_size < sizeof(profile) ||
profile.matcher.struct_size < sizeof(profile.matcher))
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
"ignored invalid profile at index " + std::to_string(index)));
continue;
}
const std::string profileId = copyString(profile.id);
const bool validMatcherName = validStringView(profile.matcher.elf_name);
const bool matcherPresent = profile.matcher.elf_name.size != 0 ||
profile.matcher.entry_point != 0 ||
profile.matcher.crc32 != 0;
if (!validStringView(profile.id) || !validMatcherName ||
profileId.empty() || !matcherPresent ||
profile.sid_count == 0 || !profile.sids ||
!profile.create || !profile.destroy || !profile.reset ||
!profile.handle_rpc)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
"ignored invalid profile at index " + std::to_string(index)));
continue;
}
if (profile.sid_count > kMaxPluginSids)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
"ignored profile with too many SIDs: " + profileId));
continue;
}
std::vector<uint32_t> sids(profile.sids, profile.sids + profile.sid_count);
ProfileDefinition definition;
definition.id = profileId;
definition.provider = provider;
definition.matcher.elfName = copyString(profile.matcher.elf_name);
definition.matcher.entryPoint = profile.matcher.entry_point;
definition.matcher.crc32 = profile.matcher.crc32;
const std::shared_ptr<void> keepAlive = library;
definition.factory = [keepAlive, profile, profileId, sids = std::move(sids)](
IopHost &host,
const GameIdentity &identity) mutable
{
ServiceList services;
services.push_back(std::make_unique<PluginService>(host,
keepAlive,
profile,
profileId,
sids,
identity));
return services;
};
profiles.push_back(std::move(definition));
++acceptedProfiles;
}
if (acceptedProfiles == 0)
{
diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "no valid profiles"));
continue;
}
diagnostics.push_back(formatPluginDiagnostic(canonicalPath,
"loaded " + std::to_string(acceptedProfiles) + " profile(s)"));
m_libraries.push_back(std::move(library));
}
if (ec)
{
diagnostics.push_back(formatPluginDiagnostic(searchPath, ec.message()));
}
}
return true;
#endif
}
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "iop_service.h"
#include <filesystem>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
namespace ps2x::iop::detail
{
class PluginCatalog
{
public:
explicit PluginCatalog(IopHost &host);
~PluginCatalog();
PluginCatalog(const PluginCatalog &) = delete;
PluginCatalog &operator=(const PluginCatalog &) = delete;
bool load(const std::vector<std::filesystem::path> &searchPaths,
std::vector<ProfileDefinition> &profiles,
std::vector<std::string> &diagnostics,
std::string *error);
private:
class DynamicLibrary;
IopHost &m_host;
std::vector<std::shared_ptr<DynamicLibrary>> m_libraries;
std::unordered_set<std::string> m_loadedPaths;
};
}
+13 -6
View File
@@ -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
+1 -1
View File
@@ -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.
+22 -77
View File
@@ -21,16 +21,25 @@
#include <filesystem>
#include <iostream>
#include <iomanip>
#include <memory>
#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<uint32_t, 4> completionCallbacks{};
std::array<uint32_t, 2> 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<std::filesystem::path> 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<PS2IopHostAdapter> m_iopHost;
std::unique_ptr<ps2x::iop::IopSubsystem> m_iopSubsystem;
PS2AudioBackend m_audioBackend;
PSPadBackend m_padBackend;
VU1Interpreter m_vu0;
-5
View File
@@ -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);
-39
View File
@@ -1,39 +0,0 @@
#ifndef PS2_IOP_H
#define PS2_IOP_H
#include <cstdint>
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
@@ -1,23 +0,0 @@
#ifndef PS2_IOP_AUDIO_H
#define PS2_IOP_AUDIO_H
#include <cstdint>
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
-27
View File
@@ -1,27 +0,0 @@
#pragma once
#include <cstdint>
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);
}
@@ -1,17 +0,0 @@
#pragma once
#include <cstdint>
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);
}
@@ -1,17 +0,0 @@
#pragma once
#include <cstdint>
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);
}
+118 -52
View File
@@ -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<VoiceTransferState, kLibSdCoreCount> voiceTransfers{};
std::array<BlockTransferState, kLibSdCoreCount> 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<std::mutex> 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<uint16_t>(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<uint16_t>(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<uint32_t>::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)
+41 -12
View File
@@ -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 <map>
@@ -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<uint32_t>(xfer.size),
});
}
if (!copyGuestByteRange(rdram, xfer.dest, xfer.src, static_cast<uint32_t>(xfer.size)))
{
ok = false;
break;
}
ps2_syscalls::noteDtxSifDmaTransfer(
rdram,
xfer.src,
xfer.dest,
static_cast<uint32_t>(xfer.size));
if (runtime)
{
PS2IopTransport::notifyTransfer(runtime, rdram, {
ps2x::iop::SifTransferKind::SetDma,
ps2x::iop::SifTransferPhase::AfterCopy,
xfer.src,
xfer.dest,
static_cast<uint32_t>(xfer.size),
});
}
}
}
@@ -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 <iostream>
@@ -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<uint32_t, RpcServerState> g_rpc_servers;
inline std::unordered_map<uint32_t, RpcClientState> 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<uint32_t, uint32_t> 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<uint32_t, DtxTransferState> 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<uint32_t, DtxSjxState> 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<uint32_t, DtxPs2RnaState> 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<uint32_t, DtxSjrmtState> 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<int32_t, SifModuleRecord> g_sif_modules_by_id;
inline std::unordered_map<std::string, int32_t> g_sif_module_id_by_path;
inline int32_t g_next_sif_module_id = 1;
inline uint32_t g_sif_module_log_count = 0;
inline uint32_t g_sif_module_log_count = 0;
File diff suppressed because it is too large Load Diff
@@ -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);
+5 -127
View File
@@ -5,10 +5,8 @@
#include "ps2_syscalls.h"
#include "ps2_log.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
#include <optional>
@@ -60,57 +58,6 @@ namespace
return leaf.string();
}
uint32_t crc32Update(uint32_t crc, const uint8_t *data, size_t size)
{
static std::array<uint32_t, 256> table = []()
{
std::array<uint32_t, 256> 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<uint8_t, 4096> chunk{};
uint32_t crc = 0xFFFFFFFFu;
while (file.good())
{
file.read(reinterpret_cast<char *>(chunk.data()), static_cast<std::streamsize>(chunk.size()));
const std::streamsize got = file.gcount();
if (got <= 0)
{
break;
}
crc = crc32Update(crc, chunk.data(), static_cast<size_t>(got));
}
crcOut = ~crc;
return true;
}
std::optional<PS2Runtime::RecompiledFunction> 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<Descriptor> 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);
}
+178 -107
View File
@@ -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 <typename T>
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<std::mutex> 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<std::mutex> 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()
? "<core only>"
: 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<void *>(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<SifRpcDebugEvent> 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<unsigned long long>(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"))
-105
View File
@@ -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;
}
-37
View File
@@ -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;
}
}
-514
View File
@@ -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 <algorithm>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <string>
#include <unordered_map>
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<uint32_t, ClFileHandle> g_clFileHandles;
std::unordered_map<uint32_t, ClFileLoad> 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<const char *>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint32_t>(recvSize, 0x40u));
}
}
auto writeResult = [&](int32_t status, uint32_t value) {
if (recvBufAddr && recvSize >= sizeof(uint32_t))
{
writeGuestU32(rdram, recvBufAddr + 0u, static_cast<uint32_t>(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<uint32_t>(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<uint64_t>(end);
fileSize = static_cast<uint32_t>(std::min<uint64_t>(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<uint64_t>(PS2_RAM_SIZE - dstOffset);
const uint64_t maxRequestedBytes = requestedBytes ? requestedBytes : hostFileSize;
const size_t bytesToCopy = static_cast<size_t>(
std::min<uint64_t>({hostFileSize, maxRequestedBytes, maxGuestBytes}));
status = kClFileLoadStatusComplete;
if (dst && bytesToCopy > 0u)
{
file.seekg(0, std::ios::beg);
file.read(reinterpret_cast<char *>(dst), static_cast<std::streamsize>(bytesToCopy));
if (static_cast<size_t>(file.gcount()) != bytesToCopy)
{
status = kClFileLoadStatusFailed;
}
}
}
}
}
}
uint32_t loadHandle = 0u;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<uint32_t>(std::min<long>(end, 0x7FFFFFFF));
}
std::fseek(file, 0, SEEK_SET);
}
uint32_t handle = 0u;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint32_t>(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<uint32_t>(bytesRead));
return true;
}
case 0x03u:
{
uint32_t handle = 0u;
readGuestU32(rdram, sendBufAddr, handle);
std::lock_guard<std::mutex> lock(g_clFileMutex);
auto loadIt = g_clFileLoads.find(handle);
if (loadIt != g_clFileLoads.end())
{
writeResult(static_cast<int32_t>(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<std::mutex> 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<std::mutex> 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;
}
}
-89
View File
@@ -1,89 +0,0 @@
#include "runtime/ps2_iop_dbcman.h"
#include "runtime/ps2_memory.h"
#include <cstring>
#include <iostream>
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;
}
}
}
+503
View File
@@ -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 <algorithm>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <limits>
#include <utility>
#if !defined(_WIN32)
#include <sys/types.h>
#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<std::mutex> 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<uint32_t>::max() - address)
{
return false;
}
uint8_t *const first = getMemPtr(rdram, address);
uint8_t *const last = getMemPtr(rdram, address + static_cast<uint32_t>(size - 1));
if (!first || !last || last < first || static_cast<size_t>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint64_t>(std::numeric_limits<int64_t>::max()) ||
_fseeki64(it->second.stream, static_cast<int64_t>(offset), SEEK_SET) != 0)
#else
if (offset > static_cast<uint64_t>(std::numeric_limits<off_t>::max()) ||
fseeko(it->second.stream, static_cast<off_t>(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<std::mutex> 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<int>(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;
}
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include "ps2x/iop/iop_host.h"
#include <cstdint>
#include <cstdio>
#include <mutex>
#include <unordered_map>
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<std::recursive_mutex> 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<uint64_t, HostFile> m_hostFiles;
uint64_t m_nextHostFileHandle = 1u;
};
-325
View File
@@ -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 <algorithm>
#include <array>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
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<std::filesystem::path, 2> 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<std::streamoff>(offsetBytes), std::ios::beg);
if (!file.good())
{
return false;
}
file.read(reinterpret_cast<char *>(dst), static_cast<std::streamsize>(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<size_t>(
std::min<uint64_t>(requested, static_cast<uint64_t>(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<uint64_t>(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<size_t>(std::min<uint64_t>(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<size_t>(
std::min<uint64_t>(byteCount, static_cast<uint64_t>(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<uint64_t>(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<uint32_t>(sendSize / kCommandBytes, 32u);
for (uint32_t commandIndex = 0; commandIndex < commandCount; ++commandIndex)
{
std::array<uint32_t, 8> 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<size_t>(
std::min<uint64_t>(byteCount, static_cast<uint64_t>(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;
}
}
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include "ps2_runtime.h"
#include "ps2x/iop/iop_subsystem.h"
#include <utility>
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();
}
}
};
+123 -7
View File
@@ -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 <iostream>
#include <fstream>
@@ -107,6 +109,48 @@ namespace
thread_local DispatchHistory g_dispatchHistory;
thread_local std::unordered_map<PS2Runtime *, uint32_t> 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<uint32_t, 256> table = []
{
std::array<uint32_t, 256> 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<uint8_t, 16 * 1024> buffer{};
while (file.good())
{
file.read(reinterpret_cast<char *>(buffer.data()), static_cast<std::streamsize>(buffer.size()));
const std::streamsize count = file.gcount();
for (std::streamsize i = 0; i < count; ++i)
{
crc = table[(crc ^ buffer[static_cast<size_t>(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<PS2IopHostAdapter>(*this);
m_iopSubsystem = std::make_unique<ps2x::iop::IopSubsystem>(*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<std::filesystem::path> 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();
+47 -1
View File
@@ -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="$<TARGET_FILE_DIR:ps2_iop_fake_plugin>"
)
add_dependencies(ps2_test_lib
ps2_iop_fake_plugin
ps2_iop_bad_abi_plugin
ps2_iop_missing_symbol_plugin
)
endif()
add_executable(ps2x_tests
src/main.cpp
$<TARGET_OBJECTS:ps2_test_function_table>
+15
View File
@@ -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;
}
+6
View File
@@ -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;
}
+350
View File
@@ -0,0 +1,350 @@
#include "ps2x/iop/plugin_api.h"
#include <cstdint>
#include <cstring>
#include <new>
namespace
{
constexpr uint32_t kSyntheticSid = 0xF00DCAFEu;
constexpr uint32_t kCoreCollisionSid = 0x80001300u;
constexpr uint32_t kSyntheticFunction = 0x42u;
constexpr uint32_t kCoreCollisionFunction = 0x99u;
constexpr uint32_t kSyntheticEntryPoint = 0x00123456u;
constexpr uint32_t kSpecificRecvXEntryPoint = kSyntheticEntryPoint + 0x100u;
constexpr uint32_t kSyntheticCrc32 = 0xA1B2C3D4u;
constexpr uint32_t kResponseXor = 0xA5A55A5Au;
constexpr uint32_t kCoreCollisionResponse = 0xC0DEF00Du;
template <size_t Size>
constexpr ps2x_iop_string_view_v1 stringView(const char (&value)[Size])
{
return {value, Size - 1u};
}
struct FakePluginState
{
const ps2x_iop_host_api_v1 *host = nullptr;
uint64_t resetGeneration = 0;
uint64_t rpcCalls = 0;
uint64_t transfers = 0;
};
void log(FakePluginState *state, uint32_t level, ps2x_iop_string_view_v1 message)
{
if (state && state->host && state->host->log)
{
state->host->log(state->host->userdata, level, message);
}
}
void *createProfile(const ps2x_iop_host_api_v1 *host,
const ps2x_iop_game_identity_v1 *identity)
{
if (!host || host->abi_version != PS2X_IOP_ABI_VERSION_V1 ||
host->struct_size < sizeof(*host) || !identity ||
identity->struct_size < sizeof(*identity) ||
(identity->entry_point != kSyntheticEntryPoint &&
identity->entry_point != kSpecificRecvXEntryPoint) ||
identity->crc32 != kSyntheticCrc32)
{
return nullptr;
}
auto *state = new (std::nothrow) FakePluginState{};
if (!state)
{
return nullptr;
}
state->host = host;
log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-create"));
return state;
}
void destroyProfile(void *instance)
{
auto *state = static_cast<FakePluginState *>(instance);
log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-destroy"));
delete state;
}
int32_t resetProfile(void *instance)
{
auto *state = static_cast<FakePluginState *>(instance);
if (!state)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
++state->resetGeneration;
state->rpcCalls = 0;
state->transfers = 0;
return PS2X_IOP_STATUS_OK_V1;
}
uint32_t selectRpcAbi(void *instance, const ps2x_iop_rpc_abi_request_v1 *request)
{
if (!instance || !request || request->struct_size < sizeof(*request))
{
return PS2X_IOP_RPC_ABI_DEFAULT_V1;
}
if (request->bound_sid == kSyntheticSid && request->function == kSyntheticFunction)
{
return PS2X_IOP_RPC_ABI_STACK_V1;
}
return PS2X_IOP_RPC_ABI_DEFAULT_V1;
}
int32_t handleRpc(void *instance,
const ps2x_iop_rpc_request_v1 *request,
ps2x_iop_rpc_result_v1 *result)
{
auto *state = static_cast<FakePluginState *>(instance);
if (!state || !request || request->struct_size < sizeof(*request) ||
!result || result->struct_size < sizeof(*result))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
result->handled = 0u;
result->result_address = 0u;
result->signal_nowait_completion = 0u;
result->callback_policy = PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1;
if (request->sid == kCoreCollisionSid &&
request->function == kCoreCollisionFunction)
{
if (request->receive.size < sizeof(kCoreCollisionResponse) ||
!state->host->write_guest ||
state->host->write_guest(state->host->userdata,
request->receive.address,
&kCoreCollisionResponse,
sizeof(kCoreCollisionResponse)) !=
PS2X_IOP_STATUS_OK_V1)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
++state->rpcCalls;
result->handled = 1u;
result->result_address = request->receive.address;
return PS2X_IOP_STATUS_OK_V1;
}
if (request->sid != kSyntheticSid || request->function != kSyntheticFunction)
{
return PS2X_IOP_STATUS_OK_V1;
}
uint32_t input = 0u;
if (request->send.size < sizeof(input) || !state->host->read_guest ||
state->host->read_guest(state->host->userdata,
request->send.address,
&input,
sizeof(input)) != PS2X_IOP_STATUS_OK_V1)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
const uint32_t output = input ^ kResponseXor;
if (request->receive.size < sizeof(output) || !state->host->write_guest ||
state->host->write_guest(state->host->userdata,
request->receive.address,
&output,
sizeof(output)) != PS2X_IOP_STATUS_OK_V1)
{
return PS2X_IOP_STATUS_FAILED_V1;
}
++state->rpcCalls;
result->handled = 1u;
result->result_address = request->receive.address;
result->signal_nowait_completion = 1u;
result->callback_policy = PS2X_IOP_CALLBACK_SUPPRESS_V1;
return PS2X_IOP_STATUS_OK_V1;
}
int32_t onSifTransfer(void *instance, const ps2x_iop_sif_transfer_v1 *transfer)
{
auto *state = static_cast<FakePluginState *>(instance);
if (!state || !transfer || transfer->struct_size < sizeof(*transfer))
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
++state->transfers;
return PS2X_IOP_STATUS_OK_V1;
}
size_t debugMetricCount(void *instance)
{
return instance ? 3u : 0u;
}
int32_t debugMetric(void *instance, size_t index, ps2x_iop_debug_metric_v1 *metric)
{
const auto *state = static_cast<const FakePluginState *>(instance);
if (!state || !metric || metric->struct_size < sizeof(*metric) || index >= 3u)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
metric->struct_size = sizeof(*metric);
metric->hexadecimal = 0u;
switch (index)
{
case 0u:
metric->name = stringView("reset_generation");
metric->value = state->resetGeneration;
break;
case 1u:
metric->name = stringView("rpc_calls");
metric->value = state->rpcCalls;
break;
case 2u:
metric->name = stringView("sif_transfers");
metric->value = state->transfers;
break;
default:
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
return PS2X_IOP_STATUS_OK_V1;
}
constexpr uint32_t kSids[] = {kSyntheticSid, kCoreCollisionSid};
constexpr uint32_t kDuplicateSids[] = {kSyntheticSid, kSyntheticSid};
constexpr uint32_t kAmbiguousSids[] = {kSyntheticSid};
const ps2x_iop_profile_api_v1 kProfiles[] = {
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-test-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("synthetic_iop_test.elf"),
kSyntheticEntryPoint,
kSyntheticCrc32,
},
2u,
kSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-duplicate-sid-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("synthetic_duplicate.elf"),
kSyntheticEntryPoint,
kSyntheticCrc32,
},
2u,
kDuplicateSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-ambiguous-recvx-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("slus_201.84"),
0u,
0u,
},
1u,
kAmbiguousSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-specific-recvx-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("slus_201.84"),
kSpecificRecvXEntryPoint,
kSyntheticCrc32,
},
1u,
kAmbiguousSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
{
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_profile_api_v1),
stringView("synthetic-invalid-large-sid-profile"),
{
sizeof(ps2x_iop_game_matcher_v1),
stringView("synthetic_invalid.elf"),
kSyntheticEntryPoint,
kSyntheticCrc32,
},
257u,
kAmbiguousSids,
&createProfile,
&destroyProfile,
&resetProfile,
&selectRpcAbi,
&handleRpc,
&onSifTransfer,
&debugMetricCount,
&debugMetric,
},
};
const ps2x_iop_plugin_api_v1 kPlugin = {
PS2X_IOP_ABI_VERSION_V1,
sizeof(ps2x_iop_plugin_api_v1),
stringView("ps2x-test-plugin"),
stringView("1.0.0"),
5u,
kProfiles,
};
}
extern "C" PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1(
uint32_t hostAbiVersion,
ps2x_iop_plugin_api_v1 *pluginApi)
{
if (hostAbiVersion != PS2X_IOP_ABI_VERSION_V1)
{
return PS2X_IOP_STATUS_UNSUPPORTED_V1;
}
if (!pluginApi)
{
return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1;
}
if (pluginApi->struct_size < sizeof(*pluginApi))
{
return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1;
}
*pluginApi = kPlugin;
return PS2X_IOP_STATUS_OK_V1;
}
+2
View File
@@ -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();
+816
View File
@@ -0,0 +1,816 @@
#include "MiniTest.h"
#include "ps2x/iop/iop_subsystem.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <limits>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#elif defined(__linux__)
#include <dlfcn.h>
#endif
namespace
{
using namespace ps2x::iop;
constexpr uint32_t kSyntheticSid = 0xF00DCAFEu;
constexpr uint32_t kCoreCollisionSid = 0x80001300u;
constexpr uint32_t kSyntheticFunction = 0x42u;
constexpr uint32_t kCoreCollisionFunction = 0x99u;
constexpr uint32_t kSyntheticEntryPoint = 0x00123456u;
constexpr uint32_t kSpecificRecvXEntryPoint = kSyntheticEntryPoint + 0x100u;
constexpr uint32_t kSyntheticCrc32 = 0xA1B2C3D4u;
constexpr uint32_t kResponseXor = 0xA5A55A5Au;
constexpr uint32_t kCoreCollisionResponse = 0xC0DEF00Du;
class FakeIopHost final : public IopHost
{
public:
explicit FakeIopHost(size_t memorySize = 0x10000u)
: memory(memorySize, 0u)
{
}
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<uint64_t>(nextGuestAddress) + effectiveAlignment - 1u) /
effectiveAlignment) *
effectiveAlignment;
if (aligned + size > memory.size())
{
return 0u;
}
nextGuestAddress = static_cast<uint32_t>(aligned + size);
guestAllocations.push_back(static_cast<uint32_t>(aligned));
return static_cast<uint32_t>(aligned);
}
void freeGuest(uint32_t address) override
{
freedGuestAddresses.push_back(address);
}
void audioCommand(uint32_t sid,
uint32_t function,
GuestBuffer send,
GuestBuffer receive) override
{
lastAudioSid = sid;
lastAudioFunction = function;
lastAudioSend = send;
lastAudioReceive = receive;
++audioCalls;
}
std::string hostPath(HostPathKind kind) const override
{
switch (kind)
{
case HostPathKind::CdRoot:
return "fake/cd";
case HostPathKind::CdImage:
return "fake/disc.iso";
case HostPathKind::HostRoot:
return "fake/host";
case HostPathKind::MemoryCardRoot:
return "fake/mc0";
default:
return "fake/elf";
}
}
std::string translateGuestPath(std::string_view path) const override
{
return "translated/" + std::string(path);
}
uint64_t openHostFile(std::string_view path) override
{
const auto file = hostFileContents.find(std::string(path));
if (file == hostFileContents.end())
{
return 0u;
}
const uint64_t handle = nextHostFileHandle++;
openHostFiles.emplace(handle, file->first);
return handle;
}
bool hostFileSize(uint64_t handle, uint64_t &size) const override
{
size = 0u;
const auto open = openHostFiles.find(handle);
if (open == openHostFiles.end())
{
return false;
}
const auto file = hostFileContents.find(open->second);
if (file == hostFileContents.end())
{
return false;
}
size = file->second.size();
return true;
}
bool readHostFile(uint64_t handle,
uint64_t offset,
void *destination,
size_t size,
size_t &bytesRead) override
{
bytesRead = 0u;
if (!destination && size != 0u)
{
return false;
}
const auto open = openHostFiles.find(handle);
if (open == openHostFiles.end())
{
return false;
}
const auto file = hostFileContents.find(open->second);
if (file == hostFileContents.end() || offset > file->second.size())
{
return false;
}
bytesRead = std::min<size_t>(size, file->second.size() - static_cast<size_t>(offset));
if (bytesRead != 0u)
{
std::memcpy(destination,
file->second.data() + static_cast<size_t>(offset),
bytesRead);
}
return true;
}
void closeHostFile(uint64_t handle) override
{
if (openHostFiles.erase(handle) != 0u)
{
closedHostFileHandles.push_back(handle);
}
}
int32_t memoryCard(const MemoryCardRequest &request) override
{
lastMemoryCardRequest = request;
++memoryCardCalls;
return 0;
}
bool hasGuestFunction(uint32_t address) const override
{
return address == guestFunctionAddress;
}
bool invokeGuestFunction(uint64_t callToken,
uint32_t address,
uint32_t a0,
uint32_t a1,
uint32_t a2,
uint32_t a3,
uint32_t *resultAddress) override
{
if (!hasGuestFunction(address))
{
return false;
}
lastCallToken = callToken;
lastGuestArguments = {a0, a1, a2, a3};
if (resultAddress)
{
*resultAddress = guestFunctionResult;
}
return true;
}
void log(LogLevel level, std::string_view message) override
{
logs.emplace_back(level, std::string(message));
}
bool writeWord(uint32_t address, uint32_t value)
{
return writeGuest(address, &value, sizeof(value));
}
uint32_t readWord(uint32_t address) const
{
uint32_t value = 0u;
(void)readGuest(address, &value, sizeof(value));
return value;
}
bool hasLog(std::string_view expected) const
{
return std::any_of(logs.begin(), logs.end(), [&](const auto &entry)
{ return entry.second == expected; });
}
std::vector<uint8_t> memory;
uint32_t nextHandle = 0x8000u;
uint32_t nextGuestAddress = 0x4000u;
std::vector<uint32_t> guestAllocations;
std::vector<uint32_t> freedGuestAddresses;
uint32_t audioCalls = 0u;
uint32_t lastAudioSid = 0u;
uint32_t lastAudioFunction = 0u;
GuestBuffer lastAudioSend{};
GuestBuffer lastAudioReceive{};
uint32_t memoryCardCalls = 0u;
MemoryCardRequest lastMemoryCardRequest{};
uint32_t guestFunctionAddress = 0x2000u;
uint32_t guestFunctionResult = 0x3000u;
uint64_t lastCallToken = 0u;
std::vector<uint32_t> lastGuestArguments;
std::vector<std::pair<LogLevel, std::string>> logs;
std::unordered_map<std::string, std::vector<uint8_t>> hostFileContents;
std::unordered_map<uint64_t, std::string> openHostFiles;
std::vector<uint64_t> closedHostFileHandles;
uint64_t nextHostFileHandle = 1u;
private:
bool contains(uint32_t address, size_t size) const
{
const uint64_t end = static_cast<uint64_t>(address) + static_cast<uint64_t>(size);
return end <= memory.size();
}
};
bool containsDiagnostic(const DebugSnapshot &snapshot, std::string_view text)
{
return std::any_of(snapshot.diagnostics.begin(), snapshot.diagnostics.end(), [&](const std::string &diagnostic)
{ return diagnostic.find(text) != std::string::npos; });
}
const DebugService *findService(const DebugSnapshot &snapshot, std::string_view name)
{
const auto it = std::find_if(snapshot.services.begin(), snapshot.services.end(), [&](const DebugService &service)
{ return service.name == name; });
return it == snapshot.services.end() ? nullptr : &*it;
}
uint64_t metricValue(const DebugService &service, std::string_view name)
{
const auto it = std::find_if(service.metrics.begin(), service.metrics.end(), [&](const DebugMetric &metric)
{ return metric.name == name; });
return it == service.metrics.end() ? std::numeric_limits<uint64_t>::max() : it->value;
}
bool pluginModuleIsLoaded(const std::filesystem::path &path)
{
#if defined(_WIN32)
return GetModuleHandleW(path.c_str()) != nullptr;
#elif defined(__linux__)
void *handle = dlopen(path.c_str(), RTLD_NOW | RTLD_NOLOAD);
if (!handle)
{
return false;
}
dlclose(handle);
return true;
#else
(void)path;
return false;
#endif
}
}
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<uint8_t, 8> command{};
command[0] = 0x29u;
command[1] = 0u;
t.IsTrue(host.writeGuest(kCommandAddress, command.data(), command.size()),
"valid TSNDDRV command should be writable");
ps2x::iop::RpcRequest commandRequest{};
commandRequest.sid = 0u;
commandRequest.function = 0u;
commandRequest.send = {kCommandAddress, static_cast<uint32_t>(command.size())};
t.IsTrue(subsystem.handleRpc(commandRequest).handled,
"TSNDDRV should handle the characterized command queue");
int16_t writtenChecksum = 0;
t.IsTrue(host.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
});
}
+119 -5
View File
@@ -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<uint8_t> 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<uint8_t> 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)
{
+18 -27
View File
@@ -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 <array>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
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;
+374 -61
View File
@@ -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 <array>
#include <atomic>
@@ -11,6 +11,8 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <stdexcept>
#include <vector>
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<uint32_t> g_lotrSoundCallbackHits{0u};
std::atomic<uint32_t> g_recvxSoundCallbackHits{0u};
std::atomic<uint32_t> g_dtxDispatcherHits{0u};
std::atomic<uint32_t> g_dtxDispatcherRpcNum{0u};
std::atomic<uint32_t> g_dtxDispatcherSendBuf{0u};
std::atomic<uint32_t> g_dtxDispatcherSendSize{0u};
constexpr uint32_t K_DTX_DISPATCH_RESULT_ADDR = 0x0002D800u;
constexpr uint32_t K_DTX_DISPATCH_RESULT_MARKER = 0xD15CA7C1u;
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<int64_t>(K_DTX_DISPATCH_RESULT_ADDR));
ctx->pc = ::getRegU32(ctx, 31);
}
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
{
ctx.r[reg] = _mm_set_epi64x(0, static_cast<int64_t>(value));
@@ -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<uint8_t> 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<uint32_t>(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<uint32_t>(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<uint8_t>(0),
"SDRDRV load status should report completed");
});
tc.Run("MCSERV RPC init and get info report a formatted PS2 card", [](TestCase &t)
{
TestEnv env;
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<int32_t>(env.rdram.data(), kRecvAddr + 0u), 0,
"MCSERV init result should succeed");
t.IsTrue(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u) >= 0x205u,
"MCSERV init should expose a supported mcserv version");
t.IsTrue(readGuestStruct<uint32_t>(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<int32_t>(env.rdram.data(), kRecvAddr), 0,
"get info result should succeed");
t.Equals(readGuestStruct<int32_t>(env.rdram.data(), kEndParamAddr + 0u), 2,
"get info should report PS2 card type");
t.Equals(readGuestStruct<int32_t>(env.rdram.data(), kEndParamAddr + 4u), 0x2000,
"get info should report free clusters");
t.Equals(readGuestStruct<int32_t>(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<uint32_t>(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<uint32_t, 5> 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<uint32_t>(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<uint8_t>(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<uint8_t> 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<uint32_t>(semaId));
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), KE_OK, "RECVX snddrv submission should complete");
};
constexpr uint32_t kBusyBeforeCompletion = 0x11111111u;
writeGuestU32(env.rdram.data(), kBusyFlagAddr, kBusyBeforeCompletion);
callSoundDriver(kCompletionOnlyCallback);
t.Equals(g_recvxSoundCallbackHits.load(), 0u,
"recognized RECVX completion callback should not execute guest code");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kBusyFlagAddr), kBusyBeforeCompletion,
"completion-only callback should preserve the RECVX busy flag");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), semaId, "completion-only callback should signal its semaphore");
writeGuestU32(env.rdram.data(), kBusyFlagAddr, 0x22222222u);
callSoundDriver(kClearBusyCallback);
t.Equals(g_recvxSoundCallbackHits.load(), 0u,
"recognized RECVX clear-busy callback should not execute guest code");
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kBusyFlagAddr), 0u,
"designated RECVX callback should clear the guest busy flag");
setRegU32(env.ctx, 4, static_cast<uint32_t>(semaId));
PollSema(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), semaId, "clear-busy callback should signal its semaphore");
setRegU32(env.ctx, 4, kClientAddr);
SifCheckStatRpc(env.rdram.data(), &env.ctx, &env.runtime);
t.Equals(getRegS32(env.ctx, 2), 0, "RECVX snddrv client should no longer be RPC-busy");
});
tc.Run("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<uint32_t>(env.rdram.data(), kRecvAddr), kFallbackValue,
"fallback DTX emulation should return the first send word for an unknown command");
writeGuestU32(env.rdram.data(), kFnTableSlot, kRegisteredHandlerAddr);
writeGuestU32(env.rdram.data(), kRecvAddr, 0u);
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<uint32_t>(env.rdram.data(), kRecvAddr), K_DTX_DISPATCH_RESULT_MARKER,
"SifCallRpc should copy the dispatcher result into the receive buffer");
});
tc.Run("SifCallRpc prefers stack ABI for DTX URPC when both packs look plausible", [](TestCase &t)
{
TestEnv env;
setRecvxDtxCompatLayout();
configureProfile(env, "slus_201.84");
constexpr uint32_t kClientAddr = 0x0002B000u;
constexpr uint32_t kDtxSid = 0x7D000000u;