Abstract FS (#4749)

* abstract fs part1

* ops ifile :P

* added backedn to fs/file_system

* memory handling for non hosted backends

* remove ZArchive

* total remove

* make linux happy

* refactor loader to support backends

* AVIO streamer for ffmpeg

* Improved IterateDirectory so it can getdents in zarchieve works

* support for fstat in zachieve

* fixed module loading

* fs : added ReadFile

* improve file reading

* fixed trophy extraction , npbind,playgo

* argg

* fixed headers

* zarchive improvements for concurrent access

* playgo fix

* system service fix

* fixed restart from zar

* fixed save data icon read from zarchive

* fonts fix

* some stephen's comments fixups

* improved

* fixed rest of stephen's comments

* even more stephen review fixes

* possible linux fix

* fixed stephen's issue with multi games

* added thread worker to zarchive to solve some race conditions videos should be working fine now

* fixed paths for updates,mods

* fixes for updates , splash screen , icon

* fixed case sentitive issues in linux?
This commit is contained in:
georgemoralis
2026-07-28 20:05:58 +03:00
committed by GitHub
parent 4f89152f93
commit b45b289761
45 changed files with 2274 additions and 402 deletions
+6
View File
@@ -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
+8
View File
@@ -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")
+25
View File
@@ -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()
Vendored Submodule
+1
Submodule externals/zarchive added at 965b66c8d6
Vendored Submodule
+1
Submodule externals/zstd added at 5c7b7bad26
+3 -3
View File
@@ -73,7 +73,7 @@ class ElfInfo {
u32 sdk_ver = 0;
PSFAttributes psf_attributes{};
std::filesystem::path splash_path{};
std::vector<u8> splash_data{};
std::filesystem::path game_folder{};
std::vector<std::string> npCommIds{};
std::map<s32, std::string> 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<u8>& GetSplashData() const {
return splash_data;
}
[[nodiscard]] const std::filesystem::path& GetGameFolder() const {
+12 -8
View File
@@ -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<char*>(buf.data()), sz))
return false;
const u64 size = buf.size();
return Load(std::span<const u8>{buf});
}
bool NPBindFile::Load(std::span<const u8> 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);
}
+2
View File
@@ -3,6 +3,7 @@
#pragma once
#include <cstdint>
#include <span>
#include <string>
#include <vector>
#include "common/endian.h"
@@ -48,6 +49,7 @@ public:
// Load from file
bool Load(const std::string& path);
bool Load(std::span<const u8> data);
// Accessors
const NpBindHeader& Header() const {
+57 -1
View File
@@ -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 <cstring>
#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<const u8> 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<const char*>(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<playgo_chunk_attr_entry_t*>(&chunk_attrs_data[0]);
auto chunk_mchunks = reinterpret_cast<u16*>(&chunk_mchunks_data[0]);
auto chunk_labels = reinterpret_cast<char*>(&chunk_labels_data[0]);
auto mchunk_attrs = reinterpret_cast<playgo_mchunk_attr_entry_t*>(&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<u16*>(((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) {
+2
View File
@@ -4,6 +4,7 @@
#pragma once
#include <filesystem>
#include <mutex>
#include <span>
#include <vector>
#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<const u8> data);
bool LoadChunks(const Common::FS::IOFile& file);
PlaygoHeader& GetPlaygoHeader() {
+239
View File
@@ -0,0 +1,239 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <system_error>
#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
#include <sys/stat.h>
#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<s64>(m_file.ReadRaw<u8>(dst, size));
}
s64 HostFile::Write(const void* src, u64 size) {
if (!m_file.IsOpen() || m_read_only) {
return -1;
}
return static_cast<s64>(m_file.WriteRaw<u8>(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<u64>(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<s64>(st.st_mtim.tv_sec);
out.mtime_nsec = static_cast<s64>(st.st_mtim.tv_nsec);
out.atime_sec = static_cast<s64>(st.st_atim.tv_sec);
out.atime_nsec = static_cast<s64>(st.st_atim.tv_nsec);
out.ctime_sec = static_cast<s64>(st.st_ctim.tv_sec);
out.ctime_nsec = static_cast<s64>(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<s64>(st.st_mtimespec.tv_sec);
out.mtime_nsec = static_cast<s64>(st.st_mtimespec.tv_nsec);
out.atime_sec = static_cast<s64>(st.st_atimespec.tv_sec);
out.atime_nsec = static_cast<s64>(st.st_atimespec.tv_nsec);
out.ctime_sec = static_cast<s64>(st.st_ctimespec.tv_sec);
out.ctime_nsec = static_cast<s64>(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<std::chrono::nanoseconds>(
ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
const auto secs = std::chrono::time_point_cast<std::chrono::seconds>(sctp);
const auto nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(sctp - secs);
out.mtime_sec = static_cast<s64>(secs.time_since_epoch().count());
out.mtime_nsec = static_cast<s64>(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<std::filesystem::path> 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<IFile> 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<HostFile>(path, mode, m_read_only || !writable);
if (!handle->IsOpen()) {
return nullptr;
}
return handle;
}
std::unique_ptr<IDirectory> 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<HostDirectory>(path);
}
} // namespace Core::FileSys
+104
View File
@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <tsl/robin_map.h>
#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<std::filesystem::path> 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<IFile> Open(std::string_view rel_path,
Common::FS::FileAccessMode mode) override;
std::unique_ptr<IDirectory> OpenDir(std::string_view rel_path) override;
bool IsReadOnly() const override {
return m_read_only;
}
std::optional<std::filesystem::path> RootHostPath() const override {
return m_root;
}
private:
std::filesystem::path Resolve(std::string_view rel_path) const;
std::optional<std::filesystem::path> 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<std::string, std::filesystem::path> m_case_cache;
};
} // namespace Core::FileSys
+355
View File
@@ -0,0 +1,355 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <string>
#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
#include <sys/stat.h>
#endif
#include <zarchive/zarchivereader.h>
#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<SharedReader> 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<ReadJob*>(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<s64>(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<s64>(m_position);
break;
case Common::FS::SeekOrigin::End:
base = static_cast<s64>(m_size);
break;
}
const s64 target = base + offset;
if (target < 0) {
return false;
}
m_position = static_cast<u64>(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<s64>(st.st_mtim.tv_sec);
out.mtime_nsec = static_cast<s64>(st.st_mtim.tv_nsec);
out.atime_sec = static_cast<s64>(st.st_atim.tv_sec);
out.atime_nsec = static_cast<s64>(st.st_atim.tv_nsec);
out.ctime_sec = static_cast<s64>(st.st_ctim.tv_sec);
out.ctime_nsec = static_cast<s64>(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<s64>(st.st_mtimespec.tv_sec);
out.mtime_nsec = static_cast<s64>(st.st_mtimespec.tv_nsec);
out.atime_sec = static_cast<s64>(st.st_atimespec.tv_sec);
out.atime_nsec = static_cast<s64>(st.st_atimespec.tv_nsec);
out.ctime_sec = static_cast<s64>(st.st_ctimespec.tv_sec);
out.ctime_nsec = static_cast<s64>(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<std::chrono::nanoseconds>(
ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
const auto secs = std::chrono::time_point_cast<std::chrono::seconds>(sctp);
const auto nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(sctp - secs);
out.mtime_sec = static_cast<s64>(secs.time_since_epoch().count());
out.mtime_nsec = static_cast<s64>(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<MapJob*>(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<SharedReader> 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<SharedReader>(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<IFile> 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<ZArchiveFile>(m_reader, node, size, m_archive_path);
}
std::unique_ptr<IDirectory> 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<ZArchiveDirectory>(m_reader, node);
}
} // namespace Core::FileSys
+134
View File
@@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <filesystem>
#include <memory>
#include <mutex>
#include <string_view>
#include <thread>
#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<SharedReader> 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<std::filesystem::path> 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<SharedReader> 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<SharedReader> reader, uint32_t node);
~ZArchiveDirectory() override = default;
bool Next(DirEntry& out) override;
void Rewind() override;
private:
std::shared_ptr<SharedReader> 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<IFile> Open(std::string_view rel_path,
Common::FS::FileAccessMode mode) override;
std::unique_ptr<IDirectory> OpenDir(std::string_view rel_path) override;
bool IsReadOnly() const override {
return true;
}
std::optional<std::filesystem::path> 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<SharedReader> m_reader;
};
} // namespace Core::FileSys
+294 -49
View File
@@ -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 <algorithm>
#include <array>
#include <unordered_set>
#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<std::filesystem::path> 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<std::string_view, 3> 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<std::filesystem::path> 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<std::shared_ptr<IBackend>> 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<IBackend> {
if (std::filesystem::is_directory(p)) {
return std::make_shared<HostFsBackend>(p, ro);
}
const auto try_zar = [ro](const std::filesystem::path& zar) -> std::shared_ptr<IBackend> {
if (std::filesystem::is_regular_file(zar) && zar.extension() == ".zar") {
auto backend = std::make_shared<ZArchiveBackend>(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<IBackend> {
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<IBackend> 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<MntPair> 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<std::string> 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<std::pair<std::filesystem::path, bool>> {
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<std::string> 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<IFile> 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<IFile> 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<IDirectory> 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<std::vector<u8>> 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<u8> buf(size);
if (size == 0) {
return buf;
}
const s64 got = handle->Read(buf.data(), size);
if (got < 0 || static_cast<u64>(got) != size) {
return std::nullopt;
}
return buf;
}
int HandleTable::CreateHandle() {
std::scoped_lock lock{m_mutex};
+80 -7
View File
@@ -4,6 +4,7 @@
#pragma once
#include <atomic>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
@@ -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<std::filesystem::path> BaseGameFromOverlay(
const std::filesystem::path& path);
[[nodiscard]] std::optional<std::filesystem::path> 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<std::shared_ptr<IBackend>> 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<IFile> Open(std::string_view guest_path, bool writable = false);
// Open with an explicit host access mode.
std::unique_ptr<IFile> Open(std::string_view guest_path, Common::FS::FileAccessMode mode);
/// Opens a directory through the mount's backend stack.
std::unique_ptr<IDirectory> 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<std::vector<u8>> 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<MntPair> 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<FileType> type{FileType::Regular};
std::filesystem::path m_host_name;
std::string m_guest_name;
Common::FS::IOFile f;
std::unique_ptr<IFile> handle;
std::mutex m_mutex;
std::shared_ptr<Directories::BaseDirectory> directory; // only valid for type == Directory
std::shared_ptr<Devices::BaseDevice> device; // only valid for type == Device
std::shared_ptr<Libraries::Net::Socket> socket; // only valid for type == Socket
std::shared_ptr<Libraries::Net::Epoll> epoll; // only valid for type == Epoll
std::shared_ptr<Libraries::Net::Resolver> 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<s64>(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 {
+205
View File
@@ -0,0 +1,205 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#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<void(u8* addr, u64 size, u64 offset, u32 prot, uintptr_t mapping_handle)>
map_native;
// Reserve size bytes of anonymous, writable memory at addr so
// the backend can populate it from Read().
std::function<void(u8* addr, u64 size)> map_anonymous;
// Apply final protection to a populated region.
std::function<void(u8* addr, u64 size, u32 prot)> 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<std::filesystem::path> GetHostPath() const {
return std::nullopt;
}
virtual std::optional<std::filesystem::path> 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<IFile> handle) : m_handle(std::move(handle)) {}
void Reset(std::unique_ptr<IFile> handle) {
m_handle = std::move(handle);
}
bool IsOpen() const {
return m_handle && m_handle->IsOpen();
}
IFile* Get() const {
return m_handle.get();
}
template <typename T>
bool ReadObject(T& obj) {
static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable");
static_assert(!std::is_pointer_v<T>, "T must not be a pointer");
if (!IsOpen()) {
return false;
}
return m_handle->Read(&obj, sizeof(T)) == static_cast<s64>(sizeof(T));
}
template <typename T>
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<size_t>(got) / sizeof(T);
}
template <typename T>
size_t Read(T& data) {
if constexpr (Common::IsContiguousContainer<T>) {
using ContiguousType = typename T::value_type;
static_assert(std::is_trivially_copyable_v<ContiguousType>,
"container element type must be trivially copyable");
return ReadRaw<ContiguousType>(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<s64>(m_handle->Tell());
}
u64 GetSize() const {
if (!IsOpen()) {
return 0;
}
return m_handle->Size();
}
private:
std::unique_ptr<IFile> 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<IFile> Open(std::string_view rel_path,
Common::FS::FileAccessMode mode) = 0;
virtual std::unique_ptr<IDirectory> OpenDir(std::string_view rel_path) = 0;
virtual bool IsReadOnly() const = 0;
virtual std::optional<std::filesystem::path> RootHostPath() const {
return std::nullopt;
}
};
} // namespace Core::FileSys
@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include "core/libraries/avplayer/avplayer_handle_streamer.h"
extern "C" {
#include <libavformat/avio.h>
#include <libavutil/error.h>
#include <libavutil/mem.h>
}
constexpr u32 AVPLAYER_AVIO_BUFFER_SIZE = 4096;
namespace Libraries::AvPlayer {
AvPlayerHandleStreamer::AvPlayerHandleStreamer(std::unique_ptr<Core::FileSys::IFile> 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<u8*>(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<AvPlayerHandleStreamer*>(opaque);
if (self->m_position >= self->m_file_size) {
return AVERROR_EOF;
}
if (self->m_position + static_cast<u64>(size) > self->m_file_size) {
size = static_cast<s32>(self->m_file_size - self->m_position);
}
self->m_handle->Seek(static_cast<s64>(self->m_position), Common::FS::SeekOrigin::SetOrigin);
const s64 got = self->m_handle->Read(buffer, static_cast<u64>(size));
if (got <= 0) {
return AVERROR_EOF;
}
self->m_position += static_cast<u64>(got);
return static_cast<s32>(got);
}
s64 AvPlayerHandleStreamer::Seek(void* opaque, s64 offset, int whence) {
auto* const self = reinterpret_cast<AvPlayerHandleStreamer*>(opaque);
if ((whence & AVSEEK_SIZE) != 0) {
return static_cast<s64>(self->m_file_size);
}
const int base = whence & ~AVSEEK_FORCE;
u64 new_pos = self->m_position;
if (base == SEEK_SET) {
new_pos = static_cast<u64>(std::max<s64>(0, offset));
} else if (base == SEEK_CUR) {
new_pos = static_cast<u64>(std::max<s64>(0, static_cast<s64>(self->m_position) + offset));
} else if (base == SEEK_END) {
new_pos = static_cast<u64>(std::max<s64>(0, static_cast<s64>(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<s64>(new_pos);
}
} // namespace Libraries::AvPlayer
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#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<Core::FileSys::IFile> 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<Core::FileSys::IFile> m_handle;
u64 m_position{0};
u64 m_file_size{0};
AVIOContext* m_avio_context{nullptr};
};
} // namespace Libraries::AvPlayer
@@ -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<AvPlayerFileStreamer>(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<Core::FileSys::MntPoints>::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<AvPlayerHandleStreamer>(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);
@@ -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 {
+2 -1
View File
@@ -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<unsigned char> file_bytes;
if (LoadGuestFileBytes(path_to_open, file_bytes) &&
if (LoadGuestPathBytes(guest_path, file_bytes) &&
file_bytes.size() <= std::numeric_limits<u32>::max()) {
auto& st = Internal::GetState(handle);
Internal::DestroyFreeTypeFace(st.ext_ft_face);
+20
View File
@@ -943,6 +943,26 @@ bool LoadGuestFileBytes(const std::filesystem::path& host_path,
return true;
}
bool LoadGuestPathBytes(const char* guest_path, std::vector<unsigned char>& out_bytes) {
if (!guest_path) {
return false;
}
if (guest_path[0] == '/') {
if (!g_mnt) {
g_mnt = Common::Singleton<Core::FileSys::MntPoints>::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) {
+1
View File
@@ -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<unsigned char>& out_bytes);
bool LoadGuestPathBytes(const char* guest_path, std::vector<unsigned char>& out_bytes);
FaceMetrics LoadFaceMetrics(FT_Face face);
void PopulateStateMetrics(FontState& st, const FaceMetrics& m);
float ComputeSysfontScaleFactor(FT_Face face, int units_per_em);
+20 -3
View File
@@ -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<std::vector<unsigned char>> shared_data;
std::shared_ptr<std::vector<u8>> 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<Core::FileSys::MntPoints>::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::vector<u8>>(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<const FT_Byte*>(data),
static_cast<FT_Long>(size), static_cast<FT_Long>(subFontIndex),
&face);
} else if (archive_font_bytes) {
ft_err = FT_New_Memory_Face(ctx->ft_lib,
reinterpret_cast<const FT_Byte*>(archive_font_bytes->data()),
static_cast<FT_Long>(archive_font_bytes->size()),
static_cast<FT_Long>(subFontIndex), &face);
if (ft_err == 0 && face) {
shared_data = archive_font_bytes;
data = archive_font_bytes->data();
size = static_cast<u32>(archive_font_bytes->size());
}
} else {
std::vector<std::string> candidates;
candidates.emplace_back(open_path);
+127 -115
View File
@@ -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 <map>
@@ -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<u8>(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<u8> 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<VAddr>(buf), std::min<u64>(nbytes, remaining));
if (file_buf.capacity() < nbytes) {
file_buf.reserve(nbytes);
}
u64 bytes = file.ReadRaw<u8>(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<u8>(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<Core::FileSys::MntPoints>::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<s64>(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<s64>(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<std::chrono::seconds>(mtimestamp.time_since_epoch())
.count();
}
} else {
sb->st_size =
fs::exists(path_name, ec) ? static_cast<s64>(fs::file_size(path_name, ec)) : 0;
sb->st_mtim.tv_sec =
std::chrono::duration_cast<std::chrono::seconds>(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<std::chrono::seconds>(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<s64>(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<OrbisKernelTimespec*>(&filestat.st_atim);
sb->st_mtim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_mtim);
sb->st_ctim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_ctim);
#elif defined(__APPLE__)
struct stat filestat = {};
stat(file->f.GetPath().c_str(), &filestat);
sb->st_atim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_atimespec);
sb->st_mtim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_mtimespec);
sb->st_ctim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_ctimespec);
#else
const auto ft = std::filesystem::last_write_time(file->f.GetPath());
const auto sctp = std::chrono::time_point_cast<std::chrono::nanoseconds>(
ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
const auto secs = std::chrono::time_point_cast<std::chrono::seconds>(sctp);
const auto nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(sctp - secs);
sb->st_mtim.tv_sec = static_cast<int64_t>(secs.time_since_epoch().count());
sb->st_mtim.tv_nsec = static_cast<int64_t>(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<Core::FileSys::HandleTable>::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<u8>(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<s32>(file->f.GetFileMapping());
if (auto* host = file->GetHostFile()) {
native_fd = static_cast<s32>(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<s32>(file->f.GetFileMapping());
if (auto* host = file->GetHostFile()) {
return static_cast<s32>(host->GetFileMapping());
}
return -1;
case Core::FileSys::FileType::Device:
return -1;
case Core::FileSys::FileType::Socket: {
+3 -8
View File
@@ -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<Core::FileSys::MntPoints>::Instance();
auto* linker = Common::Singleton<Core::Linker>::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;
}
+6 -3
View File
@@ -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<PlaygoFile>();
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::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<const u8>{*bytes})) {
LOG_WARNING(Lib_PlayGo, "Could not parse PlayGo file");
}
} else {
LOG_WARNING(Lib_PlayGo, "Could not open PlayGo file");
}
@@ -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 <fmt/chrono.h>
@@ -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) {
@@ -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<u8>(bytes->data(), bytes->size());
}
}
exists = true;
+6 -4
View File
@@ -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<u8>(bytes->data(), bytes->size());
}
} else {
IOFile file(icon_path, Common::FS::FileAccessMode::Create);
@@ -118,7 +118,6 @@ bool validateModuleId(s32 id) {
}
s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
auto* linker = Common::Singleton<Core::Linker>::Instance();
auto* game_info = Common::Singleton<Common::ElfInfo>::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) {
+13 -3
View File
@@ -1876,18 +1876,28 @@ int PS4_SYSV_ABI sceSystemServiceLoadExec(const char* path, const char* argv[])
LOG_DEBUG(Lib_SystemService, "called");
auto emu = Common::Singleton<Core::Emulator>::Instance();
auto mnt = Common::Singleton<Core::FileSys::MntPoints>::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<std::string> 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;
}
+22 -5
View File
@@ -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<std::string>& 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<Core::FileSys::MntPoints>::Instance();
const std::string as_guest = elf_name.generic_string();
std::unique_ptr<Core::FileSys::IFile> 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<Core::FileSys::HostFile>(
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<Module>(memory, elf_name, max_tls_index);
auto module = std::make_unique<Module>(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;
+14 -1
View File
@@ -4,6 +4,7 @@
#include <fmt/core.h>
#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<Core::FileSys::HostFile>(
file_name, Common::FS::FileAccessMode::Read, /*read_only=*/true);
if (!handle->IsOpen()) {
return;
}
Open(std::move(handle));
}
void Elf::Open(std::unique_ptr<Core::FileSys::IFile> 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;
+3 -1
View File
@@ -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<Core::FileSys::IFile> 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<self_segment_header> m_self_segments;
+44 -8
View File
@@ -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<u32>(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<VAddr>(addr), sz, off, p, map_handle);
};
map_ctx.map_anonymous = [&](u8* addr, u64 sz) {
constexpr auto kAnonMarker = static_cast<u64>(-1);
constexpr auto kNoFd = static_cast<uintptr_t>(-1);
const auto rw_prot = std::bit_cast<u32>(MemoryProt::CpuRead | MemoryProt::CpuWrite);
impl.MapFile(reinterpret_cast<VAddr>(addr), sz, kAnonMarker, rw_prot, kNoFd);
};
map_ctx.protect = [&](u8* addr, u64 sz, u32 p) {
const auto mp = std::bit_cast<MemoryProt>(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<VAddr>(addr), sz, perms);
};
file->handle->Map(reinterpret_cast<u8*>(mapped_addr), size, phys_addr, std::bit_cast<u32>(prot),
map_ctx);
*out_addr = std::bit_cast<void*>(mapped_addr);
return ORBIS_OK;
+12 -1
View File
@@ -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<Core::FileSys::IFile> 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) {
+2
View File
@@ -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<Core::FileSys::IFile> handle, u32& max_tls_index);
~Module();
VAddr GetBaseAddress() const noexcept {
+249 -126
View File
@@ -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<Core::FileSys::MntPoints>::Instance();
std::unique_ptr<Core::FileSys::IFile> 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<s32, std::string> ExtractTrophies(const std::filesystem::path& npbind_path,
const std::filesystem::path& trophy_dir) {
std::map<s32, std::string> ExtractTrophies(std::string_view npbind_guest,
std::string_view trophy_dir_guest) {
std::map<s32, std::string> trophy_index_map{};
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
NPBindFile npbind;
if (!npbind.Load(npbind_path.string())) {
const auto npbind_bytes = mnt->ReadFile(npbind_guest);
if (!npbind_bytes || !npbind.Load(std::span<const u8>{*npbind_bytes})) {
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
return trophy_index_map;
}
@@ -127,64 +141,103 @@ std::map<s32, std::string> 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<s32>(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<u8>(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<std::string> 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<Core::FileSys::MntPoints>::Instance();
@@ -225,55 +320,59 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> 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<PSF>::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<u8> psf_buf(psf_handle->Size());
if (psf_handle->Read(psf_buf.data(), psf_buf.size()) == static_cast<s64>(psf_buf.size())) {
auto* param_sfo = Common::Singleton<PSF>::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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string>& guest_args) {
std::vector<std::string> args;
auto mnt = Common::Singleton<Core::FileSys::MntPoints>::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<Core::FileSys::MntPoints>::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");
+3 -5
View File
@@ -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;
+11 -1
View File
@@ -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()) {
+4 -25
View File
@@ -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<const u8> 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<u8> buf(fileSize);
const size_t bytesRead = file.ReadRaw<u8>(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<u8>(png_data.begin(), png_data.end()));
}
void WindowSDL::WaitEvent() {
+2 -1
View File
@@ -3,6 +3,7 @@
#pragma once
#include <span>
#include <string>
#include "common/types.h"
@@ -71,7 +72,7 @@ public:
return window_info;
}
void SetIcon(const std::filesystem::path& path);
void SetIcon(std::span<const u8> png_data);
void WaitEvent();
void InitTimers();
@@ -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()) {