diff options
| author | Jonas <104419015+jojo-chaechae@users.noreply.github.com> | 2026-11-03 04:36:01 +0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-11-03 04:36:01 +0600 |
| commit | 8362c44ff835d7be73d2adad9ce3e2d4467a6aeb (patch) | |
| tree | c1eb12a54510bbeaed67a8a524b5ce9e8576cfcd /src | |
| parent | 155bf34fc297b6b8e4c281ecf7ea5d9ce3140762 (diff) | |
| parent | fe3c344573140b7864c9a86f204eb9c0dce0d548 (diff) | |
Merge pull request #334 from snowstormxd/codex/upstream-mkw-test
MKW performance improvements
Diffstat (limited to 'src')
36 files changed, 1224 insertions, 64 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/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/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/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 48e14daaef..3167a28baf 100644 --- a/src/tests/core/nup_writer.cpp +++ b/src/tests/core/nup_writer.cpp @@ -215,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); @@ -502,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/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 2d90a949c3..40369b16ef 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -16,6 +16,7 @@ #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" @@ -29,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(); @@ -232,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; } @@ -1114,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 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/engines/draw_manager.cpp b/src/video_core/engines/draw_manager.cpp index 974a9a6e9a..564220b71c 100644 --- a/src/video_core/engines/draw_manager.cpp +++ b/src/video_core/engines/draw_manager.cpp @@ -313,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()) { @@ -321,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/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 4ef7d42979..92313988bd 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -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) { @@ -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 c5d1eb3230..7646ddbbe3 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -3064,6 +3064,7 @@ 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; @@ -3352,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/gpu.cpp b/src/video_core/gpu.cpp index 444dc6d015..6dbabcb43a 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -232,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); diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h index f65b6daf2c..b9a0225094 100644 --- a/src/video_core/renderer_vulkan/vk_buffer_cache.h +++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h @@ -169,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; @@ -278,6 +303,8 @@ private: 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_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index a3cd93484e..0223f56ff1 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1072,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/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/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/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 02b36cf9cf..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" @@ -458,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 @@ -1965,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) { diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index a3b030a6a6..d6b0e5b312 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -503,6 +503,7 @@ private: #endif MultiplayerState* multiplayer_state = nullptr; + bool use_recommended_mkw_settings = false; GRenderWindow* render_window = nullptr; GameList* game_list = nullptr; |
