Skip to content

[HLSL] Use 0 to represent unbounded resources#186022

Merged
joaosaffran merged 14 commits into
llvm:mainfrom
joaosaffran:bugfix/hlsl/use-0-unbounded
Mar 16, 2026
Merged

[HLSL] Use 0 to represent unbounded resources#186022
joaosaffran merged 14 commits into
llvm:mainfrom
joaosaffran:bugfix/hlsl/use-0-unbounded

Conversation

@joaosaffran

@joaosaffran joaosaffran commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

SPIRV backend uses 0 to represent unbounded arrays. This patch makes unbounded resources be represented with 0 when binding them, as well as makes sure the backend uses OpTypeRuntimeArray to represent such cases.
Fix: #183367

@llvmbot llvmbot added clang:codegen IR generation bugs: mangling, exceptions, etc. HLSL HLSL Language Support backend:SPIR-V labels Mar 12, 2026
@llvmbot

llvmbot commented Mar 12, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-llvm-analysis
@llvm/pr-subscribers-backend-directx
@llvm/pr-subscribers-hlsl

@llvm/pr-subscribers-clang-codegen

Author: None (joaosaffran)

Changes

SPIRV backend uses 0 to represent unbounded arrays. This patch makes unbounded resources account for this, as well as makes sure the backend uses OpTypeRuntimeArray to represent such cases.


Full diff: https://github.com/llvm/llvm-project/pull/186022.diff

4 Files Affected:

  • (modified) clang/lib/CodeGen/CGHLSLRuntime.cpp (+6-4)
  • (modified) clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl (+11-6)
  • (modified) llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp (+4)
  • (added) llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll (+34)
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.cpp b/clang/lib/CodeGen/CGHLSLRuntime.cpp
index b695d016c0524..04daa5e611412 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.cpp
+++ b/clang/lib/CodeGen/CGHLSLRuntime.cpp
@@ -117,11 +117,13 @@ static const ValueDecl *getArrayDecl(const ArraySubscriptExpr *ASE) {
 }
 
 // Get the total size of the array, or -1 if the array is unbounded.
