[HLSL][SPIR-V] Add vk::ext_builtin_output attribute (#188268)

This attribute is similar to the already implemented ext_builtin_input
attribute.
One important bit is the `static` storage class: HLSL uses static
differently than C/C++. This is a known weirdness:
 See https://github.com/microsoft/hlsl-specs/issues/350

In C/C++, when we declare a variable as 'extern', we often expect
another module to declare the symbole. In HLSL, the pipeline will
'declare' the symbol. Hence in this case, we need to emit the global
variable.

Related WG-HLSL:
  https://github.com/llvm/wg-hlsl/blob/main/proposals/0031-semantics.md

---------

Co-authored-by: Steven Perron <stevenperron@google.com>
This commit is contained in:
Nathan Gauër 2026-03-25 17:07:35 +01:00 committed by GitHub
parent 414f0c3362
commit 18250fd47b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 136 additions and 3 deletions

View File

@ -62,6 +62,7 @@ enum class LangAS : unsigned {
hlsl_private,
hlsl_device,
hlsl_input,
hlsl_output,
hlsl_push_constant,
// Wasm specific address spaces.

View File

@ -146,6 +146,12 @@ def HLSLInputBuiltin
S->getType().isConstQualified()}],
"static const globals">;
def HLSLOutputBuiltin
: SubsetSubject<Var, [{S->hasGlobalStorage() &&
S->getStorageClass() == StorageClass::SC_Static &&
!S->getType().isConstQualified()}],
"non-const static globals">;
def GlobalVar : SubsetSubject<Var,
[{S->hasGlobalStorage()}], "global variables">;
@ -5217,6 +5223,14 @@ def HLSLVkExtBuiltinInput : InheritableAttr {
let Documentation = [HLSLVkExtBuiltinInputDocs];
}
def HLSLVkExtBuiltinOutput : InheritableAttr {
let Spellings = [CXX11<"vk", "ext_builtin_output">];
let Args = [UnsignedArgument<"BuiltIn">];
let Subjects = SubjectList<[HLSLOutputBuiltin], ErrorDiag>;
let LangOpts = [HLSL];
let Documentation = [HLSLVkExtBuiltinOutputDocs];
}
def HLSLVkPushConstant : InheritableAttr {
let Spellings = [CXX11<"vk", "push_constant">];
let Args = [];

View File

@ -8978,6 +8978,29 @@ https://github.com/microsoft/hlsl-specs/blob/main/proposals/0011-inline-spirv.md
}];
}
def HLSLVkExtBuiltinOutputDocs : Documentation {
let Category = DocCatVariable;
let Content = [{
Vulkan shaders have `Output` builtins. Those variables are externally
visible to the driver/pipeline, but each copy is private to the current
lane.
Those builtins can be declared using the `[[vk::ext_builtin_output]]`
attribute like follows:
.. code-block:: c++
[[vk::ext_builtin_output(/* Position */ 0)]]
static float4 position;
This variable will be lowered into a module-level variable, with the `Output`
storage class, and the `BuiltIn 0` decoration.
The full documentation for this inline SPIR-V attribute can be found here:
https://github.com/microsoft/hlsl-specs/blob/main/proposals/0011-inline-spirv.md
}];
}
def HLSLVkPushConstantDocs : Documentation {
let Category = DocCatVariable;
let Content = [{

View File

@ -190,6 +190,7 @@ public:
void handleSemanticAttr(Decl *D, const ParsedAttr &AL);
void handleVkExtBuiltinInputAttr(Decl *D, const ParsedAttr &AL);
void handleVkExtBuiltinOutputAttr(Decl *D, const ParsedAttr &AL);
void handleVkPushConstantAttr(Decl *D, const ParsedAttr &AL);
bool CheckBuiltinFunctionCall(unsigned BuiltinID, CallExpr *TheCall);

View File

@ -101,6 +101,7 @@ bool Qualifiers::isTargetAddressSpaceSupersetOf(LangAS A, LangAS B,
(A == LangAS::Default && B == LangAS::hlsl_private) ||
(A == LangAS::Default && B == LangAS::hlsl_device) ||
(A == LangAS::Default && B == LangAS::hlsl_input) ||
(A == LangAS::Default && B == LangAS::hlsl_output) ||
(A == LangAS::Default && B == LangAS::hlsl_push_constant) ||
// Conversions from target specific address spaces may be legal
// depending on the target information.

View File

@ -2707,6 +2707,8 @@ std::string Qualifiers::getAddrSpaceAsString(LangAS AS) {
return "hlsl_device";
case LangAS::hlsl_input:
return "hlsl_input";
case LangAS::hlsl_output:
return "hlsl_output";
case LangAS::hlsl_push_constant:
return "hlsl_push_constant";
case LangAS::wasm_funcref:

View File

@ -52,7 +52,8 @@ static const LangASMap FakeAddrSpaceMap = {
15, // hlsl_private
16, // hlsl_device
17, // hlsl_input
18, // hlsl_push_constant
18, // hlsl_output
19, // hlsl_push_constant
20, // wasm_funcref
};

View File

@ -49,6 +49,7 @@ static const unsigned ARM64AddrSpaceMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.

View File

@ -53,6 +53,7 @@ const LangASMap AMDGPUTargetInfo::AMDGPUDefIsGenMap = {
llvm::AMDGPUAS::PRIVATE_ADDRESS, // hlsl_private
llvm::AMDGPUAS::GLOBAL_ADDRESS, // hlsl_device
llvm::AMDGPUAS::PRIVATE_ADDRESS, // hlsl_input
llvm::AMDGPUAS::PRIVATE_ADDRESS, // hlsl_output
llvm::AMDGPUAS::GLOBAL_ADDRESS, // hlsl_push_constant
};
@ -82,6 +83,7 @@ const LangASMap AMDGPUTargetInfo::AMDGPUDefIsPrivMap = {
llvm::AMDGPUAS::PRIVATE_ADDRESS, // hlsl_private
llvm::AMDGPUAS::GLOBAL_ADDRESS, // hlsl_device
llvm::AMDGPUAS::PRIVATE_ADDRESS, // hlsl_input
llvm::AMDGPUAS::PRIVATE_ADDRESS, // hlsl_output
llvm::AMDGPUAS::GLOBAL_ADDRESS, // hlsl_push_constant
};
} // namespace targets

View File

@ -46,6 +46,7 @@ static const unsigned DirectXAddrSpaceMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.

View File

@ -50,6 +50,7 @@ static const unsigned NVPTXAddrSpaceMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.

View File

@ -52,6 +52,7 @@ static const unsigned SPIRDefIsPrivMap[] = {
10, // hlsl_private
11, // hlsl_device
7, // hlsl_input
8, // hlsl_output
13, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.
@ -89,6 +90,7 @@ static const unsigned SPIRDefIsGenMap[] = {
10, // hlsl_private
11, // hlsl_device
7, // hlsl_input
8, // hlsl_output
13, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.

View File

@ -46,6 +46,7 @@ static const unsigned ZOSAddressMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
0 // wasm_funcref
};

View File

@ -55,6 +55,7 @@ static const unsigned TCEOpenCLAddrSpaceMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.

View File

@ -46,6 +46,7 @@ static const unsigned WebAssemblyAddrSpaceMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
20, // wasm_funcref
};

View File

@ -50,6 +50,7 @@ static const unsigned X86AddrSpaceMap[] = {
0, // hlsl_private
0, // hlsl_device
0, // hlsl_input
0, // hlsl_output
0, // hlsl_push_constant
// Wasm address space values for this target are dummy values,
// as it is only enabled for Wasm targets.

View File

@ -1163,6 +1163,8 @@ void CGHLSLRuntime::handleGlobalVarDefinition(const VarDecl *VD,
llvm::GlobalVariable *GV) {
if (auto Attr = VD->getAttr<HLSLVkExtBuiltinInputAttr>())
addSPIRVBuiltinDecoration(GV, Attr->getBuiltIn());
if (auto Attr = VD->getAttr<HLSLVkExtBuiltinOutputAttr>())
addSPIRVBuiltinDecoration(GV, Attr->getBuiltIn());
}
llvm::Instruction *CGHLSLRuntime::getConvergenceToken(BasicBlock &BB) {

View File

@ -4441,6 +4441,17 @@ void CodeGenModule::EmitGlobal(GlobalDecl GD) {
return;
}
}
// HLSL extern globals can be read/written to by the pipeline. Those
// are declared, but never defined.
if (LangOpts.HLSL) {
if (VD->getStorageClass() == SC_Extern) {
auto GV = cast<llvm::GlobalVariable>(GetAddrOfGlobalVar(VD));
getHLSLRuntime().handleGlobalVarDefinition(VD, GV);
return;
}
}
// If this declaration may have caused an inline variable definition to
// change linkage, make sure that it's emitted.
if (Context.getInlineVariableDefinitionKind(VD) ==

View File

@ -7953,6 +7953,9 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
case ParsedAttr::AT_HLSLVkExtBuiltinInput:
S.HLSL().handleVkExtBuiltinInputAttr(D, AL);
break;
case ParsedAttr::AT_HLSLVkExtBuiltinOutput:
S.HLSL().handleVkExtBuiltinOutputAttr(D, AL);
break;
case ParsedAttr::AT_HLSLVkPushConstant:
S.HLSL().handleVkPushConstantAttr(D, AL);
break;

View File

@ -1842,6 +1842,14 @@ void SemaHLSL::handleVkExtBuiltinInputAttr(Decl *D, const ParsedAttr &AL) {
HLSLVkExtBuiltinInputAttr(getASTContext(), AL, ID));
}
void SemaHLSL::handleVkExtBuiltinOutputAttr(Decl *D, const ParsedAttr &AL) {
uint32_t ID;
if (!SemaRef.checkUInt32Argument(AL, AL.getArgAsExpr(0), ID))
return;
D->addAttr(::new (getASTContext())
HLSLVkExtBuiltinOutputAttr(getASTContext(), AL, ID));
}
void SemaHLSL::handleVkPushConstantAttr(Decl *D, const ParsedAttr &AL) {
D->addAttr(::new (getASTContext())
HLSLVkPushConstantAttr(getASTContext(), AL));
@ -4857,6 +4865,19 @@ void SemaHLSL::deduceAddressSpace(VarDecl *Decl) {
return;
}
if (Decl->hasAttr<HLSLVkExtBuiltinOutputAttr>()) {
LangAS ImplAS = LangAS::hlsl_output;
Type = SemaRef.getASTContext().getAddrSpaceQualType(Type, ImplAS);
Decl->setType(Type);
// HLSL uses `static` differently than C++. For BuiltIn output, the static
// does not imply private to the module scope.
// Marking it as external to reflect the semantic this attribute brings.
// See https://github.com/microsoft/hlsl-specs/issues/350
Decl->setStorageClass(SC_Extern);
return;
}
bool IsVulkan = getASTContext().getTargetInfo().getTriple().getOS() ==
llvm::Triple::Vulkan;
if (IsVulkan && Decl->hasAttr<HLSLVkPushConstantAttr>()) {

View File

@ -0,0 +1,15 @@
// RUN: %clang_cc1 -finclude-default-header -x hlsl -triple \
// RUN: spirv-unknown-vulkan1.3-vertex %s -emit-llvm -O3 -o - | FileCheck %s
[[vk::ext_builtin_output(/* Position */ 0)]]
static float4 position;
// CHECK: @position = external hidden local_unnamed_addr addrspace(8) global <4 x float>, align 16, !spirv.Decorations [[META0:![0-9]+]]
RWStructuredBuffer<float4> input : register(u1, space0);
void main() {
position = input[0];
// CHECK: store <4 x float> %[[#]], ptr addrspace(8) @position, align 16
}
// CHECK: [[META0]] = !{[[META1:![0-9]+]]}
// CHECK: [[META1]] = !{i32 11, i32 0}

View File

@ -0,0 +1,27 @@
// RUN: %clang_cc1 -triple spirv-unknown-vulkan1.3-compute -x hlsl -hlsl-entry foo -finclude-default-header -o - %s -verify
// expected-error@+1 {{'vk::ext_builtin_output' attribute only applies to non-const static globals}}
[[vk::ext_builtin_output(/* Position */ 0)]]
float4 position0;
// expected-error@+1 {{'vk::ext_builtin_output' attribute only applies to non-const static globals}}
[[vk::ext_builtin_output(/* Position */ 0)]]
// expected-error@+1 {{default initialization of an object of const type 'const hlsl_private float4' (aka 'const hlsl_private vector<float, 4>')}}
static const float4 position1;
// expected-error@+1 {{'vk::ext_builtin_output' attribute takes one argument}}
[[vk::ext_builtin_output()]]
static float4 position2;
// expected-error@+1 {{'vk::ext_builtin_output' attribute requires an integer constant}}
[[vk::ext_builtin_output(0.4f)]]
static float4 position3;
// expected-error@+1 {{'vk::ext_builtin_output' attribute only applies to non-const static globals}}
[[vk::ext_builtin_output(0)]]
void some_function() {
}
[numthreads(1,1,1)]
void foo() {
}

View File

@ -43,7 +43,7 @@ void neg() {
template <long int I>
void tooBig() {
__attribute__((address_space(I))) int *bounds; // expected-error {{address space is larger than the maximum supported (8388581)}}
__attribute__((address_space(I))) int *bounds; // expected-error {{address space is larger than the maximum supported (8388580)}}
}
template <long int I>
@ -101,7 +101,7 @@ int main() {
car<1, 2, 3>(); // expected-note {{in instantiation of function template specialization 'car<1, 2, 3>' requested here}}
HasASTemplateFields<1> HASTF;
neg<-1>(); // expected-note {{in instantiation of function template specialization 'neg<-1>' requested here}}
correct<0x7FFFE5>();
correct<0x7FFFE4>();
tooBig<8388650>(); // expected-note {{in instantiation of function template specialization 'tooBig<8388650L>' requested here}}
__attribute__((address_space(1))) char *x;