From cf5f57180f7d58b2bd3b56c7688bf96cb1f9cd6d Mon Sep 17 00:00:00 2001 From: Antonio Guastella <65413476+Whoneon@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:40:57 +0100 Subject: [PATCH] Feature/mc0 IO + Win IO fixes (#57) * mc0: map memory card paths to mcRoot and related io tests * Fix mc0 test setup to initialize temp paths * fix: implementing WIN32 I/O support in syscalls * fix: avoid fioWrite deadlock Do not lock the fd map while calling getHostFile; use the syscall file mutex around fwrite instead --- ps2xRuntime/include/ps2_runtime.h | 1 + ps2xRuntime/src/lib/ps2_runtime.cpp | 7 + .../helpers/ps2_syscalls_helpers_path.inl | 19 ++ .../helpers/ps2_syscalls_helpers_runtime.inl | 6 + .../src/lib/syscalls/ps2_syscalls_fileio.inl | 87 +++--- ps2xTest/CMakeLists.txt | 3 + ps2xTest/src/main.cpp | 2 + ps2xTest/src/ps2_runtime_io_tests.cpp | 278 ++++++++++++++++++ 8 files changed, 356 insertions(+), 47 deletions(-) create mode 100644 ps2xTest/src/ps2_runtime_io_tests.cpp diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 216300c..658dae5 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -351,6 +351,7 @@ public: std::filesystem::path elfDirectory; std::filesystem::path hostRoot; std::filesystem::path cdRoot; + std::filesystem::path mcRoot; std::filesystem::path cdImage; }; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index c241c0f..3fee4e1 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -118,6 +118,7 @@ namespace defaults.elfDirectory = ec ? std::filesystem::path(".") : cwd.lexically_normal(); defaults.hostRoot = defaults.elfDirectory; defaults.cdRoot = defaults.elfDirectory; + defaults.mcRoot = defaults.elfDirectory / "mc0"; return defaults; }(); @@ -404,6 +405,7 @@ void PS2Runtime::setIoPaths(const IoPaths &paths) normalized.elfDirectory = normalizeAbsolutePath(normalized.elfDirectory); normalized.hostRoot = normalizeAbsolutePath(normalized.hostRoot); normalized.cdRoot = normalizeAbsolutePath(normalized.cdRoot); + normalized.mcRoot = normalizeAbsolutePath(normalized.mcRoot); normalized.cdImage = normalizeAbsolutePath(normalized.cdImage); if (normalized.elfDirectory.empty() && !normalized.elfPath.empty()) @@ -419,6 +421,10 @@ void PS2Runtime::setIoPaths(const IoPaths &paths) { normalized.cdRoot = normalized.elfDirectory; } + if (normalized.mcRoot.empty()) + { + normalized.mcRoot = normalized.elfDirectory / "mc0"; + } runtimeIoPaths() = normalized; } @@ -436,6 +442,7 @@ void PS2Runtime::configureIoPathsFromElf(const std::string &elfPath) { paths.hostRoot = paths.elfDirectory; paths.cdRoot = paths.elfDirectory; + paths.mcRoot = paths.elfDirectory / "mc0"; } paths.cdImage.clear(); diff --git a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_path.inl b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_path.inl index 528ae90..712fe7c 100644 --- a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_path.inl +++ b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_path.inl @@ -1,5 +1,7 @@ namespace { + constexpr char kMc0Prefix[] = "mc0:"; + std::string toLowerAscii(std::string value) { std::transform(value.begin(), value.end(), value.begin(), @@ -77,4 +79,21 @@ namespace const std::filesystem::path cwd = std::filesystem::current_path(ec); return ec ? std::filesystem::path(".") : cwd.lexically_normal(); } + + std::filesystem::path getConfiguredMcRoot() + { + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + if (!paths.mcRoot.empty()) + { + return paths.mcRoot; + } + if (!paths.elfDirectory.empty()) + { + return paths.elfDirectory / "mc0"; + } + + std::error_code ec; + const std::filesystem::path cwd = std::filesystem::current_path(ec); + return ec ? std::filesystem::path("mc0") : (cwd / "mc0").lexically_normal(); + } } diff --git a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl index 184d91f..5d6aa40 100644 --- a/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl +++ b/ps2xRuntime/src/lib/syscalls/helpers/ps2_syscalls_helpers_runtime.inl @@ -359,6 +359,12 @@ std::string translatePs2Path(const char *ps2Path) return resolveWithBase(getConfiguredCdRoot(), pathStr.substr(prefixLength)); } + if (lower.rfind(kMc0Prefix, 0) == 0) + { + const std::size_t prefixLength = sizeof(kMc0Prefix) - 1; + return resolveWithBase(getConfiguredMcRoot(), pathStr.substr(prefixLength)); + } + if (!pathStr.empty() && (pathStr.front() == '/' || pathStr.front() == '\\')) { return resolveWithBase(getConfiguredCdRoot(), pathStr); diff --git a/ps2xRuntime/src/lib/syscalls/ps2_syscalls_fileio.inl b/ps2xRuntime/src/lib/syscalls/ps2_syscalls_fileio.inl index 7f51d9f..0ed8954 100644 --- a/ps2xRuntime/src/lib/syscalls/ps2_syscalls_fileio.inl +++ b/ps2xRuntime/src/lib/syscalls/ps2_syscalls_fileio.inl @@ -182,22 +182,22 @@ void fioWrite(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) return; } + FILE *fp = getHostFile(ps2Fd); + if (!fp) + { + setReturnS32(ctx, -1); // -EFAULT + return; + } + + if (size == 0) + { + setReturnS32(ctx, 0); // Wrote 0 bytes + return; + } + size_t bytesWritten = 0; { - std::lock_guard lock(g_fd_mutex); - FILE *fp = getHostFile(ps2Fd); - if (!fp) - { - setReturnS32(ctx, -1); // -EFAULT - return; - } - - if (size == 0) - { - setReturnS32(ctx, 0); // Wrote 0 bytes - return; - } - + std::lock_guard lock(g_sys_fd_mutex); bytesWritten = ::fwrite(hostBuf, 1, size, fp); if (bytesWritten < size && ferror(fp)) { @@ -273,7 +273,7 @@ void fioLseek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) void fioMkdir(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { uint32_t pathAddr = getRegU32(ctx, 4); // $a0 - // int mode = (int)getRegU32(ctx, 5); + // int mode = (int)getRegU32(ctx, 5); // $a1 - ignored on host const char *ps2Path = reinterpret_cast(getConstMemPtr(rdram, pathAddr)); if (!ps2Path) @@ -289,20 +289,18 @@ void fioMkdir(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) setReturnS32(ctx, -1); return; } + std::error_code ec; + bool success = std::filesystem::create_directory(hostPath, ec); -#ifdef _WIN32 - int ret = -1; -#else - int ret = ::mkdir(hostPath.c_str(), 0775); -#endif - - if (ret != 0) + if (!success && ec) { - std::cerr << "fioMkdir error: mkdir failed for '" << hostPath << "': " << strerror(errno) << std::endl; - setReturnS32(ctx, -1); // errno + std::cerr << "fioMkdir error: create_directory failed for '" << hostPath + << "': " << ec.message() << std::endl; + setReturnS32(ctx, -1); } else { + std::cout << "fioMkdir: Created directory '" << hostPath << "'" << std::endl; setReturnS32(ctx, 0); // Success } } @@ -326,21 +324,18 @@ void fioChdir(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) return; } - std::cerr << "fioChdir: Attempting host chdir to '" << hostPath << "' (Stub - Check side effects)" << std::endl; + std::error_code ec; + std::filesystem::current_path(hostPath, ec); -#ifdef _WIN32 - int ret = -1; -#else - int ret = ::chdir(hostPath.c_str()); -#endif - - if (ret != 0) + if (ec) { - std::cerr << "fioChdir error: chdir failed for '" << hostPath << "': " << strerror(errno) << std::endl; + std::cerr << "fioChdir error: current_path failed for '" << hostPath + << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { + std::cout << "fioChdir: Changed directory to '" << hostPath << "'" << std::endl; setReturnS32(ctx, 0); // Success } } @@ -363,19 +358,18 @@ void fioRmdir(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) return; } -#ifdef _WIN32 - int ret = -1; -#else - int ret = ::rmdir(hostPath.c_str()); -#endif + std::error_code ec; + bool success = std::filesystem::remove(hostPath, ec); - if (ret != 0) + if (!success || ec) { - std::cerr << "fioRmdir error: rmdir failed for '" << hostPath << "': " << strerror(errno) << std::endl; + std::cerr << "fioRmdir error: remove failed for '" << hostPath + << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { + std::cout << "fioRmdir: Removed directory '" << hostPath << "'" << std::endl; setReturnS32(ctx, 0); // Success } } @@ -432,19 +426,18 @@ void fioRemove(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) return; } -#ifdef _WIN32 - int ret = -1; -#else - int ret = ::unlink(hostPath.c_str()); -#endif + std::error_code ec; + bool success = std::filesystem::remove(hostPath, ec); - if (ret != 0) + if (!success || ec) { - std::cerr << "fioRemove error: unlink failed for '" << hostPath << "': " << strerror(errno) << std::endl; + std::cerr << "fioRemove error: remove failed for '" << hostPath + << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { + std::cout << "fioRemove: Removed file '" << hostPath << "'" << std::endl; setReturnS32(ctx, 0); // Success } } diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 2f5fd3f..d91cadc 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(ps2x_tests src/code_generator_tests.cpp src/r5900_decoder_tests.cpp src/elf_analyzer_tests.cpp + src/ps2_runtime_io_tests.cpp ) option(PRINT_GENERATED_CODE "Print generated code in tests" OFF) @@ -21,9 +22,11 @@ target_include_directories(ps2x_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/ps2xRecomp/include ${CMAKE_SOURCE_DIR}/ps2xAnalyzer/include + ${CMAKE_SOURCE_DIR}/ps2xRuntime/include ) target_link_libraries(ps2x_tests PRIVATE ps2_recomp_lib ps2_analyzer_lib + ps2_runtime ) diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index c0bbee8..940b0da 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -3,11 +3,13 @@ void register_code_generator_tests(); void register_r5900_decoder_tests(); void register_elf_analyzer_tests(); +void register_ps2_runtime_io_tests(); int main() { register_code_generator_tests(); register_r5900_decoder_tests(); register_elf_analyzer_tests(); + register_ps2_runtime_io_tests(); return MiniTest::Run(); } diff --git a/ps2xTest/src/ps2_runtime_io_tests.cpp b/ps2xTest/src/ps2_runtime_io_tests.cpp new file mode 100644 index 0000000..77c2039 --- /dev/null +++ b/ps2xTest/src/ps2_runtime_io_tests.cpp @@ -0,0 +1,278 @@ +#include "MiniTest.h" +#include "ps2_runtime.h" +#include "ps2_syscalls.h" + +#include +#include +#include +#include +#include + +using namespace ps2_syscalls; + +namespace +{ + // Guest memory address ranges for test data + constexpr uint32_t GUEST_STRING_AREA_START = 0x1000; + constexpr uint32_t GUEST_BUFFER_AREA_START = 0x2000; + + // Common file I/O flag combinations + constexpr uint32_t PS2_FIO_WRITE_CREATE_TRUNC = + PS2_FIO_O_WRONLY | PS2_FIO_O_CREAT | PS2_FIO_O_TRUNC; + + void setRegU32(R5900Context &ctx, int reg, uint32_t value) + { + ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); + } + + int32_t getRegS32(const R5900Context *ctx, int reg) + { + return static_cast(::getRegU32(ctx, reg)); + } + + void writeGuestString(uint8_t *rdram, uint32_t addr, const std::string &value) + { + std::memcpy(rdram + addr, value.c_str(), value.size() + 1); + } + + struct TempPaths + { + std::filesystem::path base; + std::filesystem::path mcRoot; + std::filesystem::path cdRoot; + + ~TempPaths() + { + std::error_code ec; + std::filesystem::remove_all(base, ec); + } + }; + + TempPaths makeTempPaths() + { + TempPaths paths; + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + paths.base = std::filesystem::temp_directory_path() + / ("ps2recomp-mc0-" + std::to_string(now)); + paths.mcRoot = paths.base / "mcroot"; + paths.cdRoot = paths.base / "cdroot"; + std::filesystem::create_directories(paths.mcRoot); + std::filesystem::create_directories(paths.cdRoot); + return paths; + } + + struct TestContext + { + TempPaths paths; + std::vector rdram; + R5900Context ctx; + + TestContext() : paths(makeTempPaths()), rdram(PS2_RAM_SIZE, 0) + { + PS2Runtime::IoPaths ioPaths; + ioPaths.elfDirectory = paths.cdRoot; + ioPaths.hostRoot = paths.cdRoot; + ioPaths.cdRoot = paths.cdRoot; + ioPaths.mcRoot = paths.mcRoot; + PS2Runtime::setIoPaths(ioPaths); + } + }; +} + +void register_ps2_runtime_io_tests() +{ + MiniTest::Case("PS2RuntimeIO", [](TestCase &tc) + { + tc.Run("mc0 directory creation", [](TestCase &t) + { + TestContext test; + + const std::string dirPath = "mc0:/SAVEDATA"; + const uint32_t dirAddr = GUEST_STRING_AREA_START; + writeGuestString(test.rdram.data(), dirAddr, dirPath); + + setRegU32(test.ctx, 4, dirAddr); + fioMkdir(test.rdram.data(), &test.ctx, nullptr); + + const int32_t result = getRegS32(&test.ctx, 2); + t.IsTrue(result >= 0, "fioMkdir should succeed for mc0: directory"); + + const std::filesystem::path expected = test.paths.mcRoot / "SAVEDATA"; + t.IsTrue(std::filesystem::exists(expected), + "Directory should exist under mcRoot"); + t.IsTrue(std::filesystem::is_directory(expected), + "Created path should be a directory"); + }); + + tc.Run("mc0 file write operations", [](TestCase &t) + { + TestContext test; + + // Setup: create directory first + const std::string dirPath = "mc0:/SAVEDATA"; + const uint32_t dirAddr = GUEST_STRING_AREA_START; + writeGuestString(test.rdram.data(), dirAddr, dirPath); + setRegU32(test.ctx, 4, dirAddr); + fioMkdir(test.rdram.data(), &test.ctx, nullptr); + + // Test: open file for writing + const std::string filePath = "mc0:/SAVEDATA/test.txt"; + const uint32_t fileAddr = GUEST_STRING_AREA_START + 0x100; + writeGuestString(test.rdram.data(), fileAddr, filePath); + + setRegU32(test.ctx, 4, fileAddr); + setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC); + fioOpen(test.rdram.data(), &test.ctx, nullptr); + + const int32_t fd = getRegS32(&test.ctx, 2); + t.IsTrue(fd >= 0, "fioOpen should return valid file descriptor"); + + // Write payload + const std::string payload = "hello mc0"; + const uint32_t bufAddr = GUEST_BUFFER_AREA_START; + std::memcpy(test.rdram.data() + bufAddr, payload.data(), payload.size()); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, bufAddr); + setRegU32(test.ctx, 6, static_cast(payload.size())); + fioWrite(test.rdram.data(), &test.ctx, nullptr); + + const int32_t bytesWritten = getRegS32(&test.ctx, 2); + t.Equals(bytesWritten, static_cast(payload.size()), + "fioWrite should write all bytes"); + + // Close file + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, nullptr); + + const int32_t closeResult = getRegS32(&test.ctx, 2); + t.IsTrue(closeResult >= 0, "fioClose should succeed"); + + // Verify on host filesystem + const std::filesystem::path expectedPath = + test.paths.mcRoot / "SAVEDATA" / "test.txt"; + t.IsTrue(std::filesystem::exists(expectedPath), + "File should exist under mcRoot"); + + std::ifstream in(expectedPath, std::ios::binary); + std::string readback( + (std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + t.Equals(readback, payload, "File content should match written payload"); + }); + + tc.Run("mc0 file read operations", [](TestCase &t) + { + TestContext test; + + // Setup: create directory and write file + const std::string dirPath = "mc0:/SAVEDATA"; + const uint32_t dirAddr = GUEST_STRING_AREA_START; + writeGuestString(test.rdram.data(), dirAddr, dirPath); + setRegU32(test.ctx, 4, dirAddr); + fioMkdir(test.rdram.data(), &test.ctx, nullptr); + + const std::string filePath = "mc0:/SAVEDATA/test.txt"; + const uint32_t fileAddr = GUEST_STRING_AREA_START + 0x100; + writeGuestString(test.rdram.data(), fileAddr, filePath); + + // Write data + const std::string payload = "hello mc0 read test"; + const uint32_t writeBufAddr = GUEST_BUFFER_AREA_START; + std::memcpy(test.rdram.data() + writeBufAddr, payload.data(), payload.size()); + + setRegU32(test.ctx, 4, fileAddr); + setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC); + fioOpen(test.rdram.data(), &test.ctx, nullptr); + int32_t fd = getRegS32(&test.ctx, 2); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, writeBufAddr); + setRegU32(test.ctx, 6, static_cast(payload.size())); + fioWrite(test.rdram.data(), &test.ctx, nullptr); + + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, nullptr); + + // Test: read back via fioRead + setRegU32(test.ctx, 4, fileAddr); + setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY); + fioOpen(test.rdram.data(), &test.ctx, nullptr); + fd = getRegS32(&test.ctx, 2); + t.IsTrue(fd >= 0, "fioOpen for reading should succeed"); + + // Read into different buffer area + const uint32_t readBufAddr = GUEST_BUFFER_AREA_START + 0x1000; + std::memset(test.rdram.data() + readBufAddr, 0, payload.size()); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, readBufAddr); + setRegU32(test.ctx, 6, static_cast(payload.size())); + fioRead(test.rdram.data(), &test.ctx, nullptr); + + const int32_t bytesRead = getRegS32(&test.ctx, 2); + t.Equals(bytesRead, static_cast(payload.size()), + "fioRead should read all bytes"); + + std::string readback( + reinterpret_cast(test.rdram.data() + readBufAddr), + payload.size() + ); + t.Equals(readback, payload, "fioRead content should match original"); + + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, nullptr); + }); + + tc.Run("mc0 paths isolated from cdRoot", [](TestCase &t) + { + TestContext test; + + const std::string dirPath = "mc0:/ISOLATED"; + const std::string filePath = "mc0:/ISOLATED/test.txt"; + const uint32_t dirAddr = GUEST_STRING_AREA_START; + const uint32_t fileAddr = GUEST_STRING_AREA_START + 0x100; + + writeGuestString(test.rdram.data(), dirAddr, dirPath); + writeGuestString(test.rdram.data(), fileAddr, filePath); + + // Create directory and file on mc0: + setRegU32(test.ctx, 4, dirAddr); + fioMkdir(test.rdram.data(), &test.ctx, nullptr); + + setRegU32(test.ctx, 4, fileAddr); + setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC); + fioOpen(test.rdram.data(), &test.ctx, nullptr); + const int32_t fd = getRegS32(&test.ctx, 2); + + const std::string payload = "isolation test"; + const uint32_t bufAddr = GUEST_BUFFER_AREA_START; + std::memcpy(test.rdram.data() + bufAddr, payload.data(), payload.size()); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, bufAddr); + setRegU32(test.ctx, 6, static_cast(payload.size())); + fioWrite(test.rdram.data(), &test.ctx, nullptr); + + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, nullptr); + + // Verify isolation + const std::filesystem::path expectedMc = + test.paths.mcRoot / "ISOLATED" / "test.txt"; + const std::filesystem::path unexpectedCd = + test.paths.cdRoot / "ISOLATED" / "test.txt"; + + t.IsTrue(std::filesystem::exists(expectedMc), + "mc0: file should exist under mcRoot"); + t.IsFalse(std::filesystem::exists(unexpectedCd), + "mc0: file should NOT exist under cdRoot"); + + // Verify mcRoot directory structure + t.IsTrue(std::filesystem::exists(test.paths.mcRoot / "ISOLATED"), + "mc0: directory should exist under mcRoot"); + t.IsFalse(std::filesystem::exists(test.paths.cdRoot / "ISOLATED"), + "mc0: directory should NOT exist under cdRoot"); + }); + }); +}