aboutsummaryrefslogtreecommitdiff
path: root/src/tests
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
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')
-rw-r--r--src/tests/CMakeLists.txt8
-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
-rw-r--r--src/tests/video_core/image_overlap.cpp93
-rw-r--r--src/tests/video_core/native_semaphore.cpp63
-rw-r--r--src/tests/video_core/native_shader.cpp2841
-rw-r--r--src/tests/video_core/native_swizzle.cpp67
-rw-r--r--src/tests/video_core/pending_download.cpp112
9 files changed, 3857 insertions, 0 deletions
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 875c40604c..1bcb954334 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -17,7 +17,15 @@ add_executable(tests
common/undefined_fix.cpp
common/unique_function.cpp
core/core_timing.cpp
+ core/program_metadata.cpp
+ core/native_gpu_characteristics.cpp
+ core/graphics_supervisor.cpp
core/internal_network/network.cpp
+ video_core/native_semaphore.cpp
+ video_core/native_shader.cpp
+ video_core/native_swizzle.cpp
+ video_core/image_overlap.cpp
+ video_core/pending_download.cpp
video_core/memory_tracker.cpp
input_common/calibration_configuration_job.cpp
)
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});
+}
diff --git a/src/tests/video_core/image_overlap.cpp b/src/tests/video_core/image_overlap.cpp
new file mode 100644
index 0000000000..a4a08c72ea
--- /dev/null
+++ b/src/tests/video_core/image_overlap.cpp
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/generators/catch_generators.hpp>
+#include "video_core/texture_cache/image_base.h"
+#include "video_core/texture_cache/image_info.h"
+#include "video_core/texture_cache/image_view_info.h"
+#include "video_core/texture_cache/util.h"
+
+namespace {
+VideoCommon::ImageInfo ArrayInfo(bool native) {
+ VideoCommon::ImageInfo info;
+ info.type = VideoCommon::ImageType::e2D;
+ info.format = VideoCore::Surface::PixelFormat::A8B8G8R8_UNORM;
+ info.size = {64, 64, 1};
+ info.native_tiling = native;
+ info.layer_stride = VideoCommon::CalculateLayerStride(info);
+ info.maybe_unaligned_layer_stride = info.layer_stride;
+ return info;
+}
+}
+
+TEST_CASE("Repeated views inside an existing array do not grow its storage", "[texture-overlap]") {
+ const auto single = ArrayInfo(GENERATE(false, true));
+ auto storage = single;
+ storage.resources.layers = 3;
+ constexpr GPUVAddr gpu = 0x100000;
+ constexpr VAddr cpu = 0x200000;
+ for (u32 frame = 0; frame < 100; ++frame) {
+ const VideoCommon::ImageBase existing{storage, gpu, cpu};
+ const u32 offset = (1 + frame % 2) * single.layer_stride;
+ const auto joined = VideoCommon::ResolveOverlap(single, gpu + offset, cpu + offset,
+ existing, true, false, true);
+ REQUIRE(joined);
+ REQUIRE(joined->gpu_addr == gpu);
+ REQUIRE(joined->cpu_addr == cpu);
+ REQUIRE(joined->resources.layers == 3);
+ storage.resources = joined->resources;
+ REQUIRE(VideoCommon::CalculateUnswizzledSizeBytes(storage) == 64 * 64 * 4 * 3);
+ }
+}
+
+TEST_CASE("Array overlap extends only to the end of the new view", "[texture-overlap]") {
+ auto storage = ArrayInfo(GENERATE(false, true));
+ storage.resources.layers = 3;
+ auto view = storage;
+ view.resources.layers = 4;
+ constexpr GPUVAddr gpu = 0x100000;
+ constexpr VAddr cpu = 0x200000;
+ const VideoCommon::ImageBase existing{storage, gpu, cpu};
+ const u32 offset = 2 * storage.layer_stride;
+ const auto joined = VideoCommon::ResolveOverlap(view, gpu + offset, cpu + offset,
+ existing, true, false, true);
+ REQUIRE(joined);
+ REQUIRE(joined->resources.layers == 6);
+}
+
+TEST_CASE("Native texture views retain their direct address without a legacy array prefix", "[texture-overlap]") {
+ Tegra::Texture::TICEntry raw{};
+ raw.format.Assign(Tegra::Texture::TextureFormat::A8B8G8R8);
+ raw.r_type.Assign(Tegra::Texture::ComponentType::UNORM);
+ raw.g_type.Assign(Tegra::Texture::ComponentType::UNORM);
+ raw.b_type.Assign(Tegra::Texture::ComponentType::UNORM);
+ raw.a_type.Assign(Tegra::Texture::ComponentType::UNORM);
+ raw.header_version.Assign(Tegra::Texture::TICHeaderVersion::BlockLinear);
+ raw.block_height.Assign(4);
+ raw.width_minus_one.Assign(1919);
+ raw.height_minus_1.Assign(1079);
+ const bool array = GENERATE(false, true);
+ raw.texture_type.Assign(array ? Tegra::Texture::TextureType::Texture2DArray : Tegra::Texture::TextureType::Texture2D);
+ raw.depth_minus_1.Assign(array ? 2 : 0);
+ raw.normalized_coords.Assign(1);
+ raw.address_high.Assign(0x81);
+ raw.address_low = 0x870000;
+ raw.layer_base_0_2.Assign(1);
+ raw.layer_base_3_7.Assign(4);
+ REQUIRE(raw.BaseLayer() == 33);
+ REQUIRE(VideoCommon::TextureViewHeader(raw, false) == raw);
+ const auto native = VideoCommon::TextureViewHeader(raw, true);
+ REQUIRE(native.BaseLayer() == 0);
+ REQUIRE(native.Address() == raw.Address());
+ const VideoCommon::ImageInfo info(native);
+ REQUIRE(info.size.width == 1920);
+ REQUIRE(info.size.height == 1080);
+ REQUIRE(info.resources.layers == (array ? 3 : 1));
+ REQUIRE(VideoCommon::CalculateUnswizzledSizeBytes(info) == size_t{1920} * 1080 * 4 * (array ? 3U : 1U));
+ const VideoCommon::ImageViewInfo view(native, 0);
+ REQUIRE(view.range.base.layer == 0);
+ REQUIRE(view.type == (array ? VideoCommon::ImageViewType::e2DArray : VideoCommon::ImageViewType::e2D));
+ REQUIRE(view.range.extent.layers == (array ? 3 : 1));
+ REQUIRE(VideoCommon::ImageInfo(raw).resources.layers == (array ? 36 : 34));
+}
diff --git a/src/tests/video_core/native_semaphore.cpp b/src/tests/video_core/native_semaphore.cpp
new file mode 100644
index 0000000000..8dcafd91bc
--- /dev/null
+++ b/src/tests/video_core/native_semaphore.cpp
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <optional>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "common/common_types.h"
+#include "video_core/native_semaphore.h"
+
+TEST_CASE("Native semaphore predicates require the actual producer value", "[native-gpu]") {
+ const Tegra::NativeSemaphoreWait equal{0x1000, 840, 1};
+ REQUIRE_FALSE(equal.Matches(0));
+ REQUIRE_FALSE(equal.Matches(839));
+ REQUIRE(equal.Matches(840));
+ REQUIRE_FALSE(equal.Matches(841));
+ const Tegra::NativeSemaphoreWait gequal{0x1000, 840, 4};
+ REQUIRE_FALSE(gequal.Matches(0));
+ REQUIRE_FALSE(gequal.Matches(839));
+ REQUIRE(gequal.Matches(840));
+ REQUIRE(gequal.Matches(841));
+ const Tegra::NativeSemaphoreWait mask{0x1000, 0x24, 8};
+ REQUIRE_FALSE(mask.Matches(0));
+ REQUIRE_FALSE(mask.Matches(0x10));
+ REQUIRE(mask.Matches(4));
+ REQUIRE(mask.Matches(0x20));
+ REQUIRE_FALSE(Tegra::NativeSemaphoreWait::Supported(0));
+ REQUIRE_FALSE(Tegra::NativeSemaphoreWait::Supported(2));
+ REQUIRE_FALSE(Tegra::NativeSemaphoreWait{0x1000, 0, 2}.Matches(0));
+}
+
+TEST_CASE("Native channels resume in dependency order without completing blocked work", "[native-gpu]") {
+ const std::array<size_t, 3> channels{0, 1, 2};
+ std::array<std::optional<Tegra::NativeSemaphoreWait>, 3> waits{
+ Tegra::NativeSemaphoreWait{0x1000, 1, 4},
+ Tegra::NativeSemaphoreWait{0x2000, 1, 1},
+ Tegra::NativeSemaphoreWait{0x3000, 1, 4},
+ };
+ std::array<u32, 3> words{};
+ std::array<u32, 3> following_commands{};
+ std::vector<size_t> order;
+ const auto ready = [&](size_t channel) {
+ return waits[channel] && waits[channel]->Matches(words[channel]);
+ };
+ const auto dispatch = [&](size_t channel) {
+ REQUIRE(ready(channel));
+ waits[channel].reset();
+ ++following_commands[channel];
+ order.push_back(channel);
+ if (channel) words[channel - 1] = 1;
+ };
+ REQUIRE(Tegra::ResumeReadyNativeChannels(channels, ready, dispatch) == 0);
+ REQUIRE(order.empty());
+ REQUIRE(following_commands == std::array<u32, 3>{0, 0, 0});
+ // A real independent producer releases the last channel. Its remaining
+ // FIFO commands release the next channel, then the original consumer.
+ words[2] = 1;
+ REQUIRE(Tegra::ResumeReadyNativeChannels(channels, ready, dispatch) == 3);
+ REQUIRE(order == std::vector<size_t>{2, 1, 0});
+ REQUIRE(following_commands == std::array<u32, 3>{1, 1, 1});
+ REQUIRE(Tegra::ResumeReadyNativeChannels(channels, ready, dispatch) == 0);
+ REQUIRE(following_commands == std::array<u32, 3>{1, 1, 1});
+}
diff --git a/src/tests/video_core/native_shader.cpp b/src/tests/video_core/native_shader.cpp
new file mode 100644
index 0000000000..76424b7dbf
--- /dev/null
+++ b/src/tests/video_core/native_shader.cpp
@@ -0,0 +1,2841 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <bit>
+#include <algorithm>
+#include <cmath>
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <fstream>
+#include <limits>
+#include <unordered_map>
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/generators/catch_generators.hpp>
+#include "video_core/texture_cache/format_lookup_table.h"
+#include "shader_recompiler/environment.h"
+#include "shader_recompiler/frontend/ampere/lower_to_maxwell.h"
+#include "shader_recompiler/frontend/ampere/program_override.h"
+#include "shader_recompiler/frontend/ampere/program_capture.h"
+#include "shader_recompiler/frontend/ampere/program_address_map.h"
+#include "shader_recompiler/frontend/ir/basic_block.h"
+#include "shader_recompiler/frontend/ir/modifiers.h"
+#include "shader_recompiler/frontend/maxwell/decode.h"
+#include "shader_recompiler/frontend/maxwell/instruction.h"
+#include "shader_recompiler/frontend/maxwell/translate_program.h"
+#include "shader_recompiler/host_translate_info.h"
+#include "shader_recompiler/ir_opt/passes.h"
+#include "shader_recompiler/frontend/maxwell/translate/impl/impl.h"
+
+namespace {
+namespace IR = Shader::IR;
+using Native = Shader::Ampere::Instruction;
+
+class EmptyEnvironment : public Shader::Environment {
+public:
+ u64 ReadInstruction(u32) override { throw std::logic_error("unexpected instruction read"); }
+ u32 ReadCbufValue(u32, u32) override { throw std::logic_error("unexpected constant folding"); }
+ Shader::TextureType ReadTextureType(u32) override { return {}; }
+ Shader::TexturePixelFormat ReadTexturePixelFormat(u32) override { return {}; }
+ bool IsTexturePixelFormatInteger(u32) override { return false; }
+ u32 ReadViewportTransformState() override { return 1; }
+ u32 TextureBoundBuffer() const override { return 0; }
+ u32 LocalMemorySize() const override { return 0; }
+ u32 SharedMemorySize() const override { return 0; }
+ std::array<u32, 3> WorkgroupSize() const override { return {}; }
+ bool HasHLEMacroState() const override { return false; }
+ std::optional<Shader::ReplaceConstant> GetReplaceConstBuffer(u32, u32) override { return {}; }
+ void Dump(u64, u64) override {}
+};
+
+constexpr std::array<Native, 7> VertexTransform{{
+ {{0xff007321, 0x000080ff, 0x00000400, 0x000e2200}}, // load two input components
+ {{0x00037a02, 0x00c00000, 0x00000f00, 0x000fe400}}, // scale X
+ {{0x00027a02, 0x00c00100, 0x00000f00, 0x000fe200}}, // scale Y
+ {{0x00007623, 0x00c00200, 0x00010003, 0x001fe200}}, // X * scale X + offset X
+ {{0x01017623, 0x00c00300, 0x00010002, 0x000fc400}}, // Y * scale Y + offset Y
+ {{0xff007322, 0x00007000, 0x000004ff, 0x000fe200}}, // store position X/Y
+ {{0x0000794d, 0x00000000, 0x03800000, 0x000fea00}},
+}};
+
+// Evaluate the existing frontend's emitted IR, independently of native bit
+// decoding, to check actual attribute/constant/register data flow.
+std::array<float, 2> EvaluateVertex(IR::Block& block, std::array<float, 2> input,
+ std::array<float, 4> constants, s32 vertex_id = 0) {
+ std::unordered_map<const IR::Inst*, u32> values;
+ std::unordered_map<const IR::Inst*, std::array<u32, 2>> pairs;
+ std::array<u32, 256> regs{};
+ std::array<float, 2> output{};
+ const auto get = [&](const IR::Value& value) {
+ if (!value.IsImmediate()) return values.at(value.Inst());
+ if (value.Type() == IR::Type::U1) return static_cast<u32>(value.U1());
+ return value.Type() == IR::Type::F32 ? std::bit_cast<u32>(value.F32()) : value.U32();
+ };
+ for (const auto& inst : block.Instructions()) {
+ const auto arg = [&](size_t i) { return get(inst.Arg(i)); };
+ switch (inst.GetOpcode()) {
+ case IR::Opcode::GetRegister:
+ values[&inst] = regs[static_cast<u32>(inst.Arg(0).Reg())];
+ break;
+ case IR::Opcode::SetRegister:
+ if (inst.Arg(0).Reg() != IR::Reg::RZ) regs[static_cast<u32>(inst.Arg(0).Reg())] = arg(1);
+ break;
+ case IR::Opcode::GetAttributeU32:
+ REQUIRE(static_cast<u32>(inst.Arg(0).Attribute()) == 0x2fc / 4);
+ values[&inst] = static_cast<u32>(vertex_id);
+ break;
+ case IR::Opcode::ShiftLeftLogical32:
+ values[&inst] = arg(0) << arg(1);
+ break;
+ case IR::Opcode::ShiftRightArithmetic32:
+ values[&inst] = static_cast<u32>(static_cast<s32>(arg(0)) >> arg(1));
+ break;
+ case IR::Opcode::SLessThan:
+ values[&inst] = static_cast<s32>(arg(0)) < static_cast<s32>(arg(1));
+ break;
+ case IR::Opcode::ULessThan:
+ values[&inst] = arg(0) < arg(1);
+ break;
+ case IR::Opcode::SelectU32:
+ values[&inst] = arg(0) ? arg(1) : arg(2);
+ break;
+ case IR::Opcode::SMin32:
+ values[&inst] = static_cast<u32>(std::min(static_cast<s32>(arg(0)), static_cast<s32>(arg(1))));
+ break;
+ case IR::Opcode::SMax32:
+ values[&inst] = static_cast<u32>(std::max(static_cast<s32>(arg(0)), static_cast<s32>(arg(1))));
+ break;
+ case IR::Opcode::INeg32:
+ values[&inst] = 0U - arg(0);
+ break;
+ case IR::Opcode::IAdd32:
+ values[&inst] = arg(0) + arg(1);
+ break;
+ case IR::Opcode::ConvertF32S32:
+ values[&inst] = std::bit_cast<u32>(static_cast<float>(static_cast<s32>(arg(0))));
+ break;
+ case IR::Opcode::GetAttribute: {
+ const auto attr = static_cast<u32>(inst.Arg(0).Attribute());
+ if (attr == 0x2fc / 4) {
+ values[&inst] = static_cast<u32>(vertex_id);
+ break;
+ }
+ REQUIRE(attr >= 0x80 / 4);
+ REQUIRE(attr < 0x88 / 4);
+ values[&inst] = std::bit_cast<u32>(input[attr - 0x80 / 4]);
+ break;
+ }
+ case IR::Opcode::GetCbufU32x2:
+ REQUIRE(arg(0) == 3);
+ REQUIRE(arg(1) % 4 == 0);
+ REQUIRE(arg(1) + 8 <= sizeof(constants));
+ pairs[&inst] = {std::bit_cast<u32>(constants[arg(1) / 4]), std::bit_cast<u32>(constants[arg(1) / 4 + 1])};
+ break;
+ case IR::Opcode::CompositeExtractU32x2:
+ values[&inst] = pairs.at(inst.Arg(0).Inst()).at(arg(1));
+ break;
+ case IR::Opcode::GetCbufF32:
+ case IR::Opcode::GetCbufU32:
+ REQUIRE(arg(0) == 3);
+ REQUIRE(arg(1) < 16);
+ values[&inst] = std::bit_cast<u32>(constants[arg(1) / 4]);
+ break;
+ case IR::Opcode::BitCastF32U32:
+ case IR::Opcode::BitCastU32F32:
+ values[&inst] = arg(0);
+ break;
+ case IR::Opcode::LogicalAnd:
+ values[&inst] = arg(0) && arg(1);
+ break;
+ case IR::Opcode::LogicalOr:
+ values[&inst] = arg(0) || arg(1);
+ break;
+ case IR::Opcode::LogicalNot:
+ values[&inst] = !arg(0);
+ break;
+ case IR::Opcode::FPIsNan32:
+ values[&inst] = std::isnan(std::bit_cast<float>(arg(0)));
+ break;
+ case IR::Opcode::FPOrdGreaterThanEqual32:
+ values[&inst] = std::bit_cast<float>(arg(0)) >= std::bit_cast<float>(arg(1));
+ break;
+ case IR::Opcode::FPRoundEven32:
+ values[&inst] = std::bit_cast<u32>(std::nearbyint(std::bit_cast<float>(arg(0))));
+ break;
+ case IR::Opcode::FPFloor32:
+ values[&inst] = std::bit_cast<u32>(std::floor(std::bit_cast<float>(arg(0))));
+ break;
+ case IR::Opcode::FPTrunc32:
+ values[&inst] = std::bit_cast<u32>(std::trunc(std::bit_cast<float>(arg(0))));
+ break;
+ case IR::Opcode::FPClamp32:
+ values[&inst] = std::bit_cast<u32>(std::clamp(std::bit_cast<float>(arg(0)),
+ std::bit_cast<float>(arg(1)), std::bit_cast<float>(arg(2))));
+ break;
+ case IR::Opcode::ConvertU32F32:
+ values[&inst] = static_cast<u32>(std::bit_cast<float>(arg(0)));
+ break;
+ case IR::Opcode::ConvertF32U32:
+ values[&inst] = std::bit_cast<u32>(static_cast<float>(arg(0)));
+ break;
+ case IR::Opcode::BitFieldUExtract:
+ values[&inst] = (arg(0) >> arg(1)) & ((1U << arg(2)) - 1);
+ break;
+ case IR::Opcode::BitwiseOr32:
+ values[&inst] = arg(0) | arg(1);
+ break;
+ case IR::Opcode::FPOrdLessThan32:
+ values[&inst] = std::bit_cast<float>(arg(0)) < std::bit_cast<float>(arg(1));
+ break;
+ case IR::Opcode::BitwiseXor32:
+ values[&inst] = arg(0) ^ arg(1);
+ break;
+ case IR::Opcode::BitwiseNot32:
+ values[&inst] = ~arg(0);
+ break;
+ case IR::Opcode::BitwiseAnd32:
+ values[&inst] = arg(0) & arg(1);
+ break;
+ case IR::Opcode::FPAdd32:
+ values[&inst] = std::bit_cast<u32>(std::bit_cast<float>(arg(0)) + std::bit_cast<float>(arg(1)));
+ break;
+ case IR::Opcode::FPMul32:
+ values[&inst] = std::bit_cast<u32>(std::bit_cast<float>(arg(0)) * std::bit_cast<float>(arg(1)));
+ break;
+ case IR::Opcode::FPMin32:
+ values[&inst] = std::bit_cast<u32>(std::fmin(std::bit_cast<float>(arg(0)), std::bit_cast<float>(arg(1))));
+ break;
+ case IR::Opcode::FPMax32:
+ values[&inst] = std::bit_cast<u32>(std::fmax(std::bit_cast<float>(arg(0)), std::bit_cast<float>(arg(1))));
+ break;
+ case IR::Opcode::SelectF32:
+ values[&inst] = arg(0) ? arg(1) : arg(2);
+ break;
+ case IR::Opcode::FPFma32: {
+ const auto control = inst.Flags<IR::FpControl>();
+ REQUIRE(control.fmz_mode == IR::FmzMode::FTZ);
+ REQUIRE(control.rounding == IR::FpRounding::RN);
+ values[&inst] = std::bit_cast<u32>(std::fma(std::bit_cast<float>(arg(0)),
+ std::bit_cast<float>(arg(1)), std::bit_cast<float>(arg(2))));
+ break;
+ }
+ case IR::Opcode::SetAttribute: {
+ const auto attr = static_cast<u32>(inst.Arg(0).Attribute());
+ REQUIRE(attr >= 0x70 / 4);
+ REQUIRE(attr < 0x78 / 4);
+ output[attr - 0x70 / 4] = std::bit_cast<float>(arg(1));
+ break;
+ }
+ default:
+ FAIL("Unexpected IR operation " << IR::NameOf(inst.GetOpcode()));
+ }
+ }
+ return output;
+}
+} // namespace
+
+TEST_CASE("Native vertex lowering preserves attribute transforms through existing shader IR", "[native-shader]") {
+ const auto lowered = Shader::Ampere::LowerToMaxwell(VertexTransform);
+ REQUIRE(lowered);
+ REQUIRE(lowered.source_size == VertexTransform.size() * 16);
+ REQUIRE(lowered.instructions.size() >= VertexTransform.size());
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const u64 word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::ALD: visitor.ALD(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::FFMA_rc: visitor.FFMA_rc(word); break;
+ case Opcode::FFMA_reg: visitor.FFMA_reg(word); break;
+ case Opcode::MOV_reg: visitor.MOV_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected lowered vertex instruction");
+ }
+ }
+ REQUIRE(EvaluateVertex(block, {2, 4}, {3, 7, 5, 11}) == std::array<float, 2>{11, 39});
+ REQUIRE(EvaluateVertex(block, {-4, 0.5f}, {-2, 4, 1, -3}) == std::array<float, 2>{9, -1});
+}
+
+TEST_CASE("Native lowering rejects unsupported forms without a partial executable", "[native-shader]") {
+ auto code = VertexTransform;
+ code[3].words[2] |= 1U << 12; // DNZ has no accepted lowering yet.
+ auto result = Shader::Ampere::LowerToMaxwell(code);
+ REQUIRE_FALSE(result);
+ REQUIRE(result.error_offset == 3 * 16);
+ REQUIRE(result.instructions.empty());
+ code = VertexTransform;
+ code[1].words[2] &= ~(15U << 8); // Partial quad-lane mask.
+ result = Shader::Ampere::LowerToMaxwell(code);
+ REQUIRE_FALSE(result);
+ REQUIRE(result.instructions.empty());
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell({}));
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(std::span{VertexTransform}.first(6)));
+}
+
+TEST_CASE("Native instruction predicates survive lowering", "[native-shader]") {
+ auto code = VertexTransform;
+ code[3].words[0] = (code[3].words[0] & ~0xf000U) | 0x9000U;
+ const auto result = Shader::Ampere::LowerToMaxwell(code);
+ REQUIRE(result);
+ const auto pred = Shader::Maxwell::Instruction{result.instructions[result.source_instruction_offsets[3]]}.Pred();
+ REQUIRE(pred.index == 1);
+ REQUIRE(pred.negated);
+}
+
+// These private input fixtures are read from the investigation directory, not
+// embedded or distributed with the emulator's source tree.
+TEST_CASE("Captured native shader pair compiles through existing control flow and shader IR", "[.native-shader-captured]") {
+ const char* directory = std::getenv("EDEN_NATIVE_SHADER_FIXTURES");
+ REQUIRE(directory != nullptr);
+ class FixtureEnvironment final : public EmptyEnvironment {
+ public:
+ FixtureEnvironment(const std::vector<u64>& code_, std::span<const u8> header, Shader::Stage stage_)
+ : code{code_} {
+ std::memcpy(&sph, header.data(), sizeof(sph));
+ stage = stage_;
+ start_address = 0;
+ is_proprietary_driver = true;
+ }
+ u64 ReadInstruction(u32 address) override { return code.at(address / 8); }
+ u32 TranslateProgramAddress(u32 address) const override {
+ const auto translated = Shader::Ampere::NativeProgramAddress(code, address);
+ if (!translated) throw std::logic_error("invalid private native branch target " + std::to_string(address));
+ return *translated;
+ }
+ u32 LocalMemorySize() const override { return static_cast<u32>(sph.LocalMemorySize()) + sph.common3.shader_local_memory_crs_size; }
+ u32 TextureBoundBuffer() const override { return 2; }
+ u32 ReadCbufValue(u32 bank, u32 offset) override {
+ if (bank == 1) {
+ REQUIRE(offset % 4 == 0);
+ return cbuf1.at(offset / 4);
+ }
+ REQUIRE(bank == 2);
+ return 0; // Synthetic texture handle; only compilation is tested here.
+ }
+ Shader::TextureType ReadTextureType(u32) override { return Shader::TextureType::Color2D; }
+ const std::vector<u64>& code;
+ std::vector<u32> cbuf1;
+ };
+ for (const int stage : {1, 5}) {
+ const auto path = std::filesystem::path{directory} / ("native-graphics-shader-" + std::to_string(stage) + ".bin");
+ std::ifstream file(path, std::ios::binary | std::ios::ate);
+ REQUIRE(file.good());
+ const auto size = static_cast<size_t>(file.tellg());
+ REQUIRE(size >= 0x90);
+ std::vector<u8> bytes(size);
+ file.seekg(0);
+ file.read(reinterpret_cast<char*>(bytes.data()), bytes.size());
+ std::vector<Native> input((size - 0x80) / 16);
+ std::memcpy(input.data(), bytes.data() + 0x80, input.size() * 16);
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ INFO("stage=" << stage << " offset=" << lowered.error_offset << " error=" << lowered.error);
+ REQUIRE(lowered);
+ Shader::Ampere::ProgramCaptureState capture;
+ u32 captured_size{};
+ for (u32 i = 0; i < input.size(); ++i) {
+ const auto status = capture.Observe(input[i], i);
+ REQUIRE(status != Shader::Ampere::ProgramCaptureState::Result::InvalidTarget);
+ if (status == Shader::Ampere::ProgramCaptureState::Result::Complete) {
+ captured_size = (i + 1) * sizeof(Native);
+ break;
+ }
+ }
+ REQUIRE(captured_size == lowered.source_size);
+ std::vector<u64> code(0x50 / 8);
+ for (const u64 word : lowered.instructions) {
+ if ((code.size() * 8) % 32 == 0) code.push_back(0);
+ code.push_back(word);
+ }
+ code.push_back(0);
+ Shader::Ampere::AppendNativeAddressMap(code, lowered.source_instruction_offsets);
+ FixtureEnvironment env{code, std::span{bytes}.first(0x50), stage == 1 ? Shader::Stage::VertexB : Shader::Stage::Fragment};
+ auto constants_path = std::filesystem::path{directory} / ("cbuf1-" + std::to_string(stage) + ".bin");
+ if (!std::filesystem::exists(constants_path)) constants_path = std::filesystem::path{directory} / "cbuf1.bin";
+ std::ifstream constants(constants_path, std::ios::binary | std::ios::ate);
+ if (constants) {
+ env.cbuf1.resize(static_cast<size_t>(constants.tellg()) / 4);
+ constants.seekg(0);
+ constants.read(reinterpret_cast<char*>(env.cbuf1.data()), env.cbuf1.size() * 4);
+ }
+ Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_pool;
+ Shader::ObjectPool<IR::Inst> inst_pool;
+ Shader::ObjectPool<IR::Block> block_pool;
+ Shader::Maxwell::Flow::CFG cfg{env, flow_pool, 0x50};
+ Shader::HostTranslateInfo host;
+ host.support_int64 = true;
+ host.support_float64 = true;
+ host.support_float16 = true;
+ const auto program = Shader::Maxwell::TranslateProgram(inst_pool, block_pool, env, cfg, host);
+ REQUIRE(program.stage == env.ShaderStage());
+ REQUIRE_FALSE(program.blocks.empty());
+ REQUIRE_FALSE(program.post_order_blocks.empty());
+ }
+}
+
+TEST_CASE("Native scalar texture sampling preserves coordinates and split outputs", "[native-shader]") {
+ std::array<Native, 2> input{{
+ {{0x04007b60, 0x30800805, 0x041e0f02, 0x100fc200}},
+ VertexTransform.back(),
+ }};
+ const bool predicated = GENERATE(false, true);
+ if (predicated) input[0].words[0] = (input[0].words[0] & ~0xf000U) | 0x3000;
+ const bool explicit_lod = GENERATE(false, true);
+ if (explicit_lod) input[0].words[2] |= 1U << 23;
+ const auto result = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(result);
+ REQUIRE(result.required_texture_buffer == 2);
+ REQUIRE(Shader::Maxwell::Instruction{result.instructions.front()}.Pred().index == (predicated ? 3U : 7U));
+ REQUIRE_FALSE(Shader::Maxwell::Instruction{result.instructions.front()}.Pred().negated);
+ REQUIRE(Shader::Maxwell::Decode(result.instructions.front()) == Shader::Maxwell::Opcode::TEXS);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TEXS(result.instructions.front());
+ std::vector<u32> reads;
+ std::vector<u32> writes;
+ u32 samples = 0;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::GetRegister) reads.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == (explicit_lod ? IR::Opcode::BoundImageSampleExplicitLod : IR::Opcode::BoundImageSampleImplicitLod)) {
+ ++samples;
+ if (explicit_lod) REQUIRE(inst.Arg(2).F32() == 0.0f);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructF32x2);
+ for (size_t component = 0; component < 2; ++component) {
+ const auto* cast = coords->Arg(component).Inst();
+ REQUIRE(cast->GetOpcode() == IR::Opcode::BitCastF32U32);
+ const auto* reg = cast->Arg(0).Inst();
+ REQUIRE(reg->GetOpcode() == IR::Opcode::GetRegister);
+ REQUIRE(static_cast<u32>(reg->Arg(0).Reg()) == 4 + component);
+ }
+ REQUIRE(inst.Arg(0).U32() == 0x20);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type == Shader::TextureType::Color2D);
+ }
+ }
+ REQUIRE(samples == 1);
+ std::ranges::sort(reads);
+ REQUIRE(reads == std::vector<u32>{4, 5});
+ REQUIRE(writes == std::vector<u32>{0, 1, 2, 3});
+ // Reject unsupported LOD modes, a different cbuf bank, and sparse results.
+ for (u32 bit : {89U, 54U, 81U}) {
+ auto unsupported = input;
+ unsupported[0].words[bit / 32] ^= 1U << (bit % 32);
+ const auto rejected = Shader::Ampere::LowerToMaxwell(unsupported);
+ REQUIRE_FALSE(rejected);
+ REQUIRE(rejected.instructions.empty());
+ }
+}
+
+TEST_CASE("Native texel fetch keeps integer coordinates and RGB outputs", "[native-shader]") {
+ std::array<Native, 3> input{{
+ {{0x00027305, 0x00000004, 0x0021f100, 0x001ff000}},
+ {{0x02007b66, 0x30800a05, 0x049e0702, 0x101fc200}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(lowered.required_texture_buffer == 2);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions[0]) == Shader::Maxwell::Opcode::F2I_reg);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions[1]) == Shader::Maxwell::Opcode::TLDS);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.F2I_reg(lowered.instructions[0]);
+ visitor.TLDS(lowered.instructions[1]);
+ std::vector<u32> writes;
+ u32 fetches = 0;
+ bool truncates = false;
+ bool handles_nan = false;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == IR::Opcode::FPTrunc32) {
+ truncates = true;
+ REQUIRE(inst.Flags<IR::FpControl>().fmz_mode == IR::FmzMode::FTZ);
+ }
+ if (inst.GetOpcode() == IR::Opcode::FPIsNan32) handles_nan = true;
+ if (inst.GetOpcode() == IR::Opcode::BoundImageFetch) {
+ ++fetches;
+ REQUIRE(inst.Arg(0).U32() == 0x28);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type == Shader::TextureType::Color2D);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructU32x2);
+ REQUIRE(coords->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{2});
+ REQUIRE(coords->Arg(1).Inst()->Arg(0).Reg() == IR::Reg{5});
+ REQUIRE(inst.Arg(3).U32() == 0);
+ }
+ }
+ REQUIRE(fetches == 1);
+ REQUIRE(truncates);
+ REQUIRE(handles_nan);
+ REQUIRE(writes == std::vector<u32>{2, 0, 1, 2});
+ auto unsupported = input;
+ unsupported[0].words[2] ^= 1U << 13; // NTZ is required by this conversion lowering.
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(unsupported));
+ unsupported = input;
+ unsupported[1].words[2] ^= 1U << 14; // multisample fetch is not implemented here.
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(unsupported));
+}
+
+TEST_CASE("Native texture component masks preserve sampled components", "[native-shader]") {
+ for (const u32 mask : {1U, 2U, 4U, 8U, 3U, 9U, 10U, 12U, 7U, 11U, 13U, 14U, 15U}) {
+ const bool split = std::popcount(mask) > 2;
+ std::array<Native, 2> input{{
+ {{0x04007b60, 0x30800805, 0x041e0f02, 0x100fc200}}, VertexTransform.back(),
+ }};
+ input[0].words[2] = (input[0].words[2] & ~0xfffU) | (split ? 6U : 255U) | (mask << 8);
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TEXS(lowered.instructions.front());
+ std::vector<u32> extracted;
+ std::vector<u32> writes;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::CompositeExtractF32x4) extracted.push_back(inst.Arg(1).U32());
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ }
+ std::vector<u32> expected;
+ for (u32 component = 0; component < 4; ++component) if (mask & (1U << component)) expected.push_back(component);
+ REQUIRE(extracted == expected);
+ REQUIRE(writes.size() == expected.size());
+ for (u32 i = 0; i < writes.size(); ++i) REQUIRE(writes[i] == (i < 2 ? i : i + 4));
+ }
+}
+
+TEST_CASE("Native half packing and arithmetic preserve lane order numerically", "[native-shader]") {
+ const std::array<Native, 15> input{{
+ {{0x0102723e, 0x00000000, 0x000000ff, 0}}, // PACK_AB R2, R1, R0
+ {{0x02027232, 0x00000002, 0x00000000, 0}}, // HMUL2 R2, R2, R2
+ {{0xff037230, 0x20000002, 0x00004100, 0}}, // extract low half to F32 R3
+ {{0xff047230, 0x30000002, 0x00004100, 0}}, // extract high half to F32 R4
+ {{0xff05723e, 0x00000000, 0x00004002, 0}}, // MERGE_C R5, R0, R2
+ {{0x02067230, 0x20000002, 0x00000800, 0}}, // add two low halves: (2,2)
+ {{0x02077231, 0x20000002, 0x00040802, 0}}, // fused 1*1+1: (2,2)
+ {{0x02087240, 0x30000002, 0x03800800, 0}}, // min of low/high: (1,1)
+ {{0x02097840, 0x3c003c00, 0x07800c00, 0}}, // max of high/one: (4,4)
+ {{0x000a7308, 0x00000008, 0x00001200, 0}}, // reciprocal of half one
+ {{0x020b7816, 0x00005410, 0x00000007, 0}}, // concatenate low halves
+ {{0xff0c7230, 0xa0000008, 0x00004100, 0}}, // extract negated low half
+ {{0x020d7230, 0x30000002, 0x00000f00, 0}}, // -abs(high) + high: (0,0)
+ {{0x020e7431, 0x3c003c00, 0x00400802, 0}}, // (1,1) * (1,-4) + (1,1)
+ VertexTransform.back(),
+ }};
+ auto code = input;
+ const u32 selector = GENERATE(0x5410U, 0x5432U, 0x7610U, 0x7632U);
+ code[10].words[1] = selector;
+ if (selector == 0x7610 || selector == 0x7632) code[10].words[2] = 2;
+ const auto lowered = Shader::Ampere::LowerToMaxwell(code);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const u64 word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::F2F_reg: visitor.F2F_reg(word); break;
+ case Opcode::IADD_reg: visitor.IADD_reg(word); break;
+ case Opcode::ISCADD_reg: visitor.ISCADD_reg(word); break;
+ case Opcode::HMUL2_reg: visitor.HMUL2_reg(word); break;
+ case Opcode::HADD2_reg: visitor.HADD2_reg(word); break;
+ case Opcode::HFMA2_reg: visitor.HFMA2_reg(word); break;
+ case Opcode::FMNMX_reg: visitor.FMNMX_reg(word); break;
+ case Opcode::MUFU: visitor.MUFU(word); break;
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::SHR_imm: visitor.SHR_imm(word); break;
+ case Opcode::LOP32I: visitor.LOP32I(word); break;
+ case Opcode::MOV_reg: visitor.MOV_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected canonical half instruction");
+ }
+ }
+ // Independent values with exact binary16 representations exercise packing,
+ // multiplication and extraction through the actual emitted IR data flow.
+ const std::array<std::pair<u16, float>, 5> halves{{{u16{0x4200}, 3.0f}, {u16{0}, 0.0f}, {u16{0x3c00}, 1.0f}, {u16{0x4000}, 2.0f}, {u16{0x4400}, 4.0f}}};
+ const auto from_half = [&](u16 bits) {
+ for (const auto& [encoded, value] : halves) if (encoded == (bits & 0x7fff)) return (bits & 0x8000) ? -value : value;
+ throw std::logic_error("unexpected half value");
+ };
+ const auto to_half = [&](float value) {
+ for (const auto& [encoded, reference] : halves) if (std::abs(value) == reference) return static_cast<u16>(encoded | (std::signbit(value) ? 0x8000 : 0));
+ throw std::logic_error("unexpected float value");
+ };
+ std::array<u32, 256> regs{};
+ regs[0] = std::bit_cast<u32>(1.0f);
+ regs[1] = std::bit_cast<u32>(2.0f);
+ std::unordered_map<const IR::Inst*, u64> values;
+ const auto get = [&](const IR::Value& value) -> u64 {
+ if (!value.IsImmediate()) return values.at(value.Inst());
+ if (value.Type() == IR::Type::U1) return value.U1();
+ if (value.Type() == IR::Type::F32) return std::bit_cast<u32>(value.F32());
+ if (value.Type() == IR::Type::U16 || value.Type() == IR::Type::F16) return value.U16();
+ return value.U32();
+ };
+ for (const auto& inst : block.Instructions()) {
+ const auto arg = [&](size_t i) { return get(inst.Arg(i)); };
+ switch (inst.GetOpcode()) {
+ case IR::Opcode::GetRegister: values[&inst] = regs[static_cast<u32>(inst.Arg(0).Reg())]; break;
+ case IR::Opcode::SetRegister: regs[static_cast<u32>(inst.Arg(0).Reg())] = static_cast<u32>(arg(1)); break;
+ case IR::Opcode::BitCastF32U32:
+ case IR::Opcode::BitCastU32F32: values[&inst] = arg(0); break;
+ case IR::Opcode::ConvertF16F32: values[&inst] = to_half(std::bit_cast<float>(static_cast<u32>(arg(0)))); break;
+ case IR::Opcode::ConvertF32F16: values[&inst] = std::bit_cast<u32>(from_half(static_cast<u16>(arg(0)))); break;
+ case IR::Opcode::CompositeConstructF16x2: values[&inst] = arg(0) | (arg(1) << 32); break;
+ case IR::Opcode::CompositeExtractF16x2: values[&inst] = (arg(0) >> (arg(1) * 32)) & 0xffff; break;
+ case IR::Opcode::PackFloat2x16: values[&inst] = (arg(0) & 0xffff) | ((arg(0) >> 32) << 16); break;
+ case IR::Opcode::UnpackFloat2x16: values[&inst] = (arg(0) & 0xffff) | ((arg(0) >> 16) << 32); break;
+ case IR::Opcode::FPMul16: values[&inst] = to_half(from_half(static_cast<u16>(arg(0))) * from_half(static_cast<u16>(arg(1)))); break;
+ case IR::Opcode::FPAdd16: values[&inst] = to_half(from_half(static_cast<u16>(arg(0))) + from_half(static_cast<u16>(arg(1)))); break;
+ case IR::Opcode::FPFma16: values[&inst] = to_half(std::fma(from_half(static_cast<u16>(arg(0))), from_half(static_cast<u16>(arg(1))), from_half(static_cast<u16>(arg(2))))); break;
+ case IR::Opcode::FPNeg16: values[&inst] = arg(0) ^ 0x8000; break;
+ case IR::Opcode::FPAbs16: values[&inst] = arg(0) & 0x7fff; break;
+ case IR::Opcode::FPAdd32: values[&inst] = std::bit_cast<u32>(std::bit_cast<float>(static_cast<u32>(arg(0))) + std::bit_cast<float>(static_cast<u32>(arg(1)))); break;
+ case IR::Opcode::FPMin32: values[&inst] = std::bit_cast<u32>(std::fmin(std::bit_cast<float>(static_cast<u32>(arg(0))), std::bit_cast<float>(static_cast<u32>(arg(1))))); break;
+ case IR::Opcode::FPMax32: values[&inst] = std::bit_cast<u32>(std::fmax(std::bit_cast<float>(static_cast<u32>(arg(0))), std::bit_cast<float>(static_cast<u32>(arg(1))))); break;
+ case IR::Opcode::FPRecip32: values[&inst] = std::bit_cast<u32>(1.0f / std::bit_cast<float>(static_cast<u32>(arg(0)))); break;
+ case IR::Opcode::SelectF32: values[&inst] = arg(0) ? arg(1) : arg(2); break;
+ case IR::Opcode::BitwiseAnd32: values[&inst] = arg(0) & arg(1); break;
+ case IR::Opcode::BitwiseXor32: values[&inst] = arg(0) ^ arg(1); break;
+
+ case IR::Opcode::ShiftRightLogical32: values[&inst] = static_cast<u32>(arg(0)) >> arg(1); break;
+ case IR::Opcode::SLessThan: values[&inst] = static_cast<s32>(arg(0)) < static_cast<s32>(arg(1)); break;
+ case IR::Opcode::ULessThan: values[&inst] = static_cast<u32>(arg(0)) < static_cast<u32>(arg(1)); break;
+ case IR::Opcode::SelectU32: values[&inst] = arg(0) ? arg(1) : arg(2); break;
+ case IR::Opcode::IAdd32: values[&inst] = static_cast<u32>(arg(0) + arg(1)); break;
+ case IR::Opcode::ShiftLeftLogical32: values[&inst] = static_cast<u32>(arg(0) << arg(1)); break;
+ default: FAIL("Unexpected IR operation in half arithmetic");
+ }
+ }
+ REQUIRE(regs[2] == 0x44003c00);
+ REQUIRE(regs[5] == 0x3c003c00);
+ REQUIRE(regs[6] == 0x40004000U);
+ REQUIRE(regs[7] == 0x40004000U);
+ REQUIRE(regs[8] == 0x3c003c00U);
+ REQUIRE(regs[9] == 0x44004400U);
+ REQUIRE(regs[10] == 0x3c00U);
+ REQUIRE(regs[11] == (selector == 0x7632 ? 0x44004400U : selector == 0x7610 ? 0x44003c00U : selector == 0x5432 ? 0x40004400U : 0x40003c00U));
+ REQUIRE(std::bit_cast<float>(regs[12]) == -1.0f);
+ REQUIRE(regs[13] == 0U);
+ REQUIRE(regs[14] == 0xc2004000U);
+
+ REQUIRE(std::bit_cast<float>(regs[3]) == 1.0f);
+ REQUIRE(std::bit_cast<float>(regs[4]) == 4.0f);
+}
+
+TEST_CASE("Live shader overrides require the exact original instructions", "[native-shader]") {
+ const auto program = Shader::Ampere::LowerToMaxwell(VertexTransform);
+ REQUIRE(program);
+ const auto bytes = Shader::Ampere::EncodeProgramOverride(VertexTransform, program);
+ REQUIRE_FALSE(bytes.empty());
+ REQUIRE(Shader::Ampere::ProgramOverrideSourceSize(bytes) == VertexTransform.size() * 16);
+ REQUIRE(Shader::Ampere::ProgramOverrideSourceSize(std::span{bytes}.first(12)) == 0);
+ auto invalid_source = bytes;
+ invalid_source[8] ^= 1;
+ REQUIRE(Shader::Ampere::ProgramOverrideSourceSize(invalid_source) == 0);
+ const auto decoded = Shader::Ampere::DecodeProgramOverride(bytes, VertexTransform);
+ REQUIRE(decoded);
+ REQUIRE(decoded.instructions == program.instructions);
+ REQUIRE(decoded.source_instruction_offsets == program.source_instruction_offsets);
+ auto invalid_map = bytes;
+ const u32 invalid_target = static_cast<u32>(program.instructions.size());
+ std::memcpy(invalid_map.data() + invalid_map.size() - program.source_instruction_offsets.size() * 4,
+ &invalid_target, sizeof(invalid_target));
+ const auto invalid_mapping = Shader::Ampere::DecodeProgramOverride(invalid_map, VertexTransform);
+ REQUIRE_FALSE(invalid_mapping);
+ REQUIRE(invalid_mapping.instructions.empty());
+
+ REQUIRE(decoded.source_size == program.source_size);
+ REQUIRE(decoded.required_texture_buffer == program.required_texture_buffer);
+ auto different = VertexTransform;
+ different[0].words[1] ^= 4;
+ REQUIRE(Shader::Ampere::NativeProgramKey(different) != Shader::Ampere::NativeProgramKey(VertexTransform));
+ const auto mismatch = Shader::Ampere::DecodeProgramOverride(bytes, different);
+ REQUIRE_FALSE(mismatch);
+ REQUIRE(mismatch.instructions.empty());
+ REQUIRE_FALSE(Shader::Ampere::DecodeProgramOverride(std::span{bytes}.first(12), VertexTransform));
+ REQUIRE_FALSE(Shader::Ampere::DecodeProgramOverride(std::span{bytes}.first(bytes.size() - 1), VertexTransform));
+ auto wrong_version = bytes;
+ wrong_version[4] ^= 1;
+ REQUIRE_FALSE(Shader::Ampere::DecodeProgramOverride(wrong_version, VertexTransform));
+ auto wrong_binding = bytes;
+ const u32 bank = 18;
+ std::memcpy(wrong_binding.data() + 16, &bank, sizeof(bank));
+ REQUIRE_FALSE(Shader::Ampere::DecodeProgramOverride(wrong_binding, VertexTransform));
+}
+
+TEST_CASE("Native integer shader forms preserve signed arithmetic and immediate values", "[native-shader]") {
+ // Synthetic vertex program: output (id + arithmetic_shift(id, 1), 4.0).
+ const std::array<Native, 7> input{{
+ {{0xff007321, 0x0002fcff, 0x00000000, 0x000e2200}},
+ {{0xff017819, 0x00000001, 0x00011400, 0x000fe400}},
+ {{0x00007824, 0x00000001, 0x078e0201, 0x000fe200}},
+ {{0x00007245, 0x00000000, 0x00201400, 0x000fc600}},
+ {{0xff017424, 0x40800000, 0x078e00ff, 0x000fe200}},
+ {{0xff007322, 0x00007000, 0x000004ff, 0x000fe200}},
+ VertexTransform.back(),
+ }};
+ auto program = input;
+ const u32 mode = GENERATE(0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U);
+ if (mode == 7) program[2] = Native{{0x00007217, 1, 0x03800200, 0}};
+ if (mode == 8) program[2] = Native{{0x00007217, 1, 0x07800200, 0}};
+ if (mode == 5) program[2] = Native{{0x00007210, 0x00000001, 0x07ffe1ff, 0}};
+ if (mode == 6) program[2] = Native{{0x00007817, 0x0000003f, 0x07800200, 0}};
+ if (mode == 4) program[2] = Native{{0x00007817, 0x0000003f, 0x03800200, 0}};
+ if (mode == 1) program[2] = Native{{0x00007210, 0x00000001, 0x07ffe0ff, 0x000fe200}};
+ if (mode == 3) program[2] = Native{{0x00007210, 0x80000001, 0x07ffe0ff, 0x000fe200}};
+ if (mode == 2) program[1] = Native{{0x00017819, 0x00000001, 0x000006ff, 0x000fe200}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(program);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const u64 word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::ALD: visitor.ALD(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::SHR_imm: visitor.SHR_imm(word); break;
+ case Opcode::SHL_imm: visitor.SHL_imm(word); break;
+ case Opcode::IMNMX_reg: visitor.IMNMX_reg(word); break;
+ case Opcode::IMNMX_imm: visitor.IMNMX_imm(word); break;
+ case Opcode::IADD_reg: visitor.IADD_reg(word); break;
+ case Opcode::I2F_reg: visitor.I2F_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected lowered integer shader instruction");
+ }
+ }
+ for (const s32 id : {0, 1, 2, -1, -3, 12345}) {
+ REQUIRE(EvaluateVertex(block, {}, {}, id) ==
+ std::array<float, 2>{static_cast<float>(mode == 2 ? id * 3 : mode == 3 ? id - (id >> 1) : mode == 4 ? std::min<s32>(id, 63) : mode == 5 ? -id + (id >> 1) : mode == 6 ? std::max<s32>(id, 63) : mode == 7 ? std::min<s32>(id, id >> 1) : mode == 8 ? std::max<s32>(id, id >> 1) : id + (id >> 1)), 4.0f});
+ }
+}
+
+TEST_CASE("Native single-channel texel fetch writes only its selected result", "[native-shader]") {
+ std::array<Native, 2> input{{
+ {{0x04017b66, 0x30800805, 0x049e01ff, 0x001fc200}},
+ VertexTransform.back(),
+ }};
+ const u32 component = GENERATE(0U, 1U, 2U, 3U);
+ input[0].words[2] = (input[0].words[2] & ~0xf00U) | (1U << (8 + component));
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(lowered.required_texture_buffer == 2);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TLDS(lowered.instructions.front());
+ std::vector<u32> writes;
+ u32 fetches = 0;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::SetRegister)
+ writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == IR::Opcode::BoundImageFetch) {
+ ++fetches;
+ REQUIRE(inst.Arg(0).U32() == 0x20);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructU32x2);
+ REQUIRE(coords->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{4});
+ REQUIRE(coords->Arg(1).Inst()->Arg(0).Reg() == IR::Reg{5});
+ }
+ if (inst.GetOpcode() == IR::Opcode::CompositeExtractF32x4)
+ REQUIRE(inst.Arg(1).U32() == component);
+ }
+ REQUIRE(fetches == 1);
+ REQUIRE(writes == std::vector<u32>{1});
+}
+
+TEST_CASE("Native IMAD move loads the actual constant-buffer value", "[native-shader]") {
+ const std::array<Native, 3> input{{
+ {{0xff007624, 0x00c00100, 0x078e00ff, 0x000fe200}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0x000fe200}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions.front()) == Shader::Maxwell::Opcode::MOV_cbuf);
+ visitor.MOV_cbuf(lowered.instructions[0]);
+ visitor.AST(lowered.instructions[1]);
+ REQUIRE(EvaluateVertex(block, {}, {13, -2.5f, 99, 7}) == std::array<float, 2>{-2.5f, 0.0f});
+ auto unsupported = input;
+ unsupported[0].words[1] |= 1U << 30;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(unsupported));
+}
+
+TEST_CASE("Depth-stencil descriptors ignore the unused blue and alpha component types", "[native-gpu]") {
+ using Tegra::Texture::ComponentType;
+ using Tegra::Texture::TextureFormat;
+ using VideoCore::Surface::PixelFormat;
+ for (const auto unused : {ComponentType::UNORM, ComponentType::UINT}) {
+ REQUIRE(VideoCommon::PixelFormatFromTextureInfo(TextureFormat::S8Z24,
+ ComponentType::UNORM, ComponentType::UINT, unused, unused, false) ==
+ PixelFormat::D24_UNORM_S8_UINT);
+ }
+}
+
+TEST_CASE("Native floating min/max clamps actual shader input", "[native-shader]") {
+ const std::array<Native, 5> input{{
+ {{0xff007321, 0x000080ff, 0x00000000, 0x000e2200}},
+ {{0x00007a09, 0x00c00000, 0x07810000, 0x000fe400}},
+ {{0x00007a09, 0x00c00100, 0x03810000, 0x000fe400}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0x000fe200}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::ALD: visitor.ALD(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::FMNMX_reg: visitor.FMNMX_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected native min/max lowering");
+ }
+ }
+ for (const float value : {-20.0f, -1.0f, 0.25f, 2.0f, 100.0f})
+ REQUIRE(EvaluateVertex(block, {value, 0}, {-1, 2, 0, 0}) ==
+ std::array<float, 2>{std::clamp(value, -1.0f, 2.0f), 0});
+}
+
+TEST_CASE("Native boolean float comparison preserves register moves and absolute inputs", "[native-shader]") {
+ const std::array<Native, 5> input{{
+ {{0xff007321, 0x000080ff, 0x00000000, 0}},
+ {{0xff017224, 0x000000ff, 0x078e0000, 0}},
+ {{0x01007a0a, 0x00c00000, 0x03811200, 0}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::ALD: visitor.ALD(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV_reg: visitor.MOV_reg(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::LOP32I: visitor.LOP32I(word); break;
+ case Opcode::FSET_reg: visitor.FSET_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected native boolean float comparison lowering");
+ }
+ }
+ for (const float value : {-3.0f, -1.0f, 0.0f, 2.0f, 4.0f})
+ REQUIRE(EvaluateVertex(block, {value, 0}, {2, 0, 0, 0}) ==
+ std::array<float, 2>{std::abs(value) < 2.0f ? 1.0f : 0.0f, 0});
+}
+
+TEST_CASE("Native unsigned conversion saturates, clears NaNs, and extracts the low byte", "[native-shader]") {
+ std::array<Native, 5> input{{
+ {{0xff007321, 0x000080ff, 0x00000000, 0}},
+ {{0x00017305, 0x00000000, 0x0021f000, 0}},
+ {{0x00007306, 0x00000001, 0x00001000, 0}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0}},
+ VertexTransform.back(),
+ }};
+ const bool constant_source = GENERATE(false, true);
+ if (constant_source) input[1] = Native{{0x00017b05, 0x00c00000, 0x0021f000, 0}};
+ const u32 conversion_mode = GENERATE(0U, 1U, 2U);
+ const bool short_round = conversion_mode == 1;
+ if (conversion_mode == 2) input[1].words[2] = 0x00207000;
+ if (short_round) input[1].words[2] = 0x00212800;
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::ALD: visitor.ALD(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::FSET_reg: visitor.FSET_reg(word); break;
+ case Opcode::FMNMX_reg: visitor.FMNMX_reg(word); break;
+ case Opcode::LOP_reg: visitor.LOP_reg(word); break;
+ case Opcode::F2I_reg: visitor.F2I_reg(word); break;
+ case Opcode::I2F_reg: visitor.I2F_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected unsigned conversion lowering");
+ }
+ }
+ const std::array<std::pair<float, float>, 9> cases{{
+ {std::numeric_limits<float>::quiet_NaN(), 0.0f},
+ {-std::numeric_limits<float>::infinity(), 0.0f}, {-3.0f, 0.0f}, {1.9f, 1.0f},
+ {300.75f, 44.0f}, {65535.0f, 255.0f}, {4294967040.0f, 0.0f},
+ {4294967296.0f, 255.0f}, {std::numeric_limits<float>::infinity(), 255.0f},
+ }};
+ for (const auto& [value, expected] : cases) {
+ const float rounded = std::isnan(value) ? 0.0f : static_cast<float>(static_cast<u32>(
+ std::clamp(std::nearbyint(static_cast<double>(value)), 0.0, 65535.0)) & 255);
+ REQUIRE(EvaluateVertex(block, {value, 0}, {value, 0, 0, 0}) == std::array<float, 2>{short_round ? rounded : expected, 0});
+ }
+}
+
+TEST_CASE("Native depth-comparison sample preserves XY coordinates and reference depth", "[native-shader]") {
+ const std::array<Native, 2> input{{
+ {{0x04017b60, 0x30800806, 0x041e41ff, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TEXS(lowered.instructions.front());
+ u32 samples = 0;
+ std::vector<u32> writes;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::SetRegister)
+ writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == IR::Opcode::BoundImageSampleDrefImplicitLod) {
+ ++samples;
+ REQUIRE(inst.Arg(0).U32() == 0x20);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructF32x2);
+ for (u32 i = 0; i < 2; ++i)
+ REQUIRE(coords->Arg(i).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{4 + i});
+ REQUIRE(inst.Arg(2).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{6});
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().is_depth == 1);
+ }
+ }
+ REQUIRE(samples == 1);
+ REQUIRE(writes == std::vector<u32>{1});
+}
+
+TEST_CASE("Native array texel fetch preserves layer and separates XY registers", "[native-shader]") {
+ std::array<Native, 2> input{{
+ {{0x06007b66, 0xb0800809, 0x009e0f02, 0}},
+ VertexTransform.back(),
+ }};
+ const bool sparse = GENERATE(false, true);
+ if (sparse) input[0].words[2] = 0x009e0b23;
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.MOV_reg(lowered.instructions[0]);
+ visitor.MOV_reg(lowered.instructions[1]);
+ visitor.TLDS(lowered.instructions[2]);
+ u32 fetches = 0;
+ std::vector<u32> writes, components;
+ std::vector<std::pair<u32, u32>> copies;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::SetRegister && static_cast<u32>(inst.Arg(0).Reg()) < 250)
+ writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == IR::Opcode::CompositeExtractF32x4)
+ components.push_back(inst.Arg(1).U32());
+ if (inst.GetOpcode() == IR::Opcode::SetRegister &&
+ static_cast<u32>(inst.Arg(0).Reg()) >= 250) {
+ REQUIRE(inst.Arg(1).Inst()->GetOpcode() == IR::Opcode::GetRegister);
+ copies.emplace_back(static_cast<u32>(inst.Arg(0).Reg()),
+ static_cast<u32>(inst.Arg(1).Inst()->Arg(0).Reg()));
+ }
+ if (inst.GetOpcode() == IR::Opcode::BoundImageFetch) {
+ ++fetches;
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type == Shader::TextureType::ColorArray2D);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructU32x3);
+ REQUIRE(coords->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{250});
+ REQUIRE(coords->Arg(1).Inst()->Arg(0).Reg() == IR::Reg{251});
+ const auto* layer = coords->Arg(2).Inst();
+ REQUIRE(layer->GetOpcode() == IR::Opcode::BitFieldUExtract);
+ REQUIRE(layer->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{6});
+ REQUIRE(layer->Arg(1).U32() == 0);
+ REQUIRE(layer->Arg(2).U32() == 16);
+ }
+ }
+ REQUIRE(fetches == 1);
+ REQUIRE(writes == (sparse ? std::vector<u32>{0, 1, 35} : std::vector<u32>{0, 1, 2, 3}));
+ REQUIRE(components == (sparse ? std::vector<u32>{0, 1, 3} : std::vector<u32>{0, 1, 2, 3}));
+ REQUIRE(copies == std::vector<std::pair<u32, u32>>{{250, 7}, {251, 9}});
+}
+
+TEST_CASE("Native bitwise LUT forms preserve source bits without changing predicates", "[native-shader]") {
+ std::array<Native, 5> input{{
+ {{0x00007a02, 0x00c00000, 0x00000f00, 0}},
+ {{0x00027a02, 0x00c00200, 0x00000f00, 0}},
+ {{0x00007812, 0x80000000, 0x078ef802, 0}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0}},
+ VertexTransform.back(),
+ }};
+ const u32 mode = GENERATE(0U, 1U, 2U);
+ if (mode == 2) input[2] = Native{{0x00007812, 0x80000000, 0x078ee202, 0}};
+ if (mode == 1) input[2] = Native{{0x00007212, 0x00000002, 0x078efcff, 0}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::LOP_reg: visitor.LOP_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected native bitwise LUT lowering");
+ }
+ }
+ for (const auto& inst : block.Instructions()) REQUIRE(inst.GetOpcode() != IR::Opcode::SetPred);
+ for (const float a : {-1.0f, 1.0f}) for (const float c : {-2.0f, 2.0f}) {
+ const u32 c_bits = std::bit_cast<u32>(c);
+ const u32 a_bits = std::bit_cast<u32>(a);
+ const u32 expected = mode == 2 ? (a_bits & 0x80000000U) | (c_bits & 0x7fffffffU) :
+ a_bits | (mode == 1 ? c_bits : c_bits & 0x80000000U);
+ REQUIRE(EvaluateVertex(block, {}, {a, 0, c, 0}) ==
+ std::array<float, 2>{std::bit_cast<float>(expected), 0});
+ }
+}
+
+TEST_CASE("Native one-dimensional texel fetch preserves its integer coordinate", "[native-shader]") {
+ const std::array<Native, 2> input{{
+ {{0x04017b66, 0x108008ff, 0x009e01ff, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TLDS(lowered.instructions.front());
+ u32 fetches = 0;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageFetch) {
+ ++fetches;
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type == Shader::TextureType::Color1D);
+ REQUIRE(inst.Arg(1).Inst()->GetOpcode() == IR::Opcode::GetRegister);
+ REQUIRE(inst.Arg(1).Inst()->Arg(0).Reg() == IR::Reg{4});
+ }
+ }
+ REQUIRE(fetches == 1);
+}
+
+TEST_CASE("Native call and return preserve values through the actual shader frontend", "[native-shader]") {
+ std::array<Native, 9> input{{
+ {{0x00007802, 0x40000000, 0x00000f00, 0}}, // argument 2.0
+ {{0x00377802, 0x00000030, 0x00000f00, 0}}, // native return PC
+ {{0x00007944, 0x00000020, 0x03c00000, 0}}, // call offset0x50
+ {{0xff007322, 0x00007003, 0x000000ff, 0}},
+ VertexTransform.back(),
+ {{0x00037421, 0x40400000, 0x00010000, 0}}, // callee: add 3.0
+ {{0x00087202, 0x00000037, 0x00000f00, 0}},
+ {{0x00097802, 0x00000000, 0x00000f00, 0}},
+ {{0x08007950, 0xffffff70, 0x03c3ffff, 0}},
+ }};
+ const bool alternate_link = GENERATE(false, true);
+ if (alternate_link) {
+ input[1].words[0] = 0x00397802;
+ input[6] = Native{{0x00007202, 0x00000039, 0x00000f00, 0}};
+ input[7].words[0] = 0x00017802;
+ input[8].words[0] = 0x00007950;
+ }
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(lowered.source_size == input.size() * 16);
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(std::span{input}.first(5)));
+ auto broken = input;
+ broken[1].words[1] += 16;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(broken));
+ broken = input;
+ broken.back().words[1] += 16;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(broken));
+ std::vector<u64> code(0x50 / 8);
+ for (const auto word : lowered.instructions) {
+ if ((code.size() * 8) % 32 == 0) code.push_back(0);
+ code.push_back(word);
+ }
+ class CallEnvironment final : public EmptyEnvironment {
+ public:
+ explicit CallEnvironment(const std::vector<u64>& code_) : code{code_} { stage = Shader::Stage::VertexB; }
+ u64 ReadInstruction(u32 address) override { return code.at(address / 8); }
+ const std::vector<u64>& code;
+ } env{code};
+ Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_pool;
+ Shader::ObjectPool<IR::Inst> inst_pool;
+ Shader::ObjectPool<IR::Block> block_pool;
+ Shader::Maxwell::Flow::CFG cfg{env, flow_pool, 0x50};
+ REQUIRE(cfg.Functions().size() == 2);
+ Shader::HostTranslateInfo host;
+ const auto program = Shader::Maxwell::TranslateProgram(inst_pool, block_pool, env, cfg, host);
+ u32 stores = 0;
+ for (const auto* block : program.blocks) {
+ for (const auto& inst : block->Instructions()) {
+ if (inst.GetOpcode() != IR::Opcode::SetAttribute) continue;
+ ++stores;
+ REQUIRE(inst.Arg(1).IsImmediate());
+ REQUIRE(inst.Arg(1).F32() == 5.0f);
+ }
+ }
+ REQUIRE(stores == 1);
+}
+
+TEST_CASE("Native scaled multiplication preserves captured power-of-two scales", "[native-shader]") {
+ std::array<Native, 4> input{{
+ VertexTransform.front(),
+ {{0x00007220, 0x00000001, 0x00310000, 0}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0}},
+ VertexTransform.back(),
+ }};
+ const u32 scale = GENERATE(3U, 4U, 5U, 6U);
+ input[1].words[2] = (scale << 20) | 0x10000;
+ const float factor = scale == 3 ? 0.5f : scale == 5 ? 2.0f : scale == 6 ? 4.0f : 1.0f;
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.ALD(lowered.instructions[0]);
+ visitor.FMUL_reg(lowered.instructions[1]);
+ visitor.AST(lowered.instructions[2]);
+ REQUIRE(EvaluateVertex(block, {8, 4}, {}) == std::array<float, 2>{32 * factor, 0});
+ REQUIRE(EvaluateVertex(block, {-2, 3}, {}) == std::array<float, 2>{-6 * factor, 0});
+}
+
+TEST_CASE("Native integer multiply preserves high words and modular multiply-add", "[native-shader]") {
+ const u32 mode = GENERATE(0U, 1U, 2U, 3U, 4U);
+ Native instruction{{0x00037827, 0xcccccccd, 0x078e00ff, 0}};
+ if (mode == 1) instruction = Native{{0x00037824, 0xfffffff6, 0x078e0202, 0}};
+ if (mode == 2) instruction = Native{{0x00037a24, 0x00c00000, 0x078e0202, 0}};
+ if (mode == 4) instruction = Native{{0x00037811, 0x00000070, 0x078e20ff, 0}};
+ if (mode == 3) instruction = Native{{0x00037224, 0x00000001, 0x078e0202, 0}};
+ const std::array<Native, 2> input{{instruction, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::XMAD_imm: visitor.XMAD_imm(word); break;
+ case Opcode::XMAD_reg: visitor.XMAD_reg(word); break;
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::ISCADD_reg: visitor.ISCADD_reg(word); break;
+ case Opcode::SHR_imm: visitor.SHR_imm(word); break;
+ case Opcode::IADD_reg: visitor.IADD_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected canonical integer multiplication instruction");
+ }
+ }
+ for (u32 a : {0U, 1U, 9U, 10U, 65535U, 65536U, 0x80000000U, 0xfffffffeU, 0xffffffffU, 0x12345678U}) {
+ std::array<u32, 256> regs{};
+ regs[0] = a;
+ regs[1] = 0x76543210;
+ regs[2] = 0xfedcba98;
+ std::unordered_map<const IR::Inst*, u32> values;
+ const auto get = [&](const IR::Value& value) -> u32 {
+ if (!value.IsImmediate()) return values.at(value.Inst());
+ return value.Type() == IR::Type::U1 ? static_cast<u32>(value.U1()) : value.U32();
+ };
+ for (const auto& inst : block.Instructions()) {
+ const auto arg = [&](size_t i) { return get(inst.Arg(i)); };
+ switch (inst.GetOpcode()) {
+ case IR::Opcode::GetRegister: values[&inst] = regs[static_cast<u32>(inst.Arg(0).Reg())]; break;
+ case IR::Opcode::SetRegister: regs[static_cast<u32>(inst.Arg(0).Reg())] = arg(1); break;
+ case IR::Opcode::GetCbufU32:
+ REQUIRE(arg(0) == 3);
+ REQUIRE(arg(1) == 0);
+ values[&inst] = 0x76543210;
+ break;
+ case IR::Opcode::BitFieldUExtract: values[&inst] = (arg(0) >> arg(1)) & ((1U << arg(2)) - 1); break;
+ case IR::Opcode::IMul32: values[&inst] = arg(0) * arg(1); break;
+ case IR::Opcode::IAdd32: values[&inst] = arg(0) + arg(1); break;
+ case IR::Opcode::ShiftLeftLogical32: values[&inst] = arg(0) << arg(1); break;
+ case IR::Opcode::ShiftRightLogical32: values[&inst] = arg(0) >> arg(1); break;
+ case IR::Opcode::ULessThan: values[&inst] = arg(0) < arg(1); break;
+ case IR::Opcode::SLessThan: values[&inst] = static_cast<s32>(arg(0)) < static_cast<s32>(arg(1)); break;
+ case IR::Opcode::SelectU32: values[&inst] = arg(0) ? arg(1) : arg(2); break;
+ default: FAIL("Unexpected multiplication IR operation " << IR::NameOf(inst.GetOpcode()));
+ }
+ }
+ const u32 b = mode == 0 ? 0xcccccccd : mode == 1 ? 0xfffffff6 : 0x76543210;
+ const u64 product = u64{a} * b;
+ const u32 expected = mode == 4 ? (a << 4) + 0x70U : mode == 0 ? static_cast<u32>(product >> 32)
+ : static_cast<u32>(product + 0xfedcba98U);
+ REQUIRE(regs[3] == expected);
+ }
+}
+
+TEST_CASE("Native uniform constants keep their indexed load and arithmetic values", "[native-shader]") {
+ const std::array<Native, 7> input{{
+ {{0xff007321, 0x000080ff, 0x00000000, 0}},
+ {{0x00047ab9, 0x00c00000, 0x00000800, 0}},
+ {{0x04047899, 0x00000002, 0x0800063f, 0}},
+ {{0x04057abb, 0x00c00000, 0x08000800, 0}},
+ {{0x00007c20, 0x00000005, 0x08410000, 0}},
+ {{0x00007e21, 0x80000005, 0x08010000, 0}},
+ VertexTransform.back(),
+ }};
+ auto code = input;
+ // Store the computed scalar instead of ending immediately.
+ std::vector<Native> program(code.begin(), code.end() - 1);
+ program.push_back(Native{{0xff007322, 0x00007000, 0x000000ff, 0}});
+ program.push_back(VertexTransform.back());
+ const auto lowered = Shader::Ampere::LowerToMaxwell(program);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::ALD: visitor.ALD(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::MOV_reg: visitor.MOV_reg(word); break;
+ case Opcode::LOP32I: visitor.LOP32I(word); break;
+ case Opcode::SHL_imm: visitor.SHL_imm(word); break;
+ case Opcode::LDC: visitor.LDC(word); break;
+ case Opcode::FMUL_reg: visitor.FMUL_reg(word); break;
+ case Opcode::FADD_reg: visitor.FADD_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected uniform instruction lowering");
+ }
+ }
+ const std::array<float, 4> constants{std::bit_cast<float>(1U), 3.0f, 7.0f, 9.0f};
+ REQUIRE(EvaluateVertex(block, {2, 0}, constants) == std::array<float, 2>{3, 0});
+ REQUIRE(EvaluateVertex(block, {4, 0}, constants) == std::array<float, 2>{9, 0});
+ auto conflict = program;
+ conflict[0].words[0] = 0xfff87321; // Native R248 conflicts with the reserved uniform bank.
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(conflict));
+}
+
+TEST_CASE("Native predicate-output LUTs preserve boolean bit tests", "[native-shader]") {
+ const std::array<Native, 3> input{{
+ {{0x00ff7812, 0x00000001, 0x0780c0ff, 0}},
+ {{0x01ff7812, 0x00000001, 0x07824802, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::LOP_reg: visitor.LOP_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected predicate LUT lowering");
+ }
+ }
+ for (u32 a = 0; a < 4; ++a) for (u32 b = 0; b < 4; ++b) for (u32 c = 0; c < 4; ++c) {
+ std::array<u32, 256> regs{};
+ regs[0] = a; regs[1] = b; regs[2] = c;
+ std::array<bool, 7> predicates{false, false, true, false, true, false, true};
+ std::unordered_map<const IR::Inst*, u32> values;
+ const auto get = [&](const IR::Value& value) -> u32 {
+ if (!value.IsImmediate()) return values.at(value.Inst());
+ return value.Type() == IR::Type::U1 ? static_cast<u32>(value.U1()) : value.U32();
+ };
+ for (const auto& inst : block.Instructions()) {
+ const auto arg = [&](size_t i) { return get(inst.Arg(i)); };
+ switch (inst.GetOpcode()) {
+ case IR::Opcode::GetRegister: values[&inst] = regs[static_cast<u32>(inst.Arg(0).Reg())]; break;
+ case IR::Opcode::SetRegister: regs[static_cast<u32>(inst.Arg(0).Reg())] = arg(1); break;
+ case IR::Opcode::BitwiseAnd32: values[&inst] = arg(0) & arg(1); break;
+ case IR::Opcode::BitwiseOr32: values[&inst] = arg(0) | arg(1); break;
+ case IR::Opcode::BitwiseXor32: values[&inst] = arg(0) ^ arg(1); break;
+ case IR::Opcode::INotEqual: values[&inst] = arg(0) != arg(1); break;
+ case IR::Opcode::SetPred: predicates[static_cast<u32>(inst.Arg(0).Pred())] = arg(1) != 0; break;
+ default: FAIL("Unexpected predicate LUT IR " << IR::NameOf(inst.GetOpcode()));
+ }
+ }
+ REQUIRE(predicates[0] == ((a & 1) != 0));
+ REQUIRE(predicates[1] == (((b ^ c) & 1) != 0));
+ REQUIRE(std::array<bool, 5>{predicates[2], predicates[3], predicates[4], predicates[5], predicates[6]} ==
+ std::array<bool, 5>{true, false, true, false, true});
+ }
+}
+
+TEST_CASE("Native cube sampling preserves XYZ, explicit LOD, and split RGB destinations", "[native-shader]") {
+ std::array<Native, 2> input{{
+ {{0x04007b60, 0x70800808, 0x059e0703, 0}},
+ VertexTransform.back(),
+ }};
+ const bool volume = GENERATE(false, true);
+ if (volume) {
+ input[0].words[1] = (input[0].words[1] & ~(7U << 29)) | (2U << 29);
+ input[0].words[2] = (input[0].words[2] & ~(7U << 23)) | (1U << 23);
+ }
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::MOV_reg: visitor.MOV_reg(word); break;
+ case Opcode::TEX: visitor.TEX(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected cube sampling lowering");
+ }
+ }
+ u32 samples = 0;
+ std::vector<std::pair<u32, u32>> copies;
+ std::vector<u32> writes;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) {
+ const u32 target = static_cast<u32>(inst.Arg(0).Reg());
+ if (target < 248) writes.push_back(target);
+ if (inst.Arg(1).Inst()->GetOpcode() == IR::Opcode::GetRegister)
+ copies.emplace_back(target, static_cast<u32>(inst.Arg(1).Inst()->Arg(0).Reg()));
+ }
+ if (inst.GetOpcode() == IR::Opcode::BoundImageSampleExplicitLod) {
+ ++samples;
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type == (volume ? Shader::TextureType::Color3D : Shader::TextureType::ColorCube));
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructF32x3);
+ for (u32 i = 0; i < 3; ++i)
+ REQUIRE(coords->Arg(i).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{250 + i});
+ if (volume) REQUIRE(inst.Arg(2).F32() == 0.0f);
+ else REQUIRE(inst.Arg(2).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{9});
+ }
+ }
+ REQUIRE(samples == 1);
+ REQUIRE(writes == std::vector<u32>{0, 1, 3});
+ REQUIRE(copies == std::vector<std::pair<u32, u32>>{{250, 4}, {251, 5}, {252, 8}, {0, 250}, {1, 251}, {3, 252}});
+}
+
+TEST_CASE("Native branch maps survive code caching and reject invalid addresses", "[native-shader]") {
+ std::vector<u64> code(64);
+ const std::array<u32, 4> offsets{0, 2, 4, 8};
+ Shader::Ampere::AppendNativeAddressMap(code, offsets);
+ REQUIRE(Shader::Ampere::HasNativeAddressMap(code));
+ REQUIRE(Shader::Ampere::NativeProgramAddress(code, 0) == 0x50U);
+ REQUIRE(Shader::Ampere::NativeProgramAddress(code, 16) == 0x68U);
+ const auto restored = code;
+ REQUIRE(Shader::Ampere::NativeProgramAddress(restored, 48) == 0xa8U);
+ REQUIRE_FALSE(Shader::Ampere::NativeProgramAddress(code, 1));
+ REQUIRE_FALSE(Shader::Ampere::NativeProgramAddress(code, 64));
+ code[code.size() - 2] = 100000;
+ REQUIRE_FALSE(Shader::Ampere::NativeProgramAddress(code, 0));
+}
+
+TEST_CASE("Native computed jumps retain table values while mapping code destinations", "[native-shader]") {
+ const std::array<Native, 12> input{{
+ {{0xff007321, 0x0002fcff, 0x00000000, 0}},
+ {{0x00007817, 0x00000001, 0x03800000, 0}},
+ {{0x00027819, 0x00000002, 0x000006ff, 0}},
+ {{0x02007b82, 0x00400000, 0x00000800, 0}},
+ {{0xff037421, 0x3f000000, 0x00010000, 0}},
+ {{0x00007949, 0xffffffa0, 0x0383ffff, 0}},
+ {{0x00007802, 0x3f800000, 0x00000f00, 0}},
+ {{0x00007947, 0x00000020, 0x03800000, 0}},
+ {{0x00007802, 0x40000000, 0x00000f00, 0}},
+ {{0x00007947, 0x00000000, 0x03800000, 0}},
+ {{0xff007322, 0x00007000, 0x000000ff, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ std::vector<u64> code(0x50 / 8);
+ for (const auto word : lowered.instructions) {
+ if ((code.size() * 8) % 32 == 0) code.push_back(0);
+ code.push_back(word);
+ }
+ code.push_back(0);
+ Shader::Ampere::AppendNativeAddressMap(code, lowered.source_instruction_offsets);
+ class BranchEnvironment final : public EmptyEnvironment {
+ public:
+ explicit BranchEnvironment(const std::vector<u64>& code_) : code{code_} { stage = Shader::Stage::VertexB; }
+ u64 ReadInstruction(u32 address) override { return code.at(address / 8); }
+ u32 ReadCbufValue(u32 bank, u32 offset) override {
+ REQUIRE(bank == 1);
+ REQUIRE((offset == 0 || offset == 4));
+ return offset == 0 ? 0x60 : 0x80;
+ }
+ u32 TranslateProgramAddress(u32 address) const override {
+ const auto target = Shader::Ampere::NativeProgramAddress(code, address);
+ if (!target) throw std::logic_error("invalid synthetic branch target");
+ return *target;
+ }
+ const std::vector<u64>& code;
+ } env{code};
+ Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_pool;
+ Shader::ObjectPool<IR::Inst> inst_pool;
+ Shader::ObjectPool<IR::Block> block_pool;
+ Shader::Maxwell::Flow::CFG cfg{env, flow_pool, 0x50};
+ u32 jumps = 0;
+ for (const auto& block : cfg.Functions().front().blocks) {
+ if (block.end_class != Shader::Maxwell::Flow::EndClass::IndirectBranch) continue;
+ ++jumps;
+ REQUIRE(block.branch_offset == 0);
+ REQUIRE(block.indirect_branches.size() == 2);
+ for (const auto& branch : block.indirect_branches) {
+ REQUIRE((branch.address == 0x60 || branch.address == 0x80));
+ REQUIRE(branch.block->begin.Offset() == env.TranslateProgramAddress(branch.address));
+ REQUIRE(branch.block->begin.Offset() != branch.address);
+ }
+ }
+ REQUIRE(jumps == 1);
+ Shader::HostTranslateInfo host;
+ const auto program = Shader::Maxwell::TranslateProgram(inst_pool, block_pool, env, cfg, host);
+ REQUIRE_FALSE(program.blocks.empty());
+}
+
+TEST_CASE("Native forward branches retain paths beyond an early unconditional exit", "[native-shader]") {
+ const Native exit{{0x0000794d, 0, 0x03800000, 0x000fea00}};
+ const std::array<Native, 4> source{{
+ {{0x00000947, 0x20, 0x03800000, 0x000fea00}},
+ exit,
+ {{0x00017802, 42, 0x00000f00, 0x000fe400}},
+ exit,
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(source);
+ REQUIRE(lowered);
+ REQUIRE(lowered.source_size == sizeof(source));
+ REQUIRE(lowered.source_instruction_offsets.size() == source.size());
+ REQUIRE(lowered.instructions.size() == 4);
+ const auto truncated = Shader::Ampere::LowerToMaxwell(std::span{source}.first(2));
+ REQUIRE_FALSE(truncated);
+ REQUIRE(truncated.instructions.empty());
+}
+
+TEST_CASE("Native register-to-predicate writes only masked predicate bits", "[native-shader]") {
+ const std::array<Native, 2> source{{{{0x05007804, 7, 0, 0x000fe200}}, VertexTransform.back()}};
+ for (u32 byte = 0; byte < 4; ++byte) {
+ auto selected = source;
+ selected[0].words[2] = byte << 12;
+ const auto lowered = Shader::Ampere::LowerToMaxwell(selected);
+ REQUIRE(lowered);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions[0]) == Shader::Maxwell::Opcode::R2P_imm);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.R2P_imm(lowered.instructions[0]);
+ for (u32 value = 0; value < 128; ++value) {
+ std::array<bool, 7> predicates{true, false, true, false, true, false, true};
+ std::unordered_map<const IR::Inst*, u32> values;
+ const auto get = [&](const IR::Value& v) -> u32 {
+ if (!v.IsImmediate()) return values.at(v.Inst());
+ return v.Type() == IR::Type::U1 ? static_cast<u32>(v.U1()) : v.U32();
+ };
+ for (const auto& inst : block.Instructions()) {
+ const auto arg = [&](size_t i) { return get(inst.Arg(i)); };
+ switch (inst.GetOpcode()) {
+ case IR::Opcode::GetRegister:
+ REQUIRE(static_cast<u32>(inst.Arg(0).Reg()) == 5);
+ values[&inst] = value << (byte * 8); break;
+ case IR::Opcode::GetPred: values[&inst] = predicates[static_cast<u32>(inst.Arg(0).Pred())]; break;
+ case IR::Opcode::SetPred: predicates[static_cast<u32>(inst.Arg(0).Pred())] = arg(1) != 0; break;
+ case IR::Opcode::BitFieldUExtract: values[&inst] = (arg(0) >> arg(1)) & ((1U << arg(2)) - 1); break;
+ case IR::Opcode::GetZeroFromOp: values[&inst] = arg(0) == 0; break;
+ case IR::Opcode::IEqual: values[&inst] = arg(0) == arg(1); break;
+ case IR::Opcode::LogicalNot: values[&inst] = !arg(0); break;
+ case IR::Opcode::SelectU1: values[&inst] = arg(0) ? arg(1) : arg(2); break;
+ default: FAIL("Unexpected R2P IR " << IR::NameOf(inst.GetOpcode()));
+ }
+ }
+ for (u32 bit = 0; bit < 7; ++bit)
+ REQUIRE(predicates[bit] == (bit < 3 ? bool(value & (1U << bit)) : bool(bit % 2 == 0)));
+ }
+ }
+ auto invalid = source;
+ invalid[0].words[2] = 1;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(invalid));
+}
+
+TEST_CASE("Native 2D explicit LOD sampling preserves its paired coordinates", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0xc007b60, 0x30800812, 0x59e070b, 0x100f4200}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TEXS(lowered.instructions[0]);
+ u32 samples = 0;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() != IR::Opcode::BoundImageSampleExplicitLod) continue;
+ ++samples;
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type == Shader::TextureType::Color2D);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->GetOpcode() == IR::Opcode::CompositeConstructF32x2);
+ for (u32 i = 0; i < 2; ++i)
+ REQUIRE(coords->Arg(i).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{12 + i});
+ REQUIRE(inst.Arg(2).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{18});
+ }
+ REQUIRE(samples == 1);
+}
+
+namespace {
+using Warp = std::array<u32, 32>;
+void ExecuteNativeWarp(IR::Block& block, u32 active,
+ std::array<std::array<u32, 256>, 32>& registers,
+ std::array<std::array<bool, 7>, 32>& predicates) {
+ std::unordered_map<const IR::Inst*, Warp> values;
+ const auto get = [&](const IR::Value& value) -> Warp {
+ if (!value.IsImmediate()) return values.at(value.Inst());
+ Warp result;
+ result.fill(value.Type() == IR::Type::U1 ? static_cast<u32>(value.U1()) : value.U32());
+ return result;
+ };
+ for (const auto& inst : block.Instructions()) {
+ std::array<Warp, 4> args{};
+ for (size_t i = 0; i < inst.NumArgs(); ++i) {
+ if (inst.Arg(i).Type() != IR::Type::Reg && inst.Arg(i).Type() != IR::Type::Pred)
+ args[i] = get(inst.Arg(i));
+ }
+ Warp result{};
+ const auto op = inst.GetOpcode();
+ if (op == IR::Opcode::SubgroupBallot || op == IR::Opcode::VoteAny || op == IR::Opcode::VoteAll) {
+ u32 mask = 0;
+ for (u32 lane = 0; lane < 32; ++lane)
+ if ((active & (1U << lane)) && args[0][lane]) mask |= 1U << lane;
+ result.fill(op == IR::Opcode::SubgroupBallot ? mask : op == IR::Opcode::VoteAny ? mask != 0 : mask == active);
+ } else {
+ for (u32 lane = 0; lane < 32; ++lane) {
+ const auto a = args[0][lane], b = args[1][lane], c = args[2][lane];
+ switch (op) {
+ case IR::Opcode::GetRegister: result[lane] = static_cast<u32>(inst.Arg(0).Reg()) == 255 ? 0 : registers[lane][static_cast<u32>(inst.Arg(0).Reg())]; break;
+ case IR::Opcode::SetRegister:
+ if ((active & (1U << lane)) && static_cast<u32>(inst.Arg(0).Reg()) != 255) registers[lane][static_cast<u32>(inst.Arg(0).Reg())] = b;
+ break;
+ case IR::Opcode::GetPred: result[lane] = predicates[lane][static_cast<u32>(inst.Arg(0).Pred())]; break;
+ case IR::Opcode::SetPred:
+ if (active & (1U << lane)) predicates[lane][static_cast<u32>(inst.Arg(0).Pred())] = b != 0;
+ break;
+ case IR::Opcode::BitCastF32U32: case IR::Opcode::BitCastU32F32: result[lane] = a; break;
+ case IR::Opcode::SelectU32: case IR::Opcode::SelectU1: result[lane] = a ? b : c; break;
+ case IR::Opcode::BitwiseAnd32: result[lane] = a & b; break;
+ case IR::Opcode::BitwiseOr32: result[lane] = a | b; break;
+ case IR::Opcode::ShiftRightLogical32: result[lane] = b < 32 ? a >> b : 0; break;
+ case IR::Opcode::ShiftLeftLogical32: result[lane] = b < 32 ? a << b : 0; break;
+ case IR::Opcode::INeg32: result[lane] = 0U - a; break;
+ case IR::Opcode::IAdd32: result[lane] = a + b; break;
+ case IR::Opcode::FindUMsb32: result[lane] = a ? 31U - std::countl_zero(a) : ~0U; break;
+ case IR::Opcode::BitFieldUExtract: result[lane] = (a >> b) & ((1U << c) - 1); break;
+ case IR::Opcode::ULessThan: result[lane] = a < b; break;
+ case IR::Opcode::SLessThan: result[lane] = static_cast<s32>(a) < static_cast<s32>(b); break;
+ case IR::Opcode::IEqual: result[lane] = a == b; break;
+ case IR::Opcode::GetZeroFromOp: result[lane] = a == 0; break;
+ case IR::Opcode::LogicalAnd: result[lane] = a && b; break;
+ case IR::Opcode::LogicalNot: result[lane] = !a; break;
+ case IR::Opcode::GetInBoundsFromOp: result[lane] = get(inst.Arg(0).Inst()->Arg(1))[lane] <= 31; break;
+ case IR::Opcode::FSwizzleAdd: {
+ const u32 mode = (c >> ((lane & 3) * 2)) & 3;
+ const float x = std::bit_cast<float>(a), y = std::bit_cast<float>(b);
+ const float ma = mode == 0 || mode == 2 ? -1.0f : mode == 1 ? 1.0f : 0.0f;
+ const float mb = mode == 0 || mode == 1 ? -1.0f : mode == 2 ? 1.0f : 0.0f;
+ result[lane] = std::bit_cast<u32>(x * ma + y * mb);
+ break;
+ }
+ case IR::Opcode::ShuffleButterfly: result[lane] = args[0][lane ^ b]; break;
+ case IR::Opcode::ShuffleIndex: result[lane] = (active & (1U << (b & 31))) ? args[0][b & 31] : 0xdeadbeefU; break;
+ default: FAIL("Unexpected warp-uniform IR " << IR::NameOf(op));
+ }
+ }
+ }
+ values.emplace(&inst, result);
+ }
+}
+}
+
+TEST_CASE("Native warp uniform transfers use the first selected active lane and preserve predicates", "[native-shader]") {
+ const u32 operation = GENERATE(0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U);
+ const Native instruction = operation < 2
+ ? Native{{operation == 0 ? 0x020513c2U : 0x020593c2U, 0, 0x000e0000, 0}}
+ : operation < 4 ? Native{{0x003f7886, 0, operation == 2 ? 0x00800100U : 0x00800000U, 0}}
+ : Native{{0x0000781c, 0, operation == 4 ? 0x03f2f008U : operation == 5 ? 0x03f0f070U : operation == 6 ? 0x03f0e170U : 0x00f41070U, 0}};
+ const std::array<Native, 2> source{{instruction, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(source);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::PSETP: visitor.PSETP(word); break;
+ case Opcode::SHR_imm: visitor.SHR_imm(word); break;
+ case Opcode::SHL_imm: visitor.SHL_imm(word); break;
+ case Opcode::MOV32I: visitor.MOV32I(word); break;
+ case Opcode::P2R_imm: visitor.P2R_imm(word); break;
+ case Opcode::R2P_imm: visitor.R2P_imm(word); break;
+ case Opcode::VOTE: visitor.VOTE(word); break;
+ case Opcode::IADD_reg: visitor.IADD_reg(word); break;
+ case Opcode::LOP_reg: visitor.LOP_reg(word); break;
+ case Opcode::FLO_reg: visitor.FLO_reg(word); break;
+ case Opcode::SHFL: visitor.SHFL(word); break;
+ case Opcode::SEL_reg: visitor.SEL_reg(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected warp-uniform lowering");
+ }
+ }
+ using Warp = std::array<u32, 32>;
+ for (const u32 active : {1U, 0x80000000U, 0x80000001U, 0x55555555U, 0xffffffffU}) {
+ for (const u32 selected : {0U, 1U, 0x80000000U, 0xaaaaaaaaU, 0xffffffffU}) {
+ std::array<std::array<u32, 256>, 32> registers{};
+ std::array<std::array<bool, 7>, 32> predicates{};
+ for (u32 lane = 0; lane < 32; ++lane) {
+ registers[lane][2] = 100 + lane * 17;
+ registers[lane][221] = 0xcafebeef;
+ registers[lane][239] = (selected & (1U << lane)) ? 64 : 0;
+ for (u32 p = 0; p < 7; ++p) predicates[lane][p] = ((lane + p) & 1) != 0;
+ predicates[lane][1] = ((selected & (1U << lane)) != 0) != (operation == 4);
+ }
+ const auto original_predicates = predicates;
+ ExecuteNativeWarp(block, active, registers, predicates);
+ const u32 ballot = active & (operation == 1 ? ~selected : selected);
+ const u32 first = ballot ? std::countr_zero(ballot) : 0;
+ for (u32 lane = 0; lane < 32; ++lane) {
+ if (!(active & (1U << lane))) continue;
+ auto expected_predicates = original_predicates[lane];
+ if (operation == 7) expected_predicates[2] = original_predicates[lane][1] && original_predicates[lane][0];
+ if (operation == 4) expected_predicates[1] = (selected & (1U << lane)) != 0;
+ if (operation == 5 || operation == 6) expected_predicates[0] = operation == 5;
+ REQUIRE(predicates[lane] == expected_predicates);
+ if (operation < 2) REQUIRE(registers[lane][221] == (ballot ? 100 + first * 17 : 0xcafebeef));
+ else if (operation < 4) REQUIRE(registers[lane][239] == (operation == 2 ? (ballot ? 64U : 0U) : (ballot == active ? 64U : 0U)));
+ }
+ }
+ }
+}
+
+TEST_CASE("Native uniform 64-bit loads populate consecutive registers", "[native-shader]") {
+ const std::array<Native, 6> input{{
+ {{0x00057ab9, 0x00c00000, 0x00000800, 0}},
+ {{0x05067abb, 0x00c00100, 0x08000a00, 0}},
+ {{0x00007c02, 6, 0x08000f00, 0}},
+ {{0x00017c02, 7, 0x08000f00, 0}},
+ {{0xff007322, 0x00007000, 0x000004ff, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (const auto word : lowered.instructions) {
+ using Shader::Maxwell::Opcode;
+ switch (Shader::Maxwell::Decode(word)) {
+ case Opcode::MOV_cbuf: visitor.MOV_cbuf(word); break;
+ case Opcode::MOV_reg: visitor.MOV_reg(word); break;
+ case Opcode::LDC: visitor.LDC(word); break;
+ case Opcode::AST: visitor.AST(word); break;
+ case Opcode::EXIT: break;
+ default: FAIL("Unexpected uniform pair lowering");
+ }
+ }
+ REQUIRE(EvaluateVertex(block, {}, {std::bit_cast<float>(0U), 3, 7, 9}) == std::array<float, 2>{3, 7});
+}
+
+TEST_CASE("Native texture dimension queries retain mip input and width-height outputs", "[native-shader]") {
+ const std::array<Native, 2> source{{{{0x03027b6f, 0x00800800, 0x000003ff, 0}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(source);
+ REQUIRE(lowered);
+ REQUIRE(lowered.required_texture_buffer == 2);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions[0]) == Shader::Maxwell::Opcode::TXQ);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.TXQ(lowered.instructions[0]);
+ std::vector<std::pair<u32, u32>> outputs;
+ u32 queries = 0;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageQueryDimensions) {
+ ++queries;
+ REQUIRE(inst.Arg(0).U32() == 32);
+ REQUIRE(inst.Arg(1).Inst()->Arg(0).Reg() == IR::Reg{3});
+ REQUIRE(inst.Arg(2).U1());
+ }
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) {
+ const auto* extract = inst.Arg(1).Inst();
+ REQUIRE(extract->GetOpcode() == IR::Opcode::CompositeExtractU32x4);
+ outputs.emplace_back(static_cast<u32>(inst.Arg(0).Reg()), extract->Arg(1).U32());
+ }
+ }
+ REQUIRE(queries == 1);
+ REQUIRE(outputs == std::vector<std::pair<u32, u32>>{{2, 0}, {3, 1}});
+ auto invalid = source;
+ invalid[0].words[1] |= 1U << 30;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(invalid));
+}
+
+TEST_CASE("Native scalar global stores retain both uniform address words and source data", "[native-shader]") {
+ std::array<Native, 2> input{{
+ {{0xff007986, 7, 0x0c10a904, 0}},
+ VertexTransform.back(),
+ }};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions.front()) == Shader::Maxwell::Opcode::STG);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.STG(lowered.instructions.front());
+ std::vector<u32> registers;
+ u32 stores{};
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::GetRegister)
+ registers.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ if (inst.GetOpcode() == IR::Opcode::CompositeConstructU32x2) {
+ REQUIRE(inst.Arg(0).Inst()->Arg(0).Reg() == IR::Reg::R220);
+ REQUIRE(inst.Arg(1).Inst()->Arg(0).Reg() == IR::Reg::R221);
+ }
+ if (inst.GetOpcode() == IR::Opcode::WriteGlobal32) {
+ REQUIRE(inst.Arg(1).Inst()->Arg(0).Reg() == IR::Reg::R7);
+ ++stores;
+ }
+ }
+ std::sort(registers.begin(), registers.end());
+ REQUIRE(registers == std::vector<u32>{7, 220, 221});
+ REQUIRE(stores == 1);
+ input[0].words[2] |= 1; // An unaligned uniform address pair is unsupported.
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(input));
+ input[0].words[2] = 0x0c10ab04; // A different store width must not become scalar.
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(input));
+}
+
+TEST_CASE("Captured native compute compiles with shared memory and atomics", "[.native-compute-captured]") {
+ const char* directory = std::getenv("EDEN_NATIVE_SHADER_FIXTURES");
+ REQUIRE(directory != nullptr);
+ std::ifstream file(std::filesystem::path{directory} / "compute.source.bin", std::ios::binary | std::ios::ate);
+ REQUIRE(file.good());
+ const auto size = static_cast<size_t>(file.tellg());
+ REQUIRE(size % 16 == 0);
+ std::vector<Native> input(size / 16);
+ file.seekg(0);
+ file.read(reinterpret_cast<char*>(input.data()), size);
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ INFO("offset=" << lowered.error_offset << " error=" << lowered.error);
+ REQUIRE(lowered);
+ std::vector<u64> code(0x50 / 8);
+ for (u64 word : lowered.instructions) {
+ if ((code.size() * 8) % 32 == 0) code.push_back(0);
+ code.push_back(word);
+ }
+ code.push_back(0);
+ Shader::Ampere::AppendNativeAddressMap(code, lowered.source_instruction_offsets);
+ std::array<u32, 64> qmd{};
+ std::ifstream descriptor(std::filesystem::path{directory} / "compute.qmd.bin", std::ios::binary);
+ REQUIRE(descriptor.good());
+ descriptor.read(reinterpret_cast<char*>(qmd.data()), sizeof(qmd));
+ const std::array<u32, 3> group{qmd[18] >> 16, qmd[19] & 0xffff, qmd[19] >> 16};
+ const u32 shared_size = qmd[17] & 0x3ffff;
+ class ComputeFixtureEnvironment final : public EmptyEnvironment {
+ public:
+ ComputeFixtureEnvironment(const std::vector<u64>& code_, u32 shared_, std::array<u32, 3> group_, Shader::TextureType texture_)
+ : code{code_}, shared{shared_}, group{group_}, texture{texture_} {
+ stage = Shader::Stage::Compute;
+ start_address = 0x50;
+ is_proprietary_driver = true;
+ }
+ u64 ReadInstruction(u32 address) override { return code.at(address / 8); }
+ u32 TranslateProgramAddress(u32 address) const override {
+ const auto translated = Shader::Ampere::NativeProgramAddress(code, address);
+ if (!translated) throw std::logic_error("invalid compute branch target");
+ return *translated;
+ }
+ u32 ReadCbufValue(u32, u32) override { return 0; }
+ u32 TextureBoundBuffer() const override { return 0; }
+ u32 SharedMemorySize() const override { return shared; }
+ std::array<u32, 3> WorkgroupSize() const override { return group; }
+ Shader::TextureType ReadTextureType(u32) override { return texture; }
+ const std::vector<u64>& code;
+ u32 shared;
+ std::array<u32, 3> group;
+ Shader::TextureType texture;
+ } env{code, shared_size, group,
+ std::any_of(input.begin(), input.end(), [](const auto& instruction) {
+ return instruction.Bits(0, 12) == 0xb66 && instruction.Bits(61, 3) == 2;
+ }) ? Shader::TextureType::Color3D : std::any_of(input.begin(), input.end(), [](const auto& instruction) {
+ return (instruction.Bits(0, 12) == 0xb66 && instruction.Bits(61, 3) == 1) ||
+ instruction.Bits(0, 12) == 0xb97 || instruction.Bits(0, 12) == 0xb9b;
+ }) ? Shader::TextureType::Color2D : Shader::TextureType::Color1D};
+ Shader::ObjectPool<Shader::Maxwell::Flow::Block> flow_pool;
+ Shader::ObjectPool<IR::Inst> inst_pool;
+ Shader::ObjectPool<IR::Block> block_pool;
+ Shader::Maxwell::Flow::CFG cfg{env, flow_pool, env.StartAddress()};
+ Shader::HostTranslateInfo host;
+ host.support_int64 = true;
+ host.support_float64 = true;
+ host.support_float16 = true;
+ host.support_conditional_barrier = true;
+ const auto program = Shader::Maxwell::TranslateProgram(inst_pool, block_pool, env, cfg, host);
+ REQUIRE(program.stage == Shader::Stage::Compute);
+ REQUIRE_FALSE(program.blocks.empty());
+ u32 barriers{}, shared_atomics{}, global_atomics{}, stores{}, image_stores{}, image_loads{};
+ for (const auto* block : program.blocks) for (const auto& inst : block->Instructions()) {
+ image_loads += inst.GetOpcode() == IR::Opcode::ImageRead || inst.GetOpcode() == IR::Opcode::BoundImageRead;
+ barriers += inst.GetOpcode() == IR::Opcode::Barrier;
+ shared_atomics += inst.GetOpcode() == IR::Opcode::SharedAtomicIAdd32;
+ global_atomics += inst.GetOpcode() == IR::Opcode::GlobalAtomicIAdd32 || inst.GetOpcode() == IR::Opcode::StorageAtomicIAdd32;
+ stores += inst.GetOpcode() == IR::Opcode::WriteGlobal32 || inst.GetOpcode() == IR::Opcode::WriteStorage32;
+ image_stores += inst.GetOpcode() == IR::Opcode::ImageWrite || inst.GetOpcode() == IR::Opcode::BoundImageWrite;
+ }
+ const bool surface = std::any_of(input.begin(), input.end(), [](const auto& instruction) { return instruction.Bits(0, 12) == 0xb9b; });
+ if (surface) {
+ const auto expected_barriers = std::count_if(input.begin(), input.end(), [](const auto& instruction) {
+ return instruction.Bits(0, 12) == 0xb1d;
+ });
+ REQUIRE(barriers >= expected_barriers);
+ REQUIRE(image_stores >= 1);
+ const auto expected_loads = std::count_if(input.begin(), input.end(), [](const auto& instruction) {
+ return instruction.Bits(0, 12) == 0xb97;
+ });
+ REQUIRE(image_loads >= expected_loads);
+ } else if (std::any_of(input.begin(), input.end(), [](const auto& instruction) { return instruction.Bits(0, 12) == 0xf8c; })) {
+ REQUIRE(barriers >= 2);
+ REQUIRE(shared_atomics >= 4);
+ REQUIRE(global_atomics >= 2);
+ } else if (shared_size) {
+ REQUIRE(barriers >= 5);
+ REQUIRE(shared_atomics >= 2);
+ REQUIRE(global_atomics >= 1);
+ REQUIRE(stores >= 8);
+ } else {
+ REQUIRE(barriers == 0);
+ REQUIRE(stores >= 4);
+ }
+}
+
+namespace {
+double DecodeTestHalf(u16 bits) {
+ const u32 exponent = (bits >> 10) & 31, mantissa = bits & 1023;
+ const double value = exponent == 31 ? (mantissa ? std::numeric_limits<double>::quiet_NaN()
+ : std::numeric_limits<double>::infinity())
+ : exponent == 0 ? std::ldexp(static_cast<double>(mantissa), -24)
+ : std::ldexp(static_cast<double>(1024 + mantissa), static_cast<int>(exponent) - 25);
+ return (bits & 0x8000) ? -value : value;
+}
+u16 EncodeTestHalf(double value) {
+ const u16 sign = std::signbit(value) ? 0x8000 : 0;
+ value = std::abs(value);
+ if (std::isnan(value)) return 0xfe01; // Deliberately noncanonical: ReLU must canonicalize it.
+ if (value >= 65520) return sign | 0x7c00;
+ if (value < std::ldexp(1.0, -14))
+ return sign | static_cast<u16>(std::nearbyint(std::ldexp(value, 24)));
+ int exponent{};
+ std::frexp(value, &exponent);
+ --exponent;
+ u32 mantissa = static_cast<u32>(std::nearbyint(std::ldexp(value, 10 - exponent)));
+ if (mantissa == 2048) { ++exponent; mantissa = 1024; }
+ return sign | static_cast<u16>(((exponent + 15) << 10) | (mantissa - 1024));
+}
+struct ScalarNativeMachine {
+ std::array<u32, 256> regs{};
+ std::array<bool, 7> predicates{};
+ std::unordered_map<u64, u32> constants, global, shared;
+ u32 workgroup_barriers{}, device_barriers{};
+ void Execute(IR::Block& block) {
+ std::unordered_map<const IR::Inst*, u64> values;
+ bool carry{}, zero{}, sign{}, overflow{};
+ const auto get = [&](const IR::Value& value) -> u64 {
+ if (!value.IsImmediate()) return values.at(value.Inst());
+ if (value.Type() == IR::Type::U1) return value.U1();
+ if (value.Type() == IR::Type::F16 || value.Type() == IR::Type::U16) return value.U16();
+ if (value.Type() == IR::Type::U64) return value.U64();
+ if (value.Type() == IR::Type::F32) return std::bit_cast<u32>(value.F32());
+ return value.U32();
+ };
+ for (const auto& inst : block.Instructions()) {
+ const auto arg = [&](size_t i) { return get(inst.Arg(i)); };
+ u64 result{};
+ switch (inst.GetOpcode()) {
+ case IR::Opcode::BitCastF32U32: case IR::Opcode::BitCastU32F32: result = arg(0); break;
+ case IR::Opcode::UnpackFloat2x16: result = (arg(0) & 0xffff) | ((arg(0) >> 16) << 32); break;
+ case IR::Opcode::PackFloat2x16: result = (arg(0) & 0xffff) | ((arg(0) >> 32) << 16); break;
+ case IR::Opcode::CompositeConstructF16x2: result = arg(0) | (arg(1) << 32); break;
+ case IR::Opcode::CompositeExtractF16x2: result = (arg(0) >> (arg(1) * 32)) & 0xffff; break;
+ case IR::Opcode::FPNeg16: result = arg(0) ^ 0x8000; break;
+ case IR::Opcode::FPAbs16: result = arg(0) & 0x7fff; break;
+ case IR::Opcode::FPFma16: result = EncodeTestHalf(std::fma(DecodeTestHalf(static_cast<u16>(arg(0))),
+ DecodeTestHalf(static_cast<u16>(arg(1))), DecodeTestHalf(static_cast<u16>(arg(2))))); break;
+ case IR::Opcode::FPFloor32: result = std::bit_cast<u32>(std::floor(std::bit_cast<float>(static_cast<u32>(arg(0))))); break;
+ case IR::Opcode::ConvertF32F16: result = std::bit_cast<u32>(static_cast<float>(DecodeTestHalf(static_cast<u16>(arg(0))))); break;
+ case IR::Opcode::FPOrdEqual32: result = std::bit_cast<float>(static_cast<u32>(arg(0))) == std::bit_cast<float>(static_cast<u32>(arg(1))); break;
+ case IR::Opcode::FPOrdGreaterThanEqual32: result = std::bit_cast<float>(static_cast<u32>(arg(0))) >= std::bit_cast<float>(static_cast<u32>(arg(1))); break;
+ case IR::Opcode::FPMax32: result = std::bit_cast<u32>(std::fmax(std::bit_cast<float>(static_cast<u32>(arg(0))), std::bit_cast<float>(static_cast<u32>(arg(1))))); break;
+ case IR::Opcode::FPMin32: result = std::bit_cast<u32>(std::fmin(std::bit_cast<float>(static_cast<u32>(arg(0))), std::bit_cast<float>(static_cast<u32>(arg(1))))); break;
+ case IR::Opcode::ConvertU32F32: result = static_cast<u32>(std::bit_cast<float>(static_cast<u32>(arg(0)))); break;
+ case IR::Opcode::FPTrunc32: result = std::bit_cast<u32>(std::trunc(std::bit_cast<float>(static_cast<u32>(arg(0))))); break;
+ case IR::Opcode::FPNeg32: result = arg(0) ^ 0x80000000U; break;
+ case IR::Opcode::FPAbs32: result = arg(0) & 0x7fffffffU; break;
+ case IR::Opcode::ConvertF16F32: result = EncodeTestHalf(std::bit_cast<float>(static_cast<u32>(arg(0)))); break;
+ case IR::Opcode::FPRoundEven32: result = std::bit_cast<u32>(std::nearbyint(std::bit_cast<float>(static_cast<u32>(arg(0))))); break;
+ case IR::Opcode::FPIsNan32: result = std::isnan(std::bit_cast<float>(static_cast<u32>(arg(0)))); break;
+ case IR::Opcode::FPClamp32: result = std::bit_cast<u32>(std::clamp(std::bit_cast<float>(static_cast<u32>(arg(0))), std::bit_cast<float>(static_cast<u32>(arg(1))), std::bit_cast<float>(static_cast<u32>(arg(2))))); break;
+ case IR::Opcode::ConvertS32F32: {
+ const float value = std::bit_cast<float>(static_cast<u32>(arg(0)));
+ result = std::isnan(value) ? 0x87654321U : static_cast<u32>(static_cast<s32>(value));
+ break;
+ }
+ case IR::Opcode::GetRegister: result = inst.Arg(0).Reg() == IR::Reg::RZ ? 0 : regs[static_cast<u32>(inst.Arg(0).Reg())]; break;
+ case IR::Opcode::SetRegister: if (inst.Arg(0).Reg() != IR::Reg::RZ) regs[static_cast<u32>(inst.Arg(0).Reg())] = static_cast<u32>(arg(1)); break;
+ case IR::Opcode::GetPred: result = predicates[static_cast<u32>(inst.Arg(0).Pred())]; break;
+ case IR::Opcode::SetPred: predicates[static_cast<u32>(inst.Arg(0).Pred())] = arg(1) != 0; break;
+ case IR::Opcode::IAdd32: result = static_cast<u32>(arg(0) + arg(1)); break;
+ case IR::Opcode::ISub32: result = static_cast<u32>(arg(0) - arg(1)); break;
+ case IR::Opcode::IMul32: result = static_cast<u32>(arg(0) * arg(1)); break;
+ case IR::Opcode::IAdd64: result = arg(0) + arg(1); break;
+ case IR::Opcode::UMax32: result = std::max(arg(0), arg(1)); break;
+ case IR::Opcode::UMin32: result = std::min(arg(0), arg(1)); break;
+ case IR::Opcode::FindUMsb32: result = arg(0) ? 31U - std::countl_zero(static_cast<u32>(arg(0))) : ~0U; break;
+ case IR::Opcode::BitReverse32: {
+ const u32 source = static_cast<u32>(arg(0));
+ for (u32 bit = 0; bit < 32; ++bit) result |= u64{(source >> bit) & 1U} << (31 - bit);
+ break;
+ }
+ case IR::Opcode::ShiftLeftLogical64: result = arg(1) < 64 ? arg(0) << arg(1) : 0; break;
+ case IR::Opcode::ShiftRightLogical64: result = arg(1) < 64 ? arg(0) >> arg(1) : 0; break;
+ case IR::Opcode::INeg32: result = 0U - static_cast<u32>(arg(0)); break;
+ case IR::Opcode::ShiftLeftLogical32: result = arg(1) < 32 ? static_cast<u32>(arg(0) << arg(1)) : 0; break;
+ case IR::Opcode::ShiftRightArithmetic32: result = static_cast<u32>(static_cast<s32>(arg(0)) >> std::min<u64>(arg(1), 31)); break;
+ case IR::Opcode::ShiftRightLogical32: result = arg(1) < 32 ? static_cast<u32>(arg(0)) >> arg(1) : 0; break;
+ case IR::Opcode::BitwiseAnd32: result = arg(0) & arg(1); break;
+ case IR::Opcode::BitwiseOr32: result = arg(0) | arg(1); break;
+ case IR::Opcode::BitwiseXor32: result = arg(0) ^ arg(1); break;
+ case IR::Opcode::BitwiseNot32: result = arg(0) ^ 0xffffffffULL; break;
+ case IR::Opcode::BitFieldUExtract: result = (arg(0) >> arg(1)) & ((u64{1} << arg(2)) - 1); break;
+ case IR::Opcode::BitFieldInsert: { const u64 mask = ((u64{1} << arg(3)) - 1) << arg(2); result = (arg(0) & ~mask) | ((arg(1) << arg(2)) & mask); break; }
+ case IR::Opcode::SelectF32: case IR::Opcode::SelectU32: case IR::Opcode::SelectU1: result = arg(0) ? arg(1) : arg(2); break;
+ case IR::Opcode::LogicalAnd: result = arg(0) && arg(1); break;
+ case IR::Opcode::LogicalOr: result = arg(0) || arg(1); break;
+ case IR::Opcode::LogicalNot: result = !arg(0); break;
+ case IR::Opcode::SMin32: result = static_cast<u32>(std::min(static_cast<s32>(arg(0)), static_cast<s32>(arg(1)))); break;
+ case IR::Opcode::SMax32: result = static_cast<u32>(std::max(static_cast<s32>(arg(0)), static_cast<s32>(arg(1)))); break;
+ case IR::Opcode::SLessThan: result = static_cast<s32>(arg(0)) < static_cast<s32>(arg(1)); break;
+ case IR::Opcode::ULessThan: result = arg(0) < arg(1); break;
+ case IR::Opcode::IEqual: result = arg(0) == arg(1); break;
+ case IR::Opcode::GetCFlag: result = carry; break;
+ case IR::Opcode::SetCFlag: carry = arg(0) != 0; break;
+ case IR::Opcode::GetZFlag: result = zero; break;
+ case IR::Opcode::SetZFlag: zero = arg(0) != 0; break;
+ case IR::Opcode::GetSFlag: result = sign; break;
+ case IR::Opcode::SetSFlag: sign = arg(0) != 0; break;
+ case IR::Opcode::GetOFlag: result = overflow; break;
+ case IR::Opcode::SetOFlag: overflow = arg(0) != 0; break;
+ case IR::Opcode::GetCarryFromOp: { const auto* sum = inst.Arg(0).Inst(); result = get(sum->Arg(0)) + get(sum->Arg(1)) > 0xffffffffULL; break; }
+ case IR::Opcode::GetZeroFromOp: result = arg(0) == 0; break;
+ case IR::Opcode::GetSignFromOp: result = (arg(0) & 0x80000000U) != 0; break;
+ case IR::Opcode::GetOverflowFromOp: { const auto* sum = inst.Arg(0).Inst(); result = ((get(sum->Arg(0)) ^ arg(0)) & (get(sum->Arg(1)) ^ arg(0)) & 0x80000000U) != 0; break; }
+ case IR::Opcode::GetCbufU32: result = constants.at((arg(0) << 32) | arg(1)); break;
+ case IR::Opcode::CompositeConstructU32x2: result = arg(0) | (arg(1) << 32); break;
+ case IR::Opcode::PackUint2x32: case IR::Opcode::UnpackUint2x32: result = arg(0); break;
+ case IR::Opcode::CompositeExtractU32x2: result = static_cast<u32>(arg(0) >> (arg(1) * 32)); break;
+ case IR::Opcode::WriteSharedU64: shared[arg(0)] = static_cast<u32>(arg(1)); shared[arg(0) + 4] = static_cast<u32>(arg(1) >> 32); break;
+ case IR::Opcode::LoadSharedU64: result = shared.at(arg(0)) | (u64{shared.at(arg(0) + 4)} << 32); break;
+ case IR::Opcode::WriteGlobal32: global[arg(0)] = static_cast<u32>(arg(1)); break;
+ case IR::Opcode::WriteSharedU32: shared[arg(0)] = static_cast<u32>(arg(1)); break;
+ case IR::Opcode::LoadSharedU32: result = shared.at(arg(0)); break;
+ case IR::Opcode::GlobalAtomicIAdd32: result = global.at(arg(0)); global[arg(0)] += static_cast<u32>(arg(1)); break;
+ case IR::Opcode::SharedAtomicIAdd32: result = shared.at(arg(0)); shared[arg(0)] += static_cast<u32>(arg(1)); break;
+ case IR::Opcode::Barrier: ++workgroup_barriers; break;
+ case IR::Opcode::DeviceMemoryBarrier: ++device_barriers; break;
+ default: FAIL("Unexpected scalar compute IR " << IR::NameOf(inst.GetOpcode()));
+ }
+ values[&inst] = result;
+ }
+ }
+};
+void TranslateScalarNative(std::span<const Native> input, IR::Block& block) {
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ INFO("offset=" << lowered.error_offset << " error=" << lowered.error);
+ REQUIRE(lowered);
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (u64 word : lowered.instructions) {
+ const auto opcode = Shader::Maxwell::Decode(word);
+ if (opcode == Shader::Maxwell::Opcode::EXIT) continue;
+ switch (opcode) {
+#define INST(name, cute, encode) case Shader::Maxwell::Opcode::name: visitor.name(word); break;
+#include "shader_recompiler/frontend/maxwell/maxwell.inc"
+#undef INST
+ }
+ }
+}
+}
+
+TEST_CASE("Native uniform comparisons and selects preserve independent predicate banks", "[native-shader]") {
+ const std::array<Native, 7> input{{
+ {{0x09057891, 0x40, 0x0f8e303f, 0}},
+ {{0x0500728c, 7, 0x0bf21070, 0}},
+ {{0x05057287, 7, 0x08800000, 0}},
+ {{0x05057290, 7, 0x0fffe13f, 0}},
+ {{0x0400728c, 6, 0x0bf01070, 0}},
+ {{0x04047287, 6, 0x08000000, 0}},
+ VertexTransform.back(),
+ }};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 width : {100U, 256U}) {
+ ScalarNativeMachine machine;
+ machine.regs[225] = 2;
+ machine.regs[223] = width;
+ machine.regs[220] = 40;
+ machine.regs[222] = 32;
+ for (u32 p = 0; p < 7; ++p) machine.predicates[p] = (p & 1) != 0;
+ const auto original = machine.predicates;
+ machine.Execute(block);
+ REQUIRE(machine.regs[221] == width - std::min(192U, width));
+ REQUIRE(machine.regs[220] == 32);
+ REQUIRE(((machine.regs[239] & 0x20) != 0) == (192U < width));
+ REQUIRE((machine.regs[239] & 0x40) == 0);
+ REQUIRE(machine.predicates == original);
+ }
+}
+
+TEST_CASE("Native global stores carry a register offset into the high address word", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x02007986, 9, 0x0810a906, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[222] = 0xfffffff0;
+ machine.regs[223] = 0x21;
+ machine.regs[2] = 0x30;
+ machine.regs[9] = 0xdeadbeef;
+ machine.Execute(block);
+ REQUIRE(machine.global == std::unordered_map<u64, u32>{{0x2200000020ULL, 0xdeadbeef}});
+}
+
+TEST_CASE("Native LEA predicate carry reaches the high pointer word", "[native-shader]") {
+ const std::array<Native, 4> input{{
+ {{0x00007a11, 0x800, 0x078010ff, 0}},
+ {{0xff017a10, 0x900, 0x007fe4ff, 0}},
+ {{0x00007386, 7, 0x0010a900, 0}},
+ VertexTransform.back(),
+ }};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 base : {0x100U, 0xfffffff0U}) {
+ ScalarNativeMachine machine;
+ machine.regs[0] = 8;
+ machine.regs[7] = 0x12345678;
+ machine.constants[0x20] = base;
+ machine.constants[0x24] = 0x21;
+ machine.Execute(block);
+ REQUIRE(machine.global == std::unordered_map<u64, u32>{{(u64{0x21} << 32) + base + 32, 0x12345678}});
+ }
+}
+
+TEST_CASE("Native shared atomics and barriers retain real shared-memory effects", "[native-shader]") {
+ const std::array<Native, 6> input{{
+ {{0x03007388, 0x407, 0x4800, 0}},
+ {{0x03097984, 0x400, 0x4800, 0}},
+ {{0xff0a738c, 0x7fc09, 0, 0}},
+ {{0x00007992, 0, 0x5000, 0}},
+ {{0x00007b1d, 0, 0, 0}},
+ VertexTransform.back(),
+ }};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[3] = 3;
+ machine.regs[7] = 5;
+ machine.shared[0x7fc] = 100;
+ machine.Execute(block);
+ REQUIRE(machine.shared.at(16) == 5);
+ REQUIRE(machine.shared.at(0x7fc) == 105);
+ REQUIRE(machine.regs[10] == 100);
+ REQUIRE(machine.workgroup_barriers == 1);
+ REQUIRE(machine.device_barriers == 1);
+}
+
+TEST_CASE("Native uniform high shifts and multiply operands retain numeric values", "[native-shader]") {
+ std::array<Native, 7> input{{
+ {{0x00057882, 123, 0, 0}},
+ {{0x3f047899, 5, 0x08010605, 0}},
+ {{0x05057899, 5, 0x0800063f, 0}},
+ {{0x01027c24, 5, 0x0f8e0202, 0}},
+ {{0x00007c17, 4, 0x0b800200, 0}},
+ {{0x000a7424, 4, 0x078e0205, 0}},
+ VertexTransform.back(),
+ }};
+ for (u32 value : {0U, 1U, 123U, 0x07ffffffU, 0xffffffffU}) {
+ input[0].words[1] = value;
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[0] = 5000;
+ machine.regs[1] = 11;
+ machine.regs[2] = 7;
+ machine.regs[5] = 3;
+ machine.Execute(block);
+ const u32 shifted = value << 5;
+ const u32 minimum = static_cast<u32>(std::min(5000, static_cast<s32>(shifted)));
+ REQUIRE(machine.regs[220] == shifted);
+ REQUIRE(machine.regs[221] == shifted);
+ REQUIRE(machine.regs[2] == 11U * shifted + 7);
+ REQUIRE(machine.regs[0] == minimum);
+ REQUIRE(machine.regs[10] == minimum * 3U + 4);
+ }
+}
+
+TEST_CASE("Native bit reversal and register shifts preserve FFT indexing", "[native-shader]") {
+ const std::array<Native, 6> input{{
+ {{0x00037301, 19, 0, 0}},
+ {{0x03017211, 255, 0x078f50ff, 0}},
+ {{0xff087219, 4, 0x00011613, 0}},
+ {{0x0f0f7219, 12, 0x00000eff, 0}},
+ {{0xff047224, 255, 0x078e0a0c, 0}},
+ VertexTransform.back(),
+ }};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 shift : {0U, 1U, 10U, 31U, 32U, 33U, 63U}) {
+ ScalarNativeMachine machine;
+ machine.regs[19] = 0x12345678;
+ machine.regs[4] = shift;
+ machine.regs[12] = shift;
+ machine.regs[15] = 0x9abcdef0;
+ machine.Execute(block);
+ REQUIRE(machine.regs[3] == 0x1e6a2c48);
+ REQUIRE(machine.regs[1] == (0x1e6a2c48U >> 22));
+ REQUIRE(machine.regs[8] == (shift < 32 ? 0x12345678U >> shift : 0));
+ REQUIRE(machine.regs[15] == (0x9abcdef0U << (shift & 31)));
+ REQUIRE(machine.regs[4] == 0U - shift);
+ }
+}
+
+TEST_CASE("Native paired shared values keep both words under scaled addressing", "[native-shader]") {
+ const std::array<Native, 3> input{{
+ {{0x13007388, 4, 0x8a00, 0}},
+ {{0x13027984, 0, 0x8a00, 0}},
+ VertexTransform.back(),
+ }};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[19] = 17;
+ machine.regs[4] = 0x12345678;
+ machine.regs[5] = 0x9abcdef0;
+ machine.Execute(block);
+ REQUIRE(machine.shared.at(17 * 8) == 0x12345678);
+ REQUIRE(machine.shared.at(17 * 8 + 4) == 0x9abcdef0);
+ REQUIRE(machine.regs[2] == 0x12345678);
+ REQUIRE(machine.regs[3] == 0x9abcdef0);
+}
+
+TEST_CASE("Native formatted surface stores retain coordinates channels and descriptor", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00007b9b, 0x60000a04, 0x00100f00, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 writes{};
+ for (const auto& inst : block.Instructions()) if (inst.GetOpcode() == IR::Opcode::BoundImageWrite) {
+ REQUIRE(inst.Arg(0).U32() == 40);
+ const auto info = inst.Flags<IR::TextureInstInfo>();
+ REQUIRE(info.type.Value() == Shader::TextureType::Color2D);
+ const auto* coordinates = inst.Arg(1).Inst();
+ REQUIRE(coordinates->Arg(0).Inst()->Arg(0).Reg() == IR::Reg::R0);
+ REQUIRE(coordinates->Arg(1).Inst()->Arg(0).Reg() == IR::Reg::R1);
+ const auto* color = inst.Arg(2).Inst();
+ for (u32 component = 0; component < 4; ++component)
+ REQUIRE(static_cast<u32>(color->Arg(component).Inst()->Arg(0).Reg()) == 4 + component);
+ ++writes;
+ }
+ REQUIRE(writes == 1);
+}
+
+TEST_CASE("Native predicated predicate copies retain their guard and source", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x0000281c, 0, 0x01f0f070, 0}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ const auto predicate = Shader::Maxwell::Instruction{lowered.instructions.front()}.Pred();
+ REQUIRE(predicate.index == 2);
+ REQUIRE_FALSE(predicate.negated);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (bool source : {false, true}) {
+ ScalarNativeMachine machine;
+ machine.predicates[3] = source;
+ machine.Execute(block);
+ REQUIRE(machine.predicates[0] == source);
+ }
+}
+
+TEST_CASE("Native NTZ nearest conversion rounds ties to even and clears NaNs", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00117305, 2, 0x00203100, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 rounds{};
+ for (const auto& inst : block.Instructions()) if (inst.GetOpcode() == IR::Opcode::FPRoundEven32) {
+ REQUIRE(inst.Flags<IR::FpControl>().fmz_mode == IR::FmzMode::None);
+ ++rounds;
+ }
+ REQUIRE(rounds == 1);
+ for (const auto [value, expected] : std::array<std::pair<float, s32>, 9>{{
+ {0.5f, 0}, {1.5f, 2}, {2.5f, 2}, {-0.5f, 0}, {-1.5f, -2}, {-2.5f, -2},
+ {0.9f, 1}, {-0.9f, -1}, {std::numeric_limits<float>::quiet_NaN(), 0},
+ }}) {
+ ScalarNativeMachine machine;
+ machine.regs[2] = std::bit_cast<u32>(value);
+ machine.Execute(block);
+ REQUIRE(machine.regs[17] == static_cast<u32>(expected));
+ }
+ auto unsupported = input;
+ unsupported[0].words[2] ^= 1U << 13;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(unsupported));
+}
+
+TEST_CASE("Native predicate packing merges only selected predicate bits", "[native-shader]") {
+ for (u32 mask : {0U, 8U, 16U, 0x55U, 0x7fU}) {
+ const std::array<Native, 2> input{{{{0x02137803, mask, 0, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 bits = 0; bits < 128; ++bits) {
+ ScalarNativeMachine machine;
+ machine.regs[2] = 0xa5a5a5a5U;
+ for (u32 i = 0; i < 7; ++i) machine.predicates[i] = (bits >> i) & 1;
+ const auto saved = machine.predicates;
+ machine.Execute(block);
+ REQUIRE(machine.regs[19] == ((0xa5a5a5a5U & ~mask) | (bits & mask)));
+ REQUIRE(machine.predicates == saved);
+ }
+ }
+}
+
+TEST_CASE("Native signed NTZ floor conversion rounds negative fractions down", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00237305, 0x24, 0x00207100, 0x000e2400}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (const auto [value, expected] : std::array<std::pair<float, s32>, 8>{{
+ {1.9f, 1}, {-1.1f, -2}, {-0.1f, -1}, {0.9f, 0},
+ {0.0f, 0}, {-2.0f, -2}, {12345.5f, 12345},
+ {std::numeric_limits<float>::quiet_NaN(), 0},
+ }}) {
+ ScalarNativeMachine machine;
+ machine.regs[36] = std::bit_cast<u32>(value);
+ machine.Execute(block);
+ REQUIRE(machine.regs[35] == static_cast<u32>(expected));
+ }
+}
+
+TEST_CASE("Native dependency waits retain the full six-bit pending-operation count", "[native-shader]") {
+ for (u32 count : {0U, 1U, 15U, 22U, 63U}) {
+ const std::array<Native, 2> input{{{{0x0000791a, 0xd000U | (count << 6), 0, 0}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(lowered.instructions.size() == 1);
+ }
+ const std::array<Native, 2> unsupported{{{{0x0000791a, 0xc000, 0, 0}}, VertexTransform.back()}};
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(unsupported));
+}
+
+TEST_CASE("Native histogram warp match compares both address words and preserves predicates", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00ff73a1, 0, 0x00000200, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 active : {1U, 0x80000000U, 0x80000001U, 0x55555555U, 0xffffffffU}) {
+ for (u32 mismatch : {0U, 1U, 2U}) {
+ std::array<std::array<u32, 256>, 32> registers{};
+ std::array<std::array<bool, 7>, 32> predicates{};
+ const u32 first = std::countr_zero(active);
+ for (u32 lane = 0; lane < 32; ++lane) {
+ registers[lane][0] = 0xfffffff0U + (mismatch == 1 && lane != first ? 4 : 0);
+ registers[lane][1] = 0x21U + (mismatch == 2 && lane != first ? 1 : 0);
+ for (u32 p = 0; p < 7; ++p) predicates[lane][p] = ((lane + p) & 1) != 0;
+ }
+ const auto original = predicates;
+ ExecuteNativeWarp(block, active, registers, predicates);
+ for (u32 lane = 0; lane < 32; ++lane) {
+ auto expected = original[lane];
+ if (active & (1U << lane)) expected[0] = mismatch == 0 || std::popcount(active) == 1;
+ REQUIRE(predicates[lane] == expected);
+ }
+ }
+ }
+}
+
+TEST_CASE("Native histogram reduction sums only active lanes with unsigned wrap", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x030573c4, 0, 0x0000c000, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 active : {1U, 0x80000000U, 0x80000001U, 0x55555555U, 0xffffffffU}) {
+ std::array<std::array<u32, 256>, 32> registers{};
+ std::array<std::array<bool, 7>, 32> predicates{};
+ u32 sum = 0;
+ for (u32 lane = 0; lane < 32; ++lane) {
+ registers[lane][3] = 0xfffffff0U + lane * 17;
+ registers[lane][221] = 0xcafebeef;
+ if (active & (1U << lane)) sum += registers[lane][3];
+ for (u32 p = 0; p < 7; ++p) predicates[lane][p] = ((lane + p) & 1) != 0;
+ }
+ const auto original = predicates;
+ ExecuteNativeWarp(block, active, registers, predicates);
+ REQUIRE(predicates == original);
+ for (u32 lane = 0; lane < 32; ++lane)
+ REQUIRE(registers[lane][221] == ((active & (1U << lane)) ? sum : 0xcafebeef));
+ }
+}
+
+TEST_CASE("Native histogram pointer carry and global reduction retain the address and addend", "[native-shader]") {
+ const std::array<Native, 4> input{{
+ {{0x00007a10, 0x00001000, 0x07f1e0ff, 0}},
+ {{0xff017624, 0x00001100, 0x000e06ff, 0}},
+ {{0x0000798e, 3, 0x0010e100, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 low : {0U, 0xfffffff0U, 0xffffffffU}) {
+ ScalarNativeMachine machine;
+ machine.regs[0] = 32;
+ machine.regs[3] = 17;
+ machine.constants[0x40] = low;
+ machine.constants[0x44] = 0x21;
+ const u64 address = 0x2100000000ULL + low + 32;
+ machine.global[address] = 0xfffffff8U;
+ machine.Execute(block);
+ REQUIRE(machine.regs[0] == static_cast<u32>(address));
+ REQUIRE(machine.regs[1] == address >> 32);
+ REQUIRE(machine.predicates[0] == (low > 0xffffffdfU));
+ REQUIRE(machine.global == std::unordered_map<u64, u32>{{address, 9}});
+ }
+}
+
+TEST_CASE("Native histogram shared increment scales its address and increments once", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x03ff7f8c, 0, 0x0d00403f, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[3] = 63;
+ machine.shared[252] = 0xffffffffU;
+ machine.Execute(block);
+ REQUIRE(machine.shared == std::unordered_map<u64, u32>{{252, 0}});
+}
+
+TEST_CASE("Native histogram uniform leading bit and comparison select the highest active lane", "[native-shader]") {
+ const std::array<Native, 3> input{{{{0x000472bd, 4, 0x080e0000, 0}},
+ {{0x02007c0c, 4, 0x0bf02070, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 mask : {1U, 0x80000000U, 0x55555555U, 0xffffffffU}) {
+ for (u32 lane = 0; lane < 32; ++lane) {
+ ScalarNativeMachine machine;
+ machine.regs[220] = mask;
+ machine.regs[2] = lane;
+ machine.Execute(block);
+ REQUIRE(machine.regs[220] == 31U - std::countl_zero(mask));
+ REQUIRE(machine.predicates[0] == (lane == 31U - std::countl_zero(mask)));
+ }
+ }
+}
+
+TEST_CASE("Native formatted surface loads retain red data coordinates descriptor and guard", "[native-shader]") {
+ const Native instruction = GENERATE(Native{{0x400a0b97, 0x60001200, 0x001e0100, 0}},
+ Native{{0x12250b97, 0x60001200, 0x001e0100, 0}});
+ const std::array<Native, 2> input{{instruction, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(lowered.required_texture_buffer == 0);
+ const auto guard = Shader::Maxwell::Instruction{lowered.instructions.front()}.Pred();
+ REQUIRE(guard.index == 0);
+ REQUIRE_FALSE(guard.negated);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 loads{}, writes{};
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageRead) {
+ REQUIRE(inst.Arg(0).U32() == 0x12 * 4);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type.Value() == Shader::TextureType::Color2D);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(static_cast<u32>(coords->Arg(0).Inst()->Arg(0).Reg()) == instruction.Bits(24, 8));
+ REQUIRE(static_cast<u32>(coords->Arg(1).Inst()->Arg(0).Reg()) == instruction.Bits(24, 8) + 1);
+ ++loads;
+ }
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) {
+ REQUIRE(static_cast<u32>(inst.Arg(0).Reg()) == instruction.Bits(16, 8));
+ const auto* red = inst.Arg(1).Inst();
+ REQUIRE(red->GetOpcode() == IR::Opcode::CompositeExtractU32x4);
+ REQUIRE(red->Arg(1).U32() == 0);
+ REQUIRE(red->Arg(0).Inst()->GetOpcode() == IR::Opcode::BoundImageRead);
+ ++writes;
+ }
+ }
+ REQUIRE(loads == 1);
+ REQUIRE(writes == 1);
+ auto invalid = input;
+ invalid[0].words[2] ^= 0x200; // Reject other channel masks until implemented.
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(invalid));
+}
+
+TEST_CASE("Native volume texel fetch preserves XYZ LOD descriptor and split RGBA outputs", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00087b66, 0x50000a02, 0x049e0f0a, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 fetches{};
+ std::vector<u32> writes;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageFetch) {
+ REQUIRE(inst.Arg(0).U32() == 40);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type.Value() == Shader::TextureType::Color3D);
+ const auto* coords = inst.Arg(1).Inst();
+ for (u32 i = 0; i < 3; ++i)
+ REQUIRE(static_cast<u32>(coords->Arg(i).Inst()->Arg(0).Reg()) == i);
+ REQUIRE(inst.Arg(3).U32() == 0);
+ ++fetches;
+ }
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) {
+ writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ const auto* extract = inst.Arg(1).Inst()->Arg(0).Inst();
+ REQUIRE(extract->GetOpcode() == IR::Opcode::CompositeExtractF32x4);
+ REQUIRE(extract->Arg(1).U32() == writes.size() - 1);
+ }
+ }
+ REQUIRE(fetches == 1);
+ REQUIRE(writes == std::vector<u32>{8, 9, 10, 11});
+ auto invalid = input;
+ invalid[0].words[0] |= 1U << 24;
+ REQUIRE_FALSE(Shader::Ampere::LowerToMaxwell(invalid));
+}
+
+TEST_CASE("Native volume surface store retains XYZ and all four data channels", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00007b9b, 0xa0000c04, 0x00100f00, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 stores{};
+ for (const auto& inst : block.Instructions()) if (inst.GetOpcode() == IR::Opcode::BoundImageWrite) {
+ REQUIRE(inst.Arg(0).U32() == 48);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type.Value() == Shader::TextureType::Color3D);
+ for (u32 i = 0; i < 3; ++i)
+ REQUIRE(static_cast<u32>(inst.Arg(1).Inst()->Arg(i).Inst()->Arg(0).Reg()) == i);
+ for (u32 i = 0; i < 4; ++i)
+ REQUIRE(static_cast<u32>(inst.Arg(2).Inst()->Arg(i).Inst()->Arg(0).Reg()) == 4 + i);
+ ++stores;
+ }
+ REQUIRE(stores == 1);
+}
+
+TEST_CASE("Native half FMA ReLU preserves fusion values above one and canonical NaNs", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x0d127231, 0x3000000d, 0x03848d10, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ const std::array<std::array<u16, 3>, 9> cases{{
+ {{0x3c00, 0x4400, 0x4200}}, // -1*1+4 = 3, not saturated to one
+ {{0x4000, 0x3c00, 0x0000}}, // -2*2+1 < 0
+ {{0x3c00, 0x3c00, 0x0000}},
+ {{0x0000, 0x0001, 0x0001}}, // smallest positive subnormal survives
+ {{0x7c00, 0x0000, 0x0000}},
+ {{0x0000, 0x7c00, 0x7c00}},
+ {{0x7e01, 0x3c00, 0x7fff}},
+ {{0xfe01, 0x3c00, 0x7fff}},
+ {{0x8000, 0x8000, 0x0000}},
+ }};
+ for (const auto& values : cases) {
+ ScalarNativeMachine machine;
+ machine.regs[13] = (u32{values[0]} << 16) | 0x4400;
+ machine.regs[16] = 0xc0000000 | values[1];
+ machine.predicates = {true, false, true, false, true, false, true};
+ const auto predicates = machine.predicates;
+ machine.Execute(block);
+ REQUIRE(machine.regs[18] == (u32{values[2]} | (u32{values[2]} << 16)));
+ REQUIRE(machine.predicates == predicates);
+ }
+ // A separate register operand permits a fused product residual that would
+ // vanish if the multiplication rounded before adding its third operand.
+ auto fused = input;
+ fused[0].words[1] = 0x3000000e;
+ IR::Block fused_block{pool};
+ TranslateScalarNative(fused, fused_block);
+ ScalarNativeMachine machine;
+ machine.regs[13] = 0x3c013c01;
+ machine.regs[14] = 0x3bfe3bfe;
+ machine.regs[16] = 0x3c003c00;
+ machine.Execute(fused_block);
+ REQUIRE(machine.regs[18] == 0x00100010); // 2^-20 in each half
+}
+
+TEST_CASE("Native half FMA ReLU accepts packed immediates without losing input aliases", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x02027831, 0x3c003c00, 0x03968805, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (const auto [source, addend, expected] : std::array<std::array<u16, 3>, 4>{{
+ {{0x4000, 0x3c00, 0x3c00}}, {{0x3c00, 0x4400, 0x0000}},
+ {{0x4400, 0x3c00, 0x4200}}, {{0x7e01, 0x3c00, 0x7fff}},
+ }}) {
+ ScalarNativeMachine machine;
+ machine.regs[2] = source | 0xc4000000U;
+ machine.regs[5] = (u32{addend} << 16) | 0x4800;
+ machine.Execute(block);
+ REQUIRE(machine.regs[2] == (u32{expected} | (u32{expected} << 16)));
+ }
+}
+
+TEST_CASE("Native quad butterfly and swizzled add retain horizontal and vertical differences", "[native-shader]") {
+ const bool vertical = GENERATE(false, true);
+ const std::array<Native, 3> input{{
+ {{0x05107f89, vertical ? 0x0c5c0300U : 0x0c3c0300U, 0x000e0000, 0}},
+ {{0x100c7822, vertical ? 0x5aU : 0x66U, 5, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ std::array<std::array<u32, 256>, 32> registers{};
+ std::array<std::array<bool, 7>, 32> predicates{};
+ for (u32 lane = 0; lane < 32; ++lane)
+ registers[lane][5] = std::bit_cast<u32>(static_cast<float>(lane / 4 * 100 + (lane & 1) * 3 + ((lane >> 1) & 1) * 11));
+ ExecuteNativeWarp(block, 0xffffffffU, registers, predicates);
+ for (u32 lane = 0; lane < 32; ++lane) {
+ REQUIRE(registers[lane][16] == registers[lane ^ (vertical ? 2 : 1)][5]);
+ REQUIRE(std::bit_cast<float>(registers[lane][12]) == (vertical ? 11.0f : 3.0f));
+ }
+}
+
+TEST_CASE("Native array gradient fetch retains array index XY derivatives descriptor and RGBA", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x04047b6c, 0xa080080c, 0x001e0f06, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 samples{};
+ std::vector<u32> writes;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageGradient) {
+ REQUIRE(inst.Arg(0).U32() == 32);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type.Value() == Shader::TextureType::ColorArray2D);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().num_derivatives.Value() == 2);
+ const auto* coords = inst.Arg(1).Inst();
+ REQUIRE(coords->Arg(0).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{4});
+ REQUIRE(coords->Arg(1).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{5});
+ const auto* layer = coords->Arg(2).Inst()->Arg(0).Inst();
+ REQUIRE(layer->GetOpcode() == IR::Opcode::BitFieldUExtract);
+ REQUIRE(layer->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{6});
+ REQUIRE(layer->Arg(2).U32() == 16);
+ for (u32 component = 0; component < 4; ++component)
+ REQUIRE(inst.Arg(2).Inst()->Arg(component).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{12 + component});
+ ++samples;
+ }
+ if (inst.GetOpcode() == IR::Opcode::SetRegister) writes.push_back(static_cast<u32>(inst.Arg(0).Reg()));
+ }
+ REQUIRE(samples == 1);
+ REQUIRE(writes == std::vector<u32>{4, 5, 6, 7});
+}
+
+TEST_CASE("Native capture distinguishes forward branches from call targets beyond main EXIT", "[native-shader]") {
+ using Capture = Shader::Ampere::ProgramCaptureState;
+ Capture calls;
+ REQUIRE(calls.Observe(Native{{0x7944, 48, 0x03c00000, 0}}, 1) == Capture::Result::Continue);
+ REQUIRE(calls.Observe(VertexTransform.back(), 2) == Capture::Result::Continue);
+ REQUIRE(calls.Observe(Native{{0x08007950, 0, 0, 0}}, 6) == Capture::Result::Complete);
+ Capture branches;
+ REQUIRE(branches.Observe(Native{{0x7947, 48, 0x03800000, 0}}, 0) == Capture::Result::Continue);
+ REQUIRE(branches.Observe(VertexTransform.back(), 1) == Capture::Result::Continue);
+ REQUIRE(branches.Observe(VertexTransform.back(), 4) == Capture::Result::Complete);
+ Capture multiple;
+ REQUIRE(multiple.Observe(Native{{0x7944, 96, 0x03c00000, 0}}, 1) == Capture::Result::Continue);
+ REQUIRE(multiple.Observe(Native{{0x7944, 128, 0x03c00000, 0}}, 3) == Capture::Result::Continue);
+ REQUIRE(multiple.Observe(VertexTransform.back(), 4) == Capture::Result::Continue);
+ REQUIRE(multiple.Observe(Native{{0x08007950, 0, 0, 0}}, 9) == Capture::Result::Continue);
+ REQUIRE(multiple.Observe(Native{{0x08007950, 0, 0, 0}}, 13) == Capture::Result::Complete);
+ Capture loop_exit;
+ REQUIRE(loop_exit.Observe(Native{{0x0944, 16, 0x03c00000, 0}}, 0) == Capture::Result::Continue);
+ REQUIRE(loop_exit.Observe(Native{{0x7947, 0xfffffff0, 0x0383ffff, 0}}, 1) == Capture::Result::Continue);
+ REQUIRE(loop_exit.Observe(Native{{0x7941, 0, 0x03800000, 0}}, 2) == Capture::Result::Continue);
+ REQUIRE(loop_exit.Observe(VertexTransform.back(), 3) == Capture::Result::Complete);
+ Capture invalid;
+ REQUIRE(invalid.Observe(Native{{0x7947, 1, 0x03800000, 0}}, 0) == Capture::Result::InvalidTarget);
+}
+
+TEST_CASE("Native implicit volume sample preserves XYZ and all four split channels", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x0a0a7b60, 0x5080400e, 0x041e0f0c, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 samples{};
+ std::vector<std::pair<u32, u32>> copies;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageSampleImplicitLod) {
+ REQUIRE(inst.Arg(0).U32() == 256);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type.Value() == Shader::TextureType::Color3D);
+ for (u32 i = 0; i < 3; ++i)
+ REQUIRE(inst.Arg(1).Inst()->Arg(i).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{250 + i});
+ ++samples;
+ }
+ if (inst.GetOpcode() == IR::Opcode::SetRegister && inst.Arg(1).Inst()->GetOpcode() == IR::Opcode::GetRegister)
+ copies.emplace_back(static_cast<u32>(inst.Arg(0).Reg()), static_cast<u32>(inst.Arg(1).Inst()->Arg(0).Reg()));
+ }
+ REQUIRE(samples == 1);
+ REQUIRE(copies == std::vector<std::pair<u32, u32>>{{250, 10}, {251, 11}, {252, 14}, {10, 250}, {11, 251}, {12, 252}, {13, 253}});
+}
+
+TEST_CASE("Native array shadow sampling retains layer XY depth reference and scalar destination", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x082e7b60, 0xb080140a, 0x001e41ff, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ u32 samples{};
+ std::vector<std::pair<u32, u32>> copies;
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::BoundImageSampleDrefImplicitLod) {
+ REQUIRE(inst.Arg(0).U32() == 80);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().type.Value() == Shader::TextureType::ColorArray2D);
+ REQUIRE(inst.Flags<IR::TextureInstInfo>().is_depth.Value() == 1);
+ REQUIRE(inst.Arg(1).Inst()->Arg(0).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{251});
+ REQUIRE(inst.Arg(1).Inst()->Arg(1).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{252});
+ REQUIRE(inst.Arg(2).Inst()->Arg(0).Inst()->Arg(0).Reg() == IR::Reg{11});
+ ++samples;
+ }
+ if (inst.GetOpcode() == IR::Opcode::SetRegister && inst.Arg(1).Inst()->GetOpcode() == IR::Opcode::GetRegister)
+ copies.emplace_back(static_cast<u32>(inst.Arg(0).Reg()), static_cast<u32>(inst.Arg(1).Inst()->Arg(0).Reg()));
+ }
+ REQUIRE(samples == 1);
+ REQUIRE(copies == std::vector<std::pair<u32, u32>>{{250, 8}, {251, 9}, {252, 10}});
+ REQUIRE(block.Instructions().back().Arg(0).Reg() == IR::Reg{46});
+}
+
+TEST_CASE("Native immediate uniform comparison and select preserve predicates at unsigned boundaries", "[native-shader]") {
+ const std::array<Native, 3> input{{{{0x0400788c, 6, 0x0bf01070, 0}}, {{0x04057887, 6, 0x08000000, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 value : {0U, 5U, 6U, 7U, 0x80000000U, 0xffffffffU}) {
+ ScalarNativeMachine machine;
+ machine.regs[220] = value;
+ for (u32 i = 0; i < 7; ++i) machine.predicates[i] = (i & 1) != 0;
+ const auto saved = machine.predicates;
+ machine.Execute(block);
+ REQUIRE(((machine.regs[239] >> 6) & 1) == (value < 6));
+ REQUIRE(machine.regs[221] == std::min(value, 6U));
+ REQUIRE(machine.predicates == saved);
+ }
+}
+
+TEST_CASE("Native uniform signed right shift sign extends the high word", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x3f057899, 31, 0x08011404, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 value : {0U, 1U, 0x7fffffffU, 0x80000000U, 0xffffffffU}) {
+ ScalarNativeMachine machine;
+ machine.regs[220] = value;
+ machine.Execute(block);
+ REQUIRE(machine.regs[221] == ((value & 0x80000000U) ? 0xffffffffU : 0U));
+ }
+}
+
+TEST_CASE("Native indexed constant load adds a signed byte displacement", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x30077b82, 0x00fe0500, 0x00000800, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[48] = 0x800;
+ machine.constants[(u64{3} << 32) | 0x14] = 0x12345678;
+ machine.Execute(block);
+ REQUIRE(machine.regs[7] == 0x12345678);
+}
+
+TEST_CASE("Native local spill retains byte address data and execution predicate", "[native-shader]") {
+ const std::array<Native, 3> input{{{{0xff001387, 85, 0x00100800, 0}},
+ {{0xff2a7983, 0, 0x00300800, 0}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions[0]) == Shader::Maxwell::Opcode::STL);
+ REQUIRE(Shader::Maxwell::Instruction{lowered.instructions[0]}.Pred().index == 1);
+ REQUIRE((lowered.instructions[0] & 255) == 85);
+ REQUIRE((lowered.instructions[1] & 255) == 42);
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ class LocalEnvironment : public EmptyEnvironment {
+ public:
+ u32 LocalMemorySize() const override { return 4; }
+ } env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.STL(lowered.instructions[0]);
+ visitor.LDL(lowered.instructions[1]);
+ u32 loads{}, stores{};
+ for (const auto& inst : block.Instructions()) {
+ if (inst.GetOpcode() == IR::Opcode::LoadLocal) {
+ REQUIRE(inst.Arg(0).U32() == 0);
+ ++loads;
+ }
+ if (inst.GetOpcode() == IR::Opcode::WriteLocal) {
+ REQUIRE(inst.Arg(0).U32() == 0);
+ REQUIRE(inst.Arg(1).Inst()->Arg(0).Reg() == IR::Reg{85});
+ ++stores;
+ }
+ }
+ REQUIRE(loads == 1);
+ REQUIRE(stores == 1);
+}
+
+TEST_CASE("Native scalar half conversion applies absolute value before rounding", "[native-shader]") {
+ const bool negate = GENERATE(false, true);
+ const std::array<Native, 2> input{{{{0x00047304, negate ? 0x80000000U : 0x40000000U, 0x00200800, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (const auto [value, expected] : std::array<std::pair<float, u32>, 7>{{
+ {-1.0f, 0x3c00}, {-0.0f, 0}, {-2.5f, 0x4100},
+ {1.00048828125f, 0x3c00}, {1.00146484375f, 0x3c02},
+ {-0x1p-24f, 1}, {-std::numeric_limits<float>::infinity(), 0x7c00},
+ }}) {
+ ScalarNativeMachine machine;
+ machine.regs[0] = std::bit_cast<u32>(value);
+ machine.Execute(block);
+ REQUIRE(machine.regs[4] == (expected | (negate && !std::signbit(value) ? 0x8000U : 0U)));
+ }
+}
+
+TEST_CASE("Native predicated half ReLU guards its complete expansion including predicate scratch", "[native-shader]") {
+ for (u32 pred : {1U, 6U, 9U, 14U}) {
+ const std::array<Native, 2> input{{{{0x12140231U | (pred << 12), 0x20000012, 0x03848914, 0}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(Shader::Maxwell::Decode(lowered.instructions.front()) == Shader::Maxwell::Opcode::BRA);
+ REQUIRE(((lowered.instructions.front() >> 16) & 15) == (pred ^ 8));
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ EmptyEnvironment env;
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ for (size_t i = 1; i + 1 < lowered.instructions.size(); ++i) {
+ const u64 word = lowered.instructions[i];
+ REQUIRE(((word >> 16) & 15) == 7);
+ switch (Shader::Maxwell::Decode(word)) {
+#define INST(name, cute, encode) case Shader::Maxwell::Opcode::name: visitor.name(word); break;
+#include "shader_recompiler/frontend/maxwell/maxwell.inc"
+#undef INST
+ }
+ }
+ for (bool value : {false, true}) {
+ ScalarNativeMachine machine;
+ machine.regs[18] = 0x3c003c00;
+ machine.regs[20] = 0x44004400;
+ machine.predicates.fill(true);
+ machine.predicates[pred & 7] = value;
+ const auto saved = machine.predicates;
+ if (value != ((pred & 8) != 0)) machine.Execute(block);
+ REQUIRE(machine.regs[20] == (value != ((pred & 8) != 0) ? 0x42004200U : 0x44004400U));
+ REQUIRE(machine.predicates == saved);
+ }
+ }
+}
+
+TEST_CASE("Native unsigned half conversion truncates and saturates NTZ edge cases", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x000e7305, 13, 0x0010f000, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (const auto [value, expected] : std::array<std::pair<u32, u32>, 8>{{
+ {0x3e00, 1}, {0x4100, 2}, {0xbe00, 0}, {0x0001, 0},
+ {0x7bff, 65504}, {0x7c00, 0xffffffffU}, {0xfc00, 0}, {0x7e00, 0},
+ }}) {
+ ScalarNativeMachine machine;
+ machine.regs[13] = 0xdead0000 | value;
+ machine.Execute(block);
+ REQUIRE(machine.regs[14] == expected);
+ }
+}
+
+TEST_CASE("Native predicate LUT OR combines three inputs while preserving other predicates", "[native-shader]") {
+ const std::array<Native, 3> input{{{{0x0000781c, 0, 0x01f25f16, 0}}, {{0x0000781c, 0, 0x00703570, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ for (u32 bits = 0; bits < 128; ++bits) {
+ ScalarNativeMachine machine;
+ for (u32 i = 0; i < 7; ++i) machine.predicates[i] = (bits >> i) & 1;
+ auto expected = machine.predicates;
+ expected[1] = expected[1] || expected[2] || expected[3];
+ expected[0] = expected[0] || expected[1];
+ machine.Execute(block);
+ REQUIRE(machine.predicates == expected);
+ }
+}
+
+TEST_CASE("Native half FMA absolute source clears each half sign before broadcast", "[native-shader]") {
+ for (bool broadcast : {false, true}) {
+ const std::array<Native, 2> input{{{{0x1a087231, 0x20000008,
+ broadcast ? 0x00040a0dU : 0x0004020dU, 0}}, VertexTransform.back()}};
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ TranslateScalarNative(input, block);
+ ScalarNativeMachine machine;
+ machine.regs[26] = 0xc400c000;
+ machine.regs[8] = 0x40003c00;
+ machine.regs[13] = 0x44004200;
+ machine.Execute(block);
+ REQUIRE(machine.regs[8] == (broadcast ? 0x45004500U : 0x47004500U));
+ }
+}
+
+TEST_CASE("Native constant interpolation retains attribute and flat input behavior", "[native-shader]") {
+ const std::array<Native, 2> input{{{{0x00047326, 255, 0x000e403c, 0}}, VertexTransform.back()}};
+ const auto lowered = Shader::Ampere::LowerToMaxwell(input);
+ REQUIRE(lowered);
+ REQUIRE(((lowered.instructions[0] >> 54) & 3) == 2);
+ class FlatEnvironment : public EmptyEnvironment {
+ public:
+ FlatEnvironment() { sph.ps.imap_generic_vector[7].raw = 0x55; }
+ } env;
+ Shader::ObjectPool<IR::Inst> pool;
+ IR::Block block{pool};
+ Shader::Maxwell::TranslatorVisitor visitor{env, block};
+ visitor.IPA(lowered.instructions[0]);
+ u32 reads{};
+ for (const auto& inst : block.Instructions()) {
+ REQUIRE(inst.GetOpcode() != IR::Opcode::FPMul32);
+ if (inst.GetOpcode() == IR::Opcode::GetAttribute) {
+ REQUIRE(inst.Arg(0).Attribute() == IR::Attribute{60});
+ ++reads;
+ }
+ }
+ REQUIRE(reads == 1);
+}
diff --git a/src/tests/video_core/native_swizzle.cpp b/src/tests/video_core/native_swizzle.cpp
new file mode 100644
index 0000000000..93c4b85dac
--- /dev/null
+++ b/src/tests/video_core/native_swizzle.cpp
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <bit>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "video_core/textures/decoders.h"
+
+namespace {
+u64 Digest(const std::vector<u8>& bytes) {
+ u64 digest = 14695981039346656037ULL;
+ for (const u8 byte : bytes) digest = (digest ^ byte) * 1099511628211ULL;
+ return digest;
+}
+}
+
+TEST_CASE("Native Orin texture mips match independent upstream swizzle vectors", "[native-swizzle]") {
+ struct Fixture { u32 width, height, levels, bytes; std::array<u64, 4> hashes; };
+ const std::array fixtures{
+ Fixture{32, 64, 2, 8, {0x9fcd630e81008de4ULL, 0x0b316aa26800970aULL}},
+ Fixture{128, 256, 4, 16, {0x1d16743f55444bd6ULL, 0x628ec170d3dc24b6ULL, 0x9d5ce8671743eadcULL, 0x68c86243a7167475ULL}},
+ Fixture{128, 128, 3, 8, {0xa3694bb8d6333e7eULL, 0xe748d5f21bd06d5dULL, 0xabfa06266de4d322ULL}},
+ Fixture{256, 64, 2, 16, {0x9820bfdf5000d898ULL, 0x2177c49e446a9450ULL}},
+ Fixture{64, 256, 4, 16, {0x30e15cf8e750e118ULL, 0x286e806b76ac89e4ULL, 0xa0dc28aebfbe0862ULL, 0xdca4a8b9b78cafc2ULL}},
+ };
+ for (const auto& fixture : fixtures) {
+ u32 state = 0x12345678;
+ for (u32 level = 0; level < fixture.levels; ++level) {
+ const u32 width = (std::max(1U, fixture.width >> level) + 3) / 4;
+ const u32 height = (std::max(1U, fixture.height >> level) + 3) / 4;
+ u32 block_height = 16;
+ while (block_height > 1 && height <= block_height * 4) block_height >>= 1;
+ const u32 block_log2 = static_cast<u32>(std::countr_zero(block_height));
+ const size_t size = Tegra::Texture::CalculateSize(true, fixture.bytes, width, height, 1, block_log2, 0);
+ std::vector<u8> tiled(size);
+ for (auto& byte : tiled) { state = state * 1664525U + 1013904223U; byte = static_cast<u8>(state >> 24); }
+ std::vector<u8> linear(width * height * fixture.bytes);
+ Tegra::Texture::UnswizzleTexture(linear, tiled, fixture.bytes, width, height, 1, block_log2, 0, 1, true);
+ INFO("width=" << width << " height=" << height << " mip=" << level);
+ REQUIRE(Digest(linear) == fixture.hashes[level]);
+ auto restored = tiled;
+ Tegra::Texture::SwizzleTexture(restored, linear, fixture.bytes, width, height, 1, block_log2, 0, 1, true);
+ REQUIRE(restored == tiled);
+ }
+ }
+}
+
+TEST_CASE("Native and legacy GOB byte lanes remain distinct", "[native-swizzle]") {
+ std::vector<u8> tiled(512);
+ for (u32 i = 0; i < tiled.size(); ++i) tiled[i] = static_cast<u8>(i / 16);
+ std::vector<u8> native(512), legacy(512);
+ Tegra::Texture::UnswizzleTexture(native, tiled, 1, 64, 8, 1, 0, 0, 1, true);
+ Tegra::Texture::UnswizzleTexture(legacy, tiled, 1, 64, 8, 1, 0, 0);
+ REQUIRE(native[16] == 4);
+ REQUIRE(legacy[16] == 2);
+ REQUIRE(native[2 * 64] == 2);
+ REQUIRE(legacy[2 * 64] == 4);
+ REQUIRE(native != legacy);
+ std::vector<u8> crop(16 * 3);
+ Tegra::Texture::UnswizzleSubrect(crop, tiled, 1, 64, 8, 1, 16, 2, 16, 3, 0, 0, 16, true);
+ for (u32 y = 0; y < 3; ++y) {
+ REQUIRE(std::equal(crop.begin() + y * 16, crop.begin() + (y + 1) * 16,
+ native.begin() + (y + 2) * 64 + 16));
+ }
+}
diff --git a/src/tests/video_core/pending_download.cpp b/src/tests/video_core/pending_download.cpp
new file mode 100644
index 0000000000..4b2a218933
--- /dev/null
+++ b/src/tests/video_core/pending_download.cpp
@@ -0,0 +1,112 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <deque>
+#include <memory>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "common/slot_vector.h"
+#include "video_core/texture_cache/pending_download.h"
+
+namespace {
+struct DownloadImage {
+ DownloadImage(u32 value, std::shared_ptr<std::vector<u32>> destroyed_)
+ : handle{std::make_unique<u32>(value)}, destroyed{std::move(destroyed_)} {}
+ DownloadImage(DownloadImage&&) noexcept = default;
+ DownloadImage& operator=(DownloadImage&&) noexcept = default;
+ ~DownloadImage() { if (handle) destroyed->push_back(*handle); }
+ std::unique_ptr<u32> handle;
+ std::shared_ptr<std::vector<u32>> destroyed;
+};
+using Download = VideoCommon::PendingImageDownload<DownloadImage>;
+}
+
+TEST_CASE("Queued texture downloads survive deletion and reuse of their cache slot", "[texture-download]") {
+ auto destroyed = std::make_shared<std::vector<u32>>();
+ Common::SlotVector<DownloadImage> images;
+ const auto old_id = images.insert(42U, destroyed);
+ std::vector<Download> pending{{true, 0, old_id, {}}, {false, 0, old_id, {}}};
+ std::deque<std::vector<Download>> submitted{{{true, 0, old_id, {}}}};
+ REQUIRE(VideoCommon::RetainPendingImageDownloads(old_id, images[old_id], pending, submitted));
+ REQUIRE_FALSE(images[old_id].handle);
+ REQUIRE(pending[0].retired_image == submitted[0][0].retired_image);
+ REQUIRE_FALSE(pending[1].retired_image); // Buffer-download slot IDs are a separate namespace.
+ images.erase(old_id);
+ const auto replacement_id = images.insert(99U, destroyed);
+ REQUIRE(replacement_id == old_id);
+ REQUIRE(*pending[0].GetImage(images).handle == 42);
+ REQUIRE(*submitted[0][0].GetImage(images).handle == 42);
+ REQUIRE(*images[replacement_id].handle == 99);
+ // Retiring the replacement must not redirect an earlier generation's downloads.
+ pending.push_back({true, 1, replacement_id, {}});
+ REQUIRE(VideoCommon::RetainPendingImageDownloads(replacement_id, images[replacement_id], pending, submitted));
+ images.erase(replacement_id);
+ REQUIRE(*pending[0].GetImage(images).handle == 42);
+ REQUIRE(*pending[2].GetImage(images).handle == 99);
+ REQUIRE(destroyed->empty());
+ pending.clear();
+ REQUIRE(*destroyed == std::vector<u32>{99});
+ REQUIRE(*submitted[0][0].GetImage(images).handle == 42);
+ submitted.pop_front();
+ REQUIRE(*destroyed == std::vector<u32>{99, 42});
+}
+
+TEST_CASE("Textures without queued downloads retain ordinary cache retirement", "[texture-download]") {
+ auto destroyed = std::make_shared<std::vector<u32>>();
+ Common::SlotVector<DownloadImage> images;
+ const auto id = images.insert(7U, destroyed);
+ std::vector<Download> pending{{false, 0, id, {}}};
+ std::deque<std::vector<Download>> submitted;
+ REQUIRE_FALSE(VideoCommon::RetainPendingImageDownloads(id, images[id], pending, submitted));
+ REQUIRE(*images[id].handle == 7);
+ images.erase(id);
+ REQUIRE(*destroyed == std::vector<u32>{7});
+}
+
+TEST_CASE("Repeated whole-image downloads keep first occurrence and buffer requests", "[texture-download]") {
+ auto destroyed = std::make_shared<std::vector<u32>>();
+ Common::SlotVector<DownloadImage> images;
+ const auto a = images.insert(42U, destroyed);
+ const auto b = images.insert(99U, destroyed);
+ std::vector<Download> pending{{true, 0, a, {}}, {true, 0, b, {}},
+ {false, 3, Common::SlotId{10000}, {}}, {true, 0, a, {}}};
+ const auto writeback = [&](const auto& downloads) {
+ u32 value = 0;
+ for (auto it = downloads.rbegin(); it != downloads.rend(); ++it)
+ if (it->is_swizzle) value = *it->GetImage(images).handle;
+ return value;
+ };
+ const u32 expected = writeback(pending);
+ REQUIRE(VideoCommon::DeduplicatePendingImageDownloads(pending, images) == 1);
+ REQUIRE(pending.size() == 3);
+ REQUIRE(pending[0].object_id == a);
+ REQUIRE(pending[1].object_id == b);
+ REQUIRE_FALSE(pending[2].is_swizzle);
+ REQUIRE(pending[2].async_buffer_id == 3);
+ REQUIRE(writeback(pending) == expected);
+ REQUIRE(expected == 42);
+}
+
+TEST_CASE("Readback deduplication distinguishes recycled image generations", "[texture-download]") {
+ auto destroyed = std::make_shared<std::vector<u32>>();
+ Common::SlotVector<DownloadImage> images;
+ const auto id = images.insert(42U, destroyed);
+ std::vector<Download> pending{{true, 0, id, {}}};
+ std::deque<std::vector<Download>> submitted;
+ REQUIRE(VideoCommon::RetainPendingImageDownloads(id, images[id], pending, submitted));
+ auto old = pending.front();
+ images.erase(id);
+ const auto replacement = images.insert(99U, destroyed);
+ REQUIRE(replacement == id);
+ pending.push_back({true, 0, replacement, {}});
+ for (size_t i = 0; i < 1000; ++i) pending.push_back(old);
+ REQUIRE(VideoCommon::DeduplicatePendingImageDownloads(pending, images) == 1000);
+ REQUIRE(pending.size() == 2);
+ REQUIRE(*pending[0].GetImage(images).handle == 42);
+ REQUIRE(*pending[1].GetImage(images).handle == 99);
+ REQUIRE(destroyed->empty());
+ pending.clear();
+ REQUIRE(destroyed->empty());
+ old.retired_image.reset();
+ REQUIRE(*destroyed == std::vector<u32>{42});
+}