[HLSL] Add globals for resources embedded in structs (#184281)

For each resource or resource array member of a struct declared at global scope or inside a `cbuffer`, create an implicit global variable of the same resource type. The variable name will be derived from the struct instance name and the member name and will be associated with the struct declaration using a new attribute `HLSLAssociatedResourceDeclAttr`.

Closes #182988
This commit is contained in:
Helena Kotas 2026-03-18 17:40:13 -07:00 committed by GitHub
parent 8176bc0e9b
commit 39b6a4d84a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 443 additions and 14 deletions

View File

@ -17,6 +17,7 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/Attr.h"
#include "clang/AST/DeclBase.h"
#include "clang/Basic/IdentifierTable.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/Support/Compiler.h"
#include "llvm/Frontend/HLSL/HLSLResource.h"
@ -108,6 +109,39 @@ inline uint32_t getResourceDimensions(llvm::dxil::ResourceDimension Dim) {
llvm_unreachable("Unhandled llvm::dxil::ResourceDimension enum.");
}
// Helper class for building a name of a global resource variable that
// gets created for a resource embedded in a struct or class. This will
// also be used from CodeGen to build a name that matches the resource
// access with the corresponding declaration.
class EmbeddedResourceNameBuilder {
llvm::SmallString<64> Name;
llvm::SmallVector<unsigned> Offsets;
inline static constexpr std::string_view BaseClassDelim = "::";
inline static constexpr std::string_view FieldDelim = ".";
inline static constexpr std::string_view ArrayIndexDelim = FieldDelim;
public:
EmbeddedResourceNameBuilder(llvm::StringRef BaseName) : Name(BaseName) {}
EmbeddedResourceNameBuilder() : Name("") {}
void pushName(llvm::StringRef N) { pushName(N, FieldDelim); }
void pushBaseName(llvm::StringRef N);
void pushArrayIndex(uint64_t Index);
void pop() {
assert(!Offsets.empty() && "no name to pop");
Name.resize(Offsets.pop_back_val());
}
IdentifierInfo *getNameAsIdentifier(ASTContext &AST) const {
return &AST.Idents.get(Name);
}
private:
void pushName(llvm::StringRef N, llvm::StringRef Delim);
};
} // namespace hlsl
} // namespace clang

View File

@ -5053,6 +5053,14 @@ def HLSLResourceBinding: InheritableAttr {
}];
}
def HLSLAssociatedResourceDecl : InheritableAttr {
let Spellings = [];
let Args = [DeclArgument<Var, "ResDecl">];
let Subjects = SubjectList<[ExternalGlobalVar], ErrorDiag>;
let LangOpts = [HLSL];
let Documentation = [InternalOnly];
}
def HLSLUnparsedSemantic : HLSLAnnotationAttr {
let Spellings = [];
let Args = [DefaultIntArgument<"Index", 0>,

View File

@ -225,6 +225,13 @@ public:
const IdentifierInfo *CompName,
SourceLocation CompLoc);
uint32_t getNextImplicitBindingOrderID() {
return ImplicitBindingNextOrderID++;
}
bool initGlobalResourceDecl(VarDecl *VD);
bool initGlobalResourceArrayDecl(VarDecl *VD);
private:
// HLSL resource type attributes need to be processed all at once.
// This is a list to collect them.
@ -318,12 +325,7 @@ private:
const Attr *A, llvm::Triple::EnvironmentType Stage, IOType CurrentIOType,
std::initializer_list<SemanticStageInfo> AllowedStages);
uint32_t getNextImplicitBindingOrderID() {
return ImplicitBindingNextOrderID++;
}
bool initGlobalResourceDecl(VarDecl *VD);
bool initGlobalResourceArrayDecl(VarDecl *VD);
void handleGlobalStructOrArrayOfWithResources(VarDecl *VD);
// Infer a common global binding info for an Expr
//

View File

