[llvm] [SPIRV] Add support for heap resources (PR #224488)

Helena Kotas via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 17 18:53:09 PDT 2026


https://github.com/hekota updated https://github.com/llvm/llvm-project/pull/224488

>From 603557e71c5c47ec614655ac930cf92d388825aa Mon Sep 17 00:00:00 2001
From: Helena Kotas <hekotas at microsoft.com>
Date: Tue, 8 Sep 2026 13:30:11 -0700
Subject: [PATCH 1/3] Rename pass to SPIRVLegalizeImplicitAndHeapBinding

---
 llvm/lib/Target/SPIRV/CMakeLists.txt          |  2 +-
 llvm/lib/Target/SPIRV/SPIRV.h                 |  8 ++---
 .../Target/SPIRV/SPIRVCodeGenPassBuilder.cpp  |  2 +-
 ...> SPIRVLegalizeImplicitAndHeapBinding.cpp} | 31 ++++++++++---------
 llvm/lib/Target/SPIRV/SPIRVPassRegistry.def   |  2 +-
 llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp  |  4 +--
 .../passes/SPIRVLegalizeImplicitBinding.ll    |  2 +-
 7 files changed, 27 insertions(+), 24 deletions(-)
 rename llvm/lib/Target/SPIRV/{SPIRVLegalizeImplicitBinding.cpp => SPIRVLegalizeImplicitAndHeapBinding.cpp} (89%)

