diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb363c8b82..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: @@ -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: diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt index f8d3e4437a..5b77089048 100644 --- a/ps2xIOP/CMakeLists.txt +++ b/ps2xIOP/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(ps2_iop STATIC src/emulator/imports/iop_sysmem.cpp src/emulator/imports/iop_timrman.cpp src/emulator/imports/iop_vblank.cpp + src/modules/cdvdman.cpp src/modules/dbcman.cpp src/modules/libsd.cpp src/modules/mcserv.cpp @@ -61,7 +62,6 @@ if(PS2X_IOP_BUILD_TESTS) target_link_libraries(ps2_iop_import_version_tests PRIVATE ps2_iop) target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests) - endif() install(TARGETS ps2_iop 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. diff --git a/ps2xIOP/include/ps2x/iop/iop_host.h b/ps2xIOP/include/ps2x/iop/iop_host.h index 1eceb08348..bad06cd0cd 100644 --- a/ps2xIOP/include/ps2x/iop/iop_host.h +++ b/ps2xIOP/include/ps2x/iop/iop_host.h @@ -96,7 +96,23 @@ namespace ps2x::iop virtual void audioCommand(uint32_t sid, uint32_t function, GuestBuffer send, GuestBuffer receive) = 0; + struct CdFileInfo + { + uint32_t lsn = 0u; + uint32_t size = 0u; + bool directory = false; + std::string name; + }; + virtual std::string hostPath(HostPathKind kind) const = 0; + // Resolve a CD-ROM path against the mounted disc image and return its real + // ISO9660 extent. Hosts without a mounted image may keep the default. + virtual bool findCdFile(std::string_view path, CdFileInfo &result) const + { + (void)path; + (void)result; + return false; + } virtual std::string translateGuestPath(std::string_view path) const = 0; virtual uint64_t openHostFile(std::string_view path) = 0; virtual bool hostFileSize(uint64_t handle, uint64_t &size) const = 0; diff --git a/ps2xIOP/src/emulator/imports/iop_cdvd.cpp b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp index ad58fb4e43..b2022a5255 100644 --- a/ps2xIOP/src/emulator/imports/iop_cdvd.cpp +++ b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp @@ -600,11 +600,32 @@ namespace ps2x::iop::detail return false; const std::string guestPath = memory.readString(nameAddress, 1024u); + + // When a real ISO is mounted, return the file's actual LSN and size + // from its ISO9660 directory record. The old virtual-directory index + // describes a host folder and its synthetic LSNs do not match sectors + // in the mounted disc image. + ps2x::iop::IopHost::CdFileInfo discEntry; + const bool hasMountedDisc = !host.hostPath(HostPathKind::CdImage).empty(); + if (hasMountedDisc) + { + if (!host.findCdFile(guestPath, discEntry)) + return false; + + std::array result{}; + writeLe32(result.data(), discEntry.lsn); + writeLe32(result.data() + 4u, discEntry.size); + const std::string leaf = normalizedIsoComponent(discEntry.name); + std::memcpy(result.data() + 8u, leaf.data(), std::min(16u, leaf.size())); + result[24u] = discEntry.directory ? 2u : 0u; + return memory.writeRam(resultAddress, result.data(), result.size()); + } + + // Keep the synthetic host-directory disc for non-ISO runner setups. 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); 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) 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; }; } 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) diff --git a/ps2xIOP/src/iop_subsystem.cpp b/ps2xIOP/src/iop_subsystem.cpp index b498155409..2863ad5abf 100644 --- a/ps2xIOP/src/iop_subsystem.cpp +++ b/ps2xIOP/src/iop_subsystem.cpp @@ -23,6 +23,7 @@ namespace ps2x::iop coreServices.emplace_back(detail::createMcservService(host)); coreServices.emplace_back(detail::createDbcmanService(host)); coreServices.emplace_back(detail::createLibSdService(host)); + coreServices.emplace_back(detail::createCdvdmanService(host)); refreshServiceModuleKeys(); rebuildRoutes(); } @@ -177,9 +178,7 @@ namespace ps2x::iop { const RpcAbi selected = service->selectRpcAbi(request); if (selected != RpcAbi::RuntimeDefault) - { return selected; - } } } return RpcAbi::RuntimeDefault; @@ -188,9 +187,7 @@ namespace ps2x::iop bool IopSubsystem::canBindRpc(uint32_t sid) const noexcept { if (m_impl->routes.find(sid) != m_impl->routes.end()) - { return true; - } return m_impl->emulator.hasRpcServer(sid); } @@ -201,9 +198,7 @@ namespace ps2x::iop RpcResult emulated = m_impl->emulator.handleRpc(request); if (emulated.handled || !hle) - { return emulated; - } return hle->handleRpc(request); } @@ -212,9 +207,7 @@ namespace ps2x::iop for (auto &service : m_impl->coreServices) { if (service && m_impl->serviceActive(*service)) - { service->onSifTransfer(transfer); - } } m_impl->emulator.onSifTransfer(transfer); } @@ -259,9 +252,7 @@ namespace ps2x::iop snapshot.emulatorRpcServers = m_impl->emulator.rpcServerCount(); snapshot.diagnostics = m_impl->loadOutcomes; if (!m_impl->lastError.empty()) - { snapshot.diagnostics.push_back(m_impl->lastError); - } for (const auto &service : m_impl->coreServices) { diff --git a/ps2xIOP/src/module_factories.h b/ps2xIOP/src/module_factories.h index f0da295aa7..c4e3269887 100644 --- a/ps2xIOP/src/module_factories.h +++ b/ps2xIOP/src/module_factories.h @@ -4,6 +4,7 @@ namespace ps2x::iop::detail { + std::unique_ptr createCdvdmanService(IopHost &host); std::unique_ptr createDbcmanService(IopHost &host); std::unique_ptr createLibSdService(IopHost &host); std::unique_ptr createMcservService(IopHost &host); diff --git a/ps2xIOP/src/modules/cdvdman.cpp b/ps2xIOP/src/modules/cdvdman.cpp new file mode 100644 index 0000000000..5c5ec97768 --- /dev/null +++ b/ps2xIOP/src/modules/cdvdman.cpp @@ -0,0 +1,112 @@ +#include "module_factories.h" +#include "../rpc_reply.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + // These endpoints are provided by the PS2 ROM's cdvdman/cdvdfsv + // services. They must be bindable before a game loads a disc IRX. + constexpr uint32_t kCcmdSid = 0x80000592u; + constexpr uint32_t kScmdSid = 0x80000593u; + constexpr uint32_t kStcmdSid = 0x80000595u; + constexpr uint32_t kSearchFileSid = 0x8000059Au; + + bool isObservedBootCommand(const RpcRequest &request) + { + // Known libcdvd initialization handshake observed in PS2Recomp. + return request.sid == kScmdSid && request.function == 0x27u; + } + + void writeBootHandshakeReply(IopHost &host, const RpcRequest &request) + { + // The caller polls the first result word. The remaining words are + // neutral drive-state values until a full CDVD device model exists. + const std::array words{1u, 0u, 0u}; + (void)writeRpcWords(host, request.receive, words); + } + + class CdvdmanService final : public IopService + { + public: + explicit CdvdmanService(IopHost &host) : m_host(host) {} + + [[nodiscard]] std::string_view name() const override { return "cdvdman"; } + + [[nodiscard]] std::span sids() const override + { + return kSids; + } + + void reset() override + { + std::lock_guard lock(m_mutex); + m_unknownRpcLogCount = 0u; + } + + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override + { + RpcResult response{}; + response.handled = true; + response.resultAddress = request.receive.address; + + if (isObservedBootCommand(request)) + { + writeBootHandshakeReply(m_host, request); + } + else + { + logUnknown(request); + if (request.receive.address != 0u && request.receive.size != 0u) + { + (void)m_host.zeroGuest(request.receive.address, request.receive.size); + } + } + return response; + } + + void appendDebugMetrics(std::vector &metrics) const override + { + std::lock_guard lock(m_mutex); + metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false}); + } + + private: + void logUnknown(const RpcRequest &request) + { + { + std::lock_guard lock(m_mutex); + if (m_unknownRpcLogCount >= 32u) + return; + ++m_unknownRpcLogCount; + } + + std::ostringstream message; + message << "CDVD unknown RPC sid=0x" << std::hex << request.sid + << " function=0x" << request.function + << " send=0x" << request.send.address + << "/" << std::dec << request.send.size + << " recv=0x" << std::hex << request.receive.address + << "/" << std::dec << request.receive.size; + m_host.log(LogLevel::Warning, message.str()); + } + + IopHost &m_host; + mutable std::mutex m_mutex; + uint32_t m_unknownRpcLogCount = 0u; + inline static constexpr std::array kSids{ + kCcmdSid, kScmdSid, kStcmdSid, kSearchFileSid}; + }; + } + + std::unique_ptr createCdvdmanService(IopHost &host) + { + return std::make_unique(host); + } +} diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp index 3035618443..d727075413 100644 --- a/ps2xIOP/tests/iop_import_tests.cpp +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -4,11 +4,13 @@ #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" #include +#include #include #include #include @@ -16,6 +18,7 @@ #include #include #include +#include namespace { @@ -49,6 +52,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 +157,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 +471,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; diff --git a/ps2xRecomp/src/lib/vu_translation_helpers.cpp b/ps2xRecomp/src/lib/vu_translation_helpers.cpp index 509fd07b0a..25a48dc87d 100644 --- a/ps2xRecomp/src/lib/vu_translation_helpers.cpp +++ b/ps2xRecomp/src/lib/vu_translation_helpers.cpp @@ -164,17 +164,33 @@ namespace ps2recomp std::string CodeGenerator::translateVU_VCALLMSR(const Instruction &inst) { - // VCALLMSR calls a VU0 microprogram at address stored in integer register - uint8_t vis_reg_idx = inst.rd; // Source integer register (vis) + // VCALLMSR's source field can address VI0..VI15 or the CMSAR0 control register. + const uint32_t source_reg = inst.rd; + std::string source_expr; + + if (source_reg < 16) + { + source_expr = fmt::format("ctx->vi[{}]", source_reg); + } + else if (source_reg == VU0_CR_CMSAR0) + { + source_expr = "ctx->vu0_cmsar0"; + } + else + { + return emitUnhandledInstruction( + inst, + fmt::format("Unsupported VCALLMSR source register: {}", source_reg)); + } return fmt::format( "{{ " - " uint16_t instr_index = ctx->vi[{}] & 0x1FF; " // Get instruction index from VI[IS], mask to 9 bits - " uint32_t target_byte_addr = (uint32_t)instr_index << 3; " // Convert to byte address + " uint16_t instr_index = static_cast({} & 0x1FF); " + " uint32_t target_byte_addr = (uint32_t)instr_index << 3; " " ctx->vu0_pc = target_byte_addr; " " runtime->vu0StartMicroProgram(rdram, ctx, target_byte_addr); " "}}", - vis_reg_idx); + source_expr); } std::string CodeGenerator::translateVU_VRNEXT(const Instruction &inst) diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 589d76850a..6ebe29f1c4 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -377,6 +377,7 @@ endfunction() add_library(ps2_runtime STATIC src/lib/game_overrides.cpp + src/lib/iso9660_reader.cpp src/lib/gs/ps2_gif_arbiter.cpp src/lib/ps2_audio.cpp src/lib/ps2_audio_vag.cpp diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 4b9242cb4c..2779ea6974 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -280,6 +280,7 @@ class PS2Runtime std::filesystem::path cdRoot; std::filesystem::path mcRoot; std::filesystem::path cdImage; + std::shared_ptr cdIsoImage; }; PS2Runtime(); @@ -358,6 +359,13 @@ class PS2Runtime static const IoPaths &getIoPaths(); static void setIoPaths(const IoPaths &paths); static void configureIoPathsFromElf(const std::string &elfPath); + // Mount an ISO9660 CD image after validating SYSTEM.CNF and its PS2 ELF boot file. + static bool mountCdIsoImage(const std::filesystem::path &isoPath, + std::string_view bootElfName, + std::string *error = nullptr); + static bool extractMountedCdIsoBootElf(std::string_view bootElfName, + const std::filesystem::path &destinationPath, + std::string *error = nullptr); void SignalException(R5900Context *ctx, PS2Exception exception); diff --git a/ps2xRuntime/include/runtime/iso9660_reader.h b/ps2xRuntime/include/runtime/iso9660_reader.h new file mode 100644 index 0000000000..f65a65e2d1 --- /dev/null +++ b/ps2xRuntime/include/runtime/iso9660_reader.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iso9660 +{ + inline constexpr uint32_t kLogicalBlockSize = 2048u; + + struct DirectoryEntry + { + std::string name; + uint32_t extentLba = 0u; + uint32_t size = 0u; + uint8_t flags = 0u; + + [[nodiscard]] bool isDirectory() const noexcept + { + return (flags & 0x02u) != 0u; + } + }; + + // Read-only ISO9660 image reader for PS2 disc images. It supports ordinary + // ISO9660 directory records and the ;version suffix used by PS2 discs. + class Image + { + public: + Image() = default; + ~Image() = default; + + Image(const Image &) = delete; + Image &operator=(const Image &) = delete; + + [[nodiscard]] bool open(const std::filesystem::path &path, std::string *error = nullptr); + void close() noexcept; + + [[nodiscard]] bool isOpen() const noexcept { return m_stream.is_open(); } + [[nodiscard]] uint64_t imageSize() const noexcept { return m_imageSize; } + + // Resolve a path relative to the ISO root. Both '/' and '\\' are accepted. + // File names are compared case-insensitively and ;version suffixes are ignored. + [[nodiscard]] bool findFile(std::string_view path, DirectoryEntry &result) const; + + // Search all directories for a file name, useful for locating the boot ELF + // when an authoring tool placed it below the ISO root. + [[nodiscard]] bool findFileRecursive(std::string_view fileName, DirectoryEntry &result) const; + + // Read a range from an ISO file entry. A short read at EOF is successful and + // is reported through bytesRead when supplied. + [[nodiscard]] bool readFile(const DirectoryEntry &entry, + uint64_t offset, + void *destination, + size_t size, + size_t *bytesRead = nullptr) const; + + [[nodiscard]] bool readFileFully(const DirectoryEntry &entry, + std::vector &destination) const; + + private: + [[nodiscard]] bool readAt(uint64_t offset, void *destination, size_t size) const; + [[nodiscard]] bool loadDirectory(const DirectoryEntry &directory, + std::vector &entries) const; + [[nodiscard]] bool entryRangeIsValid(uint32_t extentLba, uint32_t size) const noexcept; + + mutable std::ifstream m_stream; + mutable std::mutex m_streamMutex; + uint64_t m_imageSize = 0u; + DirectoryEntry m_rootDirectory; + }; +} diff --git a/ps2xRuntime/include/runtime/ps2_vfs.h b/ps2xRuntime/include/runtime/ps2_vfs.h index 95b0bdcb5d..2c922f09b4 100644 --- a/ps2xRuntime/include/runtime/ps2_vfs.h +++ b/ps2xRuntime/include/runtime/ps2_vfs.h @@ -13,6 +13,8 @@ #include #include +namespace ps2x::iso9660 { class Image; } + class PS2RomDevice; struct PS2VfsMounts @@ -20,6 +22,7 @@ struct PS2VfsMounts std::filesystem::path hostRoot; std::filesystem::path cdRoot; std::filesystem::path memoryCard0Root; + std::shared_ptr cdIsoImage; }; struct PS2VfsStat diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index c5e31c48d8..6dc0c56de0 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -1350,6 +1350,16 @@ void EeScheduler::completeVSync(uint64_t tick) const int result = wait.fixedResult >= 0 ? wait.fixedResult : static_cast((tick - 1u) & 1u); + static std::atomic s_vsyncWakeLogCount{0u}; + const uint32_t logIndex = s_vsyncWakeLogCount.fetch_add(1u, std::memory_order_relaxed); + if (logIndex < 128u) + { + RUNTIME_LOG("EE_VSYNC_WAKE index=" << logIndex + << " thread=" << id + << " tick=" << tick + << " result=" << result + << std::endl); + } makeReady(*waiter, result, false); } publishSnapshot(); @@ -1867,7 +1877,17 @@ void EeScheduler::processEvent(const EeEvent &event) requestStop(); break; case EeEventType::VBlankStart: + { ++m_vsyncTick; + static std::atomic s_vblankLogCount{0u}; + const uint32_t vblankLogIndex = s_vblankLogCount.fetch_add(1u, std::memory_order_relaxed); + if (vblankLogIndex < 128u) + { + RUNTIME_LOG("EE_VBLANK_START index=" << vblankLogIndex + << " tick=" << m_vsyncTick + << " ee_cycle=" << m_eeCycle + << std::endl); + } m_runtime.memory().gs().vsyncTick.store(m_vsyncTick, std::memory_order_release); if ((m_vsyncTick & 1u) != 0u) { @@ -1902,6 +1922,7 @@ void EeScheduler::processEvent(const EeEvent &event) } dispatchIrq(false, 2u); break; + } case EeEventType::ExternalWake: completeExternalWait(event.id, event.value, KE_OK); break; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp index 56a978eae4..4ee465e438 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp @@ -661,7 +661,12 @@ namespace ps2_stubs void sceCdStatus(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - setReturnS32(ctx, g_cdInitialized ? 6 : 0); + // CD/DVD reads in this runtime are synchronous: there is no pending + // hardware read to report. SCECdStatPause (0x0A) is the idle/paused + // state expected by games that wait for the drive before booting. + // Returning SCECdStatRead (6) forever can trap those boot loops. + constexpr int32_t kCdStatusPause = 0x0A; + setReturnS32(ctx, g_cdInitialized ? kCdStatusPause : 0); } void sceCdStInit(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index aed4cb5437..054a3d46ef 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -810,6 +810,18 @@ namespace ps2_stubs if (runtime) { + // PMODE and SMODE2 are privileged GS registers, not normal + // GIF drawing registers. Keep them in the GS register state; + // routing their old 0x41/0x42 aliases through the GIF frontend + // incorrectly treated them as SCISSOR_2 and ALPHA_1. That left + // PMODE disabled, so the Android presenter displayed its + // magenta "no framebuffer" fallback forever. + auto &gsRegs = runtime->memory().gs(); + gsRegs.pmode = pmode; + gsRegs.smode2 = smode2; + + // The remaining display registers use the runtime's existing + // privileged-register aliases (0x59..0x5F) through GIF A+D. uint32_t pktAddr = runtime->guestMalloc(128u, 16u); if (pktAddr != 0u) { @@ -817,27 +829,23 @@ namespace ps2_stubs if (pkt) { uint64_t *q = reinterpret_cast(pkt); - q[0] = makeGiftagAplusD(7u); + q[0] = makeGiftagAplusD(5u); q[1] = 0xEULL; - q[2] = pmode; - q[3] = 0x41ULL; - q[4] = smode2; - q[5] = 0x42ULL; + q[2] = dispfb; + q[3] = 0x59ULL; + q[4] = display; + q[5] = 0x5aULL; q[6] = dispfb; - q[7] = 0x59ULL; + q[7] = 0x5bULL; q[8] = display; - q[9] = 0x5aULL; - q[10] = dispfb; - q[11] = 0x5bULL; - q[12] = display; - q[13] = 0x5cULL; - q[14] = bgcolor; - q[15] = 0x5fULL; + q[9] = 0x5cULL; + q[10] = bgcolor; + q[11] = 0x5fULL; constexpr uint32_t GIF_CHANNEL = 0x1000A000; constexpr uint32_t CHCR_STR_MODE0 = 0x101u; auto &mem = runtime->memory(); mem.writeIORegister(GIF_CHANNEL + 0x10u, pktAddr); - mem.writeIORegister(GIF_CHANNEL + 0x20u, 8u); + mem.writeIORegister(GIF_CHANNEL + 0x20u, 6u); mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0); mem.processPendingTransfers(); runtime->guestFree(pktAddr); diff --git a/ps2xRuntime/src/lib/iso9660_reader.cpp b/ps2xRuntime/src/lib/iso9660_reader.cpp new file mode 100644 index 0000000000..2e742c7e7c --- /dev/null +++ b/ps2xRuntime/src/lib/iso9660_reader.cpp @@ -0,0 +1,397 @@ +#include "runtime/iso9660_reader.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iso9660 +{ + namespace + { + constexpr uint64_t kVolumeDescriptorStart = 16u; + constexpr uint32_t kMaximumDirectoryBytes = 64u * 1024u * 1024u; + constexpr size_t kMaximumVisitedDirectories = 65536u; + + uint16_t readLe16(const uint8_t *value) + { + return static_cast(value[0]) | + static_cast(static_cast(value[1]) << 8u); + } + + uint32_t readLe32(const uint8_t *value) + { + return static_cast(value[0]) | + (static_cast(value[1]) << 8u) | + (static_cast(value[2]) << 16u) | + (static_cast(value[3]) << 24u); + } + + std::string normalizedName(std::string_view name) + { + const size_t version = name.find(';'); + if (version != std::string_view::npos) + name = name.substr(0u, version); + + while (!name.empty() && name.back() == '.') + name.remove_suffix(1u); + + std::string result; + result.reserve(name.size()); + for (const unsigned char character : name) + result.push_back(static_cast(std::toupper(character))); + return result; + } + + bool parseDirectoryRecord(const uint8_t *record, + size_t recordLength, + DirectoryEntry &result, + bool allowSpecialIdentifier = false) + { + if (!record || recordLength < 34u || record[0] < 34u || record[0] > recordLength) + return false; + + const uint8_t identifierLength = record[32u]; + if (identifierLength == 0u || static_cast(33u + identifierLength) > record[0]) + return false; + + // ISO9660 uses one-byte identifiers 0 and 1 for "." and "..". + // The PVD root directory record is also encoded with identifier 0. + if (identifierLength == 1u && (record[33u] == 0u || record[33u] == 1u)) + { + if (!allowSpecialIdentifier) + return false; + result.name = record[33u] == 0u ? "." : ".."; + } + else + { + result.name.assign(reinterpret_cast(record + 33u), identifierLength); + } + // Extended-attribute blocks precede the file data extent. + const uint64_t dataExtent = static_cast(readLe32(record + 2u)) + record[1u]; + if (dataExtent > std::numeric_limits::max()) + return false; + result.extentLba = static_cast(dataExtent); + result.size = readLe32(record + 10u); + result.flags = record[25u]; + return true; + } + + bool isSafeDirectoryEntry(const DirectoryEntry &entry) + { + return !entry.name.empty() && entry.extentLba != 0u; + } + } + + bool Image::open(const std::filesystem::path &path, std::string *error) + { + close(); + + const auto fail = [this, error](const char *message) + { + close(); + if (error) + *error = message; + return false; + }; + + std::error_code sizeError; + const uint64_t fileSize = std::filesystem::file_size(path, sizeError); + if (sizeError) + return fail("Unable to determine ISO image size"); + if (fileSize < (kVolumeDescriptorStart + 1u) * kLogicalBlockSize) + return fail("File is too small to contain an ISO9660 volume descriptor"); + + m_stream.open(path, std::ios::binary); + if (!m_stream) + return fail("Unable to open ISO image"); + + m_imageSize = fileSize; + std::array descriptor{}; + + bool foundPrimaryVolumeDescriptor = false; + for (uint64_t sector = kVolumeDescriptorStart; sector < kVolumeDescriptorStart + 64u; ++sector) + { + const uint64_t offset = sector * kLogicalBlockSize; + if (!readAt(offset, descriptor.data(), descriptor.size())) + break; + + if (descriptor[1u] != 'C' || descriptor[2u] != 'D' || + descriptor[3u] != '0' || descriptor[4u] != '0' || + descriptor[5u] != '1' || descriptor[6u] != 1u) + { + return fail("Invalid ISO9660 volume descriptor signature"); + } + + if (descriptor[0u] == 255u) + break; + if (descriptor[0u] != 1u) + continue; + if (readLe16(descriptor.data() + 128u) != kLogicalBlockSize) + return fail("Unsupported ISO9660 logical block size"); + + DirectoryEntry root; + const uint8_t rootRecordLength = descriptor[156u]; + if (rootRecordLength < 34u || + !parseDirectoryRecord(descriptor.data() + 156u, + rootRecordLength, + root, + true) || + !root.isDirectory() || + !entryRangeIsValid(root.extentLba, root.size)) + { + return fail("Invalid ISO9660 root directory record"); + } + + m_rootDirectory = std::move(root); + foundPrimaryVolumeDescriptor = true; + break; + } + + if (!foundPrimaryVolumeDescriptor) + return fail("ISO9660 primary volume descriptor not found"); + + if (error) + error->clear(); + return true; + } + + void Image::close() noexcept + { + std::lock_guard lock(m_streamMutex); + if (m_stream.is_open()) + m_stream.close(); + m_stream.clear(); + m_imageSize = 0u; + m_rootDirectory = {}; + } + + bool Image::readAt(uint64_t offset, void *destination, size_t size) const + { + std::lock_guard lock(m_streamMutex); + if (!m_stream.is_open() || (!destination && size != 0u)) + return false; + if (offset > m_imageSize || static_cast(size) > m_imageSize - offset) + return false; + if (size == 0u) + return true; + if (offset > static_cast(std::numeric_limits::max()) || + size > static_cast(std::numeric_limits::max())) + { + return false; + } + + m_stream.clear(); + m_stream.seekg(static_cast(offset), std::ios::beg); + if (!m_stream) + return false; + + m_stream.read(static_cast(destination), static_cast(size)); + return m_stream.gcount() == static_cast(size); + } + + bool Image::entryRangeIsValid(uint32_t extentLba, uint32_t size) const noexcept + { + if (extentLba == 0u) + return false; + + const uint64_t offset = static_cast(extentLba) * kLogicalBlockSize; + return offset <= m_imageSize && static_cast(size) <= m_imageSize - offset; + } + + bool Image::loadDirectory(const DirectoryEntry &directory, + std::vector &entries) const + { + entries.clear(); + if (!directory.isDirectory() || + directory.size > kMaximumDirectoryBytes || + !entryRangeIsValid(directory.extentLba, directory.size)) + { + return false; + } + + std::vector bytes(directory.size); + const uint64_t offset = static_cast(directory.extentLba) * kLogicalBlockSize; + if (!bytes.empty() && !readAt(offset, bytes.data(), bytes.size())) + return false; + + size_t cursor = 0u; + while (cursor < bytes.size()) + { + const uint8_t recordLength = bytes[cursor]; + if (recordLength == 0u) + { + const size_t nextSector = + ((cursor / kLogicalBlockSize) + 1u) * kLogicalBlockSize; + if (nextSector <= cursor) + break; + cursor = std::min(nextSector, bytes.size()); + continue; + } + + if (static_cast(recordLength) > bytes.size() - cursor) + return false; + + DirectoryEntry entry; + if (parseDirectoryRecord(bytes.data() + cursor, recordLength, entry)) + { + if (!isSafeDirectoryEntry(entry) || + !entryRangeIsValid(entry.extentLba, entry.size)) + { + return false; + } + entries.push_back(std::move(entry)); + } + + cursor += recordLength; + } + return true; + } + + bool Image::findFile(std::string_view path, DirectoryEntry &result) const + { + result = {}; + if (!isOpen()) + return false; + + std::vector components; + size_t start = 0u; + while (start < path.size()) + { + while (start < path.size() && (path[start] == '/' || path[start] == '\\')) + ++start; + if (start >= path.size()) + break; + + size_t end = start; + while (end < path.size() && path[end] != '/' && path[end] != '\\') + ++end; + + const std::string component = normalizedName(path.substr(start, end - start)); + if (!component.empty() && component != ".") + { + if (component == "..") + return false; + components.push_back(component); + } + start = end; + } + + if (components.empty()) + return false; + + DirectoryEntry currentDirectory = m_rootDirectory; + for (size_t componentIndex = 0u; componentIndex < components.size(); ++componentIndex) + { + std::vector entries; + if (!loadDirectory(currentDirectory, entries)) + return false; + + const auto found = std::find_if(entries.begin(), entries.end(), + [&components, componentIndex](const DirectoryEntry &entry) + { + return normalizedName(entry.name) == components[componentIndex]; + }); + if (found == entries.end()) + return false; + + if (componentIndex + 1u == components.size()) + { + result = *found; + return true; + } + if (!found->isDirectory()) + return false; + currentDirectory = *found; + } + + return false; + } + + bool Image::findFileRecursive(std::string_view fileName, DirectoryEntry &result) const + { + result = {}; + if (!isOpen()) + return false; + + const std::string wanted = normalizedName(fileName); + if (wanted.empty()) + return false; + + std::vector pending{m_rootDirectory}; + std::unordered_set visited; + size_t cursor = 0u; + + while (cursor < pending.size() && visited.size() < kMaximumVisitedDirectories) + { + const DirectoryEntry directory = pending[cursor++]; + if (!visited.insert(directory.extentLba).second) + continue; + + std::vector entries; + if (!loadDirectory(directory, entries)) + return false; + + for (const DirectoryEntry &entry : entries) + { + if (!entry.isDirectory() && normalizedName(entry.name) == wanted) + { + result = entry; + return true; + } + if (entry.isDirectory() && visited.find(entry.extentLba) == visited.end()) + pending.push_back(entry); + } + } + + return false; + } + + bool Image::readFile(const DirectoryEntry &entry, + uint64_t offset, + void *destination, + size_t size, + size_t *bytesRead) const + { + if (bytesRead) + *bytesRead = 0u; + if (!isOpen() || entry.isDirectory() || + !entryRangeIsValid(entry.extentLba, entry.size) || + offset > entry.size || (!destination && size != 0u)) + { + return false; + } + + const uint64_t remaining = static_cast(entry.size) - offset; + const size_t count = static_cast(std::min(remaining, size)); + const uint64_t imageOffset = + static_cast(entry.extentLba) * kLogicalBlockSize + offset; + + if (!readAt(imageOffset, destination, count)) + return false; + if (bytesRead) + *bytesRead = count; + return true; + } + + bool Image::readFileFully(const DirectoryEntry &entry, + std::vector &destination) const + { + destination.clear(); + if (entry.isDirectory() || !entryRangeIsValid(entry.extentLba, entry.size)) + return false; + + destination.resize(entry.size); + size_t bytesRead = 0u; + if (!readFile(entry, 0u, destination.data(), destination.size(), &bytesRead) || + bytesRead != destination.size()) + { + destination.clear(); + return false; + } + return true; + } +} diff --git a/ps2xRuntime/src/lib/ps2_iop_host.cpp b/ps2xRuntime/src/lib/ps2_iop_host.cpp index 8d46e03292..1624f8ecd6 100644 --- a/ps2xRuntime/src/lib/ps2_iop_host.cpp +++ b/ps2xRuntime/src/lib/ps2_iop_host.cpp @@ -286,10 +286,44 @@ std::string PS2IopHostAdapter::hostPath(ps2x::iop::HostPathKind kind) const } } +bool PS2IopHostAdapter::findCdFile(std::string_view path, + ps2x::iop::IopHost::CdFileInfo &result) const +{ + result = {}; + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + const ps2x::iop::ParsedPs2Path parsed = ps2x::iop::parsePs2Path(path); + if (!parsed || parsed.device != ps2x::iop::Ps2PathDevice::Cdrom || + !paths.cdIsoImage || !paths.cdIsoImage->isOpen()) + { + return false; + } + + ps2x::iso9660::DirectoryEntry entry; + if (!paths.cdIsoImage->findFile(parsed.path, entry)) + { + return false; + } + + result.lsn = entry.extentLba; + result.size = entry.size; + result.directory = entry.isDirectory(); + result.name = entry.name; + return true; +} + std::string PS2IopHostAdapter::translateGuestPath(std::string_view path) const { const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); - const PS2VfsMounts mounts{paths.hostRoot, paths.cdRoot, paths.mcRoot}; + const ps2x::iop::ParsedPs2Path parsed = ps2x::iop::parsePs2Path(path); + if (parsed && parsed.device == ps2x::iop::Ps2PathDevice::Cdrom && paths.cdIsoImage) + { + ps2x::iso9660::DirectoryEntry entry; + if (!paths.cdIsoImage->findFile(parsed.path, entry) || entry.isDirectory()) + return {}; + return std::string("ps2x-iso://") + parsed.path; + } + + const PS2VfsMounts mounts{paths.hostRoot, paths.cdRoot, paths.mcRoot, paths.cdIsoImage}; std::filesystem::path hostPath; if (!m_runtime.vfs().resolveHostPath(path, mounts, hostPath)) return {}; @@ -303,6 +337,29 @@ uint64_t PS2IopHostAdapter::openHostFile(std::string_view path) return 0u; } + constexpr std::string_view kIsoHandlePrefix = "ps2x-iso://"; + if (path.starts_with(kIsoHandlePrefix)) + { + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + if (!paths.cdIsoImage) + return 0u; + + ps2x::iso9660::DirectoryEntry entry; + if (!paths.cdIsoImage->findFile(path.substr(kIsoHandlePrefix.size()), entry) || + entry.isDirectory()) + { + return 0u; + } + + std::lock_guard lock(m_hostFileMutex); + uint64_t handle = m_nextHostFileHandle++; + if (handle == 0u) + handle = m_nextHostFileHandle++; + m_hostFiles.emplace(handle, + HostFile{nullptr, entry.size, paths.cdIsoImage, std::move(entry)}); + return handle; + } + const std::filesystem::path hostPath{std::string(path)}; #if defined(_WIN32) std::FILE *stream = ::_wfopen(hostPath.c_str(), L"rb"); @@ -337,7 +394,7 @@ bool PS2IopHostAdapter::hostFileSize(uint64_t handle, uint64_t &size) const size = 0u; std::lock_guard lock(m_hostFileMutex); const auto it = m_hostFiles.find(handle); - if (it == m_hostFiles.end() || !it->second.stream) + if (it == m_hostFiles.end() || (!it->second.stream && !it->second.isoImage)) { return false; } @@ -359,10 +416,20 @@ bool PS2IopHostAdapter::readHostFile(uint64_t handle, std::lock_guard lock(m_hostFileMutex); const auto it = m_hostFiles.find(handle); - if (it == m_hostFiles.end() || !it->second.stream) + if (it == m_hostFiles.end()) { return false; } + if (it->second.isoImage) + { + return it->second.isoImage->readFile(it->second.isoEntry, + offset, + destination, + size, + &bytesRead); + } + if (!it->second.stream) + return false; #if defined(_WIN32) if (offset > static_cast(std::numeric_limits::max()) || diff --git a/ps2xRuntime/src/lib/ps2_iop_host.h b/ps2xRuntime/src/lib/ps2_iop_host.h index 89d8576a82..af6a99e67b 100644 --- a/ps2xRuntime/src/lib/ps2_iop_host.h +++ b/ps2xRuntime/src/lib/ps2_iop_host.h @@ -1,10 +1,12 @@ #pragma once #include "ps2x/iop/iop_host.h" +#include "runtime/iso9660_reader.h" #include #include #include +#include #include class PS2Runtime; @@ -62,6 +64,7 @@ class PS2IopHostAdapter final : public ps2x::iop::IopHost ps2x::iop::GuestBuffer receive) override; std::string hostPath(ps2x::iop::HostPathKind kind) const override; + bool findCdFile(std::string_view path, ps2x::iop::IopHost::CdFileInfo &result) const override; std::string translateGuestPath(std::string_view path) const override; uint64_t openHostFile(std::string_view path) override; bool hostFileSize(uint64_t handle, uint64_t &size) const override; @@ -101,6 +104,8 @@ class PS2IopHostAdapter final : public ps2x::iop::IopHost { std::FILE *stream = nullptr; uint64_t size = 0u; + std::shared_ptr isoImage; + ps2x::iso9660::DirectoryEntry isoEntry; }; mutable std::mutex m_hostFileMutex; std::unordered_map m_hostFiles; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 45cb9d267e..f4f31e3e4c 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -13,11 +13,13 @@ #include "ps2_host_backend.h" #include "ps2_iop_host.h" #include "ps2x/iop/iop_subsystem.h" +#include "runtime/iso9660_reader.h" #include #include #include #include +#include #include #include #include @@ -26,6 +28,8 @@ #include #include #include +#include +#include namespace ps2_stubs { @@ -87,6 +91,34 @@ struct ProgramHeader namespace { + std::mutex g_diagnosticLogMutex; + std::filesystem::path g_diagnosticLogPath; + + void setPersistentDiagnosticLog(const std::filesystem::path &elfPath) + { + std::lock_guard lock(g_diagnosticLogMutex); + g_diagnosticLogPath = elfPath.parent_path() / "manhunt2_runtime.log"; + std::ofstream output(g_diagnosticLogPath, std::ios::out | std::ios::app); + if (output) + { + output << "RUNTIME_LOG_ATTACHED ELF=" << elfPath.string() << '\n'; + output.flush(); + } + } + + void writePersistentDiagnostic(const std::string &message) + { + std::lock_guard lock(g_diagnosticLogMutex); + if (g_diagnosticLogPath.empty()) + return; + std::ofstream output(g_diagnosticLogPath, std::ios::out | std::ios::app); + if (output) + { + output << message << '\n'; + output.flush(); + } + } + constexpr uint32_t kGuestHeapDefaultBase = 0x00100000u; constexpr uint32_t kGuestHeapDefaultAlignment = 16u; constexpr uint32_t kGuestHeapSafetyPad = 0x1000u; @@ -107,7 +139,8 @@ namespace bool wrapped = false; }; - thread_local DispatchHistory g_dispatchHistory; + std::mutex g_dispatchHistoryMutex; + DispatchHistory g_dispatchHistory; bool computeFileCrc32(const std::string &path, uint32_t &crcOut) { @@ -153,6 +186,7 @@ namespace void pushDispatchPc(uint32_t pc) { + std::lock_guard lock(g_dispatchHistoryMutex); DispatchHistory &h = g_dispatchHistory; h.pcs[h.next] = pc; h.next = (h.next + 1u) % static_cast(h.pcs.size()); @@ -164,6 +198,7 @@ namespace std::string formatDispatchHistory() { + std::lock_guard lock(g_dispatchHistoryMutex); const DispatchHistory &h = g_dispatchHistory; const uint32_t count = h.wrapped ? static_cast(h.pcs.size()) : h.next; if (count == 0u) @@ -471,6 +506,39 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint } } + PS2_IF_AGRESSIVE_LOGS({ + static uint32_t s_frameStatsLogCount = 0u; + if (s_frameStatsLogCount < 8u || (s_frameStatsLogCount % 30u) == 0u) + { + uint64_t nonBlackPixels = 0u; + uint64_t rgbSum = 0u; + uint64_t hash = 14695981039346656037ull; + for (size_t i = 0u; i + 3u < s_uploadBuffer.size(); i += 4u) + { + const uint8_t r = s_uploadBuffer[i + 0u]; + const uint8_t g = s_uploadBuffer[i + 1u]; + const uint8_t b = s_uploadBuffer[i + 2u]; + if (r != 0u || g != 0u || b != 0u) + ++nonBlackPixels; + rgbSum += static_cast(r) + static_cast(g) + static_cast(b); + for (size_t channel = 0u; channel < 4u; ++channel) + { + hash ^= s_uploadBuffer[i + channel]; + hash *= 1099511628211ull; + } + } + std::cout << "[frame:stats] idx=" << s_frameStatsLogCount + << " tick=" << currentTick + << " nonBlackPixels=" << nonBlackPixels + << " rgbSum=" << rgbSum + << " fnv1a64=0x" << std::hex << hash << std::dec + << " imageUploads=" << rt->gs().nativeImageUploadCount() + << " gifPackets=" << rt->gs().nativePackedGIFPacketCount() + << std::endl; + } + ++s_frameStatsLogCount; + }); + UpdateTexture(tex, s_uploadBuffer.data()); outWidth = width; outHeight = height; @@ -760,11 +828,13 @@ bool PS2Runtime::initialize(const char *title) bool PS2Runtime::loadELF(const std::string &elfPath) { configureIoPathsFromElf(elfPath); + writePersistentDiagnostic("ELF_LOAD_BEGIN path=" + elfPath); std::ifstream file(elfPath, std::ios::binary); if (!file) { std::cerr << "Failed to open ELF file: " << elfPath << std::endl; + writePersistentDiagnostic("ELF_OPEN_FAILED"); return false; } @@ -773,6 +843,7 @@ bool PS2Runtime::loadELF(const std::string &elfPath) if (fileSize < static_cast(sizeof(ElfHeader))) { std::cerr << "ELF file is too small: " << elfPath << std::endl; + writePersistentDiagnostic("ELF_TOO_SMALL size=" + std::to_string(fileSize)); return false; } file.seekg(0, std::ios::beg); @@ -781,30 +852,35 @@ bool PS2Runtime::loadELF(const std::string &elfPath) if (!file.read(reinterpret_cast(&header), sizeof(header))) { std::cerr << "Failed to read ELF header from: " << elfPath << std::endl; + writePersistentDiagnostic("ELF_HEADER_READ_FAILED"); return false; } if (header.magic != ELF_MAGIC) { std::cerr << "Invalid ELF magic number" << std::endl; + writePersistentDiagnostic("ELF_INVALID_MAGIC"); return false; } if (header.elf_class != 1u || header.endianness != 1u) { std::cerr << "Unsupported ELF format (expected 32-bit little-endian)." << std::endl; + writePersistentDiagnostic("ELF_UNSUPPORTED_CLASS_OR_ENDIAN"); return false; } if (header.machine != EM_MIPS || header.type != ET_EXEC) { std::cerr << "Not a MIPS executable ELF file" << std::endl; + writePersistentDiagnostic("ELF_NOT_MIPS_EXECUTABLE"); return false; } if (header.phnum != 0u && header.phentsize < sizeof(ProgramHeader)) { std::cerr << "Unsupported ELF program-header entry size: " << header.phentsize << std::endl; + writePersistentDiagnostic("ELF_BAD_PROGRAM_HEADER_SIZE"); return false; } @@ -814,9 +890,19 @@ bool PS2Runtime::loadELF(const std::string &elfPath) if (programHeaderTableEnd > static_cast(fileSize)) { std::cerr << "ELF program-header table is out of range." << std::endl; + writePersistentDiagnostic("ELF_PROGRAM_HEADER_TABLE_OUT_OF_RANGE"); return false; } + { + std::ostringstream diagnostic; + diagnostic << "ELF_HEADER_OK size=" << fileSize + << " entry=0x" << std::hex << header.entry + << " phnum=" << std::dec << header.phnum + << " phentsize=" << header.phentsize; + writePersistentDiagnostic(diagnostic.str()); + } + m_cpuContext.pc = header.entry; m_debugPc.store(m_cpuContext.pc, std::memory_order_relaxed); @@ -946,6 +1032,7 @@ bool PS2Runtime::loadELF(const std::string &elfPath) if (!loadedAnySegment) { std::cerr << "ELF contains no loadable PT_LOAD segments." << std::endl; + writePersistentDiagnostic("ELF_NO_LOADABLE_SEGMENTS"); return false; } @@ -998,6 +1085,7 @@ bool PS2Runtime::loadELF(const std::string &elfPath) if (!m_romDevice.configure(identity, &romError)) { std::cerr << "[ROM0] failed to configure profile: " << romError << std::endl; + writePersistentDiagnostic("ROM_PROFILE_FAILED: " + romError); return false; } @@ -1010,6 +1098,14 @@ bool PS2Runtime::loadELF(const std::string &elfPath) elfCrc32Valid); RUNTIME_LOG("ELF file loaded successfully. Entry point: 0x" << std::hex << m_cpuContext.pc << std::dec); + { + std::ostringstream diagnostic; + diagnostic << "ELF_LOAD_OK entry=0x" << std::hex << m_cpuContext.pc + << " module_base=0x" << module.baseAddress + << " module_size=0x" << module.size << std::dec + << " crc32=" << (elfCrc32Valid ? "valid" : "unavailable"); + writePersistentDiagnostic(diagnostic.str()); + } return true; } @@ -1066,6 +1162,189 @@ void PS2Runtime::configureIoPathsFromElf(const std::string &elfPath) } setIoPaths(paths); + setPersistentDiagnosticLog(paths.elfPath); + writePersistentDiagnostic("IO_PATHS_READY iso=" + paths.cdImage.string() + + " elf_dir=" + paths.elfDirectory.string()); +} + +bool PS2Runtime::mountCdIsoImage(const std::filesystem::path &isoPath, + std::string_view bootElfName, + std::string *error) +{ + const auto fail = [error](const std::string &message) + { + if (error) + *error = message; + return false; + }; + + if (isoPath.empty() || bootElfName.empty()) + return fail("ISO path and expected PS2 boot ELF name are required"); + + auto image = std::make_shared(); + std::string imageError; + const std::filesystem::path normalizedIsoPath = normalizeAbsolutePath(isoPath); + if (normalizedIsoPath.empty() || !image->open(normalizedIsoPath, &imageError)) + return fail(imageError.empty() ? "Unable to open the selected ISO image" : imageError); + + ps2x::iso9660::DirectoryEntry bootEntry; + if (!image->findFile(bootElfName, bootEntry) && + !image->findFileRecursive(bootElfName, bootEntry)) + { + return fail("The ISO does not contain the expected boot ELF: " + std::string(bootElfName)); + } + if (bootEntry.isDirectory() || bootEntry.size < 52u) + return fail("The selected boot entry is not a valid-sized PS2 ELF file"); + + std::array elfHeader{}; + size_t headerBytesRead = 0u; + if (!image->readFile(bootEntry, 0u, elfHeader.data(), elfHeader.size(), &headerBytesRead) || + headerBytesRead != elfHeader.size()) + { + return fail("Unable to read the PS2 boot ELF header from the ISO"); + } + + if (elfHeader[0] != 0x7Fu || elfHeader[1] != 'E' || + elfHeader[2] != 'L' || elfHeader[3] != 'F') + { + return fail("The selected boot file does not have an ELF signature"); + } + if (elfHeader[4] != 1u || elfHeader[5] != 1u || elfHeader[6] != 1u) + return fail("The selected boot ELF is not a 32-bit little-endian ELF file"); + if (elfHeader[18] != 8u || elfHeader[19] != 0u) + return fail("The selected boot ELF is not for the MIPS architecture"); + + ps2x::iso9660::DirectoryEntry systemCnfEntry; + if (!image->findFile("SYSTEM.CNF", systemCnfEntry) && + !image->findFileRecursive("SYSTEM.CNF", systemCnfEntry)) + { + return fail("SYSTEM.CNF was not found in the selected ISO"); + } + + std::vector systemCnfBytes; + if (!image->readFileFully(systemCnfEntry, systemCnfBytes)) + return fail("Unable to read SYSTEM.CNF from the selected ISO"); + + std::string systemCnf(systemCnfBytes.begin(), systemCnfBytes.end()); + std::transform(systemCnf.begin(), systemCnf.end(), systemCnf.begin(), + [](unsigned char ch) { return static_cast(std::toupper(ch)); }); + std::string expectedBootName(bootElfName); + const size_t versionSuffix = expectedBootName.find(';'); + if (versionSuffix != std::string::npos) + expectedBootName.erase(versionSuffix); + std::transform(expectedBootName.begin(), expectedBootName.end(), expectedBootName.begin(), + [](unsigned char ch) { return static_cast(std::toupper(ch)); }); + + if (systemCnf.find("BOOT2") == std::string::npos || + systemCnf.find(expectedBootName) == std::string::npos) + { + return fail("SYSTEM.CNF does not reference the expected boot ELF " + expectedBootName); + } + + IoPaths paths = runtimeIoPaths(); + paths.cdImage = normalizedIsoPath; + paths.cdIsoImage = std::move(image); + setIoPaths(paths); + + if (error) + error->clear(); + std::cout << "[ISO9660] Mounted " << normalizedIsoPath.string() + << " | boot=" << bootEntry.name + << " | LBA=" << bootEntry.extentLba + << " | size=" << bootEntry.size << std::endl; + return true; +} + +bool PS2Runtime::extractMountedCdIsoBootElf(std::string_view bootElfName, + const std::filesystem::path &destinationPath, + std::string *error) +{ + const auto fail = [error](const std::string &message) + { + if (error) + *error = message; + return false; + }; + + const IoPaths &paths = runtimeIoPaths(); + if (!paths.cdIsoImage || !paths.cdIsoImage->isOpen()) + return fail("No CD-ROM ISO image is mounted"); + if (destinationPath.empty()) + return fail("Boot ELF destination path is empty"); + + const std::filesystem::path normalizedDestination = normalizeAbsolutePath(destinationPath); + if (normalizedDestination.empty() || normalizedDestination == paths.cdImage) + return fail("Boot ELF destination must be a writable file separate from the ISO image"); + + ps2x::iso9660::DirectoryEntry bootEntry; + if (!paths.cdIsoImage->findFile(bootElfName, bootEntry) && + !paths.cdIsoImage->findFileRecursive(bootElfName, bootEntry)) + { + return fail("Mounted ISO no longer contains the expected boot ELF"); + } + + std::vector elfBytes; + if (!paths.cdIsoImage->readFileFully(bootEntry, elfBytes) || elfBytes.size() < 52u) + return fail("Unable to extract the complete boot ELF from the mounted ISO"); + + if (elfBytes[0] != 0x7Fu || elfBytes[1] != 'E' || + elfBytes[2] != 'L' || elfBytes[3] != 'F' || + elfBytes[4] != 1u || elfBytes[5] != 1u || + elfBytes[18] != 8u || elfBytes[19] != 0u) + { + return fail("Extracted boot file failed the PS2 ELF header check"); + } + + std::error_code filesystemError; + const std::filesystem::path parent = normalizedDestination.parent_path(); + if (!parent.empty()) + { + std::filesystem::create_directories(parent, filesystemError); + if (filesystemError) + return fail("Unable to create the boot ELF output directory: " + filesystemError.message()); + } + + std::filesystem::path temporaryPath = normalizedDestination; + temporaryPath += ".tmp"; + { + std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); + if (!output) + return fail("Unable to create the extracted boot ELF file"); + output.write(reinterpret_cast(elfBytes.data()), + static_cast(elfBytes.size())); + output.flush(); + if (!output.good()) + { + output.close(); + std::filesystem::remove(temporaryPath, filesystemError); + return fail("Failed while writing the extracted boot ELF"); + } + } + + std::filesystem::rename(temporaryPath, normalizedDestination, filesystemError); + if (filesystemError) + { + filesystemError.clear(); + std::filesystem::remove(normalizedDestination, filesystemError); + if (filesystemError) + { + std::filesystem::remove(temporaryPath, filesystemError); + return fail("Unable to replace the previous boot ELF file"); + } + std::filesystem::rename(temporaryPath, normalizedDestination, filesystemError); + } + if (filesystemError) + { + std::filesystem::remove(temporaryPath, filesystemError); + return fail("Unable to finalize the extracted boot ELF file"); + } + + if (error) + error->clear(); + std::cout << "[ISO9660] Extracted boot ELF " << bootEntry.name + << " -> " << normalizedDestination.string() + << " (" << elfBytes.size() << " bytes)" << std::endl; + return true; } namespace @@ -2218,6 +2497,17 @@ bool PS2Runtime::eeCheckpointDue(uint32_t cycles) noexcept { const uint64_t currentTick = m_eeScheduler->currentVSyncTick(); const uint64_t waitTicks = std::max(1u, ticks); + static std::atomic s_vsyncWaitLogCount{0u}; + const uint32_t logIndex = s_vsyncWaitLogCount.fetch_add(1u, std::memory_order_relaxed); + if (logIndex < 128u) + { + RUNTIME_LOG("EE_VSYNC_WAIT index=" << logIndex + << " requested_ticks=" << ticks + << " current_tick=" << currentTick + << " target_tick=" << (currentTick + waitTicks) + << " resume_pc=0x" << std::hex << resumePc << std::dec + << std::endl); + } m_eeScheduler->waitVSync(currentTick + waitTicks - 1u, 0, [resumePc](R5900Context &context) @@ -2341,6 +2631,11 @@ void PS2Runtime::HandleIntegerOverflow(R5900Context *ctx) void PS2Runtime::run() { + { + std::ostringstream diagnostic; + diagnostic << "RUNTIME_RUN_BEGIN pc=0x" << std::hex << m_cpuContext.pc; + writePersistentDiagnostic(diagnostic.str()); + } m_stopRequested.store(false, std::memory_order_relaxed); ps2_stubs::resetSifState(); resetIop(); @@ -2374,22 +2669,28 @@ void PS2Runtime::run() uint32_t pc = m_debugPc.load(std::memory_order_relaxed); RUNTIME_LOG("Game thread returned. PC=0x" << std::hex << pc << " RA=0x" << static_cast(_mm_extract_epi32(m_cpuContext.r[31], 0)) << std::dec << std::endl); + std::ostringstream diagnostic; + diagnostic << "GAME_THREAD_RETURN pc=0x" << std::hex << pc + << " ra=0x" << static_cast(_mm_extract_epi32(m_cpuContext.r[31], 0)); + writePersistentDiagnostic(diagnostic.str()); } catch (const std::exception &e) { std::cerr << "Error during program execution: " << e.what() << std::endl; + writePersistentDiagnostic(std::string("GAME_THREAD_EXCEPTION: ") + e.what()); } catch (...) { std::cerr << "Error during program execution: unknown exception" << std::endl; + writePersistentDiagnostic("GAME_THREAD_EXCEPTION: unknown"); } gameThreadFinished.store(true, std::memory_order_release); }); uint64_t tick = 0; while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire)) { + ++tick; PS2_IF_AGRESSIVE_LOGS({ - tick++; if ((tick % 120) == 0) { uint64_t curDma = m_memory.dmaStartCount(); @@ -2423,6 +2724,38 @@ void PS2Runtime::run() uint32_t presentWidth = FB_WIDTH; uint32_t presentHeight = DEFAULT_DISPLAY_HEIGHT; UploadFrame(frameTex, this, presentWidth, presentHeight); + if ((tick % 120u) == 0u) + { + const uint64_t curDma = m_memory.dmaStartCount(); + const uint64_t curGif = m_memory.gifCopyCount(); + const uint64_t curGs = m_memory.gsWriteCount(); + const uint64_t curVif = m_memory.vifWriteCount(); + const GSRegisters &gs = m_memory.gs(); + const auto eeSnapshot = m_eeScheduler->snapshot(); + std::ostringstream diagnostic; + const uint32_t diagnosticPc = m_debugPc.load(std::memory_order_relaxed); + uint32_t instructionWords[4]{}; + const uint8_t *diagnosticRam = m_memory.getRDRAM(); + if (diagnosticRam && diagnosticPc <= PS2_RAM_MASK - sizeof(instructionWords)) + { + std::memcpy(instructionWords, diagnosticRam + (diagnosticPc & PS2_RAM_MASK), sizeof(instructionWords)); + } + diagnostic << "RUNTIME_TICK tick=" << tick + << " pc=0x" << std::hex << diagnosticPc + << " ra=0x" << m_debugRa.load(std::memory_order_relaxed) + << " sp=0x" << m_debugSp.load(std::memory_order_relaxed) + << " gp=0x" << m_debugGp.load(std::memory_order_relaxed) + << " insn=" << instructionWords[0] << "," << instructionWords[1] + << "," << instructionWords[2] << "," << instructionWords[3] + << " dispatch_trace=" << formatDispatchHistory() + << " dispfb1=0x" << gs.dispfb1 + << " display1=0x" << gs.display1 << std::dec + << " active_threads=" << eeSnapshot.threads.size() + << " dma=" << curDma << " gif=" << curGif + << " gs_writes=" << curGs << " vif=" << curVif + << " frame=" << presentWidth << "x" << presentHeight; + writePersistentDiagnostic(diagnostic.str()); + } BeginDrawing(); ClearBackground(BLACK); @@ -2469,4 +2802,5 @@ void PS2Runtime::run() CloseWindow(); RUNTIME_LOG("[run] exiting loop"); + writePersistentDiagnostic("RUNTIME_RUN_END"); } diff --git a/ps2xRuntime/src/lib/ps2_vfs.cpp b/ps2xRuntime/src/lib/ps2_vfs.cpp index aff25e6607..5e8f9d9564 100644 --- a/ps2xRuntime/src/lib/ps2_vfs.cpp +++ b/ps2xRuntime/src/lib/ps2_vfs.cpp @@ -2,6 +2,7 @@ #include "runtime/ps2_memory.h" #include "runtime/ps2_rom_device.h" +#include "runtime/iso9660_reader.h" #include #include @@ -105,6 +106,59 @@ namespace size_t m_position = 0u; }; + class IsoOpenFile final : public IPS2OpenFile + { + public: + IsoOpenFile(std::shared_ptr image, + ps2x::iso9660::DirectoryEntry entry) + : m_image(std::move(image)), m_entry(std::move(entry)) + { + } + + int64_t read(void *destination, size_t size) override + { + if (!m_image || (!destination && size != 0u)) + return -1; + size_t bytesRead = 0u; + if (!m_image->readFile(m_entry, m_position, destination, size, &bytesRead)) + return -1; + m_position += bytesRead; + return static_cast(bytesRead); + } + + int64_t write(const void *, size_t) override + { + return -1; + } + + int64_t seek(int64_t offset, int whence) override + { + int64_t base = 0; + if (whence == SEEK_CUR) + base = static_cast(m_position); + else if (whence == SEEK_END) + base = static_cast(m_entry.size); + else if (whence != SEEK_SET) + return -1; + + if ((offset > 0 && base > std::numeric_limits::max() - offset) || + (offset < 0 && base < std::numeric_limits::min() - offset)) + { + return -1; + } + const int64_t position = base + offset; + if (position < 0 || static_cast(position) > m_entry.size) + return -1; + m_position = static_cast(position); + return position; + } + + private: + std::shared_ptr m_image; + ps2x::iso9660::DirectoryEntry m_entry; + uint64_t m_position = 0u; + }; + const char *hostMode(uint32_t flags) { const bool read = (flags & PS2_FIO_O_RDONLY) != 0u || (flags & PS2_FIO_O_RDWR) == PS2_FIO_O_RDWR; @@ -171,6 +225,17 @@ int32_t PS2Vfs::open(std::string_view path, uint32_t flags, const PS2VfsMounts & return -1; file = std::make_unique(std::move(bytes)); } + else if (parsed.device == ps2x::iop::Ps2PathDevice::Cdrom && mounts.cdIsoImage) + { + const uint32_t access = flags & PS2_FIO_O_RDWR; + if (access != PS2_FIO_O_RDONLY || (flags & (PS2_FIO_O_CREAT | PS2_FIO_O_TRUNC | PS2_FIO_O_APPEND)) != 0u) + return -1; + + ps2x::iso9660::DirectoryEntry entry; + if (!mounts.cdIsoImage->findFile(parsed.path, entry) || entry.isDirectory()) + return -1; + file = std::make_unique(mounts.cdIsoImage, std::move(entry)); + } else { std::filesystem::path hostPath; @@ -246,6 +311,17 @@ bool PS2Vfs::stat(std::string_view path, const PS2VfsMounts &mounts, const PS2Ro return true; } + if (parsed.device == ps2x::iop::Ps2PathDevice::Cdrom && mounts.cdIsoImage) + { + ps2x::iso9660::DirectoryEntry entry; + if (!mounts.cdIsoImage->findFile(parsed.path, entry)) + return false; + result.directory = entry.isDirectory(); + result.size = entry.size; + result.readOnly = true; + return true; + } + std::filesystem::path hostPath; if (!resolveHostPath(path, mounts, hostPath)) return false; diff --git a/ps2xRuntime/src/main.cpp b/ps2xRuntime/src/main.cpp index 563bcbd74c..4003962365 100644 --- a/ps2xRuntime/src/main.cpp +++ b/ps2xRuntime/src/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #if defined(__ANDROID__) #include @@ -25,6 +26,40 @@ namespace { + std::filesystem::path g_androidDiagnosticPath; + + void beginAndroidDiagnostic(const std::filesystem::path &bootElfPath) + { +#if defined(__ANDROID__) + g_androidDiagnosticPath = bootElfPath.parent_path() / "manhunt2_runtime.log"; + std::ofstream output(g_androidDiagnosticPath, std::ios::out | std::ios::trunc); + if (output) + { + output << "Manhunt 2 Android startup diagnostic\n"; + output << "BOOT_ELF=" << bootElfPath.string() << '\n'; + output.flush(); + } +#else + (void)bootElfPath; +#endif + } + + void writeAndroidDiagnostic(const std::string &message) + { +#if defined(__ANDROID__) + if (g_androidDiagnosticPath.empty()) + return; + std::ofstream output(g_androidDiagnosticPath, std::ios::out | std::ios::app); + if (output) + { + output << message << '\n'; + output.flush(); + } +#else + (void)message; +#endif + } + #if defined(__ANDROID__) int g_logcatPipeFds[2]{-1, -1}; std::thread g_logcatThread; @@ -97,6 +132,7 @@ namespace std::set_terminate([]() { std::cerr << "[terminate] unhandled exception" << std::endl; + writeAndroidDiagnostic("FATAL_TERMINATE"); const std::exception_ptr ep = std::current_exception(); if (ep) { @@ -174,6 +210,43 @@ int main(int argc, char *argv[]) try { std::filesystem::path pathObj = getExecutablePath(argc, argv); + beginAndroidDiagnostic(pathObj); + writeAndroidDiagnostic("BOOT_PATH_RESOLVED"); + +#if defined(__ANDROID__) + // The Android setup screen copies the user's selected disc image here. + // Keep the existing ELF boot path: only the small boot executable is + // extracted; all CD-ROM resources remain mounted from the ISO. + const std::filesystem::path selectedIsoPath = pathObj.parent_path() / "Manhunt2.iso"; + writeAndroidDiagnostic("ISO_CHECK path=" + selectedIsoPath.string()); + std::error_code isoFileError; + if (std::filesystem::exists(selectedIsoPath, isoFileError) && !isoFileError) + { + const auto isoSize = std::filesystem::file_size(selectedIsoPath, isoFileError); + writeAndroidDiagnostic("ISO_FOUND size=" + std::to_string(isoFileError ? 0 : isoSize)); + std::string isoError; + if (!PS2Runtime::mountCdIsoImage(selectedIsoPath, "SLES_548.19;1", &isoError)) + { + std::cerr << "[ISO9660] Disc validation failed: " << isoError << std::endl; + writeAndroidDiagnostic("ISO_VALIDATION_FAILED: " + isoError); + return 1; + } + if (!PS2Runtime::extractMountedCdIsoBootElf("SLES_548.19;1", pathObj, &isoError)) + { + std::cerr << "[ISO9660] Boot ELF extraction failed: " << isoError << std::endl; + writeAndroidDiagnostic("ELF_EXTRACTION_FAILED: " + isoError); + return 1; + } + writeAndroidDiagnostic("ISO_MOUNT_AND_ELF_EXTRACTION_OK"); + } + else if (isoFileError) + { + std::cerr << "[ISO9660] Unable to inspect selected disc image: " + << isoFileError.message() << std::endl; + writeAndroidDiagnostic("ISO_STAT_FAILED: " + isoFileError.message()); + return 1; + } +#endif std::string filePathStr = pathObj.string(); std::string elfName = pathObj.filename().string(); @@ -217,16 +290,21 @@ int main(int argc, char *argv[]) if (!runtime.initialize(windowTitle.c_str())) { std::cerr << "Failed to initialize PS2 runtime" << std::endl; + writeAndroidDiagnostic("RUNTIME_INITIALIZE_FAILED"); return 1; } + writeAndroidDiagnostic("RUNTIME_INITIALIZE_OK"); if (!runtime.loadELF(filePathStr)) { std::cerr << "Failed to load ELF file: " << filePathStr << std::endl; + writeAndroidDiagnostic("ELF_LOAD_FAILED: " + filePathStr); return 1; } + writeAndroidDiagnostic("ELF_LOAD_OK; ENTERING_GAME_RUNTIME"); runtime.run(); + writeAndroidDiagnostic("GAME_RUNTIME_RETURNED"); #ifdef _DEBUG ps2_log::print_saved_location(); @@ -238,10 +316,12 @@ int main(int argc, char *argv[]) catch (const std::exception &e) { std::cerr << "[main] fatal exception: " << e.what() << std::endl; + writeAndroidDiagnostic(std::string("MAIN_EXCEPTION: ") + e.what()); } catch (...) { std::cerr << "[main] fatal exception: unknown" << std::endl; + writeAndroidDiagnostic("MAIN_EXCEPTION: unknown"); } std::cout.flush(); diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 3eaa8b3d28..cc8268cb48 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -21,6 +21,7 @@ target_include_directories(ps2_test_function_table PRIVATE ) add_library(ps2_test_lib STATIC + src/iso9660_reader_tests.cpp src/code_generator_tests.cpp src/r5900_decoder_tests.cpp src/elf_analyzer_tests.cpp diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 463b6cff1a..18975d07d1 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -2006,6 +2006,28 @@ void register_code_generator_tests() "jalr fallback should not dispatch directly to tail-jump delay slot"); }); + tc.Run("VCALLMSR reads CMSAR0 without indexing past VI registers", [](TestCase &t) { + CodeGenerator gen({}, {}); + + Instruction viSource{}; + viSource.rs = COP2_CO; + viSource.function = VU0_S1_VCALLMSR; + viSource.rd = 3; + const std::string viCode = gen.translateVUInstruction(viSource); + t.IsTrue(viCode.find("ctx->vi[3] & 0x1FF") != std::string::npos, + "VCALLMSR with VI3 must read the ordinary VI register"); + + Instruction cmsarSource{}; + cmsarSource.rs = COP2_CO; + cmsarSource.function = VU0_S1_VCALLMSR; + cmsarSource.rd = VU0_CR_CMSAR0; + const std::string cmsarCode = gen.translateVUInstruction(cmsarSource); + t.IsTrue(cmsarCode.find("ctx->vu0_cmsar0 & 0x1FF") != std::string::npos, + "VCALLMSR with CMSAR0 must read the dedicated control-register field"); + t.IsTrue(cmsarCode.find("ctx->vi[27]") == std::string::npos, + "VCALLMSR must never index VI[27], because VI has only 16 entries"); + }); + tc.Run("VU random helpers emit line comments on separate lines", [](TestCase &t) { CodeGenerator gen({}, {}); diff --git a/ps2xTest/src/iso9660_reader_tests.cpp b/ps2xTest/src/iso9660_reader_tests.cpp new file mode 100644 index 0000000000..fab35308b2 --- /dev/null +++ b/ps2xTest/src/iso9660_reader_tests.cpp @@ -0,0 +1,326 @@ +#include "MiniTest.h" +#include "runtime/iso9660_reader.h" +#include "runtime/ps2_vfs.h" +#include "runtime/ps2_rom_device.h" +#include "ps2_runtime.h" +#include "ps2_iop_host.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr size_t kSectorSize = 2048u; + + void putLe16(uint8_t *out, uint16_t value) + { + out[0] = static_cast(value); + out[1] = static_cast(value >> 8u); + } + + void putBe16(uint8_t *out, uint16_t value) + { + out[0] = static_cast(value >> 8u); + out[1] = static_cast(value); + } + + void putLe32(uint8_t *out, uint32_t value) + { + out[0] = static_cast(value); + out[1] = static_cast(value >> 8u); + out[2] = static_cast(value >> 16u); + out[3] = static_cast(value >> 24u); + } + + void putBe32(uint8_t *out, uint32_t value) + { + out[0] = static_cast(value >> 24u); + out[1] = static_cast(value >> 16u); + out[2] = static_cast(value >> 8u); + out[3] = static_cast(value); + } + + size_t writeDirectoryRecord(uint8_t *out, + uint32_t lba, + uint32_t size, + uint8_t flags, + const std::string &name) + { + const size_t identifierLength = name.size(); + const size_t recordLength = 33u + identifierLength + ((identifierLength % 2u) == 0u ? 1u : 0u); + std::memset(out, 0, recordLength); + out[0] = static_cast(recordLength); + out[1] = 0u; // extended-attribute length + putLe32(out + 2u, lba); + putBe32(out + 6u, lba); + putLe32(out + 10u, size); + putBe32(out + 14u, size); + out[25u] = flags; + putLe16(out + 28u, 1u); + putBe16(out + 30u, 1u); + out[32u] = static_cast(identifierLength); + std::memcpy(out + 33u, name.data(), identifierLength); + return recordLength; + } + + std::vector makeIsoFixture() + { + // Sectors 16/17: primary and terminator descriptors; + // sectors 20/21: root and SYSTEM directories; sector 22: boot ELF bytes. + std::vector image(24u * kSectorSize, 0u); + + uint8_t *pvd = image.data() + 16u * kSectorSize; + pvd[0] = 1u; + std::memcpy(pvd + 1u, "CD001", 5u); + pvd[6] = 1u; + putLe32(pvd + 80u, 24u); + putBe32(pvd + 84u, 24u); + putLe16(pvd + 128u, static_cast(kSectorSize)); + putBe16(pvd + 130u, static_cast(kSectorSize)); + const size_t rootLength = writeDirectoryRecord(pvd + 156u, 20u, + static_cast(kSectorSize), + 0x02u, std::string(1u, '\0')); + pvd[156u] = static_cast(rootLength); + + uint8_t *terminator = image.data() + 17u * kSectorSize; + terminator[0] = 255u; + std::memcpy(terminator + 1u, "CD001", 5u); + terminator[6] = 1u; + + uint8_t *root = image.data() + 20u * kSectorSize; + size_t cursor = 0u; + cursor += writeDirectoryRecord(root + cursor, 21u, + static_cast(kSectorSize), + 0x02u, "SYSTEM"); + cursor += writeDirectoryRecord(root + cursor, 22u, 7u, 0u, + "ROOTFILE.BIN;1"); + + uint8_t *system = image.data() + 21u * kSectorSize; + cursor = 0u; + const std::string systemConfig = + "BOOT2 = cdrom0:\\SLES_548.19;1\nVER = 2.00\nVMODE = PAL\n"; + cursor += writeDirectoryRecord(system + cursor, 22u, 52u, 0u, + "SLES_548.19;1"); + cursor += writeDirectoryRecord(system + cursor, 23u, + static_cast(systemConfig.size()), 0u, + "SYSTEM.CNF;1"); + + std::array elfHeader{}; + elfHeader[0] = 0x7Fu; + elfHeader[1] = 'E'; + elfHeader[2] = 'L'; + elfHeader[3] = 'F'; + elfHeader[4] = 1u; // ELFCLASS32 + elfHeader[5] = 1u; // little-endian + elfHeader[6] = 1u; // ELF version + elfHeader[16] = 2u; // ET_EXEC + elfHeader[18] = 8u; // EM_MIPS + elfHeader[20] = 1u; + std::memcpy(image.data() + 22u * kSectorSize, elfHeader.data(), elfHeader.size()); + std::memcpy(image.data() + 23u * kSectorSize, + systemConfig.data(), systemConfig.size()); + return image; + } + + struct TemporaryIso + { + std::filesystem::path directory; + std::filesystem::path path; + + TemporaryIso() + { + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + directory = std::filesystem::temp_directory_path() / + ("ps2recomp-iso9660-" + std::to_string(stamp)); + std::filesystem::create_directories(directory); + path = directory / "fixture.iso"; + } + + ~TemporaryIso() + { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + + bool write(const std::vector &bytes) const + { + std::ofstream output(path, std::ios::binary); + if (!output) + return false; + output.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + return output.good(); + } + }; +} + +void register_iso9660_reader_tests() +{ + MiniTest::Case("ISO9660Reader", [](TestCase &tc) + { + tc.Run("opens a PS2-style ISO and finds the boot ELF by path and recursive search", + [](TestCase &t) + { + TemporaryIso fixture; + t.IsTrue(fixture.write(makeIsoFixture()), "the synthetic ISO fixture should be written"); + + ps2x::iso9660::Image image; + std::string error; + t.IsTrue(image.open(fixture.path, &error), + "the ISO9660 primary volume descriptor and root record should be accepted"); + t.Equals(error, std::string(), "successful open should clear the error message"); + t.Equals(image.imageSize(), static_cast(24u * kSectorSize), + "the reader should report the exact image size"); + + ps2x::iso9660::DirectoryEntry boot; + t.IsTrue(image.findFile("system/sles_548.19", boot), + "path lookup should ignore case and the ISO ;version suffix"); + t.Equals(boot.extentLba, 22u, "boot ELF should resolve to the expected sector"); + t.Equals(boot.size, 52u, "boot ELF should report its directory-record size"); + + ps2x::iso9660::DirectoryEntry recursive; + t.IsTrue(image.findFileRecursive("SLES_548.19", recursive), + "recursive search should find the boot ELF below the root"); + t.Equals(recursive.extentLba, 22u, + "recursive search should return the same boot ELF extent"); + + std::vector bytes; + t.IsTrue(image.readFileFully(boot, bytes), "the boot ELF contents should be readable"); + t.Equals(bytes.size(), static_cast(52u), "the complete file size should be read"); + t.IsTrue(std::memcmp(bytes.data(), "\x7F" "ELF", 4u) == 0, + "the boot file should expose a valid ELF magic signature"); + }); + + tc.Run("runtime validates SYSTEM.CNF and the MIPS boot ELF before mounting", + [](TestCase &t) + { + TemporaryIso fixture; + t.IsTrue(fixture.write(makeIsoFixture()), "the synthetic ISO fixture should be written"); + + const PS2Runtime::IoPaths previousPaths = PS2Runtime::getIoPaths(); + std::string error; + const bool mounted = PS2Runtime::mountCdIsoImage( + fixture.path, "SLES_548.19;1", &error); + t.IsTrue(mounted, + "runtime should accept an ISO whose SYSTEM.CNF and ELF match the expected serial"); + t.Equals(error, std::string(), "successful mount should clear the error message"); + t.IsTrue(PS2Runtime::getIoPaths().cdIsoImage != nullptr, + "successful validation should publish the ISO mount to the runtime"); + t.Equals(PS2Runtime::getIoPaths().cdIsoImage->imageSize(), + static_cast(24u * kSectorSize), + "the runtime should retain the mounted image"); + + const std::filesystem::path extractedElf = fixture.directory / "SLES_548.19"; + t.IsTrue(PS2Runtime::extractMountedCdIsoBootElf( + "SLES_548.19;1", extractedElf, &error), + "runtime should extract only the boot ELF to the app-private boot path"); + t.IsTrue(std::filesystem::exists(extractedElf), + "the extracted boot ELF should be created"); + std::ifstream extracted(extractedElf, std::ios::binary); + std::array magic{}; + extracted.read(magic.data(), static_cast(magic.size())); + t.IsTrue(std::memcmp(magic.data(), "\x7F" "ELF", magic.size()) == 0, + "the extracted file should preserve its PS2 ELF signature"); + PS2Runtime::setIoPaths(previousPaths); + }); + + tc.Run("VFS opens and reads files from a mounted CD-ROM ISO", [](TestCase &t) + { + TemporaryIso fixture; + t.IsTrue(fixture.write(makeIsoFixture()), "the synthetic ISO fixture should be written"); + + auto image = std::make_shared(); + std::string error; + t.IsTrue(image->open(fixture.path, &error), "the image should mount before VFS use"); + + PS2Vfs vfs; + PS2RomDevice rom; + PS2VfsMounts mounts; + mounts.cdIsoImage = image; + + const int32_t descriptor = vfs.open("cdrom0:\\SYSTEM\\SLES_548.19;1", + 1u, // PS2_FIO_O_RDONLY + mounts, + rom); + t.IsTrue(descriptor >= 3, "VFS should open a PS2 CD-ROM file from the mounted ISO"); + + std::array bytes{}; + t.Equals(vfs.read(descriptor, bytes.data(), bytes.size()), int64_t{4}, + "VFS should return the requested boot ELF header bytes"); + t.IsTrue(std::memcmp(bytes.data(), "\x7F" "ELF", bytes.size()) == 0, + "VFS should read the ELF signature directly from the ISO extent"); + + t.Equals(vfs.seek(descriptor, 0, SEEK_SET), int64_t{0}, + "VFS should seek to the beginning of an ISO-backed file"); + t.Equals(vfs.close(descriptor), 0, "VFS should close the ISO-backed descriptor"); + + PS2VfsStat stat; + t.IsTrue(vfs.stat("cdrom0:/SYSTEM/SLES_548.19;1", mounts, rom, stat), + "VFS stat should resolve an ISO-backed file"); + t.Equals(stat.size, uint64_t{52}, "VFS stat should expose the directory-record file size"); + t.IsTrue(stat.readOnly, "CD-ROM ISO entries must be read-only"); + }); + + tc.Run("IOP host adapter opens ISO-backed cdrom0 paths", [](TestCase &t) + { + TemporaryIso fixture; + t.IsTrue(fixture.write(makeIsoFixture()), "the synthetic ISO fixture should be written"); + + auto image = std::make_shared(); + std::string error; + t.IsTrue(image->open(fixture.path, &error), "the ISO should open for IOP adapter testing"); + + const PS2Runtime::IoPaths previousPaths = PS2Runtime::getIoPaths(); + PS2Runtime::IoPaths paths = previousPaths; + paths.cdImage = fixture.path; + paths.cdIsoImage = image; + PS2Runtime::setIoPaths(paths); + + PS2Runtime runtime; + PS2IopHostAdapter host(runtime); + const std::string translated = host.translateGuestPath( + "cdrom0:\\SYSTEM\\SLES_548.19;1"); + t.IsTrue(translated.rfind("ps2x-iso://", 0u) == 0u, + "IOP path translation should return the ISO-backed handle path"); + + const uint64_t handle = host.openHostFile(translated); + t.IsTrue(handle != 0u, "IOP host should open the translated ISO file"); + uint64_t fileSize = 0u; + t.IsTrue(host.hostFileSize(handle, fileSize), + "IOP host should report the ISO directory-record file size"); + t.Equals(fileSize, uint64_t{52}, "IOP host should report the boot ELF size"); + + std::array magic{}; + size_t bytesRead = 0u; + t.IsTrue(host.readHostFile(handle, 0u, magic.data(), magic.size(), bytesRead), + "IOP host should read data from the ISO file extent"); + t.Equals(bytesRead, magic.size(), "IOP host should return the requested byte count"); + t.IsTrue(std::memcmp(magic.data(), "\x7F" "ELF", magic.size()) == 0, + "IOP host should return the PS2 ELF signature"); + host.closeHostFile(handle); + + PS2Runtime::setIoPaths(previousPaths); + }); + + tc.Run("rejects a file without an ISO9660 descriptor signature", [](TestCase &t) + { + TemporaryIso fixture; + std::vector invalid(24u * kSectorSize, 0u); + t.IsTrue(fixture.write(invalid), "the invalid fixture should be written"); + + ps2x::iso9660::Image image; + std::string error; + t.IsFalse(image.open(fixture.path, &error), + "a zero-filled file must not be accepted as an ISO9660 image"); + t.IsTrue(!error.empty(), "invalid image rejection should explain the failure"); + }); + }); +} diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 7b78891488..600ba72281 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -3,6 +3,7 @@ #include void register_code_generator_tests(); +void register_iso9660_reader_tests(); void register_r5900_decoder_tests(); void register_elf_analyzer_tests(); void register_pad_input_tests(); @@ -26,6 +27,7 @@ int main() MiniTest::BeforeEach(reset_ps2_test_function_table); register_code_generator_tests(); + register_iso9660_reader_tests(); register_r5900_decoder_tests(); register_elf_analyzer_tests(); register_pad_input_tests(); diff --git a/ps2xTest/src/ps2_iop_tests.cpp b/ps2xTest/src/ps2_iop_tests.cpp index f1172e90a2..2c488db3f3 100644 --- a/ps2xTest/src/ps2_iop_tests.cpp +++ b/ps2xTest/src/ps2_iop_tests.cpp @@ -54,6 +54,31 @@ void register_ps2_iop_tests() "stopping LIBSD should deactivate its RPC endpoint"); }); + tc.Run("CDVD ROM RPC endpoints are available before loading a disc module", [](TestCase &t) + { + iop_test::Host host; + ps2x::iop::IopSubsystem subsystem(host); + + constexpr uint32_t kCcmdSid = 0x80000592u; + constexpr uint32_t kScmdSid = 0x80000593u; + constexpr uint32_t kStcmdSid = 0x80000595u; + constexpr uint32_t kSearchSid = 0x8000059Au; + t.IsTrue(subsystem.canBindRpc(kCcmdSid), "CCMD must be bindable from the ROM service"); + t.IsTrue(subsystem.canBindRpc(kScmdSid), "SCMD must be bindable from the ROM service"); + t.IsTrue(subsystem.canBindRpc(kStcmdSid), "STCMD must be bindable from the ROM service"); + t.IsTrue(subsystem.canBindRpc(kSearchSid), "CDVD file search must be bindable from the ROM service"); + + host.fill(0x800u, 12u); + ps2x::iop::RpcRequest request{}; + request.sid = kScmdSid; + request.function = 0x27u; + request.receive = {0x800u, 12u}; + const ps2x::iop::RpcResult result = subsystem.handleRpc(request); + t.IsTrue(result.handled, "the CDVD ROM service must answer the observed initialization handshake"); + t.Equals(result.resultAddress, request.receive.address, "the RPC result must reference its receive buffer"); + t.Equals(host.word(0x800u), 1u, "SCMD 0x27 must return a nonzero initialization status"); + }); + tc.Run("unknown SID remains unhandled without a loaded IRX", [](TestCase &t) { iop_test::Host host;