[clang] [VK][HLSL][SPIRV] Implement vk::builtin("PointSize") (PR #231275)
Jonathan Zakharov via cfe-commits
cfe-commits at lists.llvm.org
Mon Oct 12 12:51:49 PDT 2026
https://github.com/jzakharovnv created https://github.com/llvm/llvm-project/pull/231275
- **[HLSL][SPIRV] Implement vk::builtin attribute**
- **[HLSL][SPIRV] Implement vk::builtin attribute**
- **[HLSL][SPIRV] Implement vk::builtin(PointSize) validation and CodeGen**
>From 85d55c3d7723a73eb6cd9b1c912c31a6c15ae2db Mon Sep 17 00:00:00 2001
From: Jonathan Zakharov <jzakharov at nvidia.com>
Date: Mon, 12 Oct 2026 11:13:10 -0700
Subject: [PATCH 1/3] [HLSL][SPIRV] Implement vk::builtin attribute
---
clang/include/clang/Basic/Attr.td | 8 +++
clang/include/clang/Basic/AttrDocs.td | 22 ++++++
.../clang/Basic/DiagnosticSemaKinds.td | 3 +
clang/include/clang/Sema/SemaHLSL.h | 1 +
clang/lib/Sema/SemaDeclAttr.cpp | 3 +
clang/lib/Sema/SemaHLSL.cpp | 52 +++++++++++++-
clang/test/AST/HLSL/vk-builtin-attr.hlsl | 29 ++++++++
...a-attribute-supported-attributes-list.test | 1 +
...antic.explicit-mix-vk-builtin-control.hlsl | 15 +++++
...emantic.explicit-mix-vk-builtin-field.hlsl | 18 +++++
...emantic.explicit-mix-vk-builtin-param.hlsl | 11 +++
...mantic.explicit-mix-vk-builtin-return.hlsl | 14 ++++
.../SemaHLSL/vk-builtin-attr-non-spirv.hlsl | 16 +++++
clang/test/SemaHLSL/vk-builtin-attr.hlsl | 67 +++++++++++++++++++
14 files changed, 259 insertions(+), 1 deletion(-)
create mode 100644 clang/test/AST/HLSL/vk-builtin-attr.hlsl
create mode 100644 clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-control.hlsl
create mode 100644 clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-field.hlsl
create mode 100644 clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-param.hlsl
create mode 100644 clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-return.hlsl
create mode 100644 clang/test/SemaHLSL/vk-builtin-attr-non-spirv.hlsl
create mode 100644 clang/test/SemaHLSL/vk-builtin-attr.hlsl
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index b41c1c78ddd99..91af26ecaaa80 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -5462,6 +5462,14 @@ def HLSLVkLocation : HLSLAnnotationAttr {
let Documentation = [HLSLVkLocationDocs];
}
+def HLSLVkBuiltIn : HLSLAnnotationAttr, TargetSpecificAttr<TargetSPIRV> {
+ let Spellings = [CXX11<"vk", "builtin">];
+ let Args = [StringArgument<"BuiltIn">];
+ let Subjects = SubjectList<[ParmVar, Field, Function], ErrorDiag>;
+ let LangOpts = [HLSL];
+ let Documentation = [HLSLVkBuiltInDocs];
+}
+
// `row_major` / `column_major` are HLSL keywords that select the in-memory
// layout of a matrix-typed declaration.
def HLSLRowMajor : TypeAttr {
diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td
index e8ce3c3dea7f3..bc830670689e0 100644
--- a/clang/include/clang/Basic/AttrDocs.td
+++ b/clang/include/clang/Basic/AttrDocs.td
@@ -9741,6 +9741,28 @@ This attribute maps to the `Location` SPIR-V decoration.
}];
}
+def HLSLVkBuiltInDocs : Documentation {
+ let Category = DocCatVariable;
+ let Content = [{
+Some Vulkan built-in variables have no equivalent in native HLSL, so no
+system-value semantic can express them. The `[[vk::builtin]]` attribute marks a
+stage input/output variable as a SPIR-V built-in variable, identified by name.
+Allowed on function parameters, function returns, and struct fields. This
+attribute has no effect when used outside of an entrypoint parameter/parameter
+field/return value.
+
+The attribute is only available when targeting SPIR-V. For any other target it
+is ignored with an "unknown attribute ignored" warning.
+
+The attribute takes a single string literal argument naming the built-in.
+Supported built-in names are added incrementally, each landing with its own
+type/stage validation and CodeGen support.
+
+A declaration annotated with `[[vk::builtin]]` does not take part in
+`vk::location` assignment, and cannot also be annotated with `[[vk::location]]`.
+ }];
+}
+
def HLSLMatrixLayoutDocs : Documentation {
let Category = DocCatVariable;
let Heading = "row_major, column_major";
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index cc87f556d50dc..50de8f772ace2 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -13788,6 +13788,9 @@ def err_hlsl_semantic_unsupported_iotype_for_stage
def err_hlsl_semantic_partial_explicit_indexing
: Error<"partial explicit stage input location assignment via "
"vk::location(X) unsupported">;
+def err_hlsl_vk_builtin_unsupported_value
+ : Error<"'%0' is not a supported value for the 'vk::builtin' attribute; "
+ "supported values are: %1">;
def warn_hlsl_user_defined_type_missing_member: Warning<"binding type '%select{t|u|b|s|c}0' only applies to types containing %select{SRV resources|UAV resources|constant buffer resources|sampler state|numeric types}0">, InGroup<LegacyConstantRegisterBinding>;
def err_hlsl_binding_type_mismatch: Error<"binding type '%select{t|u|b|s|c}0' only applies to %select{SRV resources|UAV resources|constant buffer resources|sampler state|numeric variables in the global scope}0">;
diff --git a/clang/include/clang/Sema/SemaHLSL.h b/clang/include/clang/Sema/SemaHLSL.h
index 3d62f47dfde69..160e3854d5e92 100644
--- a/clang/include/clang/Sema/SemaHLSL.h
+++ b/clang/include/clang/Sema/SemaHLSL.h
@@ -186,6 +186,7 @@ class SemaHLSL : public SemaBase {
void handleWaveSizeAttr(Decl *D, const ParsedAttr &AL);
void handleVkConstantIdAttr(Decl *D, const ParsedAttr &AL);
void handleVkBindingAttr(Decl *D, const ParsedAttr &AL);
+ void handleVkBuiltInAttr(Decl *D, const ParsedAttr &AL);
void handleVkLocationAttr(Decl *D, const ParsedAttr &AL);
void handlePackOffsetAttr(Decl *D, const ParsedAttr &AL);
void handleShaderAttr(Decl *D, const ParsedAttr &AL);
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index 9e76c640fd339..b2bd95f11a659 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -8299,6 +8299,9 @@ ProcessDeclAttribute(Sema &S, Decl *D, const ParsedAttr &AL,
case ParsedAttr::AT_HLSLVkExtBuiltinOutput:
S.HLSL().handleVkExtBuiltinOutputAttr(D, AL);
break;
+ case ParsedAttr::AT_HLSLVkBuiltIn:
+ S.HLSL().handleVkBuiltInAttr(D, AL);
+ break;
case ParsedAttr::AT_HLSLVkPushConstant:
S.HLSL().handleVkPushConstantAttr(D, AL);
break;
diff --git a/clang/lib/Sema/SemaHLSL.cpp b/clang/lib/Sema/SemaHLSL.cpp
index 4fe7ed151e918..7adeaab6c88f1 100644
--- a/clang/lib/Sema/SemaHLSL.cpp
+++ b/clang/lib/Sema/SemaHLSL.cpp
@@ -46,6 +46,7 @@
#include "llvm/Support/DXILABI.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/raw_ostream.h"
#include "llvm/TargetParser/Triple.h"
#include <algorithm>
#include <cmath>
@@ -983,8 +984,13 @@ void SemaHLSL::ActOnTopLevelFunction(FunctionDecl *FD) {
}
static bool isVkPipelineBuiltin(const ASTContext &AstContext, FunctionDecl *FD,
+ const DeclaratorDecl *D,
HLSLAppliedSemanticAttr *Semantic,
bool IsInput) {
+ // Explicitly annotated built-ins skip location assignment
+ if (D->hasAttr<HLSLVkBuiltInAttr>())
+ return true;
+
if (AstContext.getTargetInfo().getTriple().getOS() != llvm::Triple::Vulkan)
return false;
@@ -1048,7 +1054,7 @@ bool SemaHLSL::determineActiveSemanticOnScalar(FunctionDecl *FD,
unsigned Location = ActiveSemantic.Index.value_or(0);
- if (!isVkPipelineBuiltin(getASTContext(), FD, A,
+ if (!isVkPipelineBuiltin(getASTContext(), FD, D, A,
any(SC.CurrentIOType & IOType::In))) {
bool HasVkLocation = false;
if (auto *A = D->getAttr<HLSLVkLocationAttr>()) {
@@ -2157,10 +2163,54 @@ void SemaHLSL::handleVkLocationAttr(Decl *D, const ParsedAttr &AL) {
if (!SemaRef.checkUInt32Argument(AL, AL.getArgAsExpr(0), Location))
return;
+ // vk::builtin and vk::location mutually exclusive
+ if (const auto *BuiltIn = D->getAttr<HLSLVkBuiltInAttr>()) {
+ Diag(AL.getLoc(), diag::err_attributes_are_not_compatible)
+ << AL << BuiltIn
+ << (AL.isRegularKeywordAttribute() ||
+ BuiltIn->isRegularKeywordAttribute());
+ Diag(BuiltIn->getLocation(), diag::note_conflicting_attribute);
+ }
+
D->addAttr(::new (getASTContext())
HLSLVkLocationAttr(getASTContext(), AL, Location));
}
+void SemaHLSL::handleVkBuiltInAttr(Decl *D, const ParsedAttr &AL) {
+ StringRef BuiltIn;
+ SourceLocation ArgLoc;
+ if (!SemaRef.checkStringLiteralArgumentAttr(AL, 0, BuiltIn, &ArgLoc))
+ return;
+
+ static constexpr StringRef SupportedBuiltIns[] = {
+ "PointSize", // TODO: uncomment build-ins as they're fully implemented
+ // "HelperInvocation", "BaseVertex", "BaseInstance",
+ // "DrawIndex", "DeviceIndex", "ViewportMaskNV",
+ };
+ if (!llvm::is_contained(SupportedBuiltIns, BuiltIn)) {
+ std::string Quoted;
+ llvm::raw_string_ostream OS(Quoted);
+ llvm::interleaveComma(SupportedBuiltIns, OS,
+ [&](StringRef S) { OS << "'" << S << "'"; });
+ Diag(ArgLoc, diag::err_hlsl_vk_builtin_unsupported_value)
+ << BuiltIn << Quoted;
+ return;
+ }
+
+ // `vk::builtin` and `vk::location` are mutually exclusive. Diagnose but
+ // still attach -- see the matching comment in handleVkLocationAttr.
+ if (const auto *Location = D->getAttr<HLSLVkLocationAttr>()) {
+ Diag(AL.getLoc(), diag::err_attributes_are_not_compatible)
+ << AL << Location
+ << (AL.isRegularKeywordAttribute() ||
+ Location->isRegularKeywordAttribute());
+ Diag(Location->getLoc(), diag::note_conflicting_attribute);
+ }
+
+ D->addAttr(::new (getASTContext())
+ HLSLVkBuiltInAttr(getASTContext(), AL, BuiltIn));
+}
+
void SemaHLSL::handleSemanticAttr(Decl *D, const ParsedAttr &AL) {
uint32_t IndexValue(0), ExplicitIndex(0);
if (!SemaRef.checkUInt32Argument(AL, AL.getArgAsExpr(0), IndexValue) ||
diff --git a/clang/test/AST/HLSL/vk-builtin-attr.hlsl b/clang/test/AST/HLSL/vk-builtin-attr.hlsl
new file mode 100644
index 0000000000000..16d42a0b13637
--- /dev/null
+++ b/clang/test/AST/HLSL/vk-builtin-attr.hlsl
@@ -0,0 +1,29 @@
+// RUN: %clang_cc1 -triple spirv-unknown-vulkan1.3-library -finclude-default-header -ast-dump -o - %s | FileCheck %s -check-prefixes=CHECK,SPV
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.8-library -finclude-default-header -ast-dump -o - %s | FileCheck %s -check-prefixes=CHECK,DXIL
+
+// vk::builtin only exists when targeting SPIR-V.
+
+// CHECK: CXXRecordDecl {{.*}} struct S definition
+// CHECK: FieldDecl {{.*}} a 'float'
+// SPV-NEXT: HLSLVkBuiltInAttr {{.*}} "PointSize"
+// DXIL-NOT: HLSLVkBuiltInAttr
+// CHECK: HLSLParsedSemanticAttr {{.*}} "PSIZE"
+// CHECK: FieldDecl {{.*}} b 'float'
+// CHECK-NEXT: HLSLVkLocationAttr {{.*}} 1
+// CHECK-NEXT: HLSLParsedSemanticAttr {{.*}} "B"
+struct S {
+ [[vk::builtin("PointSize")]] float a : PSIZE;
+ [[vk::location(1)]] float b : B;
+};
+
+// CHECK: FunctionDecl {{.*}} param 'float (float)'
+// CHECK-NEXT: ParmVarDecl {{.*}} p 'float'
+// SPV-NEXT: HLSLVkBuiltInAttr {{.*}} "PointSize"
+// DXIL-NOT: HLSLVkBuiltInAttr
+// CHECK: HLSLParsedSemanticAttr {{.*}} "PSIZE"
+float param([[vk::builtin("PointSize")]] float p : PSIZE) : A { return p; }
+
+// CHECK: FunctionDecl {{.*}} func 'float ()'
+// SPV: HLSLVkBuiltInAttr {{.*}} "PointSize"
+// DXIL-NOT: HLSLVkBuiltInAttr
+[[vk::builtin("PointSize")]] float func() : PSIZE { return 1.0f; }
diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test
index 8f8c1aa3d3f9c..05e4e86f22f9d 100644
--- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test
+++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test
@@ -90,6 +90,7 @@
// CHECK-NEXT: GCCStruct (SubjectMatchRule_record)
// CHECK-NEXT: GNUInline (SubjectMatchRule_function)
// CHECK-NEXT: HIPManaged (SubjectMatchRule_variable)
+// CHECK-NEXT: HLSLVkBuiltIn (SubjectMatchRule_variable_is_parameter, SubjectMatchRule_field, SubjectMatchRule_function)
// CHECK-NEXT: HLSLVkLocation (SubjectMatchRule_variable_is_parameter, SubjectMatchRule_field, SubjectMatchRule_function)
// CHECK-NEXT: HLSLVkPushConstant (SubjectMatchRule_variable_is_global)
// CHECK-NEXT: Hot (SubjectMatchRule_function)
diff --git a/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-control.hlsl b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-control.hlsl
new file mode 100644
index 0000000000000..77079ae318970
--- /dev/null
+++ b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-control.hlsl
@@ -0,0 +1,15 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-unknown-vulkan1.3-vertex -hlsl-entry main1 -fsyntax-only -verify -verify-ignore-unexpected=note %s
+
+// Control for semantic.explicit-mix-vk-builtin-field.hlsl: without the
+// vk::builtin attribute, the same struct shape is still rejected.
+struct VSOutput {
+ [[vk::location(0)]] float4 color : COLOR;
+ float size : PSIZE;
+ // expected-error at -1 {{partial explicit stage input location assignment via vk::location(X) unsupported}}
+};
+
+[shader("vertex")]
+VSOutput main1() {
+ VSOutput output = (VSOutput)0;
+ return output;
+}
diff --git a/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-field.hlsl b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-field.hlsl
new file mode 100644
index 0000000000000..5b5b8acf373b2
--- /dev/null
+++ b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-field.hlsl
@@ -0,0 +1,18 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-unknown-vulkan1.3-vertex -hlsl-entry main1 -fsyntax-only -verify %s
+
+// vk::builtin fields are exempt from the "all or nothing" explicit-location
+// check, so they can be mixed with vk::location annotated fields.
+struct VSOutput {
+ [[vk::location(0)]] float4 color : COLOR;
+ [[vk::location(1)]] float2 uv : TEXCOORD0;
+ [[vk::builtin("PointSize")]] float size : PSIZE;
+ [[vk::location(2)]] float other : TEXCOORD1;
+};
+
+[shader("vertex")]
+VSOutput main1() {
+ VSOutput output = (VSOutput)0;
+ return output;
+}
+
+// expected-no-diagnostics
diff --git a/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-param.hlsl b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-param.hlsl
new file mode 100644
index 0000000000000..0bba0908d2433
--- /dev/null
+++ b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-param.hlsl
@@ -0,0 +1,11 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-unknown-vulkan1.3-pixel -hlsl-entry main1 -fsyntax-only -verify %s
+
+// Same as semantic.explicit-mix-vk-builtin-field.hlsl, but for a vk::builtin
+// annotated entry-point parameter instead of a struct field.
+[shader("pixel")]
+float4 main1([[vk::location(0)]] float4 a : A,
+ [[vk::builtin("PointSize")]] float size : PSIZE) : SV_Target {
+ return a * size;
+}
+
+// expected-no-diagnostics
diff --git a/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-return.hlsl b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-return.hlsl
new file mode 100644
index 0000000000000..d6dfdf793843d
--- /dev/null
+++ b/clang/test/SemaHLSL/Semantics/semantic.explicit-mix-vk-builtin-return.hlsl
@@ -0,0 +1,14 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-unknown-vulkan1.3-vertex -hlsl-entry main1 -fsyntax-only -verify %s
+
+// Same as semantic.explicit-mix-vk-builtin-field.hlsl, but vk::builtin
+// annotates the entry point's return value (the Function subject), mixed
+// with an explicit output location on an `out` parameter.
+[[vk::builtin("PointSize")]]
+[shader("vertex")]
+float main1(
+ [[vk::location(0)]] float4 a : A, [[vk::location(1)]] out float4 b : B) : PSIZE {
+ b = a;
+ return a.x;
+}
+
+// expected-no-diagnostics
diff --git a/clang/test/SemaHLSL/vk-builtin-attr-non-spirv.hlsl b/clang/test/SemaHLSL/vk-builtin-attr-non-spirv.hlsl
new file mode 100644
index 0000000000000..51a3536891401
--- /dev/null
+++ b/clang/test/SemaHLSL/vk-builtin-attr-non-spirv.hlsl
@@ -0,0 +1,16 @@
+// RUN: %clang_cc1 -finclude-default-header -triple dxil-pc-shadermodel6.8-library -verify %s
+
+// vk::builtin only exists when targeting SPIR-V; elsewhere it is unknown.
+
+struct S {
+ // expected-warning at +1 {{unknown attribute 'vk::builtin' ignored}}
+ [[vk::builtin("PointSize")]] float size : PSIZE;
+};
+
+// expected-warning at +1 {{unknown attribute 'vk::builtin' ignored}}
+float param([[vk::builtin("PointSize")]] float size : PSIZE) : A {
+ return size;
+}
+
+// expected-warning at +1 {{unknown attribute 'vk::builtin' ignored}}
+[[vk::builtin("PointSize")]] float func() : PSIZE { return 1.0f; }
diff --git a/clang/test/SemaHLSL/vk-builtin-attr.hlsl b/clang/test/SemaHLSL/vk-builtin-attr.hlsl
new file mode 100644
index 0000000000000..a330bf19e7a71
--- /dev/null
+++ b/clang/test/SemaHLSL/vk-builtin-attr.hlsl
@@ -0,0 +1,67 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-compute -verify %s
+
+// Valid subjects: field, parameter, function.
+struct Valid {
+ [[vk::builtin("PointSize")]] float size : PSIZE;
+};
+
+float param([[vk::builtin("PointSize")]] float size : PSIZE) : A { return size; }
+
+[[vk::builtin("PointSize")]] float func() : PSIZE { return 1.0f; }
+
+// Invalid subjects.
+// expected-error at +1 {{'vk::builtin' attribute only applies to parameters, non-static data members, and functions}}
+[[vk::builtin("PointSize")]] static float global_var;
+
+// expected-error at +1 {{'vk::builtin' attribute only applies to parameters, non-static data members, and functions}}
+[[vk::builtin("PointSize")]] cbuffer InvalidCBuffer {
+ float cb;
+}
+
+// Unsupported built-in name.
+struct BadName {
+ // expected-error at +1 {{'Bogus' is not a supported value for the 'vk::builtin' attribute; supported values are: 'PointSize'}}
+ [[vk::builtin("Bogus")]] float a : A;
+ // The name is case sensitive.
+ // expected-error at +1 {{'pointsize' is not a supported value for the 'vk::builtin' attribute; supported values are: 'PointSize'}}
+ [[vk::builtin("pointsize")]] float b : B;
+ // expected-error at +1 {{'' is not a supported value for the 'vk::builtin' attribute; supported values are: 'PointSize'}}
+ [[vk::builtin("")]] float c : C;
+};
+
+// Wrong argument count and type.
+struct BadArgs {
+ // expected-error at +1 {{'vk::builtin' attribute takes one argument}}
+ [[vk::builtin()]] float a : A;
+ // expected-error at +1 {{'vk::builtin' attribute takes one argument}}
+ [[vk::builtin("PointSize", "PointSize")]] float b : B;
+ // expected-error at +1 {{expected string literal as argument of 'builtin' attribute}}
+ [[vk::builtin(5)]] float c : C;
+};
+
+// Templates: the argument is a string literal, so it is validated when the
+// template is declared, not when it is instantiated.
+template <typename T> struct Templated {
+ // expected-error at +1 {{'Bogus' is not a supported value for the 'vk::builtin' attribute; supported values are: 'PointSize'}}
+ [[vk::builtin("Bogus")]] T a : A;
+ [[vk::builtin("PointSize")]] T b : PSIZE;
+};
+Templated<float> TemplatedInstance;
+
+// Combining `vk::builtin` and `vk::location` is rejected, whatever the order.
+struct Combined {
+ // expected-error at +2 {{'vk::location' and 'vk::builtin' attributes are not compatible}}
+ // expected-note at +1 {{conflicting attribute is here}}
+ [[vk::builtin("PointSize")]] [[vk::location(1)]] float a : A;
+ // expected-error at +2 {{'vk::builtin' and 'vk::location' attributes are not compatible}}
+ // expected-note at +1 {{conflicting attribute is here}}
+ [[vk::location(2)]] [[vk::builtin("PointSize")]] float b : B;
+};
+
+// expected-error at +2 {{'vk::location' and 'vk::builtin' attributes are not compatible}}
+// expected-note at +1 {{conflicting attribute is here}}
+[[vk::builtin("PointSize")]] [[vk::location(0)]] float func_combined() : A;
+
+[shader("compute")]
+[numthreads(1, 1, 1)]
+void main() {}
>From a916aca7aeff687886147601e620afc806bbaa1f Mon Sep 17 00:00:00 2001
From: Jonathan Zakharov <jzakharov at nvidia.com>
Date: Mon, 12 Oct 2026 11:13:10 -0700
Subject: [PATCH 2/3] [HLSL][SPIRV] Implement vk::builtin attribute
---
clang/lib/Sema/SemaHLSL.cpp | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/clang/lib/Sema/SemaHLSL.cpp b/clang/lib/Sema/SemaHLSL.cpp
index 7adeaab6c88f1..214c659b992bb 100644
--- a/clang/lib/Sema/SemaHLSL.cpp
+++ b/clang/lib/Sema/SemaHLSL.cpp
@@ -2163,7 +2163,15 @@ void SemaHLSL::handleVkLocationAttr(Decl *D, const ParsedAttr &AL) {
if (!SemaRef.checkUInt32Argument(AL, AL.getArgAsExpr(0), Location))
return;
+<<<<<<< HEAD
// vk::builtin and vk::location mutually exclusive
+=======
+ // `vk::builtin` and `vk::location` are mutually exclusive. Diagnose but
+ // still attach: DXC accumulates diagnostics rather than short-circuiting
+ // (a single decl can simultaneously violate this, a builtin's required
+ // type, and its legal stage), so later checks must still be able to see
+ // this attribute rather than silently never running.
+>>>>>>> 9ecd11ae9b5d ([HLSL][SPIRV] Implement vk::builtin attribute)
if (const auto *BuiltIn = D->getAttr<HLSLVkBuiltInAttr>()) {
Diag(AL.getLoc(), diag::err_attributes_are_not_compatible)
<< AL << BuiltIn
>From ad9490a1f6e1f5c8fa77e54e8e8b005c5ee91077 Mon Sep 17 00:00:00 2001
From: Jonathan Zakharov <jzakharov at nvidia.com>
Date: Mon, 12 Oct 2026 11:26:51 -0700
Subject: [PATCH 3/3] [HLSL][SPIRV] Implement vk::builtin(PointSize) validation
and CodeGen
---
clang/include/clang/Basic/AttrDocs.td | 15 +++++-
.../clang/Basic/DiagnosticSemaKinds.td | 4 ++
clang/lib/CodeGen/CGHLSLRuntime.cpp | 29 +++++++++++
clang/lib/Sema/SemaHLSL.cpp | 50 ++++++++++++++++++-
.../vk-builtin-point-size-return.hlsl | 18 +++++++
.../CodeGenHLSL/vk-builtin-point-size.hlsl | 33 ++++++++++++
.../vk-builtin-point-size-inout-pixel.hlsl | 14 ++++++
.../vk-builtin-point-size-inout-vertex.hlsl | 14 ++++++
.../vk-builtin-point-size-invalid.hlsl | 12 +++++
.../test/SemaHLSL/vk-builtin-point-size.hlsl | 12 +++++
10 files changed, 197 insertions(+), 4 deletions(-)
create mode 100644 clang/test/CodeGenHLSL/vk-builtin-point-size-return.hlsl
create mode 100644 clang/test/CodeGenHLSL/vk-builtin-point-size.hlsl
create mode 100644 clang/test/SemaHLSL/vk-builtin-point-size-inout-pixel.hlsl
create mode 100644 clang/test/SemaHLSL/vk-builtin-point-size-inout-vertex.hlsl
create mode 100644 clang/test/SemaHLSL/vk-builtin-point-size-invalid.hlsl
create mode 100644 clang/test/SemaHLSL/vk-builtin-point-size.hlsl
diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td
index bc830670689e0..6a3d4a1ce01cf 100644
--- a/clang/include/clang/Basic/AttrDocs.td
+++ b/clang/include/clang/Basic/AttrDocs.td
@@ -9755,11 +9755,22 @@ The attribute is only available when targeting SPIR-V. For any other target it
is ignored with an "unknown attribute ignored" warning.
The attribute takes a single string literal argument naming the built-in.
-Supported built-in names are added incrementally, each landing with its own
-type/stage validation and CodeGen support.
+Currently supported built-ins:
+
+* `PointSize`: the GLSL equivalent is `gl_PointSize`. Must be of floating-point
+ type. Can be used as a vertex shader output, a hull or domain shader control
+ point input or output, a geometry shader input or output, a pixel shader
+ input, or a mesh shader output.
A declaration annotated with `[[vk::builtin]]` does not take part in
`vk::location` assignment, and cannot also be annotated with `[[vk::location]]`.
+
+```c++
+struct VSOutput {
+ [[vk::location(0)]] float4 color : COLOR;
+ [[vk::builtin("PointSize")]] float size : PSIZE;
+};
+```
}];
}
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 50de8f772ace2..12b66e5462c6e 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -13791,6 +13791,10 @@ def err_hlsl_semantic_partial_explicit_indexing
def err_hlsl_vk_builtin_unsupported_value
: Error<"'%0' is not a supported value for the 'vk::builtin' attribute; "
"supported values are: %1">;
+def err_hlsl_vk_builtin_wrong_type
+ : Error<"'%0' builtin must be of type %1">;
+def err_hlsl_vk_builtin_wrong_stage
+ : Error<"'%0' builtin cannot be used as %1 shader %2">;
def warn_hlsl_user_defined_type_missing_member: Warning<"binding type '%select{t|u|b|s|c}0' only applies to types containing %select{SRV resources|UAV resources|constant buffer resources|sampler state|numeric types}0">, InGroup<LegacyConstantRegisterBinding>;
def err_hlsl_binding_type_mismatch: Error<"binding type '%select{t|u|b|s|c}0' only applies to %select{SRV resources|UAV resources|constant buffer resources|sampler state|numeric variables in the global scope}0">;
diff --git a/clang/lib/CodeGen/CGHLSLRuntime.cpp b/clang/lib/CodeGen/CGHLSLRuntime.cpp
index f770981935952..8b3c5fbf42c3d 100644
--- a/clang/lib/CodeGen/CGHLSLRuntime.cpp
+++ b/clang/lib/CodeGen/CGHLSLRuntime.cpp
@@ -36,6 +36,7 @@
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringSwitch.h"
#include "llvm/Frontend/HLSL/HLSLResource.h"
#include "llvm/Frontend/HLSL/RootSignatureMetadata.h"
#include "llvm/IR/Constants.h"
@@ -1570,6 +1571,18 @@ static void createSPIRVBuiltinStore(IRBuilder<> &B, llvm::Module &M,
B.CreateStore(Source, GV);
}
+// Maps a [[vk::builtin(...)]] name to its SPIR-V spec `BuiltIn` decoration.
+// Deliberately not creating dependency on llvm::SPIRV::BuiltIn in front end.
+// Sema's handleVkBuiltInAttr only allows names handled here to reach CodeGen.
+static unsigned getSPIRVVkBuiltinID(StringRef BuiltIn) {
+ unsigned ID = llvm::StringSwitch<unsigned>(BuiltIn)
+ .Case("PointSize", /* BuiltIn::PointSize */ 1)
+ .Default(-1);
+ assert(ID != static_cast<unsigned>(-1) &&
+ "Sema should only allow builtin names implemented here");
+ return ID;
+}
+
void CGHLSLRuntime::emitSystemSemanticStore(
IRBuilder<> &B, llvm::Value *Source, const clang::DeclaratorDecl *Decl,
HLSLAppliedSemanticAttr *Semantic,
@@ -1618,6 +1631,14 @@ llvm::Value *CGHLSLRuntime::handleScalarSemanticLoad(
const clang::DeclaratorDecl *Decl, HLSLAppliedSemanticAttr *Semantic,
SemanticSignatures &Signature) {
+ // [[vk::builtin(...)]] takes precedence over the recognized-semantic
+ // dispatch, so without this check they'd always be treated as an arbitrary
+ // user semantic. The attribute is only ever attached when targeting SPIR-V.
+ if (const auto *BuiltInAttr = Decl->getAttr<HLSLVkBuiltInAttr>())
+ return createSPIRVBuiltinLoad(
+ B, CGM.getModule(), Type, Semantic->getAttrName()->getName(),
+ getSPIRVVkBuiltinID(BuiltInAttr->getBuiltIn()));
+
std::optional<unsigned> Index = Semantic->getSemanticIndex();
llvm::dxbc::PSV::SemanticKind SemanticKind =
llvm::hlsl::getSemanticKind(Semantic->getAttrName()->getName());
@@ -1640,6 +1661,14 @@ void CGHLSLRuntime::handleScalarSemanticStore(IRBuilder<> &B,
const clang::DeclaratorDecl *Decl,
HLSLAppliedSemanticAttr *Semantic,
SemanticSignatures &Signature) {
+ // See the matching comment in `handleScalarSemanticLoad`
+ if (const auto *BuiltInAttr = Decl->getAttr<HLSLVkBuiltInAttr>()) {
+ createSPIRVBuiltinStore(B, CGM.getModule(), Source,
+ Semantic->getAttrName()->getName(),
+ getSPIRVVkBuiltinID(BuiltInAttr->getBuiltIn()));
+ return;
+ }
+
std::optional<unsigned> Index = Semantic->getSemanticIndex();
llvm::dxbc::PSV::SemanticKind SemanticKind =
llvm::hlsl::getSemanticKind(Semantic->getAttrName()->getName());
diff --git a/clang/lib/Sema/SemaHLSL.cpp b/clang/lib/Sema/SemaHLSL.cpp
index 214c659b992bb..0a8c3d9ee79c6 100644
--- a/clang/lib/Sema/SemaHLSL.cpp
+++ b/clang/lib/Sema/SemaHLSL.cpp
@@ -1015,6 +1015,46 @@ static bool isVkPipelineBuiltin(const ASTContext &AstContext, FunctionDecl *FD,
}
}
+static void validateVkBuiltinUsage(Sema &S, FunctionDecl *FD,
+ const DeclaratorDecl *D,
+ const HLSLVkBuiltInAttr *Attr,
+ IOType CurrentIOType) {
+ const ASTContext &AstContext = S.getASTContext();
+ if (AstContext.getTargetInfo().getTriple().getOS() != llvm::Triple::Vulkan)
+ return;
+
+ StringRef BuiltIn = Attr->getBuiltIn();
+
+ QualType T =
+ (D == FD ? FD->getReturnType() : D->getType()).getNonReferenceType();
+ const auto *ShaderAttr = FD->getAttr<HLSLShaderAttr>();
+ assert(ShaderAttr && "Entry point has no shader attribute");
+ llvm::Triple::EnvironmentType Stage = ShaderAttr->getType();
+ bool IsIn = any(CurrentIOType & IOType::In);
+ bool IsOut = any(CurrentIOType & IOType::Out);
+
+ if (BuiltIn == "PointSize") {
+ if (!T->isFloatingType())
+ S.Diag(Attr->getLocation(), diag::err_hlsl_vk_builtin_wrong_type)
+ << BuiltIn << S.getASTContext().FloatTy;
+
+ bool LegalStage = (Stage == llvm::Triple::Vertex && IsOut) ||
+ Stage == llvm::Triple::Hull ||
+ Stage == llvm::Triple::Domain ||
+ Stage == llvm::Triple::Geometry ||
+ (Stage == llvm::Triple::Pixel && IsIn) ||
+ (Stage == llvm::Triple::Mesh && IsOut);
+ if (!LegalStage) {
+ StringRef IOTypeName = (IsIn && IsOut) ? "inputs and outputs"
+ : IsIn ? "inputs"
+ : "outputs";
+ S.Diag(Attr->getLocation(), diag::err_hlsl_vk_builtin_wrong_stage)
+ << BuiltIn << llvm::Triple::getEnvironmentTypeName(Stage)
+ << IOTypeName;
+ }
+ }
+}
+
bool SemaHLSL::determineActiveSemanticOnScalar(FunctionDecl *FD,
DeclaratorDecl *OutputDecl,
DeclaratorDecl *D,
@@ -1054,8 +1094,14 @@ bool SemaHLSL::determineActiveSemanticOnScalar(FunctionDecl *FD,
unsigned Location = ActiveSemantic.Index.value_or(0);
- if (!isVkPipelineBuiltin(getASTContext(), FD, D, A,
- any(SC.CurrentIOType & IOType::In))) {
+ bool IsBuiltin = isVkPipelineBuiltin(getASTContext(), FD, D, A,
+ any(SC.CurrentIOType & IOType::In));
+ if (IsBuiltin) {
+ if (const auto *BuiltInAttr = D->getAttr<HLSLVkBuiltInAttr>())
+ validateVkBuiltinUsage(SemaRef, FD, D, BuiltInAttr, SC.CurrentIOType);
+ }
+
+ if (!IsBuiltin) {
bool HasVkLocation = false;
if (auto *A = D->getAttr<HLSLVkLocationAttr>()) {
HasVkLocation = true;
diff --git a/clang/test/CodeGenHLSL/vk-builtin-point-size-return.hlsl b/clang/test/CodeGenHLSL/vk-builtin-point-size-return.hlsl
new file mode 100644
index 0000000000000..2e999f3226b31
--- /dev/null
+++ b/clang/test/CodeGenHLSL/vk-builtin-point-size-return.hlsl
@@ -0,0 +1,18 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-vertex -hlsl-entry main -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s
+
+// Same as vk-builtin-point-size.hlsl, but [[vk::builtin("PointSize")]]
+// annotates the entry point's return value (the Function subject) rather
+// than a struct field.
+
+[[vk::builtin("PointSize")]]
+[shader("vertex")]
+float main([[vk::location(0)]] float4 a : A) : PSIZE {
+ return a.x;
+}
+
+// CHECK: @PSIZE = {{.*}} addrspace(8) global float, !spirv.Decorations [[PSIZE_DECO:![0-9]+]]
+// CHECK: define void @main()
+// CHECK: [[PT_SIZE:%[0-9]+]] = call spir_func float @{{.*}}main{{.*}}
+// CHECK-NEXT: store float [[PT_SIZE]], ptr addrspace(8) @PSIZE
+// CHECK-DAG: [[PSIZE_DECO]] = !{[[PSIZE_OP:![0-9]+]]}
+// CHECK-DAG: [[PSIZE_OP]] = !{i32 11, i32 1}
diff --git a/clang/test/CodeGenHLSL/vk-builtin-point-size.hlsl b/clang/test/CodeGenHLSL/vk-builtin-point-size.hlsl
new file mode 100644
index 0000000000000..a1f42192c9b91
--- /dev/null
+++ b/clang/test/CodeGenHLSL/vk-builtin-point-size.hlsl
@@ -0,0 +1,33 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-vertex -hlsl-entry main -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s
+// RUN: %clang_cc1 -finclude-default-header -triple dxil-pc-shadermodel6.8-vertex -hlsl-entry main -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefix=DXIL
+
+// On SPIR-V, [[vk::builtin("PointSize")]] lowers to a BuiltIn PointSize
+// decoration instead of an ordinary Location-based user semantic. The
+// attribute doesn't exist on DXIL, so there PSIZE is an ordinary semantic.
+
+struct VSOutput {
+ float4 pos : SV_Position;
+ float4 color : COLOR;
+ [[vk::builtin("PointSize")]] float size : PSIZE;
+};
+
+[shader("vertex")]
+VSOutput main() {
+ VSOutput output = (VSOutput)0;
+ return output;
+}
+
+// CHECK: @COLOR0 = {{.*}}!spirv.Decorations ![[COLOR_MD:[0-9]+]]
+// CHECK: @PSIZE = {{.*}}addrspace(8){{.*}}!spirv.Decorations ![[PSIZE_MD:[0-9]+]]
+// CHECK: define void @main()
+// CHECK: store float %{{.*}}, ptr addrspace(8) @PSIZE
+
+// BuiltIn decoration (11) with value 1 (PointSize), vs. COLOR0's Location (30).
+// CHECK: ![[COLOR_MD]] = !{![[COLOR_DEC:[0-9]+]]}
+// CHECK: ![[COLOR_DEC]] = !{i32 30,
+// CHECK: ![[PSIZE_MD]] = !{![[PSIZE_DEC:[0-9]+]]}
+// CHECK: ![[PSIZE_DEC]] = !{i32 11, i32 1}
+
+// DXIL: define void @main()
+// DXIL-NOT: PointSize
+// DXIL: !{{[0-9]+}} = !{i32 {{[0-9]+}}, !"PSIZE",
diff --git a/clang/test/SemaHLSL/vk-builtin-point-size-inout-pixel.hlsl b/clang/test/SemaHLSL/vk-builtin-point-size-inout-pixel.hlsl
new file mode 100644
index 0000000000000..3fc454105e90d
--- /dev/null
+++ b/clang/test/SemaHLSL/vk-builtin-point-size-inout-pixel.hlsl
@@ -0,0 +1,14 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-pixel -hlsl-entry ps -fsyntax-only -verify %s
+
+// PointSize is only legal as a pixel shader input, never an output. An
+// `inout` parameter is currently treated as output-only here, so this is
+// rejected (see the sibling vk-builtin-point-size-inout-vertex.hlsl, where
+// `inout` on a stage where PointSize is a legal output is accepted).
+
+[shader("pixel")]
+float4 ps([[vk::builtin("PointSize")]] inout float size : PSIZE) : SV_Target {
+ // expected-error at -1 {{semantic 'PSIZE' is not supported in pixel shader outputs}}
+ // expected-error at -2 {{'PointSize' builtin cannot be used as pixel shader outputs}}
+ size *= 2.0;
+ return float4(size, 0, 0, 1);
+}
diff --git a/clang/test/SemaHLSL/vk-builtin-point-size-inout-vertex.hlsl b/clang/test/SemaHLSL/vk-builtin-point-size-inout-vertex.hlsl
new file mode 100644
index 0000000000000..f0a541c48e829
--- /dev/null
+++ b/clang/test/SemaHLSL/vk-builtin-point-size-inout-vertex.hlsl
@@ -0,0 +1,14 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-vertex -hlsl-entry vs -fsyntax-only -verify %s
+
+// An `inout` PointSize parameter on a vertex shader is accepted, same as
+// plain `out` (see the sibling vk-builtin-point-size-inout-pixel.hlsl for a
+// stage where `inout`'s current treatment as output-only changes the
+// outcome).
+
+[shader("vertex")]
+float4 vs([[vk::builtin("PointSize")]] inout float size : PSIZE) : SV_Position {
+ size *= 2.0;
+ return float4(0, 0, 0, 1);
+}
+
+// expected-no-diagnostics
diff --git a/clang/test/SemaHLSL/vk-builtin-point-size-invalid.hlsl b/clang/test/SemaHLSL/vk-builtin-point-size-invalid.hlsl
new file mode 100644
index 0000000000000..cc7d5cbd1b485
--- /dev/null
+++ b/clang/test/SemaHLSL/vk-builtin-point-size-invalid.hlsl
@@ -0,0 +1,12 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-vertex -hlsl-entry main -fsyntax-only -verify %s
+
+// A single decl can violate multiple PointSize rules at once -- combined
+// with vk::location, wrong type, and wrong stage -- and all diagnostics
+// fire together rather than short-circuiting on the first.
+float main([[vk::location(5), vk::builtin("PointSize")]] int input : A) : B {
+ // expected-error at -1 {{'vk::builtin' and 'vk::location' attributes are not compatible}}
+ // expected-note at -2 {{conflicting attribute is here}}
+ // expected-error at -3 {{'PointSize' builtin must be of type 'float'}}
+ // expected-error at -4 {{'PointSize' builtin cannot be used as vertex shader inputs}}
+ return (float)input;
+}
diff --git a/clang/test/SemaHLSL/vk-builtin-point-size.hlsl b/clang/test/SemaHLSL/vk-builtin-point-size.hlsl
new file mode 100644
index 0000000000000..0fcf7130ddd2a
--- /dev/null
+++ b/clang/test/SemaHLSL/vk-builtin-point-size.hlsl
@@ -0,0 +1,12 @@
+// RUN: %clang_cc1 -finclude-default-header -triple spirv-pc-vulkan1.3-vertex -hlsl-entry legal -fsyntax-only -verify %s
+
+// PointSize must be a float, and is only legal as a vertex shader output
+// (plus other stages not exercised here).
+[shader("vertex")]
+float legal([[vk::location(0)]] float4 a : A,
+ [[vk::builtin("PointSize")]] out float size : PSIZE) : B {
+ size = a.x;
+ return a.y;
+}
+
+// expected-no-diagnostics
More information about the cfe-commits
mailing list