diff options
| author | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:01:57 +0100 |
|---|---|---|
| committer | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:01:57 +0100 |
| commit | b374709aa202d586dd09f81f3389da8d73fea0d7 (patch) | |
| tree | 5c4033d827c517aca9cc73dc497fbb3e81291bf1 /src/core | |
| parent | 021c9724371e45fa694c9ee3106b9011364d2853 (diff) | |
| parent | ce1b1de6f1beed36958368f89cda466937ff9716 (diff) | |
Merge remote-tracking branch 'origin/nx2' into codex/upstream-loading-hotkeys
Diffstat (limited to 'src/core')
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 = ¬ification_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 |
