[clang] [llvm] [SPIRV] Pack 64-bit vec2 typed buffers into 4x32-bit images (PR #208626)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Jul 9 23:01:52 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-backend-spir-v
Author: Kaitlin Peng (kmpeng)
<details>
<summary>Changes</summary>
Fixes llvm/offload-test-suite#<!-- -->1037 on the Clang side.
SPIR-V has no multi-component 64-bit image format, so `[RW]Buffer<uint/int64_t2>` was being lowered to an `Unknown` image format, and the offloader then backed it with the Vulkan `R64G64` format. Since `R64G64` is an optional format and most (if not all) devices do not support it for texel buffers, we got all 0 outputs.
This PR changes the lowering to pack `uint/int64_t2` typed buffer elements into 4-component 32-bit images and bitcast during load/store. This approach mirrors DirectX, where `uint/int64_t2` is backed by 4x32-bit channels and reinterpreted in the shader. The PR also adds `RWBuffer<uint/int64_t2>` load/store tests to `Unsigned/SignedBufferLoadStore.ll`.
---
Full diff: https://github.com/llvm/llvm-project/pull/208626.diff
5 Files Affected:
- (modified) clang/lib/CodeGen/Targets/SPIR.cpp (+8)
- (modified) clang/test/CodeGenHLSL/resources/RWBuffer-imageformat.hlsl (+2-2)
- (modified) llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp (+43-14)
- (modified) llvm/test/CodeGen/SPIRV/hlsl-resources/SignedBufferLoadStore.ll (+26)
- (modified) llvm/test/CodeGen/SPIRV/hlsl-resources/UnsignedBufferLoadStore.ll (+26)
``````````diff
diff --git a/clang/lib/CodeGen/Targets/SPIR.cpp b/clang/lib/CodeGen/Targets/SPIR.cpp
index 88e7c7f9482a4..2c92fbde4f3c3 100644
--- a/clang/lib/CodeGen/Targets/SPIR.cpp
+++ b/clang/lib/CodeGen/Targets/SPIR.cpp
@@ -890,6 +890,14 @@ llvm::Type *CommonSPIRTargetCodeGenInfo::getSPIRVImageTypeFromHLSLResource(
"The element type for a SPIR-V resource must be a scalar integer or "
"floating point type.");
+ // SPIR-V has no 64-bit multi-component image format, so pack a 2-component
+ // 64-bit typed buffer into a 4-component 32-bit image. The backend
+ // reinterprets it with OpBitcast on load and store.
+ if (SampledType->isIntegerTy(64) && NumChannels == 2) {
+ SampledType = llvm::Type::getInt32Ty(Ctx);
+ NumChannels = 4;
+ }
+
// These parameters correspond to the operands to the OpTypeImage SPIR-V
// instruction. See
// https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpTypeImage.
diff --git a/clang/test/CodeGenHLSL/resources/RWBuffer-imageformat.hlsl b/clang/test/CodeGenHLSL/resources/RWBuffer-imageformat.hlsl
index aebee894f79d5..6b967bb1e3e3a 100644
--- a/clang/test/CodeGenHLSL/resources/RWBuffer-imageformat.hlsl
+++ b/clang/test/CodeGenHLSL/resources/RWBuffer-imageformat.hlsl
@@ -23,11 +23,11 @@ RWBuffer<uint4> rwb_uint4;
// 64-bit integers
// CHECK: %"class.hlsl::RWBuffer.7" = type { target("spirv.SignedImage", i64, 5, 2, 0, 0, 2, 41) }
RWBuffer<int64_t> rwb_i64;
-// CHECK: %"class.hlsl::RWBuffer.8" = type { target("spirv.SignedImage", i64, 5, 2, 0, 0, 2, 0) }
+// CHECK: %"class.hlsl::RWBuffer.8" = type { target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 21) }
RWBuffer<int64_t2> rwb_i64_2;
// CHECK: %"class.hlsl::RWBuffer.9" = type { target("spirv.Image", i64, 5, 2, 0, 0, 2, 40) }
RWBuffer<uint64_t> rwb_u64;
-// CHECK: %"class.hlsl::RWBuffer.10" = type { target("spirv.Image", i64, 5, 2, 0, 0, 2, 0) }
+// CHECK: %"class.hlsl::RWBuffer.10" = type { target("spirv.Image", i32, 5, 2, 0, 0, 2, 30) }
RWBuffer<uint64_t2> rwb_u64_2;
// Floats
diff --git a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
index 11128eadef95f..15bb399c2ca09 100644
--- a/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
+++ b/llvm/lib/Target/SPIRV/SPIRVInstructionSelector.cpp
@@ -2073,6 +2073,24 @@ bool SPIRVInstructionSelector::selectStore(MachineInstr &I) const {
Register IdxReg = IntPtrDef->getOperand(3).getReg();
if (HandleType->getOpcode() == SPIRV::OpTypeImage) {
+ SPIRVTypeInst SampledType =
+ GR.getSPIRVTypeForVReg(HandleType->getOperand(1).getReg());
+ SPIRVTypeInst StoreValCompType =
+ GR.getScalarOrVectorComponentType(GR.getSPIRVTypeForVReg(StoreVal));
+ if (StoreValCompType && StoreValCompType != SampledType) {
+ // A wide element (e.g. int64_t2) is emulated with a narrower packed
+ // image. This bitcasts the value to match the format.
+ SPIRVTypeInst PackedType = widenTypeToVec4(SampledType, I);
+ Register PackedReg =
+ MRI->createVirtualRegister(GR.getRegClass(PackedType));
+ BuildMI(*I.getParent(), I, I.getDebugLoc(), TII.get(SPIRV::OpBitcast))
+ .addDef(PackedReg)
+ .addUse(GR.getSPIRVTypeID(PackedType))
+ .addUse(StoreVal)
+ .constrainAllUses(TII, TRI, RBI);
+ StoreVal = PackedReg;
+ }
+
auto BMI = BuildMI(*I.getParent(), I, I.getDebugLoc(),
TII.get(SPIRV::OpImageWrite))
.addUse(NewHandleReg)
@@ -6286,22 +6304,20 @@ bool SPIRVInstructionSelector::generateImageReadOrFetch(
};
uint64_t ResultSize = GR.getScalarOrVectorComponentCount(ResType);
- if (ResultSize == 4) {
- auto BMI =
- BuildMI(*Pos.getParent(), Pos, Loc,
- TII.get(IsFetch ? SPIRV::OpImageFetch : SPIRV::OpImageRead))
- .addDef(ResVReg)
- .addUse(GR.getSPIRVTypeID(ResType))
- .addUse(ImageReg)
- .addUse(IdxReg);
- AddOperands(BMI);
- BMI.constrainAllUses(TII, TRI, RBI);
- return true;
- }
+ // A wide element (e.g. int64_t2) is emulated with a narrower packed image, so
+ // its sampled type is different from the result.
+ SPIRVTypeInst SampledType =
+ GR.getSPIRVTypeForVReg(ImageType->getOperand(1).getReg());
+ bool IsPacked = SampledType != GR.getScalarOrVectorComponentType(ResType);
+ SPIRVTypeInst ReadType =
+ widenTypeToVec4(IsPacked ? SampledType : ResType, Pos);
+ bool ReadTypeMatchesResult = ReadType == ResType;
+ // Read directly into the result, or into a temporary to bitcast/extract.
+ Register ReadReg = ReadTypeMatchesResult
+ ? ResVReg
+ : MRI->createVirtualRegister(GR.getRegClass(ReadType));
- SPIRVTypeInst ReadType = widenTypeToVec4(ResType, Pos);
- Register ReadReg = MRI->createVirtualRegister(GR.getRegClass(ReadType));
auto BMI =
BuildMI(*Pos.getParent(), Pos, Loc,
TII.get(IsFetch ? SPIRV::OpImageFetch : SPIRV::OpImageRead))
@@ -6312,6 +6328,19 @@ bool SPIRVInstructionSelector::generateImageReadOrFetch(
AddOperands(BMI);
BMI.constrainAllUses(TII, TRI, RBI);
+ if (ReadTypeMatchesResult)
+ return true;
+
+ if (IsPacked) {
+ // Reinterpret the packed vector as the wide result type.
+ BuildMI(*Pos.getParent(), Pos, Loc, TII.get(SPIRV::OpBitcast))
+ .addDef(ResVReg)
+ .addUse(GR.getSPIRVTypeID(ResType))
+ .addUse(ReadReg)
+ .constrainAllUses(TII, TRI, RBI);
+ return true;
+ }
+
if (ResultSize == 1) {
BuildMI(*Pos.getParent(), Pos, Loc, TII.get(SPIRV::OpCompositeExtract))
.addDef(ResVReg)
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/SignedBufferLoadStore.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/SignedBufferLoadStore.ll
index d15f88b5dd799..56ee2fd5ab011 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/SignedBufferLoadStore.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/SignedBufferLoadStore.ll
@@ -2,10 +2,13 @@
; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
@.str.b0 = private unnamed_addr constant [3 x i8] c"B0\00", align 1
+ at .str.b1 = private unnamed_addr constant [3 x i8] c"B1\00", align 1
; CHECK-DAG: [[uint:%[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: [[ulong:%[0-9]+]] = OpTypeInt 64 0
; CHECK-DAG: [[v2int:%[0-9]+]] = OpTypeVector [[uint]] 2
; CHECK-DAG: [[v4int:%[0-9]+]] = OpTypeVector [[uint]] 4
+; CHECK-DAG: [[v2ulong:%[0-9]+]] = OpTypeVector [[ulong]] 2
; CHECK-DAG: [[zero:%[0-9]+]] = OpConstant [[uint]] 0
; CHECK-DAG: [[one:%[0-9]+]] = OpConstant [[uint]] 1
; CHECK-DAG: [[twenty:%[0-9]+]] = OpConstant [[uint]] 20
@@ -13,6 +16,9 @@
; CHECK-DAG: [[ImageType:%[0-9]+]] = OpTypeImage [[uint]] Buffer 2 0 0 2 Unknown
; CHECK-DAG: [[ImagePtr:%[0-9]+]] = OpTypePointer UniformConstant [[ImageType]]
; CHECK-DAG: [[Var:%[0-9]+]] = OpVariable [[ImagePtr]] UniformConstant
+; CHECK-DAG: [[PackedImageType:%[0-9]+]] = OpTypeImage [[uint]] Buffer 2 0 0 2 Rgba32i
+; CHECK-DAG: [[PackedImagePtr:%[0-9]+]] = OpTypePointer UniformConstant [[PackedImageType]]
+; CHECK-DAG: [[PackedVar:%[0-9]+]] = OpVariable [[PackedImagePtr]] UniformConstant
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, inaccessiblemem: none)
; CHECK: OpFunction
@@ -133,5 +139,25 @@ bb_both:
ret void
}
+; A 64-bit 2-component element has no representable image format, so it is packed into a 4-component 32-bit image.
+; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, inaccessiblemem: none)
+; CHECK: OpFunction
+define void @main_int64_vector2() local_unnamed_addr #0 {
+entry:
+; CHECK: [[H:%[0-9]+]] = OpLoad [[PackedImageType]] [[PackedVar]]
+ %s_h.i = tail call target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 21) @llvm.spv.resource.handlefrombinding.tspirv.SignedImage_i32_5_2_0_0_2_21t(i32 4, i32 5, i32 1, i32 0, ptr nonnull @.str.b1)
+
+; CHECK: [[R:%[0-9]+]] = OpImageRead [[v4int]] [[H]] [[one]] SignExtend
+; CHECK: [[V:%[0-9]+]] = OpBitcast [[v2ulong]] [[R]]
+ %0 = tail call noundef nonnull align 16 dereferenceable(16) ptr @llvm.spv.resource.getpointer.p0.tspirv.SignedImage_i32_5_2_0_0_2_21t(target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 21) %s_h.i, i32 1)
+ %1 = load <2 x i64>, ptr %0, align 16
+
+; CHECK: [[W:%[0-9]+]] = OpBitcast [[v4int]] [[V]]
+; CHECK: OpImageWrite {{%[0-9]+}} [[zero]] [[W]] SignExtend
+ %2 = tail call noundef nonnull align 16 dereferenceable(16) ptr @llvm.spv.resource.getpointer.p0.tspirv.SignedImage_i32_5_2_0_0_2_21t(target("spirv.SignedImage", i32, 5, 2, 0, 0, 2, 21) %s_h.i, i32 0)
+ store <2 x i64> %1, ptr %2, align 16
+ ret void
+}
+
attributes #0 = { mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, inaccessiblemem: none) "frame-pointer"="all" "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
attributes #1 = { mustprogress nocallback nofree nosync nounwind willreturn memory(none) }
diff --git a/llvm/test/CodeGen/SPIRV/hlsl-resources/UnsignedBufferLoadStore.ll b/llvm/test/CodeGen/SPIRV/hlsl-resources/UnsignedBufferLoadStore.ll
index d038c5ce2574d..59602d1be77bf 100644
--- a/llvm/test/CodeGen/SPIRV/hlsl-resources/UnsignedBufferLoadStore.ll
+++ b/llvm/test/CodeGen/SPIRV/hlsl-resources/UnsignedBufferLoadStore.ll
@@ -2,10 +2,13 @@
; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv1.6-vulkan1.3-library %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
@.str.b0 = private unnamed_addr constant [3 x i8] c"B0\00", align 1
+ at .str.b1 = private unnamed_addr constant [3 x i8] c"B1\00", align 1
; CHECK-DAG: [[uint:%[0-9]+]] = OpTypeInt 32 0
+; CHECK-DAG: [[ulong:%[0-9]+]] = OpTypeInt 64 0
; CHECK-DAG: [[v2int:%[0-9]+]] = OpTypeVector [[uint]] 2
; CHECK-DAG: [[v4int:%[0-9]+]] = OpTypeVector [[uint]] 4
+; CHECK-DAG: [[v2ulong:%[0-9]+]] = OpTypeVector [[ulong]] 2
; CHECK-DAG: [[zero:%[0-9]+]] = OpConstant [[uint]] 0
; CHECK-DAG: [[one:%[0-9]+]] = OpConstant [[uint]] 1
; CHECK-DAG: [[twenty:%[0-9]+]] = OpConstant [[uint]] 20
@@ -13,6 +16,9 @@
; CHECK-DAG: [[ImageType:%[0-9]+]] = OpTypeImage [[uint]] Buffer 2 0 0 2 Unknown
; CHECK-DAG: [[ImagePtr:%[0-9]+]] = OpTypePointer UniformConstant [[ImageType]]
; CHECK-DAG: [[Var:%[0-9]+]] = OpVariable [[ImagePtr]] UniformConstant
+; CHECK-DAG: [[PackedImageType:%[0-9]+]] = OpTypeImage [[uint]] Buffer 2 0 0 2 Rgba32ui
+; CHECK-DAG: [[PackedImagePtr:%[0-9]+]] = OpTypePointer UniformConstant [[PackedImageType]]
+; CHECK-DAG: [[PackedVar:%[0-9]+]] = OpVariable [[PackedImagePtr]] UniformConstant
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, inaccessiblemem: none)
; CHECK: OpFunction
@@ -133,5 +139,25 @@ bb_both:
ret void
}
+; A 64-bit 2-component element has no representable image format, so it is packed into a 4-component 32-bit image.
+; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, inaccessiblemem: none)
+; CHECK: OpFunction
+define void @main_uint64_vector2() local_unnamed_addr #0 {
+entry:
+; CHECK: [[H:%[0-9]+]] = OpLoad [[PackedImageType]] [[PackedVar]]
+ %s_h.i = tail call target("spirv.Image", i32, 5, 2, 0, 0, 2, 30) @llvm.spv.resource.handlefrombinding.tspirv.Image_i32_5_2_0_0_2_30t(i32 4, i32 5, i32 1, i32 0, ptr nonnull @.str.b1)
+
+; CHECK: [[R:%[0-9]+]] = OpImageRead [[v4int]] [[H]] [[one]]
+; CHECK: [[V:%[0-9]+]] = OpBitcast [[v2ulong]] [[R]]
+ %0 = tail call noundef nonnull align 16 dereferenceable(16) ptr @llvm.spv.resource.getpointer.p0.tspirv.Image_i32_5_2_0_0_2_30t(target("spirv.Image", i32, 5, 2, 0, 0, 2, 30) %s_h.i, i32 1)
+ %1 = load <2 x i64>, ptr %0, align 16
+
+; CHECK: [[W:%[0-9]+]] = OpBitcast [[v4int]] [[V]]
+; CHECK: OpImageWrite {{%[0-9]+}} [[zero]] [[W]]
+ %2 = tail call noundef nonnull align 16 dereferenceable(16) ptr @llvm.spv.resource.getpointer.p0.tspirv.Image_i32_5_2_0_0_2_30t(target("spirv.Image", i32, 5, 2, 0, 0, 2, 30) %s_h.i, i32 0)
+ store <2 x i64> %1, ptr %2, align 16
+ ret void
+}
+
attributes #0 = { mustprogress nofree noinline norecurse nosync nounwind willreturn memory(readwrite, inaccessiblemem: none) "frame-pointer"="all" "hlsl.numthreads"="1,1,1" "hlsl.shader"="compute" "no-trapping-math"="true" "stack-protector-buffer-size"="8" }
attributes #1 = { mustprogress nocallback nofree nosync nounwind willreturn memory(none) }
``````````
</details>
https://github.com/llvm/llvm-project/pull/208626
More information about the cfe-commits
mailing list