CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
1074 lines
42 KiB
C++
1074 lines
42 KiB
C++
// Copyright (c) 2026 Advanced Micro Devices, Inc.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
/// @file scalar_scc_test.cpp
|
|
/// @brief Cross-architecture scalar SCC execution and preservation tests.
|
|
|
|
#include "rocjitsu/analysis/def_use_chain.h"
|
|
#include "rocjitsu/code/rj_code.h"
|
|
#include "rocjitsu/isa/decoder.h"
|
|
#include "rocjitsu/isa/instruction.h"
|
|
#include "rocjitsu/vm/amdgpu/compute_unit.h"
|
|
#include "rocjitsu/vm/amdgpu/gpu_memory.h"
|
|
#include "rocjitsu/vm/amdgpu/l2_cache.h"
|
|
#include "rocjitsu/vm/amdgpu/wavefront.h"
|
|
#include "util/data_types.h"
|
|
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna1/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna2/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna3/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna4/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/gfx1250/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/rdna1/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/rdna2/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/rdna3/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/rdna3_5/test_encodings.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/rdna4/test_encodings.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
namespace {
|
|
|
|
using namespace rocjitsu;
|
|
|
|
using EncodingWords = std::array<uint32_t, 2>;
|
|
using EncodingLookup = std::optional<EncodingWords> (*)(std::string_view);
|
|
|
|
enum class WrexecDialect {
|
|
Andn,
|
|
AndNot,
|
|
};
|
|
|
|
template <typename Entry, size_t N>
|
|
std::optional<EncodingWords> find_test_encoding(const Entry (&encodings)[N],
|
|
std::string_view mnemonic) {
|
|
for (const auto &encoding : encodings) {
|
|
if (encoding.mnemonic == mnemonic)
|
|
return encoding.words;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<EncodingWords> cdna1_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(cdna1::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> cdna2_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(cdna2::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> cdna3_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(cdna3::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> cdna4_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(cdna4::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> rdna1_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(rdna1::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> rdna2_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(rdna2::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> rdna3_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(rdna3::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> rdna3_5_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(rdna3_5::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> rdna4_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(rdna4::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
std::optional<EncodingWords> gfx1250_encoding(std::string_view mnemonic) {
|
|
return find_test_encoding(gfx1250::test_data::ENCODINGS, mnemonic);
|
|
}
|
|
|
|
class ScalarSccProfile {
|
|
public:
|
|
rj_code_arch_t arch;
|
|
std::string_view name;
|
|
EncodingLookup find_encoding;
|
|
WrexecDialect wrexec_dialect;
|
|
bool has_wrexec_b32;
|
|
};
|
|
|
|
const std::array<ScalarSccProfile, 10> kScalarSccProfiles{{
|
|
{ROCJITSU_CODE_ARCH_CDNA1, "cdna1", cdna1_encoding, WrexecDialect::Andn, false},
|
|
{ROCJITSU_CODE_ARCH_CDNA2, "cdna2", cdna2_encoding, WrexecDialect::Andn, false},
|
|
{ROCJITSU_CODE_ARCH_CDNA3, "cdna3", cdna3_encoding, WrexecDialect::Andn, false},
|
|
{ROCJITSU_CODE_ARCH_CDNA4, "cdna4", cdna4_encoding, WrexecDialect::Andn, false},
|
|
{ROCJITSU_CODE_ARCH_RDNA1, "rdna1", rdna1_encoding, WrexecDialect::Andn, true},
|
|
{ROCJITSU_CODE_ARCH_RDNA2, "rdna2", rdna2_encoding, WrexecDialect::Andn, true},
|
|
{ROCJITSU_CODE_ARCH_RDNA3, "rdna3", rdna3_encoding, WrexecDialect::AndNot, true},
|
|
{ROCJITSU_CODE_ARCH_RDNA3_5, "rdna3_5", rdna3_5_encoding, WrexecDialect::AndNot, true},
|
|
{ROCJITSU_CODE_ARCH_RDNA4, "rdna4", rdna4_encoding, WrexecDialect::AndNot, true},
|
|
{ROCJITSU_CODE_ARCH_GFX1250, "gfx1250", gfx1250_encoding, WrexecDialect::AndNot, true},
|
|
}};
|
|
|
|
EncodingWords require_encoding(const ScalarSccProfile &profile, std::string_view mnemonic) {
|
|
auto words = profile.find_encoding(mnemonic);
|
|
if (!words) {
|
|
ADD_FAILURE() << profile.name << " missing generated encoding for " << mnemonic;
|
|
return {};
|
|
}
|
|
return *words;
|
|
}
|
|
|
|
std::optional<std::string_view> first_supported(const ScalarSccProfile &profile,
|
|
std::initializer_list<std::string_view> mnemonics) {
|
|
for (const auto mnemonic : mnemonics) {
|
|
if (profile.find_encoding(mnemonic))
|
|
return mnemonic;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string_view require_first_supported(const ScalarSccProfile &profile,
|
|
std::initializer_list<std::string_view> mnemonics,
|
|
std::string_view instruction_family) {
|
|
auto mnemonic = first_supported(profile, mnemonics);
|
|
if (!mnemonic) {
|
|
ADD_FAILURE() << profile.name << " has no encoding for " << instruction_family;
|
|
return {};
|
|
}
|
|
return *mnemonic;
|
|
}
|
|
|
|
EncodingWords encode_sop1(const ScalarSccProfile &profile, std::string_view mnemonic, uint32_t sdst,
|
|
uint32_t ssrc0) {
|
|
auto words = require_encoding(profile, mnemonic);
|
|
constexpr uint32_t kOperandMask = 0xFFu | (0x7Fu << 16);
|
|
words[0] = (words[0] & ~kOperandMask) | (ssrc0 & 0xFFu) | ((sdst & 0x7Fu) << 16);
|
|
return words;
|
|
}
|
|
|
|
EncodingWords encode_sop2(const ScalarSccProfile &profile, std::string_view mnemonic, uint32_t sdst,
|
|
uint32_t ssrc0, uint32_t ssrc1) {
|
|
auto words = require_encoding(profile, mnemonic);
|
|
constexpr uint32_t kOperandMask = 0xFFFFu | (0x7Fu << 16);
|
|
words[0] = (words[0] & ~kOperandMask) | (ssrc0 & 0xFFu) | ((ssrc1 & 0xFFu) << 8) |
|
|
((sdst & 0x7Fu) << 16);
|
|
return words;
|
|
}
|
|
|
|
EncodingWords encode_sopc(const ScalarSccProfile &profile, std::string_view mnemonic,
|
|
uint32_t ssrc0, uint32_t ssrc1) {
|
|
auto words = require_encoding(profile, mnemonic);
|
|
words[0] = (words[0] & ~0xFFFFu) | (ssrc0 & 0xFFu) | ((ssrc1 & 0xFFu) << 8);
|
|
return words;
|
|
}
|
|
|
|
EncodingWords encode_sopk(const ScalarSccProfile &profile, std::string_view mnemonic, uint32_t sdst,
|
|
uint32_t simm16) {
|
|
auto words = require_encoding(profile, mnemonic);
|
|
constexpr uint32_t kOperandMask = 0xFFFFu | (0x7Fu << 16);
|
|
words[0] = (words[0] & ~kOperandMask) | (simm16 & 0xFFFFu) | ((sdst & 0x7Fu) << 16);
|
|
return words;
|
|
}
|
|
|
|
class ScalarSccFixture {
|
|
public:
|
|
ScalarSccFixture(const ScalarSccProfile &profile, std::string_view test_name)
|
|
: profile(profile),
|
|
gpu_mem(std::string(profile.name) + "_" + std::string(test_name) + "_mem"),
|
|
l2(std::string(profile.name) + "_" + std::string(test_name) + "_l2") {
|
|
config.arch = profile.arch;
|
|
config.num_wf_slots = 1;
|
|
config.sgprs_per_wf = 106;
|
|
config.vgprs_per_wf = 256;
|
|
config.lds_size_kb = 64;
|
|
cu = amdgpu::ComputeUnitCore::create(std::string(profile.name), config, &gpu_mem, &l2);
|
|
decoder = Decoder::create(profile.arch);
|
|
if (cu)
|
|
wf = cu->dispatch_wf(0, 0, config.sgprs_per_wf, config.vgprs_per_wf);
|
|
}
|
|
|
|
~ScalarSccFixture() {
|
|
// A wave frees its SGPR/VGPR blocks when it halts (s_endpgm-equivalent
|
|
// termination), so halt the resident wave to release its resources rather
|
|
// than a CU-wide reset.
|
|
if (wf && !wf->is_halted())
|
|
wf->halt();
|
|
}
|
|
|
|
bool ready() const { return cu != nullptr && decoder != nullptr && wf != nullptr; }
|
|
|
|
std::unique_ptr<Instruction> decode(const EncodingWords &words,
|
|
std::string_view expected_mnemonic) {
|
|
std::unique_ptr<Instruction> inst(decoder->decode(words.data()));
|
|
if (inst) {
|
|
EXPECT_EQ(std::string_view(inst->mnemonic()), expected_mnemonic) << profile.name;
|
|
}
|
|
return inst;
|
|
}
|
|
|
|
void expect_scc_consumer(bool expected_scc, std::string_view context) {
|
|
constexpr uint32_t kCmovSentinel = 0xDEADBEEFu;
|
|
const auto cmov_words = encode_sopk(profile, "s_cmovk_i32", /*sdst=*/8, /*simm16=*/0x1234u);
|
|
auto cmov = decode(cmov_words, "s_cmovk_i32");
|
|
ASSERT_NE(cmov, nullptr) << profile.name;
|
|
EXPECT_EQ(wf->read_scc(), expected_scc) << profile.name << " " << context;
|
|
cu->write_sgpr(sgpr_base() + 8, kCmovSentinel);
|
|
cu->execute_instruction(cmov.get(), *wf);
|
|
EXPECT_EQ(cu->read_sgpr(sgpr_base() + 8), expected_scc ? 0x1234u : kCmovSentinel)
|
|
<< profile.name << " " << context;
|
|
}
|
|
|
|
uint32_t sgpr_base() const { return wf->sgpr_alloc().base; }
|
|
|
|
const ScalarSccProfile &profile;
|
|
amdgpu::GpuMemory gpu_mem;
|
|
amdgpu::L2Cache l2;
|
|
amdgpu::ComputeUnitCore::Config config{};
|
|
std::unique_ptr<amdgpu::ComputeUnitCore> cu;
|
|
std::unique_ptr<Decoder> decoder;
|
|
amdgpu::Wavefront *wf = nullptr;
|
|
};
|
|
|
|
class WrexecPair {
|
|
public:
|
|
WrexecDialect dialect;
|
|
std::string_view first;
|
|
std::string_view second;
|
|
bool is_b64;
|
|
};
|
|
|
|
constexpr std::array<WrexecPair, 4> kWrexecPairs{{
|
|
{WrexecDialect::Andn, "s_andn1_wrexec_b32", "s_andn2_wrexec_b32", false},
|
|
{WrexecDialect::Andn, "s_andn1_wrexec_b64", "s_andn2_wrexec_b64", true},
|
|
{WrexecDialect::AndNot, "s_and_not0_wrexec_b32", "s_and_not1_wrexec_b32", false},
|
|
{WrexecDialect::AndNot, "s_and_not0_wrexec_b64", "s_and_not1_wrexec_b64", true},
|
|
}};
|
|
|
|
bool profile_expects_wrexec_pair(const ScalarSccProfile &profile, const WrexecPair &pair) {
|
|
return profile.wrexec_dialect == pair.dialect && (pair.is_b64 || profile.has_wrexec_b32);
|
|
}
|
|
|
|
bool expect_wrexec_availability(const ScalarSccProfile &profile, const WrexecPair &pair) {
|
|
const bool expected = profile_expects_wrexec_pair(profile, pair);
|
|
const bool first_available = profile.find_encoding(pair.first).has_value();
|
|
const bool second_available = profile.find_encoding(pair.second).has_value();
|
|
EXPECT_EQ(first_available, expected) << profile.name << " " << pair.first;
|
|
EXPECT_EQ(second_available, expected) << profile.name << " " << pair.second;
|
|
return first_available && second_available;
|
|
}
|
|
|
|
void expect_wrexec_def_use(const ScalarSccProfile &profile, const WrexecPair &pair) {
|
|
auto decoder = Decoder::create(profile.arch);
|
|
ASSERT_NE(decoder, nullptr) << profile.name;
|
|
|
|
constexpr uint32_t kSourceSgpr = 0;
|
|
constexpr uint32_t kDestSgpr = 4;
|
|
const uint8_t width = pair.is_b64 ? 2 : 1;
|
|
for (const auto mnemonic : {pair.first, pair.second}) {
|
|
const auto words = encode_sop1(profile, mnemonic, kDestSgpr, kSourceSgpr);
|
|
std::unique_ptr<Instruction> inst(decoder->decode(words.data()));
|
|
ASSERT_NE(inst, nullptr) << profile.name << " " << mnemonic;
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()), mnemonic) << profile.name;
|
|
|
|
const RegisterRef source{RegClass::SGPR, kSourceSgpr, width};
|
|
const RegisterRef destination{RegClass::SGPR, kDestSgpr, width};
|
|
// The implicit EXEC read/write and SCC def are modeled as inert fieldless
|
|
// operands (to_register_ref() == nullopt): they add to the operand counts
|
|
// but contribute nothing to def/use at this time. The field-bearing SGPR
|
|
// source/dest stay at index 0.
|
|
ASSERT_EQ(inst->num_src_operands(), 2) << profile.name << " " << mnemonic;
|
|
ASSERT_EQ(inst->num_dst_operands(), 3) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->src_operand(0)->to_register_ref(), source) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->dst_operand(0)->to_register_ref(), destination)
|
|
<< profile.name << " " << mnemonic;
|
|
for (uint8_t i = 1; i < inst->num_src_operands(); ++i) {
|
|
EXPECT_TRUE(inst->src_operand(i)->is_fieldless()) << profile.name << " " << mnemonic;
|
|
EXPECT_FALSE(inst->src_operand(i)->to_register_ref()) << profile.name << " " << mnemonic;
|
|
}
|
|
for (uint8_t i = 1; i < inst->num_dst_operands(); ++i) {
|
|
EXPECT_TRUE(inst->dst_operand(i)->is_fieldless()) << profile.name << " " << mnemonic;
|
|
EXPECT_FALSE(inst->dst_operand(i)->to_register_ref()) << profile.name << " " << mnemonic;
|
|
}
|
|
|
|
const InstDefUse def_use(*inst);
|
|
EXPECT_TRUE(def_use.uses.contains(source)) << profile.name << " " << mnemonic;
|
|
EXPECT_FALSE(def_use.uses.contains(destination)) << profile.name << " " << mnemonic;
|
|
EXPECT_TRUE(def_use.defs.contains(destination)) << profile.name << " " << mnemonic;
|
|
EXPECT_FALSE(def_use.defs.contains(source)) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(def_use.uses.size(), static_cast<size_t>(width)) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(def_use.defs.size(), static_cast<size_t>(width)) << profile.name << " " << mnemonic;
|
|
}
|
|
}
|
|
|
|
void run_wrexec_scc_cases(const ScalarSccProfile &profile, const WrexecPair &pair) {
|
|
ScalarSccFixture fixture(profile, "wrexec_scc");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
class Case {
|
|
public:
|
|
std::string_view mnemonic;
|
|
uint64_t old_exec;
|
|
uint64_t src;
|
|
uint64_t expected;
|
|
};
|
|
|
|
const uint64_t mask = pair.is_b64 ? ~uint64_t{0} : uint64_t{0xFFFFFFFFu};
|
|
const std::array<Case, 4> cases{{
|
|
{pair.first, 0x00FF00FF00FF00FFULL, 0x000F000F000F000FULL, 0x00F000F000F000F0ULL & mask},
|
|
{pair.first, 0x00FF00FF00FF00FFULL, ~uint64_t{0}, 0u},
|
|
{pair.second, 0x0F0F0F0F0F0F0F0FULL, 0xF0F0F0F0F0F0F0F0ULL, 0xF0F0F0F0F0F0F0F0ULL & mask},
|
|
{pair.second, ~uint64_t{0}, 0xF0F0F0F0F0F0F0F0ULL, 0u},
|
|
}};
|
|
|
|
constexpr uint32_t kSourceSgpr = 0;
|
|
constexpr uint32_t kDestSgpr = 4;
|
|
constexpr uint32_t kSourceHighSentinel = 0xA5A55A5Au;
|
|
constexpr uint32_t kDestHighSentinel = 0x5A5AA5A5u;
|
|
constexpr uint32_t kExecHighSentinel = 0xC3C33C3Cu;
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
for (const auto &test_case : cases) {
|
|
const auto words = encode_sop1(profile, test_case.mnemonic, kDestSgpr, kSourceSgpr);
|
|
auto inst = fixture.decode(words, test_case.mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb + kSourceSgpr, static_cast<uint32_t>(test_case.src));
|
|
fixture.cu->write_sgpr(sb + kSourceSgpr + 1, pair.is_b64
|
|
? static_cast<uint32_t>(test_case.src >> 32)
|
|
: kSourceHighSentinel);
|
|
fixture.cu->write_sgpr(sb + kDestSgpr, 0xBAD0D57u);
|
|
fixture.cu->write_sgpr(sb + kDestSgpr + 1, kDestHighSentinel);
|
|
const uint64_t raw_exec = pair.is_b64 ? test_case.old_exec
|
|
: (static_cast<uint64_t>(kExecHighSentinel) << 32) |
|
|
(test_case.old_exec & 0xFFFFFFFFULL);
|
|
fixture.wf->set_exec_raw(raw_exec);
|
|
const bool expected_scc = test_case.expected != 0;
|
|
fixture.wf->write_scc(!expected_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
|
|
uint64_t actual = fixture.cu->read_sgpr(sb + kDestSgpr);
|
|
if (pair.is_b64) {
|
|
actual |= static_cast<uint64_t>(fixture.cu->read_sgpr(sb + kDestSgpr + 1)) << 32;
|
|
EXPECT_EQ(fixture.wf->exec_raw(), test_case.expected)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
} else {
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + kSourceSgpr + 1), kSourceHighSentinel)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + kDestSgpr + 1), kDestHighSentinel)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
EXPECT_EQ(static_cast<uint32_t>(fixture.wf->exec_raw() >> 32), kExecHighSentinel)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
}
|
|
EXPECT_EQ(actual, test_case.expected) << profile.name << " " << test_case.mnemonic;
|
|
const uint64_t lane_mask = fixture.wf->wf_size() == 64 ? ~uint64_t{0} : uint64_t{0xFFFFFFFFu};
|
|
EXPECT_EQ(fixture.wf->exec(), test_case.expected & lane_mask)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
fixture.expect_scc_consumer(expected_scc, test_case.mnemonic);
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, WrexecWritesDestinationExecAndScc) {
|
|
for (const auto &profile : kScalarSccProfiles) {
|
|
for (const auto &candidate : kWrexecPairs) {
|
|
const bool expected = profile_expects_wrexec_pair(profile, candidate);
|
|
const bool available = expect_wrexec_availability(profile, candidate);
|
|
if (expected && available)
|
|
run_wrexec_scc_cases(profile, candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, WrexecRegisterDefUseMatchesArchitectureMatrix) {
|
|
for (const auto &profile : kScalarSccProfiles) {
|
|
for (const auto &candidate : kWrexecPairs) {
|
|
const bool expected = profile_expects_wrexec_pair(profile, candidate);
|
|
const bool available = expect_wrexec_availability(profile, candidate);
|
|
if (expected && available)
|
|
expect_wrexec_def_use(profile, candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
enum class SaveexecOp {
|
|
And,
|
|
Or,
|
|
Xor,
|
|
ExecAndNotSrc,
|
|
ExecOrNotSrc,
|
|
SrcAndNotExec,
|
|
SrcOrNotExec,
|
|
Nand,
|
|
Nor,
|
|
Xnor,
|
|
};
|
|
|
|
class SaveexecVariant {
|
|
public:
|
|
std::string_view mnemonic_stem;
|
|
SaveexecOp operation;
|
|
};
|
|
|
|
constexpr std::array<SaveexecVariant, 14> kSaveexecVariants{{
|
|
{"s_and_saveexec", SaveexecOp::And},
|
|
{"s_or_saveexec", SaveexecOp::Or},
|
|
{"s_xor_saveexec", SaveexecOp::Xor},
|
|
{"s_andn1_saveexec", SaveexecOp::ExecAndNotSrc},
|
|
{"s_orn1_saveexec", SaveexecOp::ExecOrNotSrc},
|
|
{"s_andn2_saveexec", SaveexecOp::SrcAndNotExec},
|
|
{"s_orn2_saveexec", SaveexecOp::SrcOrNotExec},
|
|
{"s_and_not0_saveexec", SaveexecOp::ExecAndNotSrc},
|
|
{"s_or_not0_saveexec", SaveexecOp::ExecOrNotSrc},
|
|
{"s_and_not1_saveexec", SaveexecOp::SrcAndNotExec},
|
|
{"s_or_not1_saveexec", SaveexecOp::SrcOrNotExec},
|
|
{"s_nand_saveexec", SaveexecOp::Nand},
|
|
{"s_nor_saveexec", SaveexecOp::Nor},
|
|
{"s_xnor_saveexec", SaveexecOp::Xnor},
|
|
}};
|
|
|
|
uint64_t evaluate_saveexec(SaveexecOp operation, uint64_t old_exec, uint64_t src) {
|
|
switch (operation) {
|
|
case SaveexecOp::And:
|
|
return old_exec & src;
|
|
case SaveexecOp::Or:
|
|
return old_exec | src;
|
|
case SaveexecOp::Xor:
|
|
return old_exec ^ src;
|
|
case SaveexecOp::ExecAndNotSrc:
|
|
return old_exec & ~src;
|
|
case SaveexecOp::ExecOrNotSrc:
|
|
return old_exec | ~src;
|
|
case SaveexecOp::SrcAndNotExec:
|
|
return src & ~old_exec;
|
|
case SaveexecOp::SrcOrNotExec:
|
|
return src | ~old_exec;
|
|
case SaveexecOp::Nand:
|
|
return ~(old_exec & src);
|
|
case SaveexecOp::Nor:
|
|
return ~(old_exec | src);
|
|
case SaveexecOp::Xnor:
|
|
return ~(old_exec ^ src);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::pair<uint64_t, uint64_t> zero_result_saveexec_inputs(SaveexecOp operation) {
|
|
constexpr uint64_t kAll = ~uint64_t{0};
|
|
switch (operation) {
|
|
case SaveexecOp::And:
|
|
case SaveexecOp::Or:
|
|
return {0, 0};
|
|
case SaveexecOp::Xor:
|
|
case SaveexecOp::ExecAndNotSrc:
|
|
case SaveexecOp::SrcAndNotExec:
|
|
case SaveexecOp::Nand:
|
|
return {kAll, kAll};
|
|
case SaveexecOp::ExecOrNotSrc:
|
|
return {0, kAll};
|
|
case SaveexecOp::SrcOrNotExec:
|
|
case SaveexecOp::Nor:
|
|
return {kAll, 0};
|
|
case SaveexecOp::Xnor:
|
|
return {0, kAll};
|
|
}
|
|
return {0, 0};
|
|
}
|
|
|
|
void run_saveexec_case(ScalarSccFixture &fixture, const ScalarSccProfile &profile,
|
|
const SaveexecVariant &variant, bool is_b64, uint64_t old_exec, uint64_t src,
|
|
uint32_t sdst = 4) {
|
|
constexpr uint32_t kHighSentinel = 0xA5A55A5Au;
|
|
const uint64_t width_mask = is_b64 ? ~uint64_t{0} : uint64_t{0xFFFFFFFFu};
|
|
old_exec &= width_mask;
|
|
src &= width_mask;
|
|
const uint64_t expected = evaluate_saveexec(variant.operation, old_exec, src) & width_mask;
|
|
const std::string mnemonic = std::string(variant.mnemonic_stem) + (is_b64 ? "_b64" : "_b32");
|
|
const auto words = encode_sop1(profile, mnemonic, sdst, /*ssrc0=*/0);
|
|
auto inst = fixture.decode(words, mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
fixture.cu->write_sgpr(sb, static_cast<uint32_t>(src));
|
|
if (is_b64)
|
|
fixture.cu->write_sgpr(sb + 1, static_cast<uint32_t>(src >> 32));
|
|
else
|
|
fixture.cu->write_sgpr(sb + sdst + 1, kHighSentinel);
|
|
|
|
const uint64_t initial_raw_exec =
|
|
is_b64 ? old_exec : (static_cast<uint64_t>(kHighSentinel) << 32) | old_exec;
|
|
fixture.wf->set_exec_raw(initial_raw_exec);
|
|
fixture.wf->write_scc(expected == 0);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
|
|
if (is_b64) {
|
|
const uint64_t saved = fixture.cu->read_sgpr(sb + sdst) |
|
|
(static_cast<uint64_t>(fixture.cu->read_sgpr(sb + sdst + 1)) << 32);
|
|
EXPECT_EQ(saved, old_exec) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(fixture.wf->exec_raw(), expected) << profile.name << " " << mnemonic;
|
|
} else {
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + sdst), static_cast<uint32_t>(old_exec))
|
|
<< profile.name << " " << mnemonic;
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + sdst + 1), kHighSentinel)
|
|
<< profile.name << " " << mnemonic;
|
|
EXPECT_EQ(fixture.wf->exec_raw(), (static_cast<uint64_t>(kHighSentinel) << 32) | expected)
|
|
<< profile.name << " " << mnemonic;
|
|
}
|
|
|
|
const uint64_t lane_mask = fixture.wf->wf_size() == 64 ? ~uint64_t{0} : uint64_t{0xFFFFFFFFu};
|
|
EXPECT_EQ(fixture.wf->exec(), expected & lane_mask) << profile.name << " " << mnemonic;
|
|
fixture.expect_scc_consumer(expected != 0, mnemonic);
|
|
}
|
|
|
|
TEST(ScalarSccTest, SaveexecSupportsAliasedSourceAndDestination) {
|
|
constexpr uint64_t kOldExec = 0x0F0FF0F00FF00FF0ULL;
|
|
constexpr uint64_t kSource = 0x3333CCCC5555AAAAULL;
|
|
const auto &variant = kSaveexecVariants.front();
|
|
size_t b32_coverage = 0;
|
|
size_t b64_coverage = 0;
|
|
|
|
for (const auto &profile : kScalarSccProfiles) {
|
|
ScalarSccFixture fixture(profile, "saveexec_alias");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
for (const bool is_b64 : {false, true}) {
|
|
const std::string mnemonic = std::string(variant.mnemonic_stem) + (is_b64 ? "_b64" : "_b32");
|
|
if (!profile.find_encoding(mnemonic))
|
|
continue;
|
|
|
|
run_saveexec_case(fixture, profile, variant, is_b64, kOldExec, kSource, /*sdst=*/0);
|
|
++(is_b64 ? b64_coverage : b32_coverage);
|
|
}
|
|
}
|
|
|
|
EXPECT_GT(b32_coverage, 0u);
|
|
EXPECT_GT(b64_coverage, 0u);
|
|
}
|
|
|
|
TEST(ScalarSccTest, SaveexecBooleanMatrixPreservesExecAndWritesScc) {
|
|
constexpr uint64_t kDistinguishingOldExec = 0x0F0FF0F00FF00FF0ULL;
|
|
constexpr uint64_t kDistinguishingSource = 0x3333CCCC5555AAAAULL;
|
|
std::array<size_t, kSaveexecVariants.size()> b32_coverage{};
|
|
std::array<size_t, kSaveexecVariants.size()> b64_coverage{};
|
|
|
|
for (const auto &profile : kScalarSccProfiles) {
|
|
ScalarSccFixture fixture(profile, "saveexec_boolean_scc");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
for (size_t index = 0; index < kSaveexecVariants.size(); ++index) {
|
|
const auto &variant = kSaveexecVariants[index];
|
|
for (const bool is_b64 : {false, true}) {
|
|
const std::string mnemonic =
|
|
std::string(variant.mnemonic_stem) + (is_b64 ? "_b64" : "_b32");
|
|
if (!profile.find_encoding(mnemonic))
|
|
continue;
|
|
|
|
const uint64_t width_mask = is_b64 ? ~uint64_t{0} : uint64_t{0xFFFFFFFFu};
|
|
ASSERT_NE(
|
|
evaluate_saveexec(variant.operation, kDistinguishingOldExec, kDistinguishingSource) &
|
|
width_mask,
|
|
0u)
|
|
<< mnemonic;
|
|
run_saveexec_case(fixture, profile, variant, is_b64, kDistinguishingOldExec,
|
|
kDistinguishingSource);
|
|
|
|
const auto [zero_old_exec, zero_source] = zero_result_saveexec_inputs(variant.operation);
|
|
ASSERT_EQ(evaluate_saveexec(variant.operation, zero_old_exec, zero_source) & width_mask, 0u)
|
|
<< mnemonic;
|
|
run_saveexec_case(fixture, profile, variant, is_b64, zero_old_exec, zero_source);
|
|
|
|
++(is_b64 ? b64_coverage[index] : b32_coverage[index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t index = 0; index < kSaveexecVariants.size(); ++index) {
|
|
EXPECT_GT(b32_coverage[index], 0u) << kSaveexecVariants[index].mnemonic_stem;
|
|
EXPECT_GT(b64_coverage[index], 0u) << kSaveexecVariants[index].mnemonic_stem;
|
|
}
|
|
}
|
|
|
|
std::string_view addk_mnemonic(const ScalarSccProfile &profile) {
|
|
return require_first_supported(profile, {"s_addk_i32", "s_addk_co_i32"}, "ADDK");
|
|
}
|
|
|
|
void run_addk_scc_cases(const ScalarSccProfile &profile) {
|
|
ScalarSccFixture fixture(profile, "addk_scc");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
class Case {
|
|
public:
|
|
uint32_t initial;
|
|
uint16_t immediate;
|
|
uint32_t expected;
|
|
bool expected_scc;
|
|
};
|
|
constexpr std::array<Case, 4> cases{{
|
|
{0x7FFFFFFFu, 1u, 0x80000000u, true},
|
|
{0xFFFFFFFFu, 1u, 0u, false},
|
|
{0x80000000u, 0xFFFFu, 0x7FFFFFFFu, true},
|
|
{5u, 0xFFFDu, 2u, false},
|
|
}};
|
|
|
|
const auto mnemonic = addk_mnemonic(profile);
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
for (const auto &test_case : cases) {
|
|
const auto words = encode_sopk(profile, mnemonic, /*sdst=*/4, test_case.immediate);
|
|
auto inst = fixture.decode(words, mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb + 4, test_case.initial);
|
|
fixture.wf->write_scc(!test_case.expected_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + 4), test_case.expected) << profile.name;
|
|
fixture.expect_scc_consumer(test_case.expected_scc, mnemonic);
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, AddkUsesSignedOverflowAndFeedsSccConsumer) {
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_addk_scc_cases(profile);
|
|
}
|
|
|
|
void run_mulk_cases(const ScalarSccProfile &profile) {
|
|
ScalarSccFixture fixture(profile, "mulk");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
class Case {
|
|
public:
|
|
uint32_t initial;
|
|
uint16_t immediate;
|
|
uint32_t expected;
|
|
};
|
|
constexpr std::array<Case, 4> cases{{
|
|
{7u, 3u, 21u},
|
|
{7u, 0xFFFDu, 0xFFFFFFEBu},
|
|
{0x12345678u, 0u, 0u},
|
|
{0x80000001u, 2u, 2u},
|
|
}};
|
|
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
for (const auto &test_case : cases) {
|
|
const auto words = encode_sopk(profile, "s_mulk_i32", /*sdst=*/4, test_case.immediate);
|
|
auto inst = fixture.decode(words, "s_mulk_i32");
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
for (const bool initial_scc : {false, true}) {
|
|
fixture.cu->write_sgpr(sb + 4, test_case.initial);
|
|
fixture.wf->write_scc(initial_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + 4), test_case.expected) << profile.name;
|
|
fixture.expect_scc_consumer(initial_scc, "s_mulk_i32");
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, MulkUsesOldDestinationAndSignedImmediate) {
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_mulk_cases(profile);
|
|
}
|
|
|
|
TEST(ScalarSccTest, AddkAndMulkRegisterDestinationRead) {
|
|
for (const auto &profile : kScalarSccProfiles) {
|
|
auto decoder = Decoder::create(profile.arch);
|
|
ASSERT_NE(decoder, nullptr) << profile.name;
|
|
const std::array mnemonics{addk_mnemonic(profile), std::string_view{"s_mulk_i32"}};
|
|
for (const auto mnemonic : mnemonics) {
|
|
const auto words = encode_sopk(profile, mnemonic, /*sdst=*/4, /*simm16=*/1);
|
|
std::unique_ptr<Instruction> inst(decoder->decode(words.data()));
|
|
ASSERT_NE(inst, nullptr) << profile.name << " " << mnemonic;
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()), mnemonic) << profile.name;
|
|
|
|
// s_addk_i32 sets SCC, modeled as an inert fieldless dst operand;
|
|
// s_mulk_i32 does not touch SCC. SCC carries no def/use either way.
|
|
const bool sets_scc = mnemonic != std::string_view{"s_mulk_i32"};
|
|
ASSERT_EQ(inst->num_src_operands(), 2) << profile.name << " " << mnemonic;
|
|
ASSERT_EQ(inst->num_dst_operands(), sets_scc ? 2 : 1) << profile.name << " " << mnemonic;
|
|
if (sets_scc) {
|
|
EXPECT_TRUE(inst->dst_operand(1)->is_fieldless()) << profile.name << " " << mnemonic;
|
|
EXPECT_FALSE(inst->dst_operand(1)->to_register_ref()) << profile.name << " " << mnemonic;
|
|
}
|
|
EXPECT_EQ(inst->src_operand(0)->to_register_ref(), (RegisterRef{RegClass::SGPR, 4, 1}))
|
|
<< profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->src_operand(0), inst->dst_operand(0)) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->src_operand(0)->name(), "s4") << profile.name << " " << mnemonic;
|
|
EXPECT_FALSE(inst->src_operand(1)->to_register_ref()) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->src_operand(1)->encoding_value(), 1) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->src_operand(1)->name(), "1") << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(inst->dst_operand(0)->to_register_ref(), (RegisterRef{RegClass::SGPR, 4, 1}))
|
|
<< profile.name << " " << mnemonic;
|
|
|
|
const InstDefUse def_use(*inst);
|
|
EXPECT_TRUE(def_use.uses.contains(RegisterRef{RegClass::SGPR, 4, 1}))
|
|
<< profile.name << " " << mnemonic;
|
|
EXPECT_TRUE(def_use.defs.contains(RegisterRef{RegClass::SGPR, 4, 1}))
|
|
<< profile.name << " " << mnemonic;
|
|
EXPECT_EQ(def_use.uses.size(), 1u) << profile.name << " " << mnemonic;
|
|
EXPECT_EQ(def_use.defs.size(), 1u) << profile.name << " " << mnemonic;
|
|
}
|
|
}
|
|
}
|
|
|
|
void run_sop2_scc_cases(const ScalarSccProfile &profile) {
|
|
ScalarSccFixture fixture(profile, "sop2_scc");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
const auto add_u32 =
|
|
require_first_supported(profile, {"s_add_u32", "s_add_co_u32"}, "unsigned ADD");
|
|
const auto sub_u32 =
|
|
require_first_supported(profile, {"s_sub_u32", "s_sub_co_u32"}, "unsigned SUB");
|
|
const auto add_i32 =
|
|
require_first_supported(profile, {"s_add_i32", "s_add_co_i32"}, "signed ADD");
|
|
const auto sub_i32 =
|
|
require_first_supported(profile, {"s_sub_i32", "s_sub_co_i32"}, "signed SUB");
|
|
|
|
class Case {
|
|
public:
|
|
std::string_view mnemonic;
|
|
uint32_t lhs;
|
|
uint32_t rhs;
|
|
uint32_t expected_result;
|
|
bool expected_scc;
|
|
};
|
|
const std::array<Case, 18> cases{{
|
|
{add_u32, 0xFFFFFFFFu, 1u, 0u, true},
|
|
{add_u32, 7u, 5u, 12u, false},
|
|
{sub_u32, 0u, 1u, 0xFFFFFFFFu, true},
|
|
{sub_u32, 7u, 5u, 2u, false},
|
|
{add_i32, 0x7FFFFFFFu, 1u, 0x80000000u, true},
|
|
{add_i32, 0xFFFFFFFFu, 1u, 0u, false},
|
|
{sub_i32, 0x80000000u, 1u, 0x7FFFFFFFu, true},
|
|
{sub_i32, 5u, 3u, 2u, false},
|
|
{"s_and_b32", 0xF3u, 0x0Fu, 3u, true},
|
|
{"s_and_b32", 0xF0u, 0x0Fu, 0u, false},
|
|
{"s_min_i32", 0xFFFFFFFCu, 3u, 0xFFFFFFFCu, true},
|
|
{"s_min_i32", 4u, 0xFFFFFFFEu, 0xFFFFFFFEu, false},
|
|
{"s_max_i32", 7u, 3u, 7u, true},
|
|
{"s_max_i32", 0xFFFFFFFCu, 3u, 3u, false},
|
|
{"s_max_i32", 0x80000000u, 0x80000000u, 0x80000000u, false},
|
|
{"s_max_u32", 7u, 3u, 7u, true},
|
|
{"s_max_u32", 3u, 7u, 7u, false},
|
|
{"s_max_u32", 0xFFFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu, false},
|
|
}};
|
|
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
for (const auto &test_case : cases) {
|
|
const auto words =
|
|
encode_sop2(profile, test_case.mnemonic, /*sdst=*/2, /*ssrc0=*/0, /*ssrc1=*/1);
|
|
auto inst = fixture.decode(words, test_case.mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb, test_case.lhs);
|
|
fixture.cu->write_sgpr(sb + 1, test_case.rhs);
|
|
fixture.wf->write_scc(!test_case.expected_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + 2), test_case.expected_result)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
fixture.expect_scc_consumer(test_case.expected_scc, test_case.mnemonic);
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, Sop2SccModesFeedSccConsumer) {
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_sop2_scc_cases(profile);
|
|
}
|
|
|
|
void run_sop2_carry_input_cases(const ScalarSccProfile &profile) {
|
|
ScalarSccFixture fixture(profile, "sop2_carry_input_scc");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
const auto addc =
|
|
require_first_supported(profile, {"s_addc_u32", "s_addc_co_u32", "s_add_co_ci_u32"}, "ADDC");
|
|
const auto subb =
|
|
require_first_supported(profile, {"s_subb_u32", "s_subb_co_u32", "s_sub_co_ci_u32"}, "SUBB");
|
|
ASSERT_FALSE(addc.empty()) << profile.name;
|
|
ASSERT_FALSE(subb.empty()) << profile.name;
|
|
|
|
class Case {
|
|
public:
|
|
std::string_view mnemonic;
|
|
uint32_t lhs;
|
|
uint32_t rhs;
|
|
bool input_scc;
|
|
uint32_t expected_result;
|
|
bool expected_scc;
|
|
};
|
|
const std::array<Case, 8> cases{{
|
|
{addc, 1u, 2u, false, 3u, false},
|
|
{addc, 1u, 2u, true, 4u, false},
|
|
{addc, 0xFFFFFFFFu, 1u, false, 0u, true},
|
|
{addc, 0xFFFFFFFFu, 0u, true, 0u, true},
|
|
{subb, 5u, 3u, false, 2u, false},
|
|
{subb, 5u, 3u, true, 1u, false},
|
|
{subb, 0u, 1u, false, 0xFFFFFFFFu, true},
|
|
{subb, 0u, 0u, true, 0xFFFFFFFFu, true},
|
|
}};
|
|
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
for (const auto &test_case : cases) {
|
|
const auto words =
|
|
encode_sop2(profile, test_case.mnemonic, /*sdst=*/2, /*ssrc0=*/0, /*ssrc1=*/1);
|
|
auto inst = fixture.decode(words, test_case.mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb, test_case.lhs);
|
|
fixture.cu->write_sgpr(sb + 1, test_case.rhs);
|
|
fixture.wf->write_scc(test_case.input_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + 2), test_case.expected_result)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
fixture.expect_scc_consumer(test_case.expected_scc, test_case.mnemonic);
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, Sop2CarryInputReadsAndWritesScc) {
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_sop2_carry_input_cases(profile);
|
|
}
|
|
|
|
void run_sopc_sopk_compare_scc_cases(const ScalarSccProfile &profile) {
|
|
ScalarSccFixture fixture(profile, "scalar_compare_scc");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
|
|
class SopcCase {
|
|
public:
|
|
std::string_view mnemonic;
|
|
uint32_t lhs;
|
|
uint32_t rhs;
|
|
bool expected_scc;
|
|
};
|
|
constexpr std::array<SopcCase, 4> sopc_cases{{
|
|
{"s_cmp_eq_i32", 5u, 5u, true},
|
|
{"s_cmp_eq_i32", 5u, 6u, false},
|
|
{"s_bitcmp0_b32", 0u, 31u, true},
|
|
{"s_bitcmp0_b32", 0x80000000u, 31u, false},
|
|
}};
|
|
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
for (const auto &test_case : sopc_cases) {
|
|
const auto words = encode_sopc(profile, test_case.mnemonic, /*ssrc0=*/0, /*ssrc1=*/1);
|
|
auto inst = fixture.decode(words, test_case.mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb, test_case.lhs);
|
|
fixture.cu->write_sgpr(sb + 1, test_case.rhs);
|
|
fixture.wf->write_scc(!test_case.expected_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
fixture.expect_scc_consumer(test_case.expected_scc, test_case.mnemonic);
|
|
}
|
|
|
|
if (!profile.find_encoding("s_cmpk_eq_i32"))
|
|
return;
|
|
|
|
class SopkCase {
|
|
public:
|
|
uint32_t src;
|
|
uint16_t immediate;
|
|
bool expected_scc;
|
|
};
|
|
constexpr std::array<SopkCase, 2> sopk_cases{{
|
|
{0xFFFFFFFFu, 0xFFFFu, true},
|
|
{0x0000FFFFu, 0xFFFFu, false},
|
|
}};
|
|
for (const auto &test_case : sopk_cases) {
|
|
const auto words = encode_sopk(profile, "s_cmpk_eq_i32", /*sdst=*/4, test_case.immediate);
|
|
auto inst = fixture.decode(words, "s_cmpk_eq_i32");
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb + 4, test_case.src);
|
|
fixture.wf->write_scc(!test_case.expected_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
fixture.expect_scc_consumer(test_case.expected_scc, "s_cmpk_eq_i32");
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, SopcAndSopkCompareFeedSccConsumer) {
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_sopc_sopk_compare_scc_cases(profile);
|
|
}
|
|
|
|
class ScalarUnarySccCase {
|
|
public:
|
|
std::string_view mnemonic;
|
|
uint64_t src;
|
|
uint64_t expected;
|
|
bool result_is_64_bit = false;
|
|
};
|
|
|
|
template <size_t N>
|
|
void run_scalar_unary_preserves_scc(const ScalarSccProfile &profile,
|
|
const std::array<ScalarUnarySccCase, N> &cases,
|
|
std::string_view test_name) {
|
|
bool has_supported_case = false;
|
|
for (const auto &test_case : cases)
|
|
has_supported_case |= profile.find_encoding(test_case.mnemonic).has_value();
|
|
if (!has_supported_case)
|
|
return;
|
|
|
|
ScalarSccFixture fixture(profile, test_name);
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
constexpr uint32_t kHighDestinationSentinel = 0xA5A55A5Au;
|
|
|
|
for (const auto &test_case : cases) {
|
|
if (!profile.find_encoding(test_case.mnemonic))
|
|
continue;
|
|
const auto words = encode_sop1(profile, test_case.mnemonic, /*sdst=*/2, /*ssrc0=*/0);
|
|
for (bool initial_scc : std::array<bool, 2>{false, true}) {
|
|
auto inst = fixture.decode(words, test_case.mnemonic);
|
|
ASSERT_NE(inst, nullptr) << profile.name;
|
|
|
|
fixture.cu->write_sgpr(sb, static_cast<uint32_t>(test_case.src));
|
|
fixture.cu->write_sgpr(sb + 1, static_cast<uint32_t>(test_case.src >> 32));
|
|
fixture.cu->write_sgpr(sb + 2, 0u);
|
|
fixture.cu->write_sgpr(sb + 3, kHighDestinationSentinel);
|
|
fixture.wf->write_scc(initial_scc);
|
|
fixture.cu->execute_instruction(inst.get(), *fixture.wf);
|
|
|
|
uint64_t actual = fixture.cu->read_sgpr(sb + 2);
|
|
if (test_case.result_is_64_bit) {
|
|
actual |= static_cast<uint64_t>(fixture.cu->read_sgpr(sb + 3)) << 32;
|
|
} else {
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + 3), kHighDestinationSentinel)
|
|
<< profile.name << " " << test_case.mnemonic;
|
|
}
|
|
EXPECT_EQ(actual, test_case.expected) << profile.name << " " << test_case.mnemonic;
|
|
EXPECT_EQ(fixture.wf->read_scc(), initial_scc)
|
|
<< profile.name << " " << test_case.mnemonic << " initial_scc=" << initial_scc;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(ScalarSccTest, ScalarCvtPreservesScc) {
|
|
const uint32_t one_f32 = std::bit_cast<uint32_t>(1.0f);
|
|
const uint32_t one_f16 = util::f32_to_f16(1.0f);
|
|
const uint32_t qnan_f32 = 0x7FC00000u;
|
|
const uint32_t i32_overflow_f32 = std::bit_cast<uint32_t>(2147483648.0f);
|
|
const uint32_t below_i32_min_f32 = std::bit_cast<uint32_t>(-2147483904.0f);
|
|
const uint32_t u32_overflow_f32 = std::bit_cast<uint32_t>(4294967296.0f);
|
|
|
|
const std::array<ScalarUnarySccCase, 13> cases{{
|
|
{"s_cvt_f32_i32", 1u, one_f32},
|
|
{"s_cvt_f32_u32", 1u, one_f32},
|
|
{"s_cvt_i32_f32", one_f32, 1u},
|
|
{"s_cvt_i32_f32", qnan_f32, 0u},
|
|
{"s_cvt_i32_f32", i32_overflow_f32, 0x7FFFFFFFu},
|
|
{"s_cvt_i32_f32", below_i32_min_f32, 0x80000000u},
|
|
{"s_cvt_u32_f32", one_f32, 1u},
|
|
{"s_cvt_u32_f32", qnan_f32, 0u},
|
|
{"s_cvt_u32_f32", std::bit_cast<uint32_t>(-1.0f), 0u},
|
|
{"s_cvt_u32_f32", u32_overflow_f32, 0xFFFFFFFFu},
|
|
{"s_cvt_f16_f32", one_f32, one_f16},
|
|
{"s_cvt_f32_f16", one_f16, one_f32},
|
|
{"s_cvt_hi_f32_f16", one_f16 << 16, one_f32},
|
|
}};
|
|
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_scalar_unary_preserves_scc(profile, cases, "scalar_cvt_scc");
|
|
}
|
|
|
|
TEST(ScalarSccTest, ScalarScanPreservesScc) {
|
|
constexpr std::array<ScalarUnarySccCase, 18> cases{{
|
|
{"s_ff0_i32_b32", 0x0000000FULL, 4u},
|
|
{"s_ff0_i32_b64", 0x00000000FFFFFFFFULL, 32u},
|
|
{"s_ff1_i32_b32", 0x00000100ULL, 8u},
|
|
{"s_ff1_i32_b64", 0x0000000100000000ULL, 32u},
|
|
{"s_flbit_i32_b32", 0x00F00000ULL, 8u},
|
|
{"s_flbit_i32_b64", 0x0000000100000000ULL, 31u},
|
|
{"s_flbit_i32", 0x40000000ULL, 1u},
|
|
{"s_flbit_i32_i64", 0x0000000100000000ULL, 31u},
|
|
{"s_ctz_i32_b32", 0x00000100ULL, 8u},
|
|
{"s_ctz_i32_b64", 0x0000000100000000ULL, 32u},
|
|
{"s_clz_i32_u32", 0x00F00000ULL, 8u},
|
|
{"s_clz_i32_u64", 0x0000000100000000ULL, 31u},
|
|
{"s_cls_i32", 0x40000000ULL, 1u},
|
|
{"s_cls_i32", 0u, 0xFFFFFFFFu},
|
|
{"s_cls_i32", 0xFFFFFFFFULL, 0xFFFFFFFFu},
|
|
{"s_cls_i32_i64", 0x0000010080000000ULL, 23u},
|
|
{"s_cls_i32_i64", 0u, 0xFFFFFFFFu},
|
|
{"s_cls_i32_i64", 0xFFFFFFFFFFFFFFFFULL, 0xFFFFFFFFu},
|
|
}};
|
|
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_scalar_unary_preserves_scc(profile, cases, "scalar_scan_scc");
|
|
}
|
|
|
|
TEST(ScalarSccTest, ScalarMiscUnaryPreservesScc) {
|
|
constexpr std::array<ScalarUnarySccCase, 10> cases{{
|
|
{"s_brev_b32", 1u, 0x80000000u},
|
|
{"s_brev_b64", 1u, 0x8000000000000000ULL, true},
|
|
{"s_ceil_f32", 0x3FC00000u, 0x40000000u},
|
|
{"s_floor_f32", 0x3FC00000u, 0x3F800000u},
|
|
{"s_trunc_f32", 0x3FC00000u, 0x3F800000u},
|
|
{"s_rndne_f32", 0x3FC00000u, 0x40000000u},
|
|
{"s_ceil_f16", 0x3E00u, 0x4000u},
|
|
{"s_floor_f16", 0x3E00u, 0x3C00u},
|
|
{"s_trunc_f16", 0x3E00u, 0x3C00u},
|
|
{"s_rndne_f16", 0x3E00u, 0x4000u},
|
|
}};
|
|
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_scalar_unary_preserves_scc(profile, cases, "scalar_misc_unary_scc");
|
|
}
|
|
|
|
void run_scalar_scan_carry_chain(const ScalarSccProfile &profile) {
|
|
const auto scan_mnemonic = first_supported(profile, {"s_ff1_i32_b32", "s_ctz_i32_b32"});
|
|
const auto add_mnemonic = first_supported(profile, {"s_add_u32", "s_add_co_u32"});
|
|
const auto addc_mnemonic =
|
|
first_supported(profile, {"s_addc_u32", "s_addc_co_u32", "s_add_co_ci_u32"});
|
|
ASSERT_TRUE(scan_mnemonic.has_value()) << profile.name;
|
|
ASSERT_TRUE(add_mnemonic.has_value()) << profile.name;
|
|
ASSERT_TRUE(addc_mnemonic.has_value()) << profile.name;
|
|
|
|
ScalarSccFixture fixture(profile, "scalar_scan_carry");
|
|
ASSERT_TRUE(fixture.ready()) << profile.name;
|
|
const uint32_t sb = fixture.sgpr_base();
|
|
const auto add_words = encode_sop2(profile, *add_mnemonic, /*sdst=*/0, /*ssrc0=*/4, /*ssrc1=*/0);
|
|
const auto scan_words = encode_sop1(profile, *scan_mnemonic, /*sdst=*/10, /*ssrc0=*/12);
|
|
const auto addc_words =
|
|
encode_sop2(profile, *addc_mnemonic, /*sdst=*/1, /*ssrc0=*/5, /*ssrc1=*/1);
|
|
|
|
auto add = fixture.decode(add_words, *add_mnemonic);
|
|
auto scan = fixture.decode(scan_words, *scan_mnemonic);
|
|
auto addc = fixture.decode(addc_words, *addc_mnemonic);
|
|
ASSERT_NE(add, nullptr);
|
|
ASSERT_NE(scan, nullptr);
|
|
ASSERT_NE(addc, nullptr);
|
|
|
|
fixture.cu->write_sgpr(sb, 1u);
|
|
fixture.cu->write_sgpr(sb + 1, 0u);
|
|
fixture.cu->write_sgpr(sb + 4, 0xFFFFFFFFu);
|
|
fixture.cu->write_sgpr(sb + 5, 0x54u);
|
|
fixture.cu->write_sgpr(sb + 10, 0u);
|
|
fixture.cu->write_sgpr(sb + 12, 1u);
|
|
fixture.wf->write_scc(false);
|
|
|
|
fixture.cu->execute_instruction(add.get(), *fixture.wf);
|
|
ASSERT_EQ(fixture.cu->read_sgpr(sb), 0u);
|
|
ASSERT_TRUE(fixture.wf->read_scc());
|
|
|
|
fixture.cu->execute_instruction(scan.get(), *fixture.wf);
|
|
ASSERT_EQ(fixture.cu->read_sgpr(sb + 10), 0u);
|
|
ASSERT_TRUE(fixture.wf->read_scc());
|
|
|
|
fixture.cu->execute_instruction(addc.get(), *fixture.wf);
|
|
EXPECT_EQ(fixture.cu->read_sgpr(sb + 1), 0x55u);
|
|
EXPECT_FALSE(fixture.wf->read_scc());
|
|
}
|
|
|
|
TEST(ScalarSccTest, ScalarScanDoesNotBreakAddcCarryChain) {
|
|
for (const auto &profile : kScalarSccProfiles)
|
|
run_scalar_scan_carry_chain(profile);
|
|
}
|
|
|
|
} // namespace
|