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

Helena Kotas via llvm-commits llvm-commits at lists.llvm.org
Tue Sep 22 12:51:19 PDT 2026


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

>From 614dfe9129c43dbb0bd18c64848a2f8a0397dd63 Mon Sep 17 00:00:00 2001
From: Helena Kotas <hekotas at microsoft.com>
Date: Fri, 18 Sep 2026 14:09:31 -0700
Subject: [PATCH 1/4] [SPIRV] Rename pass to SPIRVLegalizeResourceBinding

---
 llvm/lib/Target/SPIRV/CMakeLists.txt          |  2 +-
 llvm/lib/Target/SPIRV/SPIRV.h                 |  8 ++---
 .../Target/SPIRV/SPIRVCodeGenPassBuilder.cpp  |  2 +-
 ...g.cpp => SPIRVLegalizeResourceBinding.cpp} | 36 +++++++++----------
 llvm/lib/Target/SPIRV/SPIRVPassRegistry.def   |  2 +-
 llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp  |  4 +--
 llvm/test/CodeGen/SPIRV/llc-pipeline.ll       |  4 +--
 ... SPIRVLegalizeResourceBinding-Implicit.ll} |  2 +-
 8 files changed, 30 insertions(+), 30 deletions(-)
 rename llvm/lib/Target/SPIRV/{SPIRVLegalizeImplicitBinding.cpp => SPIRVLegalizeResourceBinding.cpp} (88%)
 rename llvm/test/CodeGen/SPIRV/passes/{SPIRVLegalizeImplicitBinding.ll => SPIRVLegalizeResourceBinding-Implicit.ll} (94%)

diff --git a/llvm/lib/Target/SPIRV/CMakeLists.txt b/llvm/lib/Target/SPIRV/CMakeLists.txt
index 32026491fdaedb..ab8b425ba35ca9 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
+  SPIRVLegalizeResourceBinding.cpp
   SPIRVLegalizeZeroSizeArrays.cpp
   SPIRVLegalizePointerCast.cpp
   SPIRVMergeRegionExitTargets.cpp
diff --git a/llvm/lib/Target/SPIRV/SPIRV.h b/llvm/lib/Target/SPIRV/SPIRV.h
index 8ac55ee28b13f0..ccc7aa52ec52c8 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 SPIRVLegalizeResourceBindingPass
+    : public RequiredPassInfoMixin<SPIRVLegalizeResourceBindingPass> {
 public:
   PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
 };
 
