Support mountable-content DLC (addcont with real content) (#1581)

Support mountable-content DLC (addcont) and fix PlayGo chunk count query
This commit is contained in:
Felipe
2026-10-16 23:44:28 -03:00
committed by GitHub
parent e93b4b0378
commit b95ea4bb5f
5 changed files with 64 additions and 12 deletions
+8 -2
View File
@@ -338,14 +338,20 @@ static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_pat
std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) { std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) {
Common::LockGuard lock(m_mutex); Common::LockGuard lock(m_mutex);
// Guests join mount points and names with extra slashes (e.g. "/addcont0//file").
std::string normalized = Common::FixFilenameSlash(mounted_name);
normalized.erase(std::unique(normalized.begin(), normalized.end(),
[](char a, char b) { return a == '/' && b == '/'; }),
normalized.end());
// Match the entire guest path so a mount root works with or without a trailing slash. // Match the entire guest path so a mount root works with or without a trailing slash.
const auto mounted_path = Common::FixDirectorySlash(mounted_name); const auto mounted_path = Common::FixDirectorySlash(normalized);
const auto it = std::find_if( const auto it = std::find_if(
m_mount_pairs.begin(), m_mount_pairs.end(), m_mount_pairs.begin(), m_mount_pairs.end(),
[&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); }); [&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); });
if (it != m_mount_pairs.end()) { if (it != m_mount_pairs.end()) {
const auto& p = *it; const auto& p = *it;
auto rel_path = Common::RemoveFirst(Common::FixFilenameSlash(mounted_name), p.point.size()); auto rel_path = Common::RemoveFirst(normalized, p.point.size());
while (rel_path.starts_with('/')) { while (rel_path.starts_with('/')) {
rel_path = Common::RemoveFirst(rel_path, 1); rel_path = Common::RemoveFirst(rel_path, 1);
} }
+22
View File
@@ -25,6 +25,9 @@
#include <string> #include <string>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <atomic>
#include <cinttypes>
#include <iterator>
#include <variant> #include <variant>
#include <vector> #include <vector>
@@ -176,6 +179,14 @@ static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::Fi
return OK; return OK;
} }
// Mounts can appear after a failed lookup (e.g. additional content), so forget negative results.
void InvalidateFailedResolves() {
std::scoped_lock lock(g_mutex);
for (auto it = g_resolved_paths.begin(); it != g_resolved_paths.end();) {
it = it->second.result != OK ? g_resolved_paths.erase(it) : std::next(it);
}
}
static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) { static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) {
if (guest_path == nullptr || guest_path[0] == '\0') { if (guest_path == nullptr || guest_path[0] == '\0') {
return LibKernel::KERNEL_ERROR_EINVAL; return LibKernel::KERNEL_ERROR_EINVAL;
@@ -225,6 +236,10 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
log_missing = true; log_missing = true;
} }
} }
if (!log_missing && info.result == OK && path.starts_with("/addcont")) {
LOGF("\tAPR resolve: %s -> %s (%" PRIu64 " bytes)\n", guest_path,
Common::PathToString(info.host_path).c_str(), info.file_size);
}
if (log_missing) { if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str()); LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
} }
@@ -1181,6 +1196,13 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
uint64_t bytes_read = 0; uint64_t bytes_read = 0;
auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination, auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination,
command.size, &bytes_read); command.size, &bytes_read);
static std::atomic<uint32_t> s_logged_dlc_reads {0};
if (host_path.filename() == "re_dlc_000.pak" && s_logged_dlc_reads.fetch_add(1) < 32) {
LOGF("\tAPR read dlc: id=0x%08" PRIx32 ", offset=0x%016" PRIx64 ", size=0x%016" PRIx64
", read=0x%016" PRIx64 ", result=0x%08" PRIx32 "\n",
command.file_id, command.file_offset, command.size, bytes_read,
static_cast<uint32_t>(result));
}
if (result != OK) { if (result != OK) {
LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n", LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n",
command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str()); command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str());
+20 -5
View File
@@ -22,13 +22,16 @@
namespace Libs { namespace Libs {
namespace AprShared {
void InvalidateFailedResolves();
}
LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1); LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1);
namespace AppContent { namespace AppContent {
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3; constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */ constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */ constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */ constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */
constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */ constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */
@@ -207,7 +210,10 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
for (const auto& m: g_addcont_mounts) { for (const auto& m: g_addcont_mounts) {
if (m.label == entry->entitlement_label) { if (m.label == entry->entitlement_label) {
return APP_CONTENT_ERROR_BUSY; const auto guest_point = m.point.substr(0, m.point.size() - 1);
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
LOGF("\t already mounted %s\n", guest_point.c_str());
return OK;
} }
} }
@@ -232,7 +238,15 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
LOGF("\t mounted %s -> %s\n", mount.point.c_str(), LOGF("\t mounted %s -> %s\n", mount.point.c_str(),
Common::PathToString(entry->mount_dir).c_str()); Common::PathToString(entry->mount_dir).c_str());
std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1); AprShared::InvalidateFailedResolves();
// Titles scan /download0/pdlc for downloaded content; it must exist even when empty.
if (const auto pdlc = LibKernel::FileSystem::GetRealFilename("/download0/pdlc"); !pdlc.empty()) {
std::filesystem::create_directories(pdlc, ec);
}
const auto guest_point = mount.point.substr(0, mount.point.size() - 1);
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
g_addcont_mounts.push_back(std::move(mount)); g_addcont_mounts.push_back(std::move(mount));
return OK; return OK;
@@ -251,8 +265,8 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
return APP_CONTENT_ERROR_NOT_MOUNTED; return APP_CONTENT_ERROR_NOT_MOUNTED;
} }
const std::string point(mount_point->data, const std::string point = Common::FixDirectorySlash(
strnlen(mount_point->data, sizeof(mount_point->data))); std::string_view(mount_point->data, strnlen(mount_point->data, sizeof(mount_point->data))));
std::lock_guard lock(g_addcont_mutex); std::lock_guard lock(g_addcont_mutex);
@@ -263,6 +277,7 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
} }
LibKernel::FileSystem::Umount(point); LibKernel::FileSystem::Umount(point);
g_addcont_mounts.erase(it); g_addcont_mounts.erase(it);
AprShared::InvalidateFailedResolves();
return OK; return OK;
} }
+8 -2
View File
@@ -221,11 +221,17 @@ int KYTY_SYSV_ABI PlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list,
if (out_entries == nullptr) { if (out_entries == nullptr) {
return PLAYGO_ERROR_BAD_POINTER; return PLAYGO_ERROR_BAD_POINTER;
} }
if (number_of_entries != 0 && out_chunk_id_list == nullptr) { if (out_chunk_id_list != nullptr && number_of_entries == 0) {
return PLAYGO_ERROR_BAD_POINTER; return PLAYGO_ERROR_BAD_SIZE;
} }
ensure_chunks_loaded(); ensure_chunks_loaded();
// A null output buffer asks for the total chunk count.
if (out_chunk_id_list == nullptr) {
*out_entries = g_chunks_num;
return OK;
}
const uint32_t entries = std::min(number_of_entries, g_chunks_num); const uint32_t entries = std::min(number_of_entries, g_chunks_num);
for (uint32_t i = 0; i < entries; i++) { for (uint32_t i = 0; i < entries; i++) {
out_chunk_id_list[i] = static_cast<uint16_t>(i); out_chunk_id_list[i] = static_cast<uint16_t>(i);
+6 -3
View File
@@ -413,7 +413,9 @@ void TestAddContent(const std::filesystem::path& root) {
Check(mount(0, nullptr, &point) == static_cast<int>(0x80d90002u) && Check(mount(0, nullptr, &point) == static_cast<int>(0x80d90002u) &&
list(0, nullptr, 0, nullptr) == static_cast<int>(0x817d0002u), list(0, nullptr, 0, nullptr) == static_cast<int>(0x817d0002u),
"null add-on parameters return parameter errors"); "null add-on parameters return parameter errors");
Check(mount(0, &info[3].label, &point) == OK, "mount owned add-on"); Check(mount(0, &info[3].label, &point) == OK &&
!std::string_view(point.data).ends_with('/'),
"mount owned add-on without trailing slash");
const std::string mounted_file = std::string(point.data) + "/payload.txt"; const std::string mounted_file = std::string(point.data) + "/payload.txt";
const int fd = FileSystem::KernelOpen(mounted_file.c_str(), 0, 0); const int fd = FileSystem::KernelOpen(mounted_file.c_str(), 0, 0);
std::array<char, 11> payload {}; std::array<char, 11> payload {};
@@ -422,8 +424,9 @@ void TestAddContent(const std::filesystem::path& root) {
FileSystem::KernelClose(fd) == OK, FileSystem::KernelClose(fd) == OK,
"guest reads files from the selected title's add-on"); "guest reads files from the selected title's add-on");
MountPoint repeated {}; MountPoint repeated {};
Check(mount(0, &info[3].label, &repeated) == static_cast<int>(0x80d90003u), Check(mount(0, &info[3].label, &repeated) == OK &&
"duplicate mount returns BUSY"); std::strcmp(repeated.data, point.data) == 0,
"duplicate mount returns the same mount point");
Check(unmount(&point) == OK && FileSystem::GetRealFilename(mounted_file).empty() && Check(unmount(&point) == OK && FileSystem::GetRealFilename(mounted_file).empty() &&
unmount(&point) == static_cast<int>(0x80d90004u), unmount(&point) == static_cast<int>(0x80d90004u),
"unmount removes mapping and repeated unmount reports NOT_MOUNTED"); "unmount removes mapping and repeated unmount reports NOT_MOUNTED");