-static int getTotalArraySize(ASTContext &AST, const clang::Type *Ty) {
+static int getTotalArraySize(ASTContext &AST, llvm::Triple::ArchType Arch,
+                             const clang::Type *Ty) {
   Ty = Ty->getUnqualifiedDesugaredType();
   assert(Ty->isArrayType() && "expected array type");
   if (Ty->isIncompleteArrayType())
-    return -1;
+    // Spirv uses 0 to represent unbounded arrays.
+    return Arch == llvm::Triple::ArchType::dxil ? -1 : 0;
   return AST.getConstantArrayElementCount(cast<ConstantArrayType>(Ty));
 }
 
@@ -1280,7 +1282,7 @@ std::optional<LValue> CGHLSLRuntime::emitResourceArraySubscriptExpr(
 
   // Calculate total array size (= range size).
   llvm::Value *Range = llvm::ConstantInt::getSigned(
-      CGM.IntTy, getTotalArraySize(AST, ResArrayTy));
+      CGM.IntTy, getTotalArraySize(AST, getArch(), ResArrayTy));
 
   // If the result of the subscript operation is a single resource, call the
   // constructor.
@@ -1345,7 +1347,7 @@ bool CGHLSLRuntime::emitResourceArrayCopy(LValue &LHS, Expr *RHSExpr,
       AggValueSlot::DoesNotOverlap);
 
   // Create Value for index and total array size (= range size).
-  int Size = getTotalArraySize(AST, ResArrayTy);
+  int Size = getTotalArraySize(AST, getArch(), ResArrayTy);
   llvm::Value *Zero = llvm::ConstantInt::get(CGM.IntTy, 0);
   llvm::Value *Range = llvm::ConstantInt::get(CGM.IntTy, Size);
 
diff --git a/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl b/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl
index 6756a26bfc124..7d5b9626db138 100644
--- a/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl
+++ b/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl
@@ -30,14 +30,15 @@ void main(uint GI : SV_GroupIndex) {
   // and explicit binding (u10, space1) 
   // CHECK: @hlsl::RWBuffer<float>::__createFromBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer.0") align {{(4|8)}} %[[Tmp0]],
-  // CHECK-SAME: i32 noundef 10, i32 noundef 1, i32 noundef -1, i32 noundef 100, ptr noundef @A.str)
+  // DXIL-SAME: i32 noundef 10, i32 noundef 1, i32 noundef -1, i32 noundef 100, ptr noundef @A.str)
+  // SPV-SAME: i32 noundef 10, i32 noundef 1, i32 noundef 0, i32 noundef 100, ptr noundef @A.str)
   // CHECK-NEXT: %[[BufPtr:.*]] = call {{.*}} ptr{{.*}} @hlsl::RWBuffer<float>::operator[](unsigned int)(ptr {{.*}} %[[Tmp0]], i32 noundef 0)
   // CHECK-NEXT: %[[Value1:.*]] = load float, ptr{{.*}} %[[BufPtr]], align 4
   // CHECK-NEXT: store float %[[Value1]], ptr %a, align 4
   float a = A[100][0];
 
   // Make sure B[2][3] is translated to a local RWBuffer<int>[4] array where each array element
-  // is initialized by a constructor call with range -1 and index 52-55 and implicit binding 
+  // is initialized by a constructor call with range 0 and index 52-55 and implicit binding 
   // (space 0, order_id 0) 
   // The first index is calculated from the array dimensions (unbounded x 5 x 4) and indices (2, 3)
   // as 2 * 5 * 4 + 3 * 4 = 52 and the following indices are sequential.
@@ -45,22 +46,26 @@ void main(uint GI : SV_GroupIndex) {
   // CHECK-NEXT: %[[Ptr_Tmp2_0:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 0
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_0]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 52, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 52, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 52, ptr noundef @[[BufB]])
   
   // CHECK-NEXT: %[[Ptr_Tmp2_1:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 1
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_1]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 53, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 53, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 53, ptr noundef @[[BufB]])
   
   // CHECK-NEXT: %[[Ptr_Tmp2_2:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 2
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_2]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 54, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 54, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 54, ptr noundef @[[BufB]])
 
   // CHECK-NEXT: %[[Ptr_Tmp2_3:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 3
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_3]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 55, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 55, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 55, ptr noundef @[[BufB]])
 
   // DXIL-NEXT: call void @llvm.memcpy.p0.p0.i32(ptr align 4 %[[Tmp1]], ptr align 4 %[[Tmp2]], i32 16, i1 false)
   // SPV-NEXT: call void @llvm.memcpy.p0.p0.i64(ptr align 8 %[[Tmp1]], ptr align 8 %[[Tmp2]], i64 32, i1 false)
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index 3e50e4a0e8c80..5e966df6372db 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -6012,6 +6012,10 @@ bool SPIRVInstructionSelector::loadHandleBeforePosition(
     SC = GR.getPointerStorageClass(ResType);
   }
 
+  if (ResType->getOpcode() == SPIRV::OpTypeImage && ArraySize == 0)
+    MIRBuilder.buildInstr(SPIRV::OpCapability)
+        .addImm(SPIRV::Capability::RuntimeDescriptorArrayEXT);
+
   Register VarReg =
       buildPointerToResource(SPIRVTypeInst(VarType), SC, Set, Binding,
                              ArraySize, IndexReg, Name, MIRBuilder);
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll
new file mode 100644
index 0000000000000..8be0da57a797b
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll
@@ -0,0 +1,34 @@
+; 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 %}
+
+; CHECK-DAG: OpCapability RuntimeDescriptorArrayEXT
+; CHECK-DAG: %[[int32:[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: %[[rwbuffer:[0-9]+]] = OpTypeImage %[[int32]] Buffer 2 0 0 2 R32i
+; CHECK-DAG: OpTypeRuntimeArray %[[rwbuffer]]
+
+; This IR was emmited from the following HLSL code:
+; [[vk::binding(0)]]
+; RWBuffer<int> Buf[] : register(u0);
+; 
+; [numthreads(4,2,1)]
+; void main(uint GI : SV_GroupIndex) {
+;     Buf[0][0] = 0;
+; }
+
+
+
+@Buf.str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+; Function Attrs: convergent noinline norecurse
+define void @main() #0 {
+entry:
+  %2 = call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32 0, i32 0, i32 0, i32 0, ptr @Buf.str)
+  %3 = call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.SignedImage_i32_5_2_0_0_2_24t(target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24)  %2, i32 0)
+  store i32 0, ptr addrspace(11) %3, align 4
+  ret void
+}
+; Function Attrs: nocallback nofree nosync nounwind willreturn memory(none)
+declare target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32, i32, i32, i32, ptr) #3
+
+attributes #0 = { convergent noinline norecurse "hlsl.numthreads"="4,2,1" "hlsl.shader"="compute" "no-nans-fp-math"="true" "no-signed-zeros-fp-math"="true" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
+attributes #3 = { nocallback nofree nosync nounwind willreturn memory(none) }

