// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 to_register_ref() const override { return ref_; } std::optional literal64_value() const override { return lit_; } std::optional 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 ref_; std::optional lit_; std::optional 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, `. TestInstruction(std::string_view mnemonic, std::initializer_list defs = {}, std::initializer_list uses = {}, uint64_t flags = 0, std::optional branch_delta = std::nullopt, std::initializer_list implicit_uses = {}, std::optional literal_src = std::nullopt, std::optional 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 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 dst_storage_{}; std::array src_storage_{}; std::vector implicit_uses_; std::optional branch_delta_; }; class TestTextSection : public Section { public: TestTextSection(std::unique_ptr 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 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(byte_size); std::memcpy(data.get(), words.data(), byte_size); sections_.push_back(std::make_unique(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(*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, : 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> build_test_blocks(std::vector ops, std::span extra_leaders = {}) { std::vector words; words.reserve(ops.size()); for (TestOpcode op : ops) words.push_back(static_cast(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> &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 block_scope(const std::vector> &blocks) { std::vector 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 text_span(const CodeObject &co) { const Section *text = co.text_sections().front(); return {reinterpret_cast(text->data()), text->size()}; } LivenessAnalysis analyze_scope(const std::vector> &blocks) { auto scope = block_scope(blocks); const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64); return LivenessAnalysis(KernelBlockScope(scope), std::make_unique(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> &blocks, std::span extra_edges) { auto scope = block_scope(blocks); const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64, extra_edges); return LivenessAnalysis(KernelBlockScope(scope), std::make_unique(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(ssrc0), .sdst = static_cast(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(ssrc0), .ssrc1 = static_cast(ssrc1), .sdst = static_cast(sdst)})[0]; } uint32_t pack_sopc(uint16_t op, uint16_t ssrc0, uint16_t ssrc1) { return cdna3::build_sopc( op, {.ssrc0 = static_cast(ssrc0), .ssrc1 = static_cast(ssrc1)})[0]; } uint32_t build_s_call_b64(uint16_t sdst, int16_t simm16) { return cdna3::build_sopk(cdna3::kSCallB64Sopk, {.simm16 = static_cast(simm16), .sdst = static_cast(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 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(&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(kAtomicReturn, 2, 1); expect_gfx1250_buffer_cmpswap_def_use(kAtomicNoReturn, 2, 0); expect_gfx1250_buffer_cmpswap_def_use(kAtomicReturn, 4, 2); expect_gfx1250_buffer_cmpswap_def_use(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(&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(&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 kWritelaneV141S4Lane2 = {0xd28a008du, 0x00010404u}; auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4); std::unique_ptr 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 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 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(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 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 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 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 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 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 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{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 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 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 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 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 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 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 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 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 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 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(-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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 clobbered_sregs{kPcSreg, kPcSreg + 1}; for (uint16_t clobbered_sreg : clobbered_sregs) { SCOPED_TRACE(clobbered_sreg); std::vector 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 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 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 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 getpc_offsets{fixups[0].source_getpc_offset, fixups[1].source_getpc_offset}; std::ranges::sort(getpc_offsets); EXPECT_EQ(getpc_offsets, (std::vector{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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(-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 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 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 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 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 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 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 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(sec->data()); const size_t inst_data_size = sec->size() / sizeof(uint32_t); std::vector> 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 inst(decoder->decode(&inst_data[pc], byte_offset)); ASSERT_NE(inst, nullptr); const uint32_t inst_words = static_cast(inst->size()) / sizeof(uint32_t); byte_offset += inst->size(); pc += inst_words; owned.push_back(std::move(inst)); } std::vector decoded_insts; decoded_insts.reserve(owned.size()); for (const auto &inst : owned) decoded_insts.push_back(inst.get()); const auto text = std::span(reinterpret_cast(sec->data()), sec->size()); const auto fixups = discover_indirect_branch_edges( std::span(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(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(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(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(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(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(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(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(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(1 + bank * 256); EXPECT_EQ(liveness.find_globally_unused_vgpr_run(&*instruction, 1, candidate, 1, static_cast(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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 insts_of(BasicBlock &block) { std::vector 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> build_cdna4_blocks(std::vector 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(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 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 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(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(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(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(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(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(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 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 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 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 kernel_blocks{kernel0}; const ExecMaskAnalysis kernel_exec(KernelBlockScope(kernel_blocks), /*wave_size=*/64); LivenessAnalysis kernel_liveness{KernelBlockScope(kernel_blocks), std::make_unique(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 decode_cdna4(const std::array &words) { auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4); return std::unique_ptr(decoder ? decoder->decode(words.data()) : nullptr); } std::unique_ptr decode_gfx1250(const std::array &words) { auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_GFX1250); return std::unique_ptr(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 decode_rdna4(const std::array &words) { auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_RDNA4); return std::unique_ptr(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 words{0xD7610005U, 0x00000404U, 0U, 0U}; std::unique_ptr 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