From 81f50cf4e937397ac9ae36f3adb23b4b4f08d26f Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:32:46 -0300 Subject: [PATCH 1/9] Add host file handles to IOP IOMAN --- ps2xIOP/src/emulator/imports/iop_ioman.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.h b/ps2xIOP/src/emulator/imports/iop_ioman.h index 182f786b17..aa2468f11e 100644 --- a/ps2xIOP/src/emulator/imports/iop_ioman.h +++ b/ps2xIOP/src/emulator/imports/iop_ioman.h @@ -1,6 +1,9 @@ #pragma once +#include "ps2x/iop/iop_host.h" + #include +#include #include #include @@ -13,7 +16,7 @@ namespace ps2x::iop::detail class IopIoman { public: - explicit IopIoman(IopMemory &memory) noexcept; + IopIoman(IopHost &host, IopMemory &memory) noexcept; void reset(); [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor); @@ -26,7 +29,16 @@ namespace ps2x::iop::detail std::string name; }; + struct OpenFile + { + uint64_t hostHandle = 0u; + uint64_t offset = 0u; + uint64_t size = 0u; + }; + + IopHost &m_host; IopMemory &m_memory; std::vector m_devices; + std::map m_openFiles; }; } From 7762d9a280ca44c41aa5cc11afae6951a5b4e343 Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:32:49 -0300 Subject: [PATCH 2/9] Implement read-only IOP IOMAN file operations --- ps2xIOP/src/emulator/imports/iop_ioman.cpp | 160 ++++++++++++++++++++- 1 file changed, 158 insertions(+), 2 deletions(-) diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.cpp b/ps2xIOP/src/emulator/imports/iop_ioman.cpp index 3eb41190f5..8ab9cb309f 100644 --- a/ps2xIOP/src/emulator/imports/iop_ioman.cpp +++ b/ps2xIOP/src/emulator/imports/iop_ioman.cpp @@ -8,13 +8,19 @@ namespace ps2x::iop::detail { - IopIoman::IopIoman(IopMemory &memory) noexcept - : m_memory(memory) + IopIoman::IopIoman(IopHost &host, IopMemory &memory) noexcept + : m_host(host), m_memory(memory) { } void IopIoman::reset() { + for (const auto &[fd, file] : m_openFiles) + { + (void)fd; + m_host.closeHostFile(file.hostHandle); + } + m_openFiles.clear(); m_devices.clear(); } @@ -29,6 +35,156 @@ namespace ps2x::iop::detail switch (ordinal) { + case 4: // open + { + constexpr uint32_t kAccessModeMask = 0x0003u; + constexpr uint32_t kReadOnly = 0x0001u; + constexpr uint32_t kCreate = 0x0200u; + constexpr uint32_t kTruncate = 0x0400u; + constexpr uint32_t kExclusive = 0x0800u; + + const std::string guestPath = m_memory.readString(a0, 1024u); + const uint32_t mode = cpu.gpr[5]; + if (guestPath.empty() || + (mode & kAccessModeMask) != kReadOnly || + (mode & (kCreate | kTruncate | kExclusive)) != 0u) + { + setV0(0xFFFFFFFFu); + return true; + } + + const std::string hostPath = m_host.translateGuestPath(guestPath); + if (hostPath.empty()) + { + setV0(0xFFFFFFFFu); + return true; + } + + const uint64_t handle = m_host.openHostFile(hostPath); + if (handle == 0u) + { + setV0(0xFFFFFFFFu); + return true; + } + + uint64_t size = 0u; + if (!m_host.hostFileSize(handle, size)) + { + m_host.closeHostFile(handle); + setV0(0xFFFFFFFFu); + return true; + } + + int32_t fd = -1; + for (int32_t candidate = 3; candidate < 256; ++candidate) + { + if (m_openFiles.find(candidate) == m_openFiles.end()) + { + fd = candidate; + break; + } + } + if (fd < 0) + { + m_host.closeHostFile(handle); + setV0(0xFFFFFFFFu); + return true; + } + + m_openFiles.emplace(fd, OpenFile{handle, 0u, size}); + setV0(static_cast(fd)); + return true; + } + case 5: // close + { + const int32_t fd = static_cast(a0); + const auto file = m_openFiles.find(fd); + if (file == m_openFiles.end()) + { + setV0(0xFFFFFFFFu); + return true; + } + + m_host.closeHostFile(file->second.hostHandle); + m_openFiles.erase(file); + setV0(0u); + return true; + } + case 6: // read + { + const int32_t fd = static_cast(a0); + const auto file = m_openFiles.find(fd); + const uint32_t destination = cpu.gpr[5]; + const uint32_t requested = cpu.gpr[6]; + if (file == m_openFiles.end() || requested > IopMemory::RamSize) + { + setV0(0xFFFFFFFFu); + return true; + } + if (requested == 0u) + { + setV0(0u); + return true; + } + + std::vector buffer(requested); + size_t bytesRead = 0u; + if (!m_host.readHostFile(file->second.hostHandle, + file->second.offset, + buffer.data(), + buffer.size(), + bytesRead) || + bytesRead > buffer.size() || + !m_memory.writeRam(destination, buffer.data(), bytesRead)) + { + setV0(0xFFFFFFFFu); + return true; + } + + file->second.offset += bytesRead; + setV0(static_cast(bytesRead)); + return true; + } + case 8: // lseek + { + const int32_t fd = static_cast(a0); + const auto file = m_openFiles.find(fd); + if (file == m_openFiles.end()) + { + setV0(0xFFFFFFFFu); + return true; + } + + const int32_t offset = static_cast(cpu.gpr[5]); + const uint32_t whence = cpu.gpr[6]; + int64_t base = 0; + switch (whence) + { + case 0u: // SEEK_SET + base = 0; + break; + case 1u: // SEEK_CUR + base = static_cast(file->second.offset); + break; + case 2u: // SEEK_END + base = static_cast(file->second.size); + break; + default: + setV0(0xFFFFFFFFu); + return true; + } + + const int64_t newOffset = base + static_cast(offset); + if (newOffset < 0 || newOffset > 0x7FFFFFFFLL) + { + setV0(0xFFFFFFFFu); + return true; + } + + file->second.offset = static_cast(newOffset); + setV0(static_cast(newOffset)); + return true; + } case 20: // AddDrv { if (a0 == 0u || m_devices.size() >= kMaxDevices) From 34736c50d57d6a1ae2622746d4f843817519b9eb Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:32:51 -0300 Subject: [PATCH 3/9] Pass IOP host adapter into IOMAN --- ps2xIOP/src/emulator/iop_emulator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp index e5f63a9fe2..c95981cf6f 100644 --- a/ps2xIOP/src/emulator/iop_emulator.cpp +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -105,7 +105,7 @@ namespace ps2x::iop::detail heaplib(memory), intrman(memory), timrman(), - ioman(memory), + ioman(host, memory), cpuCore(memory), imports(memory), loadcore(memory, imports) From aaa76b1f8516007bf843bafa18ad1abdf16fd38f Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:32:54 -0300 Subject: [PATCH 4/9] Test IOP IOMAN open read seek and close --- ps2xIOP/tests/iop_import_tests.cpp | 143 ++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp index 3035618443..71ce04dd05 100644 --- a/ps2xIOP/tests/iop_import_tests.cpp +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -4,6 +4,7 @@ #include "emulator/imports/iop_cdvd.h" #include "emulator/imports/iop_imports.h" #include "emulator/imports/iop_loadcore.h" +#include "emulator/imports/iop_ioman.h" #include "emulator/imports/iop_timrman.h" #include "emulator/services/iop_rpc.h" #include "ps2x/iop/iop_host.h" @@ -49,6 +50,62 @@ namespace void log(LogLevel, std::string_view) override {} }; + class MemoryFileHost final : public NullHost + { + public: + MemoryFileHost() + : bytes{'A', 'B', 'C', 'D', 'E', 'F'} + { + } + + std::string translateGuestPath(std::string_view path) const override + { + return path == "cdrom0:\\AUDIO\\BANK.BIN;1" ? "virtual:/AUDIO/BANK.BIN" : std::string{}; + } + + uint64_t openHostFile(std::string_view path) override + { + if (path != "virtual:/AUDIO/BANK.BIN") + return 0u; + ++openCount; + return 1u; + } + + bool hostFileSize(uint64_t handle, uint64_t &size) const override + { + if (handle != 1u) + return false; + size = bytes.size(); + return true; + } + + bool readHostFile(uint64_t handle, + uint64_t offset, + void *destination, + size_t size, + size_t &bytesRead) override + { + bytesRead = 0u; + if (handle != 1u || (!destination && size != 0u)) + return false; + if (offset >= bytes.size() || size == 0u) + return true; + bytesRead = std::min(size, bytes.size() - static_cast(offset)); + std::memcpy(destination, bytes.data() + offset, bytesRead); + return true; + } + + void closeHostFile(uint64_t handle) override + { + if (handle == 1u) + ++closeCount; + } + + std::vector bytes; + uint32_t openCount = 0u; + uint32_t closeCount = 0u; + }; + class CdRootHost final : public NullHost { public: @@ -98,6 +155,88 @@ namespace return false; } + bool testIomanReadOnlyFileOperations() + { + MemoryFileHost host; + IopMemory memory; + IopIoman ioman(host, memory); + RecordingExecutor executor; + + constexpr uint32_t pathAddress = 0x2000u; + constexpr uint32_t firstBuffer = 0x2100u; + constexpr uint32_t secondBuffer = 0x2120u; + const char guestPath[] = "cdrom0:\\AUDIO\\BANK.BIN;1"; + if (!expect(memory.writeRam(pathAddress, guestPath, sizeof(guestPath)), + "could not place the IOP test path in memory")) + return false; + + IopCpuState cpu{}; + cpu.gpr[4] = pathAddress; + cpu.gpr[5] = 0x0001u; // FIO_O_RDONLY + if (!expect(ioman.dispatchImport(4u, cpu, executor), "ioman:4 (open) was not handled") || + !expect(cpu.gpr[2] == 3u, "ioman:4 did not return the first available file descriptor") || + !expect(host.openCount == 1u, "ioman:4 did not open the translated VFS path")) + return false; + const uint32_t fd = cpu.gpr[2]; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = firstBuffer; + cpu.gpr[6] = 4u; + if (!expect(ioman.dispatchImport(6u, cpu, executor), "ioman:6 (read) was not handled") || + !expect(cpu.gpr[2] == 4u, "ioman:6 returned the wrong byte count")) + return false; + char firstRead[4]{}; + if (!expect(memory.readRam(firstBuffer, firstRead, sizeof(firstRead)), + "ioman:6 did not write into IOP RAM") || + !expect(std::string(firstRead, sizeof(firstRead)) == "ABCD", + "ioman:6 read the wrong bytes")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = 2u; + cpu.gpr[6] = 0u; // FIO_SEEK_SET + if (!expect(ioman.dispatchImport(8u, cpu, executor), "ioman:8 (lseek) was not handled") || + !expect(cpu.gpr[2] == 2u, "ioman:8 did not seek to the requested offset")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = secondBuffer; + cpu.gpr[6] = 3u; + if (!expect(ioman.dispatchImport(6u, cpu, executor), "second ioman:6 (read) was not handled") || + !expect(cpu.gpr[2] == 3u, "second ioman:6 returned the wrong byte count")) + return false; + char secondRead[3]{}; + if (!expect(memory.readRam(secondBuffer, secondRead, sizeof(secondRead)), + "second ioman:6 did not write into IOP RAM") || + !expect(std::string(secondRead, sizeof(secondRead)) == "CDE", + "ioman:8/ioman:6 did not preserve the file position")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + if (!expect(ioman.dispatchImport(5u, cpu, executor), "ioman:5 (close) was not handled") || + !expect(cpu.gpr[2] == 0u, "ioman:5 did not return success") || + !expect(host.closeCount == 1u, "ioman:5 did not close the host file")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = firstBuffer; + cpu.gpr[6] = 1u; + if (!expect(ioman.dispatchImport(6u, cpu, executor), "ioman:6 did not handle an invalid descriptor") || + !expect(cpu.gpr[2] == 0xFFFFFFFFu, "ioman:6 did not reject an invalid descriptor")) + return false; + + cpu = {}; + cpu.gpr[4] = pathAddress; + cpu.gpr[5] = 0x0002u; // FIO_O_WRONLY is not supported by the read-only host adapter. + return expect(ioman.dispatchImport(4u, cpu, executor), "ioman:4 did not handle a write-only open request") && + expect(cpu.gpr[2] == 0xFFFFFFFFu, "ioman:4 accepted a write-only open request"); + } + bool testLoadcoreRebootLibraryMode() { IopMemory memory; @@ -330,8 +469,8 @@ namespace int main() { - if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() || - !testTimrmanPeriodicCallback()) + if (!testIomanReadOnlyFileOperations() || !testLoadcoreRebootLibraryMode() || + !testCdvdSpecialControl() || !testCdvdSearchFile() || !testTimrmanPeriodicCallback()) return 1; std::cout << "ps2xIOP import tests passed\n"; return 0; From 303d3c93755567f3e455292e92fcfaae7e7e7f24 Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:33:25 -0300 Subject: [PATCH 5/9] Add explicit standard headers for IOMAN tests --- ps2xIOP/tests/iop_import_tests.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp index 71ce04dd05..d727075413 100644 --- a/ps2xIOP/tests/iop_import_tests.cpp +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -10,6 +10,7 @@ #include "ps2x/iop/iop_host.h" #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include namespace { From 7d0db93bbe6b26852cbc625f19e7bb1a0aebe469 Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:33:36 -0300 Subject: [PATCH 6/9] Run IOP import tests in CI --- .github/workflows/build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb363c8b82..621f29d738 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,13 +30,15 @@ jobs: export CC=clang export CXX=clang++ fi - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1 + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1 -DPS2X_IOP_BUILD_TESTS=ON - name: Build run: cmake --build build --config Release - name: Tests - run: ./build/ps2xTest/ps2x_tests + run: | + ./build/ps2xTest/ps2x_tests + ./build/ps2xIOP/ps2_iop_import_tests windows-msvc: strategy: @@ -63,13 +65,14 @@ jobs: - name: Build shell: cmd run: | - cmake -B build + cmake -B build -DPS2X_IOP_BUILD_TESTS=ON cmake --build build --config Release - name: Tests shell: cmd run: | .\build\ps2xTest\Release\ps2x_tests.exe + .\build\ps2xIOP\Release\ps2_iop_import_tests.exe - name: Upload artifact uses: actions/upload-artifact@v6 with: From eff85c00e170bac050715aa09d9faeba2b6170e2 Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:36:21 -0300 Subject: [PATCH 7/9] Document read-only IOMAN file operation scope --- ps2xIOP/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ps2xIOP/README.md b/ps2xIOP/README.md index 2f47d9e7d4..1ef7c45803 100644 --- a/ps2xIOP/README.md +++ b/ps2xIOP/README.md @@ -55,3 +55,6 @@ ctest --test-dir out/build/iop-tests --output-on-failure The suites cover IRX execution, RPC, imports, version resolution and generic HLE compatibility. `ps2x_tests` also covers runtime SIF RPC/DMA integration. + + +The IOMAN compatibility layer currently supports read-only `open`, `close`, `read`, and `lseek` operations through the host VFS adapter. Write and directory operations are not implemented by this layer yet. From d208e1cd2c0cbdd3ee49d1c90684110e9caa4d2f Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:36:49 -0300 Subject: [PATCH 8/9] Allow manual GitHub Actions runs --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb363c8b82..43fbe81b69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: build -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: linux-gcc-clang: From 71ddb7435a49b670b21607a7c33b2463737e100b Mon Sep 17 00:00:00 2001 From: LTDCrissOK Date: Thu, 5 Nov 2026 18:36:52 -0300 Subject: [PATCH 9/9] Allow manual GitHub Actions runs --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 621f29d738..ad896c516b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: build -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: linux-gcc-clang: