diff options
| author | Jonas <104419015+jojo-chaechae@users.noreply.github.com> | 2026-11-03 04:18:21 +0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-11-03 04:18:21 +0600 |
| commit | 155bf34fc297b6b8e4c281ecf7ea5d9ce3140762 (patch) | |
| tree | ef69b034a8bd3bfc710c182c52e34e0acd4b3735 /src/core | |
| parent | 05259375e0fb436c78882f8d9b95f9e2ce69af13 (diff) | |
| parent | 9f4f01f76e8556941c0c0c3f57edc1d600ed23ea (diff) | |
Merge pull request #337 from Sssnowyyy/nx2-review-fixes
nx2: fix pokopia regressions, textures that handle shader computes, and more
Diffstat (limited to 'src/core')
| -rw-r--r-- | src/core/file_sys/nup_container.cpp | 210 | ||||
| -rw-r--r-- | src/core/file_sys/nup_container.h | 54 | ||||
| -rw-r--r-- | src/core/file_sys/nup_writer.h | 7 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp | 10 | ||||
| -rw-r--r-- | src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp | 5 | ||||
| -rw-r--r-- | src/core/hle/service/nx2_gsv.h | 77 |
6 files changed, 296 insertions, 67 deletions
diff --git a/src/core/file_sys/nup_container.cpp b/src/core/file_sys/nup_container.cpp index 0e485b44ec..27ac4486ca 100644 --- a/src/core/file_sys/nup_container.cpp +++ b/src/core/file_sys/nup_container.cpp @@ -8,11 +8,15 @@ #include <cstring> #include <list> #include <map> +#include <random> +#include <span> #include <unordered_map> +#include <fmt/format.h> #include <openssl/evp.h> #include <zstd.h> +#include "common/fs/path_util.h" #include "common/logging.h" #include "common/swap.h" #include "core/file_sys/vfs/vfs_offset.h" @@ -31,6 +35,7 @@ constexpr u32 TocMagic = 0x01434F54; // "TOC\x01" little-endian constexpr u8 CodecRaw = 0; constexpr u8 CodecZstd = 1; constexpr u8 PackageFlagStandaloneUpdate = 0x01; +constexpr u32 MaxDirDepth = 128; #pragma pack(push, 1) struct Superblock { @@ -142,8 +147,8 @@ bool Sha256(const u8* data, std::size_t size, std::array<u8, 0x20>& out) { 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. +/// A member file whose extent is zstd-compressed. Decompresses on first read and caches the +/// result (LRU-evicted); a file over half the cache cap is decompressed once to a temporary file. class NupVfsFile final : public VfsFile { public: NupVfsFile(VirtualFile backing_, u64 extent_offset_, u64 extent_size_, u64 uncompressed_size_, @@ -151,7 +156,7 @@ public: 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) {} + cache_index(cache_index_), cache_enabled(uncompressed_size <= cache->cap / 2) {} std::string GetName() const override { return name; @@ -183,6 +188,10 @@ public: } length = std::min(length, static_cast<std::size_t>(uncompressed_size) - offset); + if (!cache_enabled) { + return ReadSpilled(data, length, offset); + } + { std::scoped_lock lock{cache->mutex}; if (const auto* cached = cache->Find(cache_index)) { @@ -198,9 +207,7 @@ public: } { std::scoped_lock lock{cache->mutex}; - if (cache_enabled) { - cache->Insert(cache_index, scratch); - } + cache->Insert(cache_index, scratch); if (const auto* cached = cache->Find(cache_index)) { std::memcpy(data, cached->data() + offset, length); return length; @@ -220,6 +227,88 @@ public: } private: + std::size_t ReadSpilled(u8* data, std::size_t length, std::size_t offset) const { + std::shared_ptr<NupSpillFile> spill; + { + std::scoped_lock lock{cache->mutex}; + spill = cache->GetSpill(cache_index); + } + std::scoped_lock lock{spill->mutex}; + if (!spill->attempted) { + spill->attempted = true; + DecompressToFile(*spill); + } + if (!spill->file.IsOpen() || !spill->file.Seek(static_cast<s64>(offset))) { + return 0; + } + return spill->file.ReadSpan(std::span{data, length}); + } + + void DecompressToFile(NupSpillFile& spill) const { + const auto fail = [&](const char* reason) { + LOG_ERROR(Loader, "NUP zstd decompression of {} to a temporary file failed: {}", name, + reason); + spill.file.Close(); + if (!spill.path.empty()) { + std::error_code ec; + std::filesystem::remove(spill.path, ec); + } + }; + if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) { + return fail("extent out of range"); + } + + std::random_device random; + std::error_code ec; + std::filesystem::create_directories(cache->spill_dir, ec); + spill.path = + cache->spill_dir / fmt::format("{:08x}{:08x}-{}.tmp", random(), random(), cache_index); + Common::FS::IOFile out{spill.path, Common::FS::FileAccessMode::Write}; + if (!out.IsOpen()) { + return fail("cannot create the temporary file"); + } + + std::unique_ptr<ZSTD_DCtx, decltype(&ZSTD_freeDCtx)> dctx{ZSTD_createDCtx(), + &ZSTD_freeDCtx}; + std::vector<u8> in_buffer(ZSTD_DStreamInSize()); + std::vector<u8> out_buffer(ZSTD_DStreamOutSize()); + u64 read = 0; + u64 written = 0; + bool frame_done = false; + while (!frame_done && read < extent_size) { + const auto chunk = + static_cast<std::size_t>(std::min<u64>(in_buffer.size(), extent_size - read)); + if (backing->Read(in_buffer.data(), chunk, + static_cast<std::size_t>(extent_offset + read)) != chunk) { + return fail("short read of the compressed data"); + } + read += chunk; + ZSTD_inBuffer input{in_buffer.data(), chunk, 0}; + ZSTD_outBuffer output{}; + do { + output = {out_buffer.data(), out_buffer.size(), 0}; + const std::size_t result = ZSTD_decompressStream(dctx.get(), &output, &input); + if (ZSTD_isError(result) || output.pos > uncompressed_size - written) { + return fail("corrupt or oversized data"); + } + if (out.WriteSpan(std::span<const u8>{out_buffer.data(), output.pos}) != + output.pos) { + return fail("cannot write the temporary file"); + } + written += output.pos; + frame_done = result == 0; + } while (!frame_done && (input.pos < input.size || output.pos == output.size)); + } + if (!frame_done || written != uncompressed_size) { + return fail("size mismatch"); + } + out.Close(); + spill.file.Open(spill.path, Common::FS::FileAccessMode::Read); + if (!spill.file.IsOpen()) { + fail("cannot reopen the temporary file"); + } + } + std::vector<u8> Decompress() const { if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) { return {}; @@ -251,6 +340,13 @@ private: } // namespace +NupSpillFile::~NupSpillFile() { + file.Close(); + if (!path.empty()) { + std::error_code ec; + std::filesystem::remove(path, ec); + } +} bool NupContainer::IsNupFile(const VirtualFile& file) { if (file == nullptr || file->GetSize() < SuperblockSize) { @@ -261,8 +357,13 @@ bool NupContainer::IsNupFile(const VirtualFile& file) { return magic == HeaderMagic; } -NupContainer::NupContainer(VirtualFile file_) +NupContainer::NupContainer(VirtualFile file_, std::size_t memory_cache_cap, + std::filesystem::path spill_dir_) : file(std::move(file_)), cache(std::make_shared<NupDecompressCache>()) { + cache->cap = memory_cache_cap; + cache->spill_dir = spill_dir_.empty() + ? Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "nup" + : std::move(spill_dir_); if (file == nullptr) { return; } @@ -293,9 +394,9 @@ bool NupContainer::Parse() { 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) { + if (file_size != file->GetSize() || toc_offset < SuperblockSize || blob_offset > file_size || + toc_offset > blob_offset || toc_size > blob_offset - toc_offset || + blob_offset % 0x10 != 0) { status = Loader::ResultStatus::ErrorNUPBadFormat; return false; } @@ -325,15 +426,22 @@ bool NupContainer::Parse() { return false; } + // The counts come from the file, so bound them by the TOC size before multiplying. + if (header.dir_count > toc.size() / DirRecordSize || + header.file_count > toc.size() / FileRecordSize) { + 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) { + if (needed > toc.size() || header.pool_offset < needed || header.pool_offset > toc.size() || + header.pool_size > toc.size() - header.pool_offset || + (header.pool_offset - needed) % sizeof(u32) != 0) { status = Loader::ResultStatus::ErrorNUPBadFormat; return false; } + const u64 children_offset = needed; + const u64 children_bytes = header.pool_offset - needed; const u8* base = toc.data(); std::size_t pos = TocHeaderSize; @@ -358,8 +466,8 @@ bool NupContainer::Parse() { 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) { + if (record.meta_size > 0 && record.meta_offset != 0 && record.meta_offset <= file_size && + record.meta_size <= file_size - record.meta_offset) { package.meta_offset = record.meta_offset; package.meta_size = record.meta_size; } @@ -371,8 +479,9 @@ bool NupContainer::Parse() { 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) { + if (static_cast<u64>(record.first_child) + record.child_count > + children_bytes / sizeof(u32) || + static_cast<u64>(record.name_offset) + record.name_length > header.pool_size) { status = Loader::ResultStatus::ErrorNUPBadFormat; return false; } @@ -392,9 +501,9 @@ bool NupContainer::Parse() { (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) { + record.extent_offset % 0x10 != 0 || record.extent_offset > file_size || + record.extent_size > file_size - record.extent_offset)) || + static_cast<u64>(record.name_offset) + record.name_length > header.pool_size) { status = Loader::ResultStatus::ErrorNUPBadFormat; return false; } @@ -417,6 +526,11 @@ bool NupContainer::Parse() { } } + if (!ValidateDirTree()) { + status = Loader::ResultStatus::ErrorNUPBadFormat; + return false; + } + for (auto& package : packages) { if (!ParseMeta(package)) { status = Loader::ResultStatus::ErrorNUPBadFormat; @@ -426,6 +540,57 @@ bool NupContainer::Parse() { return true; } +bool NupContainer::ValidateDirTree() const { + enum : u8 { Unvisited, InProgress, Done }; + struct Frame { + u32 dir; + u32 next; + }; + std::vector<u8> state(dirs.size(), Unvisited); + std::vector<u32> height(dirs.size(), 0); + std::vector<Frame> stack; + for (u32 start = 0; start < dirs.size(); ++start) { + if (state[start] != Unvisited) { + continue; + } + state[start] = InProgress; + stack.push_back({start, 0}); + while (!stack.empty()) { + const Frame frame = stack.back(); + const DirNode& node = dirs[frame.dir]; + if (frame.next == node.child_count) { + state[frame.dir] = Done; + stack.pop_back(); + if (!stack.empty()) { + u32& parent_height = height[stack.back().dir]; + parent_height = std::max(parent_height, height[frame.dir] + 1); + } + continue; + } + ++stack.back().next; + const u32 child = children[static_cast<std::size_t>(node.first_child) + frame.next]; + if (child >= dirs.size()) { + continue; + } + if (state[child] == InProgress) { + LOG_ERROR(Loader, "NUP directory tree contains a cycle"); + return false; + } + if (state[child] == Done) { + height[frame.dir] = std::max(height[frame.dir], height[child] + 1); + } else { + state[child] = InProgress; + stack.push_back({child, 0}); + } + if (stack.size() + height[child] > MaxDirDepth) { + LOG_ERROR(Loader, "NUP directory tree is nested too deeply"); + return false; + } + } + } + return true; +} + bool NupContainer::ParseMeta(PackageInfo& package) { if (package.meta_offset == 0) { return true; @@ -547,7 +712,8 @@ VirtualFile NupContainer::OpenFile(std::size_t 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 u64 end = static_cast<u64>(node.first_child) + node.child_count; + for (u64 slot = node.first_child; slot < end; ++slot) { const u32 child = children[slot]; if (child >= dirs.size()) { out.AddFile(OpenFile(child - static_cast<u32>(dirs.size()))); diff --git a/src/core/file_sys/nup_container.h b/src/core/file_sys/nup_container.h index 118b137a7b..8787717d47 100644 --- a/src/core/file_sys/nup_container.h +++ b/src/core/file_sys/nup_container.h @@ -4,6 +4,7 @@ #pragma once #include <array> +#include <filesystem> #include <functional> #include <list> #include <memory> @@ -14,19 +15,36 @@ #include <vector> #include "common/common_types.h" +#include "common/fs/file.h" #include "core/file_sys/vfs/vfs_types.h" #include "core/file_sys/vfs/vfs_vector.h" #include "core/loader/loader.h" namespace FileSys { +/// A zstd member too large for the in-memory cache, decompressed once into a temporary file. +struct NupSpillFile { + ~NupSpillFile(); + + std::mutex mutex; + std::filesystem::path path; + Common::FS::IOFile file; + bool attempted{}; +}; + /// LRU over decompressed zstd extents, shared by the files of one container. struct NupDecompressCache { + static constexpr std::size_t DefaultCap = 512 * 1024 * 1024; + + using Entry = std::pair<std::size_t, std::vector<u8>>; + 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::list<Entry> lru; + std::unordered_map<std::size_t, std::list<Entry>::iterator> index; std::size_t bytes{}; + std::size_t cap{DefaultCap}; + std::filesystem::path spill_dir; + std::unordered_map<std::size_t, std::shared_ptr<NupSpillFile>> spills; std::vector<u8>* Find(std::size_t file_index) { const auto it = index.find(file_index); @@ -37,21 +55,31 @@ struct NupDecompressCache { return &it->second->second; } + /// Takes ownership of `data` only when it is inserted. void Insert(std::size_t file_index, std::vector<u8>& data) { - while (bytes + data.size() > DecompressCacheCap && !lru.empty()) { + if (index.contains(file_index)) { + return; + } + while (bytes + data.size() > cap && !lru.empty()) { bytes -= lru.back().second.size(); index.erase(lru.back().first); lru.pop_back(); } - if (bytes + data.size() > DecompressCacheCap) { + if (bytes + data.size() > cap) { return; } - lru.emplace_front(file_index, data); - index.emplace(file_index, lru.begin()); bytes += data.size(); + lru.emplace_front(file_index, std::move(data)); + index.emplace(file_index, lru.begin()); } - static constexpr std::size_t DecompressCacheCap = 512 * 1024 * 1024; + std::shared_ptr<NupSpillFile> GetSpill(std::size_t file_index) { + auto& spill = spills[file_index]; + if (!spill) { + spill = std::make_shared<NupSpillFile>(); + } + return spill; + } }; /** @@ -101,7 +129,11 @@ public: /// Recursively merges overlay into a copy of base; overlay paths win. static VirtualDir MergeShadowDirs(const VirtualDir& base, const VirtualDir& overlay); - explicit NupContainer(VirtualFile file_); + /// Zstd members larger than half of `memory_cache_cap` are decompressed to a temporary file + /// in `spill_dir` (the cache directory by default) instead of being held in memory. + explicit NupContainer(VirtualFile file_, + std::size_t memory_cache_cap = NupDecompressCache::DefaultCap, + std::filesystem::path spill_dir_ = {}); ~NupContainer(); Loader::ResultStatus GetStatus() const { @@ -119,7 +151,8 @@ public: 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. + /// the container, zstd extents are lazily decompressed, cached in memory + /// or, when large, in a temporary file. VirtualFile OpenFile(std::size_t file_index); VirtualDir GetExeFSDir(std::size_t package_index); @@ -151,6 +184,7 @@ private: bool Parse(); bool ParseMeta(PackageInfo& package); + bool ValidateDirTree() const; void BuildDirTree(u32 dir_index, VectorVfsDirectory& out); VirtualDir BuildPackageDir(u32 dir_index); std::vector<u8> ReadExtentRaw(const FileNode& node) const; diff --git a/src/core/file_sys/nup_writer.h b/src/core/file_sys/nup_writer.h index d2423dd1f4..dd7b7e0883 100644 --- a/src/core/file_sys/nup_writer.h +++ b/src/core/file_sys/nup_writer.h @@ -25,10 +25,9 @@ struct PackProgress { 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. +// NupContainer keeps a zstd file in memory only while it fits half of its 512 MiB decompression +// cache; a larger one is decompressed to a temporary file on first read, which costs time and +// disk space. Files above this size are therefore stored raw. constexpr u64 DefaultRawThreshold = 256ULL * 1024 * 1024; // Application metadata written to the base package (or the first package when there is no base). 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 40313a10fb..c5b23bef3e 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -176,7 +176,7 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) { Nx2GpuMemoryRegistry::Get().RecordDeviceRange(cpu_address, size, device_address); // 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] { + system.GPU().MapInOrder([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, @@ -253,7 +253,7 @@ void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) { } nx2_sparse_ranges[gpu_address] = {size, device_address}; // On the GPU thread, as in Nx2Map. - system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] { + system.GPU().MapInOrder([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, @@ -270,7 +270,7 @@ void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { --it; const auto& [start, range] = *it; const auto& [region_size, device_address] = range; - if (gpu_address + size > start + region_size) { + if (size > region_size || gpu_address - start > region_size - size) { return; } system.Host1x().MemoryManager().Map(device_address + (gpu_address - start), cpu_address, size, @@ -287,7 +287,7 @@ void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) { --it; const auto& [start, range] = *it; const auto& [region_size, device_address] = range; - if (gpu_address + size > start + region_size) { + if (size > region_size || gpu_address - start > region_size - size) { return; } // In GPU-thread order: command lists submitted before the request still see the old tile. @@ -295,7 +295,7 @@ void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) { // whose contents are now what the game's memory holds there. const DAddr device = device_address + (gpu_address - start); auto& smmu = system.Host1x().MemoryManager(); - system.GPU().RunInOrder([&smmu, device, cpu_address, size, asid = session->asid] { + system.GPU().MapInOrder([&smmu, device, cpu_address, size, asid = session->asid] { smmu.Unmap(device, size); if (cpu_address != 0) { smmu.Map(device, cpu_address, size, asid, true); diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp index 848c15d6f8..79586446d5 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp @@ -445,7 +445,10 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL const char* value = std::getenv("EDEN_NX2_PB_SNAPSHOT"); return value == nullptr || value[0] != '0'; }(); - if (snapshot && channel_state->memory_manager) { + if (snapshot && channel_state->memory_manager && channel_state->payload) { + // Before the copy: a suspension during it must count as one after it. + entries.snapshot_suspensions = + channel_state->payload->dma_pusher.suspensions.load(std::memory_order_acquire); size_t words = 0; for (const auto& entry : entries.command_lists) { words += entry.size; diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 37c797fa3f..259681a2a2 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -15,6 +15,7 @@ #include <cstdlib> #include <cstdio> #include <initializer_list> +#include <limits> #include <atomic> #include <deque> #include <map> @@ -1076,13 +1077,18 @@ private: } const u64 gpu_address = request.gpu_address; const u64 size = request.size; + if (size > std::numeric_limits<u64>::max() - gpu_address) { + LOG_WARNING(Service, "nx2:gpu 216 [{:#x}, +{:#x}] wraps the address space", gpu_address, + size); + return; + } auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); auto it = m_sparse_regions.upper_bound(gpu_address); SparseRegion* region = nullptr; u64 start = 0; if (it != m_sparse_regions.begin()) { --it; - if (gpu_address + size <= it->first + it->second.size) { + if (size <= it->second.size && gpu_address - it->first <= it->second.size - size) { start = it->first; region = &it->second; } @@ -1145,7 +1151,7 @@ private: return std::nullopt; } --it; - if (gpu_address + size > it->first + it->second) { + if (size > it->second || gpu_address - it->first > it->second - size) { return std::nullopt; } return it->first; @@ -1240,7 +1246,7 @@ private: return std::nullopt; } --it; - if (gpu_address + size > it->first + it->second.size) { + if (size > it->second.size || gpu_address - it->first > it->second.size - size) { return std::nullopt; } return it->second.cpu_address + (gpu_address - it->first); @@ -1705,37 +1711,41 @@ private: LOG_INFO(Service, "nx2:vi2 229 present #{} token={:#x} slot={} image={}", m_presents, u64{w[2]} | (u64{w[3]} << 32), w[4], w[5]); } - // Wait for the previous frame's fence instead of this one, so the game prepares frame N+1 - // while the GPU thread runs frame N (the console keeps several frames in flight). Waiting - // for this frame serialized the two: 8-56 ms of every 34-105 ms frame on title 1. - // EDEN_NX2_FRAMES_AHEAD=0 restores the wait for the current frame. - static const bool frames_ahead = [] { + // Wait for this frame's fence, so the game doesn't write the next frame's data (constant + // buffers holding texture handles, vertex data, Pokopia's glyph atlas) while the GPU + // thread still reads this frame's. But title 1's GPU waits on semaphores its CPU releases + // only after this present returns: while a channel stays suspended on one, wait for the + // previous frame's fence instead and let the game prepare frame N+1 meanwhile (waiting + // for this frame there cost 8-56 ms of every 34-105 ms frame, or never finished). + // EDEN_NX2_FRAMES_AHEAD=1 always waits for the previous frame, =0 always for this one. + static const int frames_ahead = [] { const char* value = std::getenv("EDEN_NX2_FRAMES_AHEAD"); - return value == nullptr || value[0] != '0'; + return value == nullptr ? -1 : value[0] == '1' ? 1 : 0; }(); const auto entered = std::chrono::steady_clock::now(); - if (frames_ahead) { + if (frames_ahead == 1 || (frames_ahead == -1 && !WaitForFrame(~w[22], w[24], true))) { 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); + // With EDEN_NX2_FRAMES_AHEAD=1, 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. Not when a + // suspended channel brought us here: this frame can't finish before the present + // returns. + const bool gpu_behind = frames_ahead == 1 && 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 { + } else if (frames_ahead == 0) { WaitForFrame(~w[22], w[24]); } + m_previous_fence_id = ~w[22]; + m_previous_fence_value = w[24]; const auto waited = std::chrono::steady_clock::now(); Composite(w[5], ~w[22], w[24], w[7]); const auto composited = std::chrono::steady_clock::now(); @@ -1832,9 +1842,12 @@ private: m_next_vblank += interval; } - void WaitForFrame(u32 fence_id, u32 fence_value) { + /// Waits until the fence is reached. With `unless_gpu_waits`, gives up after a few ms once a + /// GPU channel was seen suspended on a semaphore (it may be one this thread releases later). + /// Returns whether the fence was reached. + bool WaitForFrame(u32 fence_id, u32 fence_value, bool unless_gpu_waits = false) { if (fence_id >= 192) { - return; + return true; } auto& syncpoints = system.Host1x().GetSyncpointManager(); const auto start = std::chrono::steady_clock::now(); @@ -1859,11 +1872,24 @@ private: wake->cv.notify_all(); }); std::unique_lock lk{wake->mutex}; + // Title 1's channels resume and suspend again while they wait on this thread, so any + // suspension counts, not only an unbroken one. + bool gpu_waited = false; while (!wake->done && system.IsPoweredOn() && std::chrono::steady_clock::now() < deadline) { - wake->cv.wait_for(lk, std::chrono::milliseconds{50}); + if (unless_gpu_waits) { + gpu_waited |= system.GPU().HasSuspendedChannels(); + if (gpu_waited && + std::chrono::steady_clock::now() - start > std::chrono::milliseconds{3}) { + return false; + } + } + wake->cv.wait_for(lk, std::chrono::milliseconds{unless_gpu_waits ? 1 : 50}); } } + if (unless_gpu_waits && !syncpoints.IsReadyGuest(fence_id, fence_value)) { + return false; + } if (!syncpoints.IsReadyGuest(fence_id, fence_value) && m_wait_timeouts++ < 64) { LOG_WARNING(Service, "nx2:vi2 present #{} fence ({}, {}) not reached in 4 s (now {})", m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id)); @@ -1875,6 +1901,7 @@ private: std::chrono::steady_clock::now() - start) .count()); } + return syncpoints.IsReadyGuest(fence_id, fence_value); } // Shows a presented native image: word 5 is the image id, ~word 22 / word 24 the GPU fence |