@llvmbot

llvmbot commented Mar 12, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-backend-spir-v

Author: None (joaosaffran)

Changes

SPIRV backend uses 0 to represent unbounded arrays. This patch makes unbounded resources account for this, as well as makes sure the backend uses OpTypeRuntimeArray to represent such cases.


Full diff: https://github.com/llvm/llvm-project/pull/186022.diff

4 Files Affected:

  • (modified) clang/lib/CodeGen/CGHLSLRuntime.cpp (+6-4)
  • (modified) clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl (+11-6)
  • (modified) llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp (+4)
  • (added) llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll (+34)
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.cpp b/clang/lib/CodeGen/CGHLSLRuntime.cpp
index b695d016c0524..04daa5e611412 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.cpp
+++ b/clang/lib/CodeGen/CGHLSLRuntime.cpp
@@ -117,11 +117,13 @@ static const ValueDecl *getArrayDecl(const ArraySubscriptExpr *ASE) {
 }
 
 // Get the total size of the array, or -1 if the array is unbounded.
-static int getTotalArraySize(ASTContext &AST, const clang::Type *Ty) {
+static int getTotalArraySize(ASTContext &AST, llvm::Triple::ArchType Arch,
+                             const clang::Type *Ty) {
   Ty = Ty->getUnqualifiedDesugaredType();
   assert(Ty->isArrayType() && "expected array type");
   if (Ty->isIncompleteArrayType())
-    return -1;
+    // Spirv uses 0 to represent unbounded arrays.
+    return Arch == llvm::Triple::ArchType::dxil ? -1 : 0;
   return AST.getConstantArrayElementCount(cast<ConstantArrayType>(Ty));
 }
 
@@ -1280,7 +1282,7 @@ std::optional<LValue> CGHLSLRuntime::emitResourceArraySubscriptExpr(
 
   // Calculate total array size (= range size).
   llvm::Value *Range = llvm::ConstantInt::getSigned(
-      CGM.IntTy, getTotalArraySize(AST, ResArrayTy));
+      CGM.IntTy, getTotalArraySize(AST, getArch(), ResArrayTy));
 
   // If the result of the subscript operation is a single resource, call the
   // constructor.
@@ -1345,7 +1347,7 @@ bool CGHLSLRuntime::emitResourceArrayCopy(LValue &LHS, Expr *RHSExpr,
       AggValueSlot::DoesNotOverlap);
 
   // Create Value for index and total array size (= range size).
-  int Size = getTotalArraySize(AST, ResArrayTy);
+  int Size = getTotalArraySize(AST, getArch(), ResArrayTy);
   llvm::Value *Zero = llvm::ConstantInt::get(CGM.IntTy, 0);
   llvm::Value *Range = llvm::ConstantInt::get(CGM.IntTy, Size);
 
diff --git a/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl b/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl
index 6756a26bfc124..7d5b9626db138 100644
--- a/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl
+++ b/clang/test/CodeGenHLSL/resources/res-array-global-unbounded.hlsl
@@ -30,14 +30,15 @@ void main(uint GI : SV_GroupIndex) {
   // and explicit binding (u10, space1) 
   // CHECK: @hlsl::RWBuffer<float>::__createFromBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer.0") align {{(4|8)}} %[[Tmp0]],
-  // CHECK-SAME: i32 noundef 10, i32 noundef 1, i32 noundef -1, i32 noundef 100, ptr noundef @A.str)
+  // DXIL-SAME: i32 noundef 10, i32 noundef 1, i32 noundef -1, i32 noundef 100, ptr noundef @A.str)
+  // SPV-SAME: i32 noundef 10, i32 noundef 1, i32 noundef 0, i32 noundef 100, ptr noundef @A.str)
   // CHECK-NEXT: %[[BufPtr:.*]] = call {{.*}} ptr{{.*}} @hlsl::RWBuffer<float>::operator[](unsigned int)(ptr {{.*}} %[[Tmp0]], i32 noundef 0)
   // CHECK-NEXT: %[[Value1:.*]] = load float, ptr{{.*}} %[[BufPtr]], align 4
   // CHECK-NEXT: store float %[[Value1]], ptr %a, align 4
   float a = A[100][0];
 
   // Make sure B[2][3] is translated to a local RWBuffer<int>[4] array where each array element
-  // is initialized by a constructor call with range -1 and index 52-55 and implicit binding 
+  // is initialized by a constructor call with range 0 and index 52-55 and implicit binding 
   // (space 0, order_id 0) 
   // The first index is calculated from the array dimensions (unbounded x 5 x 4) and indices (2, 3)
   // as 2 * 5 * 4 + 3 * 4 = 52 and the following indices are sequential.
@@ -45,22 +46,26 @@ void main(uint GI : SV_GroupIndex) {
   // CHECK-NEXT: %[[Ptr_Tmp2_0:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 0
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_0]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 52, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 52, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 52, ptr noundef @[[BufB]])
   
   // CHECK-NEXT: %[[Ptr_Tmp2_1:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 1
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_1]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 53, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 53, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 53, ptr noundef @[[BufB]])
   
   // CHECK-NEXT: %[[Ptr_Tmp2_2:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 2
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_2]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 54, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 54, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 54, ptr noundef @[[BufB]])
 
   // CHECK-NEXT: %[[Ptr_Tmp2_3:.*]] = getelementptr [4 x %"class.hlsl::RWBuffer"], ptr %[[Tmp2]], i32 0, i32 3
   // CHECK-NEXT: call void @hlsl::RWBuffer<int>::__createFromImplicitBinding(unsigned int, unsigned int, int, unsigned int, char const*)
   // CHECK-SAME: (ptr {{.*}} sret(%"class.hlsl::RWBuffer") align {{(4|8)}} %[[Ptr_Tmp2_3]], 