-ModulePass *createSPIRVLegalizeImplicitBindingPass();
+ModulePass *createSPIRVLegalizeResourceBindingPass();
 
 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 initializeSPIRVLegalizeResourceBindingLegacyPass(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 f47e2e56fd8a77..34c8060e97de57 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(SPIRVLegalizeResourceBindingPass(), 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/SPIRVLegalizeResourceBinding.cpp
similarity index 88%
rename from llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitBinding.cpp
rename to llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
index 40550ee9b29606..81a307a588f4d0 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizeImplicitBinding.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
@@ -1,4 +1,4 @@
-//===- SPIRVLegalizeImplicitBinding.cpp - Legalize implicit bindings ----*- C++
+//===- SPIRVLegalizeResourceBinding.cpp - Legalize resource bindings ----*- C++
 //-*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
@@ -28,7 +28,7 @@
 using namespace llvm;
 
 namespace {
-class SPIRVLegalizeImplicitBindingImpl {
+class SPIRVLegalizeResourceBindingImpl {
 public:
   bool runOnModule(Module &M);
 
@@ -45,15 +45,15 @@ class SPIRVLegalizeImplicitBindingImpl {
   bool MayHaveImplicitBindings = false;
 };
 
-class SPIRVLegalizeImplicitBindingLegacy : public ModulePass {
+class SPIRVLegalizeResourceBindingLegacy : public ModulePass {
 public:
   static char ID;
-  SPIRVLegalizeImplicitBindingLegacy() : ModulePass(ID) {}
+  SPIRVLegalizeResourceBindingLegacy() : ModulePass(ID) {}
   StringRef getPassName() const override {
-    return "SPIRV Legalize Implicit Binding";
+    return "SPIRV Legalize Resource Binding";
   }
   bool runOnModule(Module &M) override {
-    return SPIRVLegalizeImplicitBindingImpl().runOnModule(M);
+    return SPIRVLegalizeResourceBindingImpl().runOnModule(M);
   }
 };
 
@@ -75,7 +75,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 SPIRVLegalizeResourceBindingImpl::collectBindingInfo(Module &M) {
 
   auto addBinding = [&](uint32_t DescSet, uint32_t Binding) {
     if (UsedBindings.size() <= DescSet) {
@@ -122,7 +122,7 @@ void SPIRVLegalizeImplicitBindingImpl::collectBindingInfo(Module &M) {
   }
 }
 
-uint32_t SPIRVLegalizeImplicitBindingImpl::getAndReserveFirstUnusedBinding(
+uint32_t SPIRVLegalizeResourceBindingImpl::getAndReserveFirstUnusedBinding(
     uint32_t DescSet) {
   if (UsedBindings.size() <= DescSet) {
     UsedBindings.resize(DescSet + 1);
@@ -181,7 +181,7 @@ static void replaceWithCounterHandleFromBinding(Module &M, CallInst *CI,
   CI->eraseFromParent();
 }
 
-bool SPIRVLegalizeImplicitBindingImpl::replaceImplicitBindingCalls(Module &M) {
+bool SPIRVLegalizeResourceBindingImpl::replaceImplicitBindingCalls(Module &M) {
   // Collect all implicit binding calls.
   SmallVector<std::pair<uint32_t, CallInst *>> IBCalls;
   bool Changed = false;
@@ -247,7 +247,7 @@ bool SPIRVLegalizeImplicitBindingImpl::replaceImplicitBindingCalls(Module &M) {
   return Changed;
 }
 
-bool SPIRVLegalizeImplicitBindingImpl::runOnModule(Module &M) {
+bool SPIRVLegalizeResourceBindingImpl::runOnModule(Module &M) {
   collectBindingInfo(M);
 
   bool Changed = false;
@@ -259,18 +259,18 @@ bool SPIRVLegalizeImplicitBindingImpl::runOnModule(Module &M) {
 } // namespace
 
 PreservedAnalyses
-SPIRVLegalizeImplicitBindingPass::run(Module &M, ModuleAnalysisManager &AM) {
-  return SPIRVLegalizeImplicitBindingImpl().runOnModule(M)
+SPIRVLegalizeResourceBindingPass::run(Module &M, ModuleAnalysisManager &AM) {
+  return SPIRVLegalizeResourceBindingImpl().runOnModule(M)
              ? PreservedAnalyses::none()
              : PreservedAnalyses::all();
 }
 
-char SPIRVLegalizeImplicitBindingLegacy::ID = 0;
+char SPIRVLegalizeResourceBindingLegacy::ID = 0;
 
-INITIALIZE_PASS(SPIRVLegalizeImplicitBindingLegacy,
-                "legalize-spirv-implicit-binding",
-                "Legalize SPIR-V implicit bindings", false, false)
+INITIALIZE_PASS(SPIRVLegalizeResourceBindingLegacy,
+                "legalize-spirv-resource-binding",
+                "Legalize SPIR-V resource bindings", false, false)
 
-ModulePass *llvm::createSPIRVLegalizeImplicitBindingPass() {
-  return new SPIRVLegalizeImplicitBindingLegacy();
+ModulePass *llvm::createSPIRVLegalizeResourceBindingPass() {
+  return new SPIRVLegalizeResourceBindingLegacy();
 }
diff --git a/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def b/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
index eb73ae03ad3660..4cd3a94b017b4c 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-resource-binding", SPIRVLegalizeResourceBindingPass())
 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 6999afbaf21bec..4e1dd3647f78ab 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);
+  initializeSPIRVLegalizeResourceBindingLegacyPass(PR);
   initializeSPIRVCtorDtorLoweringLegacyPass(PR);
   initializeSPIRVFinalizeShaderLinkageLegacyPass(PR);
 }
@@ -224,7 +224,7 @@ void SPIRVPassConfig::addISelPrepare() {
   }
   SPIRVTargetMachine &TM = getTM<SPIRVTargetMachine>();
   addPass(createStripConvergenceIntrinsicsPass());
-  addPass(createSPIRVLegalizeImplicitBindingPass());
+  addPass(createSPIRVLegalizeResourceBindingPass());
   addPass(createSPIRVLegalizeZeroSizeArraysPass(TM));
   addPass(createSPIRVCBufferAccessLegacyPass());
   addPass(createSPIRVPushConstantAccessLegacyPass(&TM));
diff --git a/llvm/test/CodeGen/SPIRV/llc-pipeline.ll b/llvm/test/CodeGen/SPIRV/llc-pipeline.ll
index d79ee802861228..7b85311ef5bdaf 100644
--- a/llvm/test/CodeGen/SPIRV/llc-pipeline.ll
+++ b/llvm/test/CodeGen/SPIRV/llc-pipeline.ll
@@ -42,7 +42,7 @@
 ; SPIRV-O0-NEXT:      Natural Loop Information
 ; SPIRV-O0-NEXT:      Canonicalize natural loops
 ; SPIRV-O0-NEXT:      Strip convergence intrinsics and operand bundles
-; SPIRV-O0-NEXT:    SPIRV Legalize Implicit Binding
+; SPIRV-O0-NEXT:    SPIRV Legalize Resource Binding
 ; SPIRV-O0-NEXT:    SPIRV Legalize Zero-Size Arrays
 ; SPIRV-O0-NEXT:    SPIRV CBuffer Access
 ; SPIRV-O0-NEXT:    SPIRV push constant Access
@@ -153,7 +153,7 @@
 ; SPIRV-Opt-NEXT:      Natural Loop Information
 ; SPIRV-Opt-NEXT:      Canonicalize natural loops
 ; SPIRV-Opt-NEXT:      Strip convergence intrinsics and operand bundles
-; SPIRV-Opt-NEXT:    SPIRV Legalize Implicit Binding
+; SPIRV-Opt-NEXT:    SPIRV Legalize Resource Binding
 ; SPIRV-Opt-NEXT:    SPIRV Legalize Zero-Size Arrays
 ; SPIRV-Opt-NEXT:    SPIRV CBuffer Access
 ; SPIRV-Opt-NEXT:    SPIRV push constant Access
diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeResourceBinding-Implicit.ll
similarity index 94%
rename from llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll
rename to llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeResourceBinding-Implicit.ll
index eb0d0311bedf0b..c69c52f4af81c5 100644
--- a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeImplicitBinding.ll
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeResourceBinding-Implicit.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-resource-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 6b88ca3a6932138b9967dd51f04db5a346da5043 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/4] [SPIRV] Add handling of heap resources

Fixes #208054

Assisted by GPT-5.6 Sol
---
 llvm/include/llvm/IR/IntrinsicsSPIRV.td       |   9 +
 .../SPIRV/SPIRVLegalizeResourceBinding.cpp    | 193 ++++++++++++++++--
 .../hlsl-resources/HeapBinding-cvb-srv-uav.ll |  79 +++++++
 .../HeapBinding-texture-sampler.ll            |  68 ++++++
 .../hlsl-resources/HeapBinding-uav-counter.ll |  60 ++++++
 .../SPIRVLegalizeRegisterBinding-Heap.ll      |  44 ++++
 6 files changed, 441 insertions(+), 12 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/SPIRVLegalizeRegisterBinding-Heap.ll

diff --git a/llvm/include/llvm/IR/IntrinsicsSPIRV.td b/llvm/include/llvm/IR/IntrinsicsSPIRV.td
index 86b49a8ee446a2..d634253b1ccbe9 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/SPIRVLegalizeResourceBinding.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
index 81a307a588f4d0..4fc7bfb99fab67 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
@@ -8,14 +8,18 @@
 //===----------------------------------------------------------------------===//
 //
 // 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.
 //
 //===----------------------------------------------------------------------===//
 
 #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"
@@ -23,6 +27,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;
@@ -36,13 +41,16 @@ class SPIRVLegalizeResourceBindingImpl {
   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.
+  // 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 MayHaveHeapBindings = false;
 };
 
 class SPIRVLegalizeResourceBindingLegacy : public ModulePass {
@@ -116,6 +124,10 @@ void SPIRVLegalizeResourceBindingImpl::collectBindingInfo(Module &M) {
     case Intrinsic::spv_resource_counterhandlefromimplicitbinding:
       MayHaveImplicitBindings = true;
       break;
+    case Intrinsic::spv_resource_handlefromheap:
+    case Intrinsic::spv_resource_counterhandlefromheap:
+      MayHaveHeapBindings = true;
+      break;
     default:
       break;
     }
@@ -142,18 +154,19 @@ uint32_t SPIRVLegalizeResourceBindingImpl::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 &&
-         "unexpected implicit binding intrinsic");
+  assert((CI->getIntrinsicID() ==
+              Intrinsic::spv_resource_handlefromimplicitbinding ||
+          CI->getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) &&
+         "unexpected binding intrinsic");
   IRBuilder<> Builder(CI);
   Value *DescSetOp = Builder.getInt32(DescSet);
   Value *BindingOp = Builder.getInt32(Binding);
   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();
@@ -164,9 +177,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 binding intrinsic");
   IRBuilder<> Builder(CI);
   Value *DescSetOp = Builder.getInt32(DescSet);
   Value *BindingOp = Builder.getInt32(Binding);
@@ -247,12 +262,166 @@ bool SPIRVLegalizeResourceBindingImpl::replaceImplicitBindingCalls(Module &M) {
   return Changed;
 }
 
+GlobalVariable *createHeapNameString(Module &M, StringRef Name) {
+  SmallString<32> GlobalStringName(Name);
+  uint32_t HeapNameLen = Name.size();
+  for (unsigned Suffix = 1;; ++Suffix) {
+    GlobalStringName.append(".str");
+    if (!M.getNamedValue(GlobalStringName))
+      break;
+    GlobalStringName.resize(Name.size());
+    raw_svector_ostream(GlobalStringName) << '.' << Suffix;
+    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;
+}
+
+// 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 SPIRVLegalizeResourceBindingImpl::replaceHeapBindingCalls(Module &M) {
+  // 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() || F.user_empty())
+      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 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) {
+      TargetExtType *ResType = cast<TargetExtType>(F->getReturnType());
+      GlobalVariable *HeapNameGV = nullptr;
+      auto It = ResourceDescriptorHeaps.find(ResType);
+      if (It == ResourceDescriptorHeaps.end()) {
+        HeapNameGV = createHeapNameString(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 =
+        createHeapNameString(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 SPIRVLegalizeResourceBindingImpl::runOnModule(Module &M) {
   collectBindingInfo(M);
 
   bool Changed = false;
   if (MayHaveImplicitBindings)
     Changed |= replaceImplicitBindingCalls(M);
+  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
new file mode 100644
index 00000000000000..b079f8959a157e
--- /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 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"
+
+%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 00000000000000..18caf302a5ebd0
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
@@ -0,0 +1,68 @@
+; 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: [[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 00000000000000..036c369c80be75
--- /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 [[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
+
+; 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/SPIRVLegalizeRegisterBinding-Heap.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeRegisterBinding-Heap.ll
new file mode 100644
index 00000000000000..e4afc7a956e16c
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeRegisterBinding-Heap.ll
@@ -0,0 +1,44 @@
+; RUN: opt -S -passes=spirv-legalize-resource-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 2b940d469cd5716fc0b0d451bd48318f6cc44403 Mon Sep 17 00:00:00 2001
From: Helena Kotas <hekotas at microsoft.com>
Date: Mon, 21 Sep 2026 18:10:19 -0700
Subject: [PATCH 3/4] update validator environment

---
 .../CodeGen/SPIRV/hlsl-resources/HeapBinding-cvb-srv-uav.ll     | 2 +-
 .../CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll | 2 +-
 .../CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

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 b079f8959a157e..084bb40ecf8961 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,5 +1,5 @@
 ; 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 %}
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env spv1.6 %}
 
 ; 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
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 18caf302a5ebd0..3c5b0fb68ae181 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
@@ -1,5 +1,5 @@
 ; 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 %}
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env spv1.6 %}
 
 ; This test declares storage buffer Buf at descriptor set 0, binding 1, and
 ; creates one texture and two samplers by dynamically indexing into descriptor
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 036c369c80be75..4613b27d0aee31 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-uav-counter.ll
@@ -1,5 +1,5 @@
 ; 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 %}
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env spv1.6 %}
 
 ; 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.

>From a8345387a77cdf46f488ca5ab823d1b98448ad09 Mon Sep 17 00:00:00 2001
From: Helena Kotas <hekotas at microsoft.com>
Date: Tue, 22 Sep 2026 11:33:10 -0700
Subject: [PATCH 4/4] code review feedback - add check for existing strings and
 other fixes

---
 .../SPIRV/SPIRVLegalizeResourceBinding.cpp    | 36 ++++++++++++++-----
 .../HeapBinding-texture-sampler.ll            |  2 +-
 .../SPIRVLegalizeRegisterBinding-Heap2.ll     | 26 ++++++++++++++
 3 files changed, 55 insertions(+), 9 deletions(-)
 create mode 100644 llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeRegisterBinding-Heap2.ll

diff --git a/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp b/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
index 4fc7bfb99fab67..ced6932fb9bd49 100644
--- a/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVLegalizeResourceBinding.cpp
@@ -262,19 +262,38 @@ bool SPIRVLegalizeResourceBindingImpl::replaceImplicitBindingCalls(Module &M) {
   return Changed;
 }
 
+bool moduleContainsConstantString(Module &M, StringRef Str) {
+  for (GlobalVariable &GV : M.globals()) {
+    if (!GV.hasInitializer())
+      continue;
+    if (ConstantDataArray *CDA =
+            dyn_cast<ConstantDataArray>(GV.getInitializer())) {
+      if (CDA->isString() && CDA->getAsCString() == Str)
+        return true;
+    }
+  }
+  return false;
+}
+
+// Creates unique global variable for the heap name string, making
+// sure that each heap name string is unique within the module.
 GlobalVariable *createHeapNameString(Module &M, StringRef Name) {
   SmallString<32> GlobalStringName(Name);
   uint32_t HeapNameLen = Name.size();
+  StringRef HeapName;
   for (unsigned Suffix = 1;; ++Suffix) {
     GlobalStringName.append(".str");
-    if (!M.getNamedValue(GlobalStringName))
-      break;
+    if (!M.getNamedValue(GlobalStringName)) {
+      // Make sure the module does not already have a constant
+      // string with this value.
+      HeapName = GlobalStringName.substr(0, HeapNameLen);
+      if (!moduleContainsConstantString(M, HeapName))
+        break;
+    }
     GlobalStringName.resize(Name.size());
     raw_svector_ostream(GlobalStringName) << '.' << Suffix;
     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,
@@ -290,7 +309,7 @@ GlobalVariable *createHeapNameString(Module &M, StringRef Name) {
 // `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
@@ -300,7 +319,7 @@ GlobalVariable *createHeapNameString(Module &M, StringRef Name) {
 // 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
@@ -341,7 +360,7 @@ bool SPIRVLegalizeResourceBindingImpl::replaceHeapBindingCalls(Module &M) {
 
   if (!CbvSrvUavs.empty()) {
     // For CBV/UAV/SRV resources we need to create a different
-    // ResourceDecriptorHeap name for each unique resource type. They will all
+    // ResourceDescriptorHeap 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;
@@ -396,10 +415,11 @@ bool SPIRVLegalizeResourceBindingImpl::replaceHeapBindingCalls(Module &M) {
 
   if (!Counters.empty()) {
     uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
+    [[maybe_unused]] Type *CounterHandleTy = Counters.front()->getReturnType();
     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() &&
+      assert(F->getReturnType() == CounterHandleTy &&
              "counter handle type mismatch");
       for (User *U : make_early_inc_range(F->users())) {
         if (CallInst *CI = dyn_cast<CallInst>(U)) {
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 3c5b0fb68ae181..8f29f498381744 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/HeapBinding-texture-sampler.ll
@@ -62,7 +62,7 @@ entry:
   %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
+  store <4 x float> %Value1, ptr addrspace(11) %BufPtr1, align 4
 
   ret void
 }
diff --git a/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeRegisterBinding-Heap2.ll b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeRegisterBinding-Heap2.ll
new file mode 100644
index 00000000000000..aa469afa96d3ce
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/passes/SPIRVLegalizeRegisterBinding-Heap2.ll
@@ -0,0 +1,26 @@
+; RUN: opt -S -passes=spirv-legalize-resource-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
+ at .str = private unnamed_addr constant [23 x i8] c"ResourceDescriptorHeap\00", align 1
+
+; Verify that a unique string is created for the heap name if the constant
+; string "ResourceDescriptorHeap" already exists in the module.
+
+; CHECK-DAG: @ResourceDescriptorHeap.1.str = private unnamed_addr constant [25 x i8] c"ResourceDescriptorHeap.1\00", align 1
+; CHECK-NOT: @ResourceDescriptorHeap.str = private unnamed_addr constant [23 x i8] c"ResourceDescriptorHeap\00", align 1
+
+; CHECK-LABEL: define void @main(
+define void @main() local_unnamed_addr #0 {
+entry:
+
+; Heap resource - bound to resource array 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 0, i32 13, ptr @ResourceDescriptorHeap.1.str)
+  %heap_resource = 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)
+
+  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



More information about the llvm-commits mailing list