diff --git a/llvm/lib/Target/SPIRV/CMakeLists.txt b/llvm/lib/Target/SPIRV/CMakeLists.txt
index 32026491fdaed..8e22242bb0ebd 100644
--- a/llvm/lib/Target/SPIRV/CMakeLists.txt
+++ b/llvm/lib/Target/SPIRV/CMakeLists.txt
@@ -28,7 +28,7 @@ add_llvm_target(SPIRVCodeGen
   SPIRVGlobalRegistry.cpp
   SPIRVInstrInfo.cpp
   SPIRVInstructionSelector.cpp
-  SPIRVLegalizeImplicitBinding.cpp
+  SPIRVLegalizeImplicitAndHeapBinding.cpp
   SPIRVLegalizeZeroSizeArrays.cpp
   SPIRVLegalizePointerCast.cpp
   SPIRVMergeRegionExitTargets.cpp
diff --git a/llvm/lib/Target/SPIRV/SPIRV.h b/llvm/lib/Target/SPIRV/SPIRV.h
index 8ac55ee28b13f..c2952c0abdea9 100644
--- a/llvm/lib/Target/SPIRV/SPIRV.h
+++ b/llvm/lib/Target/SPIRV/SPIRV.h
@@ -69,13 +69,13 @@ class SPIRVMergeRegionExitTargetsPass
 
 FunctionPass *createSPIRVMergeRegionExitTargetsPass();
 
-class SPIRVLegalizeImplicitBindingPass
-    : public RequiredPassInfoMixin<SPIRVLegalizeImplicitBindingPass> {
+class SPIRVLegalizeImplicitAndHeapBindingPass
+    : public RequiredPassInfoMixin<SPIRVLegalizeImplicitAndHeapBindingPass> {
 public:
   PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
 };
 
-ModulePass *createSPIRVLegalizeImplicitBindingPass();
+ModulePass *createSPIRVLegalizeImplicitAndHeapBindingPass();
 
 class SPIRVLegalizeZeroSizeArraysPass
     : public RequiredPassInfoMixin<SPIRVLegalizeZeroSizeArraysPass> {
@@ -193,7 +193,7 @@ void initializeSPIRVRegularizerLegacyPass(PassRegistry &);
 void initializeSPIRVMergeRegionExitTargetsLegacyPass(PassRegistry &);
 void initializeSPIRVPrepareFunctionsLegacyPass(PassRegistry &);
 void initializeSPIRVPrepareGlobalsLegacyPass(PassRegistry &);
-void initializeSPIRVLegalizeImplicitBindingLegacyPass(PassRegistry &);
+void initializeSPIRVLegalizeImplicitAndHeapBindingLegacyPass(PassRegistry &);
 void initializeSPIRVLegalizeZeroSizeArraysLegacyPass(PassRegistry &);
 void initializeSPIRVFinalizeShaderLinkageLegacyPass(PassRegistry &);
 void initializeSPIRVCtorDtorLoweringLegacyPass(PassRegistry &);
diff --git a/llvm/lib/Target/SPIRV/SPIRVCodeGenPassBuilder.cpp b/llvm/lib/Target/SPIRV/SPIRVCodeGenPassBuilder.cpp
index f47e2e56fd8a7..eb8c323e808cb 100644
--- a/llvm/lib/Target/SPIRV/SPIRVCodeGenPassBuilder.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVCodeGenPassBuilder.cpp
@@ -140,7 +140,7 @@ void SPIRVCodeGenPassBuilder::addISelPrepare(PassManagerWrapper &PMW) {
   }
   addFunctionPass(StripConvergenceIntrinsicsPass(), PMW);
   flushFPMsToMPM(PMW);
-  addModulePass(SPIRVLegalizeImplicitBindingPass(), PMW);
+  addModulePass(SPIRVLegalizeImplicitAndHeapBindingPass(), PMW);
   addModulePass(SPIRVLegalizeZeroSizeArraysPass(getTM()), PMW);
   addModulePass(SPIRVCBufferAccessPass(), PMW);
   addModulePass(SPIRVPushConstantAccessPass(getTM()), PMW);
diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitBinding.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
similarity index 89%
rename from llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitBinding.cpp
rename to llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
index 40550ee9b2960..3d185e134c0f7 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitBinding.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
@@ -1,4 +1,5 @@
-//===- SPIRVLegalizeImplicitBinding.cpp - Legalize implicit bindings ----*- C++
+//===- SPIRVLegalizeImplicitAndHeapBinding.cpp - Legalize implicit bindings
+//----*- C++
 //-*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
@@ -28,7 +29,7 @@
 using namespace llvm;
 
 namespace {
-class SPIRVLegalizeImplicitBindingImpl {
+class SPIRVLegalizeImplicitAndHeapBindingImpl {
 public:
   bool runOnModule(Module &M);
 
@@ -45,15 +46,15 @@ class SPIRVLegalizeImplicitBindingImpl {
   bool MayHaveImplicitBindings = false;
 };
 
-class SPIRVLegalizeImplicitBindingLegacy : public ModulePass {
+class SPIRVLegalizeImplicitAndHeapBindingLegacy : public ModulePass {
 public:
   static char ID;
-  SPIRVLegalizeImplicitBindingLegacy() : ModulePass(ID) {}
+  SPIRVLegalizeImplicitAndHeapBindingLegacy() : ModulePass(ID) {}
   StringRef getPassName() const override {
     return "SPIRV Legalize Implicit Binding";
   }
   bool runOnModule(Module &M) override {
-    return SPIRVLegalizeImplicitBindingImpl().runOnModule(M);
+    return SPIRVLegalizeImplicitAndHeapBindingImpl().runOnModule(M);
   }
 };
 
@@ -75,7 +76,7 @@ static uint32_t getDescSet(const CallInst *CI) {
 // Collect all of the bindings used by llvm.spv.resource.handlefrombinding
 // and llvm.spv.resource.counterhandlefrombinding calls. Also check if there
 // are any implicit binding calls.
-void SPIRVLegalizeImplicitBindingImpl::collectBindingInfo(Module &M) {
+void SPIRVLegalizeImplicitAndHeapBindingImpl::collectBindingInfo(Module &M) {
 
   auto addBinding = [&](uint32_t DescSet, uint32_t Binding) {
     if (UsedBindings.size() <= DescSet) {
@@ -122,7 +123,8 @@ void SPIRVLegalizeImplicitBindingImpl::collectBindingInfo(Module &M) {
   }
 }
 
-uint32_t SPIRVLegalizeImplicitBindingImpl::getAndReserveFirstUnusedBinding(
+uint32_t
+SPIRVLegalizeImplicitAndHeapBindingImpl::getAndReserveFirstUnusedBinding(
     uint32_t DescSet) {
   if (UsedBindings.size() <= DescSet) {
     UsedBindings.resize(DescSet + 1);
@@ -247,7 +249,7 @@ bool SPIRVLegalizeImplicitBindingImpl::replaceImplicitBindingCalls(Module &M) {
   return Changed;
 }
 
-bool SPIRVLegalizeImplicitBindingImpl::runOnModule(Module &M) {
+bool SPIRVLegalizeImplicitAndHeapBindingImpl::runOnModule(Module &M) {
   collectBindingInfo(M);
 
   bool Changed = false;
@@ -259,18 +261,19 @@ bool SPIRVLegalizeImplicitBindingImpl::runOnModule(Module &M) {
 } // namespace
 
 PreservedAnalyses
-SPIRVLegalizeImplicitBindingPass::run(Module &M, ModuleAnalysisManager &AM) {
-  return SPIRVLegalizeImplicitBindingImpl().runOnModule(M)
+SPIRVLegalizeImplicitAndHeapBindingPass::run(Module &M,
+                                             ModuleAnalysisManager &AM) {
+  return SPIRVLegalizeImplicitAndHeapBindingImpl().runOnModule(M)
              ? PreservedAnalyses::none()
              : PreservedAnalyses::all();
 }
 
-char SPIRVLegalizeImplicitBindingLegacy::ID = 0;
+char SPIRVLegalizeImplicitAndHeapBindingLegacy::ID = 0;
 
-INITIALIZE_PASS(SPIRVLegalizeImplicitBindingLegacy,
+INITIALIZE_PASS(SPIRVLegalizeImplicitAndHeapBindingLegacy,
                 "legalize-spirv-implicit-binding",
                 "Legalize SPIR-V implicit bindings", false, false)
 
-ModulePass *llvm::createSPIRVLegalizeImplicitBindingPass() {
-  return new SPIRVLegalizeImplicitBindingLegacy();
+ModulePass *llvm::createSPIRVLegalizeImplicitAndHeapBindingPass() {
+  return new SPIRVLegalizeImplicitAndHeapBindingLegacy();
 }
diff --git a/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def b/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
index eb73ae03ad366..8c471c1733174 100644
--- a/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
+++ b/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
@@ -21,7 +21,7 @@ MODULE_PASS("spirv-asm-printer-end", SPIRVAsmPrinterEndPass())
 MODULE_PASS("spirv-cbuffer-access", SPIRVCBufferAccessPass())
 MODULE_PASS("spirv-emit-intrinsics", SPIRVEmitIntrinsicsPass(*static_cast<const SPIRVTargetMachine *>(this)))
 MODULE_PASS("spirv-finalize-shader-linkage", SPIRVFinalizeShaderLinkagePass(*static_cast<const SPIRVTargetMachine *>(this)))
-MODULE_PASS("spirv-legalize-implicit-binding", SPIRVLegalizeImplicitBindingPass())
+MODULE_PASS("spirv-legalize-implicit-and-heap-binding", SPIRVLegalizeImplicitAndHeapBindingPass())
 MODULE_PASS("spirv-legalize-zero-size-arrays", SPIRVLegalizeZeroSizeArraysPass(*static_cast<const SPIRVTargetMachine *>(this)))
 MODULE_PASS("spirv-lower-ctor-dtor", SPIRVCtorDtorLoweringPass())
 MODULE_PASS("spirv-prepare-functions", SPIRVPrepareFunctionsPass(*static_cast<const SPIRVTargetMachine *>(this)))
diff --git a/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp b/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp
index 6999afbaf21be..f0e9534d0c332 100644
--- a/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp
@@ -62,7 +62,7 @@ extern "C" LLVM_ABI LLVM_EXTERNAL_VISIBILITY void LLVMInitializeSPIRVTarget() {
   initializeSPIRVEmitIntrinsicsLegacyPass(PR);
   initializeSPIRVPrepareFunctionsLegacyPass(PR);
   initializeSPIRVPrepareGlobalsLegacyPass(PR);
-  initializeSPIRVLegalizeImplicitBindingLegacyPass(PR);
+  initializeSPIRVLegalizeImplicitAndHeapBindingLegacyPass(PR);
   initializeSPIRVCtorDtorLoweringLegacyPass(PR);
   initializeSPIRVFinalizeShaderLinkageLegacyPass(PR);
 }
@@ -224,7 +224,7 @@ void SPIRVPassConfig::addISelPrepare() {
   }
   SPIRVTargetMachine &TM = getTM<SPIRVTargetMachine>();
   addPass(createStripConvergenceIntrinsicsPass());
-  addPass(createSPIRVLegalizeImplicitBindingPass());
+  addPass(createSPIRVLegalizeImplicitAndHeapBindingPass());
   addPass(createSPIRVLegalizeZeroSizeArraysPass(TM));
   addPass(createSPIRVCBufferAccessLegacyPass());
   addPass(createSPIRVPushConstantAccessLegacyPass(&TM));
diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll
index eb0d0311bedf0..877dac9bcaf06 100644
--- a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll
@@ -1,4 +1,4 @@
-; RUN: opt -S -passes=spirv-legalize-implicit-binding -mtriple=spirv1.6-vulkan1.3-library < %s | FileCheck %s
+; RUN: opt -S -passes=spirv-legalize-implicit-and-heap-binding -mtriple=spirv1.6-vulkan1.3-library < %s | FileCheck %s
 
 @.str.b = private unnamed_addr constant [2 x i8] c"b\00", align 1
 @.str.c = private unnamed_addr constant [2 x i8] c"c\00", align 1

>From d2d786b4b4ec7d0b20f07d8cfca22b89b2e081e7 Mon Sep 17 00:00:00 2001
From: Helena Kotas <hekotas at microsoft.com>
Date: Thu, 10 Sep 2026 09:55:19 -0700
Subject: [PATCH 2/3] [SPIRV] Add handling of heap resources

Fixes #208054

Assisted by GPT-5.6 Sol
---
 llvm/include/llvm/IR/IntrinsicsSPIRV.td       |   9 +
 .../SPIRVLegalizeImplicitAndHeapBinding.cpp   | 165 +++++++++++++++++-
 .../hlsl-resources/HeapBinding-cvb-srv-uav.ll |  79 +++++++++
 .../HeapBinding-texture-sampler.ll            |  70 ++++++++
 .../hlsl-resources/HeapBinding-uav-counter.ll |  60 +++++++
 .../SPIRV/passes/SPIRVLegalizeHeapBinding.ll  |  44 +++++
 6 files changed, 418 insertions(+), 9 deletions(-)
 create mode 100644 llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeHeapBinding.ll

diff --git a/llvm/include/llvm/IR/IntrinsicsSPIRV.td b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
index 86b49a8ee446a..d634253b1ccbe 100644
--- a/llvm/include/llvm/IR/IntrinsicsSPIRV.td
+++ b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
@@ -207,6 +207,15 @@ def int_spv_rsqrt : DefaultAttrsIntrinsic<[LLVMMatchType<0>], [llvm_anyfloat_ty]
                               [llvm_any_ty, llvm_i32_ty, llvm_i32_ty],
                               [IntrNoMem, IntrConvergent]>;
 
+  def int_spv_resource_handlefromheap
+      : DefaultAttrsIntrinsic<[llvm_any_ty],
+                              [llvm_i32_ty],
+                              [IntrNoMem]>;
+  def int_spv_resource_counterhandlefromheap
+      : DefaultAttrsIntrinsic<[llvm_any_ty],
+                              [llvm_any_ty],
+                              [IntrNoMem, IntrConvergent]>;
+                              
   def int_spv_firstbituhigh : DefaultAttrsIntrinsic<[LLVMScalarOrSameVectorWidth<0, llvm_i32_ty>], [llvm_anyint_ty], [IntrNoMem]>;
   def int_spv_firstbitshigh : DefaultAttrsIntrinsic<[LLVMScalarOrSameVectorWidth<0, llvm_i32_ty>], [llvm_anyint_ty], [IntrNoMem]>;
   def int_spv_firstbitlow : DefaultAttrsIntrinsic<[LLVMScalarOrSameVectorWidth<0, llvm_i32_ty>], [llvm_anyint_ty], [IntrNoMem]>;
diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
index 3d185e134c0f7..adc74729315aa 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
@@ -17,6 +17,7 @@
 #include "SPIRV.h"
 #include "llvm/ADT/BitVector.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/IR/IRBuilder.h"
 #include "llvm/IR/InstVisitor.h"
@@ -24,6 +25,7 @@
 #include "llvm/IR/IntrinsicsSPIRV.h"
 #include "llvm/IR/Module.h"
 #include "llvm/Pass.h"
+#include "llvm/Support/raw_ostream.h"
 #include <vector>
 
 using namespace llvm;
@@ -37,13 +39,15 @@ class SPIRVLegalizeImplicitAndHeapBindingImpl {
   void collectBindingInfo(Module &M);
   uint32_t getAndReserveFirstUnusedBinding(uint32_t DescSet);
   bool replaceImplicitBindingCalls(Module &M);
+  bool replaceHeapBindingCalls(Module &M);
 
   // A map from descriptor set to a bit vector of used binding numbers.
   std::vector<BitVector> UsedBindings;
 
   // Set to true by collectBindingInfo() if there are any implicit binding
-  // declarations in the module.
+  // or heap binding calls in the module.
   bool MayHaveImplicitBindings = false;
+  bool HasHeapBinding = false;
 };
 
 class SPIRVLegalizeImplicitAndHeapBindingLegacy : public ModulePass {
@@ -117,6 +121,10 @@ void SPIRVLegalizeImplicitAndHeapBindingImpl::collectBindingInfo(Module &M) {
     case Intrinsic::spv_resource_counterhandlefromimplicitbinding:
       MayHaveImplicitBindings = true;
       break;
+    case Intrinsic::spv_resource_handlefromheap:
+    case Intrinsic::spv_resource_counterhandlefromheap:
+      HasHeapBinding = true;
+      break;
     default:
       break;
     }
@@ -144,10 +152,11 @@ SPIRVLegalizeImplicitAndHeapBindingImpl::getAndReserveFirstUnusedBinding(
 // Replace the implicit binding call with a new call using explicit binding.
 static void replaceWithHandleFromBinding(Module &M, CallInst *CI,
                                          uint32_t DescSet, uint32_t Binding,
-                                         Value *IndexOp, Value *RangeOp,
+                                         Value *RangeOp, Value *IndexOp,
                                          Value *Name) {
-  assert(CI->getIntrinsicID() ==
-             Intrinsic::spv_resource_handlefromimplicitbinding &&
+  assert((CI->getIntrinsicID() ==
+              Intrinsic::spv_resource_handlefromimplicitbinding ||
+          CI->getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) &&
          "unexpected implicit binding intrinsic");
   IRBuilder<> Builder(CI);
   Value *DescSetOp = Builder.getInt32(DescSet);
@@ -155,7 +164,7 @@ static void replaceWithHandleFromBinding(Module &M, CallInst *CI,
   Function *NewFunc = Intrinsic::getOrInsertDeclaration(
       &M, Intrinsic::spv_resource_handlefrombinding, {CI->getType()});
   CallInst *NewCI = Builder.CreateCall(
-      NewFunc, {DescSetOp, BindingOp, IndexOp, RangeOp, Name});
+      NewFunc, {DescSetOp, BindingOp, RangeOp, IndexOp, Name});
   NewCI->setCallingConv(CI->getCallingConv());
   CI->replaceAllUsesWith(NewCI);
   CI->eraseFromParent();
@@ -166,9 +175,11 @@ static void replaceWithHandleFromBinding(Module &M, CallInst *CI,
 static void replaceWithCounterHandleFromBinding(Module &M, CallInst *CI,
                                                 uint32_t DescSet,
                                                 uint32_t Binding) {
-  assert(CI->getIntrinsicID() ==
-             Intrinsic::spv_resource_counterhandlefromimplicitbinding &&
-         "unexpected implicit binding intrinsic");
+  assert(
+      (CI->getIntrinsicID() ==
+           Intrinsic::spv_resource_counterhandlefromimplicitbinding ||
+       CI->getIntrinsicID() == Intrinsic::spv_resource_counterhandlefromheap) &&
+      "unexpected implicit binding intrinsic");
   IRBuilder<> Builder(CI);
   Value *DescSetOp = Builder.getInt32(DescSet);
   Value *BindingOp = Builder.getInt32(Binding);
@@ -183,7 +194,8 @@ static void replaceWithCounterHandleFromBinding(Module &M, CallInst *CI,
   CI->eraseFromParent();
 }
 
-bool SPIRVLegalizeImplicitBindingImpl::replaceImplicitBindingCalls(Module &M) {
+bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceImplicitBindingCalls(
+    Module &M) {
   // Collect all implicit binding calls.
   SmallVector<std::pair<uint32_t, CallInst *>> IBCalls;
   bool Changed = false;
@@ -249,12 +261,147 @@ bool SPIRVLegalizeImplicitBindingImpl::replaceImplicitBindingCalls(Module &M) {
   return Changed;
 }
 
+GlobalVariable *createHeapString(Module &M, StringRef Name) {
+  SmallString<32> GlobalStringName(Name);
+  uint32_t HeapNameLen = Name.size();
+  for (unsigned Suffix = 0;; ++Suffix) {
+    GlobalStringName.append(".str");
+    if (!M.getNamedValue(GlobalStringName))
+      break;
+    GlobalStringName.resize(Name.size());
+    raw_svector_ostream(GlobalStringName) << '.' << Suffix + 1;
+    HeapNameLen = GlobalStringName.size();
+  }
+  StringRef HeapName = GlobalStringName.substr(0, HeapNameLen);
+
+  Constant *Init = ConstantDataArray::getString(M.getContext(), HeapName);
+  GlobalVariable *HeapNameGV = new GlobalVariable(
+      M, Init->getType(), /*isConstant=*/true, GlobalValue::PrivateLinkage,
+      Init, GlobalStringName, /*InsertBefore=*/nullptr,
+      GlobalVariable::NotThreadLocal, /*AddressSpace=*/0);
+  HeapNameGV->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+  HeapNameGV->setAlignment(Align(1));
+  return HeapNameGV;
+}
+
+bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceHeapBindingCalls(
+    Module &M) {
+  // First we need to collect all heap binding calls based on their kind
+  // because heap bindings are assigned in the order (CbvUavSrv, Samplers,
+  // Counters).
+  SmallVector<Function *, 8> CbvSrvUavs;
+  SmallVector<Function *, 8> Samplers;
+  SmallVector<Function *, 8> Counters;
+  bool Changed = false;
+
+  for (Function &F : M) {
+    if (!F.isDeclaration())
+      continue;
+
+    if (F.getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) {
+      TargetExtType *ResType = cast<TargetExtType>(F.getReturnType());
+      if (ResType->getName() == "spirv.Sampler")
+        Samplers.emplace_back(&F);
+      else
+        CbvSrvUavs.emplace_back(&F);
+    } else if (F.getIntrinsicID() ==
+               Intrinsic::spv_resource_counterhandlefromheap) {
+      Counters.emplace_back(&F);
+    } else
+      continue;
+  }
+
+  if (CbvSrvUavs.empty() && Samplers.empty() && Counters.empty())
+    return false;
+
+  // Heap resources are always mapped to descriptor set 0 as an unbounded
+  // runtime array.
+  constexpr uint32_t DescSet = 0;
+  Value *Zero =
+      llvm::ConstantInt::get(llvm::Type::getInt32Ty(M.getContext()), 0);
+
+  if (!CbvSrvUavs.empty()) {
+    // For CBV/UAV/SRV resources we need to create separate
+    // ResourceDecriptorHeap runtime array for each different resource type, and
+    // they are all going to be mapped to the same descriptor set.
+    uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
+    SmallDenseMap<TargetExtType *, GlobalVariable *> ResourceDescriptorHeaps;
+    for (Function *F : CbvSrvUavs) {
+      TargetExtType *ResType = cast<TargetExtType>(F->getReturnType());
+      GlobalVariable *HeapNameGV = nullptr;
+      auto It = ResourceDescriptorHeaps.find(ResType);
+      if (It == ResourceDescriptorHeaps.end()) {
+        HeapNameGV = createHeapString(M, "ResourceDescriptorHeap");
+        auto [InsertedIt, Inserted] =
+            ResourceDescriptorHeaps.try_emplace(ResType, HeapNameGV);
+        assert(Inserted && "resource heap name already exists");
+      } else {
+        HeapNameGV = It->second;
+      }
+
+      for (User *U : make_early_inc_range(F->users())) {
+        if (CallInst *CI = dyn_cast<CallInst>(U)) {
+          Value *HeapIdx = CI->getArgOperand(0);
+          replaceWithHandleFromBinding(M, CI, DescSet, Binding, Zero, HeapIdx,
+                                       HeapNameGV);
+          Changed = true;
+        }
+      }
+      F->eraseFromParent();
+    }
+  }
+
+  if (!Samplers.empty()) {
+    uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
+    // The Sampler handle type should be the same for all samplers
+    // (target("spirv.Sampler")).
+    TargetExtType *SamplerHandleType =
+        cast<TargetExtType>(Samplers.front()->getReturnType());
+
+    GlobalVariable *HeapNameGV = createHeapString(M, "SamplerDescriptorHeap");
+    for (Function *F : Samplers) {
+      assert(F->getReturnType() == SamplerHandleType &&
+             "sampler handle type mismatch");
+      for (User *U : make_early_inc_range(F->users())) {
+        if (CallInst *CI = dyn_cast<CallInst>(U)) {
+          Value *HeapIdx = CI->getArgOperand(0);
+          replaceWithHandleFromBinding(M, CI, DescSet, Binding, Zero, HeapIdx,
+                                       HeapNameGV);
+          Changed = true;
+        }
+      }
+      F->eraseFromParent();
+    }
+  }
+
+  if (!Counters.empty()) {
+    uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
+    for (Function *F : Counters) {
+      // The counter handle type should be the same for all resource types
+      // that have a counter (target("spirv.VulkanBuffer", i32, 12, 1)).
+      assert(F->getReturnType() == Counters.front()->getReturnType() &&
+             "counter handle type mismatch");
+      for (User *U : make_early_inc_range(F->users())) {
+        if (CallInst *CI = dyn_cast<CallInst>(U)) {
+          replaceWithCounterHandleFromBinding(M, CI, DescSet, Binding);
+          Changed = true;
+        }
+      }
+      F->eraseFromParent();
+    }
+  }
+
+  return Changed;
+}
+
 bool SPIRVLegalizeImplicitAndHeapBindingImpl::runOnModule(Module &M) {
   collectBindingInfo(M);
 
   bool Changed = false;
   if (MayHaveImplicitBindings)
     Changed |= replaceImplicitBindingCalls(M);
+  if (HasHeapBinding)
+    Changed |= replaceHeapBindingCalls(M);
 
   return Changed;
 }
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll
new file mode 100644
index 0000000000000..2d6eb659c49b9
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll
@@ -0,0 +1,79 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
+
+; This test declares Buf at descriptor set 0, binding 1, and creates one CBV,
+; SRV, and UAV resource from the descriptor heap. Legalization creates a
+; distinct runtime-array type for each heap resource type. All three arrays
+; share descriptor set 0, binding 0 (the first available bindings).
+
+; ModuleID = 'dyn-res-cvb-srv-uav.hlsl'
+target datalayout = "e-ve-i64:64-n8:16:32:64-G10"
+
+%S = type <{ <4 x i32> }>
+ at .str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+; CHECK-DAG: OpCapability RuntimeDescriptorArrayEXT
+
+; CHECK-DAG: OpName [[Buf:%[0-9]+]] "Buf"
+; CHECK-DAG: OpDecorate [[Buf]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Buf]] Binding 1
+
+; CHECK-DAG: OpName [[Heap0:%[0-9]+]] "ResourceDescriptorHeap"
+; CHECK-DAG: OpDecorate [[Heap0]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Heap0]] Binding 0
+
+; CHECK-DAG: OpName [[Heap1:%[0-9]+]] "ResourceDescriptorHeap.1"
+; CHECK-DAG: OpDecorate [[Heap1]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Heap1]] Binding 0
+
+; CHECK-DAG: OpName [[Heap2:%[0-9]+]] "ResourceDescriptorHeap.2"
+; CHECK-DAG: OpDecorate [[Heap2]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Heap2]] Binding 0
+
+; Types of ResourceDescriptorHeap arrays
+
+; CHECK: [[Int32:%[0-9]+]] = OpTypeInt 32 0
+; CHECK: [[Int32Vec:%[0-9]+]] = OpTypeVector [[Int32]] 4
+; CHECK: [[StructS:%[0-9]+]] = OpTypeStruct [[Int32Vec]]
+; CHECK: [[ImageType:%[0-9]+]] = OpTypeImage [[Int32]] Buffer 2 0 0 2 R32i
+; CHECK: [[RTArrayInt32Vec:%[0-9]+]] = OpTypeRuntimeArray [[Int32Vec]]
+; CHECK: [[Heap1Struct:%[0-9]+]] = OpTypeStruct [[RTArrayInt32Vec]]
+; CHECK: [[Heap0Struct:%[0-9]+]] = OpTypeStruct [[StructS]]
+; CHECK: [[RTArrayImage:%[0-9]+]] = OpTypeRuntimeArray [[ImageType]]
+; CHECK: [[Heap0RTArrayPtr:%[0-9]+]] = OpTypePointer UniformConstant [[RTArrayImage]]
+; CHECK: [[Heap1RTArray:%[0-9]+]] = OpTypeRuntimeArray [[Heap1Struct]]
+; CHECK: [[Heap1RTArrayPtr:%[0-9]+]] = OpTypePointer StorageBuffer [[Heap1RTArray]]
+; CHECK: [[Heap0RTArray:%[0-9]+]] = OpTypeRuntimeArray [[Heap0Struct]]
+; CHECK: [[Heap2RTArrayPtr:%[0-9]+]] = OpTypePointer Uniform [[Heap0RTArray]]
+
+; CHECK: [[Heap2:%[0-9]+]] = OpVariable [[Heap2RTArrayPtr]] Uniform
+; CHECK: [[Heap1:%[0-9]+]] = OpVariable [[Heap1RTArrayPtr]] StorageBuffer
+; CHECK: [[Heap0:%[0-9]+]] = OpVariable [[Heap0RTArrayPtr]] UniformConstant
+
+define void @test(i32 %CbvIndex, i32 %SrvIndex, i32 %UavIndex) {
+entry:
+  %Buf = tail call target("spirv.VulkanBuffer", [0 x <4 x i32>], 12, 1) @llvm.spv.resource.handlefrombinding.tspirv.VulkanBuffer_a0v4i32_12_1t(i32 0, i32 1, i32 1, i32 0, ptr nonnull @.str)
+
+  %TId = tail call i32 @llvm.spv.thread.id.in.group.i32(i32 0)
+  
+  %HeapCvb = tail call target("spirv.VulkanBuffer", %S, 2, 0) @llvm.spv.resource.handlefromheap.tspirv.VulkanBuffer_s_Ss_2_0t(i32 %CbvIndex)
+  %HeapSrv = tail call target("spirv.VulkanBuffer", [0 x <4 x i32>], 12, 0) @llvm.spv.resource.handlefromheap.tspirv.VulkanBuffer_a0v4i32_12_0t(i32 %SrvIndex)
+  %HeapUav = tail call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefromheap.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32 %UavIndex)
+
+  %CvbPtr = call noundef align 1 dereferenceable(16) ptr addrspace(12) @llvm.spv.resource.getbasepointer.p12.tspirv.VulkanBuffer_s_Ss_2_0t(target("spirv.VulkanBuffer", %S, 2, 0) %HeapCvb)
+  %CvbVal = load <4 x i32>, ptr addrspace(12) %CvbPtr, align 4
+  
+  %SrvPtr = call noundef align 4 dereferenceable(16) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0v4i32_12_0t.i32(target("spirv.VulkanBuffer", [0 x <4 x i32>], 12, 0) %HeapSrv, i32 %TId)
+  %SrvVal = load <4 x i32>, ptr addrspace(11) %SrvPtr, align 4
+ 
+  %UavPtr = call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.SignedImage_i32_5_2_0_0_2_24t.i32(target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) %HeapUav, i32 %TId)
+  %UavVal = load i32, ptr addrspace(11) %UavPtr, align 4
+ 
+  %0 = add <4 x i32> %SrvVal, %CvbVal
+  %1 = insertelement <4 x i32> %0, i32 %UavVal, i64 0
+
+  %BufPtr = call noundef align 4 dereferenceable(16) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0v4i32_12_1t.i32(target("spirv.VulkanBuffer", [0 x <4 x i32>], 12, 1) %Buf, i32 %TId)
+  store <4 x i32> %1, ptr addrspace(11) %BufPtr, align 4
+
+  ret void
+}
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
new file mode 100644
index 0000000000000..982ec26a312db
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
@@ -0,0 +1,70 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
+
+; This test declares storage buffer Buf at descriptor set 0, binding 1, and
+; creates one texture and two samplers by dynamically indexing into descriptor
+; heaps. SPIR-V legalization creates separate runtime arrays for resource and
+; sampler descriptors heaps, assigning the resource heap to binding 0 and the
+; sampler heap to binding 2 (the first two available bindings).
+
+ at .str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+; CHECK-DAG: OpCapability RuntimeDescriptorArrayEXT
+
+; CHECK-DAG: OpName [[Buf:%[0-9]+]] "Buf"
+; CHECK-DAG: OpDecorate [[Buf]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Buf]] Binding 1
+
+; CHECK-DAG: OpName [[ResourceHeap:%[0-9]+]] "ResourceDescriptorHeap"
+; CHECK-DAG: OpDecorate [[ResourceHeap]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[ResourceHeap]] Binding 0
+
+; CHECK-DAG: OpName [[SamplerHeap:%[0-9]+]] "SamplerDescriptorHeap"
+; CHECK-DAG: OpDecorate [[SamplerHeap]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[SamplerHeap]] Binding 2
+
+; Types of descriptor heap arrays
+
+; CHECK: [[Float32:%[0-9]+]] = OpTypeFloat 32
+; CHECK: [[SamplerType:%[0-9]+]] = OpTypeSampler
+; CHECK: [[ImageType:%[0-9]+]] = OpTypeImage [[Float32]] 2D 2 0 0 1 Unknown
+; CHECK: [[BufType:%[0-9]+]] = OpTypeImage [[Float32]] Buffer 2 0 0 2 Rgba32f
+; CHECK: [[BufPtr:%[0-9]+]] = OpTypePointer UniformConstant [[BufType]]
+; CHECK: [[ResourceRTArray:%[0-9]+]] = OpTypeRuntimeArray [[ImageType]]
+; CHECK: [[ResourceRTArrayPtr:%[0-9]+]] = OpTypePointer UniformConstant [[ResourceRTArray]]
+; CHECK: [[SamplerRTArray:%[0-9]+]] = OpTypeRuntimeArray [[SamplerType]]
+; CHECK: [[SamplerRTArrayPtr:%[0-9]+]] = OpTypePointer UniformConstant [[SamplerRTArray]]
+
+; CHECK: [[ResourceHeap]] = OpVariable [[ResourceRTArrayPtr]] UniformConstant
+; CHECK: [[SamplerHeap]] = OpVariable [[SamplerRTArrayPtr]] UniformConstant
+
+define void @test(i32 %TexIndex, i32 %Samp0Index, i32 %Samp1Index) {
+entry:
+  %Buf = tail call target("spirv.Image", float, 5, 2, 0, 0, 2, 1) @llvm.spv.resource.handlefrombinding.tspirv.Image_f32_5_2_0_0_2_1t(i32 0, i32 1, i32 1, i32 0, ptr nonnull @.str)
+
+  %Texture = tail call target("spirv.Image", float, 1, 2, 0, 0, 1, 0) @llvm.spv.resource.handlefromheap.tspirv.Image_f32_1_2_0_0_1_0t(i32 %TexIndex)
+  %Samp0 = tail call target("spirv.Sampler") @llvm.spv.resource.handlefromheap.tspirv.Samplert(i32 %Samp0Index)
+  %Samp1 = tail call target("spirv.Sampler") @llvm.spv.resource.handlefromheap.tspirv.Samplert(i32 %Samp1Index)
+
+  %Value0 = tail call reassoc nnan ninf nsz arcp afn noundef nofpclass(nan inf) <4 x float>
+        @llvm.spv.resource.samplelevel.v4f32.tspirv.Image_f32_1_2_0_0_1_0t.tspirv.Samplert.v2f32.v2i32(
+        target("spirv.Image", float, 1, 2, 0, 0, 1, 0) %Texture, target("spirv.Sampler") %Samp0,
+        <2 x float> <float 4.000000e-01, float 5.000000e-01>, float -1.000000e+00, <2 x i32> zeroinitializer)
+
+  %BufPtr0 = call noundef align 4 dereferenceable(16) ptr addrspace(11)
+        @llvm.spv.resource.getpointer.p11.tspirv.Image_f32_5_2_0_0_2_1t.i32(target("spirv.Image", float, 5, 2, 0, 0, 2, 1) %Buf, i32 0)
+
+  store <4 x float> %Value0, ptr addrspace(11) %BufPtr0, align 4
+
+  %Value1 = tail call reassoc nnan ninf nsz arcp afn noundef nofpclass(nan inf) <4 x float>
+        @llvm.spv.resource.samplelevel.v4f32.tspirv.Image_f32_1_2_0_0_1_0t.tspirv.Samplert.v2f32.v2i32(
+        target("spirv.Image", float, 1, 2, 0, 0, 1, 0) %Texture, target("spirv.Sampler") %Samp1,
+        <2 x float> <float 4.000000e-01, float 5.000000e-01>, float -1.000000e+00, <2 x i32> zeroinitializer)
+
+  %BufPtr1 = call noundef align 4 dereferenceable(16) ptr addrspace(11)
+        @llvm.spv.resource.getpointer.p11.tspirv.Image_f32_5_2_0_0_2_1t.i32(target("spirv.Image", float, 5, 2, 0, 0, 2, 1) %Buf, i32 1)
+        
+  store <4 x float> %Value0, ptr addrspace(11) %BufPtr1, align 4
+
+  ret void
+}
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
new file mode 100644
index 0000000000000..974c2d46ebf10
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
@@ -0,0 +1,60 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
+
+; This test declares storage buffer Buf at descriptor set 0, binding 1, and
+; creates a UAV and its counter by dynamically indexing into descriptor heap.
+; SPIR-V legalization creates separate runtime arrays for the resource and
+; counter descriptors, assigning the resource heap to binding 0 and the
+; counter heap to binding 2 (the first two available bindings).
+
+ at .str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+; CHECK-DAG: OpCapability RuntimeDescriptorArrayEXT
+
+; CHECK-DAG: OpName [[Heap:%[0-9]+]] "ResourceDescriptorHeap"
+; CHECK-DAG: OpDecorate [[Heap]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Heap]] Binding 0
+
+; CHECK-DAG: OpName [[Buf:%[0-9]+]] "Buf"
+; CHECK-DAG: OpDecorate [[Buf]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Buf]] Binding 1
+
+; CHECK-DAG: OpName [[CounterHeap:%[0-9]+]] "ResourceDescriptorHeap.counter"
+; CHECK-DAG: OpDecorate [[CounterHeap]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[CounterHeap]] Binding 2
+
+; Types of ResourceDescriptorHeap arrays
+
+; CHECK: [[Int32:%[0-9]+]] = OpTypeInt 32 0
+; CHECK: [[Float32:%[0-9]+]] = OpTypeFloat 32
+; CHECK: [[RTArrayFloat:%[0-9]+]] = OpTypeRuntimeArray [[Float32]]
+; CHECK: [[HeapStruct:%[0-9]+]] = OpTypeStruct [[RTArrayFloat]]
+; CHECK: [[CounterStruct:%[0-9]+]] = OpTypeStruct [[Int32]]
+; CHECK: [[CounterRTArray:%[0-9]+]] = OpTypeRuntimeArray [[CounterStruct]]
+; CHECK: [[CounterRTArrayPtr:%[0-9]+]] = OpTypePointer StorageBuffer [[CounterRTArray]]
+; CHECK: [[HeapRTArray:%[0-9]+]] = OpTypeRuntimeArray [[HeapStruct]]
+; CHECK: [[HeapRTArrayPtr:%[0-9]+]] = OpTypePointer StorageBuffer [[HeapRTArray]]
+
+; CHECK: [[Heap]] = OpVariable [[HeapRTArrayPtr]] StorageBuffer
+; CHECK: [[CounterHeap]] = OpVariable [[CounterRTArrayPtr]] StorageBuffer
+
+define void @test(i32 %Index) {
+entry:
+  %TId = tail call i32 @llvm.spv.thread.id.in.group.i32(i32 0)
+
+  %HeapUav = tail call target("spirv.VulkanBuffer", [0 x float], 12, 1) @llvm.spv.resource.handlefromheap.tspirv.VulkanBuffer_a0f32_12_1t(i32 %Index)
+  %HeapUavCounter = call target("spirv.VulkanBuffer", i32, 12, 1) @llvm.spv.resource.counterhandlefromheap.tspirv.VulkanBuffer_i32_12_1t.tspirv.VulkanBuffer_a0f32_12_1t(target("spirv.VulkanBuffer", [0 x float], 12, 1) %HeapUav)
+  
+  %UavPtr = call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0f32_12_1t.i32(target("spirv.VulkanBuffer", [0 x float], 12, 1) %HeapUav, i32 0)
+  %UavVal = load float, ptr addrspace(11) %UavPtr, align 4
+
+  %CounterVal = tail call noundef i32 @llvm.spv.resource.updatecounter.tspirv.VulkanBuffer_i32_12_1t(target("spirv.VulkanBuffer", i32, 12, 1) %HeapUavCounter, i8 1)
+  %CounterValFloat = uitofp reassoc nnan ninf nsz arcp afn i32 %CounterVal to float
+
+  %Buf = tail call target("spirv.VulkanBuffer", [0 x float], 12, 1) @llvm.spv.resource.handlefrombinding.tspirv.VulkanBuffer_a0f32_12_1t(i32 0, i32 1, i32 1, i32 0, ptr nonnull @.str)
+  %BufPtr = call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.VulkanBuffer_a0f32_12_1t.i32(target("spirv.VulkanBuffer", [0 x float], 12, 1) %Buf, i32 0)
+
+  %add = fadd reassoc nnan ninf nsz arcp afn float %CounterValFloat, %UavVal
+  store float %add, ptr addrspace(11) %BufPtr, align 4
+  ret void
+}
diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeHeapBinding.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeHeapBinding.ll
new file mode 100644
index 0000000000000..6aba3e2426818
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeHeapBinding.ll
@@ -0,0 +1,44 @@
+; RUN: opt -S -passes=spirv-legalize-implicit-and-heap-binding -mtriple=spirv1.6-vulkan1.3-library < %s | FileCheck %s
+
+ at .str.used = private unnamed_addr constant [5 x i8] c"used\00", align 1
+
+; Verify heap-binding intrinsic calls are assigned bindings after bindings
+; already used in descriptor set 0 and use the heap index as the array index.
+
+; CHECK-DAG: @ResourceDescriptorHeap.str = private unnamed_addr constant [23 x i8] c"ResourceDescriptorHeap\00", align 1
+; CHECK-DAG: @ResourceDescriptorHeap.1.str = private unnamed_addr constant [25 x i8] c"ResourceDescriptorHeap.1\00", align 1
+; CHECK-DAG: @SamplerDescriptorHeap.str = private unnamed_addr constant [22 x i8] c"SamplerDescriptorHeap\00", align 1
+
+; CHECK-LABEL: define void @main(
+define void @main() local_unnamed_addr #0 {
+entry:
+
+; Resource bound at desc 0 binding 0
+; CHECK: call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 0) @llvm.spv.resource.handlefrombinding{{.*}}(i32 0, i32 0, i32 1, i32 0, ptr @.str.used)
+  %used = call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 0) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_0t(i32 0, i32 0, i32 1, i32 0, ptr @.str.used)
+
+; Heap resource - bound to resource array at desc 0 binding 1
+; CHECK: [[HEAPHANDLE:%.*]] = call target("spirv.VulkanBuffer", [0 x i32], 12, 1) @llvm.spv.resource.handlefrombinding{{.*}}(i32 0, i32 1, i32 0, i32 7, ptr @ResourceDescriptorHeap.1.str)
+  %heap_resource_type1 = call target("spirv.VulkanBuffer", [0 x i32], 12, 1) @llvm.spv.resource.handlefromheap.tspirv.VulkanBuffer_a0i32_12_1t(i32 7)
+
+; Heap resource of different type - also bound to desc 0 binding 1
+; CHECK: call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 0) @llvm.spv.resource.handlefrombinding{{.*}}(i32 0, i32 1, i32 0, i32 13, ptr @ResourceDescriptorHeap.str)
+; CHECK: call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 0) @llvm.spv.resource.handlefrombinding{{.*}}(i32 0, i32 1, i32 0, i32 19, ptr @ResourceDescriptorHeap.str)
+  %heap_resource_type2 = call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 0) @llvm.spv.resource.handlefromheap.tspirv.SignedImage_i32_5_2_0_0_2_0t(i32 13)
+  %heap_resource_type2_again = call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 0) @llvm.spv.resource.handlefromheap.tspirv.SignedImage_i32_5_2_0_0_2_0t(i32 19)
+
+; Heap sampler - bound to desc 0 binding 2
+; CHECK: call target("spirv.Sampler") @llvm.spv.resource.handlefrombinding{{.*}}(i32 0, i32 2, i32 0, i32 8, ptr @SamplerDescriptorHeap.str)
+  %heap_sampler = call target("spirv.Sampler") @llvm.spv.resource.handlefromheap.tspirv.Samplert(i32 8)
+
+; Counters for heap resources - bound to desc 0 binding 3
+; CHECK: call target("spirv.VulkanBuffer", i32, 12, 1) @llvm.spv.resource.counterhandlefrombinding{{.*}}(target("spirv.VulkanBuffer", [0 x i32], 12, 1) [[HEAPHANDLE]], i32 0, i32 3)
+  %heap_resource_counter = call target("spirv.VulkanBuffer", i32, 12, 1) @llvm.spv.resource.counterhandlefromheap.tspirv.VulkanBuffer_i32_12_1t.tspirv.VulkanBuffer_a0i32_12_1t(target("spirv.VulkanBuffer", [0 x i32], 12, 1) %heap_resource_type1)
+
+  ret void
+}
+
+; CHECK-NOT: @llvm.spv.resource.handlefromheap
+; CHECK-NOT: @llvm.spv.resource.counterhandlefromheap
+
+attributes #0 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }
\ No newline at end of file

>From d83021db7f3c5a033d67050c8ac51b2f737c7beb Mon Sep 17 00:00:00 2001
From: Helena Kotas <hekotas at microsoft.com>
Date: Thu, 17 Sep 2026 18:50:50 -0700
Subject: [PATCH 3/3] self review

---
 .../SPIRVLegalizeImplicitAndHeapBinding.cpp   | 70 ++++++++++++-------
 .../hlsl-resources/HeapBinding-cvb-srv-uav.ll |  8 +--
 .../HeapBinding-texture-sampler.ll            |  2 -
 .../hlsl-resources/HeapBinding-uav-counter.ll |  8 +--
 4 files changed, 54 insertions(+), 34 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
index adc74729315aa..71417c24c9f4d 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitAndHeapBinding.cpp
@@ -1,6 +1,4 @@
-//===- SPIRVLegalizeImplicitAndHeapBinding.cpp - Legalize implicit bindings
-//----*- C++
-//-*-===//
+//===- SPIRVLegalizeImplicitAndHeapBinding.cpp ---------------------------*- C++
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -9,8 +7,11 @@
 //===----------------------------------------------------------------------===//
 //
 // This pass legalizes the @llvm.spv.resource.handlefromimplicitbinding
-// intrinsic by replacing it with a call to
-// @llvm.spv.resource.handlefrombinding.
+// and @llvm.spv.resource.handlefromheap intrinsics by replacing them with a
+// call to @llvm.spv.resource.handlefrombinding.
+// It also replaces any @llvm.spv.resource.counterhandlefromimplicitbinding and
+// @llvm.spv.resource.counterhandlefromheap intrinsics with calls to
+// @llvm.spv.resource.counterhandlefrombinding.
 //
 //===----------------------------------------------------------------------===//
 
@@ -44,10 +45,11 @@ class SPIRVLegalizeImplicitAndHeapBindingImpl {
   // A map from descriptor set to a bit vector of used binding numbers.
   std::vector<BitVector> UsedBindings;
 
-  // Set to true by collectBindingInfo() if there are any implicit binding
-  // or heap binding calls in the module.
+  // Set to true by collectBindingInfo() if there are possibly any implicit
+  // binding or heap binding calls in the module (if the module contains a
+  // declaration of implicit binding or heap intrinsic).
   bool MayHaveImplicitBindings = false;
-  bool HasHeapBinding = false;
+  bool MayHaveHeapBindings = false;
 };
 
 class SPIRVLegalizeImplicitAndHeapBindingLegacy : public ModulePass {
@@ -123,7 +125,7 @@ void SPIRVLegalizeImplicitAndHeapBindingImpl::collectBindingInfo(Module &M) {
       break;
     case Intrinsic::spv_resource_handlefromheap:
     case Intrinsic::spv_resource_counterhandlefromheap:
-      HasHeapBinding = true;
+      MayHaveHeapBindings = true;
       break;
     default:
       break;
@@ -157,7 +159,7 @@ static void replaceWithHandleFromBinding(Module &M, CallInst *CI,
   assert((CI->getIntrinsicID() ==
               Intrinsic::spv_resource_handlefromimplicitbinding ||
           CI->getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) &&
-         "unexpected implicit binding intrinsic");
+         "unexpected binding intrinsic");
   IRBuilder<> Builder(CI);
   Value *DescSetOp = Builder.getInt32(DescSet);
   Value *BindingOp = Builder.getInt32(Binding);
@@ -179,7 +181,7 @@ static void replaceWithCounterHandleFromBinding(Module &M, CallInst *CI,
       (CI->getIntrinsicID() ==
            Intrinsic::spv_resource_counterhandlefromimplicitbinding ||
        CI->getIntrinsicID() == Intrinsic::spv_resource_counterhandlefromheap) &&
-      "unexpected implicit binding intrinsic");
+      "unexpected binding intrinsic");
   IRBuilder<> Builder(CI);
   Value *DescSetOp = Builder.getInt32(DescSet);
   Value *BindingOp = Builder.getInt32(Binding);
@@ -261,15 +263,15 @@ bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceImplicitBindingCalls(
   return Changed;
 }
 
-GlobalVariable *createHeapString(Module &M, StringRef Name) {
+GlobalVariable *createHeapNameString(Module &M, StringRef Name) {
   SmallString<32> GlobalStringName(Name);
   uint32_t HeapNameLen = Name.size();
-  for (unsigned Suffix = 0;; ++Suffix) {
+  for (unsigned Suffix = 1;; ++Suffix) {
     GlobalStringName.append(".str");
     if (!M.getNamedValue(GlobalStringName))
       break;
     GlobalStringName.resize(Name.size());
-    raw_svector_ostream(GlobalStringName) << '.' << Suffix + 1;
+    raw_svector_ostream(GlobalStringName) << '.' << Suffix;
     HeapNameLen = GlobalStringName.size();
   }
   StringRef HeapName = GlobalStringName.substr(0, HeapNameLen);
@@ -284,18 +286,37 @@ GlobalVariable *createHeapString(Module &M, StringRef Name) {
   return HeapNameGV;
 }
 
+// The SPIR-V backend represents dynamic resources as unbounded resource arrays.
+// This function scans the module for calls to
+// `llvm.spv.resource.handlefromheap` and groups them according to whether they
+// create CBV/SRV/UAV resources or samplers. It also collects calls to
+// `llvm.spv.resource.counterhandlefromheap` intrinsics that form a third group.
+///
+// The function assigns the first available binding to each non-empty heap group
+// in this order: CBV/SRV/UAV resources, samplers, and counters. For each group,
+// it will replace the heap intrinsic calls with their explicit
+// `handlefrombinding` equivalents using the assigned binding.
+//
+// The function does not actually create the unbounded resource-array globals
+// itself. It only assigns a unique name that is shared by all resources
+// belonging to the same heap type; the existing `SPIRVInstructionSelector` will
+// create the globals.
+
+// Because the CBV/SRV/UAV group can contain resources of different types, these
+// resources must be represented by separate arrays, one for each unique
+// resource type. All of these arrays will use the same binding and therefore
+// overlap.
 bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceHeapBindingCalls(
     Module &M) {
-  // First we need to collect all heap binding calls based on their kind
-  // because heap bindings are assigned in the order (CbvUavSrv, Samplers,
-  // Counters).
+  // First we collect all used heap binding declarations and group them based on
+  // their kind.
   SmallVector<Function *, 8> CbvSrvUavs;
   SmallVector<Function *, 8> Samplers;
   SmallVector<Function *, 8> Counters;
   bool Changed = false;
 
   for (Function &F : M) {
-    if (!F.isDeclaration())
+    if (!F.isDeclaration() || F.user_empty())
       continue;
 
     if (F.getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) {
@@ -321,9 +342,9 @@ bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceHeapBindingCalls(
       llvm::ConstantInt::get(llvm::Type::getInt32Ty(M.getContext()), 0);
 
   if (!CbvSrvUavs.empty()) {
-    // For CBV/UAV/SRV resources we need to create separate
-    // ResourceDecriptorHeap runtime array for each different resource type, and
-    // they are all going to be mapped to the same descriptor set.
+    // For CBV/UAV/SRV resources we need to create a different
+    // ResourceDecriptorHeap name for each unique resource type. They will all
+    // share the same binding and will overlap.
     uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
     SmallDenseMap<TargetExtType *, GlobalVariable *> ResourceDescriptorHeaps;
     for (Function *F : CbvSrvUavs) {
@@ -331,7 +352,7 @@ bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceHeapBindingCalls(
       GlobalVariable *HeapNameGV = nullptr;
       auto It = ResourceDescriptorHeaps.find(ResType);
       if (It == ResourceDescriptorHeaps.end()) {
-        HeapNameGV = createHeapString(M, "ResourceDescriptorHeap");
+        HeapNameGV = createHeapNameString(M, "ResourceDescriptorHeap");
         auto [InsertedIt, Inserted] =
             ResourceDescriptorHeaps.try_emplace(ResType, HeapNameGV);
         assert(Inserted && "resource heap name already exists");
@@ -358,7 +379,8 @@ bool SPIRVLegalizeImplicitAndHeapBindingImpl::replaceHeapBindingCalls(
     TargetExtType *SamplerHandleType =
         cast<TargetExtType>(Samplers.front()->getReturnType());
 
-    GlobalVariable *HeapNameGV = createHeapString(M, "SamplerDescriptorHeap");
+    GlobalVariable *HeapNameGV =
+        createHeapNameString(M, "SamplerDescriptorHeap");
     for (Function *F : Samplers) {
       assert(F->getReturnType() == SamplerHandleType &&
              "sampler handle type mismatch");
@@ -400,7 +422,7 @@ bool SPIRVLegalizeImplicitAndHeapBindingImpl::runOnModule(Module &M) {
   bool Changed = false;
   if (MayHaveImplicitBindings)
     Changed |= replaceImplicitBindingCalls(M);
-  if (HasHeapBinding)
+  if (MayHaveHeapBindings)
     Changed |= replaceHeapBindingCalls(M);
 
   return Changed;
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll
index 2d6eb659c49b9..b079f8959a157 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll
@@ -1,10 +1,10 @@
 ; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
 ; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
 
-; This test declares Buf at descriptor set 0, binding 1, and creates one CBV,
-; SRV, and UAV resource from the descriptor heap. Legalization creates a
-; distinct runtime-array type for each heap resource type. All three arrays
-; share descriptor set 0, binding 0 (the first available bindings).
+; This test declares storage buffer Buf at descriptor set 0, binding 1, and
+; creates one CBV, SRV, and UAV resource from the descriptor heap. Legalization
+; creates a distinct runtime-array type for each heap resource type. All three
+; arrays share descriptor set 0, binding 0 (the first available bindings).
 
 ; ModuleID = 'dyn-res-cvb-srv-uav.hlsl'
 target datalayout = "e-ve-i64:64-n8:16:32:64-G10"
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
index 982ec26a312db..18caf302a5ebd 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
@@ -28,8 +28,6 @@
 ; CHECK: [[Float32:%[0-9]+]] = OpTypeFloat 32
 ; CHECK: [[SamplerType:%[0-9]+]] = OpTypeSampler
 ; CHECK: [[ImageType:%[0-9]+]] = OpTypeImage [[Float32]] 2D 2 0 0 1 Unknown
-; CHECK: [[BufType:%[0-9]+]] = OpTypeImage [[Float32]] Buffer 2 0 0 2 Rgba32f
-; CHECK: [[BufPtr:%[0-9]+]] = OpTypePointer UniformConstant [[BufType]]
 ; CHECK: [[ResourceRTArray:%[0-9]+]] = OpTypeRuntimeArray [[ImageType]]
 ; CHECK: [[ResourceRTArrayPtr:%[0-9]+]] = OpTypePointer UniformConstant [[ResourceRTArray]]
 ; CHECK: [[SamplerRTArray:%[0-9]+]] = OpTypeRuntimeArray [[SamplerType]]
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
index 974c2d46ebf10..036c369c80be7 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
@@ -11,14 +11,14 @@
 
 ; CHECK-DAG: OpCapability RuntimeDescriptorArrayEXT
 
-; CHECK-DAG: OpName [[Heap:%[0-9]+]] "ResourceDescriptorHeap"
-; CHECK-DAG: OpDecorate [[Heap]] DescriptorSet 0
-; CHECK-DAG: OpDecorate [[Heap]] Binding 0
-
 ; CHECK-DAG: OpName [[Buf:%[0-9]+]] "Buf"
 ; CHECK-DAG: OpDecorate [[Buf]] DescriptorSet 0
 ; CHECK-DAG: OpDecorate [[Buf]] Binding 1
 
+; CHECK-DAG: OpName [[Heap:%[0-9]+]] "ResourceDescriptorHeap"
+; CHECK-DAG: OpDecorate [[Heap]] DescriptorSet 0
+; CHECK-DAG: OpDecorate [[Heap]] Binding 0
+
 ; CHECK-DAG: OpName [[CounterHeap:%[0-9]+]] "ResourceDescriptorHeap.counter"
 ; CHECK-DAG: OpDecorate [[CounterHeap]] DescriptorSet 0
 ; CHECK-DAG: OpDecorate [[CounterHeap]] Binding 2



More information about the llvm-commits mailing list