Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HLSL] cbuffer: Create host layout structs #122820

Merged
merged 11 commits into from
Jan 24, 2025
Next Next commit
[HLSL] cbuffer: Create host layout struct and add resource handle to AST
Creates layout struct for `cbuffer` in Sema which will contains only declarations
contributing to the constant buffer layout. Anything else will be filtered out,
such as static variables decls, struct and function definitions, resources,
or empty struct and zero-sized arrays.

If the constant buffer includes a struct that contains any of the above undesirable
declarations, a new version of this struct should be created with these declarations
filtered out as well.

The definition of buffer layour struct is added to the HLSLBufferDecl node and is followed
by 'cbuffer` resource handle decl referencing the layout struct as its contained type.

Fixes #122553
  • Loading branch information
hekota committed Jan 13, 2025
commit 71ddb5a2b4cc8a9609410b436e896484401f5e90
1 change: 1 addition & 0 deletions clang/include/clang/AST/Decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -4967,6 +4967,7 @@ class HLSLBufferDecl final : public NamedDecl, public DeclContext {
SourceLocation getRBraceLoc() const { return RBraceLoc; }
void setRBraceLoc(SourceLocation L) { RBraceLoc = L; }
bool isCBuffer() const { return IsCBuffer; }
const Type *getResourceHandleType() const;

// Implement isa/cast/dyncast/etc.
static bool classof(const Decl *D) { return classofKind(D->getKind()); }
Expand Down
13 changes: 13 additions & 0 deletions clang/lib/AST/Decl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5693,6 +5693,19 @@ HLSLBufferDecl *HLSLBufferDecl::CreateDeserialized(ASTContext &C,
SourceLocation(), SourceLocation());
}

const Type *HLSLBufferDecl::getResourceHandleType() const {
// Resource handle is the last decl in the HLSLBufferDecl.
// If it is not present, it probably means the buffer is empty.
if (VarDecl *VD = llvm::dyn_cast_or_null<VarDecl>(LastDecl)) {
const Type *Ty = VD->getType().getTypePtr();
if (Ty->isHLSLAttributedResourceType()) {
assert(VD->getNameAsString() == "__handle");
return Ty;
}
}
return nullptr;
}

//===----------------------------------------------------------------------===//
// ImportDecl Implementation
//===----------------------------------------------------------------------===//
Expand Down
8 changes: 5 additions & 3 deletions clang/lib/CodeGen/CGHLSLRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,12 @@ void CGHLSLRuntime::addConstant(VarDecl *D, Buffer &CB) {
CB.Constants.emplace_back(std::make_pair(GV, LowerBound));
}

