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:
Ran-j
2026-08-19 16:53:00 -03:00
parent a6739395b3
commit a293fa433a
53 changed files with 8068 additions and 155 deletions
+4 -6
View File
@@ -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
+4 -7
View File
@@ -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).
+3 -3
View File
@@ -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";
+17
View File
@@ -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
View File
@@ -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,
+7
View File
@@ -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);
+12
View File
@@ -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;
+25 -24
View File
@@ -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;
}
+596
View File
@@ -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();
}
}
+42
View File
@@ -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;
};
}
+497
View File
@@ -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;
}
}
+42
View File
@@ -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
+42
View File
@@ -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;
};
}
+15
View File
@@ -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;
+160
View File
@@ -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;
}
}
}
+46
View File
@@ -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;
};
}
+729
View File
@@ -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;
}
}
}
+100
View File
@@ -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;
};
}
+370
View File
@@ -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;
}
}
+91
View File
@@ -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;
};
}
+572
View File
@@ -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> &sections,
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(&section, 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(&section, 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 &section : 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 &section : 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;
}
}
+48
View File
@@ -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);
};
}
+325
View File
@@ -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;
}
}
+68
View File
@@ -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;
};
}
+336
View File
@@ -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;
}
}
}
+20
View File
@@ -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;
};
}
+5
View File
@@ -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)
+61 -5
View File
@@ -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;
+1
View File
@@ -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> &sections);
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;
+1
View File
@@ -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;
};
+22
View File
@@ -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)
{
+339 -9
View File
@@ -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 &section, 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> &sections, 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> &sections,
std::unordered_set<uint32_t> &starts)
{
constexpr uint32_t kMaxLookaheadWords = 4;
for (const auto &section : 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> &sections,
std::unordered_set<uint32_t> &starts)
{
struct PointerCandidate
{
uint32_t sourceOffset;
uint32_t target;
};
constexpr uint32_t kClusterDistanceBytes = 32;
for (const auto &section : 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 &sections = parser->getSections();
for (const auto &section : 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(),
+2 -2
View File
@@ -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";
+112 -12
View File
@@ -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);
}
+413 -4
View File
@@ -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;
}
+6
View File
@@ -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;
+3 -2
View File
@@ -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;
+2
View File
@@ -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)
+37 -11
View File
@@ -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)
{
+26 -2
View File
@@ -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)
{
+5
View File
@@ -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,
+29 -1
View File
@@ -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();
+99
View File
@@ -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";
+152
View File
@@ -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;
+38 -7
View File
@@ -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);