mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-29 17:58:24 -04:00
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:
@@ -76,6 +76,7 @@ if(PS2X_BUILD_RECOMP)
|
||||
endif()
|
||||
|
||||
if(PS2X_BUILD_RUNTIME)
|
||||
add_subdirectory("ps2xIOP")
|
||||
add_subdirectory("ps2xRuntime")
|
||||
endif()
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, ¶meter, sizeof(parameter)))
|
||||
{
|
||||
result = handleNameOperation(operation, request.send.address, parameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DescriptorParameter parameter{};
|
||||
if (request.send.address != 0u &&
|
||||
request.send.size >= sizeof(parameter) &&
|
||||
m_host.readGuest(request.send.address, ¶meter, sizeof(parameter)))
|
||||
{
|
||||
if (operation == Operation::Write && parameter.origin > 0 &&
|
||||
parameter.origin <= static_cast<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 ¶meter)
|
||||
{
|
||||
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 ¶meter)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case Operation::GetInfo:
|
||||
{
|
||||
uint32_t typeAddress = 0u;
|
||||
uint32_t freeAddress = 0u;
|
||||
uint32_t formatAddress = 0u;
|
||||
if (parameter.parameter != 0u)
|
||||
{
|
||||
const uint32_t outputSize = flavor == Flavor::NewXmcserv ? 192u : 64u;
|
||||
(void)m_host.zeroGuest(parameter.parameter, outputSize);
|
||||
typeAddress = parameter.parameter;
|
||||
freeAddress = parameter.parameter + 4u;
|
||||
if (flavor == Flavor::NewXmcserv)
|
||||
{
|
||||
formatAddress = parameter.parameter + 144u;
|
||||
}
|
||||
}
|
||||
return call(MemoryCardOperation::GetInfo,
|
||||
static_cast<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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user