[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