diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-29 03:39:29 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-29 03:39:29 -0400 |
| commit | ea6c1193fbfa95e586bb6d129dc684163a12cb3e (patch) | |
| tree | e4d23143900184630deeabe27eab1321dac4a428 /src/tests | |
| parent | 0bb5055b6541c74f47603a8dff0569a509cbaf18 (diff) | |
Revert "Import experimental NS2 support and accumulated GPU fixes"
This reverts commit 535b676970. The Switch 2 support comes from the nx2
work instead, merged next.
Diffstat (limited to 'src/tests')
| -rw-r--r-- | src/tests/CMakeLists.txt | 8 | ||||
| -rw-r--r-- | src/tests/core/graphics_supervisor.cpp | 133 | ||||
| -rw-r--r-- | src/tests/core/native_gpu_characteristics.cpp | 389 | ||||
| -rw-r--r-- | src/tests/core/program_metadata.cpp | 151 | ||||
| -rw-r--r-- | src/tests/video_core/image_overlap.cpp | 93 | ||||
| -rw-r--r-- | src/tests/video_core/native_semaphore.cpp | 63 | ||||
| -rw-r--r-- | src/tests/video_core/native_shader.cpp | 2841 | ||||
| -rw-r--r-- | src/tests/video_core/native_swizzle.cpp | 67 | ||||
| -rw-r--r-- | src/tests/video_core/pending_download.cpp | 112 |
9 files changed, 0 insertions, 3857 deletions
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1bcb954334..875c40604c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -17,15 +17,7 @@ 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 deleted file mode 100644 index 08e272f09a..0000000000 --- a/src/tests/core/graphics_supervisor.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// 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 deleted file mode 100644 index cb79853ac2..0000000000 --- a/src/tests/core/native_gpu_characteristics.cpp +++ /dev/null @@ -1,389 +0,0 @@ -// 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 deleted file mode 100644 index b5af9953f4..0000000000 --- a/src/tests/core/program_metadata.cpp +++ /dev/null @@ -1,151 +0,0 @@ -// 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 deleted file mode 100644 index a4a08c72ea..0000000000 --- a/src/tests/video_core/image_overlap.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// 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 deleted file mode 100644 index 8dcafd91bc..0000000000 --- a/src/tests/video_core/native_semaphore.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// 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 deleted file mode 100644 index 76424b7dbf..0000000000 --- a/src/tests/video_core/native_shader.cpp +++ /dev/null @@ -1,2841 +0,0 @@ -// 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 deleted file mode 100644 index 93c4b85dac..0000000000 --- a/src/tests/video_core/native_swizzle.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 4b2a218933..0000000000 --- a/src/tests/video_core/pending_download.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// 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}); -} |
