aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/CMakeLists.txt7
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_32.cpp4
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_64.cpp4
-rw-r--r--src/core/core.cpp21
-rw-r--r--src/core/device_memory_manager.h7
-rw-r--r--src/core/device_memory_manager.inc44
-rw-r--r--src/core/file_sys/fsmitm_romfsbuild.cpp27
-rw-r--r--src/core/file_sys/nup_container.cpp676
-rw-r--r--src/core/file_sys/nup_container.h177
-rw-r--r--src/core/file_sys/nup_writer.cpp1459
-rw-r--r--src/core/file_sys/nup_writer.h60
-rw-r--r--src/core/file_sys/system_archive/system_archive.cpp19
-rw-r--r--src/core/file_sys/vfs/vfs_real.cpp37
-rw-r--r--src/core/file_sys/vfs/vfs_real.h8
-rw-r--r--src/core/hle/kernel/board/nintendo/nx/k_system_control.h5
-rw-r--r--src/core/hle/kernel/k_memory_manager.cpp24
-rw-r--r--src/core/hle/kernel/k_memory_manager.h6
-rw-r--r--src/core/hle/kernel/k_shared_memory.cpp5
-rw-r--r--src/core/hle/kernel/svc.cpp17
-rw-r--r--src/core/hle/service/acc/acc.cpp4
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard.cpp8
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard_types.h60
-rw-r--r--src/core/hle/service/am/service/application_functions.cpp70
-rw-r--r--src/core/hle/service/am/service/application_functions.h4
-rw-r--r--src/core/hle/service/am/service/application_proxy_service.cpp1
-rw-r--r--src/core/hle/service/am/service/audio_controller.cpp2
-rw-r--r--src/core/hle/service/am/service/common_state_getter.cpp50
-rw-r--r--src/core/hle/service/am/service/display_controller.cpp37
-rw-r--r--src/core/hle/service/am/service/graphics_scale_controller.h6
-rw-r--r--src/core/hle/service/am/service/library_applet_accessor.cpp13
-rw-r--r--src/core/hle/service/am/service/library_applet_creator.cpp4
-rw-r--r--src/core/hle/service/am/service/self_controller.cpp41
-rw-r--r--src/core/hle/service/am/service/window_controller.cpp2
-rw-r--r--src/core/hle/service/aoc/addon_content_manager.cpp14
-rw-r--r--src/core/hle/service/audio/audio.cpp3
-rw-r--r--src/core/hle/service/audio/audio_device.cpp31
-rw-r--r--src/core/hle/service/audio/audio_device.h4
-rw-r--r--src/core/hle/service/audio/audio_renderer_manager.cpp2
-rw-r--r--src/core/hle/service/audio/nx2_audio_device_manager.h539
-rw-r--r--src/core/hle/service/caps/caps_su.cpp4
-rw-r--r--src/core/hle/service/caps/caps_u.cpp24
-rw-r--r--src/core/hle/service/hid/hid_server.cpp121
-rw-r--r--src/core/hle/service/hid/hid_server.h6
-rw-r--r--src/core/hle/service/kernel_helpers.cpp19
-rw-r--r--src/core/hle/service/kernel_helpers.h4
-rw-r--r--src/core/hle/service/lm/lm.cpp5
-rw-r--r--src/core/hle/service/mii/mii.cpp6
-rw-r--r--src/core/hle/service/ns/platform_service_manager.cpp65
-rw-r--r--src/core/hle/service/nvdrv/core/container.cpp2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp57
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp27
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp25
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h51
-rw-r--r--src/core/hle/service/nvdrv/nvdrv.cpp1
-rw-r--r--src/core/hle/service/nvdrv/nvdrv_interface.cpp6
-rw-r--r--src/core/hle/service/nvnflinger/pixel_format.h1
-rw-r--r--src/core/hle/service/nx2_gsv.h854
-rw-r--r--src/core/hle/service/ro/ro.cpp23
-rw-r--r--src/core/hle/service/service.cpp109
-rw-r--r--src/core/hle/service/service.h24
-rw-r--r--src/core/hle/service/set/settings_server.cpp7
-rw-r--r--src/core/hle/service/sockets/bsd.cpp84
-rw-r--r--src/core/hle/service/sockets/nsd.cpp4
-rw-r--r--src/core/hle/service/ssl/ssl.cpp93
-rw-r--r--src/core/launch_params.cpp8
-rw-r--r--src/core/launch_params.h2
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp53
-rw-r--r--src/core/loader/deconstructed_rom_directory.h6
-rw-r--r--src/core/loader/loader.cpp35
-rw-r--r--src/core/loader/loader.h3
-rw-r--r--src/core/loader/nup.cpp280
-rw-r--r--src/core/loader/nup.h65
73 files changed, 5152 insertions, 426 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 0075838d49..eeb3202d12 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -132,6 +132,10 @@ add_library(core STATIC
file_sys/program_metadata.h
file_sys/registered_cache.cpp
file_sys/registered_cache.h
+ file_sys/nup_container.cpp
+ file_sys/nup_container.h
+ file_sys/nup_writer.cpp
+ file_sys/nup_writer.h
file_sys/romfs.cpp
file_sys/romfs.h
file_sys/romfs_factory.cpp
@@ -527,6 +531,7 @@ add_library(core STATIC
hle/service/audio/audio_out.h
hle/service/audio/audio_renderer_manager.cpp
hle/service/audio/audio_renderer_manager.h
+ hle/service/audio/nx2_audio_device_manager.h
hle/service/audio/nx2_audio_user_client.h
hle/service/audio/audio_renderer.cpp
hle/service/audio/audio_renderer.h
@@ -1155,6 +1160,8 @@ add_library(core STATIC
loader/nso.h
loader/nsp.cpp
loader/nsp.h
+ loader/nup.cpp
+ loader/nup.h
loader/xci.cpp
loader/xci.h
memory.cpp
diff --git a/src/core/arm/dynarmic/arm_dynarmic_32.cpp b/src/core/arm/dynarmic/arm_dynarmic_32.cpp
index a59aa3cac3..3a875493c6 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_32.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_32.cpp
@@ -416,6 +416,10 @@ void ArmDynarmic32::ClearInstructionCache() {
}
void ArmDynarmic32::InvalidateCacheRange(u64 addr, std::size_t size) {
+ // Drop the cached code page too: blocks recompiled after the invalidation would
+ // otherwise still decode the old instructions (e.g. a gdbstub breakpoint written
+ // into or removed from that page).
+ m_cb->last_code_addr = u64(-1);
m_jit->InvalidateCacheRange(static_cast<u32>(addr), size);
}
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
index e65e38dd9e..c5be4d2520 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
@@ -481,6 +481,10 @@ void ArmDynarmic64::ClearInstructionCache() {
}
void ArmDynarmic64::InvalidateCacheRange(u64 addr, std::size_t size) {
+ // Drop the cached code page too: blocks recompiled after the invalidation would
+ // otherwise still decode the old instructions (e.g. a gdbstub breakpoint written
+ // into or removed from that page).
+ m_cb->last_code_addr = u64(-1);
m_jit->InvalidateCacheRange(addr, size);
}
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 41871df870..3842ee25a8 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -25,6 +25,7 @@
#include "core/debugger/debugger.h"
#include "core/device_memory.h"
#include "core/file_sys/fs_filesystem.h"
+#include "core/file_sys/nup_container.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/program_metadata.h"
#include "core/file_sys/registered_cache.h"
@@ -116,9 +117,27 @@ namespace {
// Switch 2 titles boot as a loose ExeFS, with main.npdm next to main.
bool IsSwitch2Program(const FileSys::VirtualFile& file) {
+ FileSys::ProgramMetadata metadata;
+ if (FileSys::NupContainer::IsNupFile(file)) {
+ FileSys::NupContainer nup{file};
+ if (nup.GetStatus() != Loader::ResultStatus::Success) {
+ return false;
+ }
+ std::optional<std::size_t> program;
+ if (const auto base = nup.FindBasePackage();
+ base && nup.GetExeFSDir(*base) != nullptr) {
+ program = base;
+ } else if (const auto update = nup.FindBestUpdatePackage();
+ update && nup.GetExeFSDir(*update) != nullptr) {
+ program = update;
+ }
+ const auto exefs = program ? nup.GetExeFSDir(*program) : nullptr;
+ const auto npdm = exefs != nullptr ? exefs->GetFile("main.npdm") : nullptr;
+ return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success &&
+ metadata.IsSwitch2();
+ }
const auto dir = file != nullptr ? file->GetContainingDirectory() : nullptr;
const auto npdm = dir != nullptr ? dir->GetFile("main.npdm") : nullptr;
- FileSys::ProgramMetadata metadata;
return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success &&
metadata.IsSwitch2();
}
diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h
index 27ff342213..d275bdbf2d 100644
--- a/src/core/device_memory_manager.h
+++ b/src/core/device_memory_manager.h
@@ -18,6 +18,7 @@
#include "common/common_types.h"
#include "common/range_mutex.h"
#include "common/scratch_buffer.h"
+#include "common/thread_worker.h"
#include "common/sparse_large_vector.h"
namespace Core {
@@ -53,6 +54,10 @@ public:
void BindInterface(DeviceInterface* device_inter);
+ /// Marks the memory behind [address, address + size) cold (Common::HostMemory::HintCold),
+ /// from a low-priority thread: data the host GPU has its own copy of, e.g. uploaded textures.
+ void HintCold(DAddr address, size_t size);
+
DAddr Allocate(size_t size);
void AllocateFixed(DAddr start, size_t size);
void Free(DAddr start, size_t size);
@@ -197,7 +202,9 @@ private:
std::unique_ptr<DeviceMemoryManagerAllocator<Traits>> impl;
const uintptr_t physical_base;
+ const DeviceMemory* device_memory_ptr;
DeviceInterface* device_inter;
+ std::unique_ptr<Common::ThreadWorker> cold_hint_worker;
struct TrackedEntry {
VAddr cpu_backing_address;
diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc
index 83faa1b3f3..3523e3cf1c 100644
--- a/src/core/device_memory_manager.inc
+++ b/src/core/device_memory_manager.inc
@@ -171,6 +171,7 @@ struct DeviceMemoryManagerAllocator {
template <typename Traits>
DeviceMemoryManager<Traits>::DeviceMemoryManager(const DeviceMemory& device_memory_)
: physical_base{uintptr_t(device_memory_.buffer.BackingBasePointer())}
+ , device_memory_ptr{&device_memory_}
, device_inter{nullptr}
, compressed_device_addr(1ULL << ((Settings::values.memory_layout_mode.GetValue() == Settings::MemoryLayout::Memory_4Gb ? physical_min_bits : physical_max_bits) - Memory::YUZU_PAGEBITS))
, tracked_entries(device_as_size >> Memory::YUZU_PAGEBITS)
@@ -183,6 +184,49 @@ template <typename Traits>
DeviceMemoryManager<Traits>::~DeviceMemoryManager() = default;
template <typename Traits>
+void DeviceMemoryManager<Traits>::HintCold(DAddr address, size_t size) {
+ struct Run {
+ u64 physical_offset;
+ VAddr virtual_address; ///< 0: no CPU mapping known
+ u64 size;
+ };
+ std::vector<Run> runs;
+ const size_t page_end = Common::DivCeil(address + size, Memory::YUZU_PAGESIZE);
+ for (size_t page = address >> Memory::YUZU_PAGEBITS; page < page_end; ++page) {
+ const u32 compressed = tracked_entries[page].compressed_physical_ptr;
+ if (compressed == 0) {
+ continue;
+ }
+ const u64 physical = u64{compressed - 1} << Memory::YUZU_PAGEBITS;
+ const VAddr vaddr = ExtractCPUBacking(page).second;
+ if (!runs.empty()) {
+ Run& last = runs.back();
+ const bool contiguous_view =
+ last.virtual_address == 0 ? vaddr == 0 : last.virtual_address + last.size == vaddr;
+ if (last.physical_offset + last.size == physical && contiguous_view) {
+ last.size += Memory::YUZU_PAGESIZE;
+ continue;
+ }
+ }
+ runs.push_back({physical, vaddr, Memory::YUZU_PAGESIZE});
+ }
+ if (runs.empty()) {
+ return;
+ }
+ if (!cold_hint_worker) {
+ cold_hint_worker = std::make_unique<Common::ThreadWorker>(1, "DeviceMemory:ColdHint");
+ }
+ cold_hint_worker->QueueWork([this, runs = std::move(runs)] {
+ for (const Run& run : runs) {
+ device_memory_ptr->buffer.HintCold(
+ run.physical_offset, run.size,
+ run.virtual_address != 0 ? std::optional<size_t>{run.virtual_address}
+ : std::nullopt);
+ }
+ });
+}
+
+template <typename Traits>
void DeviceMemoryManager<Traits>::BindInterface(DeviceInterface* device_inter_) {
device_inter = device_inter_;
}
diff --git a/src/core/file_sys/fsmitm_romfsbuild.cpp b/src/core/file_sys/fsmitm_romfsbuild.cpp
index 8807bbd0f7..d34e24f177 100644
--- a/src/core/file_sys/fsmitm_romfsbuild.cpp
+++ b/src/core/file_sys/fsmitm_romfsbuild.cpp
@@ -55,15 +55,18 @@ static_assert(sizeof(RomFSFileEntry) == 0x20, "RomFSFileEntry has incorrect size
struct RomFSBuildFileContext;
+// The context's directories and files own the nodes; the links between them don't, or every
+// parent and child would own each other and no build would ever be freed (title 1's loose RomFS:
+// 206k files, ~50 MiB per build).
struct RomFSBuildDirectoryContext {
std::string path;
u32 cur_path_ofs = 0;
u32 path_len = 0;
u32 entry_offset = 0;
- std::shared_ptr<RomFSBuildDirectoryContext> parent;
- std::shared_ptr<RomFSBuildDirectoryContext> child;
- std::shared_ptr<RomFSBuildDirectoryContext> sibling;
- std::shared_ptr<RomFSBuildFileContext> file;
+ RomFSBuildDirectoryContext* parent = nullptr;
+ RomFSBuildDirectoryContext* child = nullptr;
+ RomFSBuildDirectoryContext* sibling = nullptr;
+ RomFSBuildFileContext* file = nullptr;
};
struct RomFSBuildFileContext {
@@ -73,8 +76,8 @@ struct RomFSBuildFileContext {
u32 entry_offset = 0;
u64 offset = 0;
u64 size = 0;
- std::shared_ptr<RomFSBuildDirectoryContext> parent;
- std::shared_ptr<RomFSBuildFileContext> sibling;
+ RomFSBuildDirectoryContext* parent = nullptr;
+ RomFSBuildFileContext* sibling = nullptr;
VirtualFile source;
};
@@ -168,7 +171,7 @@ bool RomFSBuildContext::AddDirectory(std::shared_ptr<RomFSBuildDirectoryContext>
num_dirs++;
dir_table_size +=
sizeof(RomFSDirectoryEntry) + Common::AlignUp(dir_ctx->path_len - dir_ctx->cur_path_ofs, 4);
- dir_ctx->parent = std::move(parent_dir_ctx);
+ dir_ctx->parent = parent_dir_ctx.get();
directories.emplace_back(std::move(dir_ctx));
return true;
@@ -180,7 +183,7 @@ bool RomFSBuildContext::AddFile(std::shared_ptr<RomFSBuildDirectoryContext> pare
num_files++;
file_table_size +=
sizeof(RomFSFileEntry) + Common::AlignUp(file_ctx->path_len - file_ctx->cur_path_ofs, 4);
- file_ctx->parent = std::move(parent_dir_ctx);
+ file_ctx->parent = parent_dir_ctx.get();
files.emplace_back(std::move(file_ctx));
return true;
@@ -245,11 +248,11 @@ std::vector<std::pair<u64, VirtualFile>> RomFSBuildContext::Build() {
Common::AlignUp(cur_file->path_len - cur_file->cur_path_ofs, 4));
prev_file = cur_file;
}
- // Assign deferred parent/sibling ownership.
+ // Assign deferred parent/sibling links.
for (auto it = files.rbegin(); it != files.rend(); ++it) {
auto& cur_file = *it;
cur_file->sibling = cur_file->parent->file;
- cur_file->parent->file = cur_file;
+ cur_file->parent->file = cur_file.get();
}
// Determine directory offsets.
@@ -260,11 +263,11 @@ std::vector<std::pair<u64, VirtualFile>> RomFSBuildContext::Build() {
static_cast<u32>(sizeof(RomFSDirectoryEntry) +
Common::AlignUp(cur_dir->path_len - cur_dir->cur_path_ofs, 4));
}
- // Assign deferred parent/sibling ownership.
+ // Assign deferred parent/sibling links.
for (auto it = directories.rbegin(); (*it) != root; ++it) {
auto& cur_dir = *it;
cur_dir->sibling = cur_dir->parent->child;
- cur_dir->parent->child = cur_dir;
+ cur_dir->parent->child = cur_dir.get();
}
// Create output map.
diff --git a/src/core/file_sys/nup_container.cpp b/src/core/file_sys/nup_container.cpp
new file mode 100644
index 0000000000..0e485b44ec
--- /dev/null
+++ b/src/core/file_sys/nup_container.cpp
@@ -0,0 +1,676 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/file_sys/nup_container.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstring>
+#include <list>
+#include <map>
+#include <unordered_map>
+
+#include <openssl/evp.h>
+#include <zstd.h>
+
+#include "common/logging.h"
+#include "common/swap.h"
+#include "core/file_sys/vfs/vfs_offset.h"
+
+namespace FileSys {
+namespace {
+
+constexpr u32 NoDir = 0xFFFFFFFF;
+constexpr std::size_t SuperblockSize = 0x80;
+constexpr std::size_t PackageRecordSize = 0x40;
+constexpr std::size_t DirRecordSize = 0x18;
+constexpr std::size_t FileRecordSize = 0x50;
+constexpr std::size_t TocHeaderSize = 0x30;
+constexpr std::size_t TlvHeaderSize = 0x8;
+constexpr u32 TocMagic = 0x01434F54; // "TOC\x01" little-endian
+constexpr u8 CodecRaw = 0;
+constexpr u8 CodecZstd = 1;
+constexpr u8 PackageFlagStandaloneUpdate = 0x01;
+
+#pragma pack(push, 1)
+struct Superblock {
+ std::array<u8, 4> magic;
+ u16_le header_size;
+ u16_le header_version;
+ u64_le toc_offset;
+ u64_le toc_size;
+ std::array<u8, 0x20> toc_sha256;
+ u64_le blob_offset;
+ u64_le file_size;
+ u32_le package_count;
+ u32_le flags;
+ std::array<u8, 0x10> uuid;
+ std::array<u8, 0x20> reserved;
+};
+static_assert(sizeof(Superblock) == SuperblockSize);
+
+struct TocHeader {
+ u32_le magic;
+ u32_le package_count;
+ u64_le dir_count;
+ u64_le file_count;
+ u64_le pool_offset;
+ u64_le pool_size;
+ u64_le reserved;
+};
+static_assert(sizeof(TocHeader) == TocHeaderSize);
+
+struct PackageRecord {
+ u64_le title_id; // 0x00
+ u32_le root_dir; // 0x08
+ u32_le exefs_dir; // 0x0C
+ u32_le romfs_dir; // 0x10
+ u32_le version; // 0x14
+ u8 kind; // 0x18
+ u8 flags; // 0x19
+ u16_le reserved; // 0x1A
+ u32_le reserved2; // 0x1C
+ u64_le meta_offset; // 0x20
+ u32_le meta_size; // 0x28
+ std::array<u8, 20> reserved3; // 0x2C
+};
+static_assert(sizeof(PackageRecord) == PackageRecordSize);
+static_assert(offsetof(PackageRecord, title_id) == 0x00);
+static_assert(offsetof(PackageRecord, root_dir) == 0x08);
+static_assert(offsetof(PackageRecord, exefs_dir) == 0x0C);
+static_assert(offsetof(PackageRecord, romfs_dir) == 0x10);
+static_assert(offsetof(PackageRecord, version) == 0x14);
+static_assert(offsetof(PackageRecord, kind) == 0x18);
+static_assert(offsetof(PackageRecord, flags) == 0x19);
+static_assert(offsetof(PackageRecord, reserved) == 0x1A);
+static_assert(offsetof(PackageRecord, reserved2) == 0x1C);
+static_assert(offsetof(PackageRecord, meta_offset) == 0x20);
+static_assert(offsetof(PackageRecord, meta_size) == 0x28);
+
+struct DirRecord {
+ u32_le name_offset;
+ u32_le name_length;
+ u32_le first_child;
+ u32_le child_count;
+ u64_le reserved;
+};
+static_assert(sizeof(DirRecord) == DirRecordSize);
+
+struct FileRecord {
+ u32_le name_offset; // 0x00
+ u32_le name_length; // 0x04
+ u64_le size; // 0x08
+ u64_le extent_offset; // 0x10
+ u64_le extent_size; // 0x18
+ std::array<u8, 0x20> sha256; // 0x20
+ u8 codec; // 0x40
+ u8 codec_flags; // 0x41
+ u16_le reserved2; // 0x42
+ std::array<u8, 12> reserved3; // 0x44
+};
+static_assert(sizeof(FileRecord) == FileRecordSize);
+static_assert(offsetof(FileRecord, name_offset) == 0x00);
+static_assert(offsetof(FileRecord, name_length) == 0x04);
+static_assert(offsetof(FileRecord, size) == 0x08);
+static_assert(offsetof(FileRecord, extent_offset) == 0x10);
+static_assert(offsetof(FileRecord, extent_size) == 0x18);
+static_assert(offsetof(FileRecord, sha256) == 0x20);
+static_assert(offsetof(FileRecord, codec) == 0x40);
+static_assert(offsetof(FileRecord, codec_flags) == 0x41);
+static_assert(offsetof(FileRecord, reserved2) == 0x42);
+
+struct TlvHeader {
+ u16_le type;
+ u16_le reserved;
+ u32_le size;
+};
+static_assert(sizeof(TlvHeader) == TlvHeaderSize);
+#pragma pack(pop)
+
+enum MetaType : u16 {
+ MetaNpdm = 0x0001,
+ MetaIcon = 0x0003,
+ MetaDisplayVersion = 0x0004,
+ MetaTitleName = 0x0005,
+ MetaLocalizedTitles = 0x0009,
+ MetaLocalizedIcons = 0x000A,
+};
+
+bool Sha256(const u8* data, std::size_t size, std::array<u8, 0x20>& out) {
+ unsigned int length = 0;
+ EVP_Digest(data, size, out.data(), &length, EVP_sha256(), nullptr);
+ return length == out.size();
+}
+
+/// A member file whose extent is zstd-compressed. Decompresses on first read
+/// and caches the result (LRU-evicted) unless the file exceeds the cache cap.
+class NupVfsFile final : public VfsFile {
+public:
+ NupVfsFile(VirtualFile backing_, u64 extent_offset_, u64 extent_size_, u64 uncompressed_size_,
+ std::string name_, std::shared_ptr<NupDecompressCache> cache_,
+ std::size_t cache_index_)
+ : backing(std::move(backing_)), extent_offset(extent_offset_), extent_size(extent_size_),
+ uncompressed_size(uncompressed_size_), name(std::move(name_)), cache(std::move(cache_)),
+ cache_index(cache_index_), cache_enabled(uncompressed_size <= NupDecompressCache::DecompressCacheCap) {}
+
+ std::string GetName() const override {
+ return name;
+ }
+
+ std::size_t GetSize() const override {
+ return static_cast<std::size_t>(uncompressed_size);
+ }
+
+ bool Resize(std::size_t) override {
+ return false;
+ }
+
+ VirtualDir GetContainingDirectory() const override {
+ return nullptr;
+ }
+
+ bool IsWritable() const override {
+ return false;
+ }
+
+ bool IsReadable() const override {
+ return true;
+ }
+
+ std::size_t Read(u8* data, std::size_t length, std::size_t offset) const override {
+ if (offset >= uncompressed_size) {
+ return 0;
+ }
+ length = std::min(length, static_cast<std::size_t>(uncompressed_size) - offset);
+
+ {
+ std::scoped_lock lock{cache->mutex};
+ if (const auto* cached = cache->Find(cache_index)) {
+ std::memcpy(data, cached->data() + offset, length);
+ return length;
+ }
+ }
+
+ // Miss: decompress without holding the lock, then publish and copy.
+ std::vector<u8> scratch = Decompress();
+ if (scratch.empty()) {
+ return 0;
+ }
+ {
+ std::scoped_lock lock{cache->mutex};
+ if (cache_enabled) {
+ cache->Insert(cache_index, scratch);
+ }
+ if (const auto* cached = cache->Find(cache_index)) {
+ std::memcpy(data, cached->data() + offset, length);
+ return length;
+ }
+ }
+ std::memcpy(data, scratch.data() + offset, length);
+ return length;
+ }
+
+ std::size_t Write(const u8*, std::size_t, std::size_t) override {
+ return 0;
+ }
+
+ bool Rename(std::string_view new_name) override {
+ name = std::string(new_name);
+ return true;
+ }
+
+private:
+ std::vector<u8> Decompress() const {
+ if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) {
+ return {};
+ }
+ const auto compressed = backing->ReadBytes(static_cast<std::size_t>(extent_size),
+ static_cast<std::size_t>(extent_offset));
+ if (compressed.size() != extent_size) {
+ return {};
+ }
+ std::vector<u8> out(static_cast<std::size_t>(uncompressed_size));
+ const std::size_t result =
+ ZSTD_decompress(out.data(), out.size(), compressed.data(), compressed.size());
+ if (ZSTD_isError(result) || result != out.size()) {
+ LOG_ERROR(Loader, "NUP zstd decompression failed for {}", name);
+ return {};
+ }
+ return out;
+ }
+
+ VirtualFile backing;
+ u64 extent_offset;
+ u64 extent_size;
+ u64 uncompressed_size;
+ std::string name;
+ std::shared_ptr<NupDecompressCache> cache;
+ std::size_t cache_index;
+ bool cache_enabled;
+};
+
+} // namespace
+
+
+bool NupContainer::IsNupFile(const VirtualFile& file) {
+ if (file == nullptr || file->GetSize() < SuperblockSize) {
+ return false;
+ }
+ std::array<u8, 4> magic{};
+ file->ReadObject(&magic, 0);
+ return magic == HeaderMagic;
+}
+
+NupContainer::NupContainer(VirtualFile file_)
+ : file(std::move(file_)), cache(std::make_shared<NupDecompressCache>()) {
+ if (file == nullptr) {
+ return;
+ }
+ if (Parse()) {
+ status = Loader::ResultStatus::Success;
+ } else if (status == Loader::ResultStatus::ErrorNullFile) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ }
+}
+
+NupContainer::~NupContainer() = default;
+
+bool NupContainer::Parse() {
+ if (file->GetSize() < SuperblockSize) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ Superblock super{};
+ if (file->ReadObject(&super, 0) != sizeof(Superblock)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ if (super.magic != HeaderMagic || super.header_size < SuperblockSize || super.flags != 0) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ toc_offset = super.toc_offset;
+ toc_size = super.toc_size;
+ blob_offset = super.blob_offset;
+ file_size = super.file_size;
+ if (file_size != file->GetSize() || toc_offset < SuperblockSize ||
+ blob_offset < toc_offset + toc_size || blob_offset % 0x10 != 0 ||
+ blob_offset > file_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ const auto toc = file->ReadBytes(static_cast<std::size_t>(toc_size),
+ static_cast<std::size_t>(toc_offset));
+ if (toc.size() != toc_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ std::array<u8, 0x20> toc_hash{};
+ if (!Sha256(toc.data(), toc.size(), toc_hash) || toc_hash != super.toc_sha256) {
+ LOG_ERROR(Loader, "NUP TOC SHA-256 mismatch, the file is corrupt or was modified");
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ TocHeader header{};
+ if (toc.size() < sizeof(header)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ std::memcpy(&header, toc.data(), sizeof(header));
+ if (header.magic != TocMagic || header.package_count == 0 ||
+ static_cast<u32>(header.package_count) != super.package_count) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ const u64 needed = TocHeaderSize + static_cast<u64>(header.package_count) * PackageRecordSize +
+ header.dir_count * DirRecordSize + header.file_count * FileRecordSize;
+ const u64 children_offset = needed;
+ const u64 children_bytes = header.pool_offset - needed;
+ if (toc.size() < needed || header.pool_offset < needed ||
+ header.pool_offset + header.pool_size > toc.size() || children_bytes % sizeof(u32) != 0) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ const u8* base = toc.data();
+
+ std::size_t pos = TocHeaderSize;
+ packages.reserve(header.package_count);
+ for (u32 i = 0; i < header.package_count; ++i) {
+ PackageRecord record{};
+ std::memcpy(&record, base + pos, sizeof(record));
+ pos += PackageRecordSize;
+ if (record.kind > static_cast<u8>(PackageKind::Addon) ||
+ (record.flags & ~PackageFlagStandaloneUpdate) != 0 ||
+ record.root_dir >= header.dir_count ||
+ (record.exefs_dir != NoDir && record.exefs_dir >= header.dir_count) ||
+ (record.romfs_dir != NoDir && record.romfs_dir >= header.dir_count)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ PackageInfo package;
+ package.title_id = record.title_id;
+ package.version = record.version;
+ package.kind = static_cast<PackageKind>(record.kind);
+ package.standalone_update = (record.flags & PackageFlagStandaloneUpdate) != 0;
+ package.root_dir = record.root_dir;
+ package.exefs_dir = record.exefs_dir;
+ package.romfs_dir = record.romfs_dir;
+ if (record.meta_size > 0 && record.meta_offset != 0 &&
+ record.meta_offset + record.meta_size <= file_size) {
+ package.meta_offset = record.meta_offset;
+ package.meta_size = record.meta_size;
+ }
+ packages.push_back(std::move(package));
+ }
+
+ dirs.resize(header.dir_count);
+ for (u64 i = 0; i < header.dir_count; ++i) {
+ DirRecord record{};
+ std::memcpy(&record, base + pos, sizeof(record));
+ pos += DirRecordSize;
+ if (record.first_child + record.child_count > children_bytes / sizeof(u32) ||
+ record.name_offset + record.name_length > header.pool_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ dirs[i].name = std::string(reinterpret_cast<const char*>(base) + header.pool_offset +
+ record.name_offset,
+ record.name_length);
+ dirs[i].first_child = record.first_child;
+ dirs[i].child_count = record.child_count;
+ }
+
+ files.resize(header.file_count);
+ for (u64 i = 0; i < header.file_count; ++i) {
+ FileRecord record{};
+ std::memcpy(&record, base + pos, sizeof(record));
+ pos += FileRecordSize;
+ if (record.codec > CodecZstd || record.codec_flags != 0 ||
+ (record.size == 0 && (record.extent_offset != 0 || record.extent_size != 0)) ||
+ (record.size != 0 &&
+ (record.extent_size == 0 || record.extent_offset < blob_offset ||
+ record.extent_offset % 0x10 != 0 ||
+ record.extent_offset + record.extent_size > file_size)) ||
+ record.name_offset + record.name_length > header.pool_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ files[i].name = std::string(reinterpret_cast<const char*>(base) + header.pool_offset +
+ record.name_offset,
+ record.name_length);
+ files[i].size = record.size;
+ files[i].sha256 = record.sha256;
+ files[i].extent_offset = record.extent_offset;
+ files[i].extent_size = record.extent_size;
+ files[i].codec = record.codec;
+ }
+
+ children.resize(children_bytes / sizeof(u32));
+ std::memcpy(children.data(), base + children_offset, children_bytes);
+ for (const u32 child : children) {
+ if (child >= header.dir_count + header.file_count) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ }
+
+ for (auto& package : packages) {
+ if (!ParseMeta(package)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ }
+ return true;
+}
+
+bool NupContainer::ParseMeta(PackageInfo& package) {
+ if (package.meta_offset == 0) {
+ return true;
+ }
+ const auto block = file->ReadBytes(static_cast<std::size_t>(package.meta_size),
+ static_cast<std::size_t>(package.meta_offset));
+ if (block.size() != package.meta_size) {
+ return false;
+ }
+ std::size_t pos = 0;
+ while (pos + TlvHeaderSize <= block.size()) {
+ TlvHeader header{};
+ std::memcpy(&header, block.data() + pos, sizeof(header));
+ pos += TlvHeaderSize;
+ if (pos + header.size > block.size()) {
+ return false;
+ }
+ const u8* payload = block.data() + pos;
+ pos += header.size;
+ switch (header.type) {
+ case MetaNpdm:
+ package.npdm.assign(payload, payload + header.size);
+ break;
+ case MetaIcon:
+ package.icon.assign(payload, payload + header.size);
+ break;
+ case MetaTitleName:
+ package.title_name = std::string(reinterpret_cast<const char*>(payload), header.size);
+ break;
+ case MetaDisplayVersion:
+ package.display_version =
+ std::string(reinterpret_cast<const char*>(payload), header.size);
+ break;
+ case MetaLocalizedTitles:
+ case MetaLocalizedIcons: {
+ const bool is_title = header.type == MetaLocalizedTitles;
+ std::size_t record_pos = 0;
+ while (record_pos < header.size) {
+ const u8 lang_length = payload[record_pos++];
+ if (record_pos + lang_length + sizeof(u32) > header.size) {
+ return false;
+ }
+ const std::string lang(reinterpret_cast<const char*>(payload) + record_pos,
+ lang_length);
+ record_pos += lang_length;
+ u32 data_length = 0;
+ std::memcpy(&data_length, payload + record_pos, sizeof(data_length));
+ record_pos += sizeof(data_length);
+ if (record_pos + data_length > header.size) {
+ return false;
+ }
+ if (is_title) {
+ package.localized_titles.emplace_back(
+ lang, std::string(reinterpret_cast<const char*>(payload) + record_pos,
+ data_length));
+ } else {
+ package.localized_icons.emplace_back(
+ lang, std::vector<u8>(payload + record_pos,
+ payload + record_pos + data_length));
+ }
+ record_pos += data_length;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return true;
+}
+
+std::optional<std::size_t> NupContainer::FindPackage(PackageKind kind,
+ std::optional<u64> title_id) const {
+ for (std::size_t i = 0; i < packages.size(); ++i) {
+ if (packages[i].kind != kind) {
+ continue;
+ }
+ if (title_id && packages[i].title_id != *title_id) {
+ continue;
+ }
+ return i;
+ }
+ return std::nullopt;
+}
+
+std::optional<std::size_t> NupContainer::FindBasePackage() const {
+ return FindPackage(PackageKind::Base);
+}
+
+std::optional<std::size_t> NupContainer::FindBestUpdatePackage() const {
+ std::optional<std::size_t> best;
+ for (std::size_t i = 0; i < packages.size(); ++i) {
+ if (packages[i].kind != PackageKind::Update) {
+ continue;
+ }
+ if (!best || packages[i].version > packages[*best].version) {
+ best = i;
+ }
+ }
+ return best;
+}
+
+VirtualFile NupContainer::OpenFile(std::size_t file_index) {
+ if (file_index >= files.size()) {
+ return nullptr;
+ }
+ const FileNode& node = files[file_index];
+ if (node.size == 0) {
+ return std::make_shared<VectorVfsFile>(std::vector<u8>{}, node.name);
+ }
+ if (node.codec == CodecRaw) {
+ return std::make_shared<OffsetVfsFile>(
+ file, static_cast<std::size_t>(node.extent_size),
+ static_cast<std::size_t>(node.extent_offset), node.name);
+ }
+ return std::make_shared<NupVfsFile>(file, node.extent_offset, node.extent_size, node.size,
+ node.name, cache, file_index);
+}
+
+void NupContainer::BuildDirTree(u32 dir_index, VectorVfsDirectory& out) {
+ const DirNode& node = dirs[dir_index];
+ for (u32 slot = node.first_child; slot < node.first_child + node.child_count; ++slot) {
+ const u32 child = children[slot];
+ if (child >= dirs.size()) {
+ out.AddFile(OpenFile(child - static_cast<u32>(dirs.size())));
+ } else {
+ auto sub = std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{},
+ std::vector<VirtualDir>{},
+ dirs[child].name);
+ BuildDirTree(child, *sub);
+ out.AddDirectory(sub);
+ }
+ }
+}
+
+VirtualDir NupContainer::BuildPackageDir(u32 dir_index) {
+ auto out = std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{},
+ std::vector<VirtualDir>{}, dirs[dir_index].name);
+ BuildDirTree(dir_index, *out);
+ return out;
+}
+
+VirtualDir NupContainer::GetExeFSDir(std::size_t package_index) {
+ if (package_index >= packages.size() || packages[package_index].exefs_dir == NoDir) {
+ return nullptr;
+ }
+ if (exefs_dir_cache.size() <= package_index) {
+ exefs_dir_cache.resize(packages.size());
+ }
+ if (exefs_dir_cache[package_index] == nullptr) {
+ exefs_dir_cache[package_index] = BuildPackageDir(packages[package_index].exefs_dir);
+ }
+ return exefs_dir_cache[package_index];
+}
+
+VirtualDir NupContainer::GetRomFSDir(std::size_t package_index) {
+ if (package_index >= packages.size() || packages[package_index].romfs_dir == NoDir) {
+ return nullptr;
+ }
+ if (romfs_dir_cache.size() <= package_index) {
+ romfs_dir_cache.resize(packages.size());
+ }
+ if (romfs_dir_cache[package_index] == nullptr) {
+ romfs_dir_cache[package_index] = BuildPackageDir(packages[package_index].romfs_dir);
+ }
+ return romfs_dir_cache[package_index];
+}
+
+VirtualDir NupContainer::MergeShadowDirs(const VirtualDir& base, const VirtualDir& overlay) {
+ if (base == nullptr) {
+ return overlay;
+ }
+ if (overlay == nullptr) {
+ return base;
+ }
+
+ std::map<std::string, VirtualFile, std::less<>> file_map;
+ for (const auto& entry : base->GetFiles()) {
+ file_map.insert_or_assign(entry->GetName(), entry);
+ }
+ for (const auto& entry : overlay->GetFiles()) {
+ file_map.insert_or_assign(entry->GetName(), entry);
+ }
+
+ std::map<std::string, VirtualDir, std::less<>> dir_map;
+ for (const auto& entry : base->GetSubdirectories()) {
+ dir_map.insert_or_assign(entry->GetName(), entry);
+ }
+ for (const auto& entry : overlay->GetSubdirectories()) {
+ const auto it = dir_map.find(entry->GetName());
+ if (it != dir_map.end()) {
+ dir_map.insert_or_assign(entry->GetName(), MergeShadowDirs(it->second, entry));
+ } else {
+ dir_map.insert_or_assign(entry->GetName(), entry);
+ }
+ }
+
+ std::vector<VirtualFile> merged_files;
+ merged_files.reserve(file_map.size());
+ for (const auto& [_, entry] : file_map) {
+ merged_files.push_back(entry);
+ }
+ std::vector<VirtualDir> merged_dirs;
+ merged_dirs.reserve(dir_map.size());
+ for (const auto& [_, entry] : dir_map) {
+ merged_dirs.push_back(entry);
+ }
+ return std::make_shared<VectorVfsDirectory>(std::move(merged_files), std::move(merged_dirs),
+ base->GetName());
+}
+
+std::vector<u8> NupContainer::ReadExtentRaw(const FileNode& node) const {
+ return file->ReadBytes(static_cast<std::size_t>(node.extent_size),
+ static_cast<std::size_t>(node.extent_offset));
+}
+
+Loader::ResultStatus NupContainer::VerifyIntegrity(
+ std::function<bool(std::size_t, std::size_t)> progress_callback) const {
+ const std::size_t total = files.size();
+ for (std::size_t i = 0; i < total; ++i) {
+ const FileNode& node = files[i];
+ std::vector<u8> content;
+ if (node.size == 0) {
+ content = ReadExtentRaw(node);
+ } else if (node.codec == CodecRaw) {
+ content = ReadExtentRaw(node);
+ } else {
+ NupVfsFile view(file, node.extent_offset, node.extent_size, node.size, node.name,
+ cache, i);
+ content = view.ReadAllBytes();
+ }
+ if (content.size() != node.size) {
+ LOG_ERROR(Loader, "NUP integrity check failed for {}: short read", node.name);
+ return Loader::ResultStatus::ErrorIntegrityVerificationFailed;
+ }
+ std::array<u8, 0x20> hash{};
+ if (!Sha256(content.data(), content.size(), hash) || hash != node.sha256) {
+ LOG_ERROR(Loader, "NUP integrity check failed for {}: SHA-256 mismatch", node.name);
+ return Loader::ResultStatus::ErrorIntegrityVerificationFailed;
+ }
+ if (progress_callback && !progress_callback(i + 1, total)) {
+ return Loader::ResultStatus::ErrorIntegrityVerificationFailed;
+ }
+ }
+ return Loader::ResultStatus::Success;
+}
+
+} // namespace FileSys
diff --git a/src/core/file_sys/nup_container.h b/src/core/file_sys/nup_container.h
new file mode 100644
index 0000000000..118b137a7b
--- /dev/null
+++ b/src/core/file_sys/nup_container.h
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <list>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "common/common_types.h"
+#include "core/file_sys/vfs/vfs_types.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+
+/// LRU over decompressed zstd extents, shared by the files of one container.
+struct NupDecompressCache {
+ std::mutex mutex;
+ std::list<std::pair<std::size_t, std::vector<u8>>> lru;
+ std::unordered_map<std::size_t, std::list<std::pair<std::size_t, std::vector<u8>>>::iterator>
+ index;
+ std::size_t bytes{};
+
+ std::vector<u8>* Find(std::size_t file_index) {
+ const auto it = index.find(file_index);
+ if (it == index.end()) {
+ return nullptr;
+ }
+ lru.splice(lru.begin(), lru, it->second);
+ return &it->second->second;
+ }
+
+ void Insert(std::size_t file_index, std::vector<u8>& data) {
+ while (bytes + data.size() > DecompressCacheCap && !lru.empty()) {
+ bytes -= lru.back().second.size();
+ index.erase(lru.back().first);
+ lru.pop_back();
+ }
+ if (bytes + data.size() > DecompressCacheCap) {
+ return;
+ }
+ lru.emplace_front(file_index, data);
+ index.emplace(file_index, lru.begin());
+ bytes += data.size();
+ }
+
+ static constexpr std::size_t DecompressCacheCap = 512 * 1024 * 1024;
+};
+
+/**
+ * NUP (NX Package) container: stores loose ExeFS/RomFS trees for a base program,
+ * its updates and add-on content in one file. Layout:
+ *
+ * Superblock (0x80 bytes, magic "NUP\x01")
+ * Table of Contents (packages, dir/file nodes, children array, string pool)
+ * Blob region (per-file extents, raw or zstd codec, 16-byte aligned)
+ *
+ * Per-package meta TLVs carry NPDM bytes, title names (default + localized),
+ * icons and a display version. All integers little-endian.
+ */
+class NupContainer {
+public:
+ static constexpr std::array<u8, 4> HeaderMagic{'N', 'U', 'P', 0x01};
+
+ enum class PackageKind : u8 {
+ Base = 0,
+ Update = 1,
+ Addon = 2,
+ };
+
+
+ struct PackageInfo {
+ u64 title_id{};
+ u32 version{};
+ PackageKind kind{};
+ bool standalone_update{};
+ u32 root_dir{};
+ u32 exefs_dir{};
+ u32 romfs_dir{};
+ u64 meta_offset{};
+ u32 meta_size{};
+
+ std::vector<u8> npdm;
+ std::string title_name;
+ std::vector<std::pair<std::string, std::string>> localized_titles;
+ std::vector<u8> icon;
+ std::vector<std::pair<std::string, std::vector<u8>>> localized_icons;
+ std::string display_version;
+ };
+
+ /// Quick magic check without a full parse.
+ static bool IsNupFile(const VirtualFile& file);
+
+ /// Recursively merges overlay into a copy of base; overlay paths win.
+ static VirtualDir MergeShadowDirs(const VirtualDir& base, const VirtualDir& overlay);
+
+ explicit NupContainer(VirtualFile file_);
+ ~NupContainer();
+
+ Loader::ResultStatus GetStatus() const {
+ return status;
+ }
+
+ const std::vector<PackageInfo>& GetPackages() const {
+ return packages;
+ }
+
+ std::optional<std::size_t> FindPackage(PackageKind kind,
+ std::optional<u64> title_id = std::nullopt) const;
+ std::optional<std::size_t> FindBasePackage() const;
+ /// Highest version among Update packages, if any.
+ std::optional<std::size_t> FindBestUpdatePackage() const;
+
+ /// Opens a member file by node index; raw extents are zero-copy views into
+ /// the container, zstd extents are lazily decompressed and cached.
+ VirtualFile OpenFile(std::size_t file_index);
+
+ VirtualDir GetExeFSDir(std::size_t package_index);
+ VirtualDir GetRomFSDir(std::size_t package_index);
+
+ /// Stream-hashes every member file against its stored SHA-256.
+ Loader::ResultStatus VerifyIntegrity(
+ std::function<bool(std::size_t, std::size_t)> progress_callback) const;
+
+ std::size_t GetFileCount() const {
+ return files.size();
+ }
+
+private:
+ struct DirNode {
+ std::string name;
+ u32 first_child{};
+ u32 child_count{};
+ };
+
+ struct FileNode {
+ std::string name;
+ u64 size{};
+ std::array<u8, 0x20> sha256{};
+ u64 extent_offset{};
+ u64 extent_size{};
+ u8 codec{};
+ };
+
+ bool Parse();
+ bool ParseMeta(PackageInfo& package);
+ void BuildDirTree(u32 dir_index, VectorVfsDirectory& out);
+ VirtualDir BuildPackageDir(u32 dir_index);
+ std::vector<u8> ReadExtentRaw(const FileNode& node) const;
+
+ VirtualFile file;
+ Loader::ResultStatus status{Loader::ResultStatus::ErrorNullFile};
+
+ u64 toc_offset{};
+ u64 toc_size{};
+ u64 blob_offset{};
+ u64 file_size{};
+
+ std::vector<DirNode> dirs;
+ std::vector<FileNode> files;
+ std::vector<u32> children;
+ std::vector<PackageInfo> packages;
+
+ std::vector<VirtualDir> exefs_dir_cache;
+ std::vector<VirtualDir> romfs_dir_cache;
+
+ std::shared_ptr<NupDecompressCache> cache;
+};
+
+} // namespace FileSys
diff --git a/src/core/file_sys/nup_writer.cpp b/src/core/file_sys/nup_writer.cpp
new file mode 100644
index 0000000000..8c7728ed01
--- /dev/null
+++ b/src/core/file_sys/nup_writer.cpp
@@ -0,0 +1,1459 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/file_sys/nup_writer.h"
+
+#include <algorithm>
+#include <array>
+#include <cctype>
+#include <charconv>
+#include <cstring>
+#include <limits>
+#include <map>
+#include <memory>
+#include <optional>
+#include <span>
+#include <stdexcept>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <nlohmann/json.hpp>
+#include <openssl/evp.h>
+#include <zstd.h>
+
+#include "common/fs/file.h"
+#include "common/fs/path_util.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/vfs/vfs_real.h"
+#include "core/loader/deconstructed_rom_directory.h"
+
+namespace FileSys::Nup {
+namespace {
+
+using Json = nlohmann::json;
+namespace fs = std::filesystem;
+
+constexpr std::array<u8, 4> Magic{'N', 'U', 'P', 0x01};
+constexpr std::array<u8, 4> TocMagic{'T', 'O', 'C', 0x01};
+constexpr u64 SuperblockSize = 0x80;
+constexpr u64 TocHeaderSize = 0x30;
+constexpr u64 PackageRecordSize = 0x40;
+constexpr u64 DirRecordSize = 0x18;
+constexpr u64 FileRecordSize = 0x50;
+constexpr u64 ExtentAlignment = 16;
+constexpr u32 NoDir = 0xFFFFFFFF;
+
+constexpr u8 KindBase = 0;
+constexpr u8 KindUpdate = 1;
+constexpr u8 KindAddon = 2;
+constexpr u8 FlagStandaloneUpdate = 0x01;
+constexpr u8 CodecRaw = 0;
+constexpr u8 CodecZstd = 1;
+
+constexpr u16 TlvNpdm = 0x0001;
+constexpr u16 TlvNacp = 0x0002;
+constexpr u16 TlvIcon = 0x0003;
+constexpr u16 TlvDisplayVersion = 0x0004;
+constexpr u16 TlvTitleName = 0x0005;
+constexpr u16 TlvPublisher = 0x0006;
+constexpr u16 TlvSourceDump = 0x0008;
+
+constexpr int JournalVersion = 1;
+constexpr u64 ProgressStep = 64ULL * 1024 * 1024;
+constexpr size_t MaxChunkFiles = 4096;
+constexpr size_t ReadBufferSize = 4 * 1024 * 1024;
+
+class PackError : public std::runtime_error {
+public:
+ PackError(PackResult result_, const std::string& message)
+ : std::runtime_error(message), result{result_} {}
+ PackResult result;
+};
+
+u64 AlignUp(u64 value, u64 alignment) {
+ return (value + alignment - 1) / alignment * alignment;
+}
+
+std::string ToUtf8(const fs::path& path) {
+ const auto u8 = path.generic_u8string();
+ return {u8.begin(), u8.end()};
+}
+
+fs::path FromUtf8(const std::string& value) {
+ return fs::path{std::u8string{value.begin(), value.end()}};
+}
+
+std::string Lower(std::string value) {
+ std::ranges::transform(value, value.begin(),
+ [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
+ return value;
+}
+
+fs::path WithSuffix(fs::path path, std::string_view suffix) {
+ path += suffix;
+ return path;
+}
+
+bool IsFile(const fs::path& path) {
+ std::error_code ec;
+ return fs::is_regular_file(path, ec);
+}
+
+fs::path FindDirectory(const fs::path& parent, std::string_view name) {
+ std::error_code ec;
+ if (parent.empty() || !fs::is_directory(parent, ec)) {
+ return {};
+ }
+ const auto wanted = Lower(std::string{name});
+ for (const auto& entry : fs::directory_iterator(parent, ec)) {
+ if (entry.is_directory(ec) && Lower(ToUtf8(entry.path().filename())) == wanted) {
+ return entry.path();
+ }
+ }
+ return {};
+}
+
+std::optional<u64> ParseDecimal(const std::string& name) {
+ u64 value{};
+ const auto* end = name.data() + name.size();
+ const auto [ptr, ec] = std::from_chars(name.data(), end, value);
+ if (name.empty() || ec != std::errc{} || ptr != end) {
+ return std::nullopt;
+ }
+ return value;
+}
+
+// The exefs folder a dump boots from: the newest updates/<version>, else base, else the root.
+// Each must hold main, main.npdm and a romfs folder next to it or under base.
+bool HasBootableExeFS(const fs::path& root) {
+ const auto bootable = [](const fs::path& exefs) {
+ return !exefs.empty() && IsFile(exefs / "main") && IsFile(exefs / "main.npdm");
+ };
+ fs::path exefs;
+ std::optional<u64> best_version;
+ if (const auto updates = FindDirectory(root, "updates"); !updates.empty()) {
+ std::error_code ec;
+ for (const auto& entry : fs::directory_iterator(updates, ec)) {
+ const auto version = ParseDecimal(ToUtf8(entry.path().filename()));
+ const auto candidate = FindDirectory(entry.path(), "exefs");
+ if (version && bootable(candidate) && (!best_version || *version > *best_version)) {
+ best_version = version;
+ exefs = candidate;
+ }
+ }
+ }
+ const auto base = FindDirectory(root, "base");
+ if (exefs.empty()) {
+ exefs = FindDirectory(base, "exefs");
+ }
+ if (exefs.empty()) {
+ exefs = FindDirectory(root, "exefs");
+ }
+ if (!bootable(exefs)) {
+ return false;
+ }
+ return !FindDirectory(exefs.parent_path(), "romfs").empty() ||
+ !FindDirectory(base, "romfs").empty();
+}
+
+std::optional<u64> ParseTitleId(const std::string& name) {
+ u64 value{};
+ const auto* end = name.data() + name.size();
+ const auto [ptr, ec] = std::from_chars(name.data(), end, value, 16);
+ if (name.size() != 16 || ec != std::errc{} || ptr != end) {
+ return std::nullopt;
+ }
+ return value;
+}
+
+std::string ToHex(std::span<const u8> data) {
+ static constexpr char digits[] = "0123456789abcdef";
+ std::string out;
+ out.reserve(data.size() * 2);
+ for (const u8 byte : data) {
+ out += digits[byte >> 4];
+ out += digits[byte & 0xF];
+ }
+ return out;
+}
+
+std::vector<u8> FromHex(const std::string& hex) {
+ if (hex.size() % 2 != 0) {
+ throw PackError(PackResult::IoError, "malformed journal");
+ }
+ std::vector<u8> out(hex.size() / 2);
+ for (size_t i = 0; i < out.size(); ++i) {
+ const auto [ptr, ec] = std::from_chars(hex.data() + i * 2, hex.data() + i * 2 + 2, out[i], 16);
+ if (ec != std::errc{} || ptr != hex.data() + i * 2 + 2) {
+ throw PackError(PackResult::IoError, "malformed journal");
+ }
+ }
+ return out;
+}
+
+std::vector<u8> ReadWholeFile(const fs::path& path) {
+ Common::FS::IOFile in{path, Common::FS::FileAccessMode::Read};
+ if (!in.IsOpen()) {
+ return {};
+ }
+ std::vector<u8> data(in.GetSize());
+ if (in.ReadSpan(std::span{data}) != data.size()) {
+ return {};
+ }
+ return data;
+}
+
+// Same as pack.py: the program ID at ACI0 + 0x10, whatever the NPDM header revision.
+std::optional<u64> NpdmProgramId(std::span<const u8> npdm) {
+ static constexpr std::array<u8, 4> aci0{'A', 'C', 'I', '0'};
+ const auto it = std::search(npdm.begin(), npdm.end(), aci0.begin(), aci0.end());
+ const auto offset = static_cast<size_t>(it - npdm.begin());
+ if (it == npdm.end() || offset + 0x18 > npdm.size()) {
+ return std::nullopt;
+ }
+ u64 id = 0;
+ for (size_t i = 0; i < 8; ++i) {
+ id |= static_cast<u64>(npdm[offset + 0x10 + i]) << (8 * i);
+ }
+ return id;
+}
+
+void PutLE(std::vector<u8>& buffer, size_t offset, u64 value, size_t bytes) {
+ for (size_t i = 0; i < bytes; ++i) {
+ buffer[offset + i] = static_cast<u8>(value >> (8 * i));
+ }
+}
+
+void PutBytes(std::vector<u8>& buffer, size_t offset, std::span<const u8> bytes) {
+ std::memcpy(buffer.data() + offset, bytes.data(), bytes.size());
+}
+
+using Digest = std::array<u8, 32>;
+
+class Sha256 {
+public:
+ Sha256() : ctx{EVP_MD_CTX_new()} {
+ if (!ctx) {
+ throw PackError(PackResult::IoError, "cannot initialise SHA-256");
+ }
+ Reset();
+ }
+ ~Sha256() {
+ EVP_MD_CTX_free(ctx);
+ }
+ Sha256(const Sha256&) = delete;
+ Sha256& operator=(const Sha256&) = delete;
+
+ void Reset() {
+ if (EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1) {
+ throw PackError(PackResult::IoError, "cannot initialise SHA-256");
+ }
+ }
+
+ void Update(const void* data, size_t size) {
+ EVP_DigestUpdate(ctx, data, size);
+ }
+
+ Digest Final() {
+ Digest digest{};
+ unsigned int length{};
+ EVP_DigestFinal_ex(ctx, digest.data(), &length);
+ return digest;
+ }
+
+private:
+ EVP_MD_CTX* ctx;
+};
+
+class ZstdContext {
+public:
+ // Archives must be byte-identical across platforms, so compression is always
+ // single-threaded (zstd's multithreaded frames differ) and every frame parameter is set
+ // explicitly instead of relying on library defaults.
+ explicit ZstdContext(int level) : cctx{ZSTD_createCCtx()} {
+ if (!cctx || ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 0)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_contentSizeFlag, 1)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_checksumFlag, 0)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_dictIDFlag, 0))) {
+ throw PackError(PackResult::IoError, "cannot initialise zstd");
+ }
+ }
+ ~ZstdContext() {
+ ZSTD_freeCCtx(cctx);
+ }
+ ZstdContext(const ZstdContext&) = delete;
+ ZstdContext& operator=(const ZstdContext&) = delete;
+
+ // The pledged size makes zstd record the content size in the frame header, which the
+ // format requires.
+ void Begin(u64 size) {
+ if (ZSTD_isError(ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only)) ||
+ ZSTD_isError(ZSTD_CCtx_setPledgedSrcSize(cctx, size))) {
+ throw PackError(PackResult::IoError, "cannot start zstd frame");
+ }
+ }
+
+ ZSTD_CCtx* get() const {
+ return cctx;
+ }
+
+private:
+ ZSTD_CCtx* cctx;
+};
+
+// One package and the metadata it is written with. The plan is everything that must survive a
+// low-storage run deleting its sources, so it is what the journal stores.
+struct PlanPackage {
+ u8 kind{};
+ u8 flags{};
+ u64 title_id{};
+ u32 version{};
+ std::vector<u8> meta;
+};
+
+struct PlanEntry {
+ u32 package{};
+ std::string path; // inside the package tree, e.g. "romfs/Data/file.bin"
+ std::string source; // relative to the dump root
+ bool directory{};
+};
+
+struct Plan {
+ std::vector<PlanPackage> packages;
+ std::vector<PlanEntry> entries;
+};
+
+struct Extent {
+ u64 size{};
+ Digest sha256{};
+ u64 offset{};
+ u64 stored_size{};
+ u8 codec{};
+};
+
+struct Layout {
+ struct Dir {
+ std::string name;
+ u32 first_child{};
+ u32 child_count{};
+ };
+ struct File {
+ std::string name;
+ size_t entry{};
+ };
+ struct Package {
+ u32 root_dir{};
+ u32 exefs_dir = NoDir;
+ u32 romfs_dir = NoDir;
+ };
+ std::vector<Dir> dirs;
+ std::vector<File> files;
+ std::vector<u64> children; // FileTag | file index, or dir index
+ std::vector<Package> packages;
+ std::string pool;
+ std::unordered_map<std::string, u32> pool_index;
+ u64 toc_size{};
+ u64 blob_offset{};
+
+ static constexpr u64 FileTag = 1ULL << 32;
+
+ u32 Intern(const std::string& name) {
+ const auto [it, inserted] = pool_index.try_emplace(name, static_cast<u32>(pool.size()));
+ if (inserted) {
+ pool += name;
+ }
+ return it->second;
+ }
+};
+
+// Children sorted by the bytes of their names: std::string compares as unsigned char.
+struct TreeDir {
+ struct Child {
+ std::unique_ptr<TreeDir> dir;
+ size_t file_entry{};
+ };
+ std::string name;
+ std::map<std::string, Child> children;
+
+ TreeDir* Descend(const std::string& component) {
+ auto& child = children[component];
+ if (!child.dir) {
+ child.dir = std::make_unique<TreeDir>();
+ child.dir->name = component;
+ }
+ return child.dir.get();
+ }
+};
+
+// Lays out nodes as pack.py does: directories in pre-order, each directory's child slots
+// contiguous, its files numbered in slot order before its subdirectories are visited.
+u32 AssignDir(TreeDir& dir, Layout& layout) {
+ const auto id = static_cast<u32>(layout.dirs.size());
+ layout.dirs.push_back({dir.name, static_cast<u32>(layout.children.size()),
+ static_cast<u32>(dir.children.size())});
+ const size_t slot_base = layout.children.size();
+ layout.children.resize(slot_base + dir.children.size());
+ size_t slot = slot_base;
+ for (auto& [name, child] : dir.children) {
+ if (!child.dir) {
+ layout.children[slot] = Layout::FileTag | layout.files.size();
+ layout.files.push_back({name, child.file_entry});
+ }
+ ++slot;
+ }
+ slot = slot_base;
+ for (auto& [name, child] : dir.children) {
+ if (child.dir) {
+ layout.children[slot] = AssignDir(*child.dir, layout);
+ }
+ ++slot;
+ }
+ return id;
+}
+
+Layout BuildLayout(const Plan& plan) {
+ std::vector<TreeDir> roots(plan.packages.size());
+ for (size_t i = 0; i < plan.entries.size(); ++i) {
+ const auto& entry = plan.entries[i];
+ TreeDir* dir = &roots.at(entry.package);
+ size_t start = 0;
+ while (true) {
+ const auto slash = entry.path.find('/', start);
+ const auto component = entry.path.substr(start, slash - start);
+ if (slash == std::string::npos) {
+ if (entry.directory) {
+ dir->Descend(component);
+ } else {
+ auto& child = dir->children[component];
+ if (child.dir) {
+ throw PackError(PackResult::InvalidSource,
+ "a file and a folder share the name " + entry.path);
+ }
+ child.file_entry = i;
+ }
+ break;
+ }
+ dir = dir->Descend(component);
+ start = slash + 1;
+ }
+ }
+
+ Layout layout;
+ for (auto& root : roots) {
+ Layout::Package package;
+ package.root_dir = AssignDir(root, layout);
+ const auto& root_dir = layout.dirs[package.root_dir];
+ for (u32 slot = root_dir.first_child; slot < root_dir.first_child + root_dir.child_count;
+ ++slot) {
+ const u64 child = layout.children[slot];
+ if (child & Layout::FileTag) {
+ continue;
+ }
+ const auto id = static_cast<u32>(child);
+ if (layout.dirs[id].name == "exefs") {
+ package.exefs_dir = id;
+ } else if (layout.dirs[id].name == "romfs") {
+ package.romfs_dir = id;
+ }
+ }
+ layout.packages.push_back(package);
+ }
+
+ if (layout.dirs.size() + layout.files.size() >= NoDir) {
+ throw PackError(PackResult::InvalidSource, "the dump has too many files");
+ }
+ for (const auto& dir : layout.dirs) {
+ layout.Intern(dir.name);
+ }
+ for (const auto& file : layout.files) {
+ layout.Intern(file.name);
+ }
+ if (layout.pool.size() > std::numeric_limits<u32>::max()) {
+ throw PackError(PackResult::InvalidSource, "the dump's file names are too long");
+ }
+
+ const u64 unpadded = TocHeaderSize + plan.packages.size() * PackageRecordSize +
+ layout.dirs.size() * DirRecordSize + layout.files.size() * FileRecordSize +
+ layout.children.size() * sizeof(u32) + layout.pool.size();
+ layout.toc_size = AlignUp(unpadded, ExtentAlignment);
+ layout.blob_offset = AlignUp(SuperblockSize + layout.toc_size, ExtentAlignment);
+ return layout;
+}
+
+std::vector<u8> SerializeTlvs(std::vector<std::pair<u16, std::vector<u8>>> records) {
+ std::ranges::stable_sort(records, {}, &std::pair<u16, std::vector<u8>>::first);
+ std::vector<u8> out;
+ for (const auto& [type, payload] : records) {
+ const size_t at = out.size();
+ out.resize(at + 8 + payload.size());
+ PutLE(out, at, type, 2);
+ PutLE(out, at + 4, payload.size(), 4);
+ PutBytes(out, at + 8, payload);
+ }
+ return out;
+}
+
+std::vector<u8> StringBytes(const std::string& value) {
+ return {value.begin(), value.end()};
+}
+
+// The dump's own loader supplies control data and the display version, the same way a loose
+// dump reports them when it boots (control.nacp beside it, else the RomFS's version tag).
+std::unique_ptr<Loader::AppLoader_DeconstructedRomDirectory> OpenLoader(
+ RealVfsFilesystem& vfs, const fs::path& package_dir) {
+ const auto exefs = FindDirectory(package_dir, "exefs");
+ if (exefs.empty() || !IsFile(exefs / "main")) {
+ return nullptr;
+ }
+ auto main = vfs.OpenFile(Common::FS::PathToUTF8String(exefs / "main"), OpenMode::Read);
+ if (!main) {
+ return nullptr;
+ }
+ return std::make_unique<Loader::AppLoader_DeconstructedRomDirectory>(std::move(main));
+}
+
+class Discovery {
+public:
+ Discovery(const fs::path& dump_root_, std::vector<fs::path> excluded_)
+ : dump_root{dump_root_}, excluded{std::move(excluded_)} {}
+
+ Plan Run() {
+ const auto base = FindDirectory(dump_root, "base");
+ const auto updates = FindDirectory(dump_root, "updates");
+ const auto dlc = FindDirectory(dump_root, "dlc");
+
+ std::optional<u64> base_title_id;
+ if (!base.empty() && HasContent(base)) {
+ base_title_id = AddPackage(KindBase, base, 0, {});
+ } else if (updates.empty() && HasContent(dump_root)) {
+ base_title_id = AddPackage(KindBase, dump_root, 0, {"base", "updates", "dlc"});
+ }
+
+ std::vector<std::pair<u64, fs::path>> versions;
+ std::error_code ec;
+ if (!updates.empty()) {
+ for (const auto& entry : fs::directory_iterator(updates, ec)) {
+ const auto version = ParseDecimal(ToUtf8(entry.path().filename()));
+ if (version && entry.is_directory(ec) && HasContent(entry.path())) {
+ if (*version > std::numeric_limits<u32>::max()) {
+ throw PackError(PackResult::InvalidSource,
+ "update version out of range: " + ToUtf8(entry.path()));
+ }
+ versions.emplace_back(*version, entry.path());
+ }
+ }
+ }
+ std::ranges::sort(versions);
+ const u8 update_flags = base_title_id ? 0 : FlagStandaloneUpdate;
+ for (const auto& [version, path] : versions) {
+ AddPackage(KindUpdate, path, static_cast<u32>(version), {}, update_flags,
+ base_title_id.value_or(0));
+ }
+
+ if (!dlc.empty()) {
+ std::vector<std::pair<std::string, fs::path>> addons;
+ for (const auto& title : fs::directory_iterator(dlc, ec)) {
+ const auto title_name = ToUtf8(title.path().filename());
+ if (!ParseTitleId(title_name) || !title.is_directory(ec)) {
+ continue;
+ }
+ for (const auto& content : fs::directory_iterator(title.path(), ec)) {
+ if (content.is_directory(ec) && HasContent(content.path())) {
+ addons.emplace_back(title_name + "/" + ToUtf8(content.path().filename()),
+ content.path());
+ }
+ }
+ }
+ std::ranges::sort(addons);
+ for (const auto& [name, path] : addons) {
+ AddPackage(KindAddon, path, 0, {}, 0, *ParseTitleId(name.substr(0, 16)));
+ }
+ }
+
+ if (plan.packages.empty()) {
+ throw PackError(PackResult::InvalidSource, "no packages found in the dump");
+ }
+ AddApplicationMetadata();
+ for (size_t i = 0; i < plan.packages.size(); ++i) {
+ auto& tlvs = package_tlvs[i];
+ tlvs.emplace_back(TlvSourceDump, StringBytes(ToUtf8(dump_root.filename())));
+ plan.packages[i].meta = SerializeTlvs(std::move(tlvs));
+ }
+ return std::move(plan);
+ }
+
+private:
+ static bool HasContent(const fs::path& dir) {
+ return !FindDirectory(dir, "exefs").empty() || !FindDirectory(dir, "romfs").empty();
+ }
+
+ // Returns the package's title ID: the NPDM program ID, else the folder's or base's.
+ u64 AddPackage(u8 kind, const fs::path& dir, u32 version,
+ const std::vector<std::string>& skip, u8 flags = 0, u64 fallback_id = 0) {
+ const auto index = static_cast<u32>(plan.packages.size());
+ PlanPackage package{kind, flags, fallback_id, version, {}};
+ auto& tlvs = package_tlvs.emplace_back();
+
+ const auto exefs = FindDirectory(dir, "exefs");
+ const auto romfs = FindDirectory(dir, "romfs");
+ fs::path npdm_path = exefs.empty() ? fs::path{} : exefs / "main.npdm";
+ if (!IsFile(npdm_path)) {
+ npdm_path = dir / "main.npdm";
+ }
+ if (IsFile(npdm_path)) {
+ auto npdm = ReadWholeFile(npdm_path);
+ if (const auto id = NpdmProgramId(npdm); kind != KindAddon && id && *id != 0) {
+ package.title_id = *id;
+ }
+ tlvs.emplace_back(TlvNpdm, std::move(npdm));
+ }
+
+ if (kind != KindAddon) {
+ if (auto loader = OpenLoader(vfs, dir)) {
+ std::string display_version;
+ if (loader->ReadDisplayVersion(display_version) == Loader::ResultStatus::Success &&
+ !display_version.empty()) {
+ tlvs.emplace_back(TlvDisplayVersion, StringBytes(display_version));
+ }
+ loaders.emplace_back(index, std::move(loader));
+ }
+ }
+ plan.packages.push_back(std::move(package));
+
+ if (!exefs.empty()) {
+ CollectTree(index, exefs, "exefs");
+ }
+ if (!romfs.empty()) {
+ CollectTree(index, romfs, "romfs");
+ }
+ std::error_code ec;
+ for (const auto& entry : fs::directory_iterator(dir, ec)) {
+ const auto name = ToUtf8(entry.path().filename());
+ const auto lower = Lower(name);
+ if (entry.path() == exefs || entry.path() == romfs ||
+ std::ranges::find(skip, lower) != skip.end()) {
+ continue;
+ }
+ if (entry.is_directory(ec)) {
+ CollectTree(index, entry.path(), "data/" + name);
+ } else {
+ AddFile(index, entry, "data/" + name);
+ }
+ }
+ return plan.packages.back().title_id;
+ }
+
+ // Names, icon and control data belong to the base package, or the first one without a base.
+ // Our dumps rarely carry a control folder; when one does, the base's own wins, then updates.
+ void AddApplicationMetadata() {
+ const auto app = static_cast<u32>(std::distance(
+ plan.packages.begin(), std::ranges::find(plan.packages, KindBase, &PlanPackage::kind)));
+ const u32 target = app < plan.packages.size() ? app : 0;
+ std::ranges::stable_sort(loaders, [target](const auto& a, const auto& b) {
+ return (a.first == target) > (b.first == target);
+ });
+
+ auto& tlvs = package_tlvs[target];
+ bool have_control = false;
+ bool have_icon = false;
+ for (const auto& [index, loader] : loaders) {
+ NACP control;
+ if (!have_control &&
+ loader->ReadControlData(control) == Loader::ResultStatus::Success) {
+ have_control = true;
+ tlvs.emplace_back(TlvNacp, control.GetRawBytes());
+ if (const auto name = control.GetApplicationName(); !name.empty()) {
+ tlvs.emplace_back(TlvTitleName, StringBytes(name));
+ }
+ if (const auto publisher = control.GetDeveloperName(); !publisher.empty()) {
+ tlvs.emplace_back(TlvPublisher, StringBytes(publisher));
+ }
+ }
+ std::vector<u8> icon;
+ if (!have_icon && loader->ReadIcon(icon) == Loader::ResultStatus::Success &&
+ !icon.empty()) {
+ have_icon = true;
+ tlvs.emplace_back(TlvIcon, std::move(icon));
+ }
+ }
+ }
+
+ bool IsExcluded(const fs::path& path) const {
+ const auto normal = fs::absolute(path).lexically_normal();
+ return std::ranges::find(excluded, normal) != excluded.end();
+ }
+
+ void AddFile(u32 package, const fs::directory_entry& entry, const std::string& path) {
+ std::error_code ec;
+ if (entry.is_symlink(ec)) {
+ throw PackError(PackResult::InvalidSource,
+ "symlinks are not supported: " + ToUtf8(entry.path()));
+ }
+ if (!entry.is_regular_file(ec)) {
+ throw PackError(PackResult::InvalidSource,
+ "unsupported file type: " + ToUtf8(entry.path()));
+ }
+ // Dump-tool listings are excluded by the spec.
+ if (entry.path().filename() == ".listing" || IsExcluded(entry.path())) {
+ return;
+ }
+ plan.entries.push_back({package, path, Relative(entry.path()), false});
+ }
+
+ void CollectTree(u32 package, const fs::path& root, const std::string& prefix) {
+ plan.entries.push_back({package, prefix, Relative(root), true});
+ for (auto it = fs::recursive_directory_iterator(root);
+ it != fs::recursive_directory_iterator(); ++it) {
+ const auto& entry = *it;
+ const auto path = prefix + "/" + ToUtf8(fs::relative(entry.path(), root));
+ if (!entry.is_symlink() && entry.is_directory()) {
+ plan.entries.push_back({package, path, Relative(entry.path()), true});
+ } else {
+ AddFile(package, entry, path);
+ }
+ }
+ }
+
+ std::string Relative(const fs::path& path) const {
+ return ToUtf8(fs::relative(path, dump_root));
+ }
+
+ fs::path dump_root;
+ std::vector<fs::path> excluded;
+ RealVfsFilesystem vfs;
+ Plan plan;
+ std::vector<std::vector<std::pair<u16, std::vector<u8>>>> package_tlvs;
+ std::vector<std::pair<u32, std::unique_ptr<Loader::AppLoader_DeconstructedRomDirectory>>>
+ loaders;
+};
+
+Json PlanToJson(const Plan& plan) {
+ Json packages = Json::array();
+ for (const auto& package : plan.packages) {
+ packages.push_back(Json{{"kind", package.kind},
+ {"flags", package.flags},
+ {"title_id", package.title_id},
+ {"version", package.version},
+ {"meta", ToHex(package.meta)}});
+ }
+ Json entries = Json::array();
+ for (const auto& entry : plan.entries) {
+ entries.push_back(Json{{"package", entry.package},
+ {"path", entry.path},
+ {"source", entry.source},
+ {"directory", entry.directory}});
+ }
+ return Json{{"format", "nup-journal"},
+ {"version", JournalVersion},
+ {"packages", packages},
+ {"entries", entries}};
+}
+
+Plan PlanFromJson(const Json& json) {
+ if (json.at("format") != "nup-journal" || json.at("version") != JournalVersion) {
+ throw PackError(PackResult::IoError, "unsupported journal");
+ }
+ Plan plan;
+ for (const auto& package : json.at("packages")) {
+ plan.packages.push_back({package.at("kind").get<u8>(), package.at("flags").get<u8>(),
+ package.at("title_id").get<u64>(),
+ package.at("version").get<u32>(),
+ FromHex(package.at("meta").get<std::string>())});
+ }
+ for (const auto& entry : json.at("entries")) {
+ plan.entries.push_back({entry.at("package").get<u32>(), entry.at("path").get<std::string>(),
+ entry.at("source").get<std::string>(),
+ entry.at("directory").get<bool>()});
+ }
+ return plan;
+}
+
+void RemoveFile(const fs::path& path) {
+ std::error_code ec;
+ fs::remove(path, ec);
+}
+
+bool IsEmptyDirectory(const fs::path& path) {
+ std::error_code ec;
+ return fs::is_directory(path, ec) && fs::is_empty(path, ec) && !ec;
+}
+
+void RemoveEmptyTree(const fs::path& root) {
+ std::error_code ec;
+ std::vector<fs::path> directories;
+ for (auto it = fs::recursive_directory_iterator(root, ec);
+ !ec && it != fs::recursive_directory_iterator(); it.increment(ec)) {
+ if (it->is_directory(ec)) {
+ directories.push_back(it->path());
+ }
+ }
+ for (auto it = directories.rbegin(); it != directories.rend(); ++it) {
+ if (IsEmptyDirectory(*it)) {
+ fs::remove(*it, ec);
+ }
+ }
+ if (IsEmptyDirectory(root)) {
+ fs::remove(root, ec);
+ }
+}
+
+void RemoveEmptyParents(fs::path path, const fs::path& dump_root) {
+ while (!path.empty() && path.has_relative_path()) {
+ std::error_code ec;
+ if (fs::equivalent(path, dump_root, ec) || ec || !IsEmptyDirectory(path)) {
+ break;
+ }
+ fs::remove(path, ec);
+ path = path.parent_path();
+ }
+}
+
+class Packer {
+public:
+ Packer(const fs::path& dump_root_, const fs::path& archive_, const PackOptions& options_,
+ const std::function<void(const PackProgress&)>& progress_,
+ const std::atomic_bool* cancel_)
+ : dump_root{dump_root_}, archive{archive_}, part{WithSuffix(archive_, ".part")},
+ journal{WithSuffix(archive_, ".journal")}, options{options_}, progress_cb{progress_},
+ cancel{cancel_} {}
+
+ PackResult Run() {
+ std::error_code ec;
+ if (fs::exists(archive, ec)) {
+ throw PackError(PackResult::IoError, "archive already exists: " + ToUtf8(archive));
+ }
+ // A journal without a complete plan line was cut short before anything was deleted.
+ const bool resume =
+ options.delete_sources && HasResumableJournal(archive) && LoadJournal();
+ if (resume) {
+ OpenForResume();
+ } else {
+ if (!IsLooseDump(dump_root)) {
+ throw PackError(PackResult::InvalidSource,
+ "expected exefs/main, exefs/main.npdm and a romfs folder");
+ }
+ std::vector<fs::path> excluded;
+ for (const auto& path : {archive, part, journal}) {
+ excluded.push_back(fs::absolute(path).lexically_normal());
+ }
+ plan = Discovery{dump_root, std::move(excluded)}.Run();
+ layout = BuildLayout(plan);
+ OpenNew();
+ }
+ extents.resize(layout.files.size());
+ done.resize(layout.files.size());
+ for (const auto& [index, extent] : journaled) {
+ extents[index] = extent;
+ done[index] = true;
+ Remember(extent);
+ }
+ CountTotals();
+ Report();
+
+ for (size_t i = 0; i < layout.files.size(); ++i) {
+ if (done[i]) {
+ continue;
+ }
+ PackFile(i);
+ if (options.delete_sources &&
+ (chunk_bytes >= options.chunk_bytes || chunk.size() >= MaxChunkFiles)) {
+ Checkpoint();
+ }
+ }
+ if (options.delete_sources) {
+ Checkpoint();
+ }
+
+ Finish();
+ out.Close();
+ fs::rename(part, archive, ec);
+ if (ec) {
+ throw PackError(PackResult::IoError, "cannot rename archive: " + ec.message());
+ }
+
+ if (options.delete_sources) {
+ RemoveFile(journal);
+ RemoveSourceFolders();
+ }
+ return PackResult::Success;
+ }
+
+ void Abort() {
+ out.Close();
+ if (!options.delete_sources) {
+ RemoveFile(part);
+ }
+ }
+
+private:
+ void OpenNew() {
+ RemoveFile(journal);
+ out.Open(part, Common::FS::FileAccessMode::Write);
+ if (!out.IsOpen()) {
+ throw PackError(PackResult::IoError, "cannot create " + ToUtf8(part));
+ }
+ WriteZeros(layout.blob_offset);
+ cursor = layout.blob_offset;
+ if (options.delete_sources) {
+ AppendJournal(PlanToJson(plan).dump() + '\n');
+ }
+ }
+
+ bool LoadJournal() {
+ const auto contents =
+ Common::FS::ReadStringFromFile(journal, Common::FS::FileType::BinaryFile);
+ size_t start = 0;
+ bool have_plan = false;
+ while (start < contents.size()) {
+ const auto line_end = contents.find('\n', start);
+ if (line_end == std::string::npos) {
+ break;
+ }
+ const auto line = std::string_view{contents}.substr(start, line_end - start);
+ start = line_end + 1;
+ Json json;
+ try {
+ json = Json::parse(line);
+ } catch (const Json::exception&) {
+ // Only the tail can be torn: the journal is synced before sources are deleted.
+ break;
+ }
+ if (!have_plan) {
+ plan = PlanFromJson(json);
+ layout = BuildLayout(plan);
+ resume_end = layout.blob_offset;
+ have_plan = true;
+ continue;
+ }
+ const auto index = json.at("file").get<size_t>();
+ const auto sha = FromHex(json.at("sha256").get<std::string>());
+ if (index >= layout.files.size() || sha.size() != 32) {
+ throw PackError(PackResult::IoError, "malformed journal");
+ }
+ Extent extent{json.at("size").get<u64>(), {}, json.at("offset").get<u64>(),
+ json.at("stored_size").get<u64>(), json.at("codec").get<u8>()};
+ std::ranges::copy(sha, extent.sha256.begin());
+ resume_end = std::max(resume_end, extent.offset + extent.stored_size);
+ journaled.emplace_back(index, extent);
+ }
+ return have_plan;
+ }
+
+ void OpenForResume() {
+ const u64 end = resume_end;
+ out.Open(part, Common::FS::FileAccessMode::ReadWrite);
+ if (!out.IsOpen()) {
+ throw PackError(PackResult::IoError, "cannot open " + ToUtf8(part));
+ }
+ if (out.GetSize() < end) {
+ throw PackError(PackResult::IoError, "partial archive is shorter than its journal; "
+ "delete " +
+ ToUtf8(part) + " and " + ToUtf8(journal) +
+ " to start over");
+ }
+ if (!out.SetSize(end) || !out.Seek(static_cast<s64>(end))) {
+ throw PackError(PackResult::IoError, "cannot truncate " + ToUtf8(part));
+ }
+ cursor = end;
+
+ // Journaled before an interruption, but the delete never happened.
+ for (const auto& [index, extent] : journaled) {
+ RemoveFile(SourceOf(index));
+ }
+ }
+
+ fs::path SourceOf(size_t file_index) const {
+ return dump_root / FromUtf8(plan.entries[layout.files[file_index].entry].source);
+ }
+
+ void CountTotals() {
+ for (size_t i = 0; i < layout.files.size(); ++i) {
+ ++progress.files_total;
+ if (done[i]) {
+ progress.bytes_total += extents[i].size;
+ progress.bytes_done += extents[i].size;
+ ++progress.files_done;
+ } else {
+ std::error_code ec;
+ const auto size = fs::file_size(SourceOf(i), ec);
+ if (ec) {
+ throw PackError(PackResult::InvalidSource,
+ "missing source file: " + ToUtf8(SourceOf(i)));
+ }
+ progress.bytes_total += size;
+ }
+ }
+ }
+
+ void Remember(const Extent& extent) {
+ if (extent.size == 0) {
+ return;
+ }
+ sizes_seen.insert(extent.size);
+ by_hash.try_emplace(extent.sha256, extent);
+ }
+
+ bool Cancelled() const {
+ return cancel && cancel->load();
+ }
+
+ void WriteZeros(u64 count) {
+ static constexpr std::array<u8, 4096> zeros{};
+ while (count > 0) {
+ const auto n = static_cast<size_t>(std::min<u64>(count, zeros.size()));
+ Write(zeros.data(), n);
+ count -= n;
+ }
+ }
+
+ void Write(const u8* data, size_t size) {
+ if (out.WriteSpan(std::span<const u8>{data, size}) != size) {
+ throw PackError(PackResult::IoError, "write failed, the disk may be full");
+ }
+ }
+
+ void AddProgress(u64 bytes) {
+ progress.bytes_done += bytes;
+ if (progress.bytes_done - last_report >= ProgressStep) {
+ Report();
+ }
+ }
+
+ // Reads the whole source through `sink`, checking that it still has the expected size. A file
+ // read more than once (hashed for dedup, or compressed and then stored raw) counts once.
+ template <typename Sink>
+ void ReadSource(Common::FS::IOFile& in, u64 size, Sink&& sink) {
+ if (!in.Seek(0)) {
+ throw PackError(PackResult::IoError, "cannot seek source");
+ }
+ buffer.resize(ReadBufferSize);
+ u64 total = 0;
+ while (true) {
+ if (Cancelled()) {
+ throw PackError(PackResult::Cancelled, "cancelled");
+ }
+ const auto read = in.ReadSpan(std::span{buffer});
+ if (read == 0) {
+ break;
+ }
+ total += read;
+ if (total > size) {
+ break;
+ }
+ if (total > file_counted) {
+ AddProgress(total - file_counted);
+ file_counted = total;
+ }
+ if (!sink(buffer.data(), read)) {
+ return;
+ }
+ }
+ if (total != size) {
+ throw PackError(PackResult::IoError, "a source file changed while packing");
+ }
+ }
+
+ Digest HashSource(Common::FS::IOFile& in, u64 size) {
+ hash.Reset();
+ ReadSource(in, size, [this](const u8* data, size_t n) {
+ hash.Update(data, n);
+ return true;
+ });
+ return hash.Final();
+ }
+
+ ZstdContext& Compressor() {
+ if (!compressor) {
+ compressor = std::make_unique<ZstdContext>(options.zstd_level);
+ }
+ return *compressor;
+ }
+
+ // Streams one zstd frame into the archive. Gives up (returns false) as soon as the output is
+ // no smaller than the input.
+ bool WriteCompressed(Common::FS::IOFile& in, u64 size, u64& stored) {
+ auto& zstd = Compressor();
+ zstd.Begin(size);
+ out_buffer.resize(ZSTD_CStreamOutSize());
+ stored = 0;
+ bool fits = true;
+ const auto drain = [&](ZSTD_inBuffer& input, ZSTD_EndDirective mode) {
+ while (true) {
+ ZSTD_outBuffer output{out_buffer.data(), out_buffer.size(), 0};
+ const size_t remaining = ZSTD_compressStream2(zstd.get(), &output, &input, mode);
+ if (ZSTD_isError(remaining)) {
+ throw PackError(PackResult::IoError,
+ std::string{"zstd: "} + ZSTD_getErrorName(remaining));
+ }
+ stored += output.pos;
+ if (stored >= size) {
+ return false;
+ }
+ Write(out_buffer.data(), output.pos);
+ const bool finished = mode == ZSTD_e_end ? remaining == 0
+ : input.pos == input.size;
+ if (finished) {
+ return true;
+ }
+ }
+ };
+ hash.Reset();
+ ReadSource(in, size, [&](const u8* data, size_t n) {
+ hash.Update(data, n);
+ ZSTD_inBuffer input{data, n, 0};
+ fits = drain(input, ZSTD_e_continue);
+ return fits;
+ });
+ if (!fits) {
+ return false;
+ }
+ ZSTD_inBuffer empty{nullptr, 0, 0};
+ return drain(empty, ZSTD_e_end);
+ }
+
+ void PackFile(size_t index) {
+ const auto source = SourceOf(index);
+ Common::FS::IOFile in{source, Common::FS::FileAccessMode::Read};
+ if (!in.IsOpen()) {
+ throw PackError(PackResult::IoError, "cannot read " + ToUtf8(source));
+ }
+ Extent extent;
+ extent.size = in.GetSize();
+ file_counted = 0;
+
+ std::optional<Digest> known_hash;
+ if (extent.size == 0) {
+ known_hash = HashSource(in, 0);
+ } else if (sizes_seen.contains(extent.size)) {
+ // Only a file whose size matches an earlier extent can be a duplicate; hash it before
+ // writing anything so the shared extent costs no output.
+ known_hash = HashSource(in, extent.size);
+ if (const auto it = by_hash.find(*known_hash);
+ it != by_hash.end() && it->second.size == extent.size) {
+ extent = it->second;
+ Complete(index, extent);
+ return;
+ }
+ }
+
+ if (extent.size == 0) {
+ extent.sha256 = *known_hash;
+ Complete(index, extent);
+ return;
+ }
+
+ const u64 start = AlignUp(cursor, ExtentAlignment);
+ Seek(cursor);
+ WriteZeros(start - cursor);
+ extent.offset = start;
+
+ bool compressed = false;
+ if (extent.size <= options.raw_threshold && options.zstd_level > 0) {
+ u64 stored = 0;
+ compressed = WriteCompressed(in, extent.size, stored);
+ if (compressed) {
+ extent.codec = CodecZstd;
+ extent.stored_size = stored;
+ extent.sha256 = hash.Final();
+ }
+ }
+ if (!compressed) {
+ Seek(start);
+ hash.Reset();
+ ReadSource(in, extent.size, [this](const u8* data, size_t n) {
+ hash.Update(data, n);
+ Write(data, n);
+ return true;
+ });
+ extent.codec = CodecRaw;
+ extent.stored_size = extent.size;
+ extent.sha256 = hash.Final();
+ }
+ cursor = start + extent.stored_size;
+ Remember(extent);
+ Complete(index, extent);
+ }
+
+ void Seek(u64 offset) {
+ if (!out.Seek(static_cast<s64>(offset))) {
+ throw PackError(PackResult::IoError, "cannot seek " + ToUtf8(part));
+ }
+ }
+
+ void Complete(size_t index, const Extent& extent) {
+ extents[index] = extent;
+ done[index] = true;
+ if (options.delete_sources) {
+ chunk.push_back(index);
+ chunk_bytes += extent.size;
+ }
+ ++progress.files_done;
+ Report();
+ }
+
+ void AppendJournal(const std::string& lines) {
+ Common::FS::IOFile log{journal, Common::FS::FileAccessMode::Append};
+ if (!log.IsOpen() || log.WriteString(lines) != lines.size() || !log.Commit()) {
+ throw PackError(PackResult::IoError, "cannot write " + ToUtf8(journal));
+ }
+ }
+
+ // Archive data first, then the journal, then the deletes, so an interruption loses nothing.
+ void Checkpoint() {
+ if (chunk.empty()) {
+ return;
+ }
+ if (!out.Commit()) {
+ throw PackError(PackResult::IoError, "cannot sync " + ToUtf8(part));
+ }
+ std::string lines;
+ for (const auto index : chunk) {
+ const auto& extent = extents[index];
+ lines += Json{{"file", index},
+ {"size", extent.size},
+ {"sha256", ToHex(extent.sha256)},
+ {"offset", extent.offset},
+ {"stored_size", extent.stored_size},
+ {"codec", extent.codec}}
+ .dump();
+ lines += '\n';
+ }
+ AppendJournal(lines);
+ for (const auto index : chunk) {
+ RemoveFile(SourceOf(index));
+ }
+ chunk.clear();
+ chunk_bytes = 0;
+ }
+
+ void Finish() {
+ // Meta blocks follow the last extent, as pack.py writes them.
+ Seek(cursor);
+ std::vector<std::pair<u64, u32>> meta;
+ for (const auto& package : plan.packages) {
+ if (package.meta.size() > std::numeric_limits<u32>::max()) {
+ throw PackError(PackResult::IoError, "package metadata is too large");
+ }
+ meta.emplace_back(package.meta.empty() ? 0 : cursor,
+ static_cast<u32>(package.meta.size()));
+ Write(package.meta.data(), package.meta.size());
+ cursor += package.meta.size();
+ }
+ const u64 file_size = cursor;
+
+ const auto toc = BuildToc(meta);
+ std::array<u8, 32> toc_hash{};
+ unsigned int hash_length{};
+ EVP_Digest(toc.data(), toc.size(), toc_hash.data(), &hash_length, EVP_sha256(), nullptr);
+
+ // UUID: SHA-256 over title_id (u64), version (u32) and kind (u8) of each package.
+ Sha256 uuid_hash;
+ for (const auto& package : plan.packages) {
+ std::vector<u8> identity(13);
+ PutLE(identity, 0, package.title_id, 8);
+ PutLE(identity, 8, package.version, 4);
+ identity[12] = package.kind;
+ uuid_hash.Update(identity.data(), identity.size());
+ }
+ const auto uuid = uuid_hash.Final();
+
+ std::vector<u8> super(SuperblockSize);
+ PutBytes(super, 0x00, Magic);
+ PutLE(super, 0x04, SuperblockSize, 2);
+ PutLE(super, 0x06, 1, 2);
+ PutLE(super, 0x08, SuperblockSize, 8);
+ PutLE(super, 0x10, toc.size(), 8);
+ PutBytes(super, 0x18, toc_hash);
+ PutLE(super, 0x38, layout.blob_offset, 8);
+ PutLE(super, 0x40, file_size, 8);
+ PutLE(super, 0x48, plan.packages.size(), 4);
+ PutBytes(super, 0x50, std::span{uuid}.first(16));
+
+ Seek(0);
+ Write(super.data(), super.size());
+ Write(toc.data(), toc.size());
+ if (!out.SetSize(file_size) || !out.Commit()) {
+ throw PackError(PackResult::IoError, "cannot finish " + ToUtf8(part));
+ }
+ }
+
+ // Every field is written little-endian at its documented offset. The package record is
+ // 0x40 bytes with 20 reserved bytes at 0x2C, as NupContainer reads it.
+ std::vector<u8> BuildToc(const std::vector<std::pair<u64, u32>>& meta) const {
+ std::vector<u8> toc(layout.toc_size);
+ const u64 dir_count = layout.dirs.size();
+ PutBytes(toc, 0x00, TocMagic);
+ PutLE(toc, 0x04, plan.packages.size(), 4);
+ PutLE(toc, 0x08, dir_count, 8);
+ PutLE(toc, 0x10, layout.files.size(), 8);
+
+ size_t pos = TocHeaderSize;
+ for (size_t i = 0; i < plan.packages.size(); ++i) {
+ const auto& package = plan.packages[i];
+ const auto& record = layout.packages[i];
+ PutLE(toc, pos + 0x00, package.title_id, 8);
+ PutLE(toc, pos + 0x08, record.root_dir, 4);
+ PutLE(toc, pos + 0x0C, record.exefs_dir, 4);
+ PutLE(toc, pos + 0x10, record.romfs_dir, 4);
+ PutLE(toc, pos + 0x14, package.version, 4);
+ toc[pos + 0x18] = package.kind;
+ toc[pos + 0x19] = package.flags;
+ PutLE(toc, pos + 0x20, meta[i].first, 8);
+ PutLE(toc, pos + 0x28, meta[i].second, 4);
+ pos += PackageRecordSize;
+ }
+ for (const auto& dir : layout.dirs) {
+ PutLE(toc, pos + 0x00, layout.pool_index.at(dir.name), 4);
+ PutLE(toc, pos + 0x04, dir.name.size(), 4);
+ PutLE(toc, pos + 0x08, dir.first_child, 4);
+ PutLE(toc, pos + 0x0C, dir.child_count, 4);
+ pos += DirRecordSize;
+ }
+ for (size_t i = 0; i < layout.files.size(); ++i) {
+ const auto& file = layout.files[i];
+ const auto& extent = extents[i];
+ PutLE(toc, pos + 0x00, layout.pool_index.at(file.name), 4);
+ PutLE(toc, pos + 0x04, file.name.size(), 4);
+ PutLE(toc, pos + 0x08, extent.size, 8);
+ // Empty files have no extent: offset, size and codec are all zero.
+ if (extent.size != 0) {
+ PutLE(toc, pos + 0x10, extent.offset, 8);
+ PutLE(toc, pos + 0x18, extent.stored_size, 8);
+ toc[pos + 0x40] = extent.codec;
+ }
+ PutBytes(toc, pos + 0x20, extent.sha256);
+ pos += FileRecordSize;
+ }
+ for (const u64 child : layout.children) {
+ const u64 id = (child & Layout::FileTag) ? dir_count + static_cast<u32>(child) : child;
+ PutLE(toc, pos, id, 4);
+ pos += sizeof(u32);
+ }
+ PutLE(toc, 0x18, pos, 8);
+ PutLE(toc, 0x20, layout.pool.size(), 8);
+ std::memcpy(toc.data() + pos, layout.pool.data(), layout.pool.size());
+ return toc;
+ }
+
+ void RemoveSourceFolders() {
+ for (const auto& entry : plan.entries) {
+ const auto depth = std::ranges::count(entry.path, '/');
+ const bool top = entry.directory &&
+ (depth == 0 || (depth == 1 && entry.path.starts_with("data/")));
+ if (top) {
+ const auto source = dump_root / FromUtf8(entry.source);
+ RemoveEmptyTree(source);
+ RemoveEmptyParents(source.parent_path(), dump_root);
+ }
+ }
+ }
+
+ void Report() {
+ last_report = progress.bytes_done;
+ if (progress_cb) {
+ progress_cb(progress);
+ }
+ }
+
+ fs::path dump_root;
+ fs::path archive;
+ fs::path part;
+ fs::path journal;
+ PackOptions options;
+ std::function<void(const PackProgress&)> progress_cb;
+ const std::atomic_bool* cancel;
+
+ Plan plan;
+ Layout layout;
+ std::vector<Extent> extents;
+ std::vector<bool> done;
+ std::vector<std::pair<size_t, Extent>> journaled;
+
+ struct DigestHash {
+ size_t operator()(const Digest& digest) const {
+ size_t value;
+ std::memcpy(&value, digest.data(), sizeof(value));
+ return value;
+ }
+ };
+ std::unordered_map<Digest, Extent, DigestHash> by_hash;
+ std::unordered_set<u64> sizes_seen;
+
+ Common::FS::IOFile out;
+ u64 cursor = 0;
+ u64 resume_end = 0;
+ std::vector<size_t> chunk;
+ u64 chunk_bytes = 0;
+ std::vector<u8> buffer;
+ std::vector<u8> out_buffer;
+ Sha256 hash;
+ std::unique_ptr<ZstdContext> compressor;
+ PackProgress progress{};
+ u64 last_report = 0;
+ u64 file_counted = 0;
+};
+
+} // namespace
+
+bool IsLooseDump(const fs::path& dir) {
+ return HasBootableExeFS(dir);
+}
+
+std::optional<fs::path> DumpRootFromMain(const fs::path& main) {
+ // The Qt game list stores the exefs folder rather than exefs/main.
+ std::error_code ec;
+ const auto exefs = fs::is_directory(main, ec) ? main : main.parent_path();
+ if (Lower(ToUtf8(exefs.filename())) != "exefs") {
+ return std::nullopt;
+ }
+ auto root = exefs.parent_path();
+ const auto name = Lower(ToUtf8(root.filename()));
+ if (name == "base") {
+ root = root.parent_path();
+ } else if (ParseDecimal(name) && Lower(ToUtf8(root.parent_path().filename())) == "updates") {
+ root = root.parent_path().parent_path();
+ }
+ if (!IsLooseDump(root)) {
+ return std::nullopt;
+ }
+ return root;
+}
+
+fs::path DefaultArchivePath(const fs::path& dump_root) {
+ auto root = dump_root;
+ if (!root.has_filename()) {
+ root = root.parent_path();
+ }
+ return root.parent_path() / (root.filename().u8string() + u8".nup");
+}
+
+bool HasResumableJournal(const fs::path& archive) {
+ std::error_code ec;
+ return fs::exists(WithSuffix(archive, ".part"), ec) &&
+ fs::exists(WithSuffix(archive, ".journal"), ec);
+}
+
+PackResult PackDump(const fs::path& dump_root, const fs::path& archive, const PackOptions& options,
+ const std::function<void(const PackProgress&)>& progress,
+ const std::atomic_bool* cancel, std::string* error_out) {
+ Packer packer{dump_root, archive, options, progress, cancel};
+ try {
+ return packer.Run();
+ } catch (const PackError& e) {
+ packer.Abort();
+ if (error_out) {
+ *error_out = e.what();
+ }
+ return e.result;
+ } catch (const std::exception& e) {
+ packer.Abort();
+ if (error_out) {
+ *error_out = e.what();
+ }
+ return PackResult::IoError;
+ }
+}
+
+} // namespace FileSys::Nup
diff --git a/src/core/file_sys/nup_writer.h b/src/core/file_sys/nup_writer.h
new file mode 100644
index 0000000000..231387b156
--- /dev/null
+++ b/src/core/file_sys/nup_writer.h
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <atomic>
+#include <filesystem>
+#include <functional>
+#include <optional>
+#include <string>
+
+#include "common/common_types.h"
+
+// Packs a loose dump into a NUP container. Every package is kept: base/, each
+// updates/<version>/ and each dlc/<titleid>/<n>/.
+namespace FileSys::Nup {
+
+struct PackProgress {
+ u64 bytes_done{};
+ u64 bytes_total{};
+ u64 files_done{};
+ u64 files_total{};
+};
+
+enum class PackResult { Success, Cancelled, InvalidSource, IoError };
+
+// NupContainer keeps a zstd file in memory only while it fits its 512 MiB decompression cache;
+// a larger one is decompressed again in full on every read, which makes big compressed RomFS
+// files unusable. Files above this size are therefore stored raw, with headroom below the cap so
+// that one file does not evict everything else from the cache.
+constexpr u64 DefaultRawThreshold = 256ULL * 1024 * 1024;
+
+struct PackOptions {
+ // Low-storage mode: delete each source file once its extent is synced and journaled.
+ bool delete_sources = false;
+ u64 chunk_bytes = 256ULL * 1024 * 1024;
+ int zstd_level = 3;
+ // Files larger than this are always stored raw.
+ u64 raw_threshold = DefaultRawThreshold;
+};
+
+// A loose dump holds exefs/main and main.npdm, directly or under base/ or updates/<version>/,
+// and a romfs folder. Folder names are matched case-insensitively.
+bool IsLooseDump(const std::filesystem::path& dir);
+// The dump folder a game list entry's exefs/main (or exefs folder) belongs to, if it is a loose
+// dump.
+std::optional<std::filesystem::path> DumpRootFromMain(const std::filesystem::path& main);
+std::filesystem::path DefaultArchivePath(const std::filesystem::path& dump_root);
+
+// Writes <archive>.part and renames it when done; an existing archive is never overwritten. A
+// cancelled or failed low-storage run keeps its .part and .journal, and calling this again with
+// the same arguments resumes it.
+PackResult PackDump(const std::filesystem::path& dump_root, const std::filesystem::path& archive,
+ const PackOptions& options = {},
+ const std::function<void(const PackProgress&)>& progress = {},
+ const std::atomic_bool* cancel = nullptr, std::string* error_out = nullptr);
+
+bool HasResumableJournal(const std::filesystem::path& archive);
+
+} // namespace FileSys::Nup
diff --git a/src/core/file_sys/system_archive/system_archive.cpp b/src/core/file_sys/system_archive/system_archive.cpp
index 1743d01b12..c3931b6f62 100644
--- a/src/core/file_sys/system_archive/system_archive.cpp
+++ b/src/core/file_sys/system_archive/system_archive.cpp
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cstdlib>
+
#include "common/logging.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/system_archive/mii_model.h"
@@ -12,6 +14,7 @@
#include "core/file_sys/system_archive/system_archive.h"
#include "core/file_sys/system_archive/system_version.h"
#include "core/file_sys/system_archive/time_zone_binary.h"
+#include "core/file_sys/vfs/vfs_vector.h"
namespace FileSys::SystemArchive {
@@ -76,6 +79,22 @@ constexpr inline SystemArchiveDescriptor GetSystemArchive(u64 title_id) {
VirtualFile SynthesizeSystemArchive(const u64 title_id) {
auto const desc = GetSystemArchive(title_id);
+ if (desc.name == nullptr) {
+ // Not a system archive Eden knows (fmt throws on a null string, ending the process).
+ // Switch 2 system data (ids 04...) is not installed with Switch 1 firmware, and titles
+ // abort when it cannot be mounted (Nintendo Switch Sports Resort, 0400000000000849):
+ // serve an empty archive. EDEN_NX2_EMPTY_SYSTEM_DATA=0 fails the request instead.
+ static const bool empty_nx2 = [] {
+ const char* setting = std::getenv("EDEN_NX2_EMPTY_SYSTEM_DATA");
+ return setting == nullptr || setting[0] != '0';
+ }();
+ if (empty_nx2 && (title_id >> 56) == 0x04) {
+ LOG_WARNING(Service_FS, "Empty archive for Switch 2 system data {:#016x}", title_id);
+ return CreateRomFS(std::make_shared<VectorVfsDirectory>());
+ }
+ LOG_WARNING(Service_FS, "No system archive {:#016x} to synthesize", title_id);
+ return nullptr;
+ }
LOG_INFO(Service_FS, "Synthesizing system archive '{}' ({:#016x}).", desc.name, title_id);
if (desc.supplier != nullptr) {
if (auto const dir = desc.supplier(); dir != nullptr) {
diff --git a/src/core/file_sys/vfs/vfs_real.cpp b/src/core/file_sys/vfs/vfs_real.cpp
index 5b5b54017c..7cc5315a70 100644
--- a/src/core/file_sys/vfs/vfs_real.cpp
+++ b/src/core/file_sys/vfs/vfs_real.cpp
@@ -114,7 +114,6 @@ VfsEntryType RealVfsFilesystem::GetEntryType(std::string_view path_) const {
}
VirtualFile RealVfsFilesystem::OpenFileFromEntry(std::string_view path_, std::optional<u64> size,
- std::optional<std::string> parent_path,
OpenMode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
std::scoped_lock lk{list_lock};
@@ -132,14 +131,14 @@ VirtualFile RealVfsFilesystem::OpenFileFromEntry(std::string_view path_, std::op
auto reference = std::make_unique<FileReference>();
this->InsertReferenceIntoListLocked(*reference);
- auto file = std::make_shared<RealVfsFile>(*this, std::move(reference), path, perms, size, std::move(parent_path));
+ auto file = std::make_shared<RealVfsFile>(*this, std::move(reference), path, perms, size);
cache[path] = file;
return file;
}
VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, OpenMode perms) {
- return OpenFileFromEntry(path_, {}, {}, perms);
+ return OpenFileFromEntry(path_, {}, perms);
}
VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, OpenMode perms) {
@@ -257,7 +256,8 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
return lk;
}
-void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
+void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference,
+ const std::string& path) {
if (in_dtor)
return;
@@ -266,6 +266,11 @@ void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference
// Remove from list.
this->RemoveReferenceFromListLocked(*reference);
+ // Forget the path, unless it was opened again since this file's last reference went away.
+ if (const auto it = cache.find(path); it != cache.end() && it->second.expired()) {
+ cache.erase(it);
+ }
+
// Close the file.
if (reference->file) {
reference->file.reset();
@@ -309,14 +314,11 @@ void RealVfsFilesystem::RemoveReferenceFromListLocked(FileReference& reference)
}
RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::unique_ptr<FileReference> reference_,
- const std::string& path_, OpenMode perms_, std::optional<u64> size_,
- std::optional<std::string> parent_path_)
- : base(base_), reference(std::move(reference_)), path(path_),
- parent_path(parent_path_ ? std::move(*parent_path_) : FS::GetParentPath(path_)),
- path_components(FS::SplitPathComponentsCopy(path_)), size(size_), perms(perms_) {}
+ const std::string& path_, OpenMode perms_, std::optional<u64> size_)
+ : base(base_), reference(std::move(reference_)), path(path_), size(size_), perms(perms_) {}
RealVfsFile::~RealVfsFile() {
- base.DropReference(std::move(reference));
+ base.DropReference(std::move(reference), path);
}
std::string RealVfsFile::GetName() const {
@@ -325,7 +327,14 @@ std::string RealVfsFile::GetName() const {
return FS::Android::GetFilename(path);
}
#endif
- return path_components.empty() ? "" : std::string(path_components.back());
+ // The last path component.
+ const auto end = path.find_last_not_of("\\/");
+ if (end == std::string::npos) {
+ return "";
+ }
+ const auto separator = path.find_last_of("\\/", end);
+ const auto begin = separator == std::string::npos ? 0 : separator + 1;
+ return path.substr(begin, end + 1 - begin);
}
std::size_t RealVfsFile::GetSize() const {
@@ -343,7 +352,7 @@ bool RealVfsFile::Resize(std::size_t new_size) {
}
VirtualDir RealVfsFile::GetContainingDirectory() const {
- return base.OpenDirectory(parent_path, perms);
+ return base.OpenDirectory(FS::GetParentPath(path), perms);
}
bool RealVfsFile::IsWritable() const {
@@ -372,7 +381,7 @@ std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t o
}
bool RealVfsFile::Rename(std::string_view name) {
- return base.MoveFile(path, parent_path + '/' + std::string(name)) != nullptr;
+ return base.MoveFile(path, FS::GetParentPath(path) + '/' + std::string(name)) != nullptr;
}
// TODO(DarkLordZach): MSVC would not let me combine the following two functions using 'if
@@ -390,7 +399,7 @@ std::vector<VirtualFile> RealVfsDirectory::IterateEntries<RealVfsFile, VfsFile>(
&out](const std::filesystem::directory_entry& entry) {
const auto full_path_string = FS::PathToUTF8String(entry.path());
- out.emplace_back(base.OpenFileFromEntry(full_path_string, entry.file_size(), path, perms));
+ out.emplace_back(base.OpenFileFromEntry(full_path_string, entry.file_size(), perms));
return true;
};
diff --git a/src/core/file_sys/vfs/vfs_real.h b/src/core/file_sys/vfs/vfs_real.h
index a97f7d32ea..2df125b7eb 100644
--- a/src/core/file_sys/vfs/vfs_real.h
+++ b/src/core/file_sys/vfs/vfs_real.h
@@ -62,12 +62,11 @@ private:
friend class RealVfsFile;
std::unique_lock<std::mutex> RefreshReference(const std::string& path, OpenMode perms,
FileReference& reference);
- void DropReference(std::unique_ptr<FileReference>&& reference);
+ void DropReference(std::unique_ptr<FileReference>&& reference, const std::string& path);
private:
friend class RealVfsDirectory;
VirtualFile OpenFileFromEntry(std::string_view path, std::optional<u64> size,
- std::optional<std::string> parent_path,
OpenMode perms = OpenMode::Read);
private:
@@ -84,7 +83,7 @@ class RealVfsFile : public VfsFile {
public:
RealVfsFile(RealVfsFilesystem& base, std::unique_ptr<FileReference> reference,
const std::string& path, OpenMode perms = OpenMode::Read,
- std::optional<u64> size = {}, std::optional<std::string> parent_path = {});
+ std::optional<u64> size = {});
~RealVfsFile() override;
std::string GetName() const override;
@@ -101,9 +100,8 @@ private:
RealVfsFilesystem& base;
std::unique_ptr<FileReference> reference;
+ // Only the path: a loose RomFS keeps one of these per file (title 1: 206k).
std::string path;
- std::string parent_path;
- std::vector<std::string> path_components;
std::optional<u64> size;
OpenMode perms;
};
diff --git a/src/core/hle/kernel/board/nintendo/nx/k_system_control.h b/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
index cc5572bde8..9aa527adb1 100644
--- a/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
+++ b/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
@@ -23,7 +23,10 @@ public:
// Extra DRAM on the 12 GB layout for memory the Switch 2 graphics server keeps itself (nx2:gpu
// 214 GPU-only regions). It sits in the non-secure system pool, outside the application pool.
- static constexpr size_t Nx2GraphicsServerMemorySize = 8ULL * 1024 * 1024 * 1024; // 8_GiB
+ // GPU-only regions get memory only where guest memory is written and sparse regions take
+ // their tiles from the game's own memory (216), so much less of it is used (title 1: ~1.1 GiB,
+ // all of it at boot); all of it counts against the host's commit limit from boot.
+ static constexpr size_t Nx2GraphicsServerMemorySize = 2ULL * 1024 * 1024 * 1024; // 2_GiB
public:
class Init {
diff --git a/src/core/hle/kernel/k_memory_manager.cpp b/src/core/hle/kernel/k_memory_manager.cpp
index 743e290541..bed3dd819d 100644
--- a/src/core/hle/kernel/k_memory_manager.cpp
+++ b/src/core/hle/kernel/k_memory_manager.cpp
@@ -48,6 +48,8 @@ KMemoryManager::KMemoryManager(Core::System& system)
} {}
void KMemoryManager::Initialize(KVirtualAddress management_region, size_t management_region_size) {
+ // Memory a previous kernel handed out may still hold its data.
+ m_system.DeviceMemory().buffer.ForgetContents();
// Clear the management region to zero.
const KVirtualAddress management_region_end = management_region + management_region_size;
@@ -242,10 +244,21 @@ KPhysicalAddress KMemoryManager::AllocateAndOpenContinuous(size_t num_pages, siz
// Open the first reference to the pages.
chosen_manager->OpenFirst(allocated_block, num_pages);
+ this->MarkUsed(allocated_block, num_pages);
return allocated_block;
}
+void KMemoryManager::MarkUsed(KPhysicalAddress address, size_t num_pages) {
+ m_system.DeviceMemory().buffer.MarkUsed(GetInteger(address) - Core::DramMemoryMap::Base,
+ num_pages * PageSize);
+}
+
+void KMemoryManager::MarkDirty(KPhysicalAddress address, size_t num_pages) {
+ m_system.DeviceMemory().buffer.MarkDirty(GetInteger(address) - Core::DramMemoryMap::Base,
+ num_pages * PageSize);
+}
+
Result KMemoryManager::AllocatePageGroupImpl(KPageGroup* out, size_t num_pages, Pool pool,
Direction dir, bool unoptimized, bool random) {
// Choose a heap based on our page size request.
@@ -297,6 +310,10 @@ Result KMemoryManager::AllocatePageGroupImpl(KPageGroup* out, size_t num_pages,
// Only succeed if we allocated as many pages as we wanted.
R_UNLESS(num_pages == 0, ResultOutOfMemory);
+ for (const auto& block : *out) {
+ this->MarkUsed(block.GetAddress(), block.GetNumPages());
+ }
+
// We succeeded!
R_SUCCEED();
}
@@ -525,9 +542,10 @@ bool KMemoryManager::Impl::ProcessOptimizedAllocation(KernelCore& kernel, KPhysi
// If not, it's new.
any_new = true;
- // Fill the page.
- auto* ptr = device_memory.GetPointer<u8>(m_heap.GetAddress());
- std::memset(ptr + offset * PageSize, fill_pattern, PageSize);
+ // Fill the page (a zero fill skips pages that are still zero).
+ device_memory.buffer.ClearBackingRegion(
+ GetInteger(m_heap.GetAddress()) + offset * PageSize - Core::DramMemoryMap::Base,
+ PageSize, fill_pattern);
}
offset++;
diff --git a/src/core/hle/kernel/k_memory_manager.h b/src/core/hle/kernel/k_memory_manager.h
index 2f4ae7f04f..66b97fef8a 100644
--- a/src/core/hle/kernel/k_memory_manager.h
+++ b/src/core/hle/kernel/k_memory_manager.h
@@ -100,6 +100,9 @@ public:
}
void Close(KPhysicalAddress address, size_t num_pages) {
+ // Pages that may be freed here may hold data: the next allocation clears them.
+ this->MarkDirty(address, num_pages);
+
// Repeatedly close references until we've done so for all pages.
while (num_pages) {
auto& manager = this->GetManager(address);
@@ -333,6 +336,9 @@ private:
};
private:
+ void MarkUsed(KPhysicalAddress address, size_t num_pages);
+ void MarkDirty(KPhysicalAddress address, size_t num_pages);
+
Impl& GetManager(KPhysicalAddress address) {
return m_managers[m_memory_layout.GetPhysicalLinearRegion(address).GetAttributes()];
}
diff --git a/src/core/hle/kernel/k_shared_memory.cpp b/src/core/hle/kernel/k_shared_memory.cpp
index 61f2872560..cb8877b8b2 100644
--- a/src/core/hle/kernel/k_shared_memory.cpp
+++ b/src/core/hle/kernel/k_shared_memory.cpp
@@ -59,9 +59,10 @@ Result KSharedMemory::Initialize(KernelCore& kernel, Core::DeviceMemory& device_
// Mark initialized.
m_is_initialized = true;
- // Clear all pages in the memory.
+ // Clear all pages in the memory (pages still zero are skipped).
for (const auto& block : *m_page_group) {
- std::memset(m_device_memory->GetPointer<void>(block.GetAddress()), 0, block.GetSize());
+ m_device_memory->buffer.ClearBackingRegion(
+ GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base, block.GetSize(), 0);
}
R_SUCCEED();
diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp
index 53d964ecbc..925eafc472 100644
--- a/src/core/hle/kernel/svc.cpp
+++ b/src/core/hle/kernel/svc.cpp
@@ -14,6 +14,8 @@
#include "core/core.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/svc.h"
+#include "core/hle/kernel/k_readable_event.h"
+#include "core/hle/service/kernel_helpers.h"
namespace Kernel::Svc {
@@ -2522,6 +2524,21 @@ void Call(Core::System& system, u32 imm) {
LOG_INFO(Kernel_SVC, "svc {:#04x} in t{} [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x}", imm,
GetCurrentThread(kernel).GetThreadId(),
GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2), GetArg64(args, 3));
+ if (imm == 0x18) {
+ // WaitSynchronization: the handles, with the names services gave their events.
+ std::string list;
+ const s32 count = static_cast<s32>(GetArg64(args, 2));
+ for (s32 i = 0; i < std::min(count, 8); ++i) {
+ const u32 handle = process.GetMemory().Read32(GetArg64(args, 1) + 4 * i);
+ KScopedAutoObject event =
+ process.GetHandleTable().GetObject<KReadableEvent>(kernel, handle);
+ list += fmt::format(" {:#x}{}", handle,
+ event.IsNotNull()
+ ? "=" + Service::KernelHelpers::DebugEventName(event.GetPointerUnsafe())
+ : std::string{});
+ }
+ LOG_INFO(Kernel_SVC, "svc 0x18 t{} waits on{}", GetCurrentThread(kernel).GetThreadId(), list);
+ }
}
if (imm == 0x80) {
// Switch 2 only: svcSetGpuMemoryAttribute(address, size, mask, attr). nn::gpu::sdk and
diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp
index d68b6bde0d..772da97ef6 100644
--- a/src/core/hle/service/acc/acc.cpp
+++ b/src/core/hle/service/acc/acc.cpp
@@ -696,9 +696,9 @@ public:
{4, &IManagerForApplication::LoadIdTokenCache, "LoadIdTokenCache"},
{130, &IManagerForApplication::GetNintendoAccountUserResourceCacheForApplication, "GetNintendoAccountUserResourceCacheForApplication"},
{136, &IManagerForApplication::GetNintendoAccountUserResourceCacheForApplication, "GetNintendoAccountUserResourceCache"}, // 19.0.0+
- {150, nullptr, "CreateAuthorizationRequest"},
+ {150, &IManagerForApplication::StubReplyInterface, "CreateAuthorizationRequest"},
{160, &IManagerForApplication::StoreOpenContext, "StoreOpenContext"},
- {170, nullptr, "LoadNetworkServiceLicenseKindAsync"},
+ {170, &IManagerForApplication::StubReplyInterface, "LoadNetworkServiceLicenseKindAsync"},
};
// clang-format on
diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
index db42781e18..061394bff8 100644
--- a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
+++ b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
@@ -222,6 +222,14 @@ void SoftwareKeyboard::InitializeForeground() {
sizeof(SwkbdConfigNew));
swkbd_config_common.use_utf8 = true;
break;
+ case SwkbdAppletVersion::Version1441810: {
+ // Fire Emblem: Fortune's Weave asks for UTF-8 and keeps the reply up to the first NUL, so the
+ // Switch 1 layout's use_utf8 (a byte of the native initial_string_offset) left one letter.
+ const bool decoded =
+ DecodeNativeSwkbdConfig(swkbd_config_data, swkbd_config_common, swkbd_config_new);
+ ASSERT(decoded);
+ break;
+ }
default:
UNIMPLEMENTED_MSG("Unknown SwkbdConfig revision={} with size={}", swkbd_applet_version,
swkbd_config_data.size());
diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
index f263a704cd..8ff8391aa1 100644
--- a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
+++ b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
@@ -7,6 +7,9 @@
#pragma once
#include <array>
+#include <cstddef>
+#include <cstring>
+#include <span>
#include "common/bit_field.h"
#include "common/common_funcs.h"
@@ -31,6 +34,7 @@ enum class SwkbdAppletVersion : u32_le {
Version393227 = 0x6000B, // 6.0.0 - 7.0.1
Version524301 = 0x8000D, // 8.0.0+
Version1310736 = 0x140010, // Switch 2 (Pokemon Pokopia): 0x530-byte config, UTF-8 text
+ Version1441810 = 0x160012, // Switch 2 (Fire Emblem: Fortune's Weave): native 0x530-byte config
};
enum class SwkbdType : u32 {
@@ -184,8 +188,64 @@ struct SwkbdConfigNew {
};
static_assert(sizeof(SwkbdConfigNew) == 0x4C8 - sizeof(SwkbdConfigCommon),
"SwkbdConfigNew has incorrect size.");
+
+// SwkbdAppletVersion 0x160012: the text prefix of the common part keeps its layout, the flags,
+// lengths, string offsets and dictionary fields move (checked against
+// nn::swkbd::keyboardConfig2kbdConfig in Fire Emblem: Fortune's Weave's sdk).
+struct SwkbdConfigNative {
+ std::array<u8, 0x3AA> common_prefix{};
+ bool use_utf8{};
+ bool use_blur_background{};
+ u32 max_text_length{};
+ u32 min_text_length{};
+ SwkbdPasswordMode password_mode{};
+ SwkbdTextDrawType text_draw_type{};
+ u32 initial_string_offset{};
+ u32 initial_string_length{};
+ u32 user_dictionary_offset{};
+ u32 user_dictionary_entries{};
+ std::array<u64, 24> customized_dictionary_set_entries{};
+ u8 total_customized_dictionary_set_entries{};
+ bool use_text_check{};
+ bool disable_cancel_button{};
+ u8 trigger{};
+ std::array<u32, 8> text_grouping{};
+ INSERT_PADDING_BYTES(0x80);
+};
+static_assert(sizeof(SwkbdConfigNative) == 0x530);
+static_assert(offsetof(SwkbdConfigNative, use_text_check) == 0x48D);
+static_assert(offsetof(SwkbdConfigNative, text_grouping) == 0x490);
#pragma pack(pop)
+inline bool DecodeNativeSwkbdConfig(std::span<const u8> data, SwkbdConfigCommon& common,
+ SwkbdConfigNew& config) {
+ if (data.size() != sizeof(SwkbdConfigNative)) {
+ return false;
+ }
+ SwkbdConfigNative native{};
+ std::memcpy(&native, data.data(), sizeof(native));
+ common = {};
+ config = {};
+ std::memcpy(static_cast<void*>(&common), native.common_prefix.data(), native.common_prefix.size());
+ common.enable_return_button = native.common_prefix[0x1B] != 0;
+ common.use_utf8 = native.use_utf8;
+ common.use_blur_background = native.use_blur_background;
+ common.max_text_length = native.max_text_length;
+ common.min_text_length = native.min_text_length;
+ common.password_mode = native.password_mode;
+ common.text_draw_type = native.text_draw_type;
+ common.initial_string_offset = native.initial_string_offset;
+ common.initial_string_length = native.initial_string_length;
+ common.user_dictionary_offset = native.user_dictionary_offset;
+ common.user_dictionary_entries = native.user_dictionary_entries;
+ common.use_text_check = native.use_text_check;
+ config.text_grouping = native.text_grouping;
+ config.customized_dictionary_set_entries = native.customized_dictionary_set_entries;
+ config.total_customized_dictionary_set_entries = native.total_customized_dictionary_set_entries;
+ config.disable_cancel_button = native.disable_cancel_button;
+ return true;
+}
+
struct SwkbdTextCheck {
SwkbdTextCheckResult text_check_result{};
std::array<char16_t, STRING_BUFFER_SIZE / 2> text_check_message{};
diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp
index 37dfbfa344..c63d29b4e9 100644
--- a/src/core/hle/service/am/service/application_functions.cpp
+++ b/src/core/hle/service/am/service/application_functions.cpp
@@ -58,18 +58,19 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_
: ServiceFramework{system_, "IApplicationFunctions"}, m_applet{std::move(applet)} {
// clang-format off
static const FunctionInfo functions[] = {
+ {0, &IApplicationFunctions::StubReplyZeroed, "Unknown0"},
{1, D<&IApplicationFunctions::PopLaunchParameter>, "PopLaunchParameter"},
- {10, nullptr, "CreateApplicationAndPushAndRequestToStart"},
- {11, nullptr, "CreateApplicationAndPushAndRequestToStartForQuest"},
+ {10, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationAndPushAndRequestToStart"},
+ {11, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationAndPushAndRequestToStartForQuest"},
{12, D<&IApplicationFunctions::CreateApplicationAndRequestToStart>, "CreateApplicationAndRequestToStart"},
- {13, nullptr, "CreateApplicationAndRequestToStartForQuest"},
- {14, nullptr, "CreateApplicationWithAttributeAndPushAndRequestToStartForQuest"},
- {15, nullptr, "CreateApplicationWithAttributeAndRequestToStartForQuest"},
+ {13, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationAndRequestToStartForQuest"},
+ {14, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationWithAttributeAndPushAndRequestToStartForQuest"},
+ {15, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationWithAttributeAndRequestToStartForQuest"},
{20, D<&IApplicationFunctions::EnsureSaveData>, "EnsureSaveData"},
{21, D<&IApplicationFunctions::GetDesiredLanguage>, "GetDesiredLanguage"},
{22, D<&IApplicationFunctions::SetTerminateResult>, "SetTerminateResult"},
{23, D<&IApplicationFunctions::GetDisplayVersion>, "GetDisplayVersion"},
- {24, nullptr, "GetLaunchStorageInfoForDebug"},
+ {24, &IApplicationFunctions::StubReplyZeroed, "GetLaunchStorageInfoForDebug"},
{25, D<&IApplicationFunctions::ExtendSaveData>, "ExtendSaveData"},
{26, D<&IApplicationFunctions::GetSaveDataSize>, "GetSaveDataSize"},
{27, D<&IApplicationFunctions::CreateCacheStorage>, "CreateCacheStorage"},
@@ -80,53 +81,57 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_
{32, D<&IApplicationFunctions::BeginBlockingHomeButton>, "BeginBlockingHomeButton"},
{33, D<&IApplicationFunctions::EndBlockingHomeButton>, "EndBlockingHomeButton"},
{34, nullptr, "SelectApplicationLicense"},
- {35, nullptr, "GetDeviceSaveDataSizeMax"},
- {36, nullptr, "GetLimitedApplicationLicense"},
- {37, nullptr, "GetLimitedApplicationLicenseUpgradableEvent"},
+ {35, &IApplicationFunctions::StubReplyZeroed, "GetDeviceSaveDataSizeMax"},
+ {36, &IApplicationFunctions::StubReplyZeroed, "GetLimitedApplicationLicense"},
+ {37, &IApplicationFunctions::StubReplyEvent, "GetLimitedApplicationLicenseUpgradableEvent"},
{40, D<&IApplicationFunctions::NotifyRunning>, "NotifyRunning"},
{50, D<&IApplicationFunctions::GetPseudoDeviceId>, "GetPseudoDeviceId"},
{60, D<&IApplicationFunctions::SetMediaPlaybackStateForApplication>, "SetMediaPlaybackStateForApplication"},
{65, D<&IApplicationFunctions::IsGamePlayRecordingSupported>, "IsGamePlayRecordingSupported"},
{66, D<&IApplicationFunctions::InitializeGamePlayRecording>, "InitializeGamePlayRecording"},
{67, D<&IApplicationFunctions::SetGamePlayRecordingState>, "SetGamePlayRecordingState"},
- {68, nullptr, "RequestFlushGamePlayingMovieForDebug"},
- {70, nullptr, "RequestToShutdown"},
- {71, nullptr, "RequestToReboot"},
- {72, nullptr, "RequestToSleep"},
- {80, nullptr, "ExitAndRequestToShowThanksMessage"},
+ {68, &IApplicationFunctions::StubReplyZeroed, "RequestFlushGamePlayingMovieForDebug"},
+ {70, &IApplicationFunctions::StubReplyZeroed, "RequestToShutdown"},
+ {71, &IApplicationFunctions::StubReplyZeroed, "RequestToReboot"},
+ {72, &IApplicationFunctions::StubReplyZeroed, "RequestToSleep"},
+ {80, &IApplicationFunctions::StubReplyZeroed, "ExitAndRequestToShowThanksMessage"},
{90, D<&IApplicationFunctions::EnableApplicationCrashReport>, "EnableApplicationCrashReport"},
{100, D<&IApplicationFunctions::InitializeApplicationCopyrightFrameBuffer>, "InitializeApplicationCopyrightFrameBuffer"},
{101, D<&IApplicationFunctions::SetApplicationCopyrightImage>, "SetApplicationCopyrightImage"},
{102, D<&IApplicationFunctions::SetApplicationCopyrightVisibility>, "SetApplicationCopyrightVisibility"},
{110, D<&IApplicationFunctions::QueryApplicationPlayStatistics>, "QueryApplicationPlayStatistics"},
{111, D<&IApplicationFunctions::QueryApplicationPlayStatisticsByUid>, "QueryApplicationPlayStatisticsByUid"},
+ {112, &IApplicationFunctions::StubReplyZeroed, "Unknown112"},
+ {113, &IApplicationFunctions::StubReplyZeroed, "Unknown113"},
{120, D<&IApplicationFunctions::ExecuteProgram>, "ExecuteProgram"},
{121, D<&IApplicationFunctions::ClearUserChannel>, "ClearUserChannel"},
{122, D<&IApplicationFunctions::UnpopToUserChannel>, "UnpopToUserChannel"},
{123, D<&IApplicationFunctions::GetPreviousProgramIndex>, "GetPreviousProgramIndex"},
- {124, nullptr, "EnableApplicationAllThreadDumpOnCrash"},
+ {124, &IApplicationFunctions::StubReplyZeroed, "EnableApplicationAllThreadDumpOnCrash"},
{130, D<&IApplicationFunctions::GetGpuErrorDetectedSystemEvent>, "GetGpuErrorDetectedSystemEvent"},
- {131, nullptr, "SetDelayTimeToAbortOnGpuError"},
+ {131, &IApplicationFunctions::StubReplyZeroed, "SetDelayTimeToAbortOnGpuError"},
{140, D<&IApplicationFunctions::GetFriendInvitationStorageChannelEvent>, "GetFriendInvitationStorageChannelEvent"},
{141, D<&IApplicationFunctions::TryPopFromFriendInvitationStorageChannel>, "TryPopFromFriendInvitationStorageChannel"},
{150, D<&IApplicationFunctions::GetNotificationStorageChannelEvent>, "GetNotificationStorageChannelEvent"},
- {151, nullptr, "TryPopFromNotificationStorageChannel"},
+ {151, &IApplicationFunctions::StubReplyInterface, "TryPopFromNotificationStorageChannel"},
{160, D<&IApplicationFunctions::GetHealthWarningDisappearedSystemEvent>, "GetHealthWarningDisappearedSystemEvent"},
- {170, nullptr, "SetHdcpAuthenticationActivated"},
- {180, nullptr, "GetLaunchRequiredVersion"},
- {181, nullptr, "UpgradeLaunchRequiredVersion"},
- {190, nullptr, "SendServerMaintenanceOverlayNotification"},
- {200, nullptr, "GetLastApplicationExitReason"},
+ {170, &IApplicationFunctions::StubReplyZeroed, "SetHdcpAuthenticationActivated"},
+ {180, &IApplicationFunctions::StubReplyZeroed, "GetLaunchRequiredVersion"},
+ {181, &IApplicationFunctions::StubReplyZeroed, "UpgradeLaunchRequiredVersion"},
+ {190, &IApplicationFunctions::StubReplyZeroed, "SendServerMaintenanceOverlayNotification"},
+ {200, &IApplicationFunctions::StubReplyZeroed, "GetLastApplicationExitReason"},
{210, D<&IApplicationFunctions::GetOperationModeRefreshEvent>, "GetOperationModeRefreshEvent (guessed)"},
- {220, nullptr, "Unknown220"}, // [20.0.0+]
- {300, nullptr, "CreateMovieWriter"}, // [19.0.0+]
- {310, nullptr, "Unknown310"}, // [20.0.0+]
+ {220, &IApplicationFunctions::StubReplyZeroed, "Unknown220"}, // [20.0.0+]
+ {230, &IApplicationFunctions::StubReplyZeroed, "Unknown230"},
+ {300, &IApplicationFunctions::StubReplyInterface, "CreateMovieWriter"}, // [19.0.0+]
+ {310, &IApplicationFunctions::StubReplyZeroed, "Unknown310"}, // [20.0.0+]
{320, D<&IApplicationFunctions::Unknown320>, "Unknown320"}, // [20.0.0+]
{321, D<&IApplicationFunctions::Unknown321>, "Unknown321"}, // Switch 2
{330, D<&IApplicationFunctions::Unknown330>, "Unknown330"}, // [20.0.0+]
- {500, nullptr, "StartContinuousRecordingFlushForDebug"},
- {1000, nullptr, "CreateMovieMaker"},
+ {500, &IApplicationFunctions::StubReplyEvent, "StartContinuousRecordingFlushForDebug"},
+ {1000, &IApplicationFunctions::StubReplyInterface, "CreateMovieMaker"},
{1001, D<&IApplicationFunctions::PrepareForJit>, "PrepareForJit"},
+ {2000, &IApplicationFunctions::StubReplyZeroed, "Unknown2000"},
};
// clang-format on
@@ -571,15 +576,18 @@ Result IApplicationFunctions::GetOperationModeRefreshEvent(
R_SUCCEED();
}
-Result IApplicationFunctions::Unknown320() {
- // Called by Switch 2 titles during nn::oe::Initialize; arguments unknown.
+Result IApplicationFunctions::Unknown320(Out<u16> out) {
+ // Called by Switch 2 titles during nn::oe::Initialize. It returns two bytes of data (meaning
+ // unknown); a reply without them left the caller reading stale message words.
LOG_WARNING(Service_AM, "(STUBBED) called");
+ *out = 0;
R_SUCCEED();
}
-Result IApplicationFunctions::Unknown321() {
- // Called by Switch 2 titles right after 320; arguments unknown.
+Result IApplicationFunctions::Unknown321(Out<u8> out) {
+ // Called by Switch 2 titles right after 320; returns one byte (meaning unknown).
LOG_WARNING(Service_AM, "(STUBBED) called");
+ *out = 0;
R_SUCCEED();
}
diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h
index 44b5fadff0..aca2f8c694 100644
--- a/src/core/hle/service/am/service/application_functions.h
+++ b/src/core/hle/service/am/service/application_functions.h
@@ -83,8 +83,8 @@ private:
Result GetHealthWarningDisappearedSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
Result CreateApplicationAndRequestToStart(u64 application_id);
Result GetOperationModeRefreshEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
- Result Unknown320();
- Result Unknown321();
+ Result Unknown320(Out<u16> out);
+ Result Unknown321(Out<u8> out);
Result Unknown330(Out<u8> out);
Result PrepareForJit();
diff --git a/src/core/hle/service/am/service/application_proxy_service.cpp b/src/core/hle/service/am/service/application_proxy_service.cpp
index 67306aadc0..9d332798b4 100644
--- a/src/core/hle/service/am/service/application_proxy_service.cpp
+++ b/src/core/hle/service/am/service/application_proxy_service.cpp
@@ -21,6 +21,7 @@ IApplicationProxyService::IApplicationProxyService(Core::System& system_,
{0, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy"},
// Switch 2 SDKs open the proxy with command 10, same arguments.
{10, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy (Switch 2)"},
+ {2000, &IApplicationProxyService::StubReplyInterface, "Unknown2000"},
};
RegisterHandlers(functions);
}
diff --git a/src/core/hle/service/am/service/audio_controller.cpp b/src/core/hle/service/am/service/audio_controller.cpp
index 4e9af756f2..7901f67c27 100644
--- a/src/core/hle/service/am/service/audio_controller.cpp
+++ b/src/core/hle/service/am/service/audio_controller.cpp
@@ -18,7 +18,7 @@ IAudioController::IAudioController(Core::System& system_)
{2, D<&IAudioController::GetLibraryAppletExpectedMasterVolume>, "GetLibraryAppletExpectedMasterVolume"},
{3, D<&IAudioController::ChangeMainAppletMasterVolume>, "ChangeMainAppletMasterVolume"},
{4, D<&IAudioController::SetTransparentVolumeRate>, "SetTransparentVolumeRate"},
- {5, nullptr, "Unknown5"}, //20.0.0+
+ {5, &IAudioController::StubReplyZeroed, "Unknown5"}, //20.0.0+
};
// clang-format on
diff --git a/src/core/hle/service/am/service/common_state_getter.cpp b/src/core/hle/service/am/service/common_state_getter.cpp
index 044929e434..2c5e536414 100644
--- a/src/core/hle/service/am/service/common_state_getter.cpp
+++ b/src/core/hle/service/am/service/common_state_getter.cpp
@@ -24,67 +24,73 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, std::shared_ptr<Ap
static const FunctionInfo functions[] = {
{0, D<&ICommonStateGetter::GetEventHandle>, "GetEventHandle"},
{1, D<&ICommonStateGetter::ReceiveMessage>, "ReceiveMessage"},
- {2, nullptr, "GetThisAppletKind"},
- {3, nullptr, "AllowToEnterSleep"},
- {4, nullptr, "DisallowToEnterSleep"},
+ {2, &ICommonStateGetter::StubReplyZeroed, "GetThisAppletKind"},
+ {3, &ICommonStateGetter::StubReplyZeroed, "AllowToEnterSleep"},
+ {4, &ICommonStateGetter::StubReplyZeroed, "DisallowToEnterSleep"},
{5, D<&ICommonStateGetter::GetOperationMode>, "GetOperationMode"},
{6, D<&ICommonStateGetter::GetPerformanceMode>, "GetPerformanceMode"},
- {7, nullptr, "GetCradleStatus"},
+ {7, &ICommonStateGetter::StubReplyZeroed, "GetCradleStatus"},
{8, D<&ICommonStateGetter::GetBootMode>, "GetBootMode"},
{9, D<&ICommonStateGetter::GetCurrentFocusState>, "GetCurrentFocusState"},
{10, D<&ICommonStateGetter::RequestToAcquireSleepLock>, "RequestToAcquireSleepLock"},
{11, D<&ICommonStateGetter::ReleaseSleepLock>, "ReleaseSleepLock"},
{12, D<&ICommonStateGetter::ReleaseSleepLockTransiently>, "ReleaseSleepLockTransiently"},
{13, D<&ICommonStateGetter::GetAcquiredSleepLockEvent>, "GetAcquiredSleepLockEvent"},
- {14, nullptr, "GetWakeupCount"}, //11.0.0+
- {15, nullptr, "Unknown15"}, //19.0.0+
+ {14, &ICommonStateGetter::StubReplyZeroed, "GetWakeupCount"}, //11.0.0+
+ {15, &ICommonStateGetter::StubReplyEvent, "Unknown15"}, //19.0.0+
{20, D<&ICommonStateGetter::PushToGeneralChannel>, "PushToGeneralChannel"},
{30, D<&ICommonStateGetter::GetHomeButtonReaderLockAccessor>, "GetHomeButtonReaderLockAccessor"},
{31, D<&ICommonStateGetter::GetReaderLockAccessorEx>, "GetReaderLockAccessorEx"}, //2.0.0+
{32, D<&ICommonStateGetter::GetWriterLockAccessorEx>, "GetWriterLockAccessorEx"}, //7.0.0+
- {40, nullptr, "GetCradleFwVersion"}, //2.0.0+
+ {40, &ICommonStateGetter::StubReplyZeroed, "GetCradleFwVersion"}, //2.0.0+
{50, D<&ICommonStateGetter::IsVrModeEnabled>, "IsVrModeEnabled"}, //3.0.0+
{51, D<&ICommonStateGetter::SetVrModeEnabled>, "SetVrModeEnabled"}, //3.0.0+
{52, D<&ICommonStateGetter::SetLcdBacklighOffEnabled>, "SetLcdBacklighOffEnabled"}, //4.0.0+
{53, D<&ICommonStateGetter::BeginVrModeEx>, "BeginVrModeEx"}, //7.0.0+
{54, D<&ICommonStateGetter::EndVrModeEx>, "EndVrModeEx"}, //7.0.0+
{55, D<&ICommonStateGetter::IsInControllerFirmwareUpdateSection>, "IsInControllerFirmwareUpdateSection"}, //3.0.0+
- {59, nullptr, "SetVrPositionForDebug"}, //1.0.0+
+ {59, &ICommonStateGetter::StubReplyZeroed, "SetVrPositionForDebug"}, //1.0.0+
{60, D<&ICommonStateGetter::GetDefaultDisplayResolution>, "GetDefaultDisplayResolution"},
{61, D<&ICommonStateGetter::GetDefaultDisplayResolutionChangeEvent>, "GetDefaultDisplayResolutionChangeEvent"},
{62, D<&ICommonStateGetter::GetHdcpAuthenticationState>, "GetHdcpAuthenticationState"},
{63, D<&ICommonStateGetter::GetHdcpAuthenticationStateChangeEvent>, "GetHdcpAuthenticationStateChangeEvent"},
- {64, nullptr, "SetTvPowerStateMatchingMode"},
- {65, nullptr, "GetApplicationIdByContentActionName"},
+ {64, &ICommonStateGetter::StubReplyZeroed, "SetTvPowerStateMatchingMode"},
+ {65, &ICommonStateGetter::StubReplyZeroed, "GetApplicationIdByContentActionName"},
{66, &ICommonStateGetter::SetCpuBoostMode, "SetCpuBoostMode"},
- {67, nullptr, "CancelCpuBoostMode"},
+ {67, &ICommonStateGetter::StubReplyZeroed, "CancelCpuBoostMode"},
{68, D<&ICommonStateGetter::GetBuiltInDisplayType>, "GetBuiltInDisplayType"},
{80, D<&ICommonStateGetter::PerformSystemButtonPressingIfInFocus>, "PerformSystemButtonPressingIfInFocus"},
- {90, nullptr, "SetPerformanceConfigurationChangedNotification"},
- {91, nullptr, "GetCurrentPerformanceConfiguration"},
+ {90, &ICommonStateGetter::StubReplyZeroed, "SetPerformanceConfigurationChangedNotification"},
+ {91, &ICommonStateGetter::StubReplyZeroed, "GetCurrentPerformanceConfiguration"},
{100, D<&ICommonStateGetter::SetHandlingHomeButtonShortPressedEnabled>, "SetHandlingHomeButtonShortPressedEnabled"},
- {110, nullptr, "OpenMyGpuErrorHandler"},
+ {110, &ICommonStateGetter::StubReplyInterface, "OpenMyGpuErrorHandler"},
{120, D<&ICommonStateGetter::GetAppletLaunchedHistory>, "GetAppletLaunchedHistory"}, //13.0.0+
{130, D<&ICommonStateGetter::EnableStartupLogoDisappearedMessage>, "EnableStartupLogoDisappearedMessage"}, //21.0.0+
{200, D<&ICommonStateGetter::GetOperationModeSystemInfo>, "GetOperationModeSystemInfo"},
{300, D<&ICommonStateGetter::GetSettingsPlatformRegion>, "GetSettingsPlatformRegion"},
- {400, nullptr, "ActivateMigrationService"},
- {401, nullptr, "DeactivateMigrationService"},
- {500, nullptr, "DisableSleepTillShutdown"},
- {501, nullptr, "SuppressDisablingSleepTemporarily"},
- {502, nullptr, "IsSleepEnabled"},
- {503, nullptr, "IsDisablingSleepSuppressed"},
- {600, nullptr, "SetHidInputMagnificationForApplication"}, //20.0.0+
+ {310, &ICommonStateGetter::StubReplyZeroed, "Unknown310"},
+ {320, &ICommonStateGetter::StubReplyZeroed, "Unknown320"},
+ {400, &ICommonStateGetter::StubReplyZeroed, "ActivateMigrationService"},
+ {401, &ICommonStateGetter::StubReplyZeroed, "DeactivateMigrationService"},
+ {500, &ICommonStateGetter::StubReplyZeroed, "DisableSleepTillShutdown"},
+ {501, &ICommonStateGetter::StubReplyZeroed, "SuppressDisablingSleepTemporarily"},
+ {502, &ICommonStateGetter::StubReplyZeroed, "IsSleepEnabled"},
+ {503, &ICommonStateGetter::StubReplyZeroed, "IsDisablingSleepSuppressed"},
+ {600, &ICommonStateGetter::StubReplyZeroed, "SetHidInputMagnificationForApplication"}, //20.0.0+
{610, D<&ICommonStateGetter::Unknown610>, "Unknown610"}, //21.0.0+
{611, D<&ICommonStateGetter::Unknown611>, "Unknown611"}, //22.0.0+
+ {650, &ICommonStateGetter::StubReplyZeroed, "Unknown650"},
+ {651, &ICommonStateGetter::StubReplyZeroed, "Unknown651"},
{900, D<&ICommonStateGetter::SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled>, "SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled"}, //11.0.0+
- {910, nullptr, "GetLaunchRequiredTick"}, //17.0.0+
+ {910, &ICommonStateGetter::StubReplyZeroed, "GetLaunchRequiredTick"}, //17.0.0+
{1000, D<&ICommonStateGetter::BeginVrMode3d>, "BeginVrMode3d"}, //19.0.0+
{1001, D<&ICommonStateGetter::EndVrMode3d>, "EndVrMode3d"}, //19.0.0+
{1002, D<&ICommonStateGetter::IsVrModeEnabled3d>, "IsVrModeEnabled3d"}, //19.0.0+
{1003, D<&ICommonStateGetter::GetVrLaboGoggleViewport>, "GetVrLaboGoggleViewport"}, //21.0.0+
{1004, D<&ICommonStateGetter::GetPanelPhysicalSizeForSpecificTitle>, "GetPanelPhysicalSizeForSpecificTitle"}, //21.0.0+
{1005, D<&ICommonStateGetter::GetPanelResolutionForSpecificTitle>, "GetPanelResolutionForSpecificTitle"}, //21.0.0+
+ {2000, &ICommonStateGetter::StubReplyZeroed, "Unknown2000"},
+ {2010, &ICommonStateGetter::StubReplyZeroed, "Unknown2010"},
};
// clang-format on
diff --git a/src/core/hle/service/am/service/display_controller.cpp b/src/core/hle/service/am/service/display_controller.cpp
index 8f87327b17..7e7db23708 100644
--- a/src/core/hle/service/am/service/display_controller.cpp
+++ b/src/core/hle/service/am/service/display_controller.cpp
@@ -16,34 +16,35 @@ IDisplayController::IDisplayController(Core::System& system_, std::shared_ptr<Ap
: ServiceFramework{system_, "IDisplayController"}, applet(std::move(applet_)) {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "GetLastForegroundCaptureImage"},
- {1, nullptr, "UpdateLastForegroundCaptureImage"},
- {2, nullptr, "GetLastApplicationCaptureImage"},
- {3, nullptr, "GetCallerAppletCaptureImage"},
- {4, nullptr, "UpdateCallerAppletCaptureImage"},
+ {0, &IDisplayController::StubReplyZeroed, "GetLastForegroundCaptureImage"},
+ {1, &IDisplayController::StubReplyZeroed, "UpdateLastForegroundCaptureImage"},
+ {2, &IDisplayController::StubReplyZeroed, "GetLastApplicationCaptureImage"},
+ {3, &IDisplayController::StubReplyZeroed, "GetCallerAppletCaptureImage"},
+ {4, &IDisplayController::StubReplyZeroed, "UpdateCallerAppletCaptureImage"},
{5, D<&IDisplayController::GetLastForegroundCaptureImageEx>, "GetLastForegroundCaptureImageEx"},
- {6, nullptr, "GetLastApplicationCaptureImageEx"},
+ {6, &IDisplayController::StubReplyZeroed, "GetLastApplicationCaptureImageEx"},
{7, D<&IDisplayController::GetCallerAppletCaptureImageEx>, "GetCallerAppletCaptureImageEx"},
{8, D<&IDisplayController::TakeScreenShotOfOwnLayer>, "TakeScreenShotOfOwnLayer"},
- {9, nullptr, "CopyBetweenCaptureBuffers"},
- {10, nullptr, "AcquireLastApplicationCaptureBuffer"},
- {11, nullptr, "ReleaseLastApplicationCaptureBuffer"},
- {12, nullptr, "AcquireLastForegroundCaptureBuffer"},
- {13, nullptr, "ReleaseLastForegroundCaptureBuffer"},
- {14, nullptr, "AcquireCallerAppletCaptureBuffer"},
- {15, nullptr, "ReleaseCallerAppletCaptureBuffer"},
- {16, nullptr, "AcquireLastApplicationCaptureBufferEx"},
- {17, nullptr, "AcquireLastForegroundCaptureBufferEx"},
- {18, nullptr, "AcquireCallerAppletCaptureBufferEx"},
+ {9, &IDisplayController::StubReplyZeroed, "CopyBetweenCaptureBuffers"},
+ {10, &IDisplayController::StubReplyEvent, "AcquireLastApplicationCaptureBuffer"},
+ {11, &IDisplayController::StubReplyZeroed, "ReleaseLastApplicationCaptureBuffer"},
+ {12, &IDisplayController::StubReplyEvent, "AcquireLastForegroundCaptureBuffer"},
+ {13, &IDisplayController::StubReplyZeroed, "ReleaseLastForegroundCaptureBuffer"},
+ {14, &IDisplayController::StubReplyEvent, "AcquireCallerAppletCaptureBuffer"},
+ {15, &IDisplayController::StubReplyZeroed, "ReleaseCallerAppletCaptureBuffer"},
+ {16, &IDisplayController::StubReplyEvent, "AcquireLastApplicationCaptureBufferEx"},
+ {17, &IDisplayController::StubReplyEvent, "AcquireLastForegroundCaptureBufferEx"},
+ {18, &IDisplayController::StubReplyEvent, "AcquireCallerAppletCaptureBufferEx"},
+ {19, &IDisplayController::StubReplyZeroed, "Unknown19"},
{20, D<&IDisplayController::ClearCaptureBuffer>, "ClearCaptureBuffer"},
- {21, nullptr, "ClearAppletTransitionBuffer"},
+ {21, &IDisplayController::StubReplyZeroed, "ClearAppletTransitionBuffer"},
{22, D<&IDisplayController::AcquireLastApplicationCaptureSharedBuffer>, "AcquireLastApplicationCaptureSharedBuffer"},
{23, D<&IDisplayController::ReleaseLastApplicationCaptureSharedBuffer>, "ReleaseLastApplicationCaptureSharedBuffer"},
{24, D<&IDisplayController::AcquireLastForegroundCaptureSharedBuffer>, "AcquireLastForegroundCaptureSharedBuffer"},
{25, D<&IDisplayController::ReleaseLastForegroundCaptureSharedBuffer>, "ReleaseLastForegroundCaptureSharedBuffer"},
{26, D<&IDisplayController::AcquireCallerAppletCaptureSharedBuffer>, "AcquireCallerAppletCaptureSharedBuffer"},
{27, D<&IDisplayController::ReleaseCallerAppletCaptureSharedBuffer>, "ReleaseCallerAppletCaptureSharedBuffer"},
- {28, nullptr, "TakeScreenShotOfOwnLayerEx"},
+ {28, &IDisplayController::StubReplyZeroed, "TakeScreenShotOfOwnLayerEx"},
};
// clang-format on
diff --git a/src/core/hle/service/am/service/graphics_scale_controller.h b/src/core/hle/service/am/service/graphics_scale_controller.h
index 07e38c6ac8..b31858d3ed 100644
--- a/src/core/hle/service/am/service/graphics_scale_controller.h
+++ b/src/core/hle/service/am/service/graphics_scale_controller.h
@@ -25,9 +25,13 @@ public:
}
private:
- Result GetCurrentGraphicsScale(Out<u32> out_scale) {
+ // The reply is 12 bytes; only the first word (the scale) is known. The other two used to be
+ // left to the message buffer's leftovers.
+ Result GetCurrentGraphicsScale(Out<u32> out_scale, Out<u32> out_unknown1, Out<u32> out_unknown2) {
LOG_WARNING(Service_AM, "(STUBBED) called, returning 10");
*out_scale = 10;
+ *out_unknown1 = 0;
+ *out_unknown2 = 0;
R_SUCCEED();
}
};
diff --git a/src/core/hle/service/am/service/library_applet_accessor.cpp b/src/core/hle/service/am/service/library_applet_accessor.cpp
index 849e5794cf..7119c60596 100644
--- a/src/core/hle/service/am/service/library_applet_accessor.cpp
+++ b/src/core/hle/service/am/service/library_applet_accessor.cpp
@@ -62,21 +62,22 @@ ILibraryAppletAccessor::ILibraryAppletAccessor(Core::System& system_,
{20, D<&ILibraryAppletAccessor::RequestExit>, "RequestExit"},
{25, D<&ILibraryAppletAccessor::Terminate>, "Terminate"},
{30, D<&ILibraryAppletAccessor::GetResult>, "GetResult"},
- {50, nullptr, "SetOutOfFocusApplicationSuspendingEnabled"},
+ {50, &ILibraryAppletAccessor::StubReplyZeroed, "SetOutOfFocusApplicationSuspendingEnabled"},
{60, D<&ILibraryAppletAccessor::PresetLibraryAppletGpuTimeSliceZero>, "PresetLibraryAppletGpuTimeSliceZero"}, //10.0.0+
- {80, nullptr, "RequestForLibraryAppletToGetForeground"}, //19.0.0+
- {81, nullptr, "GetCurrentChildLibraryApplet"}, //19.0.0+
+ {70, &ILibraryAppletAccessor::StubReplyZeroed, "Unknown70"},
+ {80, &ILibraryAppletAccessor::StubReplyZeroed, "RequestForLibraryAppletToGetForeground"}, //19.0.0+
+ {81, &ILibraryAppletAccessor::StubReplyInterface, "GetCurrentChildLibraryApplet"}, //19.0.0+
{90, D<&ILibraryAppletAccessor::Unknown90>, "Unknown90"}, //20.0.0+
{100, D<&ILibraryAppletAccessor::PushInData>, "PushInData"},
{101, D<&ILibraryAppletAccessor::PopOutData>, "PopOutData"},
- {102, nullptr, "PushExtraStorage"},
+ {102, &ILibraryAppletAccessor::StubReplyZeroed, "PushExtraStorage"},
{103, D<&ILibraryAppletAccessor::PushInteractiveInData>, "PushInteractiveInData"},
{104, D<&ILibraryAppletAccessor::PopInteractiveOutData>, "PopInteractiveOutData"},
{105, D<&ILibraryAppletAccessor::GetPopOutDataEvent>, "GetPopOutDataEvent"},
{106, D<&ILibraryAppletAccessor::GetPopInteractiveOutDataEvent>, "GetPopInteractiveOutDataEvent"},
- {110, nullptr, "NeedsToExitProcess"},
+ {110, &ILibraryAppletAccessor::StubReplyZeroed, "NeedsToExitProcess"},
{120, D<&ILibraryAppletAccessor::GetLibraryAppletInfo>, "GetLibraryAppletInfo"},
- {150, nullptr, "RequestForAppletToGetForeground"},
+ {150, &ILibraryAppletAccessor::StubReplyZeroed, "RequestForAppletToGetForeground"},
{160, D<&ILibraryAppletAccessor::GetIndirectLayerConsumerHandle>, "GetIndirectLayerConsumerHandle"}, //2.0.0+
{170, D<&ILibraryAppletAccessor::Unknown170>, "Unknown170"}, //22.0.0+
};
diff --git a/src/core/hle/service/am/service/library_applet_creator.cpp b/src/core/hle/service/am/service/library_applet_creator.cpp
index afdb609c00..2993ed7436 100644
--- a/src/core/hle/service/am/service/library_applet_creator.cpp
+++ b/src/core/hle/service/am/service/library_applet_creator.cpp
@@ -204,8 +204,8 @@ ILibraryAppletCreator::ILibraryAppletCreator(Core::System& system_, std::shared_
m_window_system{window_system}, m_applet{std::move(applet)} {
static const FunctionInfo functions[] = {
{0, D<&ILibraryAppletCreator::CreateLibraryApplet>, "CreateLibraryApplet"},
- {1, nullptr, "TerminateAllLibraryApplets"},
- {2, nullptr, "AreAnyLibraryAppletsLeft"},
+ {1, &ILibraryAppletCreator::StubReplyZeroed, "TerminateAllLibraryApplets"},
+ {2, &ILibraryAppletCreator::StubReplyZeroed, "AreAnyLibraryAppletsLeft"},
{3, D<&ILibraryAppletCreator::CreateLibraryAppletEx>, "CreateLibraryAppletEx"},
{10, D<&ILibraryAppletCreator::CreateStorage>, "CreateStorage"},
{11, D<&ILibraryAppletCreator::CreateTransferMemoryStorage>, "CreateTransferMemoryStorage"},
diff --git a/src/core/hle/service/am/service/self_controller.cpp b/src/core/hle/service/am/service/self_controller.cpp
index a8078e6048..d67e1b32cd 100644
--- a/src/core/hle/service/am/service/self_controller.cpp
+++ b/src/core/hle/service/am/service/self_controller.cpp
@@ -37,44 +37,59 @@ ISelfController::ISelfController(Core::System& system_, std::shared_ptr<Applet>
{14, D<&ISelfController::SetRestartMessageEnabled>, "SetRestartMessageEnabled"},
{15, D<&ISelfController::SetScreenShotAppletIdentityInfo>, "SetScreenShotAppletIdentityInfo"},
{16, D<&ISelfController::SetOutOfFocusSuspendingEnabled>, "SetOutOfFocusSuspendingEnabled"},
- {17, nullptr, "SetControllerFirmwareUpdateSection"},
- {18, nullptr, "SetRequiresCaptureButtonShortPressedMessage"},
+ {17, &ISelfController::StubReplyZeroed, "SetControllerFirmwareUpdateSection"},
+ {18, &ISelfController::StubReplyZeroed, "SetRequiresCaptureButtonShortPressedMessage"},
{19, D<&ISelfController::SetAlbumImageOrientation>, "SetAlbumImageOrientation"},
- {20, nullptr, "SetDesirableKeyboardLayout"},
- {21, nullptr, "GetScreenShotProgramId"},
+ {20, &ISelfController::StubReplyZeroed, "SetDesirableKeyboardLayout"},
+ {21, &ISelfController::StubReplyZeroed, "GetScreenShotProgramId"},
+ {22, &ISelfController::StubReplyZeroed, "Unknown22"},
+ {23, &ISelfController::StubReplyZeroed, "Unknown23"},
+ {24, &ISelfController::StubReplyZeroed, "Unknown24"},
{40, D<&ISelfController::CreateManagedDisplayLayer>, "CreateManagedDisplayLayer"},
{41, D<&ISelfController::IsSystemBufferSharingEnabled>, "IsSystemBufferSharingEnabled"},
{42, D<&ISelfController::GetSystemSharedLayerHandle>, "GetSystemSharedLayerHandle"},
{43, D<&ISelfController::GetSystemSharedBufferHandle>, "GetSystemSharedBufferHandle"},
{44, D<&ISelfController::CreateManagedDisplaySeparableLayer>, "CreateManagedDisplaySeparableLayer"},
- {45, nullptr, "SetManagedDisplayLayerSeparationMode"},
- {46, nullptr, "SetRecordingLayerCompositionEnabled"},
+ {45, &ISelfController::StubReplyZeroed, "SetManagedDisplayLayerSeparationMode"},
+ {46, &ISelfController::StubReplyZeroed, "SetRecordingLayerCompositionEnabled"},
{50, D<&ISelfController::SetHandlesRequestToDisplay>, "SetHandlesRequestToDisplay"},
{51, D<&ISelfController::ApproveToDisplay>, "ApproveToDisplay"},
{60, D<&ISelfController::OverrideAutoSleepTimeAndDimmingTime>, "OverrideAutoSleepTimeAndDimmingTime"},
{61, D<&ISelfController::SetMediaPlaybackState>, "SetMediaPlaybackState"},
{62, D<&ISelfController::SetIdleTimeDetectionExtension>, "SetIdleTimeDetectionExtension"},
{63, D<&ISelfController::GetIdleTimeDetectionExtension>, "GetIdleTimeDetectionExtension"},
- {64, nullptr, "SetInputDetectionSourceSet"},
+ {64, &ISelfController::StubReplyZeroed, "SetInputDetectionSourceSet"},
{65, D<&ISelfController::ReportUserIsActive>, "ReportUserIsActive"},
- {66, nullptr, "GetCurrentIlluminance"},
+ {66, &ISelfController::StubReplyZeroed, "GetCurrentIlluminance"},
{67, D<&ISelfController::IsIlluminanceAvailable>, "IsIlluminanceAvailable"},
{68, D<&ISelfController::SetAutoSleepDisabled>, "SetAutoSleepDisabled"},
{69, D<&ISelfController::IsAutoSleepDisabled>, "IsAutoSleepDisabled"},
- {70, nullptr, "ReportMultimediaError"},
- {71, nullptr, "GetCurrentIlluminanceEx"},
+ {70, &ISelfController::StubReplyZeroed, "ReportMultimediaError"},
+ {71, &ISelfController::StubReplyZeroed, "GetCurrentIlluminanceEx"},
{72, D<&ISelfController::SetInputDetectionPolicy>, "SetInputDetectionPolicy"},
- {80, nullptr, "SetWirelessPriorityMode"},
+ {73, &ISelfController::StubReplyZeroed, "Unknown73"},
+ {80, &ISelfController::StubReplyZeroed, "SetWirelessPriorityMode"},
{90, D<&ISelfController::GetAccumulatedSuspendedTickValue>, "GetAccumulatedSuspendedTickValue"},
{91, D<&ISelfController::GetAccumulatedSuspendedTickChangedEvent>, "GetAccumulatedSuspendedTickChangedEvent"},
{100, D<&ISelfController::SetAlbumImageTakenNotificationEnabled>, "SetAlbumImageTakenNotificationEnabled"},
- {110, nullptr, "SetApplicationAlbumUserData"},
+ {110, &ISelfController::StubReplyZeroed, "SetApplicationAlbumUserData"},
{120, D<&ISelfController::SaveCurrentScreenshot>, "SaveCurrentScreenshot"},
{130, D<&ISelfController::SetRecordVolumeMuted>, "SetRecordVolumeMuted"},
+ {200, &ISelfController::StubReplyZeroed, "Unknown200"},
+ {210, &ISelfController::StubReplyZeroed, "Unknown210"},
+ {211, &ISelfController::StubReplyZeroed, "Unknown211"},
+ {220, &ISelfController::StubReplyZeroed, "Unknown220"},
+ {221, &ISelfController::StubReplyZeroed, "Unknown221"},
{230, D<&ISelfController::Unknown230>, "Unknown230"},
{240, D<&ISelfController::Unknown240>, "Unknown240"},
- {1000, nullptr, "GetDebugStorageChannel"},
+ {1000, &ISelfController::StubReplyInterface, "GetDebugStorageChannel"},
{2000, &ISelfController::GetNx2GraphicsSystemSession, "GetGraphicsSystemSession (Switch 2, guessed)"},
+ {2100, &ISelfController::StubReplyZeroed, "Unknown2100"},
+ {2200, &ISelfController::StubReplyZeroed, "Unknown2200"},
+ {2300, &ISelfController::StubReplyZeroed, "Unknown2300"},
+ {2400, &ISelfController::StubReplyZeroed, "Unknown2400"},
+ {2401, &ISelfController::StubReplyZeroed, "Unknown2401"},
+ {2402, &ISelfController::StubReplyZeroed, "Unknown2402"},
};
// clang-format on
diff --git a/src/core/hle/service/am/service/window_controller.cpp b/src/core/hle/service/am/service/window_controller.cpp
index f2b9dccb27..20bf71083e 100644
--- a/src/core/hle/service/am/service/window_controller.cpp
+++ b/src/core/hle/service/am/service/window_controller.cpp
@@ -18,7 +18,7 @@ IWindowController::IWindowController(Core::System& system_, std::shared_ptr<Appl
m_window_system{window_system}, m_applet{std::move(applet)} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "CreateWindow"},
+ {0, &IWindowController::StubReplyInterface, "CreateWindow"},
{1, D<&IWindowController::GetAppletResourceUserId>, "GetAppletResourceUserId"},
{2, D<&IWindowController::GetAppletResourceUserIdOfCallerApplet>, "GetAppletResourceUserIdOfCallerApplet"},
{10, D<&IWindowController::AcquireForegroundRights>, "AcquireForegroundRights"},
diff --git a/src/core/hle/service/aoc/addon_content_manager.cpp b/src/core/hle/service/aoc/addon_content_manager.cpp
index be2aa34baa..c63b08e40d 100644
--- a/src/core/hle/service/aoc/addon_content_manager.cpp
+++ b/src/core/hle/service/aoc/addon_content_manager.cpp
@@ -68,19 +68,19 @@ IAddOnContentManager::IAddOnContentManager(Core::System& system_)
{6, nullptr, "PrepareAddOnContentByApplicationId"},
{7, D<&IAddOnContentManager::PrepareAddOnContent>, "PrepareAddOnContent"},
{8, D<&IAddOnContentManager::GetAddOnContentListChangedEvent>, "GetAddOnContentListChangedEvent"},
- {9, nullptr, "GetAddOnContentLostErrorCode"},
+ {9, &IAddOnContentManager::StubReplyZeroed, "GetAddOnContentLostErrorCode"},
{10, D<&IAddOnContentManager::GetAddOnContentListChangedEventWithProcessId>, "GetAddOnContentListChangedEventWithProcessId"},
{11, D<&IAddOnContentManager::NotifyMountAddOnContent>, "NotifyMountAddOnContent"},
{12, D<&IAddOnContentManager::NotifyUnmountAddOnContent>, "NotifyUnmountAddOnContent"},
- {13, nullptr, "IsAddOnContentMountedForDebug"},
+ {13, &IAddOnContentManager::StubReplyZeroed, "IsAddOnContentMountedForDebug"},
{50, D<&IAddOnContentManager::CheckAddOnContentMountStatus>, "CheckAddOnContentMountStatus"},
{100, D<&IAddOnContentManager::CreateEcPurchasedEventManager>, "CreateEcPurchasedEventManager"},
{101, D<&IAddOnContentManager::CreatePermanentEcPurchasedEventManager>, "CreatePermanentEcPurchasedEventManager"},
- {110, nullptr, "CreateContentsServiceManager"},
- {200, nullptr, "SetRequiredAddOnContentsOnContentsAvailabilityTransition"},
- {300, nullptr, "SetupHostAddOnContent"},
- {301, nullptr, "GetRegisteredAddOnContentPath"},
- {302, nullptr, "UpdateCachedList"},
+ {110, &IAddOnContentManager::StubReplyInterface, "CreateContentsServiceManager"},
+ {200, &IAddOnContentManager::StubReplyZeroed, "SetRequiredAddOnContentsOnContentsAvailabilityTransition"},
+ {300, &IAddOnContentManager::StubReplyZeroed, "SetupHostAddOnContent"},
+ {301, &IAddOnContentManager::StubReplyZeroed, "GetRegisteredAddOnContentPath"},
+ {302, &IAddOnContentManager::StubReplyZeroed, "UpdateCachedList"},
};
// clang-format on
diff --git a/src/core/hle/service/audio/audio.cpp b/src/core/hle/service/audio/audio.cpp
index 211e1cb6bc..eb1511857b 100644
--- a/src/core/hle/service/audio/audio.cpp
+++ b/src/core/hle/service/audio/audio.cpp
@@ -6,6 +6,7 @@
#include "core/core.h"
#include "core/hle/service/audio/audio.h"
+#include "core/hle/service/audio/nx2_audio_device_manager.h"
#include "core/hle/service/audio/nx2_audio_user_client.h"
#include "core/hle/service/audio/audio_controller.h"
#include "core/hle/service/audio/audio_in_manager.h"
@@ -206,6 +207,8 @@ void LoopProcess(Core::System& system) {
server_manager->RegisterNamedService("audren:u", std::make_shared<IAudioRendererManager>(system), 30);
// Switch 2 audio entry point (nn::audio::CreateUserClientSessionByHipc).
server_manager->RegisterNamedService("aud:u", std::make_shared<INx2AudioUserClient>(system), 30);
+ server_manager->RegisterNamedService("auddmg:u", std::make_shared<INx2AudioDeviceManagerService>(system), 30);
+ server_manager->RegisterNamedService("audsmx:u", std::make_shared<INx2AudioSoundMixerService>(system), 30);
server_manager->RegisterNamedService("audren:a", std::make_shared<IAudioRendererManagerForApplet>(system), 30);
server_manager->RegisterNamedService("hwopus", std::make_shared<IHardwareOpusDecoderManager>(system), 25);
ServerManager::RunServer(std::move(server_manager));
diff --git a/src/core/hle/service/audio/audio_device.cpp b/src/core/hle/service/audio/audio_device.cpp
index e9f7e074a1..8f5856b76c 100644
--- a/src/core/hle/service/audio/audio_device.cpp
+++ b/src/core/hle/service/audio/audio_device.cpp
@@ -16,7 +16,9 @@ IAudioDevice::IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u
u32 device_num)
: ServiceFramework{system_, "IAudioDevice"}, service_context{system_, "IAudioDevice"},
impl{std::make_unique<AudioDevice>(system_, applet_resource_user_id, revision)},
- event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))} {
+ event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))},
+ notification_event{service_context.CreateEvent(
+ fmt::format("IAudioDeviceNotification-{}", device_num))} {
static const FunctionInfo functions[] = {
{0, D<&IAudioDevice::ListAudioDeviceName>, "ListAudioDeviceName"},
{1, D<&IAudioDevice::SetAudioDeviceOutputVolume>, "SetAudioDeviceOutputVolume"},
@@ -27,18 +29,19 @@ IAudioDevice::IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u
{6, D<&IAudioDevice::ListAudioDeviceNameAuto>, "ListAudioDeviceNameAuto"},
{7, D<&IAudioDevice::SetAudioDeviceOutputVolumeAuto>, "SetAudioDeviceOutputVolumeAuto"},
{8, D<&IAudioDevice::GetAudioDeviceOutputVolumeAuto>, "GetAudioDeviceOutputVolumeAuto"},
+ {9, &IAudioDevice::StubReplyZeroed, "Unknown9"},
{10, D<&IAudioDevice::GetActiveAudioDeviceNameAuto>, "GetActiveAudioDeviceNameAuto"},
{11, D<&IAudioDevice::QueryAudioDeviceInputEvent>, "QueryAudioDeviceInputEvent"},
{12, D<&IAudioDevice::QueryAudioDeviceOutputEvent>, "QueryAudioDeviceOutputEvent"},
{13, D<&IAudioDevice::GetActiveAudioDeviceName>, "GetActiveAudioOutputDeviceName"},
{14, D<&IAudioDevice::ListAudioOutputDeviceName>, "ListAudioOutputDeviceName"},
- {15, nullptr, "AcquireAudioInputDeviceNotification"}, //17.0.0+
- {16, nullptr, "ReleaseAudioInputDeviceNotification"}, //17.0.0+
- {17, nullptr, "AcquireAudioOutputDeviceNotification"}, //17.0.0+
- {18, nullptr, "ReleaseAudioOutputDeviceNotification"}, //17.0.0+
+ {15, D<&IAudioDevice::AcquireAudioDeviceNotification>, "AcquireAudioInputDeviceNotification"}, //17.0.0+
+ {16, D<&IAudioDevice::ReleaseAudioDeviceNotification>, "ReleaseAudioInputDeviceNotification"}, //17.0.0+
+ {17, D<&IAudioDevice::AcquireAudioDeviceNotification>, "AcquireAudioOutputDeviceNotification"}, //17.0.0+
+ {18, D<&IAudioDevice::ReleaseAudioDeviceNotification>, "ReleaseAudioOutputDeviceNotification"}, //17.0.0+
{19, D<&IAudioDevice::SetAudioDeviceOutputVolumeAutoTuneEnabled>, "SetAudioDeviceOutputVolumeAutoTuneEnabled"}, //18.0.0+
{20, D<&IAudioDevice::IsAudioDeviceOutputVolumeAutoTuneEnabled>, "IsAudioDeviceOutputVolumeAutoTuneEnabled"}, //18.0.0+
- {21, nullptr, "IsActiveOutputDeviceEstimatedLowLatency"} //21.0.0+
+ {21, &IAudioDevice::StubReplyZeroed, "IsActiveOutputDeviceEstimatedLowLatency"} //21.0.0+
};
RegisterHandlers(functions);
@@ -46,6 +49,7 @@ IAudioDevice::IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u
}
IAudioDevice::~IAudioDevice() {
+ service_context.CloseEvent(notification_event);
service_context.CloseEvent(event);
}
@@ -145,6 +149,21 @@ Result IAudioDevice::QueryAudioDeviceOutputEvent(OutCopyHandle<Kernel::KReadable
R_SUCCEED();
}
+// 15/17(u64): an event that fires when the input/output device set changes, which it never
+// does here. Switch 2 titles (Kirby Air Riders, Switch Sports) ask for it; the auto stub
+// replied without a handle.
+Result IAudioDevice::AcquireAudioDeviceNotification(OutCopyHandle<Kernel::KReadableEvent> out_event,
+ u64 unknown) {
+ LOG_DEBUG(Service_Audio, "called, unknown={:#x}", unknown);
+ *out_event = &notification_event->GetReadableEvent();
+ R_SUCCEED();
+}
+
+Result IAudioDevice::ReleaseAudioDeviceNotification(u64 unknown) {
+ LOG_DEBUG(Service_Audio, "called, unknown={:#x}", unknown);
+ R_SUCCEED();
+}
+
Result IAudioDevice::GetActiveChannelCount(Out<u32> out_active_channel_count) {
*out_active_channel_count = system.AudioCore().GetOutputSink().GetSystemChannels();
LOG_DEBUG(Service_Audio, "(STUBBED) called. Channels={}", *out_active_channel_count);
diff --git a/src/core/hle/service/audio/audio_device.h b/src/core/hle/service/audio/audio_device.h
index f6c12fbfeb..8a7b9a70b0 100644
--- a/src/core/hle/service/audio/audio_device.h
+++ b/src/core/hle/service/audio/audio_device.h
@@ -54,10 +54,14 @@ private:
Out<s32> out_count);
Result SetAudioDeviceOutputVolumeAutoTuneEnabled(bool enabled);
Result IsAudioDeviceOutputVolumeAutoTuneEnabled(Out<bool> out_enabled);
+ Result AcquireAudioDeviceNotification(OutCopyHandle<Kernel::KReadableEvent> out_event,
+ u64 unknown);
+ Result ReleaseAudioDeviceNotification(u64 unknown);
KernelHelpers::ServiceContext service_context;
std::unique_ptr<AudioCore::Renderer::AudioDevice> impl;
Kernel::KEvent* event;
+ Kernel::KEvent* notification_event;
};
} // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audio_renderer_manager.cpp b/src/core/hle/service/audio/audio_renderer_manager.cpp
index 97d9f44ddd..d669d64cec 100644
--- a/src/core/hle/service/audio/audio_renderer_manager.cpp
+++ b/src/core/hle/service/audio/audio_renderer_manager.cpp
@@ -27,7 +27,7 @@ IAudioRendererManager::IAudioRendererManager(Core::System& system_)
{0, D<&IAudioRendererManager::OpenAudioRenderer>, "OpenAudioRenderer"},
{1, D<&IAudioRendererManager::GetWorkBufferSize>, "GetWorkBufferSize"},
{2, D<&IAudioRendererManager::GetAudioDeviceService>, "GetAudioDeviceService"},
- {3, nullptr, "OpenAudioRendererForManualExecution"},
+ {3, &IAudioRendererManager::StubReplyInterface, "OpenAudioRendererForManualExecution"},
{4, D<&IAudioRendererManager::GetAudioDeviceServiceWithRevisionInfo>, "GetAudioDeviceServiceWithRevisionInfo"},
};
// clang-format on
diff --git a/src/core/hle/service/audio/nx2_audio_device_manager.h b/src/core/hle/service/audio/nx2_audio_device_manager.h
new file mode 100644
index 0000000000..746454660e
--- /dev/null
+++ b/src/core/hle/service/audio/nx2_audio_device_manager.h
@@ -0,0 +1,539 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <chrono>
+#include <cstring>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+#include <fmt/ranges.h>
+
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/hle/kernel/k_event.h"
+#include "core/memory.h"
+#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/kernel_helpers.h"
+#include "core/hle/service/service.h"
+
+namespace Service::Audio {
+
+// Switch 2 sound mixer streams (nn::audio::sysmix, behind audsmx:u). A stream is a ring buffer in
+// the game's own memory: CreateStream hands over its address and size (4 KiB aligned), OpenStream
+// its format and length in frames, and once the streams are engaged the system takes frames out of
+// the ring at the stream's sample rate. Eden doesn't mix these into its output yet; it drains the
+// rings in real time so the game sees its audio being played (Switch Sports' menu movie feeds one).
+// The ring's shared indices, in frames modulo (frame count + 1): the system's read index at
+// buffer + 0x44, the game's write index at buffer + 0x1004.
+struct Nx2SysmixStream {
+ u32 id{};
+ u32 owner{}; ///< the user session whose timer drains it
+ u64 buffer{};
+ u64 size{};
+ u32 sample_type{};
+ u32 channels{};
+ u32 sample_rate{};
+ u32 frames{};
+ bool open{};
+ bool engaged{};
+ double owed_frames{};
+ u32 data_mode{}; ///< command 7's mode; 0 leaves the data event alone
+ s32 data_threshold{}; ///< free frames that signal the data event
+ Kernel::KEvent* state_event{};
+ Kernel::KEvent* data_event{};
+};
+
+struct Nx2SysmixStreams {
+ std::mutex mutex;
+ std::map<u32, std::shared_ptr<Nx2SysmixStream>> streams;
+ u32 next_id{1};
+};
+
+// Streams are created through audsmx:u but started and stopped through auddmg:u (AttachStream and
+// DetachStream), so both services share one list.
+inline std::shared_ptr<Nx2SysmixStreams> Nx2SysmixStreamList() {
+ static const auto list = std::make_shared<Nx2SysmixStreams>();
+ return list;
+}
+
+// Switch 2 virtual output devices (nn::audio::ListVirtualOutputDevices and friends), reached through
+// the object auddmg:u command 0 returns. Eden plays everything through one stereo 48 kHz output, so
+// there is one virtual device. The nx2: name keeps the other commands auto-stubbed and logged.
+// - 0(u32): u32 count, the device names in the out buffer (0x100 bytes each).
+// - 100(u32): the default device's name in the out buffer.
+// - 300(16-byte stream format, name buffer): whether the format is supported (result only).
+// - 401(u32, name buffer): the device's 0x228-byte property block in the out buffer. The sdk hands
+// the game the name, the channel count (0x108) and the u64 at 0x218; with the zeroed placeholder
+// reply it saw 0 channels and Switch Sports failed to set up audio (2153-1604).
+// - 500(16 bytes, name buffer): an event signalled when the device's state changes.
+// - 700(u32 stream id, u32 device functionality, name buffer): attach a sysmix stream to the
+// device, which starts it (nn::audio::devmgr::AttachStream, behind OutputStream Start).
+// - 800(u32 stream id): detach it again (Stop).
+class INx2AudioVirtualDeviceManager final : public ServiceFramework<INx2AudioVirtualDeviceManager> {
+public:
+ explicit INx2AudioVirtualDeviceManager(Core::System& system_)
+ : ServiceFramework{system_, "nx2:auddmg:u:manager"},
+ service_context{system_, "nx2:auddmg:u:manager"},
+ state_event{service_context.CreateEvent("nx2:auddmg:u:StateEvent")} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2AudioVirtualDeviceManager::ListVirtualOutputDevices, "ListVirtualOutputDevices"},
+ {100, &INx2AudioVirtualDeviceManager::GetDefaultVirtualOutputDevice, "GetDefaultVirtualOutputDevice"},
+ {300, &INx2AudioVirtualDeviceManager::IsStreamFormatSupported, "IsVirtualOutputDeviceStreamFormatSupported"},
+ {401, &INx2AudioVirtualDeviceManager::GetVirtualOutputDeviceProperties, "GetVirtualOutputDeviceProperties"},
+ {500, &INx2AudioVirtualDeviceManager::AcquireStateChangeEvent, "AcquireVirtualOutputDeviceStateEvent (guessed)"},
+ {700, &INx2AudioVirtualDeviceManager::AttachStream, "AttachStream"},
+ {800, &INx2AudioVirtualDeviceManager::DetachStream, "DetachStream"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+ ~INx2AudioVirtualDeviceManager() override {
+ service_context.CloseEvent(state_event);
+ }
+
+private:
+ static constexpr std::size_t NameSize = 0x100;
+ static constexpr std::string_view DeviceName = "AudioVirtualOutput0";
+ static constexpr std::string_view PhysicalDeviceName = "AudioTvOutput";
+
+ static void CopyName(u8* out, std::string_view name) {
+ std::memcpy(out, name.data(), std::min(name.size(), NameSize - 1));
+ }
+
+ void ListVirtualOutputDevices(HLERequestContext& ctx) {
+ std::array<u8, NameSize> names{};
+ CopyName(names.data(), DeviceName);
+ const u32 count = ctx.GetWriteBufferSize() >= NameSize ? 1 : 0;
+ if (count) {
+ ctx.WriteBuffer(names);
+ }
+ LOG_DEBUG(Service_Audio, "called, {} device", count);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(count);
+ }
+
+ void GetDefaultVirtualOutputDevice(HLERequestContext& ctx) {
+ std::array<u8, NameSize> name{};
+ CopyName(name.data(), DeviceName);
+ ctx.WriteBuffer(name);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void IsStreamFormatSupported(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "called");
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ // Property block: name (0x000), u32 type (0x100, 3), u32 (0x104, 2), u32 channel count
+ // (0x108), u32 sample rate (0x10c), physical output name (0x110), its id (0x210), u64 latency
+ // in ns (0x218), u32 device id (0x220). The sdk's output streams pass the device id to
+ // sysmix CreateStream, which refuses 0 (Switch Sports failed with 2153-1604 on a zero there).
+ void GetVirtualOutputDeviceProperties(HLERequestContext& ctx) {
+ std::array<u8, 0x228> block{};
+ const auto name = ctx.ReadBuffer(0);
+ std::memcpy(block.data(), name.data(), std::min(name.size(), NameSize - 1));
+ const auto put32 = [&](std::size_t offset, u32 value) {
+ std::memcpy(block.data() + offset, &value, sizeof(value));
+ };
+ put32(0x100, 3);
+ put32(0x104, 2);
+ put32(0x108, 2); // stereo
+ put32(0x10c, 48000); // Hz
+ CopyName(block.data() + 0x110, PhysicalDeviceName);
+ const u64 latency_ns = 10'000'000;
+ std::memcpy(block.data() + 0x218, &latency_ns, sizeof(latency_ns));
+ put32(0x220, 1);
+ ctx.WriteBuffer(block);
+ LOG_DEBUG(Service_Audio, "called for {}", Common::StringFromBuffer(name));
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void AcquireStateChangeEvent(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, state_event->GetReadableEvent());
+ }
+
+ void AttachStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto stream_id = rp.Pop<u32>();
+ const auto functionality = rp.Pop<u32>();
+ LOG_INFO(Service_Audio, "attach stream {} to {} (functionality {})", stream_id,
+ Common::StringFromBuffer(ctx.ReadBuffer(0)), functionality);
+ SetStreamEngaged(stream_id, true);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void DetachStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto stream_id = rp.Pop<u32>();
+ LOG_INFO(Service_Audio, "detach stream {}", stream_id);
+ SetStreamEngaged(stream_id, false);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void SetStreamEngaged(u32 stream_id, bool engaged) {
+ const auto list = Nx2SysmixStreamList();
+ std::scoped_lock lk{list->mutex};
+ if (const auto it = list->streams.find(stream_id); it != list->streams.end()) {
+ it->second->engaged = engaged;
+ it->second->owed_frames = 0;
+ if (it->second->state_event) {
+ it->second->state_event->Signal(system.Kernel());
+ }
+ }
+ }
+
+ KernelHelpers::ServiceContext service_context;
+ Kernel::KEvent* state_event;
+};
+
+// Switch 2 "auddmg:u": command 0 (16 bytes and the pid) returns the virtual device manager.
+class INx2AudioDeviceManagerService final : public ServiceFramework<INx2AudioDeviceManagerService> {
+public:
+ explicit INx2AudioDeviceManagerService(Core::System& system_)
+ : ServiceFramework{system_, "nx2:auddmg:u"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2AudioDeviceManagerService::OpenVirtualDeviceManager, "OpenVirtualDeviceManager (guessed)"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+private:
+ void OpenVirtualDeviceManager(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "called");
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<INx2AudioVirtualDeviceManager>(ctx, system);
+ }
+};
+
+// The stream object IUserSession command 1 returns. Commands (ids, request -> reply):
+// - 0(u32 sample type, u32 channels, u32 sample rate, u32 frames, u32 prefill, u32 flags): open.
+// - 1: close. 2: u32 state. 3: f32 gain. 4(f32 gain, s64 ramp ns): set the gain.
+// - 5 / 6: acquire (copy handle) / release the state-change event.
+// - 7(u32 mode, s32 threshold) / 8: acquire (copy handle) / release the data event.
+// - 9(s64 ns): clock the stream. 10(s32 frames) -> u32: prime. 11(s32 frames) -> u32: purge.
+// - 12(name buffer): rename.
+class INx2SysmixStreamSession final : public ServiceFramework<INx2SysmixStreamSession> {
+public:
+ explicit INx2SysmixStreamSession(Core::System& system_, std::shared_ptr<Nx2SysmixStreams> streams_,
+ std::shared_ptr<Nx2SysmixStream> stream_)
+ : ServiceFramework{system_, "nx2:audsmx:u:stream"}, service_context{system_, "nx2:audsmx:u:stream"},
+ streams{std::move(streams_)}, stream{std::move(stream_)} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2SysmixStreamSession::OpenStream, "OpenStream"},
+ {1, &INx2SysmixStreamSession::CloseStream, "CloseStream"},
+ {2, &INx2SysmixStreamSession::GetStreamState, "GetStreamState"},
+ {3, &INx2SysmixStreamSession::GetStreamGain, "GetStreamGain"},
+ {4, &INx2SysmixStreamSession::SetStreamGain, "SetStreamGain"},
+ {5, &INx2SysmixStreamSession::AcquireStateChangeEvent, "AcquireStreamStateChangeEvent"},
+ {6, &INx2SysmixStreamSession::Succeed, "ReleaseStreamStateChangeEvent"},
+ {7, &INx2SysmixStreamSession::AcquireDataEvent, "AcquireStreamDataEvent"},
+ {8, &INx2SysmixStreamSession::Succeed, "ReleaseStreamDataEvent"},
+ {9, &INx2SysmixStreamSession::Succeed, "ClockStream"},
+ {10, &INx2SysmixStreamSession::EchoFrames, "PrimeStream"},
+ {11, &INx2SysmixStreamSession::EchoFrames, "PurgeStream"},
+ {12, &INx2SysmixStreamSession::Succeed, "RenameStream"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ std::scoped_lock lk{streams->mutex};
+ stream->state_event = service_context.CreateEvent("nx2:audsmx:u:StreamStateEvent");
+ stream->data_event = service_context.CreateEvent("nx2:audsmx:u:StreamDataEvent");
+ }
+
+ ~INx2SysmixStreamSession() override {
+ // The drain timer signals these events under the lock: unlist the stream first.
+ {
+ std::scoped_lock lk{streams->mutex};
+ streams->streams.erase(stream->id);
+ service_context.CloseEvent(stream->state_event);
+ service_context.CloseEvent(stream->data_event);
+ stream->state_event = nullptr;
+ stream->data_event = nullptr;
+ }
+ }
+
+private:
+ // States the session reports: 0 created, 1 open, 2 open and engaged.
+ void OpenStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto sample_type = rp.Pop<u32>();
+ const auto channels = rp.Pop<u32>();
+ const auto sample_rate = rp.Pop<u32>();
+ const auto frames = rp.Pop<u32>();
+ const auto prefill = rp.Pop<u32>();
+ const auto flags = rp.Pop<u32>();
+ LOG_INFO(Service_Audio,
+ "stream {} open: sample type {}, {} channels, {} Hz, {} frames, prefill {}, flags {:#x}",
+ stream->id, sample_type, channels, sample_rate, frames, prefill, flags);
+ {
+ std::scoped_lock lk{streams->mutex};
+ stream->sample_type = sample_type;
+ stream->channels = channels;
+ stream->sample_rate = sample_rate;
+ stream->frames = frames;
+ stream->owed_frames = 0;
+ stream->open = true;
+ stream->state_event->Signal(system.Kernel());
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void CloseStream(HLERequestContext& ctx) {
+ {
+ std::scoped_lock lk{streams->mutex};
+ stream->open = false;
+ stream->state_event->Signal(system.Kernel());
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void GetStreamState(HLERequestContext& ctx) {
+ u32 state{};
+ {
+ std::scoped_lock lk{streams->mutex};
+ state = stream->open ? (stream->engaged ? 2 : 1) : 0;
+ }
+ LOG_DEBUG(Service_Audio, "stream {} state {}", stream->id, state);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(state);
+ }
+
+ void GetStreamGain(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(gain);
+ }
+
+ void SetStreamGain(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ gain = rp.Pop<f32>();
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void AcquireStateChangeEvent(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, stream->state_event->GetReadableEvent());
+ }
+
+ void AcquireDataEvent(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto mode = rp.Pop<u32>();
+ const auto threshold = rp.Pop<s32>();
+ LOG_INFO(Service_Audio, "stream {} data event: mode {}, threshold {}", stream->id, mode, threshold);
+ {
+ // The game waits on this before its first write, so signal it at once when the ring
+ // already has room (it is empty right after OpenStream); the drain timer keeps it up.
+ std::scoped_lock lk{streams->mutex};
+ stream->data_mode = mode;
+ stream->data_threshold = threshold;
+ if (mode != 0 && stream->open && static_cast<s64>(stream->frames) >= threshold) {
+ stream->data_event->Signal(system.Kernel());
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, stream->data_event->GetReadableEvent());
+ }
+
+ void EchoFrames(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto frames = rp.Pop<s32>();
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push<u32>(static_cast<u32>((std::max)(frames, 0)));
+ }
+
+ void Succeed(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ KernelHelpers::ServiceContext service_context;
+ std::shared_ptr<Nx2SysmixStreams> streams;
+ std::shared_ptr<Nx2SysmixStream> stream;
+ f32 gain{1.0f};
+};
+
+// The session audsmx:u command 0 returns. Commands:
+// - 1(u32, u32 creation flags, u64 pid, u64 buffer address, u64 buffer size, name buffer):
+// u32 stream id and the stream object.
+// - 2 / 3(buffer of u32 stream ids): engage / disengage those streams.
+class INx2SysmixUserSession final : public ServiceFramework<INx2SysmixUserSession> {
+public:
+ explicit INx2SysmixUserSession(Core::System& system_) : ServiceFramework{system_, "nx2:audsmx:u:session"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {1, &INx2SysmixUserSession::CreateStream, "CreateStream"},
+ {2, &INx2SysmixUserSession::EngageStreams, "EngageStreams"},
+ {3, &INx2SysmixUserSession::DisengageStreams, "DisengageStreams"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ drain_timer = Core::Timing::CreateEvent(
+ "nx2:audsmx:u drain",
+ [streams = streams, &system = system, owner = session_id](s64, std::chrono::nanoseconds)
+ -> std::optional<std::chrono::nanoseconds> {
+ Drain(system, *streams, owner);
+ return std::nullopt;
+ });
+ system.CoreTiming().ScheduleLoopingEvent(DrainInterval, DrainInterval, drain_timer);
+ }
+
+ ~INx2SysmixUserSession() override {
+ system.CoreTiming().UnscheduleEvent(drain_timer);
+ }
+
+private:
+ static constexpr std::chrono::nanoseconds DrainInterval{std::chrono::milliseconds{5}};
+ static constexpr u64 ReadIndexOffset = 0x44;
+ static constexpr u64 WriteIndexOffset = 0x1004;
+
+ // Takes up to one interval's worth of frames out of each engaged ring (frames owed while a ring
+ // is empty are dropped, as an underrun plays silence), and signals the data event of every open
+ // stream with at least its threshold of free frames, engaged or not: the game fills the ring
+ // before it engages the stream.
+ static void Drain(Core::System& system, Nx2SysmixStreams& state, u32 owner) {
+ std::scoped_lock lk{state.mutex};
+ auto& memory = system.ApplicationMemory();
+ for (auto& [id, stream] : state.streams) {
+ if (stream->owner != owner || !stream->open || stream->frames == 0 || !stream->buffer) {
+ continue;
+ }
+ const u32 capacity = stream->frames + 1;
+ const u32 write = memory.Read32(stream->buffer + WriteIndexOffset);
+ const u32 read = memory.Read32(stream->buffer + ReadIndexOffset);
+ if (write >= capacity || read >= capacity) {
+ continue;
+ }
+ u32 filled = (write + capacity - read) % capacity;
+ if (stream->engaged) {
+ stream->owed_frames += stream->sample_rate *
+ std::chrono::duration<double>(DrainInterval).count();
+ const auto owed = static_cast<u32>(stream->owed_frames);
+ stream->owed_frames -= owed;
+ const u32 taken = (std::min)(filled, owed);
+ if (taken != 0) {
+ memory.Write32(stream->buffer + ReadIndexOffset, (read + taken) % capacity);
+ filled -= taken;
+ }
+ }
+ const u32 free_frames = capacity - 1 - filled;
+ if (stream->data_event && stream->data_mode != 0 &&
+ static_cast<s64>(free_frames) >= stream->data_threshold) {
+ stream->data_event->Signal(system.Kernel());
+ }
+ }
+ }
+
+ void CreateStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto kind = rp.Pop<u32>();
+ const auto flags = rp.Pop<u32>();
+ [[maybe_unused]] const auto pid = rp.Pop<u64>();
+ const auto buffer = rp.Pop<u64>();
+ const auto size = rp.Pop<u64>();
+ const auto name = Common::StringFromBuffer(ctx.ReadBuffer(0));
+ auto stream = std::make_shared<Nx2SysmixStream>();
+ stream->buffer = buffer;
+ stream->size = size;
+ {
+ std::scoped_lock lk{streams->mutex};
+ stream->id = streams->next_id++;
+ stream->owner = session_id;
+ streams->streams.emplace(stream->id, stream);
+ }
+ LOG_INFO(Service_Audio, "stream {} '{}': kind {}, flags {:#x}, buffer {:#x} size {:#x}",
+ stream->id, name, kind, flags, buffer, size);
+ IPC::ResponseBuilder rb{ctx, 3, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.Push(stream->id);
+ rb.PushIpcInterface<INx2SysmixStreamSession>(ctx, system, streams, stream);
+ }
+
+ void SetEngaged(HLERequestContext& ctx, bool engaged) {
+ const auto buffer = ctx.ReadBuffer(0);
+ std::vector<u32> ids(buffer.size() / sizeof(u32));
+ std::memcpy(ids.data(), buffer.data(), ids.size() * sizeof(u32));
+ {
+ std::scoped_lock lk{streams->mutex};
+ for (const u32 id : ids) {
+ if (const auto it = streams->streams.find(id); it != streams->streams.end()) {
+ it->second->engaged = engaged;
+ it->second->owed_frames = 0;
+ if (it->second->state_event) {
+ it->second->state_event->Signal(system.Kernel());
+ }
+ }
+ }
+ }
+ LOG_INFO(Service_Audio, "{} streams {}", engaged ? "engage" : "disengage", fmt::join(ids, ","));
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void EngageStreams(HLERequestContext& ctx) {
+ SetEngaged(ctx, true);
+ }
+
+ void DisengageStreams(HLERequestContext& ctx) {
+ SetEngaged(ctx, false);
+ }
+
+ static inline std::atomic<u32> next_session_id{1};
+ const u32 session_id = next_session_id++;
+ std::shared_ptr<Nx2SysmixStreams> streams = Nx2SysmixStreamList();
+ std::shared_ptr<Core::Timing::EventType> drain_timer;
+};
+
+// Switch 2 "audsmx:u": command 0 (16 bytes, the pid and a copy handle) returns the user session.
+class INx2AudioSoundMixerService final : public ServiceFramework<INx2AudioSoundMixerService> {
+public:
+ explicit INx2AudioSoundMixerService(Core::System& system_)
+ : ServiceFramework{system_, "nx2:audsmx:u"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2AudioSoundMixerService::OpenUserSession, "OpenUserSession"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+private:
+ void OpenUserSession(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "called");
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<INx2SysmixUserSession>(ctx, system);
+ }
+};
+
+} // namespace Service::Audio
diff --git a/src/core/hle/service/caps/caps_su.cpp b/src/core/hle/service/caps/caps_su.cpp
index 4ecfc840e4..b19e6b7a27 100644
--- a/src/core/hle/service/caps/caps_su.cpp
+++ b/src/core/hle/service/caps/caps_su.cpp
@@ -21,10 +21,10 @@ IScreenShotApplicationService::IScreenShotApplicationService(
// clang-format off
static const FunctionInfo functions[] = {
{32, C<&IScreenShotApplicationService::SetShimLibraryVersion>, "SetShimLibraryVersion"},
- {201, nullptr, "SaveScreenShot"},
+ {201, &IScreenShotApplicationService::StubReplyZeroed, "SaveScreenShot"},
{203, C<&IScreenShotApplicationService::SaveScreenShotEx0>, "SaveScreenShotEx0"},
{205, C<&IScreenShotApplicationService::SaveScreenShotEx1>, "SaveScreenShotEx1"},
- {210, nullptr, "SaveScreenShotEx2"},
+ {210, &IScreenShotApplicationService::StubReplyZeroed, "SaveScreenShotEx2"},
};
// clang-format on
diff --git a/src/core/hle/service/caps/caps_u.cpp b/src/core/hle/service/caps/caps_u.cpp
index 6472b2bac6..eb4a10193e 100644
--- a/src/core/hle/service/caps/caps_u.cpp
+++ b/src/core/hle/service/caps/caps_u.cpp
@@ -20,17 +20,21 @@ IAlbumApplicationService::IAlbumApplicationService(Core::System& system_,
static const FunctionInfo functions[] = {
{32, C<&IAlbumApplicationService::SetShimLibraryVersion>, "SetShimLibraryVersion"},
{102, C<&IAlbumApplicationService::GetAlbumFileList0AafeAruidDeprecated>, "GetAlbumFileList0AafeAruidDeprecated"},
- {103, nullptr, "DeleteAlbumFileByAruid"},
- {104, nullptr, "GetAlbumFileSizeByAruid"},
- {105, nullptr, "DeleteAlbumFileByAruidForDebug"},
- {110, nullptr, "LoadAlbumScreenShotImageByAruid"},
- {120, nullptr, "LoadAlbumScreenShotThumbnailImageByAruid"},
- {130, nullptr, "PrecheckToCreateContentsByAruid"},
- {140, nullptr, "GetAlbumFileList1AafeAruidDeprecated"},
- {141, nullptr, "GetAlbumFileList2AafeUidAruidDeprecated"},
+ {103, &IAlbumApplicationService::StubReplyZeroed, "DeleteAlbumFileByAruid"},
+ {104, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileSizeByAruid"},
+ {105, &IAlbumApplicationService::StubReplyZeroed, "DeleteAlbumFileByAruidForDebug"},
+ {110, &IAlbumApplicationService::StubReplyZeroed, "LoadAlbumScreenShotImageByAruid"},
+ {120, &IAlbumApplicationService::StubReplyZeroed, "LoadAlbumScreenShotThumbnailImageByAruid"},
+ {130, &IAlbumApplicationService::StubReplyZeroed, "PrecheckToCreateContentsByAruid"},
+ {140, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileList1AafeAruidDeprecated"},
+ {141, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileList2AafeUidAruidDeprecated"},
{142, C<&IAlbumApplicationService::GetAlbumFileList3AaeAruid>, "GetAlbumFileList3AaeAruid"},
- {143, nullptr, "GetAlbumFileList4AaeUidAruid"},
- {144, nullptr, "GetAllAlbumFileList3AaeAruid"},
+ {143, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileList4AaeUidAruid"},
+ {144, &IAlbumApplicationService::StubReplyZeroed, "GetAllAlbumFileList3AaeAruid"},
+ {145, &IAlbumApplicationService::StubReplyZeroed, "Unknown145"},
+ {146, &IAlbumApplicationService::StubReplyZeroed, "Unknown146"},
+ {147, &IAlbumApplicationService::StubReplyZeroed, "Unknown147"},
+ {148, &IAlbumApplicationService::StubReplyZeroed, "Unknown148"},
{60002, nullptr, "OpenAccessorSessionForApplication"},
};
// clang-format on
diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp
index 04b4663bc2..20118345a9 100644
--- a/src/core/hle/service/hid/hid_server.cpp
+++ b/src/core/hle/service/hid/hid_server.cpp
@@ -74,12 +74,12 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{70, C<&IHidServer::SetSixAxisSensorFusionParameters>, "SetSixAxisSensorFusionParameters"},
{71, C<&IHidServer::GetSixAxisSensorFusionParameters>, "GetSixAxisSensorFusionParameters"},
{72, C<&IHidServer::ResetSixAxisSensorFusionParameters>, "ResetSixAxisSensorFusionParameters"},
- {73, nullptr, "SetAccelerometerParameters"},
- {74, nullptr, "GetAccelerometerParameters"},
- {75, nullptr, "ResetAccelerometerParameters"},
- {76, nullptr, "SetAccelerometerPlayMode"},
- {77, nullptr, "GetAccelerometerPlayMode"},
- {78, nullptr, "ResetAccelerometerPlayMode"},
+ {73, &IHidServer::StubReplyZeroed, "SetAccelerometerParameters"},
+ {74, &IHidServer::StubReplyZeroed, "GetAccelerometerParameters"},
+ {75, &IHidServer::StubReplyZeroed, "ResetAccelerometerParameters"},
+ {76, &IHidServer::StubReplyZeroed, "SetAccelerometerPlayMode"},
+ {77, &IHidServer::StubReplyZeroed, "GetAccelerometerPlayMode"},
+ {78, &IHidServer::StubReplyZeroed, "ResetAccelerometerPlayMode"},
{79, C<&IHidServer::SetGyroscopeZeroDriftMode>, "SetGyroscopeZeroDriftMode"},
{80, C<&IHidServer::GetGyroscopeZeroDriftMode>, "GetGyroscopeZeroDriftMode"},
{81, C<&IHidServer::ResetGyroscopeZeroDriftMode>, "ResetGyroscopeZeroDriftMode"},
@@ -87,7 +87,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{83, C<&IHidServer::IsFirmwareUpdateAvailableForSixAxisSensor>, "IsFirmwareUpdateAvailableForSixAxisSensor"},
{84, C<&IHidServer::EnableSixAxisSensorUnalteredPassthrough>, "EnableSixAxisSensorUnalteredPassthrough"},
{85, C<&IHidServer::IsSixAxisSensorUnalteredPassthroughEnabled>, "IsSixAxisSensorUnalteredPassthroughEnabled"},
- {86, nullptr, "StoreSixAxisSensorCalibrationParameter"},
+ {86, &IHidServer::StubReplyZeroed, "StoreSixAxisSensorCalibrationParameter"},
{87, C<&IHidServer::LoadSixAxisSensorCalibrationParameter>, "LoadSixAxisSensorCalibrationParameter"},
{88, C<&IHidServer::GetSixAxisSensorIcInformation>, "GetSixAxisSensorIcInformation"},
{89, C<&IHidServer::ResetIsSixAxisSensorDeviceNewlyAssigned>, "ResetIsSixAxisSensorDeviceNewlyAssigned"},
@@ -119,6 +119,13 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{134, C<&IHidServer::SetNpadAnalogStickUseCenterClamp>, "SetNpadAnalogStickUseCenterClamp"},
{135, C<&IHidServer::SetNpadCaptureButtonAssignment>, "SetNpadCaptureButtonAssignment"},
{136, C<&IHidServer::ClearNpadCaptureButtonAssignment>, "ClearNpadCaptureButtonAssignment"},
+ {137, &IHidServer::StubReplyZeroed, "Unknown137"},
+ {138, &IHidServer::StubReplyZeroed, "Unknown138"},
+ {139, &IHidServer::StubReplyZeroed, "Unknown139"},
+ {140, &IHidServer::StubReplyZeroed, "Unknown140"},
+ {150, &IHidServer::StubReplyZeroed, "Unknown150"},
+ {151, &IHidServer::StubReplyZeroed, "Unknown151"},
+ {160, &IHidServer::StubReplyZeroed, "Unknown160"},
{200, C<&IHidServer::GetVibrationDeviceInfo>, "GetVibrationDeviceInfo"},
{201, C<&IHidServer::SendVibrationValue>, "SendVibrationValue"},
{202, C<&IHidServer::GetActualVibrationValue>, "GetActualVibrationValue"},
@@ -132,7 +139,9 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{210, C<&IHidServer::EndPermitVibrationSession>, "EndPermitVibrationSession"},
{211, C<&IHidServer::IsVibrationDeviceMounted>, "IsVibrationDeviceMounted"},
{212, C<&IHidServer::SendVibrationValueInBool>, "SendVibrationValueInBool"},
+ {213, C<&IHidServer::Unknown213>, "Unknown213"},
{214, C<&IHidServer::SendNx2VibrationValues>, "SendNx2VibrationValues (guessed)"},
+ {220, C<&IHidServer::Unknown220>, "Unknown220"},
{300, C<&IHidServer::ActivateConsoleSixAxisSensor>, "ActivateConsoleSixAxisSensor"},
{301, C<&IHidServer::StartConsoleSixAxisSensor>, "StartConsoleSixAxisSensor"},
{302, C<&IHidServer::StopConsoleSixAxisSensor>, "StopConsoleSixAxisSensor"},
@@ -141,19 +150,24 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{305, C<&IHidServer::StopSevenSixAxisSensor>, "StopSevenSixAxisSensor"},
{306, C<&IHidServer::InitializeSevenSixAxisSensor>, "InitializeSevenSixAxisSensor"},
{307, C<&IHidServer::FinalizeSevenSixAxisSensor>, "FinalizeSevenSixAxisSensor"},
- {308, nullptr, "SetSevenSixAxisSensorFusionStrength"},
- {309, nullptr, "GetSevenSixAxisSensorFusionStrength"},
+ {308, &IHidServer::StubReplyZeroed, "SetSevenSixAxisSensorFusionStrength"},
+ {309, &IHidServer::StubReplyZeroed, "GetSevenSixAxisSensorFusionStrength"},
{310, C<&IHidServer::ResetSevenSixAxisSensorTimestamp>, "ResetSevenSixAxisSensorTimestamp"},
- {320, nullptr, "EnableNpadImu"}, //21.0.0+
- {321, nullptr, "DisableNpadImu"}, //21.0.0+
+ {311, &IHidServer::StubReplyZeroed, "Unknown311"},
+ {312, &IHidServer::StubReplyZeroed, "Unknown312"},
+ {320, &IHidServer::StubReplyZeroed, "EnableNpadImu"}, //21.0.0+
+ {321, &IHidServer::StubReplyZeroed, "DisableNpadImu"}, //21.0.0+
+ {322, &IHidServer::StubReplyZeroed, "Unknown322"},
+ {323, &IHidServer::StubReplyZeroed, "Unknown323"},
+ {324, &IHidServer::StubReplyZeroed, "Unknown324"},
{400, C<&IHidServer::IsUsbFullKeyControllerEnabled>, "IsUsbFullKeyControllerEnabled"},
- {401, nullptr, "EnableUsbFullKeyController"},
- {402, nullptr, "IsUsbFullKeyControllerConnected"},
- {403, nullptr, "HasBattery"},
- {404, nullptr, "HasLeftRightBattery"},
- {405, nullptr, "GetNpadInterfaceType"},
- {406, nullptr, "GetNpadLeftRightInterfaceType"},
- {407, nullptr, "GetNpadOfHighestBatteryLevel"},
+ {401, &IHidServer::StubReplyZeroed, "EnableUsbFullKeyController"},
+ {402, &IHidServer::StubReplyZeroed, "IsUsbFullKeyControllerConnected"},
+ {403, &IHidServer::StubReplyZeroed, "HasBattery"},
+ {404, &IHidServer::StubReplyZeroed, "HasLeftRightBattery"},
+ {405, &IHidServer::StubReplyZeroed, "GetNpadInterfaceType"},
+ {406, &IHidServer::StubReplyZeroed, "GetNpadLeftRightInterfaceType"},
+ {407, &IHidServer::StubReplyZeroed, "GetNpadOfHighestBatteryLevel"},
{408, nullptr, "GetNpadOfHighestBatteryLevelForJoyRight"},
{500, C<&IHidServer::GetPalmaConnectionHandle>, "GetPalmaConnectionHandle"},
{501, C<&IHidServer::InitializePalma>, "InitializePalma"},
@@ -185,11 +199,13 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{527, C<&IHidServer::EnablePalmaBoostMode>, "EnablePalmaBoostMode"},
{528, C<&IHidServer::GetPalmaBluetoothAddress>, "GetPalmaBluetoothAddress"},
{529, C<&IHidServer::SetDisallowedPalmaConnection>, "SetDisallowedPalmaConnection"},
+ {550, &IHidServer::StubReplyZeroed, "Unknown550"},
{1000, C<&IHidServer::SetNpadCommunicationMode>, "SetNpadCommunicationMode"},
{1001, C<&IHidServer::GetNpadCommunicationMode>, "GetNpadCommunicationMode"},
{1002, C<&IHidServer::SetTouchScreenConfiguration>, "SetTouchScreenConfiguration"},
{1003, C<&IHidServer::IsFirmwareUpdateNeededForNotification>, "IsFirmwareUpdateNeededForNotification"},
{1004, C<&IHidServer::SetTouchScreenResolution>, "SetTouchScreenResolution"},
+ {1005, &IHidServer::StubReplyZeroed, "Unknown1005"},
{1270, nullptr, "DeleteButtonConfigStorageRight"},
{1271, nullptr, "IsUsingCustomButtonConfig"},
{1272, nullptr, "IsAnyCustomButtonConfigEnabled"},
@@ -222,23 +238,49 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{1325, nullptr, "IsTouchScreenAutoTuneRequiredForRepairProviderReplacement"},
{1420, nullptr, "GetAppletResourceProperty"},
{2000, nullptr, "ActivateDigitizer"},
- {3000, nullptr, "GetDebugPadGenericPadMap"},
- {3001, nullptr, "SetDebugPadGenericPadMap"},
- {3002, nullptr, "ResetDebugPadGenericPadMap"},
- {3003, nullptr, "GetDebugPadKeyboardMap"},
- {3004, nullptr, "SetDebugPadKeyboardMap"},
- {3005, nullptr, "ResetDebugPadKeyboardMap"},
- {3006, nullptr, "GetFullKeyGenericPadMap"},
- {3007, nullptr, "SetFullKeyGenericPadMap"},
- {3008, nullptr, "ResetFullKeyGenericPadMap"},
- {3009, nullptr, "GetFullKeyKeyboardMap"},
- {3010, nullptr, "SetFullKeyKeyboardMap"},
- {3011, nullptr, "ResetFullKeyKeyboardMap"},
- {3012, nullptr, "GetDebugPadGenericPadMap"}, //21.0.0+
- {3013, nullptr, "SetDebugPadGenericPadMap"}, //21.0.0+
- {3014, nullptr, "GetDebugPadKeyboardMap"}, //21.0.0+
- {3015, nullptr, "SetDebugPadKeyboardMap"}, //21.0.0+
+ {3000, &IHidServer::StubReplyZeroed, "GetDebugPadGenericPadMap"},
+ {3001, &IHidServer::StubReplyZeroed, "SetDebugPadGenericPadMap"},
+ {3002, &IHidServer::StubReplyZeroed, "ResetDebugPadGenericPadMap"},
+ {3003, &IHidServer::StubReplyZeroed, "GetDebugPadKeyboardMap"},
+ {3004, &IHidServer::StubReplyZeroed, "SetDebugPadKeyboardMap"},
+ {3005, &IHidServer::StubReplyZeroed, "ResetDebugPadKeyboardMap"},
+ {3006, &IHidServer::StubReplyZeroed, "GetFullKeyGenericPadMap"},
+ {3007, &IHidServer::StubReplyZeroed, "SetFullKeyGenericPadMap"},
+ {3008, &IHidServer::StubReplyZeroed, "ResetFullKeyGenericPadMap"},
+ {3009, &IHidServer::StubReplyZeroed, "GetFullKeyKeyboardMap"},
+ {3010, &IHidServer::StubReplyZeroed, "SetFullKeyKeyboardMap"},
+ {3011, &IHidServer::StubReplyZeroed, "ResetFullKeyKeyboardMap"},
+ {3012, &IHidServer::StubReplyZeroed, "GetDebugPadGenericPadMap"}, //21.0.0+
+ {3013, &IHidServer::StubReplyZeroed, "SetDebugPadGenericPadMap"}, //21.0.0+
+ {3014, &IHidServer::StubReplyZeroed, "GetDebugPadKeyboardMap"}, //21.0.0+
+ {3015, &IHidServer::StubReplyZeroed, "SetDebugPadKeyboardMap"}, //21.0.0+
+ {3110, &IHidServer::StubReplyZeroed, "Unknown3110"},
+ {3111, &IHidServer::StubReplyZeroed, "Unknown3111"},
+ {3112, &IHidServer::StubReplyZeroed, "Unknown3112"},
+ {3113, &IHidServer::StubReplyZeroed, "Unknown3113"},
+ {3114, &IHidServer::StubReplyZeroed, "Unknown3114"},
+ {3130, &IHidServer::StubReplyZeroed, "Unknown3130"},
+ {3131, &IHidServer::StubReplyZeroed, "Unknown3131"},
+ {3132, &IHidServer::StubReplyZeroed, "Unknown3132"},
+ {3133, &IHidServer::StubReplyZeroed, "Unknown3133"},
+ {3134, &IHidServer::StubReplyZeroed, "Unknown3134"},
+ {3135, &IHidServer::StubReplyZeroed, "Unknown3135"},
+ {3136, &IHidServer::StubReplyZeroed, "Unknown3136"},
+ {3137, &IHidServer::StubReplyZeroed, "Unknown3137"},
+ {3138, &IHidServer::StubReplyZeroed, "Unknown3138"},
+ {3139, &IHidServer::StubReplyZeroed, "Unknown3139"},
+ {3140, &IHidServer::StubReplyZeroed, "Unknown3140"},
+ {3141, &IHidServer::StubReplyZeroed, "Unknown3141"},
+ {3142, &IHidServer::StubReplyZeroed, "Unknown3142"},
+ {3143, &IHidServer::StubReplyZeroed, "Unknown3143"},
+ {3144, &IHidServer::StubReplyZeroed, "Unknown3144"},
+ {3145, &IHidServer::StubReplyZeroed, "Unknown3145"},
+ {3146, &IHidServer::StubReplyZeroed, "Unknown3146"},
+ {3147, &IHidServer::StubReplyZeroed, "Unknown3147"},
+ {3148, &IHidServer::StubReplyZeroed, "Unknown3148"},
+ {3149, &IHidServer::StubReplyZeroed, "Unknown3149"},
{3150, C<&IHidServer::SetMouseLibraryVersion>, "SetMouseLibraryVersion"}, //21.0.0+
+ {5000, &IHidServer::StubReplyZeroed, "Unknown5000"},
// What? -- {12010, nullptr, "SetButtonConfigLeft"},
};
// clang-format on
@@ -1075,6 +1117,17 @@ Result IHidServer::SendNx2VibrationValues() {
R_SUCCEED();
}
+Result IHidServer::Unknown213() {
+ LOG_DEBUG(Service_HID, "(STUBBED) called");
+ R_SUCCEED();
+}
+
+Result IHidServer::Unknown220(Out<u8> out) {
+ LOG_DEBUG(Service_HID, "(STUBBED) called");
+ *out = 0;
+ R_SUCCEED();
+}
+
Result IHidServer::SendVibrationValueInBool(
bool is_vibrating, Core::HID::VibrationDeviceHandle vibration_device_handle,
ClientAppletResourceUserId aruid) {
diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h
index 574d54c415..0767bb91b7 100644
--- a/src/core/hle/service/hid/hid_server.h
+++ b/src/core/hle/service/hid/hid_server.h
@@ -192,6 +192,12 @@ private:
// Switch 2 sends vibration every frame with 214 (pointer buffers of device handles and
// values, like 206); Eden doesn't emulate vibration, so accept and drop it.
Result SendNx2VibrationValues();
+ // 213 (32 bytes, aruid): sent per controller every frame by Kirby Air Riders (tens of
+ // thousands of auto stub log lines a run); no reply data. Accepted and dropped like 214.
+ Result Unknown213();
+ // 220 (16 bytes, aruid): replies one byte (meaning unknown); answer 0 instead of leaving it to
+ // the message buffer's leftovers.
+ Result Unknown220(Out<u8> out);
Result ActivateConsoleSixAxisSensor(ClientAppletResourceUserId aruid);
Result StartConsoleSixAxisSensor(Core::HID::ConsoleSixAxisSensorHandle console_sixaxis_handle,
ClientAppletResourceUserId aruid);
diff --git a/src/core/hle/service/kernel_helpers.cpp b/src/core/hle/service/kernel_helpers.cpp
index e4d6925f19..db55c6f81a 100644
--- a/src/core/hle/service/kernel_helpers.cpp
+++ b/src/core/hle/service/kernel_helpers.cpp
@@ -48,6 +48,17 @@ ServiceContext::~ServiceContext() {
}
}
+namespace {
+std::mutex debug_names_mutex;
+std::unordered_map<const Kernel::KReadableEvent*, std::string> debug_names;
+} // Anonymous namespace
+
+std::string DebugEventName(const Kernel::KReadableEvent* event) {
+ std::scoped_lock lk{debug_names_mutex};
+ const auto it = debug_names.find(event);
+ return it != debug_names.end() ? it->second : std::string{"?"};
+}
+
Kernel::KEvent* ServiceContext::CreateEvent(std::string&& name) {
// Reserve a new event from the process resource limit
Kernel::KScopedResourceReservation event_reservation(kernel, process,
@@ -74,6 +85,10 @@ Kernel::KEvent* ServiceContext::CreateEvent(std::string&& name) {
Kernel::KEvent::Register(kernel, event);
{
+ std::scoped_lock lk{debug_names_mutex};
+ debug_names[&event->GetReadableEvent()] = name;
+ }
+ {
std::scoped_lock lk{event_names_mutex};
event_names[event] = std::move(name);
}
@@ -86,6 +101,10 @@ void ServiceContext::CloseEvent(Kernel::KEvent* event) {
std::scoped_lock lk{event_names_mutex};
event_names.erase(event);
}
+ {
+ std::scoped_lock lk{debug_names_mutex};
+ debug_names.erase(&event->GetReadableEvent());
+ }
event->GetReadableEvent().Close(kernel);
event->Close(kernel);
}
diff --git a/src/core/hle/service/kernel_helpers.h b/src/core/hle/service/kernel_helpers.h
index a62dd838aa..b03e49300f 100644
--- a/src/core/hle/service/kernel_helpers.h
+++ b/src/core/hle/service/kernel_helpers.h
@@ -16,6 +16,7 @@ namespace Kernel {
class KernelCore;
class KEvent;
class KProcess;
+class KReadableEvent;
} // namespace Kernel
namespace Service::KernelHelpers {
@@ -37,4 +38,7 @@ public:
bool process_created{false};
};
+/// The name a service gave an event (EDEN_NX2_SVC_TRACE shows the handles a thread waits on).
+std::string DebugEventName(const Kernel::KReadableEvent* event);
+
} // namespace Service::KernelHelpers
diff --git a/src/core/hle/service/lm/lm.cpp b/src/core/hle/service/lm/lm.cpp
index d631a6dd80..a7d6d6e707 100644
--- a/src/core/hle/service/lm/lm.cpp
+++ b/src/core/hle/service/lm/lm.cpp
@@ -91,8 +91,9 @@ public:
static const FunctionInfo functions[] = {
{0, &ILogger::Log, "Log"},
{1, &ILogger::SetDestination, "SetDestination"},
- {2, nullptr, "TransmitHashedLog"}, //20.0.0+
- {3, nullptr, "DevNotify"}, //20.0.0+
+ {2, &ILogger::StubReplyZeroed, "TransmitHashedLog"}, //20.0.0+
+ {3, &ILogger::StubReplyZeroed, "DevNotify"}, //20.0.0+
+ {11, &ILogger::StubReplyInterface, "Unknown11"},
};
RegisterHandlers(functions);
}
diff --git a/src/core/hle/service/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp
index 513bb671f8..ffdb17fd14 100644
--- a/src/core/hle/service/mii/mii.cpp
+++ b/src/core/hle/service/mii/mii.cpp
@@ -48,8 +48,8 @@ public:
{15, D<&IDatabaseService::DestroyFile>, "DestroyFile"},
{16, D<&IDatabaseService::DeleteFile>, "DeleteFile"},
{17, D<&IDatabaseService::Format>, "Format"},
- {18, nullptr, "Import"},
- {19, nullptr, "Export"},
+ {18, &IDatabaseService::StubReplyZeroed, "Import"},
+ {19, &IDatabaseService::StubReplyZeroed, "Export"},
{20, D<&IDatabaseService::IsBrokenDatabaseWithClearFlag>, "IsBrokenDatabaseWithClearFlag"},
{21, D<&IDatabaseService::GetIndex>, "GetIndex"},
{22, D<&IDatabaseService::SetInterfaceVersion>, "SetInterfaceVersion"},
@@ -57,6 +57,8 @@ public:
{24, D<&IDatabaseService::ConvertCoreDataToCharInfo>, "ConvertCoreDataToCharInfo"},
{25, D<&IDatabaseService::ConvertCharInfoToCoreData>, "ConvertCharInfoToCoreData"},
{26, D<&IDatabaseService::Append>, "Append"},
+ {27, &IDatabaseService::StubReplyZeroed, "Unknown27"},
+ {28, &IDatabaseService::StubReplyZeroed, "Unknown28"},
};
// clang-format on
diff --git a/src/core/hle/service/ns/platform_service_manager.cpp b/src/core/hle/service/ns/platform_service_manager.cpp
index 88aac56b1c..7bd8980a08 100644
--- a/src/core/hle/service/ns/platform_service_manager.cpp
+++ b/src/core/hle/service/ns/platform_service_manager.cpp
@@ -85,8 +85,12 @@ struct IPlatformServiceManager::Impl {
// Automatically populated based on shared_fonts dump or system archives.
// 6 builtin fonts + extra 2 for whatever may come after
boost::container::static_vector<FontRegion, 8> shared_font_regions;
- /// Plain TTF copies of the same fonts (Switch 2 GetBuiltInFontData), after the BFTTF data.
+ /// Where Switch 2 GetBuiltInFontData finds the same fonts as plain TTF data: page-aligned
+ /// copies in builtin_font_memory (the Switch 1 font memory is full with the system fonts).
boost::container::static_vector<FontRegion, 8> builtin_font_regions;
+ u32 builtin_font_memory_size{};
+ /// Made by the first GetBuiltInFontSharedMemory (Switch 2 titles only).
+ Kernel::KSharedMemory* builtin_font_memory{};
/// Backing memory for the shared font data
std::array<u8, SHARED_FONT_MEM_SIZE> shared_font;
};
@@ -122,7 +126,6 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch
// Attempt to load shared font data from disk
const auto* nand = fsc.GetSystemNANDContents();
std::size_t offset = 0;
- std::vector<std::vector<u8>> plain_fonts;
// Rebuild shared fonts from data ncas or synthesize
for (auto& font : SHARED_FONTS) {
FileSys::VirtualFile romfs;
@@ -158,34 +161,52 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch
Common::swap32);
// Font offset and size do not account for the header
const FontRegion region{u32(offset + 8), u32((font_data_u32.size() * sizeof(u32)) - 8)};
- std::vector<u8> plain(region.size);
- DecryptSharedFontToTTF(font_data_u32, plain);
- plain_fonts.push_back(std::move(plain));
DecryptSharedFont(font_data_u32, impl->shared_font, offset);
impl->shared_font_regions.push_back(region);
}
- for (const auto& plain : plain_fonts) {
- offset = Common::AlignUp(offset, 0x1000);
- if (offset + plain.size() > SHARED_FONT_MEM_SIZE) {
- LOG_WARNING(Service_NS, "No shared memory left for plain built-in font copies");
- break;
- }
- std::memcpy(impl->shared_font.data() + offset, plain.data(), plain.size());
- impl->builtin_font_regions.push_back({u32(offset), u32(plain.size())});
- offset += plain.size();
+ // The data after each BFTTF header is the plain font: GetBuiltInFontSharedMemory copies it.
+ u32 builtin_offset = 0;
+ for (const auto& region : impl->shared_font_regions) {
+ impl->builtin_font_regions.push_back({builtin_offset, region.size});
+ builtin_offset = Common::AlignUp(builtin_offset + region.size, 0x1000);
}
+ impl->builtin_font_memory_size = std::max<u32>(builtin_offset, 0x1000);
}
// Switch 2: the shared memory GetBuiltInFontData offsets point into (copy handle, u32 size); the
-// SDK maps it read-only. Eden serves its font shared memory, which holds plain copies too.
+// SDK maps it read-only. It holds plain copies of the fonts, apart from the Switch 1 font memory.
void IPlatformServiceManager::GetBuiltInFontSharedMemory(HLERequestContext& ctx) {
- LOG_WARNING(Service_NS, "(STUBBED) Switch 2 GetBuiltInFontSharedMemory");
- std::memcpy(kernel.GetFontSharedMem().GetPointer(), impl->shared_font.data(), impl->shared_font.size());
+ LOG_WARNING(Service_NS, "(STUBBED) Switch 2 GetBuiltInFontSharedMemory, {:#x} bytes",
+ impl->builtin_font_memory_size);
+ if (impl->builtin_font_memory == nullptr) {
+ auto* const memory = Kernel::KSharedMemory::Create(kernel);
+ if (const Result result = memory->Initialize(kernel, system.DeviceMemory(), nullptr,
+ Kernel::Svc::MemoryPermission::None,
+ Kernel::Svc::MemoryPermission::Read,
+ impl->builtin_font_memory_size);
+ result.IsError()) {
+ LOG_ERROR(Service_NS, "Failed to make {:#x} bytes of built-in font memory: {:#x}",
+ impl->builtin_font_memory_size, result.raw);
+ memory->Close(kernel);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
+ return;
+ }
+ Kernel::KSharedMemory::Register(kernel, memory);
+ u8* const data = memory->GetPointer();
+ std::memset(data, 0, impl->builtin_font_memory_size);
+ for (size_t i = 0; i < impl->builtin_font_regions.size(); ++i) {
+ std::memcpy(data + impl->builtin_font_regions[i].offset,
+ impl->shared_font.data() + impl->shared_font_regions[i].offset,
+ impl->builtin_font_regions[i].size);
+ }
+ impl->builtin_font_memory = memory;
+ }
IPC::ResponseBuilder rb{ctx, 3, 1};
rb.Push(ResultSuccess);
- rb.Push<u32>(static_cast<u32>(SHARED_FONT_MEM_SIZE));
- rb.PushCopyObjects(ctx, kernel.GetFontSharedMem());
+ rb.Push<u32>(impl->builtin_font_memory_size);
+ rb.PushCopyObjects(ctx, *impl->builtin_font_memory);
}
// nn::pl::GetBuiltInFontData(BuiltInFontDataOunce*, BuiltInFontTypeOunce, bool): returns a 16-byte
@@ -212,7 +233,11 @@ void IPlatformServiceManager::GetBuiltInFontData(HLERequestContext& ctx) {
rb.Push<u32>(0);
}
-IPlatformServiceManager::~IPlatformServiceManager() = default;
+IPlatformServiceManager::~IPlatformServiceManager() {
+ if (impl->builtin_font_memory != nullptr) {
+ impl->builtin_font_memory->Close(kernel);
+ }
+}
Result IPlatformServiceManager::RequestLoad(SharedFontType type) {
// Games don't call this so all fonts should be loaded
diff --git a/src/core/hle/service/nvdrv/core/container.cpp b/src/core/hle/service/nvdrv/core/container.cpp
index 9edce03f64..22db243e3f 100644
--- a/src/core/hle/service/nvdrv/core/container.cpp
+++ b/src/core/hle/service/nvdrv/core/container.cpp
@@ -11,6 +11,7 @@
#include "core/hle/service/nvdrv/core/heap_mapper.h"
#include "core/hle/service/nvdrv/core/nvmap.h"
#include "core/hle/service/nvdrv/core/syncpoint_manager.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/memory.h"
#include "video_core/host1x/host1x.h"
@@ -130,6 +131,7 @@ void Container::CloseSession(SessionId session_id) {
session.has_preallocated_area = false;
}
session.is_active = false;
+ Devices::Nx2GpuMemoryRegistry::Get().ForgetDeviceRanges(session.asid.id);
smmu.UnregisterProcess(impl->sessions[session_id.id].asid);
impl->id_pool.emplace_front(session_id.id);
}
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
index e66c72cd26..40313a10fb 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -5,6 +5,7 @@
// SPDX-FileCopyrightText: 2021 Skyline Team and Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <cstdlib>
#include <cstring>
#include <utility>
@@ -35,6 +36,13 @@ nvhost_as_gpu::nvhost_as_gpu(Core::System& system_, Module& module_, NvCore::Con
nvhost_as_gpu::~nvhost_as_gpu() {
Nx2GpuMemoryRegistry::Get().RemoveListener(this);
+ // Leave shared ranges to the process's other address spaces; like this address space's own
+ // ranges, one nobody else maps stays allocated.
+ for (const auto& [key, range] : nx2_device_ranges) {
+ if (range.shared) {
+ Nx2GpuMemoryRegistry::Get().ReleaseDeviceRange(range.asid, key.first, key.second);
+ }
+ }
}
NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
@@ -134,27 +142,43 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) {
}
// The same CPU range is often mapped twice (at its own address and in a sparse region), so
// reuse its SMMU mapping.
- DAddr device_address{};
- bool heap = false;
+ Nx2DeviceRange range{};
if (const auto it = nx2_device_ranges.find({cpu_address, size}); it != nx2_device_ranges.end()) {
- device_address = it->second.address;
- heap = it->second.heap;
+ range = it->second;
} else if (session->has_preallocated_area && session->mapper->IsInBounds(cpu_address, size)) {
- device_address = session->mapper->Map(cpu_address, size);
- heap = true;
+ range = {session->mapper->Map(cpu_address, size), true};
} else {
- auto& smmu = system.Host1x().MemoryManager();
- device_address = smmu.Allocate(Common::AlignUp(size, 0x20000));
- if (device_address == 0) {
+ const auto allocate = [&]() -> u64 {
+ auto& smmu = system.Host1x().MemoryManager();
+ const DAddr address = smmu.Allocate(Common::AlignUp(size, 0x20000));
+ if (address != 0) {
+ smmu.Map(address, cpu_address, size, session->asid, true);
+ }
+ return address;
+ };
+ // EDEN_NX2_PRIVATE_DEVICE_RANGES=1 (A/B aid): a range per address space.
+ static const bool private_ranges = std::getenv("EDEN_NX2_PRIVATE_DEVICE_RANGES") != nullptr;
+ if (private_ranges) {
+ range = {allocate(), false};
+ } else {
+ range = {Nx2GpuMemoryRegistry::Get().AcquireDeviceRange(session->asid.id, cpu_address,
+ size, allocate),
+ false, true, session->asid.id};
+ }
+ if (range.address == 0) {
LOG_CRITICAL(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x}: out of SMMU space",
cpu_address, size);
return;
}
- smmu.Map(device_address, cpu_address, size, session->asid, true);
}
- nx2_device_ranges[{cpu_address, size}] = {device_address, heap};
+ const DAddr device_address = range.address;
+ nx2_device_ranges[{cpu_address, size}] = range;
Nx2GpuMemoryRegistry::Get().RecordDeviceRange(cpu_address, size, device_address);
- gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ // On the GPU thread, like unmaps: a GMMU map takes the texture cache lock, and the callers hold
+ // nx2:gpu locks that the GPU thread takes under that lock (sparse blocks backed on write).
+ system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
+ gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ });
LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size,
cpu_address);
}
@@ -177,6 +201,10 @@ void nvhost_as_gpu::Nx2Release(u64 cpu_address, u64 size) {
}
const Nx2DeviceRange range = it->second;
nx2_device_ranges.erase(it);
+ if (range.shared &&
+ !Nx2GpuMemoryRegistry::Get().ReleaseDeviceRange(range.asid, cpu_address, size)) {
+ return; // another address space of the process still maps it
+ }
auto* session = container.GetSession(nx2_session);
auto* const mapper = range.heap && session != nullptr ? session->mapper.get() : nullptr;
auto& smmu = system.Host1x().MemoryManager();
@@ -224,7 +252,10 @@ void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) {
return;
}
nx2_sparse_ranges[gpu_address] = {size, device_address};
- gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ // On the GPU thread, as in Nx2Map.
+ system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
+ gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ });
LOG_DEBUG(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x} -> device {:#x}", gpu_address,
size, device_address);
}
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
index 39b63ea369..37b6d0d4c4 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -238,6 +238,8 @@ private:
struct Nx2DeviceRange {
DAddr address;
bool heap; ///< Mapped through the session's heap mapper (else an SMMU allocation)
+ bool shared{}; ///< A reference to the process's range in Nx2GpuMemoryRegistry
+ size_t asid{};
};
std::map<std::pair<u64, u64>, Nx2DeviceRange> nx2_device_ranges;
void Nx2Release(u64 cpu_address, u64 size);
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
index 5998940afa..93d51cd538 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
@@ -113,12 +113,22 @@ NvResult nvhost_ctrl_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8>
case 0x6:
return WrapFixedInlOut(this, &nvhost_ctrl_gpu::GetTPCMasks3, input, output,
inline_output);
- case 0x13:
- LOG_DEBUG(Service_NVDRV, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
- return NvResult::NotImplemented;
+ case 0x13: {
+ // NVGPU_GPU_IOCTL_VSMS_MAPPING: an 8-byte argument struct plus a buffer of
+ // {gpc_index, tpc_index} byte pairs, one per virtual SM. The SDK treats a pair left at
+ // its 0xc0c0 sentinel as failure, so fill every pair: one GPC, two SMs per TPC.
+ if (command.raw != 0xc0084713 || input.size() != 8 || output.size() != 8 ||
+ inline_output.size() < 2) {
+ return NvResult::BadParameter;
+ }
+ std::memcpy(output.data(), input.data(), output.size());
+ for (size_t i = 0; i + 1 < inline_output.size(); i += 2) {
+ inline_output[i] = 0; // gpc_index
+ inline_output[i + 1] = static_cast<u8>(i / 4); // tpc_index
+ }
+ LOG_DEBUG(Service_NVDRV, "VSMS mapping for {} SMs", inline_output.size() / 2);
+ return NvResult::Success;
+ }
default:
break;
}
@@ -218,6 +228,11 @@ void ApplyNx2Characteristics(GC& gc) {
if (remap) {
gc.flags |= 1ULL << 35;
}
+ // Engine support bits 40-44 (2D, 3D, compute, inline-to-memory, ZBC). The SDK filters its class
+ // enumeration by them, and NvRm's newer device path lists a class family in the device info
+ // only when its bit is set; without them Fire Emblem: Fortune's Weave aborts at boot and a
+ // device opened that way has no compute class (Fire Emblem's CUDA driver reports no device).
+ gc.flags |= 0x1FULL << 40;
// The Switch 2 layout keeps nine s16 interface versions at 0x70..0x80 (like nvgpu's
// *_ioctl_nr_last); its NvRm requires e.g. [0x78] >= 6 and enables features above 0xa, 0x1f
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 0cd72faed3..848c15d6f8 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -24,6 +24,7 @@
#include "video_core/engines/puller.h"
#include "video_core/gpu.h"
#include "video_core/host1x/host1x.h"
+#include "video_core/memory_manager.h"
namespace Service::Nvidia::Devices {
namespace {
@@ -435,6 +436,30 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
entries.submit_hashes.push_back(Tegra::HashPushbufferWords(words));
}
}
+ // The present returns before the GPU thread reaches this frame's work (nx2_gsv.h,
+ // frames ahead), and Mario Kart World then refills command memory it already
+ // submitted: the end of a frame ran with the next frame's present image as its render
+ // target, leaving an image three frames old to be shown. Keep the words as submitted.
+ // EDEN_NX2_PB_SNAPSHOT=0: execute from memory.
+ static const bool snapshot = [] {
+ const char* value = std::getenv("EDEN_NX2_PB_SNAPSHOT");
+ return value == nullptr || value[0] != '0';
+ }();
+ if (snapshot && channel_state->memory_manager) {
+ size_t words = 0;
+ for (const auto& entry : entries.command_lists) {
+ words += entry.size;
+ }
+ entries.submitted_words.resize(words);
+ entries.submitted_offsets.reserve(entries.command_lists.size());
+ size_t offset = 0;
+ for (const auto& entry : entries.command_lists) {
+ entries.submitted_offsets.push_back(static_cast<u32>(offset));
+ channel_state->memory_manager->ReadBlockUnsafe(
+ entry.addr, entries.submitted_words.data() + offset, entry.size * sizeof(u32));
+ offset += entry.size;
+ }
+ }
}
auto& flags = params.flags;
diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
index 869e17caf6..8dee9ccfb7 100644
--- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
+++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
@@ -5,6 +5,7 @@
#include <algorithm>
#include <atomic>
+#include <compare>
#include <functional>
#include <map>
#include <mutex>
@@ -166,12 +167,50 @@ public:
{
std::scoped_lock lk{device_mutex};
device_ranges.clear();
+ shared_ranges.clear();
}
std::scoped_lock lk{mutex};
mappings.clear();
sparse_regions.clear();
}
+ // Device (SMMU) range of a mapping outside the heap, shared by every address space of one
+ // process: the caches key memory by device address, so a range per address space made separate
+ // copies of the same memory, and presentation could read one the game never drew into. The
+ // first address space to map the range allocates it with `allocate` (0: out of space); every
+ // call takes a reference.
+ u64 AcquireDeviceRange(size_t asid, u64 cpu_address, u64 size,
+ const std::function<u64()>& allocate) {
+ std::scoped_lock lk{device_mutex};
+ const SharedRangeKey key{asid, cpu_address, size};
+ if (const auto it = shared_ranges.find(key); it != shared_ranges.end()) {
+ ++it->second.references;
+ return it->second.device_address;
+ }
+ const u64 device_address = allocate();
+ if (device_address != 0) {
+ shared_ranges.emplace(key, SharedRange{device_address, 1});
+ }
+ return device_address;
+ }
+
+ // Drops a reference. True for the last one: the caller frees the range.
+ bool ReleaseDeviceRange(size_t asid, u64 cpu_address, u64 size) {
+ std::scoped_lock lk{device_mutex};
+ const auto it = shared_ranges.find(SharedRangeKey{asid, cpu_address, size});
+ if (it == shared_ranges.end() || --it->second.references != 0) {
+ return false;
+ }
+ shared_ranges.erase(it);
+ return true;
+ }
+
+ // The process's session closed and its ASID can be reused: stop sharing its ranges.
+ void ForgetDeviceRanges(size_t asid) {
+ std::scoped_lock lk{device_mutex};
+ std::erase_if(shared_ranges, [asid](const auto& entry) { return entry.first.asid == asid; });
+ }
+
// Device (SMMU) address of a CPU range an address space mapped; used to present images.
void RecordDeviceRange(u64 cpu_address, u64 size, u64 device_address) {
std::scoped_lock lk{device_mutex};
@@ -197,6 +236,18 @@ private:
std::mutex device_mutex;
std::map<u64, std::pair<u64, u64>> device_ranges; // cpu start -> (size, device address)
+ struct SharedRangeKey {
+ size_t asid;
+ u64 cpu_address;
+ u64 size;
+ auto operator<=>(const SharedRangeKey&) const = default;
+ };
+ struct SharedRange {
+ u64 device_address;
+ u32 references;
+ };
+ std::map<SharedRangeKey, SharedRange> shared_ranges;
+
struct Mapping {
u64 gpu_address;
u64 cpu_address;
diff --git a/src/core/hle/service/nvdrv/nvdrv.cpp b/src/core/hle/service/nvdrv/nvdrv.cpp
index 12923496fb..7853d17235 100644
--- a/src/core/hle/service/nvdrv/nvdrv.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv.cpp
@@ -133,6 +133,7 @@ Module::Module(Core::System& system)
Devices::Nx2GpuMemoryRegistry::Get().Reset();
Tegra::Nx2ClearSemaphoreRegions();
Tegra::Nx2ClearSparseRegions();
+ Tegra::Nx2ClearGpuWrittenPages();
Devices::g_nx2_driver = false;
Tegra::g_nx2_ampere = false;
Core::g_nx2_present_count = 0;
diff --git a/src/core/hle/service/nvdrv/nvdrv_interface.cpp b/src/core/hle/service/nvdrv/nvdrv_interface.cpp
index 020036d74b..2ee0d726a2 100644
--- a/src/core/hle/service/nvdrv/nvdrv_interface.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv_interface.cpp
@@ -13,6 +13,7 @@
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/hle/service/nvdrv/nvdata.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "core/hle/service/nvdrv/nvdrv_interface.h"
@@ -36,7 +37,10 @@ void NVDRV::Open(HLERequestContext& ctx) {
const std::string device_name(Common::StringFromBuffer(buffer));
LOG_DEBUG(Service_NVDRV, "device_name={}", device_name);
- if (device_name == "/dev/nvhost-prof-gpu") {
+ // Switch 2 lists nvhost-prof-gpu with the other GPU nodes and the CUDA driver (Fire
+ // Emblem's subsdk1) opens it, so only refuse it to Switch 1 callers. Switch 2 opens get
+ // the logged stub device.
+ if (device_name == "/dev/nvhost-prof-gpu" && !Devices::g_nx2_driver) {
rb.Push<DeviceFD>(0);
rb.PushEnum(NvResult::NotSupported);
diff --git a/src/core/hle/service/nvnflinger/pixel_format.h b/src/core/hle/service/nvnflinger/pixel_format.h
index b6e8b5f8a0..a38c8be777 100644
--- a/src/core/hle/service/nvnflinger/pixel_format.h
+++ b/src/core/hle/service/nvnflinger/pixel_format.h
@@ -19,6 +19,7 @@ enum class PixelFormat : u32 {
Bgra8888 = 5,
Rgba5551 = 6,
Rgba4444 = 7,
+ Rgba1010102 = 43,
};
} // namespace Service::android
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 628bdeda5b..37c797fa3f 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -21,6 +21,7 @@
#include <optional>
#include <memory>
#include <mutex>
+#include <utility>
#include <vector>
#include "common/alignment.h"
@@ -45,13 +46,17 @@
#include "core/hle/kernel/k_resource_limit.h"
#include "core/hle/kernel/k_server_session.h"
#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_hardware_timer.h"
#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/k_transfer_memory.h"
+#include "core/hle/kernel/svc_results.h"
#include "core/memory.h"
#include "video_core/host1x/host1x.h"
#include "video_core/host1x/nx2_video_decoder.h"
#include "video_core/nx2.h"
+#include "video_core/textures/decoders.h"
#include "core/hle/service/hle_ipc.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
@@ -102,6 +107,14 @@ inline void TipcReplyWords(HLERequestContext& ctx, std::initializer_list<u32> wo
TipcReplyWords(ctx, std::span<const u32>{words.begin(), words.size()});
}
+// TIPC 0(u64 token) on the Switch 2 sessions: a handshake newer SDKs send first; the reply echoes
+// the token. (The auto stub's reply only echoed it by leaving the request's words in place.)
+inline void TipcHandshake(HLERequestContext& ctx, std::string_view session) {
+ const auto* w = ctx.CommandBuffer();
+ LOG_INFO(Service, "{} TIPC 0 handshake {:#x}", session, u64{w[2]} | (u64{w[3]} << 32));
+ TipcReplyWords(ctx, {w[2], w[3]});
+}
+
// Replies with success and a copied handle to a new session served by `iface`.
template <class T>
inline void ReturnRawSession(Core::System& system, HLERequestContext& ctx, std::shared_ptr<T> iface) {
@@ -139,6 +152,8 @@ public:
{103, &INx2SyncptSession::QueryCounter, "QueryCounter"},
{201, &INx2SyncptSession::RequestSlotEvent, "RequestSlotEvent (guessed)"},
{202, &INx2SyncptSession::CancelSlotEvent, "CancelSlotEvent (guessed)"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {21, &INx2SyncptSession::StubReplyZeroed, "Unknown21"},
});
}
@@ -160,9 +175,7 @@ public:
private:
// Newer SDKs (title 1) open with TIPC 0(u64 token) and abort unless the reply echoes it.
void Handshake(HLERequestContext& ctx) {
- const auto* w = ctx.CommandBuffer();
- LOG_INFO(Service, "nx2:syncpt TIPC 0 handshake {:#x}", u64{w[2]} | (u64{w[3]} << 32));
- TipcReplyWords(ctx, {w[2], w[3]});
+ TipcHandshake(ctx, "nx2:syncpt");
}
void GetCount(HLERequestContext& ctx) {
@@ -299,11 +312,14 @@ public:
explicit INx2GpuSession(Core::System& system_)
: ServiceFramework{system_, "nx2:gpu"}, m_context{system_, "nx2:gpu"} {
RegisterHandlersTipc({
+ {0, &INx2GpuSession::Handshake, "Handshake"},
{11, &INx2GpuSession::GetIdRange, "GetIdRange (guessed)"},
{12, &INx2GpuSession::GetIdEvent, "GetIdEvent (guessed)"},
{21, &INx2GpuSession::Start, "Start (guessed)"},
{22, &INx2GpuSession::OpenSubSession, "OpenSubSession (guessed)"},
{33, &INx2GpuSession::GetCount33, "Unknown33 (u32 count)"},
+ {41, &INx2GpuSession::CreateGraphicsDevice, "CreateGraphicsDevice"},
+ {42, &INx2GpuSession::DestroyGraphicsDevice, "DestroyGraphicsDevice"},
{201, &INx2GpuSession::GetUserPartition, "GetUserPartition (newer SDKs)"},
{202, &INx2GpuSession::GetGsvPartition, "GetGsvPartition (newer SDKs)"},
{203, &INx2GpuSession::GetVaRange, "GetVaRange (guessed)"},
@@ -316,10 +332,22 @@ public:
{214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateSparseRegion"},
{215, &INx2GpuSession::SubDestroySparseRegion, "DestroySparseRegion"},
{216, &INx2GpuSession::SubSparseMap, "ExecuteSparseMapRequests"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {23, &INx2GpuSession::StubReplyZeroed, "Unknown23"},
+ {24, &INx2GpuSession::StubReplyZeroed, "Unknown24"},
+ {25, &INx2GpuSession::StubReplyZeroed, "Unknown25"},
+ {34, &INx2GpuSession::StubReplyZeroed, "Unknown34"},
+ {51, &INx2GpuSession::StubReplyZeroed, "Unknown51"},
+ {8001, &INx2GpuSession::StubReplyZeroed, "Unknown8001"},
+ {8002, &INx2GpuSession::StubReplyZeroed, "Unknown8002"},
+ {8003, &INx2GpuSession::StubReplyZeroed, "Unknown8003"},
+ {8004, &INx2GpuSession::StubReplyZeroed, "Unknown8004"},
});
if (LazySparseRegions()) {
Tegra::Nx2SetSparseBacking(
- [this](u64 gpu_address, u64 size) { BackSparseWrite(gpu_address, size); });
+ [this](u64 gpu_address, u64 size, const u8* data) {
+ BackSparseWrite(gpu_address, size, data);
+ });
}
}
@@ -334,6 +362,10 @@ public:
}
private:
+ void Handshake(HLERequestContext& ctx) {
+ TipcHandshake(ctx, "nx2:gpu");
+ }
+
static constexpr u32 NumIds = 32;
Kernel::KEvent* EventFor(u32 id) {
@@ -395,9 +427,39 @@ private:
static_cast<u32>(second), static_cast<u32>(second >> 32)});
}
+ // An empty partition is min = ~0, max = 0 (min > max). Fire Emblem: Fortune's Weave's SDK
+ // reads (0, 0) as a one-byte partition at address 0 and aborts in nn::gpu::sdk.
void GetGsvPartition(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:gpu TIPC 202 [2]={:#x}, empty", ctx.CommandBuffer()[2]);
- TipcReplyWords(ctx, {0, 0, 0, 0});
+ TipcReplyWords(ctx, {~0U, ~0U, 0, 0});
+ }
+
+ // Fire Emblem: Fortune's Weave: 41(name u64, flags u64, debug label u64) returns a device id
+ // that later NvRm calls name; 0 is not a valid id. Only flags 0 has been seen.
+ void CreateGraphicsDevice(HLERequestContext& ctx) {
+ const auto* b = ctx.CommandBuffer();
+ const u64 name = u64{b[2]} | (u64{b[3]} << 32);
+ const u64 flags = u64{b[4]} | (u64{b[5]} << 32);
+ u32 id = 0;
+ for (u32 candidate = 1; candidate < m_devices.size(); ++candidate) {
+ if (!m_devices[candidate]) {
+ m_devices[candidate] = true;
+ id = candidate;
+ break;
+ }
+ }
+ LOG_INFO(Service, "nx2:gpu 41 create graphics device name={:#x} flags={:#x} -> id {}", name,
+ flags, id);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(id != 0 ? ResultSuccess : Kernel::ResultOutOfResource);
+ rb.Push(id);
+ }
+
+ void DestroyGraphicsDevice(HLERequestContext& ctx) {
+ const u32 id = ctx.CommandBuffer()[2] & 0xff;
+ const bool existed = id < m_devices.size() && std::exchange(m_devices[id], false);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(existed ? ResultSuccess : Kernel::ResultNotFound);
}
void OpenSubSession(HLERequestContext& ctx) {
@@ -551,12 +613,29 @@ private:
// backed whole, they ran the pool dry and the later ones got no memory at all.
// The region gets a device range of its own, unbacked pages read zero, and
// BackSparseWrite gives a block memory before the GPU writes into it.
- m_sparse_regions[start] = {size, std::vector<u8>(Common::DivCeil(size, SparseBlockSize))};
+ m_sparse_regions[start] = {
+ .size = size,
+ .blocks = std::vector<u8>(Common::DivCeil(size, SparseBlockSize)),
+ };
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size);
Tegra::Nx2SparseRegionCreated(start, size, SparseBlockSize);
Tegra::Nx2AddSparseSpan(start, size);
LOG_INFO(Service, "nx2:gpu 214 sparse region [{:#x}, {:#x}] backed as the GPU writes it",
start, last);
+ } else if (LazyGpuOnlyRegions()) {
+ // The rest is written by the host GPU, not through guest memory (title 1's and
+ // title 4's render targets, 285-300 MB regions): like a sparse region it gets a
+ // device range of its own and memory per block only when something writes guest
+ // memory there (downloads, copies). The texture cache sees ordinary memory.
+ m_sparse_regions[start] = {
+ .gpu_only = true,
+ .size = size,
+ .blocks = std::vector<u8>(Common::DivCeil(size, SparseBlockSize)),
+ };
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size);
+ Tegra::Nx2AddSparseSpan(start, size);
+ LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed as it is written",
+ start, last);
} else if (const auto address = BackGpuOnlyRegion(start, size)) {
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(start, *address, size);
LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x} "
@@ -665,6 +744,23 @@ private:
TipcReplyWords(ctx, {static_cast<u32>(count), static_cast<u32>(u64{count} >> 32)});
}
+ static bool IsAllZero(const u8* data, size_t size) {
+ size_t i = 0;
+ for (; i + sizeof(u64) <= size; i += sizeof(u64)) {
+ u64 word;
+ std::memcpy(&word, data + i, sizeof(word));
+ if (word != 0) {
+ return false;
+ }
+ }
+ for (; i < size; ++i) {
+ if (data[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
// Returns the pages of destroyed regions the GPU thread is done with to the kernel.
void FreeReleasedRegions() {
auto* const process = system.ApplicationProcess();
@@ -772,7 +868,7 @@ private:
// usually on the GPU thread): the blocks the write touches get memory, cleared, behind the
// region's device pages. The GPU mapping stays as it is, so the images over the region keep
// their storage and only see the write.
- void BackSparseWrite(u64 gpu_address, u64 size) {
+ void BackSparseWrite(u64 gpu_address, u64 size, const u8* data) {
struct Pending {
u64 region;
u64 block;
@@ -795,6 +891,7 @@ private:
}
};
bool waits_for_other = false;
+ u64 zero_bytes = 0;
{
std::unique_lock lk{m_region_mutex};
for_regions([&](u64 start, SparseRegion& region, u64 first, u64 last) {
@@ -805,6 +902,18 @@ private:
region.blocks[block] = BlockWritten;
Tegra::Nx2SparseBlockWritten(start, block);
} else if (region.blocks[block] == BlockEmpty) {
+ // Zeros over a block nobody wrote change nothing: it reads zero without
+ // memory. Title 1 clears ~1 GB of them at boot.
+ const u64 block_begin = start + block * SparseBlockSize;
+ const u64 write_begin = (std::max)(gpu_address, block_begin);
+ const u64 write_end = (std::min)(
+ {end, block_begin + SparseBlockSize, start + region.size});
+ if (data != nullptr &&
+ IsAllZero(data + (write_begin - gpu_address), write_end - write_begin) &&
+ !Tegra::Nx2SparseBlockIsWritten(start, block)) {
+ zero_bytes += write_end - write_begin;
+ continue;
+ }
region.blocks[block] = BlockPending;
todo.push_back({start, block});
} else if (region.blocks[block] == BlockPending) {
@@ -813,6 +922,16 @@ private:
}
});
}
+ if (zero_bytes != 0) {
+ std::scoped_lock lk{m_region_mutex};
+ const u64 before = m_sparse_zero_bytes;
+ m_sparse_zero_bytes += zero_bytes;
+ if (before == 0 || (before >> 28) != (m_sparse_zero_bytes >> 28)) {
+ LOG_INFO(Service, "nx2:gpu {} MiB of zeros over unwritten sparse blocks left out "
+ "(they read zero without memory)",
+ m_sparse_zero_bytes >> 20);
+ }
+ }
// Allocating and mapping happens outside the lock (see AllocateGpuOnlyBacking).
for (const auto& [start, block] : todo) {
u64 block_size = SparseBlockSize;
@@ -896,6 +1015,15 @@ private:
return lazy;
}
+ // EDEN_NX2_GPU_ONLY_LAZY=0 backs GPU-only regions whole, as before.
+ static bool LazyGpuOnlyRegions() {
+ static const bool lazy = [] {
+ const char* value = std::getenv("EDEN_NX2_GPU_ONLY_LAZY");
+ return value == nullptr || value[0] != '0';
+ }();
+ return lazy;
+ }
+
static constexpr u32 SparseRegionFlag = 0x200; ///< Word 7 of 214 (0x60c arrays, 0xa0c pools)
static constexpr u64 SparseBlockSize = 4ULL << 20;
static constexpr u8 BlockEmpty = 0;
@@ -904,6 +1032,7 @@ private:
static constexpr u8 BlockPending = 3; ///< Being allocated and mapped by BackSparseWrite
static constexpr u8 BlockWritten = 4; ///< Written in a region the game maps tiles into
struct SparseRegion {
+ bool gpu_only{}; ///< A GPU-only region (no sparse flag), backed lazily as well
u64 size;
std::vector<u8> blocks; ///< BlockEmpty / Backed / Failed / Pending / Written per block
bool warned{}; ///< Logged that the pool ran out for its blocks
@@ -913,6 +1042,7 @@ private:
std::map<u64, SparseRegion> m_sparse_regions; ///< By GPU address
u64 m_sparse_blocks{}; ///< Sparse blocks backed so far
u64 m_sparse_backed_bytes{}; ///< Sparse region memory given out so far
+ u64 m_sparse_zero_bytes{}; ///< Zeros written over unwritten sparse blocks, left out
u64 m_sparse_released_bytes{}; ///< Blocks given back once the game mapped tiles there
// nn::gpu::sdk::SparseMapRequest (216), filled from NvRm's nvgpu_as_remap_op.
@@ -1246,6 +1376,47 @@ private:
std::array<Kernel::KEvent*, NumIds> m_events{};
std::map<u32, GpuMemory> m_memory;
u32 m_next_memory_handle{1};
+ std::array<bool, 64> m_devices{}; // gpu 41 graphics device ids (0 unused)
+
+ // Every 30 s: the backing of sparse blocks and GPU-only regions is marked cold. The game
+ // writes it once (texture uploads by 2D blits: title 1 fills 2-4 GB of texture pools) and it
+ // is read again only when the texture cache builds an image over it, so the OS may compress or
+ // page it out before anything else. Last member: stops before the regions are destroyed.
+ void HintColdBacking() {
+ struct Range {
+ u64 physical_offset;
+ u64 virtual_address;
+ u64 size;
+ };
+ std::vector<Range> ranges;
+ {
+ std::scoped_lock lk{m_region_mutex};
+ for (const auto& [gpu_address, region] : m_gpu_only_regions) {
+ u64 virtual_address = region.cpu_address;
+ for (const auto& block : *region.pages) {
+ ranges.push_back({GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base,
+ virtual_address, block.GetSize()});
+ virtual_address += block.GetSize();
+ }
+ }
+ }
+ auto& buffer = system.DeviceMemory().buffer;
+ for (const Range& range : ranges) {
+ buffer.HintCold(range.physical_offset, range.size, range.virtual_address);
+ }
+ }
+ std::jthread m_cold_hint_thread{[this](std::stop_token stop) {
+ Common::SetCurrentThreadName("nx2:ColdHint");
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::Low);
+ while (!stop.stop_requested()) {
+ for (int i = 0; i < 30 && !stop.stop_requested(); ++i) {
+ std::this_thread::sleep_for(std::chrono::seconds{1});
+ }
+ if (!stop.stop_requested()) {
+ HintColdBacking();
+ }
+ }
+ }};
};
// Native memory and images handed out by the Switch 2 graphics server (gsv 111/114/423/429/315),
@@ -1264,6 +1435,10 @@ struct Nx2NativeObjects {
u64 offset{}; // into its memory (images of a decoder pool share one)
bool window_pool{};
bool decoder{}; // a video decoder output image (gsv 911)
+ // A decoder image the client reads on the GPU (gsv 428/426): written block-linear.
+ bool gpu_read{};
+ u32 gpu_maps{}; // outstanding 426 maps
+ bool mapped_here{}; // 426 added its GPU mapping (no other mapping covered it)
};
std::mutex mutex;
std::map<u32, Memory> memories;
@@ -1347,6 +1522,34 @@ private:
std::map<u32, Kernel::KEvent*> m_events;
};
+// nn::camera::sdk camera context (gsv 602). GameChat builds its local camera video storage from it
+// and polls it every frame. Nothing about the commands is known beyond the arguments seen (3 and
+// 21 take the camera name "Default", 29 a u32 0x180 = 384, the width of the 384x216 camera images),
+// and the game runs on an empty success, so they answer that without a log line per call.
+class INx2CameraSession final : public ServiceFramework<INx2CameraSession> {
+public:
+ explicit INx2CameraSession(Core::System& system_) : ServiceFramework{system_, "nx2:camera"} {
+ RegisterHandlersTipc({
+ {3, &INx2CameraSession::Stub, "Unknown3 (camera name \"Default\")"},
+ {21, &INx2CameraSession::Stub, "Unknown21 (camera name \"Default\")"},
+ {29, &INx2CameraSession::Stub, "Unknown29 (u32 0x180, polled every frame)"},
+ });
+ }
+
+private:
+ void Stub(HLERequestContext& ctx) {
+ const u32 command = ctx.GetCommand();
+ if (m_logged.insert(command).second) {
+ LOG_WARNING(Service, "(STUBBED) nx2:camera TIPC {} [2]={:#x} [3]={:#x}, returning success",
+ command, ctx.CommandBuffer()[2], ctx.CommandBuffer()[3]);
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ std::set<u32> m_logged;
+};
+
// Video decoder output images (nn::codec image pools, gsv 991/911/961). The preference
// (MakeDecodeImagePreference) holds an output layout code at word 1 and (width - 1) |
// (height - 1) << 16 at word 2. Codes come in pairs, one per CodecMode: 1/4 NV12 (format 3, what
@@ -1401,19 +1604,73 @@ public:
: ServiceFramework{system_, "nx2:vi2"}, m_context{system_, "nx2:vi2"},
m_objects{std::move(objects_)} {
RegisterHandlersTipc({
+ {0, &INx2Vi2Session::Handshake, "Handshake"},
{1, &INx2Vi2Session::ReturnOne, "Unknown1"},
{3, &INx2Vi2Session::ReturnOne, "GetDisplayCount (guessed)"},
{5, &INx2Vi2Session::GetTimestampRecorderCount, "GetTimestampRecorderCount (guessed)"},
{11, &INx2Vi2Session::ReturnEvent, "Unknown11"},
- {31, &INx2Vi2Session::GetVsyncEvent, "GetVsyncEvent (guessed)"},
- {32, &INx2Vi2Session::GetVsyncEvent, "GetDisplayVsyncEvent (guessed)"},
- // Title 1 calls these ~75 times a second with the display name "default"; the
- // auto stub's empty success reply works, but logged two lines per call (a 50 MB log
- // in an 18 minute session).
- {104, &INx2Vi2Session::SucceedQuietly, "Unknown104 (display \"default\")"},
- {105, &INx2Vi2Session::SucceedQuietly, "Unknown105 (display \"default\")"},
+ {31, &INx2Vi2Session::AcquireNativeEvent, "AcquireNativeEvent"},
+ {32, &INx2Vi2Session::DuplicateNativeEvent, "DuplicateNativeEvent"},
+ {261, &INx2Vi2Session::BindVsync, "BindDisplayVsyncEvent"},
+ {262, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayVsyncEvent"},
+ {263, &INx2Vi2Session::BindChange, "BindDisplayRefreshRateEvent"},
+ {264, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayRefreshRateEvent"},
+ {265, &INx2Vi2Session::BindChange, "BindDisplayHdrStatusEvent"},
+ {266, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayHdrStatusEvent"},
+ {267, &INx2Vi2Session::BindChange, "BindDisplayHdrPaperWhiteEvent"},
+ {268, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayHdrPaperWhiteEvent"},
+ {269, &INx2Vi2Session::BindChange, "BindDisplayBufferMaxResolutionEvent"},
+ {270, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayBufferMaxResolutionEvent"},
+ {271, &INx2Vi2Session::BindChange, "BindDisplayModeCapabilityEvent"},
+ {272, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayModeCapabilityEvent"},
+ // Title 1 calls these ~75 times a second with the display name "default"; quiet
+ // handlers (the auto stub logged two lines per call: a 50 MB log in an 18 minute
+ // session).
+ {103, &INx2Vi2Session::QueryDisplayRefreshRate, "QueryDisplayRefreshRate"},
+ {104, &INx2Vi2Session::QueryDisplayHdrStatus, "QueryDisplayHdrStatus"},
+ {105, &INx2Vi2Session::QueryDisplayHdrPaperWhite, "QueryDisplayHdrPaperWhite"},
+ {212, &INx2Vi2Session::GetLayerBufferBits, "GetLayerBufferBits"},
+ {106, &INx2Vi2Session::GetBufferMaxResolution, "GetBufferMaxResolution"},
{228, &INx2Vi2Session::CancelClientBuffer, "CancelClientBuffer"},
{229, &INx2Vi2Session::PresentClientBuffer, "PresentClientBuffer"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {13, &INx2Vi2Session::StubReplyZeroed, "Unknown13"},
+ {51, &INx2Vi2Session::StubReplyZeroed, "Unknown51"},
+ {101, &INx2Vi2Session::StubReplyZeroed, "Unknown101"},
+ {102, &INx2Vi2Session::StubReplyZeroed, "Unknown102"},
+ {107, &INx2Vi2Session::StubReplyZeroed, "Unknown107"},
+ {108, &INx2Vi2Session::StubReplyZeroed, "Unknown108"},
+ {111, &INx2Vi2Session::StubReplyZeroed, "Unknown111"},
+ {112, &INx2Vi2Session::StubReplyZeroed, "Unknown112"},
+ {113, &INx2Vi2Session::StubReplyZeroed, "Unknown113"},
+ {202, &INx2Vi2Session::StubReplyZeroed, "Unknown202"},
+ {230, &INx2Vi2Session::StubReplyZeroed, "Unknown230"},
+ {231, &INx2Vi2Session::StubReplyZeroed, "Unknown231"},
+ {232, &INx2Vi2Session::StubReplyZeroed, "Unknown232"},
+ {251, &INx2Vi2Session::StubReplyZeroed, "Unknown251"},
+ {252, &INx2Vi2Session::StubReplyZeroed, "Unknown252"},
+ {261, &INx2Vi2Session::StubReplyZeroed, "Unknown261"},
+ {262, &INx2Vi2Session::StubReplyZeroed, "Unknown262"},
+ {263, &INx2Vi2Session::StubReplyZeroed, "Unknown263"},
+ {264, &INx2Vi2Session::StubReplyZeroed, "Unknown264"},
+ {265, &INx2Vi2Session::StubReplyZeroed, "Unknown265"},
+ {266, &INx2Vi2Session::StubReplyZeroed, "Unknown266"},
+ {267, &INx2Vi2Session::StubReplyZeroed, "Unknown267"},
+ {268, &INx2Vi2Session::StubReplyZeroed, "Unknown268"},
+ {269, &INx2Vi2Session::StubReplyZeroed, "Unknown269"},
+ {270, &INx2Vi2Session::StubReplyZeroed, "Unknown270"},
+ {271, &INx2Vi2Session::StubReplyZeroed, "Unknown271"},
+ {272, &INx2Vi2Session::StubReplyZeroed, "Unknown272"},
+ {301, &INx2Vi2Session::StubReplyZeroed, "Unknown301"},
+ {302, &INx2Vi2Session::StubReplyZeroed, "Unknown302"},
+ {351, &INx2Vi2Session::StubReplyZeroed, "Unknown351"},
+ {352, &INx2Vi2Session::StubReplyZeroed, "Unknown352"},
+ {353, &INx2Vi2Session::StubReplyZeroed, "Unknown353"},
+ {354, &INx2Vi2Session::StubReplyZeroed, "Unknown354"},
+ {355, &INx2Vi2Session::StubReplyZeroed, "Unknown355"},
+ {356, &INx2Vi2Session::StubReplyZeroed, "Unknown356"},
+ {358, &INx2Vi2Session::StubReplyZeroed, "Unknown358"},
+ {359, &INx2Vi2Session::StubReplyZeroed, "Unknown359"},
});
}
@@ -1433,6 +1690,10 @@ public:
}
private:
+ void Handshake(HLERequestContext& ctx) {
+ TipcHandshake(ctx, "nx2:vi2");
+ }
+
// 229(u64 client token, u32 slot, u32 image, ...fences): result, u64 mask of buffer slots the
// client may acquire next, four zero words. Nothing is shown yet; every slot is handed back
// at once (changes.patch's default mask is 0x7fff).
@@ -1457,16 +1718,28 @@ private:
LOG_DEBUG(Service, "nx2:vi2 present #{} fence ({}, {:#x}), waiting for ({}, {:#x}); now {:#x}",
m_presents, ~w[22], w[24], m_previous_fence_id, m_previous_fence_value,
~w[22] < 192 ? system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(~w[22]) : 0U);
+ // Only run a frame ahead while the GPU keeps up. With the previous frame still on the
+ // GPU here, the GPU is the limit and running ahead gains nothing; and Splatoon Raiders,
+ // which reuses its command memory as if the GPU were at most a frame behind, rewrote
+ // command lists Eden had not read yet (misread commands, then crashes, with a slow
+ // GPU). Wait for this frame too then.
+ const bool gpu_behind =
+ m_previous_fence_id < 192 &&
+ !system.Host1x().GetSyncpointManager().IsReadyGuest(m_previous_fence_id,
+ m_previous_fence_value);
WaitForFrame(m_previous_fence_id, m_previous_fence_value);
+ if (gpu_behind) {
+ WaitForFrame(~w[22], w[24]);
+ }
m_previous_fence_id = ~w[22];
m_previous_fence_value = w[24];
} else {
WaitForFrame(~w[22], w[24]);
}
const auto waited = std::chrono::steady_clock::now();
- Composite(w[5], ~w[22], w[24]);
+ Composite(w[5], ~w[22], w[24], w[7]);
const auto composited = std::chrono::steady_clock::now();
- PaceToVblank();
+ PaceToVblank(w[12]);
const auto paced = std::chrono::steady_clock::now();
// Where each frame's time goes on the present thread, averaged per 600 presents: the
// game is blocked here while Eden's GPU thread catches up (fence) and composites.
@@ -1522,18 +1795,39 @@ private:
// present faster than that. Every slot is reported free here: hold each present until the
// next vblank instead (title 4 ran at up to 160 fps, its timing and audio with it).
std::chrono::steady_clock::time_point m_next_vblank{};
+ u32 m_swap_word{};
- void PaceToVblank() {
+ // timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave
+ // sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced
+ // at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL=<n> forces the interval.
+ void PaceToVblank(u32 timing) {
if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) {
return;
}
- const auto interval = std::chrono::nanoseconds{100'000'000'000LL / 60 / Settings::SpeedLimit()};
+ static const u32 forced_interval = [] {
+ const char* value = std::getenv("EDEN_NX2_SWAP_INTERVAL");
+ return value ? static_cast<u32>(std::strtoul(value, nullptr, 10)) : 0U;
+ }();
+ const u32 swap_interval =
+ forced_interval != 0 ? forced_interval : std::clamp<u32>(timing >> 16, 1, 4);
+ const u32 refresh_rate = (timing & 0xffff) >= 30 && (timing & 0xffff) <= 240 ? timing & 0xffff : 60;
+ if (timing != std::exchange(m_swap_word, timing)) {
+ LOG_INFO(Service, "nx2:vi2 present #{} swap interval {} at {} Hz (word 12 = {:#x})",
+ m_presents, swap_interval, refresh_rate, timing);
+ }
+ const auto interval = std::chrono::nanoseconds{
+ 100'000'000'000LL * swap_interval / refresh_rate / Settings::SpeedLimit()};
const auto now = std::chrono::steady_clock::now();
if (now > m_next_vblank + interval) {
- // Running slower than 60 fps: show at once and don't bank the lost time.
+ // Running slower than the swap interval: show at once and don't bank the lost time.
m_next_vblank = now;
} else if (now < m_next_vblank) {
- std::this_thread::sleep_until(m_next_vblank);
+ // Sleep this service thread in the guest kernel. It runs on guest core 3, and a host
+ // sleep held that core for the whole wait: game threads scheduled there ran only
+ // between presents (Fortune's Weave's loader read one RomFS block per frame).
+ const auto wait = std::chrono::duration_cast<std::chrono::nanoseconds>(m_next_vblank - now);
+ static_cast<void>(Kernel::GetCurrentThread(kernel).Sleep(
+ kernel, kernel.HardwareTimer().GetTick() + wait.count() + 2));
}
m_next_vblank += interval;
}
@@ -1586,9 +1880,12 @@ private:
// Shows a presented native image: word 5 is the image id, ~word 22 / word 24 the GPU fence
// (syncpoint, value) to wait for (layout from changes.patch). The image is a block-linear
// RGBA8 surface in the console's native GOB layout at GPU address == CPU address.
- void Composite(u32 image_id, u32 fence_id, u32 fence_value) {
+ // shown_size: the present's word 7, ((height - 1) << 16) | (width - 1) of the area the game drew
+ // into from the image's top-left corner (0 when it gives none).
+ void Composite(u32 image_id, u32 fence_id, u32 fence_value, u32 shown_size) {
u64 address{};
Nx2ImageLayout layout{};
+ u32 image_format{};
{
std::scoped_lock lk{m_objects->mutex};
const auto image = m_objects->images.find(image_id);
@@ -1601,6 +1898,7 @@ private:
}
address = memory->second.address;
layout = Nx2CalculateImageLayout(image->second.preference.data());
+ image_format = image->second.preference[6];
}
const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address);
if (!device) {
@@ -1610,6 +1908,16 @@ private:
}
u32 crop_width = layout.width;
u32 crop_height = layout.height;
+ // The present says how much of the image the game drew: Kirby Air Riders draws 1920x1080
+ // into 2560x1440 images, Mario Kart World 2560x1440 into 3840x2160 ones. Showing the whole
+ // image put the frame in the top-left corner.
+ const u32 shown_width = (shown_size & 0xFFFF) + 1, shown_height = (shown_size >> 16) + 1;
+ const bool has_shown_size = shown_size != 0 && shown_width <= layout.width &&
+ shown_height <= layout.height;
+ if (has_shown_size) {
+ crop_width = shown_width;
+ crop_height = shown_height;
+ }
// Handheld titles keep their docked present images (up to 3840x2160) and draw the frame
// into the top-left 1920x1080, the handheld panel's resolution; the console's display
// scans out only that region. Showing whole images put the frame in a corner inside black
@@ -1617,7 +1925,8 @@ private:
if (!Settings::IsDockedMode()) {
crop_width = (std::min)(crop_width, 1920U);
crop_height = (std::min)(crop_height, 1080U);
- } else if (system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL &&
+ } else if (!has_shown_size &&
+ system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL &&
layout.width == 3840 && layout.height == 2160) {
// Mario Kart World docked draws 2560x1440 into the top-left of its 4K images.
crop_width = 2560;
@@ -1639,8 +1948,9 @@ private:
(u64{crop_width} << 16) | crop_height;
if (shown != m_shown_layout) {
m_shown_layout = shown;
- LOG_INFO(Service, "nx2:vi2 composite image {} layout {}x{}, crop {}x{}",
- image_id, layout.width, layout.height, crop_width, crop_height);
+ LOG_INFO(Service,
+ "nx2:vi2 composite image {} layout {}x{}, drawn area {:#x}, crop {}x{}",
+ image_id, layout.width, layout.height, shown_size, crop_width, crop_height);
}
std::vector<Tegra::FramebufferConfig> layers{Tegra::FramebufferConfig{
.address = *device,
@@ -1648,7 +1958,9 @@ private:
.width = layout.width,
.height = layout.height,
.stride = static_cast<u32>(layout.pitch / 4),
- .pixel_format = android::PixelFormat::Rgba8888,
+ // Preference word 6: 1 = RGBA8, 4 = RGB10A2.
+ .pixel_format = image_format == 4 ? android::PixelFormat::Rgba1010102
+ : android::PixelFormat::Rgba8888,
.transform_flags = android::BufferTransformFlags::Unset,
.crop_rect = {0, 0, static_cast<int>(crop_width), static_cast<int>(crop_height)},
.blending = Tegra::BlendMode::Opaque,
@@ -1747,15 +2059,66 @@ private:
static_cast<u32>(AvailableSlots >> 32), 0, 0, 0, 0});
}
+ // nn::vi2::QueryDisplayRefreshRate(display name): u32 refresh rate in Hz, then a bool (a u64
+ // in all). nn::vi::detail::DefaultDisplay::QueryRefreshRate takes only -1, 60 or 120 and fails
+ // on anything else; Minecraft polls it and never draws while it fails. A one-word reply left
+ // the bool to whatever the message buffer held.
+ void QueryDisplayRefreshRate(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {60, 0});
+ }
+
+ // nn::vi2::QueryDisplayHdrPaperWhite(display name): u16 paper white in nits. The empty reply
+ // left it to the message buffer's leftovers; answer the BT.2408 reference white.
+ void QueryDisplayHdrPaperWhite(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {203});
+ }
+
+ // nn::vi2::QueryDisplayHdrStatus(display name): u64 HdrOutputFormat, u16. The sdk's
+ // DefaultDisplay::QueryHdrStatus takes format 1 as HDR output and fails with 0x272 on
+ // anything above 1. With the empty auto stub reply it read the request's leftover words as
+ // the format; Kirby Air Riders re-queries whenever its HDR status event fires and rebuilt its
+ // window images every few frames, which showed black. Report SDR output.
+ // nn::vi2::QueryDisplayHdrStatus(display name): u64 output format, u16 peak luminance in
+ // nits. The sdk takes format 1 as SDR and 2 as HDR and fails the query on any other value.
+ // Report SDR output with a typical SDR panel's peak.
+ void QueryDisplayHdrStatus(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {1, 0, 400});
+ }
+
+ // 212(u64 layer): u64 bits of the layer's buffer slots that are ready. The client polls it once
+ // a second until some are set; it only got going because the auto stub's reply happened to
+ // hold non-zero leftovers. Report the same slots 228/229 hand back.
+ void GetLayerBufferBits(HLERequestContext& ctx) {
+ constexpr u64 AvailableSlots = 0x7fff;
+ TipcReplyWords(ctx, {static_cast<u32>(AvailableSlots), static_cast<u32>(AvailableSlots >> 32)});
+ }
+
void ReturnOne(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:vi2 TIPC {}, returning 1", ctx.GetCommand());
TipcReplyWords(ctx, {1});
}
- // The same reply as the auto stub (success, no data), without its log lines.
- void SucceedQuietly(HLERequestContext& ctx) {
- IPC::ResponseBuilder rb{ctx, 2};
- rb.Push(ResultSuccess);
+ // The highest buffer resolution mode of the "default" display. Fire Emblem: Fortune's Weave picks
+ // its output from it: 1 -> 1280x720, 2 -> 1920x1080, 4 and 8 -> 2560x1440; 0 (the auto stub's
+ // reply), 3 and 5 fall back to 720p. Docked reports 4, handheld 2 (the screen is 1080p);
+ // EDEN_VI2_MAX_RESOLUTION_MODE=<n> overrides.
+ void GetBufferMaxResolution(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ constexpr u64 DefaultDisplay = 0x00746c7561666564ULL; // "default"
+ Result result = Kernel::ResultInvalidCombination;
+ u64 mode{};
+ if (packet[0] == 0x7a && packet[1] == 2) {
+ result = Result{ErrorModule::VI, 7};
+ if (name == DefaultDisplay) {
+ static const char* forced = std::getenv("EDEN_VI2_MAX_RESOLUTION_MODE");
+ mode = forced ? std::strtoull(forced, nullptr, 0) : Settings::IsDockedMode() ? 4 : 2;
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push(mode);
}
// Must be < 8; nn::vi::detail::LayerHistory creates 7 timestamp recorders per layer.
@@ -1774,17 +2137,105 @@ private:
rb.PushCopyObjects(ctx, event->GetReadableEvent());
}
- // 31(u32 kind) and 32(u32 display): events signalled on every vblank. Title 1's main thread
- // waits on the 32 event until it has fired twice before it submits any GPU work, and hung
- // there while it never fired.
- void GetVsyncEvent(HLERequestContext& ctx) {
- LOG_INFO(Service, "nx2:vi2 {} vsync event, [2]={:#x}", ctx.GetCommand(), ctx.CommandBuffer()[2]);
- auto* event = m_context.CreateEvent(fmt::format("nx2:vi2:Vsync{}", m_events.size()));
- m_events.push_back(event);
- {
- std::scoped_lock lk{m_vsync->mutex};
- m_vsync->events.push_back(event);
+ // 31(u32 index < 8): the session's event for that slot (the sdk picks a free index). The
+ // 261..272 commands bind a slot to a display event source and unbind it again. Until a slot
+ // is bound it fires on every vblank, as all of them used to.
+ void AcquireNativeEvent(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[2];
+ if (index >= m_native_events.size()) {
+ LOG_ERROR(Service, "nx2:vi2 31 index {} out of range", index);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultUnknown);
+ return;
+ }
+ LOG_INFO(Service, "nx2:vi2 31 native event {}", index);
+ ReplyNativeEvent(ctx, index);
+ }
+
+ // 32(u32 index, copy handle): the client hands back its handle for an acquired slot and gets
+ // the slot's event again; the handle descriptor and handle come before the index. Games call
+ // it for slot 0 right after binding it to vsync. Title 1's main thread waits on this event
+ // until it has fired twice before it submits any GPU work, and hung there while it never
+ // fired.
+ void DuplicateNativeEvent(HLERequestContext& ctx) {
+ const u32* words = ctx.CommandBuffer();
+ u32 data = 2;
+ if (words[1] >> 31) {
+ const u32 descriptor = words[2];
+ data = 3 + ((descriptor >> 1) & 0xf) + ((descriptor >> 5) & 0xf) + (descriptor & 1 ? 2 : 0);
+ }
+ const u32 index = words[data];
+ if (index >= m_native_events.size()) {
+ LOG_ERROR(Service, "nx2:vi2 32 index {} out of range", index);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultUnknown);
+ return;
}
+ LOG_INFO(Service, "nx2:vi2 32 native event {}", index);
+ ReplyNativeEvent(ctx, index);
+ }
+
+ // 261(u64 display name, u32 index): fire the slot on every vblank.
+ void BindVsync(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[4];
+ LOG_INFO(Service, "nx2:vi2 261 native event {} -> vsync", index);
+ if (index < m_native_events.size() && m_native_events[index]) {
+ SetVsyncSlot(index, true);
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ // 263/265/267/269/271(u64 display name, u32 index): fire the slot when the display's refresh
+ // rate, HDR status, HDR paper white, buffer max resolution or mode capability changes, so the
+ // game re-queries it. None of them change here: fire once so the game reads the current
+ // state, then leave the slot quiet. Kirby Air Riders re-queried its HDR status on every
+ // present while this slot fired with vsync.
+ void BindChange(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[4];
+ LOG_INFO(Service, "nx2:vi2 {} native event {} -> change notification", ctx.GetCommand(), index);
+ if (index < m_native_events.size() && m_native_events[index]) {
+ SetVsyncSlot(index, false);
+ m_native_events[index]->Signal(system.Kernel());
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ // 262..272 even (u64 display name, u32 index): detach the slot from its source.
+ void UnbindNativeEvent(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[4];
+ LOG_INFO(Service, "nx2:vi2 {} native event {} unbound", ctx.GetCommand(), index);
+ if (index < m_native_events.size() && m_native_events[index]) {
+ SetVsyncSlot(index, false);
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ void ReplyNativeEvent(HLERequestContext& ctx, u32 index) {
+ auto*& event = m_native_events[index];
+ if (!event) {
+ event = m_context.CreateEvent(fmt::format("nx2:vi2:Native{}", index));
+ m_events.push_back(event);
+ SetVsyncSlot(index, true);
+ StartVsyncTimer();
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 1, 0};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, event->GetReadableEvent());
+ }
+
+ void SetVsyncSlot(u32 index, bool on) {
+ auto* event = m_native_events[index];
+ std::scoped_lock lk{m_vsync->mutex};
+ auto& list = m_vsync->events;
+ const auto it = std::find(list.begin(), list.end(), event);
+ if (on && it == list.end()) {
+ list.push_back(event);
+ } else if (!on && it != list.end()) {
+ list.erase(it);
+ }
+ }
+
+ void StartVsyncTimer() {
if (!m_vsync_timer) {
m_vsync_timer = Core::Timing::CreateEvent(
"nx2:vi2 vsync",
@@ -1805,14 +2256,12 @@ private:
}()};
system.CoreTiming().ScheduleLoopingEvent(VblankInterval, VblankInterval, m_vsync_timer);
}
- IPC::ResponseBuilder rb{ctx, 2, 1, 0};
- rb.Push(ResultSuccess);
- rb.PushCopyObjects(ctx, event->GetReadableEvent());
}
KernelHelpers::ServiceContext m_context;
std::shared_ptr<Nx2NativeObjects> m_objects;
std::vector<Kernel::KEvent*> m_events;
+ std::array<Kernel::KEvent*, 8> m_native_events{}; ///< 31's slots, also listed in m_events
// Shared with the vsync timer callback, which can outlive the session by one call.
struct VsyncState {
std::mutex mutex;
@@ -1837,7 +2286,9 @@ inline bool Nx2FirstVideoCommand(std::string_view service, u32 command) {
// The Switch 2 video decoder (nn::codec::video::detail::ipc). gsv 902/906/1002 hand out a session
// whose TIPC 1 (CreateVideoDecoderService: handle at word 3, result at word 4) creates a decoder
// service, another session of this class. Layouts read from the SDK's nn::codec VP9 decoder:
-// - 200(copy handle: work buffer transfer memory, u64 size, u32, u32, u32): initialize.
+// - 200(copy handle: work buffer transfer memory, u64 size, u32, u32, u32): initialize. 201 adds a
+// fourth u32 (a flag that 200 leaves at 1) and 202 that and a u64: the same initialization.
+// 210(copy handle, u32 x4) replies only a result.
// - 101(A: codec name, "video/vp9\0"; u32 width, u32 height, u32 rate, u32): configure.
// - 3(): the submission-ready event (copy handle at word 3, result at word 4).
// - 6 / 207 / 8: start / stop / flush; 115: finalize.
@@ -1848,6 +2299,8 @@ inline bool Nx2FirstVideoCommand(std::string_view service, u32 command) {
// rewritten once that handle (the client's reads) has passed. Reply the image's next handle.
// - 212(u32 image id): unregister it.
// - 109(A: one compressed frame, u64 pts in us, u8 end of stream): submit input; reply a u64.
+// 400 is the same with a second A buffer after the frame, a 0x838-byte block of frame
+// information; the decoder here only needs the frame.
// - 213(): the next finished frame (16 words): u32 image id, u32 width, u32 height, u32 width,
// u32 height, u32 colour space, TimeSpan pts (ns), u8 end of stream.
// A NativeSyncHandle is {u32 SyncCounterId (~syncpoint), u32 mode, u64 value}; clients wait on it
@@ -1869,11 +2322,15 @@ public:
{109, &INx2VideoSession::SubmitInput, "SubmitInputBuffer"},
{115, &INx2VideoSession::Succeed, "Finalize"},
{200, &INx2VideoSession::Succeed, "Initialize"},
+ {201, &INx2VideoSession::Succeed, "InitializeWithFlag"},
+ {202, &INx2VideoSession::Succeed, "InitializeWithFlagAndValue"},
{207, &INx2VideoSession::Flush, "Stop"},
+ {210, &INx2VideoSession::Succeed, "Unknown210"},
{212, &INx2VideoSession::UnregisterBuffer, "UnregisterBuffer"},
{213, &INx2VideoSession::GetOutputInfo, "GetOutputInfo"},
{311, &INx2VideoSession::RegisterBuffer, "RegisterBuffer"},
{314, &INx2VideoSession::ReleaseBuffer, "ReleaseBuffer"},
+ {400, &INx2VideoSession::SubmitInput, "SubmitInputBufferWithInfo"},
});
}
@@ -2061,9 +2518,11 @@ private:
void SubmitInput(HLERequestContext& ctx) {
const auto* w = ctx.CommandBuffer();
- // Words 2-4 are the A descriptor of the compressed frame.
- const u64 pts = u64{w[5]} | (u64{w[6]} << 32);
- const bool end = (w[7] & 0xFF) != 0;
+ // Words 2-4 are the A descriptor of the compressed frame (400: 5-7 that of its information
+ // block), the data words follow.
+ const size_t words = 2 + 3 * ctx.BufferDescriptorA().size();
+ const u64 pts = u64{w[words]} | (u64{w[words + 1]} << 32);
+ const bool end = (w[words + 2] & 0xFF) != 0;
Log(ctx, "submit");
Input input{{}, pts, end};
if (!ctx.BufferDescriptorA().empty()) {
@@ -2221,6 +2680,7 @@ private:
const Decoded& frame, std::vector<u8>& bytes) {
u64 address{};
Nx2DecoderImageLayout layout{};
+ bool block_linear{};
{
std::scoped_lock lk{m_objects->mutex};
const auto image = m_objects->images.find(image_id);
@@ -2228,6 +2688,8 @@ private:
return {m_width, m_height};
}
layout = Nx2CalculateDecoderImageLayout(image->second.preference.data());
+ block_linear = image->second.gpu_read &&
+ layout.format != Tegra::Host1x::Nx2PictureFormat::Nv12;
const auto memory = m_objects->memories.find(image->second.memory);
if (memory != m_objects->memories.end() && memory->second.address != 0) {
address = memory->second.address + image->second.offset;
@@ -2241,10 +2703,25 @@ private:
const u32 width = std::min(frame.picture->width, layout.width);
const u32 height = std::min(frame.picture->height, layout.height);
bytes.resize(layout.size);
+ auto& memory = system.ApplicationMemory();
+ if (block_linear) {
+ // Read on the GPU: RGBA (the blit's format 0xd5) in the native block-linear layout,
+ // 16-GOB blocks, as our Eden tree writes these images.
+ decoder->Render(*frame.picture, Tegra::Host1x::Nx2PictureFormat::Rgba8, width, height,
+ layout.pitch, 0, bytes);
+ m_tiled.resize(layout.size);
+ Tegra::Texture::SwizzleTexture(m_tiled, bytes, 4, static_cast<u32>(layout.pitch / 4),
+ layout.height, 1, 4, 0, 1, true);
+ memory.WriteBlock(address, m_tiled.data(), m_tiled.size());
+ if (const auto device =
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address)) {
+ system.GPU().InvalidateRegion(*device, m_tiled.size());
+ }
+ return {width, height};
+ }
decoder->Render(*frame.picture, layout.format, width, height, layout.pitch,
layout.chroma_offset, bytes);
DumpFrame(bytes, layout, width, height);
- auto& memory = system.ApplicationMemory();
memory.WriteBlock(address, bytes.data(), layout.pitch * height);
if (layout.format == Tegra::Host1x::Nx2PictureFormat::Nv12) {
memory.WriteBlock(address + layout.chroma_offset, bytes.data() + layout.chroma_offset,
@@ -2290,6 +2767,7 @@ private:
u64 m_submitted{};
u32 m_syncpoint{NoSyncpoint}; ///< Counts finished frames (allocated at the first register)
u32 m_sync_base{}; ///< Its value before the first frame
+ std::vector<u8> m_tiled; ///< Decode thread: block-linear copy of a frame (WriteImage)
// Shared with the decode thread.
std::mutex m_mutex;
@@ -2333,6 +2811,7 @@ public:
{429, &INx2GraphicsSystemSession::CreateNativeImage, "CreateNativeImage"},
{430, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyNativeImage"},
{426, &INx2GraphicsSystemSession::QueryNativeImage, "MapNativeImage"},
+ {427, &INx2GraphicsSystemSession::UnmapNativeImage, "UnmapNativeImage"},
{428, &INx2GraphicsSystemSession::QueryNativeImage, "QueryNativeImage"},
{435, &INx2GraphicsSystemSession::CalculateNativeImageSize, "CalculateNativeImageSize"},
{1402, &INx2GraphicsSystemSession::OpenNvsrvSession, "OpenNvsrvSession (guessed)"},
@@ -2341,6 +2820,7 @@ public:
"CalculateDecoderImagePoolBuffer"},
{901, &INx2GraphicsSystemSession::Stub, "VideoInitialize (guessed)"},
{902, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession (guessed)"},
+ {905, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSessionWithFlag"},
{906, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSessionWithVersion"},
{1001, &INx2GraphicsSystemSession::Stub, "VideoInitialize2 (guessed)"},
{1002, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession2 (guessed)"},
@@ -2353,7 +2833,92 @@ public:
{1291, &INx2GraphicsSystemSession::CalculateChatImagePoolSize,
"CalculateChatImagePoolSize (guessed)"},
{911, &INx2GraphicsSystemSession::CreateDecoderImage, "CreateDecoderImage"},
+ // 912(u32 image id): the video group's x12, freeing one decoder output image (Switch
+ // Sports frees its four when its movie ends). Unlisting it stops the decode thread
+ // writing frames into memory the game reuses.
+ {912, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyDecoderImage"},
{961, &INx2GraphicsSystemSession::GetCpuImageDescriptor, "GetCpuUserImageDescriptor"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {11, &INx2GraphicsSystemSession::RegisterClientHandle, "RegisterClientHandle"},
+ {113, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown113"},
+ {115, &INx2GraphicsSystemSession::QueryGrantedFeatures, "QueryGrantedFeatures"},
+ {120, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown120"},
+ {121, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown121"},
+ {122, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown122"},
+ {123, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown123"},
+ {124, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown124"},
+ {125, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown125"},
+ {126, &INx2GraphicsSystemSession::GetSessionFlag, "GetSessionFlag"},
+ {131, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown131"},
+ {133, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown133"},
+ {187, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown187"},
+ {201, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission201"},
+ {211, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown211"},
+ {212, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown212"},
+ {213, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown213"},
+ {214, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown214"},
+ {215, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown215"},
+ {301, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission301"},
+ {303, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown303"},
+ {311, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool311"},
+ {312, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool312"},
+ {321, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool321"},
+ {361, &INx2GraphicsSystemSession::GetCpuImageDescriptor, "GetFeatureCpuImageDescriptor361"},
+ {391, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize391"},
+ {401, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission401"},
+ {402, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission402"},
+ {403, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission403"},
+ {424, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown424"},
+ {425, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown425"},
+ {427, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown427"},
+ {434, &INx2GraphicsSystemSession::AttachGpuAddressSpace, "AttachGpuAddressSpace"},
+ {436, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown436"},
+ {461, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown461"},
+ {462, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown462"},
+ {463, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown463"},
+ {464, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown464"},
+ {465, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown465"},
+ {466, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown466"},
+ {467, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown467"},
+ {468, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown468"},
+ {493, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown493"},
+ {494, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission494"},
+ {603, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown603"},
+ {612, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool612"},
+ {613, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown613"},
+ {614, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown614"},
+ {615, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool615"},
+ {691, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize691"},
+ {801, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission801"},
+ {802, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown802"},
+ {803, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown803"},
+ {811, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool811"},
+ {812, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool812"},
+ {813, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown813"},
+ {814, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown814"},
+ {861, &INx2GraphicsSystemSession::GetCpuImageDescriptor, "GetFeatureCpuImageDescriptor861"},
+ {891, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize891"},
+ {1003, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1003"},
+ {1011, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1011"},
+ {1012, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1012"},
+ {1091, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1091"},
+ {1101, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1101"},
+ {1102, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1102"},
+ {1111, &INx2GraphicsSystemSession::SetFeatureVersion, "SetFeatureVersion"},
+ {1112, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1112"},
+ {1113, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1113"},
+ {1121, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1121"},
+ {1203, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1203"},
+ {1212, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool1212"},
+ {1501, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1501"},
+ {1502, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1502"},
+ {1503, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1503"},
+ {1601, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1601"},
+ {1602, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1602"},
+ {1603, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1603"},
+ {1611, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool1611"},
+ {1612, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool1612"},
+ {1691, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize1691"},
});
}
@@ -2369,6 +2934,56 @@ public:
}
private:
+ // Feature permission checks (201, 301, 401-403, 1101, 1102, 1501-1503): each tests one bit of
+ // the permissions the session was opened with and fails when it is clear. Games only ask for
+ // features they are granted.
+ void CheckPermission(HLERequestContext& ctx) {
+ LOG_DEBUG(Service, "nx2:gsv TIPC {} permission check", ctx.GetCommand());
+ TipcReplyWords(ctx, {});
+ }
+
+ // 115(u64 requested feature flags): the subset that is granted, which is all of them here.
+ // A zeroed reply would deny every feature.
+ void QueryGrantedFeatures(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ LOG_INFO(Service, "nx2:gsv TIPC 115 features {:#x}", u64{w[2]} | (u64{w[3]} << 32));
+ TipcReplyWords(ctx, {w[2], w[3]});
+ }
+
+ // 126: one bit of the session's state (meaning unknown); clear.
+ void GetSessionFlag(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {0});
+ }
+
+ // 434(u32 id from nx2:gpu 33): the session takes a reference on that GPU address space.
+ void AttachGpuAddressSpace(HLERequestContext& ctx) {
+ const u32 id = ctx.CommandBuffer()[2];
+ LOG_INFO(Service, "nx2:gsv TIPC 434 address space {:#x}", id);
+ m_address_spaces.insert(id);
+ TipcReplyWords(ctx, {});
+ }
+
+ // 11(copy handle): handed over once per session and kept; a second call fails.
+ void RegisterClientHandle(HLERequestContext& ctx) {
+ if (m_client_handle_registered) {
+ LOG_WARNING(Service, "nx2:gsv TIPC 11 called twice");
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultUnknown);
+ return;
+ }
+ m_client_handle_registered = true;
+ TipcReplyWords(ctx, {});
+ }
+
+ // 1111(u32): a version or mode number the server checks and records.
+ void SetFeatureVersion(HLERequestContext& ctx) {
+ LOG_INFO(Service, "nx2:gsv TIPC 1111 value {:#x}", ctx.CommandBuffer()[2]);
+ TipcReplyWords(ctx, {});
+ }
+
+ std::set<u32> m_address_spaces; ///< Ids attached with 434
+ bool m_client_handle_registered = false; ///< Set by 11
+
// Index of the first raw data word of a TIPC request: header (2 words), then an optional
// special header with pid and copy/move handle words.
static size_t TipcDataOffset(const u32* cmd_buf) {
@@ -2392,7 +3007,9 @@ private:
// and InitializeVp9Decoder failed with 2001-0114 (title 1 aborts when a movie starts).
// Newer SDKs (nn::codec::video::detail::InitializeServerContext) send 906(u8, u64 decoder
// library version) instead of 902; title 4 sends version 0x3'00030000 and aborted with
- // InvalidHandle at its first movie while 906 was auto-stubbed.
+ // InvalidHandle at its first movie while 906 was auto-stubbed. The SDK of later titles
+ // (ipc::Initialize(bool)) sends 905(u8) instead of 902 when its flag is set; the reply is the
+ // same. 1001/1002 open the encoder's creator session (CreateVideoEncoderService uses it).
void OpenVideoSession(HLERequestContext& ctx) {
const auto* w = ctx.CommandBuffer();
const bool second = ctx.GetCommand() == 1002;
@@ -2405,13 +3022,11 @@ private:
// nn::camera::sdk::CreateCameraContext sends 601() then 602(u64 1), which returns the camera
// context session (handle at word 3, result at word 4). Auto-stubbed, 602 had no handle, the
- // SDK sent 603 (destroy) and title 1 (1.8.0) aborted with 2034-0000. Its commands fall
- // through to the nx2 auto stub.
+ // SDK sent 603 (destroy) and title 1 (1.8.0) aborted with 2034-0000. Its commands are
+ // answered by INx2CameraSession.
void OpenCameraSession(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:gsv TIPC 602, returning a nx2:camera session");
- ReturnRawSession(system, ctx,
- std::make_shared<INx2StubInterface>(
- system, INx2StubInterface::InternName("nx2:camera")));
+ ReturnRawSession(system, ctx, std::make_shared<INx2CameraSession>(system));
}
// nn::chat::gsv::Initialize sends 1201 then 1202, the same pair as 901/902 and 1001/1002.
@@ -2597,8 +3212,19 @@ private:
void DestroyNativeMemory(HLERequestContext& ctx) {
const u32 id = ctx.CommandBuffer()[2];
- std::scoped_lock lk{m_objects->mutex};
- m_objects->memories.erase(id);
+ std::vector<std::optional<std::pair<u64, u64>>> unmaps;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ for (auto& [image_id, image] : m_objects->images) {
+ if (image.memory == id && image.mapped_here) {
+ unmaps.push_back(TakeDecoderMapping(image));
+ }
+ }
+ m_objects->memories.erase(id);
+ }
+ for (const auto& range : unmaps) {
+ DropDecoderMapping(range);
+ }
LOG_DEBUG(Service, "nx2:gsv 112 DestroyNativeMemory id {}", id);
TipcReplyWords(ctx, {});
}
@@ -2648,6 +3274,8 @@ private:
TipcReplyWords(ctx, {id});
}
+ // Pool size of a feature's images (1291 here; 391, 691, 891 and 1691 for the other feature
+ // groups, which share the server template).
// nn::chat::gsv::CalculateRequiredBufferMemory: 1291(preference[16], u32 count), the request
// of 1510, replies result, u64 size, u64 alignment. Auto-stubbed, the size became 0x1000 and
// the camera pool (1211) got memory for none of its images.
@@ -2673,6 +3301,9 @@ private:
// the 429 preference words (0x10, 0x14), a guess.
void QueryNativeImage(HLERequestContext& ctx) {
const u32 handle = ctx.CommandBuffer()[2];
+ if (QueryDecoderImage(ctx, handle)) {
+ return;
+ }
std::array<u32, 16> preference{};
u64 image_offset = 0;
u64 memory_address = 0;
@@ -2717,6 +3348,108 @@ private:
}
}
+ // 428/426 for a decoder output image (Fire Emblem: Fortune's Weave's movie player blits each
+ // one, block-linear RGBA, format 0xd5, block height 16 GOBs, into a texture of its own). The
+ // reply follows our Eden tree, where the movies play: words 3-4 the GPU address, 5-6 the last
+ // byte's address, then the descriptor {1, 0, dimensions, .., 0x01000001, 1, offset, size}
+ // (offset 0 once mapped). 426 maps the image at its own address for the GPU: decoder pool
+ // memory is process memory (114) that no GPU mapping covers, and the blit read zeros.
+ bool QueryDecoderImage(HLERequestContext& ctx, u32 handle) {
+ const bool map = ctx.GetCommand() == 426;
+ u64 address{};
+ u64 size{};
+ u64 offset{};
+ u32 dimensions{};
+ bool register_mapping = false;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ const auto image = m_objects->images.find(handle);
+ if (image == m_objects->images.end() || !image->second.decoder) {
+ return false;
+ }
+ const auto memory = m_objects->memories.find(image->second.memory);
+ if (memory == m_objects->memories.end() || memory->second.address == 0) {
+ return false;
+ }
+ image->second.gpu_read = true;
+ offset = image->second.offset;
+ size = image->second.size;
+ dimensions = image->second.preference[2];
+ if (map) {
+ address = memory->second.address + offset;
+ register_mapping = image->second.gpu_maps++ == 0 &&
+ !Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address);
+ image->second.mapped_here |= register_mapping;
+ }
+ }
+ if (register_mapping) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Register(address, size);
+ }
+ LOG_INFO(Service, "nx2:gsv {} decoder image {} at {:#x}+{:#x}{}", ctx.GetCommand(), handle,
+ address, size, register_mapping ? ", mapped for the GPU" : "");
+ std::array<u32, 37> words{};
+ if (map) {
+ const u64 last = address + size - 1;
+ words[0] = static_cast<u32>(address);
+ words[1] = static_cast<u32>(address >> 32);
+ words[2] = static_cast<u32>(last);
+ words[3] = static_cast<u32>(last >> 32);
+ }
+ u32* const desc = words.data() + 4;
+ desc[0] = 1;
+ desc[2] = dimensions;
+ desc[8] = 0x01000001;
+ desc[9] = 1;
+ desc[10] = map ? 0 : static_cast<u32>(offset);
+ desc[11] = static_cast<u32>(size);
+ IPC::ResponseBuilder rb{ctx, static_cast<u32>(2 + words.size())};
+ rb.PushRaw<u32>(ResultSuccess.raw);
+ for (const u32 w : words) {
+ rb.Push<u32>(w);
+ }
+ return true;
+ }
+
+ // 427(u32 handle, u32 address space, u64 address, u64 last address): undoes a 426. The last
+ // unmap of a decoder image drops the GPU mapping 426 made.
+ void UnmapNativeImage(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ const u32 handle = w[2];
+ std::optional<std::pair<u64, u64>> unmap;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ if (const auto image = m_objects->images.find(handle);
+ image != m_objects->images.end() && image->second.gpu_maps != 0 &&
+ --image->second.gpu_maps == 0) {
+ unmap = TakeDecoderMapping(image->second);
+ }
+ }
+ LOG_DEBUG(Service, "nx2:gsv 427 image {} [{:#x}, {:#x}]", handle, Pair(w, 4), Pair(w, 6));
+ DropDecoderMapping(unmap);
+ TipcReplyWords(ctx, {});
+ }
+
+ // The range 426 mapped for an image, if it did; called with m_objects->mutex held.
+ std::optional<std::pair<u64, u64>> TakeDecoderMapping(Nx2NativeObjects::Image& image) {
+ image.gpu_maps = 0;
+ if (!std::exchange(image.mapped_here, false)) {
+ return std::nullopt;
+ }
+ const auto memory = m_objects->memories.find(image.memory);
+ if (memory == m_objects->memories.end() || memory->second.address == 0) {
+ return std::nullopt;
+ }
+ return std::pair{memory->second.address + image.offset, image.size};
+ }
+
+ static void DropDecoderMapping(const std::optional<std::pair<u64, u64>>& range) {
+ if (range) {
+ auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
+ registry.Unmap(range->first, range->second);
+ registry.ReleaseBacking(range->first, range->second);
+ }
+ }
+
// 911(preference[16], u32 count, u32 memory id) (nn::codec::InitializeDecoderImagePool):
// result, the id of the first of `count` decoder output images; the client names image i of
// the pool id + i. They share the pool's memory, one image size apart.
@@ -2775,8 +3508,15 @@ private:
void DestroyNativeImage(HLERequestContext& ctx) {
const u32 id = ctx.CommandBuffer()[2];
- std::scoped_lock lk{m_objects->mutex};
- m_objects->images.erase(id);
+ std::optional<std::pair<u64, u64>> unmap;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ if (const auto image = m_objects->images.find(id); image != m_objects->images.end()) {
+ unmap = TakeDecoderMapping(image->second);
+ m_objects->images.erase(image);
+ }
+ }
+ DropDecoderMapping(unmap);
TipcReplyWords(ctx, {});
}
diff --git a/src/core/hle/service/ro/ro.cpp b/src/core/hle/service/ro/ro.cpp
index 335efa3ad3..2af4f1cf58 100644
--- a/src/core/hle/service/ro/ro.cpp
+++ b/src/core/hle/service/ro/ro.cpp
@@ -356,6 +356,22 @@ public:
// Read NRR.
NrrHeader header{};
process->GetMemory().ReadBlock(nrr_address, std::addressof(header), sizeof(header));
+ size_t hashes_offset = header.GetHashesOffset();
+ u32 num_hashes = header.GetNumHashes();
+ if (process->IsSwitch2()) {
+ // Switch 2 NRRs carry 3072-bit keys and signatures: the fields after them (program
+ // id, size, kind, hash list offset and count) sit 0x180 bytes later, the hash list
+ // offset and count at 0x4c0.
+ std::array<u32, 2> fields{};
+ process->GetMemory().ReadBlock(nrr_address + 0x4c0, fields.data(), sizeof(fields));
+ hashes_offset = fields[0];
+ num_hashes = fields[1];
+ }
+ // The hash list lies inside the NRR (read as a Switch 1 header, a Switch 2 one asked for
+ // 51 GB of hashes).
+ R_UNLESS(hashes_offset <= nrr_size &&
+ num_hashes <= (nrr_size - hashes_offset) / sizeof(Sha256Hash),
+ RO::ResultInvalidNrr);
// Set NRR info.
context->SetNrrInfoInUse(nrr_info, true);
@@ -363,10 +379,9 @@ public:
nrr_info->nrr_heap_size = nrr_size;
// Read NRR hash list.
- nrr_info->hashes.resize(header.GetNumHashes());
- process->GetMemory().ReadBlock(nrr_address + header.GetHashesOffset(),
- nrr_info->hashes.data(),
- sizeof(Sha256Hash) * header.GetNumHashes());
+ nrr_info->hashes.resize(num_hashes);
+ process->GetMemory().ReadBlock(nrr_address + hashes_offset, nrr_info->hashes.data(),
+ sizeof(Sha256Hash) * num_hashes);
R_SUCCEED();
}
diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp
index 1490af0328..b47d67468b 100644
--- a/src/core/hle/service/service.cpp
+++ b/src/core/hle/service/service.cpp
@@ -9,6 +9,10 @@
#include <tuple>
#include <mutex>
#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <string_view>
#include <chrono>
#include <algorithm>
#include <cstdlib>
@@ -18,8 +22,10 @@
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/ipc.h"
+#include "core/hle/kernel/k_event.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/service.h"
#include "core/hle/service/nx2_stub_interface.h"
#include "core/hle/service/sm/sm.h"
@@ -49,6 +55,75 @@ ServiceFrameworkBase::ServiceFrameworkBase(Core::System& system_, const char* se
ServiceFrameworkBase::~ServiceFrameworkBase() {
// Wait for other threads to release access before destroying
const auto guard = ServiceFrameworkBase::LockService();
+ if (stub_event) {
+ stub_context->CloseEvent(stub_event);
+ }
+}
+
+namespace {
+
+// Callers read the reply words they expect without checking how many were written, so an empty
+// reply hands them the request's leftover words as data (an HDR output format, a refresh rate, a
+// device id). Stub replies carry this many zeroed words.
+constexpr u32 StubZeroedWords = 32;
+
+void PushStubZeroes(IPC::ResponseBuilder& rb) {
+ for (u32 i = 0; i < StubZeroedWords; ++i) {
+ rb.Push<u32>(0);
+ }
+}
+
+} // namespace
+
+bool ServiceFrameworkBase::StubAllowed(HLERequestContext& ctx) {
+ static const bool env_auto_stub = std::getenv("EDEN_NX2_AUTO_STUB") != nullptr;
+ const auto* const caller = ctx.GetThread().GetOwnerProcess();
+ if (Settings::values.use_auto_stub || env_auto_stub ||
+ std::string_view{service_name}.starts_with(Nx2StubPrefix) ||
+ (caller != nullptr && caller->IsSwitch2())) {
+ // Logged once per command: these are still unimplemented.
+ static std::mutex mutex;
+ static std::set<std::pair<std::string_view, u32>> logged;
+ std::scoped_lock lk{mutex};
+ if (logged.emplace(service_name, ctx.GetCommand()).second) {
+ LOG_WARNING(Service, "(STUBBED) {}::{} (placeholder reply)", service_name, ctx.GetCommand());
+ }
+ return true;
+ }
+ const auto& table = ctx.IsTipc() ? handlers_tipc : handlers;
+ const auto it = table.find(ctx.GetCommand());
+ ReportUnimplementedFunction(ctx, it == table.end() ? nullptr : &it->second);
+ return false;
+}
+
+void ServiceFrameworkBase::StubReplyZeroed(HLERequestContext& ctx) {
+ if (!StubAllowed(ctx)) {
+ return;
+ }
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords};
+ rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
+}
+
+void ServiceFrameworkBase::StubReplyEvent(HLERequestContext& ctx) {
+ if (!StubAllowed(ctx)) {
+ return;
+ }
+ if (!stub_event) {
+ stub_context = std::make_unique<KernelHelpers::ServiceContext>(system, service_name);
+ stub_event = stub_context->CreateEvent(fmt::format("{}:StubEvent", service_name));
+ }
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords, 1, 0};
+ rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
+ rb.PushCopyObjects(ctx, stub_event->GetReadableEvent());
+}
+
+void ServiceFrameworkBase::StubReplyInterface(HLERequestContext& ctx) {
+ if (!StubAllowed(ctx)) {
+ return;
+ }
+ ReturnNx2StubInterface(system, ctx, service_name);
}
void ServiceFrameworkBase::RegisterHandlersBase(const FunctionInfoBase* functions, std::size_t n) {
@@ -67,6 +142,27 @@ void ServiceFrameworkBase::RegisterHandlersBaseTipc(const FunctionInfoBase* func
void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx,
const FunctionInfoBase* info) {
+ // EDEN_NX2_AUTO_STUB=1 auto-stubs every unknown command without touching the config. Switch 2
+ // programs are always auto-stubbed: they still call many commands nobody has implemented.
+ static const bool env_auto_stub = std::getenv("EDEN_NX2_AUTO_STUB") != nullptr;
+ const auto* const caller = ctx.GetThread().GetOwnerProcess();
+ const bool auto_stub = Settings::values.use_auto_stub || env_auto_stub ||
+ std::string_view{service_name}.starts_with(Nx2StubPrefix) ||
+ (caller != nullptr && caller->IsSwitch2());
+ // Auto-stubbed commands are reported once each: title 1 polls nx2:camera 29 every frame
+ // (~47 calls a second), and every report wrote a Critical line, which flushes the log file.
+ if (auto_stub) {
+ static std::mutex reported_mutex;
+ static std::set<std::pair<std::string, u32>> reported;
+ std::scoped_lock lk{reported_mutex};
+ if (!reported.emplace(service_name, ctx.GetCommand()).second) {
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords};
+ rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
+ return;
+ }
+ }
+
auto cmd_buf = ctx.CommandBuffer();
std::string function_name = info == nullptr ? "<unknown>" : info->name;
@@ -80,16 +176,11 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx,
system.GetReporter().SaveUnimplementedFunctionReport(ctx, ctx.GetCommand(), function_name, service_name);
UNIMPLEMENTED_MSG("Unknown / unimplemented {}", fmt::to_string(buf));
- // EDEN_NX2_AUTO_STUB=1 auto-stubs every unknown command without touching the config. Switch 2
- // programs are always auto-stubbed: they still call many commands nobody has implemented.
- static const bool env_auto_stub = std::getenv("EDEN_NX2_AUTO_STUB") != nullptr;
- const auto* const caller = ctx.GetThread().GetOwnerProcess();
- if (Settings::values.use_auto_stub || env_auto_stub ||
- std::string_view{service_name}.starts_with(Nx2StubPrefix) ||
- (caller != nullptr && caller->IsSwitch2())) {
- LOG_WARNING(Service, "Using auto stub fallback!");
- IPC::ResponseBuilder rb{ctx, 2};
+ if (auto_stub) {
+ LOG_WARNING(Service, "Using auto stub fallback (repeats of this command are not logged)");
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords};
rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
}
}
diff --git a/src/core/hle/service/service.h b/src/core/hle/service/service.h
index 68b0d636d8..d1be62c8c7 100644
--- a/src/core/hle/service/service.h
+++ b/src/core/hle/service/service.h
@@ -7,6 +7,7 @@
#pragma once
#include <cstddef>
+#include <memory>
#include <mutex>
#include "common/container/unordered_map.h"
#include "common/common_types.h"
@@ -20,12 +21,17 @@ class System;
}
namespace Kernel {
+class KEvent;
class KServerSession;
class ServiceThread;
} // namespace Kernel
namespace Service {
+namespace KernelHelpers {
+class ServiceContext;
+}
+
namespace FileSystem {
class FileSystemController;
}
@@ -77,6 +83,24 @@ protected:
[[nodiscard]] virtual std::unique_lock<std::mutex> LockService() noexcept {
return std::unique_lock{lock_service};
}
+
+ /// Placeholder handlers for commands whose reply shape is known but whose meaning is not, for
+ /// Switch 2 callers (others get the unimplemented-function path, as with nullptr):
+ /// StubReplyZeroed replies with zeroed data words, StubReplyEvent adds a copy handle to an
+ /// event that never fires, and StubReplyInterface returns an nx2: stub interface whose
+ /// commands are auto-stubbed.
+ void StubReplyZeroed(HLERequestContext& ctx);
+ void StubReplyEvent(HLERequestContext& ctx);
+ void StubReplyInterface(HLERequestContext& ctx);
+
+private:
+ /// False (after reporting the command as unimplemented) for callers the stubs don't serve.
+ bool StubAllowed(HLERequestContext& ctx);
+
+ /// Event handed out by StubReplyEvent, created on first use.
+ Kernel::KEvent* stub_event{};
+ std::unique_ptr<KernelHelpers::ServiceContext> stub_context;
+
private:
template <typename T>
friend class ServiceFramework;
diff --git a/src/core/hle/service/set/settings_server.cpp b/src/core/hle/service/set/settings_server.cpp
index 942dddc9bc..78847ba082 100644
--- a/src/core/hle/service/set/settings_server.cpp
+++ b/src/core/hle/service/set/settings_server.cpp
@@ -102,9 +102,14 @@ ISettingsServer::ISettingsServer(Core::System& system_) : ServiceFramework{syste
{7, C<&ISettingsServer::GetKeyCodeMap>, "GetKeyCodeMap"},
{8, C<&ISettingsServer::GetQuestFlag>, "GetQuestFlag"},
{9, C<&ISettingsServer::GetKeyCodeMap2>, "GetKeyCodeMap2"},
- {10, nullptr, "GetFirmwareVersionForDebug"},
+ {10, &ISettingsServer::StubReplyZeroed, "GetFirmwareVersionForDebug"},
{11, C<&ISettingsServer::GetDeviceNickName>, "GetDeviceNickName"},
{12, C<&ISettingsServer::GetKeyCodeMapByPort>, "GetKeyCodeMapByPort"},
+ {13, &ISettingsServer::StubReplyZeroed, "Unknown13"},
+ {14, &ISettingsServer::StubReplyZeroed, "Unknown14"},
+ {15, &ISettingsServer::StubReplyZeroed, "Unknown15"},
+ {16, &ISettingsServer::StubReplyZeroed, "Unknown16"},
+ {17, &ISettingsServer::StubReplyZeroed, "Unknown17"},
};
// clang-format on
diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp
index 0137a3be66..02c6bb7762 100644
--- a/src/core/hle/service/sockets/bsd.cpp
+++ b/src/core/hle/service/sockets/bsd.cpp
@@ -1089,10 +1089,10 @@ BSD_USA::BSD_USA(Core::System& system_, const char* name, bool is_user_)
{1, &BSD_USA::StartMonitoring, "StartMonitoring"},
{2, &BSD_USA::Socket, "Socket"},
{3, &BSD_USA::SocketExempt, "SocketExempt"},
- {4, nullptr, "Open"},
+ {4, &BSD_USA::StubReplyZeroed, "Open"},
{5, &BSD_USA::Select, "Select"},
{6, &BSD_USA::Poll, "Poll"},
- {7, nullptr, "Sysctl"},
+ {7, &BSD_USA::StubReplyZeroed, "Sysctl"},
{8, &BSD_USA::Recv, "Recv"},
{9, &BSD_USA::RecvFrom, "RecvFrom"},
{10, &BSD_USA::Send, "Send"},
@@ -1104,33 +1104,37 @@ BSD_USA::BSD_USA(Core::System& system_, const char* name, bool is_user_)
{16, &BSD_USA::GetSockName, "GetSockName"},
{17, &BSD_USA::GetSockOpt, "GetSockOpt"},
{18, &BSD_USA::Listen, "Listen"},
- {19, nullptr, "Ioctl"},
+ {19, &BSD_USA::StubReplyZeroed, "Ioctl"},
{20, &BSD_USA::Fcntl, "Fcntl"},
{21, &BSD_USA::SetSockOpt, "SetSockOpt"},
{22, &BSD_USA::Shutdown, "Shutdown"},
- {23, nullptr, "ShutdownAllSockets"},
+ {23, &BSD_USA::StubReplyZeroed, "ShutdownAllSockets"},
{24, &BSD_USA::Write, "Write"},
{25, &BSD_USA::Read, "Read"},
{26, &BSD_USA::Close, "Close"},
{27, &BSD_USA::DuplicateSocket, "DuplicateSocket"},
- {28, nullptr, "GetResourceStatistics"},
- {29, nullptr, "RecvMMsg"}, //3.0.0+
- {30, nullptr, "SendMMsg"}, //3.0.0+
+ {28, &BSD_USA::StubReplyZeroed, "GetResourceStatistics"},
+ {29, &BSD_USA::StubReplyZeroed, "RecvMMsg"}, //3.0.0+
+ {30, &BSD_USA::StubReplyZeroed, "SendMMsg"}, //3.0.0+
{31, &BSD_USA::EventFd, "EventFd"}, //7.0.0+
- {32, nullptr, "RegisterResourceStatisticsName"}, //7.0.0+
- {33, nullptr, "RegisterClientShared"}, //10.0.0+
- {34, nullptr, "GetSocketStatistics"}, //15.0.0+
- {35, nullptr, "NifIoctl"}, //17.0.0+
- {36, nullptr, "Unknown36"}, //18.0.0+
- {37, nullptr, "Unknown37"}, //18.0.0+
- {38, nullptr, "Unknown38"}, //18.0.0+
- {39, nullptr, "Unknown39"}, //20.0.0+
- {40, nullptr, "Unknown40"}, //20.0.0+
- {41, nullptr, "Unknown41"}, //21.0.0+
- {42, nullptr, "Unknown42"}, //21.0.0+
- {43, nullptr, "Unknown43"}, //21.0.0+
- {200, nullptr, "SetThreadCoreMask"}, //15.0.0+
- {201, nullptr, "GetThreadCoreMask"}, //15.0.0+
+ {32, &BSD_USA::StubReplyZeroed, "RegisterResourceStatisticsName"}, //7.0.0+
+ {33, &BSD_USA::StubReplyZeroed, "RegisterClientShared"}, //10.0.0+
+ {34, &BSD_USA::StubReplyZeroed, "GetSocketStatistics"}, //15.0.0+
+ {35, &BSD_USA::StubReplyZeroed, "NifIoctl"}, //17.0.0+
+ // Switch 2 titles register with 36 (16 bytes and the pid; a bsd errno back) instead of
+ // RegisterClient. The auto stub left the errno to the message buffer's leftovers, and
+ // Minecraft's socket layer failed later in EventFd.
+ {36, &BSD_USA::RegisterClient, "RegisterClientSwitch2"}, //18.0.0+
+ {37, &BSD_USA::StubReplyZeroed, "Unknown37"}, //18.0.0+
+ {38, &BSD_USA::StubReplyZeroed, "Unknown38"}, //18.0.0+
+ {39, &BSD_USA::StubReplyZeroed, "Unknown39"}, //20.0.0+
+ {40, &BSD_USA::StubReplyZeroed, "Unknown40"}, //20.0.0+
+ {41, &BSD_USA::StubReplyEvent, "Unknown41"}, //21.0.0+
+ {42, &BSD_USA::StubReplyZeroed, "Unknown42"}, //21.0.0+
+ {43, &BSD_USA::StubReplyZeroed, "Unknown43"}, //21.0.0+
+ {200, &BSD_USA::StubReplyZeroed, "SetThreadCoreMask"}, //15.0.0+
+ {201, &BSD_USA::StubReplyZeroed, "GetThreadCoreMask"}, //15.0.0+
+ {44, &BSD_USA::StubReplyZeroed, "Unknown44"},
};
// clang-format on
@@ -1158,22 +1162,28 @@ BSDCFG::BSDCFG(Core::System& system_, const char *name)
: ServiceFramework{system_, name} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "SetIfUp"},
- {1, nullptr, "SetIfUpWithEvent"},
- {2, nullptr, "CancelIf"},
- {3, nullptr, "SetIfDown"},
- {4, nullptr, "GetIfState"},
- {5, nullptr, "DhcpRenew"},
- {6, nullptr, "AddStaticArpEntry"},
- {7, nullptr, "RemoveArpEntry"},
- {8, nullptr, "LookupArpEntry"},
- {9, nullptr, "LookupArpEntry2"},
- {10, nullptr, "ClearArpEntries"},
- {11, nullptr, "ClearArpEntries2"},
- {12, nullptr, "PrintArpEntries"},
- {13, nullptr, "Unknown13"},
- {14, nullptr, "Unknown14"},
- {15, nullptr, "Unknown15"},
+ {0, &BSDCFG::StubReplyZeroed, "SetIfUp"},
+ {1, &BSDCFG::StubReplyZeroed, "SetIfUpWithEvent"},
+ {2, &BSDCFG::StubReplyZeroed, "CancelIf"},
+ {3, &BSDCFG::StubReplyZeroed, "SetIfDown"},
+ {4, &BSDCFG::StubReplyZeroed, "GetIfState"},
+ {5, &BSDCFG::StubReplyZeroed, "DhcpRenew"},
+ {6, &BSDCFG::StubReplyZeroed, "AddStaticArpEntry"},
+ {7, &BSDCFG::StubReplyZeroed, "RemoveArpEntry"},
+ {8, &BSDCFG::StubReplyZeroed, "LookupArpEntry"},
+ {9, &BSDCFG::StubReplyZeroed, "LookupArpEntry2"},
+ {10, &BSDCFG::StubReplyZeroed, "ClearArpEntries"},
+ {11, &BSDCFG::StubReplyZeroed, "ClearArpEntries2"},
+ {12, &BSDCFG::StubReplyZeroed, "PrintArpEntries"},
+ {13, &BSDCFG::StubReplyZeroed, "Unknown13"},
+ {14, &BSDCFG::StubReplyZeroed, "Unknown14"},
+ {15, &BSDCFG::StubReplyZeroed, "Unknown15"},
+ {16, &BSDCFG::StubReplyZeroed, "Unknown16"},
+ {100, &BSDCFG::StubReplyZeroed, "Unknown100"},
+ {101, &BSDCFG::StubReplyZeroed, "Unknown101"},
+ {102, &BSDCFG::StubReplyZeroed, "Unknown102"},
+ {103, &BSDCFG::StubReplyZeroed, "Unknown103"},
+ {104, &BSDCFG::StubReplyZeroed, "Unknown104"},
};
// clang-format on
diff --git a/src/core/hle/service/sockets/nsd.cpp b/src/core/hle/service/sockets/nsd.cpp
index 241f2971ca..eb867cf08a 100644
--- a/src/core/hle/service/sockets/nsd.cpp
+++ b/src/core/hle/service/sockets/nsd.cpp
@@ -86,7 +86,7 @@ void NSD::SetChangeEnvironmentIdentifierDisabled(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called, disabled={}", disabled);
- IPC::ResponseBuilder rb{ctx, 1};
+ IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
@@ -114,7 +114,7 @@ void NSD::ResolveEx(HLERequestContext& ctx) {
}
ctx.WriteBuffer(fqdn_out);
- IPC::ResponseBuilder rb{ctx, 4};
+ IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(ResultSuccess);
}
diff --git a/src/core/hle/service/ssl/ssl.cpp b/src/core/hle/service/ssl/ssl.cpp
index 999efd4295..15aa478b75 100644
--- a/src/core/hle/service/ssl/ssl.cpp
+++ b/src/core/hle/service/ssl/ssl.cpp
@@ -87,7 +87,7 @@ public:
{3, D<&ISslConnection::SetIoMode>, "SetIoMode"},
{4, D<&ISslConnection::GetSocketDescriptor>, "GetSocketDescriptor"},
{5, D<&ISslConnection::GetHostName>, "GetHostName"},
- {6, nullptr, "GetVerifyOption"},
+ {6, &ISslConnection::StubReplyZeroed, "GetVerifyOption"},
{7, D<&ISslConnection::GetIoMode>, "GetIoMode"},
{8, D<&ISslConnection::DoHandshake>, "DoHandshake"},
{9, &ISslConnection::DoHandshakeGetServerCert, "DoHandshakeGetServerCert"},
@@ -105,18 +105,20 @@ public:
{21, D<&ISslConnection::GetRenegotiationMode>, "GetRenegotiationMode"},
{22, D<&ISslConnection::SetOption>, "SetOption"},
{23, D<&ISslConnection::GetOption>, "GetOption"},
- {24, nullptr, "GetVerifyCertErrors"},
- {25, nullptr, "GetCipherInfo"},
+ {24, &ISslConnection::StubReplyZeroed, "GetVerifyCertErrors"},
+ {25, &ISslConnection::StubReplyZeroed, "GetCipherInfo"},
{26, D<&ISslConnection::SetNextAlpnProto>, "SetNextAlpnProto"},
{27, D<&ISslConnection::GetNextAlpnProto>, "GetNextAlpnProto"},
- {28, nullptr, "SetDtlsSocketDescriptor"},
- {29, nullptr, "GetDtlsHandshakeTimeout"},
- {30, nullptr, "SetPrivateOption"},
- {31, nullptr, "SetSrtpCiphers"},
- {32, nullptr, "GetSrtpCipher"},
- {33, nullptr, "ExportKeyingMaterial"},
- {34, nullptr, "SetIoTimeout"},
- {35, nullptr, "GetIoTimeout"},
+ {28, &ISslConnection::StubReplyZeroed, "SetDtlsSocketDescriptor"},
+ {29, &ISslConnection::StubReplyZeroed, "GetDtlsHandshakeTimeout"},
+ {30, &ISslConnection::StubReplyZeroed, "SetPrivateOption"},
+ {31, &ISslConnection::StubReplyZeroed, "SetSrtpCiphers"},
+ {32, &ISslConnection::StubReplyZeroed, "GetSrtpCipher"},
+ {33, &ISslConnection::StubReplyZeroed, "ExportKeyingMaterial"},
+ {34, &ISslConnection::StubReplyZeroed, "SetIoTimeout"},
+ {35, &ISslConnection::StubReplyZeroed, "GetIoTimeout"},
+ {36, &ISslConnection::StubReplyZeroed, "Unknown36"},
+ {37, &ISslConnection::StubReplyZeroed, "Unknown37"},
};
// clang-format on
@@ -420,10 +422,14 @@ private:
R_SUCCEED();
}
- Result GetNextAlpnProto(OutBuffer<BufferAttr_HipcMapAlias> data, Out<u32> to_write) {
- *to_write = u32((std::min)(next_alpn_proto.size(), data.size()));
- next_alpn_proto.assign(data.begin(), data.begin() + *to_write);
- LOG_DEBUG(Service_SSL, "GetNextAlpnProto called, size={}", *to_write);
+ // Replies with two words, the ALPN state and the size written to the buffer. It used to reply
+ // with one and copy the caller's (output) buffer over the protocols SetNextAlpnProto stored.
+ // No TLS negotiation happens here, so report that nothing was negotiated.
+ Result GetNextAlpnProto(OutBuffer<BufferAttr_HipcMapAlias> data, Out<u32> out_state,
+ Out<u32> out_size) {
+ LOG_DEBUG(Service_SSL, "GetNextAlpnProto called, buffer size={}", data.size());
+ *out_state = 0;
+ *out_size = 0;
R_SUCCEED();
}
@@ -557,8 +563,10 @@ private:
rb.Push(client_id);
}
- Result RegisterInternalPki() {
+ // Replies with a u64 (the registered certificate's id); the reply used to carry no data.
+ Result RegisterInternalPki(Out<u64> out_id) {
LOG_WARNING(Service_SSL, "(STUBBED) called");
+ *out_id = 0;
R_SUCCEED();
}
};
@@ -636,7 +644,7 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
explicit ISslServiceForSystem(Core::System& system_) : ServiceFramework{system_, "ssl:s"} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, D<&ISslServiceForSystem::CreateContext>, "CreateContext"},
+ {0, &ISslServiceForSystem::CreateContext, "CreateContext"},
{1, D<&ISslServiceForSystem::GetContextCount>, "GetContextCount"},
{2, D<&ISslServiceForSystem::GetCertificates>, "GetCertificates"},
{3, D<&ISslServiceForSystem::GetCertificateBufSize>, "GetCertificateBufSize"},
@@ -646,7 +654,7 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
{7, D<&ISslServiceForSystem::SetDebugOption>, "SetDebugOption"},
{8, D<&ISslServiceForSystem::GetDebugOption>, "GetDebugOption"},
{9, D<&ISslServiceForSystem::ClearTls12FallbackFlag>, "ClearTls12FallbackFlag"},
- {100, D<&ISslServiceForSystem::CreateContextForSystem>, "CreateContextForSystem"},
+ {100, &ISslServiceForSystem::CreateContext, "CreateContextForSystem"},
{101, D<&ISslServiceForSystem::SetThreadCoreMask>, "SetThreadCoreMask"},
{102, D<&ISslServiceForSystem::GetThreadCoreMask>, "GetThreadCoreMask"},
{103, D<&ISslServiceForSystem::VerifySignature>, "VerifySignature"}
@@ -656,19 +664,30 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
RegisterHandlers(functions);
};
- Result CreateContext() {
- LOG_DEBUG(Service_SSL, "(STUBBED) called.");
+ // 0 and 100(SslVersion, padding, u64 pid placeholder; pid): an ISslContext, like ssl's
+ // CreateContext. The empty reply returned no object, so the caller got handle 0
+ // (Minecraft calls 100).
+ void CreateContext(HLERequestContext& ctx) {
+ struct Parameters {
+ SslVersion ssl_version;
+ INSERT_PADDING_BYTES(0x4);
+ u64 pid_placeholder;
+ };
+ static_assert(sizeof(Parameters) == 0x10, "Parameters is an invalid size");
- // TODO (jarrodnorwell)
+ IPC::RequestParser rp{ctx};
+ const auto parameters = rp.PopRaw<Parameters>();
+ LOG_WARNING(Service_SSL, "(STUBBED) called, command={}, api_version={}", ctx.GetCommand(),
+ parameters.ssl_version.api_version);
- return ResultSuccess;
- };
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<ISslContext>(ctx, system, parameters.ssl_version);
+ }
- Result GetContextCount() {
+ Result GetContextCount(Out<u32> out_count) {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
+ *out_count = 0;
return ResultSuccess;
};
@@ -704,11 +723,9 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
return ResultSuccess;
};
- Result FlushSessionCache() {
+ Result FlushSessionCache(Out<u32> out_count) {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
+ *out_count = 0;
return ResultSuccess;
};
@@ -736,14 +753,6 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
return ResultSuccess;
};
- Result CreateContextForSystem() {
- LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
- return ResultSuccess;
- };
-
Result SetThreadCoreMask() {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
@@ -752,11 +761,9 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
return ResultSuccess;
};
- Result GetThreadCoreMask() {
+ Result GetThreadCoreMask(Out<u64> out_mask) {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
+ *out_mask = 0;
return ResultSuccess;
};
diff --git a/src/core/launch_params.cpp b/src/core/launch_params.cpp
index 25fdd45fdb..6c436d0346 100644
--- a/src/core/launch_params.cpp
+++ b/src/core/launch_params.cpp
@@ -39,6 +39,7 @@ LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wch
{"null-render", no_argument, 0, 'n'},
{"singlecore", no_argument, 0, 's'},
{"filter", no_argument, 0, 'x'},
+ {"pack-nup", required_argument, 0, 'N'},
{0, 0, 0, 0},
// clang-format on
};
@@ -62,6 +63,9 @@ LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wch
case 'g':
p.filepath = std::string(optarg);
break;
+ case 'N':
+ p.pack_nup_source = std::string(optarg);
+ break;
case 'i': {
p.input_profile = std::string(optarg);
break;
@@ -196,7 +200,9 @@ void ApplyLaunchParams(LaunchParams const& lp) noexcept {
"-p, --program Pass following string as arguments to executable\n"
"-u, --user Select a specific user profile from 0 to 7\n"
"-d, --debug Run the GDB stub on a port from 1 to 65535\n"
- "-v, --version Output version information and exit\n",
+ "-v, --version Output version information and exit\n"
+ "--pack-nup <dump> [<out.nup>]"
+ " Pack a loose dump with all its updates and DLC into a NUP file and exit\n",
lp.argv0
);
}
diff --git a/src/core/launch_params.h b/src/core/launch_params.h
index 4da30fc1c2..8d503770d8 100644
--- a/src/core/launch_params.h
+++ b/src/core/launch_params.h
@@ -23,6 +23,8 @@ struct LaunchParams {
std::optional<int> selected_user{};
std::optional<u16> override_gdb_port{};
std::optional<std::string> log_filter{};
+ // --pack-nup <dump> [<out.nup>]: packs a loose dump instead of booting a game (eden-cli).
+ std::optional<std::string> pack_nup_source{};
u16 port = Network::DefaultRoomPort;
bool use_multiplayer = false;
bool fullscreen = false;
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 94dd3395ba..463a8c8930 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -8,6 +8,8 @@
#include <cctype>
#include <chrono>
#include <cstring>
+#include <map>
+#include <memory>
#include <mutex>
#include <string_view>
#include "common/logging.h"
@@ -92,31 +94,24 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) {
}
if (const auto parent = exefs_dir->GetParentDirectory()) {
if (const auto romfs_dir = parent->GetSubdirectory("romfs")) {
- // Keep no ownership here: launch preparation hands its image to the real loader.
+ // Building it walks every file and keeps an object for each (title 1: 206k files, ~150
+ // MiB), so loaders of the same directory share one build while any of them holds it.
static std::mutex cache_mutex;
- static std::string cached_path;
- static std::weak_ptr<FileSys::VfsFile> cached_romfs;
- const auto path = romfs_dir->GetFullPath();
- {
- std::scoped_lock lock{cache_mutex};
- if (cached_path == path) {
- if (auto cached = cached_romfs.lock()) {
- return cached;
- }
- }
+ static std::map<std::string, std::weak_ptr<FileSys::VfsFile>> cache;
+ std::scoped_lock lk{cache_mutex};
+ auto& cached = cache[romfs_dir->GetFullPath()];
+ if (auto built = cached.lock()) {
+ return built;
}
+ LOG_INFO(Loader, "Using RomFS directory next to the ExeFS");
const auto started = std::chrono::steady_clock::now();
- LOG_INFO(Loader, "Building loose RomFS directory image");
- auto image = FileSys::CreateRomFS(romfs_dir);
+ auto built = FileSys::CreateRomFS(romfs_dir);
LOG_INFO(Loader, "Loose RomFS image ready: elapsed_ms={}",
std::chrono::duration_cast<std::chrono::milliseconds>(
- std::chrono::steady_clock::now() - started).count());
- {
- std::scoped_lock lock{cache_mutex};
- cached_path = path;
- cached_romfs = image;
- }
- return image;
+ std::chrono::steady_clock::now() - started)
+ .count());
+ cached = built;
+ return built;
}
}
return nullptr;
@@ -258,6 +253,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys
name = control->GetApplicationName();
}
+ romfs_exefs_dir = file_dir;
if (const auto title_dir = file_dir->GetParentDirectory()) {
romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs"));
}
@@ -269,13 +265,17 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(
, dir(std::move(directory))
, override_update(override_update_)
{
+ romfs_exefs_dir = dir;
}
-void AppLoader_DeconstructedRomDirectory::EnsureRomFS() {
- if (!romfs_initialized) {
- romfs = FindLooseRomFS(dir ? dir : file->GetContainingDirectory());
- romfs_initialized = true;
+// Loaders made only for metadata (the game list, title lookups) never need the RomFS, and several
+// of them were alive with a whole copy each.
+const FileSys::VirtualFile& AppLoader_DeconstructedRomDirectory::LooseRomFS() {
+ if (!romfs_resolved) {
+ romfs_resolved = true;
+ romfs = FindLooseRomFS(romfs_exefs_dir);
}
+ return romfs;
}
FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& dir_file) {
@@ -336,7 +336,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
// Loose-ExeFS loads bypass the NCA/NSP loaders, which are the ones that normally apply
// LayeredFS to the RomFS; apply it here so mods in user/load/<titleid>/ work too.
- if (romfs != nullptr) {
+ if (LooseRomFS() != nullptr) {
const FileSys::PatchManager patch_manager(metadata.GetTitleID(),
system.GetFileSystemController(),
system.GetContentProvider());
@@ -446,8 +446,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
}
ResultStatus AppLoader_DeconstructedRomDirectory::ReadRomFS(FileSys::VirtualFile& out_dir) {
- EnsureRomFS();
- if (romfs == nullptr) {
+ if (LooseRomFS() == nullptr) {
return ResultStatus::ErrorNoRomFS;
}
diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h
index 477638defe..db0120f4ad 100644
--- a/src/core/loader/deconstructed_rom_directory.h
+++ b/src/core/loader/deconstructed_rom_directory.h
@@ -58,11 +58,13 @@ public:
ResultStatus ReadNSOModules(Modules& out_modules) override;
private:
- void EnsureRomFS();
+ /// The loose RomFS next to the ExeFS, found (and built) on first use.
+ const FileSys::VirtualFile& LooseRomFS();
FileSys::ProgramMetadata metadata;
FileSys::VirtualFile romfs;
- bool romfs_initialized{};
+ FileSys::VirtualDir romfs_exefs_dir; ///< Where LooseRomFS looks
+ bool romfs_resolved{};
FileSys::VirtualDir dir;
std::vector<u8> icon_data;
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index 33a93573cf..839bb4d80b 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -29,6 +29,7 @@
#include "core/loader/nro.h"
#include "core/loader/nso.h"
#include "core/loader/nsp.h"
+#include "core/loader/nup.h"
#include "core/loader/xci.h"
namespace Loader {
@@ -130,6 +131,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
return *nsp_type;
} else if (const auto xci_type = IdentifyFileLoader<AppLoader_XCI>(file)) {
return *xci_type;
+ } else if (const auto nup_type = IdentifyFileLoader<AppLoader_NUP>(file)) {
+ return *nup_type;
} else if (const auto nro_type = IdentifyFileLoader<AppLoader_NRO>(file)) {
return *nro_type;
} else if (const auto nca_type = IdentifyFileLoader<AppLoader_NCA>(file)) {
@@ -150,7 +153,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
bool IsContainerType(FileType type) {
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
- type == FileType::XCZ;
+ type == FileType::XCZ || type == FileType::NUP;
}
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
@@ -163,6 +166,24 @@ bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 progr
type = IdentifyFile(file);
}
+ if (type == FileType::NUP) {
+ FileSys::NupContainer nup{file};
+ if (nup.GetStatus() != ResultStatus::Success) {
+ return false;
+ }
+ // A base package boots normally; a standalone update (no base) carries
+ // a complete patched tree and boots on its own (SPEC section 4.1.2).
+ if (const auto base = nup.FindBasePackage();
+ base.has_value() && nup.GetExeFSDir(*base) != nullptr) {
+ return true;
+ }
+ if (const auto update = nup.FindBestUpdatePackage();
+ update.has_value() && nup.GetExeFSDir(*update) != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
if (!IsContainerType(type)) {
return false;
}
@@ -190,6 +211,8 @@ FileType GuessFromFilename(const std::string& name) {
return FileType::XCZ;
else if (extension == "nsp")
return FileType::NSP;
+ else if (extension == "nup")
+ return FileType::NUP;
else if (extension == "nsz")
return FileType::NSZ;
else if (extension == "kip")
@@ -215,6 +238,8 @@ std::string GetFileTypeString(FileType type) {
return "NSP";
case FileType::NSZ:
return "NSZ";
+ case FileType::NUP:
+ return "NUP";
case FileType::KIP:
return "KIP";
case FileType::DeconstructedRomDirectory:
@@ -227,7 +252,7 @@ std::string GetFileTypeString(FileType type) {
return "unknown";
}
-constexpr std::array<const char*, 68> RESULT_MESSAGES{
+constexpr std::array<const char*, 70> RESULT_MESSAGES{
"The operation completed successfully.",
"The loader requested to load is already loaded.",
"The operation is not implemented.",
@@ -296,6 +321,8 @@ constexpr std::array<const char*, 68> RESULT_MESSAGES{
"The INI file contains more than the maximum allowable number of KIP files.",
"Integrity verification could not be performed for this file.",
"Integrity verification failed.",
+ "The NUP container is malformed or its TOC hash does not match.",
+ "The NUP container does not contain a loadable base package.",
};
std::string GetResultStatusString(ResultStatus status) {
@@ -380,6 +407,10 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
case FileType::KIP:
return std::make_unique<AppLoader_KIP>(std::move(file));
+ // NUP (NX Package) container format.
+ case FileType::NUP:
+ return std::make_unique<AppLoader_NUP>(std::move(file));
+
// NX deconstructed ROM directory.
case FileType::DeconstructedRomDirectory:
return std::make_unique<AppLoader_DeconstructedRomDirectory>(std::move(file));
diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h
index 431aeace73..23be3597f5 100644
--- a/src/core/loader/loader.h
+++ b/src/core/loader/loader.h
@@ -47,6 +47,7 @@ enum class FileType {
XCZ,
NAX,
KIP,
+ NUP,
DeconstructedRomDirectory,
};
@@ -157,6 +158,8 @@ enum class ResultStatus : u16 {
ErrorINITooManyKIPs,
ErrorIntegrityVerificationNotImplemented,
ErrorIntegrityVerificationFailed,
+ ErrorNUPBadFormat,
+ ErrorNUPNoBasePackage,
};
std::string GetResultStatusString(ResultStatus status);
diff --git a/src/core/loader/nup.cpp b/src/core/loader/nup.cpp
new file mode 100644
index 0000000000..7ddfb8947e
--- /dev/null
+++ b/src/core/loader/nup.cpp
@@ -0,0 +1,280 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/loader/nup.h"
+
+#include <algorithm>
+#include <cstring>
+
+#include "common/logging.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/file_sys/nup_container.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/romfs.h"
+#include "core/file_sys/romfs_factory.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/loader/deconstructed_rom_directory.h"
+
+namespace Loader {
+
+namespace {
+
+using FileSys::NupContainer;
+
+// NACP stores 16 uncompressed language entries; map BCP-47 tags onto them.
+struct LanguageSlot {
+ std::string_view tag;
+ std::size_t index;
+};
+
+constexpr std::array<LanguageSlot, 18> LanguageSlots{{
+ {"en-us", 0}, {"en-gb", 1}, {"ja", 2}, {"fr", 3}, {"de", 4},
+ {"es-419", 5}, {"es", 6}, {"it", 7}, {"nl", 8}, {"fr-ca", 9},
+ {"pt", 10}, {"ru", 11}, {"ko", 12}, {"zh-hant", 13}, {"zh-hans", 14},
+ {"pt-br", 15}, {"pl", 16}, {"th", 17},
+}};
+
+void CopyNameToEntry(FileSys::LanguageEntry& entry, std::string_view name) {
+ const std::size_t length = std::min(name.size(), entry.application_name.size() - 1);
+ std::memcpy(entry.application_name.data(), name.data(), length);
+ entry.application_name[length] = '\0';
+}
+
+std::unique_ptr<FileSys::NACP> BuildNacp(const NupContainer::PackageInfo& base,
+ const std::string& version) {
+ if (base.title_name.empty() && base.localized_titles.empty() && version.empty()) {
+ return nullptr;
+ }
+ auto nacp = std::make_unique<FileSys::NACP>();
+ nacp->raw.supported_languages = static_cast<FileSys::SupportedLanguage>(0x3FFFF);
+
+ for (const auto& [lang, name] : base.localized_titles) {
+ for (const auto& slot : LanguageSlots) {
+ // The uncompressed RawNACP only stores 16 entries; Polish/Thai exist
+ // only in the compressed format, so they fall back to the default.
+ if (slot.index < 16 && slot.tag == lang) {
+ CopyNameToEntry(nacp->raw.language_entries.language_entries[slot.index], name);
+ }
+ }
+ }
+ if (!base.title_name.empty()) {
+ CopyNameToEntry(nacp->raw.language_entries.language_entries[0], base.title_name);
+ }
+ if (!version.empty()) {
+ const std::size_t length = std::min(version.size(), nacp->raw.version_string.size() - 1);
+ std::memcpy(nacp->raw.version_string.data(), version.data(), length);
+ nacp->raw.version_string[length] = '\0';
+ }
+ nacp->raw.save_data_owner_id = base.title_id;
+
+ nacp->language_entries.resize(16);
+ std::memcpy(nacp->language_entries.data(), nacp->raw.language_entries.language_entries.data(),
+ sizeof(nacp->raw.language_entries.language_entries));
+ return nacp;
+}
+
+std::string DecodeVersion(u32 version) {
+ if (version == 0) {
+ return {};
+ }
+ return fmt::format("{}.{}.{}", version >> 16, (version >> 8) & 0xFF, version & 0xFF);
+}
+
+} // namespace
+
+AppLoader_NUP::AppLoader_NUP(FileSys::VirtualFile file_)
+ : AppLoader(std::move(file_)), nup(std::make_unique<NupContainer>(file)) {
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return;
+ }
+
+ const auto base_index = nup->FindBasePackage();
+ const auto update_index = nup->FindBestUpdatePackage();
+
+ // The program comes from the base package; a standalone update container
+ // (SPEC §4.1.2) carries a complete patched tree and boots on its own.
+ std::optional<std::size_t> program_index;
+ std::optional<std::size_t> exefs_index;
+ if (base_index && nup->GetExeFSDir(*base_index) != nullptr) {
+ program_index = base_index;
+ exefs_index = update_index && nup->GetExeFSDir(*update_index) != nullptr
+ ? update_index
+ : base_index;
+ } else if (update_index && nup->GetExeFSDir(*update_index) != nullptr) {
+ LOG_INFO(Loader,
+ "NUP container is a standalone update; booting the update program directly");
+ program_index = update_index;
+ exefs_index = update_index;
+ }
+ if (!program_index || !exefs_index) {
+ LOG_ERROR(Loader, "NUP container has no package with a usable ExeFS");
+ return;
+ }
+
+ const auto& program = nup->GetPackages()[*program_index];
+ base_title_id = program.title_id;
+ has_program = true;
+
+ const auto exefs = nup->GetExeFSDir(*exefs_index);
+ if (exefs == nullptr || exefs->GetFile("main") == nullptr ||
+ exefs->GetFile("main.npdm") == nullptr) {
+ LOG_ERROR(Loader, "NUP package has no usable ExeFS");
+ return;
+ }
+ secondary_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs, true);
+
+ if (update_index) {
+ const auto& update = nup->GetPackages()[*update_index];
+ display_version = update.display_version.empty() ? DecodeVersion(update.version)
+ : update.display_version;
+ } else {
+ display_version = program.display_version.empty() ? DecodeVersion(program.version)
+ : program.display_version;
+ }
+ nacp_file = BuildNacp(program, display_version);
+ if (nacp_file != nullptr && !program.icon.empty()) {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(program.icon, "icon.dat");
+ } else if (nacp_file != nullptr) {
+ for (const auto& [lang, icon] : program.localized_icons) {
+ if (lang == "en-us") {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(icon, "icon_" + lang + ".dat");
+ break;
+ }
+ }
+ if (icon_file == nullptr && !program.localized_icons.empty()) {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(
+ program.localized_icons.front().second, "icon.dat");
+ }
+ }
+
+ const auto addon_count = std::count_if(nup->GetPackages().begin(), nup->GetPackages().end(),
+ [](const auto& package) {
+ return package.kind ==
+ NupContainer::PackageKind::Addon;
+ });
+ if (addon_count > 0) {
+ LOG_WARNING(Loader,
+ "NUP container has {} add-on content package(s); AoC mounting is not "
+ "supported yet",
+ addon_count);
+ }
+}
+
+AppLoader_NUP::~AppLoader_NUP() = default;
+
+FileType AppLoader_NUP::IdentifyType(const FileSys::VirtualFile& nup_file) {
+ return FileSys::NupContainer::IsNupFile(nup_file) ? FileType::NUP : FileType::Error;
+}
+
+AppLoader::LoadResult AppLoader_NUP::Load(Kernel::KProcess& process, Core::System& system) {
+ if (is_loaded) {
+ return {ResultStatus::ErrorAlreadyLoaded, {}};
+ }
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return {nup->GetStatus(), {}};
+ }
+ if (!has_program || secondary_loader == nullptr) {
+ return {ResultStatus::ErrorNUPNoBasePackage, {}};
+ }
+
+ const auto result = secondary_loader->Load(process, system);
+ if (result.first != ResultStatus::Success) {
+ return result;
+ }
+
+ system.GetFileSystemController().RegisterProcess(
+ process.GetProcessId(), base_title_id,
+ std::make_shared<FileSys::RomFSFactory>(*this, system.GetContentProvider(),
+ system.GetFileSystemController()));
+
+ is_loaded = true;
+ return result;
+}
+
+ResultStatus AppLoader_NUP::VerifyIntegrity(
+ std::function<bool(size_t, size_t)> progress_callback) {
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return nup->GetStatus();
+ }
+ return nup->VerifyIntegrity(std::move(progress_callback));
+}
+
+ResultStatus AppLoader_NUP::ReadRomFS(FileSys::VirtualFile& out_file) {
+ const auto base_index = nup->FindBasePackage();
+ const auto base_romfs = base_index ? nup->GetRomFSDir(*base_index) : FileSys::VirtualDir{};
+ const auto update_index = nup->FindBestUpdatePackage();
+ const auto update_romfs =
+ update_index ? nup->GetRomFSDir(*update_index) : FileSys::VirtualDir{};
+ // A standalone update carries the complete merged tree; a base+update pair
+ // overlays the update onto the base (SPEC §4.1.4).
+ const auto merged = FileSys::NupContainer::MergeShadowDirs(base_romfs, update_romfs);
+ if (merged == nullptr) {
+ return ResultStatus::ErrorNoRomFS;
+ }
+ out_file = FileSys::CreateRomFS(merged);
+ if (out_file == nullptr) {
+ return ResultStatus::ErrorNoRomFS;
+ }
+ return ResultStatus::Success;
+}
+
+bool AppLoader_NUP::IsRomFSUpdatable() const {
+ // The update is already merged into the exposed RomFS.
+ return false;
+}
+
+ResultStatus AppLoader_NUP::ReadProgramId(u64& out_program_id) {
+ if (!has_program) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ out_program_id = base_title_id;
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadProgramIds(std::vector<u64>& out_program_ids) {
+ if (!has_program) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ out_program_ids.push_back(base_title_id);
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadIcon(std::vector<u8>& buffer) {
+ if (icon_file == nullptr) {
+ return ResultStatus::ErrorNoIcon;
+ }
+ buffer = icon_file->ReadAllBytes();
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadTitle(std::string& title) {
+ if (nacp_file == nullptr) {
+ return ResultStatus::ErrorNoControl;
+ }
+ title = nacp_file->GetApplicationName();
+ if (title.empty()) {
+ return ResultStatus::ErrorNoControl;
+ }
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadControlData(FileSys::NACP& control) {
+ if (nacp_file == nullptr) {
+ return ResultStatus::ErrorNoControl;
+ }
+ control = *nacp_file;
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadNSOModules(Modules& modules) {
+ if (secondary_loader == nullptr) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ return secondary_loader->ReadNSOModules(modules);
+}
+
+} // namespace Loader
diff --git a/src/core/loader/nup.h b/src/core/loader/nup.h
new file mode 100644
index 0000000000..1f9dd5922b
--- /dev/null
+++ b/src/core/loader/nup.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "core/file_sys/nup_container.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+class NACP;
+class NupContainer;
+} // namespace FileSys
+
+namespace Loader {
+
+/// Loads a NUP (NX Package) container: loose ExeFS/RomFS trees for a base
+/// program plus its updates in one file. The newest update package is layered
+/// over the base; add-on content packages are recognized but not yet mounted.
+class AppLoader_NUP final : public AppLoader {
+public:
+ explicit AppLoader_NUP(FileSys::VirtualFile file_);
+ ~AppLoader_NUP() override;
+
+ /**
+ * Identifies whether or not the given file is a NUP container.
+ *
+ * @param nup_file The file to identify.
+ *
+ * @return FileType::NUP, or FileType::Error if the file is not a NUP.
+ */
+ static FileType IdentifyType(const FileSys::VirtualFile& nup_file);
+
+ FileType GetFileType() const override {
+ return FileType::NUP;
+ }
+
+ LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
+
+ ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override;
+
+ ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override;
+ bool IsRomFSUpdatable() const override;
+ ResultStatus ReadProgramId(u64& out_program_id) override;
+ ResultStatus ReadProgramIds(std::vector<u64>& out_program_ids) override;
+ ResultStatus ReadIcon(std::vector<u8>& buffer) override;
+ ResultStatus ReadTitle(std::string& title) override;
+ ResultStatus ReadControlData(FileSys::NACP& control) override;
+ ResultStatus ReadNSOModules(Modules& modules) override;
+
+private:
+ std::unique_ptr<FileSys::NupContainer> nup;
+ std::unique_ptr<AppLoader> secondary_loader;
+
+ std::unique_ptr<FileSys::NACP> nacp_file;
+ FileSys::VirtualFile icon_file;
+ u64 base_title_id{};
+ std::string display_version;
+ bool has_program{};
+};
+
+} // namespace Loader