aboutsummaryrefslogtreecommitdiff
path: root/src/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests')
-rw-r--r--src/tests/CMakeLists.txt7
-rw-r--r--src/tests/common/game_presets.cpp111
-rw-r--r--src/tests/common/slot_vector.cpp54
-rw-r--r--src/tests/core/core_timing.cpp3
-rw-r--r--src/tests/core/nup_writer.cpp119
-rw-r--r--src/tests/core/priority_queue.cpp111
-rw-r--r--src/tests/input_common/calibration_configuration_job.cpp20
-rw-r--r--src/tests/video_core/descriptor_table.cpp170
-rw-r--r--src/tests/video_core/memory_tracker.cpp38
-rw-r--r--src/tests/video_core/native_macro_fifo.cpp74
-rw-r--r--src/tests/video_core/shader_invalidation.cpp120
-rw-r--r--src/tests/video_core/uniform_stream_reuse.cpp116
-rw-r--r--src/tests/video_core/vertex_repack.cpp33
13 files changed, 957 insertions, 19 deletions
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));
+}