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
4808 lines
227 KiB
C++
4808 lines
227 KiB
C++
// Copyright (c) 2026 Advanced Micro Devices, Inc.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "rocjitsu/analysis/def_use_chain.h"
|
|
#include "rocjitsu/analysis/exec_state.h"
|
|
#include "rocjitsu/analysis/indirect_branch_discovery.h"
|
|
#include "rocjitsu/analysis/liveness.h"
|
|
#include "rocjitsu/code/basic_block.h"
|
|
#include "rocjitsu/code/code_object.h"
|
|
#include "rocjitsu/code/dbt/binary_translator_internal.h"
|
|
#include "rocjitsu/code/patch/instruction_builder.h"
|
|
#include "rocjitsu/code/rj_code.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna3/builders.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna3/mubuf.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna3/opcodes.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna4/builders.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna4/opcodes.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/cdna4/operand.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/gfx1250/builders.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/gfx1250/opcodes.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/gfx1250/vbuffer.h"
|
|
#include "rocjitsu/isa/arch/amdgpu/rdna3/mubuf.h"
|
|
#include "rocjitsu/isa/decoder.h"
|
|
#include "rocjitsu/isa/instruction.h"
|
|
#include "rocjitsu/isa/isa_traits.h"
|
|
#include "rocjitsu/isa/operand.h"
|
|
#include "rocjitsu/isa/register_set.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <iterator>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace rocjitsu {
|
|
namespace {
|
|
|
|
class TestOperand : public Operand {
|
|
public:
|
|
TestOperand() = default;
|
|
explicit TestOperand(RegisterRef ref) : Operand(ref.width * 32, ref.index), ref_(ref) {}
|
|
// Sub-register operand: same RegisterRef, but a caller-chosen bit width so partial
|
|
// (less-than-32-bit) defs can be exercised.
|
|
TestOperand(RegisterRef ref, int size_bits) : Operand(size_bits, ref.index), ref_(ref) {}
|
|
|
|
std::optional<RegisterRef> to_register_ref() const override { return ref_; }
|
|
std::optional<uint64_t> literal64_value() const override { return lit_; }
|
|
std::optional<uint64_t> const_value() const override {
|
|
return const_.has_value() ? const_ : literal64_value();
|
|
}
|
|
|
|
// Turn this into a 64-bit literal source operand (no register ref). Models the
|
|
// literal64 path: both literal64_value() and const_value() report it.
|
|
void set_literal(uint64_t v) {
|
|
lit_ = v;
|
|
size_bits_ = 64;
|
|
}
|
|
|
|
// Turn this into an inline-constant source operand: const_value() reports it
|
|
// but literal64_value() stays empty (mirrors how inline constants behave).
|
|
void set_inline_const(uint64_t v) {
|
|
const_ = v;
|
|
size_bits_ = 64;
|
|
}
|
|
|
|
private:
|
|
std::optional<RegisterRef> ref_;
|
|
std::optional<uint64_t> lit_;
|
|
std::optional<uint64_t> const_;
|
|
};
|
|
|
|
class TestInstruction : public Instruction {
|
|
public:
|
|
// `literal_src`, when set, appends one literal source operand carrying the
|
|
// given value via literal64_value() — models an instruction's literal64
|
|
// (SIMM) immediate, e.g. the all-ones source of `s_mov_b64 exec, <lit>`.
|
|
TestInstruction(std::string_view mnemonic, std::initializer_list<RegisterRef> defs = {},
|
|
std::initializer_list<RegisterRef> uses = {}, uint64_t flags = 0,
|
|
std::optional<int64_t> branch_delta = std::nullopt,
|
|
std::initializer_list<RegisterRef> implicit_uses = {},
|
|
std::optional<uint64_t> literal_src = std::nullopt,
|
|
std::optional<uint64_t> inline_const_src = std::nullopt, int def_size_bits = 0)
|
|
: Instruction(mnemonic, nullptr), implicit_uses_(implicit_uses), branch_delta_(branch_delta) {
|
|
size_ = 4;
|
|
flags_ = flags;
|
|
|
|
for (RegisterRef ref : defs) {
|
|
// def_size_bits == 0 keeps the default full-lane width; a non-zero value
|
|
// models a partial (sub-32-bit) def of the same register.
|
|
dst_storage_[num_dst_] =
|
|
def_size_bits == 0 ? TestOperand(ref) : TestOperand(ref, def_size_bits);
|
|
dst_operands_[num_dst_] = &dst_storage_[num_dst_];
|
|
++num_dst_;
|
|
}
|
|
for (RegisterRef ref : uses) {
|
|
src_storage_[num_src_] = TestOperand(ref);
|
|
src_operands_[num_src_] = &src_storage_[num_src_];
|
|
++num_src_;
|
|
}
|
|
if (literal_src) {
|
|
src_storage_[num_src_].set_literal(*literal_src);
|
|
src_operands_[num_src_] = &src_storage_[num_src_];
|
|
++num_src_;
|
|
}
|
|
if (inline_const_src) {
|
|
src_storage_[num_src_].set_inline_const(*inline_const_src);
|
|
src_operands_[num_src_] = &src_storage_[num_src_];
|
|
++num_src_;
|
|
}
|
|
}
|
|
|
|
std::optional<int64_t> branch_offset_bytes() const override { return branch_delta_; }
|
|
|
|
void implicit_uses(RegisterSet &uses) const override {
|
|
for (RegisterRef ref : implicit_uses_)
|
|
uses.expand(ref);
|
|
// Mirror the codegen: a sub-dword (< 32-bit) destination writes only part
|
|
// of its register lane, so the old value survives and the register is also
|
|
// read. Generated instructions surface these partial defs via implicit_uses.
|
|
for (int i = 0; i < num_dst_; ++i) {
|
|
const Operand *op = dst_operands_[i];
|
|
if (op != nullptr && op->size_bits() > 0 && op->size_bits() < REGISTER_GRANULARITY)
|
|
if (auto ref = op->to_register_ref())
|
|
uses.expand(*ref);
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::array<TestOperand, 2> dst_storage_{};
|
|
std::array<TestOperand, 4> src_storage_{};
|
|
std::vector<RegisterRef> implicit_uses_;
|
|
std::optional<int64_t> branch_delta_;
|
|
};
|
|
|
|
class TestTextSection : public Section {
|
|
public:
|
|
TestTextSection(std::unique_ptr<char[]> data, std::size_t size)
|
|
: Section(".text", std::move(data)), size_(size) {}
|
|
|
|
std::size_t size() const override { return size_; }
|
|
uint32_t sectionHeaderNameIdx() const override { return 0; }
|
|
uint64_t sectionOffset() const override { return 0; }
|
|
|
|
private:
|
|
std::size_t size_;
|
|
};
|
|
|
|
class TestCodeObject : public CodeObject {
|
|
public:
|
|
explicit TestCodeObject(std::vector<uint32_t> words) {
|
|
const auto byte_size = words.size() * sizeof(uint32_t);
|
|
image_.resize(byte_size);
|
|
std::memcpy(image_.data(), words.data(), byte_size);
|
|
|
|
auto data = std::make_unique<char[]>(byte_size);
|
|
std::memcpy(data.get(), words.data(), byte_size);
|
|
sections_.push_back(std::make_unique<TestTextSection>(std::move(data), byte_size));
|
|
text_sections_.push_back(sections_.back().get());
|
|
}
|
|
};
|
|
|
|
enum class TestOpcode : uint32_t {
|
|
Nop = 0,
|
|
End = 1,
|
|
BranchBackToStart = 2,
|
|
CBranchToElse = 3,
|
|
BranchToJoin = 4,
|
|
DefVgpr0 = 5,
|
|
UseVgpr0 = 6,
|
|
UseSgpr4 = 7,
|
|
UseSgpr7 = 8,
|
|
ReadWriteSgpr4 = 9,
|
|
PredicatedDefSgpr4 = 10,
|
|
ImplicitUseSgpr6Pair = 11,
|
|
DefSgpr4 = 12,
|
|
CBranchBackToUseSgpr4 = 13,
|
|
CBranchToElseAfterTwo = 14,
|
|
IndirectCall = 15,
|
|
IndirectBranch = 16,
|
|
WriteExecFull = 17,
|
|
WriteExecNarrow = 18,
|
|
WriteExecFullInline = 19,
|
|
WriteExecOrAllOnes = 20,
|
|
WriteExecAndSaveexec = 21,
|
|
WriteExecLoHalf = 22,
|
|
WriteExecHiHalf = 23,
|
|
WriteExecHiHalfZero = 25,
|
|
PartialDefSgpr4 = 24,
|
|
};
|
|
|
|
class TestDecoder : public Decoder {
|
|
public:
|
|
Instruction *decode(const rj_code_binary_inst_t *inst) override {
|
|
auto op = static_cast<TestOpcode>(*inst);
|
|
switch (op) {
|
|
case TestOpcode::Nop:
|
|
return new TestInstruction("test_nop");
|
|
case TestOpcode::End:
|
|
return new TestInstruction("test_end", {}, {}, PROGRAM_TERMINATOR);
|
|
case TestOpcode::BranchBackToStart:
|
|
return new TestInstruction("test_branch_back", {}, {}, BRANCH, -8);
|
|
case TestOpcode::CBranchToElse:
|
|
return new TestInstruction("test_cbranch_else", {}, {}, COND_BRANCH, 4);
|
|
case TestOpcode::BranchToJoin:
|
|
return new TestInstruction("test_branch_join", {}, {}, BRANCH, 4);
|
|
case TestOpcode::DefVgpr0:
|
|
return new TestInstruction("test_def_v0", {{RegClass::VGPR, 0, 1}});
|
|
case TestOpcode::UseVgpr0:
|
|
return new TestInstruction("test_use_v0", {}, {{RegClass::VGPR, 0, 1}});
|
|
case TestOpcode::UseSgpr4:
|
|
return new TestInstruction("test_use_s4", {}, {{RegClass::SGPR, 4, 1}});
|
|
case TestOpcode::UseSgpr7:
|
|
return new TestInstruction("test_use_s7", {}, {{RegClass::SGPR, 7, 1}});
|
|
case TestOpcode::ReadWriteSgpr4:
|
|
return new TestInstruction("test_rw_s4", {{RegClass::SGPR, 4, 1}}, {{RegClass::SGPR, 4, 1}});
|
|
case TestOpcode::PredicatedDefSgpr4:
|
|
return new TestInstruction("test_pred_def_s4", {{RegClass::SGPR, 4, 1}}, {}, PREDICATED_DEF);
|
|
case TestOpcode::ImplicitUseSgpr6Pair:
|
|
return new TestInstruction("test_implicit_use_s6_pair", {}, {}, 0, std::nullopt,
|
|
{{RegClass::SGPR, 6, 2}});
|
|
case TestOpcode::DefSgpr4:
|
|
return new TestInstruction("test_def_s4", {{RegClass::SGPR, 4, 1}});
|
|
case TestOpcode::CBranchBackToUseSgpr4:
|
|
return new TestInstruction("test_cbranch_back_to_use_s4", {}, {}, COND_BRANCH, -8);
|
|
case TestOpcode::CBranchToElseAfterTwo:
|
|
return new TestInstruction("test_cbranch_else_after_two", {}, {}, COND_BRANCH, 8);
|
|
case TestOpcode::IndirectCall:
|
|
return new TestInstruction("test_indirect_call", {}, {}, INDIRECT_CALL);
|
|
case TestOpcode::IndirectBranch:
|
|
return new TestInstruction("test_indirect_branch", {}, {}, INDIRECT_BRANCH);
|
|
case TestOpcode::WriteExecFull:
|
|
// s_mov exec, <all-ones literal>: COPY of a single all-ones source.
|
|
return new TestInstruction("test_write_exec_full", {{RegClass::EXEC, 0, 2}}, {}, RESULT_COPY,
|
|
std::nullopt, {}, ~0ULL);
|
|
case TestOpcode::WriteExecNarrow:
|
|
// exec <- sgpr (non-constant): writes EXEC but not provably all-ones.
|
|
return new TestInstruction("test_write_exec_narrow", {{RegClass::EXEC, 0, 2}},
|
|
{{RegClass::SGPR, 0, 1}}, RESULT_COPY);
|
|
case TestOpcode::WriteExecFullInline:
|
|
// s_mov exec, -1: COPY of an inline-constant all-ones source (reports
|
|
// const_value() but not literal64_value()).
|
|
return new TestInstruction("test_write_exec_full_inline", {{RegClass::EXEC, 0, 2}}, {},
|
|
RESULT_COPY, std::nullopt, {}, std::nullopt, ~0ULL);
|
|
case TestOpcode::WriteExecOrAllOnes:
|
|
// s_or_b64 exec, exec, -1: OR of a (non-constant) source and an all-ones
|
|
// inline constant -> all-ones regardless of the other operand.
|
|
return new TestInstruction("test_write_exec_or_allones", {{RegClass::EXEC, 0, 2}},
|
|
{{RegClass::SGPR, 0, 1}}, RESULT_OR, std::nullopt, {},
|
|
std::nullopt, ~0ULL);
|
|
case TestOpcode::WriteExecAndSaveexec:
|
|
// s_and_saveexec exec, -1: writes EXEC (flag), single all-ones source, but
|
|
// exec = exec & -1 = exec -> NOT all-ones. No RESULT_* -> must stay Unknown.
|
|
return new TestInstruction("test_write_exec_and_saveexec", {{RegClass::SGPR, 0, 2}}, {},
|
|
WRITES_EXEC, std::nullopt, {}, std::nullopt, ~0ULL);
|
|
case TestOpcode::WriteExecLoHalf:
|
|
// s_mov_b32 exec_lo, -1: a 32-bit COPY of all-ones into only the low half
|
|
// of EXEC. Full on Wave32; a partial write on Wave64.
|
|
return new TestInstruction("test_write_exec_lo_half", {{RegClass::EXEC, 0, 1}}, {},
|
|
RESULT_COPY, std::nullopt, {}, std::nullopt, ~0ULL);
|
|
case TestOpcode::WriteExecHiHalf:
|
|
// s_mov_b32 exec_hi, -1: a 32-bit COPY of all-ones into only the high half
|
|
// of EXEC. Never covers the active lanes, so it can never establish Full.
|
|
return new TestInstruction("test_write_exec_hi_half", {{RegClass::EXEC, 1, 1}}, {},
|
|
RESULT_COPY, std::nullopt, {}, std::nullopt, ~0ULL);
|
|
case TestOpcode::WriteExecHiHalfZero:
|
|
// s_mov_b32 exec_hi, 0: a 32-bit COPY of zero into only the high half of EXEC.
|
|
return new TestInstruction("test_write_exec_hi_half_zero", {{RegClass::EXEC, 1, 1}}, {},
|
|
RESULT_COPY, std::nullopt, {}, std::nullopt, 0ULL);
|
|
case TestOpcode::PartialDefSgpr4:
|
|
// 16-bit write to s4: defines only part of the lane, so it also reads s4.
|
|
return new TestInstruction("test_partial_def_s4", {{RegClass::SGPR, 4, 1}}, {}, 0,
|
|
std::nullopt, {}, std::nullopt, std::nullopt,
|
|
/*def_size_bits=*/16);
|
|
}
|
|
return new TestInstruction("test_end", {}, {}, PROGRAM_TERMINATOR);
|
|
}
|
|
};
|
|
|
|
std::vector<std::unique_ptr<BasicBlock>>
|
|
build_test_blocks(std::vector<TestOpcode> ops, std::span<const uint64_t> extra_leaders = {}) {
|
|
std::vector<uint32_t> words;
|
|
words.reserve(ops.size());
|
|
for (TestOpcode op : ops)
|
|
words.push_back(static_cast<uint32_t>(op));
|
|
|
|
TestCodeObject co(std::move(words));
|
|
TestDecoder decoder;
|
|
return BasicBlock::build(co, decoder, ROCJITSU_CODE_ARCH_CDNA3, extra_leaders);
|
|
}
|
|
|
|
bool has_predecessor(const BasicBlock &block, const BasicBlock *pred) {
|
|
return std::ranges::find(block.predecessors(), pred) != block.predecessors().end();
|
|
}
|
|
|
|
bool has_successor_start(const BasicBlock &block, uint64_t offset) {
|
|
return std::ranges::any_of(block.successors(), [offset](const BasicBlock *succ) {
|
|
return succ != nullptr && succ->start_offset() == offset;
|
|
});
|
|
}
|
|
|
|
BasicBlock *block_starting_at(const std::vector<std::unique_ptr<BasicBlock>> &blocks,
|
|
uint64_t offset) {
|
|
auto it = std::ranges::find_if(blocks, [offset](const auto &block) {
|
|
return block != nullptr && block->start_offset() == offset;
|
|
});
|
|
return it == blocks.end() ? nullptr : it->get();
|
|
}
|
|
|
|
std::vector<BasicBlock *> block_scope(const std::vector<std::unique_ptr<BasicBlock>> &blocks) {
|
|
std::vector<BasicBlock *> scope;
|
|
scope.reserve(blocks.size());
|
|
for (const auto &block : blocks)
|
|
scope.push_back(block.get());
|
|
return scope;
|
|
}
|
|
|
|
/// @brief View the code object's .text bytes for LivenessAnalysisOptions::text.
|
|
/// @details The gfx1250 VGPR_MSB analysis reads S_SETREG_IMM32_B32 literals from
|
|
/// this span (at src_loc()+4), so tests exercising immediate MODE writes must
|
|
/// supply it.
|
|
std::span<const uint8_t> text_span(const CodeObject &co) {
|
|
const Section *text = co.text_sections().front();
|
|
return {reinterpret_cast<const uint8_t *>(text->data()), text->size()};
|
|
}
|
|
|
|
LivenessAnalysis analyze_scope(const std::vector<std::unique_ptr<BasicBlock>> &blocks) {
|
|
auto scope = block_scope(blocks);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
return LivenessAnalysis(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec));
|
|
}
|
|
|
|
// Wire the same scoped call/return edges into BOTH analyses, as BinaryTranslator
|
|
// does; feeding them to only one makes the two disagree around calls and returns.
|
|
LivenessAnalysis analyze_scope_with_edges(const std::vector<std::unique_ptr<BasicBlock>> &blocks,
|
|
std::span<const ScopedCfgEdge> extra_edges) {
|
|
auto scope = block_scope(blocks);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64, extra_edges);
|
|
return LivenessAnalysis(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
/*options=*/{}, extra_edges);
|
|
}
|
|
|
|
// CFG/liveness tests care about decoded register effects, not the physical
|
|
// field layout. Keep their compact fixture syntax while routing construction
|
|
// through the same generated CDNA3 encoders used by production translation.
|
|
uint32_t pack_sopp(uint16_t op, uint16_t simm16) {
|
|
return cdna3::build_sopp(op, {.simm16 = simm16})[0];
|
|
}
|
|
|
|
uint32_t pack_sop1(uint16_t op, uint16_t sdst, uint16_t ssrc0) {
|
|
return cdna3::build_sop1(
|
|
op, {.ssrc0 = static_cast<uint8_t>(ssrc0), .sdst = static_cast<uint8_t>(sdst)})[0];
|
|
}
|
|
|
|
uint32_t pack_sop2(uint16_t op, uint16_t sdst, uint16_t ssrc0, uint16_t ssrc1) {
|
|
return cdna3::build_sop2(op, {.ssrc0 = static_cast<uint8_t>(ssrc0),
|
|
.ssrc1 = static_cast<uint8_t>(ssrc1),
|
|
.sdst = static_cast<uint8_t>(sdst)})[0];
|
|
}
|
|
|
|
uint32_t pack_sopc(uint16_t op, uint16_t ssrc0, uint16_t ssrc1) {
|
|
return cdna3::build_sopc(
|
|
op, {.ssrc0 = static_cast<uint8_t>(ssrc0), .ssrc1 = static_cast<uint8_t>(ssrc1)})[0];
|
|
}
|
|
|
|
uint32_t build_s_call_b64(uint16_t sdst, int16_t simm16) {
|
|
return cdna3::build_sopk(cdna3::kSCallB64Sopk, {.simm16 = static_cast<uint16_t>(simm16),
|
|
.sdst = static_cast<uint8_t>(sdst)})[0];
|
|
}
|
|
|
|
TEST(RegisterSetAnalysis, KeepsRegisterClassesSeparate) {
|
|
RegisterSet set;
|
|
set.expand({RegClass::SGPR, 4, 1});
|
|
|
|
EXPECT_TRUE(set.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_FALSE(set.contains({RegClass::VGPR, 4, 1}));
|
|
EXPECT_FALSE(set.contains({RegClass::ACC_VGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(RegisterSetAnalysis, TracksGfx1250HighBankVectorRegisters) {
|
|
RegisterSet set;
|
|
set.expand({RegClass::VGPR, 768, 2});
|
|
|
|
EXPECT_TRUE(set.contains({RegClass::VGPR, 768, 2}));
|
|
EXPECT_EQ(set.size(), 2u);
|
|
|
|
set.erase({RegClass::VGPR, 769, 1});
|
|
EXPECT_TRUE(set.contains({RegClass::VGPR, 768, 1}));
|
|
EXPECT_FALSE(set.contains({RegClass::VGPR, 769, 1}));
|
|
}
|
|
|
|
template <typename AtomicInst>
|
|
void expect_gfx1250_buffer_cmpswap_def_use(uint8_t return_control, uint8_t payload_width,
|
|
uint8_t return_width) {
|
|
gfx1250::VbufferMachineInst raw{};
|
|
raw.vdata = 4;
|
|
raw.th = return_control;
|
|
AtomicInst inst(reinterpret_cast<const gfx1250::MachineInst *>(&raw));
|
|
|
|
InstDefUse def_use(inst);
|
|
EXPECT_TRUE(def_use.uses.contains({RegClass::VGPR, 4, payload_width}));
|
|
if (return_width == 0) {
|
|
EXPECT_EQ(def_use.defs.size(), 0u);
|
|
} else {
|
|
EXPECT_TRUE(def_use.defs.contains({RegClass::VGPR, 4, return_width}));
|
|
EXPECT_FALSE(def_use.defs.contains({RegClass::VGPR, 4, payload_width}));
|
|
}
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Gfx1250BufferCmpswapReturnUsesElementWidth) {
|
|
constexpr uint8_t kAtomicNoReturn = 0;
|
|
constexpr uint8_t kAtomicReturn = 1;
|
|
|
|
expect_gfx1250_buffer_cmpswap_def_use<gfx1250::BufferAtomicCmpswapB32Vbuffer>(kAtomicReturn, 2,
|
|
1);
|
|
expect_gfx1250_buffer_cmpswap_def_use<gfx1250::BufferAtomicCmpswapB32Vbuffer>(kAtomicNoReturn, 2,
|
|
0);
|
|
expect_gfx1250_buffer_cmpswap_def_use<gfx1250::BufferAtomicCmpswapB64Vbuffer>(kAtomicReturn, 4,
|
|
2);
|
|
expect_gfx1250_buffer_cmpswap_def_use<gfx1250::BufferAtomicCmpswapB64Vbuffer>(kAtomicNoReturn, 4,
|
|
0);
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, MubufCmpswapReturnUsesElementWidthAndTargetGate) {
|
|
cdna3::MubufMachineInst cdna_raw{};
|
|
cdna_raw.vdata = 4;
|
|
cdna_raw.acc = 1;
|
|
for (uint8_t sc0 : {uint8_t{0}, uint8_t{1}}) {
|
|
cdna_raw.sc0 = sc0;
|
|
cdna3::BufferAtomicCmpswapMubuf inst(reinterpret_cast<const cdna3::MachineInst *>(&cdna_raw));
|
|
InstDefUse def_use(inst);
|
|
EXPECT_TRUE(def_use.uses.contains({RegClass::ACC_VGPR, 4, 2}));
|
|
EXPECT_EQ(def_use.defs.contains({RegClass::ACC_VGPR, 4, 1}), sc0 != 0);
|
|
EXPECT_FALSE(def_use.defs.contains({RegClass::ACC_VGPR, 4, 2}));
|
|
}
|
|
|
|
rdna3::MubufMachineInst rdna_raw{};
|
|
rdna_raw.vdata = 8;
|
|
for (uint8_t glc : {uint8_t{0}, uint8_t{1}}) {
|
|
rdna_raw.glc = glc;
|
|
rdna3::BufferAtomicCmpswapB32Mubuf inst(
|
|
reinterpret_cast<const rdna3::MachineInst *>(&rdna_raw));
|
|
InstDefUse def_use(inst);
|
|
EXPECT_TRUE(def_use.uses.contains({RegClass::VGPR, 8, 2}));
|
|
EXPECT_EQ(def_use.defs.contains({RegClass::VGPR, 8, 1}), glc != 0);
|
|
EXPECT_FALSE(def_use.defs.contains({RegClass::VGPR, 8, 2}));
|
|
}
|
|
}
|
|
|
|
TEST(RegisterSetAnalysis, IgnoresSpecialRegisterClasses) {
|
|
RegisterSet set;
|
|
set.expand({RegClass::EXEC, 0, 2});
|
|
set.expand({RegClass::SCC, 0, 1});
|
|
set.expand({RegClass::FLAT_SCRATCH, 0, 2});
|
|
|
|
EXPECT_TRUE(set.none());
|
|
EXPECT_FALSE(set.contains({RegClass::EXEC, 0, 1}));
|
|
EXPECT_FALSE(set.contains({RegClass::SCC, 0, 1}));
|
|
EXPECT_FALSE(set.contains({RegClass::FLAT_SCRATCH, 0, 2}));
|
|
}
|
|
|
|
TEST(RegisterSetAnalysis, GeneratedCdna4OperandsMapTrackedRegisterRefs) {
|
|
cdna4::Operand sgpr(32, cdna4::OperandType::OPR_SRC, cdna4::OpSelSrc::OPR_SRC_SGPR_MIN + 7);
|
|
cdna4::Operand vgpr(32, cdna4::OperandType::OPR_SRC, cdna4::OpSelSrc::OPR_SRC_VGPR_MIN + 7);
|
|
cdna4::Operand acc(32, cdna4::OperandType::OPR_SRC_ACCVGPR,
|
|
cdna4::OpSelSrcAccvgpr::OPR_SRC_ACCVGPR_ACC_MIN + 7);
|
|
cdna4::Operand imm32(32, cdna4::OperandType::OPR_SIMM32, 123);
|
|
|
|
ASSERT_TRUE(sgpr.to_register_ref().has_value());
|
|
EXPECT_EQ(*sgpr.to_register_ref(), (RegisterRef{RegClass::SGPR, 7, 1}));
|
|
ASSERT_TRUE(vgpr.to_register_ref().has_value());
|
|
EXPECT_EQ(*vgpr.to_register_ref(), (RegisterRef{RegClass::VGPR, 7, 1}));
|
|
ASSERT_TRUE(acc.to_register_ref().has_value());
|
|
EXPECT_EQ(*acc.to_register_ref(), (RegisterRef{RegClass::ACC_VGPR, 7, 1}));
|
|
EXPECT_FALSE(imm32.to_register_ref().has_value());
|
|
}
|
|
|
|
TEST(RegisterSetAnalysis, Cdna4WritelaneDestinationIsUseAndDef) {
|
|
constexpr std::array<uint32_t, 2> kWritelaneV141S4Lane2 = {0xd28a008du, 0x00010404u};
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
std::unique_ptr<Instruction> inst(decoder->decode(kWritelaneV141S4Lane2.data()));
|
|
ASSERT_NE(inst, nullptr);
|
|
EXPECT_EQ(inst->mnemonic(), "v_writelane_b32");
|
|
|
|
InstDefUse du(*inst);
|
|
EXPECT_TRUE(du.defs.contains({RegClass::VGPR, 141, 1}));
|
|
EXPECT_TRUE(du.uses.contains({RegClass::VGPR, 141, 1}));
|
|
EXPECT_TRUE(du.uses.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(CfgAnalysis, LoopBackEdgeLinksPredecessor) {
|
|
auto blocks = build_test_blocks({TestOpcode::Nop, TestOpcode::BranchBackToStart});
|
|
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
ASSERT_EQ(blocks[0]->successors().size(), 1u);
|
|
EXPECT_EQ(blocks[0]->successors()[0], blocks[0].get());
|
|
EXPECT_TRUE(has_predecessor(*blocks[0], blocks[0].get()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250ClassifiesImplicitUnreachableStubTerminator) {
|
|
struct Case {
|
|
const char *name;
|
|
std::vector<uint32_t> words;
|
|
bool has_terminator;
|
|
bool has_implicit_terminator;
|
|
bool falls_through_to_undecodable_text;
|
|
};
|
|
const std::array cases = {
|
|
Case{"clang unreachable stub", {0xb9800641u, 1u, 0}, true, true, false},
|
|
Case{"clang unreachable stub with prefetch",
|
|
{0xee174000u, 0x00040000u, 0, 0x7e000000u, 0xb9800641u, 1u, 0},
|
|
true,
|
|
true,
|
|
false},
|
|
Case{"section-final clang unreachable stub", {0xb9800641u, 1u}, true, true, false},
|
|
Case{"ordinary fallthrough",
|
|
{build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), 0},
|
|
false,
|
|
false,
|
|
true},
|
|
Case{"architectural terminator",
|
|
{build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), 0},
|
|
true,
|
|
false,
|
|
false},
|
|
};
|
|
|
|
for (const auto &test_case : cases) {
|
|
SCOPED_TRACE(test_case.name);
|
|
TestCodeObject co(test_case.words);
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
EXPECT_EQ(blocks[0]->has_terminator(), test_case.has_terminator);
|
|
EXPECT_EQ(blocks[0]->has_implicit_terminator(), test_case.has_implicit_terminator);
|
|
EXPECT_EQ(blocks[0]->falls_through_to_undecodable_text(),
|
|
test_case.falls_through_to_undecodable_text);
|
|
}
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallToImplicitNonreturningTargetDropsFallthrough) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
std::vector<uint32_t> words = {
|
|
rocjitsu::build_s_call_b64(kReturnSreg, 1, ROCJITSU_CODE_ARCH_GFX1250),
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x04 continuation.
|
|
0xb9800641u,
|
|
1u,
|
|
0, // 0x08 clang unreachable-stub target followed by padding.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *target = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
EXPECT_TRUE(caller->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*caller, target->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_FALSE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, PreviousInstructionReturnsPrecedingInstructionInBlock) {
|
|
auto blocks = build_test_blocks({TestOpcode::Nop, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto instruction = blocks[0]->instructions().begin();
|
|
ASSERT_NE(instruction, blocks[0]->instructions().end());
|
|
const Instruction *first = &*instruction;
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks[0]->instructions().end());
|
|
const Instruction *second = &*instruction;
|
|
|
|
EXPECT_EQ(first->previous_instruction(), nullptr);
|
|
EXPECT_EQ(second->previous_instruction(), first);
|
|
}
|
|
|
|
TEST(CfgAnalysis, PreviousInstructionIsNullAtBranchTargetBlockEntry) {
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::CBranchToElse, TestOpcode::Nop, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
|
|
BasicBlock *target = block_starting_at(blocks, 8);
|
|
ASSERT_NE(target, nullptr);
|
|
ASSERT_NE(target->instructions().begin(), target->instructions().end());
|
|
const Instruction &entry = *target->instructions().begin();
|
|
EXPECT_EQ(entry.previous_instruction(), nullptr);
|
|
}
|
|
|
|
TEST(CfgAnalysis, NextInstructionReturnsFollowingInstructionInBlock) {
|
|
auto blocks = build_test_blocks({TestOpcode::Nop, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto instruction = blocks[0]->instructions().begin();
|
|
ASSERT_NE(instruction, blocks[0]->instructions().end());
|
|
const Instruction *first = &*instruction;
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks[0]->instructions().end());
|
|
const Instruction *second = &*instruction;
|
|
|
|
EXPECT_EQ(first->next_instruction(), second);
|
|
}
|
|
|
|
TEST(CfgAnalysis, NextInstructionIsNullAtBlockTerminator) {
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::CBranchToElse, TestOpcode::Nop, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
|
|
ASSERT_FALSE(blocks.empty());
|
|
const Instruction *terminator = blocks[0]->terminator();
|
|
ASSERT_NE(terminator, nullptr);
|
|
EXPECT_EQ(terminator->next_instruction(), nullptr);
|
|
}
|
|
|
|
TEST(CfgAnalysis, StandaloneInstructionHasNoDecodedNeighbors) {
|
|
constexpr uint32_t kNop = 0xbf800000u;
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA3);
|
|
ASSERT_NE(decoder, nullptr);
|
|
std::unique_ptr<Instruction> instruction(decoder->decode(&kNop));
|
|
|
|
ASSERT_NE(instruction, nullptr);
|
|
EXPECT_EQ(instruction->previous_instruction(), nullptr);
|
|
EXPECT_EQ(instruction->next_instruction(), nullptr);
|
|
}
|
|
|
|
TEST(CfgAnalysis, IfElseSuccessorsAndPredecessorsAreInverse) {
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::CBranchToElse, TestOpcode::BranchToJoin, TestOpcode::Nop, TestOpcode::End});
|
|
|
|
ASSERT_EQ(blocks.size(), 4u);
|
|
auto *entry = blocks[0].get();
|
|
auto *then_block = blocks[1].get();
|
|
auto *else_block = blocks[2].get();
|
|
auto *join = blocks[3].get();
|
|
|
|
ASSERT_EQ(entry->successors().size(), 2u);
|
|
EXPECT_EQ(entry->successors()[0], else_block);
|
|
EXPECT_EQ(entry->successors()[1], then_block);
|
|
ASSERT_EQ(then_block->successors().size(), 1u);
|
|
EXPECT_EQ(then_block->successors()[0], join);
|
|
ASSERT_EQ(else_block->successors().size(), 1u);
|
|
EXPECT_EQ(else_block->successors()[0], join);
|
|
|
|
EXPECT_TRUE(has_predecessor(*then_block, entry));
|
|
EXPECT_TRUE(has_predecessor(*else_block, entry));
|
|
EXPECT_TRUE(has_predecessor(*join, then_block));
|
|
EXPECT_TRUE(has_predecessor(*join, else_block));
|
|
}
|
|
|
|
TEST(CfgAnalysis, ExtraLeaderSplitsBlockAtKernelEntry) {
|
|
std::array<uint64_t, 1> kernel_entries{8};
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::Nop, TestOpcode::Nop, TestOpcode::UseSgpr4, TestOpcode::End}, kernel_entries);
|
|
|
|
ASSERT_EQ(blocks.size(), 2u);
|
|
ASSERT_EQ(blocks[0]->start_offset(), 0u);
|
|
ASSERT_EQ(blocks[0]->end_offset(), 8u);
|
|
ASSERT_EQ(blocks[1]->start_offset(), 8u);
|
|
ASSERT_EQ(blocks[0]->successors().size(), 1u);
|
|
EXPECT_EQ(blocks[0]->successors()[0], blocks[1].get());
|
|
EXPECT_TRUE(has_predecessor(*blocks[1], blocks[0].get()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, IndirectCallFallsThroughToReturnSuccessor) {
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::IndirectCall, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
|
|
ASSERT_EQ(blocks.size(), 2u);
|
|
ASSERT_EQ(blocks[0]->successors().size(), 1u);
|
|
EXPECT_EQ(blocks[0]->successors()[0], blocks[1].get());
|
|
EXPECT_TRUE(has_predecessor(*blocks[1], blocks[0].get()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, IndirectBranchHasNoStaticSuccessor) {
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::IndirectBranch, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
|
|
ASSERT_EQ(blocks.size(), 2u);
|
|
EXPECT_TRUE(blocks[0]->successors().empty());
|
|
EXPECT_TRUE(blocks[1]->predecessors().empty());
|
|
}
|
|
|
|
TEST(CfgAnalysis, IndirectRecoveryPrefilterAdmitsSetPcConsumer) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// The PC builder and setpc consumer are deliberately separated by an extra
|
|
// leader. The recovered CFG edge belongs to the setpc block, because that is
|
|
// where control flow actually leaves the straight-line path.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
20, // 0x08: target delta.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x10: s_setpc_b64.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x14: not a successor.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x18: recovered target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 1> extra_leaders{16};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, extra_leaders);
|
|
|
|
auto *builder = block_starting_at(blocks, 0);
|
|
auto *consumer = block_starting_at(blocks, 16);
|
|
auto *fallthrough = block_starting_at(blocks, 20);
|
|
auto *target = block_starting_at(blocks, 24);
|
|
ASSERT_NE(builder, nullptr);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_NE(fallthrough, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
EXPECT_TRUE(builder->static_indirect_call_fixups().empty());
|
|
ASSERT_EQ(builder->successors().size(), 1u);
|
|
EXPECT_EQ(builder->successors()[0], consumer);
|
|
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u)
|
|
<< "setpc consumer must pass the indirect-recovery prefilter";
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_call_offset, 16u);
|
|
ASSERT_EQ(consumer->successors().size(), 1u);
|
|
EXPECT_EQ(consumer->successors()[0], target);
|
|
EXPECT_FALSE(has_predecessor(*fallthrough, consumer));
|
|
}
|
|
|
|
TEST(CfgAnalysis, PcBuilderWithoutConsumerProducesNoRecoveredEdge) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // s_add_u32.
|
|
4, // Target delta.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // s_addc_u32.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4),
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
EXPECT_TRUE(blocks[0]->static_indirect_call_fixups().empty());
|
|
}
|
|
|
|
TEST(CfgAnalysis, RecoversMultipleSgprPairsFromOneBlockEntry) {
|
|
constexpr uint16_t kFirstPcSreg = 8;
|
|
constexpr uint16_t kSecondPcSreg = 20;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Both PC builders reach both successor blocks. The two pending consumers
|
|
// exercise lookup of distinct keys in the same sorted block-entry fact set.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kFirstPcSreg, 0), // 0x00: first getpc.
|
|
pack_sop2(0, kFirstPcSreg, kFirstPcSreg, kLiteralOperand), // 0x04: first add.
|
|
44, // 0x08: -> 0x30.
|
|
pack_sop2(4, kFirstPcSreg + 1, kFirstPcSreg + 1, kInlineInt0), // 0x0c.
|
|
pack_sop1(0x1c, kSecondPcSreg, 0), // 0x10: second getpc.
|
|
pack_sop2(0, kSecondPcSreg, kSecondPcSreg, kLiteralOperand), // 0x14: second add.
|
|
32, // 0x18: -> 0x34.
|
|
pack_sop2(4, kSecondPcSreg + 1, kSecondPcSreg + 1, kInlineInt0), // 0x1c.
|
|
pack_sopp(5, 1), // 0x20: -> 0x28.
|
|
pack_sop1(0x1d, 0, kFirstPcSreg), // 0x24: first consumer.
|
|
pack_sop1(0x1d, 0, kSecondPcSreg), // 0x28: second consumer.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x2c.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x30: first target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x34: second target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *first_consumer = block_starting_at(blocks, 36);
|
|
auto *second_consumer = block_starting_at(blocks, 40);
|
|
ASSERT_NE(first_consumer, nullptr);
|
|
ASSERT_NE(second_consumer, nullptr);
|
|
ASSERT_EQ(first_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(first_consumer->static_indirect_call_fixups()[0].source_target_offset, 48u);
|
|
ASSERT_EQ(second_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(second_consumer->static_indirect_call_fixups()[0].source_target_offset, 52u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, OutOfRangeIndirectConsumersRemainUnresolved) {
|
|
constexpr uint16_t kOutOfRangeSelector = 106;
|
|
const std::array<uint32_t, 2> consumers = {
|
|
pack_sop1(0x1d, 0, kOutOfRangeSelector), // s_setpc_b64 vcc.
|
|
pack_sop1(0x1e, 30, kOutOfRangeSelector), // s_swappc_b64 s[30:31], vcc.
|
|
};
|
|
|
|
for (uint32_t consumer : consumers) {
|
|
TestCodeObject co(std::vector<uint32_t>{consumer});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
EXPECT_TRUE(blocks[0]->static_indirect_call_fixups().empty());
|
|
}
|
|
}
|
|
|
|
TEST(CfgAnalysis, IgnoresUnconsumedPairWhileRecoveringPendingConsumer) {
|
|
constexpr uint16_t kUnusedPcSreg = 8;
|
|
constexpr uint16_t kUsedPcSreg = 20;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Both builders reach the consumer block, but only s[20:21] is consumed.
|
|
// The relevance filter must drop the s[8:9] transfer without disturbing the
|
|
// pending consumer's fact.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kUnusedPcSreg, 0), // 0x00: unused getpc.
|
|
pack_sop1(0x1c, kUsedPcSreg, 0), // 0x04: used getpc.
|
|
pack_sop2(0, kUsedPcSreg, kUsedPcSreg, kLiteralOperand), // 0x08: used add.
|
|
20, // 0x0c: -> 0x1c.
|
|
pack_sop2(4, kUsedPcSreg + 1, kUsedPcSreg + 1, kInlineInt0), // 0x10.
|
|
pack_sop1(0x1d, 0, kUsedPcSreg), // 0x14: consumer.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x18.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x1c: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 1> extra_leaders{20};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, extra_leaders);
|
|
|
|
auto *consumer = block_starting_at(blocks, 20);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 28u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, IncompleteFactConsumerIsFlaggedIncomplete) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Two paths reach one setpc consumer:
|
|
// * the builder path materializes a concrete PC in s[8:9]
|
|
// * the bypass path does nothing to the pair, so it arrives at its
|
|
// unconstrained kernel-entry value
|
|
// The joined fact is therefore INCOMPLETE with one concrete target. Recovery
|
|
// still records that target (for relocation/liveness) but must flag it
|
|
// incomplete, so the translator does not replace the dynamic consumer with a
|
|
// direct window that would redirect the bypass path.
|
|
std::vector<uint32_t> words = {
|
|
pack_sopp(5, 5), // 0x00: cbranch scc0 -> bypass at 0x18.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x04: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x08: s_add_u32.
|
|
28, // 0x0c: target delta -> 0x08 + 28 = 0x24.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x10: s_addc_u32.
|
|
build_s_branch(1, ROCJITSU_CODE_ARCH_CDNA4), // 0x14 -> consumer at 0x1c.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x18: bypass (leaves pair unconstrained).
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x1c: joined consumer setpc.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x20: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x24: builder target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 28);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
const auto &fixup = consumer->static_indirect_call_fixups()[0];
|
|
EXPECT_EQ(fixup.source_target_offset, 36u);
|
|
EXPECT_TRUE(fixup.source_incomplete)
|
|
<< "a consumer joined from an unconstrained path must be flagged incomplete";
|
|
}
|
|
|
|
TEST(CfgAnalysis, IncompleteSwappcTargetSetKeepsContinuation) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
std::vector<uint32_t> words = {
|
|
pack_sopp(5, 5), // 0x00 -> bypass at 0x18.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x04: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x08: s_add_u32.
|
|
28, // 0x0c: target at 0x24.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x10: s_addc_u32.
|
|
build_s_branch(1, ROCJITSU_CODE_ARCH_CDNA4), // 0x14 -> consumer at 0x1c.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x18: unconstrained bypass.
|
|
pack_sop1(0x1e, kReturnSreg, kPcSreg), // 0x1c: joined swappc consumer.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x20: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x24: known non-returning target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 28);
|
|
auto *continuation = block_starting_at(blocks, 32);
|
|
auto *target = block_starting_at(blocks, 36);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_TRUE(consumer->static_indirect_call_fixups()[0].source_incomplete);
|
|
EXPECT_TRUE(consumer->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*consumer, continuation->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*consumer, target->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, consumer));
|
|
}
|
|
|
|
TEST(CfgAnalysis, IncompleteRecoveredSetpcInCalleeKeepsOuterContinuation) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kOuterReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kOuterReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 live continuation.
|
|
pack_sopp(5, 5), // 0x08 -> bypass at 0x20.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x0c: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x10: s_add_u32.
|
|
28, // 0x14: target at 0x2c.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x18: s_addc_u32.
|
|
build_s_branch(1, ROCJITSU_CODE_ARCH_CDNA4), // 0x1c -> consumer at 0x24.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x20: unconstrained bypass.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x24: joined setpc.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x28: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x2c: known target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *callee = block_starting_at(blocks, 8);
|
|
auto *consumer = block_starting_at(blocks, 36);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_TRUE(consumer->static_indirect_call_fixups()[0].source_incomplete);
|
|
|
|
EXPECT_TRUE(caller->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*caller, callee->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, ReportsResolvedPcAddressBuilderForEveryProducer) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Recovered consumers are only one use of a getpc builder. DBT also needs the
|
|
// producer itself so it can prove a whole kernel scope holds no unrelocated
|
|
// PC-derived value, so every builder is reported with the exact byte range
|
|
// whose delta relocation may rewrite.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
16, // 0x08: 0x04 + 16 = 0x14.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x10: consumer setpc.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x14: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *builder_block = block_starting_at(blocks, 0);
|
|
ASSERT_NE(builder_block, nullptr);
|
|
ASSERT_EQ(builder_block->static_pc_address_builders().size(), 1u);
|
|
const auto &builder = builder_block->static_pc_address_builders()[0];
|
|
EXPECT_TRUE(builder.resolved);
|
|
EXPECT_TRUE(builder.contiguous);
|
|
EXPECT_EQ(builder.source_getpc_offset, 0u);
|
|
EXPECT_EQ(builder.source_recovery_begin_offset, 4u);
|
|
EXPECT_EQ(builder.source_recovery_end_offset, 16u);
|
|
EXPECT_EQ(builder.source_target_offset, 20);
|
|
EXPECT_EQ(builder.source_sreg, kPcSreg);
|
|
}
|
|
|
|
TEST(CfgAnalysis, PcAddressBuilderWithGapInstructionIsReportedNonContiguous) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kUnrelatedSreg = 20;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// An unrelated s_mov_b32 sits between the low add and the high carry. The pass
|
|
// still tracks the pair across it (the move writes s20, not the pair), so the
|
|
// builder's recorded value is known and its recovery range spans the move. The
|
|
// relocation patcher NOPs that whole range, so rewriting it would erase the
|
|
// move. The producer must therefore be reported non-contiguous even though its
|
|
// final value resolved, so the whole-scope proof declines to rewrite it.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
20, // 0x08: 0x04 + 20 = 0x18.
|
|
build_s_mov_b32(kUnrelatedSreg, 0,
|
|
ROCJITSU_CODE_ARCH_CDNA4), // 0x0c: unrelated write, in range.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x10: s_addc_u32.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x14: consumer setpc.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x18: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *builder_block = block_starting_at(blocks, 0);
|
|
ASSERT_NE(builder_block, nullptr);
|
|
ASSERT_EQ(builder_block->static_pc_address_builders().size(), 1u);
|
|
const auto &builder = builder_block->static_pc_address_builders()[0];
|
|
EXPECT_TRUE(builder.resolved);
|
|
EXPECT_FALSE(builder.contiguous)
|
|
<< "a builder range spanning an unrelated instruction must be non-contiguous";
|
|
}
|
|
|
|
TEST(CfgAnalysis, UnfollowedPcAddressBuilderIsReportedUnresolved) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kAddendSreg = 12;
|
|
|
|
// The low-half add takes a register addend the pass does not model, so the
|
|
// pair's final value is unknown. The producer still exists and still yields a
|
|
// PC-derived value at run time, so it must be reported as unresolved rather
|
|
// than omitted: omitting it would let a caller conclude the scope has no
|
|
// unrelocatable PC producer.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kAddendSreg), // 0x04: s_add_u32 with register addend.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x08: consumer setpc.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x0c.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *builder_block = block_starting_at(blocks, 0);
|
|
ASSERT_NE(builder_block, nullptr);
|
|
ASSERT_EQ(builder_block->static_pc_address_builders().size(), 1u);
|
|
EXPECT_EQ(builder_block->static_pc_address_builders()[0].source_getpc_offset, 0u);
|
|
EXPECT_FALSE(builder_block->static_pc_address_builders()[0].resolved)
|
|
<< "a producer the pass cannot follow must not be reported as relocatable";
|
|
EXPECT_TRUE(builder_block->static_indirect_call_fixups().empty());
|
|
}
|
|
|
|
TEST(CfgAnalysis, DominatedPcBuilderRemainsCompleteAcrossCallLoopBackedge) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// RCCL builds one helper address before a loop and reuses it for every
|
|
// s_swappc iteration. The consumer has both the builder entry edge and a loop
|
|
// backedge. During the first dataflow visit the backedge is still unreachable
|
|
// (BOTTOM), not an independent path with unconstrained s[8:9]. Once the loop
|
|
// becomes reachable it carries the same preserved builder back to the call.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
32, // 0x08: 0x04 + 32 = target 0x24.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
build_s_branch(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x10 -> call at 0x14.
|
|
pack_sop1(0x1e, kReturnSreg, kPcSreg), // 0x14: s_swappc_b64.
|
|
build_s_branch(-2, ROCJITSU_CODE_ARCH_CDNA4), // 0x18 -> call at 0x14.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x1c: not a target.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x20: padding.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x24: helper return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 20);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
const auto &fixup = consumer->static_indirect_call_fixups()[0];
|
|
EXPECT_EQ(fixup.source_target_offset, 36u);
|
|
EXPECT_FALSE(fixup.source_incomplete)
|
|
<< "an unreachable initial backedge must not poison a dominated PC builder";
|
|
}
|
|
|
|
TEST(CfgAnalysis, SeedsTextEntryWithLoopBackedgeForCrossBlockPcBuilder) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// The two-block loop gives block zero a structural predecessor:
|
|
//
|
|
// entry/builder -> loop latch --backedge--> entry/builder
|
|
// |
|
|
// +--fallthrough--> setpc consumer
|
|
//
|
|
// Block zero is still the external text entry and must seed reachability. Its
|
|
// local SET transfer overwrites the unconstrained external value before the
|
|
// cross-block consumer, so the recovered target remains complete.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
24, // 0x08: 0x04 + 24 = 0x1c.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
build_s_branch(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x10 -> latch at 0x14.
|
|
pack_sopp(5, static_cast<uint16_t>(-6)), // 0x14 -> entry at 0x00.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x18: cross-block consumer.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x1c: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 24);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 28u);
|
|
EXPECT_FALSE(consumer->static_indirect_call_fixups()[0].source_incomplete);
|
|
}
|
|
|
|
TEST(CfgAnalysis, MultipleUnorderedExplicitEntriesMakeIncomingPcBuilderIncomplete) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Entry A builds a static target and branches into entries B and C. Both are
|
|
// separately launchable kernels, so their externally supplied s[8:9] values
|
|
// must participate in the joins with A's concrete builder. Supply the entry
|
|
// offsets out of order with a duplicate to exercise the ordered merge.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
32, // 0x08: 0x04 + 32 = 0x24.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
pack_sopp(5, 2), // 0x10: cbranch -> entry C at 0x1c.
|
|
build_s_branch(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x14 -> entry B at 0x18.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x18: entry B setpc.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x1c: entry C setpc.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x20: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x24: A's target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 3> extra_leaders{28, 24, 28};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, extra_leaders);
|
|
|
|
for (uint64_t consumer_offset : {uint64_t{24}, uint64_t{28}}) {
|
|
auto *consumer = block_starting_at(blocks, consumer_offset);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 36u);
|
|
EXPECT_TRUE(consumer->static_indirect_call_fixups()[0].source_incomplete)
|
|
<< "each independently launchable entry must include unconstrained external SGPR state";
|
|
}
|
|
}
|
|
|
|
TEST(CfgAnalysis, RocrAbortTrapStopsTemporaryPcBuilderCfg) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// The post-trap setpc is an independent entry (for example, another kernel or
|
|
// device function). The temporary discovery CFG must apply the same ROCr
|
|
// non-returning trap-2 rule as final BasicBlock construction; otherwise the
|
|
// pre-trap builder spuriously flows into this consumer.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
20, // 0x08: would target 0x18.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
build_s_trap(ROCJITSU_CODE_ARCH_CDNA4, 2), // 0x10: abort terminator.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x14: independent consumer.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x18: would-be target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 1> extra_leaders{20};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, extra_leaders);
|
|
|
|
auto *consumer = block_starting_at(blocks, 20);
|
|
ASSERT_NE(consumer, nullptr);
|
|
EXPECT_TRUE(consumer->static_indirect_call_fixups().empty());
|
|
}
|
|
|
|
TEST(CfgAnalysis, UnreachablePostRocrAbortBlockDoesNotPoisonPcBuilder) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Model the generated complex-math kernels from the offline corpus:
|
|
//
|
|
// builder --conditional--------------------------> call
|
|
// |
|
|
// +--> s_trap 2 -X-> dead branch --^
|
|
//
|
|
// ROCr's trap 2 aborts instead of returning, so the block after it has no
|
|
// predecessor. It is not an implicit external entry: DBT supplies every
|
|
// hardware-visible kernel entry explicitly, builds a reachable scope from
|
|
// each one, and duplicates shared reachable blocks per kernel scope. Treating
|
|
// the dead block as an external root would invent an unconstrained s[8:9]
|
|
// path into the call and make this otherwise dominated builder incomplete.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
40, // 0x08: 0x04 + 40 = helper at 0x2c.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
pack_sopp(5, 3), // 0x10: s_cbranch_scc0 -> 0x20.
|
|
build_s_trap(ROCJITSU_CODE_ARCH_CDNA4, 2), // 0x14: abort terminator.
|
|
build_s_branch(1, ROCJITSU_CODE_ARCH_CDNA4), // 0x18: dead edge -> 0x20.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x1c: dead padding.
|
|
pack_sop1(0x1e, kReturnSreg, kPcSreg), // 0x20: s_swappc_b64.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x24: call continuation.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x28: padding.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x2c: helper return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, {},
|
|
ExternalEntryPolicy::ExplicitOnly);
|
|
|
|
auto *consumer = block_starting_at(blocks, 32);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
const auto &fixup = consumer->static_indirect_call_fixups()[0];
|
|
EXPECT_EQ(fixup.source_target_offset, 44u);
|
|
EXPECT_FALSE(fixup.source_incomplete)
|
|
<< "unreachable code after a non-returning trap must remain dataflow BOTTOM";
|
|
}
|
|
|
|
TEST(CfgAnalysis, DefaultEntryPolicyRecoversPredecessorlessFunction) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
// Generic BasicBlock::build callers may not know every function entry in a
|
|
// shared .text section. Preserve the conservative default that treats the
|
|
// second function as an inferred external entry, allowing its cross-block
|
|
// PC builder to reach the setpc consumer.
|
|
std::vector<uint32_t> words = {
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x00: first function.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x04: second entry, s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x08: s_add_u32.
|
|
24, // 0x0c: 0x08 + 24 = target 0x20.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x10: s_addc_u32.
|
|
build_s_branch(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x14: -> consumer at 0x18.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x18: s_setpc_b64.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x1c: padding.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x20: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 24);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 32u);
|
|
EXPECT_FALSE(consumer->static_indirect_call_fixups()[0].source_incomplete);
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallEdgeUsesTerminatorOffset) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
|
|
// The call block starts at 0x00, but the s_call_b64 terminator is at 0x04.
|
|
// CallEdge metadata is consumed later by relocation and must identify the
|
|
// actual call instruction, not the first instruction in the containing block.
|
|
std::vector<uint32_t> words = {
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x00.
|
|
build_s_call_b64(kReturnSreg, 1), // 0x04 -> callee at 0x0c.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x08 continuation.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x0c callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 8);
|
|
auto *callee = block_starting_at(blocks, 12);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
|
|
ASSERT_EQ(caller->call_edges().size(), 1u);
|
|
const BasicBlock::CallEdge &edge = caller->call_edges()[0];
|
|
EXPECT_EQ(edge.kind, BasicBlock::CallEdgeKind::DirectCall);
|
|
EXPECT_EQ(edge.callee, callee);
|
|
EXPECT_EQ(edge.continuation, continuation);
|
|
EXPECT_EQ(edge.source_call_offset, 4u);
|
|
EXPECT_TRUE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(BinaryTranslatorInternal, ScopeRootsRejectRelocationTableCallee) {
|
|
// A returning direct call makes the callee at 0x0c a call-edge target with no
|
|
// ordinary in-scope predecessor, i.e. an external root that the whole-scope
|
|
// stale-PC proof must classify. The caller (0x00) is the kernel entry.
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 continuation.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x08 callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *callee = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
ASSERT_EQ(caller->call_edges().size(), 1u);
|
|
ASSERT_EQ(caller->call_edges()[0].callee, callee);
|
|
|
|
const auto scope = block_scope(blocks);
|
|
const std::unordered_set<uint64_t> hardware_entries{caller->start_offset()};
|
|
|
|
// With no relocation-table roots, the callee is a getpc-recovered call target
|
|
// and the entry is a hardware root, so the scope is accepted.
|
|
EXPECT_TRUE(rocjitsu::internal::scope_roots_are_entry_state(scope, hardware_entries, {}));
|
|
|
|
// Marking the callee a relocation-table dispatch target makes it an
|
|
// unconstrained root: a dispatched callee receives arbitrary caller-supplied
|
|
// SGPR arguments, so the gate must fail closed even though it has a CallEdge.
|
|
const std::unordered_set<uint64_t> table_callees{callee->start_offset()};
|
|
EXPECT_FALSE(
|
|
rocjitsu::internal::scope_roots_are_entry_state(scope, hardware_entries, table_callees));
|
|
|
|
// A non-hardware, non-call, non-table external root is also rejected: drop the
|
|
// entry from the hardware set and the caller itself becomes unconstrained.
|
|
EXPECT_FALSE(rocjitsu::internal::scope_roots_are_entry_state(scope, {}, {}));
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallToNonreturningTargetDropsFallthrough) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 1), // 0x00 -> target at 0x08.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x04 unreachable padding.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x08 non-returning target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *target = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
EXPECT_TRUE(caller->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*caller, target->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_FALSE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallWithCopiedReturnPairKeepsFallthrough) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint16_t kCopiedReturnSreg = 34;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 live continuation.
|
|
pack_sop1(1, kCopiedReturnSreg, kReturnSreg), // 0x08: s_mov_b64.
|
|
pack_sop1(0x1d, 0, kCopiedReturnSreg), // 0x0c: copied-pair return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *callee = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
|
|
EXPECT_TRUE(caller->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*caller, callee->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallWithUnrecoveredTailExitKeepsFallthrough) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint16_t kUnknownTargetSreg = 0;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 live continuation.
|
|
pack_sop1(0x1d, 0, kUnknownTargetSreg), // 0x08: unrecovered tail exit.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *callee = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
|
|
EXPECT_TRUE(caller->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*caller, callee->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallCrossingScopeBoundaryKeepsFallthrough) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 live continuation.
|
|
build_s_branch(1, ROCJITSU_CODE_ARCH_CDNA4), // 0x08 -> leader at 0x10.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x0c skipped.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x10: return across boundary.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 1> extra_leaders{16};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, extra_leaders);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *callee = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
|
|
EXPECT_TRUE(caller->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*caller, callee->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, NestedReturningCallMayReturnThroughOuterPair) {
|
|
constexpr uint16_t kOuterReturnSreg = 30;
|
|
constexpr uint16_t kInnerReturnSreg = 28;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kOuterReturnSreg, 1), // 0x00 -> outer callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 outer continuation.
|
|
build_s_call_b64(kInnerReturnSreg, 1), // 0x08 -> inner callee at 0x10.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x0c inner continuation.
|
|
pack_sopp(5, 1), // 0x10 -> outer return at 0x18.
|
|
pack_sop1(0x1d, 0, kInnerReturnSreg), // 0x14: normal inner return.
|
|
pack_sop1(0x1d, 0, kOuterReturnSreg), // 0x18: direct outer return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *outer = block_starting_at(blocks, 0);
|
|
auto *outer_continuation = block_starting_at(blocks, 4);
|
|
ASSERT_NE(outer, nullptr);
|
|
ASSERT_NE(outer_continuation, nullptr);
|
|
|
|
ASSERT_EQ(outer->call_edges().size(), 1u);
|
|
EXPECT_EQ(outer->call_edges()[0].continuation, outer_continuation);
|
|
EXPECT_TRUE(has_successor_start(*outer, outer_continuation->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*outer_continuation, outer));
|
|
}
|
|
|
|
TEST(CfgAnalysis, NestedNonreturningCallsDropBothFallthroughs) {
|
|
constexpr uint16_t kOuterReturnSreg = 30;
|
|
constexpr uint16_t kInnerReturnSreg = 28;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kOuterReturnSreg, 1), // 0x00 -> outer callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 outer continuation.
|
|
build_s_call_b64(kInnerReturnSreg, 1), // 0x08 -> inner target at 0x10.
|
|
pack_sop1(0x1d, 0, kOuterReturnSreg), // 0x0c dead inner continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x10 non-returning target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *outer = block_starting_at(blocks, 0);
|
|
auto *outer_continuation = block_starting_at(blocks, 4);
|
|
auto *inner = block_starting_at(blocks, 8);
|
|
auto *inner_continuation = block_starting_at(blocks, 12);
|
|
auto *target = block_starting_at(blocks, 16);
|
|
ASSERT_NE(outer, nullptr);
|
|
ASSERT_NE(outer_continuation, nullptr);
|
|
ASSERT_NE(inner, nullptr);
|
|
ASSERT_NE(inner_continuation, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
EXPECT_TRUE(outer->call_edges().empty());
|
|
EXPECT_TRUE(inner->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*outer, inner->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*outer, outer_continuation->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*inner, target->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*inner, inner_continuation->start_offset()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, CyclicCallGraphKeepsConservativeFallthrough) {
|
|
constexpr uint16_t kOuterReturnSreg = 30;
|
|
constexpr uint16_t kRecursiveReturnSreg = 28;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kOuterReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 outer continuation.
|
|
build_s_call_b64(kRecursiveReturnSreg, -1), // 0x08 -> itself.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x0c recursive continuation.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
|
|
EXPECT_TRUE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, CallToInfiniteLoopDropsFallthrough) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 1), // 0x00 -> callee at 0x08.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 dead continuation.
|
|
build_s_branch(-1, ROCJITSU_CODE_ARCH_CDNA4), // 0x08 -> itself.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *continuation = block_starting_at(blocks, 4);
|
|
auto *callee = block_starting_at(blocks, 8);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(callee, nullptr);
|
|
|
|
EXPECT_TRUE(has_successor_start(*caller, callee->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*caller, continuation->start_offset()));
|
|
EXPECT_FALSE(has_predecessor(*continuation, caller));
|
|
}
|
|
|
|
TEST(CfgAnalysis, ZeroDeltaCallToNonreturningTargetKeepsSingleEdge) {
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
|
|
std::vector<uint32_t> words = {
|
|
build_s_call_b64(kReturnSreg, 0), // 0x00 -> target/continuation at 0x04.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x04 non-returning target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *caller = block_starting_at(blocks, 0);
|
|
auto *target = block_starting_at(blocks, 4);
|
|
ASSERT_NE(caller, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
ASSERT_EQ(caller->successors().size(), 1u);
|
|
EXPECT_EQ(caller->successors()[0], target);
|
|
ASSERT_EQ(target->predecessors().size(), 1u);
|
|
EXPECT_EQ(target->predecessors()[0], caller);
|
|
}
|
|
|
|
TEST(CfgAnalysis, DirectCallKillsCarriedPcBuilderFacts) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
constexpr uint32_t kOriginalGetpcDelta = 28;
|
|
|
|
// Without a context-sensitive call/return model, a builder materialized before
|
|
// s_call_b64 must not be reused by a continuation setpc. The callee below
|
|
// writes the same pair before returning, so recovering the continuation setpc
|
|
// would be a stale-value edge.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
kOriginalGetpcDelta, // 0x08: target delta.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
build_s_call_b64(kReturnSreg, 1), // 0x10 -> callee at 0x18.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x14: stale consumer.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kInlineInt0), // 0x18: callee clobber.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x1c: callee return.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x20: stale target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *continuation = block_starting_at(blocks, 20);
|
|
auto *stale_target = block_starting_at(blocks, 32);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(stale_target, nullptr);
|
|
|
|
EXPECT_TRUE(continuation->static_indirect_call_fixups().empty());
|
|
EXPECT_FALSE(has_successor_start(*continuation, stale_target->start_offset()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, EitherHalfKillPredecessorPreventsRecoveredConsumer) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
constexpr uint32_t kOriginalGetpcDelta = 32;
|
|
|
|
// Two paths reach the same setpc consumer:
|
|
//
|
|
// * the fallthrough path builds a concrete PC target in s[8:9]
|
|
// * the branch path writes either s8 or s9 through ordinary scalar code,
|
|
// killing that pair for this analysis
|
|
//
|
|
// The concrete builder path alone is not enough to recover the consumer. A
|
|
// real unmodeled write reaches the join, so the analysis must fail closed and
|
|
// leave the setpc for the later DBT diagnostic.
|
|
constexpr std::array<uint16_t, 2> clobbered_sregs{kPcSreg, kPcSreg + 1};
|
|
for (uint16_t clobbered_sreg : clobbered_sregs) {
|
|
SCOPED_TRACE(clobbered_sreg);
|
|
std::vector<uint32_t> words = {
|
|
pack_sopp(5, 5), // 0x00 -> kill path at 0x18.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x04: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x08: s_add_u32.
|
|
kOriginalGetpcDelta, // 0x0c: target delta.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x10: s_addc_u32.
|
|
build_s_branch(2, ROCJITSU_CODE_ARCH_CDNA4), // 0x14 -> consumer at 0x20.
|
|
pack_sop2(0, clobbered_sreg, clobbered_sreg, kInlineInt0), // 0x18: write.
|
|
build_s_branch(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x1c -> consumer.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x20: consumer.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x24: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x28: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 1> extra_leaders{40};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4, extra_leaders);
|
|
|
|
auto *consumer = block_starting_at(blocks, 32);
|
|
auto *target = block_starting_at(blocks, 40);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
EXPECT_TRUE(consumer->static_indirect_call_fixups().empty());
|
|
EXPECT_FALSE(has_successor_start(*consumer, target->start_offset()));
|
|
}
|
|
}
|
|
|
|
TEST(CfgAnalysis, RecoversSignedDeltaTemplateConsumers) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kTmpSreg = 12;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
constexpr uint32_t kInlineInt4 = 132;
|
|
constexpr uint32_t kSignedDeltaLiteral = 44;
|
|
|
|
// This is the split signed-delta template matched by static PC recovery:
|
|
// both the subtract and add halves consume the same getpc-relative target.
|
|
// The matcher deliberately recognizes this complete shape instead of tracking
|
|
// arbitrary temporary SGPR values through the branch.
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(2, kTmpSreg, kLiteralOperand, kInlineInt4), // 0x04: s_add_i32.
|
|
kSignedDeltaLiteral, // 0x08: literal.
|
|
pack_sopc(3, kTmpSreg, kInlineInt0), // 0x0c: s_cmp_ge_i32.
|
|
pack_sopp(5, 4), // 0x10 -> add half at 0x24.
|
|
pack_sop1(0x30, kTmpSreg, kTmpSreg), // 0x14: s_abs_i32.
|
|
pack_sop2(1, kPcSreg, kPcSreg, kTmpSreg), // 0x18: s_sub_u32.
|
|
pack_sop2(5, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x1c: s_subb_u32.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x20: subtract consumer.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kTmpSreg), // 0x24: s_add_u32.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x28: s_addc_u32.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x2c: add consumer.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x30: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x34: shared target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *sub_consumer = block_starting_at(blocks, 32);
|
|
auto *add_consumer = block_starting_at(blocks, 44);
|
|
auto *target = block_starting_at(blocks, 52);
|
|
ASSERT_NE(sub_consumer, nullptr);
|
|
ASSERT_NE(add_consumer, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
ASSERT_EQ(sub_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(sub_consumer->static_indirect_call_fixups()[0].source_call_offset, 32u);
|
|
EXPECT_EQ(sub_consumer->static_indirect_call_fixups()[0].source_target_offset, 52u);
|
|
EXPECT_TRUE(has_successor_start(*sub_consumer, target->start_offset()));
|
|
|
|
ASSERT_EQ(add_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(add_consumer->static_indirect_call_fixups()[0].source_call_offset, 44u);
|
|
EXPECT_EQ(add_consumer->static_indirect_call_fixups()[0].source_target_offset, 52u);
|
|
EXPECT_TRUE(has_successor_start(*add_consumer, target->start_offset()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, KeepsDistinctBuildersReachingSameTarget) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
|
|
// Two DIFFERENT getpc builders on two paths both build the SAME target (0x28)
|
|
// and reach one consumer. They are distinct lattice values (same target offset,
|
|
// different source_getpc_offset), so the consumer must retain BOTH fixups — the
|
|
// translator rewrites each builder to its own relocated address. Deduplicating
|
|
// on {call,target,sreg} alone would drop one, leaving its stale pre-relocation
|
|
// address.
|
|
std::vector<uint32_t> words = {
|
|
pack_sopp(5, 4), // 0x00: cbranch scc0 -> builder B at 0x14.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x04: builder A getpc.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x08: s_add_u32 s8, s8, lit.
|
|
0x20u, // 0x0c: delta -> 0x08 + 0x20 = 0x28.
|
|
build_s_branch(3, ROCJITSU_CODE_ARCH_CDNA4), // 0x10 -> consumer at 0x20.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x14: builder B getpc.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x18: s_add_u32 s8, s8, lit.
|
|
0x10u, // 0x1c: delta -> 0x18 + 0x10 = 0x28.
|
|
pack_sop1(0x1d, 0, kPcSreg), // 0x20: joined consumer setpc.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x24: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x28: shared target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 32);
|
|
ASSERT_NE(consumer, nullptr);
|
|
const auto &fixups = consumer->static_indirect_call_fixups();
|
|
// Both builders resolve to target 0x28 but from distinct getpc offsets (0x04,
|
|
// 0x14); both fixups must survive.
|
|
ASSERT_EQ(fixups.size(), 2u);
|
|
for (const auto &fixup : fixups)
|
|
EXPECT_EQ(fixup.source_target_offset, 40u);
|
|
std::vector<uint64_t> getpc_offsets{fixups[0].source_getpc_offset, fixups[1].source_getpc_offset};
|
|
std::ranges::sort(getpc_offsets);
|
|
EXPECT_EQ(getpc_offsets, (std::vector<uint64_t>{4u, 20u}));
|
|
}
|
|
|
|
TEST(CfgAnalysis, RecoveredSwappcToNonreturningTargetDropsFallthrough) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
|
|
std::vector<uint32_t> words = {
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x00: s_getpc_b64.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x04: s_add_u32.
|
|
20, // 0x08: target at 0x18.
|
|
pack_sop2(4, kPcSreg + 1, kPcSreg + 1, kInlineInt0), // 0x0c: s_addc_u32.
|
|
pack_sop1(0x1e, kReturnSreg, kPcSreg), // 0x10: s_swappc_b64.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_CDNA4), // 0x14: dead continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x18: terminal target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 16);
|
|
auto *continuation = block_starting_at(blocks, 20);
|
|
auto *target = block_starting_at(blocks, 24);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_TRUE(consumer->call_edges().empty());
|
|
EXPECT_TRUE(has_successor_start(*consumer, target->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*consumer, continuation->start_offset()));
|
|
EXPECT_FALSE(has_predecessor(*continuation, consumer));
|
|
}
|
|
|
|
TEST(CfgAnalysis, MixedSwappcTargetsKeepSharedContinuation) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
|
|
// Two paths build different finite targets for one swappc consumer. The
|
|
// first target returns through the saved pair; the second terminates.
|
|
std::vector<uint32_t> words = {
|
|
pack_sopp(5, 4), // 0x00 -> builder B at 0x14.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x04: builder A getpc.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x08: s_add_u32.
|
|
0x20u, // 0x0c: target A at 0x28.
|
|
build_s_branch(3, ROCJITSU_CODE_ARCH_CDNA4), // 0x10 -> consumer at 0x20.
|
|
pack_sop1(0x1c, kPcSreg, 0), // 0x14: builder B getpc.
|
|
pack_sop2(0, kPcSreg, kPcSreg, kLiteralOperand), // 0x18: s_add_u32.
|
|
0x14u, // 0x1c: target B at 0x2c.
|
|
pack_sop1(0x1e, kReturnSreg, kPcSreg), // 0x20: s_swappc_b64.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x24: continuation.
|
|
pack_sop1(0x1d, 0, kReturnSreg), // 0x28: returning target A.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_CDNA4), // 0x2c: non-returning target B.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
|
|
auto *consumer = block_starting_at(blocks, 32);
|
|
auto *continuation = block_starting_at(blocks, 36);
|
|
auto *returning_target = block_starting_at(blocks, 40);
|
|
auto *nonreturning_target = block_starting_at(blocks, 44);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_NE(continuation, nullptr);
|
|
ASSERT_NE(returning_target, nullptr);
|
|
ASSERT_NE(nonreturning_target, nullptr);
|
|
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 2u);
|
|
ASSERT_EQ(consumer->call_edges().size(), 1u);
|
|
EXPECT_EQ(consumer->call_edges()[0].callee, returning_target);
|
|
EXPECT_EQ(consumer->call_edges()[0].continuation, continuation);
|
|
EXPECT_TRUE(has_successor_start(*consumer, continuation->start_offset()));
|
|
EXPECT_TRUE(has_successor_start(*consumer, nonreturning_target->start_offset()));
|
|
EXPECT_FALSE(has_successor_start(*consumer, returning_target->start_offset()));
|
|
EXPECT_TRUE(has_predecessor(*continuation, consumer));
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250RecoversSignedDeltaTemplateWithPrefetch) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kTmpSreg = 12;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
constexpr uint32_t kInlineInt4 = 132;
|
|
constexpr uint32_t kSignedDeltaLiteral = 68;
|
|
|
|
// gfx1250 sometimes emits a prefetch setup move and s_prefetch_inst_pc_rel
|
|
// around the low/carry updates. Neither alters the PC pair, so both
|
|
// signed paths still resolve to the same target at 0x4c.
|
|
std::vector<uint32_t> words = {
|
|
gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1,
|
|
{.ssrc0 = 0, .sdst = kPcSreg})[0], // 0x00: s_get_pc_i64 s[8:9].
|
|
gfx1250::build_sop2(gfx1250::kSAddCoI32Sop2,
|
|
{.ssrc0 = kLiteralOperand, .ssrc1 = kInlineInt4, .sdst = kTmpSreg})[0],
|
|
// 0x04: s_add_co_i32.
|
|
kSignedDeltaLiteral, // 0x08: literal.
|
|
gfx1250::build_sopc(gfx1250::kSCmpGeI32Sopc,
|
|
{.ssrc0 = kTmpSreg, .ssrc1 = kInlineInt0})[0], // 0x0c: s_cmp_ge_i32.
|
|
gfx1250::build_sopp(gfx1250::kSCbranchScc1Sopp, {.simm16 = 7})[0],
|
|
// 0x10 -> add half at 0x30.
|
|
gfx1250::build_sop1(gfx1250::kSMovB32Sop1,
|
|
{.ssrc0 = 159, .sdst = 14})[0], // 0x14: s_mov_b32 s14, 31.
|
|
0xF404A000u,
|
|
0x1C000000u, // 0x18: s_prefetch_inst_pc_rel.
|
|
gfx1250::build_sop1(gfx1250::kSAbsI32Sop1,
|
|
{.ssrc0 = kTmpSreg, .sdst = kTmpSreg})[0], // 0x20: s_abs_i32.
|
|
gfx1250::build_sop2(gfx1250::kSSubCoU32Sop2,
|
|
{.ssrc0 = kPcSreg, .ssrc1 = kTmpSreg, .sdst = kPcSreg})[0],
|
|
// 0x24: s_sub_co_u32.
|
|
gfx1250::build_sop2(gfx1250::kSSubCoCiU32Sop2,
|
|
{.ssrc0 = kPcSreg + 1, .ssrc1 = kInlineInt0, .sdst = kPcSreg + 1})[0],
|
|
// 0x28: s_sub_co_ci_u32.
|
|
gfx1250::build_sop1(gfx1250::kSSetPcI64Sop1,
|
|
{.ssrc0 = kPcSreg, .sdst = 0})[0], // 0x2c: s_set_pc_i64.
|
|
gfx1250::build_sop2(gfx1250::kSAddCoU32Sop2,
|
|
{.ssrc0 = kPcSreg, .ssrc1 = kTmpSreg, .sdst = kPcSreg})[0],
|
|
// 0x30: s_add_co_u32.
|
|
gfx1250::build_sop1(gfx1250::kSMovB32Sop1,
|
|
{.ssrc0 = 159, .sdst = 14})[0], // 0x34: s_mov_b32 s14, 31.
|
|
0xF404A000u,
|
|
0x1C000000u, // 0x38: s_prefetch_inst_pc_rel.
|
|
gfx1250::build_sop2(gfx1250::kSAddCoCiU32Sop2,
|
|
{.ssrc0 = kPcSreg + 1, .ssrc1 = kInlineInt0, .sdst = kPcSreg + 1})[0],
|
|
// 0x40: s_add_co_ci_u32.
|
|
gfx1250::build_sop1(gfx1250::kSSetPcI64Sop1,
|
|
{.ssrc0 = kPcSreg, .sdst = 0})[0], // 0x44: s_set_pc_i64.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x48: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x4c: shared target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *sub_consumer = block_starting_at(blocks, 44);
|
|
auto *add_consumer = block_starting_at(blocks, 68);
|
|
auto *target = block_starting_at(blocks, 76);
|
|
ASSERT_NE(sub_consumer, nullptr);
|
|
ASSERT_NE(add_consumer, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
ASSERT_EQ(sub_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(sub_consumer->static_indirect_call_fixups()[0].source_target_offset, 76u);
|
|
EXPECT_TRUE(has_successor_start(*sub_consumer, target->start_offset()));
|
|
EXPECT_FALSE(sub_consumer->static_indirect_call_fixups()[0].source_requires_xcnt_drain);
|
|
|
|
ASSERT_EQ(add_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(add_consumer->static_indirect_call_fixups()[0].source_target_offset, 76u);
|
|
EXPECT_TRUE(has_successor_start(*add_consumer, target->start_offset()));
|
|
EXPECT_FALSE(add_consumer->static_indirect_call_fixups()[0].source_requires_xcnt_drain);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250RecoversSignedDeltaTemplateWithXcntWaitAndPrefetch) {
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kTmpSreg = 12;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
constexpr uint32_t kInlineInt4 = 132;
|
|
constexpr uint32_t kSignedDeltaLiteral = 68;
|
|
|
|
// gfx1250 compiler output drains XCNT immediately before each instruction
|
|
// prefetch. These waits and prefetches do not modify the PC pair or temporary,
|
|
// and must remain in the translated body while the two set-PC consumers are
|
|
// recovered to their common static target.
|
|
std::vector<uint32_t> words = {
|
|
gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1,
|
|
{.ssrc0 = 0, .sdst = kPcSreg})[0], // 0x00: s_get_pc_i64 s[8:9].
|
|
gfx1250::build_sop2(gfx1250::kSAddCoI32Sop2,
|
|
{.ssrc0 = kLiteralOperand, .ssrc1 = kInlineInt4, .sdst = kTmpSreg})[0],
|
|
// 0x04: s_add_co_i32.
|
|
kSignedDeltaLiteral, // 0x08: literal.
|
|
gfx1250::build_sopc(gfx1250::kSCmpGeI32Sopc,
|
|
{.ssrc0 = kTmpSreg, .ssrc1 = kInlineInt0})[0], // 0x0c: s_cmp_ge_i32.
|
|
gfx1250::build_sopp(gfx1250::kSCbranchScc1Sopp, {.simm16 = 7})[0],
|
|
// 0x10 -> add half at 0x30.
|
|
gfx1250::build_sopp(gfx1250::kSWaitXcntSopp, {.simm16 = 0})[0],
|
|
// 0x14: s_wait_xcnt 0.
|
|
0xF404A000u,
|
|
0x1C000000u, // 0x18: s_prefetch_inst_pc_rel.
|
|
gfx1250::build_sop1(gfx1250::kSAbsI32Sop1,
|
|
{.ssrc0 = kTmpSreg, .sdst = kTmpSreg})[0], // 0x20: s_abs_i32.
|
|
gfx1250::build_sop2(gfx1250::kSSubCoU32Sop2,
|
|
{.ssrc0 = kPcSreg, .ssrc1 = kTmpSreg, .sdst = kPcSreg})[0],
|
|
// 0x24: s_sub_co_u32.
|
|
gfx1250::build_sop2(gfx1250::kSSubCoCiU32Sop2,
|
|
{.ssrc0 = kPcSreg + 1, .ssrc1 = kInlineInt0, .sdst = kPcSreg + 1})[0],
|
|
// 0x28: s_sub_co_ci_u32.
|
|
gfx1250::build_sop1(gfx1250::kSSetPcI64Sop1,
|
|
{.ssrc0 = kPcSreg, .sdst = 0})[0], // 0x2c: subtract consumer.
|
|
gfx1250::build_sopp(gfx1250::kSWaitXcntSopp, {.simm16 = 0})[0],
|
|
// 0x30: s_wait_xcnt 0.
|
|
0xF404A000u,
|
|
0x1C000000u, // 0x34: s_prefetch_inst_pc_rel.
|
|
gfx1250::build_sop2(gfx1250::kSAddCoU32Sop2,
|
|
{.ssrc0 = kPcSreg, .ssrc1 = kTmpSreg, .sdst = kPcSreg})[0],
|
|
// 0x3c: s_add_co_u32.
|
|
gfx1250::build_sop2(gfx1250::kSAddCoCiU32Sop2,
|
|
{.ssrc0 = kPcSreg + 1, .ssrc1 = kInlineInt0, .sdst = kPcSreg + 1})[0],
|
|
// 0x40: s_add_co_ci_u32.
|
|
gfx1250::build_sop1(gfx1250::kSSetPcI64Sop1,
|
|
{.ssrc0 = kPcSreg, .sdst = 0})[0], // 0x44: add consumer.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x48: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x4c: shared target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *sub_consumer = block_starting_at(blocks, 44);
|
|
auto *add_consumer = block_starting_at(blocks, 68);
|
|
auto *target = block_starting_at(blocks, 76);
|
|
ASSERT_NE(sub_consumer, nullptr);
|
|
ASSERT_NE(add_consumer, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
|
|
ASSERT_EQ(sub_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(sub_consumer->static_indirect_call_fixups()[0].source_target_offset, 76u);
|
|
EXPECT_TRUE(has_successor_start(*sub_consumer, target->start_offset()));
|
|
|
|
ASSERT_EQ(add_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(add_consumer->static_indirect_call_fixups()[0].source_target_offset, 76u);
|
|
EXPECT_TRUE(has_successor_start(*add_consumer, target->start_offset()));
|
|
|
|
// The subtract half's drain is inside the range relocation overwrites, so
|
|
// both consumers of that shared range must ask the rewrite to reproduce it.
|
|
EXPECT_TRUE(sub_consumer->static_indirect_call_fixups()[0].source_requires_xcnt_drain);
|
|
EXPECT_TRUE(add_consumer->static_indirect_call_fixups()[0].source_requires_xcnt_drain);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250SignedDeltaRejectsMoveClobberingTemporary) {
|
|
// Same signed-delta template as above, but the "prefetch padding" move on the
|
|
// subtract half writes the temporary (s12) instead of an unrelated register
|
|
// (s14). That move changes the value s_abs_i32/s_sub_co_u32 consume, so recovery
|
|
// must NOT treat it as skippable padding and must NOT prove a static target for
|
|
// the subtract-half setpc. Regression for the temp-clobber gap: an s_mov whose
|
|
// destination equals tmp_sreg was previously accepted as padding.
|
|
constexpr uint16_t kPcSreg = 8;
|
|
constexpr uint16_t kTmpSreg = 12;
|
|
constexpr uint32_t kLiteralOperand = 255;
|
|
constexpr uint32_t kInlineInt0 = 128;
|
|
constexpr uint32_t kInlineInt4 = 132;
|
|
constexpr uint32_t kSignedDeltaLiteral = 68;
|
|
|
|
std::vector<uint32_t> words = {
|
|
gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1,
|
|
{.ssrc0 = 0, .sdst = kPcSreg})[0], // 0x00: s_get_pc_i64 s[8:9].
|
|
gfx1250::build_sop2(gfx1250::kSAddCoI32Sop2,
|
|
{.ssrc0 = kLiteralOperand, .ssrc1 = kInlineInt4, .sdst = kTmpSreg})[0],
|
|
// 0x04: s_add_co_i32.
|
|
kSignedDeltaLiteral, // 0x08: literal.
|
|
gfx1250::build_sopc(gfx1250::kSCmpGeI32Sopc,
|
|
{.ssrc0 = kTmpSreg, .ssrc1 = kInlineInt0})[0], // 0x0c: s_cmp_ge_i32.
|
|
gfx1250::build_sopp(gfx1250::kSCbranchScc1Sopp, {.simm16 = 7})[0],
|
|
// 0x10 -> add half at 0x30.
|
|
gfx1250::build_sop1(
|
|
gfx1250::kSMovB32Sop1,
|
|
{.ssrc0 = 159, .sdst = kTmpSreg})[0], // 0x14: s_mov_b32 s12, 31 (CLOBBER).
|
|
0xF404A000u,
|
|
0x1C000000u, // 0x18: s_prefetch_inst_pc_rel.
|
|
gfx1250::build_sop1(gfx1250::kSAbsI32Sop1,
|
|
{.ssrc0 = kTmpSreg, .sdst = kTmpSreg})[0], // 0x20: s_abs_i32.
|
|
gfx1250::build_sop2(gfx1250::kSSubCoU32Sop2,
|
|
{.ssrc0 = kPcSreg, .ssrc1 = kTmpSreg, .sdst = kPcSreg})[0],
|
|
// 0x24: s_sub_co_u32.
|
|
gfx1250::build_sop2(gfx1250::kSSubCoCiU32Sop2,
|
|
{.ssrc0 = kPcSreg + 1, .ssrc1 = kInlineInt0, .sdst = kPcSreg + 1})[0],
|
|
// 0x28: s_sub_co_ci_u32.
|
|
gfx1250::build_sop1(gfx1250::kSSetPcI64Sop1,
|
|
{.ssrc0 = kPcSreg, .sdst = 0})[0], // 0x2c: s_set_pc_i64.
|
|
gfx1250::build_sop2(gfx1250::kSAddCoU32Sop2,
|
|
{.ssrc0 = kPcSreg, .ssrc1 = kTmpSreg, .sdst = kPcSreg})[0],
|
|
// 0x30: s_add_co_u32.
|
|
gfx1250::build_sop1(gfx1250::kSMovB32Sop1,
|
|
{.ssrc0 = 159, .sdst = 14})[0], // 0x34: s_mov_b32 s14, 31.
|
|
0xF404A000u,
|
|
0x1C000000u, // 0x38: s_prefetch_inst_pc_rel.
|
|
gfx1250::build_sop2(gfx1250::kSAddCoCiU32Sop2,
|
|
{.ssrc0 = kPcSreg + 1, .ssrc1 = kInlineInt0, .sdst = kPcSreg + 1})[0],
|
|
// 0x40: s_add_co_ci_u32.
|
|
gfx1250::build_sop1(gfx1250::kSSetPcI64Sop1,
|
|
{.ssrc0 = kPcSreg, .sdst = 0})[0], // 0x44: s_set_pc_i64.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x48: not a target.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x4c: shared target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
// Clobbering the temporary on the subtract half breaks that half of the paired
|
|
// signed-delta template. Because the two halves cross-validate to the same static
|
|
// target, the whole recovery fails closed: NO block proves target 0x4c=76 — versus
|
|
// two proven halves in Gfx1250RecoversSignedDeltaTemplateWithPrefetch.
|
|
size_t resolved_to_target = 0;
|
|
for (const auto &block : blocks) {
|
|
for (const auto &fixup : block->static_indirect_call_fixups()) {
|
|
if (fixup.source_target_offset == 76u)
|
|
++resolved_to_target;
|
|
}
|
|
}
|
|
EXPECT_EQ(resolved_to_target, 0u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, IndirectRecoveryPrefilterAdmitsGfx1250LaneStashSwapPc) {
|
|
// s[0:1] builds target 0x38, is stashed in v44 lanes 0:1, then restored
|
|
// through v_readlane immediately before swappc. This is the finite static
|
|
// call idiom emitted in RCCL device functions.
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
52u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(52).
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x20: v_readlane_b32 s0, v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x28: v_readlane_b32 s1, v44, 1.
|
|
0xBE9E4900u, // 0x30: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x34: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x38: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *consumer = block_starting_at(blocks, 48);
|
|
auto *target = block_starting_at(blocks, 56);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_NE(target, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u)
|
|
<< "lane-stash swappc consumer must pass the indirect-recovery prefilter";
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 56u);
|
|
EXPECT_TRUE(has_successor_start(*consumer, target->start_offset()));
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250WideVgprWriteInvalidatesStashedLane) {
|
|
// Same stash idiom as IndirectRecoveryPrefilterAdmitsGfx1250LaneStashSwapPc,
|
|
// but a width-2 v_mov_b64 writes v[44:45] between the writelanes and the
|
|
// readlanes. That wide write overwrites the stashed VGPR, so the readlane no
|
|
// longer reconstructs the original PC and recovery must fail closed. A
|
|
// width-one-only invalidation would miss the b64 write and falsely recover a
|
|
// target.
|
|
constexpr auto clobber =
|
|
gfx1250::build_vop3(gfx1250::kVMovB64Vop3, {.vdst = 44, .src0 = 256 + 46});
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
52u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(52).
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
clobber[0], // 0x20: v_mov_b64 v[44:45], v[46:47] (wide write over v44).
|
|
clobber[1],
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x28: v_readlane_b32 s0, v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x30: v_readlane_b32 s1, v44, 1.
|
|
0xBE9E4900u, // 0x38: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x40: would-be target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
size_t total_fixups = 0;
|
|
for (const auto &block : blocks)
|
|
total_fixups += block->static_indirect_call_fixups().size();
|
|
EXPECT_EQ(total_fixups, 0u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250CarriesLaneStashAcrossProvenBlockBoundary) {
|
|
// Same stash idiom, but an unconditional branch separates the writelane stashes
|
|
// from the readlane/swappc consumer. The sole predecessor carries the identical
|
|
// physical-v44 lane definitions, so must-reaching-definition dataflow proves
|
|
// the target across the block boundary.
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
56u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(56) -> 0x04+56 = 0x3c (would-be target).
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 0})[0], // 0x20: s_branch -> 0x24.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x24: v_readlane_b32 s0, v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x2c: v_readlane_b32 s1, v44, 1.
|
|
0xBE9E4900u, // 0x34: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x38: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: would-be target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *consumer = block_starting_at(blocks, 52);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 60u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250UnreachablePostRocrAbortBlockDoesNotPoisonLaneStash) {
|
|
constexpr auto live_branch = gfx1250::build_sopp(gfx1250::kSCbranchScc0Sopp, {.simm16 = 3});
|
|
constexpr auto dead_branch = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 1});
|
|
constexpr auto call = gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = 0, .sdst = 30});
|
|
|
|
// Mirror the scalar post-trap regression with a gfx1250 PC stashed in v44:
|
|
//
|
|
// writelanes --conditional-----------------------> readlanes/call
|
|
// |
|
|
// +--> s_trap 2 -X-> dead edge --^
|
|
//
|
|
// In ExplicitOnly mode the dead post-trap block remains BOTTOM. Letting it
|
|
// contribute an empty lane-stash state would erase the valid v44 definitions
|
|
// at the join and lose this otherwise proven call target.
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
68u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(68) -> target 0x48.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: lane 1 <- s1.
|
|
live_branch[0], // 0x20: -> join at 0x30.
|
|
build_s_trap(ROCJITSU_CODE_ARCH_GFX1250, 2), // 0x24: abort terminator.
|
|
dead_branch[0], // 0x28: dead edge -> 0x30.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x2c: dead padding.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x30: v_readlane_b32 s0, v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x38: lane 1 -> s1.
|
|
call[0], // 0x40: s_swap_pc_i64.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x44: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x48: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250, {},
|
|
ExternalEntryPolicy::ExplicitOnly);
|
|
|
|
auto *consumer = block_starting_at(blocks, 64);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 72u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250ExplicitOnlyRecoversLaneStashInRecoveredCallee) {
|
|
// The kernel root first makes a scalar-recoverable call to the helper at
|
|
// 0x18. The helper is not an explicit entry and has no direct predecessor;
|
|
// it becomes reachable only after the outer call edge is recovered. Once
|
|
// reachable, its lane-stashed call to 0x58 must participate in the next
|
|
// discovery iteration.
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
20u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(20) -> helper 0x18.
|
|
0xBE9E4900u, // 0x10: outer s_swap_pc_i64 -> 0x18.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x14: outer continuation.
|
|
0xBE804700u, // 0x18: helper s_get_pc_i64.
|
|
0xA980FE00u,
|
|
60u,
|
|
0u, // 0x1c: s_add_nc_u64 ..., lit64(60) -> target 0x58.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x28: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x30: v_writelane_b32 v44, s1, 1.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x38: v_readlane_b32 s0, v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x40: lane 1 -> s1.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x48: padding.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x4c: padding.
|
|
0xBE9E4900u, // 0x50: inner call -> 0x58.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x54: inner continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x58: inner target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250, {},
|
|
ExternalEntryPolicy::ExplicitOnly);
|
|
|
|
auto *outer_consumer = block_starting_at(blocks, 16);
|
|
auto *inner_consumer = block_starting_at(blocks, 80);
|
|
ASSERT_NE(outer_consumer, nullptr);
|
|
ASSERT_NE(inner_consumer, nullptr);
|
|
ASSERT_EQ(outer_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(outer_consumer->static_indirect_call_fixups()[0].source_target_offset, 24u);
|
|
ASSERT_EQ(inner_consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(inner_consumer->static_indirect_call_fixups()[0].source_target_offset, 88u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250DirectCallKillsCarriedLaneStash) {
|
|
// The stash lives in v48, a CALLER-saved VGPR under CSR_AMDGPU_VGPRs, so a
|
|
// call is not proven to preserve it and the continuation must not recover a
|
|
// second call from the stale stash. (A callee-saved VGPR would survive; see
|
|
// Gfx1250CalleeSavedLaneStashSurvivesDirectCall.)
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr auto clobber = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 128, .vdst = 48});
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u, 56u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(56) -> stale target 0x3c.
|
|
0xD7610030u,
|
|
0x02010000u, // 0x10: v_writelane_b32 v48, s0, 0.
|
|
0xD7610030u,
|
|
0x02010201u, // 0x18: v_writelane_b32 v48, s1, 1.
|
|
rocjitsu::build_s_call_b64(kReturnSreg, 7, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x20: direct call -> callee at 0x40.
|
|
0xD7600000u,
|
|
0x02010130u, // 0x24: continuation reads the pre-call low half.
|
|
0xD7600001u,
|
|
0x02010330u, // 0x2c: continuation reads the high half.
|
|
0xBE9E4900u, // 0x34: stale s_swap_pc_i64.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x38: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: stale target.
|
|
clobber[0], // 0x40: callee clobbers v48.
|
|
rocjitsu::build_s_setpc_b64(kReturnSreg, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x44: callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
size_t total_fixups = 0;
|
|
for (const auto &block : blocks)
|
|
total_fixups += block->static_indirect_call_fixups().size();
|
|
EXPECT_EQ(total_fixups, 0u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250CalleeSavedLaneStashSurvivesDirectCall) {
|
|
// Same shape as Gfx1250DirectCallKillsCarriedLaneStash, but the stash lives
|
|
// in v44 (CALLEE-saved under CSR_AMDGPU_VGPRs). The synthetic callee writes
|
|
// only caller-saved v48, so the continuation must recover the call target
|
|
// from the surviving v44 stash.
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr auto clobber = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 128, .vdst = 48});
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u, 56u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(56) -> stashed target 0x3c.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
rocjitsu::build_s_call_b64(kReturnSreg, 7, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x20: direct call -> callee at 0x40.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x24: continuation reads the surviving low half.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x2c: reads the high half.
|
|
0xBE9E4900u, // 0x34: recovered s_swap_pc_i64.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x38: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: stashed target.
|
|
clobber[0], // 0x40: callee clobbers caller-saved v48.
|
|
rocjitsu::build_s_setpc_b64(kReturnSreg, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x44: callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
const IndirectCallFixup *continuation_fixup = nullptr;
|
|
for (const auto &block : blocks) {
|
|
for (const auto &fixup : block->static_indirect_call_fixups()) {
|
|
if (fixup.source_call_offset == 52) // 0x34: the continuation swappc.
|
|
continuation_fixup = &fixup;
|
|
}
|
|
}
|
|
ASSERT_NE(continuation_fixup, nullptr);
|
|
EXPECT_EQ(continuation_fixup->source_target_offset, 60u); // 0x3c: the stashed target.
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250BankedLaneStashDoesNotSurviveDirectCall) {
|
|
// Select bank 1 for both DST and SRC0, so the v44 operands below consistently
|
|
// address physical v300. Although low selector v44 is callee-saved, the ABI
|
|
// table does not prove physical VGPRs above v255 are preserved. The call must
|
|
// therefore discard the stash rather than mask v300 down to v44.
|
|
//
|
|
// s_set_vgpr_msb immediate byte is {DST[7:6], SRC2[5:4], SRC1[3:2], SRC0[1:0]};
|
|
// 0x41 selects bank 1 for DST and SRC0.
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr auto set_dst_src0_bank_one =
|
|
gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x41});
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u, 60u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(60) -> stale target 0x40.
|
|
set_dst_src0_bank_one[0], // 0x10: v44 DST/SRC0 operands resolve to physical v300.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x14: v_writelane_b32 physical v300, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x1c: v_writelane_b32 physical v300, s1, 1.
|
|
rocjitsu::build_s_call_b64(kReturnSreg, 7, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x24: direct call -> callee at 0x44.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x28: continuation reads physical v300 lane 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x30: reads physical v300 lane 1.
|
|
0xBE9E4900u, // 0x38: must remain dynamic.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x40: stale target.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x44: conforming callee body.
|
|
rocjitsu::build_s_setpc_b64(kReturnSreg, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x48: callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
size_t total_fixups = 0;
|
|
for (const auto &block : blocks)
|
|
total_fixups += block->static_indirect_call_fixups().size();
|
|
EXPECT_EQ(total_fixups, 0u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250IndirectCallKillsCarriedLaneStash) {
|
|
constexpr uint16_t kCallPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint16_t kStaleReturnSreg = 28;
|
|
constexpr auto stale_getpc = gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1, {.sdst = 0});
|
|
constexpr auto call_getpc = gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1, {.sdst = kCallPcSreg});
|
|
constexpr auto call_add = gfx1250::build_sop2(
|
|
gfx1250::kSAddNcU64Sop2, {.ssrc0 = kCallPcSreg, .ssrc1 = 254, .sdst = kCallPcSreg});
|
|
constexpr auto call =
|
|
gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = kCallPcSreg, .sdst = kReturnSreg});
|
|
constexpr auto stale_call =
|
|
gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = 0, .sdst = kStaleReturnSreg});
|
|
constexpr auto clobber = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 128, .vdst = 48});
|
|
|
|
// Stash a target in v48 (a CALLER-saved VGPR under CSR_AMDGPU_VGPRs), then
|
|
// issue a separately-proven indirect call whose callee clobbers v48. The
|
|
// current call is resolved from its pre-call state, but the continuation must
|
|
// not recover a second call from the stale stash because a caller-saved VGPR
|
|
// is not proven to survive the call. (A callee-saved VGPR would survive; see
|
|
// Gfx1250CalleeSavedLaneStashSurvivesIndirectCall.)
|
|
std::vector<uint32_t> words = {
|
|
stale_getpc[0], // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
72u,
|
|
0u, // 0x04: stale target 0x04 + 72 = 0x4c.
|
|
0xD7610030u,
|
|
0x02010000u, // 0x10: v_writelane_b32 v48, s0, 0.
|
|
0xD7610030u,
|
|
0x02010201u, // 0x18: v_writelane_b32 v48, s1, 1.
|
|
call_getpc[0],
|
|
call_add[0],
|
|
44u,
|
|
0u, // 0x24: call target 0x24 + 44 = 0x50.
|
|
call[0], // 0x30: resolved indirect call.
|
|
0xD7600000u,
|
|
0x02010130u, // 0x34: continuation reads the pre-call low half.
|
|
0xD7600001u,
|
|
0x02010330u, // 0x3c: reads the high half.
|
|
stale_call[0], // 0x44: stale indirect call.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x48: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x4c: stale target.
|
|
clobber[0], // 0x50: callee clobbers v48.
|
|
rocjitsu::build_s_setpc_b64(kReturnSreg, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x54: callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
size_t total_fixups = 0;
|
|
const IndirectCallFixup *call_fixup = nullptr;
|
|
for (const auto &block : blocks) {
|
|
total_fixups += block->static_indirect_call_fixups().size();
|
|
for (const auto &fixup : block->static_indirect_call_fixups()) {
|
|
if (fixup.source_call_offset == 48)
|
|
call_fixup = &fixup;
|
|
}
|
|
}
|
|
ASSERT_EQ(total_fixups, 1u);
|
|
ASSERT_NE(call_fixup, nullptr);
|
|
EXPECT_EQ(call_fixup->source_target_offset, 80u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250CalleeSavedLaneStashSurvivesIndirectCall) {
|
|
// Same shape as Gfx1250IndirectCallKillsCarriedLaneStash, but the stash lives
|
|
// in v44 (CALLEE-saved under CSR_AMDGPU_VGPRs). A conforming callee must
|
|
// preserve it, so the continuation swappc IS recovered from the stash. This
|
|
// is the RCCL ncclDevKernel pattern: a getpc code target stashed in a
|
|
// callee-saved VGPR, carried across an intervening call, then read back and
|
|
// called.
|
|
constexpr uint16_t kCallPcSreg = 8;
|
|
constexpr uint16_t kReturnSreg = 30;
|
|
constexpr uint16_t kStaleReturnSreg = 28;
|
|
constexpr auto stale_getpc = gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1, {.sdst = 0});
|
|
constexpr auto call_getpc = gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1, {.sdst = kCallPcSreg});
|
|
constexpr auto call_add = gfx1250::build_sop2(
|
|
gfx1250::kSAddNcU64Sop2, {.ssrc0 = kCallPcSreg, .ssrc1 = 254, .sdst = kCallPcSreg});
|
|
constexpr auto call =
|
|
gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = kCallPcSreg, .sdst = kReturnSreg});
|
|
constexpr auto stale_call =
|
|
gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = 0, .sdst = kStaleReturnSreg});
|
|
constexpr auto clobber = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 128, .vdst = 48});
|
|
|
|
std::vector<uint32_t> words = {
|
|
stale_getpc[0], // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
72u,
|
|
0u, // 0x04: stashed target 0x04 + 72 = 0x4c.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
call_getpc[0],
|
|
call_add[0],
|
|
44u,
|
|
0u, // 0x24: call target 0x24 + 44 = 0x50.
|
|
call[0], // 0x30: resolved intervening indirect call.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x34: continuation reads the surviving low half.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x3c: reads the high half.
|
|
stale_call[0], // 0x44: continuation indirect call.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x48: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x4c: stashed target.
|
|
clobber[0], // 0x50: callee clobbers caller-saved v48.
|
|
rocjitsu::build_s_setpc_b64(kReturnSreg, ROCJITSU_CODE_ARCH_GFX1250),
|
|
// 0x54: callee return.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
const IndirectCallFixup *continuation_fixup = nullptr;
|
|
for (const auto &block : blocks) {
|
|
for (const auto &fixup : block->static_indirect_call_fixups()) {
|
|
if (fixup.source_call_offset == 68) // 0x44: the continuation swappc.
|
|
continuation_fixup = &fixup;
|
|
}
|
|
}
|
|
ASSERT_NE(continuation_fixup, nullptr);
|
|
EXPECT_EQ(continuation_fixup->source_target_offset, 76u); // 0x4c: the stashed target.
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250SeedsTextEntryWithLoopBackedgeForLaneStash) {
|
|
constexpr auto getpc = gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1, {.sdst = 0});
|
|
constexpr auto branch = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 0});
|
|
constexpr auto backedge =
|
|
gfx1250::build_sopp(gfx1250::kSCbranchScc0Sopp, {.simm16 = static_cast<uint16_t>(-10)});
|
|
constexpr auto call = gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = 0, .sdst = 30});
|
|
|
|
// The entry/stash block and latch form a loop, so block zero has a structural
|
|
// predecessor. Architectural entry bank zero must still seed the vector-lane
|
|
// dataflow; the stash written in block zero then reaches the fallthrough
|
|
// consumer after the latch.
|
|
std::vector<uint32_t> words = {
|
|
getpc[0], // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
60u,
|
|
0u, // 0x04: target 0x04 + 60 = 0x40.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
branch[0], // 0x20: -> latch at 0x24.
|
|
backedge[0], // 0x24: conditional backedge -> block zero.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x28: v_readlane_b32 s0, v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x30: v_readlane_b32 s1, v44, 1.
|
|
call[0], // 0x38: cross-block consumer.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x40: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *consumer = block_starting_at(blocks, 56);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 64u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250ExplicitKernelEntryClearsIncomingLaneStash) {
|
|
constexpr auto getpc = gfx1250::build_sop1(gfx1250::kSGetPcI64Sop1, {.sdst = 0});
|
|
constexpr auto branch = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 1});
|
|
constexpr auto call = gfx1250::build_sop1(gfx1250::kSSwapPcI64Sop1, {.ssrc0 = 0, .sdst = 30});
|
|
|
|
// Entry A stashes a target and branches into entry B. B is independently
|
|
// launchable, so its external path has no proven v44 lane contents even
|
|
// though A's internal predecessor carries a complete stash.
|
|
std::vector<uint32_t> words = {
|
|
getpc[0], // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
64u,
|
|
0u, // 0x04: target 0x04 + 64 = 0x44.
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x10: v_writelane_b32 v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x18: v_writelane_b32 v44, s1, 1.
|
|
branch[0], // 0x20: -> independently launchable entry B at 0x28.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x24: skipped.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x28: entry B reads v44 lane 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x30: reads v44 lane 1.
|
|
call[0], // 0x38: must remain dynamic.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: continuation.
|
|
build_s_nop(0, ROCJITSU_CODE_ARCH_GFX1250), // 0x40: padding.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x44: A's target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
constexpr std::array<uint64_t, 1> extra_leaders{40};
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250, extra_leaders);
|
|
|
|
size_t total_fixups = 0;
|
|
for (const auto &block : blocks)
|
|
total_fixups += block->static_indirect_call_fixups().size();
|
|
EXPECT_EQ(total_fixups, 0u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250A0UsesLowByteOfVgprMsb) {
|
|
// The gfx1250 A0 profile stores the previous VGPR-MSB state in SIMM16[15:8].
|
|
// Only SIMM16[7:0] updates the current operand banks. Thus 0x4400 establishes
|
|
// bank zero (and records previous state 0x44); it must not redirect this stash
|
|
// to physical v300 or invalidate the already-stashed physical-v44 lanes.
|
|
constexpr auto set_bank_zero_with_previous_44 =
|
|
gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x4400});
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
56u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(56) -> target 0x3c.
|
|
set_bank_zero_with_previous_44[0],
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x14: v_writelane_b32 physical v44, s0, 0.
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x1c: v_writelane_b32 physical v44, s1, 1.
|
|
set_bank_zero_with_previous_44[0],
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x28: v_readlane_b32 s0, physical v44, 0.
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x30: v_readlane_b32 s1, physical v44, 1.
|
|
0xBE9E4900u, // 0x38: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: target/continuation.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *consumer = block_starting_at(blocks, 56);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 60u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250DoesNotRecoverLaneStashWithDifferingRoleBanks) {
|
|
// Same straight-line stash idiom as
|
|
// IndirectRecoveryPrefilterAdmitsGfx1250LaneStashSwapPc, but an
|
|
// s_set_vgpr_msb sets the DST bank to 1 while leaving the SRC0 bank at 0. The
|
|
// v_writelane writes physical v[44+256] (DST bank 1) while the v_readlane
|
|
// reads physical v44 (SRC0 bank 0). Because the roles resolve the same low
|
|
// selector to different physical VGPRs, no value actually flows, and
|
|
// recovery must fail closed rather than key both by the low selector and
|
|
// falsely reconstruct a PC.
|
|
//
|
|
// s_set_vgpr_msb immediate byte is {DST[7:6], SRC2[5:4], SRC1[3:2], SRC0[1:0]};
|
|
// 0x40 selects DST bank 1, all other roles bank 0.
|
|
constexpr auto set_dst_bank_one =
|
|
gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x40});
|
|
std::vector<uint32_t> words = {
|
|
0xBE804700u, // 0x00: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
52u,
|
|
0u, // 0x04: s_add_nc_u64 ..., lit64(52).
|
|
set_dst_bank_one[0], // 0x10: s_set_vgpr_msb (DST bank 1, SRC0 bank 0).
|
|
0xD761002Cu,
|
|
0x02010000u, // 0x14: v_writelane_b32 v44, s0, 0 (physical v300 under DST bank 1).
|
|
0xD761002Cu,
|
|
0x02010201u, // 0x1c: v_writelane_b32 v44, s1, 1.
|
|
0xD7600000u,
|
|
0x0201012Cu, // 0x24: v_readlane_b32 s0, v44, 0 (physical v44 under SRC0 bank 0).
|
|
0xD7600001u,
|
|
0x0201032Cu, // 0x2c: v_readlane_b32 s1, v44, 1.
|
|
0xBE9E4900u, // 0x34: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x38: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: would-be target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
size_t total_fixups = 0;
|
|
for (const auto &block : blocks)
|
|
total_fixups += block->static_indirect_call_fixups().size();
|
|
EXPECT_EQ(total_fixups, 0u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250InheritsBankAlongProvenCfgEdge) {
|
|
// An s_set_vgpr_msb in the entry block establishes a bank, then s_branch jumps to
|
|
// a stash block that performs the full getpc/writelane/readlane/swappc idiom with
|
|
// no local s_set_vgpr_msb. MODE is architectural state, so the sole CFG edge
|
|
// carries bank zero into the stash block. This is CFG propagation rather than
|
|
// accidental inheritance from lexical scan order.
|
|
//
|
|
// 0x00 s_set_vgpr_msb 0 ; 0x04 s_branch 0 -> next block at 0x08.
|
|
constexpr auto set_bank_zero = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0});
|
|
constexpr auto branch_to_stash = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 0});
|
|
std::vector<uint32_t> words = {
|
|
set_bank_zero[0], // 0x00: establish bank 0 (entry block).
|
|
branch_to_stash[0], // 0x04: s_branch -> stash block at 0x08.
|
|
0xBE804700u, // 0x08: s_get_pc_i64 s[0:1] (stash block).
|
|
0xA980FE00u,
|
|
52u,
|
|
0u, // 0x0c: s_add_nc_u64 ..., lit64(52).
|
|
0xD761002Cu, // 0x18: v_writelane_b32 v44, s0, 0.
|
|
0x02010000u,
|
|
0xD761002Cu, // 0x20: v_writelane_b32 v44, s1, 1.
|
|
0x02010201u,
|
|
0xD7600000u, // 0x28: v_readlane_b32 s0, v44, 0.
|
|
0x0201012Cu,
|
|
0xD7600001u, // 0x30: v_readlane_b32 s1, v44, 1.
|
|
0x0201032Cu,
|
|
0xBE9E4900u, // 0x38: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: continuation.
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x40: target.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
|
|
auto *consumer = block_starting_at(blocks, 56);
|
|
ASSERT_NE(consumer, nullptr);
|
|
ASSERT_EQ(consumer->static_indirect_call_fixups().size(), 1u);
|
|
EXPECT_EQ(consumer->static_indirect_call_fixups()[0].source_target_offset, 64u);
|
|
}
|
|
|
|
TEST(CfgAnalysis, Gfx1250DoesNotReuseStashFromSkippedFallthroughPredecessor) {
|
|
// Diamond: a conditional branch jumps DIRECTLY into the readlane/swappc consumer
|
|
// block, while the lexical fallthrough path holds the WHOLE getpc/writelane stash.
|
|
//
|
|
// A: s_cbranch_scc1 -> C (fallthrough to B)
|
|
// B: s_set_vgpr_msb 0 ; getpc/add ; v_writelane (fallthrough to C)
|
|
// C: v_readlane s0/s1, v44 ; s_swap_pc_i64 (branch target of A)
|
|
//
|
|
// On the A->C edge the entire stash in B never executes, so the value in v44 is
|
|
// not proven to reach the swappc. C is a branch target — a real block leader — so
|
|
// recovery must reset at C and fail closed, even though B lexically falls through
|
|
// into C. The lane scan is linear: B has no terminator before C, so without a
|
|
// reset at C's leader B's recorded slot leaks into C and falsely recovers a single
|
|
// target. B re-establishes its VGPR-MSB bank locally (s_set_vgpr_msb 0) so the
|
|
// writelane actually records a slot — otherwise the post-cbranch bank-unknown state
|
|
// would mask the stash and the test could not distinguish the two behaviors.
|
|
//
|
|
// s_cbranch_scc1 next_pc = 0x04, target C = 0x28: delta 36 bytes = 9 dwords.
|
|
constexpr auto cbranch_to_consumer =
|
|
gfx1250::build_sopp(gfx1250::kSCbranchScc1Sopp, {.simm16 = 9});
|
|
constexpr auto set_bank_zero = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0});
|
|
std::vector<uint32_t> words = {
|
|
cbranch_to_consumer[0], // 0x00: s_cbranch_scc1 -> C at 0x28 (block A).
|
|
set_bank_zero[0], // 0x04: s_set_vgpr_msb 0 (block B, fallthrough).
|
|
0xBE804700u, // 0x08: s_get_pc_i64 s[0:1].
|
|
0xA980FE00u,
|
|
48u,
|
|
0u, // 0x0c: s_add_nc_u64 ..., lit64(48) -> target 0x3c.
|
|
0xD761002Cu, // 0x18: v_writelane_b32 v44, s0, 0.
|
|
0x02010000u,
|
|
0xD761002Cu, // 0x20: v_writelane_b32 v44, s1, 1.
|
|
0x02010201u,
|
|
0xD7600000u, // 0x28: v_readlane_b32 s0, v44, 0 (block C, branch target).
|
|
0x0201012Cu,
|
|
0xD7600001u, // 0x30: v_readlane_b32 s1, v44, 1.
|
|
0x0201032Cu,
|
|
0xBE9E4900u, // 0x38: s_swap_pc_i64 s[30:31], s[0:1].
|
|
build_s_endpgm(ROCJITSU_CODE_ARCH_GFX1250), // 0x3c: would-be target / continuation.
|
|
};
|
|
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
|
|
// Assert on the recovery pass output directly: the must-reaching-definition
|
|
// join must discard the stash because the A->C edge bypasses it.
|
|
const auto *sec = co.text_sections().front();
|
|
const auto *inst_data = reinterpret_cast<const uint32_t *>(sec->data());
|
|
const size_t inst_data_size = sec->size() / sizeof(uint32_t);
|
|
std::vector<std::unique_ptr<Instruction>> owned;
|
|
for (size_t pc = 0, byte_offset = 0; pc < inst_data_size;) {
|
|
if (inst_data[pc] == 0) { // gfx1250 alignment padding, as in BasicBlock::build.
|
|
++pc;
|
|
byte_offset += sizeof(uint32_t);
|
|
continue;
|
|
}
|
|
std::unique_ptr<Instruction> inst(decoder->decode(&inst_data[pc], byte_offset));
|
|
ASSERT_NE(inst, nullptr);
|
|
const uint32_t inst_words = static_cast<uint32_t>(inst->size()) / sizeof(uint32_t);
|
|
byte_offset += inst->size();
|
|
pc += inst_words;
|
|
owned.push_back(std::move(inst));
|
|
}
|
|
std::vector<const Instruction *> decoded_insts;
|
|
decoded_insts.reserve(owned.size());
|
|
for (const auto &inst : owned)
|
|
decoded_insts.push_back(inst.get());
|
|
const auto text =
|
|
std::span<const uint8_t>(reinterpret_cast<const uint8_t *>(sec->data()), sec->size());
|
|
|
|
const auto fixups = discover_indirect_branch_edges(
|
|
std::span<const Instruction *const>(decoded_insts.data(), decoded_insts.size()), text,
|
|
ROCJITSU_CODE_ARCH_GFX1250);
|
|
EXPECT_TRUE(fixups.empty()) << "must-dataflow must not reuse a skipped-path stash";
|
|
}
|
|
|
|
TEST(CfgAnalysis, ReversePostOrderStraightLine) {
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::DefVgpr0, TestOpcode::UseVgpr0, TestOpcode::UseSgpr4});
|
|
auto scope = block_scope(blocks);
|
|
auto rpo = reverse_post_order(KernelBlockScope(scope));
|
|
ASSERT_EQ(rpo.size(), 1u);
|
|
EXPECT_EQ(blocks[0].get(), rpo[0]);
|
|
}
|
|
|
|
TEST(CfgAnalysis, ReversePostOrderIfElseDiamond) {
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::CBranchToElse, TestOpcode::BranchToJoin, TestOpcode::Nop, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
auto rpo = reverse_post_order(KernelBlockScope(scope));
|
|
ASSERT_EQ(rpo.size(), 4u);
|
|
EXPECT_EQ(rpo[0], blocks[0].get());
|
|
EXPECT_EQ(rpo[1], blocks[1].get());
|
|
EXPECT_EQ(rpo[2], blocks[2].get());
|
|
EXPECT_EQ(rpo[3], blocks[3].get());
|
|
}
|
|
|
|
TEST(CfgAnalysis, ReversePostOrderChangedOrder) {
|
|
auto blocks = build_test_blocks({TestOpcode::BranchToJoin, TestOpcode::BranchToJoin,
|
|
TestOpcode::BranchBackToStart, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
auto rpo = reverse_post_order(KernelBlockScope(scope));
|
|
ASSERT_EQ(rpo.size(), 4u);
|
|
EXPECT_EQ(rpo[0], blocks[0].get());
|
|
EXPECT_EQ(rpo[1], blocks[2].get());
|
|
EXPECT_EQ(rpo[2], blocks[1].get());
|
|
EXPECT_EQ(rpo[3], blocks[3].get());
|
|
}
|
|
|
|
TEST(CfgAnalysis, ReversePostOrderSelfLoop) {
|
|
auto blocks = build_test_blocks({TestOpcode::Nop, TestOpcode::BranchBackToStart});
|
|
auto scope = block_scope(blocks);
|
|
auto rpo = reverse_post_order(KernelBlockScope(scope));
|
|
ASSERT_EQ(rpo.size(), 1u);
|
|
EXPECT_EQ(blocks[0].get(), rpo[0]);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, UnavailableQueriesFailClosed) {
|
|
const TestInstruction instruction("query");
|
|
const LivenessAnalysis liveness = LivenessAnalysis::unavailable();
|
|
|
|
EXPECT_THROW((void)liveness.has_live_before(instruction), std::logic_error);
|
|
EXPECT_THROW((void)liveness.live_before(instruction), std::logic_error);
|
|
EXPECT_THROW((void)liveness.find_globally_unused_vgpr_run(&instruction, 1), std::logic_error);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ReportsWhetherLiveBeforeSnapshotWasMaterialized) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
const Instruction &use = *blocks.front()->instructions().begin();
|
|
const TestInstruction outside_scope("outside_scope");
|
|
const LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
EXPECT_TRUE(liveness.has_live_before(use));
|
|
EXPECT_FALSE(liveness.has_live_before(outside_scope));
|
|
EXPECT_TRUE(liveness.is_live_before(use, {RegClass::SGPR, 4, 1}))
|
|
<< "the materialized snapshot must contain the register used here";
|
|
EXPECT_FALSE(liveness.is_live_before(outside_scope, {RegClass::SGPR, 4, 1}))
|
|
<< "a missing snapshot reads as nothing-live, so callers must check "
|
|
"has_live_before first";
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ExecMaskedVgprDefDoesNotKillInactiveLaneValue) {
|
|
auto blocks = build_test_blocks({TestOpcode::DefVgpr0, TestOpcode::UseVgpr0, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &def = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
|
|
auto free_vgpr = liveness.find_free_run(&def, 1);
|
|
ASSERT_TRUE(free_vgpr.has_value());
|
|
EXPECT_NE(*free_vgpr, 0);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250VgprMsbResolvesPhysicalRegisterBank) {
|
|
// src0=2 and dst=2 select physical VGPR bank 2. The VOP1 source encoding
|
|
// still contains v1, but liveness must identify the architectural register
|
|
// as v513 rather than aliasing it with low-bank v1.
|
|
// The upper byte records the previous state for trap recovery and must not
|
|
// affect the active bank selected by the low byte.
|
|
constexpr auto set_vgpr_msb = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x5a82});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({set_vgpr_msb[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 2);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 2);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 1, 1, 2), 1)
|
|
<< "a known bank-2 access must not make the raw low-bank tuple look used";
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 513, 1}));
|
|
EXPECT_FALSE(liveness.is_live_before(*instruction, {RegClass::VGPR, 1, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250ImplicitVgprUseResolvesDestinationBank) {
|
|
// v_mov_b16 is a partial (16-bit) write, so it read-modify-preserves its full
|
|
// destination VGPR. That preserve-read is reported through
|
|
// implicit_use_operands() with the destination's Dst VGPR-MSB role. Set DST
|
|
// bank 2 (byte {DST[7:6],SRC2,SRC1,SRC0} = 2<<6 = 0x80) and write vdst v1: the
|
|
// architectural register read is physical v513, so it must be live before the
|
|
// move. If the implicit read stayed at low-bank v1, liveness would treat v513
|
|
// as dead and a scratch borrow could clobber it.
|
|
constexpr auto set_dst_bank_two =
|
|
gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x80});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB16Vop1, {.src0 = 128, .vdst = 1});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({set_dst_bank_two[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 2);
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 513, 1}))
|
|
<< "implicit RMW read of the destination must resolve to the DST bank";
|
|
EXPECT_FALSE(liveness.is_live_before(*instruction, {RegClass::VGPR, 1, 1}))
|
|
<< "the low-bank alias must not be treated as the read register";
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250ImplicitVgprUseResolvesDespiteExplicitBank0Alias) {
|
|
// Aliasing case: v_mov_b16 v1, v1 reads v1 as an explicit SRC0 (bank 0) and
|
|
// also preserve-reads its destination v1 in DST bank 2 (physical v513). A
|
|
// "newly-added bits" recovery would miss v513 because raw v1 is already present
|
|
// from the explicit source; the per-operand path must add v513 regardless.
|
|
// 0x80 selects DST bank 2, SRC0 bank 0.
|
|
constexpr auto set_dst_bank_two =
|
|
gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x80});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB16Vop1, {.src0 = 256 + 1, .vdst = 1});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({set_dst_bank_two[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 513, 1}))
|
|
<< "the DST-bank preserve-read must be added even though raw v1 is already an explicit use";
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 1, 1}))
|
|
<< "the explicit SRC0 bank-0 read of v1 is still live";
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250SwapImplicitReadsResolvePerRole) {
|
|
// v_swap_b16 preserve-reads BOTH operands, each in its own role: vdst in the
|
|
// DST bank and src0 in the SRC0 bank. With SRC0 bank 1 and DST bank 2, vdst=v1
|
|
// reads physical v513 (Dst) and src0=v2 reads physical v258 (Src0). Assigning
|
|
// both implicit reads the DST bank would mislocate the src0 read.
|
|
// Byte {DST[7:6],SRC2,SRC1,SRC0}: DST bank 2 (0x80) | SRC0 bank 1 (0x01) = 0x81.
|
|
constexpr auto set_banks = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x81});
|
|
constexpr auto swap = gfx1250::build_vop1(gfx1250::kVSwapB16Vop1, {.src0 = 256 + 2, .vdst = 1});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({set_banks[0], swap[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 2);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 1);
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 513, 1}))
|
|
<< "vdst preserve-read must resolve to the DST bank (v1 -> v513)";
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 258, 1}))
|
|
<< "src0 preserve-read must resolve to the SRC0 bank (v2 -> v258)";
|
|
// The mixed-role signature: resolving the hook-added src0 entry with the DST
|
|
// bank puts raw v2 at 2 + 2*256 = v514. v513/v258 above stay live either way
|
|
// (the destination supplies v513, the explicit source supplies v258), so v514
|
|
// is the only assertion that actually fails when the roles are conflated.
|
|
EXPECT_FALSE(liveness.is_live_before(*instruction, {RegClass::VGPR, 514, 1}))
|
|
<< "src0 must not be mislocated to the DST bank (v2 under DST bank 2 -> v514)";
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250DppPreserveReadResolvesToDstBank) {
|
|
// Covers the ENCODING-level preserved-destination hook, the other half of the
|
|
// implicit-operand surface: the v_mov_b16 cases above exercise the
|
|
// per-instruction partial-def path, while a partial-DPP write reaches
|
|
// implicit_use_operands() through the shared SDWA/DPP predicate on the VOP1
|
|
// encoding base. Because InstDefUse strips the VGPR class from the flat
|
|
// implicit_uses() result on gfx1250, dropping the encoding-level operand push
|
|
// would leave this read with no live destination at all rather than a wrong
|
|
// one -- a silent liveness hole, so it needs its own regression.
|
|
//
|
|
// DST bank 2 (0x80), then v_mov_b32_dpp vdst=v5 with row_mask=0x7 (partial),
|
|
// so the unwritten rows preserve the destination: raw v5 reads physical
|
|
// 5 + 2*256 = v517.
|
|
constexpr auto set_banks = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x80});
|
|
// VOP1 word0: enc[31:25]=0x3F, vdst[24:17]=5, op[15:9]=kVMovB32Vop1,
|
|
// src0[8:0]=SRC_DPP. DPP word1: row_mask[31:28]=0x7 (partial),
|
|
// bank_mask[27:24]=0xF, vsrc0[7:0]=2.
|
|
constexpr uint32_t kDppMovWord0 =
|
|
(0x3Fu << 25) | (5u << 17) | (uint32_t{gfx1250::kVMovB32Vop1} << 9) | amdgpu::SRC_DPP;
|
|
constexpr uint32_t kDppWord1Partial = (0x7u << 28) | (0xFu << 24) | 2u;
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({set_banks[0], kDppMovWord0, kDppWord1Partial, end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 2);
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 517, 1}))
|
|
<< "the partial-DPP preserve-read must resolve to the DST bank (v5 -> v517)";
|
|
EXPECT_FALSE(liveness.is_live_before(*instruction, {RegClass::VGPR, 5, 1}))
|
|
<< "the unbanked raw index must not be marked live in place of v517";
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250ImplicitVgprUseUnknownBankReadsEveryCandidate) {
|
|
// A dynamic MODE write leaves the DST bank ambiguous. The implicit preserve-read
|
|
// of v_mov_b16 vdst=v1 must then may-read all four candidate tuples, so v1,
|
|
// v257, v513, and v769 are all live before the move (the sound fallback).
|
|
constexpr uint16_t kModeAllBanksHwreg = 1u | (12u << 6) | (7u << 11);
|
|
constexpr auto setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregB32Sopk, {.simm16 = kModeAllBanksHwreg, .sdst = 0});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB16Vop1, {.src0 = 128, .vdst = 1});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({setreg[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), std::nullopt);
|
|
for (uint16_t bank = 0; bank < 4; ++bank)
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction,
|
|
{RegClass::VGPR, static_cast<uint16_t>(1 + bank * 256), 1}))
|
|
<< "unknown-bank implicit read must may-read candidate bank " << bank;
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250UnknownBankDefMakesEveryCandidateGloballyUsed) {
|
|
// A dynamic MODE write leaves the destination bank ambiguous. Whole-kernel
|
|
// usage must reserve all four candidate tuples, while backward liveness must
|
|
// not pretend the one physical write kills all four.
|
|
constexpr uint16_t kModeAllBanksHwreg = 1u | (12u << 6) | (7u << 11);
|
|
constexpr auto setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregB32Sopk, {.simm16 = kModeAllBanksHwreg, .sdst = 0});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 128, .vdst = 1});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({setreg[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), std::nullopt);
|
|
for (uint16_t bank = 0; bank < 4; ++bank) {
|
|
const uint16_t candidate = static_cast<uint16_t>(1 + bank * 256);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, candidate, 1,
|
|
static_cast<uint16_t>(candidate + 1)),
|
|
std::nullopt)
|
|
<< "unknown-bank definition must reserve candidate bank " << bank;
|
|
}
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 0, 1, 4), 0);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 2, 1, 4), 2);
|
|
|
|
const BlockLiveness &state = liveness.block_liveness(*blocks.front());
|
|
EXPECT_FALSE(state.kill.contains({RegClass::VGPR, 1, 1}));
|
|
EXPECT_FALSE(state.kill.contains({RegClass::VGPR, 257, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250RelativeVgprAccessDisablesGlobalUnusedQuery) {
|
|
// M0 can redirect the encoded v0 source to any relative tuple, including v1
|
|
// which would otherwise appear globally unused.
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovrelsB32Vop1, {.src0 = 0, .vdst = 2});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &instruction = *blocks.front()->instructions().begin();
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&instruction, 1, 1, 1, 2), std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250SwaprelDisablesGlobalUnusedQuery) {
|
|
constexpr auto swap = gfx1250::build_vop1(gfx1250::kVSwaprelB32Vop1, {.src0 = 0, .vdst = 2});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({swap[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &instruction = *blocks.front()->instructions().begin();
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&instruction, 1, 1, 1, 2), std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250GprIndexModeWriteDisablesGlobalUnusedQuery) {
|
|
// A runtime MODE[27] write can enable GPR indexing, after which ordinary
|
|
// encoded operands may access M0-offset VGPRs.
|
|
constexpr uint16_t kModeGprIdxEnableHwreg = 1u | (27u << 6);
|
|
constexpr auto setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregB32Sopk, {.simm16 = kModeGprIdxEnableHwreg, .sdst = 0});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 256, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({setreg[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 1, 1, 2), std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250ImmediateGprIndexModeWriteUsesLiteralValue) {
|
|
constexpr uint16_t kModeGprIdxEnableHwreg = 1u | (27u << 6);
|
|
constexpr auto setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kModeGprIdxEnableHwreg});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 256, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
|
|
for (uint32_t literal : {0u, 1u}) {
|
|
SCOPED_TRACE(literal);
|
|
TestCodeObject co({setreg[0], literal, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
const auto unused = liveness.find_globally_unused_vgpr_run(&*instruction, 1, 1, 1, 2);
|
|
if (literal == 0)
|
|
EXPECT_EQ(unused, 1);
|
|
else
|
|
EXPECT_EQ(unused, std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Cdna4DynamicGprIndexModeWriteDisablesGlobalUnusedQuery) {
|
|
constexpr uint16_t kModeGprIdxEnableHwreg = 1u | (27u << 6);
|
|
constexpr auto setreg =
|
|
cdna4::build_sopk(cdna4::kSSetregB32Sopk, {.simm16 = kModeGprIdxEnableHwreg, .sdst = 0});
|
|
constexpr auto move = cdna4::build_vop1(cdna4::kVMovB32Vop1, {.src0 = 256, .vdst = 0});
|
|
constexpr auto end = cdna4::build_sopp(cdna4::kSEndpgmSopp);
|
|
TestCodeObject co({setreg[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_CDNA4;
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 1, 1, 2), std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Cdna4ImmediateGprIndexModeWriteUsesLiteralValue) {
|
|
constexpr uint16_t kModeGprIdxEnableHwreg = 1u | (27u << 6);
|
|
constexpr auto setreg =
|
|
cdna4::build_sopk(cdna4::kSSetregImm32B32Sopk, {.simm16 = kModeGprIdxEnableHwreg});
|
|
constexpr auto move = cdna4::build_vop1(cdna4::kVMovB32Vop1, {.src0 = 256, .vdst = 0});
|
|
constexpr auto end = cdna4::build_sopp(cdna4::kSEndpgmSopp);
|
|
|
|
for (uint32_t literal : {0u, 1u}) {
|
|
SCOPED_TRACE(literal);
|
|
TestCodeObject co({setreg[0], literal, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_CDNA4;
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
const auto unused = liveness.find_globally_unused_vgpr_run(&*instruction, 1, 1, 1, 2);
|
|
if (literal == 0)
|
|
EXPECT_EQ(unused, 1);
|
|
else
|
|
EXPECT_EQ(unused, std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250DynamicModeWriteConservativelyUsesEveryBank) {
|
|
constexpr uint16_t kModeSrc0Hwreg = 1u | (14u << 6) | (1u << 11);
|
|
constexpr auto setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregB32Sopk, {.simm16 = kModeSrc0Hwreg, .sdst = 0});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({setreg[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), std::nullopt);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src1), 0);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src2), 0);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 0);
|
|
for (uint16_t bank = 0; bank < 4; ++bank)
|
|
EXPECT_TRUE(liveness.is_live_before(
|
|
*instruction, {RegClass::VGPR, static_cast<uint16_t>(1 + bank * 256), 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250FullLiteralModeWriteRecoversKnownBank) {
|
|
constexpr uint16_t kModeSrc0Hwreg = 1u | (14u << 6) | (1u << 11);
|
|
constexpr auto dynamic_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregB32Sopk, {.simm16 = kModeSrc0Hwreg, .sdst = 0});
|
|
constexpr auto literal_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kModeSrc0Hwreg});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({dynamic_setreg[0], literal_setreg[0], 2u << 14, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
std::advance(instruction, 2);
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(instruction.operator*().mnemonic(), "v_mov_b32_e32");
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 2);
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 513, 1}));
|
|
EXPECT_FALSE(liveness.is_live_before(*instruction, {RegClass::VGPR, 1, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250TruncatedLiteralModeWriteMarksBanksAmbiguous) {
|
|
// A mode-setting s_setreg_imm32_b32 whose 32-bit literal is not fully present in
|
|
// the .text image (truncated at the end of the section) cannot have its banks
|
|
// recovered. The analysis reads the literal from the text at src_loc()+4; when
|
|
// that word is out of range it must mark the affected banks ambiguous (nullopt)
|
|
// rather than read past the section. Model the truncation by handing the analysis
|
|
// a text span that stops just after the setreg encoding word, before its literal.
|
|
constexpr auto set_bank_two = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 2});
|
|
constexpr uint16_t kModeAllBanksHwreg = 1u | (12u << 6) | (7u << 11);
|
|
constexpr auto literal_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kModeAllBanksHwreg});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
// Full program (so decode sees a valid literal + terminator), but the analysis is
|
|
// told the text ends right after the setreg encoding word at offset 4 (its
|
|
// literal at offset 8 is out of range).
|
|
TestCodeObject co({set_bank_two[0], literal_setreg[0], 0xe4u << 12, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
// Truncate the text span to 8 bytes: the setreg (at offset 4) has no readable
|
|
// literal at offset 8.
|
|
const auto full = text_span(co);
|
|
options.text = full.subspan(0, 8);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
std::advance(instruction, 2);
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(instruction.operator*().mnemonic(), "v_mov_b32_e32");
|
|
// Bank 2 was set before the truncated mode write; because the mode write's
|
|
// literal is unreadable, the Src0 bank must be ambiguous, not the pre-write 2.
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), std::nullopt);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250PartialLiteralModeWriteUsesUnmaskedVgprFields) {
|
|
constexpr auto set_bank_one = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 1});
|
|
constexpr uint16_t kModeSrc0HighBitHwreg = 1u | (15u << 6);
|
|
constexpr auto literal_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kModeSrc0HighBitHwreg});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({set_bank_one[0], literal_setreg[0], 3u << 14, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
std::advance(instruction, 2);
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(instruction.operator*().mnemonic(), "v_mov_b32_e32");
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 3);
|
|
EXPECT_TRUE(liveness.is_live_before(*instruction, {RegClass::VGPR, 769, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250ImmediateModeWriteRecoversBanksOutsideRequestedSlice) {
|
|
constexpr uint16_t kModeSrc0Hwreg = 1u | (14u << 6) | (1u << 11);
|
|
constexpr auto dynamic_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregB32Sopk, {.simm16 = kModeSrc0Hwreg, .sdst = 0});
|
|
// Request a write to MODE bit zero. gfx1250 updates all VGPR-MSB fields from
|
|
// literal bits [19:12].
|
|
constexpr uint16_t kModeBitZeroHwreg = 1u;
|
|
constexpr auto literal_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kModeBitZeroHwreg});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({dynamic_setreg[0], literal_setreg[0], 2u << 14, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
std::advance(instruction, 2);
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 2);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250LiteralModeWriteTracksEveryRole) {
|
|
constexpr uint16_t kAllVgprMsbFieldsHwreg = 1u | (12u << 6) | (7u << 11);
|
|
constexpr auto literal_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kAllVgprMsbFieldsHwreg});
|
|
// MODE[19:12] is {src2=3, src1=2, src0=1, dst=0}.
|
|
constexpr uint32_t kModeFields = 0xe4u << 12;
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({literal_setreg[0], kModeFields, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 1);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src1), 2);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src2), 3);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 0);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250ImmediateNonModeWriteDoesNotChangeBanks) {
|
|
constexpr uint16_t kNonModeHwreg = 2u;
|
|
constexpr auto literal_setreg =
|
|
gfx1250::build_sopk(gfx1250::kSSetregImm32B32Sopk, {.simm16 = kNonModeHwreg});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({literal_setreg[0], 0x000ff000u, move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_EQ(blocks.size(), 1u);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src0), 0);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src1), 0);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Src2), 0);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*instruction, amdgpu::VgprMsbRole::Dst), 0);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250VgprMsbCfgJoinRequiresPredecessorsToAgree) {
|
|
constexpr auto branch_to_else = gfx1250::build_sopp(gfx1250::kSCbranchScc0Sopp, {.simm16 = 2});
|
|
constexpr auto set_bank_two = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 0x82});
|
|
constexpr auto branch_to_join = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 1});
|
|
constexpr auto set_bank_zero = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp);
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co(
|
|
{branch_to_else[0], set_bank_two[0], branch_to_join[0], set_bank_zero[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
auto scope = block_scope(blocks);
|
|
BasicBlock *join = block_starting_at(blocks, 16);
|
|
ASSERT_NE(join, nullptr);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &joined_move = *join->instructions().begin();
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(joined_move, amdgpu::VgprMsbRole::Src0), std::nullopt);
|
|
for (uint16_t bank = 0; bank < 4; ++bank)
|
|
EXPECT_TRUE(liveness.is_live_before(
|
|
joined_move, {RegClass::VGPR, static_cast<uint16_t>(1 + bank * 256), 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250VgprMsbCfgJoinPreservesAgreeingBank) {
|
|
constexpr auto branch_to_else = gfx1250::build_sopp(gfx1250::kSCbranchScc0Sopp, {.simm16 = 2});
|
|
constexpr auto set_bank_two = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 2});
|
|
constexpr auto branch_to_join = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 1});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co(
|
|
{branch_to_else[0], set_bank_two[0], branch_to_join[0], set_bank_two[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
auto scope = block_scope(blocks);
|
|
BasicBlock *join = block_starting_at(blocks, 16);
|
|
ASSERT_NE(join, nullptr);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &joined_move = *join->instructions().begin();
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(joined_move, amdgpu::VgprMsbRole::Src0), 2);
|
|
EXPECT_TRUE(liveness.is_live_before(joined_move, {RegClass::VGPR, 513, 1}));
|
|
EXPECT_FALSE(liveness.is_live_before(joined_move, {RegClass::VGPR, 1, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, Gfx1250VgprMsbJoinExcludesUnreachablePredecessor) {
|
|
// The entry unconditionally branches over an unreachable block that sets bank 0,
|
|
// landing on a block that sets bank 2 and falls through to the join. Only the
|
|
// reachable predecessor (bank 2) may contribute to the join; the unreachable
|
|
// bank-0 block must NOT drag the joined bank to ambiguous (nullopt). This pins
|
|
// that the fixed point excludes unreachable predecessors rather than meeting
|
|
// every structural in-edge.
|
|
//
|
|
// Layout (each op is one dword):
|
|
// 0x00 s_branch +1 -> skips the unreachable block, targets 0x08
|
|
// 0x04 s_set_vgpr_msb 0 (UNREACHABLE: no edge targets it)
|
|
// 0x08 s_set_vgpr_msb 2 (reachable target; falls through to join)
|
|
// 0x0c v_mov (join) reads v1 under the proven bank
|
|
// 0x10 s_endpgm
|
|
constexpr auto branch_over = gfx1250::build_sopp(gfx1250::kSBranchSopp, {.simm16 = 1});
|
|
constexpr auto set_bank_zero = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp);
|
|
constexpr auto set_bank_two = gfx1250::build_sopp(gfx1250::kSSetVgprMsbSopp, {.simm16 = 2});
|
|
constexpr auto move = gfx1250::build_vop1(gfx1250::kVMovB32Vop1, {.src0 = 257, .vdst = 0});
|
|
constexpr auto end = gfx1250::build_sopp(gfx1250::kSEndpgmSopp);
|
|
TestCodeObject co({branch_over[0], set_bank_zero[0], set_bank_two[0], move[0], end[0]});
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
auto blocks = BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_GFX1250);
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.arch = ROCJITSU_CODE_ARCH_GFX1250;
|
|
options.entry_block = scope.front();
|
|
options.text = text_span(co);
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction *joined_move = nullptr;
|
|
for (const auto &block : blocks) {
|
|
for (const Instruction &inst : block->instructions()) {
|
|
if (inst.mnemonic() == "v_mov_b32_e32")
|
|
joined_move = &inst;
|
|
}
|
|
}
|
|
ASSERT_NE(joined_move, nullptr);
|
|
EXPECT_EQ(liveness.vgpr_msb_bank_before(*joined_move, amdgpu::VgprMsbRole::Src0), 2);
|
|
EXPECT_TRUE(liveness.is_live_before(*joined_move, {RegClass::VGPR, 513, 1}));
|
|
}
|
|
|
|
// Collect a block's instructions in order for index-based assertions.
|
|
std::vector<const Instruction *> insts_of(BasicBlock &block) {
|
|
std::vector<const Instruction *> out;
|
|
for (const auto &inst : block.instructions())
|
|
out.push_back(&inst);
|
|
return out;
|
|
}
|
|
|
|
// Decode real CDNA4 instruction words into a CFG with the production decoder.
|
|
std::vector<std::unique_ptr<BasicBlock>> build_cdna4_blocks(std::vector<uint32_t> words) {
|
|
TestCodeObject co(std::move(words));
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
return BasicBlock::build(co, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
|
|
}
|
|
|
|
// Full end-to-end on real decoded instructions: `s_mov_b64 exec, -1` makes EXEC
|
|
// provably all-ones, so the following EXEC-masked VGPR def overwrites every lane
|
|
// and becomes a real kill — the defined VGPR is dead immediately before it. This
|
|
// exercises the whole chain: decode -> to_register_ref (EXEC dest) + const_value
|
|
// (-1 -> all-ones) -> ExecMaskAnalysis (Full) -> liveness kill.
|
|
TEST(ExecFlagsRealDecode, Cdna4SMovExecAllOnesPromotesVgprDefToKill) {
|
|
// s_mov_b64 exec, -1 ; v_mov_b32 v0, s0 ; v_mov_b32 v2, v0 ; s_endpgm
|
|
auto blocks = build_cdna4_blocks({0xBEFE01C1u, 0x7E000200u, 0x7E040300u, 0xBF810000u});
|
|
ASSERT_FALSE(blocks.empty());
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 3u);
|
|
|
|
// Sanity-check the decode so an encoding typo fails loudly.
|
|
EXPECT_EQ(insts[0]->mnemonic(), "s_mov_b64");
|
|
const Instruction &def = *insts[1];
|
|
EXPECT_TRUE(def.mnemonic().starts_with("v_mov_b32"));
|
|
ASSERT_NE(def.dst_operand(0), nullptr);
|
|
auto def_ref = def.dst_operand(0)->to_register_ref();
|
|
ASSERT_TRUE(def_ref.has_value());
|
|
EXPECT_EQ(*def_ref, (RegisterRef{RegClass::VGPR, 0, 1}));
|
|
|
|
// Proving EXEC=Full from `s_mov exec, -1` needs the s_mov to carry RESULT_COPY,
|
|
// which only exists once the ISA is regenerated with the combinator metadata.
|
|
ASSERT_TRUE(insts[0]->flags() & RESULT_COPY)
|
|
<< "s_mov lacks RESULT_COPY; regenerate ISA to enable EXEC-Full tracking";
|
|
|
|
// EXEC is provably full at the def, so its vector write is a real kill.
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(exec.before(def), ExecState::Full);
|
|
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
EXPECT_FALSE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
// Contrast: without the all-ones EXEC write, EXEC stays Unknown at the def, so
|
|
// the same vector def is not promoted to a kill and the VGPR remains live.
|
|
TEST(ExecFlagsRealDecode, Cdna4VgprDefStaysLiveWithoutFullExec) {
|
|
// v_mov_b32 v0, s0 ; v_mov_b32 v2, v0 ; s_endpgm
|
|
auto blocks = build_cdna4_blocks({0x7E000200u, 0x7E040300u, 0xBF810000u});
|
|
ASSERT_FALSE(blocks.empty());
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 2u);
|
|
const Instruction &def = *insts[0];
|
|
EXPECT_TRUE(def.mnemonic().starts_with("v_mov_b32"));
|
|
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(exec.before(def), ExecState::Unknown);
|
|
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
EXPECT_TRUE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
// v_writelane_b32 writes a single lane, so even under Full EXEC it is a
|
|
// read-modify-write of its destination and must not be promoted to a kill.
|
|
TEST(ExecFlagsRealDecode, Cdna4WritelaneDestStaysLiveUnderFullExec) {
|
|
// s_mov_b64 exec, -1 ; v_writelane_b32 v5, s4, s2 ; v_mov_b32 v2, v5 ; s_endpgm
|
|
auto blocks =
|
|
build_cdna4_blocks({0xBEFE01C1u, 0xD28A0005u, 0x00000404u, 0x7E040305u, 0xBF810000u});
|
|
ASSERT_FALSE(blocks.empty());
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 3u);
|
|
|
|
// Sanity-check the decode so an encoding typo fails loudly.
|
|
EXPECT_EQ(insts[0]->mnemonic(), "s_mov_b64");
|
|
const Instruction &writelane = *insts[1];
|
|
EXPECT_EQ(writelane.mnemonic(), "v_writelane_b32");
|
|
ASSERT_NE(writelane.dst_operand(0), nullptr);
|
|
auto dst_ref = writelane.dst_operand(0)->to_register_ref();
|
|
ASSERT_TRUE(dst_ref.has_value());
|
|
EXPECT_EQ(*dst_ref, (RegisterRef{RegClass::VGPR, 5, 1}));
|
|
|
|
// Full-EXEC tracking needs the regenerated s_mov RESULT_COPY metadata.
|
|
ASSERT_TRUE(insts[0]->flags() & RESULT_COPY)
|
|
<< "s_mov lacks RESULT_COPY; regenerate ISA to enable EXEC-Full tracking";
|
|
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(exec.before(writelane), ExecState::Full);
|
|
|
|
// Contrast Cdna4SMovExecAllOnesPromotesVgprDefToKill: the RMW dst stays live.
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
EXPECT_TRUE(liveness.is_live_before(writelane, {RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, EntryIsUnknownAllOnesIsFullNarrowingIsUnknown) {
|
|
// exec=all-ones; v0=...; exec=narrow; v0=...; end
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::DefVgpr0,
|
|
TestOpcode::WriteExecNarrow, TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_EQ(insts.size(), 5u);
|
|
EXPECT_EQ(exec.before(*insts[0]), ExecState::Unknown); // kernel entry
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Full); // after exec=all-ones
|
|
EXPECT_EQ(exec.before(*insts[2]), ExecState::Full); // narrowing not yet applied
|
|
EXPECT_EQ(exec.before(*insts[3]), ExecState::Unknown); // after narrowing write
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, LoopHeaderEntryWithBackedgeIsPinnedUnknown) {
|
|
// block0 @0: End -- a predecessor-less natural entry, so the scope-leader
|
|
// fallback does not fire. block1 @4: a self-looping "kernel entry" that writes
|
|
// EXEC all-ones and conditionally branches back to itself; its only in-scope
|
|
// predecessor is that backedge. Without pinning it, the forward meet carries
|
|
// the backedge's Full state into the header and wrongly derives Full there.
|
|
auto blocks = build_test_blocks({TestOpcode::End, TestOpcode::WriteExecFull,
|
|
TestOpcode::CBranchBackToUseSgpr4, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
BasicBlock *header = block_starting_at(blocks, 4);
|
|
ASSERT_NE(header, nullptr);
|
|
ASSERT_TRUE(has_predecessor(*header, header)) << "header must have its own backedge";
|
|
const Instruction &header_first = *insts_of(*header).front();
|
|
EXPECT_TRUE(header_first.mnemonic().starts_with("test_write_exec_full"));
|
|
|
|
// Without pinning, the backedge makes the header wrongly Full.
|
|
ExecMaskAnalysis unpinned{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(unpinned.before(header_first), ExecState::Full);
|
|
|
|
// Pinning the real entry keeps it Unknown, as at any kernel entry.
|
|
const BasicBlock *const entries[] = {header};
|
|
ExecMaskAnalysis pinned{KernelBlockScope(scope), 64, /*extra_edges=*/{}, entries};
|
|
EXPECT_EQ(pinned.before(header_first), ExecState::Unknown);
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, SecondHardwareEntryReachedAfterFullIsPinnedUnknown) {
|
|
// Models a kernel with two hardware entries (descriptor entry + kernarg-preload
|
|
// firmware entry): block0 @0 establishes Full and falls through to block1 @4, a
|
|
// second entry (forced to be a leader). block1 is reachable with Full via the
|
|
// ordinary path, but hardware may enter it directly with unknown EXEC, so both
|
|
// entries must be seeded -- unlike the single-entry tests above.
|
|
const uint64_t firmware_leader[] = {4};
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::WriteExecFull, TestOpcode::DefVgpr0, TestOpcode::End}, firmware_leader);
|
|
auto scope = block_scope(blocks);
|
|
BasicBlock *firmware = block_starting_at(blocks, 4);
|
|
ASSERT_NE(firmware, nullptr);
|
|
ASSERT_TRUE(has_predecessor(*firmware, blocks.front().get()));
|
|
const Instruction &firmware_first = *insts_of(*firmware).front();
|
|
|
|
// Without pinning, the ordinary fallthrough carries Full into the second entry.
|
|
ExecMaskAnalysis unpinned{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(unpinned.before(firmware_first), ExecState::Full);
|
|
|
|
// Seeding both entries keeps the second Unknown despite the incoming Full.
|
|
const BasicBlock *const entries[] = {blocks.front().get(), firmware};
|
|
ExecMaskAnalysis pinned{KernelBlockScope(scope), 64, /*extra_edges=*/{}, entries};
|
|
EXPECT_EQ(pinned.before(firmware_first), ExecState::Unknown);
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, OrWithAllOnesConstantIsFull) {
|
|
// exec = exec | -1 -> all-ones regardless of the prior EXEC (RESULT_OR with an
|
|
// all-ones source operand).
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::WriteExecOrAllOnes, TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 2u);
|
|
EXPECT_EQ(exec.before(*insts[0]), ExecState::Unknown); // entry
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Full); // after exec = exec | -1
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, AndSaveexecWithAllOnesStaysUnknown) {
|
|
// s_and_saveexec exec, -1: exec = exec & -1 = exec, NOT all-ones. The all-ones
|
|
// source must not be mistaken for an all-ones result (regression guard).
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::WriteExecAndSaveexec, TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 2u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Unknown); // after and-saveexec
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, PartialAllOnesWritePreservesButDoesNotEstablishFull) {
|
|
// s_mov_b32 exec_lo, -1 on Wave64 sets only the low half to all-ones.
|
|
// Keeps an already-Full mask Full
|
|
{
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::WriteExecLoHalf,
|
|
TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 3u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Full); // entering the half write
|
|
EXPECT_EQ(exec.before(*insts[2]), ExecState::Full); // half all-ones preserved Full
|
|
}
|
|
// ...Cannot establish Full from Unknown (exec_hi stays unknown).
|
|
{
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::WriteExecLoHalf, TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 2u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Unknown);
|
|
}
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, Wave32ExecLoWriteCoversFullMask) {
|
|
// On Wave32, exec_lo is the entire EXEC, so an all-ones exec_lo write is Full.
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::WriteExecLoHalf, TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 32};
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 2u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Full);
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, ExecHiWriteNeverEstablishesFull) {
|
|
// s_mov_b32 exec_hi, -1 writes bits [32,64), which never cover the active
|
|
// lanes. It must not establish Full from Unknown on either wave size — on
|
|
// Wave32 the written bits lie entirely outside the mask, on Wave64 they are a
|
|
// partial upper half.
|
|
for (uint8_t wave_size : {uint8_t{32}, uint8_t{64}}) {
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::WriteExecHiHalf, TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), wave_size};
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 2u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Unknown);
|
|
}
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, ExecHiWritePreservesButDoesNotNarrowFull) {
|
|
// From an already-Full mask, an all-ones exec_hi write keeps Full (it only
|
|
// rewrites in-range or upper bits with ones), the same as an exec_lo half.
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::WriteExecHiHalf,
|
|
TestOpcode::DefVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 3u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Full); // entering the hi write
|
|
EXPECT_EQ(exec.before(*insts[2]), ExecState::Full); // hi all-ones preserved Full
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, Wave32ExecHiZeroWritePreservesFull) {
|
|
// On Wave32 exec_hi lies outside the active mask, so an exec_hi write preserves
|
|
// the current state even for a non-all-ones value like `s_mov_b32 exec_hi, 0`
|
|
// (which previously narrowed an established Full to Unknown).
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::WriteExecHiHalfZero,
|
|
TestOpcode::DefVgpr0, TestOpcode::UseVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 32};
|
|
auto insts = insts_of(*blocks[0]);
|
|
ASSERT_GE(insts.size(), 4u);
|
|
EXPECT_EQ(exec.before(*insts[1]), ExecState::Full); // entering the exec_hi=0 write
|
|
EXPECT_EQ(exec.before(*insts[2]), ExecState::Full); // exec_hi=0 preserved Full
|
|
|
|
const ExecMaskAnalysis exec_for_liveness{KernelBlockScope(scope), /*wave_size=*/32};
|
|
const LivenessAnalysis liveness{KernelBlockScope(scope),
|
|
std::make_unique<ExecMaskAnalysis>(exec_for_liveness)};
|
|
EXPECT_FALSE(liveness.is_live_before(*insts[2], {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ExecFullPromotesVgprDefToKill) {
|
|
// exec=all-ones; def v0; use v0; end. Under full EXEC the def overwrites every
|
|
// lane, so v0 is dead immediately before the def.
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::WriteExecFull, TestOpcode::DefVgpr0, TestOpcode::UseVgpr0, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &def = *insts_of(*blocks[0])[1];
|
|
EXPECT_FALSE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
EXPECT_EQ(liveness.find_free_run(&def, 1), 0u);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ExecFullViaInlineConstantPromotesVgprDefToKill) {
|
|
// The all-ones source here is an inline constant (`s_mov exec, -1` style):
|
|
// it exposes const_value() but not literal64_value(), so this exercises the
|
|
// const_value() path the analysis relies on.
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFullInline, TestOpcode::DefVgpr0,
|
|
TestOpcode::UseVgpr0, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &def = *insts_of(*blocks[0])[1];
|
|
EXPECT_FALSE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
EXPECT_EQ(liveness.find_free_run(&def, 1), 0u);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, NarrowingExecWriteDoesNotPromoteVgprDefToKill) {
|
|
// Same shape but EXEC is only narrowed (unknown), so the def stays a
|
|
// non-kill and v0 remains live before it.
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::WriteExecNarrow, TestOpcode::DefVgpr0, TestOpcode::UseVgpr0, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &def = *insts_of(*blocks[0])[1];
|
|
EXPECT_TRUE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
|
|
auto free_vgpr = liveness.find_free_run(&def, 1);
|
|
ASSERT_TRUE(free_vgpr.has_value());
|
|
EXPECT_NE(*free_vgpr, 0u);
|
|
}
|
|
|
|
TEST(ExecMaskAnalysis, CfgJoinMeetsToUnknownUnlessAllPredecessorsFull) {
|
|
// Branch byte layout (4 bytes/inst); target = block-end + delta:
|
|
// 0: WriteExecFull
|
|
// 4: CBranchToElse (delta +4; block ends at 8 -> target 12, skips block@8)
|
|
// 8: WriteExecNarrow (fallthrough block)
|
|
// 12: DefVgpr0 (join: reached from the Full branch path and the
|
|
// 16: UseVgpr0 Unknown fallthrough path -> meet is Unknown, so the
|
|
// 20: End def must not be promoted to a kill)
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::CBranchToElse,
|
|
TestOpcode::WriteExecNarrow, TestOpcode::DefVgpr0,
|
|
TestOpcode::UseVgpr0, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
|
|
BasicBlock *join = block_starting_at(blocks, 12);
|
|
ASSERT_NE(join, nullptr);
|
|
const Instruction &def = *join->instructions().begin();
|
|
EXPECT_EQ(exec.before(def), ExecState::Unknown);
|
|
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
EXPECT_TRUE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
// Regression: EXEC must flow across the scoped caller->callee edge like liveness
|
|
// does. Without the edge the callee looks like a scope entry (Unknown) and its
|
|
// vector def is wrongly left a non-kill; with it EXEC is Full and the def kills.
|
|
TEST(ExecMaskAnalysis, CallEdgeFlowsExecFullIntoCallee) {
|
|
// Caller: exec=all-ones; end. Callee: def v0; use v0; end. The caller's End
|
|
// splits the callee into its own block, reachable only via the scoped edge.
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::End, TestOpcode::DefVgpr0,
|
|
TestOpcode::UseVgpr0, TestOpcode::End});
|
|
ASSERT_EQ(blocks.size(), 2u);
|
|
BasicBlock *caller = blocks[0].get();
|
|
BasicBlock *callee = blocks[1].get();
|
|
const Instruction &def = *callee->instructions().begin();
|
|
|
|
const std::array<ScopedCfgEdge, 1> edges{ScopedCfgEdge{.from = caller, .to = callee}};
|
|
auto scope = block_scope(blocks);
|
|
|
|
// No edge: callee is a scope entry, EXEC Unknown, def stays live.
|
|
{
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(exec.before(def), ExecState::Unknown);
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
EXPECT_TRUE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
// Same edge in both: EXEC Full at the def, so it overwrites every lane and kills v0.
|
|
{
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64, edges};
|
|
EXPECT_EQ(exec.before(def), ExecState::Full);
|
|
LivenessAnalysis liveness = analyze_scope_with_edges(blocks, edges);
|
|
EXPECT_FALSE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
}
|
|
|
|
// Regression: a scoped return->continuation edge must pull the continuation to
|
|
// Unknown when the returning path narrowed EXEC. Omitting it from exec leaves the
|
|
// continuation looking Full (from its local branch pred) and unsoundly kills.
|
|
TEST(ExecMaskAnalysis, ReturnEdgeToContinuationMeetsToUnknown) {
|
|
// 0: WriteExecFull (P0)
|
|
// 4: CBranchToElseAfterTwo (delta +8 -> branch target 16)
|
|
// 8: WriteExecNarrow (P1) return block: narrows EXEC
|
|
// 12: End P1 terminates -> reaches cont only via the edge
|
|
// 16: DefVgpr0 (cont) branch target of P0; 20: UseVgpr0; 24: End
|
|
auto blocks = build_test_blocks({TestOpcode::WriteExecFull, TestOpcode::CBranchToElseAfterTwo,
|
|
TestOpcode::WriteExecNarrow, TestOpcode::End,
|
|
TestOpcode::DefVgpr0, TestOpcode::UseVgpr0, TestOpcode::End});
|
|
BasicBlock *ret_block = block_starting_at(blocks, 8);
|
|
BasicBlock *cont = block_starting_at(blocks, 16);
|
|
ASSERT_NE(ret_block, nullptr);
|
|
ASSERT_NE(cont, nullptr);
|
|
const Instruction &def = *cont->instructions().begin();
|
|
|
|
const std::array<ScopedCfgEdge, 1> edges{ScopedCfgEdge{.from = ret_block, .to = cont}};
|
|
auto scope = block_scope(blocks);
|
|
|
|
// No edge: cont sees only its Full branch pred, so EXEC looks Full and def kills.
|
|
{
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(exec.before(def), ExecState::Full);
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
EXPECT_FALSE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
// With edge: meet of the Full branch and the narrowed return path is Unknown,
|
|
// so the def is not a kill and v0 stays live.
|
|
{
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64, edges};
|
|
EXPECT_EQ(exec.before(def), ExecState::Unknown);
|
|
LivenessAnalysis liveness = analyze_scope_with_edges(blocks, edges);
|
|
EXPECT_TRUE(liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
}
|
|
|
|
// Regression: a back-edge into the scope entry gives block 0 a predecessor, but
|
|
// EXEC at kernel entry is set by dispatch and must stay Unknown -- the loop
|
|
// back-edge must not let interior blocks' optimistic `Full` seed leak in. Guards
|
|
// the `i == 0` pin in the predecessor-based entry detection.
|
|
TEST(ExecMaskAnalysis, BackEdgeIntoScopeEntryStaysUnknown) {
|
|
// 0: CBranchToElse (+4) header/entry: loop or exit at 8
|
|
// 4: BranchBackToStart(-8) latch: unconditional back-edge to offset 0
|
|
// 8: End loop exit
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::CBranchToElse, TestOpcode::BranchBackToStart, TestOpcode::End});
|
|
BasicBlock *entry = blocks[0].get();
|
|
BasicBlock *latch = block_starting_at(blocks, 4);
|
|
ASSERT_NE(latch, nullptr);
|
|
|
|
// The latch back-edges into the entry, so it is not caught by the empty-pred
|
|
// case -- only the i==0 pin keeps it an entry.
|
|
EXPECT_TRUE(has_predecessor(*entry, latch));
|
|
|
|
auto scope = block_scope(blocks);
|
|
ExecMaskAnalysis exec{KernelBlockScope(scope), 64};
|
|
EXPECT_EQ(exec.before(*entry->instructions().begin()), ExecState::Unknown);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FindsDeadSgprAfterLiveSgpr) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(use, {RegClass::SGPR, 4, 1}));
|
|
EXPECT_EQ(liveness.find_free_sgpr(&use, 4), 5);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FindValidSgprPair) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(use, {RegClass::SGPR, 4, 1}));
|
|
EXPECT_EQ(liveness.find_free_sgpr_pair(&use, 4), 6);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FindSgprPairSkipsStraddle) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::UseSgpr7, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_EQ(liveness.find_free_sgpr_pair(&use, 4), 8);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, NoSgprPairAvailable) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_EQ(liveness.find_free_sgpr_pair(&use, REGISTER_SET_ALLOCATABLE_SGPRS + 10), std::nullopt);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, MinFreeVgprForcesScratchAllocationAboveFloor) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.min_free_vgpr = 4;
|
|
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_FALSE(liveness.is_live_before(use, {RegClass::VGPR, 0, 4}));
|
|
EXPECT_EQ(liveness.find_free_sgpr(&use, 0), 0);
|
|
EXPECT_EQ(liveness.find_free_run(&use, 1, 0), 4);
|
|
EXPECT_EQ(liveness.find_free_run(&use, 1, 7), 7);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, GloballyUnusedRunHonorsMinFreeVgprFloor) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
LivenessAnalysisOptions options;
|
|
options.min_free_vgpr = 4;
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&use, 1, 0, 1, 8), 4);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&use, 1, 0, 1, 4), std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FindsGloballyUnusedRunBeforeSiteDeadFallback) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseVgpr0, TestOpcode::Nop, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
const ExecMaskAnalysis exec{KernelBlockScope(scope), /*wave_size=*/64};
|
|
const LivenessAnalysis liveness{KernelBlockScope(scope),
|
|
std::make_unique<ExecMaskAnalysis>(exec)};
|
|
|
|
auto instruction = blocks.front()->instructions().begin();
|
|
++instruction;
|
|
ASSERT_NE(instruction, blocks.front()->instructions().end());
|
|
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 0, 1, 4), 1);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 2, 0, 2, 4), 2);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 2, 0, 1, 1), std::nullopt);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, 0, 1, 0), std::nullopt);
|
|
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
|
|
EXPECT_EQ(liveness.find_free_run(&*instruction, 1), 0)
|
|
<< "v0 is dead at this site but is not globally unused";
|
|
EXPECT_TRUE(liveness.has_materialized_cfg_liveness());
|
|
|
|
Instruction outside_scope("outside_scope", nullptr);
|
|
EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&outside_scope, 1, 0, 1, 4), std::nullopt);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FreeVgprAllocationHonorsDestinationLimit) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
|
|
LivenessAnalysisOptions limited_options;
|
|
limited_options.min_free_vgpr = 256;
|
|
LivenessAnalysis limited(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
limited_options);
|
|
EXPECT_EQ(limited.find_free_run(&use, 1), std::nullopt);
|
|
|
|
LivenessAnalysisOptions gfx1250_options;
|
|
gfx1250_options.min_free_vgpr = 256;
|
|
gfx1250_options.max_free_vgpr = 1024;
|
|
LivenessAnalysis gfx1250(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
gfx1250_options);
|
|
EXPECT_EQ(gfx1250.find_free_run(&use, 1), 256);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FindFreeRunHonorsBaseAlignment) {
|
|
auto blocks = build_test_blocks({TestOpcode::UseSgpr4, TestOpcode::End});
|
|
auto scope = block_scope(blocks);
|
|
|
|
LivenessAnalysisOptions options;
|
|
options.min_free_vgpr = 93;
|
|
|
|
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
|
|
LivenessAnalysis liveness(KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
|
|
options);
|
|
|
|
const Instruction &use = *blocks[0]->instructions().begin();
|
|
EXPECT_EQ(liveness.find_free_run(&use, 4, 0, 2), 94);
|
|
EXPECT_EQ(liveness.find_free_run(&use, 4, 94, 4), 96);
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ReadWriteSameRegisterIsLiveBeforeInstruction) {
|
|
auto blocks = build_test_blocks({TestOpcode::ReadWriteSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &read_write = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(read_write, {RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ReadWriteRegisterStaysLiveOutWhenUsedBySuccessor) {
|
|
std::array<uint64_t, 1> extra_leaders{4};
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::ReadWriteSgpr4, TestOpcode::UseSgpr4, TestOpcode::End}, extra_leaders);
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
ASSERT_EQ(blocks.size(), 2u);
|
|
const Instruction &read_write = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(read_write, {RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(liveness.block_liveness(*blocks[0]).live_out.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, PartialDefKeepsRegisterLiveBeforeInstruction) {
|
|
auto blocks = build_test_blocks({TestOpcode::PartialDefSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &partial_def = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(partial_def, {RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, PartialDefRegisterStaysLiveOutWhenUsedBySuccessor) {
|
|
std::array<uint64_t, 1> extra_leaders{4};
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::PartialDefSgpr4, TestOpcode::UseSgpr4, TestOpcode::End}, extra_leaders);
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
ASSERT_EQ(blocks.size(), 2u);
|
|
const Instruction &partial_def = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(partial_def, {RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(liveness.block_liveness(*blocks[0]).live_out.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, FullWidthDefKillsRegisterBeforeInstruction) {
|
|
auto blocks = build_test_blocks({TestOpcode::DefSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &def = *blocks[0]->instructions().begin();
|
|
EXPECT_FALSE(liveness.is_live_before(def, {RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ImplicitUseIsLiveBeforeInstruction) {
|
|
auto blocks = build_test_blocks({TestOpcode::ImplicitUseSgpr6Pair, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &implicit_use = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(implicit_use, {RegClass::SGPR, 6, 2}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, PredicatedScalarDefDoesNotKillLiveOutValue) {
|
|
auto blocks =
|
|
build_test_blocks({TestOpcode::PredicatedDefSgpr4, TestOpcode::UseSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &pred_def = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(pred_def, {RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, LoopCarriedUseRevisitsBackEdgePredecessor) {
|
|
auto blocks = build_test_blocks({TestOpcode::DefSgpr4, TestOpcode::UseSgpr4,
|
|
TestOpcode::CBranchBackToUseSgpr4, TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
auto *entry = block_starting_at(blocks, 0);
|
|
auto *loop = block_starting_at(blocks, 4);
|
|
ASSERT_NE(entry, nullptr);
|
|
ASSERT_NE(loop, nullptr);
|
|
EXPECT_TRUE(liveness.block_liveness(*entry).live_out.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(liveness.block_liveness(*loop).live_in.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(liveness.block_liveness(*loop).live_out.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, BranchMeetKeepsValueLiveWhenOneSuccessorPreservesIt) {
|
|
auto blocks = build_test_blocks({TestOpcode::CBranchToElseAfterTwo, TestOpcode::DefSgpr4,
|
|
TestOpcode::BranchToJoin, TestOpcode::Nop, TestOpcode::UseSgpr4,
|
|
TestOpcode::End});
|
|
LivenessAnalysis liveness = analyze_scope(blocks);
|
|
|
|
const Instruction &branch = *blocks[0]->instructions().begin();
|
|
EXPECT_TRUE(liveness.is_live_before(branch, {RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(liveness.block_liveness(*blocks[0]).live_out.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(LivenessAnalysis, ExplicitBlockSubsetIgnoresOutsideSuccessors) {
|
|
std::array<uint64_t, 1> kernel_entries{8};
|
|
auto blocks = build_test_blocks(
|
|
{TestOpcode::DefVgpr0, TestOpcode::Nop, TestOpcode::UseVgpr0, TestOpcode::End},
|
|
kernel_entries);
|
|
|
|
auto *kernel0 = block_starting_at(blocks, 0);
|
|
ASSERT_NE(kernel0, nullptr);
|
|
ASSERT_EQ(kernel0->successors().size(), 1u);
|
|
ASSERT_EQ(kernel0->successors()[0]->start_offset(), 8u);
|
|
|
|
const Instruction &def = *kernel0->instructions().begin();
|
|
LivenessAnalysis all_decoded_liveness = analyze_scope(blocks);
|
|
EXPECT_TRUE(all_decoded_liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
|
|
std::vector<BasicBlock *> kernel_blocks{kernel0};
|
|
const ExecMaskAnalysis kernel_exec(KernelBlockScope(kernel_blocks), /*wave_size=*/64);
|
|
LivenessAnalysis kernel_liveness{KernelBlockScope(kernel_blocks),
|
|
std::make_unique<ExecMaskAnalysis>(kernel_exec)};
|
|
EXPECT_FALSE(kernel_liveness.is_live_before(def, {RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
TEST(InstDefUse, DstOnlyVgpr) {
|
|
const TestInstruction test_inst("test_def_v0", {{RegClass::VGPR, 0, 1}});
|
|
InstDefUse idu(test_inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 0, 1}));
|
|
}
|
|
|
|
TEST(InstDefUse, SrcOnlySgpr) {
|
|
const TestInstruction test_inst("test_use_s4", {}, {{RegClass::SGPR, 4, 1}});
|
|
InstDefUse idu(test_inst);
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(InstDefUse, RWSgpr) {
|
|
const TestInstruction test_inst("test_rw_s4", {{RegClass::SGPR, 4, 1}}, {{RegClass::SGPR, 4, 1}});
|
|
InstDefUse idu(test_inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(InstDefUse, PartialDefIsAlsoUse) {
|
|
const TestInstruction test_inst("test_partial_def_s4", {{RegClass::SGPR, 4, 1}}, {}, 0,
|
|
std::nullopt, {}, std::nullopt, std::nullopt,
|
|
/*def_size_bits=*/16);
|
|
InstDefUse idu(test_inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(InstDefUse, FullWidthDefIsNotUse) {
|
|
const TestInstruction test_inst("test_def_s4", {{RegClass::SGPR, 4, 1}}, {}, 0, std::nullopt, {},
|
|
std::nullopt, std::nullopt, /*def_size_bits=*/32);
|
|
InstDefUse idu(test_inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::SGPR, 4, 1}));
|
|
}
|
|
|
|
TEST(InstDefUse, Predicated) {
|
|
const TestInstruction test_inst("test_pred_def_s4", {{RegClass::SGPR, 4, 1}}, {}, PREDICATED_DEF);
|
|
InstDefUse idu(test_inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 4, 1}));
|
|
EXPECT_TRUE(idu.has_predicated_def);
|
|
}
|
|
|
|
// --- Generated VOP1 SDWA/DPP destination-preserve reads (real decode) ---
|
|
//
|
|
// SDWA dst_unused:PRESERVE and a partial DPP row/bank mask both keep the old
|
|
// vdst value, so the decoded instruction must report vdst as an implicit use.
|
|
// InstDefUse is the per-instruction def/use set LivenessAnalysis consumes (it
|
|
// calls Instruction::implicit_uses), so a use surfacing here is exactly what
|
|
// reaches liveness -- see ImplicitUseIsLiveBeforeInstruction for that step.
|
|
//
|
|
// CDNA4 VOP1 word0: encoding[31:25]=0x3F, vdst[24:17], op[16:9]=1 (v_mov_b32),
|
|
// src0[8:0]=marker (250=SRC_DPP, 249=SRC_SDWA).
|
|
constexpr uint32_t kVop1MovWord0Dpp = (0x3Fu << 25) | (5u << 17) | (1u << 9) | 250u;
|
|
constexpr uint32_t kVop1MovWord0Sdwa = (0x3Fu << 25) | (5u << 17) | (1u << 9) | 249u;
|
|
|
|
std::unique_ptr<Instruction> decode_cdna4(const std::array<uint32_t, 2> &words) {
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
|
|
return std::unique_ptr<Instruction>(decoder ? decoder->decode(words.data()) : nullptr);
|
|
}
|
|
|
|
std::unique_ptr<Instruction> decode_gfx1250(const std::array<uint32_t, 2> &words) {
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
return std::unique_ptr<Instruction>(decoder ? decoder->decode(words.data()) : nullptr);
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Gfx1250Vop3CompareDefinesOneSgpr) {
|
|
// v_cmp_eq_u32_e64 s53, 32, v4. gfx1250 is wave32-only, so the comparison
|
|
// mask occupies s53 and must not make liveness treat the adjacent s54 as
|
|
// clobbered.
|
|
auto inst = decode_gfx1250({0xD44A0035u, 0x020208A0u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(inst->mnemonic(), "v_cmp_eq_u32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 53, 1}));
|
|
EXPECT_FALSE(idu.defs.contains({RegClass::SGPR, 54, 1}));
|
|
}
|
|
|
|
// DPP word1 fields (CDNA4): vsrc0[7:0], dpp_ctrl[16:8], bound_ctrl[19],
|
|
// bank_mask[27:24], row_mask[31:28]. With full masks, whether vdst is
|
|
// preserved depends on bound_ctrl and whether dpp_ctrl crosses a row/wave
|
|
// edge: bound_ctrl=0 + an edge-crossing ctrl leaves OOB lanes unwritten (reads
|
|
// vdst); bound_ctrl=1 writes a zero source instead (full write); a ctrl that
|
|
// never goes OOB is a full write regardless of bound_ctrl.
|
|
constexpr uint32_t kDppFullMasks = (0xFu << 28) | (0xFu << 24);
|
|
constexpr uint32_t kDppBoundCtrl = (1u << 19);
|
|
constexpr uint32_t kDppCtrlRowShr1 = 0x111u << 8; // row_shr:1 -- crosses the row edge
|
|
constexpr uint32_t kDppCtrlRowRor1 = 0x121u << 8; // row_ror:1 -- rotates within the row
|
|
|
|
TEST(GeneratedInstDefUse, DppPartialRowMaskReadsDestination) {
|
|
// DPP word1: row_mask[31:28]=0x7 (partial), bank_mask[27:24]=0xF, vsrc0[7:0]=2.
|
|
auto inst = decode_cdna4({kVop1MovWord0Dpp, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 9), "v_mov_b32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, DppFullRowMaskDoesNotReadDestination) {
|
|
// DPP word1: row_mask=0xF, bank_mask=0xF (full), dpp_ctrl=0 (quad_perm, never
|
|
// OOB) -> every lane written, no vdst read even with bound_ctrl=0.
|
|
auto inst = decode_cdna4({kVop1MovWord0Dpp, (0xFu << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, SdwaPreserveReadsDestination) {
|
|
// SDWA word1: vsrc0[7:0]=2, dst_sel[10:8]=0 (BYTE_0, != DWORD),
|
|
// dst_unused[12:11]=2 (UNUSED_PRESERVE).
|
|
auto inst = decode_cdna4({kVop1MovWord0Sdwa, (2u << 11) | (0u << 8) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, SdwaPadDoesNotReadDestination) {
|
|
// SDWA word1: dst_sel[10:8]=0, dst_unused[12:11]=0 (UNUSED_PAD) -> no read.
|
|
auto inst = decode_cdna4({kVop1MovWord0Sdwa, (0u << 11) | (0u << 8) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, DppBoundCtrlZeroEdgeCrossingReadsDestination) {
|
|
// Full masks, bound_ctrl=0, row_shr:1 -> row-edge lanes read OOB and are left
|
|
// unwritten, preserving vdst.
|
|
auto inst = decode_cdna4({kVop1MovWord0Dpp, kDppFullMasks | kDppCtrlRowShr1 | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, DppBoundCtrlOneEdgeCrossingDoesNotReadDestination) {
|
|
// Full masks, row_shr:1 but bound_ctrl=1 -> OOB lanes read a zero source and
|
|
// are still written, so every lane is defined and vdst is not read.
|
|
auto inst =
|
|
decode_cdna4({kVop1MovWord0Dpp, kDppFullMasks | kDppBoundCtrl | kDppCtrlRowShr1 | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, DppBoundCtrlZeroRotateDoesNotReadDestination) {
|
|
// Full masks, bound_ctrl=0, row_ror:1 -> a rotate never goes OOB, so every
|
|
// lane is written and vdst is not read despite bound_ctrl=0.
|
|
auto inst = decode_cdna4({kVop1MovWord0Dpp, kDppFullMasks | kDppCtrlRowRor1 | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop1DppPartialMaskReadsFullWidthDestination) {
|
|
// v_cvt_f64_i32_e32 writes a VGPR pair (v[6:7]). A partial DPP row mask
|
|
// preserves the whole 64-bit destination, so the implicit use must match the
|
|
// width-2 def -- not just the low dword.
|
|
// CDNA4 VOP1 word0: encoding[31:25]=0x3F, vdst[24:17]=6, op[16:9]=4
|
|
// (v_cvt_f64_i32), src0[8:0]=250 (SRC_DPP).
|
|
constexpr uint32_t kVop1CvtF64I32Word0Dpp = (0x3Fu << 25) | (6u << 17) | (4u << 9) | 250u;
|
|
auto inst = decode_cdna4({kVop1CvtF64I32Word0Dpp, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 13), "v_cvt_f64_i32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 6, 2}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 6, 2}));
|
|
}
|
|
|
|
// --- Generated VOP2 SDWA/DPP destination-preserve reads (real decode) ---
|
|
//
|
|
// VOP2 shares VOP1's destination-preserve rules: SDWA dst_unused:PRESERVE and a
|
|
// partial DPP row/bank mask both keep the old vdst value, so the decoded
|
|
// instruction must report vdst as an implicit use (see Vop2::implicit_uses,
|
|
// which mirrors Vop1::implicit_uses). These cases mimic the VOP1 tests above but
|
|
// exercise the VOP2 encoding path.
|
|
//
|
|
// CDNA4 VOP2 word0: encoding[31]=0, op[30:25]=1 (v_add_f32), vdst[24:17],
|
|
// vsrc1[16:9], src0[8:0]=marker (250=SRC_DPP, 249=SRC_SDWA). The DPP/SDWA word1
|
|
// layouts are identical to VOP1, so the second-word bit fields are reused.
|
|
constexpr uint32_t kVop2AddWord0Dpp = (0u << 31) | (1u << 25) | (5u << 17) | (3u << 9) | 250u;
|
|
constexpr uint32_t kVop2AddWord0Sdwa = (0u << 31) | (1u << 25) | (5u << 17) | (3u << 9) | 249u;
|
|
|
|
TEST(GeneratedInstDefUse, Vop2DppPartialRowMaskReadsDestination) {
|
|
// DPP word1: row_mask[31:28]=0x7 (partial), bank_mask[27:24]=0xF, vsrc0[7:0]=2.
|
|
auto inst = decode_cdna4({kVop2AddWord0Dpp, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 9), "v_add_f32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop2DppFullRowMaskDoesNotReadDestination) {
|
|
// DPP word1: row_mask=0xF, bank_mask=0xF (full), dpp_ctrl=0 (quad_perm, never
|
|
// OOB) -> every lane written, no vdst read even with bound_ctrl=0.
|
|
auto inst = decode_cdna4({kVop2AddWord0Dpp, (0xFu << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop2SdwaPreserveReadsDestination) {
|
|
// SDWA word1: vsrc0[7:0]=2, dst_sel[10:8]=0 (BYTE_0, != DWORD),
|
|
// dst_unused[12:11]=2 (UNUSED_PRESERVE).
|
|
auto inst = decode_cdna4({kVop2AddWord0Sdwa, (2u << 11) | (0u << 8) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop2SdwaPadDoesNotReadDestination) {
|
|
// SDWA word1: dst_sel[10:8]=0, dst_unused[12:11]=0 (UNUSED_PAD) -> no read.
|
|
auto inst = decode_cdna4({kVop2AddWord0Sdwa, (0u << 11) | (0u << 8) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop2DppBoundCtrlZeroEdgeCrossingReadsDestination) {
|
|
// Full masks, bound_ctrl=0, row_shr:1 -> row-edge lanes read OOB and are left
|
|
// unwritten, preserving vdst (mirrors the VOP1 case on the VOP2 path).
|
|
auto inst = decode_cdna4({kVop2AddWord0Dpp, kDppFullMasks | kDppCtrlRowShr1 | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop2DppBoundCtrlOneEdgeCrossingDoesNotReadDestination) {
|
|
// Full masks, row_shr:1 but bound_ctrl=1 -> OOB lanes read zero and are still
|
|
// written, so every lane is defined and vdst is not read.
|
|
auto inst =
|
|
decode_cdna4({kVop2AddWord0Dpp, kDppFullMasks | kDppBoundCtrl | kDppCtrlRowShr1 | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
// --- Generated VOP3 DPP destination-preserve reads (real decode) ---
|
|
//
|
|
// VOP3 gained DPP on gfx11+ (RDNA3/RDNA4/gfx1250) and has no SDWA, so only the
|
|
// partial-DPP path applies. Unlike VOP1/VOP2 the VOP3 vdst field can name an
|
|
// SGPR: a VOP3-re-encoded compare (v_cmp_*_e64) writes its lane mask to an SGPR
|
|
// through vdst. So Vop3::implicit_uses derives the preserved ref from the
|
|
// decoded destination operand rather than assuming VGPR -- these cases exercise
|
|
// both a VGPR-dest op and an SGPR-dest compare. VOP3 is not in CDNA, so these
|
|
// decode for RDNA4.
|
|
//
|
|
// RDNA4 VOP3 word0: encoding[31:26]=53, op[25:16], clamp[15], opsel[14:11],
|
|
// abs[10:8], vdst[7:0]. word1: src0[8:0]=marker (250=SRC_DPP), src1[17:9]. The
|
|
// DPP16 word2 layout matches VOP1/VOP2, so its bit fields are reused.
|
|
constexpr uint32_t kVop3Enc = 53u << 26;
|
|
constexpr uint32_t kVop3AddF32Op = 259u << 16; // v_add_f32_e64 (VGPR vdst)
|
|
constexpr uint32_t kVop3CmpLtF32Op = 17u << 16; // v_cmp_lt_f32_e64 (SGPR vdst)
|
|
// word1: src0=SRC_DPP, src1=VGPR3.
|
|
constexpr uint32_t kVop3DppWord1 = (3u << 9) | 250u;
|
|
|
|
std::unique_ptr<Instruction> decode_rdna4(const std::array<uint32_t, 3> &words) {
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_RDNA4);
|
|
return std::unique_ptr<Instruction>(decoder ? decoder->decode(words.data()) : nullptr);
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop3DppPartialRowMaskReadsVgprDestination) {
|
|
// v_add_f32_e64 (VGPR vdst=5), DPP word2: row_mask=0x7 (partial), bank_mask=0xF.
|
|
auto inst = decode_rdna4(
|
|
{kVop3Enc | kVop3AddF32Op | 5u, kVop3DppWord1, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 9), "v_add_f32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop3DppFullRowMaskDoesNotReadDestination) {
|
|
// Full masks, dpp_ctrl=0 (quad_perm, never OOB) -> every lane written.
|
|
auto inst = decode_rdna4(
|
|
{kVop3Enc | kVop3AddF32Op | 5u, kVop3DppWord1, (0xFu << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop3DppBoundCtrlZeroEdgeCrossingReadsDestination) {
|
|
// Full masks, bound_ctrl=0, row_shr:1 -> row-edge lanes read OOB and are left
|
|
// unwritten, preserving the VGPR vdst.
|
|
auto inst = decode_rdna4(
|
|
{kVop3Enc | kVop3AddF32Op | 5u, kVop3DppWord1, kDppFullMasks | kDppCtrlRowShr1 | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop3CmpDppPartialRowMaskDoesNotReadDestination) {
|
|
// v_cmp_lt_f32_e64 writes its lane mask to an SGPR pair via the vdst field
|
|
// (s[8:9]). The executor's non-VOPC DPP restore only touches the VGPR file at
|
|
// inst_.vdst -- a no-op that writes back the saved value -- and does NOT
|
|
// preserve the SGPR mask, which is fully written. So a partial mask reads
|
|
// neither the SGPR nor a VGPR, matching implicit_uses filtering to VGPR.
|
|
auto inst = decode_rdna4(
|
|
{kVop3Enc | kVop3CmpLtF32Op | 8u, kVop3DppWord1, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 9), "v_cmp_lt_");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 8, 2}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::SGPR, 8, 2}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::VGPR, 8, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop3pDppPartialRowMaskReadsDestination) {
|
|
// v_dot2_f32_f16 (VOP3P, VGPR vdst=6) has an explicit DPP encoding. A
|
|
// partial row mask preserves the packed VGPR destination.
|
|
// RDNA4 VOP3P word0: encoding[31:24]=204, op[22:16]=19 (v_dot2_f32_f16),
|
|
// vdst[7:0]=6. word1: src0[8:0]=250 (SRC_DPP), src1[17:9]=3 (VGPR3).
|
|
constexpr uint32_t kVop3pDot2Word0 = (204u << 24) | (19u << 16) | 6u;
|
|
constexpr uint32_t kVop3pDppWord1 = (3u << 9) | 250u;
|
|
auto inst = decode_rdna4({kVop3pDot2Word0, kVop3pDppWord1, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 14), "v_dot2_f32_f16");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 6, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 6, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, Vop3SdstEncDppPartialRowMaskReadsOnlyVgprResult) {
|
|
// v_add_co_ci_u32_e64 (VOP3_SDST_ENC) writes TWO destinations: a VGPR result
|
|
// (v6) and an SGPR carry-out (s[8:9]). The executor's DPP restore preserves
|
|
// only the VGPR result (write_vgpr); the SGPR carry is fully written, so only
|
|
// the VGPR surfaces as a use -- implicit_uses filters to RegClass::VGPR.
|
|
// RDNA4 VOP3_SDST_ENC word0: encoding[31:26]=53, op[25:16]=288, sdst[14:8]=8,
|
|
// vdst[7:0]=6. word1: src0=250 (SRC_DPP), src1[17:9]=3, src2[26:18]=10 (carry).
|
|
constexpr uint32_t kVop3SdstWord0 = (53u << 26) | (288u << 16) | (8u << 8) | 6u;
|
|
constexpr uint32_t kVop3SdstWord1 = (10u << 18) | (3u << 9) | 250u;
|
|
auto inst = decode_rdna4({kVop3SdstWord0, kVop3SdstWord1, (0x7u << 28) | (0xFu << 24) | 2u});
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()).substr(0, 14), "v_add_co_ci_u3");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 6, 1}));
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::SGPR, 8, 2}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 6, 1}));
|
|
EXPECT_FALSE(idu.uses.contains({RegClass::SGPR, 8, 2}));
|
|
}
|
|
|
|
// v_writelane_b32 writes one lane and preserves the rest of vdst, so it reads
|
|
// the old value. On CDNA4/gfx1250 (XML marks vdst output-only) that read is
|
|
// surfaced via implicit_uses. word0 = VOP3 writelane opcode, vdst=5; word1 =
|
|
// src0=s4, src1=s2 (2 << 9).
|
|
TEST(GeneratedInstDefUse, WritelaneReadsDestinationCdna4) {
|
|
auto inst = decode_cdna4({0xD28A0005U, 0x00000404U}); // v_writelane_b32 v5, s4, s2
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()), "v_writelane_b32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
TEST(GeneratedInstDefUse, WritelaneReadsDestinationGfx1250) {
|
|
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250);
|
|
ASSERT_NE(decoder, nullptr);
|
|
std::array<uint32_t, 4> words{0xD7610005U, 0x00000404U, 0U, 0U};
|
|
std::unique_ptr<Instruction> inst(decoder->decode(words.data()));
|
|
ASSERT_NE(inst, nullptr);
|
|
ASSERT_EQ(std::string_view(inst->mnemonic()), "v_writelane_b32");
|
|
|
|
InstDefUse idu(*inst);
|
|
EXPECT_TRUE(idu.defs.contains({RegClass::VGPR, 5, 1}));
|
|
EXPECT_TRUE(idu.uses.contains({RegClass::VGPR, 5, 1}));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace rocjitsu
|