-  // CHECK-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 55, ptr noundef @[[BufB]])
+  // DXIL-SAME: i32 noundef 0, i32 noundef 0, i32 noundef -1, i32 noundef 55, ptr noundef @[[BufB]])
+  // SPV-SAME: i32 noundef 0, i32 noundef 0, i32 noundef 0, i32 noundef 55, ptr noundef @[[BufB]])
 
   // DXIL-NEXT: call void @llvm.memcpy.p0.p0.i32(ptr align 4 %[[Tmp1]], ptr align 4 %[[Tmp2]], i32 16, i1 false)
   // SPV-NEXT: call void @llvm.memcpy.p0.p0.i64(ptr align 8 %[[Tmp1]], ptr align 8 %[[Tmp2]], i64 32, i1 false)
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index 3e50e4a0e8c80..5e966df6372db 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -6012,6 +6012,10 @@ bool SPIRVInstructionSelector::loadHandleBeforePosition(
     SC = GR.getPointerStorageClass(ResType);
   }
 
+  if (ResType->getOpcode() == SPIRV::OpTypeImage && ArraySize == 0)
+    MIRBuilder.buildInstr(SPIRV::OpCapability)
+        .addImm(SPIRV::Capability::RuntimeDescriptorArrayEXT);
+
   Register VarReg =
       buildPointerToResource(SPIRVTypeInst(VarType), SC, Set, Binding,
                              ArraySize, IndexReg, Name, MIRBuilder);
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll
new file mode 100644
index 0000000000000..8be0da57a797b
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/unbounded-arr.ll
@@ -0,0 +1,34 @@
+; 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 %}
+
+; CHECK-DAG: OpCapability RuntimeDescriptorArrayEXT
+; CHECK-DAG: %[[int32:[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: %[[rwbuffer:[0-9]+]] = OpTypeImage %[[int32]] Buffer 2 0 0 2 R32i
+; CHECK-DAG: OpTypeRuntimeArray %[[rwbuffer]]
+
+; This IR was emmited from the following HLSL code:
+; [[vk::binding(0)]]
+; RWBuffer<int> Buf[] : register(u0);
+; 
+; [numthreads(4,2,1)]
+; void main(uint GI : SV_GroupIndex) {
+;     Buf[0][0] = 0;
+; }
+
+
+
+@Buf.str = private unnamed_addr constant [4 x i8] c"Buf\00", align 1
+
+; Function Attrs: convergent noinline norecurse
+define void @main() #0 {
+entry:
+  %2 = call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32 0, i32 0, i32 0, i32 0, ptr @Buf.str)
+  %3 = call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.SignedImage_i32_5_2_0_0_2_24t(target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24)  %2, i32 0)
+  store i32 0, ptr addrspace(11) %3, align 4
+  ret void
+}
+; Function Attrs: nocallback nofree nosync nounwind willreturn memory(none)
+declare target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32, i32, i32, i32, ptr) #3
+
+attributes #0 = { convergent noinline norecurse "hlsl.numthreads"="4,2,1" "hlsl.shader"="compute" "no-nans-fp-math"="true" "no-signed-zeros-fp-math"="true" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
+attributes #3 = { nocallback nofree nosync nounwind willreturn memory(none) }

