diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt index 2c5442d..5fbbc45 100644 --- a/ps2xIOP/CMakeLists.txt +++ b/ps2xIOP/CMakeLists.txt @@ -6,16 +6,26 @@ 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/ps2_path.cpp + src/iop_module_manager.cpp 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/emulator/core/iop_cpu.cpp + src/emulator/core/iop_kernel.cpp + src/emulator/core/iop_memory.cpp + src/emulator/services/iop_module_loader.cpp + src/emulator/services/iop_rpc.cpp + src/emulator/imports/iop_cdvd.cpp + src/emulator/imports/iop_heaplib.cpp + src/emulator/imports/iop_imports.cpp + src/emulator/imports/iop_intrman.cpp + src/emulator/imports/iop_ioman.cpp + src/emulator/imports/iop_loadcore.cpp + src/emulator/imports/iop_stdio.cpp + src/emulator/imports/iop_sysclib.cpp + src/emulator/imports/iop_sysmem.cpp + src/emulator/imports/iop_timrman.cpp + src/emulator/imports/iop_vblank.cpp src/builtin_profiles.cpp src/plugin_loader.cpp src/modules/dbcman.cpp @@ -53,6 +63,11 @@ if(PS2X_IOP_BUILD_TESTS) 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) + + add_executable(ps2_iop_import_tests tests/iop_import_tests.cpp) + target_link_libraries(ps2_iop_import_tests PRIVATE ps2_iop) + target_include_directories(ps2_iop_import_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + add_test(NAME ps2_iop_import_tests COMMAND ps2_iop_import_tests) endif() install(TARGETS ps2_iop diff --git a/ps2xIOP/include/ps2x/iop/iop_host.h b/ps2xIOP/include/ps2x/iop/iop_host.h index 1c872ef..1eceb08 100644 --- a/ps2xIOP/include/ps2x/iop/iop_host.h +++ b/ps2xIOP/include/ps2x/iop/iop_host.h @@ -62,6 +62,34 @@ namespace ps2x::iop virtual bool writeGuest(uint32_t address, const void *source, size_t size) = 0; virtual bool zeroGuest(uint32_t address, size_t size) = 0; virtual bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const = 0; + + // IOP RAM is a distinct address space from the EE guest. TODO remove this later + virtual bool readIopMemory(uint32_t address, void *destination, size_t size) const + { + (void)address; + (void)destination; + (void)size; + return false; + } + virtual bool writeIopMemory(uint32_t address, const void *source, size_t size) + { + (void)address; + (void)source; + (void)size; + return false; + } + virtual bool zeroIopMemory(uint32_t address, size_t size) + { + (void)address; + (void)size; + return false; + } + virtual bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const + { + (void)address; + normalized = 0u; + return false; + } virtual uint32_t allocateIopHandle(IopHandleKind kind) = 0; virtual uint32_t allocateGuest(uint32_t size, uint32_t alignment) = 0; virtual void freeGuest(uint32_t address) = 0; @@ -90,6 +118,18 @@ namespace ps2x::iop uint32_t a3, uint32_t *resultAddress) = 0; + // Deliver an IOP -> EE SIF command packet. The default keeps hosts + // which do not emulate the EE command dispatcher source-compatible. + virtual bool sendSifCommand(uint32_t commandId, + const void *packet, + size_t packetSize) + { + (void)commandId; + (void)packet; + (void)packetSize; + return false; + } + virtual void log(LogLevel level, std::string_view message) = 0; }; } diff --git a/ps2xIOP/include/ps2x/iop/iop_subsystem.h b/ps2xIOP/include/ps2x/iop/iop_subsystem.h index 8ac8f69..284be55 100644 --- a/ps2xIOP/include/ps2x/iop/iop_subsystem.h +++ b/ps2xIOP/include/ps2x/iop/iop_subsystem.h @@ -38,6 +38,14 @@ namespace ps2x::iop [[nodiscard]] RpcResult handleRpc(const RpcRequest &request); void onSifTransfer(const SifTransfer &transfer); + // Physical IOP RAM access shared by the emulator, SIF DMA, and HLE services. Addresses are IOP addresses. + [[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u); + [[nodiscard]] bool freeMemory(uint32_t address); + [[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const; + [[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size); + [[nodiscard]] bool zeroMemory(uint32_t address, size_t size); + [[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const; + [[nodiscard]] DebugSnapshot debugSnapshot() const; private: diff --git a/ps2xIOP/include/ps2x/iop/iop_types.h b/ps2xIOP/include/ps2x/iop/iop_types.h index 4a81203..7529a43 100644 --- a/ps2xIOP/include/ps2x/iop/iop_types.h +++ b/ps2xIOP/include/ps2x/iop/iop_types.h @@ -139,6 +139,7 @@ namespace ps2x::iop std::string name; std::vector sids; bool profileSpecific = false; + bool active = true; std::vector metrics; }; diff --git a/ps2xIOP/include/ps2x/iop/ps2_path.h b/ps2xIOP/include/ps2x/iop/ps2_path.h new file mode 100644 index 0000000..f9e02dc --- /dev/null +++ b/ps2xIOP/include/ps2x/iop/ps2_path.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace ps2x::iop +{ + enum class Ps2PathDevice + { + Invalid, + Host, + Cdrom, + MemoryCard0, + Rom0, + NativeHost, + }; + + struct ParsedPs2Path + { + Ps2PathDevice device = Ps2PathDevice::Invalid; + std::string deviceName; + std::string path; + + [[nodiscard]] explicit operator bool() const noexcept + { + return device != Ps2PathDevice::Invalid; + } + }; + + [[nodiscard]] ParsedPs2Path parsePs2Path(std::string_view path); + + // Returns a lower-case module/file leaf without an optional .irx suffix. + [[nodiscard]] std::string ps2PathLeafKey(const ParsedPs2Path &path); + [[nodiscard]] std::string ps2PathLeafKey(std::string_view path); +} diff --git a/ps2xIOP/src/emulator/iop_cpu.cpp b/ps2xIOP/src/emulator/core/iop_cpu.cpp similarity index 100% rename from ps2xIOP/src/emulator/iop_cpu.cpp rename to ps2xIOP/src/emulator/core/iop_cpu.cpp diff --git a/ps2xIOP/src/emulator/iop_cpu.h b/ps2xIOP/src/emulator/core/iop_cpu.h similarity index 100% rename from ps2xIOP/src/emulator/iop_cpu.h rename to ps2xIOP/src/emulator/core/iop_cpu.h diff --git a/ps2xIOP/src/emulator/iop_kernel.cpp b/ps2xIOP/src/emulator/core/iop_kernel.cpp similarity index 95% rename from ps2xIOP/src/emulator/iop_kernel.cpp rename to ps2xIOP/src/emulator/core/iop_kernel.cpp index cc9cdc0..4832d8f 100644 --- a/ps2xIOP/src/emulator/iop_kernel.cpp +++ b/ps2xIOP/src/emulator/core/iop_kernel.cpp @@ -1,7 +1,7 @@ #include "iop_kernel.h" #include "iop_memory.h" -#include "iop_emulator_const.h" +#include "../iop_emulator_const.h" #include @@ -502,6 +502,28 @@ namespace ps2x::iop::detail } } + int IopKernel::createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits) + { + EventFlag event; + event.id = static_cast(m_nextEventFlagId++); + event.attr = attr; + event.option = option; + event.bits = bits; + const int id = event.id; + m_eventFlags.emplace(id, event); + return id; + } + + bool IopKernel::setInternalEventFlag(int id, uint32_t bits) + { + const auto event = m_eventFlags.find(id); + if (event == m_eventFlags.end()) + return false; + event->second.bits |= bits; + wakeEventWaiters(event->second); + return true; + } + bool IopKernel::dispatchEventImport(uint16_t ordinal, IopCpuState &cpu) { const auto setV0 = [&](int32_t value) @@ -514,13 +536,9 @@ namespace ps2x::iop::detail 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); + setV0(createInternalEventFlag(m_memory.read32(descriptor + 0u), + m_memory.read32(descriptor + 4u), + m_memory.read32(descriptor + 8u))); return true; } case 5: @@ -546,14 +564,11 @@ namespace ps2x::iop::detail case 6: case 7: { - const auto event = m_eventFlags.find(static_cast(cpu.gpr[4])); - if (event == m_eventFlags.end()) + if (!setInternalEventFlag(static_cast(cpu.gpr[4]), cpu.gpr[5])) { setV0(-1); return true; } - event->second.bits |= cpu.gpr[5]; - wakeEventWaiters(event->second); setV0(0); return true; } diff --git a/ps2xIOP/src/emulator/iop_kernel.h b/ps2xIOP/src/emulator/core/iop_kernel.h similarity index 94% rename from ps2xIOP/src/emulator/iop_kernel.h rename to ps2xIOP/src/emulator/core/iop_kernel.h index 890549e..4977102 100644 --- a/ps2xIOP/src/emulator/iop_kernel.h +++ b/ps2xIOP/src/emulator/core/iop_kernel.h @@ -54,6 +54,9 @@ namespace ps2x::iop::detail [[nodiscard]] bool dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu); [[nodiscard]] bool dispatchEventImport(uint16_t ordinal, IopCpuState &cpu); + [[nodiscard]] int createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits); + [[nodiscard]] bool setInternalEventFlag(int id, uint32_t bits); + void sleepCurrent(IopCpuState &cpu); void delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu); diff --git a/ps2xIOP/src/emulator/iop_memory.cpp b/ps2xIOP/src/emulator/core/iop_memory.cpp similarity index 99% rename from ps2xIOP/src/emulator/iop_memory.cpp rename to ps2xIOP/src/emulator/core/iop_memory.cpp index 11ea156..25e9b36 100644 --- a/ps2xIOP/src/emulator/iop_memory.cpp +++ b/ps2xIOP/src/emulator/core/iop_memory.cpp @@ -84,6 +84,7 @@ namespace ps2x::iop::detail } 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) | diff --git a/ps2xIOP/src/emulator/iop_memory.h b/ps2xIOP/src/emulator/core/iop_memory.h similarity index 100% rename from ps2xIOP/src/emulator/iop_memory.h rename to ps2xIOP/src/emulator/core/iop_memory.h diff --git a/ps2xIOP/src/emulator/iop_cdvd.cpp b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp similarity index 75% rename from ps2xIOP/src/emulator/iop_cdvd.cpp rename to ps2xIOP/src/emulator/imports/iop_cdvd.cpp index 58934c0..ad58fb4 100644 --- a/ps2xIOP/src/emulator/iop_cdvd.cpp +++ b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp @@ -1,8 +1,10 @@ #include "iop_cdvd.h" -#include "iop_cpu.h" -#include "iop_memory.h" +#include "../core/iop_cpu.h" +#include "../core/iop_kernel.h" +#include "../core/iop_memory.h" #include "ps2x/iop/iop_host.h" +#include "ps2x/iop/ps2_path.h" #include #include @@ -32,6 +34,11 @@ namespace ps2x::iop::detail constexpr uint32_t kCdvdInitExit = 5u; constexpr uint32_t kCdvdCallbackRead = 1u; constexpr uint32_t kCdvdCallbackSeek = 4u; + constexpr uint32_t kCdvdInterruptReadyBits = 0x29u; + constexpr uint32_t kEventFlagMulti = 2u; + constexpr uint32_t kCdvdStreamTimeout = 5000u; + constexpr uint32_t kCdvdSyncTimeout = 15000u; + constexpr uint32_t kCdvdmanVersion = 0x0226u; uint32_t alignSectors(uint64_t bytes) { @@ -109,6 +116,23 @@ namespace ps2x::iop::detail return static_cast(std::max(kSectorSize, alignSectors(cursor) * kSectorSize)); } + std::string normalizedIsoComponent(std::string_view value) + { + std::string result(value); + const size_t semicolon = result.rfind(';'); + if (semicolon != std::string::npos && semicolon + 1u < result.size() && + std::all_of(result.begin() + static_cast(semicolon + 1u), result.end(), + [](unsigned char ch) + { return std::isdigit(ch) != 0; })) + { + result.resize(semicolon); + } + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char ch) + { return static_cast(std::toupper(ch)); }); + return result; + } + size_t writeDirectoryRecord(uint8_t *destination, uint32_t lsn, uint32_t size, @@ -151,8 +175,8 @@ namespace ps2x::iop::detail std::vector children; }; - Impl(IopHost &hostRef, IopMemory &memoryRef) - : host(hostRef), memory(memoryRef) + Impl(IopHost &hostRef, IopMemory &memoryRef, IopKernel &kernelRef) + : host(hostRef), memory(memoryRef), kernel(kernelRef) { } @@ -169,6 +193,9 @@ namespace ps2x::iop::detail mediaMode = 0u; currentLsn = 0u; lastError = kCdvdErrorNone; + streamFlag = 0u; + lastReadTimeout = 0u; + interruptEventFlagId = 0; virtualIsoBuilt = false; virtualIsoValid = false; completionCallback.reset(); @@ -203,6 +230,7 @@ namespace ps2x::iop::detail case 6: // sceCdRead if (readSectors(a0, a1, a2)) { + signalCommandComplete(); if (callback.address != 0u) { completionCallback = CompletionCallback{ @@ -220,6 +248,7 @@ namespace ps2x::iop::detail case 7: // sceCdSeek currentLsn = a0; lastError = kCdvdErrorNone; + signalCommandComplete(); if (callback.address != 0u) { completionCallback = CompletionCallback{ @@ -235,6 +264,10 @@ namespace ps2x::iop::detail cpu.gpr[2] = lastError; return true; + case 10: // sceCdSearchFile + cpu.gpr[2] = searchFile(a0, a1) ? 1u : 0u; + return true; + case 11: // sceCdSync cpu.gpr[2] = 0u; return true; @@ -259,6 +292,52 @@ namespace ps2x::iop::detail return true; } + case 50: // sceCdSC + { + const int32_t code = static_cast(a0); + switch (code) + { + case -23: // Translate a logical sector for a dual-layer disc. + // The host image exposes one continuous LSN space, so no layer offset is required. + cpu.gpr[2] = a1 != 0u ? memory.read32(a1) : 0u; + return true; + case -18: + lastReadTimeout = a1 != 0u ? memory.read32(a1) : 0u; + cpu.gpr[2] = 0u; + return true; + case -17: + cpu.gpr[2] = kCdvdStreamTimeout; + return true; + case -15: + cpu.gpr[2] = kCdvdSyncTimeout; + return true; + case -11: + cpu.gpr[2] = static_cast(ensureInterruptEventFlag()); + return true; + case -9: + cpu.gpr[2] = kCdvdmanVersion; + return true; + case -2: + lastError = a1 != 0u ? memory.read8(a1) : kCdvdErrorNone; + cpu.gpr[2] = lastError; + return true; + case -1: + case 0: + case 1: + case 2: + if (a1 != 0u) + memory.write32(a1, lastError & 0xFFu); + if (code != -1) + streamFlag = static_cast(code); + cpu.gpr[2] = streamFlag; + return true; + default: + // sceCdSC is intentionally extensible; unsupported controls are no-ops in cdvdman. + cpu.gpr[2] = 0u; + return true; + } + } + case 75: // sceCdMmode mediaMode = a0; cpu.gpr[2] = 1u; @@ -277,6 +356,22 @@ namespace ps2x::iop::detail } private: + int ensureInterruptEventFlag() + { + if (interruptEventFlagId == 0) + { + interruptEventFlagId = kernel.createInternalEventFlag( + kEventFlagMulti, 0u, kCdvdInterruptReadyBits); + } + return interruptEventFlagId; + } + + void signalCommandComplete() + { + if (interruptEventFlagId != 0) + (void)kernel.setInternalEventFlag(interruptEventFlagId, kCdvdInterruptReadyBits); + } + void closeFiles() { if (imageHandle != 0u) @@ -463,6 +558,62 @@ namespace ps2x::iop::detail return true; } + IsoNode *findVirtualIsoNode(std::string_view guestPath) + { + if (!buildVirtualIso()) + return nullptr; + + const ParsedPs2Path parsed = parsePs2Path(guestPath); + if (!parsed || parsed.device != Ps2PathDevice::Cdrom) + return nullptr; + + size_t current = 0u; + size_t begin = 0u; + while (begin <= parsed.path.size()) + { + const size_t end = parsed.path.find('/', begin); + const size_t length = (end == std::string::npos) ? parsed.path.size() - begin : end - begin; + const std::string_view component(parsed.path.data() + begin, length); + begin = (end == std::string::npos) ? parsed.path.size() + 1u : end + 1u; + + if (component.empty() || component == ".") + continue; + if (component == "..") + return nullptr; + + const std::string wanted = normalizedIsoComponent(component); + const auto child = std::find_if(nodes[current].children.begin(), nodes[current].children.end(), + [&](size_t childIndex) + { + return normalizedIsoComponent(nodes[childIndex].identifier) == wanted; + }); + if (child == nodes[current].children.end()) + return nullptr; + current = *child; + } + return &nodes[current]; + } + + bool searchFile(uint32_t resultAddress, uint32_t nameAddress) + { + if (resultAddress == 0u || nameAddress == 0u) + return false; + + const std::string guestPath = memory.readString(nameAddress, 1024u); + IsoNode *node = findVirtualIsoNode(guestPath); + if (!node) + return false; + + // sceCdlFILE: lsn, size, name[16], date/flags[8]. + std::array result{}; + writeLe32(result.data(), node->lsn); + writeLe32(result.data() + 4u, node->size); + const std::string leaf = normalizedIsoComponent(node->identifier); + std::memcpy(result.data() + 8u, leaf.data(), std::min(16u, leaf.size())); + result[24u] = node->directory ? 2u : 0u; + return memory.writeRam(resultAddress, result.data(), result.size()); + } + IsoNode *fileForSector(uint32_t lsn) { for (IsoNode &node : nodes) @@ -558,12 +709,16 @@ namespace ps2x::iop::detail IopHost &host; IopMemory &memory; + IopKernel &kernel; Callback callback; std::optional completionCallback; bool initialized = false; uint32_t mediaMode = 0u; uint32_t currentLsn = 0u; uint32_t lastError = kCdvdErrorNone; + uint32_t streamFlag = 0u; + uint32_t lastReadTimeout = 0u; + int interruptEventFlagId = 0; uint64_t imageHandle = 0u; bool virtualIsoBuilt = false; bool virtualIsoValid = false; @@ -572,8 +727,8 @@ namespace ps2x::iop::detail std::unordered_map> metadataSectors; }; - IopCdvd::IopCdvd(IopHost &host, IopMemory &memory) - : m_impl(std::make_unique(host, memory)) + IopCdvd::IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel) + : m_impl(std::make_unique(host, memory, kernel)) { } diff --git a/ps2xIOP/src/emulator/iop_cdvd.h b/ps2xIOP/src/emulator/imports/iop_cdvd.h similarity index 89% rename from ps2xIOP/src/emulator/iop_cdvd.h rename to ps2xIOP/src/emulator/imports/iop_cdvd.h index c7b5a80..2ca6048 100644 --- a/ps2xIOP/src/emulator/iop_cdvd.h +++ b/ps2xIOP/src/emulator/imports/iop_cdvd.h @@ -12,6 +12,7 @@ namespace ps2x::iop namespace ps2x::iop::detail { struct IopCpuState; + class IopKernel; class IopMemory; class IopCdvd @@ -24,7 +25,7 @@ namespace ps2x::iop::detail uint32_t reason = 0u; }; - IopCdvd(IopHost &host, IopMemory &memory); + IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel); ~IopCdvd(); IopCdvd(const IopCdvd &) = delete; diff --git a/ps2xIOP/src/emulator/imports/iop_heaplib.cpp b/ps2xIOP/src/emulator/imports/iop_heaplib.cpp new file mode 100644 index 0000000..caa1417 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_heaplib.cpp @@ -0,0 +1,54 @@ +#include "iop_heaplib.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" + +namespace ps2x::iop::detail +{ + IopHeaplib::IopHeaplib(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + bool IopHeaplib::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 4: // CreateHeap + setV0(m_memory.allocate(16u, 16u)); + return true; + case 5: // DeleteHeap + if (a0 != 0u) + (void)m_memory.freeAllocation(a0); + setV0(0u); + return true; + case 6: + setV0(m_memory.allocate(a1, 16u)); + return true; + case 7: + setV0(m_memory.freeAllocation(a1) ? 0u : 0xFFFFFFFFu); + return true; + case 8: + setV0(m_memory.maxFreeMemory()); + return true; + case 11: + setV0(0u); + return true; + case 15: + if (const auto block = m_memory.allocationContaining(a0)) + setV0(block->size); + else + setV0(0xFFFFFFFFu); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_heaplib.h b/ps2xIOP/src/emulator/imports/iop_heaplib.h new file mode 100644 index 0000000..26b7ec5 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_heaplib.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopHeaplib + { + public: + explicit IopHeaplib(IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/iop_imports.cpp b/ps2xIOP/src/emulator/imports/iop_imports.cpp similarity index 86% rename from ps2xIOP/src/emulator/iop_imports.cpp rename to ps2xIOP/src/emulator/imports/iop_imports.cpp index 396093d..6afcf5d 100644 --- a/ps2xIOP/src/emulator/iop_imports.cpp +++ b/ps2xIOP/src/emulator/imports/iop_imports.cpp @@ -1,6 +1,6 @@ #include "iop_imports.h" -#include "iop_memory.h" +#include "../core/iop_memory.h" #include #include @@ -147,6 +147,26 @@ namespace ps2x::iop::detail return found->second.functions[ordinal]; } + int32_t IopImportRegistry::setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode) + { + constexpr int32_t kLibraryNotFound = -213; + constexpr int32_t kIllegalLibrary = -214; + + if (address == 0u) + return kIllegalLibrary; + const uint32_t physical = IopMemory::physicalAddress(address); + if (physical + 12u > IopMemory::RamSize) + return kLibraryNotFound; + + const bool registered = m_libraries.find(physical) != m_libraries.end(); + if (!registered && m_memory.read32(physical) != kExportMagic) + return kLibraryNotFound; + + const uint16_t oldMode = m_memory.read16(physical + 10u); + m_memory.write16(physical + 10u, static_cast((oldMode & ~6u) | (mode & 6u))); + return 0; + } + void IopImportRegistry::eraseRange(uint32_t base, uint32_t size) { for (auto library = m_libraries.begin(); library != m_libraries.end();) diff --git a/ps2xIOP/src/emulator/iop_imports.h b/ps2xIOP/src/emulator/imports/iop_imports.h similarity index 92% rename from ps2xIOP/src/emulator/iop_imports.h rename to ps2xIOP/src/emulator/imports/iop_imports.h index 87d4f99..66a9162 100644 --- a/ps2xIOP/src/emulator/iop_imports.h +++ b/ps2xIOP/src/emulator/imports/iop_imports.h @@ -29,6 +29,7 @@ namespace ps2x::iop::detail [[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; + [[nodiscard]] int32_t setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode); void eraseRange(uint32_t base, uint32_t size); private: diff --git a/ps2xIOP/src/emulator/imports/iop_intrman.cpp b/ps2xIOP/src/emulator/imports/iop_intrman.cpp new file mode 100644 index 0000000..da3be3b --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_intrman.cpp @@ -0,0 +1,113 @@ +#include "iop_intrman.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "../services/iop_rpc.h" + +namespace ps2x::iop::detail +{ + IopIntrman::IopIntrman(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopIntrman::reset() + { + m_handlers.clear(); + m_enabled.clear(); + } + + bool IopIntrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor) + { + 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]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 3: + setV0(0u); + return true; + case 4: // RegisterIntrHandler + m_handlers[static_cast(a0)] = {a2, a3, cpu.gpr[28]}; + setV0(0u); + return true; + case 5: // ReleaseIntrHandler + m_handlers.erase(static_cast(a0)); + setV0(0u); + return true; + case 6: // EnableIntr + m_enabled[static_cast(a0)] = true; + if (a0 < 32u) + m_memory.setInterruptMask(m_memory.interruptMask() | (1u << a0)); + setV0(0u); + return true; + case 7: // DisableIntr + if (a1 != 0u) + m_memory.write32(a1, a0); + if (a0 < 32u) + m_memory.setInterruptMask(m_memory.interruptMask() & ~(1u << a0)); + m_enabled[static_cast(a0)] = false; + setV0(0u); + return true; + case 8: // CpuDisableIntr + m_memory.setInterruptControl(0u); + setV0(0u); + return true; + case 9: // CpuEnableIntr + m_memory.setInterruptControl(1u); + setV0(0u); + return true; + case 14: + setV0(a0 != 0u + ? executor.executeGuestFunctionWithBudget(a0, a1, a2, a3, 0u, cpu.gpr[28], 100000u) + : 0u); + return true; + case 15: + case 16: + case 23: + case 24: + case 25: + case 28: + case 30: + setV0(0u); + return true; + case 17: + if (a0 != 0u) + m_memory.write32(a0, m_memory.interruptControl()); + m_memory.setInterruptControl(0u); + setV0(0u); + return true; + case 18: + m_memory.setInterruptControl(a0 != 0u ? 1u : 0u); + setV0(0u); + return true; + default: + return false; + } + } + + bool IopIntrman::dispatchInterrupt(int irq, IopGuestExecutor &executor) const + { + const auto enabled = m_enabled.find(irq); + if (enabled == m_enabled.end() || !enabled->second) + return false; + const auto handler = m_handlers.find(irq); + if (handler == m_handlers.end() || handler->second.function == 0u) + return false; + + (void)executor.executeGuestFunctionWithBudget(handler->second.function, + handler->second.argument, + 0u, + 0u, + 0u, + handler->second.gp, + 100000u); + return true; + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_intrman.h b/ps2xIOP/src/emulator/imports/iop_intrman.h new file mode 100644 index 0000000..48fdb14 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_intrman.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopGuestExecutor; + class IopMemory; + + class IopIntrman + { + public: + explicit IopIntrman(IopMemory &memory) noexcept; + + void reset(); + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor); + [[nodiscard]] bool dispatchInterrupt(int irq, IopGuestExecutor &executor) const; + + private: + struct Handler + { + uint32_t function = 0u; + uint32_t argument = 0u; + uint32_t gp = 0u; + }; + + IopMemory &m_memory; + std::map m_handlers; + std::map m_enabled; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.cpp b/ps2xIOP/src/emulator/imports/iop_ioman.cpp new file mode 100644 index 0000000..3eb4119 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_ioman.cpp @@ -0,0 +1,91 @@ +#include "iop_ioman.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "../services/iop_rpc.h" + +#include + +namespace ps2x::iop::detail +{ + IopIoman::IopIoman(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopIoman::reset() + { + m_devices.clear(); + } + + bool IopIoman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor) + { + constexpr size_t kMaxDevices = 16u; + const uint32_t a0 = cpu.gpr[4]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 20: // AddDrv + { + if (a0 == 0u || m_devices.size() >= kMaxDevices) + { + setV0(0xFFFFFFFFu); + return true; + } + + const uint32_t nameAddress = m_memory.read32(a0); + const uint32_t operations = m_memory.read32(a0 + 16u); + const std::string name = m_memory.readString(nameAddress, 64u); + if (nameAddress == 0u || operations == 0u || name.empty()) + { + setV0(0xFFFFFFFFu); + return true; + } + + m_devices.push_back({a0, cpu.gpr[28], name}); + const uint32_t init = m_memory.read32(operations); + if (init != 0u) + { + const int32_t result = static_cast( + executor.executeGuestFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28])); + if (result < 0) + { + m_devices.pop_back(); + setV0(0xFFFFFFFFu); + return true; + } + } + + setV0(0u); + return true; + } + case 21: // DelDrv + { + const std::string name = m_memory.readString(a0, 64u); + const auto device = std::find_if( + m_devices.begin(), m_devices.end(), + [&](const Device &candidate) + { return candidate.name == name; }); + if (device == m_devices.end()) + { + setV0(0xFFFFFFFFu); + return true; + } + + const uint32_t operations = m_memory.read32(device->address + 16u); + const uint32_t deinit = operations != 0u ? m_memory.read32(operations + 4u) : 0u; + if (deinit != 0u) + (void)executor.executeGuestFunction(deinit, device->address, 0u, 0u, 0u, device->gp); + m_devices.erase(device); + setV0(0u); + return true; + } + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.h b/ps2xIOP/src/emulator/imports/iop_ioman.h new file mode 100644 index 0000000..182f786 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_ioman.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopGuestExecutor; + class IopMemory; + + class IopIoman + { + public: + explicit IopIoman(IopMemory &memory) noexcept; + + void reset(); + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor); + + private: + struct Device + { + uint32_t address = 0u; + uint32_t gp = 0u; + std::string name; + }; + + IopMemory &m_memory; + std::vector m_devices; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_loadcore.cpp b/ps2xIOP/src/emulator/imports/iop_loadcore.cpp new file mode 100644 index 0000000..272e493 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_loadcore.cpp @@ -0,0 +1,56 @@ +#include "iop_loadcore.h" + +#include "../core/iop_cpu.h" +#include "iop_imports.h" +#include "../core/iop_memory.h" + +namespace ps2x::iop::detail +{ + IopLoadcore::IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept + : m_memory(memory), m_imports(imports) + { + } + + bool IopLoadcore::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 3: + case 4: + case 5: + case 8: + case 9: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 20: + case 21: + setV0(0u); + return true; + case 6: + case 10: + setV0(m_imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu); + return true; + case 7: + setV0(m_imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu); + return true; + case 11: + setV0(m_imports.findTable(m_memory.readString(a0 + 12u, 8u))); + return true; + case 27: // SetRebootTimeLibraryHandlingMode + setV0(static_cast(m_imports.setRebootTimeLibraryHandlingMode(a0, cpu.gpr[5]))); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_loadcore.h b/ps2xIOP/src/emulator/imports/iop_loadcore.h new file mode 100644 index 0000000..e457a98 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_loadcore.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopImportRegistry; + class IopMemory; + + class IopLoadcore + { + public: + IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopMemory &m_memory; + IopImportRegistry &m_imports; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_stdio.cpp b/ps2xIOP/src/emulator/imports/iop_stdio.cpp new file mode 100644 index 0000000..eed2b61 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_stdio.cpp @@ -0,0 +1,67 @@ +#include "iop_stdio.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_host.h" + +#include + +namespace ps2x::iop::detail +{ + IopStdio::IopStdio(IopHost &host, IopMemory &memory) noexcept + : m_host(host), m_memory(memory) + { + } + + bool IopStdio::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + const auto logString = [&](std::string_view prefix, uint32_t address, uint32_t resultBias = 0u) + { + const std::string text = m_memory.readString(address, 2048u); + m_host.log(LogLevel::Info, std::string(prefix) + text); + setV0(static_cast(text.size()) + resultBias); + }; + + switch (ordinal) + { + case 4: // printf + logString("[IOP printf] ", a0); + return true; + case 5: // getchar + case 10: + setV0(0xFFFFFFFFu); + return true; + case 6: // putchar + m_host.log(LogLevel::Info, std::string("[IOP putchar] ") + static_cast(a0 & 0xFFu)); + setV0(a0 & 0xFFu); + return true; + case 7: // puts + logString("[IOP puts] ", a0, 1u); + return true; + case 8: // gets + case 13: + setV0(0u); + return true; + case 9: // fdprintf + logString("[IOP fdprintf] ", a1); + return true; + case 11: + setV0(a0 & 0xFFu); + return true; + case 12: // fdputs + logString("[IOP fdputs] ", a0); + return true; + case 14: // vfdprintf + logString("[IOP vfdprintf] ", a1); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_stdio.h b/ps2xIOP/src/emulator/imports/iop_stdio.h new file mode 100644 index 0000000..4c7f9c0 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_stdio.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopStdio + { + public: + IopStdio(IopHost &host, IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopHost &m_host; + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/iop_sysclib.cpp b/ps2xIOP/src/emulator/imports/iop_sysclib.cpp similarity index 98% rename from ps2xIOP/src/emulator/iop_sysclib.cpp rename to ps2xIOP/src/emulator/imports/iop_sysclib.cpp index 1e0f6f9..0517478 100644 --- a/ps2xIOP/src/emulator/iop_sysclib.cpp +++ b/ps2xIOP/src/emulator/imports/iop_sysclib.cpp @@ -1,7 +1,7 @@ #include "iop_sysclib.h" -#include "iop_cpu.h" -#include "iop_memory.h" +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" #include #include @@ -64,7 +64,7 @@ namespace ps2x::iop::detail 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. + case 5: // longjmp, TODO bc w can do it without the BIOS jmp_buf ABI. setV0(a1 == 0u ? 1u : a1); return true; case 6: diff --git a/ps2xIOP/src/emulator/iop_sysclib.h b/ps2xIOP/src/emulator/imports/iop_sysclib.h similarity index 100% rename from ps2xIOP/src/emulator/iop_sysclib.h rename to ps2xIOP/src/emulator/imports/iop_sysclib.h diff --git a/ps2xIOP/src/emulator/imports/iop_sysmem.cpp b/ps2xIOP/src/emulator/imports/iop_sysmem.cpp new file mode 100644 index 0000000..2c8d8f7 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_sysmem.cpp @@ -0,0 +1,72 @@ +#include "iop_sysmem.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_host.h" + +#include + +namespace ps2x::iop::detail +{ + IopSysmem::IopSysmem(IopHost &host, IopMemory &memory) noexcept + : m_host(host), m_memory(memory) + { + } + + bool IopSysmem::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; + }; + + switch (ordinal) + { + case 4: // AllocSysMemory + { + const uint32_t address = a0 == 2u + ? m_memory.allocate(a1, 16u, a2) + : m_memory.allocate(a1, 16u); + setV0(address); + return true; + } + case 5: // FreeSysMemory + setV0(m_memory.freeAllocation(a0) ? 0u : 0xFFFFFFFFu); + return true; + case 6: // QueryMemSize + setV0(IopMemory::RamSize); + return true; + case 7: // QueryMaxFreeMemSize + case 8: // QueryTotalFreeMemSize + setV0(m_memory.maxFreeMemory()); + return true; + case 9: // QueryBlockTopAddress + if (const auto block = m_memory.allocationContaining(a0)) + setV0(block->address); + else + setV0(0u); + return true; + case 10: // QueryBlockSize + if (const auto block = m_memory.allocationContaining(a0)) + setV0(block->size); + else + setV0(0xFFFFFFFFu); + return true; + case 14: // Kprintf + { + const std::string format = m_memory.readString(a0, 512u); + m_host.log(LogLevel::Info, std::string("[IOP Kprintf] ") + format); + setV0(static_cast(format.size())); + return true; + } + case 15: + setV0(0u); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_sysmem.h b/ps2xIOP/src/emulator/imports/iop_sysmem.h new file mode 100644 index 0000000..95d9291 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_sysmem.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopSysmem + { + public: + IopSysmem(IopHost &host, IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopHost &m_host; + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_timrman.cpp b/ps2xIOP/src/emulator/imports/iop_timrman.cpp new file mode 100644 index 0000000..3d80897 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_timrman.cpp @@ -0,0 +1,476 @@ +#include "iop_timrman.h" + +#include "../core/iop_cpu.h" +#include "../services/iop_rpc.h" + +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr int32_t kNoTimer = -150; + constexpr int32_t kIllegalTimerId = -151; + constexpr int32_t kIllegalSource = -152; + constexpr int32_t kIllegalPrescale = -153; + constexpr int32_t kTimerBusy = -154; + constexpr int32_t kTimerNotConfigured = -155; + constexpr int32_t kTimerNotRunning = -156; + constexpr int32_t kIllegalMode = -405; + constexpr uint64_t kIopClockHz = 36'864'000ull; + constexpr uint64_t kPixelClockHz = 13'500'000ull; + constexpr uint64_t kHlineClockHz = 15'734ull; + + constexpr std::array kAllocationOrder{2u, 5u, 4u, 3u, 0u, 1u}; + constexpr std::array kAddresses{ + 0xBF801100u, + 0xBF801110u, + 0xBF801120u, + 0xBF801480u, + 0xBF801490u, + 0xBF8014A0u, + }; + constexpr std::array kSources{0x0Bu, 0x0Du, 0x01u, 0x05u, 0x01u, 0x01u}; + constexpr std::array kWidths{16u, 16u, 16u, 32u, 32u, 32u}; + constexpr std::array kMaxPrescales{1u, 1u, 8u, 1u, 256u, 256u}; + constexpr std::array kIrqs{4u, 5u, 6u, 14u, 15u, 16u}; + + uint32_t errorValue(int32_t error) noexcept + { + return static_cast(error); + } + } + + void IopTimrman::reset() noexcept + { + for (size_t i = 0u; i < m_timers.size(); ++i) + { + m_timers[i] = {}; + m_timers[i].address = kAddresses[i]; + m_timers[i].sources = kSources[i]; + m_timers[i].width = kWidths[i]; + m_timers[i].maxPrescale = kMaxPrescales[i]; + m_timers[i].irq = kIrqs[i]; + } + m_holdMode = 0u; + m_servicing = false; + } + + uint32_t IopTimrman::timerId(size_t index) noexcept + { + return (static_cast(index + 1u) << 28u) | (kAddresses[index] >> 4u); + } + + IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) noexcept + { + const uint32_t encoded = id >> 28u; + if (encoded == 0u || encoded > m_timers.size()) + return nullptr; + Timer &timer = m_timers[encoded - 1u]; + return timer.users != 0u && (id & 0x0FFFFFFFu) == (timer.address >> 4u) + ? &timer + : nullptr; + } + + const IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) const noexcept + { + return const_cast(this)->timerFromId(id); + } + + uint64_t IopTimrman::ticksToCycles(const Timer &timer, uint64_t ticks) noexcept + { + const uint64_t prescale = std::max(timer.prescale, 1u); + const uint64_t sourceHz = timer.source == 2u + ? kPixelClockHz + : (timer.source == 4u ? kHlineClockHz : kIopClockHz); + if (ticks == 0u) + ticks = timer.width == 16u ? (1ull << 16u) : (1ull << 32u); + const unsigned long long scaled = ticks * prescale; + if (sourceHz == kIopClockHz) + return std::max(scaled, 1u); + const uint64_t whole = (scaled / sourceHz) * kIopClockHz; + const uint64_t remainder = scaled % sourceHz; + return std::max(1u, whole + (remainder * kIopClockHz + sourceHz - 1u) / sourceHz); + } + + uint64_t IopTimrman::elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept + { + if (!timer.running || currentCycle <= timer.counterBaseCycle) + return 0u; + const uint64_t elapsed = currentCycle - timer.counterBaseCycle; + const uint64_t sourceHz = timer.source == 2u + ? kPixelClockHz + : (timer.source == 4u ? kHlineClockHz : kIopClockHz); + return (elapsed * sourceHz) / (kIopClockHz * std::max(timer.prescale, 1u)); + } + + uint32_t IopTimrman::counterValue(const Timer &timer, uint64_t currentCycle) noexcept + { + const uint64_t value = static_cast(timer.counterBase) + elapsedTicks(timer, currentCycle); + return timer.width == 16u ? static_cast(value & 0xFFFFu) : static_cast(value); + } + + void IopTimrman::schedule(Timer &timer, uint64_t currentCycle) noexcept + { + timer.compareCycle = UINT64_MAX; + timer.overflowCycle = UINT64_MAX; + if (!timer.running) + return; + + const uint64_t current = counterValue(timer, currentCycle); + timer.counterBase = static_cast(current); + timer.counterBaseCycle = currentCycle; + + if (timer.compareCallback.function != 0u) + { + const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u); + const uint64_t compare = timer.width == 16u ? (timer.compare & 0xFFFFu) : timer.compare; + uint64_t delta = (compare + modulus - current) % modulus; + if (delta == 0u) + delta = modulus; + timer.compareCycle = currentCycle + ticksToCycles(timer, delta); + } + + if (timer.overflowCallback.function != 0u) + { + const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u); + uint64_t delta = modulus - current; + if (delta == 0u) + delta = modulus; + timer.overflowCycle = currentCycle + ticksToCycles(timer, delta); + } + } + + void IopTimrman::stop(Timer &timer, uint64_t currentCycle) noexcept + { + timer.counterBase = counterValue(timer, currentCycle); + timer.counterBaseCycle = currentCycle; + timer.running = false; + timer.liveMode = 0u; + timer.compareCycle = UINT64_MAX; + timer.overflowCycle = UINT64_MAX; + } + + bool IopTimrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) + { + 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]; + const auto setV0 = [&](uint32_t value) { cpu.gpr[2] = value; }; + + switch (ordinal) + { + case 3: // GetTimersTable + setV0(0u); + return true; + case 4: // AllocHardTimer + for (const size_t index : kAllocationOrder) + { + Timer &timer = m_timers[index]; + if (timer.users != 0u || (timer.sources & a0) == 0u || timer.width != a1 || timer.maxPrescale < a2) + continue; + timer.users = 1u; + timer.source = a0; + timer.prescale = std::max(a2, 1u); + timer.counterBaseCycle = currentCycle; + setV0(timerId(index)); + return true; + } + setV0(errorValue(kNoTimer)); + return true; + case 5: // ReferHardTimer + for (size_t index = 0u; index < m_timers.size(); ++index) + { + Timer &timer = m_timers[index]; + if (timer.users == 0u || timer.liveMode == 0u || (timer.sources & a0) == 0u || + timer.width != a1 || (timer.liveMode & a3) != a2) + continue; + ++timer.users; + setV0(timerId(index)); + return true; + } + setV0(errorValue(kNoTimer)); + return true; + case 6: // FreeHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (--timer->users == 0u) + { + const uint32_t address = timer->address; + const uint8_t sources = timer->sources; + const uint8_t width = timer->width; + const uint16_t maxPrescale = timer->maxPrescale; + const uint8_t irq = timer->irq; + *timer = {}; + timer->address = address; + timer->sources = sources; + timer->width = width; + timer->maxPrescale = maxPrescale; + timer->irq = irq; + } + setV0(0u); + return true; + } + case 7: // SetTimerMode + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (a1 == 0u) + stop(*timer, currentCycle); + else + { + timer->liveMode = a1; + timer->running = true; + timer->counterBaseCycle = currentCycle; + schedule(*timer, currentCycle); + } + setV0(0u); + return true; + } + case 8: // GetTimerStatus + case 17: // GetTimerMode + { + const Timer *timer = timerFromId(a0); + setV0(timer ? timer->liveMode : errorValue(kIllegalTimerId)); + return true; + } + case 9: // SetTimerCounter + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + timer->counterBase = timer->width == 16u ? (a1 & 0xFFFFu) : a1; + timer->counterBaseCycle = currentCycle; + schedule(*timer, currentCycle); + setV0(0u); + return true; + } + case 10: // GetTimerCounter + { + const Timer *timer = timerFromId(a0); + setV0(timer ? counterValue(*timer, currentCycle) : errorValue(kIllegalTimerId)); + return true; + } + case 11: // SetTimerCompare + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1; + schedule(*timer, currentCycle); + setV0(0u); + return true; + } + case 12: // GetTimerCompare + { + const Timer *timer = timerFromId(a0); + setV0(timer ? timer->compare : errorValue(kIllegalTimerId)); + return true; + } + case 13: // SetHoldMode + m_holdMode = (m_holdMode & ~(0xFu << ((a0 & 7u) * 4u))) | ((a1 & 0xFu) << ((a0 & 7u) * 4u)); + setV0(0u); + return true; + case 14: // GetHoldMode + setV0((m_holdMode >> ((a0 & 7u) * 4u)) & 0xFu); + return true; + case 15: // GetHoldReg + setV0(0u); + return true; + case 16: // GetHardTimerIntrCode + { + const Timer *timer = timerFromId(a0); + setV0(timer ? timer->irq : errorValue(kIllegalTimerId)); + return true; + } + case 18: // GetTimerReadFunc + // Returning a host-side register reader as a guest function is not meaningful. + setV0(0u); + return true; + case 20: // SetTimerHandler + case 21: // SetOverflowHandler + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (timer->running) + { + setV0(errorValue(kTimerNotRunning)); + return true; + } + if (ordinal == 20u) + { + timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1; + timer->compareCallback = {a2, a3, cpu.gpr[28]}; + } + else + { + timer->overflowCallback = {a1, a2, cpu.gpr[28]}; + } + setV0(0u); + return true; + } + case 22: // SetupHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (timer->running) + { + setV0(errorValue(kTimerBusy)); + return true; + } + if ((a2 != 0u && a2 != 1u && a2 != 3u && a2 != 5u && a2 != 7u)) + { + setV0(errorValue(kIllegalMode)); + return true; + } + if ((timer->sources & a1) == 0u) + { + setV0(errorValue(kIllegalSource)); + return true; + } + if (a3 == 0u || a3 > timer->maxPrescale) + { + setV0(errorValue(kIllegalPrescale)); + return true; + } + timer->source = a1; + timer->setupMode = a2; + timer->prescale = a3; + timer->configured = true; + setV0(0u); + return true; + } + case 23: // StartHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (timer->running) + { + setV0(errorValue(kTimerBusy)); + return true; + } + if (!timer->configured) + { + setV0(errorValue(kTimerNotConfigured)); + return true; + } + timer->counterBase = 0u; + timer->counterBaseCycle = currentCycle; + timer->liveMode = 0x80000000u | timer->setupMode; + timer->running = true; + schedule(*timer, currentCycle); + setV0(0u); + return true; + } + case 24: // StopHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (!timer->running) + { + setV0(errorValue(kTimerNotRunning)); + return true; + } + stop(*timer, currentCycle); + setV0(0u); + return true; + } + default: + return false; + } + } + + void IopTimrman::serviceDue(uint64_t currentCycle, IopGuestExecutor &executor) + { + if (m_servicing) + return; + m_servicing = true; + struct ServiceGuard + { + bool &flag; + ~ServiceGuard() { flag = false; } + } guard{m_servicing}; + + for (Timer &timer : m_timers) + { + if (!timer.running) + continue; + + const bool compareDue = timer.compareCycle <= currentCycle; + const bool overflowDue = timer.overflowCycle <= currentCycle; + if (!compareDue && !overflowDue) + continue; + + const Callback callback = compareDue ? timer.compareCallback : timer.overflowCallback; + timer.compareCycle = UINT64_MAX; + timer.overflowCycle = UINT64_MAX; + const uint32_t result = callback.function != 0u + ? executor.executeGuestFunctionWithBudget(callback.function, + callback.common, + 0u, + 0u, + 0u, + callback.gp, + 100000u) + : 0u; + if (!timer.running) + continue; + if (result == 0u) + { + stop(timer, currentCycle); + continue; + } + if (compareDue) + timer.compare = timer.width == 16u ? (result & 0xFFFFu) : result; + timer.counterBase = 0u; + timer.counterBaseCycle = currentCycle; + schedule(timer, currentCycle); + } + + } + + uint64_t IopTimrman::nextEventCycle(uint64_t fallback) const noexcept + { + uint64_t next = fallback; + for (const Timer &timer : m_timers) + { + next = std::min(next, timer.compareCycle); + next = std::min(next, timer.overflowCycle); + } + return next; + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_timrman.h b/ps2xIOP/src/emulator/imports/iop_timrman.h new file mode 100644 index 0000000..11b53b6 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_timrman.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopGuestExecutor; + + class IopTimrman + { + public: + void reset() noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle); + void serviceDue(uint64_t currentCycle, IopGuestExecutor &executor); + [[nodiscard]] uint64_t nextEventCycle(uint64_t fallback) const noexcept; + + private: + struct Callback + { + uint32_t function = 0u; + uint32_t common = 0u; + uint32_t gp = 0u; + }; + + struct Timer + { + uint32_t address = 0u; + uint8_t sources = 0u; + uint8_t width = 0u; + uint16_t maxPrescale = 0u; + uint8_t irq = 0u; + uint8_t users = 0u; + + uint32_t source = 1u; + uint32_t prescale = 1u; + uint32_t setupMode = 0u; + uint32_t liveMode = 0u; + uint32_t counterBase = 0u; + uint32_t compare = 0u; + uint64_t counterBaseCycle = 0u; + uint64_t compareCycle = UINT64_MAX; + uint64_t overflowCycle = UINT64_MAX; + bool configured = false; + bool running = false; + + Callback compareCallback; + Callback overflowCallback; + }; + + [[nodiscard]] Timer *timerFromId(uint32_t timerId) noexcept; + [[nodiscard]] const Timer *timerFromId(uint32_t timerId) const noexcept; + [[nodiscard]] static uint32_t timerId(size_t index) noexcept; + [[nodiscard]] static uint64_t ticksToCycles(const Timer &timer, uint64_t ticks) noexcept; + [[nodiscard]] static uint64_t elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept; + [[nodiscard]] static uint32_t counterValue(const Timer &timer, uint64_t currentCycle) noexcept; + static void schedule(Timer &timer, uint64_t currentCycle) noexcept; + static void stop(Timer &timer, uint64_t currentCycle) noexcept; + + std::array m_timers{}; + uint32_t m_holdMode = 0u; + bool m_servicing = false; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_vblank.cpp b/ps2xIOP/src/emulator/imports/iop_vblank.cpp new file mode 100644 index 0000000..c3bcf33 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_vblank.cpp @@ -0,0 +1,42 @@ +#include "iop_vblank.h" + +#include "../core/iop_cpu.h" +#include "../iop_emulator_const.h" +#include "../core/iop_kernel.h" + +namespace ps2x::iop::detail +{ + IopVblank::IopVblank(IopKernel &kernel) noexcept + : m_kernel(kernel) + { + } + + bool IopVblank::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) + { + switch (ordinal) + { + case 4: // WaitVblankStart + case 5: // WaitVblankEnd + case 6: // WaitVblank + case 7: // WaitNonVblank + { + const bool waitForEnd = ordinal == 5u || ordinal == 7u; + const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u; + const uint64_t fieldStart = currentCycle - (currentCycle % kVblankPeriodCycles); + uint64_t wakeCycle = fieldStart + phase; + if (wakeCycle <= currentCycle) + wakeCycle += kVblankPeriodCycles; + m_kernel.delayCurrentUntil(wakeCycle, cpu); + cpu.gpr[2] = 0u; + return true; + } + case 8: // RegisterVblankHandler + case 9: // ReleaseVblankHandler + // Callback delivery is not required by the scheduler wait ABI yet. + cpu.gpr[2] = 0u; + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_vblank.h b/ps2xIOP/src/emulator/imports/iop_vblank.h new file mode 100644 index 0000000..1cabae7 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_vblank.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopKernel; + + class IopVblank + { + public: + explicit IopVblank(IopKernel &kernel) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle); + + private: + IopKernel &m_kernel; + }; +} diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp index 4dcef1c..ccb27bb 100644 --- a/ps2xIOP/src/emulator/iop_emulator.cpp +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -1,12 +1,20 @@ #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 "imports/iop_cdvd.h" +#include "core/iop_cpu.h" +#include "imports/iop_heaplib.h" +#include "imports/iop_imports.h" +#include "imports/iop_intrman.h" +#include "imports/iop_ioman.h" +#include "core/iop_kernel.h" +#include "imports/iop_loadcore.h" +#include "core/iop_memory.h" +#include "services/iop_module_loader.h" +#include "services/iop_rpc.h" +#include "imports/iop_stdio.h" +#include "imports/iop_sysclib.h" +#include "imports/iop_sysmem.h" +#include "imports/iop_timrman.h" +#include "imports/iop_vblank.h" #include "iop_emulator_const.h" #include @@ -15,7 +23,6 @@ #include #include #include -#include #include namespace ps2x::iop::detail @@ -73,13 +80,6 @@ namespace ps2x::iop::detail bool resident = false; }; - struct InterruptHandler - { - uint32_t function = 0; - uint32_t argument = 0; - uint32_t gp = 0; - }; - struct GuestCallback { uint32_t function = 0; @@ -93,21 +93,22 @@ namespace ps2x::iop::detail 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), + sysmem(host, memory), kernel(memory), + cdvd(host, memory, kernel), + vblank(kernel), rpc(host, memory, kernel), sysclib(memory), + stdio(host, memory), + heaplib(memory), + intrman(memory), + timrman(), + ioman(memory), cpuCore(memory), - imports(memory) + imports(memory), + loadcore(memory, imports) { reset(); } @@ -120,9 +121,9 @@ namespace ps2x::iop::detail imports.reset(); rpc.reset(); cdvd.reset(); - interruptHandlers.clear(); - iomanDevices.clear(); - interruptEnabled.clear(); + intrman.reset(); + timrman.reset(); + ioman.reset(); pendingDmaInterrupts.clear(); pendingGuestCallbacks.clear(); nextModuleId = 1; @@ -209,16 +210,6 @@ namespace ps2x::iop::detail 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); @@ -249,77 +240,15 @@ namespace ps2x::iop::detail 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, "sysmem") && sysmem.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; - if (iequals(call.library, "cdvdman") && - cdvd.dispatchImport(call.ordinal, cpu)) + if (iequals(call.library, "cdvdman") && cdvd.dispatchImport(call.ordinal, cpu)) { if (const auto callback = cdvd.takeCompletionCallback()) { @@ -334,50 +263,8 @@ namespace ps2x::iop::detail 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, "loadcore") && loadcore.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; if (iequals(call.library, "thbase") || iequals(call.library, "threadman")) { @@ -403,83 +290,8 @@ namespace ps2x::iop::detail ? 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, "intrman") && intrman.dispatchImport(call.ordinal, cpu, *this)) + return ImportDisposition::Handled; if (iequals(call.library, "secrman")) { switch (call.ordinal) @@ -498,228 +310,37 @@ namespace ps2x::iop::detail } 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, "ioman") && ioman.dispatchImport(call.ordinal, cpu, *this)) + return ImportDisposition::Handled; 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")) + if (iequals(call.library, "vblank") && vblank.dispatchImport(call.ordinal, cpu, totalCycles)) + return ImportDisposition::Handled; + if (iequals(call.library, "timrman") && timrman.dispatchImport(call.ordinal, cpu, totalCycles)) + return ImportDisposition::Handled; + if (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, "stdio") && stdio.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; 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; - } - } + if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; const uint32_t target = imports.resolve(call.library, call.ordinal); if (target != 0u) @@ -847,6 +468,17 @@ namespace ps2x::iop::detail return callFunction(address, a0, a1, a2, a3, gp); } + uint32_t executeGuestFunctionWithBudget(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp, + uint32_t instructionBudget) override + { + return callFunction(address, a0, a1, a2, a3, gp, instructionBudget); + } + // Not that good to use exception handling for control flow but will do for now void servicePendingDmaInterrupts() { @@ -869,22 +501,7 @@ namespace ps2x::iop::detail 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); - } + (void)intrman.dispatchInterrupt(irq, *this); } catch (...) { @@ -944,6 +561,7 @@ namespace ps2x::iop::detail { servicePendingDmaInterrupts(); servicePendingGuestCallbacks(); + timrman.serviceDue(totalCycles, *this); IopThread *next = kernel.beginNextReady(totalCycles); if (!next) { @@ -952,6 +570,7 @@ namespace ps2x::iop::detail nextWake = std::min(nextWake, completionCycle); if (!pendingGuestCallbacks.empty()) nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first); + nextWake = timrman.nextEventCycle(nextWake); totalCycles = std::max(totalCycles + 1u, std::min(target, nextWake)); continue; } @@ -1060,16 +679,21 @@ namespace ps2x::iop::detail IopHost &host; IopMemory memory; - IopCdvd cdvd; + IopSysmem sysmem; IopKernel kernel; + IopCdvd cdvd; + IopVblank vblank; IopRpcBridge rpc; IopSysclib sysclib; + IopStdio stdio; + IopHeaplib heaplib; + IopIntrman intrman; + IopTimrman timrman; + IopIoman ioman; IopCpuCore cpuCore; IopImportRegistry imports; + IopLoadcore loadcore; std::map modules; - std::unordered_map interruptHandlers; - std::vector iomanDevices; - std::map interruptEnabled; std::map pendingDmaInterrupts; std::multimap pendingGuestCallbacks; uint32_t nextModuleId = 1; @@ -1138,6 +762,45 @@ namespace ps2x::iop::detail m_impl->rpc.onSifTransfer(transfer); } + uint32_t IopEmulator::allocateMemory(uint32_t size, uint32_t alignment) + { + return m_impl->memory.allocate(size, alignment); + } + + bool IopEmulator::freeMemory(uint32_t address) + { + return m_impl->memory.freeAllocation(address); + } + + bool IopEmulator::readMemory(uint32_t address, void *destination, size_t size) const + { + return isMemoryRange(address, size) && + m_impl->memory.readRam(address, destination, size); + } + + bool IopEmulator::writeMemory(uint32_t address, const void *source, size_t size) + { + return isMemoryRange(address, size) && + m_impl->memory.writeRam(address, source, size); + } + + bool IopEmulator::zeroMemory(uint32_t address, size_t size) + { + return isMemoryRange(address, size) && + m_impl->memory.zeroRam(address, size); + } + + bool IopEmulator::isMemoryRange(uint32_t address, size_t size) const + { + const bool physicalSegment = address < IopMemory::RamSize; + const bool cachedSegment = address >= 0x80000000u && address < 0x80200000u; + const bool uncachedSegment = address >= 0xA0000000u && address < 0xA0200000u; + if (!physicalSegment && !cachedSegment && !uncachedSegment) + return false; + const uint32_t physical = IopMemory::physicalAddress(address); + return physical <= IopMemory::RamSize && size <= IopMemory::RamSize - physical; + } + uint64_t IopEmulator::cycles() const noexcept { return m_impl->totalCycles; diff --git a/ps2xIOP/src/emulator/iop_emulator.h b/ps2xIOP/src/emulator/iop_emulator.h index 4d7bc40..01b48df 100644 --- a/ps2xIOP/src/emulator/iop_emulator.h +++ b/ps2xIOP/src/emulator/iop_emulator.h @@ -29,6 +29,13 @@ namespace ps2x::iop::detail [[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept; void onSifTransfer(const SifTransfer &transfer); + [[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u); + [[nodiscard]] bool freeMemory(uint32_t address); + [[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const; + [[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size); + [[nodiscard]] bool zeroMemory(uint32_t address, size_t size); + [[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const; + [[nodiscard]] uint64_t cycles() const noexcept; [[nodiscard]] uint64_t instructions() const noexcept; [[nodiscard]] uint32_t loadedModuleCount() const noexcept; diff --git a/ps2xIOP/src/emulator/iop_module_loader.cpp b/ps2xIOP/src/emulator/services/iop_module_loader.cpp similarity index 95% rename from ps2xIOP/src/emulator/iop_module_loader.cpp rename to ps2xIOP/src/emulator/services/iop_module_loader.cpp index 485c1ec..80e59d6 100644 --- a/ps2xIOP/src/emulator/iop_module_loader.cpp +++ b/ps2xIOP/src/emulator/services/iop_module_loader.cpp @@ -1,6 +1,6 @@ #include "iop_module_loader.h" -#include "iop_memory.h" +#include "../core/iop_memory.h" #include "ps2x/iop/iop_subsystem.h" #include @@ -170,9 +170,7 @@ namespace ps2x::iop::detail 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)) + 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); @@ -198,9 +196,7 @@ namespace ps2x::iop::detail if (relsec.type == SHT_RELA) { Elf32Rela relocation{}; - std::memcpy(&relocation, - image.data() + relsec.offset + offset, - sizeof(relocation)); + std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation)); relocationOffset = relocation.offset; relocationInfo = relocation.info; explicitAddend = relocation.addend; @@ -224,8 +220,7 @@ namespace ps2x::iop::detail symbolValue = symbol.value; if (symbol.shndx != 0u) { - symbolValue = static_cast( - static_cast(symbolValue) + delta); + symbolValue = static_cast(static_cast(symbolValue) + delta); } } } @@ -258,16 +253,12 @@ namespace ps2x::iop::detail break; case R_MIPS_32: case R_MIPS_REL32: - memory.write32(place, - static_cast( - static_cast(addend) + symbolValue)); + 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)); + memory.write32(place, (word & 0xFC000000u) | ((target >> 2u) & 0x03FFFFFFu)); break; } case R_MIPS_HI16: @@ -501,9 +492,7 @@ namespace ps2x::iop::detail else { if (!checkedRange(image.size(), section.offset, section.size) || - !memory.writeRam(destination, - image.data() + section.offset, - section.size)) + !memory.writeRam(destination, image.data() + section.offset, section.size)) return result; } } diff --git a/ps2xIOP/src/emulator/iop_module_loader.h b/ps2xIOP/src/emulator/services/iop_module_loader.h similarity index 100% rename from ps2xIOP/src/emulator/iop_module_loader.h rename to ps2xIOP/src/emulator/services/iop_module_loader.h diff --git a/ps2xIOP/src/emulator/iop_rpc.cpp b/ps2xIOP/src/emulator/services/iop_rpc.cpp similarity index 77% rename from ps2xIOP/src/emulator/iop_rpc.cpp rename to ps2xIOP/src/emulator/services/iop_rpc.cpp index b0025c9..cf4fdc6 100644 --- a/ps2xIOP/src/emulator/iop_rpc.cpp +++ b/ps2xIOP/src/emulator/services/iop_rpc.cpp @@ -1,12 +1,13 @@ #include "iop_rpc.h" -#include "iop_cpu.h" -#include "iop_kernel.h" -#include "iop_memory.h" +#include "../core/iop_cpu.h" +#include "../core/iop_kernel.h" +#include "../core/iop_memory.h" #include "ps2x/iop/iop_host.h" #include #include +#include #include #include @@ -119,8 +120,6 @@ namespace ps2x::iop::detail 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 @@ -148,13 +147,67 @@ namespace ps2x::iop::detail case 9: case 10: case 11: - case 12: - case 13: case 14: // InitRpc case 15: case 16: setV0(0); return true; + case 12: // sceSifSendCmd + case 13: // isceSifSendCmd + { + constexpr uint32_t kHeaderSize = 16u; + constexpr uint32_t kMaxPacketSize = 112u; + const uint32_t commandId = cpu.gpr[4]; + const uint32_t packetAddress = cpu.gpr[5]; + const uint32_t packetSize = cpu.gpr[6]; + const uint32_t extraSource = cpu.gpr[7]; + const uint32_t stackPointer = cpu.gpr[29]; + const uint32_t extraDestination = m_memory.read32(stackPointer + 16u); + const int32_t signedExtraSize = static_cast(m_memory.read32(stackPointer + 20u)); + + if (packetAddress == 0u || packetSize < kHeaderSize || packetSize > kMaxPacketSize || + !m_memory.ownsRamRange(packetAddress, packetSize)) + { + setV0(0u); + return true; + } + + std::array packet{}; + if (!m_memory.readRam(packetAddress, packet.data(), packetSize)) + { + setV0(0u); + return true; + } + + uint32_t extraSize = 0u; + if (signedExtraSize > 0) + { + extraSize = static_cast(signedExtraSize); + if (extraSource == 0u || extraDestination == 0u || + !m_memory.ownsRamRange(extraSource, extraSize) || + !m_host.writeGuest(extraDestination, m_memory.ram().data() + IopMemory::physicalAddress(extraSource), extraSize)) + { + setV0(0u); + return true; + } + } + + const uint32_t sizeWord = packetSize | (extraSize << 8u); + std::memcpy(packet.data() + 0u, &sizeWord, sizeof(sizeWord)); + std::memcpy(packet.data() + 4u, &extraDestination, sizeof(extraDestination)); + std::memcpy(packet.data() + 8u, &commandId, sizeof(commandId)); + + if (!m_host.sendSifCommand(commandId, packet.data(), packetSize)) + { + // A command without an EE handler is still a completed DMA on real hardware. Only malformed packets fail above. + } + + 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 17: // sceSifRegisterRpc { RpcServer server; @@ -255,8 +308,7 @@ namespace ps2x::iop::detail 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); + (void)m_host.zeroGuest(request.receive.address + copySize, request.receive.size - copySize); } } @@ -270,39 +322,11 @@ namespace ps2x::iop::detail 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); - } + // The EE SIF transport owns the actual directional memory movement. + // Services still receive both phases through IopSubsystem, but mirroring + // IOP bytes through an equal-numbered EE address would alias two distinct + // PS2 address spaces and can overwrite live game data. + (void)transfer; } void IopRpcBridge::removeServersInRange(uint32_t base, uint32_t size) diff --git a/ps2xIOP/src/emulator/iop_rpc.h b/ps2xIOP/src/emulator/services/iop_rpc.h similarity index 74% rename from ps2xIOP/src/emulator/iop_rpc.h rename to ps2xIOP/src/emulator/services/iop_rpc.h index 82fdfc2..a4286ba 100644 --- a/ps2xIOP/src/emulator/iop_rpc.h +++ b/ps2xIOP/src/emulator/services/iop_rpc.h @@ -28,6 +28,16 @@ namespace ps2x::iop::detail uint32_t a2, uint32_t a3, uint32_t gp) = 0; + [[nodiscard]] virtual uint32_t executeGuestFunctionWithBudget(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp, + uint32_t instructionBudget) + { + return executeGuestFunction(address, a0, a1, a2, a3, gp); + } }; class IopRpcBridge diff --git a/ps2xIOP/src/iop_module_manager.cpp b/ps2xIOP/src/iop_module_manager.cpp new file mode 100644 index 0000000..5576d20 --- /dev/null +++ b/ps2xIOP/src/iop_module_manager.cpp @@ -0,0 +1,177 @@ +#include "iop_module_manager.h" + +#include "ps2x/iop/ps2_path.h" + +#include + +namespace ps2x::iop::detail +{ + IopModuleManager::IopModuleManager() + { + // ROM modules that the no-BIOS HLE environment can legitimately provide. + // Entries with RPC services become routable only after load. + constexpr std::string_view modules[] = { + "sysmem", + "loadcore", + "intrman", + "sifman", + "sifcmd", + "sifinit", + "ioman", + "iomanx", + "modload", + "stdio", + "sysclib", + "thbase", + "thevent", + "thsemap", + "thmsgbx", + "timrman", + "vblank", + "secrman", + "sio2man", + "sio2d", + "padman", + "mcman", + "mcserv", + "libsd", + "cdvdman", + "cdvdfsv", + "dev9", + "usbd", + "usbhdfsd", + "udnl", + "fileio", + "poweroff", + "netman", + "ps2ip", + "dbcman", + "dbcm", + }; + for (const std::string_view module : modules) + m_builtinKeys.emplace(module); + } + + void IopModuleManager::reset() + { + m_records.clear(); + m_hleIdsByKey.clear(); + m_loadedKeyReferences.clear(); + m_nextHleId = 0x40000000; + } + + void IopModuleManager::setServiceModuleKeys(std::vector keys) + { + m_serviceKeys.clear(); + for (std::string &key : keys) + { + const std::string normalized = ps2PathLeafKey(key); + if (!normalized.empty()) + m_serviceKeys.emplace(normalized); + } + } + + ModuleLoadResult IopModuleManager::loadHle(std::string_view path) + { + ModuleLoadResult result{true, -1, -1}; + const std::string key = ps2PathLeafKey(path); + if (key.empty() || (!m_builtinKeys.contains(key) && !m_serviceKeys.contains(key))) + return result; + + const auto existing = m_hleIdsByKey.find(key); + if (existing != m_hleIdsByKey.end()) + { + Record &record = m_records[existing->second]; + ++record.references; + addLoadedKey(key); + result.moduleId = existing->second; + result.startResult = 0; + return result; + } + + if (m_nextHleId <= 0) + return result; + const int32_t id = m_nextHleId++; + m_records.emplace(id, Record{key, 1u, false}); + m_hleIdsByKey.emplace(key, id); + addLoadedKey(key); + result.moduleId = id; + result.startResult = 0; + return result; + } + + void IopModuleManager::observePhysicalLoad(int32_t moduleId, std::string_view path) + { + if (moduleId <= 0) + return; + const std::string key = ps2PathLeafKey(path); + if (key.empty()) + return; + m_records[moduleId] = Record{key, 1u, true}; + addLoadedKey(key); + } + + bool IopModuleManager::stopHle(int32_t moduleId, int32_t *result) + { + const auto found = m_records.find(moduleId); + if (found == m_records.end() || found->second.physical) + return false; + + Record &record = found->second; + removeLoadedKey(record.key); + if (record.references > 1u) + { + --record.references; + } + else + { + m_hleIdsByKey.erase(record.key); + m_records.erase(found); + } + if (result) + *result = 0; + return true; + } + + void IopModuleManager::observePhysicalStop(int32_t moduleId) + { + const auto found = m_records.find(moduleId); + if (found == m_records.end() || !found->second.physical) + return; + removeLoadedKey(found->second.key); + m_records.erase(found); + } + + bool IopModuleManager::isLoaded(std::span aliases) const + { + if (aliases.empty()) + return true; + return std::any_of(aliases.begin(), aliases.end(), [&](std::string_view alias) + { + const std::string key = ps2PathLeafKey(alias); + const auto found = m_loadedKeyReferences.find(key); + return found != m_loadedKeyReferences.end() && found->second != 0u; }); + } + + bool IopModuleManager::recognizes(std::string_view path) const + { + const std::string key = ps2PathLeafKey(path); + return m_builtinKeys.contains(key) || m_serviceKeys.contains(key); + } + + void IopModuleManager::addLoadedKey(std::string_view key) + { + ++m_loadedKeyReferences[std::string(key)]; + } + + void IopModuleManager::removeLoadedKey(std::string_view key) + { + const auto found = m_loadedKeyReferences.find(std::string(key)); + if (found == m_loadedKeyReferences.end()) + return; + if (found->second > 1u) + --found->second; + else + m_loadedKeyReferences.erase(found); + } +} diff --git a/ps2xIOP/src/iop_module_manager.h b/ps2xIOP/src/iop_module_manager.h new file mode 100644 index 0000000..33ab9f6 --- /dev/null +++ b/ps2xIOP/src/iop_module_manager.h @@ -0,0 +1,49 @@ +#pragma once + +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopModuleManager + { + public: + IopModuleManager(); + + void reset(); + void setServiceModuleKeys(std::vector keys); + + [[nodiscard]] ModuleLoadResult loadHle(std::string_view path); + void observePhysicalLoad(int32_t moduleId, std::string_view path); + [[nodiscard]] bool stopHle(int32_t moduleId, int32_t *result); + void observePhysicalStop(int32_t moduleId); + + [[nodiscard]] bool isLoaded(std::span aliases) const; + [[nodiscard]] bool recognizes(std::string_view path) const; + + private: + struct Record + { + std::string key; + uint32_t references = 0u; + bool physical = false; + }; + + void addLoadedKey(std::string_view key); + void removeLoadedKey(std::string_view key); + + std::unordered_set m_builtinKeys; + std::unordered_set m_serviceKeys; + std::unordered_map m_records; + std::unordered_map m_hleIdsByKey; + std::unordered_map m_loadedKeyReferences; + int32_t m_nextHleId = 0x40000000; + }; +} diff --git a/ps2xIOP/src/iop_service.h b/ps2xIOP/src/iop_service.h index 56ccd89..631c73a 100644 --- a/ps2xIOP/src/iop_service.h +++ b/ps2xIOP/src/iop_service.h @@ -18,6 +18,13 @@ namespace ps2x::iop::detail [[nodiscard]] virtual std::string_view name() const = 0; [[nodiscard]] virtual std::span sids() const = 0; + // A service with aliases is dormant until one of these IOP modules is + // actually loaded. Profile services can omit aliases when the profile + // itself is the explicit compatibility contract. + [[nodiscard]] virtual std::span moduleAliases() const + { + return {}; + } virtual void reset() = 0; [[nodiscard]] virtual RpcAbi selectRpcAbi(const RpcAbiRequest &request) const diff --git a/ps2xIOP/src/iop_subsystem.cpp b/ps2xIOP/src/iop_subsystem.cpp index e32fcb8..fbfe7e1 100644 --- a/ps2xIOP/src/iop_subsystem.cpp +++ b/ps2xIOP/src/iop_subsystem.cpp @@ -1,8 +1,10 @@ #include "ps2x/iop/iop_subsystem.h" #include "iop_service.h" +#include "iop_module_manager.h" #include "emulator/iop_emulator.h" #include "plugin_loader.h" +#include "ps2x/iop/ps2_path.h" #include #include @@ -69,11 +71,39 @@ namespace ps2x::iop { public: explicit Impl(IopHost &hostRef) - : host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles()), emulator(hostRef) + : host(hostRef), + pluginCatalog(hostRef), + coreServices(detail::createCoreServices(hostRef)), + profiles(detail::createBuiltinProfiles()), + emulator(hostRef) { + refreshServiceModuleKeys(); rebuildRoutes(); } + bool serviceActive(const detail::IopService &service) const + { + return moduleManager.isLoaded(service.moduleAliases()); + } + + void refreshServiceModuleKeys() + { + std::vector keys; + auto collect = [&](const detail::ServiceList &services) + { + for (const auto &service : services) + { + if (!service) + continue; + for (std::string_view alias : service->moduleAliases()) + keys.emplace_back(alias); + } + }; + collect(coreServices); + collect(profileServices); + moduleManager.setServiceModuleKeys(std::move(keys)); + } + void rebuildRoutes() { routes.clear(); @@ -82,7 +112,7 @@ namespace ps2x::iop std::unordered_map layer; for (const auto &service : services) { - if (!service) + if (!service || !serviceActive(*service)) { continue; } @@ -119,6 +149,7 @@ namespace ps2x::iop std::string activeProvider; std::string lastError; bool routesValid = true; + detail::IopModuleManager moduleManager; detail::IopEmulator emulator; }; @@ -180,6 +211,7 @@ namespace ps2x::iop { *error = m_impl->lastError; } + m_impl->refreshServiceModuleKeys(); m_impl->rebuildRoutes(); return false; } @@ -199,6 +231,7 @@ namespace ps2x::iop { *error = m_impl->lastError; } + m_impl->refreshServiceModuleKeys(); m_impl->rebuildRoutes(); return false; } @@ -209,11 +242,13 @@ namespace ps2x::iop { *error = m_impl->lastError; } + m_impl->refreshServiceModuleKeys(); m_impl->rebuildRoutes(); return false; } } + m_impl->refreshServiceModuleKeys(); m_impl->rebuildRoutes(); if (!m_impl->routesValid) { @@ -221,6 +256,7 @@ namespace ps2x::iop m_impl->profileServices.clear(); m_impl->activeProfile.clear(); m_impl->activeProvider.clear(); + m_impl->refreshServiceModuleKeys(); m_impl->rebuildRoutes(); m_impl->lastError = routeError; if (error) @@ -236,6 +272,7 @@ namespace ps2x::iop void IopSubsystem::reset() { + m_impl->moduleManager.reset(); for (auto &service : m_impl->coreServices) { if (service) @@ -251,11 +288,31 @@ namespace ps2x::iop } } m_impl->emulator.reset(); + m_impl->refreshServiceModuleKeys(); + m_impl->rebuildRoutes(); } ModuleLoadResult IopSubsystem::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize) { - return m_impl->emulator.loadModule(path, arguments, argumentSize); + const ParsedPs2Path parsed = parsePs2Path(path); + if (!parsed) + return {true, -1, -1}; + + if (parsed.device != Ps2PathDevice::Rom0) + { + ModuleLoadResult physical = m_impl->emulator.loadModule(path, arguments, argumentSize); + if (physical.moduleId > 0) + { + m_impl->moduleManager.observePhysicalLoad(physical.moduleId, path); + m_impl->rebuildRoutes(); + return physical; + } + } + + ModuleLoadResult hle = m_impl->moduleManager.loadHle(path); + if (hle.moduleId > 0) + m_impl->rebuildRoutes(); + return hle; } ModuleLoadResult IopSubsystem::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) @@ -265,7 +322,16 @@ namespace ps2x::iop bool IopSubsystem::stopModule(int32_t moduleId, int32_t *result) { - return m_impl->emulator.stopModule(moduleId, result); + if (m_impl->moduleManager.stopHle(moduleId, result)) + { + m_impl->rebuildRoutes(); + return true; + } + if (!m_impl->emulator.stopModule(moduleId, result)) + return false; + m_impl->moduleManager.observePhysicalStop(moduleId); + m_impl->rebuildRoutes(); + return true; } void IopSubsystem::runEeCycles(uint64_t eeCycles) noexcept @@ -277,7 +343,7 @@ namespace ps2x::iop { for (const auto &service : m_impl->profileServices) { - if (service) + if (service && m_impl->serviceActive(*service)) { const RpcAbi selected = service->selectRpcAbi(request); if (selected != RpcAbi::RuntimeDefault) @@ -288,7 +354,7 @@ namespace ps2x::iop } for (const auto &service : m_impl->coreServices) { - if (service) + if (service && m_impl->serviceActive(*service)) { const RpcAbi selected = service->selectRpcAbi(request); if (selected != RpcAbi::RuntimeDefault) @@ -341,14 +407,14 @@ namespace ps2x::iop { for (auto &service : m_impl->coreServices) { - if (service) + if (service && m_impl->serviceActive(*service)) { service->onSifTransfer(transfer); } } for (auto &service : m_impl->profileServices) { - if (service) + if (service && m_impl->serviceActive(*service)) { service->onSifTransfer(transfer); } @@ -356,6 +422,36 @@ namespace ps2x::iop m_impl->emulator.onSifTransfer(transfer); } + uint32_t IopSubsystem::allocateMemory(uint32_t size, uint32_t alignment) + { + return m_impl->emulator.allocateMemory(size, alignment); + } + + bool IopSubsystem::freeMemory(uint32_t address) + { + return m_impl->emulator.freeMemory(address); + } + + bool IopSubsystem::readMemory(uint32_t address, void *destination, size_t size) const + { + return m_impl->emulator.readMemory(address, destination, size); + } + + bool IopSubsystem::writeMemory(uint32_t address, const void *source, size_t size) + { + return m_impl->emulator.writeMemory(address, source, size); + } + + bool IopSubsystem::zeroMemory(uint32_t address, size_t size) + { + return m_impl->emulator.zeroMemory(address, size); + } + + bool IopSubsystem::isMemoryRange(uint32_t address, size_t size) const + { + return m_impl->emulator.isMemoryRange(address, size); + } + DebugSnapshot IopSubsystem::debugSnapshot() const { DebugSnapshot snapshot; @@ -384,6 +480,7 @@ namespace ps2x::iop row.name = service->name(); row.sids.assign(service->sids().begin(), service->sids().end()); row.profileSpecific = profileSpecific; + row.active = m_impl->serviceActive(*service); service->appendDebugMetrics(row.metrics); snapshot.services.push_back(std::move(row)); } diff --git a/ps2xIOP/src/modules/cri_dtx.cpp b/ps2xIOP/src/modules/cri_dtx.cpp index 55386f5..df74f8f 100644 --- a/ps2xIOP/src/modules/cri_dtx.cpp +++ b/ps2xIOP/src/modules/cri_dtx.cpp @@ -187,7 +187,7 @@ namespace ps2x::iop::detail uint32_t normalizedSource = 0u; uint32_t normalizedDestination = 0u; if (!m_host.normalizeGuestAddress(transfer.sourceAddress, normalizedSource) || - !m_host.normalizeGuestAddress(transfer.destinationAddress, normalizedDestination)) + !m_host.normalizeIopAddress(transfer.destinationAddress, normalizedDestination)) { return; } @@ -333,6 +333,13 @@ namespace ps2x::iop::detail return normalized; } + uint32_t normalizeIopAddress(uint32_t address) const + { + uint32_t normalized = 0u; + (void)m_host.normalizeIopAddress(address, normalized); + return normalized; + } + RpcResult handleCreateTransport(const RpcRequest &request, RpcResult result) { uint32_t dtxId = 0u; @@ -351,7 +358,7 @@ namespace ps2x::iop::detail } const uint32_t normalizedEeWorkAddress = normalizeAddress(eeWorkAddress); - const uint32_t normalizedIopWorkAddress = normalizeAddress(iopWorkAddress); + const uint32_t normalizedIopWorkAddress = normalizeIopAddress(iopWorkAddress); uint32_t remoteHandle = 0u; { diff --git a/ps2xIOP/src/modules/dbcman.cpp b/ps2xIOP/src/modules/dbcman.cpp index 6c5e63d..5fc356b 100644 --- a/ps2xIOP/src/modules/dbcman.cpp +++ b/ps2xIOP/src/modules/dbcman.cpp @@ -33,6 +33,11 @@ namespace ps2x::iop::detail return kSids; } + [[nodiscard]] std::span moduleAliases() const override + { + return kModuleAliases; + } + void reset() override { std::lock_guard lock(m_mutex); @@ -99,6 +104,7 @@ namespace ps2x::iop::detail private: inline static constexpr std::array kSids{kDbcManSid}; + inline static constexpr std::array kModuleAliases{"dbcman", "dbcm", "dbcmserv"}; IopHost &m_host; mutable std::mutex m_mutex; diff --git a/ps2xIOP/src/modules/libsd.cpp b/ps2xIOP/src/modules/libsd.cpp index 56d9a5c..974b86c 100644 --- a/ps2xIOP/src/modules/libsd.cpp +++ b/ps2xIOP/src/modules/libsd.cpp @@ -27,6 +27,11 @@ namespace ps2x::iop::detail return kSids; } + [[nodiscard]] std::span moduleAliases() const override + { + return kModuleAliases; + } + void reset() override { } @@ -51,6 +56,7 @@ namespace ps2x::iop::detail private: inline static constexpr std::array kSids{kLibSdSid}; + inline static constexpr std::array kModuleAliases{"libsd"}; IopHost &m_host; }; diff --git a/ps2xIOP/src/modules/mcserv.cpp b/ps2xIOP/src/modules/mcserv.cpp index b3a71c2..86db416 100644 --- a/ps2xIOP/src/modules/mcserv.cpp +++ b/ps2xIOP/src/modules/mcserv.cpp @@ -82,43 +82,75 @@ namespace ps2x::iop::detail flavor = Flavor::NewXmcserv; switch (function) { - case 0xFEu: return Operation::Init; - case 0x01u: return Operation::GetInfo; - case 0x02u: return Operation::Open; - case 0x03u: return Operation::Close; - case 0x04u: return Operation::Seek; - case 0x05u: return Operation::Read; - case 0x06u: return Operation::Write; - case 0x0Au: return Operation::Flush; - case 0x0Cu: return Operation::Chdir; - case 0x0Du: return Operation::GetDir; - case 0x0Eu: return Operation::SetInfo; - case 0x0Fu: return Operation::Delete; - case 0x10u: return Operation::Format; - case 0x11u: return Operation::Unformat; - case 0x12u: return Operation::GetEnt; - case 0x14u: return Operation::ChangePriority; - default: break; + case 0xFEu: + return Operation::Init; + case 0x01u: + return Operation::GetInfo; + case 0x02u: + return Operation::Open; + case 0x03u: + return Operation::Close; + case 0x04u: + return Operation::Seek; + case 0x05u: + return Operation::Read; + case 0x06u: + return Operation::Write; + case 0x0Au: + return Operation::Flush; + case 0x0Cu: + return Operation::Chdir; + case 0x0Du: + return Operation::GetDir; + case 0x0Eu: + return Operation::SetInfo; + case 0x0Fu: + return Operation::Delete; + case 0x10u: + return Operation::Format; + case 0x11u: + return Operation::Unformat; + case 0x12u: + return Operation::GetEnt; + case 0x14u: + return Operation::ChangePriority; + default: + break; } flavor = Flavor::OldMcserv; switch (function) { - case 0x70u: return Operation::Init; - case 0x71u: return Operation::Open; - case 0x72u: return Operation::Close; - case 0x73u: return Operation::Read; - case 0x74u: return Operation::Write; - case 0x75u: return Operation::Seek; - case 0x76u: return Operation::GetDir; - case 0x77u: return Operation::Format; - case 0x78u: return Operation::GetInfo; - case 0x79u: return Operation::Delete; - case 0x7Au: return Operation::Flush; - case 0x7Bu: return Operation::Chdir; - case 0x7Cu: return Operation::SetInfo; - case 0x80u: return Operation::Unformat; - default: return Operation::Unknown; + case 0x70u: + return Operation::Init; + case 0x71u: + return Operation::Open; + case 0x72u: + return Operation::Close; + case 0x73u: + return Operation::Read; + case 0x74u: + return Operation::Write; + case 0x75u: + return Operation::Seek; + case 0x76u: + return Operation::GetDir; + case 0x77u: + return Operation::Format; + case 0x78u: + return Operation::GetInfo; + case 0x79u: + return Operation::Delete; + case 0x7Au: + return Operation::Flush; + case 0x7Bu: + return Operation::Chdir; + case 0x7Cu: + return Operation::SetInfo; + case 0x80u: + return Operation::Unformat; + default: + return Operation::Unknown; } } @@ -136,6 +168,7 @@ namespace ps2x::iop::detail [[nodiscard]] std::string_view name() const override { return "MCSERV"; } [[nodiscard]] std::span sids() const override { return m_sids; } + [[nodiscard]] std::span moduleAliases() const override { return m_moduleAliases; } void reset() override { @@ -245,8 +278,7 @@ namespace ps2x::iop::detail (void)m_host.writeGuest(receive.address, &result, sizeof(result)); if (receive.size > sizeof(result)) { - (void)m_host.zeroGuest(receive.address + sizeof(result), - receive.size - sizeof(result)); + (void)m_host.zeroGuest(receive.address + sizeof(result), receive.size - sizeof(result)); } } @@ -270,8 +302,7 @@ namespace ps2x::iop::detail uint32_t sendAddress, const NameParameter ¶meter) { - const uint32_t nameAddress = - sendAddress + static_cast(offsetof(NameParameter, name)); + const uint32_t nameAddress = sendAddress + static_cast(offsetof(NameParameter, name)); const uint32_t port = static_cast(parameter.port); const uint32_t slot = static_cast(parameter.slot); switch (operation) @@ -281,12 +312,9 @@ namespace ps2x::iop::detail { return call(MemoryCardOperation::Mkdir, port, slot, nameAddress); } - return call(MemoryCardOperation::Open, - port, slot, nameAddress, - static_cast(parameter.flags)); + return call(MemoryCardOperation::Open, port, slot, nameAddress, static_cast(parameter.flags)); case Operation::Chdir: - return call(MemoryCardOperation::Chdir, - port, slot, nameAddress, parameter.pointer); + return call(MemoryCardOperation::Chdir, port, slot, nameAddress, parameter.pointer); case Operation::SetInfo: return call(MemoryCardOperation::SetFileInfo, port, slot, nameAddress); case Operation::Delete: @@ -302,9 +330,7 @@ namespace ps2x::iop::detail } } - int32_t handleDescriptorOperation(Operation operation, - Flavor flavor, - const DescriptorParameter ¶meter) + int32_t handleDescriptorOperation(Operation operation, Flavor flavor, const DescriptorParameter ¶meter) { switch (operation) { @@ -392,6 +418,7 @@ namespace ps2x::iop::detail mutable std::mutex m_mutex; uint32_t m_unknownRpcLogCount = 0u; const std::array m_sids = {kMcservSid, kMcservDev9Sid}; + const std::array m_moduleAliases = {"mcserv", "xmcserv"}; }; } diff --git a/ps2xIOP/src/modules/tsnddrv.cpp b/ps2xIOP/src/modules/tsnddrv.cpp index 155970c..01d9999 100644 --- a/ps2xIOP/src/modules/tsnddrv.cpp +++ b/ps2xIOP/src/modules/tsnddrv.cpp @@ -52,6 +52,19 @@ namespace ps2x::iop::detail return host.writeGuest(address, &value, sizeof(value)); } + template + bool readIopPod(const IopHost &host, uint32_t address, T &value) + { + value = {}; + return host.readIopMemory(address, &value, sizeof(value)); + } + + template + bool writeIopPod(IopHost &host, uint32_t address, const T &value) + { + return host.writeIopMemory(address, &value, sizeof(value)); + } + template bool hasAnyNonZero(const std::array &values) { @@ -174,9 +187,7 @@ namespace ps2x::iop::detail handleCommandBuffer(request.send); result.handled = true; } - else if (request.sid == kStateSid && - (request.function == kGetStatusAddressFunction || - request.function == kGetAddressTableFunction)) + else if (request.sid == kStateSid && (request.function == kGetStatusAddressFunction || request.function == kGetAddressTableFunction)) { uint32_t responseAddress = 0u; { @@ -195,8 +206,7 @@ namespace ps2x::iop::detail (void)writeGuestPod(m_host, request.receive.address, responseAddress); if (request.receive.size > sizeof(uint32_t)) { - (void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t), - request.receive.size - sizeof(uint32_t)); + (void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t), request.receive.size - sizeof(uint32_t)); } result.resultAddress = request.receive.address; } @@ -210,7 +220,8 @@ namespace ps2x::iop::detail const auto rule = std::find_if( m_bindings.completionRules.begin(), m_bindings.completionRules.end(), - [&](const TsnddrvCompletionRule &candidate) { + [&](const TsnddrvCompletionRule &candidate) + { return candidate.eeFunction == request.endFunction; }); if (rule != m_bindings.completionRules.end()) @@ -223,9 +234,7 @@ namespace ps2x::iop::detail if (rule->clearBusy) { constexpr uint32_t idle = 0u; - (void)writeGuestPod(m_host, - m_bindings.busyFlagAddress, - idle); + (void)writeGuestPod(m_host, m_bindings.busyFlagAddress, idle); } } } @@ -280,15 +289,10 @@ namespace ps2x::iop::detail { const TsnddrvGuestArena &arena = m_bindings.arena; const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment); - const uint32_t addressTableAddress = - alignUp(statusAddress + kStatusSize, arena.tableAlignment); - const uint32_t hdBaseAddress = - alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)), - arena.storageAlignment); - const uint32_t sqBaseAddress = - alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment); - const uint32_t dataBaseAddress = - alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment); + const uint32_t addressTableAddress = alignUp(statusAddress + kStatusSize, arena.tableAlignment); + const uint32_t hdBaseAddress = alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)), arena.storageAlignment); + const uint32_t sqBaseAddress = alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment); + const uint32_t dataBaseAddress = alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment); const uint32_t storageEnd = dataBaseAddress + arena.dataBytes; if (storageEnd > arena.limit) { @@ -313,23 +317,17 @@ namespace ps2x::iop::detail if (!m_state.initialized) { - if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) || - !m_host.zeroGuest(m_state.addressTableAddress, - kAddressTableEntries * sizeof(uint32_t)) || - !m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize)) + if (!m_host.zeroIopMemory(m_state.statusAddress, kStatusSize) || + !m_host.zeroIopMemory(m_state.addressTableAddress, kAddressTableEntries * sizeof(uint32_t)) || + !m_host.zeroIopMemory(m_state.storageBaseAddress, m_state.storageSize)) { return false; } - if (!writeGuestPod(m_host, - m_state.addressTableAddress + (0u * sizeof(uint32_t)), - m_state.hdBaseAddress) || - !writeGuestPod(m_host, - m_state.addressTableAddress + (1u * sizeof(uint32_t)), - m_state.sqBaseAddress) || - !writeGuestPod(m_host, - m_state.addressTableAddress + (2u * sizeof(uint32_t)), - m_state.dataBaseAddress)) + if ( + !writeIopPod(m_host, m_state.addressTableAddress + (0u * sizeof(uint32_t)), m_state.hdBaseAddress) || + !writeIopPod(m_host, m_state.addressTableAddress + (1u * sizeof(uint32_t)), m_state.sqBaseAddress) || + !writeIopPod(m_host, m_state.addressTableAddress + (2u * sizeof(uint32_t)), m_state.dataBaseAddress)) { return false; } @@ -339,9 +337,7 @@ namespace ps2x::iop::detail return true; } - int16_t checkValue(bool seTable, - uint32_t index, - uint32_t count) const + int16_t checkValue(bool seTable, uint32_t index, uint32_t count) const { if (index >= count) { @@ -352,10 +348,7 @@ namespace ps2x::iop::detail { const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress; int16_t value = 0; - if (readGuestPod(m_host, - base + (index * sizeof(int16_t)), - value) && - value != 0) + if (readGuestPod(m_host, base + (index * sizeof(int16_t)), value) && value != 0) { return value; } @@ -370,19 +363,13 @@ namespace ps2x::iop::detail { std::array seValues{}; std::array midiValues{}; - const bool seReadable = m_host.readGuest(candidate.seAddress, - seValues.data(), - sizeof(seValues)); - const bool midiReadable = m_host.readGuest(candidate.midiAddress, - midiValues.data(), - sizeof(midiValues)); + const bool seReadable = m_host.readGuest(candidate.seAddress, seValues.data(), sizeof(seValues)); + const bool midiReadable = m_host.readGuest(candidate.midiAddress, midiValues.data(), sizeof(midiValues)); if (seReadable && midiReadable && !firstReadable) { firstReadable = &candidate; } - const bool looksLive = - (seReadable && hasAnyNonZero(seValues)) || - (midiReadable && hasAnyNonZero(midiValues)); + const bool looksLive = (seReadable && hasAnyNonZero(seValues)) || (midiReadable && hasAnyNonZero(midiValues)); if (seReadable && midiReadable && looksLive) { seBase = candidate.seAddress; @@ -409,35 +396,23 @@ namespace ps2x::iop::detail return; } - auto backfillSlots = [&](uint32_t statusOffset, - uint32_t compatBase, - uint32_t slotCount) + auto backfillSlots = [&](uint32_t statusOffset, uint32_t compatBase, uint32_t slotCount) { for (uint32_t slot = 0u; slot < slotCount; ++slot) { int16_t liveValue = 0; - if (!readGuestPod(m_host, - m_state.statusAddress + statusOffset + - (slot * sizeof(int16_t)), - liveValue) || - liveValue != 0) + if (!readIopPod(m_host, m_state.statusAddress + statusOffset + (slot * sizeof(int16_t)), liveValue) || liveValue != 0) { continue; } int16_t compatValue = 0; - if (!readGuestPod(m_host, - compatBase + (slot * sizeof(int16_t)), - compatValue) || - compatValue == 0) + if (!readGuestPod(m_host, compatBase + (slot * sizeof(int16_t)), compatValue) || compatValue == 0) { continue; } - (void)writeGuestPod(m_host, - m_state.statusAddress + statusOffset + - (slot * sizeof(int16_t)), - compatValue); + (void)writeIopPod(m_host, m_state.statusAddress + statusOffset + (slot * sizeof(int16_t)), compatValue); } }; @@ -458,28 +433,23 @@ namespace ps2x::iop::detail { const uint32_t port = command[1] & 0x0Fu; uint16_t midiInfo = 0u; - (void)readGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); + (void)readIopPod(m_host, + m_state.statusAddress + kMidiInfoOffset, + midiInfo); midiInfo = static_cast(midiInfo | static_cast(1u << port)); - (void)writeGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); + (void)writeIopPod(m_host, + m_state.statusAddress + kMidiInfoOffset, + midiInfo); break; } case 0x21u: // SdrBgmStop { const uint32_t port = command[1] & 0x0Fu; uint16_t midiInfo = 0u; - (void)readGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); - midiInfo = static_cast(midiInfo & - ~static_cast(1u << port)); - (void)writeGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); + (void)readIopPod(m_host, m_state.statusAddress + kMidiInfoOffset, midiInfo); + midiInfo = static_cast(midiInfo & ~static_cast(1u << port)); + (void)writeIopPod(m_host, m_state.statusAddress + kMidiInfoOffset, midiInfo); break; } case 0x28u: // SdrHDDataSet @@ -490,10 +460,7 @@ namespace ps2x::iop::detail break; } const int16_t checksum = checkValue(false, port, 4u); - (void)writeGuestPod(m_host, - m_state.statusAddress + kMidiSumOffset + - (port * sizeof(int16_t)), - checksum); + (void)writeIopPod(m_host, m_state.statusAddress + kMidiSumOffset + (port * sizeof(int16_t)), checksum); break; } case 0x29u: // SdrHDDataSet2 @@ -504,15 +471,11 @@ namespace ps2x::iop::detail break; } const int16_t checksum = checkValue(true, port, 5u); - (void)writeGuestPod(m_host, - m_state.statusAddress + kSeSumOffset + - (port * sizeof(int16_t)), - checksum); + (void)writeIopPod(m_host, m_state.statusAddress + kSeSumOffset + (port * sizeof(int16_t)), checksum); break; } case 0x10u: // SdrSeAllStop - (void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset, - 6u * sizeof(uint16_t)); + (void)m_host.zeroIopMemory(m_state.statusAddress + kSeInfoOffset, 6u * sizeof(uint16_t)); break; default: break; @@ -569,12 +532,13 @@ namespace ps2x::iop::detail std::unique_ptr createTsnddrvService(IopHost &host, TsnddrvBindings bindings) { - const auto isPowerOfTwo = [](uint32_t value) { + const auto isPowerOfTwo = [](uint32_t value) + { return value != 0u && (value & (value - 1u)) == 0u; }; - const auto alignUp64 = [](uint64_t value, uint32_t alignment) { - return (value + (alignment - 1u)) & - ~static_cast(alignment - 1u); + const auto alignUp64 = [](uint64_t value, uint32_t alignment) + { + return (value + (alignment - 1u)) & ~static_cast(alignment - 1u); }; const TsnddrvGuestArena &arena = bindings.arena; @@ -590,8 +554,7 @@ namespace ps2x::iop::detail } uint64_t end = alignUp64(arena.base, arena.statusAlignment) + kStatusSize; - end = alignUp64(end, arena.tableAlignment) + - (kAddressTableEntries * sizeof(uint32_t)); + end = alignUp64(end, arena.tableAlignment) + (kAddressTableEntries * sizeof(uint32_t)); end = alignUp64(end, arena.storageAlignment) + arena.hdBytes; end = alignUp64(end, arena.storageAlignment) + arena.sqBytes; end = alignUp64(end, arena.storageAlignment) + arena.dataBytes; diff --git a/ps2xIOP/src/ps2_path.cpp b/ps2xIOP/src/ps2_path.cpp new file mode 100644 index 0000000..420f053 --- /dev/null +++ b/ps2xIOP/src/ps2_path.cpp @@ -0,0 +1,123 @@ +#include "ps2x/iop/ps2_path.h" + +#include +#include + +namespace ps2x::iop +{ + namespace + { + std::string lowerAscii(std::string_view value) + { + std::string result(value); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch) + { return static_cast(std::tolower(ch)); }); + return result; + } + + void normalizeSuffix(std::string &suffix) + { + std::replace(suffix.begin(), suffix.end(), '\\', '/'); + while (!suffix.empty() && suffix.front() == '/') + suffix.erase(suffix.begin()); + + const size_t semicolon = suffix.rfind(';'); + if (semicolon == std::string::npos || semicolon + 1u == suffix.size()) + return; + + const bool numeric = std::all_of(suffix.begin() + static_cast(semicolon + 1u), + suffix.end(), + [](unsigned char ch) + { return std::isdigit(ch) != 0; }); + if (numeric) + suffix.erase(semicolon); + } + } + + ParsedPs2Path parsePs2Path(std::string_view value) + { + ParsedPs2Path result; + if (value.empty()) + return result; + + const std::string lower = lowerAscii(value); + size_t prefixLength = 0u; + if (lower.rfind("host0:", 0u) == 0u) + { + result.device = Ps2PathDevice::Host; + result.deviceName = "host0"; + prefixLength = 6u; + } + else if (lower.rfind("host:", 0u) == 0u) + { + result.device = Ps2PathDevice::Host; + result.deviceName = "host"; + prefixLength = 5u; + } + else if (lower.rfind("cdrom0:", 0u) == 0u) + { + result.device = Ps2PathDevice::Cdrom; + result.deviceName = "cdrom0"; + prefixLength = 7u; + } + else if (lower.rfind("cdrom:", 0u) == 0u) + { + result.device = Ps2PathDevice::Cdrom; + result.deviceName = "cdrom"; + prefixLength = 6u; + } + else if (lower.rfind("mc0:", 0u) == 0u) + { + result.device = Ps2PathDevice::MemoryCard0; + result.deviceName = "mc0"; + prefixLength = 4u; + } + else if (lower.rfind("rom0:", 0u) == 0u) + { + result.device = Ps2PathDevice::Rom0; + result.deviceName = "rom0"; + prefixLength = 5u; + } + else if (value.size() > 2u && std::isalpha(static_cast(value[0])) && + value[1] == ':' && (value[2] == '/' || value[2] == '\\')) + { + result.device = Ps2PathDevice::NativeHost; + result.deviceName = "native"; + } + else if (value.find(':') != std::string_view::npos) + { + // TODO maybe log an error here, but don't fail the parse. This is a non-standard device name. + return result; + } + else + { + result.device = Ps2PathDevice::Cdrom; + result.deviceName = "cdrom0"; + } + + result.path.assign(value.substr(prefixLength)); + if (result.device != Ps2PathDevice::NativeHost) + normalizeSuffix(result.path); + return result; + } + + std::string ps2PathLeafKey(const ParsedPs2Path &parsed) + { + if (!parsed) + return {}; + std::string path = parsed.path; + std::replace(path.begin(), path.end(), '\\', '/'); + const size_t slash = path.find_last_of('/'); + if (slash != std::string::npos) + path.erase(0u, slash + 1u); + path = lowerAscii(path); + if (path.size() > 4u && path.ends_with(".irx")) + path.resize(path.size() - 4u); + return path; + } + + std::string ps2PathLeafKey(std::string_view path) + { + return ps2PathLeafKey(parsePs2Path(path)); + } +} diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp new file mode 100644 index 0000000..3035618 --- /dev/null +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -0,0 +1,338 @@ +#include "emulator/core/iop_cpu.h" +#include "emulator/core/iop_kernel.h" +#include "emulator/core/iop_memory.h" +#include "emulator/imports/iop_cdvd.h" +#include "emulator/imports/iop_imports.h" +#include "emulator/imports/iop_loadcore.h" +#include "emulator/imports/iop_timrman.h" +#include "emulator/services/iop_rpc.h" +#include "ps2x/iop/iop_host.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace ps2x::iop; + using namespace ps2x::iop::detail; + + constexpr uint32_t kExportMagic = 0x41C00000u; + constexpr int32_t kLibraryNotFound = -213; + constexpr int32_t kIllegalLibrary = -214; + + class NullHost : public IopHost + { + public: + bool readGuest(uint32_t, void *, size_t) const override { return false; } + bool writeGuest(uint32_t, const void *, size_t) override { return false; } + bool zeroGuest(uint32_t, size_t) override { return false; } + bool normalizeGuestAddress(uint32_t, uint32_t &) const override { return false; } + 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) const override { return {}; } + 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) override {} + }; + + class CdRootHost final : public NullHost + { + public: + explicit CdRootHost(std::filesystem::path rootPath) + : root(std::move(rootPath)) + { + } + + std::string hostPath(HostPathKind kind) const override + { + return kind == HostPathKind::CdRoot ? root.string() : std::string{}; + } + + private: + std::filesystem::path root; + }; + + class RecordingExecutor final : public IopGuestExecutor + { + public: + uint32_t executeGuestFunction(uint32_t address, + uint32_t a0, + uint32_t, + uint32_t, + uint32_t, + uint32_t gp) override + { + ++calls; + lastAddress = address; + lastArgument = a0; + lastGp = gp; + return callbackResult; + } + + uint32_t callbackResult = 0u; + uint32_t calls = 0u; + uint32_t lastAddress = 0u; + uint32_t lastArgument = 0u; + uint32_t lastGp = 0u; + }; + + bool expect(bool condition, std::string_view message) + { + if (condition) + return true; + std::cerr << "FAIL: " << message << '\n'; + return false; + } + + bool testLoadcoreRebootLibraryMode() + { + IopMemory memory; + IopImportRegistry imports(memory); + IopLoadcore loadcore(memory, imports); + + IopCpuState cpu{}; + cpu.gpr[4] = 0u; + cpu.gpr[5] = 2u; + if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 was not handled") || + !expect(static_cast(cpu.gpr[2]) == kIllegalLibrary, + "loadcore:27 did not reject a null export table")) + return false; + + constexpr uint32_t table = 0x1000u; + memory.write32(table, kExportMagic); + memory.write16(table + 8u, 0x0101u); + memory.write16(table + 10u, 0x1234u); + const char name[8] = {'t', 'e', 's', 't', 'l', 'i', 'b', '\0'}; + (void)memory.writeRam(table + 12u, name, sizeof(name)); + memory.write32(table + 20u, 0u); + + cpu = {}; + cpu.gpr[4] = table; + cpu.gpr[5] = 2u; + if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a valid export table") || + !expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a valid export table") || + !expect(memory.read16(table + 10u) == 0x1232u, + "loadcore:27 did not replace only export mode bits 1 and 2")) + return false; + + constexpr uint32_t invalidTable = 0x1100u; + memory.write32(invalidTable, 0xDEADBEEFu); + cpu = {}; + cpu.gpr[4] = invalidTable; + cpu.gpr[5] = 6u; + if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 did not consume an invalid-table call") || + !expect(static_cast(cpu.gpr[2]) == kLibraryNotFound, + "loadcore:27 returned the wrong invalid-table error")) + return false; + + if (!expect(imports.registerExportTable(table), "test export table did not register")) + return false; + memory.write32(table, 0u); + cpu = {}; + cpu.gpr[4] = table; + cpu.gpr[5] = 6u; + return expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a registered table") && + expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a registered table") && + expect(memory.read16(table + 10u) == 0x1236u, + "loadcore:27 did not update a registered table's mode"); + } + + bool pollEvent(IopKernel &kernel, int eventId, uint32_t bits, uint32_t resultAddress, int32_t expected) + { + IopCpuState cpu{}; + cpu.gpr[4] = static_cast(eventId); + cpu.gpr[5] = bits; + cpu.gpr[6] = 0u; // WEF_AND + cpu.gpr[7] = resultAddress; + return expect(kernel.dispatchEventImport(11u, cpu), "PollEventFlag was not handled") && + expect(static_cast(cpu.gpr[2]) == expected, "PollEventFlag returned an unexpected result"); + } + + bool testCdvdSpecialControl() + { + NullHost host; + IopMemory memory; + IopKernel kernel(memory); + kernel.reset(); + IopCdvd cdvd(host, memory, kernel); + cdvd.reset(); + + constexpr uint32_t param = 0x2000u; + constexpr uint32_t eventResult = 0x2010u; + IopCpuState cpu{}; + cpu.gpr[4] = static_cast(-11); // sceCdSC: return cdvdman interrupt event flag + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "cdvdman:50 was not handled") || + !expect(static_cast(cpu.gpr[2]) > 0, "sceCdSC(-11) did not return a valid event flag")) + return false; + const int eventId = static_cast(cpu.gpr[2]); + + if (!pollEvent(kernel, eventId, 0x29u, eventResult, 0) || + !expect(memory.read32(eventResult) == 0x29u, "cdvdman event flag did not start with bits 0x29")) + return false; + + IopCpuState clear{}; + clear.gpr[4] = static_cast(eventId); + clear.gpr[5] = ~0x29u; + if (!expect(kernel.dispatchEventImport(8u, clear), "ClearEventFlag was not handled") || + !pollEvent(kernel, eventId, 0x29u, eventResult, -418)) + return false; + + cpu = {}; + cpu.gpr[4] = 0x12345u; + if (!expect(cdvd.dispatchImport(7u, cpu), "sceCdSeek was not handled") || + !pollEvent(kernel, eventId, 0x29u, eventResult, 0)) + return false; + + memory.write8(param, 0x30u); + cpu = {}; + cpu.gpr[4] = static_cast(-2); + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-2) was not handled") || + !expect(cpu.gpr[2] == 0x30u, "sceCdSC(-2) did not store the low-byte error")) + return false; + + memory.write32(param, 0u); + cpu = {}; + cpu.gpr[4] = static_cast(-1); + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-1) was not handled") || + !expect(cpu.gpr[2] == 0u, "sceCdSC(-1) returned the wrong initial stream state") || + !expect(memory.read32(param) == 0x30u, "sceCdSC(-1) did not publish the last error")) + return false; + + cpu = {}; + cpu.gpr[4] = 2u; + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(2) was not handled") || + !expect(cpu.gpr[2] == 2u, "sceCdSC(2) did not update the stream state")) + return false; + + cpu = {}; + cpu.gpr[4] = static_cast(-1); + cpu.gpr[5] = param; + return expect(cdvd.dispatchImport(50u, cpu), "second sceCdSC(-1) was not handled") && + expect(cpu.gpr[2] == 2u, "sceCdSC(-1) did not preserve the stream state"); + } + + bool testCdvdSearchFile() + { + const auto suffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path root = + std::filesystem::temp_directory_path() / ("ps2x-iop-cdvd-search-" + suffix); + const std::filesystem::path movieDirectory = root / "MOVIE"; + const std::filesystem::path moviePath = movieDirectory / "OPENING.PSS"; + std::error_code error; + std::filesystem::create_directories(movieDirectory, error); + if (!expect(!error, "could not create the temporary CD root")) + return false; + { + std::ofstream movie(moviePath, std::ios::binary); + movie.write("PSS!", 4); + } + + CdRootHost host(root); + IopMemory memory; + IopKernel kernel(memory); + kernel.reset(); + IopCdvd cdvd(host, memory, kernel); + cdvd.reset(); + + constexpr uint32_t resultAddress = 0x2400u; + constexpr uint32_t pathAddress = 0x2480u; + const char path[] = "cdrom0:\\movie\\opening.pss;1"; + (void)memory.writeRam(pathAddress, path, sizeof(path)); + + IopCpuState cpu{}; + cpu.gpr[4] = resultAddress; + cpu.gpr[5] = pathAddress; + const bool handled = cdvd.dispatchImport(10u, cpu); + const bool passed = + expect(handled, "cdvdman:10 was not handled") && + expect(cpu.gpr[2] == 1u, "sceCdSearchFile did not find a case-insensitive ISO path") && + expect(memory.read32(resultAddress) >= 20u, "sceCdSearchFile returned an invalid LSN") && + expect(memory.read32(resultAddress + 4u) == 4u, "sceCdSearchFile returned the wrong size") && + expect(memory.readString(resultAddress + 8u, 16u) == "OPENING.PSS", + "sceCdSearchFile returned the wrong file name"); + + std::filesystem::remove_all(root, error); + return passed; + } + + bool testTimrmanPeriodicCallback() + { + IopTimrman timrman; + timrman.reset(); + IopCpuState cpu{}; + + cpu.gpr[4] = 1u; // SYSCLK + cpu.gpr[5] = 32u; + cpu.gpr[6] = 1u; + if (!expect(timrman.dispatchImport(4u, cpu, 100u), "AllocHardTimer was not handled") || + !expect(static_cast(cpu.gpr[2]) > 0, "AllocHardTimer did not allocate a 32-bit timer")) + return false; + const uint32_t timerId = cpu.gpr[2]; + + cpu = {}; + cpu.gpr[4] = timerId; + cpu.gpr[5] = 100u; + cpu.gpr[6] = 0x12340u; + cpu.gpr[7] = 0x45670u; + cpu.gpr[28] = 0x89AB0u; + if (!expect(timrman.dispatchImport(20u, cpu, 100u), "SetTimerHandler was not handled") || + !expect(cpu.gpr[2] == 0u, "SetTimerHandler failed")) + return false; + + cpu = {}; + cpu.gpr[4] = timerId; + cpu.gpr[5] = 1u; + cpu.gpr[6] = 0u; + cpu.gpr[7] = 1u; + if (!expect(timrman.dispatchImport(22u, cpu, 100u), "SetupHardTimer was not handled") || + !expect(cpu.gpr[2] == 0u, "SetupHardTimer failed")) + return false; + + cpu = {}; + cpu.gpr[4] = timerId; + if (!expect(timrman.dispatchImport(23u, cpu, 100u), "StartHardTimer was not handled") || + !expect(cpu.gpr[2] == 0u, "StartHardTimer failed") || + !expect(timrman.nextEventCycle(1000u) == 200u, "timer compare was scheduled at the wrong cycle")) + return false; + + RecordingExecutor executor; + executor.callbackResult = 100u; + timrman.serviceDue(199u, executor); + if (!expect(executor.calls == 0u, "timer callback ran too early")) + return false; + timrman.serviceDue(200u, executor); + return expect(executor.calls == 1u, "timer callback did not run") && + expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") && + expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") && + expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") && + expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare"); + } +} + +int main() +{ + if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() || + !testTimrmanPeriodicCallback()) + return 1; + std::cout << "ps2xIOP import tests passed\n"; + return 0; +} diff --git a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java index 00860e3..694c518 100644 --- a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java +++ b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java @@ -115,7 +115,7 @@ public class ExportPS2Functions extends GhidraScript { "fioWrite", "fioLseek", "fioMkdir", "fioChdir", "fioRmdir", "fioGetstat", "fioRemove", "SetGsCrt", "GsSetCrt", "GsGetIMR", "iGsGetIMR", "GsPutIMR", "iGsPutIMR", "SetVSyncFlag", "SetSyscall", "GsSetVideoMode", "GetOsdConfigParam", "SetOsdConfigParam", - "EnableCache", "DisableCache", "GetRomName", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart", + "EnableCache", "DisableCache", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart", "sceSifLoadModule", "sceSifLoadModuleBuffer", "SetupThread", "EndOfHeap", "GetMemorySize", "Deci2Call", "QueryBootMode", "GetThreadTLS", "Copy", "GetEntryAddress", "RegisterExitHandler", "ret0", "ret1", "reta0", "calloc_r", "free_r", "realloc_r", "memalign_r", "malloc_r", "malloc_extend_top", "malloc_trim_r", "mbtowc_r", "printf_r",