diff --git a/.gitmodules b/.gitmodules index 248a04748..3da8de040 100644 --- a/.gitmodules +++ b/.gitmodules @@ -158,3 +158,9 @@ [submodule "externals/protobuf"] path = externals/protobuf url = https://github.com/shadexternals/protobuf.git +[submodule "externals/zstd"] + path = externals/zstd + url = https://github.com/shadexternals/zstd.git +[submodule "externals/zarchive"] + path = externals/zarchive + url = https://github.com/shadexternals/ZArchive.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 90a435e4c..3a2e9decb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -404,6 +404,8 @@ set(AVPLAYER_LIB src/core/libraries/avplayer/avplayer_common.cpp src/core/libraries/avplayer/avplayer_common.h src/core/libraries/avplayer/avplayer_file_streamer.cpp src/core/libraries/avplayer/avplayer_file_streamer.h + src/core/libraries/avplayer/avplayer_handle_streamer.cpp + src/core/libraries/avplayer/avplayer_handle_streamer.h src/core/libraries/avplayer/avplayer_impl.cpp src/core/libraries/avplayer/avplayer_impl.h src/core/libraries/avplayer/avplayer_source.cpp @@ -876,6 +878,11 @@ set(CORE src/core/aerolib/stubs.cpp src/core/file_format/npbind.h src/core/file_sys/fs.cpp src/core/file_sys/fs.h + src/core/file_sys/ifile.h + src/core/file_sys/backends/host_fs.cpp + src/core/file_sys/backends/host_fs.h + src/core/file_sys/backends/zarchive_fs.cpp + src/core/file_sys/backends/zarchive_fs.h src/core/ipc/ipc.cpp src/core/ipc/ipc.h src/core/loader/dwarf.cpp @@ -1245,6 +1252,7 @@ target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAlloca target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib miniupnpc::miniupnpc) target_link_libraries(shadps4 PRIVATE Freetype::Freetype) target_link_libraries(shadps4 PRIVATE libprotobuf) +target_link_libraries(shadps4 PRIVATE ZArchive::zarchive) target_include_directories(shadps4 PRIVATE "${SHADNET_PROTO_OUT}") if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 05e8f7cfa..159ce9c6f 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -394,3 +394,28 @@ if(MSVC) set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "" FORCE) endif() add_subdirectory(protobuf EXCLUDE_FROM_ALL) + +# zstd (dependency of ZArchive) +if (NOT TARGET libzstd_static) + set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) + set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE) + set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE) + set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE) + set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE) + add_subdirectory(zstd/build/cmake) +endif() + +# ZArchive (.zar archive support) +if (NOT TARGET zarchive) + add_library(zarchive STATIC + zarchive/src/zarchivereader.cpp + zarchive/src/zarchivewriter.cpp + zarchive/src/sha_256.c + ) + target_include_directories(zarchive PUBLIC zarchive/include) + target_include_directories(zarchive PRIVATE zstd/lib) + target_link_libraries(zarchive PRIVATE libzstd_static) + target_compile_features(zarchive PRIVATE cxx_std_20) + add_library(ZArchive::zarchive ALIAS zarchive) +endif() diff --git a/externals/zarchive b/externals/zarchive new file mode 160000 index 000000000..965b66c8d --- /dev/null +++ b/externals/zarchive @@ -0,0 +1 @@ +Subproject commit 965b66c8d67b6b7e30fd63b3b75aa91a99ff303b diff --git a/externals/zstd b/externals/zstd new file mode 160000 index 000000000..5c7b7bad2 --- /dev/null +++ b/externals/zstd @@ -0,0 +1 @@ +Subproject commit 5c7b7bad26808e6b40ac3b3d0075466e27738a9d diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 6241ccc0d..69c2163c9 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -73,7 +73,7 @@ class ElfInfo { u32 sdk_ver = 0; PSFAttributes psf_attributes{}; - std::filesystem::path splash_path{}; + std::vector splash_data{}; std::filesystem::path game_folder{}; std::vector npCommIds{}; std::map trophy_index_map{}; @@ -138,8 +138,8 @@ public: return psf_attributes; } - [[nodiscard]] const std::filesystem::path& GetSplashPath() const { - return splash_path; + [[nodiscard]] const std::vector& GetSplashData() const { + return splash_data; } [[nodiscard]] const std::filesystem::path& GetGameFolder() const { diff --git a/src/core/file_format/npbind.cpp b/src/core/file_format/npbind.cpp index b2900efa0..fd302c1a1 100644 --- a/src/core/file_format/npbind.cpp +++ b/src/core/file_format/npbind.cpp @@ -9,8 +9,6 @@ #include "npbind.h" bool NPBindFile::Load(const std::string& path) { - Clear(); // Clear any existing data - std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) return false; @@ -24,12 +22,18 @@ bool NPBindFile::Load(const std::string& path) { if (!f.read(reinterpret_cast(buf.data()), sz)) return false; - const u64 size = buf.size(); + return Load(std::span{buf}); +} + +bool NPBindFile::Load(std::span data) { + Clear(); // Clear any existing data + + const u64 size = data.size(); if (size < sizeof(NpBindHeader)) return false; // Read header - memcpy(&m_header, buf.data(), sizeof(NpBindHeader)); + memcpy(&m_header, data.data(), sizeof(NpBindHeader)); if (m_header.magic != NPBIND_MAGIC) return false; @@ -53,14 +57,14 @@ bool NPBindFile::Load(const std::string& path) { if (offset + 4 > size) return false; - memcpy(&e.type, &buf[offset], 2); - memcpy(&e.size, &buf[offset + 2], 2); + memcpy(&e.type, &data[offset], 2); + memcpy(&e.size, &data[offset + 2], 2); offset += 4; if (offset + e.size > size) return false; - e.data.assign(buf.begin() + offset, buf.begin() + offset + e.size); + e.data.assign(data.begin() + offset, data.begin() + offset + e.size); offset += e.size; return true; }; @@ -89,7 +93,7 @@ bool NPBindFile::Load(const std::string& path) { // Read digest if available if (size >= 20) { // Digest is typically the last 20 bytes, independent of offset - memcpy(m_digest, &buf[size - 20], 20); + memcpy(m_digest, &data[size - 20], 20); } else { memset(m_digest, 0, 20); } diff --git a/src/core/file_format/npbind.h b/src/core/file_format/npbind.h index 44d6528bd..13acc658b 100644 --- a/src/core/file_format/npbind.h +++ b/src/core/file_format/npbind.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include #include "common/endian.h" @@ -48,6 +49,7 @@ public: // Load from file bool Load(const std::string& path); + bool Load(std::span data); // Accessors const NpBindHeader& Header() const { diff --git a/src/core/file_format/playgo_chunk.cpp b/src/core/file_format/playgo_chunk.cpp index 4e2faea45..c7c7356f0 100644 --- a/src/core/file_format/playgo_chunk.cpp +++ b/src/core/file_format/playgo_chunk.cpp @@ -1,6 +1,8 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "playgo_chunk.h" bool PlaygoFile::Open(const std::filesystem::path& filepath) { @@ -14,6 +16,60 @@ bool PlaygoFile::Open(const std::filesystem::path& filepath) { return false; } +bool PlaygoFile::Open(std::span data) { + if (data.size() < sizeof(PlaygoHeader)) { + return false; + } + std::memcpy(&playgoHeader, data.data(), sizeof(PlaygoHeader)); + if (playgoHeader.magic != PLAYGO_MAGIC) { + return false; + } + + const auto load_chunk = [&](const chunk_t chunk, std::string& out) -> bool { + if (chunk.offset + chunk.length > data.size()) { + return false; + } + out.assign(reinterpret_cast(data.data() + chunk.offset), chunk.length); + return true; + }; + + std::string chunk_attrs_data, chunk_mchunks_data, chunk_labels_data, mchunk_attrs_data; + if (!load_chunk(playgoHeader.chunk_attrs, chunk_attrs_data)) + return false; + if (!load_chunk(playgoHeader.chunk_mchunks, chunk_mchunks_data)) + return false; + if (!load_chunk(playgoHeader.chunk_labels, chunk_labels_data)) + return false; + if (!load_chunk(playgoHeader.mchunk_attrs, mchunk_attrs_data)) + return false; + + chunks.resize(playgoHeader.chunk_count); + + auto chunk_attrs = reinterpret_cast(&chunk_attrs_data[0]); + auto chunk_mchunks = reinterpret_cast(&chunk_mchunks_data[0]); + auto chunk_labels = reinterpret_cast(&chunk_labels_data[0]); + auto mchunk_attrs = reinterpret_cast(&mchunk_attrs_data[0]); + + for (u16 i = 0; i < playgoHeader.chunk_count; i++) { + chunks[i].req_locus = chunk_attrs[i].req_locus; + chunks[i].language_mask = chunk_attrs[i].language_mask; + chunks[i].label_name = std::string(chunk_labels + chunk_attrs[i].label_offset); + + u64 total_size = 0; + u16 mchunk_count = chunk_attrs[i].mchunk_count; + if (mchunk_count != 0) { + auto mchunks = + reinterpret_cast(((u8*)chunk_mchunks + chunk_attrs[i].mchunks_offset)); + for (u16 j = 0; j < mchunk_count; j++) { + u16 mchunk_id = mchunks[j]; + total_size += mchunk_attrs[mchunk_id].size.size; + } + } + chunks[i].total_size = total_size; + } + return true; +} + bool PlaygoFile::LoadChunks(const Common::FS::IOFile& file) { if (file.IsOpen()) { if (playgoHeader.magic == PLAYGO_MAGIC) { diff --git a/src/core/file_format/playgo_chunk.h b/src/core/file_format/playgo_chunk.h index 12d8f022e..40a6f7d4b 100644 --- a/src/core/file_format/playgo_chunk.h +++ b/src/core/file_format/playgo_chunk.h @@ -4,6 +4,7 @@ #pragma once #include #include +#include #include #include "common/io_file.h" #include "core/libraries/playgo/playgo_types.h" @@ -111,6 +112,7 @@ public: ~PlaygoFile() = default; bool Open(const std::filesystem::path& filepath); + bool Open(std::span data); bool LoadChunks(const Common::FS::IOFile& file); PlaygoHeader& GetPlaygoHeader() { diff --git a/src/core/file_sys/backends/host_fs.cpp b/src/core/file_sys/backends/host_fs.cpp new file mode 100644 index 000000000..85c9d0832 --- /dev/null +++ b/src/core/file_sys/backends/host_fs.cpp @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) +#include +#endif + +#include "common/string_util.h" +#include "core/file_sys/backends/host_fs.h" +#include "core/file_sys/fs.h" + +namespace Core::FileSys { + +HostFile::HostFile(std::filesystem::path host_path, Common::FS::FileAccessMode mode, bool read_only) + : m_path(std::move(host_path)), m_file(m_path, mode), m_read_only(read_only) {} + +s64 HostFile::Read(void* dst, u64 size) { + if (!m_file.IsOpen()) { + return -1; + } + return static_cast(m_file.ReadRaw(dst, size)); +} + +s64 HostFile::Write(const void* src, u64 size) { + if (!m_file.IsOpen() || m_read_only) { + return -1; + } + return static_cast(m_file.WriteRaw(src, size)); +} + +bool HostFile::Seek(s64 offset, Common::FS::SeekOrigin origin) { + if (!m_file.IsOpen()) { + return false; + } + return m_file.Seek(offset, origin); +} + +u64 HostFile::Tell() const { + if (!m_file.IsOpen()) { + return 0; + } + return static_cast(m_file.Tell()); +} + +u64 HostFile::Size() const { + if (!m_file.IsOpen()) { + return 0; + } + return m_file.GetSize(); +} + +bool HostFile::Flush() { + if (!m_file.IsOpen()) { + return false; + } + return m_file.Flush(); +} + +bool HostFile::IsOpen() const { + return m_file.IsOpen(); +} + +void HostFile::Stat(FileStat& out) { + out.size = Size(); + out.is_directory = false; +#if defined(__linux__) || defined(__FreeBSD__) + struct stat st = {}; + if (::stat(m_path.string().c_str(), &st) == 0) { + out.mtime_sec = static_cast(st.st_mtim.tv_sec); + out.mtime_nsec = static_cast(st.st_mtim.tv_nsec); + out.atime_sec = static_cast(st.st_atim.tv_sec); + out.atime_nsec = static_cast(st.st_atim.tv_nsec); + out.ctime_sec = static_cast(st.st_ctim.tv_sec); + out.ctime_nsec = static_cast(st.st_ctim.tv_nsec); + } +#elif defined(__APPLE__) + struct stat st = {}; + if (::stat(m_path.string().c_str(), &st) == 0) { + out.mtime_sec = static_cast(st.st_mtimespec.tv_sec); + out.mtime_nsec = static_cast(st.st_mtimespec.tv_nsec); + out.atime_sec = static_cast(st.st_atimespec.tv_sec); + out.atime_nsec = static_cast(st.st_atimespec.tv_nsec); + out.ctime_sec = static_cast(st.st_ctimespec.tv_sec); + out.ctime_nsec = static_cast(st.st_ctimespec.tv_nsec); + } +#else + std::error_code ec; + const auto ft = std::filesystem::last_write_time(m_path, ec); + if (!ec) { + const auto sctp = std::chrono::time_point_cast( + ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + const auto secs = std::chrono::time_point_cast(sctp); + const auto nsecs = std::chrono::duration_cast(sctp - secs); + out.mtime_sec = static_cast(secs.time_since_epoch().count()); + out.mtime_nsec = static_cast(nsecs.count()); + out.atime_sec = out.mtime_sec; + out.atime_nsec = out.mtime_nsec; + out.ctime_sec = out.mtime_sec; + out.ctime_nsec = out.mtime_nsec; + } +#endif +} + +HostDirectory::HostDirectory(const std::filesystem::path& root) : m_root(root) { + std::error_code ec; + m_it = std::filesystem::directory_iterator(m_root, ec); + m_end = std::filesystem::directory_iterator{}; +} + +bool HostDirectory::Next(DirEntry& out) { + if (m_it == m_end) { + return false; + } + + const auto& entry = *m_it; + std::error_code ec; + out.name = entry.path().filename().string(); + out.is_directory = entry.is_directory(ec); + out.size = out.is_directory ? 0 : entry.file_size(ec); + + ++m_it; + return true; +} + +void HostDirectory::Rewind() { + std::error_code ec; + m_it = std::filesystem::directory_iterator(m_root, ec); +} + +HostFsBackend::HostFsBackend(std::filesystem::path root, bool read_only) + : m_root(std::move(root)), m_read_only(read_only) {} + +std::filesystem::path HostFsBackend::Resolve(std::string_view rel_path) const { + if (rel_path.empty()) { + return m_root; + } + std::filesystem::path direct = m_root; + direct /= rel_path; + if constexpr (!NeedsCaseInsensitiveSearch) { + return direct; + } else { + std::error_code ec; + if (std::filesystem::exists(direct, ec)) { + return direct; + } + return ResolveCaseInsensitive(rel_path).value_or(std::move(direct)); + } +} + +std::optional HostFsBackend::ResolveCaseInsensitive( + std::string_view rel_path) const { + std::scoped_lock lk{m_case_cache_mutex}; + + std::filesystem::path current = m_root; + std::string prefix; + std::error_code ec; + + for (const auto& part : std::filesystem::path(rel_path)) { + const std::string part_str = part.string(); + if (part_str.empty() || part_str == ".") { + continue; + } + if (!prefix.empty()) { + prefix += '/'; + } + prefix += Common::ToLower(part_str); + + if (const auto it = m_case_cache.find(prefix); it != m_case_cache.end()) { + current = it->second; + continue; + } + + if (std::filesystem::path candidate = current / part_str; + std::filesystem::exists(candidate, ec)) { + current = std::move(candidate); + m_case_cache.emplace(prefix, current); + continue; + } + + const std::string part_low = Common::ToLower(part_str); + bool found = false; + for (const auto& entry : std::filesystem::directory_iterator(current, ec)) { + const auto name = entry.path().filename(); + if (Common::ToLower(name.string()) != part_low) { + continue; + } + current /= name; + m_case_cache.emplace(prefix, current); + found = true; + break; + } + if (!found) { + return std::nullopt; + } + } + return current; +} + +bool HostFsBackend::Exists(std::string_view rel_path) { + std::error_code ec; + return std::filesystem::exists(Resolve(rel_path), ec); +} + +bool HostFsBackend::IsDirectory(std::string_view rel_path) { + std::error_code ec; + return std::filesystem::is_directory(Resolve(rel_path), ec); +} + +std::unique_ptr HostFsBackend::Open(std::string_view rel_path, + Common::FS::FileAccessMode mode) { + const bool writable = mode != Common::FS::FileAccessMode::Read; + if (writable && m_read_only) { + return nullptr; + } + const auto path = Resolve(rel_path); + std::error_code ec; + if (mode != Common::FS::FileAccessMode::Create && !std::filesystem::is_regular_file(path, ec)) { + return nullptr; + } + auto handle = std::make_unique(path, mode, m_read_only || !writable); + if (!handle->IsOpen()) { + return nullptr; + } + return handle; +} + +std::unique_ptr HostFsBackend::OpenDir(std::string_view rel_path) { + const auto path = Resolve(rel_path); + std::error_code ec; + if (!std::filesystem::is_directory(path, ec)) { + return nullptr; + } + return std::make_unique(path); +} + +} // namespace Core::FileSys diff --git a/src/core/file_sys/backends/host_fs.h b/src/core/file_sys/backends/host_fs.h new file mode 100644 index 000000000..d9c716461 --- /dev/null +++ b/src/core/file_sys/backends/host_fs.h @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "common/io_file.h" +#include "core/file_sys/ifile.h" + +namespace Core::FileSys { + +/// IFile implementation backed by a real host file. +class HostFile final : public IFile { +public: + HostFile(std::filesystem::path host_path, Common::FS::FileAccessMode mode, bool read_only); + ~HostFile() override = default; + + s64 Read(void* dst, u64 size) override; + s64 Write(const void* src, u64 size) override; + bool Seek(s64 offset, Common::FS::SeekOrigin origin) override; + u64 Tell() const override; + u64 Size() const override; + bool Flush() override; + bool IsOpen() const override; + + std::optional GetHostPath() const override { + return m_path; + } + + Common::FS::IOFile* GetHostFile() override { + return &m_file; + } + + MmapPolicy GetMmapPolicy() const override { + return MmapPolicy::Native; + } + + bool IsReadOnly() const override { + return m_read_only; + } + + bool IsWriteOnly() const override { + return m_file.IsOpen() && m_file.IsWriteOnly(); + } + + void Stat(FileStat& out) override; + +private: + std::filesystem::path m_path; + Common::FS::IOFile m_file; + bool m_read_only; +}; + +/// Streaming iterator over a host directory. +class HostDirectory final : public IDirectory { +public: + explicit HostDirectory(const std::filesystem::path& root); + ~HostDirectory() override = default; + + bool Next(DirEntry& out) override; + void Rewind() override; + +private: + std::filesystem::path m_root; + std::filesystem::directory_iterator m_it; + std::filesystem::directory_iterator m_end; +}; + +class HostFsBackend final : public IBackend { +public: + HostFsBackend(std::filesystem::path root, bool read_only); + ~HostFsBackend() override = default; + + bool Exists(std::string_view rel_path) override; + bool IsDirectory(std::string_view rel_path) override; + + std::unique_ptr Open(std::string_view rel_path, + Common::FS::FileAccessMode mode) override; + std::unique_ptr OpenDir(std::string_view rel_path) override; + + bool IsReadOnly() const override { + return m_read_only; + } + + std::optional RootHostPath() const override { + return m_root; + } + +private: + std::filesystem::path Resolve(std::string_view rel_path) const; + std::optional ResolveCaseInsensitive(std::string_view rel_path) const; + std::filesystem::path m_root; + bool m_read_only; + mutable std::mutex m_case_cache_mutex; + mutable tsl::robin_map m_case_cache; +}; + +} // namespace Core::FileSys diff --git a/src/core/file_sys/backends/zarchive_fs.cpp b/src/core/file_sys/backends/zarchive_fs.cpp new file mode 100644 index 000000000..a6610f95a --- /dev/null +++ b/src/core/file_sys/backends/zarchive_fs.cpp @@ -0,0 +1,355 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) +#include +#endif + +#include + +#include "common/logging/log.h" +#include "common/thread.h" +#include "core/file_sys/backends/zarchive_fs.h" + +namespace Core::FileSys { + +// ZArchive lookups do not accept a leading slash. +std::string_view NormalizeRel(std::string_view rel) { + while (!rel.empty() && rel.front() == '/') { + rel.remove_prefix(1); + } + return rel; +} + +SharedReader::SharedReader(ZArchiveReader* r) : reader(r) { + if (reader == nullptr) { + return; + } + m_worker = std::thread([this] { WorkerLoop(); }); + m_worker_id = m_worker.get_id(); +} + +SharedReader::~SharedReader() { + if (m_worker.joinable()) { + { + std::scoped_lock lk{m_job_mutex}; + m_stop = true; + } + m_job_cv.notify_all(); + m_worker.join(); + } + delete reader; +} + +void SharedReader::WorkerLoop() { + Common::SetCurrentThreadName("shadPS4:ZArchiveIO"); + std::unique_lock lk{m_job_mutex}; + while (true) { + m_job_cv.wait(lk, [this] { return m_has_job || m_stop; }); + if (!m_has_job) { + break; + } + const auto fn = m_job_fn; + auto* const ctx = m_job_ctx; + lk.unlock(); + fn(ctx); + lk.lock(); + m_has_job = false; + m_job_cv.notify_all(); + } +} + +void SharedReader::Dispatch(JobFn fn, void* ctx) { + if (!m_worker.joinable() || std::this_thread::get_id() == m_worker_id) { + fn(ctx); + return; + } + std::unique_lock lk{m_job_mutex}; + m_job_cv.wait(lk, [this] { return !m_has_job; }); + m_job_fn = fn; + m_job_ctx = ctx; + m_has_job = true; + m_job_cv.notify_all(); + m_job_cv.wait(lk, [this] { return !m_has_job; }); +} + +ZArchiveFile::ZArchiveFile(std::shared_ptr reader, uint32_t node, u64 size, + std::filesystem::path archive_path) + : m_reader(std::move(reader)), m_node(node), m_size(size), + m_archive_path(std::move(archive_path)) {} + +s64 ZArchiveFile::Read(void* dst, u64 size) { + if (!IsOpen() || size == 0) { + return 0; + } + u64 pos; + u64 clamped; + { + std::scoped_lock lk{m_position_mutex}; + pos = m_position; + if (pos >= m_size) { + return 0; + } + clamped = std::min(size, m_size - pos); + } + + struct ReadJob { + ZArchiveReader* reader; + uint32_t node; + u64 pos; + u64 size; + void* dst; + u64 out; + } job{m_reader->reader, m_node, pos, clamped, dst, 0}; + + { + std::scoped_lock lk{m_reader->mutex}; + m_reader->Dispatch( + [](void* p) { + auto* j = static_cast(p); + j->out = j->reader->ReadFromFile(j->node, j->pos, j->size, j->dst); + }, + &job); + } + const u64 read = job.out; + + { + std::scoped_lock lk{m_position_mutex}; + m_position = pos + read; + } + return static_cast(read); +} + +s64 ZArchiveFile::Write(const void* /*src*/, u64 /*size*/) { + // ZArchive is read-only + return -1; +} + +bool ZArchiveFile::Seek(s64 offset, Common::FS::SeekOrigin origin) { + std::scoped_lock lk{m_position_mutex}; + s64 base = 0; + switch (origin) { + case Common::FS::SeekOrigin::SetOrigin: + base = 0; + break; + case Common::FS::SeekOrigin::CurrentPosition: + base = static_cast(m_position); + break; + case Common::FS::SeekOrigin::End: + base = static_cast(m_size); + break; + } + const s64 target = base + offset; + if (target < 0) { + return false; + } + m_position = static_cast(target); + return true; +} + +u64 ZArchiveFile::Tell() const { + std::scoped_lock lk{m_position_mutex}; + return m_position; +} + +u64 ZArchiveFile::Size() const { + return m_size; +} + +bool ZArchiveFile::Flush() { + // Read-only backend, nothing to flush. + return true; +} + +bool ZArchiveFile::IsOpen() const { + return m_reader && m_reader->reader != nullptr; +} + +void ZArchiveFile::Stat(FileStat& out) { + out.size = m_size; + out.is_directory = false; + // Every entry inside a .zar shares the archive's mtime +#if defined(__linux__) || defined(__FreeBSD__) + struct stat st = {}; + if (::stat(m_archive_path.string().c_str(), &st) == 0) { + out.mtime_sec = static_cast(st.st_mtim.tv_sec); + out.mtime_nsec = static_cast(st.st_mtim.tv_nsec); + out.atime_sec = static_cast(st.st_atim.tv_sec); + out.atime_nsec = static_cast(st.st_atim.tv_nsec); + out.ctime_sec = static_cast(st.st_ctim.tv_sec); + out.ctime_nsec = static_cast(st.st_ctim.tv_nsec); + } +#elif defined(__APPLE__) + struct stat st = {}; + if (::stat(m_archive_path.string().c_str(), &st) == 0) { + out.mtime_sec = static_cast(st.st_mtimespec.tv_sec); + out.mtime_nsec = static_cast(st.st_mtimespec.tv_nsec); + out.atime_sec = static_cast(st.st_atimespec.tv_sec); + out.atime_nsec = static_cast(st.st_atimespec.tv_nsec); + out.ctime_sec = static_cast(st.st_ctimespec.tv_sec); + out.ctime_nsec = static_cast(st.st_ctimespec.tv_nsec); + } +#else + std::error_code ec; + const auto ft = std::filesystem::last_write_time(m_archive_path, ec); + if (!ec) { + const auto sctp = std::chrono::time_point_cast( + ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + const auto secs = std::chrono::time_point_cast(sctp); + const auto nsecs = std::chrono::duration_cast(sctp - secs); + out.mtime_sec = static_cast(secs.time_since_epoch().count()); + out.mtime_nsec = static_cast(nsecs.count()); + out.atime_sec = out.mtime_sec; + out.atime_nsec = out.mtime_nsec; + out.ctime_sec = out.mtime_sec; + out.ctime_nsec = out.mtime_nsec; + } +#endif +} + +bool ZArchiveFile::Map(u8* addr, u64 size, u64 offset, u32 raw_prot, const FileMapContext& ctx) { + if (!IsOpen()) { + return false; + } + // Reserve writable anonymous pages, populate them from the archive at + // the requested offset, then drop to the requested protection. + ctx.map_anonymous(addr, size); + + // Read straight into the mapped region. + struct MapJob { + ZArchiveReader* reader; + uint32_t node; + u8* dst; + u64 remaining; + u64 pos; + } job{m_reader->reader, m_node, addr, size, offset}; + + { + std::scoped_lock lk{m_reader->mutex}; + m_reader->Dispatch( + [](void* p) { + auto* j = static_cast(p); + while (j->remaining > 0) { + const u64 got = j->reader->ReadFromFile(j->node, j->pos, j->remaining, j->dst); + if (got == 0) { + std::memset(j->dst, 0, j->remaining); + break; + } + j->dst += got; + j->pos += got; + j->remaining -= got; + } + }, + &job); + } + + ctx.protect(addr, size, raw_prot); + return true; +} + +ZArchiveDirectory::ZArchiveDirectory(std::shared_ptr reader, uint32_t node) + : m_reader(std::move(reader)), m_node(node) { + if (m_reader && m_reader->reader) { + std::scoped_lock lk{m_reader->mutex}; + m_count = m_reader->reader->GetDirEntryCount(m_node); + } +} + +bool ZArchiveDirectory::Next(DirEntry& out) { + if (!m_reader || !m_reader->reader || m_index >= m_count) { + return false; + } + ::ZArchiveReader::DirEntry entry{}; + { + std::scoped_lock lk{m_reader->mutex}; + if (!m_reader->reader->GetDirEntry(m_node, m_index, entry)) { + return false; + } + } + ++m_index; + out.name.assign(entry.name.data(), entry.name.size()); + out.is_directory = entry.isDirectory; + out.size = entry.isFile ? entry.size : 0; + return true; +} + +void ZArchiveDirectory::Rewind() { + m_index = 0; +} + +ZArchiveBackend::ZArchiveBackend(const std::filesystem::path& archive_path) + : m_archive_path(archive_path) { + ZArchiveReader* raw = ZArchiveReader::OpenFromFile(archive_path); + if (!raw) { + LOG_ERROR(Kernel_Fs, "Failed to open ZArchive: {}", archive_path.string()); + return; + } + m_reader = std::make_shared(raw); +} + +ZArchiveBackend::~ZArchiveBackend() = default; + +uint32_t ZArchiveBackend::LookUp(std::string_view rel_path, bool allow_file, bool allow_directory) { + if (!IsOpen()) { + return ZARCHIVE_INVALID_NODE; + } + const auto normalized = NormalizeRel(rel_path); + std::scoped_lock lk{m_reader->mutex}; + return m_reader->reader->LookUp(normalized, allow_file, allow_directory); +} + +bool ZArchiveBackend::Exists(std::string_view rel_path) { + return LookUp(rel_path, /*allow_file=*/true, /*allow_directory=*/true) != ZARCHIVE_INVALID_NODE; +} + +bool ZArchiveBackend::IsDirectory(std::string_view rel_path) { + const auto node = LookUp(rel_path, /*allow_file=*/true, /*allow_directory=*/true); + if (node == ZARCHIVE_INVALID_NODE) { + return false; + } + std::scoped_lock lk{m_reader->mutex}; + return m_reader->reader->IsDirectory(node); +} + +std::unique_ptr ZArchiveBackend::Open(std::string_view rel_path, + Common::FS::FileAccessMode mode) { + // ZArchive is read-only,any write/create/append mode is rejected. + if (mode != Common::FS::FileAccessMode::Read) { + return nullptr; + } + const auto node = LookUp(rel_path, /*allow_file=*/true, /*allow_directory=*/false); + if (node == ZARCHIVE_INVALID_NODE) { + return nullptr; + } + u64 size; + { + std::scoped_lock lk{m_reader->mutex}; + if (!m_reader->reader->IsFile(node)) { + return nullptr; + } + size = m_reader->reader->GetFileSize(node); + } + return std::make_unique(m_reader, node, size, m_archive_path); +} + +std::unique_ptr ZArchiveBackend::OpenDir(std::string_view rel_path) { + const auto node = LookUp(rel_path, /*allow_file=*/false, /*allow_directory=*/true); + if (node == ZARCHIVE_INVALID_NODE) { + return nullptr; + } + { + std::scoped_lock lk{m_reader->mutex}; + if (!m_reader->reader->IsDirectory(node)) { + return nullptr; + } + } + return std::make_unique(m_reader, node); +} + +} // namespace Core::FileSys diff --git a/src/core/file_sys/backends/zarchive_fs.h b/src/core/file_sys/backends/zarchive_fs.h new file mode 100644 index 000000000..9a882d7d7 --- /dev/null +++ b/src/core/file_sys/backends/zarchive_fs.h @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "core/file_sys/ifile.h" + +class ZArchiveReader; + +namespace Core::FileSys { + +struct SharedReader { + explicit SharedReader(ZArchiveReader* r); + ~SharedReader(); + + SharedReader(const SharedReader&) = delete; + SharedReader& operator=(const SharedReader&) = delete; + + using JobFn = void (*)(void*); + void Dispatch(JobFn fn, void* ctx); + + ZArchiveReader* reader{nullptr}; + std::mutex mutex; + +private: + void WorkerLoop(); + + std::mutex m_job_mutex; + std::condition_variable m_job_cv; + JobFn m_job_fn{nullptr}; + void* m_job_ctx{nullptr}; + bool m_has_job{false}; + bool m_stop{false}; + std::thread::id m_worker_id; + std::thread m_worker; +}; + +// IFile implementation backed by a node inside a ZArchive (.zar) file. +class ZArchiveFile final : public IFile { +public: + ZArchiveFile(std::shared_ptr reader, uint32_t node, u64 size, + std::filesystem::path archive_path); + ~ZArchiveFile() override = default; + + s64 Read(void* dst, u64 size) override; + s64 Write(const void* src, u64 size) override; + bool Seek(s64 offset, Common::FS::SeekOrigin origin) override; + u64 Tell() const override; + u64 Size() const override; + bool Flush() override; + bool IsOpen() const override; + + std::optional GetMetadataHostPath() const override { + return m_archive_path; + } + + void Stat(FileStat& out) override; + + MmapPolicy GetMmapPolicy() const override { + return MmapPolicy::Copy; + } + + bool Map(u8* addr, u64 size, u64 offset, u32 raw_prot, const FileMapContext& ctx) override; + + bool IsReadOnly() const override { + return true; + } + +private: + std::shared_ptr m_reader; + uint32_t m_node; + u64 m_size; + std::filesystem::path m_archive_path; + mutable std::mutex m_position_mutex; + u64 m_position{0}; +}; + +// Streaming directory iterator over a ZArchive node. +class ZArchiveDirectory final : public IDirectory { +public: + ZArchiveDirectory(std::shared_ptr reader, uint32_t node); + ~ZArchiveDirectory() override = default; + + bool Next(DirEntry& out) override; + void Rewind() override; + +private: + std::shared_ptr m_reader; + uint32_t m_node; + uint32_t m_index{0}; + uint32_t m_count{0}; +}; + +// Backend that maps a mounted namespace onto the contents of a .zar +class ZArchiveBackend final : public IBackend { +public: + explicit ZArchiveBackend(const std::filesystem::path& archive_path); + ~ZArchiveBackend() override; + + bool IsOpen() const { + return m_reader && m_reader->reader != nullptr; + } + + bool Exists(std::string_view rel_path) override; + bool IsDirectory(std::string_view rel_path) override; + + std::unique_ptr Open(std::string_view rel_path, + Common::FS::FileAccessMode mode) override; + std::unique_ptr OpenDir(std::string_view rel_path) override; + + bool IsReadOnly() const override { + return true; + } + + std::optional RootHostPath() const override { + return std::nullopt; + } + +private: + uint32_t LookUp(std::string_view rel_path, bool allow_file, bool allow_directory); + + std::filesystem::path m_archive_path; + std::shared_ptr m_reader; +}; + +} // namespace Core::FileSys diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index e9157d48a..72c228f19 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -1,8 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include +#include "common/assert.h" #include "common/string_util.h" +#include "core/file_sys/backends/host_fs.h" +#include "core/file_sys/backends/zarchive_fs.h" #include "core/file_sys/devices/logger.h" #include "core/file_sys/devices/nop_device.h" #include "core/file_sys/fs.h" @@ -20,11 +25,101 @@ std::string RemoveTrailingSlashes(const std::string& path) { return path_sanitized; } +std::filesystem::path OverlayPath(const std::filesystem::path& base, std::string_view suffix) { + std::filesystem::path result = base; + if (result.extension() == ".zar") { + result.replace_extension(); + } + result += std::string{suffix}; + return result; +} + +std::optional BaseGameFromOverlay(const std::filesystem::path& path) { + std::filesystem::path stem_path = path; + if (stem_path.extension() == ".zar") { + stem_path.replace_extension(); + } + const std::string name = stem_path.filename().string(); + static constexpr std::array suffixes{"-UPDATE", "-patch", "-mods"}; + for (const auto suffix : suffixes) { + if (name.size() > suffix.size() && name.ends_with(suffix)) { + return stem_path.parent_path() / name.substr(0, name.size() - suffix.size()); + } + } + return std::nullopt; +} + +std::optional ResolveGameRoot(const std::filesystem::path& root) { + if (std::filesystem::is_directory(root)) { + return root; + } + if (root.extension() == ".zar" && std::filesystem::is_regular_file(root)) { + return root; + } + std::filesystem::path with_ext = root; + with_ext += ".zar"; + if (std::filesystem::is_regular_file(with_ext)) { + return with_ext; + } + return std::nullopt; +} + void MntPoints::Mount(const std::filesystem::path& host_folder, const std::string& guest_folder, bool read_only) { std::scoped_lock lock{m_mutex}; const auto guest_folder_sanitized = RemoveTrailingSlashes(guest_folder); - m_mnt_pairs.emplace_back(host_folder, guest_folder_sanitized, read_only); + // Build the backend stack for this mount. + std::vector> stack; + const bool eligible_for_overlays = + guest_folder_sanitized == "/app0" || guest_folder_sanitized == "/hostapp"; + const auto make_backend = [](const std::filesystem::path& p, + bool ro) -> std::shared_ptr { + if (std::filesystem::is_directory(p)) { + return std::make_shared(p, ro); + } + const auto try_zar = [ro](const std::filesystem::path& zar) -> std::shared_ptr { + if (std::filesystem::is_regular_file(zar) && zar.extension() == ".zar") { + auto backend = std::make_shared(zar); + if (backend->IsOpen()) { + return backend; + } + } + return nullptr; + }; + if (auto b = try_zar(p)) { + return b; + } + std::filesystem::path with_ext = p; + with_ext += ".zar"; + return try_zar(with_ext); + }; + + const auto probe_overlay = + [&make_backend](const std::filesystem::path& base, + std::string_view suffix) -> std::shared_ptr { + return make_backend(OverlayPath(base, suffix), /*ro=*/true); + }; + // check for mods , updates,patch + if (eligible_for_overlays) { + if (auto mods = probe_overlay(host_folder, "-mods")) { + stack.push_back(std::move(mods)); + } + if (!ignore_game_patches) { + auto patch = probe_overlay(host_folder, "-UPDATE"); + if (!patch) { + patch = probe_overlay(host_folder, "-patch"); + } + if (patch) { + stack.push_back(std::move(patch)); + } + } + } + + std::shared_ptr base = make_backend(host_folder, read_only); + ASSERT_MSG(base, "Mount: base path does not resolve to a backend: {}", host_folder.string()); + stack.push_back(std::move(base)); + + m_mnt_pairs.emplace_back(host_folder, guest_folder_sanitized, read_only, std::move(stack)); } void MntPoints::Unmount(const std::filesystem::path& host_folder, const std::string& guest_folder) { @@ -54,7 +149,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea if (path.length() > 255) return ""; - const std::optional mount = GetMount(corrected_path); + const auto* mount = GetMount(corrected_path); if (!mount) { return ""; } @@ -63,20 +158,20 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea *is_read_only = mount->read_only; } + const bool host_backed = + mount->backends.empty() || mount->backends.back()->RootHostPath().has_value(); + const auto corrected_path_sanitized = RemoveTrailingSlashes(corrected_path); std::filesystem::path host_path = mount->host_path; // Update folder is either mount + "-UPDATE" or mount + "-patch" - std::filesystem::path patch_path = mount->host_path; - patch_path += "-UPDATE"; + std::filesystem::path patch_path = OverlayPath(mount->host_path, "-UPDATE"); if (!std::filesystem::exists(patch_path)) { - patch_path = mount->host_path; - patch_path += "-patch"; + patch_path = OverlayPath(mount->host_path, "-patch"); } // Mods folder can only be at mount + "-mods" - std::filesystem::path mods_path = mount->host_path; - mods_path += "-mods"; + std::filesystem::path mods_path = OverlayPath(mount->host_path, "-mods"); // If we're just retrieving the mount, return the correct mount path. if (corrected_path_sanitized == mount->mount) { @@ -112,7 +207,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea return patch_path; } - if (!NeedsCaseInsensitiveSearch) { + if (!NeedsCaseInsensitiveSearch || !host_backed) { return host_path; } @@ -191,63 +286,213 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea return host_path; } -// TODO: Does not handle mount points inside mount points. +// Normalize a guest path +std::optional SanitizeGuestPath(std::string_view path) { + if (path.length() > 255) { + return std::nullopt; + } + std::string corrected(path); + size_t pos = corrected.find("//"); + while (pos != std::string::npos) { + corrected.replace(pos, 2, "/"); + pos = corrected.find("//", pos + 1); + } + return corrected; +} + +// Strip the mount prefix from a corrected guest path +std::string_view RelativeToMount(const std::string& corrected, const MntPoints::MntPair& mount) { + if (corrected.size() <= mount.mount.size() + 1) { + return {}; + } + return std::string_view{corrected}.substr(mount.mount.size() + 1); +} + void MntPoints::IterateDirectory(std::string_view guest_directory, const IterateDirectoryCallback& callback) { - const auto base_path = GetHostPath(guest_directory, nullptr, HostPathType::Base); + const auto corrected_opt = SanitizeGuestPath(guest_directory); + if (!corrected_opt) { + return; + } + const auto& corrected = *corrected_opt; + const auto mount = GetMount(corrected); + if (!mount || mount->backends.empty()) { + return; + } + const auto rel = std::string{RelativeToMount(corrected, *mount)}; - // Forces path types so as not to resolve to base path - const auto patch_path = GetHostPath(guest_directory, nullptr, HostPathType::Patch); - const auto mod_path = GetHostPath(guest_directory, nullptr, HostPathType::Mod); + const auto& base_backend = mount->backends.back(); + std::filesystem::path base_host; + if (auto root = base_backend->RootHostPath(); root.has_value()) { + base_host = rel.empty() ? *root : (*root / rel); + } else { + base_host = std::filesystem::path(corrected); + } - // Prepend entries for . and .., as both are treated as files on PS4. - callback(base_path / ".", false); - callback(base_path / "..", false); + // Prepend "." and ".." (PS4 exposes these as file entries via getdents). + callback(base_host / ".", false); + callback(base_host / "..", false); - // Pass 1: Any files that existed in the base directory, using mod/patch directory if needed. - if (std::filesystem::exists(base_path)) { - for (const auto& entry : std::filesystem::directory_iterator(base_path)) { - const auto mod_entry_path = mod_path / entry.path().filename(); - const auto patch_entry_path = patch_path / entry.path().filename(); - if (std::filesystem::exists(mod_entry_path)) { - callback(mod_entry_path, !std::filesystem::is_directory(mod_entry_path)); - continue; - } else if (std::filesystem::exists(patch_entry_path)) { - callback(patch_entry_path, !std::filesystem::is_directory(patch_entry_path)); + const auto resolve = + [&](const std::string& leaf) -> std::optional> { + const std::string entry_rel = rel.empty() ? leaf : rel + "/" + leaf; + for (const auto& b : mount->backends) { + if (!b->Exists(entry_rel)) { continue; } - callback(entry.path(), !entry.is_directory()); + const bool is_dir = b->IsDirectory(entry_rel); + std::filesystem::path host; + if (auto root = b->RootHostPath(); root.has_value()) { + host = rel.empty() ? (*root / leaf) : (*root / rel / leaf); + } else { + host = base_host / leaf; + } + return std::make_pair(host, is_dir); } - } + return std::nullopt; + }; - // Pass 2: Any files that exist only in the patch directory. - if (std::filesystem::exists(patch_path)) { - for (const auto& entry : std::filesystem::directory_iterator(patch_path)) { - const auto base_entry_path = base_path / entry.path().filename(); - if (!std::filesystem::exists(base_entry_path)) { - const auto mod_entry_path = mod_path / entry.path().filename(); - if (std::filesystem::exists(mod_entry_path)) { - callback(mod_entry_path, !std::filesystem::is_directory(mod_entry_path)); - continue; - } - callback(entry.path(), !entry.is_directory()); + std::unordered_set emitted; + const auto emit_key = [](const std::string& name) { + return NeedsCaseInsensitiveSearch ? Common::ToLower(name) : name; + }; + + if (auto dir = base_backend->OpenDir(rel)) { + DirEntry entry; + while (dir->Next(entry)) { + if (auto hit = resolve(entry.name)) { + emitted.insert(emit_key(entry.name)); + callback(hit->first, /*is_file=*/!hit->second); } } } - // Pass 3: Any files that exist only in the mod directory (confirmed this can be valid) - if (std::filesystem::exists(mod_path)) { - for (const auto& entry : std::filesystem::directory_iterator(mod_path)) { - const auto base_entry_path = base_path / entry.path().filename(); - const auto patch_entry_path = patch_path / entry.path().filename(); - if (!std::filesystem::exists(base_entry_path) && - !std::filesystem::exists(patch_entry_path)) { - callback(entry.path(), !entry.is_directory()); + for (size_t i = mount->backends.size() - 1; i-- > 0;) { + const auto& overlay = mount->backends[i]; + auto dir = overlay->OpenDir(rel); + if (!dir) { + continue; + } + DirEntry entry; + while (dir->Next(entry)) { + if (emitted.contains(emit_key(entry.name))) { + continue; + } + if (auto hit = resolve(entry.name)) { + emitted.insert(emit_key(entry.name)); + callback(hit->first, /*is_file=*/!hit->second); } } } } +bool MntPoints::Exists(std::string_view guest_path) { + const auto corrected = SanitizeGuestPath(guest_path); + if (!corrected) { + return false; + } + const auto mount = GetMount(*corrected); + if (!mount || mount->backends.empty()) { + return false; + } + const auto rel = RelativeToMount(*corrected, *mount); + for (const auto& backend : mount->backends) { + if (backend->Exists(rel)) { + return true; + } + } + return false; +} + +bool MntPoints::IsDirectory(std::string_view guest_path) { + const auto corrected = SanitizeGuestPath(guest_path); + if (!corrected) { + return false; + } + const auto mount = GetMount(*corrected); + if (!mount || mount->backends.empty()) { + return false; + } + const auto rel = RelativeToMount(*corrected, *mount); + for (const auto& backend : mount->backends) { + if (backend->Exists(rel)) { + return backend->IsDirectory(rel); + } + } + return false; +} + +std::unique_ptr MntPoints::Open(std::string_view guest_path, bool writable) { + return Open(guest_path, writable ? Common::FS::FileAccessMode::ReadWrite + : Common::FS::FileAccessMode::Read); +} + +std::unique_ptr MntPoints::Open(std::string_view guest_path, + Common::FS::FileAccessMode mode) { + const auto corrected = SanitizeGuestPath(guest_path); + if (!corrected) { + return nullptr; + } + const auto mount = GetMount(*corrected); + if (!mount || mount->backends.empty()) { + return nullptr; + } + const bool writable = mode != Common::FS::FileAccessMode::Read; + if (writable && mount->read_only) { + return nullptr; + } + const auto rel = RelativeToMount(*corrected, *mount); + const std::string rel_copy{rel}; + + if (mode == Common::FS::FileAccessMode::Create) { + return mount->backends.back()->Open(rel_copy, mode); + } + + for (const auto& backend : mount->backends) { + if (backend->Exists(rel) && !backend->IsDirectory(rel)) { + return backend->Open(rel_copy, mode); + } + } + return nullptr; +} + +std::unique_ptr MntPoints::OpenDir(std::string_view guest_path) { + const auto corrected = SanitizeGuestPath(guest_path); + if (!corrected) { + return nullptr; + } + const auto mount = GetMount(*corrected); + if (!mount || mount->backends.empty()) { + return nullptr; + } + const auto rel = RelativeToMount(*corrected, *mount); + const std::string rel_copy{rel}; + + for (const auto& backend : mount->backends) { + if (backend->Exists(rel) && backend->IsDirectory(rel)) { + return backend->OpenDir(rel_copy); + } + } + return nullptr; +} + +std::optional> MntPoints::ReadFile(std::string_view guest_path) { + auto handle = Open(guest_path, /*writable=*/false); + if (!handle) { + return std::nullopt; + } + const u64 size = handle->Size(); + std::vector buf(size); + if (size == 0) { + return buf; + } + const s64 got = handle->Read(buf.data(), size); + if (got < 0 || static_cast(got) != size) { + return std::nullopt; + } + return buf; +} + int HandleTable::CreateHandle() { std::scoped_lock lock{m_mutex}; diff --git a/src/core/file_sys/fs.h b/src/core/file_sys/fs.h index 9a782b19e..d05b00ed1 100644 --- a/src/core/file_sys/fs.h +++ b/src/core/file_sys/fs.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -13,6 +14,7 @@ #include "common/logging/formatter.h" #include "core/file_sys/devices/base_device.h" #include "core/file_sys/directories/base_directory.h" +#include "core/file_sys/ifile.h" namespace Libraries::Net { struct Socket; @@ -22,18 +24,31 @@ struct Resolver; namespace Core::FileSys { -class MntPoints { +/// Builds the path of an overlay that sits next to a game +[[nodiscard]] std::filesystem::path OverlayPath(const std::filesystem::path& base, + std::string_view suffix); + +/// Inverse of OverlayPath. +[[nodiscard]] std::optional BaseGameFromOverlay( + const std::filesystem::path& path); + +[[nodiscard]] std::optional ResolveGameRoot( + const std::filesystem::path& root); + #ifdef _WIN64 - static constexpr bool NeedsCaseInsensitiveSearch = false; +inline constexpr bool NeedsCaseInsensitiveSearch = false; #else - static constexpr bool NeedsCaseInsensitiveSearch = true; +inline constexpr bool NeedsCaseInsensitiveSearch = true; #endif + +class MntPoints { public: static bool ignore_game_patches; struct MntPair { std::filesystem::path host_path; std::string mount; // e.g /app0 bool read_only; + std::vector> backends; }; enum class HostPathType { @@ -59,6 +74,28 @@ public: void IterateDirectory(std::string_view guest_directory, const IterateDirectoryCallback& callback); + /// Returns true if the guest path exists in any backend of the + /// mount stack. Mirrors fs::exists() on the resolved host path. + bool Exists(std::string_view guest_path); + + /// Returns true if the guest path resolves to a directory in any + /// backend of the mount stack. + bool IsDirectory(std::string_view guest_path); + + /// Opens the guest path through the mount's backend stack. Returns + /// nullptr when the path does not exist or the caller requested + /// writable access on a read-only mount. + std::unique_ptr Open(std::string_view guest_path, bool writable = false); + // Open with an explicit host access mode. + std::unique_ptr Open(std::string_view guest_path, Common::FS::FileAccessMode mode); + + /// Opens a directory through the mount's backend stack. + std::unique_ptr OpenDir(std::string_view guest_path); + + /// open + read the whole file as bytes. Returns nullopt + /// when the file does not exist or is unreadable. + std::optional> ReadFile(std::string_view guest_path); + const MntPair* GetMountFromHostPath(const std::string& host_path) { std::scoped_lock lock{m_mutex}; const auto it = std::ranges::find_if(m_mnt_pairs, [&](const MntPair& mount) { @@ -67,7 +104,7 @@ public: return it == m_mnt_pairs.end() ? nullptr : &*it; } - const std::optional GetMount(const std::string& guest_path) { + const MntPair* GetMount(const std::string& guest_path) { std::scoped_lock lock{m_mutex}; const auto it = std::ranges::find_if(m_mnt_pairs, [&](const auto& mount) { // When doing starts-with check, add a trailing slash to make sure we don't match @@ -75,9 +112,9 @@ public: return guest_path == mount.mount || guest_path.starts_with(mount.mount + "/"); }); if (it == m_mnt_pairs.end()) { - return std::nullopt; + return nullptr; } - return *it; + return &*it; } private: @@ -102,13 +139,49 @@ struct File { std::atomic type{FileType::Regular}; std::filesystem::path m_host_name; std::string m_guest_name; - Common::FS::IOFile f; + std::unique_ptr handle; std::mutex m_mutex; std::shared_ptr directory; // only valid for type == Directory std::shared_ptr device; // only valid for type == Device std::shared_ptr socket; // only valid for type == Socket std::shared_ptr epoll; // only valid for type == Epoll std::shared_ptr resolver; // only valid for type == Resolver + + bool IsBackendOpen() const { + return handle && handle->IsOpen(); + } + + s64 Read(void* dst, u64 size) { + return handle ? handle->Read(dst, size) : -1; + } + + s64 Write(const void* src, u64 size) { + return handle ? handle->Write(src, size) : -1; + } + + bool Seek(s64 offset, Common::FS::SeekOrigin origin = Common::FS::SeekOrigin::SetOrigin) { + return handle ? handle->Seek(offset, origin) : false; + } + + s64 Tell() const { + return handle ? static_cast(handle->Tell()) : -1; + } + + u64 GetSize() const { + return handle ? handle->Size() : 0; + } + + bool Flush() { + return handle ? handle->Flush() : false; + } + + bool IsWriteOnly() const { + return handle ? handle->IsWriteOnly() : false; + } + + Common::FS::IOFile* GetHostFile() const { + return handle ? handle->GetHostFile() : nullptr; + } }; class HandleTable { diff --git a/src/core/file_sys/ifile.h b/src/core/file_sys/ifile.h new file mode 100644 index 000000000..fd10abee9 --- /dev/null +++ b/src/core/file_sys/ifile.h @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "common/concepts.h" +#include "common/io_file.h" +#include "common/types.h" + +namespace Core::FileSys { + +// Policy declared by an IFile backend for host mmap. +enum class MmapPolicy { + Native, // Backed by a real host file,mmap the fd directly. + Copy, // Backend can populate anon pages on demand (decompression, etc.). + Unsupported, // Caller must fall back to buffered reads. +}; + +struct DirEntry { + std::string name; + bool is_directory{false}; + u64 size{0}; +}; + +struct FileStat { + u64 size{0}; + s64 mtime_sec{0}; + s64 mtime_nsec{0}; + s64 atime_sec{0}; + s64 atime_nsec{0}; + s64 ctime_sec{0}; + s64 ctime_nsec{0}; + bool is_directory{false}; +}; + +struct FileMapContext { + // Map the host file (identified by mapping_handle) natively at + // addr with the given raw guest protection bits. + std::function + map_native; + // Reserve size bytes of anonymous, writable memory at addr so + // the backend can populate it from Read(). + std::function map_anonymous; + // Apply final protection to a populated region. + std::function protect; + // Native mapping handle for the host file (0 for non-host backends). + uintptr_t mapping_handle{0}; +}; + +class IFile { +public: + virtual ~IFile() = default; + + virtual s64 Read(void* dst, u64 size) = 0; + virtual s64 Write(const void* src, u64 size) = 0; + virtual bool Seek(s64 offset, Common::FS::SeekOrigin origin) = 0; + virtual u64 Tell() const = 0; + virtual u64 Size() const = 0; + virtual bool Flush() = 0; + virtual bool IsOpen() const = 0; + + virtual std::optional GetHostPath() const { + return std::nullopt; + } + + virtual std::optional GetMetadataHostPath() const { + return GetHostPath(); + } + + virtual void Stat(FileStat& out) { + out.size = Size(); + out.is_directory = false; + } + + virtual Common::FS::IOFile* GetHostFile() { + return nullptr; + } + + virtual MmapPolicy GetMmapPolicy() const { + return MmapPolicy::Unsupported; + } + + virtual bool Map(u8* addr, u64 size, u64 offset, u32 raw_prot, const FileMapContext& ctx) { + ctx.map_native(addr, size, offset, raw_prot, ctx.mapping_handle); + return true; + } + + virtual bool IsReadOnly() const { + return true; + } + + virtual bool IsWriteOnly() const { + return false; + } +}; + +class FileReader { +public: + FileReader() = default; + explicit FileReader(std::unique_ptr handle) : m_handle(std::move(handle)) {} + + void Reset(std::unique_ptr handle) { + m_handle = std::move(handle); + } + + bool IsOpen() const { + return m_handle && m_handle->IsOpen(); + } + + IFile* Get() const { + return m_handle.get(); + } + + template + bool ReadObject(T& obj) { + static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); + static_assert(!std::is_pointer_v, "T must not be a pointer"); + if (!IsOpen()) { + return false; + } + return m_handle->Read(&obj, sizeof(T)) == static_cast(sizeof(T)); + } + + template + size_t ReadRaw(void* dst, size_t count) { + if (!IsOpen() || count == 0) { + return 0; + } + const auto bytes_requested = count * sizeof(T); + const s64 got = m_handle->Read(dst, bytes_requested); + if (got <= 0) { + return 0; + } + return static_cast(got) / sizeof(T); + } + + template + size_t Read(T& data) { + if constexpr (Common::IsContiguousContainer) { + using ContiguousType = typename T::value_type; + static_assert(std::is_trivially_copyable_v, + "container element type must be trivially copyable"); + return ReadRaw(data.data(), data.size()); + } else { + return ReadObject(data) ? 1 : 0; + } + } + + bool Seek(s64 offset, Common::FS::SeekOrigin origin = Common::FS::SeekOrigin::SetOrigin) { + if (!IsOpen()) { + return false; + } + return m_handle->Seek(offset, origin); + } + + s64 Tell() const { + if (!IsOpen()) { + return -1; + } + return static_cast(m_handle->Tell()); + } + + u64 GetSize() const { + if (!IsOpen()) { + return 0; + } + return m_handle->Size(); + } + +private: + std::unique_ptr m_handle; +}; + +class IDirectory { +public: + virtual ~IDirectory() = default; + virtual bool Next(DirEntry& out) = 0; + virtual void Rewind() = 0; +}; + +class IBackend { +public: + virtual ~IBackend() = default; + + virtual bool Exists(std::string_view rel_path) = 0; + virtual bool IsDirectory(std::string_view rel_path) = 0; + virtual std::unique_ptr Open(std::string_view rel_path, + Common::FS::FileAccessMode mode) = 0; + virtual std::unique_ptr OpenDir(std::string_view rel_path) = 0; + + virtual bool IsReadOnly() const = 0; + virtual std::optional RootHostPath() const { + return std::nullopt; + } +}; + +} // namespace Core::FileSys diff --git a/src/core/libraries/avplayer/avplayer_handle_streamer.cpp b/src/core/libraries/avplayer/avplayer_handle_streamer.cpp new file mode 100644 index 000000000..1f61857b4 --- /dev/null +++ b/src/core/libraries/avplayer/avplayer_handle_streamer.cpp @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "core/libraries/avplayer/avplayer_handle_streamer.h" + +extern "C" { +#include +#include +#include +} + +constexpr u32 AVPLAYER_AVIO_BUFFER_SIZE = 4096; + +namespace Libraries::AvPlayer { + +AvPlayerHandleStreamer::AvPlayerHandleStreamer(std::unique_ptr handle) + : m_handle(std::move(handle)) {} + +AvPlayerHandleStreamer::~AvPlayerHandleStreamer() { + if (m_avio_context != nullptr) { + avio_context_free(&m_avio_context); + } +} + +bool AvPlayerHandleStreamer::Init(std::string_view /*path*/) { + if (!m_handle || !m_handle->IsOpen()) { + return false; + } + m_file_size = m_handle->Size(); + + const auto avio_buffer = reinterpret_cast(av_malloc(AVPLAYER_AVIO_BUFFER_SIZE)); + m_avio_context = avio_alloc_context(avio_buffer, AVPLAYER_AVIO_BUFFER_SIZE, + /*write_flag=*/0, this, &AvPlayerHandleStreamer::ReadPacket, + /*write_packet=*/nullptr, &AvPlayerHandleStreamer::Seek); + return m_avio_context != nullptr; +} + +void AvPlayerHandleStreamer::Reset() { + m_position = 0; + if (m_handle) { + m_handle->Seek(0, Common::FS::SeekOrigin::SetOrigin); + } +} + +s32 AvPlayerHandleStreamer::ReadPacket(void* opaque, u8* buffer, s32 size) { + auto* const self = reinterpret_cast(opaque); + if (self->m_position >= self->m_file_size) { + return AVERROR_EOF; + } + if (self->m_position + static_cast(size) > self->m_file_size) { + size = static_cast(self->m_file_size - self->m_position); + } + self->m_handle->Seek(static_cast(self->m_position), Common::FS::SeekOrigin::SetOrigin); + const s64 got = self->m_handle->Read(buffer, static_cast(size)); + if (got <= 0) { + return AVERROR_EOF; + } + self->m_position += static_cast(got); + return static_cast(got); +} + +s64 AvPlayerHandleStreamer::Seek(void* opaque, s64 offset, int whence) { + auto* const self = reinterpret_cast(opaque); + if ((whence & AVSEEK_SIZE) != 0) { + return static_cast(self->m_file_size); + } + const int base = whence & ~AVSEEK_FORCE; + u64 new_pos = self->m_position; + if (base == SEEK_SET) { + new_pos = static_cast(std::max(0, offset)); + } else if (base == SEEK_CUR) { + new_pos = static_cast(std::max(0, static_cast(self->m_position) + offset)); + } else if (base == SEEK_END) { + new_pos = static_cast(std::max(0, static_cast(self->m_file_size) + offset)); + } else { + return -1; + } + new_pos = std::min(new_pos, self->m_file_size); + self->m_position = new_pos; + return static_cast(new_pos); +} + +} // namespace Libraries::AvPlayer diff --git a/src/core/libraries/avplayer/avplayer_handle_streamer.h b/src/core/libraries/avplayer/avplayer_handle_streamer.h new file mode 100644 index 000000000..87123831a --- /dev/null +++ b/src/core/libraries/avplayer/avplayer_handle_streamer.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "core/file_sys/ifile.h" +#include "core/libraries/avplayer/avplayer_data_streamer.h" + +struct AVIOContext; + +namespace Libraries::AvPlayer { + +/// AVIO streamer that reads from a Core::FileSys::IFile handle. +class AvPlayerHandleStreamer final : public IDataStreamer { +public: + explicit AvPlayerHandleStreamer(std::unique_ptr handle); + ~AvPlayerHandleStreamer() override; + bool Init(std::string_view path) override; + void Reset() override; + + AVIOContext* GetContext() override { + return m_avio_context; + } + +private: + static s32 ReadPacket(void* opaque, u8* buffer, s32 size); + static s64 Seek(void* opaque, s64 offset, int whence); + + std::unique_ptr m_handle; + u64 m_position{0}; + u64 m_file_size{0}; + AVIOContext* m_avio_context{nullptr}; +}; + +} // namespace Libraries::AvPlayer diff --git a/src/core/libraries/avplayer/avplayer_source.cpp b/src/core/libraries/avplayer/avplayer_source.cpp index 96514a88d..89aeb74cc 100644 --- a/src/core/libraries/avplayer/avplayer_source.cpp +++ b/src/core/libraries/avplayer/avplayer_source.cpp @@ -7,6 +7,7 @@ #include "core/file_sys/fs.h" #include "core/libraries/avplayer/avplayer_error.h" #include "core/libraries/avplayer/avplayer_file_streamer.h" +#include "core/libraries/avplayer/avplayer_handle_streamer.h" #include "core/libraries/avplayer/avplayer_source.h" #include "core/libraries/videodec/video_utils.h" #include "core/memory.h" @@ -58,21 +59,58 @@ bool AvPlayerSource::Init(const AvPlayerInitData& init_data, std::string_view pa std::min(std::max(2, init_data.num_output_video_framebuffers), 16); AVFormatContext* context = avformat_alloc_context(); + if (context == nullptr) { + LOG_ERROR(Lib_AvPlayer, "Could not allocate AVFormatContext for {}", path); + return false; + } + + const auto attach_custom_io = [&]() { + context->pb = m_up_data_streamer->GetContext(); + context->flags |= AVFMT_FLAG_CUSTOM_IO; + }; + if (init_data.file_replacement.open != nullptr) { + LOG_INFO(Lib_AvPlayer, "Opening {} through the game-provided file replacement", path); m_up_data_streamer = std::make_unique(init_data.file_replacement); if (!m_up_data_streamer->Init(path)) { + avformat_free_context(context); return false; } - context->pb = m_up_data_streamer->GetContext(); + attach_custom_io(); if (AVPLAYER_IS_ERROR(avformat_open_input(&context, nullptr, nullptr, nullptr))) { return false; } } else { const auto mnt = Common::Singleton::Instance(); - const auto filepath = mnt->GetHostPath(path); - if (AVPLAYER_IS_ERROR( - avformat_open_input(&context, filepath.string().c_str(), nullptr, nullptr))) { - return false; + auto handle = mnt->Open(path, /*writable=*/false); + if (handle) { + if (auto host = handle->GetHostPath(); host.has_value()) { + LOG_INFO(Lib_AvPlayer, "Opening {} directly from host path {}", path, + host->string()); + if (AVPLAYER_IS_ERROR( + avformat_open_input(&context, host->string().c_str(), nullptr, nullptr))) { + return false; + } + } else { + LOG_INFO(Lib_AvPlayer, "Opening {} through a non-host backend handle", path); + m_up_data_streamer = std::make_unique(std::move(handle)); + if (!m_up_data_streamer->Init(path)) { + avformat_free_context(context); + return false; + } + attach_custom_io(); + if (AVPLAYER_IS_ERROR(avformat_open_input(&context, nullptr, nullptr, nullptr))) { + return false; + } + } + } else { + const auto filepath = mnt->GetHostPath(path); + LOG_INFO(Lib_AvPlayer, "Opening {} via GetHostPath fallback {}", path, + filepath.string()); + if (AVPLAYER_IS_ERROR( + avformat_open_input(&context, filepath.string().c_str(), nullptr, nullptr))) { + return false; + } } } m_avformat_context = AVFormatContextPtr(context, &ReleaseAVFormatContext); diff --git a/src/core/libraries/avplayer/avplayer_state.cpp b/src/core/libraries/avplayer/avplayer_state.cpp index 71341d1d4..e55455df4 100644 --- a/src/core/libraries/avplayer/avplayer_state.cpp +++ b/src/core/libraries/avplayer/avplayer_state.cpp @@ -458,8 +458,12 @@ void AvPlayerState::ProcessEvent() { break; } case AvEventType::AddSource: { - std::shared_lock lock(m_source_mutex); - if (m_up_source->FindStreams()) { + bool found = false; + { + std::unique_lock lock(m_source_mutex); + found = m_up_source != nullptr && m_up_source->FindStreams(); + } + if (found) { SetState(AvState::Ready); OnPlaybackStateChanged(AvState::Ready); } else { diff --git a/src/core/libraries/font/font.cpp b/src/core/libraries/font/font.cpp index e1c4ac88a..2f968792c 100644 --- a/src/core/libraries/font/font.cpp +++ b/src/core/libraries/font/font.cpp @@ -580,6 +580,7 @@ using Internal::GetNativeFont; using Internal::kPointsPerInch; using Internal::kStyleFrameMagic; using Internal::LoadGuestFileBytes; +using Internal::LoadGuestPathBytes; using Internal::LogCachedStyleOnce; using Internal::LogFontOpenError; using Internal::LogRenderResultSample; @@ -3193,7 +3194,7 @@ s32 PS4_SYSV_ABI sceFontOpenFontFile(OrbisFontLib library, const char* guest_pat ReleaseLibraryLock(lib, prev_lib_lock); std::vector file_bytes; - if (LoadGuestFileBytes(path_to_open, file_bytes) && + if (LoadGuestPathBytes(guest_path, file_bytes) && file_bytes.size() <= std::numeric_limits::max()) { auto& st = Internal::GetState(handle); Internal::DestroyFreeTypeFace(st.ext_ft_face); diff --git a/src/core/libraries/font/font_internal.cpp b/src/core/libraries/font/font_internal.cpp index 9489038f9..87ef8265f 100644 --- a/src/core/libraries/font/font_internal.cpp +++ b/src/core/libraries/font/font_internal.cpp @@ -943,6 +943,26 @@ bool LoadGuestFileBytes(const std::filesystem::path& host_path, return true; } +bool LoadGuestPathBytes(const char* guest_path, std::vector& out_bytes) { + if (!guest_path) { + return false; + } + if (guest_path[0] == '/') { + if (!g_mnt) { + g_mnt = Common::Singleton::Instance(); + } + if (g_mnt) { + if (auto bytes = g_mnt->ReadFile(guest_path)) { + out_bytes = std::move(*bytes); + return true; + } + } + return false; + } + // Non-guest path: treat as a host path (builtin/system fonts). + return LoadGuestFileBytes(std::filesystem::path(guest_path), out_bytes); +} + FaceMetrics LoadFaceMetrics(FT_Face face) { FaceMetrics m{}; if (!face) { diff --git a/src/core/libraries/font/font_internal.h b/src/core/libraries/font/font_internal.h index cca526a4a..635b7fc8a 100644 --- a/src/core/libraries/font/font_internal.h +++ b/src/core/libraries/font/font_internal.h @@ -865,6 +865,7 @@ void LogFontOpenParams(const Libraries::Font::OrbisFontOpenParams* params); std::filesystem::path ResolveGuestPath(const char* guest_path); bool LoadGuestFileBytes(const std::filesystem::path& host_path, std::vector& out_bytes); +bool LoadGuestPathBytes(const char* guest_path, std::vector& out_bytes); FaceMetrics LoadFaceMetrics(FT_Face face); void PopulateStateMetrics(FontState& st, const FaceMetrics& m); float ComputeSysfontScaleFactor(FT_Face face, int units_per_em); diff --git a/src/core/libraries/font/fontft_internal.cpp b/src/core/libraries/font/fontft_internal.cpp index e6c94f3a8..62fc64f1a 100644 --- a/src/core/libraries/font/fontft_internal.cpp +++ b/src/core/libraries/font/fontft_internal.cpp @@ -2976,6 +2976,7 @@ s32 PS4_SYSV_ABI LibraryOpenFontMemoryStub(void* library, u32 mode, const void* u32 size = 0; void* owned_data = nullptr; std::shared_ptr> shared_data; + std::shared_ptr> archive_font_bytes; std::string open_path; std::filesystem::path host_path_fs{}; @@ -2994,9 +2995,15 @@ s32 PS4_SYSV_ABI LibraryOpenFontMemoryStub(void* library, u32 mode, const void* open_path = path; if (path[0] == '/') { auto* mnt = Common::Singleton::Instance(); - host_path_fs = mnt ? mnt->GetHostPath(path) : std::filesystem::path{}; - if (!host_path_fs.empty()) { - open_path = host_path_fs.string(); + if (mnt) { + if (auto handle = mnt->Open(path, /*writable=*/false)) { + if (auto host = handle->GetHostPath(); host.has_value()) { + host_path_fs = *host; + open_path = host_path_fs.string(); + } else if (auto bytes = mnt->ReadFile(path)) { + archive_font_bytes = std::make_shared>(std::move(*bytes)); + } + } } } } else { @@ -3017,6 +3024,16 @@ s32 PS4_SYSV_ABI LibraryOpenFontMemoryStub(void* library, u32 mode, const void* ft_err = FT_New_Memory_Face(ctx->ft_lib, reinterpret_cast(data), static_cast(size), static_cast(subFontIndex), &face); + } else if (archive_font_bytes) { + ft_err = FT_New_Memory_Face(ctx->ft_lib, + reinterpret_cast(archive_font_bytes->data()), + static_cast(archive_font_bytes->size()), + static_cast(subFontIndex), &face); + if (ft_err == 0 && face) { + shared_data = archive_font_bytes; + data = archive_font_bytes->data(); + size = static_cast(archive_font_bytes->size()); + } } else { std::vector candidates; candidates.emplace_back(open_path); diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 188f3e1e6..40c28002f 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -131,8 +131,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { bool read_only = false; file->m_guest_name = path; file->m_host_name = mnt->GetHostPath(file->m_guest_name, &read_only); - bool exists = fs::exists(file->m_host_name); - s32 e = 0; + bool exists = mnt->Exists(file->m_guest_name); if (create) { if (excl && exists) { @@ -162,14 +161,14 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { return -1; } - if (fs::is_directory(file->m_host_name) || directory) { + if (mnt->IsDirectory(file->m_guest_name) || directory) { // Directories can be opened even if the directory flag isn't set. // In these cases, error behavior is identical to the directory code path. directory = true; } if (directory) { - if (!fs::is_directory(file->m_host_name)) { + if (!mnt->IsDirectory(file->m_guest_name)) { // If the opened file is not a directory, return ENOTDIR. // This will trigger when create & directory is specified, this is expected. h->DeleteHandle(handle); @@ -207,59 +206,46 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { } else { file->type = Core::FileSys::FileType::Regular; - if (truncate && read_only) { - // Can't open files with truncate flag in a read only directory - h->DeleteHandle(handle); - *__Error() = POSIX_EROFS; - LOG_ERROR(Kernel_Fs, "Truncating {} failed, path is read-only", raw_path); - return -1; - } else if (truncate) { - // Open the file as read-write so we can truncate regardless of flags. - // Since open starts by closing the file, this won't interfere with later open calls. - e = file->f.Open(file->m_host_name, Common::FS::FileAccessMode::ReadWrite); - if (e == 0) { - // If the file was opened successfully, reduce size to 0 - file->f.SetSize(0); - } - } - - if (read) { - // Open exclusively for reading - e = file->f.Open(file->m_host_name, Common::FS::FileAccessMode::Read); - } else if (read_only) { - // Can't open files with write/read-write access in a read only directory + // Reject writes to read-only mounts up front, so we can report + // the right errno instead of a generic open failure. + if ((write || rdwr || truncate) && read_only) { h->DeleteHandle(handle); *__Error() = POSIX_EROFS; LOG_ERROR(Kernel_Fs, "Opening {} for writing failed, path is read-only", raw_path); return -1; + } + + // Map the guest open flags onto a single host access mode. + Common::FS::FileAccessMode access_mode; + if (truncate) { + access_mode = Common::FS::FileAccessMode::ReadWrite; + } else if (read) { + access_mode = Common::FS::FileAccessMode::Read; } else if (write) { - if (append) { - // Open exclusively for appending - e = file->f.Open(file->m_host_name, Common::FS::FileAccessMode::Append); - } else { - // Open exclusively for writing - e = file->f.Open(file->m_host_name, Common::FS::FileAccessMode::Write); - } + access_mode = + append ? Common::FS::FileAccessMode::Append : Common::FS::FileAccessMode::Write; } else if (rdwr) { - // Read and write - if (append) { - // Open for reading and appending - e = file->f.Open(file->m_host_name, Common::FS::FileAccessMode::ReadAppend); - } else { - // Open for reading and writing - e = file->f.Open(file->m_host_name, Common::FS::FileAccessMode::ReadWrite); + access_mode = append ? Common::FS::FileAccessMode::ReadAppend + : Common::FS::FileAccessMode::ReadWrite; + } else { + access_mode = Common::FS::FileAccessMode::Read; + } + + file->handle = mnt->Open(file->m_guest_name, access_mode); + if (!file->handle || !file->handle->IsOpen()) { + h->DeleteHandle(handle); + *__Error() = (write || rdwr || truncate) ? POSIX_EROFS : POSIX_EIO; + LOG_ERROR(Kernel_Fs, "Opening {} failed, backend did not serve the file", raw_path); + return -1; + } + + if (truncate) { + if (auto* host = file->handle->GetHostFile()) { + host->SetSize(0); } } } - if (e != 0) { - // Open failed in platform-specific code, errno needs to be converted. - h->DeleteHandle(handle); - SetPosixErrno(e); - LOG_ERROR(Kernel_Fs, "Opening {} failed, error = {}", raw_path, *__Error()); - return -1; - } - file->is_opened = true; return handle; } @@ -288,7 +274,7 @@ s32 PS4_SYSV_ABI close(s32 fd) { return -1; } if (file->type == Core::FileSys::FileType::Regular) { - file->f.Close(); + file->handle.reset(); } else if (file->type == Core::FileSys::FileType::Socket) { file->socket->Close(); } @@ -336,7 +322,7 @@ s64 PS4_SYSV_ABI write(s32 fd, const void* buf, u64 nbytes) { return -1; } - return file->f.WriteRaw(buf, nbytes); + return file->Write(buf, nbytes); } s64 PS4_SYSV_ABI posix_write(s32 fd, const void* buf, u64 nbytes) { @@ -354,15 +340,18 @@ s64 PS4_SYSV_ABI sceKernelWrite(s32 fd, const void* buf, u64 nbytes) { static thread_local std::vector file_buf{}; -s64 ReadFile(Common::FS::IOFile& file, void* buf, u64 nbytes) { +s64 ReadFile(Core::FileSys::File* file, void* buf, u64 nbytes) { const auto* memory = Core::Memory::Instance(); // Invalidate up to the actual number of bytes that could be read. - const auto remaining = file.GetSize() - file.Tell(); + const auto remaining = file->GetSize() - file->Tell(); memory->InvalidateMemory(reinterpret_cast(buf), std::min(nbytes, remaining)); if (file_buf.capacity() < nbytes) { file_buf.reserve(nbytes); } - u64 bytes = file.ReadRaw(file_buf.data(), nbytes); + s64 bytes = file->Read(file_buf.data(), nbytes); + if (bytes < 0) { + return bytes; + } std::memcpy(buf, file_buf.data(), bytes); return bytes; } @@ -392,14 +381,14 @@ s64 PS4_SYSV_ABI readv(s32 fd, const OrbisKernelIovec* iov, s32 iovcnt) { return result; } - if (file->f.IsWriteOnly()) { + if (file->IsWriteOnly()) { *__Error() = POSIX_EBADF; return -1; } s64 total_read = 0; for (s32 i = 0; i < iovcnt; i++) { - total_read += ReadFile(file->f, iov[i].iov_base, iov[i].iov_len); + total_read += ReadFile(file, iov[i].iov_base, iov[i].iov_len); } return total_read; } @@ -441,7 +430,7 @@ s64 PS4_SYSV_ABI writev(s32 fd, const OrbisKernelIovec* iov, s32 iovcnt) { s64 total_written = 0; for (s32 i = 0; i < iovcnt; i++) { - total_written += file->f.WriteRaw(iov[i].iov_base, iov[i].iov_len); + total_written += file->Write(iov[i].iov_base, iov[i].iov_len); } return total_written; } @@ -501,7 +490,7 @@ s64 PS4_SYSV_ABI posix_lseek(s32 fd, s64 offset, s32 whence) { return -1; } - if (!file->f.Seek(offset, origin)) { + if (!file->Seek(offset, origin)) { if (errno != 0) { // Seek failed in platform-specific code, errno needs to be converted. SetPosixErrno(errno); @@ -511,7 +500,7 @@ s64 PS4_SYSV_ABI posix_lseek(s32 fd, s64 offset, s32 whence) { return -1; } - s64 result = file->f.Tell(); + s64 result = file->Tell(); if (result < 0) { // Tell failed in platform-specific code, errno needs to be converted. SetPosixErrno(errno); @@ -557,12 +546,12 @@ s64 PS4_SYSV_ABI read(s32 fd, void* buf, u64 nbytes) { return file->socket->ReceivePacket(buf, nbytes, 0, nullptr, 0); } - if (file->f.IsWriteOnly()) { + if (file->IsWriteOnly()) { *__Error() = POSIX_EBADF; return -1; } - return ReadFile(file->f, buf, nbytes); + return ReadFile(file, buf, nbytes); } s64 PS4_SYSV_ABI posix_read(s32 fd, void* buf, u64 nbytes) { @@ -593,7 +582,7 @@ s32 PS4_SYSV_ABI posix_mkdir(const char* path, u16 mode) { bool ro = false; const auto dir_name = mnt->GetHostPath(path, &ro); - if (fs::exists(dir_name)) { + if (mnt->Exists(path)) { *__Error() = POSIX_EEXIST; return -1; } @@ -679,8 +668,9 @@ s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { auto* mnt = Common::Singleton::Instance(); const auto path_name = mnt->GetHostPath(path); std::memset(sb, 0, sizeof(OrbisKernelStat)); - const bool is_dir = fs::is_directory(path_name); - const bool is_file = fs::is_regular_file(path_name); + + const bool is_dir = mnt->IsDirectory(path); + const bool is_file = !is_dir && mnt->Exists(path); if (!is_dir && !is_file) { *__Error() = POSIX_ENOENT; return -1; @@ -690,10 +680,12 @@ s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { const auto now_sys = std::chrono::system_clock::now(); const auto now_file = fs::file_time_type::clock::now(); // calculate the file modified time - const auto mtime = fs::last_write_time(path_name); + std::error_code ec; + const auto mtime = + fs::exists(path_name, ec) ? fs::last_write_time(path_name, ec) : fs::file_time_type{}; const auto mtimestamp = now_sys + (mtime - now_file); - if (fs::is_directory(path_name)) { + if (is_dir) { sb->st_mode = 0000777u | 0040000u; sb->st_size = 65536; sb->st_blksize = 65536; @@ -703,11 +695,31 @@ s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { // TODO incomplete } else { sb->st_mode = 0000777u | 0100000u; - sb->st_size = static_cast(fs::file_size(path_name)); + if (auto handle = mnt->Open(path, /*writable=*/false)) { + Core::FileSys::FileStat fst{}; + handle->Stat(fst); + sb->st_size = static_cast(fst.size); + if (fst.mtime_sec != 0 || fst.mtime_nsec != 0) { + sb->st_mtim.tv_sec = fst.mtime_sec; + sb->st_mtim.tv_nsec = fst.mtime_nsec; + sb->st_atim.tv_sec = fst.atime_sec; + sb->st_atim.tv_nsec = fst.atime_nsec; + sb->st_ctim.tv_sec = fst.ctime_sec; + sb->st_ctim.tv_nsec = fst.ctime_nsec; + } else { + sb->st_mtim.tv_sec = + std::chrono::duration_cast(mtimestamp.time_since_epoch()) + .count(); + } + } else { + sb->st_size = + fs::exists(path_name, ec) ? static_cast(fs::file_size(path_name, ec)) : 0; + sb->st_mtim.tv_sec = + std::chrono::duration_cast(mtimestamp.time_since_epoch()) + .count(); + } sb->st_blksize = 512; sb->st_blocks = (sb->st_size + 511) / 512; - sb->st_mtim.tv_sec = - std::chrono::duration_cast(mtimestamp.time_since_epoch()).count(); // TODO incomplete } @@ -736,7 +748,7 @@ s32 PS4_SYSV_ABI sceKernelCheckReachability(const char* path) { } } const auto path_name = mnt->GetHostPath(guest_path); - if (!fs::exists(path_name)) { + if (!mnt->Exists(guest_path)) { return ORBIS_KERNEL_ERROR_ENOENT; } return ORBIS_OK; @@ -767,33 +779,19 @@ s32 PS4_SYSV_ABI fstat(s32 fd, OrbisKernelStat* sb) { } case Core::FileSys::FileType::Regular: { sb->st_mode = 0000777u | 0100000u; - sb->st_size = file->f.GetSize(); sb->st_blksize = 512; + Core::FileSys::FileStat fst{}; + if (file->handle) { + file->handle->Stat(fst); + } + sb->st_size = static_cast(fst.size); sb->st_blocks = (sb->st_size + 511) / 512; -#if defined(__linux__) || defined(__FreeBSD__) - struct stat filestat = {}; - stat(file->f.GetPath().c_str(), &filestat); - sb->st_atim = *reinterpret_cast(&filestat.st_atim); - sb->st_mtim = *reinterpret_cast(&filestat.st_mtim); - sb->st_ctim = *reinterpret_cast(&filestat.st_ctim); -#elif defined(__APPLE__) - struct stat filestat = {}; - stat(file->f.GetPath().c_str(), &filestat); - sb->st_atim = *reinterpret_cast(&filestat.st_atimespec); - sb->st_mtim = *reinterpret_cast(&filestat.st_mtimespec); - sb->st_ctim = *reinterpret_cast(&filestat.st_ctimespec); -#else - const auto ft = std::filesystem::last_write_time(file->f.GetPath()); - const auto sctp = std::chrono::time_point_cast( - ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); - const auto secs = std::chrono::time_point_cast(sctp); - const auto nsecs = std::chrono::duration_cast(sctp - secs); - - sb->st_mtim.tv_sec = static_cast(secs.time_since_epoch().count()); - sb->st_mtim.tv_nsec = static_cast(nsecs.count()); - sb->st_atim = sb->st_mtim; - sb->st_ctim = sb->st_mtim; -#endif + sb->st_mtim.tv_sec = fst.mtime_sec; + sb->st_mtim.tv_nsec = fst.mtime_nsec; + sb->st_atim.tv_sec = fst.atime_sec; + sb->st_atim.tv_nsec = fst.atime_nsec; + sb->st_ctim.tv_sec = fst.ctime_sec; + sb->st_ctim.tv_nsec = fst.ctime_nsec; // TODO incomplete break; } @@ -856,8 +854,12 @@ s32 PS4_SYSV_ABI posix_ftruncate(s32 fd, s64 length) { *__Error() = POSIX_EACCES; return -1; } - - file->f.SetSize(length); + auto* host = file->GetHostFile(); + if (host == nullptr) { + *__Error() = POSIX_EROFS; + return -1; + } + host->SetSize(length); return ORBIS_OK; } @@ -919,10 +921,15 @@ s32 PS4_SYSV_ABI posix_rename(const char* from, const char* to) { auto* h = Common::Singleton::Instance(); auto file = h->GetFile(src_path); if (file) { - auto access_mode = file->f.GetAccessMode(); - file->f.Close(); + Common::FS::FileAccessMode access_mode = Common::FS::FileAccessMode::ReadWrite; + if (auto* host = file->GetHostFile()) { + access_mode = host->GetAccessMode(); + } + file->handle.reset(); fs::remove(src_path); - file->f.Open(dst_path, access_mode); + // Reopen through the mount stack at the destination guest path. + file->handle = mnt->Open(std::string_view(to), access_mode); + file->m_guest_name = to; } else { fs::remove_all(src_path); } @@ -969,22 +976,22 @@ s64 PS4_SYSV_ABI posix_preadv(s32 fd, OrbisKernelIovec* iov, s32 iovcnt, s64 off return result; } - if (file->f.IsWriteOnly()) { + if (file->IsWriteOnly()) { *__Error() = POSIX_EBADF; return -1; } - const s64 pos = file->f.Tell(); + const s64 pos = file->Tell(); SCOPE_EXIT { - file->f.Seek(pos); + file->Seek(pos); }; - if (!file->f.Seek(offset)) { + if (!file->Seek(offset)) { *__Error() = POSIX_EIO; return -1; } s64 total_read = 0; for (s32 i = 0; i < iovcnt; i++) { - total_read += ReadFile(file->f, iov[i].iov_base, iov[i].iov_len); + total_read += ReadFile(file, iov[i].iov_base, iov[i].iov_len); } return total_read; } @@ -1024,7 +1031,7 @@ s32 PS4_SYSV_ABI posix_fsync(s32 fd) { } return result; } - file->f.Flush(); + file->Flush(); return ORBIS_OK; } @@ -1138,17 +1145,17 @@ s64 PS4_SYSV_ABI posix_pwritev(s32 fd, const OrbisKernelIovec* iov, s32 iovcnt, return -1; } - const s64 pos = file->f.Tell(); + const s64 pos = file->Tell(); SCOPE_EXIT { - file->f.Seek(pos); + file->Seek(pos); }; - if (!file->f.Seek(offset)) { + if (!file->Seek(offset)) { *__Error() = POSIX_EIO; return -1; } s64 total_written = 0; for (s32 i = 0; i < iovcnt; i++) { - total_written += file->f.WriteRaw(iov[i].iov_base, iov[i].iov_len); + total_written += file->Write(iov[i].iov_base, iov[i].iov_len); } return total_written; } @@ -1211,8 +1218,8 @@ s32 PS4_SYSV_ABI posix_unlink(const char* path) { // File to unlink hasn't been opened, manually open and unlink it. Common::FS::IOFile file(host_path, Common::FS::FileAccessMode::ReadWrite); file.Unlink(); - } else { - file->f.Unlink(); + } else if (auto* host = file->GetHostFile()) { + host->Unlink(); } LOG_INFO(Kernel_Fs, "Unlinked {}", path); @@ -1279,7 +1286,7 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set_posix* readfds, fd_set_posix* wri } auto* file = h->GetFile(i); - if (!file || ((file->type == Core::FileSys::FileType::Regular && !file->f.IsOpen()) || + if (!file || ((file->type == Core::FileSys::FileType::Regular && !file->IsBackendOpen()) || (file->type == Core::FileSys::FileType::Socket && !file->is_opened))) { LOG_ERROR(Kernel_Fs, "fd {} is null or not opened", i); *__Error() = POSIX_EBADF; @@ -1289,7 +1296,9 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set_posix* readfds, fd_set_posix* wri s32 native_fd = -1; switch (file->type) { case Core::FileSys::FileType::Regular: - native_fd = static_cast(file->f.GetFileMapping()); + if (auto* host = file->GetHostFile()) { + native_fd = static_cast(host->GetFileMapping()); + } break; case Core::FileSys::FileType::Socket: { auto sock = file->socket->Native(); @@ -1438,7 +1447,7 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set* readfds, fd_set* writefds, fd_se if (read || write || except) { auto* file = h->GetFile(i); if (file == nullptr || - ((file->type == Core::FileSys::FileType::Regular && !file->f.IsOpen()) || + ((file->type == Core::FileSys::FileType::Regular && !file->IsBackendOpen()) || (file->type == Core::FileSys::FileType::Socket && !file->is_opened))) { LOG_ERROR(Kernel_Fs, "fd {} is null or not opened", i); *__Error() = POSIX_EBADF; @@ -1448,7 +1457,10 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set* readfds, fd_set* writefds, fd_se s32 native_fd = [&] { switch (file->type) { case Core::FileSys::FileType::Regular: - return static_cast(file->f.GetFileMapping()); + if (auto* host = file->GetHostFile()) { + return static_cast(host->GetFileMapping()); + } + return -1; case Core::FileSys::FileType::Device: return -1; case Core::FileSys::FileType::Socket: { diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index 16b7159e9..99e7edf57 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -76,10 +76,8 @@ s32 PS4_SYSV_ABI sceKernelLoadStartModule(const char* moduleFileName, u64 args, LOG_INFO(Lib_Kernel, "called filename = {}, args = {}", moduleFileName, args); ASSERT(flags == 0); - auto* mnt = Common::Singleton::Instance(); auto* linker = Common::Singleton::Instance(); - std::filesystem::path path; std::string guest_path(moduleFileName); s32 handle = -1; @@ -87,22 +85,19 @@ s32 PS4_SYSV_ABI sceKernelLoadStartModule(const char* moduleFileName, u64 args, if (guest_path[0] == '/') { // try load /system/common/lib/ +path // try load /system/priv/lib/ +path - path = mnt->GetHostPath(guest_path); - handle = linker->LoadAndStartModule(path, args, argp, pRes); + handle = linker->LoadAndStartModule(guest_path, args, argp, pRes); if (handle != -1) return handle; } else { if (!guest_path.contains('/')) { - path = mnt->GetHostPath("/app0/" + guest_path); - handle = linker->LoadAndStartModule(path, args, argp, pRes); + handle = linker->LoadAndStartModule("/app0/" + guest_path, args, argp, pRes); if (handle != -1) return handle; // if ((flags & 0x10000) != 0) // try load /system/priv/lib/ +basename // try load /system/common/lib/ +basename } else { - path = mnt->GetHostPath(guest_path); - handle = linker->LoadAndStartModule(path, args, argp, pRes); + handle = linker->LoadAndStartModule(guest_path, args, argp, pRes); if (handle != -1) return handle; } diff --git a/src/core/libraries/playgo/playgo.cpp b/src/core/libraries/playgo/playgo.cpp index 24f8d6761..a643a72eb 100644 --- a/src/core/libraries/playgo/playgo.cpp +++ b/src/core/libraries/playgo/playgo.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" @@ -253,8 +253,11 @@ s32 PS4_SYSV_ABI scePlayGoInitialize(OrbisPlayGoInitParams* param) { playgo = std::make_unique(); auto* mnt = Common::Singleton::Instance(); - const auto file_path = mnt->GetHostPath("/app0/sce_sys/playgo-chunk.dat"); - if (!playgo->Open(file_path)) { + if (auto bytes = mnt->ReadFile("/app0/sce_sys/playgo-chunk.dat")) { + if (!playgo->Open(std::span{*bytes})) { + LOG_WARNING(Lib_PlayGo, "Could not parse PlayGo file"); + } + } else { LOG_WARNING(Lib_PlayGo, "Could not open PlayGo file"); } diff --git a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp index f73647fca..e5269924d 100644 --- a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp +++ b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -138,9 +138,8 @@ SaveDialogState::SaveDialogState(const OrbisSaveDataDialogParam& param) { auto buf = (u8*)new_item->iconBuf; icon = RefCountedTexture::DecodePngTexture({buf, buf + new_item->iconSize}); } else { - const auto& src_icon = g_mnt->GetHostPath("/app0/sce_sys/save_data.png"); - if (std::filesystem::exists(src_icon)) { - icon = RefCountedTexture::DecodePngFile(src_icon); + if (auto bytes = g_mnt->ReadFile("/app0/sce_sys/save_data.png")) { + icon = RefCountedTexture::DecodePngTexture(std::move(*bytes)); } } if (new_item->title != nullptr) { diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index 463baa50b..599227e02 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -106,7 +106,7 @@ SaveInstance::SaveInstance(int slot_num, Libraries::UserService::OrbisUserServic mount_point = "/savedata" + std::to_string(slot_num); this->exists = fs::exists(param_sfo_path); - this->mounted = g_mnt->GetMount(mount_point) != std::nullopt; + this->mounted = g_mnt->GetMount(mount_point) != nullptr; } SaveInstance::~SaveInstance() { @@ -151,13 +151,13 @@ void SaveInstance::SetupAndMount(bool read_only, bool copy_icon, bool ignore_cor if (!exists) { CreateFiles(); if (copy_icon) { - const auto& src_icon = g_mnt->GetHostPath("/app0/sce_sys/save_data.png"); - if (fs::exists(src_icon)) { + if (auto bytes = g_mnt->ReadFile("/app0/sce_sys/save_data.png")) { auto output_icon = GetIconPath(); if (fs::exists(output_icon)) { fs::remove(output_icon); } - fs::copy_file(src_icon, output_icon); + Common::FS::IOFile dst(output_icon, Common::FS::FileAccessMode::Create); + dst.WriteRaw(bytes->data(), bytes->size()); } } exists = true; diff --git a/src/core/libraries/save_data/save_memory.cpp b/src/core/libraries/save_data/save_memory.cpp index 0a16d4756..cdca6789e 100644 --- a/src/core/libraries/save_data/save_memory.cpp +++ b/src/core/libraries/save_data/save_memory.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "save_memory.h" @@ -139,13 +139,15 @@ void SetIcon(u32 slot_id, void* buf, size_t buf_size) { const auto& data = g_attached_slots[slot_id]; const auto icon_path = data.folder_path / sce_sys / "icon0.png"; if (buf == nullptr) { - const auto& src_icon = g_mnt->GetHostPath("/app0/sce_sys/save_data.png"); if (fs::exists(icon_path)) { fs::remove(icon_path); } - if (fs::exists(src_icon)) { + // Read the source through the mount stack so archive-backed base + // games can seed save-slot icons too. + if (auto bytes = g_mnt->ReadFile("/app0/sce_sys/save_data.png")) { fs::create_directories(icon_path.parent_path()); - fs::copy_file(src_icon, icon_path); + IOFile dst(icon_path, Common::FS::FileAccessMode::Create); + dst.WriteRaw(bytes->data(), bytes->size()); } } else { IOFile file(icon_path, Common::FS::FileAccessMode::Create); diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp index 0b4058c77..b6aba7303 100644 --- a/src/core/libraries/sysmodule/sysmodule_internal.cpp +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -118,7 +118,6 @@ bool validateModuleId(s32 id) { } s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { - auto* mnt = Common::Singleton::Instance(); auto* linker = Common::Singleton::Instance(); auto* game_info = Common::Singleton::Instance(); @@ -135,10 +134,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsGame) != 0) { std::string guest_path = std::string("/app0/sce_module/").append(mod.name); guest_path.append(".prx"); - const auto& host_path = mnt->GetHostPath(guest_path); - - // For convenience, load through linker directly instead of loading through libkernel calls. - s32 result = linker->LoadAndStartModule(host_path, argc, argv, &start_result); + s32 result = linker->LoadAndStartModule(guest_path, argc, argv, &start_result); // If the module is missing, the library prints a very helpful message for developers. // We'll just log an error. if (result < 0) { diff --git a/src/core/libraries/system/systemservice.cpp b/src/core/libraries/system/systemservice.cpp index 3c943f236..4c082a0c1 100644 --- a/src/core/libraries/system/systemservice.cpp +++ b/src/core/libraries/system/systemservice.cpp @@ -1876,18 +1876,28 @@ int PS4_SYSV_ABI sceSystemServiceLoadExec(const char* path, const char* argv[]) LOG_DEBUG(Lib_SystemService, "called"); auto emu = Common::Singleton::Instance(); auto mnt = Common::Singleton::Instance(); - auto hostPath = mnt->GetHostPath(std::string_view(path)); - if (hostPath.empty()) { + // Validate through the mount stack so archive-backed targets resolve. + if (!mnt->Exists(std::string_view(path))) { LOG_INFO(Lib_SystemService, "Restart called with invalid file '{}', exiting.", path); std::quick_exit(0); } + std::filesystem::path exec_path; + if (auto handle = mnt->Open(std::string_view(path), /*writable=*/false)) { + if (auto host = handle->GetHostPath(); host.has_value()) { + exec_path = *host; + } else { + exec_path = std::filesystem::path(std::string_view(path)); + } + } else { + exec_path = mnt->GetHostPath(std::string_view(path)); + } std::vector args; if (argv != nullptr) { for (const char** ptr = argv; *ptr != nullptr; ptr++) { args.push_back(std::string(*ptr)); } } - emu->Restart(hostPath, args); + emu->Restart(exec_path, args); return ORBIS_OK; } diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 88a7f214a..f913ebe08 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -8,12 +8,15 @@ #include "common/logging/formatter.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "common/singleton.h" #include "common/string_util.h" #include "common/thread.h" #include "core/aerolib/aerolib.h" #include "core/aerolib/stubs.h" #include "core/devtools/widget/module_list.h" #include "core/emulator_settings.h" +#include "core/file_sys/backends/host_fs.h" +#include "core/file_sys/fs.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/threads.h" @@ -218,13 +221,27 @@ void Linker::Execute(const std::vector& args) { s32 Linker::LoadModule(const std::filesystem::path& elf_name, bool is_dynamic) { std::scoped_lock lk{mutex}; - - if (!std::filesystem::exists(elf_name)) { - LOG_ERROR(Core_Linker, "Provided file {} does not exist", elf_name.string()); - return -1; + auto* mnt = Common::Singleton::Instance(); + const std::string as_guest = elf_name.generic_string(); + std::unique_ptr handle; + if (!as_guest.empty() && as_guest.front() == '/') { + handle = mnt->Open(as_guest, /*writable=*/false); + } + if (!handle) { + if (!std::filesystem::exists(elf_name)) { + LOG_ERROR(Core_Linker, "Provided file {} does not exist", elf_name.string()); + return -1; + } + auto host = std::make_unique( + elf_name, Common::FS::FileAccessMode::Read, /*read_only=*/true); + if (!host->IsOpen()) { + LOG_ERROR(Core_Linker, "Provided file {} could not be opened", elf_name.string()); + return -1; + } + handle = std::move(host); } - auto module = std::make_unique(memory, elf_name, max_tls_index); + auto module = std::make_unique(memory, elf_name, std::move(handle), max_tls_index); if (!module->IsValid()) { LOG_ERROR(Core_Linker, "Provided file {} is not valid ELF file", elf_name.string()); return -1; diff --git a/src/core/loader/elf.cpp b/src/core/loader/elf.cpp index ed3f43c1c..705cea558 100644 --- a/src/core/loader/elf.cpp +++ b/src/core/loader/elf.cpp @@ -4,6 +4,7 @@ #include #include "common/assert.h" #include "common/logging/log.h" +#include "core/file_sys/backends/host_fs.h" #include "core/loader/elf.h" namespace Core::Loader { @@ -179,7 +180,19 @@ static std::string_view GetMachine(e_machine_es machine) { Elf::~Elf() = default; void Elf::Open(const std::filesystem::path& file_name) { - m_f.Open(file_name, FileAccessMode::Read); + auto handle = std::make_unique( + file_name, Common::FS::FileAccessMode::Read, /*read_only=*/true); + if (!handle->IsOpen()) { + return; + } + Open(std::move(handle)); +} + +void Elf::Open(std::unique_ptr handle) { + m_f.Reset(std::move(handle)); + if (!m_f.IsOpen()) { + return; + } if (!m_f.ReadObject(m_self)) { LOG_ERROR(Loader, "Unable to read self header!"); return; diff --git a/src/core/loader/elf.h b/src/core/loader/elf.h index 1dc0c60a1..3740015af 100644 --- a/src/core/loader/elf.h +++ b/src/core/loader/elf.h @@ -9,6 +9,7 @@ #include "common/io_file.h" #include "common/types.h" +#include "core/file_sys/ifile.h" struct self_header { static constexpr u32 signature = 0x1D3D154Fu; @@ -466,6 +467,7 @@ public: ~Elf(); void Open(const std::filesystem::path& file_name); + void Open(std::unique_ptr handle); bool IsSelfFile() const; bool IsElfFile() const; @@ -508,7 +510,7 @@ public: void PHeaderDebugDump(const std::filesystem::path& file_name); private: - Common::FS::IOFile m_f{}; + Core::FileSys::FileReader m_f{}; bool is_self{}; self_header m_self{}; std::vector m_self_segments; diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 5a6e0a793..463053bc6 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -703,14 +703,23 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory prot |= MemoryProt::CpuRead; } - handle = file->f.GetFileMapping(); + // Detect a non-host backend (ZArchive, ...). + Common::FS::IOFile* host_file = file->handle ? file->handle->GetHostFile() : nullptr; + const bool non_host_backed = file->handle && host_file == nullptr; - if (False(file->f.GetAccessMode() & Common::FS::FileAccessMode::Write) && - False(file->f.GetAccessMode() & Common::FS::FileAccessMode::Append)) { - // If the file does not have write access, ensure prot does not contain write - // permissions. On real hardware, these mappings succeed, but the memory cannot be - // written to. + if (non_host_backed) { + // Non-host backends are read-only prot &= ~MemoryProt::CpuWrite; + } else { + handle = host_file->GetFileMapping(); + + if (False(host_file->GetAccessMode() & Common::FS::FileAccessMode::Write) && + False(host_file->GetAccessMode() & Common::FS::FileAccessMode::Append)) { + // If the file does not have write access, ensure prot does not contain write + // permissions. On real hardware, these mappings succeed, but the memory cannot be + // written to. + prot &= ~MemoryProt::CpuWrite; + } } if (prot >= MemoryProt::GpuRead) { @@ -759,9 +768,36 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory auto& new_vma = new_vma_handle->second; new_vma.fd = fd; auto mapped_addr = new_vma.base; - bool is_exec = True(prot & MemoryProt::CpuExec); - impl.MapFile(mapped_addr, size, phys_addr, std::bit_cast(prot), handle); + // Delegate the actual mapping to the file backend. + Core::FileSys::FileMapContext map_ctx{}; + map_ctx.mapping_handle = handle; + map_ctx.map_native = [&](u8* addr, u64 sz, u64 off, u32 p, uintptr_t map_handle) { + impl.MapFile(reinterpret_cast(addr), sz, off, p, map_handle); + }; + map_ctx.map_anonymous = [&](u8* addr, u64 sz) { + constexpr auto kAnonMarker = static_cast(-1); + constexpr auto kNoFd = static_cast(-1); + const auto rw_prot = std::bit_cast(MemoryProt::CpuRead | MemoryProt::CpuWrite); + impl.MapFile(reinterpret_cast(addr), sz, kAnonMarker, rw_prot, kNoFd); + }; + map_ctx.protect = [&](u8* addr, u64 sz, u32 p) { + const auto mp = std::bit_cast(p); + Core::MemoryPermission perms{}; + if (True(mp & MemoryProt::CpuRead)) { + perms |= Core::MemoryPermission::Read; + } + if (True(mp & MemoryProt::CpuReadWrite)) { + perms |= Core::MemoryPermission::ReadWrite; + } + if (True(mp & MemoryProt::CpuExec)) { + perms |= Core::MemoryPermission::Execute; + } + impl.Protect(reinterpret_cast(addr), sz, perms); + }; + + file->handle->Map(reinterpret_cast(mapped_addr), size, phys_addr, std::bit_cast(prot), + map_ctx); *out_addr = std::bit_cast(mapped_addr); return ORBIS_OK; diff --git a/src/core/module.cpp b/src/core/module.cpp index bc5e15615..20b92dacf 100644 --- a/src/core/module.cpp +++ b/src/core/module.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/alignment.h" @@ -93,6 +93,17 @@ Module::Module(Core::MemoryManager* memory_, const std::filesystem::path& file_, } } +Module::Module(Core::MemoryManager* memory_, const std::filesystem::path& file_, + std::unique_ptr handle, u32& max_tls_index) + : memory{memory_}, file{file_}, name{file.filename().string()} { + elf.Open(std::move(handle)); + if (elf.IsElfFile()) { + LoadModuleToMemory(max_tls_index); + LoadDynamicInfo(); + LoadSymbols(); + } +} + Module::~Module() = default; s32 Module::Start(u64 args, const void* argp, void* param) { diff --git a/src/core/module.h b/src/core/module.h index 8dde0f467..a4e448e13 100644 --- a/src/core/module.h +++ b/src/core/module.h @@ -143,6 +143,8 @@ class Module { public: explicit Module(Core::MemoryManager* memory, const std::filesystem::path& file, u32& max_tls_index); + explicit Module(Core::MemoryManager* memory, const std::filesystem::path& file, + std::unique_ptr handle, u32& max_tls_index); ~Module(); VAddr GetBaseAddress() const noexcept { diff --git a/src/emulator.cpp b/src/emulator.cpp index 43991813f..6202a4bbd 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -90,9 +90,20 @@ void Emulator::Shutdown() { exit_done = true; } -s32 ReadCompiledSdkVersion(const std::filesystem::path& file) { +s32 ReadCompiledSdkVersion(const std::string& guest_or_host_path) { + auto* mnt = Common::Singleton::Instance(); + std::unique_ptr handle; + if (!guest_or_host_path.empty() && guest_or_host_path.front() == '/') { + handle = mnt->Open(guest_or_host_path, /*writable=*/false); + } + Core::Loader::Elf elf; - elf.Open(file); + if (handle) { + elf.Open(std::move(handle)); + } else { + elf.Open(std::filesystem::path{guest_or_host_path}); + } + if (!elf.IsElfFile()) { return 0; } @@ -109,12 +120,15 @@ s32 ReadCompiledSdkVersion(const std::filesystem::path& file) { return 0; } -std::map ExtractTrophies(const std::filesystem::path& npbind_path, - const std::filesystem::path& trophy_dir) { +std::map ExtractTrophies(std::string_view npbind_guest, + std::string_view trophy_dir_guest) { std::map trophy_index_map{}; + auto* mnt = Common::Singleton::Instance(); + NPBindFile npbind; - if (!npbind.Load(npbind_path.string())) { + const auto npbind_bytes = mnt->ReadFile(npbind_guest); + if (!npbind_bytes || !npbind.Load(std::span{*npbind_bytes})) { LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); return trophy_index_map; } @@ -127,64 +141,103 @@ std::map ExtractTrophies(const std::filesystem::path& npbind_p auto& game_info = Common::ElfInfo::Instance(); game_info.SetNpCommIds(np_comm_ids); - if (!std::filesystem::exists(trophy_dir)) { + if (!mnt->IsDirectory(trophy_dir_guest)) { LOG_WARNING(Common_Filesystem, "Game does not contain a trophy directory"); return trophy_index_map; } - std::string pattern = "trophy"; - for (const auto& entry : std::filesystem::directory_iterator(trophy_dir)) { - if (entry.is_regular_file() && entry.path().extension() == ".trp") { - std::string filename = entry.path().stem().string(); // "trophy00", "trophy01", etc. + auto dir = mnt->OpenDir(trophy_dir_guest); + if (!dir) { + return trophy_index_map; + } - // Check if filename starts with "trophy" - if (filename.find(pattern) != 0) { - continue; - } + const std::string pattern = "trophy"; + Core::FileSys::DirEntry entry; + while (dir->Next(entry)) { + if (entry.is_directory) { + continue; + } + // Extension check: entry names carry the raw leaf, no path. + if (entry.name.size() < 4 || entry.name.compare(entry.name.size() - 4, 4, ".trp") != 0) { + continue; + } + const std::string stem = entry.name.substr(0, entry.name.size() - 4); + if (stem.find(pattern) != 0) { + continue; + } - // Extract the number part - std::string num_str = filename.substr(pattern.length()); - s32 trophy_index = std::stoi(num_str); + // Extract the number part + const std::string num_str = stem.substr(pattern.length()); + s32 trophy_index; + try { + trophy_index = std::stoi(num_str); + } catch (...) { + continue; + } - if (np_comm_ids.size() <= trophy_index) { - // This logic currently assumes each NPCommID corresponds to a trophy index. - LOG_WARNING(Common_Filesystem, - "Trophy index {} does not have a corresponding NPCommId", trophy_index); - continue; - } + if (static_cast(np_comm_ids.size()) <= trophy_index) { + LOG_WARNING(Common_Filesystem, "Trophy index {} does not have a corresponding NPCommId", + trophy_index); + continue; + } - // Add the relevant trophies to our trophy index map. - // This currently assumes the order of NPCommIDs matches the order of trophies. - std::string np_comm_id = np_comm_ids[trophy_index]; - trophy_index_map[trophy_index] = np_comm_id; - LOG_DEBUG(Loader, "Mapped trophy index {} to NPCommID: {}", trophy_index, np_comm_id); + const std::string np_comm_id = np_comm_ids[trophy_index]; + trophy_index_map[trophy_index] = np_comm_id; + LOG_DEBUG(Loader, "Mapped trophy index {} to NPCommID: {}", trophy_index, np_comm_id); - // Extract the actual trophies if they're no extracted yet - const auto& trophy_output_dir = - Common::FS::GetUserPath(Common::FS::PathType::TrophyDir) / np_comm_id; - if (!std::filesystem::exists(trophy_output_dir)) { - TRP trp; - if (!trp.Extract(entry, np_comm_id, trophy_output_dir)) { - LOG_ERROR(Loader, "Couldn't extract trophy file {}", filename); - continue; + // Extract the actual trophies if they're not extracted yet. + const auto& trophy_output_dir = + Common::FS::GetUserPath(Common::FS::PathType::TrophyDir) / np_comm_id; + if (!std::filesystem::exists(trophy_output_dir)) { + const std::string entry_guest = std::string(trophy_dir_guest) + "/" + entry.name; + std::filesystem::path trp_source; + std::filesystem::path temp_extract; + if (auto handle = mnt->Open(entry_guest, /*writable=*/false)) { + if (auto host = handle->GetHostPath(); host.has_value()) { + trp_source = *host; + } else { + // Archive-backed: dump bytes to a temp file for TRP. + if (auto bytes = mnt->ReadFile(entry_guest)) { + temp_extract = std::filesystem::temp_directory_path() / + (np_comm_id + "_" + entry.name); + Common::FS::IOFile out(temp_extract, Common::FS::FileAccessMode::Create); + out.WriteRaw(bytes->data(), bytes->size()); + out.Close(); + trp_source = temp_extract; + } } } + if (trp_source.empty()) { + LOG_ERROR(Loader, "Couldn't read trophy file {}", entry.name); + continue; + } + TRP trp; + const bool ok = trp.Extract(trp_source, np_comm_id, trophy_output_dir); + if (!temp_extract.empty()) { + std::error_code ec; + std::filesystem::remove(temp_extract, ec); + } + if (!ok) { + LOG_ERROR(Loader, "Couldn't extract trophy file {}", entry.name); + continue; + } + } - // Move extracted trophy contents into each user's folder - for (User user : UserSettings.GetUserManager().GetValidUsers()) { - auto const user_trophy_file = EmulatorSettings.GetHomeDir() / - std::to_string(user.user_id) / "trophy" / - (np_comm_id + ".xml"); - if (!std::filesystem::exists(user_trophy_file)) { - auto temp = user_trophy_file.parent_path(); - std::filesystem::create_directories(temp); - std::error_code discard; - std::filesystem::copy_file(trophy_output_dir / "Xml" / "TROPCONF.XML", - user_trophy_file, discard); - } + // Move extracted trophy contents into each user's folder + for (User user : UserSettings.GetUserManager().GetValidUsers()) { + auto const user_trophy_file = EmulatorSettings.GetHomeDir() / + std::to_string(user.user_id) / "trophy" / + (np_comm_id + ".xml"); + if (!std::filesystem::exists(user_trophy_file)) { + auto temp = user_trophy_file.parent_path(); + std::filesystem::create_directories(temp); + std::error_code discard; + std::filesystem::copy_file(trophy_output_dir / "Xml" / "TROPCONF.XML", + user_trophy_file, discard); } } } + return trophy_index_map; } @@ -200,24 +253,66 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } std::filesystem::path game_folder; - if (p_game_folder.has_value()) { - game_folder = p_game_folder.value(); - } else { - game_folder = file.parent_path(); - if (const auto game_folder_name = game_folder.filename().string(); - game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch") || - game_folder_name.ends_with("-mods")) { - // If an executable was launched from a separate update directory, - // use the base game directory as the game folder. - const std::string base_name = game_folder_name.substr(0, game_folder_name.rfind('-')); - const auto base_path = game_folder.parent_path() / base_name; - if (std::filesystem::is_directory(base_path)) { - game_folder = base_path; + + // Archive detection. + std::filesystem::path archive_path; + std::filesystem::path archive_inner; + { + std::filesystem::path accum; + bool found = false; + for (const auto& comp : file) { + if (!found) { + accum /= comp; + if (comp.extension() == ".zar") { + found = true; + archive_path = accum; + } + } else { + archive_inner /= comp; } } + // Only treat it as an archive if the .zar element is a real file. + if (found && !std::filesystem::is_regular_file(archive_path)) { + found = false; + } + if (found && archive_inner.empty()) { + archive_inner = "eboot.bin"; + } + } + const bool from_archive = !archive_path.empty(); + + const auto rebase_to_base_game = [](std::filesystem::path& folder) { + if (const auto base = FileSys::BaseGameFromOverlay(folder)) { + if (const auto resolved = FileSys::ResolveGameRoot(*base)) { + LOG_INFO(Loader, "Launched from overlay {}, using base game {} as /app0", + folder.string(), resolved->string()); + folder = *resolved; + } else { + LOG_WARNING(Loader, "Launched from overlay {} but no base game was found", + folder.string()); + } + } + }; + + std::filesystem::path eboot_name; + + if (from_archive) { + game_folder = archive_path; + file = archive_path / archive_inner; + eboot_name = archive_inner; + rebase_to_base_game(game_folder); } - std::filesystem::path eboot_name = std::filesystem::relative(file, game_folder); + if (!from_archive) { + if (p_game_folder.has_value()) { + game_folder = p_game_folder.value(); + eboot_name = std::filesystem::relative(file, game_folder); + } else { + game_folder = file.parent_path(); + eboot_name = std::filesystem::relative(file, game_folder); + rebase_to_base_game(game_folder); + } + } // Applications expect to be run from /app0 so mount the file's parent path as app0. auto* mnt = Common::Singleton::Instance(); @@ -225,55 +320,59 @@ void Emulator::Run(std::filesystem::path file, std::vector args, // Certain games may use /hostapp as well such as CUSA001100 mnt->Mount(game_folder, "/hostapp", true); - const auto param_sfo_path = mnt->GetHostPath("/app0/sce_sys/param.sfo"); - const auto param_sfo_exists = std::filesystem::exists(param_sfo_path); - // Load param.sfo details if it exists std::string id; std::string title; std::string app_version; u32 sdk_version; u32 fw_version; + bool param_sfo_exists = false; Common::PSFAttributes psf_attributes{}; - if (param_sfo_exists) { - auto* param_sfo = Common::Singleton::Instance(); - ASSERT_MSG(param_sfo->Open(param_sfo_path), "Failed to open param.sfo"); - const auto content_id = param_sfo->GetString("CONTENT_ID"); - const auto title_id = param_sfo->GetString("TITLE_ID"); - if (content_id.has_value() && !content_id->empty()) { - id = std::string(*content_id, 7, 9); - } else if (title_id.has_value()) { - id = *title_id; - } - title = param_sfo->GetString("TITLE").value_or("Unknown title"); - fw_version = param_sfo->GetInteger("SYSTEM_VER").value_or(0x4700000); - app_version = param_sfo->GetString("APP_VER").value_or("Unknown version"); - if (const auto raw_attributes = param_sfo->GetInteger("ATTRIBUTE")) { - psf_attributes.raw = *raw_attributes; - } + if (auto psf_handle = mnt->Open("/app0/sce_sys/param.sfo", /*writable=*/false)) { + std::vector psf_buf(psf_handle->Size()); + if (psf_handle->Read(psf_buf.data(), psf_buf.size()) == static_cast(psf_buf.size())) { + auto* param_sfo = Common::Singleton::Instance(); + ASSERT_MSG(param_sfo->Open(psf_buf), "Failed to open param.sfo"); + param_sfo_exists = true; - // Extract sdk version from pubtool info. - std::string_view pubtool_info = - param_sfo->GetString("PUBTOOLINFO").value_or("Unknown value"); - u64 sdk_ver_offset = pubtool_info.find("sdk_ver"); + const auto content_id = param_sfo->GetString("CONTENT_ID"); + const auto title_id = param_sfo->GetString("TITLE_ID"); + if (content_id.has_value() && !content_id->empty()) { + id = std::string(*content_id, 7, 9); + } else if (title_id.has_value()) { + id = *title_id; + } + title = param_sfo->GetString("TITLE").value_or("Unknown title"); + fw_version = param_sfo->GetInteger("SYSTEM_VER").value_or(0x4700000); + app_version = param_sfo->GetString("APP_VER").value_or("Unknown version"); + if (const auto raw_attributes = param_sfo->GetInteger("ATTRIBUTE")) { + psf_attributes.raw = *raw_attributes; + } - if (sdk_ver_offset == pubtool_info.npos) { - // Default to using firmware version if SDK version is not found. - sdk_version = fw_version; - } else { - // Increment offset to account for sdk_ver= part of string. - sdk_ver_offset += 8; - u64 sdk_ver_len = pubtool_info.find(",", sdk_ver_offset); - if (sdk_ver_len == pubtool_info.npos) { - // If there's no more commas, this is likely the last entry of pubtool info. - // Use string length instead. - sdk_ver_len = pubtool_info.size(); + // Extract sdk version from pubtool info. + std::string_view pubtool_info = + param_sfo->GetString("PUBTOOLINFO").value_or("Unknown value"); + u64 sdk_ver_offset = pubtool_info.find("sdk_ver"); + + if (sdk_ver_offset == pubtool_info.npos) { + // Default to using firmware version if SDK version is not found. + sdk_version = fw_version; + } else { + // Increment offset to account for sdk_ver= part of string. + sdk_ver_offset += 8; + u64 sdk_ver_len = pubtool_info.find(",", sdk_ver_offset); + if (sdk_ver_len == pubtool_info.npos) { + // If there's no more commas, this is likely the last entry of pubtool info. + // Use string length instead. + sdk_ver_len = pubtool_info.size(); + } + sdk_ver_len -= sdk_ver_offset; + std::string sdk_ver_string = + pubtool_info.substr(sdk_ver_offset, sdk_ver_len).data(); + // Number is stored in base 16. + sdk_version = std::stoi(sdk_ver_string, nullptr, 16); } - sdk_ver_len -= sdk_ver_offset; - std::string sdk_ver_string = pubtool_info.substr(sdk_ver_offset, sdk_ver_len).data(); - // Number is stored in base 16. - sdk_version = std::stoi(sdk_ver_string, nullptr, 16); } } @@ -283,7 +382,6 @@ void Emulator::Run(std::filesystem::path file, std::vector args, : "shad_log.txt"); auto guest_eboot_path = "/app0/" + eboot_name.generic_string(); - const auto eboot_path = mnt->GetHostPath(guest_eboot_path); auto& game_info = Common::ElfInfo::Instance(); game_info.initialized = true; @@ -292,19 +390,19 @@ void Emulator::Run(std::filesystem::path file, std::vector args, game_info.app_ver = app_version; game_info.firmware_ver = fw_version & 0xFFF00000; game_info.raw_firmware_ver = fw_version; - game_info.sdk_ver = ReadCompiledSdkVersion(eboot_path); + game_info.sdk_ver = ReadCompiledSdkVersion(guest_eboot_path); game_info.psf_attributes = psf_attributes; - const auto pic1_path = mnt->GetHostPath("/app0/sce_sys/pic1.png"); - if (std::filesystem::exists(pic1_path)) { - game_info.splash_path = pic1_path; + if (auto splash = mnt->ReadFile("/app0/sce_sys/pic1.png")) { + game_info.splash_data = std::move(*splash); + } else { + LOG_INFO(Loader, "No splash image found at /app0/sce_sys/pic1.png"); } game_info.game_folder = game_folder; - if (!std::filesystem::exists(file)) { - LOG_CRITICAL(Loader, "eboot.bin does not exist: {}", - std::filesystem::absolute(file).string()); + if (!mnt->Exists(guest_eboot_path)) { + LOG_CRITICAL(Loader, "eboot.bin does not exist: {}", guest_eboot_path); std::quick_exit(0); } @@ -378,8 +476,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, args.insert(args.begin(), guest_eboot_path); } - std::filesystem::path mods_folder = game_folder; - mods_folder += "-mods"; + const auto mods_folder = FileSys::OverlayPath(game_folder, "-mods"); if (std::filesystem::exists(mods_folder) && !std::filesystem::is_empty(mods_folder)) { LOG_INFO(Loader, "Files found in game mods folder"); @@ -401,10 +498,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, MemoryPatcher::g_game_serial = id; } - // Extract and load trophies - std::filesystem::path npbind_path = mnt->GetHostPath("/app0/sce_sys/npbind.dat"); - std::filesystem::path trophy_dir = mnt->GetHostPath("/app0/sce_sys/trophy"); - game_info.trophy_index_map = ExtractTrophies(npbind_path, trophy_dir); + // Extract and load trophies. + game_info.trophy_index_map = + ExtractTrophies("/app0/sce_sys/npbind.dat", "/app0/sce_sys/trophy"); std::string game_title = fmt::format("{} - {} <{}>", id, title, app_version); std::string window_title = ""; @@ -432,8 +528,11 @@ void Emulator::Run(std::filesystem::path file, std::vector args, g_window = window.get(); - std::filesystem::path icon_path = mnt->GetHostPath("/app0/sce_sys/icon0.png"); - window->SetIcon(icon_path); + if (auto icon = mnt->ReadFile("/app0/sce_sys/icon0.png")) { + window->SetIcon(*icon); + } else { + window->SetIcon({}); + } const auto& mount_data_dir = Common::FS::GetUserPath(Common::FS::PathType::GameDataDir); mnt->Mount(mount_data_dir, "/data"); @@ -492,10 +591,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, // Initialize kernel and library facilities. Libraries::InitHLELibs(&linker->GetHLESymbols()); - // Load the module with the linker - if (linker->LoadModule(eboot_path) == -1) { - LOG_CRITICAL(Loader, "Failed to load game's eboot.bin: {}", - Common::FS::PathToUTF8String(std::filesystem::absolute(eboot_path))); + // Load the module with the linker. + if (linker->LoadModule(guest_eboot_path) == -1) { + LOG_CRITICAL(Loader, "Failed to load game's eboot.bin: {}", guest_eboot_path); std::quick_exit(0); } @@ -538,15 +636,40 @@ void Emulator::Restart(std::filesystem::path eboot_path, const std::vector& guest_args) { std::vector args; - auto mnt = Common::Singleton::Instance(); - auto game_path = mnt->GetHostPath("/app0"); + auto& game_info = Common::ElfInfo::Instance(); + const auto& game_folder = game_info.GetGameFolder(); + const bool from_archive = + std::filesystem::is_regular_file(game_folder) && game_folder.extension() == ".zar"; args.push_back("--log-append"); - args.push_back("--game"); - args.push_back(Common::FS::PathToUTF8String(eboot_path)); - args.push_back("--override-root"); - args.push_back(Common::FS::PathToUTF8String(game_path)); + if (from_archive) { + // Archive-backed base game: relaunch by pointing --game at the + // .zar itself. Run() re-detects the extension and re-mounts it. + std::filesystem::path relaunch = game_folder; + std::string guest = Common::FS::PathToUTF8String(eboot_path); + for (const std::string_view prefix : {"/app0/", "/hostapp/"}) { + if (guest.starts_with(prefix)) { + guest.erase(0, prefix.size()); + break; + } + } + if (!guest.empty() && guest != "eboot.bin") { + relaunch /= guest; + } + + args.push_back("--game"); + args.push_back(Common::FS::PathToUTF8String(relaunch)); + } else { + auto mnt = Common::Singleton::Instance(); + auto game_path = mnt->GetHostPath("/app0"); + + args.push_back("--game"); + args.push_back(Common::FS::PathToUTF8String(eboot_path)); + + args.push_back("--override-root"); + args.push_back(Common::FS::PathToUTF8String(game_path)); + } if (FileSys::MntPoints::ignore_game_patches) { args.push_back("--ignore-game-patch"); diff --git a/src/imgui/big_picture/big_picture.cpp b/src/imgui/big_picture/big_picture.cpp index fdf77b484..fcca5bcd6 100644 --- a/src/imgui/big_picture/big_picture.cpp +++ b/src/imgui/big_picture/big_picture.cpp @@ -8,6 +8,7 @@ #include "core/devtools/layer.h" #include "core/emulator_settings.h" #include "core/file_format/psf.h" +#include "core/file_sys/fs.h" #include "emulator.h" #include "imgui/big_picture/big_picture.h" #include "imgui/big_picture/imgui_impl_sdl3_big_picture.h" @@ -34,11 +35,8 @@ namespace { std::filesystem::path UpdateChecker(const std::string sceItem, std::filesystem::path game_folder) { std::filesystem::path outputPath; - auto update_folder = game_folder; - update_folder += "-UPDATE"; - - auto patch_folder = game_folder; - patch_folder += "-patch"; + const auto update_folder = Core::FileSys::OverlayPath(game_folder, "-UPDATE"); + const auto patch_folder = Core::FileSys::OverlayPath(game_folder, "-patch"); if (std::filesystem::exists(update_folder / "sce_sys" / sceItem)) { outputPath = update_folder / "sce_sys" / sceItem; diff --git a/src/main.cpp b/src/main.cpp index 4dd8026bc..ebe35bb20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -209,7 +209,17 @@ int main(int argc, char* argv[]) { // ---- Resolve game path or ID ---- std::filesystem::path ebootPath(*gamePath); - if (!std::filesystem::exists(ebootPath)) { + const auto archive_component_exists = [](const std::filesystem::path& p) -> bool { + std::filesystem::path accum; + for (const auto& comp : p) { + accum /= comp; + if (comp.extension() == ".zar") { + return std::filesystem::is_regular_file(accum); + } + } + return false; + }; + if (!std::filesystem::exists(ebootPath) && !archive_component_exists(ebootPath)) { bool found = false; constexpr int maxDepth = 5; for (const auto& installDir : EmulatorSettings.GetGameInstallDirs()) { diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 4c725d295..070281ac0 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -190,34 +190,13 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controller WindowSDL::~WindowSDL() = default; -void WindowSDL::SetIcon(const std::filesystem::path& path) { - if (!std::filesystem::exists(path)) { - LOG_WARNING(Core, "Could not find icon file '{}', using default icon.", - fmt::UTF(path.u8string())); +void WindowSDL::SetIcon(std::span png_data) { + if (png_data.empty()) { + LOG_WARNING(Core, "No window icon data available, using default icon."); SetDefaultWindowIcon(window); return; } - - Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read, - Common::FS::FileType::BinaryFile, - Common::FS::FileShareFlag::ShareReadWrite}; - if (!file.IsOpen()) { - LOG_ERROR(Core, "Failed to open window icon file '{}'.", fmt::UTF(path.u8string())); - SetDefaultWindowIcon(window); - return; - } - - const u64 fileSize = file.GetSize(); - std::vector buf(fileSize); - const size_t bytesRead = file.ReadRaw(buf.data(), fileSize); - file.Close(); - if (bytesRead < fileSize) { - LOG_ERROR(Core, "Failed to read window icon file '{}'.", fmt::UTF(path.u8string())); - SetDefaultWindowIcon(window); - return; - } - - SetWindowIcon(window, buf); + SetWindowIcon(window, std::vector(png_data.begin(), png_data.end())); } void WindowSDL::WaitEvent() { diff --git a/src/sdl_window.h b/src/sdl_window.h index 74526c695..5f82bf6e0 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "common/types.h" @@ -71,7 +72,7 @@ public: return window_info; } - void SetIcon(const std::filesystem::path& path); + void SetIcon(std::span png_data); void WaitEvent(); void InitTimers(); diff --git a/src/video_core/renderer_vulkan/vk_presenter.cpp b/src/video_core/renderer_vulkan/vk_presenter.cpp index 5a6d4a2d5..ddf2e713c 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.cpp +++ b/src/video_core/renderer_vulkan/vk_presenter.cpp @@ -970,9 +970,9 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { if (Libraries::SystemService::IsSplashVisible()) { // draw splash if (!splash_img.has_value()) { splash_img.emplace(); - auto splash_path = Common::ElfInfo::Instance().GetSplashPath(); - if (!splash_path.empty()) { - splash_img = ImGui::RefCountedTexture::DecodePngFile(splash_path); + const auto& splash_data = Common::ElfInfo::Instance().GetSplashData(); + if (!splash_data.empty()) { + splash_img = ImGui::RefCountedTexture::DecodePngTexture(splash_data); } } if (auto& splash_image = this->splash_img.value()) {