@bogner bogner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not how we should do this - having the value we use to represent unbounded depend on the target is a recipe for user error and confusion. If we're changing to use 0 for unbounded instead of -1, we need to do this for all targets and update whatever documentation we have (ie, change the definition of the intrinsic such that the value 0 means unbounded). Otherwise, if we need this to stay as -1 for DirectX for some good reason, then we should instead take @s-perron's suggestion in #185551 (comment) and handle converting this to a zero in the SPIR-V backend early (when we're handling the intrinsic itself).

@llvmbot llvmbot added backend:DirectX llvm:analysis Includes value tracking, cost tables and constant folding labels Mar 12, 2026
@joaosaffran joaosaffran marked this pull request as draft March 12, 2026 17:55
@github-actions

github-actions Bot commented Mar 12, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 199341 tests passed
  • 6211 tests skipped

✅ The build succeeded and all tests passed.

@joaosaffran joaosaffran force-pushed the bugfix/hlsl/use-0-unbounded branch from ab3bf95 to 1bc576f Compare March 12, 2026 20:02
@joaosaffran joaosaffran requested a review from bogner March 12, 2026 20:03
@github-actions

github-actions Bot commented Mar 12, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@joaosaffran joaosaffran changed the title [HLSL][SPIRV] Use 0 to represent unbounded resources when targeting SPIRV [HLSL] Use 0 to represent unbounded resources Mar 12, 2026
@joaosaffran joaosaffran marked this pull request as ready for review March 12, 2026 20:33
Comment thread clang/lib/CodeGen/CGHLSLRuntime.cpp Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to update this comment

Comment thread llvm/lib/Analysis/DXILResource.cpp Outdated
Size, HandleTy, Name};
ResourceInfo RI = ResourceInfo{
/*RecordID=*/0, Space, LowerBound,
Size == 0 ? ~0U : Size, HandleTy, Name};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like converting to ~0U here makes this harder to follow. At some points in the backend, 0 means unbounded, but at other points, it's ~0U.

It would be better to consistently use 0 throughout. The main places where we need to do something different are where we convert to an UpperBound (which already has special logic everywhere it happens), a few places where we print specially, and when we emit this into the metadata (we'll need to convert from 0 to ~0U to match the DXIL semantics there).

Comment on lines +30 to +31
; Function Attrs: nocallback nofree nosync nounwind willreturn memory(none)
declare target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32, i32, i32, i32, ptr) #3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can simplify by removing the declaration here and letting the parser find it.

