[clang] [llvm] [HLSL][SPIRV] Support Flat and Location decorators in the frontend (PR #210116)
Farzon Lotfi via cfe-commits
cfe-commits at lists.llvm.org
Fri Jul 17 10:19:41 PDT 2026
https://github.com/farzonl updated https://github.com/llvm/llvm-project/pull/210116
>From a5ea01283f44aefec49f2337cf7ddbc1e9640580 Mon Sep 17 00:00:00 2001
From: Farzon Lotfi <farzon at farzon.org>
Date: Thu, 16 Jul 2026 13:11:16 -0400
Subject: [PATCH 1/2] [Clang][HLSL][SPIRV] Support Flat and Location decorators
in the frontend
fixes #194293
fixes #194432
In HLSL we need both Location and and Flat must be emitted as a single
`spirv.Decorations` node.
Also we need to apply Flat decoration to pixe/fragment shades with
inputs of integer or double because only 32 bit floats can be
interpolated.
Assisted with Claude Opus 4.8 via Copilot
---
clang/lib/CodeGen/CGHLSLRuntime.cpp | 62 ++++++++++++++++---
clang/lib/CodeGen/CGHLSLRuntime.h | 7 ++-
.../semantics/flat-decoration.ps.hlsl | 29 +++++++++
llvm/test/CodeGen/SPIRV/semantics/flat.ps.ll | 51 +++++++++++++++
4 files changed, 137 insertions(+), 12 deletions(-)
create mode 100644 clang/test/CodeGenHLSL/semantics/flat-decoration.ps.hlsl
create mode 100644 llvm/test/CodeGen/SPIRV/semantics/flat.ps.ll
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.cpp b/clang/lib/CodeGen/CGHLSLRuntime.cpp
index 8794579166b6a..283bfeb73df11 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.cpp
+++ b/clang/lib/CodeGen/CGHLSLRuntime.cpp
@@ -1062,6 +1062,26 @@ static void addLocationDecoration(llvm::GlobalVariable *GV, unsigned Location) {
GV->addMetadata("spirv.Decorations", *Decoration);
}
+// A fragment shader input interface variable whose base type is an integer or
+// a 64-bit float (double) cannot be interpolated by the rasterizer. The Vulkan
+// specification requires these variables to be decorated with Flat (see
+// VUID-StandaloneSpirv-Flat-04744). Arrays and vectors are unwrapped to inspect
+// their base scalar type.
+static bool inputRequiresFlatDecoration(llvm::Type *Ty) {
+ while (true) {
+ if (auto *AT = dyn_cast<llvm::ArrayType>(Ty)) {
+ Ty = AT->getElementType();
+ continue;
+ }
+ if (auto *VT = dyn_cast<llvm::FixedVectorType>(Ty)) {
+ Ty = VT->getElementType();
+ continue;
+ }
+ break;
+ }
+ return Ty->isIntegerTy() || Ty->isDoubleTy();
+}
+
static llvm::Value *createSPIRVBuiltinLoad(IRBuilder<> &B, llvm::Module &M,
llvm::Type *Ty, const Twine &Name,
unsigned BuiltInID) {
@@ -1077,20 +1097,35 @@ static llvm::Value *createSPIRVBuiltinLoad(IRBuilder<> &B, llvm::Module &M,
static llvm::Value *createSPIRVLocationLoad(IRBuilder<> &B, llvm::Module &M,
llvm::Type *Ty, unsigned Location,
- StringRef Name) {
+ StringRef Name, bool NeedsFlat) {
auto *GV = new llvm::GlobalVariable(
M, Ty, /* isConstant= */ true, llvm::GlobalValue::ExternalLinkage,
/* Initializer= */ nullptr, /* Name= */ Name, /* insertBefore= */ nullptr,
llvm::GlobalVariable::GeneralDynamicTLSModel,
/* AddressSpace */ 7, /* isExternallyInitialized= */ true);
GV->setVisibility(llvm::GlobalValue::HiddenVisibility);
- addLocationDecoration(GV, Location);
+
+ // Emit all decorations as a single `spirv.Decorations` node. Attaching
+ // multiple `spirv.Decorations` metadata nodes to the same global is not
+ // supported by the SPIR-V backend and results in all but one being dropped.
+ LLVMContext &Ctx = GV->getContext();
+ SmallVector<Metadata *, 2> Decorations;
+ Decorations.push_back(
+ MDNode::get(Ctx, {ConstantAsMetadata::get(B.getInt32(/* Location */ 30)),
+ ConstantAsMetadata::get(B.getInt32(Location))}));
+ if (NeedsFlat)
+ Decorations.push_back(
+ MDNode::get(Ctx, {ConstantAsMetadata::get(
+ B.getInt32(/* Spirv::Decoration::Flat */ 14))}));
+ GV->addMetadata("spirv.Decorations", *MDNode::get(Ctx, Decorations));
+
return B.CreateLoad(Ty, GV);
}
llvm::Value *CGHLSLRuntime::emitSPIRVUserSemanticLoad(
- llvm::IRBuilder<> &B, llvm::Type *Type, const clang::DeclaratorDecl *Decl,
- HLSLAppliedSemanticAttr *Semantic, std::optional<unsigned> Index) {
+ llvm::IRBuilder<> &B, const FunctionDecl *FD, llvm::Type *Type,
+ const clang::DeclaratorDecl *Decl, HLSLAppliedSemanticAttr *Semantic,
+ std::optional<unsigned> Index) {
Twine BaseName = Twine(Semantic->getAttrName()->getName());
Twine VariableName = BaseName.concat(Twine(Index.value_or(0)));
@@ -1104,8 +1139,14 @@ llvm::Value *CGHLSLRuntime::emitSPIRVUserSemanticLoad(
unsigned ElementCount = AT ? AT->getNumElements() : 1;
SPIRVLastAssignedInputSemanticLocation += ElementCount;
+ const auto *ShaderAttr = FD->getAttr<HLSLShaderAttr>();
+ bool NeedsFlat =
+ ShaderAttr &&
+ ShaderAttr->getType() == llvm::Triple::EnvironmentType::Pixel &&
+ inputRequiresFlatDecoration(Type);
+
return createSPIRVLocationLoad(B, CGM.getModule(), Type, Location,
- VariableName.str());
+ VariableName.str(), NeedsFlat);
}
static void createSPIRVLocationStore(IRBuilder<> &B, llvm::Module &M,
@@ -1197,10 +1238,11 @@ void CGHLSLRuntime::emitDXILUserSemanticStore(llvm::IRBuilder<> &B,
}
llvm::Value *CGHLSLRuntime::emitUserSemanticLoad(
- IRBuilder<> &B, llvm::Type *Type, const clang::DeclaratorDecl *Decl,
- HLSLAppliedSemanticAttr *Semantic, std::optional<unsigned> Index) {
+ IRBuilder<> &B, const FunctionDecl *FD, llvm::Type *Type,
+ const clang::DeclaratorDecl *Decl, HLSLAppliedSemanticAttr *Semantic,
+ std::optional<unsigned> Index) {
if (CGM.getTarget().getTriple().isSPIRV())
- return emitSPIRVUserSemanticLoad(B, Type, Decl, Semantic, Index);
+ return emitSPIRVUserSemanticLoad(B, FD, Type, Decl, Semantic, Index);
if (CGM.getTarget().getTriple().isDXIL())
return emitDXILUserSemanticLoad(B, Type, Semantic, Index);
@@ -1275,7 +1317,7 @@ llvm::Value *CGHLSLRuntime::emitSystemSemanticLoad(
}
if (ST == Triple::EnvironmentType::Vertex) {
- return emitUserSemanticLoad(B, Type, Decl, Semantic, Index);
+ return emitUserSemanticLoad(B, FD, Type, Decl, Semantic, Index);
}
}
@@ -1344,7 +1386,7 @@ llvm::Value *CGHLSLRuntime::handleScalarSemanticLoad(
std::optional<unsigned> Index = Semantic->getSemanticIndex();
if (Semantic->getAttrName()->getName().starts_with_insensitive("SV_"))
return emitSystemSemanticLoad(B, FD, Type, Decl, Semantic, Index);
- return emitUserSemanticLoad(B, Type, Decl, Semantic, Index);
+ return emitUserSemanticLoad(B, FD, Type, Decl, Semantic, Index);
}
void CGHLSLRuntime::handleScalarSemanticStore(
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.h b/clang/lib/CodeGen/CGHLSLRuntime.h
index cf47b1633fd3c..0664eef464f98 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.h
+++ b/clang/lib/CodeGen/CGHLSLRuntime.h
@@ -335,14 +335,17 @@ class CGHLSLRuntime {
llvm::GlobalVariable *GV,
HLSLResourceBindingAttr *RBA);
- llvm::Value *emitSPIRVUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
+ llvm::Value *emitSPIRVUserSemanticLoad(llvm::IRBuilder<> &B,
+ const FunctionDecl *FD,
+ llvm::Type *Type,
const clang::DeclaratorDecl *Decl,
HLSLAppliedSemanticAttr *Semantic,
std::optional<unsigned> Index);
llvm::Value *emitDXILUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
HLSLAppliedSemanticAttr *Semantic,
std::optional<unsigned> Index);
- llvm::Value *emitUserSemanticLoad(llvm::IRBuilder<> &B, llvm::Type *Type,
+ llvm::Value *emitUserSemanticLoad(llvm::IRBuilder<> &B,
+ const FunctionDecl *FD, llvm::Type *Type,
const clang::DeclaratorDecl *Decl,
HLSLAppliedSemanticAttr *Semantic,
std::optional<unsigned> Index);
diff --git a/clang/test/CodeGenHLSL/semantics/flat-decoration.ps.hlsl b/clang/test/CodeGenHLSL/semantics/flat-decoration.ps.hlsl
new file mode 100644
index 0000000000000..5e97be23386bd
--- /dev/null
+++ b/clang/test/CodeGenHLSL/semantics/flat-decoration.ps.hlsl
@@ -0,0 +1,29 @@
+// RUN: %clang_cc1 -triple spirv-pc-vulkan1.3-pixel -x hlsl -emit-llvm -finclude-default-header -disable-llvm-passes -o - %s | FileCheck %s --check-prefix=CHECK-SPIRV
+
+// CHECK-SPIRV-DAG: @A0 = external hidden thread_local addrspace(7) externally_initialized constant float, !spirv.Decorations ![[#FLOAT_MD:]]
+// CHECK-SPIRV-DAG: @B0 = external hidden thread_local addrspace(7) externally_initialized constant i32, !spirv.Decorations ![[#INT_MD:]]
+// CHECK-SPIRV-DAG: @C0 = external hidden thread_local addrspace(7) externally_initialized constant double, !spirv.Decorations ![[#DOUBLE_MD:]]
+// CHECK-SPIRV-DAG: @D0 = external hidden thread_local addrspace(7) externally_initialized constant <2 x i32>, !spirv.Decorations ![[#IVEC_MD:]]
+
+float main(float a : A, int b : B, double c : C, int2 d : D) : SV_Target {
+ return a + b + (float)c + d.x + d.y;
+}
+
+// The float input only carries a Location decoration (no Flat).
+// CHECK-SPIRV-DAG: ![[#FLOAT_MD]] = !{![[#FLOAT_LOC:]]}
+// CHECK-SPIRV-DAG: ![[#FLOAT_LOC]] = !{i32 30, i32 0}
+
+// The integer input carries both Location and Flat decorations in one node.
+// CHECK-SPIRV-DAG: ![[#INT_MD]] = !{![[#INT_LOC:]], ![[#FLAT:]]}
+// CHECK-SPIRV-DAG: ![[#INT_LOC]] = !{i32 30, i32 1}
+
+// The double input carries both Location and Flat decorations in one node.
+// CHECK-SPIRV-DAG: ![[#DOUBLE_MD]] = !{![[#DOUBLE_LOC:]], ![[#FLAT]]}
+// CHECK-SPIRV-DAG: ![[#DOUBLE_LOC]] = !{i32 30, i32 2}
+
+// The integer vector input carries both Location and Flat decorations in one node.
+// CHECK-SPIRV-DAG: ![[#IVEC_MD]] = !{![[#IVEC_LOC:]], ![[#FLAT]]}
+// CHECK-SPIRV-DAG: ![[#IVEC_LOC]] = !{i32 30, i32 3}
+
+// CHECK-SPIRV-DAG: ![[#FLAT]] = !{i32 14}
+// `-> SPIR-V decoration 'Flat'
diff --git a/llvm/test/CodeGen/SPIRV/semantics/flat.ps.ll b/llvm/test/CodeGen/SPIRV/semantics/flat.ps.ll
new file mode 100644
index 0000000000000..82934a85f9949
--- /dev/null
+++ b/llvm/test/CodeGen/SPIRV/semantics/flat.ps.ll
@@ -0,0 +1,51 @@
+; RUN: llc -O0 -verify-machineinstrs -mtriple=spirv-unknown-vulkan1.3-pixel %s -o - | FileCheck %s
+; RUN: %if spirv-tools %{ llc -O0 -mtriple=spirv-unknown-vulkan1.3-pixel %s -o - -filetype=obj | spirv-val --target-env vulkan1.3 %}
+
+; This test is the simplified llvm IR of flat-decoration.ps.hlsl. It exists
+; because we can't do end to end testing but this gets us close & shows that
+; the generated code will now pass the spirv validator by adding the Flat &
+; Location decorators.
+
+; CHECK-DAG: OpEntryPoint Fragment %[[#entry:]] "main"
+; CHECK-DAG: OpExecutionMode %[[#entry]] OriginUpperLeft
+
+; CHECK-DAG: OpDecorate %[[#INT_IN:]] Location 0
+; CHECK-DAG: OpDecorate %[[#INT_IN]] Flat
+; CHECK-DAG: OpDecorate %[[#DOUBLE_IN:]] Location 1
+; CHECK-DAG: OpDecorate %[[#DOUBLE_IN]] Flat
+; CHECK-DAG: OpDecorate %[[#FLOAT_IN:]] Location 2
+; CHECK-NOT: OpDecorate %[[#FLOAT_IN]] Flat
+
+ at B0 = external hidden thread_local addrspace(7) externally_initialized constant i32, !spirv.Decorations !0
+ at C0 = external hidden thread_local addrspace(7) externally_initialized constant double, !spirv.Decorations !3
+ at A0 = external hidden thread_local addrspace(7) externally_initialized constant float, !spirv.Decorations !5
+ at SV_Target = external hidden thread_local addrspace(8) global float, !spirv.Decorations !7
+
+define void @main() #0 {
+entry:
+ %0 = load i32, ptr addrspace(7) @B0, align 4
+ %1 = load double, ptr addrspace(7) @C0, align 8
+ %2 = load float, ptr addrspace(7) @A0, align 4
+ %conv.i = sitofp i32 %0 to float
+ %conv.d = fptrunc double %1 to float
+ %add1 = fadd float %2, %conv.i
+ %add2 = fadd float %add1, %conv.d
+ store float %add2, ptr addrspace(8) @SV_Target, align 4
+ ret void
+}
+
+attributes #0 = { "hlsl.shader"="pixel" }
+
+; Integer input: Location 0 and Flat.
+!0 = !{!1, !2}
+!1 = !{i32 30, i32 0}
+!2 = !{i32 14}
+; Double input: Location 1 and Flat.
+!3 = !{!4, !2}
+!4 = !{i32 30, i32 1}
+; Float input: Location 2 only (interpolated, no Flat).
+!5 = !{!6}
+!6 = !{i32 30, i32 2}
+; SV_Target output: Location 0.
+!7 = !{!8}
+!8 = !{i32 30, i32 0}
>From b37f670fc4a10b92b47313ee6c91033457e3fe58 Mon Sep 17 00:00:00 2001
From: Farzon Lotfi <farzon at farzon.org>
Date: Fri, 17 Jul 2026 13:18:46 -0400
Subject: [PATCH 2/2] address pr concerns
---
clang/lib/CodeGen/CGHLSLRuntime.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.cpp b/clang/lib/CodeGen/CGHLSLRuntime.cpp
index 283bfeb73df11..ba33ee27bb9b1 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.cpp
+++ b/clang/lib/CodeGen/CGHLSLRuntime.cpp
@@ -1111,12 +1111,13 @@ static llvm::Value *createSPIRVLocationLoad(IRBuilder<> &B, llvm::Module &M,
LLVMContext &Ctx = GV->getContext();
SmallVector<Metadata *, 2> Decorations;
Decorations.push_back(
- MDNode::get(Ctx, {ConstantAsMetadata::get(B.getInt32(/* Location */ 30)),
+ MDNode::get(Ctx, {ConstantAsMetadata::get(
+ B.getInt32(/* SPIRV::Decoration::Location */ 30)),
ConstantAsMetadata::get(B.getInt32(Location))}));
if (NeedsFlat)
Decorations.push_back(
MDNode::get(Ctx, {ConstantAsMetadata::get(
- B.getInt32(/* Spirv::Decoration::Flat */ 14))}));
+ B.getInt32(/* SPIRV::Decoration::Flat */ 14))}));
GV->addMetadata("spirv.Decorations", *MDNode::get(Ctx, Decorations));
return B.CreateLoad(Ty, GV);
More information about the cfe-commits
mailing list