mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
feat: IOP emulator
refactor: codegen to catch callbacks on mips code feat: added a lot of entries or IOP emulator
This commit is contained in:
@@ -79,9 +79,7 @@ Fallback workflow for quick local experiments or ELFs with debug symbol :
|
||||
./ps2_analyzer your_game.elf config.toml
|
||||
```
|
||||
|
||||
Use this only when you do not have a Ghidra project yet. The native analyzer is faster to start, but it is less accurate on stripped retail games and more likely to miss internal callable entry points.
|
||||
|
||||
See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for the recommended path.
|
||||
See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for ghdira instructions.
|
||||
|
||||
Then build generated output and link with `ps2xRuntime`.
|
||||
|
||||
@@ -164,9 +162,8 @@ Use Game Override modules when:
|
||||
6. Re-test from cold boot after each batch.
|
||||
|
||||
### Limitations
|
||||
|
||||
* Graphics Synthesizer and other hardware components need external implementation
|
||||
* VU1 microcode is not complete.
|
||||
|
||||
* Performance is very bad for VU and GS
|
||||
* Hardware emulation is partial and many paths are stubbed.
|
||||
|
||||
### Acknowledgments
|
||||
@@ -175,3 +172,4 @@ Use Game Override modules when:
|
||||
* Uses ELFIO for ELF parsing
|
||||
* Uses toml11 for TOML parsing
|
||||
* Uses fmt for string formatting
|
||||
* Reference for runtime PCSX2
|
||||
@@ -32,8 +32,7 @@ Japanese set, and depends on samples that retained relocations. Treat the result
|
||||
high-confidence hint rather than a complete SDK catalog: it can miss SDK variants that
|
||||
were not present in the sampled games, and ambiguous matches are intentionally ignored.
|
||||
|
||||
### 4. Ghidra Integration (For Retail and Stripped Games, Preferred)
|
||||
This is the recommended workflow for almost every commercial game:
|
||||
### 4. Ghidra Integration
|
||||
1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
2. Run it in Ghidra to export a CSV map of all functions.
|
||||
3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`.
|
||||
@@ -63,8 +62,7 @@ ps2_analyzer <input_elf> <output_toml> [sce_symbol_db_dir]
|
||||
1. Open `game.elf` in Ghidra.
|
||||
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
3. Use the exported TOML and CSV.
|
||||
4. Run the recompiler:
|
||||
`ps2recomp config.toml`
|
||||
4. Run the recompiler: `ps2recomp config.toml`
|
||||
|
||||
Fallback:
|
||||
1. Run `ps2_analyzer game.elf config.toml`.
|
||||
@@ -74,8 +72,8 @@ Fallback:
|
||||
The tool creates a TOML file with the following sections:
|
||||
* `[general]`: Paths to ELF and Ghidra maps.
|
||||
* `stubs`: Runtime-known functions to be replaced by C++ stubs or syscall handlers.
|
||||
* `untracked_stubs`: Detected library-like functions without runtime handlers. This is
|
||||
informational only and is ignored by the recompiler.
|
||||
* `untracked_stubs`: Detected library-like functions without runtime handlers. This is informational only and is ignored by the recompiler.
|
||||
* `entry_points`: Guest functions without runtime handlers that may be referenced by address.
|
||||
* `skip`: Legacy compatibility field. The analyzer no longer auto-populates it.
|
||||
* `[patches]`: Individual instructions that need to be replaced (SYSCALLs, COP0, etc.).
|
||||
|
||||
@@ -83,6 +81,5 @@ The tool creates a TOML file with the following sections:
|
||||
|
||||
* Heuristics may not catch all special cases in highly optimized code.
|
||||
* Self-modifying code is flagged but requires manual review.
|
||||
* Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries.
|
||||
|
||||
For more details on the recompilation process, see the [Main README](../README.md).
|
||||
|
||||
@@ -138,9 +138,9 @@ namespace ps2recomp
|
||||
}
|
||||
file << "]\n\n";
|
||||
|
||||
file << "# Detected library-like functions without runtime handlers.\n";
|
||||
file << "# This is informational only; PS2Recomp ignores this list and recompiles them normally.\n";
|
||||
file << "untracked_stubs = [\n";
|
||||
file << "# Guest functions without runtime handlers that may be referenced by address.\n";
|
||||
file << "# PS2Recomp keeps their guest implementation and exposes exact callable entries.\n";
|
||||
file << "entry_points = [\n";
|
||||
for (const auto &func : untrackedStubEntries)
|
||||
{
|
||||
file << " \"" << func << "\",\n";
|
||||
|
||||
@@ -3,9 +3,19 @@ cmake_minimum_required(VERSION 3.21)
|
||||
project(ps2xIOP LANGUAGES CXX)
|
||||
|
||||
option(PS2X_IOP_ENABLE_PLUGINS "Enable dynamic ps2xIOP plugins" OFF)
|
||||
option(PS2X_IOP_BUILD_TESTS "Build ps2xIOP emulator smoke tests" OFF)
|
||||
|
||||
add_library(ps2_iop STATIC
|
||||
src/iop_subsystem.cpp
|
||||
src/emulator/iop_emulator.cpp
|
||||
src/emulator/iop_cdvd.cpp
|
||||
src/emulator/iop_cpu.cpp
|
||||
src/emulator/iop_imports.cpp
|
||||
src/emulator/iop_kernel.cpp
|
||||
src/emulator/iop_memory.cpp
|
||||
src/emulator/iop_module_loader.cpp
|
||||
src/emulator/iop_rpc.cpp
|
||||
src/emulator/iop_sysclib.cpp
|
||||
src/builtin_profiles.cpp
|
||||
src/plugin_loader.cpp
|
||||
src/modules/dbcman.cpp
|
||||
@@ -38,6 +48,13 @@ if(PS2X_IOP_ENABLE_PLUGINS AND UNIX AND NOT APPLE)
|
||||
target_link_libraries(ps2_iop PRIVATE ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
|
||||
if(PS2X_IOP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(ps2_iop_emulator_tests tests/iop_emulator_tests.cpp)
|
||||
target_link_libraries(ps2_iop_emulator_tests PRIVATE ps2_iop)
|
||||
add_test(NAME ps2_iop_emulator_tests COMMAND ps2_iop_emulator_tests)
|
||||
endif()
|
||||
|
||||
install(TARGETS ps2_iop
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
|
||||
+24
-57
@@ -1,57 +1,22 @@
|
||||
# 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.
|
||||
`ps2xIOP` is the IOP subsystem used by `ps2xRuntime`. It combines high-level service/profile path with an R3000A-backed IRX execution path.
|
||||
No PS2 BIOS is required by the emulator backend: IRX imports for the kernel-facing libraries are handled by a small virtual IOP kernel, while the IRX module itself executes as original MIPS code.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `ps2_iop`/`ps2x::iop` is a C++20 static library linked into the runtime.
|
||||
> Optional `.dll` and `.so` files are native profile plugins 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.
|
||||
> Optional `.dll` and `.so` files are native profile plugins. They extend
|
||||
> the HLE profile catalog; they are not PS2 IRX modules.
|
||||
|
||||
## Architecture
|
||||
## Execution policy
|
||||
|
||||
```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.
|
||||
The subsystem is always hybrid. Original IRX modules execute on the R3000A
|
||||
path, while core and game-profile HLE services are available for endpoints
|
||||
that the loaded modules do not provide. A server registered by a physical IRX
|
||||
is normally authoritative for its SID; HLE is the fallback. A profile service
|
||||
may explicitly replace a physical endpoint when it is a compatibility stub.
|
||||
There is no runtime mode
|
||||
switch or environment variable to create divergent boot paths.
|
||||
|
||||
## Built-in services and profiles
|
||||
|
||||
@@ -99,13 +64,14 @@ the shadowed core service.
|
||||
|
||||
## Dispatch and transfer flow
|
||||
|
||||
`IopSubsystem` exposes five operations used by the runtime:
|
||||
`IopSubsystem` exposes the profile/HLE operations plus emulator lifecycle entry points:
|
||||
|
||||
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.
|
||||
1. `configure(GameIdentity)` selects the active compatibility profile.
|
||||
2. `reset()` resets both active HLE services and the emulator state.
|
||||
3. `loadModule(...)` / `loadModuleBuffer(...)` load and start an IRX.
|
||||
4. `stopModule(...)` releases an emulated module and its owned runtime state.
|
||||
5. `runEeCycles(...)` advances the IOP from EE cycle accounting.
|
||||
6. `selectRpcAbi(...)`, `handleRpc(...)`, and `onSifTransfer(...)` provide the SIF transport bridge.
|
||||
|
||||
`RpcResult::handled` indicates whether a service consumed the request. The
|
||||
result can also request completion semaphore signals and can suppress the
|
||||
@@ -142,7 +108,7 @@ Linux with `PS2X_IOP_ENABLE_PLUGINS=ON`.
|
||||
| Windows | `.dll` | Supported |
|
||||
| Linux | `.so` | Supported |
|
||||
|
||||
When enable By default, the runtime scans `iop_plugins/` next to the executable. Discovery
|
||||
When enabled, the runtime scans `iop_plugins/` next to the executable. Discovery
|
||||
is non-recursive. An embedding application can replace the search directories
|
||||
before calling `initialize()`:
|
||||
|
||||
@@ -209,9 +175,10 @@ 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.
|
||||
`debugSnapshot()` exposes emulator cycle/instruction counts, loaded
|
||||
IRX/thread/RPC-server counts, the active profile and provider, registered core
|
||||
and profile services, service metrics, loader diagnostics, and the last
|
||||
selection error. The runtime debugger renders this data in the **IOP/SIF** tab.
|
||||
|
||||
Registry behavior, instance isolation, reset, built-in services, profile
|
||||
precedence, plugin discovery, ABI rejection, ambiguity, dispatch, destruction,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
@@ -27,7 +28,13 @@ namespace ps2x::iop
|
||||
bool configure(const GameIdentity &identity, std::string *error = nullptr);
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result = nullptr);
|
||||
void runEeCycles(uint64_t eeCycles) noexcept;
|
||||
|
||||
[[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const;
|
||||
[[nodiscard]] bool canBindRpc(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace ps2x::iop
|
||||
uint32_t crc32 = 0;
|
||||
};
|
||||
|
||||
struct ModuleLoadResult
|
||||
{
|
||||
bool handled = false;
|
||||
int32_t moduleId = -1;
|
||||
int32_t startResult = -1;
|
||||
};
|
||||
|
||||
enum class RpcAbi : uint32_t
|
||||
{
|
||||
RuntimeDefault = 0,
|
||||
@@ -137,6 +144,11 @@ namespace ps2x::iop
|
||||
|
||||
struct DebugSnapshot
|
||||
{
|
||||
uint64_t emulatorCycles = 0;
|
||||
uint64_t emulatorInstructions = 0;
|
||||
uint32_t emulatorLoadedModules = 0;
|
||||
uint32_t emulatorThreads = 0;
|
||||
uint32_t emulatorRpcServers = 0;
|
||||
std::string activeProfile;
|
||||
std::string activeProvider;
|
||||
std::vector<DebugService> services;
|
||||
|
||||
@@ -71,6 +71,7 @@ namespace ps2x::iop::detail
|
||||
.zeroReceiveBuffer = true,
|
||||
.signalNowaitCompletion = true,
|
||||
.completeQueuedPlayStreams = true,
|
||||
.overridePhysicalServer = true,
|
||||
.suppressedCompletionCallbacks = {},
|
||||
};
|
||||
}
|
||||
@@ -106,7 +107,6 @@ namespace ps2x::iop::detail
|
||||
std::vector<ProfileDefinition> createBuiltinProfiles()
|
||||
{
|
||||
std::vector<ProfileDefinition> profiles;
|
||||
|
||||
profiles.push_back({
|
||||
"recvx-us",
|
||||
"builtin",
|
||||
@@ -120,30 +120,31 @@ namespace ps2x::iop::detail
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
// TODO remove this on next release
|
||||
// profiles.push_back({
|
||||
// "lotr-two-towers-us",
|
||||
// "builtin",
|
||||
// {.elfName = "SLUS_205.78"},
|
||||
// [](IopHost &host, const GameIdentity &)
|
||||
// {
|
||||
// ServiceList services;
|
||||
// services.emplace_back(createClFileService(host, lotrClFileBindings()));
|
||||
// services.emplace_back(createSoundUpdateStubService(host, lotrSoundBindings()));
|
||||
// return services;
|
||||
// },
|
||||
// });
|
||||
|
||||
profiles.push_back({
|
||||
"fatal-frame-us",
|
||||
"builtin",
|
||||
{.elfName = "SLUS_203.88"},
|
||||
[](IopHost &host, const GameIdentity &)
|
||||
{
|
||||
ServiceList services;
|
||||
services.emplace_back(createSdrdrvService(host, fatalFrameSdrdrvBindings()));
|
||||
return services;
|
||||
},
|
||||
});
|
||||
// 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,596 @@
|
||||
#include "iop_cdvd.h"
|
||||
|
||||
#include "iop_cpu.h"
|
||||
#include "iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kSectorSize = 2048u;
|
||||
constexpr uint32_t kPrimaryVolumeDescriptorLsn = 16u;
|
||||
constexpr uint32_t kVolumeDescriptorTerminatorLsn = 17u;
|
||||
constexpr uint32_t kFirstDirectoryLsn = 20u;
|
||||
constexpr uint32_t kCdvdErrorNone = 0u;
|
||||
constexpr uint32_t kCdvdErrorRead = 0x30u;
|
||||
constexpr uint32_t kCdvdTypePs2Dvd = 0x14u;
|
||||
constexpr uint32_t kCdvdReadyComplete = 2u;
|
||||
constexpr uint32_t kCdvdStatusPause = 0x0Au;
|
||||
constexpr uint32_t kCdvdInitExit = 5u;
|
||||
constexpr uint32_t kCdvdCallbackRead = 1u;
|
||||
constexpr uint32_t kCdvdCallbackSeek = 4u;
|
||||
|
||||
uint32_t alignSectors(uint64_t bytes)
|
||||
{
|
||||
return static_cast<uint32_t>((bytes + kSectorSize - 1u) / kSectorSize);
|
||||
}
|
||||
|
||||
void writeLe16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
destination[0] = static_cast<uint8_t>(value);
|
||||
destination[1] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
|
||||
void writeBe16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
destination[0] = static_cast<uint8_t>(value >> 8u);
|
||||
destination[1] = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
void writeLe32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
destination[i] = static_cast<uint8_t>(value >> (i * 8u));
|
||||
}
|
||||
|
||||
void writeBe32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
destination[i] = static_cast<uint8_t>(value >> ((3u - i) * 8u));
|
||||
}
|
||||
|
||||
void writeBoth16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
writeLe16(destination, value);
|
||||
writeBe16(destination + 2u, value);
|
||||
}
|
||||
|
||||
void writeBoth32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
writeLe32(destination, value);
|
||||
writeBe32(destination + 4u, value);
|
||||
}
|
||||
|
||||
std::string isoName(const std::filesystem::path &path, bool directory)
|
||||
{
|
||||
std::string name = path.filename().string();
|
||||
for (char &character : name)
|
||||
{
|
||||
const unsigned char byte = static_cast<unsigned char>(character);
|
||||
character = byte < 0x80u ? static_cast<char>(std::toupper(byte)) : '_';
|
||||
}
|
||||
if (name.size() > 200u)
|
||||
name.resize(200u);
|
||||
if (!directory && name.find(';') == std::string::npos)
|
||||
name += ";1";
|
||||
return name;
|
||||
}
|
||||
|
||||
size_t directoryRecordSize(size_t identifierSize)
|
||||
{
|
||||
const size_t unpadded = 33u + identifierSize;
|
||||
return unpadded + (unpadded & 1u);
|
||||
}
|
||||
|
||||
uint32_t directoryBytesFor(const std::vector<size_t> &identifierSizes)
|
||||
{
|
||||
uint64_t cursor = 0u;
|
||||
for (const size_t identifierSize : identifierSizes)
|
||||
{
|
||||
const uint64_t recordSize = directoryRecordSize(identifierSize);
|
||||
const uint64_t sectorOffset = cursor % kSectorSize;
|
||||
if (sectorOffset + recordSize > kSectorSize)
|
||||
cursor += kSectorSize - sectorOffset;
|
||||
cursor += recordSize;
|
||||
}
|
||||
return static_cast<uint32_t>(std::max<uint64_t>(kSectorSize, alignSectors(cursor) * kSectorSize));
|
||||
}
|
||||
|
||||
size_t writeDirectoryRecord(uint8_t *destination,
|
||||
uint32_t lsn,
|
||||
uint32_t size,
|
||||
bool directory,
|
||||
const uint8_t *identifier,
|
||||
size_t identifierSize)
|
||||
{
|
||||
const size_t recordSize = directoryRecordSize(identifierSize);
|
||||
std::memset(destination, 0, recordSize);
|
||||
destination[0] = static_cast<uint8_t>(recordSize);
|
||||
writeBoth32(destination + 2u, lsn);
|
||||
writeBoth32(destination + 10u, size);
|
||||
destination[25] = directory ? 2u : 0u;
|
||||
writeBoth16(destination + 28u, 1u);
|
||||
destination[32] = static_cast<uint8_t>(identifierSize);
|
||||
std::memcpy(destination + 33u, identifier, identifierSize);
|
||||
return recordSize;
|
||||
}
|
||||
}
|
||||
|
||||
class IopCdvd::Impl
|
||||
{
|
||||
public:
|
||||
struct Callback
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
struct IsoNode
|
||||
{
|
||||
std::filesystem::path hostPath;
|
||||
std::string identifier;
|
||||
size_t parent = 0u;
|
||||
bool directory = false;
|
||||
uint32_t lsn = 0u;
|
||||
uint32_t size = 0u;
|
||||
uint32_t sectors = 0u;
|
||||
uint64_t handle = 0u;
|
||||
std::vector<size_t> children;
|
||||
};
|
||||
|
||||
Impl(IopHost &hostRef, IopMemory &memoryRef)
|
||||
: host(hostRef), memory(memoryRef)
|
||||
{
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
closeFiles();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
closeFiles();
|
||||
callback = {};
|
||||
initialized = false;
|
||||
mediaMode = 0u;
|
||||
currentLsn = 0u;
|
||||
lastError = kCdvdErrorNone;
|
||||
virtualIsoBuilt = false;
|
||||
virtualIsoValid = false;
|
||||
completionCallback.reset();
|
||||
nodes.clear();
|
||||
metadataSectors.clear();
|
||||
imageHandle = 0u;
|
||||
}
|
||||
|
||||
bool dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // sceCdInit
|
||||
initialized = a0 != kCdvdInitExit;
|
||||
if (initialized)
|
||||
{
|
||||
callback = {};
|
||||
completionCallback.reset();
|
||||
}
|
||||
lastError = kCdvdErrorNone;
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 5: // sceCdStandby
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 6: // sceCdRead
|
||||
if (readSectors(a0, a1, a2))
|
||||
{
|
||||
if (callback.address != 0u)
|
||||
{
|
||||
completionCallback = CompletionCallback{
|
||||
callback.address,
|
||||
callback.gp,
|
||||
kCdvdCallbackRead,
|
||||
};
|
||||
}
|
||||
cpu.gpr[2] = 1u;
|
||||
}
|
||||
else
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
|
||||
case 7: // sceCdSeek
|
||||
currentLsn = a0;
|
||||
lastError = kCdvdErrorNone;
|
||||
if (callback.address != 0u)
|
||||
{
|
||||
completionCallback = CompletionCallback{
|
||||
callback.address,
|
||||
callback.gp,
|
||||
kCdvdCallbackSeek,
|
||||
};
|
||||
}
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 8: // sceCdGetError
|
||||
cpu.gpr[2] = lastError;
|
||||
return true;
|
||||
|
||||
case 11: // sceCdSync
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
|
||||
case 12: // sceCdGetDiskType
|
||||
cpu.gpr[2] = kCdvdTypePs2Dvd;
|
||||
return true;
|
||||
|
||||
case 13: // sceCdDiskReady
|
||||
cpu.gpr[2] = kCdvdReadyComplete;
|
||||
return true;
|
||||
|
||||
case 28: // sceCdStatus
|
||||
cpu.gpr[2] = kCdvdStatusPause;
|
||||
return true;
|
||||
|
||||
case 37: // sceCdCallback
|
||||
{
|
||||
const uint32_t previous = callback.address;
|
||||
callback = {a0, cpu.gpr[28]};
|
||||
cpu.gpr[2] = previous;
|
||||
return true;
|
||||
}
|
||||
|
||||
case 75: // sceCdMmode
|
||||
mediaMode = a0;
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<CompletionCallback> takeCompletionCallback() noexcept
|
||||
{
|
||||
std::optional<CompletionCallback> result = completionCallback;
|
||||
completionCallback.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void closeFiles()
|
||||
{
|
||||
if (imageHandle != 0u)
|
||||
host.closeHostFile(imageHandle);
|
||||
imageHandle = 0u;
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (node.handle != 0u)
|
||||
host.closeHostFile(node.handle);
|
||||
node.handle = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool addDirectory(size_t parent, const std::filesystem::path &path)
|
||||
{
|
||||
std::error_code error;
|
||||
std::vector<std::filesystem::directory_entry> entries;
|
||||
for (
|
||||
std::filesystem::directory_iterator iterator(path, std::filesystem::directory_options::skip_permission_denied, error),
|
||||
end;
|
||||
!error && iterator != end;
|
||||
iterator.increment(error))
|
||||
{
|
||||
const std::filesystem::directory_entry &entry = *iterator;
|
||||
if (entry.is_symlink(error))
|
||||
{
|
||||
error.clear();
|
||||
continue;
|
||||
}
|
||||
error.clear();
|
||||
if (entry.is_directory(error) || entry.is_regular_file(error))
|
||||
entries.push_back(entry);
|
||||
error.clear();
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const auto &lhs, const auto &rhs)
|
||||
{
|
||||
return isoName(lhs.path(), lhs.is_directory()) < isoName(rhs.path(), rhs.is_directory());
|
||||
});
|
||||
|
||||
for (const auto &entry : entries)
|
||||
{
|
||||
error.clear();
|
||||
const bool directory = entry.is_directory(error);
|
||||
if (error)
|
||||
continue;
|
||||
IsoNode node;
|
||||
node.hostPath = entry.path();
|
||||
node.identifier = isoName(entry.path(), directory);
|
||||
node.parent = parent;
|
||||
node.directory = directory;
|
||||
if (!directory)
|
||||
{
|
||||
const uint64_t fileSize = entry.file_size(error);
|
||||
if (error)
|
||||
continue;
|
||||
node.size = static_cast<uint32_t>(std::min<uint64_t>(fileSize, std::numeric_limits<uint32_t>::max()));
|
||||
}
|
||||
const size_t index = nodes.size();
|
||||
nodes.push_back(std::move(node));
|
||||
nodes[parent].children.push_back(index);
|
||||
if (directory && !addDirectory(index, entry.path()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool buildVirtualIso()
|
||||
{
|
||||
if (virtualIsoBuilt)
|
||||
return virtualIsoValid;
|
||||
virtualIsoBuilt = true;
|
||||
|
||||
const std::string rootValue = host.hostPath(HostPathKind::CdRoot);
|
||||
if (rootValue.empty())
|
||||
return false;
|
||||
const std::filesystem::path rootPath(rootValue);
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_directory(rootPath, error) || error)
|
||||
return false;
|
||||
|
||||
nodes.clear();
|
||||
IsoNode root;
|
||||
root.hostPath = rootPath;
|
||||
root.identifier.clear();
|
||||
root.parent = 0u;
|
||||
root.directory = true;
|
||||
nodes.push_back(std::move(root));
|
||||
if (!addDirectory(0u, rootPath))
|
||||
return false;
|
||||
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory)
|
||||
continue;
|
||||
std::vector<size_t> identifierSizes = {1u, 1u};
|
||||
identifierSizes.reserve(node.children.size() + 2u);
|
||||
for (const size_t child : node.children)
|
||||
identifierSizes.push_back(nodes[child].identifier.size());
|
||||
node.size = directoryBytesFor(identifierSizes);
|
||||
node.sectors = node.size / kSectorSize;
|
||||
}
|
||||
|
||||
uint32_t cursor = kFirstDirectoryLsn;
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory)
|
||||
continue;
|
||||
node.lsn = cursor;
|
||||
cursor += node.sectors;
|
||||
}
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (node.directory)
|
||||
continue;
|
||||
node.lsn = cursor;
|
||||
node.sectors = alignSectors(node.size);
|
||||
cursor += node.sectors;
|
||||
}
|
||||
volumeSectors = std::max<uint32_t>(cursor, 32u);
|
||||
|
||||
std::array<uint8_t, kSectorSize> primary{};
|
||||
primary[0] = 1u;
|
||||
std::memcpy(primary.data() + 1u, "CD001", 5u);
|
||||
primary[6] = 1u;
|
||||
std::memset(primary.data() + 8u, ' ', 32u);
|
||||
std::memcpy(primary.data() + 8u, "PS2XRECOMP", 10u);
|
||||
std::memset(primary.data() + 40u, ' ', 32u);
|
||||
std::memcpy(primary.data() + 40u, "PS2X VIRTUAL DISC", 17u);
|
||||
writeBoth32(primary.data() + 80u, volumeSectors);
|
||||
writeBoth16(primary.data() + 120u, 1u);
|
||||
writeBoth16(primary.data() + 124u, 1u);
|
||||
writeBoth16(primary.data() + 128u, static_cast<uint16_t>(kSectorSize));
|
||||
const uint8_t rootIdentifier = 0u;
|
||||
(void)writeDirectoryRecord(primary.data() + 156u, nodes[0].lsn, nodes[0].size, true, &rootIdentifier, 1u);
|
||||
primary[881] = 1u;
|
||||
metadataSectors[kPrimaryVolumeDescriptorLsn] = primary;
|
||||
|
||||
std::array<uint8_t, kSectorSize> terminator{};
|
||||
terminator[0] = 255u;
|
||||
std::memcpy(terminator.data() + 1u, "CD001", 5u);
|
||||
terminator[6] = 1u;
|
||||
metadataSectors[kVolumeDescriptorTerminatorLsn] = terminator;
|
||||
|
||||
for (size_t nodeIndex = 0u; nodeIndex < nodes.size(); ++nodeIndex)
|
||||
{
|
||||
const IsoNode &node = nodes[nodeIndex];
|
||||
if (!node.directory)
|
||||
continue;
|
||||
std::vector<uint8_t> bytes(node.size, 0u);
|
||||
size_t offset = 0u;
|
||||
const auto appendRecord = [&](const IsoNode &entry, const uint8_t *identifier, size_t identifierSize)
|
||||
{
|
||||
const size_t recordSize = directoryRecordSize(identifierSize);
|
||||
const size_t sectorOffset = offset % kSectorSize;
|
||||
if (sectorOffset + recordSize > kSectorSize)
|
||||
offset += kSectorSize - sectorOffset;
|
||||
offset += writeDirectoryRecord(bytes.data() + offset,
|
||||
entry.lsn,
|
||||
entry.size,
|
||||
entry.directory,
|
||||
identifier,
|
||||
identifierSize);
|
||||
};
|
||||
const uint8_t selfIdentifier = 0u;
|
||||
const uint8_t parentIdentifier = 1u;
|
||||
appendRecord(node, &selfIdentifier, 1u);
|
||||
appendRecord(nodes[node.parent], &parentIdentifier, 1u);
|
||||
for (const size_t childIndex : node.children)
|
||||
{
|
||||
const IsoNode &child = nodes[childIndex];
|
||||
appendRecord(child, reinterpret_cast<const uint8_t *>(child.identifier.data()), child.identifier.size());
|
||||
}
|
||||
for (uint32_t sector = 0u; sector < node.sectors; ++sector)
|
||||
{
|
||||
std::array<uint8_t, kSectorSize> contents{};
|
||||
std::memcpy(contents.data(), bytes.data() + sector * kSectorSize, kSectorSize);
|
||||
metadataSectors[node.lsn + sector] = contents;
|
||||
}
|
||||
}
|
||||
|
||||
virtualIsoValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
IsoNode *fileForSector(uint32_t lsn)
|
||||
{
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory && lsn >= node.lsn && lsn < node.lsn + node.sectors)
|
||||
return &node;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool readVirtualSector(uint32_t lsn, uint8_t *destination)
|
||||
{
|
||||
const auto metadata = metadataSectors.find(lsn);
|
||||
if (metadata != metadataSectors.end())
|
||||
{
|
||||
std::memcpy(destination, metadata->second.data(), kSectorSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
IsoNode *node = fileForSector(lsn);
|
||||
if (!node)
|
||||
{
|
||||
std::memset(destination, 0, kSectorSize);
|
||||
return lsn < volumeSectors;
|
||||
}
|
||||
if (node->handle == 0u)
|
||||
node->handle = host.openHostFile(node->hostPath.string());
|
||||
if (node->handle == 0u)
|
||||
return false;
|
||||
|
||||
std::memset(destination, 0, kSectorSize);
|
||||
const uint64_t offset = static_cast<uint64_t>(lsn - node->lsn) * kSectorSize;
|
||||
const size_t wanted = static_cast<size_t>(std::min<uint64_t>(kSectorSize, static_cast<uint64_t>(node->size) - offset));
|
||||
size_t bytesRead = 0u;
|
||||
return host.readHostFile(node->handle, offset, destination, wanted, bytesRead) && bytesRead == wanted;
|
||||
}
|
||||
|
||||
bool readSectors(uint32_t lsn, uint32_t sectors, uint32_t destination)
|
||||
{
|
||||
if (sectors == 0u)
|
||||
{
|
||||
lastError = kCdvdErrorNone;
|
||||
return true;
|
||||
}
|
||||
const uint64_t byteCount64 = static_cast<uint64_t>(sectors) * kSectorSize;
|
||||
if (byteCount64 > IopMemory::RamSize || !memory.ownsRamRange(destination, static_cast<size_t>(byteCount64)))
|
||||
{
|
||||
lastError = kCdvdErrorRead;
|
||||
return false;
|
||||
}
|
||||
const size_t byteCount = static_cast<size_t>(byteCount64);
|
||||
std::vector<uint8_t> bytes(byteCount, 0u);
|
||||
|
||||
bool read = false;
|
||||
const std::string imagePath = host.hostPath(HostPathKind::CdImage);
|
||||
if (!imagePath.empty())
|
||||
{
|
||||
if (imageHandle == 0u)
|
||||
imageHandle = host.openHostFile(imagePath);
|
||||
if (imageHandle != 0u)
|
||||
{
|
||||
size_t bytesRead = 0u;
|
||||
read = host.readHostFile(imageHandle,
|
||||
static_cast<uint64_t>(lsn) * kSectorSize,
|
||||
bytes.data(),
|
||||
byteCount,
|
||||
bytesRead) &&
|
||||
bytesRead == byteCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (!read && buildVirtualIso())
|
||||
{
|
||||
read = true;
|
||||
for (uint32_t sector = 0u; sector < sectors; ++sector)
|
||||
{
|
||||
if (!readVirtualSector(lsn + sector, bytes.data() + static_cast<size_t>(sector) * kSectorSize))
|
||||
{
|
||||
read = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!read || !memory.writeRam(destination, bytes.data(), bytes.size()))
|
||||
{
|
||||
lastError = kCdvdErrorRead;
|
||||
return false;
|
||||
}
|
||||
lastError = kCdvdErrorNone;
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
IopMemory &memory;
|
||||
Callback callback;
|
||||
std::optional<CompletionCallback> completionCallback;
|
||||
bool initialized = false;
|
||||
uint32_t mediaMode = 0u;
|
||||
uint32_t currentLsn = 0u;
|
||||
uint32_t lastError = kCdvdErrorNone;
|
||||
uint64_t imageHandle = 0u;
|
||||
bool virtualIsoBuilt = false;
|
||||
bool virtualIsoValid = false;
|
||||
uint32_t volumeSectors = 0u;
|
||||
std::vector<IsoNode> nodes;
|
||||
std::unordered_map<uint32_t, std::array<uint8_t, kSectorSize>> metadataSectors;
|
||||
};
|
||||
|
||||
IopCdvd::IopCdvd(IopHost &host, IopMemory &memory)
|
||||
: m_impl(std::make_unique<Impl>(host, memory))
|
||||
{
|
||||
}
|
||||
|
||||
IopCdvd::~IopCdvd() = default;
|
||||
|
||||
void IopCdvd::reset() noexcept
|
||||
{
|
||||
m_impl->reset();
|
||||
}
|
||||
|
||||
bool IopCdvd::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
return m_impl->dispatchImport(ordinal, cpu);
|
||||
}
|
||||
|
||||
std::optional<IopCdvd::CompletionCallback> IopCdvd::takeCompletionCallback() noexcept
|
||||
{
|
||||
return m_impl->takeCompletionCallback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopCdvd
|
||||
{
|
||||
public:
|
||||
struct CompletionCallback
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
uint32_t reason = 0u;
|
||||
};
|
||||
|
||||
IopCdvd(IopHost &host, IopMemory &memory);
|
||||
~IopCdvd();
|
||||
|
||||
IopCdvd(const IopCdvd &) = delete;
|
||||
IopCdvd &operator=(const IopCdvd &) = delete;
|
||||
|
||||
void reset() noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] std::optional<CompletionCallback> takeCompletionCallback() noexcept;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
#include "iop_cpu.h"
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopCpuCore::IopCpuCore(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopCpuCore::writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg)
|
||||
{
|
||||
if (reg == 0u)
|
||||
return;
|
||||
cpu.gpr[reg] = value;
|
||||
writtenReg = reg;
|
||||
}
|
||||
|
||||
void IopCpuCore::scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue)
|
||||
{
|
||||
if (reg == 0u)
|
||||
return;
|
||||
scheduled = true;
|
||||
scheduledReg = reg;
|
||||
scheduledValue = value;
|
||||
}
|
||||
|
||||
void IopCpuCore::raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress) const
|
||||
{
|
||||
uint32_t cause = cpu.cop0[13] & ~0x7Cu;
|
||||
cause |= (code & 0x1Fu) << 2u;
|
||||
if (delaySlot)
|
||||
{
|
||||
cause |= 0x80000000u;
|
||||
cpu.cop0[14] = faultPc - 4u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cause &= ~0x80000000u;
|
||||
cpu.cop0[14] = faultPc;
|
||||
}
|
||||
cpu.cop0[13] = cause;
|
||||
if (badAddress)
|
||||
cpu.cop0[8] = *badAddress;
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
cpu.cop0[12] = (status & ~0x3Fu) | ((status << 2u) & 0x3Fu);
|
||||
cpu.pc = (status & (1u << 22u)) ? 0xBFC00180u : 0x80000080u;
|
||||
cpu.branchPending = false;
|
||||
cpu.pendingLoad = false;
|
||||
cpu.exception = true;
|
||||
}
|
||||
|
||||
bool IopCpuCore::executeInstruction(IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t pc = cpu.pc;
|
||||
const uint32_t instruction = m_memory.read32(pc);
|
||||
const bool wasDelaySlot = cpu.branchPending;
|
||||
const uint32_t priorBranchTarget = cpu.branchTarget;
|
||||
|
||||
cpu.branchPending = false;
|
||||
cpu.exception = false;
|
||||
cpu.yielded = false;
|
||||
|
||||
const uint32_t opcode = instruction >> 26u;
|
||||
const uint32_t rs = (instruction >> 21u) & 31u;
|
||||
const uint32_t rt = (instruction >> 16u) & 31u;
|
||||
const uint32_t rd = (instruction >> 11u) & 31u;
|
||||
const uint32_t sa = (instruction >> 6u) & 31u;
|
||||
const uint32_t funct = instruction & 63u;
|
||||
const uint32_t imm = instruction & 0xFFFFu;
|
||||
const int32_t simm = static_cast<int16_t>(imm);
|
||||
const uint32_t nextPc = pc + 4u;
|
||||
|
||||
uint32_t writtenReg = 0u;
|
||||
bool scheduledLoad = false;
|
||||
uint32_t scheduledReg = 0u;
|
||||
uint32_t scheduledValue = 0u;
|
||||
bool newBranch = false;
|
||||
uint32_t newBranchTarget = 0u;
|
||||
|
||||
auto branch = [&](bool condition)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
newBranch = true;
|
||||
newBranchTarget = nextPc + (static_cast<uint32_t>(simm) << 2u);
|
||||
}
|
||||
};
|
||||
auto write = [&](uint32_t reg, uint32_t value)
|
||||
{
|
||||
writeRegister(cpu, reg, value, writtenReg);
|
||||
};
|
||||
auto load = [&](uint32_t reg, uint32_t value)
|
||||
{
|
||||
scheduleLoad(reg, value, scheduledLoad, scheduledReg, scheduledValue);
|
||||
};
|
||||
auto overflowAdd = [&](int32_t lhs, int32_t rhs, uint32_t reg)
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(lhs) + rhs;
|
||||
if (result > std::numeric_limits<int32_t>::max() || result < std::numeric_limits<int32_t>::min())
|
||||
raiseException(cpu, 12u, pc, wasDelaySlot);
|
||||
else
|
||||
write(reg, static_cast<uint32_t>(static_cast<int32_t>(result)));
|
||||
};
|
||||
auto overflowSub = [&](int32_t lhs, int32_t rhs, uint32_t reg)
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(lhs) - rhs;
|
||||
if (result > std::numeric_limits<int32_t>::max() || result < std::numeric_limits<int32_t>::min())
|
||||
raiseException(cpu, 12u, pc, wasDelaySlot);
|
||||
else
|
||||
write(reg, static_cast<uint32_t>(static_cast<int32_t>(result)));
|
||||
};
|
||||
|
||||
// TODO kill this magic number and make it a constant somewhere
|
||||
switch (opcode)
|
||||
{
|
||||
case 0x00:
|
||||
switch (funct)
|
||||
{
|
||||
case 0x00:
|
||||
write(rd, cpu.gpr[rt] << sa);
|
||||
break;
|
||||
case 0x02:
|
||||
write(rd, cpu.gpr[rt] >> sa);
|
||||
break;
|
||||
case 0x03:
|
||||
write(rd, static_cast<uint32_t>(static_cast<int32_t>(cpu.gpr[rt]) >> sa));
|
||||
break;
|
||||
case 0x04:
|
||||
write(rd, cpu.gpr[rt] << (cpu.gpr[rs] & 31u));
|
||||
break;
|
||||
case 0x06:
|
||||
write(rd, cpu.gpr[rt] >> (cpu.gpr[rs] & 31u));
|
||||
break;
|
||||
case 0x07:
|
||||
write(rd, static_cast<uint32_t>(static_cast<int32_t>(cpu.gpr[rt]) >> (cpu.gpr[rs] & 31u)));
|
||||
break;
|
||||
case 0x08:
|
||||
newBranch = true;
|
||||
newBranchTarget = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x09:
|
||||
write(rd ? rd : 31u, pc + 8u);
|
||||
newBranch = true;
|
||||
newBranchTarget = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x0C:
|
||||
raiseException(cpu, 8u, pc, wasDelaySlot);
|
||||
break;
|
||||
case 0x0D:
|
||||
raiseException(cpu, 9u, pc, wasDelaySlot);
|
||||
break;
|
||||
case 0x10:
|
||||
write(rd, cpu.hi);
|
||||
break;
|
||||
case 0x11:
|
||||
cpu.hi = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x12:
|
||||
write(rd, cpu.lo);
|
||||
break;
|
||||
case 0x13:
|
||||
cpu.lo = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x18:
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(static_cast<int32_t>(cpu.gpr[rs])) * static_cast<int64_t>(static_cast<int32_t>(cpu.gpr[rt]));
|
||||
cpu.lo = static_cast<uint32_t>(result);
|
||||
cpu.hi = static_cast<uint32_t>(static_cast<uint64_t>(result) >> 32u);
|
||||
break;
|
||||
}
|
||||
case 0x19:
|
||||
{
|
||||
const uint64_t result = static_cast<uint64_t>(cpu.gpr[rs]) * cpu.gpr[rt];
|
||||
cpu.lo = static_cast<uint32_t>(result);
|
||||
cpu.hi = static_cast<uint32_t>(result >> 32u);
|
||||
break;
|
||||
}
|
||||
case 0x1A:
|
||||
{
|
||||
const int32_t lhs = static_cast<int32_t>(cpu.gpr[rs]);
|
||||
const int32_t rhs = static_cast<int32_t>(cpu.gpr[rt]);
|
||||
if (rhs == 0)
|
||||
{
|
||||
cpu.lo = lhs >= 0 ? 0xFFFFFFFFu : 1u;
|
||||
cpu.hi = static_cast<uint32_t>(lhs);
|
||||
}
|
||||
else if (lhs == std::numeric_limits<int32_t>::min() && rhs == -1)
|
||||
{
|
||||
cpu.lo = static_cast<uint32_t>(lhs);
|
||||
cpu.hi = 0u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.lo = static_cast<uint32_t>(lhs / rhs);
|
||||
cpu.hi = static_cast<uint32_t>(lhs % rhs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x1B:
|
||||
if (cpu.gpr[rt] == 0u)
|
||||
{
|
||||
cpu.lo = 0xFFFFFFFFu;
|
||||
cpu.hi = cpu.gpr[rs];
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.lo = cpu.gpr[rs] / cpu.gpr[rt];
|
||||
cpu.hi = cpu.gpr[rs] % cpu.gpr[rt];
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
overflowAdd(static_cast<int32_t>(cpu.gpr[rs]), static_cast<int32_t>(cpu.gpr[rt]), rd);
|
||||
break;
|
||||
case 0x21:
|
||||
write(rd, cpu.gpr[rs] + cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x22:
|
||||
overflowSub(static_cast<int32_t>(cpu.gpr[rs]), static_cast<int32_t>(cpu.gpr[rt]), rd);
|
||||
break;
|
||||
case 0x23:
|
||||
write(rd, cpu.gpr[rs] - cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x24:
|
||||
write(rd, cpu.gpr[rs] & cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x25:
|
||||
write(rd, cpu.gpr[rs] | cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x26:
|
||||
write(rd, cpu.gpr[rs] ^ cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x27:
|
||||
write(rd, ~(cpu.gpr[rs] | cpu.gpr[rt]));
|
||||
break;
|
||||
case 0x2A:
|
||||
write(rd, static_cast<int32_t>(cpu.gpr[rs]) < static_cast<int32_t>(cpu.gpr[rt]) ? 1u : 0u);
|
||||
break;
|
||||
case 0x2B:
|
||||
write(rd, cpu.gpr[rs] < cpu.gpr[rt] ? 1u : 0u);
|
||||
break;
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x01:
|
||||
switch (rt)
|
||||
{
|
||||
case 0x00:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) < 0);
|
||||
break;
|
||||
case 0x01:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) >= 0);
|
||||
break;
|
||||
case 0x10:
|
||||
write(31u, pc + 8u);
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) < 0);
|
||||
break;
|
||||
case 0x11:
|
||||
write(31u, pc + 8u);
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) >= 0);
|
||||
break;
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x02:
|
||||
newBranch = true;
|
||||
newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u);
|
||||
break;
|
||||
case 0x03:
|
||||
write(31u, pc + 8u);
|
||||
newBranch = true;
|
||||
newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u);
|
||||
break;
|
||||
case 0x04:
|
||||
branch(cpu.gpr[rs] == cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x05:
|
||||
branch(cpu.gpr[rs] != cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x06:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) <= 0);
|
||||
break;
|
||||
case 0x07:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) > 0);
|
||||
break;
|
||||
case 0x08:
|
||||
overflowAdd(static_cast<int32_t>(cpu.gpr[rs]), simm, rt);
|
||||
break;
|
||||
case 0x09:
|
||||
write(rt, cpu.gpr[rs] + static_cast<uint32_t>(simm));
|
||||
break;
|
||||
case 0x0A:
|
||||
write(rt, static_cast<int32_t>(cpu.gpr[rs]) < simm ? 1u : 0u);
|
||||
break;
|
||||
case 0x0B:
|
||||
write(rt, cpu.gpr[rs] < static_cast<uint32_t>(simm) ? 1u : 0u);
|
||||
break;
|
||||
case 0x0C:
|
||||
write(rt, cpu.gpr[rs] & imm);
|
||||
break;
|
||||
case 0x0D:
|
||||
write(rt, cpu.gpr[rs] | imm);
|
||||
break;
|
||||
case 0x0E:
|
||||
write(rt, cpu.gpr[rs] ^ imm);
|
||||
break;
|
||||
case 0x0F:
|
||||
write(rt, imm << 16u);
|
||||
break;
|
||||
case 0x10:
|
||||
{
|
||||
const uint32_t copRs = rs;
|
||||
if (copRs == 0x00)
|
||||
load(rt, cpu.cop0[rd]);
|
||||
else if (copRs == 0x04)
|
||||
cpu.cop0[rd] = cpu.gpr[rt];
|
||||
else if (copRs == 0x10 && funct == 0x10)
|
||||
{
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
cpu.cop0[12] = (status & ~0x0Fu) | ((status >> 2u) & 0x0Fu);
|
||||
}
|
||||
else
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
case 0x20:
|
||||
case 0x24:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint8_t value = m_memory.read8(address);
|
||||
load(rt, opcode == 0x20
|
||||
? static_cast<uint32_t>(static_cast<int32_t>(static_cast<int8_t>(value)))
|
||||
: value);
|
||||
break;
|
||||
}
|
||||
case 0x21:
|
||||
case 0x25:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 1u)
|
||||
{
|
||||
raiseException(cpu, 4u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
const uint16_t value = m_memory.read16(address);
|
||||
load(rt, opcode == 0x21 ? static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(value))) : value);
|
||||
break;
|
||||
}
|
||||
case 0x22:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t memory = m_memory.read32(address & ~3u);
|
||||
const uint32_t old = cpu.gpr[rt];
|
||||
static constexpr uint32_t masks[4] = {0x00FFFFFFu, 0x0000FFFFu, 0x000000FFu, 0x00000000u};
|
||||
static constexpr uint32_t shifts[4] = {24u, 16u, 8u, 0u};
|
||||
load(rt, (old & masks[address & 3u]) | (memory << shifts[address & 3u]));
|
||||
break;
|
||||
}
|
||||
case 0x23:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 3u)
|
||||
{
|
||||
raiseException(cpu, 4u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
load(rt, m_memory.read32(address));
|
||||
break;
|
||||
}
|
||||
case 0x26:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t memory = m_memory.read32(address & ~3u);
|
||||
const uint32_t old = cpu.gpr[rt];
|
||||
static constexpr uint32_t masks[4] = {0x00000000u, 0xFF000000u, 0xFFFF0000u, 0xFFFFFF00u};
|
||||
static constexpr uint32_t shifts[4] = {0u, 8u, 16u, 24u};
|
||||
load(rt, (old & masks[address & 3u]) | (memory >> shifts[address & 3u]));
|
||||
break;
|
||||
}
|
||||
case 0x28:
|
||||
m_memory.write8(cpu.gpr[rs] + static_cast<uint32_t>(simm), static_cast<uint8_t>(cpu.gpr[rt]));
|
||||
break;
|
||||
case 0x29:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 1u)
|
||||
{
|
||||
raiseException(cpu, 5u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
m_memory.write16(address, static_cast<uint16_t>(cpu.gpr[rt]));
|
||||
break;
|
||||
}
|
||||
case 0x2A:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t aligned = address & ~3u;
|
||||
const uint32_t old = m_memory.read32(aligned);
|
||||
const uint32_t value = cpu.gpr[rt];
|
||||
uint32_t result = old;
|
||||
switch (address & 3u)
|
||||
{
|
||||
case 0u:
|
||||
result = (old & 0xFFFFFF00u) | (value >> 24u);
|
||||
break;
|
||||
case 1u:
|
||||
result = (old & 0xFFFF0000u) | (value >> 16u);
|
||||
break;
|
||||
case 2u:
|
||||
result = (old & 0xFF000000u) | (value >> 8u);
|
||||
break;
|
||||
case 3u:
|
||||
result = value;
|
||||
break;
|
||||
}
|
||||
m_memory.write32(aligned, result);
|
||||
break;
|
||||
}
|
||||
case 0x2B:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 3u)
|
||||
{
|
||||
raiseException(cpu, 5u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
m_memory.write32(address, cpu.gpr[rt]);
|
||||
break;
|
||||
}
|
||||
case 0x2E:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t aligned = address & ~3u;
|
||||
const uint32_t old = m_memory.read32(aligned);
|
||||
const uint32_t value = cpu.gpr[rt];
|
||||
uint32_t result = old;
|
||||
switch (address & 3u)
|
||||
{
|
||||
case 0u:
|
||||
result = value;
|
||||
break;
|
||||
case 1u:
|
||||
result = (old & 0x000000FFu) | (value << 8u);
|
||||
break;
|
||||
case 2u:
|
||||
result = (old & 0x0000FFFFu) | (value << 16u);
|
||||
break;
|
||||
case 3u:
|
||||
result = (old & 0x00FFFFFFu) | (value << 24u);
|
||||
break;
|
||||
}
|
||||
m_memory.write32(aligned, result);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
|
||||
cpu.gpr[0] = 0u;
|
||||
if (cpu.exception)
|
||||
return !cpu.stopped;
|
||||
|
||||
if (cpu.pendingLoad)
|
||||
{
|
||||
if (cpu.pendingLoadReg != 0u && cpu.pendingLoadReg != writtenReg)
|
||||
cpu.gpr[cpu.pendingLoadReg] = cpu.pendingLoadValue;
|
||||
cpu.pendingLoad = false;
|
||||
}
|
||||
if (scheduledLoad)
|
||||
{
|
||||
cpu.pendingLoad = true;
|
||||
cpu.pendingLoadReg = scheduledReg;
|
||||
cpu.pendingLoadValue = scheduledValue;
|
||||
}
|
||||
cpu.gpr[0] = 0u;
|
||||
|
||||
if (wasDelaySlot)
|
||||
{
|
||||
cpu.pc = priorBranchTarget;
|
||||
cpu.branchPending = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.pc = nextPc;
|
||||
cpu.branchPending = newBranch;
|
||||
cpu.branchTarget = newBranchTarget;
|
||||
}
|
||||
return !cpu.stopped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
struct IopCpuState
|
||||
{
|
||||
std::array<uint32_t, 32> gpr{};
|
||||
uint32_t hi = 0;
|
||||
uint32_t lo = 0;
|
||||
uint32_t pc = 0;
|
||||
std::array<uint32_t, 32> cop0{};
|
||||
uint32_t pendingLoadReg = 0;
|
||||
uint32_t pendingLoadValue = 0;
|
||||
bool pendingLoad = false;
|
||||
bool branchPending = false;
|
||||
uint32_t branchTarget = 0;
|
||||
bool stopped = false;
|
||||
bool yielded = false;
|
||||
bool exception = false;
|
||||
};
|
||||
|
||||
class IopCpuCore
|
||||
{
|
||||
public:
|
||||
explicit IopCpuCore(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool executeInstruction(IopCpuState &cpu);
|
||||
void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress = std::nullopt) const;
|
||||
|
||||
private:
|
||||
static void writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg);
|
||||
static void scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue);
|
||||
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopEmulator
|
||||
{
|
||||
public:
|
||||
explicit IopEmulator(IopHost &host);
|
||||
~IopEmulator();
|
||||
|
||||
IopEmulator(const IopEmulator &) = delete;
|
||||
IopEmulator &operator=(const IopEmulator &) = delete;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize);
|
||||
[[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize);
|
||||
[[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result);
|
||||
void runEeCycles(uint64_t eeCycles) noexcept;
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
|
||||
[[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept;
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
|
||||
[[nodiscard]] uint64_t cycles() const noexcept;
|
||||
[[nodiscard]] uint64_t instructions() const noexcept;
|
||||
[[nodiscard]] uint32_t loadedModuleCount() const noexcept;
|
||||
[[nodiscard]] uint32_t threadCount() const noexcept;
|
||||
[[nodiscard]] uint32_t rpcServerCount() const noexcept;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
constexpr uint32_t kThreadReturnSentinel = 0x1FFFFF00u;
|
||||
constexpr uint32_t kCallReturnSentinel = 0x1FFFFF04u;
|
||||
constexpr uint64_t kIopClockHz = 36'864'000ull;
|
||||
// NTSC field cadence (approximately 59.94 Hz). VBlank imports are
|
||||
// scheduler waits, not no-op timing hints: returning immediately lets
|
||||
// high-priority IRX threads busy-loop and starve RPC server threads.
|
||||
constexpr uint64_t kVblankPeriodCycles = (kIopClockHz * 1001ull + 30'000ull) / 60'000ull;
|
||||
constexpr uint64_t kVblankEndPhaseCycles = kVblankPeriodCycles / 16ull;
|
||||
constexpr uint32_t kDefaultSlice = 256u;
|
||||
constexpr uint32_t kMaxCallInstructions = 2'000'000u;
|
||||
constexpr uint32_t kModuleLoadBase = 0x00010000u;
|
||||
constexpr uint32_t kStackGuardBytes = 64u;
|
||||
@@ -0,0 +1,160 @@
|
||||
#include "iop_imports.h"
|
||||
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kImportMagic = 0x41E00000u;
|
||||
constexpr uint32_t kExportMagic = 0x41C00000u;
|
||||
|
||||
bool equalsIgnoreCase(std::string_view lhs, std::string_view rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(rhs[i])))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string trimLibraryName(const char *name)
|
||||
{
|
||||
size_t length = 0u;
|
||||
while (length < 8u && name[length] != '\0')
|
||||
++length;
|
||||
return std::string(name, length);
|
||||
}
|
||||
}
|
||||
|
||||
IopImportRegistry::IopImportRegistry(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopImportRegistry::reset()
|
||||
{
|
||||
m_libraries.clear();
|
||||
}
|
||||
|
||||
std::optional<IopImportCall> IopImportRegistry::decode(uint32_t pc) const
|
||||
{
|
||||
if (m_memory.read32(pc) != 0x03E00008u)
|
||||
return std::nullopt;
|
||||
const uint32_t delay = m_memory.read32(pc + 4u);
|
||||
if ((delay & 0xFFFF0000u) != 0x24000000u)
|
||||
return std::nullopt;
|
||||
|
||||
const uint32_t physicalPc = IopMemory::physicalAddress(pc);
|
||||
const uint32_t searchBegin = physicalPc > 0x10000u ? physicalPc - 0x10000u : 0u;
|
||||
for (uint32_t candidate = physicalPc & ~3u; candidate >= searchBegin + 20u; candidate -= 4u)
|
||||
{
|
||||
const uint32_t table = candidate - 20u;
|
||||
if (m_memory.read32(table) != kImportMagic)
|
||||
{
|
||||
if (candidate == searchBegin + 20u)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
char name[9]{};
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
name[i] = static_cast<char>(m_memory.read8(table + 12u + i));
|
||||
const uint32_t stubs = table + 20u;
|
||||
if (physicalPc < stubs || ((physicalPc - stubs) & 7u) != 0u)
|
||||
continue;
|
||||
|
||||
bool valid = false;
|
||||
for (uint32_t stub = stubs;
|
||||
stub + 7u < IopMemory::RamSize && stub <= physicalPc;
|
||||
stub += 8u)
|
||||
{
|
||||
const uint32_t first = m_memory.read32(stub);
|
||||
const uint32_t second = m_memory.read32(stub + 4u);
|
||||
if (first == 0u && second == 0u)
|
||||
break;
|
||||
if (stub == physicalPc)
|
||||
{
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
return IopImportCall{
|
||||
trimLibraryName(name),
|
||||
static_cast<uint16_t>(delay & 0xFFFFu),
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool IopImportRegistry::registerExportTable(uint32_t address)
|
||||
{
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
if (physical + 20u > IopMemory::RamSize ||
|
||||
m_memory.read32(physical) != kExportMagic)
|
||||
return false;
|
||||
|
||||
char name[9]{};
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
name[i] = static_cast<char>(m_memory.read8(physical + 12u + i));
|
||||
|
||||
ExportLibrary library;
|
||||
library.tableAddress = physical;
|
||||
library.version = m_memory.read16(physical + 8u);
|
||||
library.name = trimLibraryName(name);
|
||||
for (uint32_t cursor = physical + 20u; cursor + 3u < IopMemory::RamSize; cursor += 4u)
|
||||
{
|
||||
const uint32_t function = m_memory.read32(cursor);
|
||||
if (function == 0u)
|
||||
break;
|
||||
library.functions.push_back(function);
|
||||
if (library.functions.size() > 1024u)
|
||||
return false;
|
||||
}
|
||||
m_libraries[physical] = std::move(library);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopImportRegistry::releaseExportTable(uint32_t address)
|
||||
{
|
||||
return m_libraries.erase(IopMemory::physicalAddress(address)) != 0u;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::findTable(std::string_view library) const
|
||||
{
|
||||
const auto found = std::find_if(m_libraries.begin(), m_libraries.end(), [&](const auto &entry)
|
||||
{ return equalsIgnoreCase(entry.second.name, library); });
|
||||
return found != m_libraries.end() ? found->second.tableAddress : 0u;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::resolve(std::string_view library, uint16_t ordinal) const
|
||||
{
|
||||
const auto found = std::find_if(m_libraries.begin(), m_libraries.end(), [&](const auto &entry)
|
||||
{ return equalsIgnoreCase(entry.second.name, library); });
|
||||
if (found == m_libraries.end() || ordinal >= found->second.functions.size())
|
||||
return 0u;
|
||||
return found->second.functions[ordinal];
|
||||
}
|
||||
|
||||
void IopImportRegistry::eraseRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto library = m_libraries.begin(); library != m_libraries.end();)
|
||||
{
|
||||
if (library->first >= base && library->first < base + size)
|
||||
library = m_libraries.erase(library);
|
||||
else
|
||||
++library;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
struct IopImportCall
|
||||
{
|
||||
std::string library;
|
||||
uint16_t ordinal = 0;
|
||||
};
|
||||
|
||||
class IopImportRegistry
|
||||
{
|
||||
public:
|
||||
explicit IopImportRegistry(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc) const;
|
||||
[[nodiscard]] bool registerExportTable(uint32_t address);
|
||||
[[nodiscard]] bool releaseExportTable(uint32_t address);
|
||||
[[nodiscard]] uint32_t findTable(std::string_view library) const;
|
||||
[[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal) const;
|
||||
void eraseRange(uint32_t base, uint32_t size);
|
||||
|
||||
private:
|
||||
struct ExportLibrary
|
||||
{
|
||||
uint32_t tableAddress = 0;
|
||||
uint16_t version = 0;
|
||||
std::string name;
|
||||
std::vector<uint32_t> functions;
|
||||
};
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<uint32_t, ExportLibrary> m_libraries;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
#include "iop_kernel.h"
|
||||
|
||||
#include "iop_memory.h"
|
||||
#include "iop_emulator_const.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment <= 1u)
|
||||
return value;
|
||||
const uint32_t mask = alignment - 1u;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
IopKernel::IopKernel(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopKernel::reset()
|
||||
{
|
||||
m_threads.clear();
|
||||
m_semaphores.clear();
|
||||
m_eventFlags.clear();
|
||||
m_nextThreadId = 1;
|
||||
m_nextSemaphoreId = 1;
|
||||
m_nextEventFlagId = 1;
|
||||
m_currentThread = nullptr;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // CreateThread
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
IopThread thread;
|
||||
thread.id = static_cast<int>(m_nextThreadId++);
|
||||
thread.attr = m_memory.read32(descriptor + 0u);
|
||||
thread.option = m_memory.read32(descriptor + 4u);
|
||||
thread.entry = m_memory.read32(descriptor + 8u);
|
||||
thread.stackSize = std::max<uint32_t>(m_memory.read32(descriptor + 12u), 0x100u);
|
||||
thread.priority = std::clamp<uint32_t>(m_memory.read32(descriptor + 16u), 1u, 126u);
|
||||
thread.initialPriority = thread.priority;
|
||||
thread.stackBase = m_memory.allocate(thread.stackSize + kStackGuardBytes, 16u);
|
||||
if (thread.stackBase == 0u)
|
||||
{
|
||||
setV0(-400);
|
||||
return true;
|
||||
}
|
||||
const int id = thread.id;
|
||||
m_threads.emplace(id, std::move(thread));
|
||||
setV0(id);
|
||||
return true;
|
||||
}
|
||||
case 5: // DeleteThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.stackBase != 0u)
|
||||
(void)m_memory.freeAllocation(it->second.stackBase);
|
||||
m_threads.erase(it);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 6: // StartThread
|
||||
case 7: // StartThreadArgs
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
IopThread &thread = it->second;
|
||||
thread.cpu = {};
|
||||
thread.cpu.pc = thread.entry;
|
||||
thread.cpu.gpr[4] = cpu.gpr[5];
|
||||
thread.cpu.gpr[5] = ordinal == 7 ? cpu.gpr[6] : 0u;
|
||||
thread.cpu.gpr[28] = cpu.gpr[28];
|
||||
thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u;
|
||||
thread.cpu.gpr[31] = kThreadReturnSentinel;
|
||||
thread.state = IopThreadState::Ready;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8: // ExitThread
|
||||
case 9: // ExitDeleteThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
m_currentThread->state = ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant;
|
||||
cpu.stopped = true;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 10:
|
||||
case 11: // TerminateThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.state = IopThreadState::Dormant;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 12:
|
||||
case 13:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 14:
|
||||
case 15:
|
||||
{
|
||||
int id = static_cast<int>(cpu.gpr[4]);
|
||||
if (id == 0 && m_currentThread != nullptr)
|
||||
id = m_currentThread->id;
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.priority = std::clamp<uint32_t>(cpu.gpr[5], 1u, 126u);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 16:
|
||||
case 17:
|
||||
setV0(0);
|
||||
cpu.yielded = true;
|
||||
return true;
|
||||
case 18:
|
||||
case 19:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Sleep ||
|
||||
it->second.state == IopThreadState::Delay ||
|
||||
it->second.state == IopThreadState::Semaphore ||
|
||||
it->second.state == IopThreadState::EventFlag)
|
||||
{
|
||||
it->second.state = IopThreadState::Ready;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 20:
|
||||
setV0(m_currentThread != nullptr ? m_currentThread->id : 0);
|
||||
return true;
|
||||
case 21:
|
||||
setV0(0x1000);
|
||||
return true;
|
||||
case 22:
|
||||
case 23:
|
||||
setV0(referThreadStatus(static_cast<int>(cpu.gpr[4]), cpu.gpr[5]) ? 0 : -1);
|
||||
return true;
|
||||
case 24: // SleepThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
if (m_currentThread->wakeupCount > 0)
|
||||
--m_currentThread->wakeupCount;
|
||||
else
|
||||
sleepCurrent(cpu);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 25:
|
||||
case 26:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Sleep)
|
||||
it->second.state = IopThreadState::Ready;
|
||||
else
|
||||
++it->second.wakeupCount;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 27:
|
||||
case 28:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const int old = it->second.wakeupCount;
|
||||
it->second.wakeupCount = 0;
|
||||
setV0(old);
|
||||
return true;
|
||||
}
|
||||
case 29:
|
||||
case 30: // SuspendThread / iSuspendThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.state = IopThreadState::Suspended;
|
||||
if (m_currentThread == &it->second)
|
||||
cpu.yielded = true;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 31:
|
||||
case 32: // ResumeThread / iResumeThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Suspended)
|
||||
it->second.state = IopThreadState::Ready;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 33: // DelayThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
const uint64_t delayCycles = (static_cast<uint64_t>(cpu.gpr[4]) * kIopClockHz + 999'999ull) / 1'000'000ull;
|
||||
delayCurrentUntil(currentCycle + std::max<uint64_t>(delayCycles, 1u), cpu);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 34: // GetSystemTime
|
||||
m_memory.write32(cpu.gpr[4], static_cast<uint32_t>(currentCycle));
|
||||
m_memory.write32(cpu.gpr[4] + 4u, static_cast<uint32_t>(currentCycle >> 32u));
|
||||
setV0(0);
|
||||
return true;
|
||||
case 35:
|
||||
case 36:
|
||||
case 37:
|
||||
case 38:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 39: // USec2SysClock
|
||||
{
|
||||
const uint64_t cycles = (static_cast<uint64_t>(cpu.gpr[4]) * kIopClockHz) / 1'000'000ull;
|
||||
m_memory.write32(cpu.gpr[5], static_cast<uint32_t>(cycles));
|
||||
m_memory.write32(cpu.gpr[5] + 4u, static_cast<uint32_t>(cycles >> 32u));
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 40:
|
||||
{
|
||||
const uint64_t cycles = static_cast<uint64_t>(m_memory.read32(cpu.gpr[4])) | (static_cast<uint64_t>(m_memory.read32(cpu.gpr[4] + 4u)) << 32u);
|
||||
const uint64_t usec = (cycles * 1'000'000ull) / kIopClockHz;
|
||||
if (cpu.gpr[5] != 0u)
|
||||
m_memory.write32(cpu.gpr[5], static_cast<uint32_t>(usec / 1'000'000ull));
|
||||
if (cpu.gpr[6] != 0u)
|
||||
m_memory.write32(cpu.gpr[6], static_cast<uint32_t>(usec % 1'000'000ull));
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 41:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::referThreadStatus(int id, uint32_t outputAddress)
|
||||
{
|
||||
if (id == 0 && m_currentThread != nullptr)
|
||||
id = m_currentThread->id;
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end() || outputAddress == 0u)
|
||||
return false;
|
||||
|
||||
const IopThread &thread = it->second;
|
||||
uint32_t status = 0x10u;
|
||||
switch (thread.state)
|
||||
{
|
||||
case IopThreadState::Running:
|
||||
status = 0x01u;
|
||||
break;
|
||||
case IopThreadState::Ready:
|
||||
status = 0x02u;
|
||||
break;
|
||||
case IopThreadState::Sleep:
|
||||
case IopThreadState::Delay:
|
||||
case IopThreadState::Semaphore:
|
||||
case IopThreadState::EventFlag:
|
||||
status = 0x04u;
|
||||
break;
|
||||
case IopThreadState::Suspended:
|
||||
status = 0x08u;
|
||||
break;
|
||||
default:
|
||||
status = 0x10u;
|
||||
break;
|
||||
}
|
||||
|
||||
m_memory.write32(outputAddress + 0u, thread.attr);
|
||||
m_memory.write32(outputAddress + 4u, thread.option);
|
||||
m_memory.write32(outputAddress + 8u, status);
|
||||
m_memory.write32(outputAddress + 12u, thread.entry);
|
||||
m_memory.write32(outputAddress + 16u, thread.stackBase);
|
||||
m_memory.write32(outputAddress + 20u, thread.stackSize);
|
||||
m_memory.write32(outputAddress + 24u, thread.cpu.gpr[28]);
|
||||
m_memory.write32(outputAddress + 28u, thread.initialPriority);
|
||||
m_memory.write32(outputAddress + 32u, thread.priority);
|
||||
m_memory.write32(outputAddress + 36u, thread.state == IopThreadState::Sleep ? 1u : thread.state == IopThreadState::Delay ? 2u
|
||||
: thread.state == IopThreadState::Semaphore ? 3u
|
||||
: thread.state == IopThreadState::EventFlag ? 4u
|
||||
: 0u);
|
||||
m_memory.write32(outputAddress + 40u, static_cast<uint32_t>(thread.waitId));
|
||||
m_memory.write32(outputAddress + 44u, static_cast<uint32_t>(thread.wakeupCount));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4:
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
Semaphore semaphore;
|
||||
semaphore.id = static_cast<int>(m_nextSemaphoreId++);
|
||||
semaphore.attr = m_memory.read32(descriptor + 0u);
|
||||
semaphore.option = m_memory.read32(descriptor + 4u);
|
||||
semaphore.current = static_cast<int>(m_memory.read32(descriptor + 8u));
|
||||
semaphore.maximum = std::max(1, static_cast<int>(m_memory.read32(descriptor + 12u)));
|
||||
m_semaphores.emplace(semaphore.id, semaphore);
|
||||
setV0(semaphore.id);
|
||||
return true;
|
||||
}
|
||||
case 5:
|
||||
setV0(m_semaphores.erase(static_cast<int>(cpu.gpr[4])) != 0u ? 0 : -1);
|
||||
return true;
|
||||
case 6:
|
||||
case 7:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.current < it->second.maximum)
|
||||
++it->second.current;
|
||||
wakeOneSemaphore(id);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8:
|
||||
case 9:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.current > 0)
|
||||
{
|
||||
--it->second.current;
|
||||
setV0(0);
|
||||
}
|
||||
else if (ordinal == 9)
|
||||
setV0(-419);
|
||||
else if (m_currentThread != nullptr)
|
||||
{
|
||||
m_currentThread->state = IopThreadState::Semaphore;
|
||||
m_currentThread->waitId = id;
|
||||
cpu.yielded = true;
|
||||
setV0(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case 11:
|
||||
case 12:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const uint32_t outputAddress = cpu.gpr[5];
|
||||
if (outputAddress != 0u)
|
||||
{
|
||||
m_memory.write32(outputAddress + 0u, it->second.attr);
|
||||
m_memory.write32(outputAddress + 4u, it->second.option);
|
||||
m_memory.write32(outputAddress + 8u, 0u);
|
||||
m_memory.write32(outputAddress + 12u, static_cast<uint32_t>(it->second.maximum));
|
||||
m_memory.write32(outputAddress + 16u, static_cast<uint32_t>(it->second.current));
|
||||
uint32_t waiters = 0u;
|
||||
for (const auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Semaphore && thread.waitId == id)
|
||||
++waiters;
|
||||
}
|
||||
m_memory.write32(outputAddress + 20u, waiters);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::wakeOneSemaphore(int id)
|
||||
{
|
||||
IopThread *best = nullptr;
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::Semaphore || thread.waitId != id)
|
||||
continue;
|
||||
if (best == nullptr || thread.priority < best->priority)
|
||||
best = &thread;
|
||||
}
|
||||
|
||||
const auto semaphore = m_semaphores.find(id);
|
||||
if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0)
|
||||
{
|
||||
--semaphore->second.current;
|
||||
best->state = IopThreadState::Ready;
|
||||
best->waitId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode)
|
||||
{
|
||||
if (bits == 0u)
|
||||
return false;
|
||||
return (mode & 1u) != 0u ? (event.bits & bits) != 0u : (event.bits & bits) == bits;
|
||||
}
|
||||
|
||||
void IopKernel::wakeEventWaiters(EventFlag &event)
|
||||
{
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::EventFlag || thread.waitId != event.id)
|
||||
continue;
|
||||
if (!eventSatisfied(event, thread.waitBits, thread.waitMode))
|
||||
continue;
|
||||
|
||||
if (thread.waitResultAddress != 0u)
|
||||
m_memory.write32(thread.waitResultAddress, event.bits);
|
||||
thread.cpu.gpr[2] = 0u;
|
||||
if ((thread.waitMode & 0x10u) != 0u)
|
||||
event.bits = 0u;
|
||||
thread.state = IopThreadState::Ready;
|
||||
thread.waitId = 0;
|
||||
thread.waitBits = 0;
|
||||
thread.waitMode = 0;
|
||||
thread.waitResultAddress = 0;
|
||||
if (event.bits == 0u)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchEventImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4:
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
EventFlag event;
|
||||
event.id = static_cast<int>(m_nextEventFlagId++);
|
||||
event.attr = m_memory.read32(descriptor + 0u);
|
||||
event.option = m_memory.read32(descriptor + 4u);
|
||||
event.bits = m_memory.read32(descriptor + 8u);
|
||||
m_eventFlags.emplace(event.id, event);
|
||||
setV0(event.id);
|
||||
return true;
|
||||
}
|
||||
case 5:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
if (m_eventFlags.erase(id) == 0u)
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == id)
|
||||
{
|
||||
thread.state = IopThreadState::Ready;
|
||||
thread.waitId = 0;
|
||||
thread.cpu.gpr[2] = static_cast<uint32_t>(-1);
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 6:
|
||||
case 7:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
event->second.bits |= cpu.gpr[5];
|
||||
wakeEventWaiters(event->second);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8:
|
||||
case 9:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
// IOP ClearEventFlag applies a mask: callers pass ~bitsToClear.
|
||||
event->second.bits &= cpu.gpr[5];
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 10: // WaitEventFlag
|
||||
case 11: // PollEventFlag
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const uint32_t bits = cpu.gpr[5];
|
||||
const uint32_t mode = cpu.gpr[6];
|
||||
if (eventSatisfied(event->second, bits, mode))
|
||||
{
|
||||
if (cpu.gpr[7] != 0u)
|
||||
m_memory.write32(cpu.gpr[7], event->second.bits);
|
||||
if ((mode & 0x10u) != 0u)
|
||||
event->second.bits = 0u;
|
||||
setV0(0);
|
||||
}
|
||||
else if (ordinal == 11)
|
||||
setV0(-418);
|
||||
else if (m_currentThread != nullptr)
|
||||
{
|
||||
m_currentThread->state = IopThreadState::EventFlag;
|
||||
m_currentThread->waitId = event->second.id;
|
||||
m_currentThread->waitBits = bits;
|
||||
m_currentThread->waitMode = mode;
|
||||
m_currentThread->waitResultAddress = cpu.gpr[7];
|
||||
setV0(0);
|
||||
cpu.yielded = true;
|
||||
}
|
||||
else
|
||||
setV0(-418);
|
||||
return true;
|
||||
}
|
||||
case 13:
|
||||
case 14:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (cpu.gpr[5] != 0u)
|
||||
{
|
||||
uint32_t waiters = 0u;
|
||||
for (const auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == event->second.id)
|
||||
++waiters;
|
||||
}
|
||||
m_memory.write32(cpu.gpr[5] + 0u, event->second.attr);
|
||||
m_memory.write32(cpu.gpr[5] + 4u, event->second.option);
|
||||
m_memory.write32(cpu.gpr[5] + 8u, event->second.bits);
|
||||
m_memory.write32(cpu.gpr[5] + 12u, event->second.bits);
|
||||
m_memory.write32(cpu.gpr[5] + 16u, waiters);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::sleepCurrent(IopCpuState &cpu)
|
||||
{
|
||||
if (m_currentThread == nullptr)
|
||||
return;
|
||||
m_currentThread->state = IopThreadState::Sleep;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
|
||||
void IopKernel::delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu)
|
||||
{
|
||||
if (m_currentThread == nullptr)
|
||||
return;
|
||||
m_currentThread->wakeCycle = wakeCycle;
|
||||
m_currentThread->state = IopThreadState::Delay;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
|
||||
IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
|
||||
{
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Delay && thread.wakeCycle <= currentCycle)
|
||||
thread.state = IopThreadState::Ready;
|
||||
}
|
||||
|
||||
IopThread *next = nullptr;
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::Ready)
|
||||
continue;
|
||||
if (next == nullptr || thread.priority < next->priority ||
|
||||
(thread.priority == next->priority && thread.id < next->id))
|
||||
next = &thread;
|
||||
}
|
||||
if (next == nullptr)
|
||||
return nullptr;
|
||||
|
||||
m_currentThread = next;
|
||||
next->state = IopThreadState::Running;
|
||||
next->cpu.stopped = false;
|
||||
next->cpu.yielded = false;
|
||||
return next;
|
||||
}
|
||||
|
||||
uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const
|
||||
{
|
||||
uint64_t nextWake = fallback;
|
||||
for (const auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Delay)
|
||||
nextWake = std::min(nextWake, thread.wakeCycle);
|
||||
}
|
||||
return nextWake;
|
||||
}
|
||||
|
||||
void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
|
||||
{
|
||||
if (thread.cpu.pc == returnSentinel || thread.cpu.stopped)
|
||||
thread.state = IopThreadState::Dormant;
|
||||
else if (thread.state == IopThreadState::Running)
|
||||
thread.state = IopThreadState::Ready;
|
||||
m_currentThread = nullptr;
|
||||
cleanupDeadThreads();
|
||||
}
|
||||
|
||||
void IopKernel::cleanupDeadThreads()
|
||||
{
|
||||
for (auto thread = m_threads.begin(); thread != m_threads.end();)
|
||||
{
|
||||
if (thread->second.state != IopThreadState::Dead)
|
||||
{
|
||||
++thread;
|
||||
continue;
|
||||
}
|
||||
if (thread->second.stackBase != 0u)
|
||||
(void)m_memory.freeAllocation(thread->second.stackBase);
|
||||
thread = m_threads.erase(thread);
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::terminateThreadsInRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
|
||||
if (pc >= base && pc < base + size)
|
||||
thread.state = IopThreadState::Dead;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "iop_cpu.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
enum class IopThreadState : uint8_t
|
||||
{
|
||||
Dormant,
|
||||
Ready,
|
||||
Running,
|
||||
Sleep,
|
||||
Delay,
|
||||
Semaphore,
|
||||
EventFlag,
|
||||
Suspended,
|
||||
Dead,
|
||||
};
|
||||
|
||||
struct IopThread
|
||||
{
|
||||
int id = 0;
|
||||
IopThreadState state = IopThreadState::Dormant;
|
||||
IopCpuState cpu;
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackBase = 0;
|
||||
uint32_t stackSize = 0;
|
||||
uint32_t priority = 0x40;
|
||||
uint32_t initialPriority = 0x40;
|
||||
uint32_t option = 0;
|
||||
uint32_t attr = 0;
|
||||
uint64_t wakeCycle = 0;
|
||||
int waitId = 0;
|
||||
uint32_t waitBits = 0;
|
||||
uint32_t waitMode = 0;
|
||||
uint32_t waitResultAddress = 0;
|
||||
int wakeupCount = 0;
|
||||
};
|
||||
|
||||
class IopKernel
|
||||
{
|
||||
public:
|
||||
explicit IopKernel(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] bool dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
[[nodiscard]] bool dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] bool dispatchEventImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
void sleepCurrent(IopCpuState &cpu);
|
||||
void delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu);
|
||||
|
||||
[[nodiscard]] IopThread *beginNextReady(uint64_t currentCycle);
|
||||
[[nodiscard]] uint64_t nextWakeCycle(uint64_t fallback) const;
|
||||
void endTimeslice(IopThread &thread, uint32_t returnSentinel);
|
||||
void cleanupDeadThreads();
|
||||
void terminateThreadsInRange(uint32_t base, uint32_t size);
|
||||
|
||||
[[nodiscard]] size_t threadCount() const noexcept { return m_threads.size(); }
|
||||
|
||||
private:
|
||||
struct Semaphore
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
int current = 0;
|
||||
int maximum = 1;
|
||||
};
|
||||
|
||||
struct EventFlag
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t bits = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool referThreadStatus(int id, uint32_t outputAddress);
|
||||
void wakeOneSemaphore(int id);
|
||||
[[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode);
|
||||
void wakeEventWaiters(EventFlag &event);
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<int, IopThread> m_threads;
|
||||
std::map<int, Semaphore> m_semaphores;
|
||||
std::map<int, EventFlag> m_eventFlags;
|
||||
uint32_t m_nextThreadId = 1;
|
||||
uint32_t m_nextSemaphoreId = 1;
|
||||
uint32_t m_nextEventFlagId = 1;
|
||||
IopThread *m_currentThread = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kDmaSpu0Chcr = 0x1F8010C8u;
|
||||
constexpr uint32_t kDmaSpu1Chcr = 0x1F801508u;
|
||||
constexpr uint32_t kDmaStart = 1u << 24u;
|
||||
constexpr int kDmaSpu0Irq = 0x24;
|
||||
constexpr int kDmaSpu1Irq = 0x28;
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
return (value + alignment - 1u) & ~(alignment - 1u);
|
||||
}
|
||||
}
|
||||
|
||||
IopMemory::IopMemory()
|
||||
: m_ram(RamSize), m_owned(RamSize), m_scratch(ScratchSize)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void IopMemory::reset()
|
||||
{
|
||||
std::fill(m_ram.begin(), m_ram.end(), uint8_t{0});
|
||||
std::fill(m_owned.begin(), m_owned.end(), uint8_t{0});
|
||||
std::fill(m_scratch.begin(), m_scratch.end(), uint8_t{0});
|
||||
m_hardware.clear();
|
||||
m_allocations.clear();
|
||||
m_heapCursor = HeapBase;
|
||||
m_interruptStatus = 0;
|
||||
m_interruptMask = 0;
|
||||
m_interruptControl = 1;
|
||||
m_dmaStart.reset();
|
||||
}
|
||||
|
||||
uint32_t IopMemory::physicalAddress(uint32_t address) noexcept
|
||||
{
|
||||
return address & 0x1FFFFFFFu;
|
||||
}
|
||||
|
||||
uint8_t IopMemory::read8(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys < RamSize)
|
||||
return m_ram[phys];
|
||||
if (phys >= ScratchBase && phys < ScratchBase + ScratchSize)
|
||||
return m_scratch[phys - ScratchBase];
|
||||
const uint32_t value = readHardware32(phys & ~3u);
|
||||
return static_cast<uint8_t>(value >> ((phys & 3u) * 8u));
|
||||
}
|
||||
|
||||
uint16_t IopMemory::read16(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys + 1u < RamSize)
|
||||
{
|
||||
uint16_t value;
|
||||
std::memcpy(&value, m_ram.data() + phys, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
return static_cast<uint16_t>(read8(address) | (static_cast<uint16_t>(read8(address + 1u)) << 8u));
|
||||
}
|
||||
|
||||
uint32_t IopMemory::read32(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((phys & 3u) == 0u && phys + 3u < RamSize)
|
||||
{
|
||||
uint32_t value;
|
||||
std::memcpy(&value, m_ram.data() + phys, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize)
|
||||
{
|
||||
uint32_t value;
|
||||
std::memcpy(&value, m_scratch.data() + (phys - ScratchBase), sizeof(value));
|
||||
return value;
|
||||
}
|
||||
if ((phys & 3u) == 0u && isHardwareAddress(phys))
|
||||
return readHardware32(phys);
|
||||
return static_cast<uint32_t>(read8(address)) |
|
||||
(static_cast<uint32_t>(read8(address + 1u)) << 8u) |
|
||||
(static_cast<uint32_t>(read8(address + 2u)) << 16u) |
|
||||
(static_cast<uint32_t>(read8(address + 3u)) << 24u);
|
||||
}
|
||||
|
||||
void IopMemory::write8(uint32_t address, uint8_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys < RamSize)
|
||||
{
|
||||
m_ram[phys] = value;
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if (phys >= ScratchBase && phys < ScratchBase + ScratchSize)
|
||||
{
|
||||
m_scratch[phys - ScratchBase] = value;
|
||||
return;
|
||||
}
|
||||
const uint32_t aligned = phys & ~3u;
|
||||
uint32_t current = readHardware32(aligned);
|
||||
const uint32_t shift = (phys & 3u) * 8u;
|
||||
current = (current & ~(0xFFu << shift)) | (static_cast<uint32_t>(value) << shift);
|
||||
writeHardware32(aligned, current);
|
||||
}
|
||||
|
||||
void IopMemory::write16(uint32_t address, uint16_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys + 1u < RamSize)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, &value, sizeof(value));
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
write8(address, static_cast<uint8_t>(value));
|
||||
write8(address + 1u, static_cast<uint8_t>(value >> 8u));
|
||||
}
|
||||
|
||||
void IopMemory::write32(uint32_t address, uint32_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((phys & 3u) == 0u && phys + 3u < RamSize)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, &value, sizeof(value));
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize)
|
||||
{
|
||||
std::memcpy(m_scratch.data() + (phys - ScratchBase), &value, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if ((phys & 3u) == 0u)
|
||||
{
|
||||
writeHardware32(phys, value);
|
||||
return;
|
||||
}
|
||||
write8(address, static_cast<uint8_t>(value));
|
||||
write8(address + 1u, static_cast<uint8_t>(value >> 8u));
|
||||
write8(address + 2u, static_cast<uint8_t>(value >> 16u));
|
||||
write8(address + 3u, static_cast<uint8_t>(value >> 24u));
|
||||
}
|
||||
|
||||
bool IopMemory::readRam(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((!destination && size != 0u) || phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(destination, m_ram.data() + phys, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::writeRam(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((!source && size != 0u) || phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, source, size);
|
||||
markOwned(phys, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::zeroRam(uint32_t address, size_t size)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memset(m_ram.data() + phys, 0, size);
|
||||
markOwned(phys, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::ownsRamRange(uint32_t address, size_t size) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
return std::all_of(m_owned.begin() + phys, m_owned.begin() + phys + size,
|
||||
[](uint8_t value)
|
||||
{ return value != 0u; });
|
||||
}
|
||||
|
||||
void IopMemory::markOwned(uint32_t address, size_t size)
|
||||
{
|
||||
if (address > RamSize || size > RamSize - address)
|
||||
return;
|
||||
std::fill(m_owned.begin() + address, m_owned.begin() + address + size, uint8_t{1});
|
||||
}
|
||||
|
||||
bool IopMemory::isHardwareAddress(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
return (phys >= HardwareBase && phys < HardwareEnd) ||
|
||||
(phys >= Spu2Base && phys < Spu2End) ||
|
||||
(phys >= SifBase && phys < SifEnd);
|
||||
}
|
||||
|
||||
uint32_t IopMemory::readHardware32(uint32_t address) const
|
||||
{
|
||||
const auto value = m_hardware.find(address);
|
||||
if (value != m_hardware.end())
|
||||
return value->second;
|
||||
switch (address)
|
||||
{
|
||||
case 0x1F801070u:
|
||||
return m_interruptStatus;
|
||||
case 0x1F801074u:
|
||||
return m_interruptMask;
|
||||
case 0x1F801078u:
|
||||
return m_interruptControl;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
void IopMemory::writeHardware32(uint32_t address, uint32_t value)
|
||||
{
|
||||
switch (address)
|
||||
{
|
||||
case 0x1F801070u:
|
||||
m_interruptStatus &= value;
|
||||
return;
|
||||
case 0x1F801074u:
|
||||
m_interruptMask = value;
|
||||
return;
|
||||
case 0x1F801078u:
|
||||
m_interruptControl = value & 1u;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
m_hardware[address] = value;
|
||||
if ((address != kDmaSpu0Chcr && address != kDmaSpu1Chcr) || (value & kDmaStart) == 0u)
|
||||
return;
|
||||
|
||||
const bool secondCore = address == kDmaSpu1Chcr;
|
||||
m_hardware[address] = value & ~kDmaStart;
|
||||
|
||||
const uint32_t statusAddress = 0x1F900344u + (secondCore ? 0x400u : 0u);
|
||||
const uint32_t alignedStatus = statusAddress & ~3u;
|
||||
const uint32_t shift = (statusAddress & 2u) * 8u;
|
||||
uint32_t status = 0u;
|
||||
if (const auto current = m_hardware.find(alignedStatus); current != m_hardware.end())
|
||||
status = current->second;
|
||||
status |= 0x80u << shift;
|
||||
m_hardware[alignedStatus] = status;
|
||||
|
||||
const uint32_t blockControlAddress = address - sizeof(uint32_t);
|
||||
uint32_t blockControl = 0u;
|
||||
if (const auto current = m_hardware.find(blockControlAddress); current != m_hardware.end())
|
||||
blockControl = current->second;
|
||||
const uint32_t wordsPerBlock = std::max<uint32_t>(blockControl & 0xFFFFu, 1u);
|
||||
const uint32_t blockCount = std::max<uint32_t>(blockControl >> 16u, 1u);
|
||||
const uint64_t transferWords = static_cast<uint64_t>(wordsPerBlock) * blockCount;
|
||||
m_dmaStart = DmaStart{
|
||||
secondCore ? kDmaSpu1Irq : kDmaSpu0Irq,
|
||||
std::max<uint64_t>(transferWords * 2u, 64u),
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<IopMemory::DmaStart> IopMemory::takeDmaStart() noexcept
|
||||
{
|
||||
std::optional<DmaStart> result = m_dmaStart;
|
||||
m_dmaStart.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t IopMemory::allocate(uint32_t size, uint32_t alignment, std::optional<uint32_t> fixed)
|
||||
{
|
||||
size = alignUp(std::max(size, 1u), 16u);
|
||||
alignment = std::max<uint32_t>(alignment, 4u);
|
||||
if (fixed)
|
||||
{
|
||||
const uint32_t address = *fixed;
|
||||
if (address < HeapBase || address + size > HeapLimit)
|
||||
return 0u;
|
||||
for (const auto &block : m_allocations)
|
||||
if (address < block.address + block.size && block.address < address + size)
|
||||
return 0u;
|
||||
m_allocations.push_back({address, size});
|
||||
markOwned(address, size);
|
||||
return address;
|
||||
}
|
||||
|
||||
uint32_t candidate = alignUp(m_heapCursor, alignment);
|
||||
for (;;)
|
||||
{
|
||||
bool overlap = false;
|
||||
for (const auto &block : m_allocations)
|
||||
{
|
||||
if (candidate < block.address + block.size && block.address < candidate + size)
|
||||
{
|
||||
candidate = alignUp(block.address + block.size, alignment);
|
||||
overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!overlap)
|
||||
break;
|
||||
}
|
||||
if (candidate > HeapLimit || size > HeapLimit - candidate)
|
||||
return 0u;
|
||||
m_allocations.push_back({candidate, size});
|
||||
markOwned(candidate, size);
|
||||
m_heapCursor = std::max(m_heapCursor, candidate + size);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
bool IopMemory::freeAllocation(uint32_t address)
|
||||
{
|
||||
const auto block = std::find_if(m_allocations.begin(), m_allocations.end(),
|
||||
[&](const Allocation &candidate)
|
||||
{ return candidate.address == address; });
|
||||
if (block == m_allocations.end())
|
||||
return false;
|
||||
std::fill(m_owned.begin() + block->address,
|
||||
m_owned.begin() + block->address + block->size,
|
||||
uint8_t{0});
|
||||
m_allocations.erase(block);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t IopMemory::maxFreeMemory() const
|
||||
{
|
||||
return m_heapCursor < HeapLimit ? HeapLimit - m_heapCursor : 0u;
|
||||
}
|
||||
|
||||
std::optional<IopMemory::Allocation> IopMemory::allocationContaining(uint32_t address) const
|
||||
{
|
||||
const auto block = std::find_if(m_allocations.begin(), m_allocations.end(),
|
||||
[&](const Allocation &candidate)
|
||||
{
|
||||
return address >= candidate.address &&
|
||||
address < candidate.address + candidate.size;
|
||||
});
|
||||
if (block == m_allocations.end())
|
||||
return std::nullopt;
|
||||
return *block;
|
||||
}
|
||||
|
||||
std::string IopMemory::readString(uint32_t address, size_t limit) const
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(std::min<size_t>(limit, 64u));
|
||||
for (size_t i = 0; i < limit; ++i)
|
||||
{
|
||||
const char ch = static_cast<char>(read8(address + static_cast<uint32_t>(i)));
|
||||
if (ch == '\0')
|
||||
break;
|
||||
result.push_back(ch);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory
|
||||
{
|
||||
public:
|
||||
static constexpr uint32_t RamSize = 2u * 1024u * 1024u;
|
||||
static constexpr uint32_t ScratchBase = 0x1F800000u;
|
||||
static constexpr uint32_t ScratchSize = 0x400u;
|
||||
static constexpr uint32_t HardwareBase = 0x1F801000u;
|
||||
static constexpr uint32_t HardwareEnd = 0x1F900000u;
|
||||
static constexpr uint32_t Spu2Base = 0x1F900000u;
|
||||
static constexpr uint32_t Spu2End = 0x1FA00000u;
|
||||
static constexpr uint32_t SifBase = 0x1D000000u;
|
||||
static constexpr uint32_t SifEnd = 0x1D001000u;
|
||||
static constexpr uint32_t HeapBase = 0x00120000u;
|
||||
static constexpr uint32_t HeapLimit = 0x001F0000u;
|
||||
|
||||
struct Allocation
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
struct DmaStart
|
||||
{
|
||||
int irq = 0;
|
||||
uint64_t delayCycles = 0;
|
||||
};
|
||||
|
||||
IopMemory();
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] uint8_t read8(uint32_t address) const;
|
||||
[[nodiscard]] uint16_t read16(uint32_t address) const;
|
||||
[[nodiscard]] uint32_t read32(uint32_t address) const;
|
||||
void write8(uint32_t address, uint8_t value);
|
||||
void write16(uint32_t address, uint16_t value);
|
||||
void write32(uint32_t address, uint32_t value);
|
||||
|
||||
[[nodiscard]] bool readRam(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeRam(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroRam(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool ownsRamRange(uint32_t address, size_t size) const;
|
||||
[[nodiscard]] bool isHardwareAddress(uint32_t address) const;
|
||||
[[nodiscard]] std::string readString(uint32_t address, size_t limit = 1024u) const;
|
||||
|
||||
[[nodiscard]] uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional<uint32_t> fixed = std::nullopt);
|
||||
[[nodiscard]] bool freeAllocation(uint32_t address);
|
||||
[[nodiscard]] uint32_t maxFreeMemory() const;
|
||||
[[nodiscard]] std::optional<Allocation> allocationContaining(uint32_t address) const;
|
||||
|
||||
[[nodiscard]] uint32_t interruptStatus() const noexcept { return m_interruptStatus; }
|
||||
[[nodiscard]] uint32_t interruptMask() const noexcept { return m_interruptMask; }
|
||||
[[nodiscard]] uint32_t interruptControl() const noexcept { return m_interruptControl; }
|
||||
void setInterruptStatus(uint32_t value) noexcept { m_interruptStatus = value; }
|
||||
void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; }
|
||||
void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; }
|
||||
|
||||
[[nodiscard]] std::optional<DmaStart> takeDmaStart() noexcept;
|
||||
[[nodiscard]] std::span<const uint8_t> ram() const noexcept { return m_ram; }
|
||||
|
||||
[[nodiscard]] static uint32_t physicalAddress(uint32_t address) noexcept;
|
||||
|
||||
private:
|
||||
[[nodiscard]] uint32_t readHardware32(uint32_t address) const;
|
||||
void writeHardware32(uint32_t address, uint32_t value);
|
||||
void markOwned(uint32_t address, size_t size);
|
||||
|
||||
std::vector<uint8_t> m_ram;
|
||||
std::vector<uint8_t> m_owned;
|
||||
std::vector<uint8_t> m_scratch;
|
||||
std::unordered_map<uint32_t, uint32_t> m_hardware;
|
||||
std::vector<Allocation> m_allocations;
|
||||
uint32_t m_heapCursor = HeapBase;
|
||||
uint32_t m_interruptStatus = 0;
|
||||
uint32_t m_interruptMask = 0;
|
||||
uint32_t m_interruptControl = 1;
|
||||
std::optional<DmaStart> m_dmaStart;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
#include "iop_module_loader.h"
|
||||
|
||||
#include "iop_memory.h"
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kMaxImageSize = 64u * 1024u * 1024u;
|
||||
constexpr uint32_t kModuleLoadBase = 0x00010000u;
|
||||
|
||||
constexpr uint16_t ET_EXEC = 2;
|
||||
constexpr uint16_t ET_SCE_IOPRELEXEC = 0xFF80u;
|
||||
constexpr uint16_t ET_SCE_IOPRELEXEC2 = 0xFF81u;
|
||||
constexpr uint16_t EM_MIPS = 8;
|
||||
constexpr uint32_t PT_LOAD = 1;
|
||||
constexpr uint32_t PT_SCE_IOPMOD = 0x70000080u;
|
||||
constexpr uint32_t PT_MIPS_REGINFO = 0x70000000u;
|
||||
constexpr uint32_t SHT_SYMTAB = 2;
|
||||
constexpr uint32_t SHT_MIPS_REGINFO = 0x70000006u;
|
||||
constexpr uint32_t SHT_RELA = 4;
|
||||
constexpr uint32_t SHT_NOBITS = 8;
|
||||
constexpr uint32_t SHT_REL = 9;
|
||||
constexpr uint32_t SHF_ALLOC = 0x2;
|
||||
constexpr uint32_t R_MIPS_NONE = 0;
|
||||
constexpr uint32_t R_MIPS_16 = 1;
|
||||
constexpr uint32_t R_MIPS_32 = 2;
|
||||
constexpr uint32_t R_MIPS_REL32 = 3;
|
||||
constexpr uint32_t R_MIPS_26 = 4;
|
||||
constexpr uint32_t R_MIPS_HI16 = 5;
|
||||
constexpr uint32_t R_MIPS_LO16 = 6;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct Elf32Ehdr
|
||||
{
|
||||
unsigned char ident[16];
|
||||
uint16_t type;
|
||||
uint16_t machine;
|
||||
uint32_t version;
|
||||
uint32_t entry;
|
||||
uint32_t phoff;
|
||||
uint32_t shoff;
|
||||
uint32_t flags;
|
||||
uint16_t ehsize;
|
||||
uint16_t phentsize;
|
||||
uint16_t phnum;
|
||||
uint16_t shentsize;
|
||||
uint16_t shnum;
|
||||
uint16_t shstrndx;
|
||||
};
|
||||
|
||||
struct Elf32Phdr
|
||||
{
|
||||
uint32_t type;
|
||||
uint32_t offset;
|
||||
uint32_t vaddr;
|
||||
uint32_t paddr;
|
||||
uint32_t filesz;
|
||||
uint32_t memsz;
|
||||
uint32_t flags;
|
||||
uint32_t align;
|
||||
};
|
||||
|
||||
struct Elf32Shdr
|
||||
{
|
||||
uint32_t name;
|
||||
uint32_t type;
|
||||
uint32_t flags;
|
||||
uint32_t addr;
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
uint32_t link;
|
||||
uint32_t info;
|
||||
uint32_t addralign;
|
||||
uint32_t entsize;
|
||||
};
|
||||
|
||||
struct Elf32Sym
|
||||
{
|
||||
uint32_t name;
|
||||
uint32_t value;
|
||||
uint32_t size;
|
||||
uint8_t info;
|
||||
uint8_t other;
|
||||
uint16_t shndx;
|
||||
};
|
||||
|
||||
struct Elf32Rel
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t info;
|
||||
};
|
||||
|
||||
struct Elf32Rela
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t info;
|
||||
int32_t addend;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(Elf32Ehdr) == 52);
|
||||
static_assert(sizeof(Elf32Phdr) == 32);
|
||||
static_assert(sizeof(Elf32Shdr) == 40);
|
||||
static_assert(sizeof(Elf32Sym) == 16);
|
||||
|
||||
struct PendingHi16
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t symbolValue = 0;
|
||||
uint32_t symbolIndex = 0;
|
||||
};
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment <= 1u)
|
||||
return value;
|
||||
const uint32_t mask = alignment - 1u;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
|
||||
bool checkedRange(size_t total, uint32_t offset, uint32_t size)
|
||||
{
|
||||
return offset <= total && size <= total - offset;
|
||||
}
|
||||
|
||||
bool validElfHeader(const Elf32Ehdr &header)
|
||||
{
|
||||
return header.ident[0] == 0x7Fu &&
|
||||
header.ident[1] == 'E' &&
|
||||
header.ident[2] == 'L' &&
|
||||
header.ident[3] == 'F' &&
|
||||
header.ident[4] == 1 &&
|
||||
header.ident[5] == 1 &&
|
||||
header.machine == EM_MIPS &&
|
||||
header.ehsize >= sizeof(Elf32Ehdr);
|
||||
}
|
||||
|
||||
bool applyRelocations(std::span<const uint8_t> image,
|
||||
const std::vector<Elf32Shdr> §ions,
|
||||
int64_t delta,
|
||||
uint32_t loadBase,
|
||||
bool isIopRelocatable,
|
||||
IopMemory &memory)
|
||||
{
|
||||
if (sections.empty())
|
||||
return true;
|
||||
|
||||
bool allSupported = true;
|
||||
std::vector<PendingHi16> hi16;
|
||||
for (size_t sectionIndex = 0; sectionIndex < sections.size(); ++sectionIndex)
|
||||
{
|
||||
const Elf32Shdr &relsec = sections[sectionIndex];
|
||||
if (relsec.type != SHT_REL && relsec.type != SHT_RELA)
|
||||
continue;
|
||||
if (relsec.info >= sections.size())
|
||||
continue;
|
||||
|
||||
const Elf32Shdr &targetSection = sections[relsec.info];
|
||||
const uint32_t targetBase = static_cast<uint32_t>(static_cast<int64_t>(targetSection.addr) + delta);
|
||||
|
||||
std::span<const Elf32Sym> symbols;
|
||||
std::vector<Elf32Sym> symbolStorage;
|
||||
if (relsec.link < sections.size())
|
||||
{
|
||||
const Elf32Shdr &symsec = sections[relsec.link];
|
||||
if (symsec.type == SHT_SYMTAB &&
|
||||
symsec.entsize >= sizeof(Elf32Sym) &&
|
||||
checkedRange(image.size(), symsec.offset, symsec.size))
|
||||
{
|
||||
const size_t count = symsec.size / symsec.entsize;
|
||||
symbolStorage.resize(count);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
std::memcpy(&symbolStorage[i], image.data() + symsec.offset + i * symsec.entsize, sizeof(Elf32Sym));
|
||||
}
|
||||
symbols = symbolStorage;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t entrySize = relsec.type == SHT_RELA
|
||||
? std::max<uint32_t>(relsec.entsize, sizeof(Elf32Rela))
|
||||
: std::max<uint32_t>(relsec.entsize, sizeof(Elf32Rel));
|
||||
if (entrySize == 0u || !checkedRange(image.size(), relsec.offset, relsec.size))
|
||||
continue;
|
||||
|
||||
for (uint32_t offset = 0; offset + entrySize <= relsec.size; offset += entrySize)
|
||||
{
|
||||
uint32_t relocationOffset = 0u;
|
||||
uint32_t relocationInfo = 0u;
|
||||
int32_t explicitAddend = 0;
|
||||
if (relsec.type == SHT_RELA)
|
||||
{
|
||||
Elf32Rela relocation{};
|
||||
std::memcpy(&relocation,
|
||||
image.data() + relsec.offset + offset,
|
||||
sizeof(relocation));
|
||||
relocationOffset = relocation.offset;
|
||||
relocationInfo = relocation.info;
|
||||
explicitAddend = relocation.addend;
|
||||
}
|
||||
else
|
||||
{
|
||||
Elf32Rel relocation{};
|
||||
std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation));
|
||||
relocationOffset = relocation.offset;
|
||||
relocationInfo = relocation.info;
|
||||
}
|
||||
|
||||
const uint32_t type = relocationInfo & 0xFFu;
|
||||
const uint32_t symbolIndex = relocationInfo >> 8u;
|
||||
uint32_t symbolValue = isIopRelocatable ? loadBase : 0u;
|
||||
if (symbolIndex < symbols.size())
|
||||
{
|
||||
const Elf32Sym &symbol = symbols[symbolIndex];
|
||||
if (!isIopRelocatable || symbolIndex != 0u)
|
||||
{
|
||||
symbolValue = symbol.value;
|
||||
if (symbol.shndx != 0u)
|
||||
{
|
||||
symbolValue = static_cast<uint32_t>(
|
||||
static_cast<int64_t>(symbolValue) + delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sony IOP relocatable executables use absolute image offsets
|
||||
// and symbol index zero. loadcore applies them as loadBase +
|
||||
// r_offset; normal ELF REL sections use a section-relative offset.
|
||||
const uint64_t place64 = isIopRelocatable
|
||||
? static_cast<uint64_t>(loadBase) + relocationOffset
|
||||
: static_cast<uint64_t>(targetBase) + relocationOffset;
|
||||
if (place64 > std::numeric_limits<uint32_t>::max())
|
||||
{
|
||||
allSupported = false;
|
||||
continue;
|
||||
}
|
||||
const uint32_t place = static_cast<uint32_t>(place64);
|
||||
if (place + 3u >= IopMemory::RamSize)
|
||||
{
|
||||
allSupported = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t word = memory.read32(place);
|
||||
const int32_t addend = relsec.type == SHT_RELA
|
||||
? explicitAddend
|
||||
: static_cast<int32_t>(word);
|
||||
switch (type)
|
||||
{
|
||||
case R_MIPS_NONE:
|
||||
break;
|
||||
case R_MIPS_32:
|
||||
case R_MIPS_REL32:
|
||||
memory.write32(place,
|
||||
static_cast<uint32_t>(
|
||||
static_cast<int64_t>(addend) + symbolValue));
|
||||
break;
|
||||
case R_MIPS_26:
|
||||
{
|
||||
const uint32_t target = ((word & 0x03FFFFFFu) << 2u) + symbolValue;
|
||||
memory.write32(place,
|
||||
(word & 0xFC000000u) |
|
||||
((target >> 2u) & 0x03FFFFFFu));
|
||||
break;
|
||||
}
|
||||
case R_MIPS_HI16:
|
||||
hi16.push_back({place, symbolValue, symbolIndex});
|
||||
break;
|
||||
case R_MIPS_LO16:
|
||||
{
|
||||
const int32_t lo = static_cast<int16_t>(word & 0xFFFFu);
|
||||
for (auto pending = hi16.begin(); pending != hi16.end();)
|
||||
{
|
||||
if (pending->symbolIndex != symbolIndex)
|
||||
{
|
||||
++pending;
|
||||
continue;
|
||||
}
|
||||
const uint32_t hiWord = memory.read32(pending->address);
|
||||
const int32_t hi = static_cast<int16_t>(hiWord & 0xFFFFu) << 16u;
|
||||
const int64_t full = static_cast<int64_t>(hi) + lo + pending->symbolValue;
|
||||
const uint32_t relocatedHi = static_cast<uint32_t>((full + 0x8000) >> 16u) & 0xFFFFu;
|
||||
memory.write32(pending->address, (hiWord & 0xFFFF0000u) | relocatedHi);
|
||||
pending = hi16.erase(pending);
|
||||
}
|
||||
const int64_t full = static_cast<int64_t>(lo) + symbolValue;
|
||||
memory.write32(place, (word & 0xFFFF0000u) | (static_cast<uint32_t>(full) & 0xFFFFu));
|
||||
break;
|
||||
}
|
||||
case R_MIPS_16:
|
||||
memory.write32(place, (word & 0xFFFF0000u) | (static_cast<uint32_t>(addend + symbolValue) & 0xFFFFu));
|
||||
break;
|
||||
default:
|
||||
allSupported = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSupported;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopModuleLoader::readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector<uint8_t> &bytes)
|
||||
{
|
||||
const std::string translated = host.translateGuestPath(guestPath);
|
||||
const std::string_view path = translated.empty() ? guestPath : std::string_view(translated);
|
||||
const uint64_t handle = host.openHostFile(path);
|
||||
if (handle == 0u)
|
||||
return false;
|
||||
|
||||
uint64_t size = 0u;
|
||||
if (!host.hostFileSize(handle, size) || size == 0u || size > kMaxImageSize)
|
||||
{
|
||||
host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes.resize(static_cast<size_t>(size));
|
||||
size_t bytesRead = 0u;
|
||||
const bool ok = host.readHostFile(handle, 0u, bytes.data(), bytes.size(), bytesRead) && bytesRead == bytes.size();
|
||||
host.closeHostFile(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IopModuleLoader::readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector<uint8_t> &bytes)
|
||||
{
|
||||
Elf32Ehdr header{};
|
||||
if (!host.readGuest(guestAddress, &header, sizeof(header)) || !validElfHeader(header))
|
||||
return false;
|
||||
|
||||
uint64_t required = sizeof(header);
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(header.phoff) + static_cast<uint64_t>(header.phentsize) * header.phnum);
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(header.shoff) + static_cast<uint64_t>(header.shentsize) * header.shnum);
|
||||
|
||||
if (required > kMaxImageSize)
|
||||
return false; // Should we log an error here? TODO check later
|
||||
|
||||
bytes.resize(static_cast<size_t>(required));
|
||||
if (!host.readGuest(guestAddress, bytes.data(), bytes.size()))
|
||||
return false;
|
||||
|
||||
if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr))
|
||||
{
|
||||
for (uint16_t i = 0; i < header.shnum; ++i)
|
||||
{
|
||||
Elf32Shdr section{};
|
||||
const size_t offset = static_cast<size_t>(header.shoff) + static_cast<size_t>(i) * header.shentsize;
|
||||
std::memcpy(§ion, bytes.data() + offset, sizeof(section));
|
||||
if (section.type != SHT_NOBITS)
|
||||
{
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(section.offset) + section.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr))
|
||||
{
|
||||
for (uint16_t i = 0; i < header.phnum; ++i)
|
||||
{
|
||||
Elf32Phdr program{};
|
||||
const size_t offset = static_cast<size_t>(header.phoff) + static_cast<size_t>(i) * header.phentsize;
|
||||
std::memcpy(&program, bytes.data() + offset, sizeof(program));
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(program.offset) + program.filesz);
|
||||
}
|
||||
}
|
||||
if (required > kMaxImageSize)
|
||||
return false;
|
||||
|
||||
bytes.resize(static_cast<size_t>(required));
|
||||
return host.readGuest(guestAddress, bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
IopImageLoadResult IopModuleLoader::load(std::span<const uint8_t> image, IopMemory &memory, uint32_t moduleCursor)
|
||||
{
|
||||
IopImageLoadResult result;
|
||||
result.nextModuleCursor = moduleCursor;
|
||||
if (image.size() < sizeof(Elf32Ehdr))
|
||||
return result;
|
||||
|
||||
Elf32Ehdr header{};
|
||||
std::memcpy(&header, image.data(), sizeof(header));
|
||||
if (!validElfHeader(header))
|
||||
{
|
||||
result.error = IopImageLoadError::InvalidElf;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t minVaddr = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t maxVaddr = 0u;
|
||||
bool hasLoad = false;
|
||||
std::vector<Elf32Phdr> programHeaders;
|
||||
if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr) && checkedRange(image.size(), header.phoff, static_cast<uint32_t>(header.phentsize) * header.phnum))
|
||||
{
|
||||
programHeaders.reserve(header.phnum);
|
||||
for (uint16_t i = 0; i < header.phnum; ++i)
|
||||
{
|
||||
Elf32Phdr program{};
|
||||
std::memcpy(&program, image.data() + header.phoff + static_cast<size_t>(i) * header.phentsize, sizeof(program));
|
||||
programHeaders.push_back(program);
|
||||
if (program.type == PT_LOAD && program.memsz != 0u)
|
||||
{
|
||||
hasLoad = true;
|
||||
minVaddr = std::min(minVaddr, program.vaddr);
|
||||
maxVaddr = std::max(maxVaddr, program.vaddr + program.memsz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Elf32Shdr> sectionHeaders;
|
||||
if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr) && checkedRange(image.size(), header.shoff, static_cast<uint32_t>(header.shentsize) * header.shnum))
|
||||
{
|
||||
sectionHeaders.reserve(header.shnum);
|
||||
for (uint16_t i = 0; i < header.shnum; ++i)
|
||||
{
|
||||
Elf32Shdr section{};
|
||||
std::memcpy(§ion, image.data() + header.shoff + static_cast<size_t>(i) * header.shentsize, sizeof(section));
|
||||
sectionHeaders.push_back(section);
|
||||
if (!hasLoad && (section.flags & SHF_ALLOC) != 0u && section.size != 0u)
|
||||
{
|
||||
minVaddr = std::min(minVaddr, section.addr);
|
||||
maxVaddr = std::max(maxVaddr, section.addr + section.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minVaddr == std::numeric_limits<uint32_t>::max())
|
||||
minVaddr = 0u;
|
||||
uint32_t span = maxVaddr > minVaddr ? maxVaddr - minVaddr : 0x1000u;
|
||||
span = alignUp(span, 0x100u);
|
||||
const bool relocate = header.type != ET_EXEC ||
|
||||
maxVaddr > IopMemory::RamSize ||
|
||||
(minVaddr < kModuleLoadBase && minVaddr != 0u);
|
||||
uint32_t base = 0u;
|
||||
int64_t delta = 0;
|
||||
if (relocate)
|
||||
{
|
||||
base = alignUp(moduleCursor, 0x100u);
|
||||
if (base + span >= IopMemory::HeapBase)
|
||||
{
|
||||
result.error = IopImageLoadError::ArenaExhausted;
|
||||
return result;
|
||||
}
|
||||
delta = static_cast<int64_t>(base) - minVaddr;
|
||||
result.nextModuleCursor = base + span;
|
||||
}
|
||||
else
|
||||
{
|
||||
base = minVaddr;
|
||||
}
|
||||
|
||||
if (hasLoad)
|
||||
{
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (program.type != PT_LOAD || program.memsz == 0u)
|
||||
continue;
|
||||
if (!checkedRange(image.size(), program.offset, program.filesz) ||
|
||||
program.memsz < program.filesz)
|
||||
return result;
|
||||
const uint32_t destination = static_cast<uint32_t>(static_cast<int64_t>(program.vaddr) + delta);
|
||||
if (destination >= IopMemory::RamSize || program.memsz > IopMemory::RamSize - destination)
|
||||
return result;
|
||||
if (!memory.writeRam(destination, image.data() + program.offset, program.filesz))
|
||||
return result;
|
||||
if (program.memsz > program.filesz && !memory.zeroRam(destination + program.filesz, program.memsz - program.filesz))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t sectionCursor = base;
|
||||
for (auto §ion : sectionHeaders)
|
||||
{
|
||||
if ((section.flags & SHF_ALLOC) == 0u || section.size == 0u)
|
||||
continue;
|
||||
uint32_t destination = 0u;
|
||||
if (section.addr != 0u)
|
||||
{
|
||||
destination = static_cast<uint32_t>(static_cast<int64_t>(section.addr) + delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
sectionCursor = alignUp(sectionCursor, std::max<uint32_t>(section.addralign, 4u));
|
||||
destination = sectionCursor;
|
||||
section.addr = static_cast<uint32_t>(static_cast<int64_t>(destination) - delta);
|
||||
sectionCursor += section.size;
|
||||
}
|
||||
if (destination >= IopMemory::RamSize || section.size > IopMemory::RamSize - destination)
|
||||
return result;
|
||||
if (section.type == SHT_NOBITS)
|
||||
{
|
||||
if (!memory.zeroRam(destination, section.size))
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!checkedRange(image.size(), section.offset, section.size) ||
|
||||
!memory.writeRam(destination,
|
||||
image.data() + section.offset,
|
||||
section.size))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool isIopRelocatable = header.type == ET_SCE_IOPRELEXEC || header.type == ET_SCE_IOPRELEXEC2;
|
||||
result.relocationsComplete = applyRelocations(image,
|
||||
sectionHeaders,
|
||||
delta,
|
||||
base,
|
||||
isIopRelocatable,
|
||||
memory);
|
||||
|
||||
result.base = base;
|
||||
result.size = span;
|
||||
result.entry = static_cast<uint32_t>(static_cast<int64_t>(header.entry) + delta);
|
||||
result.gp = 0u;
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (program.type == PT_SCE_IOPMOD && program.filesz >= 12u && checkedRange(image.size(), program.offset, 12u))
|
||||
{
|
||||
uint32_t entry = 0u;
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&entry, image.data() + program.offset + 4u, sizeof(entry));
|
||||
std::memcpy(&gp, image.data() + program.offset + 8u, sizeof(gp));
|
||||
result.entry = static_cast<uint32_t>(static_cast<int64_t>(entry) + delta);
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (result.gp != 0u)
|
||||
break;
|
||||
if (program.type == PT_MIPS_REGINFO && program.filesz >= 24u && checkedRange(image.size(), program.offset, 24u))
|
||||
{
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&gp, image.data() + program.offset + 20u, sizeof(gp));
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result.gp == 0u)
|
||||
{
|
||||
for (const auto §ion : sectionHeaders)
|
||||
{
|
||||
if (section.type == SHT_MIPS_REGINFO && section.size >= 24u && checkedRange(image.size(), section.offset, 24u))
|
||||
{
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&gp, image.data() + section.offset + 20u, sizeof(gp));
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.error = IopImageLoadError::None;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
enum class IopImageLoadError : uint8_t
|
||||
{
|
||||
None,
|
||||
InvalidElf,
|
||||
ArenaExhausted,
|
||||
MalformedImage,
|
||||
};
|
||||
|
||||
struct IopImageLoadResult
|
||||
{
|
||||
IopImageLoadError error = IopImageLoadError::MalformedImage;
|
||||
uint32_t base = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t nextModuleCursor = 0;
|
||||
bool relocationsComplete = true;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const noexcept
|
||||
{
|
||||
return error == IopImageLoadError::None;
|
||||
}
|
||||
};
|
||||
|
||||
class IopModuleLoader
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] static bool readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector<uint8_t> &bytes);
|
||||
[[nodiscard]] static bool readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector<uint8_t> &bytes);
|
||||
[[nodiscard]] static IopImageLoadResult load(std::span<const uint8_t> image, IopMemory &memory, uint32_t moduleCursor);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
#include "iop_rpc.h"
|
||||
|
||||
#include "iop_cpu.h"
|
||||
#include "iop_kernel.h"
|
||||
#include "iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopRpcBridge::IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept
|
||||
: m_host(host), m_memory(memory), m_kernel(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
void IopRpcBridge::reset()
|
||||
{
|
||||
m_servers.clear();
|
||||
m_nextDmaId = 1u;
|
||||
m_sifInitialized = false;
|
||||
}
|
||||
|
||||
bool IopRpcBridge::dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // sceSifDma2Init
|
||||
case 5: // sceSifInit
|
||||
m_sifInitialized = true;
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 7: // sceSifSetDma
|
||||
{
|
||||
constexpr uint32_t kDescriptorSize = 16u;
|
||||
constexpr uint32_t kMaxDescriptors = 32u;
|
||||
const uint32_t descriptorAddress = cpu.gpr[4];
|
||||
const uint32_t descriptorCount = cpu.gpr[5];
|
||||
if (descriptorAddress == 0u || descriptorCount == 0u || descriptorCount > kMaxDescriptors)
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct PendingTransfer
|
||||
{
|
||||
uint32_t source = 0u;
|
||||
uint32_t destination = 0u;
|
||||
uint32_t size = 0u;
|
||||
};
|
||||
|
||||
std::array<uint32_t, kMaxDescriptors * 4u> descriptorWords{};
|
||||
const size_t descriptorBytes = static_cast<size_t>(descriptorCount) * kDescriptorSize;
|
||||
if (!m_memory.readRam(descriptorAddress, descriptorWords.data(), descriptorBytes))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<PendingTransfer, kMaxDescriptors> pending{};
|
||||
uint32_t pendingCount = 0u;
|
||||
uint32_t largestTransfer = 0u;
|
||||
for (uint32_t i = 0u; i < descriptorCount; ++i)
|
||||
{
|
||||
const uint32_t source = descriptorWords[i * 4u + 0u];
|
||||
const uint32_t destination = descriptorWords[i * 4u + 1u];
|
||||
const int32_t signedSize = static_cast<int32_t>(descriptorWords[i * 4u + 2u]);
|
||||
if (signedSize <= 0)
|
||||
continue;
|
||||
|
||||
const uint32_t size = static_cast<uint32_t>(signedSize);
|
||||
if (!m_memory.ownsRamRange(source, size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
pending[pendingCount++] = {source, destination, size};
|
||||
largestTransfer = std::max(largestTransfer, size);
|
||||
}
|
||||
|
||||
// IOP-side sceSifSetDma sends IOP RAM to the EE. Validate all EE
|
||||
// destinations before committing any write so a bad chain cannot
|
||||
// partially update guest memory, but maybe we could skip this check if we trust the EE-side SIF driver to validate the chain ?!
|
||||
// TODO check later
|
||||
std::vector<uint8_t> scratch(largestTransfer);
|
||||
for (uint32_t i = 0u; i < pendingCount; ++i)
|
||||
{
|
||||
const PendingTransfer &transfer = pending[i];
|
||||
if (!m_host.readGuest(transfer.destination, scratch.data(), transfer.size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t i = 0u; i < pendingCount; ++i)
|
||||
{
|
||||
const PendingTransfer &transfer = pending[i];
|
||||
if (!m_memory.readRam(transfer.source, scratch.data(), transfer.size) || !m_host.writeGuest(transfer.destination, scratch.data(), transfer.size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t dmaId = m_nextDmaId++;
|
||||
if (m_nextDmaId == 0u || m_nextDmaId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))
|
||||
{
|
||||
m_nextDmaId = 1u;
|
||||
}
|
||||
setV0(dmaId);
|
||||
return true;
|
||||
}
|
||||
case 8: // sceSifDmaStat
|
||||
// Transfers are applied synchronously above. The IOP API reports
|
||||
// a negative value once the transaction is no longer active.
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 29: // sceSifCheckInit
|
||||
setV0(m_sifInitialized ? 1u : 0u);
|
||||
return true;
|
||||
default:
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopRpcBridge::dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // InitCmd
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
case 8:
|
||||
case 9:
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 13:
|
||||
case 14: // InitRpc
|
||||
case 15:
|
||||
case 16:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 17: // sceSifRegisterRpc
|
||||
{
|
||||
RpcServer server;
|
||||
server.serverData = cpu.gpr[4];
|
||||
server.sid = cpu.gpr[5];
|
||||
server.function = cpu.gpr[6];
|
||||
server.gp = cpu.gpr[28];
|
||||
server.buffer = cpu.gpr[7];
|
||||
const uint32_t stackPointer = cpu.gpr[29];
|
||||
server.callback = m_memory.read32(stackPointer + 16u);
|
||||
server.callbackBuffer = m_memory.read32(stackPointer + 20u);
|
||||
server.queue = m_memory.read32(stackPointer + 24u);
|
||||
m_servers[server.sid] = server;
|
||||
if (server.serverData != 0u)
|
||||
{
|
||||
m_memory.write32(server.serverData + 0x20u, server.sid);
|
||||
m_memory.write32(server.serverData + 0x28u, server.function);
|
||||
m_memory.write32(server.serverData + 0x2Cu, server.buffer);
|
||||
}
|
||||
setV0(server.serverData);
|
||||
return true;
|
||||
}
|
||||
case 18:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 19: // SetRpcQueue
|
||||
setV0(cpu.gpr[4]);
|
||||
return true;
|
||||
case 20:
|
||||
case 21:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 22: // RpcLoop
|
||||
m_kernel.sleepCurrent(cpu);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 23:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 24: // RemoveRpc
|
||||
{
|
||||
const uint32_t serverData = cpu.gpr[4];
|
||||
for (auto server = m_servers.begin(); server != m_servers.end(); ++server)
|
||||
{
|
||||
if (server->second.serverData == serverData)
|
||||
{
|
||||
m_servers.erase(server);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 25:
|
||||
case 26:
|
||||
case 27:
|
||||
case 28:
|
||||
case 29:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RpcResult IopRpcBridge::handleRpc(const RpcRequest &request, IopGuestExecutor &executor)
|
||||
{
|
||||
RpcResult result{};
|
||||
const auto serverIt = m_servers.find(request.sid);
|
||||
if (serverIt == m_servers.end() || serverIt->second.function == 0u)
|
||||
return result;
|
||||
|
||||
RpcServer &server = serverIt->second;
|
||||
if (request.send.size != 0u && server.buffer != 0u)
|
||||
{
|
||||
const uint32_t copySize = std::min<uint32_t>(request.send.size, IopMemory::RamSize - std::min(server.buffer, IopMemory::RamSize));
|
||||
if (copySize != 0u)
|
||||
{
|
||||
std::vector<uint8_t> payload(copySize);
|
||||
if (m_host.readGuest(request.send.address, payload.data(), payload.size()))
|
||||
(void)m_memory.writeRam(server.buffer, payload.data(), payload.size());
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t returnPointer = executor.executeGuestFunction(server.function,
|
||||
request.function,
|
||||
server.buffer,
|
||||
request.send.size,
|
||||
0u,
|
||||
server.gp);
|
||||
if (returnPointer == 0u)
|
||||
returnPointer = server.buffer;
|
||||
if (request.receive.address != 0u && request.receive.size != 0u && returnPointer != 0u)
|
||||
{
|
||||
const uint32_t physical = IopMemory::physicalAddress(returnPointer);
|
||||
if (physical < IopMemory::RamSize)
|
||||
{
|
||||
const uint32_t copySize = std::min<uint32_t>(request.receive.size, IopMemory::RamSize - physical);
|
||||
(void)m_host.writeGuest(request.receive.address, m_memory.ram().data() + physical, copySize);
|
||||
if (copySize < request.receive.size)
|
||||
(void)m_host.zeroGuest(request.receive.address + copySize,
|
||||
request.receive.size - copySize);
|
||||
}
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
result.serverDispatchPolicy = ServerDispatchPolicy::Suppress;
|
||||
result.signalNowaitCompletion = true;
|
||||
result.signalCompletion = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
void IopRpcBridge::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
if (transfer.size == 0u)
|
||||
return;
|
||||
|
||||
if (transfer.kind == SifTransferKind::SetDma)
|
||||
{
|
||||
// sceSifSetDma transfers EE memory to IOP RAM. The runtime performs its
|
||||
// legacy EE-side mirror first, then gives the physical emulator the same
|
||||
// payload so the IOP observes it at the requested destination.
|
||||
if (transfer.phase != SifTransferPhase::AfterCopy)
|
||||
return;
|
||||
const uint32_t destination = IopMemory::physicalAddress(transfer.destinationAddress);
|
||||
if (destination >= IopMemory::RamSize)
|
||||
return;
|
||||
const size_t copySize = std::min<size_t>(transfer.size, IopMemory::RamSize - destination);
|
||||
std::vector<uint8_t> data(copySize);
|
||||
if (m_host.readGuest(transfer.sourceAddress, data.data(), data.size()))
|
||||
(void)m_memory.writeRam(destination, data.data(), data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
if (transfer.kind == SifTransferKind::GetOtherData && transfer.phase == SifTransferPhase::BeforeCopy)
|
||||
{
|
||||
// sceSifGetOtherData is the reverse direction: its source is an IOP
|
||||
// address and its destination is in EE memory. Stage the physical IOP
|
||||
// bytes at the source's EE mirror before the runtime performs the copy.
|
||||
const uint32_t source = IopMemory::physicalAddress(transfer.sourceAddress);
|
||||
if (source >= IopMemory::RamSize)
|
||||
return;
|
||||
const size_t copySize = std::min<size_t>(transfer.size, IopMemory::RamSize - source);
|
||||
if (!m_memory.ownsRamRange(source, copySize))
|
||||
return;
|
||||
(void)m_host.writeGuest(transfer.sourceAddress, m_memory.ram().data() + source, copySize);
|
||||
}
|
||||
}
|
||||
|
||||
void IopRpcBridge::removeServersInRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto server = m_servers.begin(); server != m_servers.end();)
|
||||
{
|
||||
const uint32_t function = IopMemory::physicalAddress(server->second.function);
|
||||
if (function >= base && function < base + size)
|
||||
server = m_servers.erase(server);
|
||||
else
|
||||
++server;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopRpcBridge::hasServer(uint32_t sid) const noexcept
|
||||
{
|
||||
const auto server = m_servers.find(sid);
|
||||
return server != m_servers.end() && server->second.function != 0u;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
class IopMemory;
|
||||
|
||||
class IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
virtual ~IopGuestExecutor() = default;
|
||||
|
||||
[[nodiscard]] virtual uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp) = 0;
|
||||
};
|
||||
|
||||
class IopRpcBridge
|
||||
{
|
||||
public:
|
||||
IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] bool dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request, IopGuestExecutor &executor);
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
void removeServersInRange(uint32_t base, uint32_t size);
|
||||
|
||||
[[nodiscard]] bool hasServer(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] size_t serverCount() const noexcept { return m_servers.size(); }
|
||||
|
||||
private:
|
||||
struct RpcServer
|
||||
{
|
||||
uint32_t sid = 0;
|
||||
uint32_t serverData = 0;
|
||||
uint32_t function = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t buffer = 0;
|
||||
uint32_t callback = 0;
|
||||
uint32_t callbackBuffer = 0;
|
||||
uint32_t queue = 0;
|
||||
};
|
||||
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
IopKernel &m_kernel;
|
||||
std::unordered_map<uint32_t, RpcServer> m_servers;
|
||||
uint32_t m_nextDmaId = 1u;
|
||||
bool m_sifInitialized = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
#include "iop_sysclib.h"
|
||||
|
||||
#include "iop_cpu.h"
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopSysclib::IopSysclib(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopSysclib::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
const auto compare = [&](uint32_t lhs, uint32_t rhs, uint32_t count) -> int32_t
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(lhs + i);
|
||||
const uint8_t right = m_memory.read8(rhs + i);
|
||||
if (left != right)
|
||||
return static_cast<int32_t>(left) - static_cast<int32_t>(right);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const auto copy = [&](uint32_t destination, uint32_t source, uint32_t count)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
m_memory.write8(destination + i, m_memory.read8(source + i));
|
||||
};
|
||||
const auto appendString = [&](uint32_t destination, uint32_t source, std::optional<uint32_t> maxAppend = std::nullopt)
|
||||
{
|
||||
uint32_t destinationOffset = 0;
|
||||
while (m_memory.read8(destination + destinationOffset) != 0u && destinationOffset < (1u << 20))
|
||||
++destinationOffset;
|
||||
|
||||
uint32_t sourceOffset = 0;
|
||||
while (sourceOffset < (1u << 20) && (!maxAppend || sourceOffset < *maxAppend))
|
||||
{
|
||||
const uint8_t character = m_memory.read8(source + sourceOffset);
|
||||
m_memory.write8(destination + destinationOffset + sourceOffset, character);
|
||||
++sourceOffset;
|
||||
if (character == 0u)
|
||||
return;
|
||||
}
|
||||
m_memory.write8(destination + destinationOffset + sourceOffset, 0u);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // setjmp - enough for callers which only test the initial return.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 5: // longjmp cannot be safely synthesized without the BIOS jmp_buf ABI.
|
||||
setV0(a1 == 0u ? 1u : a1);
|
||||
return true;
|
||||
case 6:
|
||||
setV0(static_cast<uint32_t>(std::toupper(static_cast<unsigned char>(a0))));
|
||||
return true;
|
||||
case 7:
|
||||
setV0(static_cast<uint32_t>(std::tolower(static_cast<unsigned char>(a0))));
|
||||
return true;
|
||||
case 8:
|
||||
case 9: // ctype table is optional for most IRXs.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 10: // memchr
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
if (m_memory.read8(a0 + i) == static_cast<uint8_t>(a1))
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 11:
|
||||
setV0(static_cast<uint32_t>(compare(a0, a1, a2)));
|
||||
return true;
|
||||
case 12:
|
||||
copy(a0, a1, a2);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 13:
|
||||
{
|
||||
std::vector<uint8_t> temporary(a2);
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
temporary[i] = m_memory.read8(a1 + i);
|
||||
(void)m_memory.writeRam(a0, temporary.data(), temporary.size());
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 14:
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write8(a0 + i, static_cast<uint8_t>(a1));
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 15: // bcmp
|
||||
setV0(static_cast<uint32_t>(compare(a0, a1, a2)));
|
||||
return true;
|
||||
case 16: // bcopy(src,dst,n)
|
||||
copy(a1, a0, a2);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 17:
|
||||
for (uint32_t i = 0; i < a1; ++i)
|
||||
m_memory.write8(a0 + i, 0u);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 18: // prnt
|
||||
setV0(0);
|
||||
return true;
|
||||
case 19: // sprintf: preserve useful literal formats even before full vararg formatting.
|
||||
case 42: // vsprintf fallback: copy format literal.
|
||||
{
|
||||
const std::string format = m_memory.readString(a1, 4096u);
|
||||
for (size_t i = 0; i <= format.size(); ++i)
|
||||
{
|
||||
m_memory.write8(a0 + static_cast<uint32_t>(i), i < format.size() ? static_cast<uint8_t>(format[i]) : 0u);
|
||||
}
|
||||
setV0(static_cast<uint32_t>(format.size()));
|
||||
return true;
|
||||
}
|
||||
case 20:
|
||||
appendString(a0, a1);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 21: // strchr
|
||||
case 25: // index
|
||||
{
|
||||
const uint8_t needle = static_cast<uint8_t>(a1);
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a0 + i);
|
||||
if (character == needle)
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 22: // strcmp
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(a0 + i);
|
||||
const uint8_t right = m_memory.read8(a1 + i);
|
||||
if (left != right)
|
||||
{
|
||||
setV0(static_cast<uint32_t>(static_cast<int32_t>(left) - static_cast<int32_t>(right)));
|
||||
return true;
|
||||
}
|
||||
if (left == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 23: // strcpy
|
||||
{
|
||||
uint32_t i = 0;
|
||||
for (;; ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a1 + i);
|
||||
m_memory.write8(a0 + i, character);
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 24: // strcspn
|
||||
{
|
||||
const std::string reject = m_memory.readString(a1, 4096u);
|
||||
uint32_t count = 0;
|
||||
for (; count < (1u << 20); ++count)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + count));
|
||||
if (character == 0 || reject.find(character) != std::string::npos)
|
||||
break;
|
||||
}
|
||||
setV0(count);
|
||||
return true;
|
||||
}
|
||||
case 26: // rindex
|
||||
case 32: // strrchr
|
||||
{
|
||||
const uint8_t needle = static_cast<uint8_t>(a1);
|
||||
uint32_t found = 0u;
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a0 + i);
|
||||
if (character == needle)
|
||||
found = a0 + i;
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(found);
|
||||
return true;
|
||||
}
|
||||
case 27:
|
||||
setV0(static_cast<uint32_t>(m_memory.readString(a0, 1u << 20).size()));
|
||||
return true;
|
||||
case 28:
|
||||
appendString(a0, a1, a2);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 29: // strncmp
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(a0 + i);
|
||||
const uint8_t right = m_memory.read8(a1 + i);
|
||||
if (left != right)
|
||||
{
|
||||
setV0(static_cast<uint32_t>(static_cast<int32_t>(left) - static_cast<int32_t>(right)));
|
||||
return true;
|
||||
}
|
||||
if (left == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 30: // strncpy
|
||||
{
|
||||
bool ended = false;
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
const uint8_t character = ended ? 0u : m_memory.read8(a1 + i);
|
||||
if (character == 0u)
|
||||
ended = true;
|
||||
m_memory.write8(a0 + i, character);
|
||||
}
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 31: // strpbrk
|
||||
{
|
||||
const std::string accept = m_memory.readString(a1, 4096u);
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + i));
|
||||
if (character == 0)
|
||||
break;
|
||||
if (accept.find(character) != std::string::npos)
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 33: // strspn
|
||||
{
|
||||
const std::string accept = m_memory.readString(a1, 4096u);
|
||||
uint32_t count = 0;
|
||||
for (; count < (1u << 20); ++count)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + count));
|
||||
if (character == 0 || accept.find(character) == std::string::npos)
|
||||
break;
|
||||
}
|
||||
setV0(count);
|
||||
return true;
|
||||
}
|
||||
case 34: // strstr
|
||||
{
|
||||
const std::string needle = m_memory.readString(a1, 4096u);
|
||||
if (needle.empty())
|
||||
{
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
const std::string haystack = m_memory.readString(a0, 1u << 20);
|
||||
const size_t position = haystack.find(needle);
|
||||
setV0(position == std::string::npos
|
||||
? 0u
|
||||
: a0 + static_cast<uint32_t>(position));
|
||||
return true;
|
||||
}
|
||||
case 35: // strtok state is intentionally not shared across modules yet.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 36:
|
||||
case 38: // strtol / strtoul
|
||||
{
|
||||
const std::string value = m_memory.readString(a0, 4096u);
|
||||
char *end = nullptr;
|
||||
const int base = static_cast<int>(a2);
|
||||
const unsigned long parsed = ordinal == 36
|
||||
? static_cast<unsigned long>(std::strtol(value.c_str(), &end, base))
|
||||
: std::strtoul(value.c_str(), &end, base);
|
||||
if (a1 != 0u)
|
||||
{
|
||||
m_memory.write32(a1, a0 + static_cast<uint32_t>(end - value.c_str()));
|
||||
}
|
||||
setV0(static_cast<uint32_t>(parsed));
|
||||
return true;
|
||||
}
|
||||
case 37: // atob
|
||||
setV0(0);
|
||||
return true;
|
||||
case 40: // _wmemcopy, count is 32-bit words
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write32(a0 + i * 4u, m_memory.read32(a1 + i * 4u));
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 41:
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write32(a0 + i * 4u, a1);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 43:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopSysclib
|
||||
{
|
||||
public:
|
||||
explicit IopSysclib(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,11 @@ namespace ps2x::iop::detail
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual bool overridesPhysicalRpcServer() const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual RpcResult handleRpc(const RpcRequest &request) = 0;
|
||||
|
||||
virtual void onSifTransfer(const SifTransfer &transfer)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include "iop_service.h"
|
||||
#include "emulator/iop_emulator.h"
|
||||
#include "plugin_loader.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -68,7 +69,7 @@ namespace ps2x::iop
|
||||
{
|
||||
public:
|
||||
explicit Impl(IopHost &hostRef)
|
||||
: host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles())
|
||||
: host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles()), emulator(hostRef)
|
||||
{
|
||||
rebuildRoutes();
|
||||
}
|
||||
@@ -118,6 +119,7 @@ namespace ps2x::iop
|
||||
std::string activeProvider;
|
||||
std::string lastError;
|
||||
bool routesValid = true;
|
||||
detail::IopEmulator emulator;
|
||||
};
|
||||
|
||||
IopSubsystem::IopSubsystem(IopHost &host)
|
||||
@@ -248,6 +250,27 @@ namespace ps2x::iop
|
||||
service->reset();
|
||||
}
|
||||
}
|
||||
m_impl->emulator.reset();
|
||||
}
|
||||
|
||||
ModuleLoadResult IopSubsystem::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->emulator.loadModule(path, arguments, argumentSize);
|
||||
}
|
||||
|
||||
ModuleLoadResult IopSubsystem::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->emulator.loadModuleBuffer(guestAddress, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool IopSubsystem::stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
return m_impl->emulator.stopModule(moduleId, result);
|
||||
}
|
||||
|
||||
void IopSubsystem::runEeCycles(uint64_t eeCycles) noexcept
|
||||
{
|
||||
m_impl->emulator.runEeCycles(eeCycles);
|
||||
}
|
||||
|
||||
RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const
|
||||
@@ -277,14 +300,41 @@ namespace ps2x::iop
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
bool IopSubsystem::canBindRpc(uint32_t sid) const noexcept
|
||||
{
|
||||
if (m_impl->routes.find(sid) != m_impl->routes.end())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return m_impl->emulator.hasRpcServer(sid);
|
||||
}
|
||||
|
||||
RpcResult IopSubsystem::handleRpc(const RpcRequest &request)
|
||||
{
|
||||
const auto it = m_impl->routes.find(request.sid);
|
||||
if (it == m_impl->routes.end() || !it->second)
|
||||
const auto route = m_impl->routes.find(request.sid);
|
||||
detail::IopService *hle = route != m_impl->routes.end() ? route->second : nullptr;
|
||||
|
||||
// A profile can deliberately replace a physical endpoint when running
|
||||
// that IRX is outside the selected compatibility scope (for example,
|
||||
// disabling a game's audio driver while keeping the rest of its IOP
|
||||
// modules physical).
|
||||
if (hle && hle->overridesPhysicalRpcServer())
|
||||
{
|
||||
return {};
|
||||
RpcResult overridden = hle->handleRpc(request);
|
||||
if (overridden.handled)
|
||||
{
|
||||
return overridden;
|
||||
}
|
||||
}
|
||||
return it->second->handleRpc(request);
|
||||
|
||||
// Otherwise physical servers are authoritative and HLE remains a
|
||||
// compatibility fallback for endpoints no loaded IRX provides.
|
||||
RpcResult emulated = m_impl->emulator.handleRpc(request);
|
||||
if (emulated.handled || !hle || hle->overridesPhysicalRpcServer())
|
||||
{
|
||||
return emulated;
|
||||
}
|
||||
return hle->handleRpc(request);
|
||||
}
|
||||
|
||||
void IopSubsystem::onSifTransfer(const SifTransfer &transfer)
|
||||
@@ -303,11 +353,17 @@ namespace ps2x::iop
|
||||
service->onSifTransfer(transfer);
|
||||
}
|
||||
}
|
||||
m_impl->emulator.onSifTransfer(transfer);
|
||||
}
|
||||
|
||||
DebugSnapshot IopSubsystem::debugSnapshot() const
|
||||
{
|
||||
DebugSnapshot snapshot;
|
||||
snapshot.emulatorCycles = m_impl->emulator.cycles();
|
||||
snapshot.emulatorInstructions = m_impl->emulator.instructions();
|
||||
snapshot.emulatorLoadedModules = m_impl->emulator.loadedModuleCount();
|
||||
snapshot.emulatorThreads = m_impl->emulator.threadCount();
|
||||
snapshot.emulatorRpcServers = m_impl->emulator.rpcServerCount();
|
||||
snapshot.activeProfile = m_impl->activeProfile;
|
||||
snapshot.activeProvider = m_impl->activeProvider;
|
||||
snapshot.diagnostics = m_impl->diagnostics;
|
||||
|
||||
@@ -108,6 +108,7 @@ namespace ps2x::iop::detail
|
||||
bool zeroReceiveBuffer = true;
|
||||
bool signalNowaitCompletion = false;
|
||||
bool completeQueuedPlayStreams = false;
|
||||
bool overridePhysicalServer = false;
|
||||
std::vector<uint32_t> suppressedCompletionCallbacks;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ namespace ps2x::iop::detail
|
||||
return m_sids;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool overridesPhysicalRpcServer() const noexcept override
|
||||
{
|
||||
return m_bindings.overridePhysicalServer;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,48 @@
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
inline constexpr uint32_t MIPS_INSTRUCTION_SIZE = sizeof(uint32_t);
|
||||
inline constexpr uint16_t MIPS_IMMEDIATE_SIGN_BIT = 0x8000u;
|
||||
inline constexpr uint32_t MIPS_JUMP_TARGET_SHIFT = 2u;
|
||||
inline constexpr uint32_t MIPS_JUMP_REGION_MASK = 0xF0000000u;
|
||||
|
||||
// R5900 general-purpose register indices used by the encoded RS/RT/RD fields.
|
||||
enum GprRegisters : uint32_t
|
||||
{
|
||||
GPR_ZERO = 0,
|
||||
GPR_AT = 1,
|
||||
GPR_V0 = 2,
|
||||
GPR_V1 = 3,
|
||||
GPR_A0 = 4,
|
||||
GPR_A1 = 5,
|
||||
GPR_A2 = 6,
|
||||
GPR_A3 = 7,
|
||||
GPR_T0 = 8,
|
||||
GPR_T1 = 9,
|
||||
GPR_T2 = 10,
|
||||
GPR_T3 = 11,
|
||||
GPR_T4 = 12,
|
||||
GPR_T5 = 13,
|
||||
GPR_T6 = 14,
|
||||
GPR_T7 = 15,
|
||||
GPR_S0 = 16,
|
||||
GPR_S1 = 17,
|
||||
GPR_S2 = 18,
|
||||
GPR_S3 = 19,
|
||||
GPR_S4 = 20,
|
||||
GPR_S5 = 21,
|
||||
GPR_S6 = 22,
|
||||
GPR_S7 = 23,
|
||||
GPR_T8 = 24,
|
||||
GPR_T9 = 25,
|
||||
GPR_K0 = 26,
|
||||
GPR_K1 = 27,
|
||||
GPR_GP = 28,
|
||||
GPR_SP = 29,
|
||||
GPR_FP = 30,
|
||||
GPR_RA = 31,
|
||||
};
|
||||
|
||||
// Basic MIPS opcodes (shared with R4300i)
|
||||
enum MipsOpcodes
|
||||
{
|
||||
|
||||
@@ -42,9 +42,12 @@ namespace ps2recomp
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::vector<Section> §ions);
|
||||
static size_t ResliceEntryFunctions(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions);
|
||||
static size_t ResliceEntryFunctions(std::vector<Function> &functions, std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions);
|
||||
static size_t CollectInternalEntryTargets(
|
||||
const std::vector<Function> &functions,
|
||||
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::unordered_set<uint32_t> &entryAddresses,
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner);
|
||||
|
||||
static std::string ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength);
|
||||
|
||||
@@ -67,6 +70,7 @@ namespace ps2recomp
|
||||
std::unordered_set<std::string> m_stubFunctions;
|
||||
std::unordered_set<uint32_t> m_stubFunctionStarts;
|
||||
std::unordered_map<uint32_t, std::string> m_stubHandlerBindingsByStart;
|
||||
std::unordered_set<uint32_t> m_entryPointHintStarts;
|
||||
std::unordered_set<uint32_t> m_correctnessCriticalFunctionStarts;
|
||||
std::map<uint32_t, std::string> m_generatedStubs;
|
||||
std::unordered_map<uint32_t, std::string> m_functionRenames;
|
||||
|
||||
@@ -187,6 +187,7 @@ namespace ps2recomp
|
||||
std::vector<std::string> skipFunctions;
|
||||
std::unordered_map<uint32_t, std::string> patches;
|
||||
std::vector<std::string> stubImplementations;
|
||||
std::vector<std::string> entryPointHints;
|
||||
std::unordered_map<uint32_t, uint32_t> mmioByInstructionAddress;
|
||||
std::vector<JumpTable> jumpTables;
|
||||
};
|
||||
|
||||
@@ -74,6 +74,27 @@ namespace ps2recomp
|
||||
config.stubImplementations = toml::find<std::vector<std::string>>(data, "stubs");
|
||||
}
|
||||
|
||||
auto appendEntryPointHints = [&](const toml::value &table, const char *key)
|
||||
{
|
||||
if (!table.contains(key) || !table.at(key).is_array())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const auto values = toml::find<std::vector<std::string>>(table, key);
|
||||
config.entryPointHints.insert(
|
||||
config.entryPointHints.end(), values.begin(), values.end());
|
||||
};
|
||||
appendEntryPointHints(general, "entry_points");
|
||||
appendEntryPointHints(data, "entry_points");
|
||||
// Backward compatibility
|
||||
appendEntryPointHints(general, "untracked_stubs");
|
||||
appendEntryPointHints(data, "untracked_stubs");
|
||||
|
||||
std::sort(config.entryPointHints.begin(), config.entryPointHints.end());
|
||||
config.entryPointHints.erase(
|
||||
std::unique(config.entryPointHints.begin(), config.entryPointHints.end()),
|
||||
config.entryPointHints.end());
|
||||
|
||||
if (general.contains("skip") && general.at("skip").is_array())
|
||||
{
|
||||
config.skipFunctions = toml::find<std::vector<std::string>>(general, "skip");
|
||||
@@ -276,6 +297,7 @@ namespace ps2recomp
|
||||
general["patch_cache"] = config.patchCache;
|
||||
general["skip"] = config.skipFunctions;
|
||||
general["stubs"] = config.stubImplementations;
|
||||
general["entry_points"] = config.entryPointHints;
|
||||
data["general"] = general;
|
||||
|
||||
if (!config.mmioByInstructionAddress.empty())
|
||||
|
||||
@@ -135,6 +135,11 @@ namespace ps2recomp
|
||||
|
||||
for (const auto &inst : instructions)
|
||||
{
|
||||
if (inst.opcode == OPCODE_SPECIAL && inst.function == SPECIAL_SYSCALL)
|
||||
{
|
||||
queueResumeEntryTarget(inst.address + 4u);
|
||||
}
|
||||
|
||||
bool isStaticJump = (inst.opcode == OPCODE_J || inst.opcode == OPCODE_JAL);
|
||||
if (inst.isBranch && inst.opcode != OPCODE_J && inst.opcode != OPCODE_JAL)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ps2recomp/elf_parser.h"
|
||||
#include "ps2recomp/instructions.h"
|
||||
#include "ps2recomp/recompiler_reporter.h"
|
||||
#include "ps2recomp/types.h"
|
||||
#include <iostream>
|
||||
@@ -116,6 +117,8 @@ namespace
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace ps2recomp;
|
||||
|
||||
bool HasDwarfSections(const ELFIO::elfio &elf)
|
||||
{
|
||||
for (ELFIO::Elf_Half i = 0; i < elf.sections.size(); ++i)
|
||||
@@ -453,7 +456,328 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void ScanJalTargetsFallback(ps2recomp::ElfParser *parser, std::vector<ps2recomp::Function> &outFunctions)
|
||||
bool ReadSectionWord(const ps2recomp::Section §ion, uint32_t offset, uint32_t &outWord)
|
||||
{
|
||||
if (!section.data || offset > section.size || section.size - offset < sizeof(uint32_t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(&outWord, section.data + offset, sizeof(uint32_t));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LooksLikeCallableEntry(const std::vector<ps2recomp::Section> §ions, uint32_t address, bool allowLeafThunk)
|
||||
{
|
||||
if ((address % MIPS_INSTRUCTION_SIZE) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const ps2recomp::Section *section = FindCodeSectionByAddress(sections, address);
|
||||
if (!section || !section->data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t startOffset = address - section->address;
|
||||
constexpr uint32_t kProbeWords = 8;
|
||||
|
||||
for (uint32_t index = 0; index < kProbeWords; ++index)
|
||||
{
|
||||
uint32_t raw = 0;
|
||||
if (!ReadSectionWord(*section, startOffset + (index * MIPS_INSTRUCTION_SIZE), raw))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t opcode = OPCODE(raw);
|
||||
const uint32_t rs = RS(raw);
|
||||
const uint32_t rt = RT(raw);
|
||||
const uint16_t immediate = static_cast<uint16_t>(IMMEDIATE(raw));
|
||||
|
||||
// Non-leaf functions normally allocate their stack frame immediately.
|
||||
// Accept ADDIU/DADDIU $sp,$sp,-N in the first few instructions.
|
||||
if (index < 4 &&
|
||||
(opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU) &&
|
||||
rs == GPR_SP && rt == GPR_SP &&
|
||||
(immediate & MIPS_IMMEDIATE_SIGN_BIT) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Some prologues set up GP before saving RA, so also recognize the
|
||||
// common SW/SD/SQ $ra,offset($sp) forms in the entry window.
|
||||
if ((opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) &&
|
||||
rs == GPR_SP && rt == GPR_RA)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Leaf callbacks and vtable thunks often have no stack frame at all.
|
||||
if (allowLeafThunk &&
|
||||
opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JR && rs == GPR_RA)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WritesGpr(uint32_t raw, uint32_t reg)
|
||||
{
|
||||
if (reg == GPR_ZERO)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t opcode = OPCODE(raw);
|
||||
const uint32_t rt = RT(raw);
|
||||
const uint32_t rd = RD(raw);
|
||||
|
||||
if (opcode == OPCODE_SPECIAL || opcode == OPCODE_MMI)
|
||||
{
|
||||
return rd == reg;
|
||||
}
|
||||
|
||||
if (opcode == OPCODE_JAL)
|
||||
{
|
||||
return reg == GPR_RA;
|
||||
}
|
||||
|
||||
bool writesRt = false;
|
||||
switch (opcode)
|
||||
{
|
||||
case OPCODE_ADDI:
|
||||
case OPCODE_ADDIU:
|
||||
case OPCODE_SLTI:
|
||||
case OPCODE_SLTIU:
|
||||
case OPCODE_ANDI:
|
||||
case OPCODE_ORI:
|
||||
case OPCODE_XORI:
|
||||
case OPCODE_LUI:
|
||||
case OPCODE_DADDI:
|
||||
case OPCODE_DADDIU:
|
||||
case OPCODE_LDL:
|
||||
case OPCODE_LDR:
|
||||
case OPCODE_LQ:
|
||||
case OPCODE_LB:
|
||||
case OPCODE_LH:
|
||||
case OPCODE_LWL:
|
||||
case OPCODE_LW:
|
||||
case OPCODE_LBU:
|
||||
case OPCODE_LHU:
|
||||
case OPCODE_LWR:
|
||||
case OPCODE_LWU:
|
||||
case OPCODE_LL:
|
||||
case OPCODE_LLD:
|
||||
case OPCODE_LD:
|
||||
writesRt = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return writesRt && rt == reg;
|
||||
}
|
||||
|
||||
bool IsControlTransfer(uint32_t raw)
|
||||
{
|
||||
const uint32_t opcode = OPCODE(raw);
|
||||
switch (opcode)
|
||||
{
|
||||
case OPCODE_REGIMM:
|
||||
case OPCODE_J:
|
||||
case OPCODE_JAL:
|
||||
case OPCODE_BEQ:
|
||||
case OPCODE_BNE:
|
||||
case OPCODE_BLEZ:
|
||||
case OPCODE_BGTZ:
|
||||
case OPCODE_BEQL:
|
||||
case OPCODE_BNEL:
|
||||
case OPCODE_BLEZL:
|
||||
case OPCODE_BGTZL:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (opcode != OPCODE_SPECIAL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t function = FUNCTION(raw);
|
||||
return function == SPECIAL_JR || function == SPECIAL_JALR;
|
||||
}
|
||||
|
||||
bool IsCallInstruction(uint32_t raw)
|
||||
{
|
||||
const uint32_t opcode = OPCODE(raw);
|
||||
return opcode == OPCODE_JAL ||
|
||||
(opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JALR);
|
||||
}
|
||||
|
||||
void ScanMaterializedCodeAddresses(const std::vector<ps2recomp::Section> §ions,
|
||||
std::unordered_set<uint32_t> &starts)
|
||||
{
|
||||
constexpr uint32_t kMaxLookaheadWords = 4;
|
||||
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (!section.isCode || !section.data || section.size < (2u * MIPS_INSTRUCTION_SIZE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size;
|
||||
offset += MIPS_INSTRUCTION_SIZE)
|
||||
{
|
||||
uint32_t upperRaw = 0;
|
||||
if (!ReadSectionWord(section, offset, upperRaw) ||
|
||||
OPCODE(upperRaw) != OPCODE_LUI)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t upperReg = RT(upperRaw);
|
||||
if (upperReg == GPR_ZERO)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t upperValue = IMMEDIATE(upperRaw) << 16;
|
||||
bool sawControlTransfer = false;
|
||||
bool sawCallTransfer = false;
|
||||
|
||||
for (uint32_t lookahead = 1; lookahead <= kMaxLookaheadWords; ++lookahead)
|
||||
{
|
||||
uint32_t lowRaw = 0;
|
||||
if (!ReadSectionWord(section, offset + (lookahead * MIPS_INSTRUCTION_SIZE), lowRaw))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t opcode = OPCODE(lowRaw);
|
||||
const uint32_t rs = RS(lowRaw);
|
||||
const uint32_t rt = RT(lowRaw);
|
||||
|
||||
if ((opcode == OPCODE_ADDIU || opcode == OPCODE_ORI || opcode == OPCODE_DADDIU) &&
|
||||
rs == upperReg)
|
||||
{
|
||||
const uint16_t immediate = static_cast<uint16_t>(IMMEDIATE(lowRaw));
|
||||
uint32_t target = 0;
|
||||
if (opcode == OPCODE_ORI)
|
||||
{
|
||||
target = upperValue | static_cast<uint32_t>(immediate);
|
||||
}
|
||||
else // ADDIU/DADDIU use a signed low half
|
||||
{
|
||||
target = upperValue + static_cast<uint32_t>(
|
||||
static_cast<int32_t>(static_cast<int16_t>(immediate)));
|
||||
}
|
||||
|
||||
uint32_t nextRaw = 0;
|
||||
const bool followedByCall =
|
||||
ReadSectionWord(section,
|
||||
offset + ((lookahead + 1u) * MIPS_INSTRUCTION_SIZE),
|
||||
nextRaw) &&
|
||||
IsCallInstruction(nextRaw);
|
||||
const bool materializedAsCallArgument =
|
||||
rt >= GPR_A0 && rt <= GPR_A3 && (sawCallTransfer || followedByCall);
|
||||
|
||||
if (LooksLikeCallableEntry(sections, target, materializedAsCallArgument))
|
||||
{
|
||||
starts.insert(target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// The instruction immediately after a branch/call is its
|
||||
// delay slot. It may complete a callback address, but no
|
||||
// later instruction is in the same straight-line state.
|
||||
if (sawControlTransfer)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (WritesGpr(lowRaw, upperReg))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsControlTransfer(lowRaw))
|
||||
{
|
||||
sawControlTransfer = true;
|
||||
sawCallTransfer = IsCallInstruction(lowRaw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsDedicatedFunctionPointerSection(const std::string &name)
|
||||
{
|
||||
return name == ".ctors" || name == ".dtors" ||
|
||||
name == ".init_array" || name == ".fini_array";
|
||||
}
|
||||
|
||||
void ScanDataFunctionPointerTables(const std::vector<ps2recomp::Section> §ions,
|
||||
std::unordered_set<uint32_t> &starts)
|
||||
{
|
||||
struct PointerCandidate
|
||||
{
|
||||
uint32_t sourceOffset;
|
||||
uint32_t target;
|
||||
};
|
||||
|
||||
constexpr uint32_t kClusterDistanceBytes = 32;
|
||||
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (!section.isData || section.isCode || section.isBSS ||
|
||||
!section.data || section.size < MIPS_INSTRUCTION_SIZE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<PointerCandidate> candidates;
|
||||
for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size;
|
||||
offset += MIPS_INSTRUCTION_SIZE)
|
||||
{
|
||||
uint32_t target = 0;
|
||||
if (ReadSectionWord(section, offset, target) &&
|
||||
LooksLikeCallableEntry(sections, target, true))
|
||||
{
|
||||
candidates.push_back({offset, target});
|
||||
}
|
||||
}
|
||||
|
||||
const bool dedicatedPointerSection = IsDedicatedFunctionPointerSection(section.name);
|
||||
for (size_t index = 0; index < candidates.size(); ++index)
|
||||
{
|
||||
bool clustered = dedicatedPointerSection;
|
||||
if (index > 0 &&
|
||||
candidates[index].sourceOffset - candidates[index - 1].sourceOffset <= kClusterDistanceBytes)
|
||||
{
|
||||
clustered = true;
|
||||
}
|
||||
if (index + 1 < candidates.size() &&
|
||||
candidates[index + 1].sourceOffset - candidates[index].sourceOffset <= kClusterDistanceBytes)
|
||||
{
|
||||
clustered = true;
|
||||
}
|
||||
|
||||
if (clustered)
|
||||
{
|
||||
starts.insert(candidates[index].target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScanFunctionStartsFallback(ps2recomp::ElfParser *parser, std::vector<ps2recomp::Function> &outFunctions)
|
||||
{
|
||||
std::unordered_set<uint32_t> starts;
|
||||
starts.reserve(4096);
|
||||
@@ -467,26 +791,29 @@ namespace
|
||||
const auto §ions = parser->getSections();
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (!section.isCode || !section.data || section.size < 4)
|
||||
if (!section.isCode || !section.data || section.size < MIPS_INSTRUCTION_SIZE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint32_t offset = 0; offset + 4 <= section.size; offset += 4)
|
||||
for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size;
|
||||
offset += MIPS_INSTRUCTION_SIZE)
|
||||
{
|
||||
const uint32_t pc = section.address + offset;
|
||||
|
||||
uint32_t raw = 0;
|
||||
std::memcpy(&raw, section.data + offset, sizeof(uint32_t));
|
||||
|
||||
const uint32_t op = (raw >> 26) & 0x3F;
|
||||
if (op != 0x03) // JAL
|
||||
const uint32_t op = OPCODE(raw);
|
||||
if (op != OPCODE_JAL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t index = raw & 0x03FFFFFF;
|
||||
const uint32_t target = ((pc + 4) & 0xF0000000u) | (index << 2);
|
||||
const uint32_t index = TARGET(raw);
|
||||
const uint32_t target =
|
||||
((pc + MIPS_INSTRUCTION_SIZE) & MIPS_JUMP_REGION_MASK) |
|
||||
(index << MIPS_JUMP_TARGET_SHIFT);
|
||||
|
||||
if (FindCodeSectionByAddress(sections, target))
|
||||
{
|
||||
@@ -494,6 +821,9 @@ namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScanMaterializedCodeAddresses(sections, starts);
|
||||
ScanDataFunctionPointerTables(sections, starts);
|
||||
|
||||
std::vector<uint32_t> sortedStarts(starts.begin(), starts.end());
|
||||
std::sort(sortedStarts.begin(), sortedStarts.end());
|
||||
@@ -523,7 +853,7 @@ namespace
|
||||
ps2recomp::Function func{};
|
||||
func.name = MakeAutoFunctionName(start);
|
||||
func.start = start;
|
||||
func.end = (end > start) ? end : (start + 4);
|
||||
func.end = (end > start) ? end : (start + MIPS_INSTRUCTION_SIZE);
|
||||
func.isRecompiled = false;
|
||||
func.isStub = false;
|
||||
func.isSkipped = false;
|
||||
@@ -1420,7 +1750,7 @@ namespace ps2recomp
|
||||
|
||||
if (m_extraFunctions.empty())
|
||||
{
|
||||
ScanJalTargetsFallback(this, m_extraFunctions);
|
||||
ScanFunctionStartsFallback(this, m_extraFunctions);
|
||||
}
|
||||
|
||||
std::sort(m_extraFunctions.begin(), m_extraFunctions.end(),
|
||||
|
||||
@@ -45,8 +45,8 @@ namespace ps2recomp
|
||||
ss << "#include <stdexcept>\n";
|
||||
ss << "#include \"ps2_runtime_macros.h\"\n";
|
||||
ss << "#include \"ps2_runtime.h\"\n";
|
||||
ss << "#include \"ps2_recompiled_functions.h\"\n";
|
||||
ss << "#include \"ps2_recompiled_stubs.h\"\n\n";
|
||||
ss << "#include <ps2_recompiled_functions.h>\n";
|
||||
ss << "#include <ps2_recompiled_stubs.h>\n\n";
|
||||
ss << "#include \"ps2_syscalls.h\"\n";
|
||||
ss << "#include \"ps2_stubs.h\"\n\n";
|
||||
ss << "#ifdef PS2_FUNCTION_LOG_TRACKER\n";
|
||||
|
||||
@@ -102,10 +102,10 @@ namespace ps2recomp
|
||||
void writeCombinedOutputPreamble(std::ostream &output)
|
||||
{
|
||||
output << "#include <stdexcept>\n";
|
||||
output << "#include \"ps2_recompiled_functions.h\"\n\n";
|
||||
output << "#include <ps2_recompiled_functions.h>\n\n";
|
||||
output << "#include \"ps2_runtime_macros.h\"\n";
|
||||
output << "#include \"ps2_runtime.h\"\n";
|
||||
output << "#include \"ps2_recompiled_stubs.h\"\n";
|
||||
output << "#include <ps2_recompiled_stubs.h>\n";
|
||||
output << "#include \"ps2_syscalls.h\"\n";
|
||||
output << "#include \"ps2_stubs.h\"\n";
|
||||
output << "#ifdef _DEBUG\n";
|
||||
@@ -725,6 +725,71 @@ namespace ps2recomp
|
||||
|
||||
return reslicedCount;
|
||||
}
|
||||
|
||||
size_t collectInternalEntryTargetsImpl(
|
||||
const std::vector<Function> &functions,
|
||||
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::unordered_set<uint32_t> &entryAddresses,
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner)
|
||||
{
|
||||
std::unordered_set<uint32_t> functionStarts;
|
||||
functionStarts.reserve(functions.size());
|
||||
for (const auto &function : functions)
|
||||
{
|
||||
functionStarts.insert(function.start);
|
||||
}
|
||||
|
||||
size_t addedCount = 0u;
|
||||
for (uint32_t entryAddress : entryAddresses)
|
||||
{
|
||||
if (functionStarts.contains(entryAddress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const Function *owner = nullptr;
|
||||
for (const auto &function : functions)
|
||||
{
|
||||
if (!function.isRecompiled || function.isStub || function.isSkipped ||
|
||||
entryAddress <= function.start || entryAddress >= function.end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto decodedIt = decodedFunctions.find(function.start);
|
||||
if (decodedIt == decodedFunctions.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool containsInstruction = std::any_of(decodedIt->second.begin(), decodedIt->second.end(), [entryAddress](const Instruction &instruction)
|
||||
{ return instruction.address == entryAddress; });
|
||||
if (!containsInstruction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!owner || function.start > owner->start)
|
||||
{
|
||||
owner = &function;
|
||||
}
|
||||
}
|
||||
|
||||
if (!owner)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto &targets = targetsByOwner[owner->start];
|
||||
if (std::find(targets.begin(), targets.end(), entryAddress) == targets.end())
|
||||
{
|
||||
targets.push_back(entryAddress);
|
||||
++addedCount;
|
||||
}
|
||||
}
|
||||
|
||||
return addedCount;
|
||||
}
|
||||
}
|
||||
|
||||
PS2Recompiler::PS2Recompiler(const std::string &configPath)
|
||||
@@ -751,6 +816,7 @@ namespace ps2recomp
|
||||
m_stubFunctions.clear();
|
||||
m_stubFunctionStarts.clear();
|
||||
m_stubHandlerBindingsByStart.clear();
|
||||
m_entryPointHintStarts.clear();
|
||||
m_correctnessCriticalFunctionStarts.clear();
|
||||
|
||||
for (const auto &name : m_config.skipFunctions)
|
||||
@@ -792,6 +858,14 @@ namespace ps2recomp
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &hint : m_config.entryPointHints)
|
||||
{
|
||||
const FunctionSelector selector = parseFunctionSelector(hint);
|
||||
if (selector.start.has_value())
|
||||
{
|
||||
m_entryPointHintStarts.insert(*selector.start);
|
||||
}
|
||||
}
|
||||
|
||||
m_reporter.progress("parsing ELF");
|
||||
m_elfParser = std::make_unique<ElfParser>(m_config.inputPath);
|
||||
@@ -983,7 +1057,7 @@ namespace ps2recomp
|
||||
|
||||
if (isStubFunction(function))
|
||||
{
|
||||
if (!correctnessCritical || hasResolvedStubHandler(function))
|
||||
if (hasResolvedStubHandler(function))
|
||||
{
|
||||
function.isStub = true;
|
||||
function.isSkipped = false;
|
||||
@@ -991,12 +1065,15 @@ namespace ps2recomp
|
||||
continue;
|
||||
}
|
||||
|
||||
m_reporter.recordCorrectnessCriticalGuestFallback();
|
||||
if (correctnessCritical)
|
||||
{
|
||||
m_reporter.recordCorrectnessCriticalGuestFallback();
|
||||
}
|
||||
m_reporter.warningAt(
|
||||
"correctness-critical",
|
||||
"stub",
|
||||
function.name,
|
||||
function.start,
|
||||
"Unresolved initializer stub ignored; recompiling the original guest function");
|
||||
"Configured stub has no runtime handler; recompiling the original guest function");
|
||||
}
|
||||
|
||||
if (shouldSkipFunction(function))
|
||||
@@ -1882,6 +1959,22 @@ namespace ps2recomp
|
||||
targets.push_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> guestFallbackEntryAddresses = m_entryPointHintStarts;
|
||||
for (uint32_t address : m_stubFunctionStarts)
|
||||
{
|
||||
const auto bindingIt = m_stubHandlerBindingsByStart.find(address);
|
||||
if (bindingIt == m_stubHandlerBindingsByStart.end() ||
|
||||
resolveStubTarget(bindingIt->second) == StubTarget::Unknown)
|
||||
{
|
||||
guestFallbackEntryAddresses.insert(address);
|
||||
}
|
||||
}
|
||||
collectInternalEntryTargetsImpl(
|
||||
m_functions,
|
||||
m_decodedFunctions,
|
||||
guestFallbackEntryAddresses,
|
||||
m_resumeEntryTargetsByOwner);
|
||||
|
||||
size_t totalTargets = 0u;
|
||||
for (auto it = m_resumeEntryTargetsByOwner.begin(); it != m_resumeEntryTargetsByOwner.end();)
|
||||
@@ -2152,12 +2245,12 @@ namespace ps2recomp
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
std::string PS2Recompiler::clampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
|
||||
std::string PS2Recompiler::clampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength)
|
||||
{
|
||||
if (maxLength == 0)
|
||||
{
|
||||
// Keep this static helper side-effect free; callers validate arguments.
|
||||
//Better go over the limit than create files with an empty path
|
||||
// Better go over the limit than create files with an empty path
|
||||
return baseName + extension;
|
||||
}
|
||||
|
||||
@@ -2224,13 +2317,20 @@ namespace ps2recomp
|
||||
return stats.discoveredCount;
|
||||
}
|
||||
|
||||
size_t PS2Recompiler::ResliceEntryFunctions(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
|
||||
size_t PS2Recompiler::ResliceEntryFunctions(std::vector<Function> &functions, std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions)
|
||||
{
|
||||
return resliceEntryFunctionsImpl(functions, decodedFunctions);
|
||||
}
|
||||
|
||||
size_t PS2Recompiler::CollectInternalEntryTargets(
|
||||
const std::vector<Function> &functions,
|
||||
const std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
const std::unordered_set<uint32_t> &entryAddresses,
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> &targetsByOwner)
|
||||
{
|
||||
return collectInternalEntryTargetsImpl(functions, decodedFunctions, entryAddresses, targetsByOwner);
|
||||
}
|
||||
|
||||
StubTarget PS2Recompiler::resolveStubTarget(const std::string &name)
|
||||
{
|
||||
if (!ps2_runtime_calls::resolveSyscallName(name).empty())
|
||||
@@ -2244,7 +2344,7 @@ namespace ps2recomp
|
||||
return StubTarget::Unknown;
|
||||
}
|
||||
|
||||
std::string PS2Recompiler::ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength)
|
||||
std::string PS2Recompiler::ClampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength)
|
||||
{
|
||||
return clampFilenameLength(baseName, extension, maxLength);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,70 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class ExportPS2Functions extends GhidraScript {
|
||||
|
||||
// Names and values mirror ps2recomp::MipsOpcodes/SpecialFunctions/GprRegisters.
|
||||
// would be amazing cmake create this script coping the register from the header file
|
||||
private static final int MIPS_INSTRUCTION_SIZE = 4;
|
||||
private static final int MIPS_IMMEDIATE_BITS = 16;
|
||||
private static final int MIPS_IMMEDIATE_SIGN_BIT = 0x8000;
|
||||
private static final long UINT32_MASK = 0xFFFFFFFFL;
|
||||
private static final long OPCODE_MASK = 0x3FL;
|
||||
private static final long REGISTER_MASK = 0x1FL;
|
||||
private static final long IMMEDIATE_MASK = 0xFFFFL;
|
||||
private static final int OPCODE_SHIFT = 26;
|
||||
private static final int RS_SHIFT = 21;
|
||||
private static final int RT_SHIFT = 16;
|
||||
private static final int RD_SHIFT = 11;
|
||||
|
||||
private static final int GPR_ZERO = 0;
|
||||
private static final int GPR_A0 = 4;
|
||||
private static final int GPR_A3 = 7;
|
||||
private static final int GPR_SP = 29;
|
||||
private static final int GPR_RA = 31;
|
||||
|
||||
private static final int OPCODE_SPECIAL = 0x00;
|
||||
private static final int OPCODE_REGIMM = 0x01;
|
||||
private static final int OPCODE_J = 0x02;
|
||||
private static final int OPCODE_JAL = 0x03;
|
||||
private static final int OPCODE_BEQ = 0x04;
|
||||
private static final int OPCODE_BNE = 0x05;
|
||||
private static final int OPCODE_BLEZ = 0x06;
|
||||
private static final int OPCODE_BGTZ = 0x07;
|
||||
private static final int OPCODE_ADDI = 0x08;
|
||||
private static final int OPCODE_ADDIU = 0x09;
|
||||
private static final int OPCODE_SLTI = 0x0A;
|
||||
private static final int OPCODE_SLTIU = 0x0B;
|
||||
private static final int OPCODE_ANDI = 0x0C;
|
||||
private static final int OPCODE_ORI = 0x0D;
|
||||
private static final int OPCODE_XORI = 0x0E;
|
||||
private static final int OPCODE_LUI = 0x0F;
|
||||
private static final int OPCODE_BEQL = 0x14;
|
||||
private static final int OPCODE_BNEL = 0x15;
|
||||
private static final int OPCODE_BLEZL = 0x16;
|
||||
private static final int OPCODE_BGTZL = 0x17;
|
||||
private static final int OPCODE_DADDI = 0x18;
|
||||
private static final int OPCODE_DADDIU = 0x19;
|
||||
private static final int OPCODE_LDL = 0x1A;
|
||||
private static final int OPCODE_LDR = 0x1B;
|
||||
private static final int OPCODE_MMI = 0x1C;
|
||||
private static final int OPCODE_LQ = 0x1E;
|
||||
private static final int OPCODE_SQ = 0x1F;
|
||||
private static final int OPCODE_LB = 0x20;
|
||||
private static final int OPCODE_LH = 0x21;
|
||||
private static final int OPCODE_LWL = 0x22;
|
||||
private static final int OPCODE_LW = 0x23;
|
||||
private static final int OPCODE_LBU = 0x24;
|
||||
private static final int OPCODE_LHU = 0x25;
|
||||
private static final int OPCODE_LWR = 0x26;
|
||||
private static final int OPCODE_LWU = 0x27;
|
||||
private static final int OPCODE_SW = 0x2B;
|
||||
private static final int OPCODE_LL = 0x30;
|
||||
private static final int OPCODE_LLD = 0x34;
|
||||
private static final int OPCODE_LD = 0x37;
|
||||
private static final int OPCODE_SD = 0x3F;
|
||||
|
||||
private static final int SPECIAL_JR = 0x08;
|
||||
private static final int SPECIAL_JALR = 0x09;
|
||||
|
||||
// For now I have to copy all functions from the runtime handler list
|
||||
private static final Set<String> RUNTIME_HANDLER_NAMES = new HashSet<>(Arrays.asList(
|
||||
"FlushCache", "iFlushCache", "ResetEE", "SetMemoryMode", "InitThread", "CreateThread",
|
||||
@@ -207,6 +271,16 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
boolean syntheticEntry = false;
|
||||
}
|
||||
|
||||
private static final class AddressTakenCandidate {
|
||||
long sourceOffset;
|
||||
long target;
|
||||
|
||||
AddressTakenCandidate(long sourceOffset, long target) {
|
||||
this.sourceOffset = sourceOffset;
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ClassificationKind {
|
||||
STUB,
|
||||
UNTRACKED_STUB,
|
||||
@@ -224,7 +298,31 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
}
|
||||
|
||||
private static String hex(long value) {
|
||||
return String.format("0x%08X", value & 0xFFFFFFFFL);
|
||||
return String.format("0x%08X", value & UINT32_MASK);
|
||||
}
|
||||
|
||||
private static int opcode(long raw) {
|
||||
return (int) ((raw >>> OPCODE_SHIFT) & OPCODE_MASK);
|
||||
}
|
||||
|
||||
private static int rs(long raw) {
|
||||
return (int) ((raw >>> RS_SHIFT) & REGISTER_MASK);
|
||||
}
|
||||
|
||||
private static int rt(long raw) {
|
||||
return (int) ((raw >>> RT_SHIFT) & REGISTER_MASK);
|
||||
}
|
||||
|
||||
private static int rd(long raw) {
|
||||
return (int) ((raw >>> RD_SHIFT) & REGISTER_MASK);
|
||||
}
|
||||
|
||||
private static int function(long raw) {
|
||||
return (int) (raw & OPCODE_MASK);
|
||||
}
|
||||
|
||||
private static int immediate(long raw) {
|
||||
return (int) (raw & IMMEDIATE_MASK);
|
||||
}
|
||||
|
||||
private static String tomlString(String value) {
|
||||
@@ -466,8 +564,8 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
}
|
||||
|
||||
MemoryBlock fromBlock = currentProgram.getMemory().getBlock(from);
|
||||
if (fromBlock == null || !fromBlock.isExecute()) {
|
||||
continue; // lets ignore DATA/non-code refs
|
||||
if (fromBlock != null && fromBlock.isExecute()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +573,311 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
}
|
||||
|
||||
private static String makeAnonymousEntryName(long start) {
|
||||
return String.format("entry_%08x", start & 0xFFFFFFFFL);
|
||||
return String.format("entry_%08x", start & UINT32_MASK);
|
||||
}
|
||||
|
||||
private Long readWord(Address address) {
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return ((long) currentProgram.getMemory().getInt(address)) & UINT32_MASK;
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Address addressFromOffset(long offset) {
|
||||
try {
|
||||
return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset & UINT32_MASK);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean looksLikeCallableEntry(long target, boolean allowLeafThunk) {
|
||||
if ((target % MIPS_INSTRUCTION_SIZE) != 0L) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Address address = addressFromOffset(target);
|
||||
if (!isExecutableAddress(address) || currentProgram.getListing().getInstructionAt(address) == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int index = 0; index < 8; ++index) {
|
||||
Address probe;
|
||||
try {
|
||||
probe = address.add(index * (long) MIPS_INSTRUCTION_SIZE);
|
||||
} catch (Exception ignored) {
|
||||
break;
|
||||
}
|
||||
|
||||
Long rawValue = readWord(probe);
|
||||
if (rawValue == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
long raw = rawValue;
|
||||
int opcode = opcode(raw);
|
||||
int rs = rs(raw);
|
||||
int rt = rt(raw);
|
||||
int immediate = immediate(raw);
|
||||
|
||||
if (index < 4 && (opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU) && rs == GPR_SP && rt == GPR_SP && (immediate & MIPS_IMMEDIATE_SIGN_BIT) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) &&
|
||||
rs == GPR_SP && rt == GPR_RA) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowLeafThunk && opcode == OPCODE_SPECIAL &&
|
||||
function(raw) == SPECIAL_JR && rs == GPR_RA) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean writesGpr(long raw, int register) {
|
||||
if (register == GPR_ZERO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int opcode = opcode(raw);
|
||||
int rt = rt(raw);
|
||||
int rd = rd(raw);
|
||||
|
||||
if (opcode == OPCODE_SPECIAL || opcode == OPCODE_MMI) {
|
||||
return rd == register;
|
||||
}
|
||||
if (opcode == OPCODE_JAL) {
|
||||
return register == GPR_RA;
|
||||
}
|
||||
|
||||
boolean writesRt;
|
||||
switch (opcode) {
|
||||
case OPCODE_ADDI:
|
||||
case OPCODE_ADDIU:
|
||||
case OPCODE_SLTI:
|
||||
case OPCODE_SLTIU:
|
||||
case OPCODE_ANDI:
|
||||
case OPCODE_ORI:
|
||||
case OPCODE_XORI:
|
||||
case OPCODE_LUI:
|
||||
case OPCODE_DADDI:
|
||||
case OPCODE_DADDIU:
|
||||
case OPCODE_LDL:
|
||||
case OPCODE_LDR:
|
||||
case OPCODE_LQ:
|
||||
case OPCODE_LB:
|
||||
case OPCODE_LH:
|
||||
case OPCODE_LWL:
|
||||
case OPCODE_LW:
|
||||
case OPCODE_LBU:
|
||||
case OPCODE_LHU:
|
||||
case OPCODE_LWR:
|
||||
case OPCODE_LWU:
|
||||
case OPCODE_LL:
|
||||
case OPCODE_LLD:
|
||||
case OPCODE_LD:
|
||||
writesRt = true;
|
||||
break;
|
||||
default:
|
||||
writesRt = false;
|
||||
break;
|
||||
}
|
||||
return writesRt && rt == register;
|
||||
}
|
||||
|
||||
private static boolean isControlTransfer(long raw) {
|
||||
int opcode = opcode(raw);
|
||||
switch (opcode) {
|
||||
case OPCODE_REGIMM:
|
||||
case OPCODE_J:
|
||||
case OPCODE_JAL:
|
||||
case OPCODE_BEQ:
|
||||
case OPCODE_BNE:
|
||||
case OPCODE_BLEZ:
|
||||
case OPCODE_BGTZ:
|
||||
case OPCODE_BEQL:
|
||||
case OPCODE_BNEL:
|
||||
case OPCODE_BLEZL:
|
||||
case OPCODE_BGTZL:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (opcode != OPCODE_SPECIAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int function = function(raw);
|
||||
return function == SPECIAL_JR || function == SPECIAL_JALR;
|
||||
}
|
||||
|
||||
private static boolean isCallInstruction(long raw) {
|
||||
int opcode = opcode(raw);
|
||||
return opcode == OPCODE_JAL ||
|
||||
(opcode == OPCODE_SPECIAL && function(raw) == SPECIAL_JALR);
|
||||
}
|
||||
|
||||
private void addSyntheticEntry(List<FunctionRecord> records, Set<Long> existingStarts, long target) {
|
||||
target &= UINT32_MASK;
|
||||
if (!existingStarts.add(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FunctionRecord record = new FunctionRecord();
|
||||
record.name = makeAnonymousEntryName(target);
|
||||
record.start = target;
|
||||
record.syntheticEntry = true;
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
private void collectMaterializedCodeEntries(List<FunctionRecord> records, Set<Long> existingStarts) {
|
||||
AddressSet executableAddresses = new AddressSet();
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
if (block != null && block.isExecute()) {
|
||||
executableAddresses.addRange(block.getStart(), block.getEnd());
|
||||
}
|
||||
}
|
||||
|
||||
InstructionIterator instructions = currentProgram.getListing().getInstructions(executableAddresses, true);
|
||||
while (instructions.hasNext() && !monitor.isCancelled()) {
|
||||
Instruction instruction = instructions.next();
|
||||
if (instruction == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Long upperRawValue = readWord(instruction.getAddress());
|
||||
if (upperRawValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long upperRaw = upperRawValue;
|
||||
if (opcode(upperRaw) != OPCODE_LUI) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int upperRegister = rt(upperRaw);
|
||||
if (upperRegister == GPR_ZERO) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long upperValue = ((long) immediate(upperRaw)) << MIPS_IMMEDIATE_BITS;
|
||||
boolean sawControlTransfer = false;
|
||||
boolean sawCallTransfer = false;
|
||||
|
||||
for (int lookahead = 1; lookahead <= 4; ++lookahead) {
|
||||
Address lowAddress;
|
||||
try {
|
||||
lowAddress = instruction.getAddress().add(lookahead * (long) MIPS_INSTRUCTION_SIZE);
|
||||
} catch (Exception ignored) {
|
||||
break;
|
||||
}
|
||||
|
||||
Long lowRawValue = readWord(lowAddress);
|
||||
if (lowRawValue == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
long lowRaw = lowRawValue;
|
||||
int opcode = opcode(lowRaw);
|
||||
int rs = rs(lowRaw);
|
||||
int rt = rt(lowRaw);
|
||||
|
||||
if ((opcode == OPCODE_ADDIU || opcode == OPCODE_ORI || opcode == OPCODE_DADDIU) &&
|
||||
rs == upperRegister) {
|
||||
int immediate = immediate(lowRaw);
|
||||
long target;
|
||||
if (opcode == OPCODE_ORI) {
|
||||
target = upperValue | immediate;
|
||||
} else {
|
||||
target = (upperValue + (short) immediate) & UINT32_MASK;
|
||||
}
|
||||
|
||||
Long nextRaw = readWord(addressFromOffset(
|
||||
lowAddress.getOffset() + MIPS_INSTRUCTION_SIZE));
|
||||
boolean followedByCall = nextRaw != null && isCallInstruction(nextRaw);
|
||||
boolean materializedAsCallArgument =
|
||||
rt >= GPR_A0 && rt <= GPR_A3 && (sawCallTransfer || followedByCall);
|
||||
|
||||
if (looksLikeCallableEntry(target, materializedAsCallArgument)) {
|
||||
addSyntheticEntry(records, existingStarts, target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sawControlTransfer) {
|
||||
break;
|
||||
}
|
||||
if (writesGpr(lowRaw, upperRegister)) {
|
||||
break;
|
||||
}
|
||||
if (isControlTransfer(lowRaw)) {
|
||||
sawControlTransfer = true;
|
||||
sawCallTransfer = isCallInstruction(lowRaw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDedicatedFunctionPointerBlock(String name) {
|
||||
return ".ctors".equals(name) || ".dtors".equals(name) ||
|
||||
".init_array".equals(name) || ".fini_array".equals(name);
|
||||
}
|
||||
|
||||
private void collectDataFunctionPointerEntries(List<FunctionRecord> records, Set<Long> existingStarts) {
|
||||
final long clusterDistanceBytes = 32L;
|
||||
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
if (block == null || block.isExecute() || !block.isInitialized() ||
|
||||
block.getSize() < MIPS_INSTRUCTION_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<AddressTakenCandidate> candidates = new ArrayList<>();
|
||||
for (long offset = 0;
|
||||
offset + MIPS_INSTRUCTION_SIZE <= block.getSize() && !monitor.isCancelled();
|
||||
offset += MIPS_INSTRUCTION_SIZE) {
|
||||
Address source;
|
||||
try {
|
||||
source = block.getStart().add(offset);
|
||||
} catch (Exception ignored) {
|
||||
break;
|
||||
}
|
||||
|
||||
Long target = readWord(source);
|
||||
if (target != null && looksLikeCallableEntry(target, true)) {
|
||||
candidates.add(new AddressTakenCandidate(offset, target));
|
||||
}
|
||||
}
|
||||
|
||||
boolean dedicatedPointerBlock = isDedicatedFunctionPointerBlock(block.getName());
|
||||
for (int index = 0; index < candidates.size(); ++index) {
|
||||
AddressTakenCandidate candidate = candidates.get(index);
|
||||
boolean clustered = dedicatedPointerBlock;
|
||||
|
||||
if (index > 0 &&
|
||||
candidate.sourceOffset - candidates.get(index - 1).sourceOffset <= clusterDistanceBytes) {
|
||||
clustered = true;
|
||||
}
|
||||
if (index + 1 < candidates.size() &&
|
||||
candidates.get(index + 1).sourceOffset - candidate.sourceOffset <= clusterDistanceBytes) {
|
||||
clustered = true;
|
||||
}
|
||||
|
||||
if (clustered) {
|
||||
addSyntheticEntry(records, existingStarts, candidate.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<FunctionRecord> collectExecutableLabelRecords(List<FunctionRecord> functionRecords) {
|
||||
@@ -555,6 +957,13 @@ public class ExportPS2Functions extends GhidraScript {
|
||||
existingStarts.add(start);
|
||||
}
|
||||
|
||||
// Ghidra does not always promote function pointers to CALL references,
|
||||
// especially when the low half is produced in a MIPS delay slot. Mirror
|
||||
// the stripped-ELF fallback used by ElfParser so the CSV still contains
|
||||
// callback and vtable entries that are only address-taken.
|
||||
collectMaterializedCodeEntries(labelRecords, existingStarts);
|
||||
collectDataFunctionPointerEntries(labelRecords, existingStarts);
|
||||
|
||||
if (labelRecords.isEmpty()) {
|
||||
return labelRecords;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <functional>
|
||||
#if defined(_MSC_VER)
|
||||
#include <intrin.h>
|
||||
@@ -288,6 +289,9 @@ public:
|
||||
void run();
|
||||
|
||||
void setIopPluginSearchPaths(std::vector<std::filesystem::path> paths);
|
||||
[[nodiscard]] ps2x::iop::ModuleLoadResult loadIopModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] ps2x::iop::ModuleLoadResult loadIopModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] bool stopIopModule(int32_t moduleId, int32_t *result = nullptr);
|
||||
[[nodiscard]] ps2x::iop::DebugSnapshot iopDebugSnapshot() const;
|
||||
|
||||
using DebugUiCallback = void (*)(PS2Runtime &runtime, void *userData);
|
||||
@@ -463,8 +467,10 @@ private:
|
||||
void HandleIntegerOverflow(R5900Context *ctx);
|
||||
|
||||
[[nodiscard]] ps2x::iop::RpcAbi selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const;
|
||||
[[nodiscard]] bool canBindIopRpc(uint32_t sid) const noexcept;
|
||||
[[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 advanceIopEeCycles(uint64_t eeCycles) noexcept;
|
||||
void resetIop();
|
||||
|
||||
friend class PS2IopTransport;
|
||||
|
||||
@@ -391,6 +391,7 @@ void EeScheduler::accountCycles(uint32_t cycles) noexcept
|
||||
const uint64_t elapsed = std::max<uint64_t>(1u, cycles);
|
||||
m_eeCycle += elapsed;
|
||||
m_pendingEeTimerInterrupts |= m_runtime.memory().advanceEeTimers(elapsed);
|
||||
m_runtime.advanceIopEeCycles(elapsed);
|
||||
if (m_pendingEeTimerInterrupts != 0u)
|
||||
{
|
||||
m_checkpointPending.store(true, std::memory_order_release);
|
||||
@@ -1278,7 +1279,7 @@ void EeScheduler::dispatchIrq(bool dmac, uint32_t cause)
|
||||
SET_GPR_U32(&invocation.context, 4, cause);
|
||||
SET_GPR_U32(&invocation.context, 5, handler.argument);
|
||||
SET_GPR_U32(&invocation.context, 28, handler.gp);
|
||||
SET_GPR_U32(&invocation.context, 29, handler.sp);
|
||||
SET_GPR_U32(&invocation.context, 29, 0u);
|
||||
SET_GPR_U32(&invocation.context, 31, 0u);
|
||||
queueInvocation(std::move(invocation));
|
||||
}
|
||||
@@ -1918,7 +1919,7 @@ void EeScheduler::processEvent(const EeEvent &event)
|
||||
SET_GPR_U32(&invocation.context, 5, static_cast<uint32_t>(alarm.ticks));
|
||||
SET_GPR_U32(&invocation.context, 6, alarm.argument);
|
||||
SET_GPR_U32(&invocation.context, 28, alarm.gp);
|
||||
SET_GPR_U32(&invocation.context, 29, alarm.sp);
|
||||
SET_GPR_U32(&invocation.context, 29, 0u);
|
||||
SET_GPR_U32(&invocation.context, 31, 0u);
|
||||
queueInvocation(std::move(invocation));
|
||||
break;
|
||||
|
||||
@@ -709,6 +709,7 @@ namespace ps2_stubs
|
||||
|
||||
void sceSifRebootIop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
PS2IopTransport::reset(runtime);
|
||||
setReturnS32(ctx, 1);
|
||||
}
|
||||
|
||||
@@ -737,6 +738,7 @@ namespace ps2_stubs
|
||||
|
||||
void sceSifResetIop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
PS2IopTransport::reset(runtime);
|
||||
setReturnS32(ctx, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,36 @@ namespace
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool copyGuestBytesBounded(const uint8_t *rdram,
|
||||
uint32_t guestAddr,
|
||||
uint32_t byteCount,
|
||||
uint32_t maxBytes,
|
||||
std::vector<uint8_t> &out)
|
||||
{
|
||||
out.clear();
|
||||
if (byteCount == 0u)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!rdram || guestAddr == 0u || byteCount > maxBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out.resize(byteCount);
|
||||
for (uint32_t i = 0; i < byteCount; ++i)
|
||||
{
|
||||
const uint8_t *src = getConstMemPtr(rdram, guestAddr + i);
|
||||
if (!src)
|
||||
{
|
||||
out.clear();
|
||||
return false;
|
||||
}
|
||||
out[i] = *src;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string makeSifModuleBufferTag(const uint8_t *rdram, uint32_t bufferAddr)
|
||||
{
|
||||
char key[96] = {};
|
||||
@@ -121,6 +151,68 @@ namespace
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
int32_t trackSifModuleLoadExternal(const std::string &path, int32_t moduleId)
|
||||
{
|
||||
if (path.empty() || moduleId <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
const std::string pathKey = normalizeSifModulePathKey(path);
|
||||
if (pathKey.empty())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_sif_module_mutex);
|
||||
|
||||
auto idIt = g_sif_modules_by_id.find(moduleId);
|
||||
if (idIt != g_sif_modules_by_id.end())
|
||||
{
|
||||
SifModuleRecord &record = idIt->second;
|
||||
if (record.pathKey == pathKey)
|
||||
{
|
||||
record.loaded = true;
|
||||
++record.refCount;
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
if (!record.pathKey.empty())
|
||||
{
|
||||
auto oldPathIt = g_sif_module_id_by_path.find(record.pathKey);
|
||||
if (oldPathIt != g_sif_module_id_by_path.end() && oldPathIt->second == moduleId)
|
||||
{
|
||||
g_sif_module_id_by_path.erase(oldPathIt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pathIt = g_sif_module_id_by_path.find(pathKey);
|
||||
if (pathIt != g_sif_module_id_by_path.end() && pathIt->second != moduleId)
|
||||
{
|
||||
auto oldIt = g_sif_modules_by_id.find(pathIt->second);
|
||||
if (oldIt != g_sif_modules_by_id.end())
|
||||
{
|
||||
oldIt->second.loaded = false;
|
||||
oldIt->second.refCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
SifModuleRecord record;
|
||||
record.id = moduleId;
|
||||
record.path = path;
|
||||
record.pathKey = pathKey;
|
||||
record.refCount = 1;
|
||||
record.loaded = true;
|
||||
g_sif_module_id_by_path[pathKey] = moduleId;
|
||||
g_sif_modules_by_id[moduleId] = std::move(record);
|
||||
if (moduleId >= g_next_sif_module_id)
|
||||
{
|
||||
g_next_sif_module_id = moduleId + 1;
|
||||
}
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
bool trackSifModuleStop(int32_t moduleId, uint32_t *remainingRefs = nullptr)
|
||||
{
|
||||
if (moduleId <= 0)
|
||||
|
||||
@@ -155,20 +155,22 @@ namespace ps2_syscalls
|
||||
const int32_t moduleId = static_cast<int32_t>(getRegU32(ctx, 4)); // $a0
|
||||
const uint32_t resultAddr = getRegU32(ctx, 7); // $a3 (int* result, optional)
|
||||
|
||||
int32_t moduleResult = -1;
|
||||
const bool stoppedByEmulator = runtime->stopIopModule(moduleId, &moduleResult);
|
||||
uint32_t refsLeft = 0;
|
||||
const bool knownModule = trackSifModuleStop(moduleId, &refsLeft);
|
||||
const int32_t ret = knownModule ? 0 : -1;
|
||||
const int32_t ret = (stoppedByEmulator || knownModule) ? 0 : -1;
|
||||
|
||||
if (resultAddr != 0)
|
||||
{
|
||||
int32_t *hostResult = reinterpret_cast<int32_t *>(getMemPtr(rdram, resultAddr));
|
||||
if (hostResult)
|
||||
{
|
||||
*hostResult = knownModule ? 0 : -1;
|
||||
*hostResult = stoppedByEmulator ? moduleResult : (knownModule ? 0 : -1);
|
||||
}
|
||||
}
|
||||
|
||||
if (knownModule)
|
||||
if (stoppedByEmulator || knownModule)
|
||||
{
|
||||
std::string modulePath;
|
||||
{
|
||||
@@ -179,7 +181,7 @@ namespace ps2_syscalls
|
||||
modulePath = it->second.path;
|
||||
}
|
||||
}
|
||||
logSifModuleAction("stop", moduleId, modulePath, refsLeft);
|
||||
logSifModuleAction(stoppedByEmulator ? "stop-emulated" : "stop", moduleId, modulePath, refsLeft);
|
||||
}
|
||||
|
||||
setReturnS32(ctx, ret);
|
||||
@@ -187,7 +189,9 @@ namespace ps2_syscalls
|
||||
|
||||
void SifLoadModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
const uint32_t pathAddr = getRegU32(ctx, 4); // $a0
|
||||
const uint32_t pathAddr = getRegU32(ctx, 4); // $a0
|
||||
const uint32_t argumentSize = getRegU32(ctx, 5); // $a1
|
||||
const uint32_t argumentAddr = getRegU32(ctx, 6); // $a2
|
||||
const std::string modulePath = readGuestCStringBounded(rdram, pathAddr, kMaxSifModulePathBytes);
|
||||
if (modulePath.empty())
|
||||
{
|
||||
@@ -195,6 +199,29 @@ namespace ps2_syscalls
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> arguments;
|
||||
constexpr uint32_t kMaxIopModuleArguments = 64u * 1024u;
|
||||
if (!copyGuestBytesBounded(rdram, argumentAddr, argumentSize, kMaxIopModuleArguments, arguments))
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto emulated = runtime->loadIopModule(modulePath, arguments.empty() ? nullptr : arguments.data(), static_cast<uint32_t>(arguments.size()));
|
||||
if (emulated.handled)
|
||||
{
|
||||
if (emulated.moduleId <= 0)
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
trackSifModuleLoadExternal(modulePath, emulated.moduleId);
|
||||
logSifModuleAction("load-emulated", emulated.moduleId, modulePath, 1u);
|
||||
setReturnS32(ctx, emulated.moduleId);
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t moduleId = trackSifModuleLoad(modulePath);
|
||||
if (moduleId <= 0)
|
||||
{
|
||||
@@ -219,10 +246,6 @@ namespace ps2_syscalls
|
||||
void SifInitRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_rpc_mutex);
|
||||
if (runtime)
|
||||
{
|
||||
PS2IopTransport::reset(runtime);
|
||||
}
|
||||
if (!g_rpc_initialized)
|
||||
{
|
||||
g_rpc_servers.clear();
|
||||
@@ -290,9 +313,12 @@ namespace ps2_syscalls
|
||||
g_rpc_clients[clientPtr].sid = rpcId;
|
||||
}
|
||||
|
||||
if (!serverPtr)
|
||||
if (!serverPtr && PS2IopTransport::canBindRpc(runtime, rpcId))
|
||||
{
|
||||
// Allocate a dummy server so bind loops can proceed.
|
||||
// EE-side servers and HLE routes need a descriptor in guest RAM.
|
||||
// With an emulated IOP, only publish it after the IRX has actually
|
||||
// registered the SID; cd->server == nullptr is the SDK's retry
|
||||
// signal while the IOP server thread is still starting.
|
||||
serverPtr = rpcAllocServerAddr(rdram);
|
||||
if (serverPtr)
|
||||
{
|
||||
|
||||
@@ -313,15 +313,39 @@ namespace ps2_syscalls
|
||||
|
||||
void sceSifLoadModuleBuffer(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
const uint32_t bufferAddr = getRegU32(ctx, 4); // $a0
|
||||
const uint32_t bufferAddr = getRegU32(ctx, 4); // $a0
|
||||
const uint32_t argumentSize = getRegU32(ctx, 5); // $a1
|
||||
const uint32_t argumentAddr = getRegU32(ctx, 6); // $a2
|
||||
if (!rdram || bufferAddr == 0u)
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Match buffer-based module loads to stable synthetic tags so module ID lookup remains deterministic.
|
||||
const std::string moduleTag = makeSifModuleBufferTag(rdram, bufferAddr);
|
||||
std::vector<uint8_t> arguments;
|
||||
constexpr uint32_t kMaxIopModuleArguments = 64u * 1024u;
|
||||
if (!copyGuestBytesBounded(rdram, argumentAddr, argumentSize, kMaxIopModuleArguments, arguments))
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto emulated = runtime->loadIopModuleBuffer(bufferAddr, arguments.empty() ? nullptr : arguments.data(), static_cast<uint32_t>(arguments.size()));
|
||||
if (emulated.handled)
|
||||
{
|
||||
if (emulated.moduleId <= 0)
|
||||
{
|
||||
setReturnS32(ctx, -1);
|
||||
return;
|
||||
}
|
||||
trackSifModuleLoadExternal(moduleTag, emulated.moduleId);
|
||||
logSifModuleAction("load-buffer-emulated", emulated.moduleId, moduleTag, 1u);
|
||||
setReturnS32(ctx, emulated.moduleId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile mode keeps the existing deterministic synthetic IDs.
|
||||
const int32_t moduleId = trackSifModuleLoad(moduleTag);
|
||||
if (moduleId <= 0)
|
||||
{
|
||||
|
||||
@@ -37,6 +37,11 @@ public:
|
||||
: ps2x::iop::RpcResult{};
|
||||
}
|
||||
|
||||
[[nodiscard]] static bool canBindRpc(const PS2Runtime *runtime, uint32_t sid)
|
||||
{
|
||||
return !runtime || runtime->canBindIopRpc(sid);
|
||||
}
|
||||
|
||||
static void notifyTransfer(
|
||||
PS2Runtime *runtime,
|
||||
uint8_t *rdram,
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <chrono>
|
||||
@@ -481,6 +480,7 @@ PS2Runtime::PS2Runtime()
|
||||
{
|
||||
m_iopHost = std::make_unique<PS2IopHostAdapter>(*this);
|
||||
m_iopSubsystem = std::make_unique<ps2x::iop::IopSubsystem>(*m_iopHost);
|
||||
|
||||
m_eeScheduler = std::make_unique<EeScheduler>(*this);
|
||||
#if defined(PS2X_IOP_ENABLE_PLUGINS) && PS2X_IOP_ENABLE_PLUGINS && \
|
||||
!defined(PLATFORM_VITA) && (defined(_WIN32) || defined(__linux__))
|
||||
@@ -574,11 +574,34 @@ void PS2Runtime::setIopPluginSearchPaths(std::vector<std::filesystem::path> path
|
||||
m_iopSubsystem->setPluginSearchPaths(std::move(paths));
|
||||
}
|
||||
|
||||
ps2x::iop::ModuleLoadResult PS2Runtime::loadIopModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM());
|
||||
return m_iopSubsystem->loadModule(path, arguments, argumentSize);
|
||||
}
|
||||
|
||||
ps2x::iop::ModuleLoadResult PS2Runtime::loadIopModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM());
|
||||
return m_iopSubsystem->loadModuleBuffer(guestAddress, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool PS2Runtime::stopIopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM());
|
||||
return m_iopSubsystem->stopModule(moduleId, result);
|
||||
}
|
||||
|
||||
ps2x::iop::RpcAbi PS2Runtime::selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const
|
||||
{
|
||||
return m_iopSubsystem->selectRpcAbi(request);
|
||||
}
|
||||
|
||||
bool PS2Runtime::canBindIopRpc(uint32_t sid) const noexcept
|
||||
{
|
||||
return m_iopSubsystem->canBindRpc(sid);
|
||||
}
|
||||
|
||||
ps2x::iop::RpcResult PS2Runtime::handleIopRpc(uint8_t *rdram, R5900Context *ctx, ps2x::iop::RpcRequest request)
|
||||
{
|
||||
auto scope = m_iopHost->enterCall(ctx, rdram);
|
||||
@@ -592,6 +615,11 @@ void PS2Runtime::notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransf
|
||||
m_iopSubsystem->onSifTransfer(transfer);
|
||||
}
|
||||
|
||||
void PS2Runtime::advanceIopEeCycles(uint64_t eeCycles) noexcept
|
||||
{
|
||||
m_iopSubsystem->runEeCycles(eeCycles);
|
||||
}
|
||||
|
||||
void PS2Runtime::resetIop()
|
||||
{
|
||||
m_iopSubsystem->reset();
|
||||
|
||||
@@ -157,6 +157,29 @@ void register_code_generator_tests()
|
||||
{
|
||||
MiniTest::Case("CodeGenerator", [](TestCase &tc)
|
||||
{
|
||||
tc.Run("Generated sources cannot be shadowed by stale local declaration headers", [](TestCase &t) {
|
||||
Function func;
|
||||
func.name = "header_lookup";
|
||||
func.start = 0x8F00;
|
||||
func.end = 0x8F04;
|
||||
func.isRecompiled = true;
|
||||
|
||||
CodeGenerator gen({}, {});
|
||||
gen.setRenamedFunctions({{func.start, "header_lookup_0x8f00"}});
|
||||
|
||||
const std::string generated = gen.generateFunction(func, {makeNop(func.start)}, true);
|
||||
const std::string registration = gen.generateFunctionRegistration({func}, {});
|
||||
|
||||
t.IsTrue(generated.find("#include <ps2_recompiled_functions.h>") != std::string::npos,
|
||||
"function sources must resolve declarations through the configured include path");
|
||||
t.IsTrue(generated.find("#include <ps2_recompiled_stubs.h>") != std::string::npos,
|
||||
"function sources must resolve stub declarations through the configured include path");
|
||||
t.IsTrue(registration.find("#include <ps2_recompiled_functions.h>") != std::string::npos,
|
||||
"the registration source must use the same unambiguous declaration header");
|
||||
t.IsTrue(registration.find("#include <ps2_recompiled_stubs.h>") != std::string::npos,
|
||||
"the registration source must use the same unambiguous stub header");
|
||||
});
|
||||
|
||||
tc.Run("SYSCALL publishes its continuation before entering the runtime", [](TestCase &t) {
|
||||
Function func;
|
||||
func.name = "syscall_resume";
|
||||
@@ -185,6 +208,42 @@ void register_code_generator_tests()
|
||||
"the continuation PC must be visible before a syscall can transfer to the scheduler");
|
||||
});
|
||||
|
||||
tc.Run("SYSCALL fallthrough is a resumable entry", [](TestCase &t) {
|
||||
Function func;
|
||||
func.name = "syscall_resume_entry";
|
||||
func.start = 0x9100;
|
||||
func.end = 0x9108;
|
||||
func.isRecompiled = true;
|
||||
|
||||
Instruction syscall{};
|
||||
syscall.address = 0x9100;
|
||||
syscall.opcode = OPCODE_SPECIAL;
|
||||
syscall.function = SPECIAL_SYSCALL;
|
||||
syscall.raw = (0x83u << 6) | SPECIAL_SYSCALL;
|
||||
|
||||
Instruction after = makeNop(0x9104);
|
||||
CodeGenerator gen({}, {});
|
||||
CodeGenerator::AnalysisResult analysis =
|
||||
gen.collectInternalBranchTargets(func, {syscall, after});
|
||||
|
||||
t.IsTrue(analysis.resumeEntryPoints.contains(0x9104u),
|
||||
"a syscall can yield through a guest override, so its fallthrough must be resumable");
|
||||
|
||||
const std::string generated = gen.generateFunction(func, {syscall, after}, false);
|
||||
t.IsTrue(generated.find("case 0x9104u: goto label_9104;") != std::string::npos,
|
||||
"the owner wrapper must resume directly after the syscall");
|
||||
|
||||
gen.setRenamedFunctions({{0x9100u, "syscall_resume_entry_0x9100"}});
|
||||
gen.setResumeEntryTargets({{0x9100u,
|
||||
std::vector<uint32_t>(analysis.resumeEntryPoints.begin(),
|
||||
analysis.resumeEntryPoints.end())}});
|
||||
const std::string registration = gen.generateFunctionRegistration({func}, {});
|
||||
t.IsTrue(registration.find(
|
||||
"g_ps2RecompiledFunctionTable[1] = syscall_resume_entry_0x9100; // 0x9104") !=
|
||||
std::string::npos,
|
||||
"the syscall continuation must register to the owner wrapper");
|
||||
});
|
||||
|
||||
tc.Run("R5900 MULT writes rd when rd is non-zero", [](TestCase &t) {
|
||||
CodeGenerator gen({}, {});
|
||||
|
||||
@@ -606,6 +665,46 @@ void register_code_generator_tests()
|
||||
"multiple resume pcs should register to the same owner wrapper");
|
||||
});
|
||||
|
||||
tc.Run("configured internal guest handlers register to their owner wrapper", [](TestCase &t) {
|
||||
Function owner;
|
||||
owner.name = "sdk_bootstrap_owner";
|
||||
owner.start = 0x7000;
|
||||
owner.end = 0x7020;
|
||||
owner.isRecompiled = true;
|
||||
owner.isStub = false;
|
||||
|
||||
std::vector<Instruction> instructions{
|
||||
makeNop(0x7000), makeNop(0x7004), makeNop(0x7008), makeNop(0x700C),
|
||||
makeNop(0x7010), makeNop(0x7014), makeNop(0x7018), makeNop(0x701C)};
|
||||
std::vector<Function> functions{owner};
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> decoded{{owner.start, instructions}};
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> targetsByOwner;
|
||||
|
||||
const size_t added = PS2Recompiler::CollectInternalEntryTargets(
|
||||
functions, decoded, {0x7008u, 0x7018u}, targetsByOwner);
|
||||
|
||||
t.IsTrue(added == 2u,
|
||||
"both address-qualified internal handlers should be promoted");
|
||||
t.IsTrue(targetsByOwner.at(owner.start).size() == 2u,
|
||||
"both handlers should belong to the containing generated wrapper");
|
||||
|
||||
CodeGenerator gen({}, {});
|
||||
gen.setRenamedFunctions({{owner.start, "sdk_bootstrap_owner_0x7000"}});
|
||||
gen.setResumeEntryTargets(targetsByOwner);
|
||||
|
||||
const std::string generated = gen.generateFunction(owner, instructions, false);
|
||||
const std::string registration = gen.generateFunctionRegistration(functions, {});
|
||||
|
||||
t.IsTrue(generated.find("case 0x7008u: goto label_7008;") != std::string::npos,
|
||||
"the owner must enter directly at the first installed handler");
|
||||
t.IsTrue(generated.find("case 0x7018u: goto label_7018;") != std::string::npos,
|
||||
"the owner must enter directly at the second installed handler");
|
||||
t.IsTrue(registration.find("sdk_bootstrap_owner_0x7000; // 0x7008") != std::string::npos,
|
||||
"the first handler address must dispatch to its owner wrapper");
|
||||
t.IsTrue(registration.find("sdk_bootstrap_owner_0x7000; // 0x7018") != std::string::npos,
|
||||
"the second handler address must dispatch to its owner wrapper");
|
||||
});
|
||||
|
||||
tc.Run("external mid-function entry can register to the owner wrapper", [](TestCase &t) {
|
||||
Function caller;
|
||||
caller.name = "caller";
|
||||
|
||||
@@ -155,6 +155,78 @@ static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path
|
||||
return writer.save(elfPath.string());
|
||||
}
|
||||
|
||||
static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::path &elfPath)
|
||||
{
|
||||
ELFIO::elfio writer;
|
||||
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
|
||||
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
|
||||
writer.set_type(ELFIO::ET_EXEC);
|
||||
writer.set_machine(ELFIO::EM_MIPS);
|
||||
writer.set_entry(0x00100000u);
|
||||
|
||||
ELFIO::section *text = writer.sections.add(".text");
|
||||
text->set_type(ELFIO::SHT_PROGBITS);
|
||||
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
|
||||
text->set_addr_align(4);
|
||||
text->set_address(0x00100000u);
|
||||
|
||||
std::array<uint32_t, 30> textWords{};
|
||||
textWords[0] = 0x3C040010u; // lui a0,0x10
|
||||
textWords[1] = 0xAC800000u; // sw zero,0(a0)
|
||||
textWords[2] = 0x0C040008u; // jal 0x00100020 (callback registrar)
|
||||
textWords[3] = 0x24840040u; // addiu a0,a0,0x40 (delay slot)
|
||||
textWords[4] = 0x03E00008u; // jr ra
|
||||
textWords[5] = 0x00000000u; // nop
|
||||
textWords[6] = 0x3C080010u; // lui t0,0x10
|
||||
textWords[7] = 0x25080070u; // addiu t0,t0,0x70 (code label, not a callback argument)
|
||||
|
||||
textWords[8] = 0x03E00008u; // registrar at 0x00100020
|
||||
textWords[9] = 0x00000000u;
|
||||
|
||||
textWords[16] = 0x27BDFFF0u; // callback at 0x00100040: addiu sp,sp,-0x10
|
||||
textWords[17] = 0xFFBF0000u; // sd ra,0(sp)
|
||||
textWords[18] = 0xDFBF0000u; // ld ra,0(sp)
|
||||
textWords[19] = 0x03E00008u; // jr ra
|
||||
textWords[20] = 0x27BD0010u; // addiu sp,sp,0x10
|
||||
|
||||
textWords[24] = 0x03E00008u; // table leaf at 0x00100060
|
||||
textWords[25] = 0x00000000u;
|
||||
textWords[26] = 0x03E00008u; // table leaf at 0x00100068
|
||||
textWords[27] = 0x00000000u;
|
||||
textWords[28] = 0x03E00008u; // isolated pointer target at 0x00100070
|
||||
textWords[29] = 0x00000000u;
|
||||
|
||||
text->set_data(reinterpret_cast<const char *>(textWords.data()),
|
||||
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
|
||||
|
||||
ELFIO::section *rodata = writer.sections.add(".rodata");
|
||||
rodata->set_type(ELFIO::SHT_PROGBITS);
|
||||
rodata->set_flags(ELFIO::SHF_ALLOC);
|
||||
rodata->set_addr_align(4);
|
||||
rodata->set_address(0x00200000u);
|
||||
|
||||
std::array<uint32_t, 20> tableWords{};
|
||||
tableWords[1] = 0x00100060u;
|
||||
tableWords[3] = 0x00100068u;
|
||||
tableWords[16] = 0x00100070u; // plausible entry, but not part of a pointer cluster
|
||||
rodata->set_data(reinterpret_cast<const char *>(tableWords.data()),
|
||||
static_cast<ELFIO::Elf_Word>(tableWords.size() * sizeof(uint32_t)));
|
||||
|
||||
ELFIO::segment *textSegment = writer.segments.add();
|
||||
textSegment->set_type(ELFIO::PT_LOAD);
|
||||
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
|
||||
textSegment->set_align(0x1000);
|
||||
textSegment->add_section_index(text->get_index(), text->get_addr_align());
|
||||
|
||||
ELFIO::segment *dataSegment = writer.segments.add();
|
||||
dataSegment->set_type(ELFIO::PT_LOAD);
|
||||
dataSegment->set_flags(ELFIO::PF_R);
|
||||
dataSegment->set_align(0x1000);
|
||||
dataSegment->add_section_index(rodata->get_index(), rodata->get_addr_align());
|
||||
|
||||
return writer.save(elfPath.string());
|
||||
}
|
||||
|
||||
static bool writeMinimalMipsElfWithInitializer(const std::filesystem::path &elfPath,
|
||||
const std::string &functionName,
|
||||
uint32_t initializerTarget)
|
||||
@@ -838,6 +910,42 @@ void register_ps2_recompiler_tests()
|
||||
std::filesystem::remove(configPath, removeError);
|
||||
});
|
||||
|
||||
tc.Run("config manager loads modern and legacy guest entry hints", [](TestCase &t) {
|
||||
const auto uniqueSuffix = std::to_string(
|
||||
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
const std::filesystem::path configPath =
|
||||
std::filesystem::temp_directory_path() / ("ps2recomp-entry-hints-" + uniqueSuffix + ".toml");
|
||||
|
||||
std::ofstream configFile(configPath);
|
||||
t.IsTrue(static_cast<bool>(configFile), "temp config file should be writable");
|
||||
if (!configFile)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
configFile << "[general]\n";
|
||||
configFile << "input = \"dummy.elf\"\n";
|
||||
configFile << "output = \"out\"\n";
|
||||
configFile << "entry_points = [\"callback@0x7008\"]\n";
|
||||
configFile << "untracked_stubs = [\"legacy_callback@0x7018\"]\n";
|
||||
configFile.close();
|
||||
|
||||
ConfigManager manager(configPath.string());
|
||||
const RecompilerConfig config = manager.loadConfig();
|
||||
|
||||
t.Equals(config.entryPointHints.size(), static_cast<size_t>(2),
|
||||
"modern and legacy entry metadata should be merged");
|
||||
t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(),
|
||||
"callback@0x7008") != config.entryPointHints.end(),
|
||||
"modern entry_points metadata should load");
|
||||
t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(),
|
||||
"legacy_callback@0x7018") != config.entryPointHints.end(),
|
||||
"legacy untracked_stubs metadata should remain compatible");
|
||||
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove(configPath, removeError);
|
||||
});
|
||||
|
||||
tc.Run("elf parser ignores STT_FUNC symbols in non-executable sections", [](TestCase &t) {
|
||||
const auto uniqueSuffix = std::to_string(
|
||||
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
@@ -947,6 +1055,50 @@ void register_ps2_recompiler_tests()
|
||||
std::filesystem::remove(mapPath, removeError);
|
||||
});
|
||||
|
||||
tc.Run("elf parser discovers address-taken callbacks in stripped ELFs", [](TestCase &t) {
|
||||
const auto uniqueSuffix = std::to_string(
|
||||
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
const std::filesystem::path elfPath =
|
||||
std::filesystem::temp_directory_path() / ("ps2recomp-address-taken-" + uniqueSuffix + ".elf");
|
||||
|
||||
const bool writeOk = writeMinimalMipsElfWithAddressTakenCallbacks(elfPath);
|
||||
t.IsTrue(writeOk, "temporary stripped ELF should be generated");
|
||||
if (!writeOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ElfParser parser(elfPath.string());
|
||||
const bool parseOk = parser.parse();
|
||||
t.IsTrue(parseOk, "generated ELF should parse");
|
||||
if (!parseOk)
|
||||
{
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove(elfPath, removeError);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto functions = parser.extractFunctions();
|
||||
auto hasStart = [&functions](uint32_t start)
|
||||
{
|
||||
return std::any_of(functions.begin(), functions.end(),
|
||||
[start](const Function &function)
|
||||
{ return function.start == start; });
|
||||
};
|
||||
|
||||
t.IsTrue(hasStart(0x00100040u),
|
||||
"LUI plus delay-slot ADDIU should discover the callback entry");
|
||||
t.IsTrue(hasStart(0x00100060u),
|
||||
"clustered rodata pointers should discover the first leaf callback");
|
||||
t.IsTrue(hasStart(0x00100068u),
|
||||
"clustered rodata pointers should discover the second leaf callback");
|
||||
t.IsFalse(hasStart(0x00100070u),
|
||||
"an isolated data pointer or non-callback code materialization must not become a function");
|
||||
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove(elfPath, removeError);
|
||||
});
|
||||
|
||||
tc.Run("runtime call resolution includes Veronica compatibility aliases", [](TestCase &t) {
|
||||
t.Equals(ps2_runtime_calls::resolveSyscallName("ReleaseAlarm"), std::string_view{"ReleaseAlarm"},
|
||||
"ReleaseAlarm should resolve as a syscall name");
|
||||
|
||||
@@ -52,6 +52,11 @@ namespace
|
||||
constexpr uint32_t kIrqWaitPc = 0x00160200u;
|
||||
constexpr uint32_t kIrqResumePc = 0x00160210u;
|
||||
constexpr uint32_t kIntcHandlerPc = 0x00160220u;
|
||||
constexpr uint32_t kIrqStackWaitPc = 0x00160230u;
|
||||
constexpr uint32_t kIrqStackResumePc = 0x00160240u;
|
||||
constexpr uint32_t kIrqStackHandlerPc = 0x00160250u;
|
||||
constexpr uint32_t kIrqRegistrationSp = 0x001E0000u;
|
||||
constexpr uint32_t kIrqRegistrationGuardAddr = kIrqRegistrationSp - 16u;
|
||||
constexpr uint32_t kISemaWaitPc = 0x00160300u;
|
||||
constexpr uint32_t kISemaResumePc = 0x00160310u;
|
||||
constexpr uint32_t kISemaDriverPc = 0x00160320u;
|
||||
@@ -83,6 +88,7 @@ namespace
|
||||
uint64_t g_vsyncTick = 0;
|
||||
uint64_t g_vsyncCsr = 0;
|
||||
std::atomic<bool> g_timer2Resumed{false};
|
||||
uint32_t g_irqObservedSp = 0u;
|
||||
|
||||
void setRegU32(R5900Context &ctx, int reg, uint32_t value)
|
||||
{
|
||||
@@ -184,6 +190,34 @@ namespace
|
||||
runtime->requestStop();
|
||||
}
|
||||
|
||||
void schedulerIrqStackHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *)
|
||||
{
|
||||
g_irqObservedSp = getRegU32(ctx, 29);
|
||||
const uint64_t clobber = 0u;
|
||||
std::memcpy(rdram + g_irqObservedSp - sizeof(clobber), &clobber, sizeof(clobber));
|
||||
ctx->pc = 0u;
|
||||
}
|
||||
|
||||
void schedulerIrqStackWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
EeScheduler &scheduler = runtime->eeScheduler();
|
||||
scheduler.addIrqHandler(false,
|
||||
2u,
|
||||
kIrqStackHandlerPc,
|
||||
true,
|
||||
0u,
|
||||
0u,
|
||||
kIrqRegistrationSp);
|
||||
ctx->pc = kIrqStackResumePc;
|
||||
scheduler.waitVSync(scheduler.currentVSyncTick());
|
||||
}
|
||||
|
||||
void schedulerIrqStackResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
ctx->pc = 0u;
|
||||
runtime->requestStop();
|
||||
}
|
||||
|
||||
void schedulerISemaHandler(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
g_dispatchTrace.push_back(3);
|
||||
@@ -467,6 +501,32 @@ void register_ps2_runtime_interrupt_tests()
|
||||
"the IRQ frame should receive its registered argument");
|
||||
});
|
||||
|
||||
tc.Run("IRQ callbacks use an isolated invocation stack", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
env.runtime.registerFunction(kIrqStackWaitPc, schedulerIrqStackWait);
|
||||
env.runtime.registerFunction(kIrqStackResumePc, schedulerIrqStackResume);
|
||||
env.runtime.registerFunction(kIrqStackHandlerPc, schedulerIrqStackHandler);
|
||||
|
||||
constexpr uint64_t guard = 0x1122334455667788ull;
|
||||
std::memcpy(env.rdram.data() + kIrqRegistrationGuardAddr, &guard, sizeof(guard));
|
||||
g_irqObservedSp = 0u;
|
||||
|
||||
R5900Context mainContext{};
|
||||
mainContext.pc = kIrqStackWaitPc;
|
||||
env.runtime.eeScheduler().reset(env.rdram.data(), mainContext);
|
||||
env.runtime.eeScheduler().run();
|
||||
|
||||
uint64_t guardAfter = 0u;
|
||||
std::memcpy(&guardAfter,
|
||||
env.rdram.data() + kIrqRegistrationGuardAddr,
|
||||
sizeof(guardAfter));
|
||||
t.IsTrue(g_irqObservedSp != 0u && g_irqObservedSp != kIrqRegistrationSp,
|
||||
"IRQ handler must not reuse the transient stack captured at registration");
|
||||
t.Equals(guardAfter, guard,
|
||||
"IRQ handler stack writes must not clobber the registering thread's live frame");
|
||||
});
|
||||
|
||||
tc.Run("iSignalSema defers selection until IRQ return", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
|
||||
@@ -319,6 +319,39 @@ void register_ps2_sif_rpc_tests()
|
||||
{
|
||||
MiniTest::Case("PS2SifRpc", [](TestCase &tc)
|
||||
{
|
||||
tc.Run("SifInitRpc does not reset the running IOP", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
|
||||
env.runtime.eeScheduler().accountCycles(80u);
|
||||
const uint64_t cyclesBeforeInit = env.runtime.iopDebugSnapshot().emulatorCycles;
|
||||
t.IsTrue(cyclesBeforeInit != 0u, "IOP cycle counter should advance before RPC initialization");
|
||||
|
||||
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
|
||||
|
||||
t.Equals(env.runtime.iopDebugSnapshot().emulatorCycles, cyclesBeforeInit,
|
||||
"SifInitRpc must not reboot or reset the IOP");
|
||||
});
|
||||
|
||||
tc.Run("emulated RPC bind waits for a registered IOP server", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
|
||||
constexpr uint32_t kClientAddr = 0x00021F00u;
|
||||
constexpr uint32_t kUnregisteredSid = 0x13572468u;
|
||||
|
||||
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
|
||||
setRegU32(env.ctx, 4, kClientAddr);
|
||||
setRegU32(env.ctx, 5, kUnregisteredSid);
|
||||
setRegU32(env.ctx, 6, 0u);
|
||||
SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime);
|
||||
|
||||
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc transport should complete");
|
||||
const SifRpcClientData client = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
|
||||
t.Equals(client.server, 0u,
|
||||
"client server pointer must stay null until the emulated IRX registers its SID");
|
||||
});
|
||||
|
||||
tc.Run("register bind call updates descriptors and payload", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
@@ -826,7 +859,7 @@ void register_ps2_sif_rpc_tests()
|
||||
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)
|
||||
tc.Run("hybrid bind before register waits then remaps", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
|
||||
@@ -846,11 +879,9 @@ void register_ps2_sif_rpc_tests()
|
||||
t.Equals(getRegS32(env.ctx, 2), KE_OK, "initial bind without registered server should still succeed");
|
||||
|
||||
const SifRpcClientData clientBeforeRegister = readGuestStruct<SifRpcClientData>(env.rdram.data(), kClientAddr);
|
||||
t.IsTrue(clientBeforeRegister.server != 0u, "bind should allocate placeholder server when sid is missing");
|
||||
t.IsTrue(clientBeforeRegister.server >= 0x01F10000u && clientBeforeRegister.server < 0x01F20000u,
|
||||
"placeholder server should come from rpc server pool");
|
||||
t.Equals(clientBeforeRegister.buf, 0u, "placeholder server starts with empty buf");
|
||||
t.Equals(clientBeforeRegister.cbuf, 0u, "placeholder server starts with empty cbuf");
|
||||
t.Equals(clientBeforeRegister.server, 0u, "bind must wait until a hybrid backend owns the SID");
|
||||
t.Equals(clientBeforeRegister.buf, 0u, "unbound client starts with empty buf");
|
||||
t.Equals(clientBeforeRegister.cbuf, 0u, "unbound client starts with empty cbuf");
|
||||
|
||||
setRegU32(env.ctx, 4, kQdAddr);
|
||||
setRegU32(env.ctx, 5, 0x44u);
|
||||
@@ -873,7 +904,7 @@ void register_ps2_sif_rpc_tests()
|
||||
t.Equals(clientAfterRegister.server, kSdAddr, "register should remap pre-bound clients to concrete server descriptor");
|
||||
t.Equals(clientAfterRegister.buf, kServerBufAddr, "register should update client buf from server descriptor");
|
||||
t.Equals(clientAfterRegister.cbuf, kServerCbufAddr, "register should update client cbuf from server descriptor");
|
||||
t.IsTrue(clientAfterRegister.server != clientBeforeRegister.server, "client server pointer should switch from placeholder to real server");
|
||||
t.IsTrue(clientAfterRegister.server != clientBeforeRegister.server, "client server pointer should switch from unbound to real server");
|
||||
|
||||
setRegU32(env.ctx, 4, kSdAddr);
|
||||
setRegU32(env.ctx, 5, kQdAddr);
|
||||
|
||||
Reference in New Issue
Block a user