Comment on lines +25 to +26
%2 = call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_24t(i32 0, i32 0, i32 0, i32 0, ptr @Buf.str)
%3 = call noundef align 4 dereferenceable(4) ptr addrspace(11) @llvm.spv.resource.getpointer.p11.tspirv.SignedImage_i32_5_2_0_0_2_24t(target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 24) %2, i32 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This IR is technically invalid, since the numbered values aren't contiguous and starting from 0. Better to name these values instead of using numbers in any case.

We could also simplify this slightly by just saying @llvm.spv.resource.handlefrombinding( and @llvm.spv.resource.getpointer( instead of spelling out the overloads.

@joaosaffran joaosaffran marked this pull request as draft March 13, 2026 02:08
@joaosaffran joaosaffran marked this pull request as ready for review March 16, 2026 19:10
@joaosaffran joaosaffran requested a review from bogner March 16, 2026 19:10

@bob80905 bob80905 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@joaosaffran joaosaffran merged commit a12b612 into llvm:main Mar 16, 2026
14 of 15 checks passed
@llvm-ci

llvm-ci commented Mar 16, 2026

Copy link
Copy Markdown

LLVM Buildbot has detected a new failure on builder openmp-s390x-linux running on systemz-1 while building clang,llvm at step 6 "test-openmp".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/88/builds/21500

Here is the relevant piece of the build log for the reference
Step 6 (test-openmp) failure: test (failure)
******************** TEST 'libomp :: tasking/issue-94260-2.c' FAILED ********************
Exit Code: -11

Command Output (stdout):
--
# RUN: at line 1
/home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/./bin/clang -fopenmp   -I /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/src -I /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.src/openmp/runtime/test -L /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/src  -fno-omit-frame-pointer -mbackchain -I /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.src/openmp/runtime/test/ompt /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.src/openmp/runtime/test/tasking/issue-94260-2.c -o /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/test/tasking/Output/issue-94260-2.c.tmp -lm -latomic && /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/test/tasking/Output/issue-94260-2.c.tmp
# executed command: /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/./bin/clang -fopenmp -I /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/src -I /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.src/openmp/runtime/test -L /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/src -fno-omit-frame-pointer -mbackchain -I /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.src/openmp/runtime/test/ompt /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.src/openmp/runtime/test/tasking/issue-94260-2.c -o /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/test/tasking/Output/issue-94260-2.c.tmp -lm -latomic
# executed command: /home/uweigand/sandbox/buildbot/openmp-s390x-linux/llvm.build/runtimes/runtimes-bins/openmp/runtime/test/tasking/Output/issue-94260-2.c.tmp
# note: command had no output on stdout or stderr
# error: command failed with exit status: -11

--

********************


hekota added a commit to hekota/llvm-project that referenced this pull request Mar 17, 2026
joaosaffran added a commit that referenced this pull request Mar 19, 2026
…87174)

this patch updates the shader flags to account for 0 being used to
represent unbounded arrays. This was a missed updated from the previous
pr #186022. This change is required to make sure the following offload
test pass dxv validation:

```
  OffloadTest-clang-d3d12 :: Feature/ResourceArrays/multi-dim-unbounded-array-nuri.test
  OffloadTest-clang-d3d12 :: Feature/ResourceArrays/multi-dim-unbounded-array.test
  OffloadTest-clang-d3d12 :: Feature/ResourceArrays/unbounded-array-nuri.test
  OffloadTest-clang-d3d12 :: Feature/ResourceArrays/unbounded-array.test
```

---------

Co-authored-by: Joao Saffran <jderezende@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend:DirectX backend:SPIR-V clang:codegen IR generation bugs: mangling, exceptions, etc. HLSL HLSL Language Support llvm:analysis Includes value tracking, cost tables and constant folding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[HLSL][Buffers][Vulkan] Buffer declaration emitting OpTypeArray instead of OpTypeRuntimeArray

5 participants