[llvm] [AMDGPU] Add support for the llvm.frameaddress intrinsic (PR #205588)

Joseph Huber via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 28 09:51:13 PDT 2026


https://github.com/jhuber6 updated https://github.com/llvm/llvm-project/pull/205588

>From 3fa191af2a7b205db3904f26dfe0c2a28e733268 Mon Sep 17 00:00:00 2001
From: Joseph Huber <huberjn at outlook.com>
Date: Wed, 24 Jun 2026 10:22:53 -0500
Subject: [PATCH] [AMDGPU] Add support for the llvm.frameaddress intrinsic

Summary:
This is pretty much identical to the returnaddress case, except that it
returns a frame pointer in the 'stack' instead of a program counter
location.

This closes a gap and also should give us the tools needed to further
munge around with CFA stacks on the device. The motivation is richer
stack traces without relying on rocgdb.

New Test

comments

DVGPR and simplification.
---
 .../AMDGPU/AMDGPUInstructionSelector.cpp      | 33 +++++++
 .../Target/AMDGPU/AMDGPUInstructionSelector.h |  1 +
 .../AMDGPU/AMDGPURegBankLegalizeRules.cpp     |  2 +
 .../Target/AMDGPU/AMDGPURegisterBankInfo.cpp  |  3 +-
 llvm/lib/Target/AMDGPU/SIISelLowering.cpp     | 22 +++++
 llvm/lib/Target/AMDGPU/SIISelLowering.h       |  1 +
 .../test/CodeGen/AMDGPU/frameaddress-dvgpr.ll | 44 +++++++++
 llvm/test/CodeGen/AMDGPU/frameaddress.ll      | 92 +++++++++++++++++++
 8 files changed, 197 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/CodeGen/AMDGPU/frameaddress-dvgpr.ll
 create mode 100644 llvm/test/CodeGen/AMDGPU/frameaddress.ll

diff --git a/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.cpp b/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.cpp
index 6c4e7fc629307..1f124fcc7129f 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.cpp
@@ -1290,6 +1290,8 @@ bool AMDGPUInstructionSelector::selectG_INTRINSIC(MachineInstr &I) const {
     return selectGroupStaticSize(I);
   case Intrinsic::returnaddress:
     return selectReturnAddress(I);
+  case Intrinsic::frameaddress:
+    return selectFrameAddress(I);
   case Intrinsic::amdgcn_smfmac_f32_16x16x32_f16:
   case Intrinsic::amdgcn_smfmac_f32_32x32x16_f16:
   case Intrinsic::amdgcn_smfmac_f32_16x16x32_bf16:
@@ -1878,6 +1880,37 @@ bool AMDGPUInstructionSelector::selectReturnAddress(MachineInstr &I) const {
   return true;
 }
 
+bool AMDGPUInstructionSelector::selectFrameAddress(MachineInstr &I) const {
+  MachineBasicBlock *MBB = I.getParent();
+  MachineFunction &MF = *MBB->getParent();
+  const DebugLoc &DL = I.getDebugLoc();
+
+  MachineOperand &Dst = I.getOperand(0);
+  Register DstReg = Dst.getReg();
+  unsigned Depth = I.getOperand(2).getImm();
+
+  const TargetRegisterClass *RC =
+      TRI.getConstrainedRegClassForOperand(Dst, *MRI);
+  if (!RBI.constrainGenericRegister(DstReg, *RC, *MRI))
+    return false;
+
+  Register FrameReg = TRI.getFrameRegister(MF);
+  // Entry functions without a reserved FP have scratch base 0. Setting
+  // FrameAddressIsTaken here would force hasFP with FP_REG still unset.
+  if (Depth != 0 ||
+      (MF.getInfo<SIMachineFunctionInfo>()->isEntryFunction() && !FrameReg)) {
+    BuildMI(*MBB, &I, DL, TII.get(AMDGPU::S_MOV_B32), DstReg).addImm(0);
+    I.eraseFromParent();
+    return true;
+  }
+
+  MF.getFrameInfo().setFrameAddressIsTaken(true);
+  FrameReg = TRI.getFrameRegister(MF);
+  BuildMI(*MBB, &I, DL, TII.get(AMDGPU::COPY), DstReg).addReg(FrameReg);
+  I.eraseFromParent();
+  return true;
+}
+
 bool AMDGPUInstructionSelector::selectEndCfIntrinsic(MachineInstr &MI) const {
   // FIXME: Manually selecting to avoid dealing with the SReg_1 trick
   // SelectionDAG uses for wave32 vs wave64.
diff --git a/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.h b/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.h
index 1fb7a231a6829..3e5e0fe592f2d 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.h
+++ b/llvm/lib/Target/AMDGPU/AMDGPUInstructionSelector.h
@@ -118,6 +118,7 @@ class AMDGPUInstructionSelector final : public InstructionSelector {
   bool selectRelocConstant(MachineInstr &I) const;
   bool selectGroupStaticSize(MachineInstr &I) const;
   bool selectReturnAddress(MachineInstr &I) const;
+  bool selectFrameAddress(MachineInstr &I) const;
   bool selectG_INTRINSIC(MachineInstr &I) const;
 
   bool selectEndCfIntrinsic(MachineInstr &MI) const;
diff --git a/llvm/lib/Target/AMDGPU/AMDGPURegBankLegalizeRules.cpp b/llvm/lib/Target/AMDGPU/AMDGPURegBankLegalizeRules.cpp
index bd21d5ca650dc..c932e591816a8 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPURegBankLegalizeRules.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPURegBankLegalizeRules.cpp
@@ -1777,6 +1777,8 @@ RegBankLegalizeRules::RegBankLegalizeRules(const GCNSubtarget &_ST,
 
   addRulesForIOpcs({returnaddress}).Any({{UniP0}, {{SgprP0}, {}}});
 
+  addRulesForIOpcs({frameaddress}).Any({{UniP5}, {{SgprP5}, {}}});
+
   // Note: amdgcn.icmp with i1 inputs is legalized to ballot in the legalizer,
   // so no S1 rules are needed here.
   addRulesForIOpcs({amdgcn_icmp})
diff --git a/llvm/lib/Target/AMDGPU/AMDGPURegisterBankInfo.cpp b/llvm/lib/Target/AMDGPU/AMDGPURegisterBankInfo.cpp
index a9ee4dbe48316..b1ed24281bd8f 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPURegisterBankInfo.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPURegisterBankInfo.cpp
@@ -4934,7 +4934,8 @@ AMDGPURegisterBankInfo::getInstrMapping(const MachineInstr &MI) const {
     case Intrinsic::amdgcn_s_getpc:
     case Intrinsic::amdgcn_groupstaticsize:
     case Intrinsic::amdgcn_reloc_constant:
-    case Intrinsic::returnaddress: {
+    case Intrinsic::returnaddress:
+    case Intrinsic::frameaddress: {
       unsigned Size = MRI.getType(MI.getOperand(0).getReg()).getSizeInBits();
       OpdsMapping[0] = AMDGPU::getValueMapping(AMDGPU::SGPRRegBankID, Size);
       break;
diff --git a/llvm/lib/Target/AMDGPU/SIISelLowering.cpp b/llvm/lib/Target/AMDGPU/SIISelLowering.cpp
index d9810e3d9fbd9..6066f602336ec 100644
--- a/llvm/lib/Target/AMDGPU/SIISelLowering.cpp
+++ b/llvm/lib/Target/AMDGPU/SIISelLowering.cpp
@@ -7646,6 +7646,8 @@ SDValue SITargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG) const {
     return LowerBRCOND(Op, DAG);
   case ISD::RETURNADDR:
     return LowerRETURNADDR(Op, DAG);
+  case ISD::FRAMEADDR:
+    return LowerFRAMEADDR(Op, DAG);
   case ISD::SPONENTRY:
     return LowerSPONENTRY(Op, DAG);
   case ISD::LOAD: {
@@ -8768,6 +8770,26 @@ SDValue SITargetLowering::LowerRETURNADDR(SDValue Op, SelectionDAG &DAG) const {
   return DAG.getCopyFromReg(DAG.getEntryNode(), DL, Reg, VT);
 }
 
+SDValue SITargetLowering::LowerFRAMEADDR(SDValue Op, SelectionDAG &DAG) const {
+  MVT VT = Op.getSimpleValueType();
+  SDLoc DL(Op);
+  if (Op.getConstantOperandVal(0) != 0)
+    return DAG.getConstant(0, DL, VT);
+
+  MachineFunction &MF = DAG.getMachineFunction();
+  const SIRegisterInfo *TRI = getSubtarget()->getRegisterInfo();
+  Register FrameReg = TRI->getFrameRegister(MF);
+
+  // Entry functions without a reserved FP have scratch base 0. Setting
+  // FrameAddressIsTaken here would force hasFP with FP_REG still unset.
+  if (MF.getInfo<SIMachineFunctionInfo>()->isEntryFunction() && !FrameReg)
+    return DAG.getConstant(0, DL, VT);
+
+  MF.getFrameInfo().setFrameAddressIsTaken(true);
+  FrameReg = TRI->getFrameRegister(MF);
+  return DAG.getCopyFromReg(DAG.getEntryNode(), DL, FrameReg, VT);
+}
+
 SDValue SITargetLowering::LowerSPONENTRY(SDValue Op, SelectionDAG &DAG) const {
   MachineFunction &MF = DAG.getMachineFunction();
   SIMachineFunctionInfo *MFI = MF.getInfo<SIMachineFunctionInfo>();
diff --git a/llvm/lib/Target/AMDGPU/SIISelLowering.h b/llvm/lib/Target/AMDGPU/SIISelLowering.h
index 8ece9f279a100..9e195da3d4ecd 100644
--- a/llvm/lib/Target/AMDGPU/SIISelLowering.h
+++ b/llvm/lib/Target/AMDGPU/SIISelLowering.h
@@ -139,6 +139,7 @@ class SITargetLowering final : public AMDGPUTargetLowering {
   SDValue LowerATOMIC_CMP_SWAP(SDValue Op, SelectionDAG &DAG) const;
   SDValue LowerBRCOND(SDValue Op, SelectionDAG &DAG) const;
   SDValue LowerRETURNADDR(SDValue Op, SelectionDAG &DAG) const;
+  SDValue LowerFRAMEADDR(SDValue Op, SelectionDAG &DAG) const;
   SDValue LowerSPONENTRY(SDValue Op, SelectionDAG &DAG) const;
   SDValue adjustLoadValueType(unsigned Opcode, MemSDNode *M,
                               SelectionDAG &DAG, ArrayRef<SDValue> Ops,
diff --git a/llvm/test/CodeGen/AMDGPU/frameaddress-dvgpr.ll b/llvm/test/CodeGen/AMDGPU/frameaddress-dvgpr.ll
new file mode 100644
index 0000000000000..a8fccdfc99293
--- /dev/null
+++ b/llvm/test/CodeGen/AMDGPU/frameaddress-dvgpr.ll
@@ -0,0 +1,44 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
+; RUN: llc -global-isel=0 -mtriple=amdgcn--amdpal -mcpu=gfx1250 < %s | FileCheck %s --check-prefix=CHECK
+; RUN: llc -global-isel=1 -mtriple=amdgcn--amdpal -mcpu=gfx1250 < %s | FileCheck %s --check-prefix=CHECK
+
+define amdgpu_cs ptr addrspace(5) @frameaddr_cs_dvgpr(i32 %val) #0 {
+; CHECK-LABEL: frameaddr_cs_dvgpr:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_setreg_imm32_b32 hwreg(HW_REG_WAVE_MODE, 25, 1), 1 ; msbs: dst=0 src0=0 src1=0 src2=0
+; CHECK-NEXT:    s_mov_b64 s[64:65], 0
+; CHECK-NEXT:    v_nop
+; CHECK-NEXT:    global_prefetch_b8 v0, s[64:65] scope:SCOPE_SE
+; CHECK-NEXT:    s_getreg_b32 s33, hwreg(HW_REG_WAVE_HW_ID2, 8, 2)
+; CHECK-NEXT:    s_delay_alu instid0(SALU_CYCLE_1) | instskip(SKIP_1) | instid1(SALU_CYCLE_1)
+; CHECK-NEXT:    s_cmp_lg_u32 0, s33
+; CHECK-NEXT:    s_cmovk_i32 s33, 0x1c0
+; CHECK-NEXT:    s_mov_b32 s0, s33
+; CHECK-NEXT:    scratch_store_b32 off, v0, s33 scope:SCOPE_SYS
+; CHECK-NEXT:    s_wait_storecnt 0x0
+; CHECK-NEXT:    ; return to shader part epilog
+  %local = alloca i32, addrspace(5)
+  store volatile i32 %val, ptr addrspace(5) %local
+  %fp = call ptr addrspace(5) @llvm.frameaddress.p5(i32 0)
+  ret ptr addrspace(5) %fp
+}
+
+define amdgpu_cs ptr addrspace(5) @frameaddr_cs_no_dvgpr(i32 %val) #1 {
+; CHECK-LABEL: frameaddr_cs_no_dvgpr:
+; CHECK:       ; %bb.0:
+; CHECK-NEXT:    s_setreg_imm32_b32 hwreg(HW_REG_WAVE_MODE, 25, 1), 1 ; msbs: dst=0 src0=0 src1=0 src2=0
+; CHECK-NEXT:    s_mov_b64 s[64:65], 0
+; CHECK-NEXT:    v_nop
+; CHECK-NEXT:    global_prefetch_b8 v0, s[64:65] scope:SCOPE_SE
+; CHECK-NEXT:    s_mov_b32 s0, 0
+; CHECK-NEXT:    scratch_store_b32 off, v0, off scope:SCOPE_SYS
+; CHECK-NEXT:    s_wait_storecnt 0x0
+; CHECK-NEXT:    ; return to shader part epilog
+  %local = alloca i32, addrspace(5)
+  store volatile i32 %val, ptr addrspace(5) %local
+  %fp = call ptr addrspace(5) @llvm.frameaddress.p5(i32 0)
+  ret ptr addrspace(5) %fp
+}
+
+attributes #0 = { nounwind "amdgpu-dynamic-vgpr-block-size"="16" }
+attributes #1 = { nounwind "amdgpu-dynamic-vgpr-block-size"="0" }
diff --git a/llvm/test/CodeGen/AMDGPU/frameaddress.ll b/llvm/test/CodeGen/AMDGPU/frameaddress.ll
new file mode 100644
index 0000000000000..001a47e37779f
--- /dev/null
+++ b/llvm/test/CodeGen/AMDGPU/frameaddress.ll
@@ -0,0 +1,92 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
+; RUN: llc -global-isel=0 -mtriple=amdgcn-amd-amdhsa -mcpu=gfx900 < %s | FileCheck --check-prefix=GCN %s
+; RUN: llc -global-isel=1 -mtriple=amdgcn-amd-amdhsa -mcpu=gfx900 < %s | FileCheck --check-prefix=GCN %s
+
+define ptr addrspace(5) @func1() {
+; GCN-LABEL: func1:
+; GCN:       ; %bb.0: ; %entry
+; GCN-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; GCN-NEXT:    s_mov_b32 s4, s33
+; GCN-NEXT:    s_mov_b32 s33, s32
+; GCN-NEXT:    v_mov_b32_e32 v0, s33
+; GCN-NEXT:    s_mov_b32 s33, s4
+; GCN-NEXT:    s_setpc_b64 s[30:31]
+entry:
+  %0 = tail call ptr addrspace(5) @llvm.frameaddress.p5(i32 0)
+  ret ptr addrspace(5) %0
+}
+
+define ptr addrspace(5) @func2() {
+; GCN-LABEL: func2:
+; GCN:       ; %bb.0: ; %entry
+; GCN-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; GCN-NEXT:    v_mov_b32_e32 v0, 0
+; GCN-NEXT:    s_setpc_b64 s[30:31]
+entry:
+  %0 = tail call ptr addrspace(5) @llvm.frameaddress.p5(i32 1)
+  ret ptr addrspace(5) %0
+}
+
+define amdgpu_kernel void @func3(ptr %out) {
+; GCN-LABEL: func3:
+; GCN:       ; %bb.0: ; %entry
+; GCN-NEXT:    s_load_dwordx2 s[0:1], s[8:9], 0x0
+; GCN-NEXT:    s_add_u32 flat_scratch_lo, s12, s17
+; GCN-NEXT:    s_addc_u32 flat_scratch_hi, s13, 0
+; GCN-NEXT:    v_mov_b32_e32 v2, 0
+; GCN-NEXT:    s_waitcnt lgkmcnt(0)
+; GCN-NEXT:    v_mov_b32_e32 v0, s0
+; GCN-NEXT:    v_mov_b32_e32 v1, s1
+; GCN-NEXT:    flat_store_dword v[0:1], v2
+; GCN-NEXT:    s_endpgm
+entry:
+  %tmp = tail call ptr addrspace(5) @llvm.frameaddress.p5(i32 0)
+  store ptr addrspace(5) %tmp, ptr %out, align 4
+  ret void
+}
+
+declare void @callee()
+
+define void @multi_use() {
+; GCN-LABEL: multi_use:
+; GCN:       ; %bb.0: ; %entry
+; GCN-NEXT:    s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+; GCN-NEXT:    s_mov_b32 s16, s33
+; GCN-NEXT:    s_mov_b32 s33, s32
+; GCN-NEXT:    s_or_saveexec_b64 s[18:19], -1
+; GCN-NEXT:    buffer_store_dword v41, off, s[0:3], s33 offset:4 ; 4-byte Folded Spill
+; GCN-NEXT:    s_mov_b64 exec, s[18:19]
+; GCN-NEXT:    v_writelane_b32 v41, s16, 2
+; GCN-NEXT:    s_addk_i32 s32, 0x400
+; GCN-NEXT:    buffer_store_dword v40, off, s[0:3], s33 ; 4-byte Folded Spill
+; GCN-NEXT:    v_writelane_b32 v41, s30, 0
+; GCN-NEXT:    v_writelane_b32 v41, s31, 1
+; GCN-NEXT:    s_getpc_b64 s[16:17]
+; GCN-NEXT:    s_add_u32 s16, s16, callee at gotpcrel32@lo+4
+; GCN-NEXT:    s_addc_u32 s17, s17, callee at gotpcrel32@hi+12
+; GCN-NEXT:    s_load_dwordx2 s[16:17], s[16:17], 0x0
+; GCN-NEXT:    v_mov_b32_e32 v40, s33
+; GCN-NEXT:    global_store_dword v[0:1], v40, off
+; GCN-NEXT:    s_waitcnt vmcnt(0) lgkmcnt(0)
+; GCN-NEXT:    s_swappc_b64 s[30:31], s[16:17]
+; GCN-NEXT:    global_store_dword v[0:1], v40, off
+; GCN-NEXT:    s_waitcnt vmcnt(0)
+; GCN-NEXT:    buffer_load_dword v40, off, s[0:3], s33 ; 4-byte Folded Reload
+; GCN-NEXT:    v_readlane_b32 s30, v41, 0
+; GCN-NEXT:    v_readlane_b32 s31, v41, 1
+; GCN-NEXT:    s_mov_b32 s32, s33
+; GCN-NEXT:    v_readlane_b32 s4, v41, 2
+; GCN-NEXT:    s_or_saveexec_b64 s[6:7], -1
+; GCN-NEXT:    buffer_load_dword v41, off, s[0:3], s33 offset:4 ; 4-byte Folded Reload
+; GCN-NEXT:    s_mov_b64 exec, s[6:7]
+; GCN-NEXT:    s_mov_b32 s33, s4
+; GCN-NEXT:    s_waitcnt vmcnt(0)
+; GCN-NEXT:    s_setpc_b64 s[30:31]
+entry:
+  %ret0 = tail call ptr addrspace(5) @llvm.frameaddress.p5(i32 0)
+  store volatile ptr addrspace(5) %ret0, ptr addrspace(1) poison
+  call void @callee()
+  %ret1 = tail call ptr addrspace(5) @llvm.frameaddress.p5(i32 0)
+  store volatile ptr addrspace(5) %ret1, ptr addrspace(1) poison
+  ret void
+}



More information about the llvm-commits mailing list