[llvm] [SPIRV][HLSL] Implement CBuffer access lowering pass (PR #159136)

Steven Perron via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 18 11:21:50 PDT 2025


https://github.com/s-perron updated https://github.com/llvm/llvm-project/pull/159136

>From 75405bcd54abfd5400ba6375691d84b0ac9f6940 Mon Sep 17 00:00:00 2001
From: Steven Perron <stevenperron at google.com>
Date: Wed, 18 Jun 2025 09:41:58 -0400
Subject: [PATCH 1/3] [SPIRV][HLSL] Implement CBuffer access lowering pass

This patch introduces a new pass, SPIRVCBufferAccess, which is responsible for translating accesses to HLSL constant buffer (cbuffer) global variables into accesses to the proper SPIR-V resource.

The pass operates by:
1. Identifying all cbuffers via the `!hlsl.cbs` metadata.
2. Replacing all uses of cbuffer member global variables with `llvm.spv.resource.getpointer` intrinsics.
3. Cleaning up the original global variables and metadata.

This approach allows subsequent passes, like SPIRVEmitIntrinsics, to correctly fold GEPs into a single OpAccessChain instruction.

The patch also includes a comprehensive set of lit tests to cover various scenarios:
- Basic cbuffer access.
- Cbuffer access within a `ConstantExpr`.
- Unused and partially unused cbuffers.

This implements the SPIR-V version of
https://github.com/llvm/wg-hlsl/blob/main/proposals/0016-constant-buffers.md#lowering-to-buffer-load-intrinsics.
---
 llvm/lib/Frontend/HLSL/CBuffer.cpp            |   4 +-
 llvm/lib/Target/SPIRV/CMakeLists.txt          |   4 +-
 llvm/lib/Target/SPIRV/SPIRV.h                 |   2 +
 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp  | 172 ++++++++++++++++++
 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h    |  23 +++
 llvm/lib/Target/SPIRV/SPIRVPassRegistry.def   |   5 +
 llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp  |   4 +
 .../CodeGen/SPIRV/hlsl-resources/cbuffer.ll   |  49 +++++
 .../hlsl-resources/cbuffer_constant_expr.ll   |  57 ++++++
 .../SPIRV/hlsl-resources/cbuffer_unused.ll    |  62 +++++++
 10 files changed, 380 insertions(+), 2 deletions(-)
 create mode 100644 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
 create mode 100644 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h
 create mode 100644 llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_constant_expr.ll
 create mode 100644 llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_unused.ll

diff --git a/llvm/lib/Frontend/HLSL/CBuffer.cpp b/llvm/lib/Frontend/HLSL/CBuffer.cpp
index 37c0d912e09ee..407b6ad6d5a7e 100644
--- a/llvm/lib/Frontend/HLSL/CBuffer.cpp
+++ b/llvm/lib/Frontend/HLSL/CBuffer.cpp
@@ -17,7 +17,9 @@ using namespace llvm::hlsl;
 
 static size_t getMemberOffset(GlobalVariable *Handle, size_t Index) {
   auto *HandleTy = cast<TargetExtType>(Handle->getValueType());
-  assert(HandleTy->getName().ends_with(".CBuffer") && "Not a cbuffer type");
+  assert((HandleTy->getName().ends_with(".CBuffer") ||
+          HandleTy->getName() == "spirv.VulkanBuffer") &&
+         "Not a cbuffer type");
   assert(HandleTy->getNumTypeParameters() == 1 && "Expected layout type");
 
   auto *LayoutTy = cast<TargetExtType>(HandleTy->getTypeParameter(0));
diff --git a/llvm/lib/Target/SPIRV/CMakeLists.txt b/llvm/lib/Target/SPIRV/CMakeLists.txt
index 6660de995e954..46afe03648531 100644
--- a/llvm/lib/Target/SPIRV/CMakeLists.txt
+++ b/llvm/lib/Target/SPIRV/CMakeLists.txt
@@ -48,6 +48,7 @@ add_llvm_target(SPIRVCodeGen
   SPIRVTargetTransformInfo.cpp
   SPIRVUtils.cpp
   SPIRVEmitNonSemanticDI.cpp
+  SPIRVCBufferAccess.cpp
 
   LINK_COMPONENTS
   Analysis
@@ -57,8 +58,9 @@ add_llvm_target(SPIRVCodeGen
   Core
   Demangle
   GlobalISel
-  SPIRVAnalysis
+  FrontendHLSL
   MC
+  SPIRVAnalysis
   SPIRVDesc
   SPIRVInfo
   ScalarOpts
diff --git a/llvm/lib/Target/SPIRV/SPIRV.h b/llvm/lib/Target/SPIRV/SPIRV.h
index 1934e98ca512f..efd49b930aa34 100644
--- a/llvm/lib/Target/SPIRV/SPIRV.h
+++ b/llvm/lib/Target/SPIRV/SPIRV.h
@@ -21,6 +21,7 @@ class RegisterBankInfo;
 
 ModulePass *createSPIRVPrepareFunctionsPass(const SPIRVTargetMachine &TM);
 FunctionPass *createSPIRVStructurizerPass();
+ModulePass *createSPIRVCBufferAccessLegacyPass();
 FunctionPass *createSPIRVMergeRegionExitTargetsPass();
 FunctionPass *createSPIRVStripConvergenceIntrinsicsPass();
 ModulePass *createSPIRVLegalizeImplicitBindingPass();
@@ -43,6 +44,7 @@ void initializeSPIRVPreLegalizerPass(PassRegistry &);
 void initializeSPIRVPreLegalizerCombinerPass(PassRegistry &);
 void initializeSPIRVPostLegalizerPass(PassRegistry &);
 void initializeSPIRVStructurizerPass(PassRegistry &);
+void initializeSPIRVCBufferAccessLegacyPass(PassRegistry &);
 void initializeSPIRVEmitIntrinsicsPass(PassRegistry &);
 void initializeSPIRVEmitNonSemanticDIPass(PassRegistry &);
 void initializeSPIRVLegalizePointerCastPass(PassRegistry &);
diff --git a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
new file mode 100644
index 0000000000000..263d9b494c85f
--- /dev/null
+++ b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
@@ -0,0 +1,172 @@
+//===- SPIRVCBufferAccess.cpp - Translate CBuffer Loads
+//--------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This pass replaces all accesses to constant buffer global variables with
+// accesses to the proper SPIR-V resource. It's designed to run after the
+// DXIL preparation passes and before the main SPIR-V legalization passes.
+//
+// The pass operates as follows:
+// 1. It finds all constant buffers by looking for the `!hlsl.cbs` metadata.
+// 2. For each cbuffer, it finds the global variable holding the resource handle
+//    and the global variables for each of the cbuffer's members.
+// 3. For each member variable, it creates a call to the
+//    `llvm.spv.resource.getpointer` intrinsic. This intrinsic takes the
+//    resource handle and the member's index within the cbuffer as arguments.
+//    The result is a pointer to that member within the SPIR-V resource.
+// 4. It then replaces all uses of the original member global variable with the
+//    pointer returned by the `getpointer` intrinsic. This effectively retargets
+//    all loads and GEPs to the new resource pointer.
+// 5. Finally, it cleans up by deleting the original global variables and the
+//    `!hlsl.cbs` metadata.
+//
+// This approach allows subsequent passes, like SPIRVEmitIntrinsics, to
+// correctly handle GEPs that operate on the result of the `getpointer` call,
+// folding them into a single OpAccessChain instruction.
+//
+//===----------------------------------------------------------------------===//
+
+#include "SPIRVCBufferAccess.h"
+#include "SPIRV.h"
+#include "llvm/Frontend/HLSL/CBuffer.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/IntrinsicsSPIRV.h"
+#include "llvm/IR/Module.h"
+
+#define DEBUG_TYPE "spirv-cbuffer-access"
+using namespace llvm;
+
+// Finds the single instruction that defines the resource handle. This is
+// typically a call to `llvm.spv.resource.handlefrombinding`.
+static Instruction *findHandleDef(GlobalVariable *HandleVar) {
+  for (User *U : HandleVar->users()) {
+    if (auto *SI = dyn_cast<StoreInst>(U)) {
+      if (auto *I = dyn_cast<Instruction>(SI->getValueOperand())) {
+        return I;
+      }
+    }
+  }
+  return nullptr;
+}
+
+static bool replaceCBufferAccesses(Module &M) {
+  std::optional<hlsl::CBufferMetadata> CBufMD = hlsl::CBufferMetadata::get(M);
+  if (!CBufMD)
+    return false;
+
+  for (const hlsl::CBufferMapping &Mapping : *CBufMD) {
+    Instruction *HandleDef = findHandleDef(Mapping.Handle);
+    if (!HandleDef) {
+      // If there's no handle definition, it might be because the cbuffer is
+      // unused. In this case, we can just clean up the globals.
+      if (Mapping.Handle->use_empty()) {
+        for (const auto &Member : Mapping.Members) {
+          if (Member.GV->use_empty()) {
+            Member.GV->eraseFromParent();
+          }
+        }
+        Mapping.Handle->eraseFromParent();
+      }
+      continue;
+    }
+
+    // The handle definition should dominate all uses of the cbuffer members.
+    // We'll insert our getpointer calls right after it.
+    IRBuilder<> Builder(HandleDef->getNextNode());
+
+    for (uint32_t Index = 0; Index < Mapping.Members.size(); ++Index) {
+      GlobalVariable *MemberGV = Mapping.Members[Index].GV;
+      if (MemberGV->use_empty()) {
+        continue;
+      }
+
+      // Create the getpointer intrinsic call.
+      Value *IndexVal = Builder.getInt32(Index);
+      Type *PtrType = MemberGV->getType();
+      Value *GetPointerCall = Builder.CreateIntrinsic(
+          PtrType, Intrinsic::spv_resource_getpointer, {HandleDef, IndexVal});
+
+      // We cannot use replaceAllUsesWith here because some uses may be
+      // ConstantExprs, which cannot be replaced with non-constants.
+      SmallVector<User *, 4> Users(MemberGV->users());
+      for (User *U : Users) {
+        if (auto *CE = dyn_cast<ConstantExpr>(U)) {
+          SmallVector<Instruction *, 4> Insts;
+          std::function<void(ConstantExpr *)> findInstructions =
+              [&](ConstantExpr *Const) {
+                for (User *ConstU : Const->users()) {
+                  if (auto *ConstCE = dyn_cast<ConstantExpr>(ConstU)) {
+                    findInstructions(ConstCE);
+                  } else if (auto *I = dyn_cast<Instruction>(ConstU)) {
+                    Insts.push_back(I);
+                  }
+                }
+              };
+          findInstructions(CE);
+
+          for (Instruction *I : Insts) {
+            Instruction *NewInst = CE->getAsInstruction();
+            NewInst->insertBefore(I);
+            I->replaceUsesOfWith(CE, NewInst);
+            NewInst->replaceUsesOfWith(MemberGV, GetPointerCall);
+          }
+        } else {
+          U->replaceUsesOfWith(MemberGV, GetPointerCall);
+        }
+      }
+    }
+  }
+
+  // Now that all uses are replaced, clean up the globals and metadata.
+  for (const hlsl::CBufferMapping &Mapping : *CBufMD) {
+    for (const auto &Member : Mapping.Members) {
+      Member.GV->eraseFromParent();
+    }
+    // Erase the stores to the handle variable before erasing the handle itself.
+    SmallVector<Instruction *, 4> HandleStores;
+    for (User *U : Mapping.Handle->users()) {
+      if (auto *SI = dyn_cast<StoreInst>(U)) {
+        HandleStores.push_back(SI);
+      }
+    }
+    for (Instruction *I : HandleStores) {
+      I->eraseFromParent();
+    }
+    Mapping.Handle->eraseFromParent();
+  }
+
+  CBufMD->eraseFromModule();
+  return true;
+}
+
+PreservedAnalyses SPIRVCBufferAccess::run(Module &M,
+                                          ModuleAnalysisManager &AM) {
+  if (replaceCBufferAccesses(M)) {
+    return PreservedAnalyses::none();
+  }
+  return PreservedAnalyses::all();
+}
+
+namespace {
+class SPIRVCBufferAccessLegacy : public ModulePass {
+public:
+  bool runOnModule(Module &M) override { return replaceCBufferAccesses(M); }
+  StringRef getPassName() const override { return "SPIRV CBuffer Access"; }
+  SPIRVCBufferAccessLegacy() : ModulePass(ID) {}
+
+  static char ID; // Pass identification.
+};
+char SPIRVCBufferAccessLegacy::ID = 0;
+} // end anonymous namespace
+
+INITIALIZE_PASS(SPIRVCBufferAccessLegacy, DEBUG_TYPE, "SPIRV CBuffer Access",
+                false, false)
+
+ModulePass *llvm::createSPIRVCBufferAccessLegacyPass() {
+  return new SPIRVCBufferAccessLegacy();
+}
diff --git a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h
new file mode 100644
index 0000000000000..a6343ea1cd6a4
--- /dev/null
+++ b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h
@@ -0,0 +1,23 @@
+//===- SPIRVCBufferAccess.cpp - Translate CBuffer Loads
+//--------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+
+#ifndef LLVM_LIB_TARGET_SPIRV_SPIRVCBUFFERACCESS_H_
+#define LLVM_LIB_TARGET_SPIRV_SPIRVCBUFFERACCESS_H_
+
+#include "llvm/IR/PassManager.h"
+
+namespace llvm {
+
+class SPIRVCBufferAccess : public PassInfoMixin<SPIRVCBufferAccess> {
+public:
+  PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
+};
+
+} // namespace llvm
+
+#endif // LLVM_LIB_TARGET_SPIRV_SPIRVCBUFFERACCESS_H_
diff --git a/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def b/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
index e4f6c71d5d791..1ce131fe7b1bf 100644
--- a/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
+++ b/llvm/lib/Target/SPIRV/SPIRVPassRegistry.def
@@ -13,6 +13,11 @@
 
 // NOTE: NO INCLUDE GUARD DESIRED!
 
+#ifndef MODULE_PASS
+#define MODULE_PASS(NAME, CREATE_PASS)
+#endif
+MODULE_PASS("spirv-cbuffer-access", SPIRVCBufferAccess())
+#undef MODULE_PASS
 
 #ifndef FUNCTION_PASS
 #define FUNCTION_PASS(NAME, CREATE_PASS)
diff --git a/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp b/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp
index 9f6f9c7225357..4dc3644074296 100644
--- a/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVTargetMachine.cpp
@@ -12,6 +12,8 @@
 
 #include "SPIRVTargetMachine.h"
 #include "SPIRV.h"
+#include "SPIRVCBufferAccess.h"
+#include "SPIRVCallLowering.h"
 #include "SPIRVGlobalRegistry.h"
 #include "SPIRVLegalizerInfo.h"
 #include "SPIRVStructurizerWrapper.h"
@@ -48,6 +50,7 @@ extern "C" LLVM_ABI LLVM_EXTERNAL_VISIBILITY void LLVMInitializeSPIRVTarget() {
   initializeSPIRVAsmPrinterPass(PR);
   initializeSPIRVConvergenceRegionAnalysisWrapperPassPass(PR);
   initializeSPIRVStructurizerPass(PR);
+  initializeSPIRVCBufferAccessLegacyPass(PR);
   initializeSPIRVPreLegalizerCombinerPass(PR);
   initializeSPIRVLegalizePointerCastPass(PR);
   initializeSPIRVRegularizerPass(PR);
@@ -206,6 +209,7 @@ void SPIRVPassConfig::addISelPrepare() {
 
   addPass(createSPIRVStripConvergenceIntrinsicsPass());
   addPass(createSPIRVLegalizeImplicitBindingPass());
+  addPass(createSPIRVCBufferAccessLegacyPass());
   addPass(createSPIRVEmitIntrinsicsPass(&getTM<SPIRVTargetMachine>()));
   if (TM.getSubtargetImpl()->isLogicalSPIRV())
     addPass(createSPIRVLegalizePointerCastPass(&getTM<SPIRVTargetMachine>()));
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer.ll
new file mode 100644
index 0000000000000..60d515e45fe50
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer.ll
@@ -0,0 +1,49 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
+
+; CHECK-DAG: OpDecorate %[[MyCBuffer:[0-9]+]] DescriptorSet 0
+; CHECK-DAG: OpDecorate %[[MyCBuffer]] Binding 0
+; CHECK-DAG: OpMemberDecorate %[[__cblayout_MyCBuffer:[0-9]+]] 0 Offset 0
+; CHECK-DAG: OpMemberDecorate %[[__cblayout_MyCBuffer]] 1 Offset 16
+; CHECK-DAG: %[[uint:[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: %[[uint_0:[0-9]+]] = OpConstant %[[uint]] 0{{$}}
+; CHECK-DAG: %[[uint_1:[0-9]+]] = OpConstant %[[uint]] 1{{$}}
+; CHECK-DAG: %[[float:[0-9]+]] = OpTypeFloat 32
+; CHECK-DAG: %[[v4float:[0-9]+]] = OpTypeVector %[[float]] 4
+; CHECK-DAG: %[[__cblayout_MyCBuffer]] = OpTypeStruct %[[v4float]] %[[v4float]]
+; CHECK-DAG: %[[wrapper:[0-9]+]] = OpTypeStruct %[[__cblayout_MyCBuffer]]
+; CHECK-DAG: %[[wrapper_ptr_t:[0-9]+]] = OpTypePointer Uniform %[[wrapper]]
+; CHECK-DAG: %[[MyCBuffer]] = OpVariable %[[wrapper_ptr_t]] Uniform
+; CHECK-DAG: %[[_ptr_Uniform_v4float:[0-9]+]] = OpTypePointer Uniform %[[v4float]]
+
+%__cblayout_MyCBuffer = type <{ <4 x float>, <4 x float> }>
+
+ at MyCBuffer.cb = local_unnamed_addr global target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_MyCBuffer, 32, 0, 16), 2, 0) poison
+ at a = external hidden local_unnamed_addr addrspace(12) global <4 x float>, align 16
+ at b = external hidden local_unnamed_addr addrspace(12) global <4 x float>, align 16
+ at MyCBuffer.str = private unnamed_addr constant [10 x i8] c"MyCBuffer\00", align 1
+ at .str = private unnamed_addr constant [7 x i8] c"output\00", align 1
+
+; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, argmem: write, inaccessiblemem: none)
+define void @main() local_unnamed_addr #1 {
+entry:
+; CHECK: %[[tmp:[0-9]+]] = OpCopyObject %[[wrapper_ptr_t]] %[[MyCBuffer]]
+  %MyCBuffer.cb_h.i.i = tail call target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_MyCBuffer, 32, 0, 16), 2, 0) @llvm.spv.resource.handlefrombinding.tspirv.VulkanBuffer_tspirv.Layout_s___cblayout_MyCBuffers_32_0_16t_2_0t(i32 0, i32 0, i32 1, i32 0, ptr nonnull @MyCBuffer.str)
+  store target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_MyCBuffer, 32, 0, 16), 2, 0) %MyCBuffer.cb_h.i.i, ptr @MyCBuffer.cb, align 8
+  %0 = tail call target("spirv.Image", <4 x float>, 5, 2, 0, 0, 2, 3) @llvm.spv.resource.handlefrombinding.tspirv.Image_v4f32_5_2_0_0_2_3t(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+; CHECK: %[[a_ptr:.+]] = OpAccessChain %[[_ptr_Uniform_v4float]] %[[tmp]] %[[uint_0]] %[[uint_0]]
+; CHECK: %[[b_ptr:.+]] = OpAccessChain %[[_ptr_Uniform_v4float]] %[[tmp]] %[[uint_0]] %[[uint_1]]
+; CHECK: %[[a_val:.+]] = OpLoad %[[v4float]] %[[a_ptr]]
+; CHECK: %[[b_val:.+]] = OpLoad %[[v4float]] %[[b_ptr]]
+  %a_val = load <4 x float>, ptr addrspace(12) @a, align 16
+  %b_val = load <4 x float>, ptr addrspace(12) @b, align 16
+  %add = fadd <4 x float> %a_val, %b_val
+  %output_ptr = tail call noundef ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.Image_v4f32_5_2_0_0_2_3t(target("spirv.Image", <4 x float>, 5, 2, 0, 0, 2, 3) %0, i32 0)
+  store <4 x float> %add, ptr addrspace(11) %output_ptr, align 16
+  ret void
+}
+
+attributes #1 = { "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" }
+
+!hlsl.cbs = !{!0}
+
+!0 = !{ptr @MyCBuffer.cb, ptr addrspace(12) @a, ptr addrspace(12) @b}
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_constant_expr.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_constant_expr.ll
new file mode 100644
index 0000000000000..9661862ad191a
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_constant_expr.ll
@@ -0,0 +1,57 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
+; Test that uses of cbuffer members inside ConstantExprs are handled correctly.
+
+; CHECK-DAG: OpDecorate %[[MyCBuffer:[0-9]+]] DescriptorSet 0
+; CHECK-DAG: OpDecorate %[[MyCBuffer]] Binding 0
+; CHECK-DAG: OpMemberDecorate %[[__cblayout_MyCBuffer:[0-9]+]] 0 Offset 0
+; CHECK-DAG: OpMemberDecorate %[[__cblayout_MyCBuffer]] 1 Offset 16
+; CHECK-DAG: %[[uint:[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: %[[uint_0:[0-9]+]] = OpConstant %[[uint]] 0{{$}}
+; CHECK-DAG: %[[uint_1:[0-9]+]] = OpConstant %[[uint]] 1{{$}}
+; CHECK-DAG: %[[float:[0-9]+]] = OpTypeFloat 32
+; CHECK-DAG: %[[v4float:[0-9]+]] = OpTypeVector %[[float]] 4
+; CHECK-DAG: %[[MyStruct:[0-9]+]] = OpTypeStruct %[[v4float]]
+; CHECK-DAG: %[[__cblayout_MyCBuffer]] = OpTypeStruct %[[MyStruct]] %[[v4float]]
+; CHECK-DAG: %[[wrapper:[0-9]+]] = OpTypeStruct %[[__cblayout_MyCBuffer]]
+; CHECK-DAG: %[[wrapper_ptr_t:[0-9]+]] = OpTypePointer Uniform %[[wrapper]]
+; CHECK-DAG: %[[MyCBuffer]] = OpVariable %[[wrapper_ptr_t]] Uniform
+; CHECK-DAG: %[[_ptr_Uniform_v4float:[0-9]+]] = OpTypePointer Uniform %[[v4float]]
+; CHECK-DAG: %[[_ptr_Uniform_float:[0-9]+]] = OpTypePointer Uniform %[[float]]
+
+%MyStruct = type { <4 x float> }
+%__cblayout_MyCBuffer = type <{ %MyStruct, <4 x float> }>
+
+ at MyCBuffer.cb = local_unnamed_addr global target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_MyCBuffer, 32, 0, 16), 2, 0) poison
+ at s = external hidden local_unnamed_addr addrspace(12) global %MyStruct, align 16
+ at v = external hidden local_unnamed_addr addrspace(12) global <4 x float>, align 16
+ at MyCBuffer.str = private unnamed_addr constant [10 x i8] c"MyCBuffer\00", align 1
+ at .str = private unnamed_addr constant [7 x i8] c"output\00", align 1
+
+define void @main() {
+entry:
+; CHECK: %[[tmp:[0-9]+]] = OpCopyObject %[[wrapper_ptr_t]] %[[MyCBuffer]]
+  %MyCBuffer.cb_h.i.i = tail call target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_MyCBuffer, 32, 0, 16), 2, 0) @llvm.spv.resource.handlefrombinding.tspirv.VulkanBuffer_tspirv.Layout_s___cblayout_MyCBuffers_32_0_16t_2_0t(i32 0, i32 0, i32 1, i32 0, ptr nonnull @MyCBuffer.str)
+  store target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_MyCBuffer, 32, 0, 16), 2, 0) %MyCBuffer.cb_h.i.i, ptr @MyCBuffer.cb, align 8
+  %0 = tail call target("spirv.Image", float, 5, 2, 0, 0, 2, 3) @llvm.spv.resource.handlefrombinding.tspirv.Image_f32_5_2_0_0_2_3t(i32 0, i32 0, i32 1, i32 0, ptr nonnull @.str)
+  
+  ; This GEP is a ConstantExpr that uses @s
+; CHECK: %[[tmp_ptr:[0-9]+]] = OpAccessChain {{%[0-9]+}} %[[tmp]] %[[uint_0]] %[[uint_0]]
+; CHECK: %[[v_ptr:.+]] = OpAccessChain %[[_ptr_Uniform_v4float]] %[[tmp]] %[[uint_0]] %[[uint_1]]
+; CHECK: %[[s_ptr_gep:[0-9]+]] = OpInBoundsAccessChain %[[_ptr_Uniform_float]] %[[tmp_ptr]] %[[uint_0]] %[[uint_1]]
+  %gep = getelementptr inbounds %MyStruct, ptr addrspace(12) @s, i32 0, i32 0, i32 1
+
+; CHECK: %[[s_val:.+]] = OpLoad %[[float]] %[[s_ptr_gep]]
+  %load_from_gep = load float, ptr addrspace(12) %gep, align 4
+
+; CHECK: %[[v_val:.+]] = OpLoad %[[v4float]] %[[v_ptr]]
+  %load_v = load <4 x float>, ptr addrspace(12) @v, align 16
+
+  %extract_v = extractelement <4 x float> %load_v, i64 0
+  %add = fadd float %load_from_gep, %extract_v
+  %get_output_ptr = tail call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.Image_f32_5_2_0_0_2_3t(target("spirv.Image", float, 5, 2, 0, 0, 2, 3) %0, i32 0)
+  store float %add, ptr addrspace(11) %get_output_ptr, align 4
+  ret void
+}
+
+!hlsl.cbs = !{!0}
+!0 = !{ptr @MyCBuffer.cb, ptr addrspace(12) @s, ptr addrspace(12) @v}
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_unused.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_unused.ll
new file mode 100644
index 0000000000000..c365452a9b404
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/cbuffer_unused.ll
@@ -0,0 +1,62 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv1.6-vulkan1.3-library %s -o - | FileCheck %s
+; Test that unused and partially unused cbuffers are handled correctly.
+
+; CHECK-DAG: OpDecorate %[[PartiallyUsedCBuffer:[0-9]+]] DescriptorSet 0
+; CHECK-DAG: OpDecorate %[[PartiallyUsedCBuffer]] Binding 1
+; CHECK-DAG: OpDecorate %[[AnotherCBuffer:[0-9]+]] DescriptorSet 0
+; CHECK-DAG: OpDecorate %[[AnotherCBuffer]] Binding 2
+; CHECK-DAG: %[[float:[0-9]+]] = OpTypeFloat 32
+; CHECK-DAG: %[[v4float:[0-9]+]] = OpTypeVector %[[float]] 4
+; CHECK-DAG: %[[_cblayout_PartiallyUsedCBuffer:[0-9]+]] = OpTypeStruct %[[float]]
+; CHECK-DAG: %[[_cblayout_AnotherCBuffer:[0-9]+]] = OpTypeStruct %[[v4float]]
+
+%__cblayout_UnusedCBuffer = type <{ float }>
+%__cblayout_PartiallyUsedCBuffer = type <{ float, i32 }>
+%__cblayout_AnotherCBuffer = type <{ <4 x float>, <4 x float> }>
+
+ at UnusedCBuffer.cb = local_unnamed_addr global target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_UnusedCBuffer, 4, 0), 2, 0) poison
+ at UnusedCBuffer.str = private unnamed_addr constant [14 x i8] c"UnusedCBuffer\00", align 1
+ at PartiallyUsedCBuffer.cb = local_unnamed_addr global target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_PartiallyUsedCBuffer, 8, 0, 4), 2, 0) poison
+ at used_member = external hidden local_unnamed_addr addrspace(12) global float, align 4
+ at PartiallyUsedCBuffer.str = private unnamed_addr constant [21 x i8] c"PartiallyUsedCBuffer\00", align 1
+ at AnotherCBuffer.cb = local_unnamed_addr global target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_AnotherCBuffer, 32, 0, 16), 2, 0) poison
+ at a = external hidden local_unnamed_addr addrspace(12) global <4 x float>, align 16
+ at AnotherCBuffer.str = private unnamed_addr constant [15 x i8] c"AnotherCBuffer\00", align 1
+ at .str = private unnamed_addr constant [7 x i8] c"output\00", align 1
+
+
+; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, argmem: write, inaccessiblemem: none)
+define void @main() local_unnamed_addr #1 {
+entry:
+  %UnusedCBuffer.cb_h.i.i = tail call target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_UnusedCBuffer, 4, 0), 2, 0) @llvm.spv.resource.handlefromimplicitbinding.tspirv.VulkanBuffer_tspirv.Layout_s___cblayout_UnusedCBuffers_4_0t_2_0t(i32 0, i32 0, i32 1, i32 0, ptr nonnull @UnusedCBuffer.str)
+  store target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_UnusedCBuffer, 4, 0), 2, 0) %UnusedCBuffer.cb_h.i.i, ptr @UnusedCBuffer.cb, align 8
+
+; CHECK: %[[tmp:[0-9]+]] = OpCopyObject {{%[0-9]+}} %[[PartiallyUsedCBuffer]]
+; CHECK: %[[used_member_ptr:.+]] = OpAccessChain %{{.+}} %[[tmp]] %{{.+}} %[[uint_0:[0-9]+]]
+  %PartiallyUsedCBuffer.cb_h.i.i = tail call target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_PartiallyUsedCBuffer, 8, 0, 4), 2, 0) @llvm.spv.resource.handlefromimplicitbinding.tspirv.VulkanBuffer_tspirv.Layout_s___cblayout_PartiallyUsedCBuffers_8_0_4t_2_0t(i32 1, i32 0, i32 1, i32 0, ptr nonnull @PartiallyUsedCBuffer.str)
+  store target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_PartiallyUsedCBuffer, 8, 0, 4), 2, 0) %PartiallyUsedCBuffer.cb_h.i.i, ptr @PartiallyUsedCBuffer.cb, align 8
+
+; CHECK: %[[tmp:[0-9]+]] = OpCopyObject {{%[0-9]+}} %[[AnotherCBuffer]]
+; CHECK: %[[a_ptr:.+]] = OpAccessChain %{{.+}} %[[tmp]] %{{.+}} %[[uint_0]]
+  %AnotherCBuffer.cb_h.i.i = tail call target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_AnotherCBuffer, 32, 0, 16), 2, 0) @llvm.spv.resource.handlefromimplicitbinding.tspirv.VulkanBuffer_tspirv.Layout_s___cblayout_AnotherCBuffers_32_0_16t_2_0t(i32 2, i32 0, i32 1, i32 0, ptr nonnull @AnotherCBuffer.str)
+  store target("spirv.VulkanBuffer", target("spirv.Layout", %__cblayout_AnotherCBuffer, 32, 0, 16), 2, 0) %AnotherCBuffer.cb_h.i.i, ptr @AnotherCBuffer.cb, align 8
+  %0 = tail call target("spirv.Image", float, 5, 2, 0, 0, 2, 1) @llvm.spv.resource.handlefromimplicitbinding.tspirv.Image_f32_5_2_0_0_2_1t(i32 3, i32 0, i32 1, i32 0, ptr nonnull @.str)
+
+  %2 = load float, ptr addrspace(12) @used_member, align 4
+  %3 = load <4 x float>, ptr addrspace(12) @a, align 16
+  %4 = extractelement <4 x float> %3, i64 0
+  %add.i = fadd reassoc nnan ninf nsz arcp afn float %4, %2
+  %vecinit3.i = insertelement <4 x float> <float poison, float 0.000000e+00, float 0.000000e+00, float 0.000000e+00>, float %add.i, i64 0
+  %5 = tail call noundef align 16 dereferenceable(16) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.Image_f32_5_2_0_0_2_1t(target("spirv.Image", float, 5, 2, 0, 0, 2, 1) %0, i32 0)
+  store <4 x float> %vecinit3.i, ptr addrspace(11) %5, align 16
+  ret void
+}
+
+
+attributes #1 = { mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, argmem: write, inaccessiblemem: none) "frame-pointer"="all" "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
+
+!hlsl.cbs = !{!0, !1, !2}
+
+!0 = distinct !{ptr @UnusedCBuffer.cb, null}
+!1 = distinct !{ptr @PartiallyUsedCBuffer.cb, ptr addrspace(12) @used_member, null}
+!2 = distinct !{ptr @AnotherCBuffer.cb, ptr addrspace(12) @a, null}

