diff --git a/README.md b/README.md index b1e6d93..dc3c489 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/ps2xAnalyzer/Readme.md b/ps2xAnalyzer/Readme.md index e6f709a..503b16c 100644 --- a/ps2xAnalyzer/Readme.md +++ b/ps2xAnalyzer/Readme.md @@ -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 [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). diff --git a/ps2xAnalyzer/src/toml_generator.cpp b/ps2xAnalyzer/src/toml_generator.cpp index d083c80..cf4a251 100644 --- a/ps2xAnalyzer/src/toml_generator.cpp +++ b/ps2xAnalyzer/src/toml_generator.cpp @@ -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"; diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt index e17b472..2c5442d 100644 --- a/ps2xIOP/CMakeLists.txt +++ b/ps2xIOP/CMakeLists.txt @@ -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 diff --git a/ps2xIOP/README.md b/ps2xIOP/README.md index 4538f50..ad20d0c 100644 --- a/ps2xIOP/README.md +++ b/ps2xIOP/README.md @@ -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, diff --git a/ps2xIOP/include/ps2x/iop/iop_subsystem.h b/ps2xIOP/include/ps2x/iop/iop_subsystem.h index 0d086a0..8ac8f69 100644 --- a/ps2xIOP/include/ps2x/iop/iop_subsystem.h +++ b/ps2xIOP/include/ps2x/iop/iop_subsystem.h @@ -6,6 +6,7 @@ #include #include #include +#include #include 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); diff --git a/ps2xIOP/include/ps2x/iop/iop_types.h b/ps2xIOP/include/ps2x/iop/iop_types.h index 37989ca..4a81203 100644 --- a/ps2xIOP/include/ps2x/iop/iop_types.h +++ b/ps2xIOP/include/ps2x/iop/iop_types.h @@ -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 services; diff --git a/ps2xIOP/src/builtin_profiles.cpp b/ps2xIOP/src/builtin_profiles.cpp index ac537ad..f1447bf 100644 --- a/ps2xIOP/src/builtin_profiles.cpp +++ b/ps2xIOP/src/builtin_profiles.cpp @@ -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 createBuiltinProfiles() { std::vector 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; } diff --git a/ps2xIOP/src/emulator/iop_cdvd.cpp b/ps2xIOP/src/emulator/iop_cdvd.cpp new file mode 100644 index 0000000..58934c0 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_cdvd.cpp @@ -0,0 +1,596 @@ +#include "iop_cdvd.h" + +#include "iop_cpu.h" +#include "iop_memory.h" +#include "ps2x/iop/iop_host.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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((bytes + kSectorSize - 1u) / kSectorSize); + } + + void writeLe16(uint8_t *destination, uint16_t value) + { + destination[0] = static_cast(value); + destination[1] = static_cast(value >> 8u); + } + + void writeBe16(uint8_t *destination, uint16_t value) + { + destination[0] = static_cast(value >> 8u); + destination[1] = static_cast(value); + } + + void writeLe32(uint8_t *destination, uint32_t value) + { + for (uint32_t i = 0u; i < 4u; ++i) + destination[i] = static_cast(value >> (i * 8u)); + } + + void writeBe32(uint8_t *destination, uint32_t value) + { + for (uint32_t i = 0u; i < 4u; ++i) + destination[i] = static_cast(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(character); + character = byte < 0x80u ? static_cast(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 &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(std::max(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(recordSize); + writeBoth32(destination + 2u, lsn); + writeBoth32(destination + 10u, size); + destination[25] = directory ? 2u : 0u; + writeBoth16(destination + 28u, 1u); + destination[32] = static_cast(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 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 takeCompletionCallback() noexcept + { + std::optional 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 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(std::min(fileSize, std::numeric_limits::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 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(cursor, 32u); + + std::array 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(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 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 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(child.identifier.data()), child.identifier.size()); + } + for (uint32_t sector = 0u; sector < node.sectors; ++sector) + { + std::array 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(lsn - node->lsn) * kSectorSize; + const size_t wanted = static_cast(std::min(kSectorSize, static_cast(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(sectors) * kSectorSize; + if (byteCount64 > IopMemory::RamSize || !memory.ownsRamRange(destination, static_cast(byteCount64))) + { + lastError = kCdvdErrorRead; + return false; + } + const size_t byteCount = static_cast(byteCount64); + std::vector 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(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(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; + 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 nodes; + std::unordered_map> metadataSectors; + }; + + IopCdvd::IopCdvd(IopHost &host, IopMemory &memory) + : m_impl(std::make_unique(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::takeCompletionCallback() noexcept + { + return m_impl->takeCompletionCallback(); + } +} diff --git a/ps2xIOP/src/emulator/iop_cdvd.h b/ps2xIOP/src/emulator/iop_cdvd.h new file mode 100644 index 0000000..c7b5a80 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_cdvd.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +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 takeCompletionCallback() noexcept; + + private: + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/ps2xIOP/src/emulator/iop_cpu.cpp b/ps2xIOP/src/emulator/iop_cpu.cpp new file mode 100644 index 0000000..e09642e --- /dev/null +++ b/ps2xIOP/src/emulator/iop_cpu.cpp @@ -0,0 +1,497 @@ +#include "iop_cpu.h" +#include "iop_memory.h" + +#include + +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 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(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(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(lhs) + rhs; + if (result > std::numeric_limits::max() || result < std::numeric_limits::min()) + raiseException(cpu, 12u, pc, wasDelaySlot); + else + write(reg, static_cast(static_cast(result))); + }; + auto overflowSub = [&](int32_t lhs, int32_t rhs, uint32_t reg) + { + const int64_t result = static_cast(lhs) - rhs; + if (result > std::numeric_limits::max() || result < std::numeric_limits::min()) + raiseException(cpu, 12u, pc, wasDelaySlot); + else + write(reg, static_cast(static_cast(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(static_cast(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(static_cast(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(static_cast(cpu.gpr[rs])) * static_cast(static_cast(cpu.gpr[rt])); + cpu.lo = static_cast(result); + cpu.hi = static_cast(static_cast(result) >> 32u); + break; + } + case 0x19: + { + const uint64_t result = static_cast(cpu.gpr[rs]) * cpu.gpr[rt]; + cpu.lo = static_cast(result); + cpu.hi = static_cast(result >> 32u); + break; + } + case 0x1A: + { + const int32_t lhs = static_cast(cpu.gpr[rs]); + const int32_t rhs = static_cast(cpu.gpr[rt]); + if (rhs == 0) + { + cpu.lo = lhs >= 0 ? 0xFFFFFFFFu : 1u; + cpu.hi = static_cast(lhs); + } + else if (lhs == std::numeric_limits::min() && rhs == -1) + { + cpu.lo = static_cast(lhs); + cpu.hi = 0u; + } + else + { + cpu.lo = static_cast(lhs / rhs); + cpu.hi = static_cast(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(cpu.gpr[rs]), static_cast(cpu.gpr[rt]), rd); + break; + case 0x21: + write(rd, cpu.gpr[rs] + cpu.gpr[rt]); + break; + case 0x22: + overflowSub(static_cast(cpu.gpr[rs]), static_cast(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(cpu.gpr[rs]) < static_cast(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(cpu.gpr[rs]) < 0); + break; + case 0x01: + branch(static_cast(cpu.gpr[rs]) >= 0); + break; + case 0x10: + write(31u, pc + 8u); + branch(static_cast(cpu.gpr[rs]) < 0); + break; + case 0x11: + write(31u, pc + 8u); + branch(static_cast(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(cpu.gpr[rs]) <= 0); + break; + case 0x07: + branch(static_cast(cpu.gpr[rs]) > 0); + break; + case 0x08: + overflowAdd(static_cast(cpu.gpr[rs]), simm, rt); + break; + case 0x09: + write(rt, cpu.gpr[rs] + static_cast(simm)); + break; + case 0x0A: + write(rt, static_cast(cpu.gpr[rs]) < simm ? 1u : 0u); + break; + case 0x0B: + write(rt, cpu.gpr[rs] < static_cast(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(simm); + const uint8_t value = m_memory.read8(address); + load(rt, opcode == 0x20 + ? static_cast(static_cast(static_cast(value))) + : value); + break; + } + case 0x21: + case 0x25: + { + const uint32_t address = cpu.gpr[rs] + static_cast(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(static_cast(static_cast(value))) : value); + break; + } + case 0x22: + { + const uint32_t address = cpu.gpr[rs] + static_cast(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(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(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(simm), static_cast(cpu.gpr[rt])); + break; + case 0x29: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + if (address & 1u) + { + raiseException(cpu, 5u, pc, wasDelaySlot, address); + break; + } + m_memory.write16(address, static_cast(cpu.gpr[rt])); + break; + } + case 0x2A: + { + const uint32_t address = cpu.gpr[rs] + static_cast(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(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(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; + } +} diff --git a/ps2xIOP/src/emulator/iop_cpu.h b/ps2xIOP/src/emulator/iop_cpu.h new file mode 100644 index 0000000..f236050 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_cpu.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopMemory; + + struct IopCpuState + { + std::array gpr{}; + uint32_t hi = 0; + uint32_t lo = 0; + uint32_t pc = 0; + std::array 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 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; + }; +} diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp new file mode 100644 index 0000000..4dcef1c --- /dev/null +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -0,0 +1,1166 @@ +#include "iop_emulator.h" +#include "iop_cdvd.h" +#include "iop_cpu.h" +#include "iop_imports.h" +#include "iop_kernel.h" +#include "iop_memory.h" +#include "iop_module_loader.h" +#include "iop_rpc.h" +#include "iop_sysclib.h" +#include "iop_emulator_const.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kRamSize = IopMemory::RamSize; + constexpr uint32_t kKernelHeapBase = IopMemory::HeapBase; + constexpr uint32_t kKernelHeapLimit = IopMemory::HeapLimit; + constexpr uint32_t kCallStackBase = kKernelHeapLimit; + constexpr uint32_t kCallStackLimit = 0x001FFF00u; + constexpr uint32_t kCallStackSize = 0x2000u; + constexpr uint32_t kCallStackCapacity = (kCallStackLimit - kCallStackBase) / kCallStackSize; + constexpr uint64_t kCdvdCompletionCycles = 128u; + + uint32_t physicalAddress(uint32_t address) + { + return IopMemory::physicalAddress(address); + } + + int32_t sign16(uint32_t value) + { + return static_cast(value & 0xFFFFu); + } + + bool iequals(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(lhs[i])) != + std::tolower(static_cast(rhs[i]))) + return false; + } + return true; + } + + } + + class IopEmulator::Impl final : public IopGuestExecutor + { + public: + using CpuState = IopCpuState; + + struct Module + { + int id = 0; + std::string path; + std::string name; + uint32_t base = 0; + uint32_t size = 0; + uint32_t entry = 0; + uint32_t gp = 0; + bool resident = false; + }; + + struct InterruptHandler + { + uint32_t function = 0; + uint32_t argument = 0; + uint32_t gp = 0; + }; + + struct GuestCallback + { + uint32_t function = 0; + uint32_t gp = 0; + }; + + struct ScheduledGuestCallback + { + uint32_t function = 0u; + uint32_t gp = 0u; + uint32_t argument = 0u; + }; + + struct IomanDevice + { + uint32_t address = 0; + uint32_t gp = 0; + std::string name; + }; + + explicit Impl(IopHost &hostRef) + : host(hostRef), + cdvd(host, memory), + kernel(memory), + rpc(host, memory, kernel), + sysclib(memory), + cpuCore(memory), + imports(memory) + { + reset(); + } + + void reset() + { + memory.reset(); + kernel.reset(); + modules.clear(); + imports.reset(); + rpc.reset(); + cdvd.reset(); + interruptHandlers.clear(); + iomanDevices.clear(); + interruptEnabled.clear(); + pendingDmaInterrupts.clear(); + pendingGuestCallbacks.clear(); + nextModuleId = 1; + moduleCursor = kModuleLoadBase; + totalCycles = 0; + totalInstructions = 0; + eeCycleCarry = 0; + activeCpu = nullptr; + lastError.clear(); + servicingDmaInterrupts = false; + servicingGuestCallbacks = false; + callDepth = 0u; + secrMcCommandHandler = {}; + secrMcDevIdHandler = {}; + checkKelfPathCallback = {}; + } + + uint8_t read8(uint32_t address) const + { + return memory.read8(address); + } + + uint16_t read16(uint32_t address) const + { + return memory.read16(address); + } + + uint32_t read32(uint32_t address) const + { + return memory.read32(address); + } + + void write8(uint32_t address, uint8_t value) + { + memory.write8(address, value); + schedulePendingDma(); + } + + void write16(uint32_t address, uint16_t value) + { + memory.write16(address, value); + schedulePendingDma(); + } + + void write32(uint32_t address, uint32_t value) + { + memory.write32(address, value); + schedulePendingDma(); + } + + void schedulePendingDma() + { + if (const auto dma = memory.takeDmaStart()) + pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles; + } + + bool readRam(uint32_t address, void *destination, size_t size) const + { + return memory.readRam(address, destination, size); + } + + bool writeRam(uint32_t address, const void *source, size_t size) + { + return memory.writeRam(address, source, size); + } + + bool zeroRam(uint32_t address, size_t size) + { + return memory.zeroRam(address, size); + } + + bool isHardwareAddress(uint32_t phys) const + { + return memory.isHardwareAddress(phys); + } + + uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional fixed = std::nullopt) + { + return memory.allocate(size, alignment, fixed); + } + + bool freeAllocation(uint32_t address) + { + return memory.freeAllocation(address); + } + + uint32_t maxFreeMemory() const + { + return memory.maxFreeMemory(); + } + + std::string readString(uint32_t address, size_t limit = 1024) const + { + return memory.readString(address, limit); + } + + void log(LogLevel level, std::string_view text) + { + host.log(level, text); + } + + bool checkInterrupt(CpuState &cpu) + { + const uint32_t status = cpu.cop0[12]; + if ((status & 1u) == 0u) + return false; + if ((status & 0x2u) != 0u) + return false; + const bool pending = memory.interruptControl() != 0u && (memory.interruptStatus() & memory.interruptMask()) != 0u; + if (!pending) + return false; + cpu.cop0[13] |= 0x400u; + cpuCore.raiseException(cpu, 0u, cpu.pc, false); + return true; + } + + enum class ImportDisposition + { + Handled, + JumpToGuest, + Missing, + }; + + ImportDisposition dispatchImport(const IopImportCall &call, CpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const uint32_t a2 = cpu.gpr[6]; + const uint32_t a3 = cpu.gpr[7]; + auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + // TODO this will become a dispatch table once we have more imports implemented, but for now this is fine. + if (iequals(call.library, "sysmem")) + { + switch (call.ordinal) + { + case 4: + { + const uint32_t mode = a0; + const uint32_t size = a1; + const uint32_t ptr = a2; + const uint32_t address = mode == 2u ? allocate(size, 16u, ptr) : allocate(size, 16u); + setV0(address); + return ImportDisposition::Handled; + } + case 5: + setV0(freeAllocation(a0) ? 0u : 0xFFFFFFFFu); + return ImportDisposition::Handled; + case 6: + setV0(kRamSize); + return ImportDisposition::Handled; + case 7: + setV0(maxFreeMemory()); + return ImportDisposition::Handled; + case 8: + setV0(maxFreeMemory()); + return ImportDisposition::Handled; + case 9: + { + if (const auto block = memory.allocationContaining(a0)) + { + setV0(block->address); + return ImportDisposition::Handled; + } + setV0(0u); + return ImportDisposition::Handled; + } + case 10: + { + if (const auto block = memory.allocationContaining(a0)) + { + setV0(block->size); + return ImportDisposition::Handled; + } + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + } + case 14: + { + const std::string format = readString(a0, 512); + log(LogLevel::Info, std::string("[IOP Kprintf] ") + format); + setV0(static_cast(format.size())); + return ImportDisposition::Handled; + } + case 15: + setV0(0); + return ImportDisposition::Handled; + default: + break; + } + } + + if (iequals(call.library, "cdvdman") && + cdvd.dispatchImport(call.ordinal, cpu)) + { + if (const auto callback = cdvd.takeCompletionCallback()) + { + pendingGuestCallbacks.emplace( + totalCycles + kCdvdCompletionCycles, + ScheduledGuestCallback{ + callback->address, + callback->gp, + callback->reason, + }); + } + return ImportDisposition::Handled; + } + + if (iequals(call.library, "loadcore")) + { + switch (call.ordinal) + { + case 3: + setV0(0); + return ImportDisposition::Handled; + case 4: + case 5: + setV0(0); + return ImportDisposition::Handled; + case 6: + case 10: + setV0(imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu); + return ImportDisposition::Handled; + case 7: + setV0(imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu); + return ImportDisposition::Handled; + case 8: + case 9: + setV0(0); + return ImportDisposition::Handled; + case 11: + { + const std::string name = readString(a0 + 12u, 8u); + setV0(imports.findTable(name)); + return ImportDisposition::Handled; + } + case 12: + setV0(0); + return ImportDisposition::Handled; + case 13: + case 14: + case 15: + case 16: + case 17: + case 20: + case 21: + setV0(0); + return ImportDisposition::Handled; + default: + break; + } + } + + if (iequals(call.library, "thbase") || iequals(call.library, "threadman")) + { + return kernel.dispatchThreadImport(call.ordinal, cpu, totalCycles) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "thsemap")) + { + return kernel.dispatchSemaphoreImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "thevent")) + { + return kernel.dispatchEventImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "sifcmd")) + { + return rpc.dispatchSifCmdImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "intrman")) + { + switch (call.ordinal) + { + case 3: + setV0(0); + return ImportDisposition::Handled; + case 4: // RegisterIntrHandler + interruptHandlers[static_cast(a0)] = {a2, a3, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + case 5: + interruptHandlers.erase(static_cast(a0)); + setV0(0); + return ImportDisposition::Handled; + case 6: + interruptEnabled[static_cast(a0)] = true; + if (a0 < 32u) + memory.setInterruptMask(memory.interruptMask() | (1u << a0)); + setV0(0); + return ImportDisposition::Handled; + case 7: + if (a1) + write32(a1, a0); + if (a0 < 32u) + memory.setInterruptMask(memory.interruptMask() & ~(1u << a0)); + interruptEnabled[static_cast(a0)] = false; + setV0(0); + return ImportDisposition::Handled; + case 8: + memory.setInterruptControl(0u); + setV0(0); + return ImportDisposition::Handled; + case 9: + memory.setInterruptControl(1u); + setV0(0); + return ImportDisposition::Handled; + case 14: + if (a0) + { + const uint32_t ret = callFunction(a0, a1, a2, a3, cpu.gpr[28], 100000u); + setV0(ret); + } + else + setV0(0); + return ImportDisposition::Handled; + case 15: + case 16: + setV0(0); + return ImportDisposition::Handled; + case 17: + if (a0) + write32(a0, memory.interruptControl()); + memory.setInterruptControl(0u); + setV0(0); + return ImportDisposition::Handled; + case 18: + memory.setInterruptControl(a0 ? 1u : 0u); + setV0(0); + return ImportDisposition::Handled; + case 23: + setV0(0); + return ImportDisposition::Handled; + case 24: + setV0(0); + return ImportDisposition::Handled; + case 25: + setV0(0); + return ImportDisposition::Handled; + case 28: + case 30: + setV0(0); + return ImportDisposition::Handled; + default: + break; + } + } + if (iequals(call.library, "secrman")) + { + switch (call.ordinal) + { + case 4: // SecrSetMcCommandHandler + secrMcCommandHandler = {a0, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + case 5: // SecrSetMcDevIDHandler + secrMcDevIdHandler = {a0, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + default: + break; + } + } + if (iequals(call.library, "modload") && call.ordinal == 13u) + { + // SetCheckKelfPathCallback. MODLOAD owns this callback in the + // real IOP; retain both the address and the registering + // module's GP so a later KELF load can invoke it faithfully. + checkKelfPathCallback = {a0, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + } + if (iequals(call.library, "ioman")) + { + switch (call.ordinal) + { + case 20: // AddDrv + { + constexpr size_t kMaxIomanDevices = 16u; + if (a0 == 0u || iomanDevices.size() >= kMaxIomanDevices) + { + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + } + + const uint32_t nameAddress = read32(a0 + 0u); + const uint32_t operations = read32(a0 + 16u); + const std::string name = readString(nameAddress, 64u); + if (nameAddress == 0u || operations == 0u || name.empty()) + { + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + } + + // The original IOMAN exposes the device before invoking + // init, then removes it again if initialization fails. + iomanDevices.push_back({a0, cpu.gpr[28], name}); + const uint32_t init = read32(operations + 0u); + if (init != 0u) + { + const int32_t initResult = static_cast( + callFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28])); + if (initResult < 0) + { + iomanDevices.pop_back(); + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + } + } + + setV0(0u); + return ImportDisposition::Handled; + } + case 21: // DelDrv + { + const std::string name = readString(a0, 64u); + const auto device = std::find_if( + iomanDevices.begin(), iomanDevices.end(), + [&](const IomanDevice &candidate) + { return candidate.name == name; }); + if (device == iomanDevices.end()) + { + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + } + + const uint32_t operations = read32(device->address + 16u); + const uint32_t deinit = operations != 0u ? read32(operations + 4u) : 0u; + if (deinit != 0u) + (void)callFunction(deinit, device->address, 0u, 0u, 0u, device->gp); + iomanDevices.erase(device); + setV0(0u); + return ImportDisposition::Handled; + } + default: + break; + } + } + if (iequals(call.library, "sifman")) + { + return rpc.dispatchSifManImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "vblank")) + { + switch (call.ordinal) + { + case 4: // WaitVblankStart + case 5: // WaitVblankEnd + case 6: // WaitVblank + case 7: // WaitNonVblank + { + const bool waitForEnd = call.ordinal == 5u || call.ordinal == 7u; + const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u; + const uint64_t fieldStart = totalCycles - (totalCycles % kVblankPeriodCycles); + uint64_t wakeCycle = fieldStart + phase; + if (wakeCycle <= totalCycles) + wakeCycle += kVblankPeriodCycles; + kernel.delayCurrentUntil(wakeCycle, cpu); + } + setV0(0); + return ImportDisposition::Handled; + case 8: // RegisterVblankHandler + case 9: // ReleaseVblankHandler + // Callback delivery is not required by the scheduler wait + // ABI yet. + setV0(0); + return ImportDisposition::Handled; + default: + break; + } + } + if (iequals(call.library, "timrman") || iequals(call.library, "dmacman")) + { + setV0(0); + return ImportDisposition::Handled; + } + if (iequals(call.library, "stdio")) + { + switch (call.ordinal) + { + case 4: // printf + { + const std::string text = readString(a0, 2048); + log(LogLevel::Info, std::string("[IOP printf] ") + text); + setV0(static_cast(text.size())); + return ImportDisposition::Handled; + } + case 5: + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; // getchar + case 6: + { + const char ch = static_cast(a0 & 0xFFu); + log(LogLevel::Info, std::string("[IOP putchar] ") + ch); + setV0(a0 & 0xFFu); + return ImportDisposition::Handled; + } + case 7: + { + const std::string text = readString(a0, 2048); + log(LogLevel::Info, std::string("[IOP puts] ") + text); + setV0(static_cast(text.size() + 1u)); + return ImportDisposition::Handled; + } + case 8: + setV0(0); + return ImportDisposition::Handled; // gets + case 9: + { + const std::string text = readString(a1, 2048); + log(LogLevel::Info, std::string("[IOP fdprintf] ") + text); + setV0(static_cast(text.size())); + return ImportDisposition::Handled; + } + case 10: + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + case 11: + setV0(a0 & 0xFFu); + return ImportDisposition::Handled; + case 12: + { + const std::string text = readString(a0, 2048); + log(LogLevel::Info, std::string("[IOP fdputs] ") + text); + setV0(static_cast(text.size())); + return ImportDisposition::Handled; + } + case 13: + setV0(0); + return ImportDisposition::Handled; + case 14: + { + const std::string text = readString(a1, 2048); + log(LogLevel::Info, std::string("[IOP vfdprintf] ") + text); + setV0(static_cast(text.size())); + return ImportDisposition::Handled; + } + default: + break; + } + } + if (iequals(call.library, "sysclib")) + { + return sysclib.dispatchImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "heaplib")) + { + switch (call.ordinal) + { + case 4: // CreateHeap - heap identity is opaque to callers. + setV0(allocate(16u, 16u)); + return ImportDisposition::Handled; + case 5: // DeleteHeap + if (a0) + freeAllocation(a0); + setV0(0); + return ImportDisposition::Handled; + case 6: + setV0(allocate(a1, 16u)); + return ImportDisposition::Handled; + case 7: + setV0(freeAllocation(a1) ? 0u : 0xFFFFFFFFu); + return ImportDisposition::Handled; + case 8: + setV0(maxFreeMemory()); + return ImportDisposition::Handled; + case 11: + setV0(0); + return ImportDisposition::Handled; + case 15: + { + if (const auto block = memory.allocationContaining(a0)) + { + setV0(block->size); + return ImportDisposition::Handled; + } + setV0(0xFFFFFFFFu); + return ImportDisposition::Handled; + } + default: + break; + } + } + + const uint32_t target = imports.resolve(call.library, call.ordinal); + if (target != 0u) + { + cpu.pc = target; + cpu.branchPending = false; + return ImportDisposition::JumpToGuest; + } + + std::ostringstream out; + out << "[IOP] unhandled import " << call.library << ':' << call.ordinal << " pc=0x" << std::hex << cpu.pc; + log(LogLevel::Warning, out.str()); + setV0(0); + return ImportDisposition::Missing; + } + + bool step(CpuState &cpu) + { + if (cpu.stopped) + return false; + if (cpu.pc == kThreadReturnSentinel || cpu.pc == kCallReturnSentinel) + { + cpu.stopped = true; + return false; + } + if (physicalAddress(cpu.pc) >= kRamSize) + { + std::ostringstream out; + out << "[IOP] execution outside RAM pc=0x" << std::hex << cpu.pc; + log(LogLevel::Error, out.str()); + cpu.stopped = true; + return false; + } + if (checkInterrupt(cpu)) + return true; + + if (const auto import = imports.decode(cpu.pc)) + { + const ImportDisposition disposition = dispatchImport(*import, cpu); + ++totalInstructions; + ++totalCycles; + if (disposition == ImportDisposition::JumpToGuest) + return true; + cpu.pc = cpu.gpr[31]; + cpu.branchPending = false; + return !cpu.stopped; + } + + const bool running = cpuCore.executeInstruction(cpu); + schedulePendingDma(); + ++totalInstructions; + ++totalCycles; + return running; + } + + uint32_t runCpu(CpuState &cpu, uint32_t instructionBudget) + { + CpuState *previous = activeCpu; + activeCpu = &cpu; + const uint64_t start = totalInstructions; + while (!cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget) + { + if (!step(cpu)) + break; + if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty()) + servicePendingDmaInterrupts(); + if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty()) + servicePendingGuestCallbacks(); + } + activeCpu = previous; + return static_cast(totalInstructions - start); + } + + uint32_t callFunction(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp, + uint32_t budget = kMaxCallInstructions) + { + struct CallDepthGuard + { + uint32_t &depth; + ~CallDepthGuard() { --depth; } + }; + + const uint32_t depth = callDepth++; + const CallDepthGuard depthGuard{callDepth}; + CpuState cpu{}; + cpu.pc = address; + cpu.gpr[4] = a0; + cpu.gpr[5] = a1; + cpu.gpr[6] = a2; + cpu.gpr[7] = a3; + cpu.gpr[28] = gp; + if (depth < kCallStackCapacity) + { + const uint32_t stackTop = kCallStackLimit - depth * kCallStackSize; + cpu.gpr[29] = stackTop - 32u; + } + else if (activeCpu && activeCpu->gpr[29] > kCallStackBase + kStackGuardBytes) + { + // Extremely deep re-entrancy borrows unused space below the + // suspended caller's live frame. Stack growth remains away + // from the caller, so its saved registers stay intact. + cpu.gpr[29] = (activeCpu->gpr[29] - kStackGuardBytes) & ~15u; + } + else + { + cpu.gpr[29] = kCallStackBase - 32u; + } + cpu.gpr[31] = kCallReturnSentinel; + runCpu(cpu, budget); + return cpu.gpr[2]; + } + + uint32_t executeGuestFunction(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp) override + { + return callFunction(address, a0, a1, a2, a3, gp); + } + + // Not that good to use exception handling for control flow but will do for now + void servicePendingDmaInterrupts() + { + if (servicingDmaInterrupts || pendingDmaInterrupts.empty()) + return; + + servicingDmaInterrupts = true; + + std::vector completed; + for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();) + { + if (it->second > totalCycles) + { + ++it; + continue; + } + completed.push_back(it->first); + it = pendingDmaInterrupts.erase(it); + } + try + { + for (const int irq : completed) + { + const auto enabled = interruptEnabled.find(irq); + if (enabled == interruptEnabled.end() || !enabled->second) + continue; + const auto handler = interruptHandlers.find(irq); + if (handler == interruptHandlers.end() || handler->second.function == 0u) + continue; + + (void)callFunction(handler->second.function, + handler->second.argument, + 0u, + 0u, + 0u, + handler->second.gp, + 100000u); + } + } + catch (...) + { + servicingDmaInterrupts = false; + throw; + } + servicingDmaInterrupts = false; + } + + void servicePendingGuestCallbacks() + { + if (servicingGuestCallbacks || pendingGuestCallbacks.empty()) + return; + + std::vector callbacks; + for (auto it = pendingGuestCallbacks.begin(); it != pendingGuestCallbacks.end();) + { + if (it->first > totalCycles) + break; + callbacks.push_back(it->second); + it = pendingGuestCallbacks.erase(it); + } + if (callbacks.empty()) + return; + + servicingGuestCallbacks = true; + try + { + for (const ScheduledGuestCallback &callback : callbacks) + { + if (callback.function != 0u) + { + (void)callFunction(callback.function, + callback.argument, + 0u, + 0u, + 0u, + callback.gp, + 100000u); + } + } + } + catch (...) + { + servicingGuestCallbacks = false; + throw; + } + servicingGuestCallbacks = false; + } + + void runCycles(uint64_t cycles) noexcept + { + try + { + const uint64_t target = totalCycles + cycles; + while (totalCycles < target) + { + servicePendingDmaInterrupts(); + servicePendingGuestCallbacks(); + IopThread *next = kernel.beginNextReady(totalCycles); + if (!next) + { + uint64_t nextWake = kernel.nextWakeCycle(target); + for (const auto &[irq, completionCycle] : pendingDmaInterrupts) + nextWake = std::min(nextWake, completionCycle); + if (!pendingGuestCallbacks.empty()) + nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first); + totalCycles = std::max(totalCycles + 1u, std::min(target, nextWake)); + continue; + } + const uint64_t before = totalCycles; + runCpu(next->cpu, static_cast(std::min(kDefaultSlice, target - totalCycles))); + kernel.endTimeslice(*next, kThreadReturnSentinel); + if (totalCycles == before) + ++totalCycles; + } + } + catch (...) + { + // Runtime scheduling must never throw through EeScheduler::accountCycles(). + } + } + + ModuleLoadResult loadImage(std::string path, std::span image, const void *arguments, uint32_t argumentSize) + { + ModuleLoadResult result{true, -1, -1}; + const IopImageLoadResult loaded = IopModuleLoader::load(image, memory, moduleCursor); + moduleCursor = loaded.nextModuleCursor; + if (!loaded) + { + if (loaded.error == IopImageLoadError::InvalidElf) + log(LogLevel::Error, "[IOP] rejected invalid/non-MIPS IRX ELF"); + else if (loaded.error == IopImageLoadError::ArenaExhausted) + log(LogLevel::Error, "[IOP] module arena exhausted"); + return result; + } + if (!loaded.relocationsComplete) + log(LogLevel::Warning, "[IOP] one or more IRX relocations were unsupported"); + + Module module; + module.id = nextModuleId++; + module.path = std::move(path); + const size_t slash = module.path.find_last_of("/\\:"); + module.name = slash == std::string::npos ? module.path : module.path.substr(slash + 1u); + module.base = loaded.base; + module.size = loaded.size; + module.entry = loaded.entry; + module.gp = loaded.gp; + + uint32_t args = 0u; + if (arguments && argumentSize) + { + args = allocate(argumentSize + 1u, 16u); + if (args) + { + writeRam(args, arguments, argumentSize); + write8(args + argumentSize, 0u); + } + } + const uint32_t startResult = callFunction(module.entry, argumentSize, args, 0u, 0u, module.gp); + if (args) + freeAllocation(args); + module.resident = startResult == 0u || startResult == 2u; + result.moduleId = module.id; + result.startResult = static_cast(startResult); + modules[module.id] = std::move(module); + + std::ostringstream out; + out << "[IOP] loaded IRX id=" << result.moduleId + << " entry=0x" << std::hex << modules[result.moduleId].entry + << " base=0x" << modules[result.moduleId].base + << " start=" << std::dec << result.startResult; + log(LogLevel::Info, out.str()); + return result; + } + + ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize) + { + std::vector image; + if (!IopModuleLoader::readWholeHostFile(host, path, image)) + { + log(LogLevel::Warning, std::string("[IOP] failed to open IRX '") + std::string(path) + "'"); + return {true, -1, -1}; + } + return loadImage(std::string(path), image, arguments, argumentSize); + } + + ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) + { + std::vector image; + if (!IopModuleLoader::readElfFromGuest(host, guestAddress, image)) + return {true, -1, -1}; + std::ostringstream tag; + tag << "buffer@0x" << std::hex << guestAddress; + return loadImage(tag.str(), image, arguments, argumentSize); + } + + bool stopModule(int32_t moduleId, int32_t *result) + { + auto it = modules.find(moduleId); + if (it == modules.end()) + return false; + // A removable IRX normally exposes a stop entry through module metadata. We do not guess it; terminate owned execution and release the image cleanly. + kernel.terminateThreadsInRange(it->second.base, it->second.size); + rpc.removeServersInRange(it->second.base, it->second.size); + imports.eraseRange(it->second.base, it->second.size); + modules.erase(it); + kernel.cleanupDeadThreads(); + if (result) + *result = 0; + return true; + } + + IopHost &host; + IopMemory memory; + IopCdvd cdvd; + IopKernel kernel; + IopRpcBridge rpc; + IopSysclib sysclib; + IopCpuCore cpuCore; + IopImportRegistry imports; + std::map modules; + std::unordered_map interruptHandlers; + std::vector iomanDevices; + std::map interruptEnabled; + std::map pendingDmaInterrupts; + std::multimap pendingGuestCallbacks; + uint32_t nextModuleId = 1; + uint32_t moduleCursor = kModuleLoadBase; + uint64_t totalCycles = 0; + uint64_t totalInstructions = 0; + uint64_t eeCycleCarry = 0; + CpuState *activeCpu = nullptr; + std::string lastError; + bool servicingDmaInterrupts = false; + bool servicingGuestCallbacks = false; + uint32_t callDepth = 0u; + GuestCallback secrMcCommandHandler; + GuestCallback secrMcDevIdHandler; + GuestCallback checkKelfPathCallback; + }; + + IopEmulator::IopEmulator(IopHost &host) + : m_impl(std::make_unique(host)) + { + } + + IopEmulator::~IopEmulator() = default; + + void IopEmulator::reset() + { + m_impl->reset(); + } + + ModuleLoadResult IopEmulator::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize) + { + return m_impl->loadModule(path, arguments, argumentSize); + } + + ModuleLoadResult IopEmulator::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) + { + return m_impl->loadModuleBuffer(guestAddress, arguments, argumentSize); + } + + bool IopEmulator::stopModule(int32_t moduleId, int32_t *result) + { + return m_impl->stopModule(moduleId, result); + } + + void IopEmulator::runEeCycles(uint64_t eeCycles) noexcept + { + const uint64_t total = m_impl->eeCycleCarry + eeCycles; + const uint64_t iopCycles = total / 8u; + m_impl->eeCycleCarry = total % 8u; + if (iopCycles) + m_impl->runCycles(iopCycles); + } + + RpcResult IopEmulator::handleRpc(const RpcRequest &request) + { + return m_impl->rpc.handleRpc(request, *m_impl); + } + + bool IopEmulator::hasRpcServer(uint32_t sid) const noexcept + { + return m_impl->rpc.hasServer(sid); + } + + void IopEmulator::onSifTransfer(const SifTransfer &transfer) + { + m_impl->rpc.onSifTransfer(transfer); + } + + uint64_t IopEmulator::cycles() const noexcept + { + return m_impl->totalCycles; + } + + uint64_t IopEmulator::instructions() const noexcept + { + return m_impl->totalInstructions; + } + + uint32_t IopEmulator::loadedModuleCount() const noexcept + { + return static_cast(m_impl->modules.size()); + } + + uint32_t IopEmulator::threadCount() const noexcept + { + return static_cast(m_impl->kernel.threadCount()); + } + + uint32_t IopEmulator::rpcServerCount() const noexcept + { + return static_cast(m_impl->rpc.serverCount()); + } + +} diff --git a/ps2xIOP/src/emulator/iop_emulator.h b/ps2xIOP/src/emulator/iop_emulator.h new file mode 100644 index 0000000..4d7bc40 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_emulator.h @@ -0,0 +1,42 @@ +#pragma once + +#include "ps2x/iop/iop_host.h" +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include + +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 m_impl; + }; +} diff --git a/ps2xIOP/src/emulator/iop_emulator_const.h b/ps2xIOP/src/emulator/iop_emulator_const.h new file mode 100644 index 0000000..7004597 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_emulator_const.h @@ -0,0 +1,15 @@ +#pragma once +#include + +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; \ No newline at end of file diff --git a/ps2xIOP/src/emulator/iop_imports.cpp b/ps2xIOP/src/emulator/iop_imports.cpp new file mode 100644 index 0000000..396093d --- /dev/null +++ b/ps2xIOP/src/emulator/iop_imports.cpp @@ -0,0 +1,160 @@ +#include "iop_imports.h" + +#include "iop_memory.h" + +#include +#include +#include + +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(lhs[i])) != + std::tolower(static_cast(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 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(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(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(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; + } + } +} diff --git a/ps2xIOP/src/emulator/iop_imports.h b/ps2xIOP/src/emulator/iop_imports.h new file mode 100644 index 0000000..87d4f99 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_imports.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 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 functions; + }; + + IopMemory &m_memory; + std::map m_libraries; + }; +} diff --git a/ps2xIOP/src/emulator/iop_kernel.cpp b/ps2xIOP/src/emulator/iop_kernel.cpp new file mode 100644 index 0000000..cc9cdc0 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_kernel.cpp @@ -0,0 +1,729 @@ +#include "iop_kernel.h" + +#include "iop_memory.h" +#include "iop_emulator_const.h" + +#include + +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(value); + }; + + switch (ordinal) + { + case 4: // CreateThread + { + const uint32_t descriptor = cpu.gpr[4]; + IopThread thread; + thread.id = static_cast(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(m_memory.read32(descriptor + 12u), 0x100u); + thread.priority = std::clamp(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(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(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(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(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(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(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(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(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(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(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(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(cpu.gpr[4]) * kIopClockHz + 999'999ull) / 1'000'000ull; + delayCurrentUntil(currentCycle + std::max(delayCycles, 1u), cpu); + } + setV0(0); + return true; + case 34: // GetSystemTime + m_memory.write32(cpu.gpr[4], static_cast(currentCycle)); + m_memory.write32(cpu.gpr[4] + 4u, static_cast(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(cpu.gpr[4]) * kIopClockHz) / 1'000'000ull; + m_memory.write32(cpu.gpr[5], static_cast(cycles)); + m_memory.write32(cpu.gpr[5] + 4u, static_cast(cycles >> 32u)); + setV0(0); + return true; + } + case 40: + { + const uint64_t cycles = static_cast(m_memory.read32(cpu.gpr[4])) | (static_cast(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(usec / 1'000'000ull)); + if (cpu.gpr[6] != 0u) + m_memory.write32(cpu.gpr[6], static_cast(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(thread.waitId)); + m_memory.write32(outputAddress + 44u, static_cast(thread.wakeupCount)); + return true; + } + + bool IopKernel::dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu) + { + const auto setV0 = [&](int32_t value) + { + cpu.gpr[2] = static_cast(value); + }; + + switch (ordinal) + { + case 4: + { + const uint32_t descriptor = cpu.gpr[4]; + Semaphore semaphore; + semaphore.id = static_cast(m_nextSemaphoreId++); + semaphore.attr = m_memory.read32(descriptor + 0u); + semaphore.option = m_memory.read32(descriptor + 4u); + semaphore.current = static_cast(m_memory.read32(descriptor + 8u)); + semaphore.maximum = std::max(1, static_cast(m_memory.read32(descriptor + 12u))); + m_semaphores.emplace(semaphore.id, semaphore); + setV0(semaphore.id); + return true; + } + case 5: + setV0(m_semaphores.erase(static_cast(cpu.gpr[4])) != 0u ? 0 : -1); + return true; + case 6: + case 7: + { + const int id = static_cast(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(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(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(it->second.maximum)); + m_memory.write32(outputAddress + 16u, static_cast(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(value); + }; + + switch (ordinal) + { + case 4: + { + const uint32_t descriptor = cpu.gpr[4]; + EventFlag event; + event.id = static_cast(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(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(-1); + } + } + setV0(0); + return true; + } + case 6: + case 7: + { + const auto event = m_eventFlags.find(static_cast(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(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(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(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; + } + } +} diff --git a/ps2xIOP/src/emulator/iop_kernel.h b/ps2xIOP/src/emulator/iop_kernel.h new file mode 100644 index 0000000..890549e --- /dev/null +++ b/ps2xIOP/src/emulator/iop_kernel.h @@ -0,0 +1,100 @@ +#pragma once + +#include "iop_cpu.h" + +#include +#include +#include + +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 m_threads; + std::map m_semaphores; + std::map m_eventFlags; + uint32_t m_nextThreadId = 1; + uint32_t m_nextSemaphoreId = 1; + uint32_t m_nextEventFlagId = 1; + IopThread *m_currentThread = nullptr; + }; +} diff --git a/ps2xIOP/src/emulator/iop_memory.cpp b/ps2xIOP/src/emulator/iop_memory.cpp new file mode 100644 index 0000000..11ea156 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_memory.cpp @@ -0,0 +1,370 @@ +#include "iop_memory.h" + +#include +#include + +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(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(read8(address) | (static_cast(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(read8(address)) | + (static_cast(read8(address + 1u)) << 8u) | + (static_cast(read8(address + 2u)) << 16u) | + (static_cast(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(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(value)); + write8(address + 1u, static_cast(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(value)); + write8(address + 1u, static_cast(value >> 8u)); + write8(address + 2u, static_cast(value >> 16u)); + write8(address + 3u, static_cast(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(blockControl & 0xFFFFu, 1u); + const uint32_t blockCount = std::max(blockControl >> 16u, 1u); + const uint64_t transferWords = static_cast(wordsPerBlock) * blockCount; + m_dmaStart = DmaStart{ + secondCore ? kDmaSpu1Irq : kDmaSpu0Irq, + std::max(transferWords * 2u, 64u), + }; + } + + std::optional IopMemory::takeDmaStart() noexcept + { + std::optional result = m_dmaStart; + m_dmaStart.reset(); + return result; + } + + uint32_t IopMemory::allocate(uint32_t size, uint32_t alignment, std::optional fixed) + { + size = alignUp(std::max(size, 1u), 16u); + alignment = std::max(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::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(limit, 64u)); + for (size_t i = 0; i < limit; ++i) + { + const char ch = static_cast(read8(address + static_cast(i))); + if (ch == '\0') + break; + result.push_back(ch); + } + return result; + } +} diff --git a/ps2xIOP/src/emulator/iop_memory.h b/ps2xIOP/src/emulator/iop_memory.h new file mode 100644 index 0000000..a932a5d --- /dev/null +++ b/ps2xIOP/src/emulator/iop_memory.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 fixed = std::nullopt); + [[nodiscard]] bool freeAllocation(uint32_t address); + [[nodiscard]] uint32_t maxFreeMemory() const; + [[nodiscard]] std::optional 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 takeDmaStart() noexcept; + [[nodiscard]] std::span 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 m_ram; + std::vector m_owned; + std::vector m_scratch; + std::unordered_map m_hardware; + std::vector m_allocations; + uint32_t m_heapCursor = HeapBase; + uint32_t m_interruptStatus = 0; + uint32_t m_interruptMask = 0; + uint32_t m_interruptControl = 1; + std::optional m_dmaStart; + }; +} diff --git a/ps2xIOP/src/emulator/iop_module_loader.cpp b/ps2xIOP/src/emulator/iop_module_loader.cpp new file mode 100644 index 0000000..485c1ec --- /dev/null +++ b/ps2xIOP/src/emulator/iop_module_loader.cpp @@ -0,0 +1,572 @@ +#include "iop_module_loader.h" + +#include "iop_memory.h" +#include "ps2x/iop/iop_subsystem.h" + +#include +#include +#include +#include + +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 image, + const std::vector §ions, + int64_t delta, + uint32_t loadBase, + bool isIopRelocatable, + IopMemory &memory) + { + if (sections.empty()) + return true; + + bool allSupported = true; + std::vector 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(static_cast(targetSection.addr) + delta); + + std::span symbols; + std::vector 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(relsec.entsize, sizeof(Elf32Rela)) + : std::max(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( + static_cast(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(loadBase) + relocationOffset + : static_cast(targetBase) + relocationOffset; + if (place64 > std::numeric_limits::max()) + { + allSupported = false; + continue; + } + const uint32_t place = static_cast(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(word); + switch (type) + { + case R_MIPS_NONE: + break; + case R_MIPS_32: + case R_MIPS_REL32: + memory.write32(place, + static_cast( + static_cast(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(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(hiWord & 0xFFFFu) << 16u; + const int64_t full = static_cast(hi) + lo + pending->symbolValue; + const uint32_t relocatedHi = static_cast((full + 0x8000) >> 16u) & 0xFFFFu; + memory.write32(pending->address, (hiWord & 0xFFFF0000u) | relocatedHi); + pending = hi16.erase(pending); + } + const int64_t full = static_cast(lo) + symbolValue; + memory.write32(place, (word & 0xFFFF0000u) | (static_cast(full) & 0xFFFFu)); + break; + } + case R_MIPS_16: + memory.write32(place, (word & 0xFFFF0000u) | (static_cast(addend + symbolValue) & 0xFFFFu)); + break; + default: + allSupported = false; + break; + } + } + } + return allSupported; + } + } + + bool IopModuleLoader::readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector &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)); + 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 &bytes) + { + Elf32Ehdr header{}; + if (!host.readGuest(guestAddress, &header, sizeof(header)) || !validElfHeader(header)) + return false; + + uint64_t required = sizeof(header); + required = std::max(required, static_cast(header.phoff) + static_cast(header.phentsize) * header.phnum); + required = std::max(required, static_cast(header.shoff) + static_cast(header.shentsize) * header.shnum); + + if (required > kMaxImageSize) + return false; // Should we log an error here? TODO check later + + bytes.resize(static_cast(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(header.shoff) + static_cast(i) * header.shentsize; + std::memcpy(§ion, bytes.data() + offset, sizeof(section)); + if (section.type != SHT_NOBITS) + { + required = std::max(required, static_cast(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(header.phoff) + static_cast(i) * header.phentsize; + std::memcpy(&program, bytes.data() + offset, sizeof(program)); + required = std::max(required, static_cast(program.offset) + program.filesz); + } + } + if (required > kMaxImageSize) + return false; + + bytes.resize(static_cast(required)); + return host.readGuest(guestAddress, bytes.data(), bytes.size()); + } + + IopImageLoadResult IopModuleLoader::load(std::span 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::max(); + uint32_t maxVaddr = 0u; + bool hasLoad = false; + std::vector programHeaders; + if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr) && checkedRange(image.size(), header.phoff, static_cast(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(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 sectionHeaders; + if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr) && checkedRange(image.size(), header.shoff, static_cast(header.shentsize) * header.shnum)) + { + sectionHeaders.reserve(header.shnum); + for (uint16_t i = 0; i < header.shnum; ++i) + { + Elf32Shdr section{}; + std::memcpy(§ion, image.data() + header.shoff + static_cast(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::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(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(static_cast(program.vaddr) + delta); + if (destination >= IopMemory::RamSize || program.memsz > IopMemory::RamSize - destination) + return result; + if (!memory.writeRam(destination, image.data() + program.offset, program.filesz)) + return result; + if (program.memsz > program.filesz && !memory.zeroRam(destination + program.filesz, program.memsz - program.filesz)) + return result; + } + } + else + { + uint32_t sectionCursor = base; + for (auto §ion : sectionHeaders) + { + if ((section.flags & SHF_ALLOC) == 0u || section.size == 0u) + continue; + uint32_t destination = 0u; + if (section.addr != 0u) + { + destination = static_cast(static_cast(section.addr) + delta); + } + else + { + sectionCursor = alignUp(sectionCursor, std::max(section.addralign, 4u)); + destination = sectionCursor; + section.addr = static_cast(static_cast(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(static_cast(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(static_cast(entry) + delta); + result.gp = gp != 0u + ? static_cast(static_cast(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(static_cast(gp) + delta) + : 0u; + break; + } + } + if (result.gp == 0u) + { + for (const auto §ion : sectionHeaders) + { + if (section.type == SHT_MIPS_REGINFO && section.size >= 24u && checkedRange(image.size(), section.offset, 24u)) + { + uint32_t gp = 0u; + std::memcpy(&gp, image.data() + section.offset + 20u, sizeof(gp)); + result.gp = gp != 0u + ? static_cast(static_cast(gp) + delta) + : 0u; + break; + } + } + } + + result.error = IopImageLoadError::None; + return result; + } +} diff --git a/ps2xIOP/src/emulator/iop_module_loader.h b/ps2xIOP/src/emulator/iop_module_loader.h new file mode 100644 index 0000000..6cd9ef6 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_module_loader.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +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 &bytes); + [[nodiscard]] static bool readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector &bytes); + [[nodiscard]] static IopImageLoadResult load(std::span image, IopMemory &memory, uint32_t moduleCursor); + }; +} diff --git a/ps2xIOP/src/emulator/iop_rpc.cpp b/ps2xIOP/src/emulator/iop_rpc.cpp new file mode 100644 index 0000000..b0025c9 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_rpc.cpp @@ -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 +#include +#include +#include + +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 descriptorWords{}; + const size_t descriptorBytes = static_cast(descriptorCount) * kDescriptorSize; + if (!m_memory.readRam(descriptorAddress, descriptorWords.data(), descriptorBytes)) + { + setV0(0u); + return true; + } + + std::array 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(descriptorWords[i * 4u + 2u]); + if (signedSize <= 0) + continue; + + const uint32_t size = static_cast(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 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(std::numeric_limits::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(request.send.size, IopMemory::RamSize - std::min(server.buffer, IopMemory::RamSize)); + if (copySize != 0u) + { + std::vector 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(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(transfer.size, IopMemory::RamSize - destination); + std::vector 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(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; + } +} diff --git a/ps2xIOP/src/emulator/iop_rpc.h b/ps2xIOP/src/emulator/iop_rpc.h new file mode 100644 index 0000000..82fdfc2 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_rpc.h @@ -0,0 +1,68 @@ +#pragma once + +#include "ps2x/iop/iop_types.h" + +#include +#include +#include + +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 m_servers; + uint32_t m_nextDmaId = 1u; + bool m_sifInitialized = false; + }; +} diff --git a/ps2xIOP/src/emulator/iop_sysclib.cpp b/ps2xIOP/src/emulator/iop_sysclib.cpp new file mode 100644 index 0000000..1e0f6f9 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_sysclib.cpp @@ -0,0 +1,336 @@ +#include "iop_sysclib.h" + +#include "iop_cpu.h" +#include "iop_memory.h" + +#include +#include +#include +#include +#include + +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(left) - static_cast(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 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(std::toupper(static_cast(a0)))); + return true; + case 7: + setV0(static_cast(std::tolower(static_cast(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(a1)) + { + setV0(a0 + i); + return true; + } + } + setV0(0); + return true; + case 11: + setV0(static_cast(compare(a0, a1, a2))); + return true; + case 12: + copy(a0, a1, a2); + setV0(a0); + return true; + case 13: + { + std::vector 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(a1)); + setV0(a0); + return true; + case 15: // bcmp + setV0(static_cast(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(i), i < format.size() ? static_cast(format[i]) : 0u); + } + setV0(static_cast(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(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(static_cast(left) - static_cast(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(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(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(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(static_cast(left) - static_cast(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(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(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(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(a2); + const unsigned long parsed = ordinal == 36 + ? static_cast(std::strtol(value.c_str(), &end, base)) + : std::strtoul(value.c_str(), &end, base); + if (a1 != 0u) + { + m_memory.write32(a1, a0 + static_cast(end - value.c_str())); + } + setV0(static_cast(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; + } + } +} diff --git a/ps2xIOP/src/emulator/iop_sysclib.h b/ps2xIOP/src/emulator/iop_sysclib.h new file mode 100644 index 0000000..a462a6e --- /dev/null +++ b/ps2xIOP/src/emulator/iop_sysclib.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +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; + }; +} diff --git a/ps2xIOP/src/iop_service.h b/ps2xIOP/src/iop_service.h index 7e3667a..56ccd89 100644 --- a/ps2xIOP/src/iop_service.h +++ b/ps2xIOP/src/iop_service.h @@ -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) diff --git a/ps2xIOP/src/iop_subsystem.cpp b/ps2xIOP/src/iop_subsystem.cpp index d848f8d..e32fcb8 100644 --- a/ps2xIOP/src/iop_subsystem.cpp +++ b/ps2xIOP/src/iop_subsystem.cpp @@ -1,6 +1,7 @@ #include "ps2x/iop/iop_subsystem.h" #include "iop_service.h" +#include "emulator/iop_emulator.h" #include "plugin_loader.h" #include @@ -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; diff --git a/ps2xIOP/src/module_factories.h b/ps2xIOP/src/module_factories.h index 8e0f422..ce21280 100644 --- a/ps2xIOP/src/module_factories.h +++ b/ps2xIOP/src/module_factories.h @@ -108,6 +108,7 @@ namespace ps2x::iop::detail bool zeroReceiveBuffer = true; bool signalNowaitCompletion = false; bool completeQueuedPlayStreams = false; + bool overridePhysicalServer = false; std::vector suppressedCompletionCallbacks; }; diff --git a/ps2xIOP/src/modules/sound_update_stub.cpp b/ps2xIOP/src/modules/sound_update_stub.cpp index 68c1c8c..4fa055c 100644 --- a/ps2xIOP/src/modules/sound_update_stub.cpp +++ b/ps2xIOP/src/modules/sound_update_stub.cpp @@ -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 lock(m_mutex); diff --git a/ps2xIOP/tests/iop_emulator_tests.cpp b/ps2xIOP/tests/iop_emulator_tests.cpp new file mode 100644 index 0000000..b449715 --- /dev/null +++ b/ps2xIOP/tests/iop_emulator_tests.cpp @@ -0,0 +1,1143 @@ +#include "ps2x/iop/iop_subsystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace ps2x::iop; + + class TestHost final : public IopHost + { + public: + explicit TestHost(size_t guestBytes = 4096u) + : guest(guestBytes, 0u) + { + } + + bool readGuest(uint32_t address, void *destination, size_t size) const override + { + if ((!destination && size != 0u) || address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memcpy(destination, guest.data() + address, size); + return true; + } + + bool writeGuest(uint32_t address, const void *source, size_t size) override + { + if ((!source && size != 0u) || address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memcpy(guest.data() + address, source, size); + return true; + } + + bool zeroGuest(uint32_t address, size_t size) override + { + if (address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memset(guest.data() + address, 0, size); + return true; + } + + bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override + { + normalized = address; + return address <= guest.size(); + } + + uint32_t allocateIopHandle(IopHandleKind) override { return 1u; } + uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; } + void freeGuest(uint32_t) override {} + void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {} + std::string hostPath(HostPathKind kind) const override + { + return kind == HostPathKind::CdRoot ? cdRoot : std::string{}; + } + std::string translateGuestPath(std::string_view path) const override { return std::string(path); } + uint64_t openHostFile(std::string_view) override { return 0u; } + bool hostFileSize(uint64_t, uint64_t &) const override { return false; } + bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; } + void closeHostFile(uint64_t) override {} + int32_t memoryCard(const MemoryCardRequest &) override { return 0; } + bool hasGuestFunction(uint32_t) const override { return false; } + bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; } + void log(LogLevel, std::string_view message) override { logs.emplace_back(message); } + + std::vector guest; + std::vector logs; + std::string cdRoot; + }; + +#pragma pack(push, 1) + struct ElfHeader + { + uint8_t 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 ProgramHeader + { + 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 SectionHeader + { + uint32_t name; + uint32_t type; + uint32_t flags; + uint32_t address; + uint32_t offset; + uint32_t size; + uint32_t link; + uint32_t info; + uint32_t alignment; + uint32_t entrySize; + }; + + struct Relocation + { + uint32_t offset; + uint32_t info; + }; +#pragma pack(pop) + + static_assert(sizeof(ElfHeader) == 52u); + static_assert(sizeof(ProgramHeader) == 32u); + static_assert(sizeof(SectionHeader) == 40u); + static_assert(sizeof(Relocation) == 8u); + + void writeMinimalIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; + header.ident[1] = 'E'; + header.ident[2] = 'L'; + header.ident[3] = 'F'; + header.ident[4] = 1u; + header.ident[5] = 1u; + header.ident[6] = 1u; + header.type = 2u; + header.machine = 8u; + header.version = 1u; + header.entry = loadAddress; + header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); + header.phentsize = sizeof(ProgramHeader); + header.phnum = 1u; + + // Branch is deliberately included so the test checks that its delay slot runs. + const uint32_t code[] = { + 0x24080001u, // addiu t0, zero, 1 + 0x11080002u, // beq t0, t0, +2 + 0x24020007u, // addiu v0, zero, 7 (delay slot) + 0x24020063u, // addiu v0, zero, 99 (must be skipped) + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + + ProgramHeader program{}; + program.type = 1u; + program.offset = codeOffset; + program.vaddr = loadAddress; + program.paddr = loadAddress; + program.filesz = sizeof(code); + program.memsz = sizeof(code); + program.flags = 5u; + program.align = 4u; + + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + } + + void writeRpcServerIrx(TestHost &host, + uint32_t address, + uint32_t sid = 0xF00DCAFEu) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableOffset = 0x60u; + constexpr uint32_t importStubAddress = loadAddress + importTableOffset + 20u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; + header.ident[1] = 'E'; + header.ident[2] = 'L'; + header.ident[3] = 'F'; + header.ident[4] = 1u; + header.ident[5] = 1u; + header.ident[6] = 1u; + header.type = 2u; + header.machine = 8u; + header.version = 1u; + header.entry = loadAddress; + header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); + header.phentsize = sizeof(ProgramHeader); + header.phnum = 1u; + + const uint32_t code[] = { + 0x27BDFFE0u, // addiu sp, sp, -0x20 + 0xAFBF001Cu, // sw ra, 0x1c(sp) + 0x3C040001u, // lui a0, 1 + 0x34840200u, // ori a0, a0, 0x200 (server data) + 0x3C050000u | ((sid >> 16u) & 0xFFFFu), // lui a1, SID upper + 0x34A50000u | (sid & 0xFFFFu), // ori a1, a1, SID lower + 0x3C060001u, // lui a2, 1 + 0x34C60300u, // ori a2, a2, 0x300 (server function) + 0x3C070001u, // lui a3, 1 + 0x34E70400u, // ori a3, a3, 0x400 (server buffer) + 0xAFA00010u, // sw zero, 0x10(sp) + 0xAFA00014u, // sw zero, 0x14(sp) + 0xAFA00018u, // sw zero, 0x18(sp) + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), // jal sceSifRegisterRpc + 0x00000000u, // nop + 0x8FBF001Cu, // lw ra, 0x1c(sp) + 0x00001021u, // addu v0, zero, zero + 0x27BD0020u, // addiu sp, sp, 0x20 + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + const uint32_t importTable[] = { + 0x41E00000u, // IRX import magic + 0x00000000u, + 0x00000101u, + 0x63666973u, // "sifc" + 0x0000646Du, // "md" + 0x03E00008u, // jr ra + 0x24000011u, // addiu zero, zero, 17 (sceSifRegisterRpc) + 0x00000000u, + 0x00000000u, + }; + + ProgramHeader program{}; + program.type = 1u; + program.offset = codeOffset; + program.vaddr = loadAddress; + program.paddr = loadAddress; + program.filesz = 0xA0u; + program.memsz = 0x500u; + program.flags = 7u; + program.align = 4u; + + std::fill(host.guest.begin() + address, host.guest.end(), 0u); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + importTableOffset, + importTable, sizeof(importTable)); + } + + void writeRelocatableRpcServerIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t entryAddress = 0x20u; + constexpr uint32_t importTableAddress = 0x80u; + constexpr uint32_t importStubAddress = importTableAddress + 20u; + constexpr uint32_t handlerAddress = 0xC0u; + constexpr uint32_t gpAddress = 0xD0u; + constexpr uint32_t iopModFileOffset = 0xE0u; + constexpr uint32_t relocationFileOffset = 0x200u; + constexpr uint32_t sectionTableOffset = 0x240u; + constexpr uint32_t rpcSid = 0xA11CE001u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; + header.ident[1] = 'E'; + header.ident[2] = 'L'; + header.ident[3] = 'F'; + header.ident[4] = 1u; + header.ident[5] = 1u; + header.ident[6] = 1u; + header.type = 0xFF80u; // ET_SCE_IOPRELEXEC + header.machine = 8u; + header.version = 1u; + header.entry = entryAddress; + header.phoff = sizeof(ElfHeader); + header.shoff = sectionTableOffset; + header.ehsize = sizeof(ElfHeader); + header.phentsize = sizeof(ProgramHeader); + header.phnum = 2u; + header.shentsize = sizeof(SectionHeader); + header.shnum = 3u; + + const uint32_t code[] = { + 0x27BDFFE0u, // addiu sp, sp, -0x20 + 0xAFBF001Cu, // sw ra, 0x1c(sp) + 0x00002021u, // addu a0, zero, zero + 0x3C05A11Cu, // lui a1, 0xa11c + 0x34A5E001u, // ori a1, a1, 0xe001 + 0x3C060000u, // lui a2, 0 (HI16 handler address) + 0x24C600C0u, // addiu a2, a2, 0xc0 (LO16 handler address) + 0x00003821u, // addu a3, zero, zero + 0xAFA00010u, // sw zero, 0x10(sp) + 0xAFA00014u, // sw zero, 0x14(sp) + 0xAFA00018u, // sw zero, 0x18(sp) + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), // jal sceSifRegisterRpc + 0x00000000u, // nop + 0x8FBF001Cu, // lw ra, 0x1c(sp) + 0x00001021u, // addu v0, zero, zero + 0x27BD0020u, // addiu sp, sp, 0x20 + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + const uint32_t importTable[] = { + 0x41E00000u, + 0x00000000u, + 0x00000101u, + 0x63666973u, // "sifc" + 0x0000646Du, // "md" + 0x03E00008u, + 0x24000011u, // sceSifRegisterRpc + 0x00000000u, + }; + const uint32_t handler[] = { + 0x03E00008u, // jr ra + 0x03801021u, // addu v0, gp, zero + }; + constexpr uint32_t gpData = 0x47505250u; // "GPRP" + const uint32_t iopModuleHeader[] = { + 0u, + entryAddress, + gpAddress, + }; + + ProgramHeader iopModule{}; + iopModule.type = 0x70000080u; // PT_SCE_IOPMOD + iopModule.offset = iopModFileOffset; + iopModule.filesz = sizeof(iopModuleHeader); + iopModule.memsz = sizeof(iopModuleHeader); + iopModule.align = 4u; + + ProgramHeader program{}; + program.type = 1u; + program.offset = codeOffset; + program.vaddr = 0u; + program.paddr = 0u; + program.filesz = gpAddress + sizeof(gpData); + program.memsz = 0x500u; + program.flags = 7u; + program.align = 4u; + + SectionHeader text{}; + text.type = 1u; // SHT_PROGBITS + text.flags = 0x6u; // SHF_ALLOC | SHF_EXECINSTR + text.address = entryAddress; + text.offset = codeOffset + entryAddress; + text.size = 0xB0u; + text.alignment = 4u; + + SectionHeader relocations{}; + relocations.type = 9u; // SHT_REL + relocations.offset = relocationFileOffset; + relocations.size = 3u * sizeof(Relocation); + relocations.info = 1u; // target .text + relocations.alignment = 4u; + relocations.entrySize = sizeof(Relocation); + + const Relocation relocationData[] = { + {entryAddress + 5u * sizeof(uint32_t), 5u}, // symbol 0, R_MIPS_HI16 + {entryAddress + 6u * sizeof(uint32_t), 6u}, // symbol 0, R_MIPS_LO16 + {entryAddress + 11u * sizeof(uint32_t), 4u}, // symbol 0, R_MIPS_26 + }; + + std::fill(host.guest.begin() + address, host.guest.end(), 0u); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &iopModule, sizeof(iopModule)); + std::memcpy(host.guest.data() + address + sizeof(header) + sizeof(iopModule), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + iopModFileOffset, + iopModuleHeader, sizeof(iopModuleHeader)); + std::memcpy(host.guest.data() + address + codeOffset + entryAddress, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + importTableAddress, + importTable, sizeof(importTable)); + std::memcpy(host.guest.data() + address + codeOffset + handlerAddress, + handler, sizeof(handler)); + std::memcpy(host.guest.data() + address + codeOffset + gpAddress, + &gpData, sizeof(gpData)); + std::memcpy(host.guest.data() + address + relocationFileOffset, + relocationData, sizeof(relocationData)); + std::memcpy(host.guest.data() + address + sectionTableOffset + sizeof(SectionHeader), + &text, sizeof(text)); + std::memcpy(host.guest.data() + address + sectionTableOffset + 2u * sizeof(SectionHeader), + &relocations, sizeof(relocations)); + } + + void writeExportProviderIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t targetAddress = loadAddress + 0x60u; + constexpr uint32_t exportTableAddress = loadAddress + 0x80u; + constexpr uint32_t importTableAddress = loadAddress + 0xC0u; + constexpr uint32_t importStubAddress = importTableAddress + 20u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0xF0u; program.memsz = 0xF0u; program.flags = 7u; program.align = 4u; + + const uint32_t code[] = { + 0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040001u, 0x34840080u, + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), 0x00000000u, + 0x8FBF001Cu, 0x00001021u, + 0x27BD0020u, 0x03E00008u, 0x00000000u, + }; + const uint32_t target[] = {0x03E00008u, 0x24020042u}; + const uint32_t exportTable[] = { + 0x41C00000u, 0u, 0x00000101u, + 0x6C747374u, 0x00006269u, // "tstlib" + loadAddress, loadAddress, loadAddress, targetAddress, 0u, + }; + const uint32_t importTable[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64616F6Cu, 0x65726F63u, // "loadcore" + 0x03E00008u, 0x24000006u, 0u, 0u, + }; + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + 0x60u, target, sizeof(target)); + std::memcpy(host.guest.data() + address + codeOffset + 0x80u, exportTable, sizeof(exportTable)); + std::memcpy(host.guest.data() + address + codeOffset + 0xC0u, importTable, sizeof(importTable)); + } + + void writeExportConsumerIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00011000u; + constexpr uint32_t importTableAddress = loadAddress + 0x40u; + constexpr uint32_t importStubAddress = importTableAddress + 20u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x80u; program.memsz = 0x80u; program.flags = 7u; program.align = 4u; + + const uint32_t code[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), 0x00000000u, + 0x8FBF000Cu, 0x27BD0010u, + 0x03E00008u, 0x00000000u, + }; + const uint32_t importTable[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x6C747374u, 0x00006269u, // "tstlib" + 0x03E00008u, 0x24000003u, 0u, 0u, + }; + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + 0x40u, importTable, sizeof(importTable)); + } + + void writeVblankSchedulingIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t highThreadAddress = loadAddress + 0x100u; + constexpr uint32_t lowThreadAddress = loadAddress + 0x140u; + constexpr uint32_t thbaseTableAddress = loadAddress + 0x180u; + constexpr uint32_t createThreadStub = thbaseTableAddress + 20u; + constexpr uint32_t startThreadStub = createThreadStub + 8u; + constexpr uint32_t vblankTableAddress = loadAddress + 0x1C0u; + constexpr uint32_t waitVblankEndStub = vblankTableAddress + 20u; + constexpr uint32_t highThreadDescriptor = loadAddress + 0x300u; + constexpr uint32_t lowThreadDescriptor = loadAddress + 0x320u; + constexpr uint32_t lowThreadMarker = loadAddress + 0x400u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x500u; program.memsz = 0x500u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040001u, 0x34840300u, jal(createThreadStub), 0x00000000u, + 0x00408021u, // move s0, v0 + 0x3C040001u, 0x34840320u, jal(createThreadStub), 0x00000000u, + 0x00408821u, // move s1, v0 + 0x02002021u, 0x00002821u, jal(startThreadStub), 0x00000000u, + 0x02202021u, 0x00002821u, jal(startThreadStub), 0x00000000u, + 0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0x00000000u, + }; + const uint32_t highThread[] = { + jal(waitVblankEndStub), 0x00000000u, + jump(highThreadAddress), 0x00000000u, + }; + const uint32_t lowThread[] = { + 0x3C080001u, 0x35080400u, + 0x24090001u, 0xAD090000u, + 0x03E00008u, 0x00000000u, + }; + const uint32_t thbaseImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x61626874u, 0x00006573u, // "thbase" + 0x03E00008u, 0x24000004u, // CreateThread + 0x03E00008u, 0x24000006u, // StartThread + 0u, 0u, + }; + const uint32_t vblankImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x616C6276u, 0x00006B6Eu, // "vblank" + 0x03E00008u, 0x24000005u, // WaitVblankEnd + 0u, 0u, + }; + const uint32_t highDescriptor[] = {0u, 0u, highThreadAddress, 0x400u, 10u}; + const uint32_t lowDescriptor[] = {0u, 0u, lowThreadAddress, 0x400u, 20u}; + + std::vector segment(program.filesz, 0u); + const auto put = [&](uint32_t offset, const void *data, size_t size) + { + std::memcpy(segment.data() + offset, data, size); + }; + put(0u, entry, sizeof(entry)); + put(0x100u, highThread, sizeof(highThread)); + put(0x140u, lowThread, sizeof(lowThread)); + put(0x180u, thbaseImports, sizeof(thbaseImports)); + put(0x1C0u, vblankImports, sizeof(vblankImports)); + put(0x300u, highDescriptor, sizeof(highDescriptor)); + put(0x320u, lowDescriptor, sizeof(lowDescriptor)); + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeSifDmaIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t setDmaStub = importTableAddress + 20u; + constexpr uint32_t dmaStatStub = setDmaStub + 8u; + constexpr uint32_t descriptorAddress = loadAddress + 0x180u; + constexpr uint32_t payloadAddress = loadAddress + 0x1A0u; + constexpr uint32_t eeDestination = 0xC00u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x200u; program.memsz = 0x200u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x3C040001u, 0x34840180u, 0x24050001u, + jal(setDmaStub), 0x00000000u, + 0x00402021u, // move a0, v0 + jal(dmaStatStub), 0x00000000u, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x6D666973u, 0x00006E61u, // "sifman" + 0x03E00008u, 0x24000007u, // sceSifSetDma + 0x03E00008u, 0x24000008u, // sceSifDmaStat + 0u, 0u, + }; + const uint32_t descriptor[] = { + payloadAddress, eeDestination, sizeof(uint32_t), 0u, + }; + constexpr uint32_t payload = 0x53494621u; // "SIF!" + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + 0x180u, descriptor, sizeof(descriptor)); + std::memcpy(segment.data() + 0x1A0u, &payload, sizeof(payload)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeMcmanRegistrationIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t initAddress = loadAddress + 0x100u; + constexpr uint32_t deinitAddress = loadAddress + 0x120u; + constexpr uint32_t callbackAddress = loadAddress + 0x140u; + constexpr uint32_t deviceAddress = loadAddress + 0x200u; + constexpr uint32_t operationsAddress = loadAddress + 0x220u; + constexpr uint32_t nameAddress = loadAddress + 0x280u; + constexpr uint32_t markerAddress = loadAddress + 0x290u; + constexpr uint32_t secrmanTableAddress = loadAddress + 0x300u; + constexpr uint32_t secrCommandStub = secrmanTableAddress + 20u; + constexpr uint32_t secrDeviceIdStub = secrCommandStub + 8u; + constexpr uint32_t modloadTableAddress = loadAddress + 0x340u; + constexpr uint32_t setKelfCallbackStub = modloadTableAddress + 20u; + constexpr uint32_t iomanTableAddress = loadAddress + 0x380u; + constexpr uint32_t deleteMissingDriverStub = iomanTableAddress + 20u; + constexpr uint32_t addDriverStub = deleteMissingDriverStub + 8u; + constexpr uint32_t deleteDriverStub = addDriverStub + 8u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x400u; program.memsz = 0x400u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + constexpr uint32_t epilogueAddress = loadAddress + 35u * sizeof(uint32_t); + const uint32_t entry[] = { + 0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040001u, 0x34840140u, jal(secrCommandStub), 0x00000000u, + 0x3C040001u, 0x34840140u, jal(secrDeviceIdStub), 0x00000000u, + 0x3C040001u, 0x34840140u, jal(setKelfCallbackStub), 0x00000000u, + 0x3C040001u, 0x34840280u, jal(deleteMissingDriverStub), 0x00000000u, + 0x3C040001u, 0x34840200u, jal(addDriverStub), 0x00000000u, + 0x1440000Bu, 0x00000000u, // bnez v0, failure + 0x3C040001u, 0x34840280u, jal(deleteDriverStub), 0x00000000u, + 0x14400005u, 0x00000000u, // bnez v0, failure + 0x3C080001u, 0x8D020290u, jump(epilogueAddress), 0x00000000u, + 0x2402FFFFu, // failure: return -1 + 0x8FBF001Cu, 0x27BD0020u, 0x03E00008u, 0x00000000u, + }; + const uint32_t init[] = { + 0x3C080001u, 0x35080290u, 0x24090001u, 0xAD090000u, + 0x03E00008u, 0x00001021u, + }; + const uint32_t deinit[] = { + 0x3C080001u, 0x35080290u, 0x8D090000u, 0x00000000u, + 0x25290001u, 0xAD090000u, 0x03E00008u, 0x00001021u, + }; + const uint32_t callback[] = {0x03E00008u, 0x00001021u}; + const uint32_t device[] = {nameAddress, 0u, 0u, 0u, operationsAddress}; + uint32_t operations[17]{}; + operations[0] = initAddress; + operations[1] = deinitAddress; + const char name[] = "mc"; + const uint32_t secrmanImports[] = { + 0x41E00000u, 0u, 0x00000104u, + 0x72636573u, 0x006E616Du, // "secrman" + 0x03E00008u, 0x24000004u, + 0x03E00008u, 0x24000005u, + 0u, 0u, + }; + const uint32_t modloadImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x6C646F6Du, 0x0064616Fu, // "modload" + 0x03E00008u, 0x2400000Du, + 0u, 0u, + }; + const uint32_t iomanImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x616D6F69u, 0x0000006Eu, // "ioman" + 0x03E00008u, 0x24000015u, // DelDrv + 0x03E00008u, 0x24000014u, // AddDrv + 0x03E00008u, 0x24000015u, // DelDrv + 0u, 0u, + }; + + std::vector segment(program.filesz, 0u); + const auto put = [&](uint32_t offset, const void *data, size_t size) + { + std::memcpy(segment.data() + offset, data, size); + }; + put(0x000u, entry, sizeof(entry)); + put(0x100u, init, sizeof(init)); + put(0x120u, deinit, sizeof(deinit)); + put(0x140u, callback, sizeof(callback)); + put(0x200u, device, sizeof(device)); + put(0x220u, operations, sizeof(operations)); + put(0x280u, name, sizeof(name)); + put(0x300u, secrmanImports, sizeof(secrmanImports)); + put(0x340u, modloadImports, sizeof(modloadImports)); + put(0x380u, iomanImports, sizeof(iomanImports)); + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeCdvdLifecycleIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t initStub = importTableAddress + 20u; + constexpr uint32_t callbackStub = initStub + 8u; + constexpr uint32_t epilogueAddress = loadAddress + 26u * sizeof(uint32_t); + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x140u; program.memsz = 0x140u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x00002021u, jal(initStub), 0x00000000u, + 0x24080001u, 0x14480012u, 0x00000000u, + 0x3C041234u, 0x34845678u, jal(callbackStub), 0x00000000u, + 0x1440000Cu, 0x00000000u, + 0x3C0489ABu, 0x3484CDEFu, jal(callbackStub), 0x00000000u, + 0x3C081234u, 0x35085678u, 0x14480004u, 0x00000000u, + 0x00001021u, jump(epilogueAddress), 0x00000000u, + 0x2402FFFFu, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x24000004u, // sceCdInit + 0x03E00008u, 0x24000025u, // sceCdCallback + 0u, 0u, + }; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeCdvdPvdReadIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t readBuffer = loadAddress + 0x800u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t callbackStub = importTableAddress + 20u; + constexpr uint32_t readStub = callbackStub + 8u; + constexpr uint32_t callbackFunction = loadAddress + 0x180u; + constexpr uint32_t epilogueAddress = loadAddress + 26u * sizeof(uint32_t); + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x200u; program.memsz = 0x1000u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x3C040001u, 0x34840180u, jal(callbackStub), 0x00000000u, + 0x24040010u, 0x24050001u, + 0x3C060001u, 0x34C60800u, jal(readStub), 0x00000000u, + 0x1040000Cu, 0x00000000u, + 0x90C80000u, 0x24090001u, 0x15090008u, 0x00000000u, + 0x90C80001u, 0x24090043u, 0x15090004u, 0x00000000u, + 0x00001021u, jump(epilogueAddress), 0x00000000u, + 0x2402FFFFu, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x24000025u, // sceCdCallback + 0x03E00008u, 0x24000006u, // sceCdRead + 0u, 0u, + }; + const uint32_t callback[] = { + 0x3C080001u, 0x350801C0u, 0xAD040000u, + 0x03E00008u, 0x00000000u, + }; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + (callbackFunction - loadAddress), callback, sizeof(callback)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeCdvdSeekIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t initStub = importTableAddress + 20u; + constexpr uint32_t callbackStub = initStub + 8u; + constexpr uint32_t seekStub = callbackStub + 8u; + constexpr uint32_t callbackFunction = loadAddress + 0x180u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x200u; program.memsz = 0x200u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x00002021u, jal(initStub), 0x00000000u, + 0x3C040001u, 0x34840180u, jal(callbackStub), 0x00000000u, + 0x24041234u, jal(seekStub), 0x00000000u, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x24000004u, // sceCdInit + 0x03E00008u, 0x24000025u, // sceCdCallback + 0x03E00008u, 0x24000007u, // sceCdSeek + 0u, 0u, + }; + const uint32_t callback[] = { + 0x3C080001u, 0x350801C0u, 0xAD040000u, + 0x03E00008u, 0x00001021u, + }; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + (callbackFunction - loadAddress), callback, sizeof(callback)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + bool expect(bool value, const char *message) + { + if (!value) + std::cerr << "FAIL: " << message << '\n'; + return value; + } +} + +int main() +{ + TestHost host(0x20000u); + IopSubsystem iop(host); + + writeMinimalIrx(host, 0x100u); + const ModuleLoadResult result = iop.loadModuleBuffer(0x100u); + const DebugSnapshot snapshot = iop.debugSnapshot(); + + if (!expect(result.handled, "Emulator must claim IRX loads")) return 1; + if (!expect(result.moduleId == 1, "First emulated IRX must get module id 1")) return 1; + if (!expect(result.startResult == 7, "R3000A branch delay slot result mismatch")) return 1; + if (!expect(snapshot.emulatorLoadedModules == 1u, "Loaded-module debug count mismatch")) return 1; + if (!expect(snapshot.emulatorInstructions >= 5u, "Instruction counter did not advance")) return 1; + + int32_t stopResult = -1; + if (!expect(iop.stopModule(result.moduleId, &stopResult), "Emulated module stop failed")) return 1; + if (!expect(stopResult == 0, "Emulated module stop result mismatch")) return 1; + if (!expect(iop.debugSnapshot().emulatorLoadedModules == 0u, "Module was not released")) return 1; + + constexpr uint32_t rpcSid = 0xF00DCAFEu; + writeRpcServerIrx(host, 0x100u); + const ModuleLoadResult rpcModule = iop.loadModuleBuffer(0x100u); + if (!expect(rpcModule.handled && rpcModule.startResult == 0, + "Synthetic RPC server IRX did not start")) return 1; + if (!expect(iop.debugSnapshot().emulatorRpcServers == 1u, + "Registered RPC server count mismatch")) return 1; + if (!expect(iop.canBindRpc(rpcSid), + "Emulator did not expose the SID registered by the IRX")) return 1; + + iop.reset(); + if (!expect(!iop.canBindRpc(rpcSid), + "IOP reset did not remove the registered RPC server")) return 1; + + constexpr uint32_t lotrSoundSid = 0x00012345u; + GameIdentity lotrIdentity{}; + lotrIdentity.elfName = "SLUS_205.78"; + std::string profileError; + if (!expect(iop.configure(lotrIdentity, &profileError), + "Could not configure the LotR IOP profile")) return 1; + writeRpcServerIrx(host, 0x100u, lotrSoundSid); + const ModuleLoadResult physicalSoundModule = iop.loadModuleBuffer(0x100u); + if (!expect(physicalSoundModule.handled && physicalSoundModule.startResult == 0, + "Synthetic physical sound RPC server did not start")) return 1; + + constexpr uint32_t soundSendAddress = 0x800u; + constexpr uint32_t soundReceiveAddress = 0x900u; + uint16_t emptySoundCommandCount = 0u; + std::memcpy(host.guest.data() + soundSendAddress, + &emptySoundCommandCount, + sizeof(emptySoundCommandCount)); + std::fill_n(host.guest.data() + soundReceiveAddress, 32u, 0xA5u); + RpcRequest soundRequest{}; + soundRequest.sid = lotrSoundSid; + soundRequest.send = {soundSendAddress, 32u}; + soundRequest.receive = {soundReceiveAddress, 32u}; + const RpcResult soundResult = iop.handleRpc(soundRequest); + uint32_t soundResponseCounter = 0u; + std::memcpy(&soundResponseCounter, + host.guest.data() + soundReceiveAddress + sizeof(uint32_t), + sizeof(soundResponseCounter)); + if (!expect(soundResult.handled, + "LotR sound RPC was not handled")) return 1; + if (!expect(soundResponseCounter == 1u, + "Physical sound server bypassed the LotR compatibility stub")) return 1; + + constexpr uint32_t relocatableRpcSid = 0xA11CE001u; + writeRelocatableRpcServerIrx(host, 0x100u); + const ModuleLoadResult relocatableRpcModule = iop.loadModuleBuffer(0x100u); + if (!expect(relocatableRpcModule.handled && relocatableRpcModule.startResult == 0, + "Relocatable RPC server IRX did not start")) return 1; + if (!expect(iop.canBindRpc(relocatableRpcSid), + "R_MIPS_26 did not relocate a symbol-less IRX import call")) return 1; + + RpcRequest relocatableRequest{}; + relocatableRequest.sid = relocatableRpcSid; + relocatableRequest.receive = {0x800u, sizeof(uint32_t)}; + const uint64_t rpcInstructionsBefore = iop.debugSnapshot().emulatorInstructions; + const RpcResult relocatableRpcResult = iop.handleRpc(relocatableRequest); + const uint64_t rpcInstructions = iop.debugSnapshot().emulatorInstructions - rpcInstructionsBefore; + if (!expect(relocatableRpcResult.handled, + "Relocatable IRX RPC handler was not dispatched")) return 1; + if (!expect(rpcInstructions < 100u, + "R_MIPS_HI16/LO16 did not relocate the IRX RPC handler")) return 1; + uint32_t relocatedGpData = 0u; + std::memcpy(&relocatedGpData, host.guest.data() + relocatableRequest.receive.address, + sizeof(relocatedGpData)); + if (!expect(relocatedGpData == 0x47505250u, + "Physical RPC callback did not inherit the registering module's GP")) return 1; + + iop.reset(); + writeExportProviderIrx(host, 0x100u); + const ModuleLoadResult exportProvider = iop.loadModuleBuffer(0x100u); + if (!expect(exportProvider.handled && exportProvider.startResult == 0, + "Synthetic export provider did not register")) return 1; + writeExportConsumerIrx(host, 0x500u); + const ModuleLoadResult exportConsumer = iop.loadModuleBuffer(0x500u); + if (!expect(exportConsumer.handled && exportConsumer.startResult == 0x42, + "IRX export ordinal was shifted by implicit function slots")) return 1; + + iop.reset(); + constexpr uint32_t eeSource = 0x100u; + constexpr uint32_t iopBuffer = 0x500u; + constexpr uint32_t eeDestination = 0x900u; + const uint32_t transferPayload = 0x53494621u; // "SIF!" + std::memcpy(host.guest.data() + eeSource, &transferPayload, sizeof(transferPayload)); + iop.onSifTransfer({ + SifTransferKind::SetDma, + SifTransferPhase::AfterCopy, + eeSource, + iopBuffer, + sizeof(transferPayload), + }); + std::memset(host.guest.data() + iopBuffer, 0, sizeof(transferPayload)); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + iopBuffer, + eeDestination, + sizeof(transferPayload), + }); + uint32_t stagedIopPayload = 0u; + std::memcpy(&stagedIopPayload, host.guest.data() + iopBuffer, sizeof(stagedIopPayload)); + if (!expect(stagedIopPayload == transferPayload, + "sceSifGetOtherData did not stage physical IOP memory for an EE copy")) return 1; + + iop.reset(); + constexpr uint32_t sifDmaDestination = 0xC00u; + writeSifDmaIrx(host, 0x100u); + const ModuleLoadResult sifDmaModule = iop.loadModuleBuffer(0x100u); + uint32_t sifDmaPayload = 0u; + std::memcpy(&sifDmaPayload, host.guest.data() + sifDmaDestination, + sizeof(sifDmaPayload)); + if (!expect(sifDmaModule.handled && sifDmaModule.startResult == -1, + "sceSifDmaStat did not report the synchronous transfer as complete")) return 1; + if (!expect(sifDmaPayload == 0x53494621u, + "IOP sceSifSetDma did not copy the payload into EE memory")) return 1; + + iop.reset(); + writeVblankSchedulingIrx(host, 0x100u); + const ModuleLoadResult vblankModule = iop.loadModuleBuffer(0x100u); + if (!expect(vblankModule.handled && vblankModule.startResult == 0, + "Synthetic VBlank scheduling IRX did not start")) return 1; + iop.runEeCycles(8u * 4096u); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + 0x00010400u, + 0x1000u, + sizeof(uint32_t), + }); + uint32_t lowPriorityMarker = 0u; + std::memcpy(&lowPriorityMarker, host.guest.data() + 0x00010400u, sizeof(lowPriorityMarker)); + if (!expect(lowPriorityMarker == 1u, + "WaitVblankEnd returned immediately and starved a lower-priority IOP thread")) return 1; + + iop.reset(); + host.logs.clear(); + writeMcmanRegistrationIrx(host, 0x100u); + const ModuleLoadResult mcmanRegistration = iop.loadModuleBuffer(0x100u); + if (mcmanRegistration.startResult != 2) + { + std::cerr << "MCMAN synthetic start result: " << mcmanRegistration.startResult << '\n'; + for (const auto &message : host.logs) + std::cerr << message << '\n'; + } + if (!expect(mcmanRegistration.handled && mcmanRegistration.startResult == 2, + "MCMAN callback registration or IOMAN AddDrv/DelDrv lifecycle failed")) return 1; + const bool emittedUnhandledImport = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) { return message.find("unhandled import") != std::string::npos; }); + if (!expect(!emittedUnhandledImport, + "MCMAN registration still emitted an unhandled IOP import")) return 1; + + iop.reset(); + host.logs.clear(); + writeCdvdLifecycleIrx(host, 0x100u); + const ModuleLoadResult cdvdLifecycle = iop.loadModuleBuffer(0x100u); + if (!expect(cdvdLifecycle.handled && cdvdLifecycle.startResult == 0, + "CDVD init or callback replacement semantics failed")) return 1; + const bool emittedUnhandledCdvdImport = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) { return message.find("unhandled import cdvdman") != std::string::npos; }); + if (!expect(!emittedUnhandledCdvdImport, + "CDVD lifecycle still emitted an unhandled IOP import")) return 1; + + iop.reset(); + writeCdvdLifecycleIrx(host, 0x100u); + const ModuleLoadResult cdvdAfterReset = iop.loadModuleBuffer(0x100u); + if (!expect(cdvdAfterReset.handled && cdvdAfterReset.startResult == 0, + "IOP reset did not clear the CDVD callback")) return 1; + + iop.reset(); + const auto uniqueSuffix = std::chrono::steady_clock::now().time_since_epoch().count(); + const std::filesystem::path virtualCdRoot = + std::filesystem::temp_directory_path() / + ("ps2x-iop-cdvd-" + std::to_string(uniqueSuffix)); + std::error_code cdRootError; + std::filesystem::create_directory(virtualCdRoot, cdRootError); + if (!expect(!cdRootError, "Could not create the virtual-CD test directory")) return 1; + host.cdRoot = virtualCdRoot.string(); + writeCdvdPvdReadIrx(host, 0x100u); + const ModuleLoadResult cdvdPvdRead = iop.loadModuleBuffer(0x100u); + iop.runEeCycles(4096u); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + 0x000101C0u, + 0x1000u, + sizeof(uint32_t), + }); + uint32_t cdvdCallbackReason = 0u; + std::memcpy(&cdvdCallbackReason, host.guest.data() + 0x000101C0u, + sizeof(cdvdCallbackReason)); + host.cdRoot.clear(); + std::filesystem::remove(virtualCdRoot, cdRootError); + if (!expect(cdvdPvdRead.handled && cdvdPvdRead.startResult == 0, + "CDVD did not expose the extracted CD root as an ISO9660 PVD")) return 1; + if (!expect(cdvdCallbackReason == 1u, + "CDVD read completion did not invoke the registered guest callback")) return 1; + + iop.reset(); + host.logs.clear(); + writeCdvdSeekIrx(host, 0x100u); + const ModuleLoadResult cdvdSeek = iop.loadModuleBuffer(0x100u); + iop.runEeCycles(4096u); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + 0x000101C0u, + 0x1100u, + sizeof(uint32_t), + }); + uint32_t cdvdSeekCallbackReason = 0u; + std::memcpy(&cdvdSeekCallbackReason, host.guest.data() + 0x000101C0u, + sizeof(cdvdSeekCallbackReason)); + if (!expect(cdvdSeek.handled && cdvdSeek.startResult == 1, + "sceCdSeek did not accept a valid LBN")) return 1; + if (!expect(cdvdSeekCallbackReason == 4u, + "sceCdSeek completion did not invoke the registered callback")) return 1; + const bool emittedUnhandledSeek = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) + { return message.find("unhandled import cdvdman:7") != std::string::npos; }); + if (!expect(!emittedUnhandledSeek, + "sceCdSeek still emitted an unhandled IOP import")) return 1; + + std::cout << "ps2xIOP emulator smoke tests passed\n"; + return 0; +} diff --git a/ps2xRecomp/include/ps2recomp/instructions.h b/ps2xRecomp/include/ps2recomp/instructions.h index bf138a6..1980a6f 100644 --- a/ps2xRecomp/include/ps2recomp/instructions.h +++ b/ps2xRecomp/include/ps2recomp/instructions.h @@ -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 { diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index ec21986..dd62275 100644 --- a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h +++ b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h @@ -42,9 +42,12 @@ namespace ps2recomp std::vector &functions, std::unordered_map> &decodedFunctions, const std::vector
§ions); - static size_t ResliceEntryFunctions( - std::vector &functions, - std::unordered_map> &decodedFunctions); + static size_t ResliceEntryFunctions(std::vector &functions, std::unordered_map> &decodedFunctions); + static size_t CollectInternalEntryTargets( + const std::vector &functions, + const std::unordered_map> &decodedFunctions, + const std::unordered_set &entryAddresses, + std::unordered_map> &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 m_stubFunctions; std::unordered_set m_stubFunctionStarts; std::unordered_map m_stubHandlerBindingsByStart; + std::unordered_set m_entryPointHintStarts; std::unordered_set m_correctnessCriticalFunctionStarts; std::map m_generatedStubs; std::unordered_map m_functionRenames; diff --git a/ps2xRecomp/include/ps2recomp/types.h b/ps2xRecomp/include/ps2recomp/types.h index 39adf9a..9bd687d 100644 --- a/ps2xRecomp/include/ps2recomp/types.h +++ b/ps2xRecomp/include/ps2recomp/types.h @@ -187,6 +187,7 @@ namespace ps2recomp std::vector skipFunctions; std::unordered_map patches; std::vector stubImplementations; + std::vector entryPointHints; std::unordered_map mmioByInstructionAddress; std::vector jumpTables; }; diff --git a/ps2xRecomp/src/lib/config_manager.cpp b/ps2xRecomp/src/lib/config_manager.cpp index b6f382d..97dc2dd 100644 --- a/ps2xRecomp/src/lib/config_manager.cpp +++ b/ps2xRecomp/src/lib/config_manager.cpp @@ -74,6 +74,27 @@ namespace ps2recomp config.stubImplementations = toml::find>(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>(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>(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()) diff --git a/ps2xRecomp/src/lib/control_flow_analyzer.cpp b/ps2xRecomp/src/lib/control_flow_analyzer.cpp index a099f5c..a50cb12 100644 --- a/ps2xRecomp/src/lib/control_flow_analyzer.cpp +++ b/ps2xRecomp/src/lib/control_flow_analyzer.cpp @@ -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) { diff --git a/ps2xRecomp/src/lib/elf_parser.cpp b/ps2xRecomp/src/lib/elf_parser.cpp index 91e43ee..1d09100 100644 --- a/ps2xRecomp/src/lib/elf_parser.cpp +++ b/ps2xRecomp/src/lib/elf_parser.cpp @@ -1,4 +1,5 @@ #include "ps2recomp/elf_parser.h" +#include "ps2recomp/instructions.h" #include "ps2recomp/recompiler_reporter.h" #include "ps2recomp/types.h" #include @@ -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 &outFunctions) + bool ReadSectionWord(const ps2recomp::Section §ion, uint32_t offset, uint32_t &outWord) + { + if (!section.data || offset > section.size || section.size - offset < sizeof(uint32_t)) + { + return false; + } + + std::memcpy(&outWord, section.data + offset, sizeof(uint32_t)); + return true; + } + + bool LooksLikeCallableEntry(const std::vector §ions, uint32_t address, bool allowLeafThunk) + { + if ((address % MIPS_INSTRUCTION_SIZE) != 0) + { + return false; + } + + const ps2recomp::Section *section = FindCodeSectionByAddress(sections, address); + if (!section || !section->data) + { + return false; + } + + const uint32_t startOffset = address - section->address; + constexpr uint32_t kProbeWords = 8; + + for (uint32_t index = 0; index < kProbeWords; ++index) + { + uint32_t raw = 0; + if (!ReadSectionWord(*section, startOffset + (index * MIPS_INSTRUCTION_SIZE), raw)) + { + break; + } + + const uint32_t opcode = OPCODE(raw); + const uint32_t rs = RS(raw); + const uint32_t rt = RT(raw); + const uint16_t immediate = static_cast(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 §ions, + std::unordered_set &starts) + { + constexpr uint32_t kMaxLookaheadWords = 4; + + for (const auto §ion : sections) + { + if (!section.isCode || !section.data || section.size < (2u * MIPS_INSTRUCTION_SIZE)) + { + continue; + } + + for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size; + offset += MIPS_INSTRUCTION_SIZE) + { + uint32_t upperRaw = 0; + if (!ReadSectionWord(section, offset, upperRaw) || + OPCODE(upperRaw) != OPCODE_LUI) + { + continue; + } + + const uint32_t upperReg = RT(upperRaw); + if (upperReg == GPR_ZERO) + { + continue; + } + + const uint32_t upperValue = IMMEDIATE(upperRaw) << 16; + bool sawControlTransfer = false; + bool sawCallTransfer = false; + + for (uint32_t lookahead = 1; lookahead <= kMaxLookaheadWords; ++lookahead) + { + uint32_t lowRaw = 0; + if (!ReadSectionWord(section, offset + (lookahead * MIPS_INSTRUCTION_SIZE), lowRaw)) + { + break; + } + + const uint32_t opcode = OPCODE(lowRaw); + const uint32_t rs = RS(lowRaw); + const uint32_t rt = RT(lowRaw); + + if ((opcode == OPCODE_ADDIU || opcode == OPCODE_ORI || opcode == OPCODE_DADDIU) && + rs == upperReg) + { + const uint16_t immediate = static_cast(IMMEDIATE(lowRaw)); + uint32_t target = 0; + if (opcode == OPCODE_ORI) + { + target = upperValue | static_cast(immediate); + } + else // ADDIU/DADDIU use a signed low half + { + target = upperValue + static_cast( + static_cast(static_cast(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 §ions, + std::unordered_set &starts) + { + struct PointerCandidate + { + uint32_t sourceOffset; + uint32_t target; + }; + + constexpr uint32_t kClusterDistanceBytes = 32; + + for (const auto §ion : sections) + { + if (!section.isData || section.isCode || section.isBSS || + !section.data || section.size < MIPS_INSTRUCTION_SIZE) + { + continue; + } + + std::vector 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 &outFunctions) { std::unordered_set starts; starts.reserve(4096); @@ -467,26 +791,29 @@ namespace const auto §ions = parser->getSections(); for (const auto §ion : sections) { - if (!section.isCode || !section.data || section.size < 4) + if (!section.isCode || !section.data || section.size < MIPS_INSTRUCTION_SIZE) { continue; } - for (uint32_t offset = 0; offset + 4 <= section.size; offset += 4) + for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size; + offset += MIPS_INSTRUCTION_SIZE) { const uint32_t pc = section.address + offset; uint32_t raw = 0; std::memcpy(&raw, section.data + offset, sizeof(uint32_t)); - const uint32_t op = (raw >> 26) & 0x3F; - if (op != 0x03) // JAL + const uint32_t op = OPCODE(raw); + if (op != OPCODE_JAL) { continue; } - const uint32_t index = raw & 0x03FFFFFF; - const uint32_t target = ((pc + 4) & 0xF0000000u) | (index << 2); + const uint32_t index = TARGET(raw); + const uint32_t target = + ((pc + MIPS_INSTRUCTION_SIZE) & MIPS_JUMP_REGION_MASK) | + (index << MIPS_JUMP_TARGET_SHIFT); if (FindCodeSectionByAddress(sections, target)) { @@ -494,6 +821,9 @@ namespace } } } + + ScanMaterializedCodeAddresses(sections, starts); + ScanDataFunctionPointerTables(sections, starts); std::vector 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(), diff --git a/ps2xRecomp/src/lib/function_emitter.cpp b/ps2xRecomp/src/lib/function_emitter.cpp index 328c0e0..3192e90 100644 --- a/ps2xRecomp/src/lib/function_emitter.cpp +++ b/ps2xRecomp/src/lib/function_emitter.cpp @@ -45,8 +45,8 @@ namespace ps2recomp ss << "#include \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 \n"; + ss << "#include \n\n"; ss << "#include \"ps2_syscalls.h\"\n"; ss << "#include \"ps2_stubs.h\"\n\n"; ss << "#ifdef PS2_FUNCTION_LOG_TRACKER\n"; diff --git a/ps2xRecomp/src/lib/ps2_recompiler.cpp b/ps2xRecomp/src/lib/ps2_recompiler.cpp index ab779a4..0547eef 100644 --- a/ps2xRecomp/src/lib/ps2_recompiler.cpp +++ b/ps2xRecomp/src/lib/ps2_recompiler.cpp @@ -102,10 +102,10 @@ namespace ps2recomp void writeCombinedOutputPreamble(std::ostream &output) { output << "#include \n"; - output << "#include \"ps2_recompiled_functions.h\"\n\n"; + output << "#include \n\n"; output << "#include \"ps2_runtime_macros.h\"\n"; output << "#include \"ps2_runtime.h\"\n"; - output << "#include \"ps2_recompiled_stubs.h\"\n"; + output << "#include \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 &functions, + const std::unordered_map> &decodedFunctions, + const std::unordered_set &entryAddresses, + std::unordered_map> &targetsByOwner) + { + std::unordered_set 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(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 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 &functions, - std::unordered_map> &decodedFunctions) + size_t PS2Recompiler::ResliceEntryFunctions(std::vector &functions, std::unordered_map> &decodedFunctions) { return resliceEntryFunctionsImpl(functions, decodedFunctions); } + size_t PS2Recompiler::CollectInternalEntryTargets( + const std::vector &functions, + const std::unordered_map> &decodedFunctions, + const std::unordered_set &entryAddresses, + std::unordered_map> &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); } diff --git a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java index f84e8a6..00860e3 100644 --- a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java +++ b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java @@ -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 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 records, Set 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 records, Set 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 records, Set 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 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 collectExecutableLabelRecords(List 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; } diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index a899408..2bfc86f 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #if defined(_MSC_VER) #include @@ -288,6 +289,9 @@ public: void run(); void setIopPluginSearchPaths(std::vector 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; diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index 3a6ec7d..04efaa5 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -391,6 +391,7 @@ void EeScheduler::accountCycles(uint32_t cycles) noexcept const uint64_t elapsed = std::max(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(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; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp index 6a07fe2..77c2bfd 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp @@ -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); } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h index d9da8ee..5498e51 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h @@ -46,6 +46,36 @@ namespace return hash; } + bool copyGuestBytesBounded(const uint8_t *rdram, + uint32_t guestAddr, + uint32_t byteCount, + uint32_t maxBytes, + std::vector &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 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) diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp index f5dfcc5..3a90547 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp @@ -155,20 +155,22 @@ namespace ps2_syscalls const int32_t moduleId = static_cast(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(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 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(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 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) { diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp index 3530e65..3b9bb6f 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp @@ -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 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(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) { diff --git a/ps2xRuntime/src/lib/ps2_iop_transport.h b/ps2xRuntime/src/lib/ps2_iop_transport.h index 988d6b2..4c9d109 100644 --- a/ps2xRuntime/src/lib/ps2_iop_transport.h +++ b/ps2xRuntime/src/lib/ps2_iop_transport.h @@ -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, diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index ddacb0c..4fd4bab 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -481,6 +480,7 @@ PS2Runtime::PS2Runtime() { m_iopHost = std::make_unique(*this); m_iopSubsystem = std::make_unique(*m_iopHost); + m_eeScheduler = std::make_unique(*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 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(); diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 2dd9c53..f767849 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -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 ") != std::string::npos, + "function sources must resolve declarations through the configured include path"); + t.IsTrue(generated.find("#include ") != std::string::npos, + "function sources must resolve stub declarations through the configured include path"); + t.IsTrue(registration.find("#include ") != std::string::npos, + "the registration source must use the same unambiguous declaration header"); + t.IsTrue(registration.find("#include ") != 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(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 instructions{ + makeNop(0x7000), makeNop(0x7004), makeNop(0x7008), makeNop(0x700C), + makeNop(0x7010), makeNop(0x7014), makeNop(0x7018), makeNop(0x701C)}; + std::vector functions{owner}; + std::unordered_map> decoded{{owner.start, instructions}}; + std::unordered_map> 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"; diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index 4dce622..9b5ec0a 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -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 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(textWords.data()), + static_cast(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 tableWords{}; + tableWords[1] = 0x00100060u; + tableWords[3] = 0x00100068u; + tableWords[16] = 0x00100070u; // plausible entry, but not part of a pointer cluster + rodata->set_data(reinterpret_cast(tableWords.data()), + static_cast(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(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(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(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(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(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"); diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index ccf4b70..5defc89 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -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 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; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index eefb495..b4fd898 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -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(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(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);