@ -95,6 +95,7 @@ add_clang_library(clangAST
ByteCode/State.cpp
ByteCode/MemberPointer.cpp
ByteCode/InterpShared.cpp
HLSLResource.cpp
ItaniumCXXABI.cpp
ItaniumMangle.cpp
JSONNodeDumper.cpp

View File

@ -0,0 +1,45 @@
//===--- HLSLResource.cpp - Helper routines for HLSL resources -----------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file provides shared routines to help analyze HLSL resources and
// their bindings during Sema and CodeGen.
//
//===----------------------------------------------------------------------===//
#include "clang/AST/HLSLResource.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/Type.h"
using namespace clang;
namespace clang {
namespace hlsl {
void EmbeddedResourceNameBuilder::pushBaseName(llvm::StringRef N) {
pushName(N, FieldDelim);
Name.append(BaseClassDelim);
}
void EmbeddedResourceNameBuilder::pushName(llvm::StringRef N,
llvm::StringRef Delim) {
Offsets.push_back(Name.size());
if (!Name.empty() && !Name.ends_with(BaseClassDelim))
Name.append(Delim);
Name.append(N);
}
void EmbeddedResourceNameBuilder::pushArrayIndex(uint64_t Index) {
llvm::raw_svector_ostream OS(Name);
Offsets.push_back(Name.size());
OS << ArrayIndexDelim;
OS << Index;
}
} // namespace hlsl
} // namespace clang

View File

@ -1812,6 +1812,12 @@ Sema::BuildFieldReferenceExpr(Expr *BaseExpr, bool IsArrow,
// except that 'mutable' members don't pick up 'const'.
if (Field->isMutable()) BaseQuals.removeConst();
// HLSL resource types do not pick up address space qualifiers from the
// base.
if (getLangOpts().HLSL && (MemberType->isHLSLResourceRecord() ||
MemberType->isHLSLResourceRecordArray()))
BaseQuals.removeAddressSpace();
Qualifiers MemberQuals =
Context.getCanonicalType(MemberType).getQualifiers();

View File

@ -342,19 +342,27 @@ static bool isZeroSizedArray(const ConstantArrayType *CAT) {
return CAT != nullptr;
}
static bool isResourceRecordTypeOrArrayOf(VarDecl *VD) {
const Type *Ty = VD->getType().getTypePtr();
static bool isResourceRecordTypeOrArrayOf(QualType Ty) {
return Ty->isHLSLResourceRecord() || Ty->isHLSLResourceRecordArray();
}
static bool isResourceRecordTypeOrArrayOf(VarDecl *VD) {
return isResourceRecordTypeOrArrayOf(VD->getType());
}
static const HLSLAttributedResourceType *
getResourceArrayHandleType(QualType QT) {
assert(QT->isHLSLResourceRecordArray() &&
"expected array of resource records");
const Type *Ty = QT->getUnqualifiedDesugaredType();
while (const ArrayType *AT = dyn_cast<ArrayType>(Ty))
Ty = AT->getArrayElementTypeNoTypeQual()->getUnqualifiedDesugaredType();
return HLSLAttributedResourceType::findHandleTypeOnResource(Ty);
}
static const HLSLAttributedResourceType *
getResourceArrayHandleType(VarDecl *VD) {
assert(VD->getType()->isHLSLResourceRecordArray() &&
"expected array of resource records");
const Type *Ty = VD->getType()->getUnqualifiedDesugaredType();
while (const ArrayType *AT = dyn_cast<ArrayType>(Ty))
Ty = AT->getArrayElementTypeNoTypeQual()->getUnqualifiedDesugaredType();
return HLSLAttributedResourceType::findHandleTypeOnResource(Ty);
return getResourceArrayHandleType(VD->getType());
}
// Returns true if the type is a leaf element type that is not valid to be
@ -4811,6 +4819,151 @@ void SemaHLSL::deduceAddressSpace(VarDecl *Decl) {
Decl->setType(Type);
}
// Creates a global variable declaration for a resource field embedded in a
// struct, assigns it a binding, initializes it, and associates it with the
// struct declaration via an HLSLAssociatedResourceDeclAttr.
static void createGlobalResourceDeclForStruct(Sema &S, VarDecl *ParentVD,
SourceLocation Loc,
IdentifierInfo *Id,
QualType ResTy) {
assert(isResourceRecordTypeOrArrayOf(ResTy) &&
"expected resource type or array of resources");
DeclContext *DC = ParentVD->getNonTransparentDeclContext();
assert(DC->isTranslationUnit() && "expected translation unit decl context");
ASTContext &AST = S.getASTContext();
VarDecl *ResDecl =
VarDecl::Create(AST, DC, Loc, Loc, Id, ResTy, nullptr, SC_None);
unsigned Range = 1;
const HLSLAttributedResourceType *ResHandleTy = nullptr;
if (const auto *AT = dyn_cast<ArrayType>(ResTy.getTypePtr())) {
const auto *CAT = dyn_cast<ConstantArrayType>(AT);
Range = CAT ? CAT->getSize().getZExtValue() : 0;
ResHandleTy = getResourceArrayHandleType(ResTy);
} else {
ResHandleTy = HLSLAttributedResourceType::findHandleTypeOnResource(
ResTy.getTypePtr());
}
// FIXME: Explicit bindings will be handled in a follow-up change. For now
// just add an implicit binding attribute.
auto *Attr =
HLSLResourceBindingAttr::CreateImplicit(S.getASTContext(), "", "0", {});
Attr->setBinding(getRegisterType(ResHandleTy), std::nullopt, 0);
Attr->setImplicitBindingOrderID(S.HLSL().getNextImplicitBindingOrderID());
ResDecl->addAttr(Attr);
ResDecl->setImplicit();
if (Range == 1)
S.HLSL().initGlobalResourceDecl(ResDecl);
else
S.HLSL().initGlobalResourceArrayDecl(ResDecl);
ParentVD->addAttr(
HLSLAssociatedResourceDeclAttr::CreateImplicit(AST, ResDecl));
DC->addDecl(ResDecl);
DeclGroupRef DG(ResDecl);
S.Consumer.HandleTopLevelDecl(DG);
}
static void
handleArrayOfStructWithResources(Sema &S, VarDecl *ParentVD,
const ConstantArrayType *CAT,
EmbeddedResourceNameBuilder &NameBuilder);
// Scans base and all fields of a struct/class type to find all embedded
// resources or resource arrays. Creates a global variable for each resource
// found.
static void
handleStructWithResources(Sema &S, VarDecl *ParentVD, const CXXRecordDecl *RD,
EmbeddedResourceNameBuilder &NameBuilder) {
// Scan the base classes.
assert(RD->getNumBases() <= 1 && "HLSL doesn't support multiple inheritance");
const auto *BasesIt = RD->bases_begin();
if (BasesIt != RD->bases_end()) {
QualType QT = BasesIt->getType();
if (QT->isHLSLIntangibleType()) {
CXXRecordDecl *BaseRD = QT->getAsCXXRecordDecl();
NameBuilder.pushBaseName(BaseRD->getName());
handleStructWithResources(S, ParentVD, BaseRD, NameBuilder);
NameBuilder.pop();
}
}
// Process this class fields.
for (const FieldDecl *FD : RD->fields()) {
QualType FDTy = FD->getType().getCanonicalType();
if (!FDTy->isHLSLIntangibleType())
continue;
NameBuilder.pushName(FD->getName());
if (isResourceRecordTypeOrArrayOf(FDTy)) {
IdentifierInfo *II = NameBuilder.getNameAsIdentifier(S.getASTContext());
createGlobalResourceDeclForStruct(S, ParentVD, FD->getLocation(), II,
FDTy);
} else if (const auto *RD = FDTy->getAsCXXRecordDecl()) {
handleStructWithResources(S, ParentVD, RD, NameBuilder);
} else if (const auto *ArrayTy = dyn_cast<ConstantArrayType>(FDTy)) {
assert(!FDTy->isHLSLResourceRecordArray() &&
"resource arrays should have been already handled");
handleArrayOfStructWithResources(S, ParentVD, ArrayTy, NameBuilder);
}
NameBuilder.pop();
}
}
// Processes array of structs with resources.
static void
handleArrayOfStructWithResources(Sema &S, VarDecl *ParentVD,
const ConstantArrayType *CAT,
EmbeddedResourceNameBuilder &NameBuilder) {
QualType ElementTy = CAT->getElementType().getCanonicalType();
assert(ElementTy->isHLSLIntangibleType() && "Expected HLSL intangible type");
const ConstantArrayType *SubCAT = dyn_cast<ConstantArrayType>(ElementTy);
const CXXRecordDecl *ElementRD = ElementTy->getAsCXXRecordDecl();
if (!SubCAT && !ElementRD)
return;
for (unsigned I = 0, E = CAT->getSize().getZExtValue(); I < E; ++I) {
NameBuilder.pushArrayIndex(I);
if (ElementRD)
handleStructWithResources(S, ParentVD, ElementRD, NameBuilder);
else
handleArrayOfStructWithResources(S, ParentVD, SubCAT, NameBuilder);
NameBuilder.pop();
}
}
// Scans all fields of a user-defined struct (or array of structs)
// to find all embedded resources or resource arrays. For each resource
// a global variable of the resource type is created and associated
// with the parent declaration (VD) through a HLSLAssociatedResourceDeclAttr
// attribute.
void SemaHLSL::handleGlobalStructOrArrayOfWithResources(VarDecl *VD) {
EmbeddedResourceNameBuilder NameBuilder(VD->getName());
const Type *VDTy = VD->getType().getTypePtr();
assert(VDTy->isHLSLIntangibleType() && !isResourceRecordTypeOrArrayOf(VD) &&
"Expected non-resource struct or array type");
if (const CXXRecordDecl *RD = VDTy->getAsCXXRecordDecl()) {
handleStructWithResources(SemaRef, VD, RD, NameBuilder);
return;
}
if (const auto *CAT = dyn_cast<ConstantArrayType>(VDTy)) {
handleArrayOfStructWithResources(SemaRef, VD, CAT, NameBuilder);
return;
}
}
void SemaHLSL::ActOnVariableDeclarator(VarDecl *VD) {
if (VD->hasGlobalStorage()) {
// make sure the declaration has a complete type
@ -4884,6 +5037,12 @@ void SemaHLSL::ActOnVariableDeclarator(VarDecl *VD) {
}
}
// Process resources in user-defined structs, or arrays of such structs.
const Type *VDTy = VD->getType().getTypePtr();
if (VD->getStorageClass() != SC_Static && VDTy->isHLSLIntangibleType() &&
!isResourceRecordTypeOrArrayOf(VD))
handleGlobalStructOrArrayOfWithResources(VD);
// Mark groupshared variables as extern so they will have
// external storage and won't be default initialized
if (VD->hasAttr<HLSLGroupSharedAddressSpaceAttr>())

View File

@ -0,0 +1,7 @@
// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-compute -verify %s
struct A {
RWBuffer<float> Buf;
};
A incompleteArray[]; // expected-error {{definition of variable with array type needs an explicit size or an initializer}}

View File

@ -0,0 +1,167 @@
// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-compute -ast-dump %s | FileCheck %s
// Single resource field in struct
// CHECK: CXXRecordDecl {{.*}} struct A
// CHECK: FieldDecl {{.*}} Buf 'RWBuffer<float>':'hlsl::RWBuffer<float>'
struct A {
RWBuffer<float> Buf;
};
// CHECK: VarDecl {{.*}} implicit a1.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} a1 'A'
// CHECK: HLSLResourceBindingAttr {{.*}} "u0" "space0"
// CHECK-NEXT: HLSLAssociatedResourceDeclAttr {{.*}} 'a1.Buf' 'hlsl::RWBuffer<float>'
A a1 : register(u0);
// Resource array in struct
// CHECK: CXXRecordDecl {{.*}} struct B
// CHECK: FieldDecl {{.*}} Bufs 'RWBuffer<float>[10]'
struct B {
RWBuffer<float> Bufs[10];
};
// CHECK: VarDecl {{.*}} implicit b1.Bufs 'hlsl::RWBuffer<float>[10]'
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} b1 'B'
// CHECK: HLSLResourceBindingAttr {{.*}} "u2" "space0"
// CHECK-NEXT: HLSLAssociatedResourceDeclAttr {{.*}} 'b1.Bufs' 'hlsl::RWBuffer<float>[10]'
B b1 : register(u2);
// Inheritance
// CHECK: CXXRecordDecl {{.*}} struct C
// CHECK: FieldDecl {{.*}} Buf2 'RWBuffer<float>':'hlsl::RWBuffer<float>'
struct C : A {
RWBuffer<float> Buf2;
};
// CHECK: VarDecl {{.*}} implicit c1.A::Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit c1.Buf2 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} c1 'C'
// CHECK: HLSLResourceBindingAttr {{.*}} "u3" "space0"
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'c1.A::Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'c1.Buf2' 'hlsl::RWBuffer<float>'
C c1 : register(u3);
// Inheritance with same named field
// CHECK: CXXRecordDecl {{.*}} struct D
// CHECK: FieldDecl {{.*}} A 'A'
struct D : A {
A A;
};
// CHECK: VarDecl {{.*}} implicit d1.A::Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit d1.A.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} d1 'D'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'd1.A::Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'd1.A.Buf' 'hlsl::RWBuffer<float>'
D d1;
// Inheritance and Multiple Resources Kinds
// CHECK: CXXRecordDecl {{.*}} class E
// CHECK: FieldDecl {{.*}} SrvBuf 'StructuredBuffer<int>':'hlsl::StructuredBuffer<int>'
class E {
StructuredBuffer<int> SrvBuf;
};
// CHECK: CXXRecordDecl {{.*}} class F
// CHECK: FieldDecl {{.*}} a 'A'
// CHECK: FieldDecl {{.*}} SrvBuf 'StructuredBuffer<float>':'hlsl::StructuredBuffer<float>'
// CHECK: FieldDecl {{.*}} Samp 'SamplerState'
class F : E {
A a;
StructuredBuffer<float> SrvBuf;
SamplerState Samp;
};
// CHECK: VarDecl {{.*}} implicit f.E::SrvBuf 'hlsl::StructuredBuffer<int>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit f.a.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit f.SrvBuf 'hlsl::StructuredBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit f.Samp 'hlsl::SamplerState' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} f 'F'
// CHECK: HLSLResourceBindingAttr {{.*}} "t0" "space0"
// CHECK: HLSLResourceBindingAttr {{.*}} "u20" "space0"
// CHECK: HLSLResourceBindingAttr {{.*}} "s3" "space0"
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'f.E::SrvBuf' 'hlsl::StructuredBuffer<int>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'f.a.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'f.SrvBuf' 'hlsl::StructuredBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'f.Samp' 'hlsl::SamplerState'
F f : register(t0) : register(u20) : register(s3);
// Array of structs with resources
// CHECK: VarDecl {{.*}} implicit arrayOfA.0.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit arrayOfA.1.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
A arrayOfA[2] : register(u0, space1);
// CHECK: CXXRecordDecl {{.*}} struct G
// CHECK: FieldDecl {{.*}} multiArray 'A[2][2]'
struct G {
A multiArray[2][2];
};
// CHECK: VarDecl {{.*}} implicit gArray.0.multiArray.0.0.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.0.multiArray.0.1.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.0.multiArray.1.0.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.0.multiArray.1.1.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.1.multiArray.0.0.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.1.multiArray.0.1.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.1.multiArray.1.0.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} implicit gArray.1.multiArray.1.1.Buf 'hlsl::RWBuffer<float>' callinit
// CHECK: HLSLResourceBindingAttr {{.*}} Implicit "" "0"
// CHECK: VarDecl {{.*}} gArray 'G[2]'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.0.multiArray.0.0.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.0.multiArray.0.1.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.0.multiArray.1.0.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.0.multiArray.1.1.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.1.multiArray.0.0.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.1.multiArray.0.1.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.1.multiArray.1.0.Buf' 'hlsl::RWBuffer<float>'
// CHECK: HLSLAssociatedResourceDeclAttr {{.*}} 'gArray.1.multiArray.1.1.Buf' 'hlsl::RWBuffer<float>'
G gArray[2] : register(u10, space2);
// Static struct with resources
// CHECK-NOT: VarDecl {{.*}} a2.Buf
// CHECK: VarDecl {{.*}} a2 'hlsl_private A' static cinit
static A a2 = { a1 };