mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-10-11 21:49:59 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Vendored
+25
@@ -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()
|
||||
|
||||
+1
Submodule externals/zarchive added at 965b66c8d6
+1
Submodule externals/zstd added at 5c7b7bad26
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user