>From 0e5355d18cd9a76231e625c8b5225fe9d4321549 Mon Sep 17 00:00:00 2001
From: Steven Perron <stevenperron at google.com>
Date: Wed, 17 Sep 2025 09:35:10 -0400
Subject: [PATCH 2/3] Remove use of deprecated function.

---
 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
index 263d9b494c85f..6fec1387c0d00 100644
--- a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
@@ -111,7 +111,7 @@ static bool replaceCBufferAccesses(Module &M) {
 
           for (Instruction *I : Insts) {
             Instruction *NewInst = CE->getAsInstruction();
-            NewInst->insertBefore(I);
+            NewInst->insertBefore(I->getIterator());
             I->replaceUsesOfWith(CE, NewInst);
             NewInst->replaceUsesOfWith(MemberGV, GetPointerCall);
           }

>From b562bf1ed1a08cd27327d4dfa710bce0857b8b21 Mon Sep 17 00:00:00 2001
From: Steven Perron <stevenperron at google.com>
Date: Thu, 18 Sep 2025 14:21:36 -0400
Subject: [PATCH 3/3] Fix for code review.

---
 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp | 63 ++++++++------------
 llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h   |  3 +-
 2 files changed, 26 insertions(+), 40 deletions(-)

diff --git a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
index 6fec1387c0d00..2441f0e1edbf3 100644
--- a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.cpp
@@ -1,5 +1,4 @@
-//===- SPIRVCBufferAccess.cpp - Translate CBuffer Loads
-//--------------------===//
+//===- SPIRVCBufferAccess.cpp - Translate CBuffer Loads ---------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -8,8 +7,7 @@
 //===----------------------------------------------------------------------===//
 //
 // This pass replaces all accesses to constant buffer global variables with
-// accesses to the proper SPIR-V resource. It's designed to run after the
-// DXIL preparation passes and before the main SPIR-V legalization passes.
+// accesses to the proper SPIR-V resource.
 //
 // The pass operates as follows:
 // 1. It finds all constant buffers by looking for the `!hlsl.cbs` metadata.
@@ -61,19 +59,7 @@ static bool replaceCBufferAccesses(Module &M) {
 
   for (const hlsl::CBufferMapping &Mapping : *CBufMD) {
     Instruction *HandleDef = findHandleDef(Mapping.Handle);
-    if (!HandleDef) {
-      // If there's no handle definition, it might be because the cbuffer is
-      // unused. In this case, we can just clean up the globals.
-      if (Mapping.Handle->use_empty()) {
-        for (const auto &Member : Mapping.Members) {
-          if (Member.GV->use_empty()) {
-            Member.GV->eraseFromParent();
-          }
-        }
-        Mapping.Handle->eraseFromParent();
-      }
-      continue;
-    }
+    // TODO: Issue error if HandleDef is nullptr.
 
     // The handle definition should dominate all uses of the cbuffer members.
     // We'll insert our getpointer calls right after it.
@@ -95,28 +81,29 @@ static bool replaceCBufferAccesses(Module &M) {
       // ConstantExprs, which cannot be replaced with non-constants.
       SmallVector<User *, 4> Users(MemberGV->users());
       for (User *U : Users) {
-        if (auto *CE = dyn_cast<ConstantExpr>(U)) {
-          SmallVector<Instruction *, 4> Insts;
-          std::function<void(ConstantExpr *)> findInstructions =
-              [&](ConstantExpr *Const) {
-                for (User *ConstU : Const->users()) {
-                  if (auto *ConstCE = dyn_cast<ConstantExpr>(ConstU)) {
-                    findInstructions(ConstCE);
-                  } else if (auto *I = dyn_cast<Instruction>(ConstU)) {
-                    Insts.push_back(I);
-                  }
-                }
-              };
-          findInstructions(CE);
-
-          for (Instruction *I : Insts) {
-            Instruction *NewInst = CE->getAsInstruction();
-            NewInst->insertBefore(I->getIterator());
-            I->replaceUsesOfWith(CE, NewInst);
-            NewInst->replaceUsesOfWith(MemberGV, GetPointerCall);
-          }
-        } else {
+        auto *CE = dyn_cast<ConstantExpr>(U);
+        if (!CE) {
           U->replaceUsesOfWith(MemberGV, GetPointerCall);
+          continue;
+        }
+        SmallVector<Instruction *, 4> Insts;
+        std::function<void(ConstantExpr *)> findInstructions =
+            [&](ConstantExpr *Const) {
+              for (User *ConstU : Const->users()) {
+                if (auto *ConstCE = dyn_cast<ConstantExpr>(ConstU)) {
+                  findInstructions(ConstCE);
+                } else if (auto *I = dyn_cast<Instruction>(ConstU)) {
+                  Insts.push_back(I);
+                }
+              }
+            };
+        findInstructions(CE);
+
+        for (Instruction *I : Insts) {
+          Instruction *NewInst = CE->getAsInstruction();
+          NewInst->insertBefore(I->getIterator());
+          I->replaceUsesOfWith(CE, NewInst);
+          NewInst->replaceUsesOfWith(MemberGV, GetPointerCall);
         }
       }
     }
diff --git a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h
index a6343ea1cd6a4..76941055c6f4c 100644
--- a/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h
+++ b/llvm/lib/Target/SPIRV/SPIRVCBufferAccess.h
@@ -1,5 +1,4 @@
-//===- SPIRVCBufferAccess.cpp - Translate CBuffer Loads
-//--------------------===//
+//===- SPIRVCBufferAccess.h - Translate CBuffer Loads ----------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.



More information about the llvm-commits mailing list