void CGHLSLRuntime::addBufferDecls(const DeclContext *DC, Buffer &CB) {
for (Decl *it : DC->decls()) {
void CGHLSLRuntime::addBufferDecls(const HLSLBufferDecl *D, Buffer &CB) {
for (Decl *it : D->decls()) {
if (auto *ConstDecl = dyn_cast<VarDecl>(it)) {
addConstant(ConstDecl, CB);
if (ConstDecl->getType().getTypePtr() != D->getResourceHandleType()) {
addConstant(ConstDecl, CB);
}
} else if (isa<CXXRecordDecl, EmptyDecl>(it)) {
// Nothing to do for this declaration.
} else if (isa<FunctionDecl>(it)) {
Expand Down
2 changes: 1 addition & 1 deletion clang/lib/CodeGen/CGHLSLRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class CGHLSLRuntime {
llvm::hlsl::ElementType ET,
BufferResBinding &Binding);
void addConstant(VarDecl *D, Buffer &CB);
void addBufferDecls(const DeclContext *DC, Buffer &CB);
void addBufferDecls(const HLSLBufferDecl *D, Buffer &CB);
llvm::Triple::ArchType getArch();
llvm::SmallVector<Buffer> Buffers;

Expand Down
245 changes: 245 additions & 0 deletions clang/lib/Sema/SemaHLSL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,26 @@
#include "clang/AST/TypeLoc.h"
#include "clang/Basic/Builtins.h"
#include "clang/Basic/DiagnosticSema.h"
#include "clang/Basic/IdentifierTable.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/Sema/Initialization.h"
#include "clang/Sema/Lookup.h"
#include "clang/Sema/ParsedAttr.h"
#include "clang/Sema/Sema.h"
#include "clang/Sema/Template.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/DXILABI.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/TargetParser/Triple.h"
#include <iterator>
#include <string>
#include <utility>

using namespace clang;
Expand Down Expand Up @@ -253,12 +257,253 @@ static void validatePackoffset(Sema &S, HLSLBufferDecl *BufDecl) {
}
}

// Returns true if the array has a zero size = if any of the dimensions is 0
static bool isZeroSizedArray(const ConstantArrayType *CAT) {
while (CAT && !CAT->isZeroSize())
CAT = dyn_cast<ConstantArrayType>(
CAT->getElementType()->getUnqualifiedDesugaredType());
return CAT != nullptr;
}

// Returns true if the struct can be used inside HLSL Buffer which means
// that it does not contain intangible types, empty structs, zero-sized arrays,
// and the same is true for its base or embedded structs.
bool isStructHLSLBufferCompatible(const CXXRecordDecl *RD) {
if (RD->getTypeForDecl()->isHLSLIntangibleType() ||
(RD->field_empty() && RD->getNumBases() == 0))
return false;
// check fields
for (const FieldDecl *Field : RD->fields()) {
QualType Ty = Field->getType();
if (Ty->isRecordType()) {
if (!isStructHLSLBufferCompatible(Ty->getAsCXXRecordDecl()))
return false;
} else if (Ty->isConstantArrayType()) {
if (isZeroSizedArray(cast<ConstantArrayType>(Ty)))
return false;
}
}
// check bases
for (const CXXBaseSpecifier &Base : RD->bases())
if (!isStructHLSLBufferCompatible(Base.getType()->getAsCXXRecordDecl()))
return false;
return true;
}

static CXXRecordDecl *findRecordDecl(Sema &S, IdentifierInfo *II,
DeclContext *DC) {
DeclarationNameInfo NameInfo =
DeclarationNameInfo(DeclarationName(II), SourceLocation());
LookupResult R(S, NameInfo, Sema::LookupOrdinaryName);
S.LookupName(R, S.getScopeForContext(DC));
if (R.isSingleResult())
return R.getAsSingle<CXXRecordDecl>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be multiple results if the name is defined in multiple accessible scopes (like current and parent scopes)? Or does it only return zero or one result from the specified scope?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only return zero or one result in the specified scope, which for the HLSLBufferDecl is the global scope. I will update the LookupNameKind to LookupTagName though to be more specific that the lookup is for a record/struct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't see cases like this tested:

namespace NS1 {
  struct Foo { ... };
  struct Bar { struct Foo { ... }; ... };
}
struct Foo { ... };
...
cbuffer CB1 {
  Foo foo1;
  NS1::Foo foo2;
  NS1::Bar::Foo foo3;
}

namespace NS2 {
  struct Foo { ... };
  cbuffer CB2 {
    ::Foo foo0;
    Foo foo1;
    NS1::Foo foo2;
    NS1::Bar::Foo foo3;
  }
}

In what decl context will implicit host layout decls be created for each type? Will there be duplicates?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean now. I have added a separate cbuffer AST test with namespaces and changed the implementation to always create the layout struct on the same context as the original struct so that it will be in the same namespace. The lookup is needs to scan just the immediate non-transparent decl context.

return nullptr;
}

// Creates a name for buffer layout struct using the provide name base.
// If the name must be unique (not previously defined), a suffix is added
// until a unique name is found.
static IdentifierInfo *getHostLayoutStructName(Sema &S,
IdentifierInfo *NameBaseII,
bool MustBeUnique,
DeclContext *DC) {
ASTContext &AST = S.getASTContext();
std::string NameBase;
if (NameBaseII) {
NameBase = NameBaseII->getName().str();
} else {
// anonymous struct
NameBase = "anon";
MustBeUnique = true;
}

std::string Name = "__hostlayout.struct." + NameBase;
IdentifierInfo *II = &AST.Idents.get(Name, tok::TokenKind::identifier);
if (!MustBeUnique)
return II;

unsigned suffix = 0;
while (true) {
if (suffix != 0)
II = &AST.Idents.get((llvm::Twine(Name) + "." + Twine(suffix)).str(),
tok::TokenKind::identifier);
if (!findRecordDecl(S, II, DC))
return II;
// declaration with that name already exists - increment suffix and try
// again until unique name is found
suffix++;
};
}

// Returns true if the record type is an HLSL resource class
static bool isResourceRecordType(const Type *Ty) {
return HLSLAttributedResourceType::findHandleTypeOnResource(Ty) != nullptr;
}

static CXXRecordDecl *createHostLayoutStruct(Sema &S, CXXRecordDecl *StructDecl,
HLSLBufferDecl *BufDecl);

// Creates a field declaration of given name and type for HLSL buffer layout
// struct. Returns nullptr if the type cannot be use in HLSL Buffer layout.
static FieldDecl *createFieldForHostLayoutStruct(Sema &S, const Type *Ty,
IdentifierInfo *II,
CXXRecordDecl *LayoutStruct,
HLSLBufferDecl *BufDecl) {
if (Ty->isRecordType()) {
if (isResourceRecordType(Ty))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't we want to catch more intangible types than just resources here? How do we make sure this is kept up-to-date for all intangible types?

On a related note, how do we ensure this code under createHostLayoutStruct always has matching/consistent logic to the code in requiresImplicitBufferLayoutStructure? Could there be a way to share the key logic components perhaps? For instance, there could be functions that return whether a leaf type is intangible (maybe this is just Type::IsHLSLIntangibleType), or is zero-sized, separate from the logic that recursively traverses a record type looking for these properties on field types. The IsHLSLIntangibleType and zero-sized detection functions are then used by both traversal paths to ensure consistent behavior.

Copy link
Contributor

@tex3d tex3d Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, replied in wrong thread. Edited to move.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an assert at the top of the createHostLayoutStruct function that will make sure the logic is consistent:

assert(requiresImplicitBufferLayoutStructure(StructDecl) && "struct is already HLSL buffer compatible");

I have also moved the checking into a shared function isInvalidConstantBufferLeafElementType. Currently the only intangible types are resources classes, but I added a check for builtin resource type as well.

return nullptr;
CXXRecordDecl *RD = Ty->getAsCXXRecordDecl();
if (!isStructHLSLBufferCompatible(RD)) {
RD = createHostLayoutStruct(S, RD, BufDecl);
if (!RD)
return nullptr;
Ty = RD->getTypeForDecl();
}
} else if (Ty->isConstantArrayType()) {
if (isZeroSizedArray(cast<ConstantArrayType>(Ty)))
return nullptr;
}
QualType QT = QualType(Ty, 0);
ASTContext &AST = S.getASTContext();
TypeSourceInfo *TSI = AST.getTrivialTypeSourceInfo(QT, SourceLocation());
auto *Field = FieldDecl::Create(AST, LayoutStruct, SourceLocation(),
SourceLocation(), II, QT, TSI, nullptr, false,
InClassInitStyle::ICIS_NoInit);
Field->setAccess(AccessSpecifier::AS_private);
return Field;
}

// Creates host layout struct for a struct included in HLSL Buffer.
// The layout struct will include only fields that are allowed in HLSL buffer.
// These fields will be filtered out:
// - resource classes
// - empty structs
// - zero-sized arrays
// Returns nullptr if the resulting layout struct would be empty.
static CXXRecordDecl *createHostLayoutStruct(Sema &S, CXXRecordDecl *StructDecl,
HLSLBufferDecl *BufDecl) {
assert(!isStructHLSLBufferCompatible(StructDecl) &&
"struct is already HLSL buffer compatible");

ASTContext &AST = S.getASTContext();
DeclContext *DC = StructDecl->getDeclContext();
IdentifierInfo *II = getHostLayoutStructName(
S, StructDecl->getIdentifier(), false, BufDecl->getDeclContext());

// reuse existing if the layout struct if it already exists
if (CXXRecordDecl *RD = findRecordDecl(S, II, DC))
return RD;

CXXRecordDecl *LS =
CXXRecordDecl::Create(AST, TagDecl::TagKind::Class, BufDecl,
SourceLocation(), SourceLocation(), II);
LS->setImplicit(true);
LS->startDefinition();

// copy base struct, create HLSL Buffer compatible version if needed
if (unsigned NumBases = StructDecl->getNumBases()) {
assert(NumBases == 1 && "HLSL supports only one base type");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HLSL actually supports multiple interface bases (abstract bases), though we don't reliably implement these in DXC.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is accurate to say HLSL supports multiple bases. The interface support in DXC is HLSL 2015-only, we don't do anything sane with it in the language versions that DXC actually supports compiling for.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface support in DXC is HLSL 2015-only

Not entirely true. We don't support use of interface instances (basically like pointers), but we do support interface definitions, inheritance from such, and overriding method implementations.

There is a bug in DXC when calling a method originally defined in an interface that is not inherited first in a multiple-inheritance scenario due to the way the MicrosoftCXXABI generates the code to access this for a method which we inherited by defaulting to that ABI in DXC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so there can be multiple interfaces, but only one base struct, correct? And interfaces have only methods and no data members, so they can be safely ignored for the layout struct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And Clang does not support interfaces yet.

Copy link
Member Author

@hekota hekota Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have filed an issue and will add a FIXME to the code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we might not be supporting interfaces in Clang, so I'll remove the FIXME.

CXXBaseSpecifier Base = *StructDecl->bases_begin();
CXXRecordDecl *BaseDecl = Base.getType()->getAsCXXRecordDecl();
if (!isStructHLSLBufferCompatible(BaseDecl)) {
BaseDecl = createHostLayoutStruct(S, BaseDecl, BufDecl);
if (BaseDecl) {
TypeSourceInfo *TSI = AST.getTrivialTypeSourceInfo(
QualType(BaseDecl->getTypeForDecl(), 0));
Base = CXXBaseSpecifier(SourceRange(), false, StructDecl->isClass(),
AS_none, TSI, SourceLocation());
}
}
if (BaseDecl) {
const CXXBaseSpecifier *BasesArray[1] = {&Base};
LS->setBases(BasesArray, 1);
}
}

// filter struct fields
for (const FieldDecl *FD : StructDecl->fields()) {
const Type *Ty = FD->getType()->getUnqualifiedDesugaredType();
if (FieldDecl *NewFD = createFieldForHostLayoutStruct(
S, Ty, FD->getIdentifier(), LS, BufDecl))
LS->addDecl(NewFD);
}
LS->completeDefinition();

if (LS->field_empty() && LS->getNumBases() == 0)
return nullptr;
BufDecl->addDecl(LS);
return LS;
}

// Creates host layout struct for HLSL Buffer. The struct will include only
// fields of types that are allowed in HLSL buffer and it will filter out:
// - static variable declarations
// - resource classes
// - empty structs
// - zero-sized arrays
// - non-variable declarations
static CXXRecordDecl *createHostLayoutStructForBuffer(Sema &S,
HLSLBufferDecl *BufDecl) {
ASTContext &AST = S.getASTContext();
IdentifierInfo *II = getHostLayoutStructName(S, BufDecl->getIdentifier(),
true, BufDecl->getDeclContext());

CXXRecordDecl *LS =
CXXRecordDecl::Create(AST, TagDecl::TagKind::Class, BufDecl,
SourceLocation(), SourceLocation(), II);
LS->setImplicit(true);
LS->startDefinition();

for (const Decl *D : BufDecl->decls()) {
const VarDecl *VD = dyn_cast<VarDecl>(D);
if (!VD || VD->getStorageClass() == SC_Static)
continue;
const Type *Ty = VD->getType()->getUnqualifiedDesugaredType();
if (FieldDecl *FD = createFieldForHostLayoutStruct(
S, Ty, VD->getIdentifier(), LS, BufDecl))
LS->addDecl(FD);
}
LS->completeDefinition();
BufDecl->addDecl(LS);
return LS;
}

// Creates a "__handle" declaration for the HLSL Buffer type
// with the corresponding HLSL resource type and adds it to the HLSLBufferDecl
static void createHLSLBufferHandle(Sema &S, HLSLBufferDecl *BufDecl,
CXXRecordDecl *LayoutStruct) {
ASTContext &AST = S.getASTContext();

HLSLAttributedResourceType::Attributes ResAttrs(
BufDecl->isCBuffer() ? ResourceClass::CBuffer : ResourceClass::SRV, false,
false);
QualType ResHandleTy = AST.getHLSLAttributedResourceType(
AST.HLSLResourceTy, QualType(LayoutStruct->getTypeForDecl(), 0),
ResAttrs);

IdentifierInfo *II = &AST.Idents.get("__handle", tok::TokenKind::identifier);
VarDecl *VD = VarDecl::Create(
BufDecl->getASTContext(), BufDecl, SourceLocation(), SourceLocation(), II,
ResHandleTy, AST.getTrivialTypeSourceInfo(ResHandleTy, SourceLocation()),
SC_None);
BufDecl->addDecl(VD);
}

// Handle end of cbuffer/tbuffer declaration
void SemaHLSL::ActOnFinishBuffer(Decl *Dcl, SourceLocation RBrace) {
auto *BufDecl = cast<HLSLBufferDecl>(Dcl);
BufDecl->setRBraceLoc(RBrace);

validatePackoffset(SemaRef, BufDecl);

// create buffer layout struct
CXXRecordDecl *LayoutStruct =
createHostLayoutStructForBuffer(SemaRef, BufDecl);

// create buffer resource handle
createHLSLBufferHandle(SemaRef, BufDecl, LayoutStruct);

SemaRef.PopDeclContext();
}

Expand Down
15 changes: 13 additions & 2 deletions clang/test/AST/HLSL/ast-dump-comment-cbuffe-tbufferr.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,23 @@ tbuffer B {
// AST-NEXT:`-ParagraphComment {{.*}}<col:4, col:17>
// AST-NEXT:`-TextComment {{.*}}<col:4, col:17> Text=" CBuffer decl."
// AST-NEXT:-VarDecl {{.*}}<line:15:5, col:11> col:11 a 'float'
// AST-NEXT:`-VarDecl {{.*}}<line:19:5, col:9> col:9 b 'int'
// AST-NEXT:-VarDecl {{.*}}<line:19:5, col:9> col:9 b 'int'
// AST-NEXT:CXXRecordDecl 0x[[CB:[0-9a-f]+]] {{.*}} implicit class __hostlayout.struct.A definition
// AST:FieldDecl 0x[[CB:[0-9a-f]+]] {{.*}} a 'float'
// AST-NEXT:FieldDecl 0x[[CB:[0-9a-f]+]] {{.*}} b 'int'
// AST-NEXT:VarDecl 0x[[CB:[0-9a-f]+]] {{.*}} __handle '__hlsl_resource_t
// AST-SAME{LITERAL}: [[hlsl::resource_class(CBuffer)]] [[hlsl::contained_type(__hostlayout.struct.A)]]'

// AST-NEXT:HLSLBufferDecl {{.*}}<line:29:1, line:38:1> line:29:9 tbuffer B
// AST-NEXT:-HLSLResourceClassAttr {{.*}} <<invalid sloc>> Implicit SRV
// AST-NEXT:-HLSLResourceAttr {{.*}} <<invalid sloc>> Implicit TBuffer
// AST-NEXT:-FullComment {{.*}}<line:28:4, col:17>
// AST-NEXT: `-ParagraphComment {{.*}}<col:4, col:17>
// AST-NEXT: `-TextComment {{.*}}<col:4, col:17> Text=" TBuffer decl."
// AST-NEXT:-VarDecl {{.*}}<line:33:5, col:11> col:11 c 'float'
// AST-NEXT:`-VarDecl {{.*}} <line:37:5, col:9> col:9 d 'int'
// AST-NEXT:-VarDecl {{.*}} <line:37:5, col:9> col:9 d 'int'
// AST-NEXT:CXXRecordDecl 0x[[CB:[0-9a-f]+]] {{.*}} implicit class __hostlayout.struct.B definition
// AST:FieldDecl 0x[[CB:[0-9a-f]+]] {{.*}} c 'float'
// AST-NEXT:FieldDecl 0x[[CB:[0-9a-f]+]] {{.*}} d 'int'
// AST-NEXT:VarDecl 0x[[CB:[0-9a-f]+]] {{.*}} __handle '__hlsl_resource_t
// AST-SAME{LITERAL}: [[hlsl::resource_class(SRV)]] [[hlsl::contained_type(__hostlayout.struct.B)]]'
Loading
Loading