diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-11-02 23:55:44 +0100 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-11-02 23:55:44 +0100 |
| commit | a931609f31672f782d2817f5764d65c00dea4dad (patch) | |
| tree | 3cb5bd166d1a54a76969afd61c1db835c9cb293d /src | |
| parent | 3e10d48548a285172ea7cb1f94c58783e1d97754 (diff) | |
| parent | 8362c44ff835d7be73d2adad9ce3e2d4467a6aeb (diff) | |
Merge branch 'nx2' into ldn/room-handshake
Diffstat (limited to 'src')
75 files changed, 2090 insertions, 271 deletions
diff --git a/src/common/game_presets.h b/src/common/game_presets.h new file mode 100644 index 0000000000..83dd4d3c3f --- /dev/null +++ b/src/common/game_presets.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/settings.h" + +namespace Settings { + +inline constexpr u64 MarioKartWorldProgramId = 0x0400C3F00006E000ULL; + +inline void SetMarioKartWorldRecommendedSettings(Values& settings, bool global) { + const auto session = [global](auto& setting, auto value) { + setting.SetGlobal(global); + setting.SetValue(value); + }; + session(settings.renderer_backend, RendererBackend::Vulkan); + session(settings.resolution_setup, ResolutionSetup::Res1X); + session(settings.gpu_accuracy, GpuAccuracy::Low); + session(settings.use_asynchronous_shaders, true); + session(settings.astc_recompression, AstcRecompression::Bc3); + session(settings.vram_usage_mode, VramUsageMode::Conservative); + session(settings.use_reactive_flushing, false); + session(settings.vsync_mode, VSyncMode::FifoRelaxed); + session(settings.use_docked_mode, ConsoleMode::Handheld); + session(settings.scaling_filter, ScalingFilter::ScaleForce); + session(settings.anti_aliasing, AntiAliasing::Fxaa); +} + +inline bool ApplyMarioKartWorldPreset(Values& settings, u64 program_id, bool accepted) { + if (!accepted || program_id != MarioKartWorldProgramId) { + return false; + } + SetMarioKartWorldRecommendedSettings(settings, false); + return true; +} + +} // namespace Settings diff --git a/src/common/settings.h b/src/common/settings.h index 74e2006d15..6669f98f4c 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -358,7 +358,7 @@ struct Values { true}; SwitchableSetting<ScalingFilter> scaling_filter{linkage, - ScalingFilter::NearestNeighbor, + ScalingFilter::ScaleForce, "scaling_filter", Category::Renderer, Specialization::Default, @@ -387,7 +387,7 @@ struct Values { true}; SwitchableSetting<AntiAliasing> anti_aliasing{linkage, - AntiAliasing::None, + AntiAliasing::Fxaa, "anti_aliasing", Category::Renderer, Specialization::Default, @@ -770,11 +770,7 @@ struct Values { Setting<s32> current_user{linkage, 0, "current_user", Category::System}; SwitchableSetting<ConsoleMode> use_docked_mode{linkage, -#ifdef __ANDROID__ ConsoleMode::Handheld, -#else - ConsoleMode::Docked, -#endif "use_docked_mode", Category::System, Specialization::Radio, diff --git a/src/common/slot_vector.h b/src/common/slot_vector.h index f6da7a59d1..8e1c06e35a 100644 --- a/src/common/slot_vector.h +++ b/src/common/slot_vector.h @@ -52,12 +52,16 @@ public: const u64* const bitset = slot_vector->stored_bitset.data(); const u32 size = static_cast<u32>(slot_vector->stored_bitset.size()) * 64; if (id.Value() < size) { - do { - ++id.index; - } while (id.Value() < size && !IsValid(bitset)); - if (id.Value() == size) { - id = SlotId{}; + u32 index = id.Value() + 1; + while (index < size) { + const u64 word = bitset[index / 64] >> (index % 64); + if (word != 0) { + index += static_cast<u32>(std::countr_zero(word)); + break; + } + index = (index / 64 + 1) * 64; } + id = index < size ? SlotId{index} : SlotId{}; } return *this; } @@ -139,7 +143,7 @@ public: if (it == stored_bitset.end()) { return end(); } - const u32 word_index = static_cast<u32>(std::distance(it, stored_bitset.begin())); + const u32 word_index = static_cast<u32>(std::distance(stored_bitset.begin(), it)); const SlotId first_id{word_index * 64 + static_cast<u32>(std::countr_zero(*it))}; return Iterator(this, first_id); } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eeb3202d12..1ff9f271d2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1290,4 +1290,9 @@ else() target_sources(core PRIVATE hle/service/ssl/ssl_backend_none.cpp) endif() +if (ENABLE_LTO AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC) + # Fibers can migrate between host threads; keep kernel TLS lookups outside LTO callers. + set_source_files_properties(hle/kernel/kernel.cpp PROPERTIES COMPILE_OPTIONS -fno-lto) +endif() + create_target_directory_groups(core) 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/kernel/k_priority_queue.h b/src/core/hle/kernel/k_priority_queue.h index 89a0c5bbea..5d93455d33 100644 --- a/src/core/hle/kernel/k_priority_queue.h +++ b/src/core/hle/kernel/k_priority_queue.h @@ -11,6 +11,7 @@ #include <bitset> #include <concepts> #include <cstddef> +#include <type_traits> #include "common/alignment.h" #include "common/assert.h" @@ -196,12 +197,7 @@ public: constexpr Member* GetFront(s32 core) const { ASSERT(IsValidCore(core)); - const s32 priority = s32([](auto const& e) { - for (size_t i = 0; i < e.size(); ++i) - if (e[i]) - return i; - return e.size(); - }(m_available_priorities[core])); + const s32 priority = s32(GetNextSet(m_available_priorities[core], size_t(-1))); if (priority <= LowestPriority) { return m_queues[priority].GetFront(core); } else { @@ -222,7 +218,17 @@ public: template<size_t N> constexpr size_t GetNextSet(std::bitset<N> const& bit, size_t n) const { - for (size_t i = n + 1; i < bit.size(); i++) + const size_t first = n + 1; + if (first >= bit.size()) { + return bit.size(); + } + if constexpr (N <= 64) { + if (!std::is_constant_evaluated()) { + const u64 remaining = bit.to_ullong() >> first; + return remaining != 0 ? first + size_t(std::countr_zero(remaining)) : bit.size(); + } + } + for (size_t i = first; i < bit.size(); i++) if (bit[i]) return i; return bit.size(); diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index f006d129a7..9e6ccc2d89 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -79,7 +79,7 @@ struct KernelCore::Impl { // And we have the guarantee that the data won't move out of the way so we can safely // take a reference to it. This isn't always universally true but this is "global" data // so it will be statically given a TLS slot anyways. - static inline thread_local ThreadLocalData tls_data = {}; + static thread_local ThreadLocalData tls_data; explicit Impl(Core::System& system_, KernelCore& kernel_) : system{system_} { tls_data.lock = true; @@ -861,6 +861,8 @@ struct KernelCore::Impl { Core::System& system; }; +thread_local ThreadLocalData KernelCore::Impl::tls_data{}; + KernelCore::KernelCore(Core::System& system) : impl{std::make_unique<Impl>(system, *this)} {} KernelCore::~KernelCore() = default; 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 diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 4b1168a59b..49352a5b49 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -81,6 +81,24 @@ struct PatchCollection { namespace { +FileSys::VirtualDir FindLooseRomFSDirectory(const FileSys::VirtualDir& parent) { + if (parent == nullptr) { + return nullptr; + } + if (const auto dir = parent->GetSubdirectory("romfs")) { + return dir; + } + for (const auto& dir : parent->GetSubdirectories()) { + const auto name = dir->GetName(); + if (name.size() == 5 && std::equal(name.begin(), name.end(), "romfs", [](char a, char b) { + return std::tolower(static_cast<unsigned char>(a)) == b; + })) { + return dir; + } + } + return nullptr; +} + // RomFS for a loose ExeFS: the first .romfs file next to main, else a romfs/ directory next to // the exefs/ directory (the usual layout of an extracted title). FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { @@ -93,7 +111,7 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { } } if (const auto parent = exefs_dir->GetParentDirectory()) { - if (const auto romfs_dir = parent->GetSubdirectory("romfs")) { + if (const auto romfs_dir = FindLooseRomFSDirectory(parent)) { // 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; @@ -268,7 +286,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys romfs_exefs_dir = file_dir; if (const auto title_dir = file_dir->GetParentDirectory()) { - romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs")); + romfs_version = RomFSProductVersion(FindLooseRomFSDirectory(title_dir)); } } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index cfc39916db..1f7315f3c1 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -549,6 +549,8 @@ struct Memory::Impl { } void RasterizerArmReadTraps(u64 vaddr, u64 size) { + // Clean cached read areas also need invalidation when reactive flushing is enabled. + read_trap_generation.fetch_add(1, std::memory_order_release); if (vaddr == 0 || size == 0 || !AddressSpaceContains(*current_page_table, vaddr, size) || !ReadTrapsEnabled()) { return; @@ -781,6 +783,19 @@ struct Memory::Impl { gpu_device_memory = &system.Host1x().MemoryManager(); } const size_t core = system.GetCurrentHostThreadID(); + if (core >= Core::Hardware::NUM_CPU_CORES) { + // A service thread has no slot in the arrays below, and it does not use a slot of a + // core. It keeps no area, so it asks on each read. + if (static std::atomic_flag logged; !logged.test_and_set(std::memory_order_relaxed)) { + LOG_INFO(HW_Memory, "Read of cached guest memory from a thread that is not a core"); + } + Common::ScratchBuffer<u32> scratch; + gpu_device_memory->ApplyOpOnPointer(p, scratch, [&](DAddr address) { + DisarmReadTraps(v_address, size); + (void)system.GPU().OnCPURead(address, size); + }); + return; + } auto& current_area = rasterizer_read_areas[core]; if (const u64 generation = read_trap_generation.load(std::memory_order_acquire); read_area_generations[core] != generation) { diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp index 620e0cf630..a7840db32f 100644 --- a/src/input_common/drivers/udp_client.cpp +++ b/src/input_common/drivers/udp_client.cpp @@ -603,7 +603,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob( const std::string& host, u16 port, std::function<void(Status)> status_callback, std::function<void(u16, u16, u16, u16)> data_callback) { - std::thread([=, this] { + worker = std::jthread([=, this] { u16 min_x{UINT16_MAX}; u16 min_y{UINT16_MAX}; u16 max_x{}; @@ -647,7 +647,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob( complete_event.Wait(); socket.Stop(); worker_thread.join(); - }).detach(); + }); } CalibrationConfigurationJob::~CalibrationConfigurationJob() { diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h index cea9f579ac..eac3df07aa 100644 --- a/src/input_common/drivers/udp_client.h +++ b/src/input_common/drivers/udp_client.h @@ -183,6 +183,7 @@ public: private: Common::Event complete_event; + std::jthread worker; }; void TestCommunication(const std::string& host, u16 port, diff --git a/src/shader_recompiler/ir_opt/texture_pass.cpp b/src/shader_recompiler/ir_opt/texture_pass.cpp index 8a72f565dc..b6aca0a118 100644 --- a/src/shader_recompiler/ir_opt/texture_pass.cpp +++ b/src/shader_recompiler/ir_opt/texture_pass.cpp @@ -16,6 +16,7 @@ #include <limits> #include <boost/container/small_vector.hpp> +#include "common/scope_exit.h" #include "common/settings.h" #include "shader_recompiler/environment.h" #include "shader_recompiler/frontend/ir/basic_block.h" @@ -345,7 +346,8 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr // Cached variants of existing helpers // A handle computed per draw has no descriptor to read at translation time: assume an ordinary -// floating-point 2D texture, the instruction's own type deciding the rest. +// floating-point 2D texture, the instruction's own type deciding the rest (see the TexturePass +// ImageQueryDimensions case, which has none). static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) { if (cbuf.handle_program != ~0U) return TextureType::Color2D; return env.ReadTextureType(GetTextureHandleCached(env, cbuf)); @@ -436,9 +438,10 @@ bool IsUndefined(IR::Opcode opcode) { opcode == IR::Opcode::UndefU64; } -// Values already emitted by the program being built, by the slot that keeps them, and how deep -// in nested BuildHandleProgram calls the build is. +// Values already emitted by the program being built, by the slot that keeps them, how many slots +// are in use, and how deep in nested BuildHandleProgram calls the build is. thread_local std::unordered_map<const IR::Inst*, u32> g_handle_slots; +thread_local u32 g_handle_slot_count{}; thread_local int g_handle_depth{}; bool BuildHandleNode(const IR::Value& value, std::vector<u32>& code, int& budget, @@ -453,7 +456,15 @@ bool BuildHandleProgram(const IR::Value& value, std::vector<u32>& code, int& bud const bool outermost = g_handle_depth++ == 0; if (outermost) { g_handle_slots.clear(); + g_handle_slot_count = 0; } + SCOPE_EXIT { + --g_handle_depth; + if (outermost) { + g_handle_slots.clear(); + g_handle_slot_count = 0; + } + }; const IR::Inst* const inst{value.InstRecursive()}; bool result; if (const auto it = g_handle_slots.find(inst); it != g_handle_slots.end()) { @@ -470,23 +481,25 @@ bool BuildHandleProgram(const IR::Value& value, std::vector<u32>& code, int& bud } else { g_handle_slots.emplace(inst, ~0U); const size_t start = code.size(); + const u32 first_slot = g_handle_slot_count; result = BuildHandleNode(value, code, budget, block); g_handle_slots.erase(inst); if (!result) { + // The Tee of every value the dropped code kept goes with it: a later use of one of + // them (another Phi input) must build it again, not load a slot nothing wrote. code.resize(start); + std::erase_if(g_handle_slots, [first_slot](const auto& entry) { + return entry.second != ~0U && entry.second >= first_slot; + }); + g_handle_slot_count = first_slot; } if (result && inst->UseCount() > 1 && g_handle_slots.size() < 256) { - const u32 slot = static_cast<u32>( - std::ranges::count_if(g_handle_slots, [](const auto& entry) { return entry.second != ~0U; })); + const u32 slot = g_handle_slot_count++; g_handle_slots.emplace(inst, slot); code.push_back(static_cast<u32>(Op::Tee)); code.push_back(slot); } } - --g_handle_depth; - if (outermost) { - g_handle_slots.clear(); - } return result; } @@ -1010,7 +1023,8 @@ public: desc.secondary_cbuf_offset == existing.secondary_cbuf_offset && desc.secondary_shift_left == existing.secondary_shift_left && desc.count == existing.count && desc.size_shift == existing.size_shift && - desc.has_secondary == existing.has_secondary; + desc.has_secondary == existing.has_secondary && + desc.handle_program == existing.handle_program; }); } @@ -1018,7 +1032,8 @@ public: const u32 index{Add(image_buffer_descriptors, desc, [&desc](const auto& existing) { return desc.format == existing.format && desc.cbuf_index == existing.cbuf_index && desc.cbuf_offset == existing.cbuf_offset && desc.count == existing.count && - desc.size_shift == existing.size_shift; + desc.size_shift == existing.size_shift && + desc.handle_program == existing.handle_program; })}; image_buffer_descriptors[index].is_written |= desc.is_written; image_buffer_descriptors[index].is_read |= desc.is_read; @@ -1036,7 +1051,8 @@ public: desc.secondary_cbuf_index == existing.secondary_cbuf_index && desc.secondary_cbuf_offset == existing.secondary_cbuf_offset && desc.secondary_shift_left == existing.secondary_shift_left && - desc.count == existing.count && desc.size_shift == existing.size_shift; + desc.count == existing.count && desc.size_shift == existing.size_shift && + desc.handle_program == existing.handle_program; })}; // TODO: Read this from TIC texture_descriptors[index].is_multisample |= desc.is_multisample; @@ -1049,7 +1065,8 @@ public: return desc.type == existing.type && desc.format == existing.format && desc.cbuf_index == existing.cbuf_index && desc.cbuf_offset == existing.cbuf_offset && desc.count == existing.count && - desc.size_shift == existing.size_shift; + desc.size_shift == existing.size_shift && + desc.handle_program == existing.handle_program; })}; image_descriptors[index].is_written |= desc.is_written; image_descriptors[index].is_read |= desc.is_read; @@ -1171,6 +1188,10 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo u32 last_cbuf_offset{}; bool last_is_integer{false}; const auto is_texture_pixel_format_integer{[&](const ConstBufferAddr& cbuf_addr) { + if (cbuf_addr.handle_program != ~0U) { + // Every handle program is at cbuf 0 offset 0: the cache below can't tell them apart. + return IsTexturePixelFormatIntegerCached(env, cbuf_addr); + } if (has_last_is_integer && last_cbuf_index == cbuf_addr.index && last_cbuf_offset == cbuf_addr.offset) { return last_is_integer; @@ -1181,6 +1202,18 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo has_last_is_integer = true; return last_is_integer; }}; + // A handle program has no descriptor to read a type from, and ImageQueryDimensions carries + // none: it takes the type of another instruction using the same handle. + std::unordered_map<u32, TextureType> handle_program_types; + for (const TextureInst& texture_inst : to_replace) { + const IR::Opcode opcode{IndexedInstruction(*texture_inst.inst)}; + if (texture_inst.cbuf.handle_program != ~0U && opcode != IR::Opcode::ImageQueryDimensions && + !IsStorageImageOpcode(opcode)) { + handle_program_types.try_emplace( + texture_inst.cbuf.handle_program, + texture_inst.inst->Flags<IR::TextureInstInfo>().type.Value()); + } + } for (TextureInst& texture_inst : to_replace) { // TODO: Handle arrays IR::Inst* const inst{texture_inst.inst}; @@ -1191,7 +1224,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo bool is_multisample{false}; switch (inst->GetOpcode()) { case IR::Opcode::ImageQueryDimensions: - flags.type.Assign(ReadTextureTypeCached(env, cbuf)); + if (const auto it{handle_program_types.find(cbuf.handle_program)}; + it != handle_program_types.end()) { + flags.type.Assign(it->second); + } else { + flags.type.Assign(ReadTextureTypeCached(env, cbuf)); + } inst->SetFlags(flags); break; case IR::Opcode::ImageSampleImplicitLod: @@ -1256,6 +1294,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo .cbuf_offset = cbuf.offset, .count = count, .size_shift = size_shift, + .handle_program = cbuf.handle_program, }); } else { index = descriptors.Add(ImageDescriptor{ @@ -1268,6 +1307,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo .cbuf_offset = cbuf.offset, .count = count, .size_shift = size_shift, + .handle_program = cbuf.handle_program, }); } break; @@ -1284,6 +1324,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo .secondary_shift_left = cbuf.secondary_shift_left, .count = count, .size_shift = size_shift, + .handle_program = cbuf.handle_program, }); } else { count = std::min(count, sampled_dynamic_cap); diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h index eeb8d768d7..e2b341b0c3 100644 --- a/src/shader_recompiler/shader_info.h +++ b/src/shader_recompiler/shader_info.h @@ -189,6 +189,9 @@ struct TextureBufferDescriptor { u32 secondary_shift_left; u32 count; u32 size_shift; + /// Index in Info::handle_programs when the handle is computed at draw time (read from a + /// storage buffer), ~0 when it is the constant buffer word. + u32 handle_program{~0U}; auto operator<=>(const TextureBufferDescriptor&) const = default; }; @@ -203,6 +206,9 @@ struct ImageBufferDescriptor { u32 cbuf_offset; u32 count; u32 size_shift; + /// Index in Info::handle_programs when the handle is computed at draw time (read from a + /// storage buffer), ~0 when it is the constant buffer word. + u32 handle_program{~0U}; auto operator<=>(const ImageBufferDescriptor&) const = default; }; @@ -240,6 +246,9 @@ struct ImageDescriptor { u32 cbuf_offset; u32 count; u32 size_shift; + /// Index in Info::handle_programs when the handle is computed at draw time (read from a + /// storage buffer), ~0 when it is the constant buffer word. + u32 handle_program{~0U}; auto operator<=>(const ImageDescriptor&) const = default; }; diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a9be2ec7a8..1eb1040e34 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -9,19 +9,26 @@ add_executable(tests common/cityhash.cpp common/container_hash.cpp common/fibers.cpp + common/game_presets.cpp common/host_memory.cpp common/param_package.cpp common/range_map.cpp common/ring_buffer.cpp common/scratch_buffer.cpp + common/slot_vector.cpp common/undefined_fix.cpp common/unique_function.cpp core/core_timing.cpp core/nup_writer.cpp + core/priority_queue.cpp core/internal_network/network.cpp video_core/memory_tracker.cpp video_core/vertex_repack.cpp video_core/ampere_returns.cpp + video_core/native_macro_fifo.cpp + video_core/uniform_stream_reuse.cpp + video_core/shader_invalidation.cpp + video_core/descriptor_table.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/common/game_presets.cpp b/src/tests/common/game_presets.cpp new file mode 100644 index 0000000000..481f5c5e1d --- /dev/null +++ b/src/tests/common/game_presets.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <memory> +#include <catch2/catch_test_macros.hpp> +#include "common/game_presets.h" + +namespace { +void SetConflictingPreferences(Settings::Values& settings, bool per_game) { + const auto set = [per_game](auto& setting, auto value) { + setting.SetGlobal(!per_game); + setting.SetValue(value); + }; + set(settings.renderer_backend, Settings::RendererBackend::Null); + set(settings.resolution_setup, Settings::ResolutionSetup::Res2X); + set(settings.gpu_accuracy, Settings::GpuAccuracy::High); + set(settings.use_asynchronous_shaders, false); + set(settings.astc_recompression, Settings::AstcRecompression::Uncompressed); + set(settings.vram_usage_mode, Settings::VramUsageMode::Aggressive); + set(settings.use_reactive_flushing, true); + set(settings.vsync_mode, Settings::VSyncMode::Immediate); + set(settings.scaling_filter, Settings::ScalingFilter::NearestNeighbor); + set(settings.anti_aliasing, Settings::AntiAliasing::Smaa); +} + +void CheckPreset(const Settings::Values& settings) { + CHECK(settings.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan); + CHECK(settings.resolution_setup.GetValue() == Settings::ResolutionSetup::Res1X); + CHECK(settings.gpu_accuracy.GetValue() == Settings::GpuAccuracy::Low); + CHECK(settings.use_asynchronous_shaders.GetValue()); + CHECK(settings.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3); + CHECK(settings.vram_usage_mode.GetValue() == Settings::VramUsageMode::Conservative); + CHECK_FALSE(settings.use_reactive_flushing.GetValue()); + CHECK(settings.vsync_mode.GetValue() == Settings::VSyncMode::FifoRelaxed); + CHECK(settings.use_docked_mode.GetValue() == Settings::ConsoleMode::Handheld); + CHECK(settings.scaling_filter.GetValue() == Settings::ScalingFilter::ScaleForce); + CHECK(settings.anti_aliasing.GetValue() == Settings::AntiAliasing::Fxaa); +} + +void CheckPreferences(const Settings::Values& settings, bool global = false) { + CHECK(settings.renderer_backend.GetValue(global) == Settings::RendererBackend::Null); + CHECK(settings.resolution_setup.GetValue(global) == Settings::ResolutionSetup::Res2X); + CHECK(settings.gpu_accuracy.GetValue(global) == Settings::GpuAccuracy::High); + CHECK_FALSE(settings.use_asynchronous_shaders.GetValue(global)); + CHECK(settings.astc_recompression.GetValue(global) == + Settings::AstcRecompression::Uncompressed); + CHECK(settings.vram_usage_mode.GetValue(global) == Settings::VramUsageMode::Aggressive); + CHECK(settings.use_reactive_flushing.GetValue(global)); + CHECK(settings.vsync_mode.GetValue(global) == Settings::VSyncMode::Immediate); + CHECK(settings.scaling_filter.GetValue(global) == Settings::ScalingFilter::NearestNeighbor); + CHECK(settings.anti_aliasing.GetValue(global) == Settings::AntiAliasing::Smaa); +} +} // namespace + +TEST_CASE("Display filter defaults use ScaleForce and FXAA", "[game_presets]") { + auto settings = std::make_unique<Settings::Values>(); + CHECK(settings->scaling_filter.GetDefault() == Settings::ScalingFilter::ScaleForce); + CHECK(settings->anti_aliasing.GetDefault() == Settings::AntiAliasing::Fxaa); +} + +TEST_CASE("Accepted MKW settings replace visible global preferences", "[game_presets]") { + auto settings = std::make_unique<Settings::Values>(); + SetConflictingPreferences(*settings, false); + Settings::SetMarioKartWorldRecommendedSettings(*settings, true); + CheckPreset(*settings); + CHECK(settings->renderer_backend.UsingGlobal()); + CHECK(settings->gpu_accuracy.UsingGlobal()); + CHECK(settings->renderer_backend.GetValue(true) == Settings::RendererBackend::Vulkan); + CHECK(settings->gpu_accuracy.GetValue(true) == Settings::GpuAccuracy::Low); +} + +TEST_CASE("MKW preset overrides saved settings for the session", "[game_presets]") { + auto settings = std::make_unique<Settings::Values>(); + SetConflictingPreferences(*settings, false); + SetConflictingPreferences(*settings, true); + REQUIRE( + Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true)); + CheckPreset(*settings); + CheckPreferences(*settings, true); + for (const auto& restore : settings->linkage.restore_functions) { + restore(); + } + CheckPreferences(*settings); + CHECK(settings->renderer_backend.UsingGlobal()); + CHECK(settings->resolution_setup.UsingGlobal()); + REQUIRE( + Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true)); + CheckPreset(*settings); +} + +TEST_CASE("MKW preset does not change unrelated games", "[game_presets]") { + auto settings = std::make_unique<Settings::Values>(); + SetConflictingPreferences(*settings, false); + CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, 0, true)); + CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, + Settings::MarioKartWorldProgramId + 1, true)); + CheckPreferences(*settings); + CHECK(settings->renderer_backend.UsingGlobal()); + CHECK(settings->resolution_setup.UsingGlobal()); +} + +TEST_CASE("Declining the MKW preset preserves global and per-game preferences", "[game_presets]") { + auto settings = std::make_unique<Settings::Values>(); + for (bool per_game : {false, true}) { + SetConflictingPreferences(*settings, per_game); + CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, + Settings::MarioKartWorldProgramId, false)); + CheckPreferences(*settings); + CHECK(settings->renderer_backend.UsingGlobal() == !per_game); + CHECK(settings->resolution_setup.UsingGlobal() == !per_game); + } +} diff --git a/src/tests/common/slot_vector.cpp b/src/tests/common/slot_vector.cpp new file mode 100644 index 0000000000..d8f242085c --- /dev/null +++ b/src/tests/common/slot_vector.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <map> +#include <random> +#include <vector> +#include <catch2/catch_test_macros.hpp> +#include "common/slot_vector.h" + +TEST_CASE("SlotVector visits live entries across empty bitmap words", "[common]") { + Common::SlotVector<u32> slots; + std::vector<Common::SlotId> ids; + for (u32 i = 0; i < 513; ++i) { + ids.push_back(slots.insert(i)); + } + for (u32 i = 0; i < 512; ++i) { + slots.erase(ids[i]); + } + auto it = slots.begin(); + REQUIRE(it != slots.end()); + REQUIRE((*it).first == ids[512]); + REQUIRE(*(*it).second == 512); + REQUIRE(++it == slots.end()); + slots.erase(ids[512]); + REQUIRE(slots.begin() == slots.end()); +} + +TEST_CASE("SlotVector sparse iteration matches a live-entry model", "[common]") { + Common::SlotVector<u32> slots; + std::map<u32, u32> live; + std::mt19937 random{9813}; + for (u32 batch = 0; batch < 200; ++batch) { + for (u32 i = 0; i < 100; ++i) { + if (!live.empty() && (random() % 3) == 0) { + auto selected = live.begin(); + std::advance(selected, random() % live.size()); + slots.erase(Common::SlotId{selected->first}); + live.erase(selected); + } else { + const u32 value = random(); + const auto id = slots.insert(value); + live.emplace(id.Value(), value); + } + } + auto expected = live.begin(); + for (const auto [id, value] : slots) { + REQUIRE(expected != live.end()); + REQUIRE(id.Value() == expected->first); + REQUIRE(*value == expected->second); + ++expected; + } + REQUIRE(expected == live.end()); + } +} diff --git a/src/tests/core/core_timing.cpp b/src/tests/core/core_timing.cpp index 81898a1d38..6b424cc52c 100644 --- a/src/tests/core/core_timing.cpp +++ b/src/tests/core/core_timing.cpp @@ -33,6 +33,9 @@ std::optional<std::chrono::nanoseconds> HostCallbackTemplate(s64 time, struct ScopeInit final { ScopeInit() { + callbacks_ran_flags.reset(); + delays.fill(0); + expected_callback = 0; core_timing.SetMulticore(true); core_timing.Initialize([]() {}); } diff --git a/src/tests/core/nup_writer.cpp b/src/tests/core/nup_writer.cpp index c0230d8694..3167a28baf 100644 --- a/src/tests/core/nup_writer.cpp +++ b/src/tests/core/nup_writer.cpp @@ -19,6 +19,7 @@ #include "core/file_sys/nup_container.h" #include "core/file_sys/nup_writer.h" #include "core/file_sys/vfs/vfs_real.h" +#include "core/file_sys/vfs/vfs_vector.h" namespace { @@ -173,6 +174,23 @@ void MakeDump(const fs::path& root) { Put(root / "dlc/0400ABCD00011001/0/romfs/addon.bin", "addon data"); } +void SetLe(std::string& buf, size_t at, u64 value, size_t bytes) { + for (size_t i = 0; i < bytes; ++i) { + buf[at + i] = static_cast<char>(value >> (8 * i)); + } +} + +// Applies `patch` to a valid archive, fixes up the TOC hash and opens the result. +template <typename Patch> +Loader::ResultStatus StatusAfterPatch(std::string nup, Patch&& patch) { + const u64 toc = Le(nup, 0x08, 8); + patch(nup, toc); + const auto hash = Sha256(std::string_view{nup}.substr(toc, Le(nup, 0x10, 8))); + std::copy(hash.begin(), hash.end(), nup.begin() + 0x18); + const auto data = std::vector<u8>(nup.begin(), nup.end()); + return NupContainer{std::make_shared<FileSys::VectorVfsFile>(data, "bad.nup")}.GetStatus(); +} + } // namespace TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContainer", "[nup]") { @@ -197,9 +215,11 @@ TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContain REQUIRE(packages[0].kind == NupContainer::PackageKind::Base); REQUIRE(packages[0].title_id == TitleId); REQUIRE(packages[0].version == 0); + REQUIRE(packages[0].display_version == "1.0.0"); REQUIRE(packages[0].npdm.size() == 0x100); REQUIRE(packages[1].kind == NupContainer::PackageKind::Update); REQUIRE(packages[1].version == 65536); + REQUIRE(packages[1].display_version == "1.1.0"); REQUIRE(packages[1].title_id == TitleId); REQUIRE_FALSE(packages[1].standalone_update); REQUIRE(packages[2].kind == NupContainer::PackageKind::Update); @@ -253,6 +273,102 @@ TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContain REQUIRE_FALSE(by_name.contains(".listing")); } +TEST_CASE("NUP reads zstd members above the memory cache through a temporary file", "[nup]") { + TempDir t; + const auto root = t.path / "Game"; + MakeDump(root); + std::string large(300 * 1024, '\0'); + u32 x = 7; + for (auto& c : large) { + x = x * 1664525 + 1013904223; + c = static_cast<char>('a' + (x >> 28)); + } + Put(root / "Base/RomFS/large.bin", large); + const auto archive = t.path / "Game.nup"; + FileSys::Nup::PackOptions options{.raw_threshold = 1024 * 1024}; + REQUIRE(FileSys::Nup::PackDump(root, archive, options) == FileSys::Nup::PackResult::Success); + for (const auto& file : RawFiles(ReadAll(archive))) { + if (file.name == "large.bin") { + REQUIRE(file.codec == 1); + } + } + + const auto spill_dir = t.path / "spill"; + FileSys::RealVfsFilesystem real; + { + // Members above 32 KiB do not fit the 64 KiB cache budget and are spilled to disk. + NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read), 64 * 1024, + spill_dir}; + REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success); + const auto file = nup.GetRomFSDir(0)->GetFile("large.bin"); + REQUIRE(file != nullptr); + REQUIRE(file->GetSize() == large.size()); + + std::string chunk(4096, '\0'); + for (size_t offset = 0; offset < large.size(); offset += chunk.size()) { + const size_t count = + file->Read(reinterpret_cast<u8*>(chunk.data()), chunk.size(), offset); + REQUIRE(count == std::min(chunk.size(), large.size() - offset)); + REQUIRE(chunk.compare(0, count, large, offset, count) == 0); + } + for (const size_t offset : {size_t{0}, size_t{123456}, size_t{77}, large.size() - 5}) { + const size_t count = file->Read(reinterpret_cast<u8*>(chunk.data()), 100, offset); + REQUIRE(count == std::min<size_t>(100, large.size() - offset)); + REQUIRE(chunk.compare(0, count, large, offset, count) == 0); + } + REQUIRE(file->Read(reinterpret_cast<u8*>(chunk.data()), 10, large.size()) == 0); + + REQUIRE(std::distance(fs::directory_iterator(spill_dir), fs::directory_iterator{}) > 0); + REQUIRE(PackedTree(nup.GetRomFSDir(0)) == SourceTree(root / "Base/RomFS")); + REQUIRE(nup.VerifyIntegrity({}) == Loader::ResultStatus::Success); + } + REQUIRE(std::distance(fs::directory_iterator(spill_dir), fs::directory_iterator{}) == 0); +} + +TEST_CASE("NUP rejects a TOC whose counts or offsets wrap", "[nup]") { + TempDir t; + const auto root = t.path / "Game"; + MakeDump(root); + const auto archive = t.path / "Game.nup"; + REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success); + const auto nup = ReadAll(archive); + const auto no_patch = [](std::string&, u64) {}; + REQUIRE(StatusAfterPatch(nup, no_patch) == Loader::ResultStatus::Success); + + // 2^61 directories wrap dir_count * 0x18 back to the real record size. + REQUIRE(StatusAfterPatch(nup, [](std::string& buf, u64 toc) { + SetLe(buf, toc + 0x08, Le(buf, toc + 0x08, 8) + (1ULL << 61), 8); + }) == Loader::ResultStatus::ErrorNUPBadFormat); + + const auto first_dir = [](const std::string& buf, u64 toc) { + return toc + 0x30 + Le(buf, toc + 0x04, 4) * 0x40; + }; + // first_child + child_count wraps to a small value. + REQUIRE(StatusAfterPatch(nup, [&](std::string& buf, u64 toc) { + SetLe(buf, first_dir(buf, toc) + 0x08, 0xFFFFFFFF, 4); + SetLe(buf, first_dir(buf, toc) + 0x0C, 2, 4); + }) == Loader::ResultStatus::ErrorNUPBadFormat); +} + +TEST_CASE("NUP rejects a directory that contains its own ancestor", "[nup]") { + TempDir t; + const auto root = t.path / "Game"; + MakeDump(root); + const auto archive = t.path / "Game.nup"; + REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success); + + // Give the base RomFS directory the same children as the package root, which lists it. + REQUIRE(StatusAfterPatch(ReadAll(archive), [](std::string& buf, u64 toc) { + const u64 dirs = toc + 0x30 + Le(buf, toc + 0x04, 4) * 0x40; + const u64 root_dir = Le(buf, toc + 0x30 + 0x08, 4); + const u64 romfs_dir = Le(buf, toc + 0x30 + 0x10, 4); + const u64 root_record = dirs + root_dir * 0x18; + const u64 romfs_record = dirs + romfs_dir * 0x18; + SetLe(buf, romfs_record + 0x08, Le(buf, root_record + 0x08, 4), 4); + SetLe(buf, romfs_record + 0x0C, Le(buf, root_record + 0x0C, 4), 4); + }) == Loader::ResultStatus::ErrorNUPBadFormat); +} + TEST_CASE("NUP marks updates standalone when the dump has no base", "[nup]") { TempDir t; const auto root = t.path / "Game"; @@ -388,7 +504,8 @@ TEST_CASE("NUP output is reproducible", "[nup]") { for (const u8 b : Sha256(ReadAll(archive))) { hex += fmt::format("{:02x}", b); } - REQUIRE(hex == "df8446903ab2c39a51af0304935eb4aa5d8ead105440976ceae7ab97133f6143"); + // The mixed-case Base/RomFS directory supplies base version 1.0.0 on every platform. + REQUIRE(hex == "718231e0974ca04c7be7bd6ed4334015b777c6e5f1ed51de23f899f81ee004ab"); } TEST_CASE("NUP writes metadata overrides to the base and the newest update", "[nup]") { diff --git a/src/tests/core/priority_queue.cpp b/src/tests/core/priority_queue.cpp new file mode 100644 index 0000000000..0d7174f793 --- /dev/null +++ b/src/tests/core/priority_queue.cpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <array> +#include <random> +#include <vector> +#include <catch2/catch_test_macros.hpp> +#include "core/hle/kernel/k_priority_queue.h" + +namespace { +struct Affinity { + u64 GetAffinityMask() const { + return 3; + } + void SetAffinityMask(u64) {} + bool GetAffinity(s32) const { + return true; + } + void SetAffinity(s32, bool) {} + void SetAll() {} +}; + +struct Member { + struct QueueEntry { + Member* prev{}; + Member* next{}; + void Initialize() { + prev = next = nullptr; + } + void SetPrev(Member* value) { + prev = value; + } + void SetNext(Member* value) { + next = value; + } + Member* GetNext() const { + return next; + } + Member* GetPrev() const { + return prev; + } + }; + std::array<QueueEntry, 2> entries{}; + s32 priority{}; + QueueEntry& GetPriorityQueueEntry(s32 core) { + return entries[core]; + } + const QueueEntry& GetPriorityQueueEntry(s32 core) const { + return entries[core]; + } + Affinity GetAffinityMask() const { + return {}; + } + s32 GetActiveCore() const { + return 0; + } + s32 GetPriority() const { + return priority; + } + bool IsDummyThread() const { + return false; + } +}; + +template <size_t Priorities> +void CheckQueue() { + using Queue = Kernel::KPriorityQueue<Member, 2, Priorities - 1, 0>; + typename Queue::KPriorityQueueImpl queue; + std::array<Member, 160> members{}; + std::array<std::array<bool, 2>, 160> present{}; + std::array<std::array<std::vector<Member*>, Priorities>, 2> model{}; + std::mt19937 random{6152}; + for (size_t i = 0; i < members.size(); ++i) { + members[i].priority = static_cast<s32>(i % Priorities); + } + REQUIRE(queue.GetFront(0) == nullptr); + for (u32 operation = 0; operation < 3000; ++operation) { + const size_t index = random() % members.size(); + const s32 core = random() % 2; + auto& member = members[index]; + auto& expected = model[core][member.priority]; + if (present[index][core]) { + queue.Remove(member.priority, core, &member); + std::erase(expected, &member); + } else if ((random() & 1) != 0) { + queue.PushFront(member.priority, core, &member); + expected.insert(expected.begin(), &member); + } else { + queue.PushBack(member.priority, core, &member); + expected.push_back(&member); + } + present[index][core] = !present[index][core]; + for (s32 checked_core = 0; checked_core < 2; ++checked_core) { + Member* actual = queue.GetFront(checked_core); + for (const auto& priority : model[checked_core]) { + for (Member* wanted : priority) { + REQUIRE(actual == wanted); + actual = queue.GetNext(checked_core, actual); + } + } + REQUIRE(actual == nullptr); + } + } +} +} // namespace + +TEST_CASE("Priority queue bitmap traversal preserves thread order", "[core]") { + CheckQueue<64>(); + CheckQueue<128>(); +} diff --git a/src/tests/input_common/calibration_configuration_job.cpp b/src/tests/input_common/calibration_configuration_job.cpp index 6ded0dfc86..c45ef157a5 100644 --- a/src/tests/input_common/calibration_configuration_job.cpp +++ b/src/tests/input_common/calibration_configuration_job.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <array> +#include <memory> #include <string> #include <thread> #include <utility> @@ -19,7 +20,7 @@ class FakeCemuhookServer { public: FakeCemuhookServer() - : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {} + : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::address_v4::loopback(), 0)) {} ~FakeCemuhookServer() { boost::system::error_code error_code; @@ -132,3 +133,20 @@ TEST_CASE("CalibrationConfigurationJob completed", "[input_common]") { REQUIRE(max_x == 200); REQUIRE(max_y == 200); } + +TEST_CASE("CalibrationConfigurationJob cancellation joins its worker", "[input_common]") { + FakeCemuhookServer server; + for (unsigned i = 0; i < 32; ++i) { + auto lifetime = std::make_shared<int>(0); + std::weak_ptr<int> observer = lifetime; + { + InputCommon::CemuhookUDP::CalibrationConfigurationJob job( + server.GetHost(), server.GetPort(), + [lifetime](InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status) {}, + [](u16, u16, u16, u16) {}); + lifetime.reset(); + job.Stop(); + } + REQUIRE(observer.expired()); + } +} diff --git a/src/tests/video_core/descriptor_table.cpp b/src/tests/video_core/descriptor_table.cpp new file mode 100644 index 0000000000..8b20564949 --- /dev/null +++ b/src/tests/video_core/descriptor_table.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <chrono> +#include <cstring> +#include <iostream> +#include <limits> +#include <random> +#include <catch2/catch_test_macros.hpp> +#include "video_core/texture_cache/descriptor_table.h" + +namespace { +struct DescriptorMemory { + std::array<std::array<u64, 128>, 2> banks{}; + + void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const { + const auto bank = (address - 0x1000) / 0x1000; + const auto offset = (address - 0x1000) % 0x1000; + std::memcpy(output, reinterpret_cast<const u8*>(banks[bank].data()) + offset, size); + } +}; +} // namespace + +TEST_CASE("Descriptor invalidation matches eager refresh across mutations", "[descriptor_table]") { + DescriptorMemory memory; + VideoCommon::DescriptorTable<u64> table; + REQUIRE(table.Synchronize(0x1000, 127)); + std::array<bool, 128> seen{}; + std::array<u64, 128> previous{}; + std::mt19937 random{0x5080}; + unsigned bank{}; + u32 limit = 127; + for (unsigned i = 0; i < 40000; ++i) { + switch (random() % 5) { + case 0: + table.Invalidate(); + seen.fill(false); + break; + case 1: + memory.banks[random() % 2][random() % 128] = random(); + break; + case 2: { + const auto next_bank = random() % 2; + const u32 next_limit = random() % 2 ? 63 : 127; + const bool changed = next_bank != bank || next_limit != limit; + REQUIRE(table.Synchronize(0x1000 + next_bank * 0x1000, next_limit) == changed); + if (changed) { + seen.fill(false); + } + bank = next_bank; + limit = next_limit; + break; + } + default: { + const u32 index = random() % (limit + 1); + const u64 expected = memory.banks[bank][index]; + const bool changed = !seen[index] || previous[index] != expected; + const auto actual = table.Read(memory, index); + REQUIRE(actual.first == expected); + REQUIRE(actual.second == changed); + seen[index] = true; + previous[index] = expected; + } + } + } +} + +TEST_CASE("Repeated image deletion cannot reuse stale descriptor view IDs", "[descriptor_table]") { + DescriptorMemory memory; + std::array<VideoCommon::DescriptorTable<u64>, 5> channels; + std::array<std::array<unsigned, 128>, 5> view_ids{}; + unsigned live_generation = 1; + const auto visit = [&](size_t channel, u32 index) { + if (channels[channel].Read(memory, index).second) { + view_ids[channel][index] = live_generation; + } + return view_ids[channel][index]; + }; + for (size_t channel = 0; channel < channels.size(); ++channel) { + REQUIRE(channels[channel].Synchronize(0x1000, 127)); + for (u32 index = 0; index < 128; ++index) { + REQUIRE(visit(channel, index) == live_generation); + } + } + for (unsigned generation = 0; generation < 32; ++generation) { + ++live_generation; + for (unsigned deletion = 0; deletion < 50; ++deletion) { + for (auto& table : channels) { + table.Invalidate(); + } + } + for (size_t channel = 0; channel < channels.size(); ++channel) { + for (u32 index = 0; index < 128; ++index) { + REQUIRE(visit(channel, index) == live_generation); + REQUIRE_FALSE(channels[channel].Read(memory, index).second); + } + } + } +} + +TEST_CASE("Descriptor limits clear exposed slots after shrink and growth", "[descriptor_table]") { + struct AddressMemory { + void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const { + std::memcpy(output, &address, size); + } + } memory; + constexpr std::array<u32, 9> limits{0, 63, 64, 127, 128, 0x7ffff, 0x80000, 0x80001, 0x100000}; + for (const u32 limit : limits) { + VideoCommon::DescriptorTable<u64> table; + REQUIRE(table.Synchronize(0x1000, limit)); + CHECK(table.Read(memory, limit).second); + CHECK_FALSE(table.Read(memory, limit).second); + table.Refresh(0x1000, 0); + table.Invalidate(); + CHECK(table.Read(memory, 0).second); + CHECK_FALSE(table.Read(memory, 0).second); + table.Refresh(0x1000, limit); + CHECK(table.Read(memory, limit).second); + CHECK_FALSE(table.Read(memory, limit).second); + table.Invalidate(); + table.Invalidate(); + CHECK(table.Read(memory, limit).second); + CHECK_FALSE(table.Read(memory, limit).second); + } + for (const u32 limit : + {std::numeric_limits<u32>::max(), std::numeric_limits<u32>::max() - 0x1000}) { + VideoCommon::DescriptorTable<u64> table; + table.Refresh(0x1000, limit); + CHECK(table.Read(memory, 0).second); + table.Invalidate(); + CHECK(table.Read(memory, 0).second); + CHECK_FALSE(table.Read(memory, 0).second); + } +} + +TEST_CASE("Descriptor invalidation CPU benchmark", "[.][cache_bench]") { + DescriptorMemory memory; + const auto measure = [&](bool deferred) { + std::array<VideoCommon::DescriptorTable<u64>, 5> tables; + for (auto& table : tables) { + REQUIRE(table.Synchronize(0x1000, 127)); + } + void (*volatile invalidate)(VideoCommon::DescriptorTable<u64>&) = + deferred ? +[](VideoCommon::DescriptorTable<u64>& table) { table.Invalidate(); } + : +[](VideoCommon::DescriptorTable<u64>& table) { + std::ranges::fill(table.read_descriptors, 0); + }; + size_t changed{}; + const auto start = std::chrono::steady_clock::now(); + for (unsigned batch = 0; batch < 2000; ++batch) { + for (unsigned deletion = 0; deletion < 50; ++deletion) { + for (auto& table : tables) { + invalidate(table); + } + } + for (auto& table : tables) { + changed += table.Read(memory, 0).second; + } + } + const auto ms = + std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start) + .count(); + REQUIRE(changed == 10000); + return ms; + }; + const double eager = measure(false); + const double deferred = measure(true); + std::cout << "Descriptor bitmap invalidation: eager=" << eager << " ms deferred=" << deferred + << " ms (CPU only, not gameplay FPS)\n"; +} diff --git a/src/tests/video_core/memory_tracker.cpp b/src/tests/video_core/memory_tracker.cpp index c2236848b5..3ac4cee840 100644 --- a/src/tests/video_core/memory_tracker.cpp +++ b/src/tests/video_core/memory_tracker.cpp @@ -30,6 +30,10 @@ class RasterizerInterface { public: void UpdatePagesCachedCount(DAddr addr, size_t size, s32 delta) { ++update_calls; + ApplyPagesCachedCount(addr, size, delta); + } + + void ApplyPagesCachedCount(DAddr addr, size_t size, s32 delta) { calls.emplace_back(addr, size, delta); const u64 page_start{addr >> Core::DEVICE_PAGEBITS}; const u64 page_end{(addr + size + Core::DEVICE_PAGESIZE - 1) >> Core::DEVICE_PAGEBITS}; @@ -45,7 +49,13 @@ public: } void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta) { - // TODO: for now assume fine? + if (ranges.empty()) { + return; + } + ++update_calls; + for (const auto& [addr, size] : ranges) { + ApplyPagesCachedCount(addr, size, delta); + } } [[nodiscard]] size_t UpdateCalls() const noexcept { return update_calls; } @@ -557,20 +567,32 @@ TEST_CASE("MemoryTracker: Cached write downloads") { REQUIRE(rasterizer.Count() == 0); } -TEST_CASE("MemoryTracker: FlushCachedWrites batching") { +TEST_CASE("MemoryTracker: cached writes preserve tracking counts") { RasterizerInterface rasterizer; std::optional<MemoryTracker> memory_track(rasterizer); memory_track->UnmarkRegionAsCpuModified(c, WORD * 2); + REQUIRE(rasterizer.UpdateCalls() == 1); + REQUIRE(rasterizer.UpdateCallsList().size() == 1); + REQUIRE(rasterizer.UpdateCallsList().front() == std::tuple{c, WORD * 2, 1}); + memory_track->CachedCpuWrite(c + PAGE, PAGE); memory_track->CachedCpuWrite(c + PAGE * 2, PAGE); memory_track->CachedCpuWrite(c + PAGE * 4, PAGE); - REQUIRE(rasterizer.UpdateCalls() == 0); + REQUIRE(rasterizer.Count() == 125); + REQUIRE(rasterizer.Count(c + PAGE) == 0); + REQUIRE(rasterizer.Count(c + PAGE * 2) == 0); + REQUIRE(rasterizer.Count(c + PAGE * 3) == 1); + REQUIRE(rasterizer.Count(c + PAGE * 4) == 0); + const auto update_calls = rasterizer.UpdateCalls(); memory_track->FlushCachedWrites(); - // Now we expect a single batch call (coalesced ranges) to the device memory manager - REQUIRE(rasterizer.UpdateCalls() == 1); - const auto& calls = rasterizer.UpdateCallsList(); - REQUIRE(std::get<0>(calls[0]) == c + PAGE); - REQUIRE(std::get<1>(calls[0]) == PAGE * 3); + REQUIRE(rasterizer.UpdateCalls() == update_calls); + REQUIRE(rasterizer.Count() == 125); + REQUIRE(memory_track->IsRegionCpuModified(c + PAGE, PAGE)); + REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 2, PAGE)); + REQUIRE_FALSE(memory_track->IsRegionCpuModified(c + PAGE * 3, PAGE)); + REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 4, PAGE)); + memory_track->MarkRegionAsCpuModified(c, WORD * 2); + REQUIRE(rasterizer.Count() == 0); } TEST_CASE("DeviceMemoryManager: UpdatePagesCachedBatch basic") { diff --git a/src/tests/video_core/native_macro_fifo.cpp b/src/tests/video_core/native_macro_fifo.cpp new file mode 100644 index 0000000000..e19b146192 --- /dev/null +++ b/src/tests/video_core/native_macro_fifo.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <vector> +#include <catch2/catch_test_macros.hpp> + +#ifdef ARCHITECTURE_x86_64 +#include "video_core/macro_turing_jit.h" + +namespace { +std::array<u32, 3> FifoInstruction(u32 method, bool end) { + std::array<u32, 3> bits{}; + const auto field = [&](u32 offset, u32 count, u32 value) { + for (u32 bit = 0; bit < count; ++bit) { + bits[(offset + bit) / 32] |= ((value >> bit) & 1) << ((offset + bit) % 32); + } + }; + field(0, 1, end); + for (u32 lane = 0; lane < 2; ++lane) { + field(15 + lane * 36, 5, 24); + field(20 + lane * 36, 5, 24); + field(25 + lane * 36, 5, 24); + } + field(30, 16, method); + field(82, 3, 5); + field(85, 4, 3); + return {bits[2], bits[1], bits[0]}; +} +} // namespace + +TEST_CASE("Owned native macro FIFOs preserve loads across DMA appends and repeated calls", + "[video_core][native_macro]") { + std::vector<u32> code; + for (const auto instruction : {FifoInstruction(0x158, false), FifoInstruction(0x200, true), + FifoInstruction(0x201, false)}) { + code.insert(code.end(), instruction.begin(), instruction.end()); + } + Tegra::TuringMacroJit jit; + jit.Prepare(code, 1); + std::array<u32, 0xc00> ram{}; + std::vector<u32> parameters; + std::vector<u32> reference; + for (u32 invocation = 0; invocation < 1000; ++invocation) { + const u32 count = 2 + invocation % 127; + parameters.push_back(count); + reference.assign(parameters.begin(), parameters.end()); + const auto run = [&](std::vector<u32>& input) { + std::vector<std::pair<u32, u32>> writes; + const auto result = jit.Execute( + 0, input, ram, [](u32) { return 0U; }, + [&](u32 method, u32 value) { + writes.emplace_back(method, value); + if (method == 0x158) { + for (u32 word = 0; word < value; ++word) { + input.push_back(invocation * 256 + word); + } + } + return true; + }); + CHECK(result.success); + CHECK(result.consumed == 3); + return writes; + }; + const auto expected = run(reference); + const auto actual = run(parameters); + REQUIRE(actual.size() == 3); + CHECK(actual == expected); + CHECK(parameters == reference); + CHECK(actual[1] == std::pair<u32, u32>{0x200, invocation * 256}); + CHECK(actual[2] == std::pair<u32, u32>{0x201, invocation * 256 + 1}); + parameters.clear(); + } +} +#endif diff --git a/src/tests/video_core/shader_invalidation.cpp b/src/tests/video_core/shader_invalidation.cpp new file mode 100644 index 0000000000..806fb5c58d --- /dev/null +++ b/src/tests/video_core/shader_invalidation.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <chrono> +#include <iostream> +#include <memory> +#include <random> +#include <vector> +#include <catch2/catch_test_macros.hpp> +#include "core/device_memory.h" +#include "video_core/shader_cache.h" + +namespace VideoCommon { +struct ShaderCacheTestAccess { + static std::unique_ptr<ShaderCache> Create(Tegra::MaxwellDeviceMemoryManager& memory) { + return std::unique_ptr<ShaderCache>{new ShaderCache{memory}}; + } + static void Register(ShaderCache& cache, VAddr address, size_t size, u64 hash) { + auto data = std::make_unique<ShaderInfo>(); + data->unique_hash = hash; + cache.Register(std::move(data), address, size); + } + static const ShaderInfo* Find(const ShaderCache& cache, VAddr address) { + return cache.TryGet(address); + } +}; +} // namespace VideoCommon + +using VideoCommon::ShaderCacheTestAccess; + +TEST_CASE("Shader batch invalidation matches sequential cache contents", "[shader_invalidation]") { + Core::DeviceMemory memory; + Tegra::MaxwellDeviceMemoryManager device_memory{memory}; + auto batched = ShaderCacheTestAccess::Create(device_memory); + auto sequential = ShaderCacheTestAccess::Create(device_memory); + std::mt19937 random{0x16'67}; + constexpr VAddr base = 0x100000; + for (unsigned iteration = 0; iteration < 600; ++iteration) { + for (unsigned slot = 0; slot < 128; ++slot) { + const auto address = base + slot * 4096; + if (!ShaderCacheTestAccess::Find(*batched, address)) { + const auto size = 1 + random() % 65536; + ShaderCacheTestAccess::Register(*batched, address, size, iteration + slot); + ShaderCacheTestAccess::Register(*sequential, address, size, iteration + slot); + } + } + std::vector<std::pair<DAddr, size_t>> writes; + for (unsigned i = 0, count = random() % 24; i < count; ++i) { + writes.emplace_back(base + random() % (128 * 4096), random() % 32768); + if (i % 4 == 0) { + writes.push_back(writes.back()); + } + } + for (const auto& [address, size] : writes) { + sequential->InvalidateRegion(address, size); + } + batched->InvalidateRegions(writes); + for (unsigned slot = 0; slot < 128; ++slot) { + const auto address = base + slot * 4096; + const auto actual = ShaderCacheTestAccess::Find(*batched, address); + const auto expected = ShaderCacheTestAccess::Find(*sequential, address); + REQUIRE(bool(actual) == bool(expected)); + if (actual) { + REQUIRE(actual->unique_hash == expected->unique_hash); + } + } + } +} + +TEST_CASE("Shader empty batches preserve pending invalidations", "[shader_invalidation]") { + Core::DeviceMemory memory; + Tegra::MaxwellDeviceMemoryManager device_memory{memory}; + auto cache = ShaderCacheTestAccess::Create(device_memory); + constexpr VAddr address = 0x100000; + ShaderCacheTestAccess::Register(*cache, address, 32768, 123); + cache->OnCacheInvalidation(address, 1); + cache->InvalidateRegions({}); + REQUIRE(ShaderCacheTestAccess::Find(*cache, address)); + const std::array unrelated{std::pair<DAddr, size_t>{address + 0x100000, 4096}}; + cache->InvalidateRegions(unrelated); + REQUIRE_FALSE(ShaderCacheTestAccess::Find(*cache, address)); +} + +TEST_CASE("Shader invalidation CPU benchmark", "[.][cache_bench]") { + Core::DeviceMemory memory; + Tegra::MaxwellDeviceMemoryManager device_memory{memory}; + auto batched = ShaderCacheTestAccess::Create(device_memory); + auto sequential = ShaderCacheTestAccess::Create(device_memory); + std::vector<std::pair<DAddr, size_t>> writes; + constexpr VAddr base = 0x100000; + for (unsigned slot = 0; slot < 768; ++slot) { + writes.emplace_back(base + slot * 65536, 1); + } + double sequential_ms{}, batched_ms{}; + for (unsigned repeat = 0; repeat < 20; ++repeat) { + for (unsigned slot = 0; slot < 1024; ++slot) { + const auto address = base + slot * 65536; + if (!ShaderCacheTestAccess::Find(*batched, address)) { + ShaderCacheTestAccess::Register(*batched, address, 64, slot); + ShaderCacheTestAccess::Register(*sequential, address, 64, slot); + } + } + auto start = std::chrono::steady_clock::now(); + for (const auto& [address, size] : writes) { + sequential->InvalidateRegion(address, size); + } + sequential_ms += + std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start) + .count(); + start = std::chrono::steady_clock::now(); + batched->InvalidateRegions(writes); + batched_ms += + std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start) + .count(); + REQUIRE_FALSE(ShaderCacheTestAccess::Find(*batched, base)); + REQUIRE(ShaderCacheTestAccess::Find(*batched, base + 1023 * 65536)); + } + std::cout << "Shader invalidation: sequential=" << sequential_ms << " ms batched=" << batched_ms + << " ms (CPU only, not gameplay FPS)\n"; +} diff --git a/src/tests/video_core/uniform_stream_reuse.cpp b/src/tests/video_core/uniform_stream_reuse.cpp new file mode 100644 index 0000000000..6059b70eeb --- /dev/null +++ b/src/tests/video_core/uniform_stream_reuse.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <array> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <span> +#include <vector> +#include <catch2/catch_test_macros.hpp> + +#include "video_core/buffer_cache/uniform_stream_reuse.h" + +namespace { +void Check(bool condition, const char* message) { + INFO(message); + REQUIRE(condition); +} + +struct Memory { + std::array<u8, 8192> bytes{}; + void ReadBlockUnsafe(u64 address, void* destination, u32 size) { + Check(address + size <= bytes.size(), "read outside guest memory"); + std::memcpy(destination, bytes.data() + address, size); + } +}; + +struct Runtime { + struct StreamUniformRef { + size_t allocation; + }; + std::vector<std::vector<u8>> allocations; + StreamUniformRef last{}, bound{}; + u64 tick{1}; + u64 CurrentTick() const { + return tick; + } + StreamUniformRef LastStreamUniformRef() const { + return last; + } + std::span<u8> BindMappedUniformBuffer(size_t, u32, u32 size) { + allocations.emplace_back(size); + last = bound = StreamUniformRef{allocations.size() - 1}; + return allocations.back(); + } + void RebindStreamUniform(StreamUniformRef ref, u32 size) { + Check(ref.allocation < allocations.size(), "reused a recycled allocation"); + Check(allocations[ref.allocation].size() == size, "reused wrong allocation size"); + bound = ref; + } + void NextTick() { + ++tick; + allocations.clear(); + } +}; + +void CheckBound(const Runtime& runtime, const Memory& memory, u64 address, u32 size) { + const auto& bytes = runtime.allocations.at(runtime.bound.allocation); + Check(bytes.size() == size && + std::memcmp(bytes.data(), memory.bytes.data() + address, size) == 0, + "bound uniform data differs from guest memory"); +} +} // namespace + +TEST_CASE("Uniform stream reuse preserves data through mutation and session resets", + "[video_core]") { + using VideoCommon::BindSmallUniformReusing; + Memory memory; + Runtime runtime; + VideoCommon::ResetUniformStreamReuse<Runtime>(); + auto bind = [&](size_t stage, u32 slot, u64 address, u32 size) { + Check(BindSmallUniformReusing(runtime, memory, stage, slot, slot, address, size), + "valid small uniform rejected"); + CheckBound(runtime, memory, address, size); + }; + bind(0, 0, 0, 64); + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 1, "identical bytes were copied twice"); + memory.bytes[7] = 23; + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 2, "changed bytes reused stale data"); + bind(0, 1, 0, 64); + bind(1, 0, 0, 64); + Check(runtime.allocations.size() == 4, "stage or slot shared the wrong allocation"); + runtime.NextTick(); + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 1, "a new command buffer reused an old allocation"); + VideoCommon::ResetUniformStreamReuse<Runtime>(); + bind(0, 0, 0, 64); + Check(runtime.allocations.size() == 2, "session reset retained an old allocation"); + bind(4, 17, 0, 4096); + const auto before = runtime.allocations.size(); + for (auto [stage, slot, size] : + {std::array<u32, 3>{5, 0, 64}, {0, 18, 64}, {0, 0, 0}, {0, 0, 4097}}) { + Check(!BindSmallUniformReusing(runtime, memory, stage, slot, slot, 0, size), + "invalid uniform range did not use the fallback"); + } + Check(runtime.allocations.size() == before, "fallback allocated stream memory"); + + u32 random = 17; + for (u32 index = 0; index < 10000; ++index) { + random = random * 1664525U + 1013904223U; + if (index % 37 == 0) + runtime.NextTick(); + if (index % 503 == 0) + VideoCommon::ResetUniformStreamReuse<Runtime>(); + const u32 size = 1 + random % 4096; + const u64 address = (random >> 12) % (memory.bytes.size() - 4096); + if (index % 3 == 0) + memory.bytes[address + random % size] ^= u8{0x5a}; + const size_t stage = (random >> 20) % 5; + const u32 slot = (random >> 24) % 18; + bind(stage, slot, address, size); + bind(stage, slot, address, size); + } +} diff --git a/src/tests/video_core/vertex_repack.cpp b/src/tests/video_core/vertex_repack.cpp index 6409be3ca4..5e21e3714c 100644 --- a/src/tests/video_core/vertex_repack.cpp +++ b/src/tests/video_core/vertex_repack.cpp @@ -76,16 +76,22 @@ TEST_CASE("Vertex repacking requires the captured tail and stride", "[vertex_rep REQUIRE(Vulkan::VertexRepackMask(regs) == 0); } -TEST_CASE("Static vertex repacking requires explicit experimental opt-in", "[vertex_repack]") { +TEST_CASE("Static vertex repacking follows the device stride alignment", "[vertex_repack]") { REQUIRE(Vulkan::VertexRepackInputSupported(true, false)); REQUIRE_FALSE(Vulkan::VertexRepackInputSupported(false, false)); REQUIRE(Vulkan::VertexRepackInputSupported(false, true)); + REQUIRE_FALSE(Vulkan::StaticVertexRepackSelected(nullptr, 1)); + REQUIRE(Vulkan::StaticVertexRepackSelected(nullptr, 4)); + REQUIRE(Vulkan::StaticVertexRepackSelected("1", 1)); + REQUIRE_FALSE(Vulkan::StaticVertexRepackSelected("0", 4)); + REQUIRE(Vulkan::StaticVertexRepackSelected("other", 4)); } TEST_CASE("Vertex repack planning keeps descriptor offsets aligned", "[vertex_repack]") { for (const u32 alignment : {4U, 16U, 64U, 256U, 4096U}) { for (const u32 offset : {0U, 1U, 3U, 255U, 4099U}) { - const auto plan = Vulkan::MakeVertexRepackParameters(offset, 70, alignment, 1U << 20, 65535); + const auto plan = Vulkan::MakeVertexRepackParameters(offset, 70, alignment, 1U << 20, + 65535, 1U << 20); REQUIRE(plan.has_value()); REQUIRE(plan->source_base % alignment == 0); REQUIRE(plan->source_base + plan->source_bias == offset); @@ -99,13 +105,22 @@ TEST_CASE("Vertex repack planning keeps descriptor offsets aligned", "[vertex_re } TEST_CASE("Vertex repack planning rejects overflow and device-limit violations", "[vertex_repack]") { - REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0, 256, 1U << 20, 65535)); - REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 0, 1U << 20, 65535)); - REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 71, 65535)); - REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(255, 69, 256, 323, 65535)); - REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 0)); - REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0xffffffffU, 256, ~u64{0}, 0xffffffffU)); - const auto one = Vulkan::MakeVertexRepackParameters(0, 69, 256, 72, 1); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0, 256, 1U << 20, 65535, 1U << 20)); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 0, 1U << 20, 65535, 1U << 20)); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 71, 65535, 1U << 20)); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(255, 69, 256, 323, 65535, 1U << 20)); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 0, 1U << 20)); + REQUIRE_FALSE( + Vulkan::MakeVertexRepackParameters(0, 0xffffffffU, 256, ~u64{0}, 0xffffffffU, ~u64{0})); + const auto one = Vulkan::MakeVertexRepackParameters(0, 69, 256, 72, 1, 72); REQUIRE(one.has_value()); REQUIRE(one->source_range == 72); } + +TEST_CASE("Vertex repack planning keeps the source range inside the buffer", "[vertex_repack]") { + const auto clamped = Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 65535, 69); + REQUIRE(clamped.has_value()); + REQUIRE(clamped->source_range == 69); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 65535, 68)); + REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(300, 69, 256, 1U << 20, 65535, 256)); +} diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index cd176dec27..40369b16ef 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -10,11 +10,13 @@ #include <memory> #include <numeric> #include <stdexcept> +#include <utility> #include <vector> #include <fmt/ranges.h> #include "common/range_sets.inc" #include "video_core/buffer_cache/buffer_cache_base.h" +#include "video_core/buffer_cache/uniform_stream_reuse.h" #include "video_core/guest_memory.h" #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/nx2.h" @@ -28,6 +30,9 @@ using Core::DEVICE_PAGESIZE; template <class P> BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_) : runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} { + // A new runtime has a new scheduler (its ticks restart) and a new stream buffer: whatever the + // stream uniform reuse remembers refers to the old ones. Before the early return below. + VideoCommon::ResetUniformStreamReuse<Runtime>(); // Ensure the first slot is used for the null buffer void(slot_buffers.insert(runtime, NullBufferParams{})); gpu_modified_ranges.Clear(); @@ -180,6 +185,7 @@ void BufferCache<P>::WriteMemory(DAddr device_addr, u64 size) { gpu_modified_ranges.Subtract(device_addr, size); } memory_tracker.MarkRegionAsCpuModified(device_addr, size); + MarkVertexBuffersStale(device_addr, size); } template <class P> @@ -230,8 +236,17 @@ std::optional<VideoCore::RasterizerDownloadArea> BufferCache<P>::GetFlushArea(DA }; area->preemtive = !IsRegionGpuModified(device_addr_start_aligned, device_addr_end_aligned - device_addr_start_aligned); - memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned, - device_addr_end_aligned - device_addr_start_aligned); + // Mark only data that the GPU wrote. A mark stays for the session and sends later GPU writes + // in the range to a download at each fence. + if (!area->preemtive) { + memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned); + } else { + // The core keeps this area and has no mark. The next GPU write here must arm the range + // again, which ends the area and sets the trap. + read_trapped_ranges.Subtract(device_addr_start_aligned, + device_addr_end_aligned - device_addr_start_aligned); + } return area; } @@ -265,6 +280,7 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am } ClearDownload(*cpu_dest_address, amount); + MarkVertexBuffersStale(*cpu_dest_address, amount); BufferId buffer_a; BufferId buffer_b; @@ -408,7 +424,8 @@ void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr template <class P> void BufferCache<P>::DisableGraphicsUniformBuffer(size_t stage, u32 index) { auto& current = channel_state->uniform_buffers[stage][index]; - if (current.device_addr == 0 && current.size == 0 && !current.buffer_id) [[likely]] { + if (current.device_addr == 0 && current.size == 0 && current.buffer_id == NULL_BUFFER_ID) + [[likely]] { return; } current = NULL_BINDING; @@ -893,7 +910,39 @@ void BufferCache<P>::UpdateVertexBufferSlot(u32 index, const Binding& binding) { } template <class P> +void BufferCache<P>::MarkVertexBuffersStale(DAddr device_addr, u64 size) { + if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) { + u32 mask = enabled_vertex_buffers_mask; + while (mask != 0) { + const u32 index = std::countr_zero(mask); + mask &= mask - 1; + const Binding& binding = v_buffer[index]; + if (device_addr < binding.device_addr + binding.size && + binding.device_addr < device_addr + size) { + stale_vertex_buffers_mask |= 1U << index; + } + } + } +} + +template <class P> +void BufferCache<P>::RefreshRepackedVertexBuffers() { + if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) { + u32 mask = std::exchange(stale_vertex_buffers_mask, 0) & enabled_vertex_buffers_mask; + auto& flags = maxwell3d->dirty.flags; + while (mask != 0) { + const u32 index = std::countr_zero(mask); + mask &= mask - 1; + if (runtime.IsVertexRepacked(index)) { + flags[Dirty::VertexBuffer0 + index] = true; + } + } + } +} + +template <class P> void BufferCache<P>::BindHostVertexBuffers() { + RefreshRepackedVertexBuffers(); #ifdef __ANDROID__ const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue(); @@ -1078,8 +1127,20 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index; channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size; // Stream buffer path to avoid stalling on non-Nvidia drivers or Vulkan - const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size); - device_memory.ReadBlockUnsafe(device_addr, span.data(), size); + bool streamed = false; + if constexpr (!IS_OPENGL && VideoCommon::StreamUniformReusable<Runtime>) { + // The same bytes as this slot's previous bind, in the same command buffer: bind that + // stream allocation again instead of allocating and copying (uniform_stream_reuse.h). + // EDEN_NX2_UBOREUSE=0 turns it off. + if (VideoCommon::UniformStreamReuseEnabled()) { + streamed = VideoCommon::BindSmallUniformReusing( + runtime, device_memory, stage, index, binding_index, device_addr, size); + } + } + if (!streamed) { + const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size); + device_memory.ReadBlockUnsafe(device_addr, span.data(), size); + } return; } // Classic cached path @@ -1655,6 +1716,7 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3 } } memory_tracker.MarkRegionAsGpuModified(device_addr, size); + MarkVertexBuffersStale(device_addr, size); gpu_modified_ranges.Add(device_addr, size); if (DownloadsAtFence(device_addr, size)) { uncommitted_gpu_modified_ranges.Add(device_addr, size); @@ -2205,6 +2267,7 @@ void BufferCache<P>::InlineMemoryImplementation(DAddr dest_address, size_t copy_ std::span<const u8> inlined_buffer) { ClearDownload(dest_address, copy_size); gpu_modified_ranges.Subtract(dest_address, copy_size); + MarkVertexBuffersStale(dest_address, copy_size); BufferId buffer_id = FindBuffer(dest_address, static_cast<u32>(copy_size), false); auto& buffer = slot_buffers[buffer_id]; diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 1f0c6cf10f..86c05c8f22 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -59,7 +59,8 @@ constexpr u32 NUM_VERTEX_BUFFERS = 32; constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4; constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18; constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8; -constexpr u32 NUM_STORAGE_BUFFERS = 16; +// Native compute shaders bind up to 18 global memory ranges; the enable masks are 32 bits wide. +constexpr u32 NUM_STORAGE_BUFFERS = 32; constexpr u32 NUM_TEXTURE_BUFFERS = 32; constexpr u32 NUM_STAGES = 5; @@ -525,6 +526,13 @@ private: void UpdateVertexBufferSlot(u32 index, const Binding& binding); + /// Flags the vertex buffers overlapping a written range so host copies derived from them are + /// rebuilt. + void MarkVertexBuffersStale(DAddr device_addr, u64 size); + + /// Re-dirties repacked vertex buffers whose source data was written since they were bound. + void RefreshRepackedVertexBuffers(); + void ClearDownload(DAddr base_addr, u64 size); void InlineMemoryImplementation(DAddr dest_address, size_t copy_size, @@ -545,6 +553,7 @@ private: u32 last_index_count = 0; u32 enabled_vertex_buffers_mask = 0; + u32 stale_vertex_buffers_mask = 0; u64 vertex_buffers_serial = 0; std::array<Binding, 32> v_buffer{}; diff --git a/src/video_core/buffer_cache/uniform_stream_reuse.h b/src/video_core/buffer_cache/uniform_stream_reuse.h new file mode 100644 index 0000000000..7ba31c4717 --- /dev/null +++ b/src/video_core/buffer_cache/uniform_stream_reuse.h @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <concepts> +#include <cstddef> +#include <cstdlib> +#include <cstring> +#include <span> +#include <type_traits> + +#include "common/common_types.h" + +namespace VideoCommon { + +/// What a buffer cache runtime offers for stream uniform reuse. +/// - StreamUniformRef: a small trivially copyable description of one stream allocation (the +/// host buffer, its offset and whatever else the runtime needs to add it to a descriptor +/// payload again, e.g. the device address of the descriptor buffer path). +/// - LastStreamUniformRef(): the allocation made by the latest BindMappedUniformBuffer call. +/// - RebindStreamUniform(ref, size): adds `ref` to the descriptor payload exactly as +/// BindMappedUniformBuffer added the allocation the first time. +template <class R> +concept StreamUniformReusable = requires(R& runtime, u32 size) { + typename R::StreamUniformRef; + requires std::is_trivially_copyable_v<typename R::StreamUniformRef>; + { runtime.LastStreamUniformRef() } -> std::same_as<typename R::StreamUniformRef>; + runtime.RebindStreamUniform(runtime.LastStreamUniformRef(), size); + { runtime.CurrentTick() } -> std::convertible_to<u64>; +}; + +/// EDEN_NX2_UBOREUSE=0 turns the reuse off (every small uniform buffer is copied, as before). +[[nodiscard]] inline bool UniformStreamReuseEnabled() noexcept { + static const bool enabled = [] { + const char* const value = std::getenv("EDEN_NX2_UBOREUSE"); + return value == nullptr || value[0] != '0'; + }(); + return enabled; +} + +template <class Ref> +class UniformStreamReuse { +public: + static constexpr u32 MaxBytes = 4096; // BufferCache's DEFAULT_SKIP_CACHE_SIZE + static constexpr size_t NumStages = 5; + static constexpr size_t NumSlots = 18; // Maxwell constant buffers per stage + + struct Slot { + Ref ref{}; + u64 tick{}; + u32 size{}; // 0: nothing streamed yet + std::array<u8, MaxBytes> bytes{}; + }; + + void Reset() noexcept { + for (auto& stage : slots) { + for (Slot& slot : stage) { + slot.size = 0; + slot.tick = 0; + } + } + } + + std::array<std::array<Slot, NumSlots>, NumStages> slots{}; + std::array<u8, MaxBytes> scratch{}; +}; + +template <class Ref> +[[nodiscard]] inline UniformStreamReuse<Ref>& UniformStreamReuseState() noexcept { + static UniformStreamReuse<Ref> state; + return state; +} + +/// Forgets every slot. Call it when a buffer cache (and so its runtime, scheduler and stream +/// buffer) is created: ticks restart with a new scheduler and the old references are dead. +template <class Runtime> +inline void ResetUniformStreamReuse() noexcept { + if constexpr (StreamUniformReusable<Runtime>) { + UniformStreamReuseState<typename Runtime::StreamUniformRef>().Reset(); + } +} + +/// Binds the small uniform buffer at `device_addr` for (stage, cbuf `index`) through the stream +/// buffer, reusing the slot's previous allocation when the bytes and the command buffer are the +/// same. Returns false (nothing bound) when the buffer does not qualify, so the caller binds it +/// the classic way. +template <class Runtime, class DeviceMemory> +[[nodiscard]] bool BindSmallUniformReusing(Runtime& runtime, DeviceMemory& device_memory, + size_t stage, u32 index, u32 binding_index, + u64 device_addr, u32 size) { + using Reuse = UniformStreamReuse<typename Runtime::StreamUniformRef>; + if (size == 0 || size > Reuse::MaxBytes || stage >= Reuse::NumStages || + index >= Reuse::NumSlots) { + return false; + } + Reuse& reuse = UniformStreamReuseState<typename Runtime::StreamUniformRef>(); + typename Reuse::Slot& slot = reuse.slots[stage][index]; + u8* const scratch = reuse.scratch.data(); + device_memory.ReadBlockUnsafe(device_addr, scratch, size); + + const bool same_bytes = + slot.size == size && std::memcmp(slot.bytes.data(), scratch, size) == 0; + if (same_bytes && slot.tick == runtime.CurrentTick()) { + runtime.RebindStreamUniform(slot.ref, size); + return true; + } + const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size); + std::memcpy(span.data(), scratch, size); + slot.ref = runtime.LastStreamUniformRef(); + // After the allocation: the tick the stream region is tagged with is the one current now. + slot.tick = runtime.CurrentTick(); + if (!same_bytes) { + slot.size = size; + std::memcpy(slot.bytes.data(), scratch, size); + } + return true; +} + +} // namespace VideoCommon diff --git a/src/video_core/control/scheduler.cpp b/src/video_core/control/scheduler.cpp index dc42906752..97d2dcd482 100644 --- a/src/video_core/control/scheduler.cpp +++ b/src/video_core/control/scheduler.cpp @@ -48,6 +48,7 @@ void Scheduler::Push(GPU& gpu, s32 channel, CommandList&& entries) { std::find(pending_channels.begin(), pending_channels.end(), channel_state) == pending_channels.end()) { pending_channels.push_back(channel_state); + any_pending.store(true, std::memory_order_release); } if (!pending_channels.empty()) { ResumePending(gpu); @@ -77,6 +78,7 @@ void Scheduler::ResumePending(GPU& gpu) { std::erase_if(pending_channels, [](const auto& state) { return !state->payload->dma_pusher.HasPendingAcquire(); }); + any_pending.store(!pending_channels.empty(), std::memory_order_release); } void Scheduler::DeclareChannel(std::shared_ptr<ChannelState> new_channel) { diff --git a/src/video_core/control/scheduler.h b/src/video_core/control/scheduler.h index 9a0f1b919f..b1aaf46b60 100644 --- a/src/video_core/control/scheduler.h +++ b/src/video_core/control/scheduler.h @@ -6,6 +6,7 @@ #pragma once +#include <atomic> #include <memory> #include <mutex> #include <vector> @@ -30,6 +31,10 @@ public: [[nodiscard]] bool HasPendingChannels() const { return !pending_channels.empty(); } + /// HasPendingChannels for threads other than the GPU thread. + [[nodiscard]] bool HasPendingChannelsAnyThread() const { + return any_pending.load(std::memory_order_acquire); + } void DeclareChannel(std::shared_ptr<ChannelState> new_channel); @@ -39,6 +44,7 @@ private: ::Common::unordered_map<s32, std::shared_ptr<ChannelState>> channels; std::mutex scheduling_guard; std::vector<std::shared_ptr<ChannelState>> pending_channels; ///< Suspended on an acquire + std::atomic<bool> any_pending{}; }; } // namespace Control diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp index 6b93bb7f60..6a79e0570e 100644 --- a/src/video_core/dma_pusher.cpp +++ b/src/video_core/dma_pusher.cpp @@ -162,6 +162,9 @@ bool DmaPusher::Step() { // read: their words at submit time are an older frame's. const bool submitted = command_resume_offset == 0 && dma_pushbuffer_subindex < command_list.submitted_offsets.size() && + (command_list.snapshot_suspensions & 1) == 0 && + command_list.snapshot_suspensions == + suspensions.load(std::memory_order_acquire) && !IsGpuWrittenCommands(dma_state.dma_get, size_t{header.size} * sizeof(u32)) && !(use_safe && memory_manager.IsMemoryDirty(dma_state.dma_get, diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h index 289786bb9e..feab652bb5 100644 --- a/src/video_core/dma_pusher.h +++ b/src/video_core/dma_pusher.h @@ -7,6 +7,7 @@ #pragma once #include <array> +#include <atomic> #include <condition_variable> #include <optional> #include <span> @@ -121,6 +122,10 @@ struct CommandList final { /// executed instead of the memory, which the game may already be refilling for a later frame. std::vector<CommandHeader> submitted_words; std::vector<u32> submitted_offsets; + /// DmaPusher::suspensions when the words were copied. A list copied during a suspension, or + /// that waited behind one, reads memory instead: games write the commands it runs before they + /// release the acquire. + u64 snapshot_suspensions = 0; }; /// FNV-1a over pushbuffer words (EDEN_NX2_PB_CHECK). @@ -139,6 +144,10 @@ inline u64 HashPushbufferWords(std::span<const u32> words) { /// details on this implementation. class DmaPusher final { public: + /// Counts suspensions on a semaphore acquire and the resumes after them, so it is odd while + /// the channel is suspended (see CommandList::snapshot_suspensions). + std::atomic<u64> suspensions{0}; + explicit DmaPusher(Core::System& system_, MemoryManager& memory_manager_, Control::ChannelState& channel_state_); ~DmaPusher(); diff --git a/src/video_core/engines/draw_manager.cpp b/src/video_core/engines/draw_manager.cpp index 038e5a2e69..564220b71c 100644 --- a/src/video_core/engines/draw_manager.cpp +++ b/src/video_core/engines/draw_manager.cpp @@ -72,11 +72,19 @@ void Maxwell3D::DrawManager::Clear(Maxwell3D& maxwell3d, u32 layer_count) { } } -void Maxwell3D::DrawManager::DrawDeferred(Maxwell3D& maxwell3d) { +void Maxwell3D::DrawManager::DrawDeferred(Maxwell3D& maxwell3d, bool loop_continues) { if (draw_state.draw_mode != DrawMode::Instance || draw_state.instance_count == 0) { + if (!loop_continues) { + draw_state.drawn_instances = 0; + } return; } - DrawEnd(maxwell3d, draw_state.instance_count + 1, true); + // The first draw of a batch also covers the loop's first instance; a batch that continues a + // loop split by a register write draws only the instances after the ones already drawn. + const bool continued = draw_state.drawn_instances != 0; + DrawEnd(maxwell3d, draw_state.instance_count + (continued ? 0 : 1), true); + draw_state.drawn_instances = + loop_continues ? draw_state.drawn_instances + draw_state.instance_count : 0; draw_state.instance_count = 0; } @@ -121,6 +129,7 @@ void Maxwell3D::DrawManager::DrawArrayInstanced(Maxwell3D& maxwell3d, Maxwell3D: draw_state.vertex_buffer.count = vertex_count; if (!subsequent) { draw_state.instance_count = 1; + draw_state.drawn_instances = 0; } draw_state.base_instance = draw_state.instance_count - 1; draw_state.draw_mode = DrawMode::Instance; @@ -220,6 +229,9 @@ void Maxwell3D::DrawManager::DrawEnd(Maxwell3D& maxwell3d, u32 instance_count, b [[fallthrough]]; case DrawMode::General: draw_state.base_instance = maxwell3d.regs.global_base_instance_index; + if (draw_state.draw_mode == DrawMode::Instance && draw_state.drawn_instances != 0) { + draw_state.base_instance += draw_state.drawn_instances + 1; + } draw_state.base_index = maxwell3d.regs.global_base_vertex_index; if (draw_state.draw_indexed) { draw_state.index_buffer = maxwell3d.regs.index_buffer; @@ -301,6 +313,8 @@ void Maxwell3D::DrawManager::UpdateTopology(Maxwell3D& maxwell3d) { } void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed, u32 instance_count) { + draw_state.draw_element_count = + draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count; LOG_TRACE(HW_GPU, "called, topology={}, count={}", draw_state.topology, draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count); UpdateTopology(maxwell3d); if (maxwell3d.ShouldExecute()) { @@ -309,6 +323,7 @@ void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed } void Maxwell3D::DrawManager::ProcessDrawIndirect(Maxwell3D& maxwell3d) { + draw_state.draw_element_count = 0; LOG_TRACE(HW_GPU, "called, topology={}, is_indexed={}, includes_count={}, buffer_size={}, max_draw_count={}", draw_state.topology, indirect_state.is_indexed, indirect_state.include_count, indirect_state.buffer_size, indirect_state.max_draw_counts); UpdateTopology(maxwell3d); if (maxwell3d.ShouldExecute()) { diff --git a/src/video_core/engines/fermi_2d.h b/src/video_core/engines/fermi_2d.h index 7fddf9fdc1..04a12fd351 100644 --- a/src/video_core/engines/fermi_2d.h +++ b/src/video_core/engines/fermi_2d.h @@ -70,6 +70,9 @@ public: ROP = 4, SrcCopyPremult = 5, BlendPremult = 6, + // Not a hardware operation: a copy that decodes sRGB-encoded values to linear, for the + // nx2 upscale fallback standing in for a program that decodes its output. + SrcCopySrgbToLinear = 7, }; enum class MemoryLayout : u32 { diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 4732167369..925b1b2ca1 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -685,8 +685,32 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { result.addr_lower = static_cast<u32>(tic.Address()); return result; }; + // Title 4's program ends by decoding its result from sRGB to linear (the 0.04045 threshold + // and the 2.4 exponent as half-precision immediates of HSETP2 and HMUL2). The copy of its + // sRGB input left the frame gamma-encoded twice, washed out: decode in the copy when the + // program does. + static std::unordered_map<GPUVAddr, bool> decodes_srgb; + const GPUVAddr program = regs.code_loc.Address(); + const auto [known, inserted] = decodes_srgb.try_emplace(program, false); + if (inserted) { + bool threshold = false, exponent = false; + std::array<u32, 0x400> block{}; + for (u64 offset = 0; offset < 0x20000 && !(threshold && exponent); offset += sizeof(block)) { + if (!memory_manager.IsFullyMappedRange(program + offset, sizeof(block))) break; + memory_manager.ReadBlock(program + offset, block.data(), sizeof(block)); + for (size_t word = 0; word < block.size(); word += 4) { + const u32 opcode = block[word] & 0xfff; + threshold |= opcode == 0x434 && block[word + 1] == 0x292d292d; + exponent |= opcode == 0x832 && block[word + 1] == 0x40cc40cc; + } + } + known->second = threshold && exponent; + LOG_INFO(HW_GPU, "Upscale fallback for program {:#x}: {}", program, + known->second ? "decodes sRGB like the program" : "plain copy"); + } const Fermi2D::Config config{ - .operation = Fermi2D::Operation::SrcCopy, + .operation = known->second ? Fermi2D::Operation::SrcCopySrgbToLinear + : Fermi2D::Operation::SrcCopy, .filter = Fermi2D::Filter::Bilinear, .must_accelerate = true, .dst_x0 = 0, diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 9731f54584..92313988bd 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -342,7 +342,7 @@ void Maxwell3D::ProcessDirtyRegisters(u32 method, u32 argument) { // the state they were begun with: NVN's draw macros reset the base instance (and its copy in // c[0][0x644]) right after the loop, before the macro returns. if (draw_manager.HasDeferredDraw() && !DrawManager::IsInstanceLoopMethod(method)) [[unlikely]] { - draw_manager.DrawDeferred(*this); + draw_manager.DrawDeferred(*this, true); } regs.reg_array[method] = argument; for (auto const& table : dirty.tables) @@ -518,6 +518,10 @@ void Maxwell3D::DumpRecentWrites(u32 method) { } LOG_ERROR(HW_GPU, "Write to {:#x} while macro {:#x} collects parameters ({} so far); last writes:", method, executing_macro, macro_params.size()); + if (!record_write_history) { + LOG_ERROR(HW_GPU, "Register write history disabled; EDEN_NX2_3D_HISTORY=1 enables it"); + return; + } for (size_t i = 0; i < recent_writes.size(); ++i) { const auto& w = recent_writes[(recent_write_index + i) % recent_writes.size()]; if (w.amount != 0) { @@ -537,7 +541,7 @@ void Maxwell3D::CallMultiMethod(Core::System& system, u32 method, const u32* bas } // As in ProcessDirtyRegisters: the paths below write without it. if (draw_manager.HasDeferredDraw() && !DrawManager::IsInstanceLoopMethod(method)) [[unlikely]] { - draw_manager.DrawDeferred(*this); + draw_manager.DrawDeferred(*this, true); } switch (method) { case MAXWELL3D_REG_INDEX(const_buffer.buffer): @@ -818,7 +822,6 @@ struct Maxwell3D::FrontState { Regs::ShadowRamControl control{}; u32 executing_macro{}; std::vector<u32> parameters; - std::vector<u32> fifo; u64 calls{}; /// A method the draw manager handles was sent since the back last ran DrawDeferred: only /// then can it have a deferred instanced draw to issue at the end of a macro. @@ -1095,9 +1098,9 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split engine.native_jit = std::make_shared<TuringMacroJit>(); } engine.native_jit->Prepare(engine.native_code, engine.native_code_generation); - fs.fifo.assign(fs.parameters.begin(), fs.parameters.end()); + const size_t parameter_count = fs.parameters.size(); const auto result = engine.native_jit->Execute( - start, fs.fifo, std::span<u32>{native_data_ram}, + start, fs.parameters, std::span<u32>{native_data_ram}, [&fs](u32 address) { return address < Regs::NUM_REGS ? fs.regs[address] : 0U; }, [&](u32 address, u32 value) { if (address >= MacroRegistersStart) { @@ -1113,7 +1116,7 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split if (!result.success) { engine.native_execution_failed = true; LOG_ERROR(HW_GPU, "Native MME (JIT, split front) failed: start={:#x}, consumed={}/{}, error={}", - start, result.consumed, fs.parameters.size(), result.error); + start, result.consumed, parameter_count, result.error); } } fs.parameters.clear(); @@ -1148,7 +1151,7 @@ void Maxwell3D::FrontDmaRead(VideoCommon::GpuSplit& split, FrontState& fs, u32 m for (u32 i = 0; i < words; ++i) { const u32 word = memory_manager.Read<u32>(source + u64{i} * sizeof(u32)); if (method == 0x158) { - fs.fifo.push_back(word); + fs.parameters.push_back(word); } else if (ram + i < native_data_ram.size()) { native_data_ram[ram + i] = word; } diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index af0e37978e..7646ddbbe3 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3064,11 +3064,14 @@ public: Maxwell3D::Regs::PrimitiveTopology topology{}; DrawMode draw_mode{}; bool draw_indexed{}; + u32 draw_element_count{}; // Zero for indirect draws whose count is not known yet. u32 base_index{}; Maxwell3D::Regs::VertexBuffer vertex_buffer; Maxwell3D::Regs::IndexBuffer index_buffer; u32 base_instance{}; u32 instance_count{}; + /// Deferred instances already drawn by flushes in the middle of the current loop. + u32 drawn_instances{}; std::vector<u8> inline_index_draw_indexes; }; struct DrawTextureState { @@ -3095,7 +3098,8 @@ public: }; void ProcessMethodCall(Maxwell3D& maxwell3d, u32 method, u32 argument); void Clear(Maxwell3D& maxwell3d, u32 layer_count); - void DrawDeferred(Maxwell3D& maxwell3d); + /// Draws the waiting instances. loop_continues is set when the loop goes on afterwards. + void DrawDeferred(Maxwell3D& maxwell3d, bool loop_continues = false); /// Instances after the first wait to be drawn together (DrawDeferred). [[nodiscard]] bool HasDeferredDraw() const noexcept { return draw_state.draw_mode == DrawMode::Instance && draw_state.instance_count != 0; @@ -3349,7 +3353,17 @@ private: }; std::array<RecentWrite, 48> recent_writes{}; size_t recent_write_index = 0; + const bool record_write_history{ +#ifdef NDEBUG + std::getenv("EDEN_NX2_3D_HISTORY") != nullptr +#else + true +#endif + }; void RecordWrite(u32 method, u32 argument, u32 amount, u32 pending) { + if (!record_write_history) { + return; + } recent_writes[recent_write_index++ % recent_writes.size()] = {method, argument, amount, pending}; } void DumpRecentWrites(u32 method); diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp index 0bb1809aa3..37ac3ec851 100644 --- a/src/video_core/engines/puller.cpp +++ b/src/video_core/engines/puller.cpp @@ -116,6 +116,7 @@ bool Puller::ResumeAcquire(DmaPusher& dma_pusher) { return false; } pending_acquire.reset(); + dma_pusher.suspensions.fetch_add(1, std::memory_order_release); Nx2SemaphoreWaiters::Get().Remove(this); return true; } @@ -257,6 +258,7 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) { dma_pusher.rasterizer->ReleaseFences(); if (!waiters.Latched(this) && !acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) { pending_acquire = acquire; + dma_pusher.suspensions.fetch_add(1, std::memory_order_release); VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::AcquireWaits, 1); } else { waiters.Remove(this); diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 53f8f063f7..6dbabcb43a 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -210,6 +210,10 @@ struct GPU::Impl { gpu_thread.Run(std::move(func), is_async); } + void MapInOrder(std::function<void()>&& func) { + gpu_thread.Run(std::move(func), is_async, true); + } + /// Notify rasterizer that any caches of the specified region should be flushed to Switch memory void FlushRegion(DAddr addr, u64 size) { gpu_thread.FlushRegion(addr, size, is_async); @@ -228,7 +232,9 @@ struct GPU::Impl { raster_area.start_address, raster_area.end_address, reads.load(std::memory_order_relaxed)); } - const u64 fence = RequestSyncOperation([this, &raster_area]() { + const u64 fence = RequestSyncOperation([this, raster_area]() { + // Apply collected guest writes before downloading GPU data over guest memory. + InvalidateGPUCache(); renderer->ReadRasterizer()->FlushRegion(raster_area.start_address, raster_area.end_address - raster_area.start_address); }); gpu_thread.TickGPU(is_async); @@ -505,6 +511,10 @@ bool GPU::IsAsync() const { return impl->is_async; } +bool GPU::HasSuspendedChannels() const { + return impl->scheduler.HasPendingChannelsAnyThread(); +} + bool GPU::UseNvdec() const { return impl->use_nvdec; } @@ -549,6 +559,10 @@ void GPU::RunInOrder(std::function<void()>&& func) { impl->RunInOrder(std::move(func)); } +void GPU::MapInOrder(std::function<void()>&& func) { + impl->MapInOrder(std::move(func)); +} + void GPU::InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) { impl->InvalidateRegion(addr, size, preserve_gpu_writes); } diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 3a6546b41b..d37dfa5990 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -217,6 +217,9 @@ public: [[nodiscard]] bool IsAsync() const; + /// Whether a channel is suspended on a semaphore acquire (callable from any thread). + [[nodiscard]] bool HasSuspendedChannels() const; + [[nodiscard]] bool UseNvdec() const; void RendererFrameEndNotify(); @@ -254,6 +257,10 @@ public: /// Runs func on the GPU thread after the command lists submitted so far. void RunInOrder(std::function<void()>&& func); + /// RunInOrder for GPU memory maps: also done before the GPU thread reads the command lists + /// submitted later (with the GPU split, on its front rather than its back). + void MapInOrder(std::function<void()>&& func); + /// Notify rasterizer that any caches of the specified region should be invalidated void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes = false); diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp index 55200b0647..3af8cb65a8 100644 --- a/src/video_core/gpu_thread.cpp +++ b/src/video_core/gpu_thread.cpp @@ -99,7 +99,12 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten } else if (std::holds_alternative<GPUTickCommand>(next.data)) { split.Defer([gpu = &system.GPU()] { gpu->TickWork(); }); } else if (auto* run = std::get_if<RunCommand>(&next.data)) { - split.DeferFunction(std::move(run->func)); + if (run->before_later_reads) { + split.Drain(); + run->func(); + } else { + split.DeferFunction(std::move(run->func)); + } } else if (const auto* flush = std::get_if<FlushRegionCommand>(&next.data)) { split.Defer([rasterizer_ptr, addr = flush->addr, size = flush->size] { rasterizer_ptr->FlushRegion(addr, size); @@ -155,8 +160,8 @@ void ThreadManager::TickGPU(bool is_async) { PushCommand(GPUTickCommand(), false, is_async); } -void ThreadManager::Run(std::function<void()>&& func, bool is_async) { - PushCommand(RunCommand{std::move(func)}, false, is_async); +void ThreadManager::Run(std::function<void()>&& func, bool is_async, bool before_later_reads) { + PushCommand(RunCommand{std::move(func), before_later_reads}, false, is_async); } void ThreadManager::InvalidateRegion(DAddr addr, u64 size) { diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h index 796c0d89f6..927453dab0 100644 --- a/src/video_core/gpu_thread.h +++ b/src/video_core/gpu_thread.h @@ -80,6 +80,9 @@ struct GPUTickCommand final {}; /// Command to run a function on the GPU thread after the commands queued before it struct RunCommand final { std::function<void()> func; + /// With the GPU split, run on its front (after draining the back) instead of deferring to the + /// back, so command lists read after this one see what func did (GMMU maps). + bool before_later_reads{}; }; using CommandData = @@ -132,7 +135,7 @@ public: void TickGPU(bool is_async); /// Runs func on the GPU thread once the commands queued before it have been processed. - void Run(std::function<void()>&& func, bool is_async); + void Run(std::function<void()>&& func, bool is_async, bool before_later_reads = false); void NotifyShutdown(); diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 9ff7563ff7..3dc26ec3ba 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -18,6 +18,7 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp ${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag + ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_srgb_to_linear.frag ${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_msaa.frag ${CMAKE_CURRENT_SOURCE_DIR}/blit_depth.frag diff --git a/src/video_core/host_shaders/blit_color_srgb_to_linear.frag b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag new file mode 100644 index 0000000000..1a38cb52a1 --- /dev/null +++ b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#version 450 + +layout(binding = 0) uniform sampler2D tex; + +layout(location = 0) in vec2 texcoord; +layout(location = 0) out vec4 color; + +// Copy of an sRGB-encoded source into a linear destination: the piecewise sRGB decode on the +// color channels, alpha unchanged. +void main() { + const vec4 value = textureLod(tex, texcoord, 0); + const vec3 low = value.rgb / 12.92; + const vec3 high = pow((value.rgb + 0.055) / 1.055, vec3(2.4)); + color = vec4(mix(high, low, lessThan(value.rgb, vec3(0.04045))), value.a); +} diff --git a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp index eb134e86b5..5fc10cfce2 100644 --- a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp +++ b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp @@ -106,19 +106,18 @@ void ComputePipeline::Configure() { const auto& cbufs{qmd.const_buffer_config}; const bool via_header_index{qmd.linked_tsc != 0}; const auto read_handle{[&](const auto& desc, u32 index) { - if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { - if (desc.handle_program != ~0U) { - // Read by the shader from a storage buffer: computed for this dispatch. - return TexturePair(VideoCommon::EvaluateTextureHandle( - info, desc.handle_program, *gpu_memory, - [&](u32 cbuf) -> GPUVAddr { - return cbuf < cbufs.size() && - ((qmd.const_buffer_enable_mask >> cbuf) & 1) != 0 - ? cbufs[cbuf].Address() - : GPUVAddr{}; - }), - via_header_index); - } + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this dispatch. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, *gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && + ((qmd.const_buffer_enable_mask >> cbuf) & + 1) != 0 + ? cbufs[cbuf].Address() + : GPUVAddr{}; + }), + via_header_index); } ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0); const u32 index_offset{index << desc.size_shift}; diff --git a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp index 81950279d5..4e02f0569b 100644 --- a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp +++ b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp @@ -309,18 +309,16 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { } const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers}; const auto read_handle{[&](const auto& desc, u32 index) { - if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { - if (desc.handle_program != ~0U) { - // Read by the shader from a storage buffer: computed for this draw. - return TexturePair(VideoCommon::EvaluateTextureHandle( - info, desc.handle_program, *gpu_memory, - [&](u32 cbuf) -> GPUVAddr { - return cbuf < cbufs.size() && cbufs[cbuf].enabled - ? cbufs[cbuf].address - : GPUVAddr{}; - }), - via_header_index); - } + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this draw. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, *gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && cbufs[cbuf].enabled + ? cbufs[cbuf].address + : GPUVAddr{}; + }), + via_header_index); } ASSERT(cbufs[desc.cbuf_index].enabled); const u32 index_offset{index << desc.size_shift}; diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp index edd151b563..506daef11e 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -11,6 +11,7 @@ #include "common/div_ceil.h" #include "common/settings.h" #include "video_core/host_shaders/blit_color_float_frag_spv.h" +#include "video_core/host_shaders/blit_color_srgb_to_linear_frag_spv.h" #include "video_core/host_shaders/blit_color_msaa_frag_spv.h" #include "video_core/host_shaders/blit_depth_frag_spv.h" #include "video_core/host_shaders/blit_depth_msaa_frag_spv.h" @@ -670,6 +671,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, full_screen_vert(BuildShader(device, FULL_SCREEN_TRIANGLE_VERT_SPV)), blit_color_to_color_frag(BuildShader(device, BLIT_COLOR_FLOAT_FRAG_SPV)), temporal_upscale_frag(BuildShader(device, TEMPORAL_UPSCALE_FRAG_SPV)), + blit_color_srgb_to_linear_frag(BuildShader(device, BLIT_COLOR_SRGB_TO_LINEAR_FRAG_SPV)), blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)), blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported() ? BuildShader(device, VULKAN_BLIT_DEPTH_STENCIL_FRAG_SPV) @@ -1331,7 +1333,10 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe } blit_color_keys.push_back(key); - const std::array stages = MakeStages(*full_screen_vert, *blit_color_to_color_frag); + const bool srgb_to_linear = + key.operation == Tegra::Engines::Fermi2D::Operation::SrcCopySrgbToLinear; + const std::array stages = MakeStages( + *full_screen_vert, srgb_to_linear ? *blit_color_srgb_to_linear_frag : *blit_color_to_color_frag); const VkPipelineColorBlendAttachmentState blend_attachment{ .blendEnable = VK_FALSE, .srcColorBlendFactor = VK_BLEND_FACTOR_ZERO, diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h index 303beeea5c..4741f9fe45 100644 --- a/src/video_core/renderer_vulkan/blit_image.h +++ b/src/video_core/renderer_vulkan/blit_image.h @@ -229,6 +229,7 @@ private: vk::ShaderModule full_screen_vert; vk::ShaderModule blit_color_to_color_frag; vk::ShaderModule temporal_upscale_frag; + vk::ShaderModule blit_color_srgb_to_linear_frag; vk::ShaderModule blit_color_msaa_frag; vk::ShaderModule blit_depth_stencil_frag; vk::ShaderModule blit_depth_frag; diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp index d1f78c0034..76a564aae9 100644 --- a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp +++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp @@ -269,7 +269,7 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe if (xfb_enabled) { RefreshXfbState(xfb_state, regs); } - if (!dynamic_vertex_input && ExperimentalStaticVertexRepackEnabled()) { + if (!dynamic_vertex_input && features.has_static_vertex_repack) { const u32 repack_mask = VertexRepackMask(regs); // Refresh from guest values even when only the stride changed: offsets must not accumulate. for (size_t index = 0; index < attributes.size(); ++index) { diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.h b/src/video_core/renderer_vulkan/fixed_pipeline_state.h index 36a4b4b7a2..1fe5cd75f3 100644 --- a/src/video_core/renderer_vulkan/fixed_pipeline_state.h +++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.h @@ -33,6 +33,7 @@ struct DynamicFeatures { bool has_dynamic_state3_logic_op_enable; bool has_dynamic_state3_line_stipple_enable; bool has_dynamic_vertex_input; + bool has_static_vertex_repack; bool has_color_write_enable; bool has_provoking_vertex; bool has_provoking_vertex_first_mode; diff --git a/src/video_core/renderer_vulkan/vertex_repack.h b/src/video_core/renderer_vulkan/vertex_repack.h index b0849c25c7..493eb41bd1 100644 --- a/src/video_core/renderer_vulkan/vertex_repack.h +++ b/src/video_core/renderer_vulkan/vertex_repack.h @@ -2,6 +2,7 @@ #pragma once +#include <algorithm> #include <cstdlib> #include <cstring> #include <limits> @@ -14,12 +15,21 @@ constexpr bool VertexRepackInputSupported(bool dynamic_input, bool experimental_ return dynamic_input || experimental_static; } -inline bool ExperimentalStaticVertexRepackEnabled() { - static const bool enabled = [] { - const char* value = std::getenv("EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK"); - return value && std::strcmp(value, "1") == 0; - }(); - return enabled; +// Pipelines without dynamic vertex input repack when the device cannot fetch the 69-byte stride. +// EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK=1 forces the repack on, =0 forces it off. +inline bool StaticVertexRepackSelected(const char* override_value, u32 stride_alignment) { + if (override_value && std::strcmp(override_value, "1") == 0) { + return true; + } + if (override_value && std::strcmp(override_value, "0") == 0) { + return false; + } + return stride_alignment > 1 && 69 % stride_alignment != 0; +} + +inline bool StaticVertexRepackEnabled(u32 stride_alignment) { + return StaticVertexRepackSelected(std::getenv("EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK"), + stride_alignment); } struct VertexRepackParameters { @@ -31,8 +41,11 @@ struct VertexRepackParameters { u32 dispatch_groups; }; -inline std::optional<VertexRepackParameters> MakeVertexRepackParameters( - u32 offset, u32 size, u64 alignment, u64 max_storage_range, u32 max_dispatch_groups) { +inline std::optional<VertexRepackParameters> MakeVertexRepackParameters(u32 offset, u32 size, + u64 alignment, + u64 max_storage_range, + u32 max_dispatch_groups, + u64 buffer_size) { if (size == 0 || alignment == 0) { return std::nullopt; } @@ -40,9 +53,11 @@ inline std::optional<VertexRepackParameters> MakeVertexRepackParameters( const u64 output_size = vertices * 72; const u64 base = u64{offset} / alignment * alignment; const u64 bias = offset - base; - const u64 range = (bias + size + 3) / 4 * 4; + // The word-rounded range must not run past the end of the source buffer + const u64 available = buffer_size > base ? buffer_size - base : 0; + const u64 range = (std::min)((bias + size + 3) / 4 * 4, available); const u64 groups = (vertices * 18 + 127) / 128; - if (output_size > std::numeric_limits<u32>::max() || + if (range < bias + size || output_size > std::numeric_limits<u32>::max() || range > std::numeric_limits<u32>::max() || output_size > max_storage_range || range > max_storage_range || groups > max_dispatch_groups) { return std::nullopt; diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp index 2449265509..43cb7d9414 100644 --- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp @@ -365,6 +365,10 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m LOG_INFO(Render_Vulkan, "69-byte vertex layout repacking {} ({})", enable_vertex_repack ? "enabled" : "disabled", "portable Vulkan layout fallback"); + static_vertex_repack = + StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment()); + LOG_INFO(Render_Vulkan, "Vertex repacking without dynamic vertex input {}", + static_vertex_repack ? "enabled" : "disabled"); if (enable_vertex_repack) { vertex_repack_pass = std::make_unique<VertexRepackPass>( device, scheduler, descriptor_pool, staging_pool, compute_pass_descriptor_queue); @@ -387,14 +391,13 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m u32 BufferCacheRuntime::SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs, bool dynamic_input) { const u32 old_mask = vertex_repack_mask; - const bool input_supported = VertexRepackInputSupported( - dynamic_input, ExperimentalStaticVertexRepackEnabled()); + const bool input_supported = VertexRepackInputSupported(dynamic_input, static_vertex_repack); vertex_repack_mask = vertex_repack_pass && input_supported ? VertexRepackMask(regs) : 0; if (vertex_repack_mask != old_mask) { LOG_DEBUG(Render_Vulkan, "Vertex repack selection: mask={:#x}, dynamic_input={}", vertex_repack_mask, dynamic_input); } - return old_mask | vertex_repack_mask; + return old_mask ^ vertex_repack_mask; } StagingBufferRef BufferCacheRuntime::UploadStagingBuffer(size_t size) { @@ -657,7 +660,8 @@ void BufferCacheRuntime::BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset return; } if (buffer != VK_NULL_HANDLE && size != 0 && IsVertexRepacked(index)) { - const auto repacked = vertex_repack_pass->Assemble(buffer, offset, size); + const auto repacked = + vertex_repack_pass->Assemble(buffer, offset, size, u64{offset} + size); if (!repacked) { return; } @@ -702,7 +706,8 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bi if (handle != VK_NULL_HANDLE && IsVertexRepacked(binding) && bindings.strides[i] == 69 && bindings.sizes[i] > 0) { const auto repacked = vertex_repack_pass->Assemble( - handle, static_cast<u32>(bindings.offsets[i]), static_cast<u32>(bindings.sizes[i])); + handle, static_cast<u32>(bindings.offsets[i]), static_cast<u32>(bindings.sizes[i]), + bindings.buffers[i]->SizeBytes()); if (!repacked) { continue; } @@ -712,7 +717,7 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bi bindings.strides[i] = 72; static bool reported = false; if (!reported) { - LOG_WARNING(Render_Vulkan, "Experimental GPU vertex repacking active: 69 -> 72 bytes"); + LOG_INFO(Render_Vulkan, "GPU vertex repacking active: 69 -> 72 bytes"); reported = true; } } diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h index d3295ec6ed..b9a0225094 100644 --- a/src/video_core/renderer_vulkan/vk_buffer_cache.h +++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h @@ -110,6 +110,7 @@ public: void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept; + /// Returns the mask of bindings whose repacked state changed. u32 SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs, bool dynamic_input); bool IsVertexRepacked(u32 binding) const { @@ -168,15 +169,40 @@ public: void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings); + /// This and the two members below are the hooks of VideoCommon::StreamUniformReusable + /// (buffer_cache/uniform_stream_reuse.h). + struct StreamUniformRef { + VkBuffer buffer; + VkDeviceAddress device_address; // base of `buffer`, 0 without buffer device address + u32 offset; + }; + std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage, [[maybe_unused]] u32 binding_index, u32 size) { const StagingBufferRef ref = staging_pool.Request(size, MemoryUsage::Upload); - guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address, - static_cast<u32>(ref.offset), size); + last_stream_uniform = StreamUniformRef{ + .buffer = ref.buffer, + .device_address = ref.device_address, + .offset = static_cast<u32>(ref.offset), + }; + RebindStreamUniform(last_stream_uniform, size); return ref.mapped_span; } + /// The allocation of the latest BindMappedUniformBuffer call. + [[nodiscard]] StreamUniformRef LastStreamUniformRef() const noexcept { + return last_stream_uniform; + } + + /// Adds `ref` to the descriptor payload exactly as BindMappedUniformBuffer did when it made + /// the allocation (it is the very same call). The caller guarantees the allocation is still + /// live: made on the scheduler tick that is current now, so the staging pool has not recycled + /// it and the GPU has not even started on it. + void RebindStreamUniform(const StreamUniformRef& ref, u32 size) { + guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address, ref.offset, size); + } + [[nodiscard]] VkDeviceSize SparseAlignmentFor(bool sparse_compatible) const noexcept { if (!sparse_compatible || !multi_range_buffers.use_sparse) { return 0; @@ -270,12 +296,15 @@ private: std::unique_ptr<Uint8Pass> uint8_pass; std::unique_ptr<VertexRepackPass> vertex_repack_pass; u32 vertex_repack_mask{}; + bool static_vertex_repack{}; QuadIndexedPass quad_index_pass; MultiRangeBufferCache multi_range_buffers; boost::container::small_vector<MultiRangeSource, 16> multi_range_sources; VkDeviceSize multi_range_total{}; + StreamUniformRef last_stream_uniform{}; + bool limit_dynamic_storage_buffers = false; u32 max_dynamic_storage_buffers = (std::numeric_limits<u32>::max)(); }; diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index cb8dcde42f..5902b7d8f5 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -335,10 +335,11 @@ VertexRepackPass::VertexRepackPass(const Device& device_, Scheduler& scheduler_, scheduler{scheduler_}, staging_pool{staging_pool_}, descriptor_queue{descriptor_queue_} {} std::optional<std::pair<VkBuffer, VkDeviceSize>> VertexRepackPass::Assemble(VkBuffer source, - u32 offset, u32 size) { + u32 offset, u32 size, + u64 buffer_size) { const auto parameters = MakeVertexRepackParameters( offset, size, device.GetStorageBufferAlignment(), device.GetMaxStorageBufferRange(), - device.GetMaxComputeWorkGroupCount()[0]); + device.GetMaxComputeWorkGroupCount()[0], buffer_size); if (!parameters) { LOG_ERROR(Render_Vulkan, "Vertex repack exceeds device limits: offset={}, size={}", offset, size); return std::nullopt; diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index 27fffbbbb7..9687bfff91 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h @@ -60,9 +60,10 @@ class VertexRepackPass final : public ComputePass { public: VertexRepackPass(const Device& device, Scheduler& scheduler, DescriptorPool& descriptor_pool, StagingBufferPool& staging_pool, ComputePassDescriptorQueue& descriptor_queue); - /// Returns nothing when the range exceeds the device limits; the binding is then left as is. - std::optional<std::pair<VkBuffer, VkDeviceSize>> Assemble(VkBuffer source, u32 offset, - u32 size); + /// Returns nothing when the range exceeds the device limits or the source buffer; the binding + /// is then left as is. + std::optional<std::pair<VkBuffer, VkDeviceSize>> Assemble(VkBuffer source, u32 offset, u32 size, + u64 buffer_size); private: Scheduler& scheduler; diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index 1cfe391ba6..3a3861930c 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -176,19 +176,18 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute, const auto& cbufs{qmd.const_buffer_config}; const bool via_header_index{qmd.linked_tsc != 0}; const auto read_handle{[&](const auto& desc, u32 index) { - if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { - if (desc.handle_program != ~0U) { - // Read by the shader from a storage buffer: computed for this dispatch. - return TexturePair(VideoCommon::EvaluateTextureHandle( - info, desc.handle_program, gpu_memory, - [&](u32 cbuf) -> GPUVAddr { - return cbuf < cbufs.size() && - ((qmd.const_buffer_enable_mask >> cbuf) & 1) != 0 - ? cbufs[cbuf].Address() - : GPUVAddr{}; - }), - via_header_index); - } + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this dispatch. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && + ((qmd.const_buffer_enable_mask >> cbuf) & + 1) != 0 + ? cbufs[cbuf].Address() + : GPUVAddr{}; + }), + via_header_index); } ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0); const u32 index_offset{index << desc.size_shift}; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index fa273f6ef9..4bd2e2735e 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -400,18 +400,16 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { } const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers}; const auto read_handle{[&](const auto& desc, u32 index) { - if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { - if (desc.handle_program != ~0U) { - // Read by the shader from a storage buffer: computed for this draw. - return TexturePair(VideoCommon::EvaluateTextureHandle( - info, desc.handle_program, *gpu_memory, - [&](u32 cbuf) -> GPUVAddr { - return cbuf < cbufs.size() && cbufs[cbuf].enabled - ? cbufs[cbuf].address - : GPUVAddr{}; - }), - via_header_index); - } + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this draw. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, *gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && cbufs[cbuf].enabled + ? cbufs[cbuf].address + : GPUVAddr{}; + }), + via_header_index); } ASSERT(cbufs[desc.cbuf_index].enabled); const u32 index_offset{index << desc.size_shift}; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index ff8e84b019..0223f56ff1 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -34,6 +34,7 @@ #include "video_core/renderer_vulkan/maxwell_to_vk.h" #include "video_core/renderer_vulkan/pipeline_helper.h" #include "video_core/renderer_vulkan/pipeline_statistics.h" +#include "video_core/renderer_vulkan/vertex_repack.h" #include "video_core/renderer_vulkan/vk_compute_pipeline.h" #include "video_core/renderer_vulkan/vk_descriptor_pool.h" #include "video_core/renderer_vulkan/vk_pipeline_cache.h" @@ -583,6 +584,8 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, dynamic_features.has_dynamic_vertex_input = device.IsExtVertexInputDynamicStateSupported() && Settings::values.vertex_input_dynamic_state.GetValue(); + dynamic_features.has_static_vertex_repack = + StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment()); dynamic_features.has_provoking_vertex = device.IsExtProvokingVertexSupported(); dynamic_features.has_provoking_vertex_first_mode = @@ -600,9 +603,17 @@ constexpr u32 UsedPipelinesMagic = 0x31505545; // "EUP1" // Pipelines the last two sessions used are built at boot. Each holds ~0.45 MB of driver memory: // title 1 built 4,800 from eight sessions, 1.5 GB more than building them on first use (but that // stalled frames 4x as often), while one session of its title screen uses ~1,500. +// MoltenVK keeps the previous eight sessions: a pipeline built while playing goes through +// SPIR-V -> MSL -> Metal, far more than the 2-3 ms the shorter window assumes, so each one +// built at boot is a stutter avoided. constexpr u8 UsedPipelinesMaxAge = 2; +constexpr u8 UsedPipelinesMaxAgeMoltenVK = 8; -std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path) { +u8 MaxUsedPipelineAge(const Device& device) { + return device.IsMoltenVK() ? UsedPipelinesMaxAgeMoltenVK : UsedPipelinesMaxAge; +} + +std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path, u8 max_age) { std::unordered_map<u64, u8> used; std::ifstream file{path, std::ios::binary}; u32 magic{}, count{}; @@ -618,7 +629,7 @@ std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path) !file.read(reinterpret_cast<char*>(&age), sizeof(age))) { break; } - if (age < UsedPipelinesMaxAge) { // lists saved with a longer window + if (age < max_age) { // lists saved with a longer window used.emplace(hash, age); } } @@ -669,7 +680,7 @@ void PipelineCache::SaveUsedPipelines() { next.emplace(hash, u8{0}); } for (const auto& [hash, age] : used_pipelines) { - if (age + 1 < UsedPipelinesMaxAge) { + if (age + 1 < MaxUsedPipelineAge(device)) { next.try_emplace(hash, static_cast<u8>(age + 1)); } } @@ -744,7 +755,7 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading } pipeline_cache_filename = base_dir / "vulkan.bin"; used_pipelines_filename = base_dir / "vulkan_used.bin"; - used_pipelines = LoadUsedPipelines(used_pipelines_filename); + used_pipelines = LoadUsedPipelines(used_pipelines_filename, MaxUsedPipelineAge(device)); used_pipelines_saved = std::chrono::steady_clock::now(); size_t skipped_pipelines = 0; size_t queued_graphics = 0; @@ -1004,6 +1015,11 @@ void PipelineCache::TickFrame() { } void PipelineCache::EvictUnusedPipelines() { + // Building a pipeline again on MoltenVK means a Metal compile mid-game, far dearer than the + // 2-3 ms below, so none are evicted there. + if (device.IsMoltenVK()) { + return; + } // Each built pipeline holds ~0.45 MB of driver memory and none was ever destroyed, so it grew // with every place a session went (title 1: 1,500 on its title screen alone). One unused for // this long is destroyed; it takes 2-3 ms to build again from the driver's pipeline cache. @@ -1056,7 +1072,7 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const // Usually these shaders are only used once for building textures so we can assume they // can't be built async const auto& draw_state = maxwell3d->draw_manager.draw_state; - if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) { + if (draw_state.draw_element_count <= 6) { return pipeline; } return nullptr; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 978f8be547..3898c571f5 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1127,12 +1127,10 @@ void RasterizerVulkan::InnerInvalidation(std::span<const std::pair<DAddr, std::s buffer_cache.WriteMemory(addr, size); } } - { - for (const auto& [addr, size] : sequences) { - query_cache.InvalidateRegion(addr, size); - pipeline_cache.InvalidateRegion(addr, size); - } + for (const auto& [addr, size] : sequences) { + query_cache.InvalidateRegion(addr, size); } + pipeline_cache.InvalidateRegions(sequences); } bool RasterizerVulkan::OnCPUWrite(DAddr addr, u64 size) { @@ -1424,10 +1422,27 @@ void RasterizerVulkan::FlushWork() { #endif // __ANDROID__ static_assert(DRAWS_TO_DISPATCH % (CHECK_MASK + 1) == 0); - if ((++draw_counter & CHECK_MASK) != CHECK_MASK) { + if ((++draw_counter & CHECK_MASK) != 0) { return; } - if (draw_counter < DRAWS_TO_DISPATCH) { + // Keep the host GPU working while native commands are still being recorded. + static const u32 native_flush_draws = [] { + constexpr u32 fallback = 128; + const char* value = std::getenv("EDEN_NX2_FLUSH_DRAWS"); + if (value == nullptr || value[0] < '0' || value[0] > '9') { + return fallback; + } + char* end = nullptr; + const unsigned long parsed = std::strtoul(value, &end, 10); + if (*end != '\0' || parsed == 0) { + return fallback; + } + return static_cast<u32>(std::clamp(parsed, 32UL, 1'048'576UL)); + }(); + const u32 draws_to_dispatch = Tegra::g_nx2_ampere.load(std::memory_order_relaxed) + ? native_flush_draws + : DRAWS_TO_DISPATCH; + if (draw_counter < draws_to_dispatch) { scheduler.DispatchWork(); return; } diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 30981fe413..928146027d 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -2350,23 +2350,31 @@ void TextureCacheRuntime::TickFrame() { }); } +namespace { + +// The host image of a texture; unbacked images are created without memory (see MakeImage). +[[nodiscard]] vk::Image MakeHostImage(TextureCacheRuntime& runtime, const ImageInfo& info, + u32 host_level_offset, GPUVAddr gpu_addr, bool backed) { + const Device& device = runtime.device; + const bool sparse_tiles = !backed && UsesSparseTiles(device, info, gpu_addr); + return MakeImage( + device, runtime.memory_allocator, TrimmedHostInfo(device, info, host_level_offset), + IsHugeAstcArray(device, info) ? std::span<const VkFormat>{HugeAstcViewFormats} + : IsOpaqueBc1Astc(device, info) ? std::span<const VkFormat>{OpaqueAstcViewFormats} + : runtime.ViewFormats(info.format), + AstcHostFormat(device, info), + !backed && (UsesSparseLayers(device, info, gpu_addr) || sparse_tiles || + IsUnwrittenSparse(device, info, gpu_addr)), + sparse_tiles && SparseTilesModeOf(device) == SparseTilesMode::Aliased); +} + +} // Anonymous namespace + Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu_addr_, VAddr cpu_addr_) : VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler}, runtime{&runtime_}, - original_image(MakeImage(runtime_.device, runtime_.memory_allocator, - TrimmedHostInfo(runtime_.device, info, host_level_offset), - IsHugeAstcArray(runtime_.device, info) - ? std::span<const VkFormat>{HugeAstcViewFormats} - : IsOpaqueBc1Astc(runtime_.device, info) - ? std::span<const VkFormat>{OpaqueAstcViewFormats} - : runtime->ViewFormats(info.format), - AstcHostFormat(runtime_.device, info), - UsesSparseLayers(runtime_.device, info, gpu_addr_) || - UsesSparseTiles(runtime_.device, info, gpu_addr_) || - IsUnwrittenSparse(runtime_.device, info, gpu_addr_), - UsesSparseTiles(runtime_.device, info, gpu_addr_) && - SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased)), + original_image(MakeHostImage(runtime_, info, host_level_offset, gpu_addr_, false)), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; @@ -2377,6 +2385,17 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu flags |= VideoCommon::ImageFlagBits::SparseLayers; } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && InitSparseTiles()) { flags |= VideoCommon::ImageFlagBits::SparseTiles; + } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && + SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased) { + // The driver can't sample unbound memory (that is why the blocks were to share one), + // so an image whose tiles could not be set up is backed whole like any other. + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: backing it whole", gpu_addr_); + original_image = MakeHostImage(runtime_, info, host_level_offset, gpu_addr_, true); + if (!original_image.IsUnbacked()) { + flags &= ~VideoCommon::ImageFlagBits::Unbacked; + } else if (texels <= (u64{1} << 28)) { + flags |= VideoCommon::ImageFlagBits::UnwrittenSparse; + } } else if (texels <= (u64{1} << 28)) { // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing // was written there yet, so the first write gets it replaced. diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index 5faed0dba9..5d41dc9697 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -6,6 +6,7 @@ #include <algorithm> #include <cstring> +#include <functional> #include <string> #include <unordered_set> #include <fmt/ranges.h> @@ -36,6 +37,17 @@ void ShaderCache::InvalidateRegion(VAddr addr, size_t size) { RemovePendingShaders(); } +void ShaderCache::InvalidateRegions(std::span<const std::pair<DAddr, size_t>> sequences) { + if (sequences.empty()) { + return; + } + std::scoped_lock lock{invalidation_mutex}; + for (const auto& [addr, size] : sequences) { + InvalidatePagesInRegion(addr, size); + } + RemovePendingShaders(); +} + void ShaderCache::OnCacheInvalidation(VAddr addr, size_t size) { std::scoped_lock lock{invalidation_mutex}; InvalidatePagesInRegion(addr, size); @@ -304,10 +316,17 @@ void ShaderCache::RemovePendingShaders() { marked_for_removal.clear(); if (!removed_shaders.empty()) { - // Remove the given shaders from the cache - std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) { - return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end(); - }); + if (removed_shaders.size() > 16) { + std::ranges::sort(removed_shaders, std::less<ShaderInfo*>{}); + std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) { + return std::binary_search(removed_shaders.begin(), removed_shaders.end(), + shader.get(), std::less<ShaderInfo*>{}); + }); + } else { + std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) { + return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end(); + }); + } } } diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index 281a90cb6f..7c370e217c 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -67,6 +67,8 @@ public: /// @param size Number of bytes of the invalidation void InvalidateRegion(VAddr addr, size_t size); + void InvalidateRegions(std::span<const std::pair<DAddr, size_t>> sequences); + /// @brief Unmarks a memory region as cached and marks it for removal /// @param addr Start address of the CPU write operation /// @param size Number of bytes of the CPU write operation @@ -104,6 +106,8 @@ protected: bool last_shaders_valid = false; private: + friend struct ShaderCacheTestAccess; + /// @brief Tries to obtain a cached shader starting in a given address /// @note Doesn't check for ranges, the given address has to be the start of the shader /// @param addr Start address of the shader, this doesn't cache for region diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index 22e661c849..0c7d8a0d42 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -316,7 +316,8 @@ std::optional<u64> GenericEnvironment::TryFindSize() { Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit, bool via_header_index, u32 raw) { const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)}; - const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit)}; + const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit, + gpu_memory->UsesNativeTiling())}; ASSERT(index <= tic_limit); const GPUVAddr descriptor_addr{tic_addr + index * sizeof(Tegra::Texture::TICEntry)}; Tegra::Texture::TICEntry entry; @@ -327,9 +328,9 @@ Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, namespace { // Reads a native program a 4 KiB page at a time instead of one 16-byte instruction at a time. -// The reads do not flush the GPU caches first (ReadBlockUnsafe): the previous per-instruction -// ReadBlock did, once per instruction. An instruction that is not inside a fully mapped page is -// left to the caller. +// Each page is read with ReadBlock, so GPU-written code is flushed first, once per page instead +// of once per instruction. An instruction that is not inside a fully mapped page is left to the +// caller. class NativeInstructionReader { public: using Instruction = Shader::Ampere::Instruction; @@ -364,7 +365,7 @@ private: page_begin = address; page_end = address; if (length >= sizeof(Instruction) && gpu_memory.IsFullyMappedRange(address, length)) { - gpu_memory.ReadBlockUnsafe(address, page.data(), length); + gpu_memory.ReadBlock(address, page.data(), length); page_end = next_page; } } @@ -414,7 +415,7 @@ std::shared_ptr<const LoweredNativeCode> GenericEnvironment::LowerNativeCode(u32 if (!native.empty()) break; throw Shader::RuntimeError("Unmapped native shader instruction at {:#x}", address); } - gpu_memory->ReadBlockUnsafe(address, &instruction, sizeof(instruction)); + gpu_memory->ReadBlock(address, &instruction, sizeof(instruction)); } native.push_back(instruction); const auto status = capture.Observe(instruction, index); diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h index 75383809c5..a441e572de 100644 --- a/src/video_core/texture_cache/descriptor_table.h +++ b/src/video_core/texture_cache/descriptor_table.h @@ -35,10 +35,11 @@ public: invalidated = true; } - [[nodiscard]] std::pair<T, bool> Read(Tegra::MemoryManager const& gpu_memory, u32 index) noexcept { + template <class MemoryManager> + [[nodiscard]] std::pair<T, bool> Read(const MemoryManager& gpu_memory, u32 index) noexcept { DEBUG_ASSERT(index <= current_limit); - if (invalidated) [[unlikely]] { - std::ranges::fill(read_descriptors, 0); + if (invalidated) { + ClearReadDescriptors(); invalidated = false; } const GPUVAddr gpu_addr = current_gpu_addr + index * sizeof(T); @@ -62,10 +63,8 @@ public: // Mario Brothership reallocates a lot of times, so use aggressive pre-alloc sizes // std::vector<T> by default uses quadratic growth, but that isn't even enough to satisfy brothership const size_t num_descriptors = ((limit + 0x80000) & (~0x7ffff)) + 1; - size_t old_size = read_descriptors.size(); read_descriptors.resize(Common::DivCeil(num_descriptors, 64U)); - old_size = (std::min)(old_size, read_descriptors.size()); - std::fill(read_descriptors.begin(), read_descriptors.begin() + old_size, 0); + ClearReadDescriptors(); invalidated = false; // descriptors.resize(num_descriptors); @@ -76,6 +75,13 @@ public: GPUVAddr current_gpu_addr{}; u32 current_limit{}; bool invalidated{}; ///< read_descriptors is to be cleared before the next read + +private: + void ClearReadDescriptors() noexcept { + // Reserved capacity is not addressable. A later limit increase clears its new range. + const size_t words = (std::min)(read_descriptors.size(), size_t{current_limit} / 64 + 1); + std::fill_n(read_descriptors.begin(), words, u64{0}); + } }; } // namespace VideoCommon diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 3f3ae9cafd..4929c2df44 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -809,7 +809,8 @@ typename P::Framebuffer* TextureCache<P>::GetFramebuffer() { template <class P> ImageViewId TextureCache<P>::VisitImageView(u32 index, bool compute) { auto& table = compute ? channel_state->compute_image_table : channel_state->graphics_image_table; - index = Tegra::Texture::TextureHeaderIndex(index, table.current_limit); + index = Tegra::Texture::TextureHeaderIndex(index, table.current_limit, + gpu_memory->UsesNativeTiling()); if (index > table.current_limit) { LOG_DEBUG(HW_GPU, "Invalid image view index={}", index); return NULL_IMAGE_VIEW_ID; diff --git a/src/video_core/textures/texture.h b/src/video_core/textures/texture.h index 79396d8a68..35a4c8eecf 100644 --- a/src/video_core/textures/texture.h +++ b/src/video_core/textures/texture.h @@ -167,10 +167,10 @@ static_assert(sizeof(TextureHandle) == 4, "TextureHandle has wrong size"); /// Some Switch 2 bindless handles (Switch Sports' scenery, read from a uniform register) hold /// the texture header's byte offset in the pool, index * 32, where the handle normally has the -/// index. Read it as an offset only when the plain index falls past the pool and the offset -/// lands inside it, so handles that already resolve are left alone. -[[nodiscard]] constexpr u32 TextureHeaderIndex(u32 index, u32 limit) { - if (index > limit && index % 32 == 0 && index / 32 <= limit) { +/// index. Read it as an offset only on Switch 2 (native tiling), when the plain index falls past +/// the pool and the offset lands inside it, so handles that already resolve are left alone. +[[nodiscard]] constexpr u32 TextureHeaderIndex(u32 index, u32 limit, bool switch2) { + if (switch2 && index > limit && index % 32 == 0 && index / 32 <= limit) { return index / 32; } return index; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 622d792210..5f75bcc18e 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -1161,6 +1161,12 @@ FN_MAX_LIMIT_LIST return properties.driver.driverID == VK_DRIVER_ID_MOLTENVK; } + /// The multiple vertex input binding strides must be. Metal needs 4 bytes, which MoltenVK + /// reports as minVertexInputBindingStrideAlignment through VK_KHR_portability_subset. + u32 GetMinVertexInputBindingStrideAlignment() const noexcept { + return IsMoltenVK() ? 4 : 1; + } + NvidiaArchitecture GetNvidiaArch() const noexcept { return nvidia_arch; } diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index e5a8f81424..8fa39e07e3 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -10,6 +10,7 @@ #include <boost/algorithm/string/split.hpp> #include "common/fs/path_util.h" +#include "common/game_presets.h" #include "common/settings.h" #include "common/settings_enums.h" #include "frontend_common/settings_generator.h" @@ -320,6 +321,7 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; constexpr int default_mouse_hide_timeout = 2500; constexpr int default_input_update_timeout = 1; +constexpr int EmuThreadStopTimeoutMs = 10'000; constexpr size_t CopyBufferSize = 1_MiB; @@ -457,6 +459,29 @@ MainWindow::MainWindow(bool has_broken_vulkan) // Gen keys if necessary OnCheckFirmwareDecryption(false); + auto* recommendations = new QMessageBox( + QMessageBox::Question, tr("Recommended Mario Kart World settings"), + tr("Use the recommended settings for Mario Kart World?\n\n" + "Vulkan, 1x resolution, GPU Fast, asynchronous shaders, BC3, conservative VRAM, " + "reactive flushing off, FIFO Relaxed VSync, handheld mode, ScaleForce, and FXAA.\n\n" + "Yes replaces your saved settings. No or closing this window keeps your settings."), + QMessageBox::Yes | QMessageBox::No, this); + recommendations->setAttribute(Qt::WA_DeleteOnClose); + recommendations->setDefaultButton(QMessageBox::No); + recommendations->setEscapeButton(QMessageBox::No); + recommendations->setWindowModality(Qt::NonModal); + connect(recommendations, &QMessageBox::finished, this, [this](int result) { + use_recommended_mkw_settings = result == QMessageBox::Yes; + if (use_recommended_mkw_settings && !emulation_running && !boot_in_progress) { + Settings::SetMarioKartWorldRecommendedSettings(Settings::values, true); + config->SaveAllValues(); + QtCommon::system->ApplySettings(); + UpdateStatusButtons(); + } + LOG_INFO(Config, "MKW recommended settings accepted: {}", use_recommended_mkw_settings); + }); + recommendations->show(); + #ifdef __unix__ OnCheckGraphicsBackend(); #endif @@ -1964,6 +1989,12 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa QtCommon::system->ApplySettings(); } + if (Settings::ApplyMarioKartWorldPreset(Settings::values, title_id, + use_recommended_mkw_settings)) { + QtCommon::system->ApplySettings(); + Settings::UpdateGPUAccuracy(); + LOG_INFO(Config, "Applied recommended MKW settings after per-game configuration"); + } Settings::LogSettings(); if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) { @@ -2209,6 +2240,23 @@ void MainWindow::OnShutdownBeginDialog() { void MainWindow::OnEmulationStopTimeExpired() { if (QtCommon::emu_thread) { QtCommon::emu_thread->ForceStop(); + emu_terminate_timer.setSingleShot(true); + emu_terminate_timer.start(EmuThreadStopTimeoutMs); + connect(&emu_terminate_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeout, + Qt::UniqueConnection); + } +} + +void MainWindow::OnEmulationStopTimeout() { + TerminateEmulationThread(); + OnEmulationStopped(); +} + +void MainWindow::TerminateEmulationThread() { + if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) { + LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it"); + QtCommon::emu_thread->terminate(); + QtCommon::emu_thread->wait(); } } @@ -2220,6 +2268,7 @@ void MainWindow::OnEmulationStopped() { return; } shutdown_timer.stop(); + emu_terminate_timer.stop(); if (QtCommon::emu_thread) { QtCommon::emu_thread->disconnect(); QtCommon::emu_thread->wait(); @@ -2309,13 +2358,17 @@ void MainWindow::ShutdownGame() { play_time_manager->Stop(); OnShutdownBegin(); OnEmulationStopTimeExpired(); - if (QtCommon::emu_thread) { + emu_terminate_timer.stop(); + if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) { QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(QtCommon::emu_thread.get(), &QThread::finished, &loop, &QEventLoop::quit); - if (QtCommon::emu_thread->isRunning()) { - loop.exec(QEventLoop::ExcludeUserInputEvents); - } + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(EmuThreadStopTimeoutMs); + loop.exec(QEventLoop::ExcludeUserInputEvents); } + TerminateEmulationThread(); OnEmulationStopped(); } diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index e2c5a0f6db..d6b0e5b312 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -279,6 +279,7 @@ private: StartGameType with_config = StartGameType::Normal); void BootGameFromList(const QString& filename, StartGameType with_config); void ShutdownGame(); + void TerminateEmulationThread(); void SetDiscordEnabled(bool state); void LoadAmiibo(const QString& filename); @@ -437,6 +438,7 @@ private slots: void OnShutdownBeginDialog(); void OnEmulationStopped(); void OnEmulationStopTimeExpired(); + void OnEmulationStopTimeout(); #ifdef ENABLE_UPDATE_CHECKER void OnEmulatorUpdateAvailable(); @@ -501,11 +503,13 @@ private: #endif MultiplayerState* multiplayer_state = nullptr; + bool use_recommended_mkw_settings = false; GRenderWindow* render_window = nullptr; GameList* game_list = nullptr; LoadingScreen* loading_screen = nullptr; QTimer shutdown_timer; + QTimer emu_terminate_timer; OverlayDialog* shutdown_dialog{}; PerformanceOverlay* perf_overlay = nullptr; #ifdef HAS_RESHADE |
