aboutsummaryrefslogtreecommitdiff
path: root/src/tests/core
diff options
context:
space:
mode:
authoryumekiru <338525349+yumekiru@users.noreply.github.com>2026-10-28 21:21:11 -0400
committeryumekiru <338525349+yumekiru@users.noreply.github.com>2026-10-28 21:21:11 -0400
commit535b6769709eb22333af26269f1b16f37adce4db (patch)
treeda827ff9c4f2f9a5309ee2804ee689f608c0ac2f /src/tests/core
parent1af7fe72927af9fc64aa4af117bc9b30043c4dc7 (diff)
Import experimental NS2 support and accumulated GPU fixes
Bring the NS2 patch and local follow-up fixes onto Eglantine master. Includes native graphics services, instruction translation, syncpoint cancellation acknowledgement, texture handling and diagnostics. Experimental checkpoint; not ready to merge. The original Eden build passed, but this rebased branch has not been built or tested. Assisted-by: OpenAI Codex (implementation, debugging, tests and PR preparation)
Diffstat (limited to 'src/tests/core')
-rw-r--r--src/tests/core/graphics_supervisor.cpp133
-rw-r--r--src/tests/core/native_gpu_characteristics.cpp389
-rw-r--r--src/tests/core/program_metadata.cpp151
3 files changed, 673 insertions, 0 deletions
diff --git a/src/tests/core/graphics_supervisor.cpp b/src/tests/core/graphics_supervisor.cpp
new file mode 100644
index 0000000000..08e272f09a
--- /dev/null
+++ b/src/tests/core/graphics_supervisor.cpp
@@ -0,0 +1,133 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include "core/hle/service/am/service/gpu_memory_registration.h"
+#include <catch2/catch_test_macros.hpp>
+#include "core/hle/service/am/service/graphics_supervisor_protocol.h"
+#include "core/hle/service/am/service/graphics_syncpoint_cache.h"
+#include "core/hle/service/am/service/native_image_layout.h"
+
+using Service::AM::GraphicsSupervisorProtocol::DecodeBootstrap;
+
+TEST_CASE("Graphics supervisor decodes the captured TIPC bootstrap", "[graphics-supervisor]") {
+ const std::array<u32, 16> packet{0x14, 0x8000000c, 2, 0xffff8001,
+ 0x51, 0, 5, 0x80000000};
+ const auto request = DecodeBootstrap(packet);
+ REQUIRE(request.has_value());
+ REQUIRE(request->process_handle == 0xffff8001);
+ REQUIRE(request->applet_resource_user_id == 0x51);
+ REQUIRE(request->flags == 0x8000000000000005ULL);
+ REQUIRE(request->parameters == std::array<u64, 4>{});
+ for (size_t length = 0; length < packet.size(); ++length) {
+ REQUIRE_FALSE(DecodeBootstrap(std::span{packet}.first(length)));
+ }
+}
+
+TEST_CASE("Graphics supervisor rejects incompatible packet layouts", "[graphics-supervisor]") {
+ const std::array<u32, 16> original{0x14, 0x8000000c, 2, 0xffff8001,
+ 0x51, 0, 5, 0x80000000};
+ for (const auto replacement : {0U, 0x15U, 0x10014U}) {
+ auto packet = original;
+ packet[0] = replacement;
+ REQUIRE_FALSE(DecodeBootstrap(packet));
+ }
+ for (const auto replacement : {0U, 0x8000000bU, 0x8000000dU}) {
+ auto packet = original;
+ packet[1] = replacement;
+ REQUIRE_FALSE(DecodeBootstrap(packet));
+ }
+ for (const auto replacement : {0U, 1U, 3U, 0x20U}) {
+ auto packet = original;
+ packet[2] = replacement;
+ REQUIRE_FALSE(DecodeBootstrap(packet));
+ }
+}
+
+
+TEST_CASE("Graphics syncpoint cache extends real counter values across wraparound", "[graphics-supervisor]") {
+ using namespace Service::AM::GraphicsSyncpointCache;
+ REQUIRE(ExtendCounter(Unallocated, 7) == 7);
+ REQUIRE(ExtendCounter(7, 12) == 12);
+ REQUIRE(ExtendCounter(0xfffffffeULL, 0xffffffffU) == 0xffffffffULL);
+ REQUIRE(ExtendCounter(0xffffffffULL, 0) == 0x100000000ULL);
+ REQUIRE(ExtendCounter(0x1ffffffffULL, 3) == 0x200000003ULL);
+ REQUIRE(Header[0] == 1);
+ REQUIRE(Header[2] + 1 == CounterCount);
+ REQUIRE(Header[3] % 8 == 0);
+ REQUIRE(Header[3] + CounterCount * 8 <= Header[5]);
+ REQUIRE(Header[7] + GroupCount * 8 <= 0x2000);
+}
+
+TEST_CASE("Graphics syncpoint cache retains wrap history across unallocated snapshots", "[graphics-supervisor]") {
+ using namespace Service::AM::GraphicsSyncpointCache;
+ CounterValue counter;
+ REQUIRE(counter.Observe(std::nullopt) == Unallocated);
+ REQUIRE(counter.Observe(0xfffffffeU) == 0xfffffffeULL);
+ REQUIRE(counter.Observe(1U) == 0x100000001ULL);
+ REQUIRE(counter.Observe(std::nullopt) == Unallocated);
+ REQUIRE(counter.Observe(3U) == 0x100000003ULL);
+}
+
+
+TEST_CASE("Native block image storage covers every padded GOB", "[graphics-supervisor]") {
+ using Service::AM::CalculateNativeImageRequirements;
+ std::array<u32, 16> preference{0, 2, 0x0437077f, 0, 7, 120, 1, 1};
+ const auto requirements = CalculateNativeImageRequirements(preference, 1);
+ REQUIRE(requirements.has_value());
+ REQUIRE(requirements->size == 8847360);
+ REQUIRE(requirements->alignment == 8192);
+ REQUIRE(requirements->size % requirements->alignment == 0);
+ preference[1] = 1; // The GPU descriptor uses the same block storage.
+ REQUIRE(CalculateNativeImageRequirements(preference, 1)->size == requirements->size);
+ preference[2] = 0; // A one-pixel image still occupies a complete block.
+ preference[5] = 1;
+ REQUIRE(CalculateNativeImageRequirements(preference, 1)->size == 8192);
+}
+
+TEST_CASE("Native block image sizing rejects unsupported layouts and undersized pitches", "[graphics-supervisor]") {
+ using Service::AM::CalculateNativeImageRequirements;
+ const std::array<u32, 16> original{0, 2, 0x0437077f, 0, 7, 120, 1, 1};
+ for (const auto count : {0U, 2U, 0xffffffffU}) {
+ REQUIRE_FALSE(CalculateNativeImageRequirements(original, count));
+ }
+ for (const auto index : {0, 1, 3, 6, 7, 8, 15}) {
+ auto preference = original;
+ preference[index] ^= 4;
+ REQUIRE_FALSE(CalculateNativeImageRequirements(preference, 1));
+ }
+ for (const auto block : {0U, 2U, 9U, 32U, 0xffffffffU}) {
+ auto preference = original;
+ preference[4] = block;
+ REQUIRE_FALSE(CalculateNativeImageRequirements(preference, 1));
+ }
+ for (const auto pitch : {0U, 119U, 4097U, 0xffffffffU}) {
+ auto preference = original;
+ preference[5] = pitch;
+ REQUIRE_FALSE(CalculateNativeImageRequirements(preference, 1));
+ }
+ REQUIRE_FALSE(CalculateNativeImageRequirements(std::span{original}.first(15), 1));
+}
+
+TEST_CASE("GPU memory registration ignores padding around its byte selectors", "[graphics-supervisor]") {
+ std::array<u32, 8> packet{0x75, 6, 0x1fdd6000, 0x21, 0x1fddcfff, 0x21, 0xaabbcc00, 0x101};
+ const auto request = Service::AM::DecodeGpuMemoryRegistration(packet);
+ REQUIRE(request.has_value());
+ REQUIRE(request->begin == 0x211fdd6000ULL);
+ REQUIRE(request->end == 0x211fddcfffULL);
+ REQUIRE(request->flags == 0);
+ REQUIRE(request->device == 1);
+ packet[7] = 0xffffff02;
+ REQUIRE(Service::AM::DecodeGpuMemoryRegistration(packet)->device == 2);
+ packet[0] = 0x76;
+ REQUIRE_FALSE(Service::AM::DecodeGpuMemoryRegistration(packet));
+ REQUIRE_FALSE(Service::AM::DecodeGpuMemoryRegistration(std::span<const u32>{packet.data(), 7}));
+}
+
+TEST_CASE("Native presentation slots ignore leftover IPC padding", "[graphics-supervisor]") {
+ using Service::AM::GraphicsSupervisorProtocol::DecodeBufferSlot;
+ REQUIRE(DecodeBufferSlot(0) == 0);
+ REQUIRE(DecodeBufferSlot(0x4f434601) == 1);
+ REQUIRE(DecodeBufferSlot(0xffffff0e) == 14);
+ REQUIRE(DecodeBufferSlot(0x4f43460f) == 15);
+}
diff --git a/src/tests/core/native_gpu_characteristics.cpp b/src/tests/core/native_gpu_characteristics.cpp
new file mode 100644
index 0000000000..cb79853ac2
--- /dev/null
+++ b/src/tests/core/native_gpu_characteristics.cpp
@@ -0,0 +1,389 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <memory>
+#include "video_core/macro.h"
+#include "video_core/native_qmd.h"
+#include "video_core/dirty_flags.h"
+#include <catch2/catch_test_macros.hpp>
+#include "core/hle/kernel/k_memory_block.h"
+#include "core/hle/service/nvdrv/devices/native_gpu_kind.h"
+#include "core/hle/service/nvdrv/devices/native_gpu_context.h"
+#include "core/hle/service/nvdrv/devices/native_gpu_characteristics.h"
+#include "core/hle/service/nvdrv/devices/gpu_address_space_layout.h"
+
+TEST_CASE("Native GPU characteristics replace topology with ioctl interface levels",
+ "[native-gpu]") {
+ std::array<u8, 0xa0> legacy;
+ legacy.fill(0xcc);
+ const u32_le arch{0x120};
+ const u32_le impl{0xb};
+ std::memcpy(legacy.data(), &arch, sizeof(arch));
+ std::memcpy(legacy.data() + 4, &impl, sizeof(impl));
+ const u32_le available{0x30000};
+ std::memcpy(legacy.data() + 0x34, &available, sizeof(available));
+ const auto native = Service::Nvidia::Devices::MakeNativeGpuCharacteristics(legacy);
+ REQUIRE(native.size() == 0xd0);
+ u32_le native_arch, native_impl;
+ std::memcpy(&native_arch, native.data(), sizeof(native_arch));
+ std::memcpy(&native_impl, native.data() + 4, sizeof(native_impl));
+ REQUIRE(static_cast<u32>(native_arch) == 0x170);
+ REQUIRE(static_cast<u32>(native_impl) == 0xe);
+ REQUIRE(std::equal(legacy.begin() + 8, legacy.begin() + 0x28, native.begin() + 8));
+ REQUIRE(std::equal(legacy.begin() + 0x2c, legacy.begin() + 0x34, native.begin() + 0x2c));
+ REQUIRE(std::equal(legacy.begin() + 0x38, legacy.begin() + 0x3c, native.begin() + 0x38));
+ REQUIRE(std::equal(legacy.begin() + 0x44, legacy.begin() + 0x50, native.begin() + 0x44));
+ REQUIRE(std::equal(legacy.begin() + 0x58, legacy.begin() + 0x70, native.begin() + 0x58));
+ u32_le sm_version, spa_version;
+ std::memcpy(&sm_version, native.data() + 0x3c, sizeof(sm_version));
+ std::memcpy(&spa_version, native.data() + 0x40, sizeof(spa_version));
+ REQUIRE(static_cast<u32>(sm_version) == 0x807);
+ REQUIRE(static_cast<u32>(spa_version) == 0x807);
+ u32_le available_sizes;
+ std::memcpy(&available_sizes, native.data() + 0x34, sizeof(available_sizes));
+ REQUIRE(static_cast<u32>(available_sizes) == 0x11000);
+ u32_le big_page_size;
+ std::memcpy(&big_page_size, native.data() + 0x28, sizeof(big_page_size));
+ REQUIRE(static_cast<u32>(big_page_size) == 0x1000);
+ const auto level = [&](size_t offset) {
+ u16_le value;
+ std::memcpy(&value, native.data() + offset, sizeof(value));
+ return static_cast<s16>(static_cast<u16>(value));
+ };
+ REQUIRE(level(0x70) == 0x1c);
+ REQUIRE(level(0x72) == 2);
+ u64_le flags;
+ std::memcpy(&flags, native.data() + 0x50, sizeof(flags));
+ REQUIRE((static_cast<u64>(flags) & (1ULL << 8)) != 0);
+ REQUIRE((static_cast<u64>(flags) & ~((1ULL << 8) | (1ULL << 26) | (1ULL << 34) | (0xfULL << 40))) == (0xccccccccccccccccULL & ~((1ULL << 8) | (1ULL << 26) | (1ULL << 34) | (0xfULL << 40))));
+ REQUIRE((static_cast<u64>(flags) & (0xfULL << 40)) == (0xfULL << 40));
+ REQUIRE((static_cast<u64>(flags) & (1ULL << 34)) != 0);
+ REQUIRE(level(0x74) == -1);
+ REQUIRE(level(0x76) == 0x1d);
+ // The captured SDK rejects the old topology value 2 here, requiring >= 6.
+ REQUIRE(level(0x78) == 0x14);
+ REQUIRE(std::all_of(native.begin() + 0x7a, native.end(), [](u8 b) { return b == 0; }));
+}
+
+TEST_CASE("Native GPU VA layout accommodates the captured NVN bootstrap reservation",
+ "[native-gpu]") {
+ using Service::Nvidia::Devices::GetGpuAddressSpaceLayout;
+ constexpr auto native = GetGpuAddressSpaceLayout(true);
+ constexpr auto legacy = GetGpuAddressSpaceLayout(false);
+ REQUIRE(native.split - native.start >= 0x7ff8000000ULL + (1ULL << 32));
+ REQUIRE(native.start == legacy.start);
+ REQUIRE(native.end == 1ULL << 40);
+ REQUIRE(native.end > native.split);
+ REQUIRE(legacy.split == 1ULL << 34);
+ REQUIRE(legacy.end == 1ULL << 37);
+}
+
+TEST_CASE("GPU region addresses retain bits above the 32-bit page index", "[native-gpu]") {
+ using Service::Nvidia::Devices::GpuPageAddress;
+ REQUIRE(GpuPageAddress(0x400000, 17) == 0x8000000000ULL);
+ REQUIRE(GpuPageAddress(0x20000, 17) == 0x400000000ULL);
+ REQUIRE(GpuPageAddress(0x8000, 12) == 0x8000000ULL);
+}
+
+TEST_CASE("GPU memory eligibility survives splitting and remains separate from cache attributes",
+ "[native-gpu]") {
+ using namespace Kernel;
+ KMemoryBlock right{KProcessAddress{0x10000}, 4, KMemoryState::Normal,
+ KMemoryPermission::UserReadWrite, KMemoryAttribute::Uncached};
+ right.UpdateAttribute(KMemoryAttribute::GpuAccessible, KMemoryAttribute::GpuAccessible);
+ KMemoryBlock left;
+ right.Split(&left, KProcessAddress{0x12000});
+ REQUIRE(left.GetNumPages() == 2);
+ REQUIRE(right.GetNumPages() == 2);
+ REQUIRE(left.GetMemoryInfo().GetSvcMemoryInfo().attribute ==
+ (Svc::MemoryAttribute::Uncached | Svc::MemoryAttribute::GpuAccessible));
+ REQUIRE(right.GetAttribute() == left.GetAttribute());
+ right.UpdateAttribute(KMemoryAttribute::GpuAccessible, KMemoryAttribute::None);
+ REQUIRE(right.GetAttribute() == KMemoryAttribute::Uncached);
+ REQUIRE(left.GetAttribute() ==
+ (KMemoryAttribute::Uncached | KMemoryAttribute::GpuAccessible));
+ REQUIRE(right.GetPermission() == KMemoryPermission::UserReadWrite);
+ REQUIRE(right.GetState() == KMemoryState::Normal);
+}
+
+TEST_CASE("Native 4 KiB allocation preference retains a separate large-page GPU pool", "[native-gpu]") {
+ using Service::Nvidia::Devices::GetGpuAddressSpaceBigPageSize;
+ REQUIRE(GetGpuAddressSpaceBigPageSize(true, 0x1000) == 0x10000);
+ REQUIRE(GetGpuAddressSpaceBigPageSize(true, 0x20000) == 0x20000);
+ REQUIRE(GetGpuAddressSpaceBigPageSize(false, 0x1000) == 0x1000);
+ REQUIRE(GetGpuAddressSpaceBigPageSize(false, 0) == 0);
+}
+
+TEST_CASE("Native generic memory kind is translated rather than treated as legacy depth memory",
+ "[native-gpu]") {
+ using Service::Nvidia::Devices::TranslateNativeUncompressedKind;
+ REQUIRE(TranslateNativeUncompressedKind(0, 0) == static_cast<u8>(Tegra::PTEKind::PITCH));
+ REQUIRE(TranslateNativeUncompressedKind(6, 6) == static_cast<u8>(Tegra::PTEKind::GENERIC_16BX2));
+ REQUIRE(TranslateNativeUncompressedKind(6, 6) != static_cast<u8>(Tegra::PTEKind::Z16_MS16_2C));
+ REQUIRE_FALSE(TranslateNativeUncompressedKind(8, 6));
+ REQUIRE_FALSE(TranslateNativeUncompressedKind(7, 7));
+ REQUIRE_FALSE(TranslateNativeUncompressedKind(0xfe, 0xfe));
+}
+
+TEST_CASE("Native mappings report the actual selected memory kind", "[native-gpu]") {
+ using Service::Nvidia::Devices::SelectNativeMemoryKind;
+ const auto normal = SelectNativeMemoryKind(6, 6);
+ REQUIRE(normal.has_value());
+ REQUIRE(normal->selected == 6);
+ REQUIRE(normal->backend == 0xfe);
+ const auto fallback = SelectNativeMemoryKind(8, 6);
+ REQUIRE(fallback.has_value());
+ REQUIRE(fallback->selected == 6);
+ REQUIRE(fallback->backend == normal->backend);
+ REQUIRE(SelectNativeMemoryKind(0, 0)->selected == 0);
+ const auto depth = SelectNativeMemoryKind(0xc, 3);
+ REQUIRE(depth.has_value());
+ REQUIRE(depth->selected == 3);
+ REQUIRE(depth->backend == static_cast<u8>(Tegra::PTEKind::S8Z24));
+ REQUIRE(SelectNativeMemoryKind(3, 3)->backend == depth->backend);
+ const auto depth16 = SelectNativeMemoryKind(0xb, 1);
+ REQUIRE(depth16.has_value());
+ REQUIRE(depth16->selected == 1);
+ REQUIRE(depth16->backend == static_cast<u8>(Tegra::PTEKind::Z16));
+ REQUIRE(SelectNativeMemoryKind(1, 1)->backend == depth16->backend);
+ REQUIRE_FALSE(SelectNativeMemoryKind(0xb, 6));
+ REQUIRE_FALSE(SelectNativeMemoryKind(0xc, 6));
+ REQUIRE_FALSE(SelectNativeMemoryKind(8, 8));
+ REQUIRE_FALSE(SelectNativeMemoryKind(8, 0));
+ REQUIRE_FALSE(SelectNativeMemoryKind(9, 6));
+}
+
+TEST_CASE("Native GPU context allocations classify explicit known engine identities", "[native-gpu]") {
+ using Service::Nvidia::Devices::GetNativeGpuContextSlot;
+ REQUIRE(GetNativeGpuContextSlot(0xc797) == 1U);
+ REQUIRE(GetNativeGpuContextSlot(0xc7c0) == 2U);
+ REQUIRE_FALSE(GetNativeGpuContextSlot(0xa143));
+ REQUIRE(GetNativeGpuContextSlot(0xc7b5) == 4U);
+ REQUIRE(GetNativeGpuContextSlot(0xc56f) == 5U);
+ REQUIRE_FALSE(GetNativeGpuContextSlot(0));
+ REQUIRE_FALSE(GetNativeGpuContextSlot(0xb197));
+ REQUIRE_FALSE(GetNativeGpuContextSlot(0xffffc797));
+ REQUIRE_FALSE(GetNativeGpuContextSlot(0xdeadbeef));
+}
+
+TEST_CASE("Native MME uploads use instruction addresses and bypass Maxwell code storage", "[native-gpu]") {
+ Tegra::MacroEngine engine{true};
+ engine.native_turing = true;
+ engine.ClearCode(8);
+ engine.AddCode(8, 0x11223344);
+ engine.AddCode(8, 0x55667788);
+ engine.AddCode(8, 0x99aabbcc);
+ REQUIRE(engine.native_code[8] == 0);
+ REQUIRE(engine.native_code[24] == 0x11223344);
+ REQUIRE(engine.native_code[25] == 0x55667788);
+ REQUIRE(engine.native_code[26] == 0x99aabbcc);
+ REQUIRE(engine.uploaded_macro_code.empty());
+ engine.ClearCode(0xffffffff);
+ engine.AddCode(0xffffffff, 1);
+ REQUIRE(engine.native_code[24] == 0x11223344);
+ engine.native_turing = false;
+ engine.AddCode(8, 7);
+ REQUIRE(engine.uploaded_macro_code.at(8).front() == 7);
+}
+
+TEST_CASE("Native MME loops consume the exact parameter count, including zero iterations", "[native-gpu]") {
+ Tegra::TuringMacroInterpreter interpreter;
+ constexpr std::array<u32, 15> code{
+ 0x00000003U, 0x18c00000U, 0x31c00300U,
+ 0x00000003U, 0x18c00000U, 0xb00c5f00U,
+ 0x00740003U, 0x18c00740U, 0x318c0300U,
+ 0x00000003U, 0x18c00000U, 0x318c0301U,
+ 0x00000003U, 0x18c00000U, 0x318c0300U,
+ };
+ std::vector<u32> output;
+ auto emit = [&](u32 method, u32 value) { REQUIRE(method == 0xd00); output.push_back(value); return true; };
+ auto read = [](u32) { return 0U; };
+ const std::array<u32, 1> zero{0};
+ auto result = interpreter.Execute(code, 0, zero, read, emit);
+ REQUIRE(result.success);
+ REQUIRE(result.consumed == 1);
+ REQUIRE(output.empty());
+ const std::array<u32, 3> two{2, 99, 88};
+ result = interpreter.Execute(code, 0, two, read, emit);
+ REQUIRE(result.success);
+ REQUIRE(result.consumed == 3);
+ REQUIRE(output == std::vector<u32>{99, 88});
+ result = interpreter.Execute(code, 0, std::span<const u32>{}, read, emit);
+ REQUIRE_FALSE(result.success);
+ REQUIRE(result.error == "parameter FIFO underflow");
+ result = interpreter.Execute(code, 5, zero, read, emit);
+ REQUIRE_FALSE(result.success);
+ REQUIRE(result.error == "instruction outside uploaded RAM");
+}
+
+TEST_CASE("Native MME pairs ALU carry with two incrementing outputs", "[native-gpu]") {
+ Tegra::TuringMacroInterpreter interpreter;
+ constexpr std::array<u32, 6> code{
+ 0x20340003U, 0x18084740U, 0x3bc00301U,
+ 0x00000003U, 0x18c00000U, 0x318c0300U,
+ };
+ const std::array<u32, 2> params{0xffffffffU, 1};
+ std::vector<std::pair<u32, u32>> writes;
+ const auto result = interpreter.Execute(code, 0, params, [](u32) { return 0U; },
+ [&](u32 method, u32 value) { writes.emplace_back(method, value); return true; });
+ REQUIRE(result.success);
+ REQUIRE(result.consumed == 2);
+ REQUIRE(writes == std::vector<std::pair<u32, u32>>{{0xd00, 0}, {0xd01, 1}});
+}
+
+TEST_CASE("Native empty QMDs release only after their dependency signals", "[native-gpu]") {
+ Tegra::NativeQmdScheduler scheduler;
+ Tegra::NativeQmdScheduler::Descriptor descriptor{};
+ descriptor[4] = 0x8040; // Captured QMD: two dependencies, no CTAs.
+ descriptor[18] = 0x00010030;
+ descriptor[24] = 0x100000;
+ descriptor[25] = 0x40880021; // One-word release, sysmembar, upper address 0x21.
+ descriptor[26] = 0;
+ u32 releases{};
+ const auto read = [&](u64, auto& out) { out = descriptor; return true; };
+ const auto release = [&](u64 address, u32 value) {
+ REQUIRE(address == 0x2100100000ULL);
+ REQUIRE(value == 42);
+ ++releases;
+ return true;
+ };
+ REQUIRE(scheduler.Signal(0x2100200000ULL, 3, read, release).empty());
+ REQUIRE(releases == 0);
+ REQUIRE(scheduler.Signal(0x2100200000ULL, 7, read, release).empty());
+ REQUIRE(releases == 0);
+ descriptor[26] = 42;
+ REQUIRE(scheduler.Signal(0x2100200000ULL, 7, read, release).empty());
+ REQUIRE(releases == 1);
+}
+
+TEST_CASE("Native QMD scheduler rejects real shader work without reporting completion", "[native-gpu]") {
+ Tegra::NativeQmdScheduler scheduler;
+ Tegra::NativeQmdScheduler::Descriptor descriptor{};
+ descriptor[4] = 1U << 14;
+ descriptor[18] = 0x30;
+ descriptor[12] = descriptor[13] = descriptor[14] = 1;
+ u32 releases{};
+ const auto result = scheduler.Signal(0x10000, 3,
+ [&](u64, auto& out) { out = descriptor; return true; },
+ [&](u64, u32) { ++releases; return true; });
+ REQUIRE(result == "native shader dispatch required");
+ REQUIRE(releases == 0);
+}
+
+TEST_CASE("Native dependent QMD signals survive until the child is uploaded", "[native-gpu]") {
+ Tegra::NativeQmdScheduler scheduler;
+ Tegra::NativeQmdScheduler::Descriptor parent{}, child{};
+ parent[4] = 1U << 14;
+ parent[18] = 0x30;
+ parent[15] = 0x200;
+ parent[16] = 9; // Enabled child decrement-dependence operation.
+ child[4] = 2U << 14;
+ child[18] = 0x30;
+ child[24] = 0x30000;
+ child[25] = 0x40880000;
+ child[26] = 99;
+ u32 releases{};
+ const auto read = [&](u64 address, auto& out) { out = address == 0x10000 ? parent : child; return true; };
+ const auto release = [&](u64 address, u32 value) {
+ REQUIRE(address == 0x30000);
+ REQUIRE(value == 99);
+ ++releases;
+ return true;
+ };
+ REQUIRE(scheduler.Signal(0x10000, 3, read, release).empty());
+ REQUIRE(scheduler.Signal(0x10000, 7, read, release).empty());
+ REQUIRE(releases == 0);
+ REQUIRE(scheduler.Signal(0x20000, 3, read, release).empty());
+ REQUIRE(releases == 0);
+ REQUIRE(scheduler.Signal(0x20000, 7, read, release).empty());
+ REQUIRE(releases == 1);
+}
+
+TEST_CASE("Native MME dual ALUs sample registers before either writeback", "[native-gpu]") {
+ Tegra::TuringMacroInterpreter interpreter;
+ constexpr std::array<u32, 12> code{
+ 0x00000003U, 0x18c00000U, 0x31c00300U,
+ 0x00000003U, 0x40c7c001U, 0xf1c00300U,
+ 0x00380483U, 0x18c00000U, 0xf190fb01U,
+ 0x00000003U, 0x18c00000U, 0x318c0300U,
+ };
+ const std::array<u32, 2> parameters{3, 0x1000000};
+ std::vector<std::pair<u32, u32>> writes;
+ const auto result = interpreter.Execute(code, 0, parameters, [](u32) { return 0U; },
+ [&](u32 method, u32 value) { writes.emplace_back(method, value); return true; });
+ REQUIRE(result.success);
+ REQUIRE(result.consumed == 2);
+ REQUIRE(writes == std::vector<std::pair<u32, u32>>{{0x120, 7}});
+}
+
+TEST_CASE("Native MME taken back edges suppress fallthrough termination", "[native-gpu]") {
+ Tegra::TuringMacroInterpreter interpreter;
+ constexpr std::array<u32, 12> code{
+ 0x00000003U, 0x18c00000U, 0x31c00300U,
+ 0x00383403U, 0x18c00000U, 0x72000b00U,
+ 0x00000003U, 0x18c017ffU, 0xf00c7701U,
+ 0x00000003U, 0x18c00000U, 0x318c0300U,
+ };
+ for (const u32 count : {1U, 3U, 16U}) {
+ std::vector<u32> writes;
+ const std::array<u32, 1> parameters{count};
+ const auto result = interpreter.Execute(code, 0, parameters, [](u32) { return 0U; },
+ [&](u32 method, u32 value) { REQUIRE(method == 0xd00); writes.push_back(value); return true; });
+ REQUIRE(result.success);
+ REQUIRE(result.consumed == 1);
+ REQUIRE(writes.size() == count);
+ REQUIRE(writes.front() == count - 1);
+ REQUIRE(writes.back() == 0);
+ }
+}
+
+TEST_CASE("Native compute QMD releases follow successful dispatch and retain reduction flags", "[native-gpu]") {
+ Tegra::NativeQmdScheduler scheduler;
+ Tegra::NativeQmdScheduler::Descriptor descriptor{};
+ descriptor[18] = 0x30;
+ descriptor[12] = descriptor[13] = descriptor[14] = 1;
+ descriptor[24] = 0x10000;
+ descriptor[25] = 0x44800021;
+ descriptor[26] = 1;
+ std::vector<int> order;
+ const auto read = [&](u64, auto& out) { out = descriptor; return true; };
+ const auto release = [&](u64 address, u32 value, u32 flags) {
+ REQUIRE(address == 0x2100010000ULL);
+ REQUIRE(value == 1);
+ REQUIRE(flags == 0x44800021);
+ order.push_back(2);
+ return true;
+ };
+ REQUIRE(scheduler.Signal(0x20000, 3, read, release,
+ [&](const auto&) { order.push_back(1); return false; }) == "native shader dispatch required");
+ REQUIRE(order == std::vector<int>{1});
+ order.clear();
+ REQUIRE(scheduler.Signal(0x20000, 3, read, release,
+ [&](const auto&) { order.push_back(1); return true; }).empty());
+ REQUIRE(order == std::vector<int>{1, 2});
+}
+
+TEST_CASE("Native vertex stream sizes use byte counts and dirty the selected stream", "[native-gpu]") {
+ using Maxwell3D = Tegra::Engines::Maxwell3D;
+ auto regs = std::make_unique<Maxwell3D::Regs>();
+ regs->reg_array.fill(0);
+ regs->reg_array[0x600 / 4 + 1] = 0x1b4e8;
+ REQUIRE(regs->NativeVertexStreamSize(0) == 111848);
+ REQUIRE(regs->vertex_stream_limits[0].Address() == 0);
+ regs->reg_array[0x600 / 4 + 31 * 2] = 0xffffff81;
+ regs->reg_array[0x600 / 4 + 31 * 2 + 1] = 0x76543210;
+ REQUIRE(regs->NativeVertexStreamSize(31) == 0x8176543210ULL);
+ REQUIRE(regs->NativeVertexStreamSize(1) == 0);
+ Maxwell3D::DirtyState::Tables tables{};
+ VideoCommon::Dirty::SetupDirtyFlags(tables);
+ for (u32 stream = 0; stream < 32; ++stream) {
+ for (u32 word = 0; word < 2; ++word) {
+ const u32 method = 0x600 / 4 + stream * 2 + word;
+ REQUIRE(tables[0][method] == VideoCommon::Dirty::VertexBuffer0 + stream);
+ REQUIRE(tables[1][method] == VideoCommon::Dirty::VertexBuffers);
+ }
+ }
+}
diff --git a/src/tests/core/program_metadata.cpp b/src/tests/core/program_metadata.cpp
new file mode 100644
index 0000000000..b5af9953f4
--- /dev/null
+++ b/src/tests/core/program_metadata.cpp
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <cstring>
+#include <memory>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/generators/catch_generators.hpp>
+
+#include "common/swap.h"
+#include "core/file_sys/program_metadata.h"
+#include "core/file_sys/romfs.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+#include "core/loader/deconstructed_rom_directory.h"
+#include "core/loader/loader.h"
+
+namespace {
+template <typename T>
+void Put(std::vector<u8>& bytes, std::size_t offset, T value) {
+ std::memcpy(bytes.data() + offset, &value, sizeof(value));
+}
+
+std::vector<u8> MakeMetadata(bool extended) {
+ std::vector<u8> bytes(0x600);
+ std::memcpy(bytes.data(), "META", 4);
+ bytes[0xC] = 7;
+ Put(bytes, 0x1C, u32_le{0x100000});
+ Put(bytes, 0x70, u32_le{0x500});
+ Put(bytes, 0x74, u32_le{0x100});
+ Put(bytes, 0x78, u32_le{0x80});
+ Put(bytes, 0x7C, u32_le{0x380});
+ const std::size_t acid = 0x80 + (extended ? 0x300 : 0x200);
+ const u32 fac = extended ? 0x340 : 0x240;
+ std::memcpy(bytes.data() + acid, "ACID", 4);
+ Put(bytes, acid + 0x20, u32_le{fac});
+ Put(bytes, acid + 0x24, u32_le{0x2C});
+ Put(bytes, 0x80 + fac + 4, u64_le{0x1234});
+ std::memcpy(bytes.data() + 0x500, "ACI0", 4);
+ Put(bytes, 0x510, u64_le{0x0100123456780000});
+ Put(bytes, 0x520, u32_le{0x40});
+ Put(bytes, 0x524, u32_le{0x1C});
+ Put(bytes, 0x530, u32_le{0x80});
+ Put(bytes, 0x534, u32_le{4});
+ Put(bytes, 0x544, u64_le{0x5678});
+ Put(bytes, 0x580, u32_le{0x030043F7});
+ return bytes;
+}
+} // namespace
+
+TEST_CASE("Program metadata reads both ACID layouts and relative access offsets", "[loader][npdm]") {
+ const bool extended = GENERATE(false, true);
+ FileSys::ProgramMetadata metadata;
+ REQUIRE(metadata.Load(std::make_shared<FileSys::VectorVfsFile>(MakeMetadata(extended))) ==
+ Loader::ResultStatus::Success);
+ REQUIRE(metadata.UsesExtendedAcid() == extended);
+ REQUIRE(metadata.GetTitleID() == 0x0100123456780000);
+ REQUIRE(metadata.GetFilesystemPermissions() == 0x5678);
+ REQUIRE(metadata.GetMainThreadStackSize() == 0x100000);
+ REQUIRE(metadata.GetKernelCapabilities() == std::vector<u32>{0x030043F7});
+}
+
+TEST_CASE("Program metadata rejects invalid sections", "[loader][npdm]") {
+ const bool extended = GENERATE(false, true);
+ auto bytes = MakeMetadata(extended);
+ const std::size_t acid = 0x80 + (extended ? 0x300 : 0x200);
+ Loader::ResultStatus expected{};
+ SECTION("NPDM magic") {
+ bytes[0] = 0;
+ expected = Loader::ResultStatus::ErrorBadNPDMHeader;
+ }
+ SECTION("ACID past EOF") {
+ Put(bytes, 0x78, u32_le{0xFFFFFFF0});
+ expected = Loader::ResultStatus::ErrorBadACIDHeader;
+ }
+ SECTION("ACID magic") {
+ bytes[acid] = 0;
+ expected = Loader::ResultStatus::ErrorBadACIDHeader;
+ }
+ SECTION("ACI magic") {
+ bytes[0x500] = 0;
+ expected = Loader::ResultStatus::ErrorBadACIHeader;
+ }
+ SECTION("FAH outside its section") {
+ Put(bytes, 0x520, u32_le{0xF0});
+ expected = Loader::ResultStatus::ErrorBadFileAccessHeader;
+ }
+ SECTION("FAC outside its section") {
+ Put(bytes, acid + 0x20, u32_le{0xFFFFFFF0});
+ expected = Loader::ResultStatus::ErrorBadFileAccessControl;
+ }
+ SECTION("Truncated FAH") {
+ Put(bytes, 0x524, u32_le{0x10});
+ expected = Loader::ResultStatus::ErrorBadFileAccessHeader;
+ }
+ SECTION("Capabilities outside their section") {
+ Put(bytes, 0x530, u32_le{0x100});
+ expected = Loader::ResultStatus::ErrorBadKernelCapabilityDescriptors;
+ }
+ SECTION("Unaligned capability count") {
+ Put(bytes, 0x534, u32_le{3});
+ expected = Loader::ResultStatus::ErrorBadKernelCapabilityDescriptors;
+ }
+ FileSys::ProgramMetadata metadata;
+ REQUIRE(metadata.Load(std::make_shared<FileSys::VectorVfsFile>(std::move(bytes))) == expected);
+}
+
+TEST_CASE("Reloading legacy metadata clears extended ACID state", "[loader][npdm]") {
+ FileSys::ProgramMetadata metadata;
+ REQUIRE(metadata.Load(std::make_shared<FileSys::VectorVfsFile>(MakeMetadata(true))) ==
+ Loader::ResultStatus::Success);
+ REQUIRE(metadata.UsesExtendedAcid());
+ REQUIRE(metadata.Load(std::make_shared<FileSys::VectorVfsFile>(MakeMetadata(false))) ==
+ Loader::ResultStatus::Success);
+ REQUIRE_FALSE(metadata.UsesExtendedAcid());
+ REQUIRE(metadata.Load(nullptr) == Loader::ResultStatus::ErrorBadNPDMHeader);
+}
+
+TEST_CASE("ExeFS main takes precedence over standalone NSO", "[loader][exefs]") {
+ auto directory = std::make_shared<FileSys::VectorVfsDirectory>();
+ auto main = std::make_shared<FileSys::VectorVfsFile>(std::vector<u8>{'N', 'S', 'O', '0'},
+ "main", directory);
+ directory->AddFile(main);
+ directory->AddFile(std::make_shared<FileSys::VectorVfsFile>(MakeMetadata(false), "main.npdm"));
+ REQUIRE(Loader::IdentifyFile(main) == Loader::FileType::DeconstructedRomDirectory);
+ directory->DeleteFile("main.npdm");
+ REQUIRE(Loader::AppLoader_DeconstructedRomDirectory::IdentifyType(main) == Loader::FileType::Error);
+}
+
+TEST_CASE("Extracted ExeFS exposes loose RomFS", "[loader][exefs]") {
+ const bool sibling = GENERATE(false, true);
+ auto root = std::make_shared<FileSys::VectorVfsDirectory>();
+ auto exefs = std::make_shared<FileSys::VectorVfsDirectory>(
+ std::vector<FileSys::VirtualFile>{}, std::vector<FileSys::VirtualDir>{}, "exefs", root);
+ root->AddDirectory(exefs);
+ auto main = std::make_shared<FileSys::VectorVfsFile>(std::vector<u8>{'N', 'S', 'O', '0'},
+ "main", exefs);
+ exefs->AddFile(main);
+ exefs->AddFile(std::make_shared<FileSys::VectorVfsFile>(MakeMetadata(false), "main.npdm"));
+ auto romfs = std::make_shared<FileSys::VectorVfsDirectory>(
+ std::vector<FileSys::VirtualFile>{}, std::vector<FileSys::VirtualDir>{}, "romfs");
+ romfs->AddFile(std::make_shared<FileSys::VectorVfsFile>(std::vector<u8>{1, 2, 3}, "asset.bin"));
+ (sibling ? root : exefs)->AddDirectory(romfs);
+ Loader::AppLoader_DeconstructedRomDirectory loader(main);
+ FileSys::VirtualFile image;
+ REQUIRE(loader.ReadRomFS(image) == Loader::ResultStatus::Success);
+ const auto extracted = FileSys::ExtractRomFS(image);
+ REQUIRE(extracted != nullptr);
+ const auto asset = extracted->GetFile("asset.bin");
+ REQUIRE(asset != nullptr);
+ REQUIRE(asset->ReadAllBytes() == std::vector<u8>{1, 2, 3});
+}