blob: 0ccb21fb2b97e1a17f3e5a636b121dbffbdfab2a [file]
//===------ SemaDXR.cpp - Semantic Analysis for DXR shader -----*- C++ -*-===//
///////////////////////////////////////////////////////////////////////////////
// //
// SemaDXR.cpp //
// Copyright (C) Nvidia Corporation. All rights reserved. //
// This file is distributed under the University of Illinois Open Source //
// License. See LICENSE.TXT for details. //
// //
// This file defines the semantic support for DXR. //
// //
///////////////////////////////////////////////////////////////////////////////
#include "clang/AST/ASTContext.h"
#include "clang/AST/Attr.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/ExternalASTSource.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Sema/SemaHLSL.h"
#include "clang/Analysis/Analyses/Dominators.h"
#include "clang/Analysis/Analyses/ReachableCode.h"
#include "clang/Analysis/CFG.h"
#include "llvm/ADT/BitVector.h"
#include "dxc/DXIL/DxilConstants.h"
#include "dxc/DXIL/DxilShaderModel.h"
#include "dxc/HlslIntrinsicOp.h"
using namespace clang;
using namespace sema;
using namespace hlsl;
namespace {
struct PayloadUse {
PayloadUse() = default;
PayloadUse(const Stmt *S, const CFGBlock *Parent)
: S(S), Parent(Parent), Member(nullptr) {}
PayloadUse(const Stmt *S, const CFGBlock *Parent, const MemberExpr *Member)
: S(S), Parent(Parent), Member(Member) {}
bool operator<(const PayloadUse &Other) const { return S < Other.S; }
const Stmt *S = nullptr;
const CFGBlock *Parent = nullptr;
const MemberExpr *Member = nullptr;
};
struct PayloadBuiltinCall {
PayloadBuiltinCall() = default;
PayloadBuiltinCall(const CallExpr *Call, const CFGBlock *Parent)
: Call(Call), Parent(Parent) {}
const CallExpr *Call = nullptr;
const CFGBlock *Parent = nullptr;
};
struct PayloadAccessInfo {
PayloadAccessInfo() = default;
PayloadAccessInfo(const MemberExpr *Member, const CallExpr *Call,
bool IsLValue)
: Member(Member), Call(Call), IsLValue(IsLValue) {}
const MemberExpr *Member = nullptr;
const CallExpr *Call = nullptr;
bool IsLValue = false;
};
struct DxrShaderDiagnoseInfo {
const FunctionDecl *funcDecl;
const VarDecl *Payload;
DXIL::PayloadAccessShaderStage Stage;
std::vector<PayloadBuiltinCall> PayloadBuiltinCalls;
std::map<const FieldDecl *, std::vector<PayloadUse>> WritesPerField;
std::map<const FieldDecl *, std::vector<PayloadUse>> ReadsPerField;
std::vector<PayloadUse> PayloadAsCallArg;
};
std::vector<const FieldDecl *>
DiagnosePayloadAccess(Sema &S, DxrShaderDiagnoseInfo &Info,
const std::set<const FieldDecl *> &FieldsToIgnoreRead,
const std::set<const FieldDecl *> &FieldsToIgnoreWrite,
std::set<const FunctionDecl *> VisitedFunctions);
const Stmt *IgnoreParensAndDecay(const Stmt *S);
// Transform the shader stage to string to be used in diagnostics
StringRef GetStringForShaderStage(DXIL::PayloadAccessShaderStage Stage) {
StringRef StageNames[] = {"caller", "closesthit", "miss", "anyhit"};
if (Stage != DXIL::PayloadAccessShaderStage::Invalid)
return StageNames[static_cast<unsigned>(Stage)];
return "";
}
// Returns the Qualifier for a Field and a given shader stage.
DXIL::PayloadAccessQualifier
GetPayloadQualifierForStage(FieldDecl *Field,
DXIL::PayloadAccessShaderStage Stage) {
bool hasRead = false;
bool hasWrite = false;
for (UnusualAnnotation *annotation : Field->getUnusualAnnotations()) {
if (auto *payloadAnnotation =
dyn_cast<hlsl::PayloadAccessAnnotation>(annotation)) {
for (auto &ShaderStage : payloadAnnotation->ShaderStages) {
if (ShaderStage != Stage)
continue;
hasRead |=
payloadAnnotation->qualifier == DXIL::PayloadAccessQualifier::Read;
hasWrite |=
payloadAnnotation->qualifier == DXIL::PayloadAccessQualifier::Write;
}
}
}
if (hasRead && hasWrite)
return DXIL::PayloadAccessQualifier::ReadWrite;
if (hasRead)
return DXIL::PayloadAccessQualifier::Read;
if (hasWrite)
return DXIL::PayloadAccessQualifier::Write;
return DXIL::PayloadAccessQualifier::NoAccess;
}
static int GetPayloadParamIdxForIntrinsic(const FunctionDecl *FD) {
HLSLIntrinsicAttr *IntrinAttr = FD->getAttr<HLSLIntrinsicAttr>();
if (!IntrinAttr)
return -1;
switch ((IntrinsicOp)IntrinAttr->getOpcode()) {
default:
return -1;
case IntrinsicOp::IOP_TraceRay:
case IntrinsicOp::MOP_DxHitObject_TraceRay:
case IntrinsicOp::MOP_DxHitObject_Invoke:
return FD->getNumParams() - 1;
}
}
static bool IsBuiltinWithPayload(const FunctionDecl *FD) {
return GetPayloadParamIdxForIntrinsic(FD) >= 0;
}
// Returns the declaration of the payload used in a call to TraceRay,
// HitObject::TraceRay or HitObject::Invoke.
const VarDecl *GetPayloadParameterForBuiltinCall(const CallExpr *Call) {
const Decl *Callee = Call->getCalleeDecl();
if (!Callee)
return nullptr;
if (!isa<FunctionDecl>(Callee))
return nullptr;
int PldParamIdx = GetPayloadParamIdxForIntrinsic(cast<FunctionDecl>(Callee));
if (PldParamIdx < 0)
return nullptr;
const Stmt *Param = IgnoreParensAndDecay(Call->getArg(PldParamIdx));
if (const DeclRefExpr *ParamRef = dyn_cast<DeclRefExpr>(Param))
if (const VarDecl *Decl = dyn_cast<VarDecl>(ParamRef->getDecl()))
return Decl;
return nullptr;
}
// Recursively extracts accesses to a payload struct from a Stmt
void GetPayloadAccesses(const Stmt *S, const DxrShaderDiagnoseInfo &Info,
std::vector<PayloadAccessInfo> &Accesses, bool IsLValue,
const MemberExpr *Member, const CallExpr *Call) {
for (auto C : S->children()) {
if (!C)
continue;
if (const DeclRefExpr *Ref = dyn_cast<DeclRefExpr>(C)) {
if (Ref->getDecl() == Info.Payload) {
Accesses.push_back(PayloadAccessInfo{Member, Call, IsLValue});
}
}
if (const ImplicitCastExpr *Cast = dyn_cast<ImplicitCastExpr>(C)) {
if (Cast->getCastKind() == CK_LValueToRValue) {
IsLValue = false;
}
}
GetPayloadAccesses(C, Info, Accesses, IsLValue,
Member ? Member : dyn_cast<MemberExpr>(C),
Call ? Call : dyn_cast<CallExpr>(C));
}
}
// Collects all reads, writes and calls with participation of the payload.
void CollectReadsWritesAndCallsForPayload(const Stmt *S,
DxrShaderDiagnoseInfo &Info,
const CFGBlock *Block) {
std::vector<PayloadAccessInfo> PayloadAccesses;
GetPayloadAccesses(S, Info, PayloadAccesses, true, dyn_cast<MemberExpr>(S),
dyn_cast<CallExpr>(S));
for (auto &Access : PayloadAccesses) {
// An access to a payload member was found.
if (Access.Member) {
FieldDecl *Field = cast<FieldDecl>(Access.Member->getMemberDecl());
if (Access.IsLValue) {
Info.WritesPerField[Field].push_back(
PayloadUse{S, Block, Access.Member});
} else {
Info.ReadsPerField[Field].push_back(
PayloadUse{S, Block, Access.Member});
}
} else if (Access.Call) {
Info.PayloadAsCallArg.push_back(PayloadUse{S, Block});
}
}
}
// Collects all calls to TraceRay, HitObject::TraceRay and HitObject::Invoke.
void CollectBuiltinCallsWithPayload(const Stmt *S, DxrShaderDiagnoseInfo &Info,
const CFGBlock *Block) {
if (const CallExpr *Call = dyn_cast<CallExpr>(S)) {
const Decl *Callee = Call->getCalleeDecl();
if (!Callee || !isa<FunctionDecl>(Callee))
return;
const FunctionDecl *CalledFunction = cast<FunctionDecl>(Callee);
if (IsBuiltinWithPayload(CalledFunction))
Info.PayloadBuiltinCalls.push_back({Call, Block});
}
}
// Find the last write to the payload field in the given block.
PayloadUse GetLastWriteInBlock(CFGBlock &Block,
ArrayRef<PayloadUse> PayloadWrites) {
PayloadUse LastWrite;
for (auto &Element : Block) { // TODO: reverse iterate?
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
auto It = std::find_if(
PayloadWrites.begin(), PayloadWrites.end(),
[&](const PayloadUse &V) { return V.S == S->getStmt(); });
if (It != std::end(PayloadWrites)) {
LastWrite = *It;
LastWrite.Parent = &Block;
}
}
}
return LastWrite;
}
// Travers the CFG until every path has reached a write or the ENTRY.
void TraverseCFGUntilWrite(CFGBlock &Current, std::vector<PayloadUse> &Writes,
ArrayRef<PayloadUse> PayloadWrites,
std::set<const CFGBlock *> &Visited) {
if (Visited.count(&Current))
return;
Visited.insert(&Current);
for (auto I = Current.pred_begin(), E = Current.pred_end(); I != E; ++I) {
CFGBlock *Pred = *I;
if (!Pred)
continue;
PayloadUse WriteInPred = GetLastWriteInBlock(*Pred, PayloadWrites);
if (!WriteInPred.S)
TraverseCFGUntilWrite(*Pred, Writes, PayloadWrites, Visited);
else
Writes.push_back(WriteInPred);
}
}
// Traverse the CFG from the EXIT backwards and stop as soon as a block has a
// write to the payload field.
std::vector<PayloadUse>
GetAllWritesReachingExit(CFG &ShaderCFG, ArrayRef<PayloadUse> PayloadWrites) {
std::vector<PayloadUse> Writes;
CFGBlock &Exit = ShaderCFG.getExit();
std::set<const CFGBlock *> Visited;
TraverseCFGUntilWrite(Exit, Writes, PayloadWrites, Visited);
return Writes;
}
// Find the first read to the payload field in the given block.
PayloadUse GetFirstReadInBlock(CFGBlock &Block,
ArrayRef<PayloadUse> PayloadReads) {
PayloadUse FirstRead;
for (auto &Element : Block) {
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
auto It = std::find_if(
PayloadReads.begin(), PayloadReads.end(),
[&](const PayloadUse &V) { return V.S == S->getStmt(); });
if (It != std::end(PayloadReads)) {
FirstRead = *It;
FirstRead.Parent = &Block;
break; // We found the first read and are done with this block.
}
}
}
return FirstRead;
}
// Travers the CFG until every path has reached a read or the EXIT.
void TraverseCFGUntilRead(CFGBlock &Current, std::vector<PayloadUse> &Reads,
ArrayRef<PayloadUse> PayloadWrites,
std::set<const CFGBlock *> &Visited) {
if (Visited.count(&Current))
return;
Visited.insert(&Current);
for (auto I = Current.succ_begin(), E = Current.succ_end(); I != E; ++I) {
CFGBlock *Succ = *I;
if (!Succ)
continue;
PayloadUse ReadInSucc = GetFirstReadInBlock(*Succ, PayloadWrites);
if (!ReadInSucc.S)
TraverseCFGUntilRead(*Succ, Reads, PayloadWrites, Visited);
else
Reads.push_back(ReadInSucc);
}
}
// Traverse the CFG from the ENTRY down and stop as soon as a block has a read
// to the payload field.
std::vector<PayloadUse>
GetAllReadsReachedFromEntry(CFG &ShaderCFG, ArrayRef<PayloadUse> PayloadReads) {
std::vector<PayloadUse> Reads;
CFGBlock &Entry = ShaderCFG.getEntry();
std::set<const CFGBlock *> Visited;
TraverseCFGUntilRead(Entry, Reads, PayloadReads, Visited);
return Reads;
}
// Returns the record type of a payload declaration.
CXXRecordDecl *GetPayloadType(const VarDecl *Payload) {
auto PayloadType = Payload->getType();
if (PayloadType->isStructureOrClassType()) {
return PayloadType->getAsCXXRecordDecl();
}
return nullptr;
}
std::vector<FieldDecl *> GetAllPayloadFields(RecordDecl *PayloadType) {
std::vector<FieldDecl *> PayloadFields;
for (FieldDecl *Field : PayloadType->fields()) {
QualType FieldType = Field->getType();
if (RecordDecl *FieldRecordDecl = FieldType->getAsCXXRecordDecl()) {
// Skip nested payload types.
if (FieldRecordDecl->hasAttr<HLSLRayPayloadAttr>()) {
auto SubTypeFields = GetAllPayloadFields(FieldRecordDecl);
PayloadFields.insert(PayloadFields.end(), SubTypeFields.begin(),
SubTypeFields.end());
continue;
}
}
PayloadFields.push_back(Field);
}
return PayloadFields;
}
// Returns true if the field is writeable in an earlier shader stage.
bool IsFieldWriteableInEarlierStage(FieldDecl *Field,
DXIL::PayloadAccessShaderStage ThisStage) {
bool isWriteableInEarlierStage = false;
switch (ThisStage) {
case DXIL::PayloadAccessShaderStage::Anyhit:
case DXIL::PayloadAccessShaderStage::Closesthit:
case DXIL::PayloadAccessShaderStage::Miss: {
auto Qualifier = GetPayloadQualifierForStage(
Field, DXIL::PayloadAccessShaderStage::Caller);
isWriteableInEarlierStage =
Qualifier == DXIL::PayloadAccessQualifier::Write ||
Qualifier == DXIL::PayloadAccessQualifier::ReadWrite;
Qualifier = GetPayloadQualifierForStage(
Field, DXIL::PayloadAccessShaderStage::Anyhit);
isWriteableInEarlierStage |=
Qualifier == DXIL::PayloadAccessQualifier::Write ||
Qualifier == DXIL::PayloadAccessQualifier::ReadWrite;
} break;
default:
break;
}
return isWriteableInEarlierStage;
}
// Emit warnings on payload writes.
void DiagnosePayloadWrites(Sema &S, CFG &ShaderCFG, DominatorTree &DT,
const DxrShaderDiagnoseInfo &Info,
ArrayRef<FieldDecl *> NonWriteableFields,
RecordDecl *PayloadType) {
for (FieldDecl *Field : NonWriteableFields) {
auto WritesToField = Info.WritesPerField.find(Field);
if (WritesToField == Info.WritesPerField.end())
continue;
const auto &WritesToDiagnose =
GetAllWritesReachingExit(ShaderCFG, WritesToField->second);
for (auto &Write : WritesToDiagnose) {
FieldDecl *MemField = cast<FieldDecl>(Write.Member->getMemberDecl());
auto Qualifier = GetPayloadQualifierForStage(MemField, Info.Stage);
if (Qualifier != DXIL::PayloadAccessQualifier::Write &&
Qualifier != DXIL::PayloadAccessQualifier::ReadWrite) {
S.Diag(Write.Member->getExprLoc(),
diag::warn_hlsl_payload_access_write_loss)
<< Field->getName() << GetStringForShaderStage(Info.Stage);
}
}
}
// Check if a field is not unconditionally written and a write form an earlier
// stage will be lost.
auto PayloadFields = GetAllPayloadFields(PayloadType);
for (FieldDecl *Field : PayloadFields) {
auto Qualifier = GetPayloadQualifierForStage(Field, Info.Stage);
if (IsFieldWriteableInEarlierStage(Field, Info.Stage) &&
Qualifier == DXIL::PayloadAccessQualifier::Write) {
// The field is writeable in an earlier stage and pure write in this
// stage. Check if we find a write that dominates the exit of the
// function.
bool fieldHasDominatingWrite = false;
auto It = Info.WritesPerField.find(Field);
if (It != Info.WritesPerField.end()) {
for (auto &Write : It->second) {
fieldHasDominatingWrite =
DT.dominates(Write.Parent, &ShaderCFG.getExit());
if (fieldHasDominatingWrite)
break;
}
}
if (!fieldHasDominatingWrite) {
S.Diag(Info.Payload->getLocation(),
diag::warn_hlsl_payload_access_data_loss)
<< Field->getName() << GetStringForShaderStage(Info.Stage);
}
}
}
}
// Returns true if A is earlier than B in Parent
bool IsEarlierStatementAs(const Stmt *A, const Stmt *B,
const CFGBlock &Parent) {
for (auto Element : Parent) {
if (auto S = Element.getAs<CFGStmt>()) {
if (S->getStmt() == A)
return true;
if (S->getStmt() == B)
return false;
}
}
return true;
}
// Returns true if the write dominates payload use.
template <typename T>
bool WriteDominatesUse(const PayloadUse &Write, const T &Use,
DominatorTree &DT) {
if (Use.Parent == Write.Parent) {
// Use and write are in the same Block.
return IsEarlierStatementAs(Write.S, Use.S, *Use.Parent);
}
return DT.dominates(Write.Parent, Use.Parent);
}
// Emit warnings for payload reads.
void DiagnosePayloadReads(Sema &S, CFG &ShaderCFG, DominatorTree &DT,
const DxrShaderDiagnoseInfo &Info,
ArrayRef<FieldDecl *> NonReadableFields) {
for (FieldDecl *Field : NonReadableFields) {
auto ReadsFromField = Info.ReadsPerField.find(Field);
if (ReadsFromField == Info.ReadsPerField.end())
continue;
auto WritesToField = Info.WritesPerField.find(Field);
bool FieldHasWrites = WritesToField != Info.WritesPerField.end();
const auto &ReadsToDiagnose =
GetAllReadsReachedFromEntry(ShaderCFG, ReadsFromField->second);
for (auto &Read : ReadsToDiagnose) {
bool ReadIsDominatedByWrite = false;
if (FieldHasWrites) {
// We found a read to a field that needs diagnose.
// We do not want to warn about fields that read but are dominated by a
// write. Find writes that dominate the read. If we found one, ignore
// the read.
for (auto Write : WritesToField->second) {
ReadIsDominatedByWrite = WriteDominatesUse(Write, Read, DT);
if (ReadIsDominatedByWrite)
break;
}
}
if (ReadIsDominatedByWrite)
continue;
FieldDecl *MemField = cast<FieldDecl>(Read.Member->getMemberDecl());
auto Qualifier = GetPayloadQualifierForStage(MemField, Info.Stage);
if (Qualifier != DXIL::PayloadAccessQualifier::Read &&
Qualifier != DXIL::PayloadAccessQualifier::ReadWrite) {
S.Diag(Read.Member->getExprLoc(),
diag::warn_hlsl_payload_access_undef_read)
<< Field->getName() << GetStringForShaderStage(Info.Stage);
}
}
}
}
// Generic CFG traversal that performs PerElementAction on every Stmt in the
// CFG.
template <bool Backward, typename Action>
void TraverseCFG(const CFGBlock &Block, Action PerElementAction,
std::set<const CFGBlock *> &Visited) {
if (Visited.count(&Block))
return;
Visited.insert(&Block);
for (const auto &Element : Block) {
PerElementAction(Block, Element);
}
if (!Backward) {
for (auto I = Block.succ_begin(), E = Block.succ_end(); I != E; ++I) {
CFGBlock *Succ = *I;
if (!Succ)
continue;
TraverseCFG</*Backward=*/false>(*Succ, PerElementAction, Visited);
}
} else {
for (auto I = Block.pred_begin(), E = Block.pred_end(); I != E; ++I) {
CFGBlock *Pred = *I;
if (!Pred)
continue;
TraverseCFG<Backward>(*Pred, PerElementAction, Visited);
}
}
}
// Forward traverse the CFG and collect calls to TraceRay, HitObject::TraceRay
// and HitObject::Invoke.
void ForwardTraverseCFGAndCollectBuiltinCallsWithPayload(
const CFGBlock &Block, DxrShaderDiagnoseInfo &Info,
std::set<const CFGBlock *> &Visited) {
auto Action = [&Info](const CFGBlock &Block, const CFGElement &Element) {
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
CollectBuiltinCallsWithPayload(S->getStmt(), Info, &Block);
}
};
TraverseCFG<false>(Block, Action, Visited);
}
// Foward traverse the CFG and collect all reads and writes to the payload.
void ForwardTraverseCFGAndCollectReadsWrites(
const CFGBlock &StartBlock, DxrShaderDiagnoseInfo &Info,
std::set<const CFGBlock *> &Visited) {
auto Action = [&Info](const CFGBlock &Block, const CFGElement &Element) {
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
CollectReadsWritesAndCallsForPayload(S->getStmt(), Info, &Block);
}
};
TraverseCFG<false>(StartBlock, Action, Visited);
}
// Backward traverse the CFG and collect all reads and writes to the payload.
void BackwardTraverseCFGAndCollectReadsWrites(
const CFGBlock &StartBlock, DxrShaderDiagnoseInfo &Info,
std::set<const CFGBlock *> &Visited) {
auto Action = [&](const CFGBlock &Block, const CFGElement &Element) {
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
CollectReadsWritesAndCallsForPayload(S->getStmt(), Info, &Block);
}
};
TraverseCFG<true>(StartBlock, Action, Visited);
}
// Returns true if the Stmt uses the Payload.
bool IsPayloadArg(const Stmt *S, const Decl *Payload) {
if (const DeclRefExpr *Ref = dyn_cast<DeclRefExpr>(S)) {
const Decl *Decl = Ref->getDecl();
if (Decl == Payload)
return true;
}
for (auto C : S->children()) {
if (IsPayloadArg(C, Payload))
return true;
}
return false;
}
bool DiagnoseCallExprForExternal(Sema &S, const FunctionDecl *FD,
const CallExpr *CE,
const ParmVarDecl *Payload);
// Collects all writes that dominate a PayloadUse in a CallExpr
// and returns a set of the Fields accessed.
std::set<const FieldDecl *>
CollectDominatingWritesForCall(PayloadUse &Use, DxrShaderDiagnoseInfo &Info,
DominatorTree &DT) {
std::set<const FieldDecl *> FieldsToIgnore;
for (auto P : Info.WritesPerField) {
for (auto Write : P.second) {
bool WriteDominatesCallSite = WriteDominatesUse(Write, Use, DT);
if (WriteDominatesCallSite) {
FieldsToIgnore.insert(P.first);
break;
}
}
}
return FieldsToIgnore;
}
// Collects all reads that are reachable from a PayloadUse in a CallExpr
// and returns a set of the Fields accessed.
std::set<const FieldDecl *>
CollectReachableWritesForCall(PayloadUse &Use,
const DxrShaderDiagnoseInfo &Info) {
std::set<const FieldDecl *> FieldsToIgnore;
assert(Use.Parent);
const CFGBlock *Current = Use.Parent;
// Traverse the CFG beginning from the block of the call and collect all
// fields written to after the call. These fields must not be diagnosed with
// warnings about lost writes.
DxrShaderDiagnoseInfo TempInfo;
TempInfo.Payload = Info.Payload;
bool foundCall = false;
for (auto &Element : *Current) {
// Search for the Call in the block
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
if (S->getStmt() == Use.S) {
foundCall = true;
continue;
}
if (foundCall)
CollectReadsWritesAndCallsForPayload(S->getStmt(), TempInfo, Current);
}
}
for (auto I = Current->succ_begin(); I != Current->succ_end(); ++I) {
CFGBlock *Succ = *I;
if (!Succ)
continue;
std::set<const CFGBlock *> Visited;
ForwardTraverseCFGAndCollectReadsWrites(*Succ, TempInfo, Visited);
}
for (auto &p : TempInfo.WritesPerField)
FieldsToIgnore.insert(p.first);
return FieldsToIgnore;
}
// Emit diagnostics when the payload is used as an argument
// in a function call.
std::map<PayloadUse, std::vector<const FieldDecl *>>
DiagnosePayloadAsFunctionArg(
Sema &S, DxrShaderDiagnoseInfo &Info, DominatorTree &DT,
const std::set<const FieldDecl *> &ParentFieldsToIgnoreRead,
const std::set<const FieldDecl *> &ParentFieldsToIgnoreWrite,
std::set<const FunctionDecl *> VisitedFunctions) {
std::map<PayloadUse, std::vector<const FieldDecl *>> WrittenFieldsInCalls;
for (PayloadUse &Use : Info.PayloadAsCallArg) {
if (const CallExpr *Call = dyn_cast<CallExpr>(Use.S)) {
const Decl *Callee = Call->getCalleeDecl();
if (!Callee || !isa<FunctionDecl>(Callee))
continue;
const FunctionDecl *CalledFunction = cast<FunctionDecl>(Callee);
// Ignore trace calls here.
if (IsBuiltinWithPayload(CalledFunction)) {
Info.PayloadBuiltinCalls.push_back(
PayloadBuiltinCall{Call, Use.Parent});
continue;
}
// Handle external function calls
if (!CalledFunction->hasBody()) {
assert(isa<ParmVarDecl>(Info.Payload));
DiagnoseCallExprForExternal(S, CalledFunction, Call,
cast<ParmVarDecl>(Info.Payload));
continue;
}
if (VisitedFunctions.count(CalledFunction))
return WrittenFieldsInCalls;
VisitedFunctions.insert(CalledFunction);
DxrShaderDiagnoseInfo CalleeInfo;
for (unsigned i = 0; i < Call->getNumArgs(); ++i) {
const Expr *Arg = Call->getArg(i);
if (IsPayloadArg(Arg, Info.Payload)) {
CalleeInfo.Payload = CalledFunction->getParamDecl(i);
break;
}
}
if (CalleeInfo.Payload) {
CalleeInfo.funcDecl = CalledFunction;
CalleeInfo.Stage = Info.Stage;
auto FieldsToIgnoreRead = CollectDominatingWritesForCall(Use, Info, DT);
auto FieldsToIgnoreWrite = CollectReachableWritesForCall(Use, Info);
FieldsToIgnoreRead.insert(ParentFieldsToIgnoreRead.begin(),
ParentFieldsToIgnoreRead.end());
FieldsToIgnoreWrite.insert(ParentFieldsToIgnoreWrite.begin(),
ParentFieldsToIgnoreWrite.end());
WrittenFieldsInCalls[Use] =
DiagnosePayloadAccess(S, CalleeInfo, FieldsToIgnoreRead,
FieldsToIgnoreWrite, VisitedFunctions);
}
}
}
return WrittenFieldsInCalls;
}
// Collect all fields that cannot be accessed for the given shader stage.
// This function recurses into nested payload types.
void CollectNonAccessableFields(
RecordDecl *PayloadType, DXIL::PayloadAccessShaderStage Stage,
const std::set<const FieldDecl *> &FieldsToIgnoreRead,
const std::set<const FieldDecl *> &FieldsToIgnoreWrite,
std::vector<FieldDecl *> &NonWriteableFields,
std::vector<FieldDecl *> &NonReadableFields) {
for (FieldDecl *Field : PayloadType->fields()) {
QualType FieldType = Field->getType();
if (RecordDecl *FieldRecordDecl = FieldType->getAsCXXRecordDecl()) {
if (FieldRecordDecl->hasAttr<HLSLRayPayloadAttr>()) {
CollectNonAccessableFields(FieldRecordDecl, Stage, FieldsToIgnoreRead,
FieldsToIgnoreWrite, NonWriteableFields,
NonReadableFields);
continue;
}
}
auto Qualifier = GetPayloadQualifierForStage(Field, Stage);
// Diagnose writes only if they are not written heigher in the call-graph.
if (!FieldsToIgnoreWrite.count(Field)) {
if (Qualifier != DXIL::PayloadAccessQualifier::Write &&
Qualifier != DXIL::PayloadAccessQualifier::ReadWrite)
NonWriteableFields.push_back(Field);
}
// Diagnose reads only if they have no write heigher in the call-graph.
if (!FieldsToIgnoreRead.count(Field)) {
if (Qualifier != DXIL::PayloadAccessQualifier::Read &&
Qualifier != DXIL::PayloadAccessQualifier::ReadWrite)
NonReadableFields.push_back(Field);
}
}
}
void CollectAccessableFields(RecordDecl *PayloadType,
const std::vector<FieldDecl *> &NonWriteableFields,
const std::vector<FieldDecl *> &NonReadableFields,
std::vector<FieldDecl *> &WriteableFields,
std::vector<FieldDecl *> &ReadableFields) {
for (FieldDecl *Field : PayloadType->fields()) {
QualType FieldType = Field->getType();
if (RecordDecl *FieldRecordDecl = FieldType->getAsCXXRecordDecl()) {
// Skip nested payload types.
if (FieldRecordDecl->hasAttr<HLSLRayPayloadAttr>()) {
CollectAccessableFields(FieldRecordDecl, NonWriteableFields,
NonReadableFields, WriteableFields,
ReadableFields);
continue;
}
}
if (std::find(NonWriteableFields.begin(), NonWriteableFields.end(),
Field) == NonWriteableFields.end())
WriteableFields.push_back(Field);
if (std::find(NonReadableFields.begin(), NonReadableFields.end(), Field) ==
NonReadableFields.end())
ReadableFields.push_back(Field);
}
}
void HandlePayloadInitializer(DxrShaderDiagnoseInfo &Info) {
const VarDecl *Payload = Info.Payload;
const Expr *Init = Payload->getInit();
if (Init) {
// If the payload has an initializer, then handle all fields as
// written. Sema will check that the initializer is correct.
// We can handle all fields as written.
RecordDecl *PayloadType = GetPayloadType(Info.Payload);
for (FieldDecl *Field : PayloadType->fields()) {
Info.WritesPerField[Field].push_back(PayloadUse{Init, nullptr, nullptr});
}
}
}
// Emit diagnostics for this call to either TraceRay, HitObject::TraceRay or
// HitObject::Invoke.
void DiagnoseBuiltinCallWithPayload(Sema &S, const VarDecl *Payload,
const PayloadBuiltinCall &PldCall,
DominatorTree &DT) {
// For each call check if write(caller) fields are written.
const DXIL::PayloadAccessShaderStage CallerStage =
DXIL::PayloadAccessShaderStage::Caller;
std::vector<FieldDecl *> WriteableFields;
std::vector<FieldDecl *> NonWriteableFields;
std::vector<FieldDecl *> ReadableFields;
std::vector<FieldDecl *> NonReadableFields;
RecordDecl *PayloadType = GetPayloadType(Payload);
// Check if the payload type used for this trace call is a payload type
if (!PayloadType->hasAttr<HLSLRayPayloadAttr>()) {
S.Diag(Payload->getLocation(), diag::err_payload_requires_attribute)
<< PayloadType->getName();
return;
}
// Verify that the payload type is legal
if (!hlsl::IsHLSLCopyableAnnotatableRecord(Payload->getType()))
S.Diag(Payload->getLocation(), diag::err_payload_attrs_must_be_udt)
<< /*payload|attributes|callable*/ 0 << /*parameter %2|type*/ 0
<< Payload;
// This will produce more details, but also catch disallowed long vectors
const TypeDiagContext DiagContext = TypeDiagContext::PayloadParameters;
if (DiagnoseTypeElements(S, Payload->getLocation(), Payload->getType(),
DiagContext, DiagContext))
return;
CollectNonAccessableFields(PayloadType, CallerStage, {}, {},
NonWriteableFields, NonReadableFields);
CollectAccessableFields(PayloadType, NonWriteableFields, NonReadableFields,
WriteableFields, ReadableFields);
// Find all writes to Payload that reaches the Trace
DxrShaderDiagnoseInfo TraceInfo;
TraceInfo.Payload = Payload;
// Handle initializers for the payload struct if any is present.
HandlePayloadInitializer(TraceInfo);
std::set<const CFGBlock *> Visited;
const CFGBlock *Parent = PldCall.Parent;
Visited.insert(Parent);
// Collect payload accesses in the same block until we reach the call
for (auto Element : *Parent) {
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
if (S->getStmt() == PldCall.Call)
break;
CollectReadsWritesAndCallsForPayload(S->getStmt(), TraceInfo, Parent);
}
}
for (auto I = Parent->pred_begin(); I != Parent->pred_end(); ++I) {
CFGBlock *Pred = *I;
if (!Pred)
continue;
BackwardTraverseCFGAndCollectReadsWrites(*Pred, TraceInfo, Visited);
}
int PldArgIdx = PldCall.Call->getNumArgs() - 1;
// Warn if a writeable field has not been written.
for (const FieldDecl *Field : WriteableFields) {
if (!TraceInfo.WritesPerField.count(Field)) {
S.Diag(PldCall.Call->getArg(PldArgIdx)->getExprLoc(),
diag::warn_hlsl_payload_access_no_write_for_trace_payload)
<< Field->getName();
}
}
// Warn if a written field is not write(caller)
for (const FieldDecl *Field : NonWriteableFields) {
if (TraceInfo.WritesPerField.count(Field)) {
S.Diag(
PldCall.Call->getArg(PldArgIdx)->getExprLoc(),
diag::warn_hlsl_payload_access_write_but_no_write_for_trace_payload)
<< Field->getName();
}
}
// After a trace call, collect all reads that are not dominated by another
// write warn if a field is not read(caller) but the value is read (undef
// read).
// Discard reads/writes from backward traversal.
TraceInfo.ReadsPerField.clear();
TraceInfo.WritesPerField.clear();
bool CallFound = false;
for (auto Element : *Parent) { // TODO: reverse iterate?
if (Optional<CFGStmt> S = Element.getAs<CFGStmt>()) {
if (S->getStmt() == PldCall.Call) {
CallFound = true;
continue;
}
if (CallFound)
CollectReadsWritesAndCallsForPayload(S->getStmt(), TraceInfo, Parent);
}
}
for (auto I = Parent->succ_begin(); I != Parent->succ_end(); ++I) {
CFGBlock *Pred = *I;
if (!Pred)
continue;
ForwardTraverseCFGAndCollectReadsWrites(*Pred, TraceInfo, Visited);
}
for (const FieldDecl *Field : ReadableFields) {
if (!TraceInfo.ReadsPerField.count(Field)) {
S.Diag(PldCall.Call->getArg(PldArgIdx)->getExprLoc(),
diag::warn_hlsl_payload_access_read_but_no_read_after_trace)
<< Field->getName();
}
}
for (const FieldDecl *Field : NonReadableFields) {
auto WritesToField = TraceInfo.WritesPerField.find(Field);
bool FieldHasWrites = WritesToField != TraceInfo.WritesPerField.end();
for (auto &Read : TraceInfo.ReadsPerField[Field]) {
bool ReadIsDominatedByWrite = false;
if (FieldHasWrites) {
// We found a read to a field that needs diagnose.
// We do not want to warn about fields that read but are dominated by
// a write. Find writes that dominate the read. If we found one,
// ignore the read.
for (auto Write : WritesToField->second) {
ReadIsDominatedByWrite = WriteDominatesUse(Write, Read, DT);
if (ReadIsDominatedByWrite)
break;
}
}
if (ReadIsDominatedByWrite)
continue;
S.Diag(Read.Member->getExprLoc(),
diag::warn_hlsl_payload_access_read_of_undef_after_trace)
<< Field->getName();
}
}
}
// Emit diagnostics for all calls to TraceRay, HitObject::TraceRay or
// HitObject::Invoke.
void DiagnoseBuiltinCallsWithPayload(Sema &S, CFG &ShaderCFG, DominatorTree &DT,
DxrShaderDiagnoseInfo &Info) {
// Collect calls with payload in the shader.
std::set<const CFGBlock *> Visited;
ForwardTraverseCFGAndCollectBuiltinCallsWithPayload(ShaderCFG.getEntry(),
Info, Visited);
std::set<const CallExpr *> Diagnosed;
for (const PayloadBuiltinCall &PldCall : Info.PayloadBuiltinCalls) {
if (Diagnosed.count(PldCall.Call))
continue;
Diagnosed.insert(PldCall.Call);
const VarDecl *Payload = GetPayloadParameterForBuiltinCall(PldCall.Call);
DiagnoseBuiltinCallWithPayload(S, Payload, PldCall, DT);
}
}
// Emit diagnostics for all access to the payload of a shader,
// and the input to TraceRay, HitObject::TraceRay or HitObject::Invoke calls.
std::vector<const FieldDecl *>
DiagnosePayloadAccess(Sema &S, DxrShaderDiagnoseInfo &Info,
const std::set<const FieldDecl *> &FieldsToIgnoreRead,
const std::set<const FieldDecl *> &FieldsToIgnoreWrite,
std::set<const FunctionDecl *> VisitedFunctions) {
clang::DominatorTree DT;
AnalysisDeclContextManager AnalysisManager;
AnalysisDeclContext *AnalysisContext =
AnalysisManager.getContext(Info.funcDecl);
CFG &TheCFG = *AnalysisContext->getCFG();
DT.buildDominatorTree(*AnalysisContext);
// Collect all Fields that gets written to return it back up through the
// recursion.
std::vector<const FieldDecl *> WrittenFields;
// Skip if we are in a RayGeneration shader without payload.
if (Info.Payload) {
std::vector<FieldDecl *> NonWriteableFields;
std::vector<FieldDecl *> NonReadableFields;
RecordDecl *PayloadType = GetPayloadType(Info.Payload);
if (!PayloadType)
return WrittenFields;
CollectNonAccessableFields(PayloadType, Info.Stage, FieldsToIgnoreRead,
FieldsToIgnoreWrite, NonWriteableFields,
NonReadableFields);
std::set<const CFGBlock *> Visited;
ForwardTraverseCFGAndCollectReadsWrites(TheCFG.getEntry(), Info, Visited);
if (Info.Payload->hasAttr<HLSLOutAttr>() ||
Info.Payload->hasAttr<HLSLInOutAttr>()) {
// If there is copy-out semantic on the payload field,
// save the written fields and return it back to the caller for
// better diagnostics in higher recursion levels.
for (auto &p : Info.WritesPerField) {
WrittenFields.push_back(p.first);
}
DiagnosePayloadWrites(S, TheCFG, DT, Info, NonWriteableFields,
PayloadType);
}
auto WrittenFieldsInCalls = DiagnosePayloadAsFunctionArg(
S, Info, DT, FieldsToIgnoreRead, FieldsToIgnoreWrite, VisitedFunctions);
// Add calls that write fields as writes to allow the diagnostics on reads
// to check if a call that writes the field dominates the read.
for (auto &P : WrittenFieldsInCalls) {
for (const FieldDecl *Field : P.second) {
Info.WritesPerField[Field].push_back(P.first);
}
}
if (Info.Payload->hasAttr<HLSLInAttr>() ||
Info.Payload->hasAttr<HLSLInOutAttr>())
DiagnosePayloadReads(S, TheCFG, DT, Info, NonReadableFields);
}
DiagnoseBuiltinCallsWithPayload(S, TheCFG, DT, Info);
return WrittenFields;
}
const Stmt *IgnoreParensAndDecay(const Stmt *S) {
for (;;) {
switch (S->getStmtClass()) {
case Stmt::ParenExprClass:
S = cast<ParenExpr>(S)->getSubExpr();
break;
case Stmt::ImplicitCastExprClass: {
const ImplicitCastExpr *castExpr = cast<ImplicitCastExpr>(S);
if (castExpr->getCastKind() != CK_ArrayToPointerDecay &&
castExpr->getCastKind() != CK_NoOp &&
castExpr->getCastKind() != CK_LValueToRValue) {
return S;
}
S = castExpr->getSubExpr();
} break;
default:
return S;
}
}
}
// Emit warnings for calls that pass the payload to extern functions.
bool DiagnoseCallExprForExternal(Sema &S, const FunctionDecl *FD,
const CallExpr *CE,
const ParmVarDecl *Payload) {
// We check if we are passing the entire payload struct to an extern function.
// Here ends what we can check, so we just issue a warning.
if (!FD->hasBody()) {
const DeclRefExpr *DRef = nullptr;
const ParmVarDecl *PDecl = nullptr;
for (unsigned i = 0; i < CE->getNumArgs(); ++i) {
const Stmt *arg = IgnoreParensAndDecay(CE->getArg(i));
if (const DeclRefExpr *ArgRef = dyn_cast<DeclRefExpr>(arg)) {
if (ArgRef->getDecl() == Payload) {
DRef = ArgRef;
PDecl = FD->getParamDecl(i);
break;
}
}
}
if (DRef) {
S.Diag(CE->getExprLoc(),
diag::warn_qualified_payload_passed_to_extern_function);
return true;
}
}
return false;
}
// Emits diagnostics for the Payload parameter of a DXR shader stage.
bool DiagnosePayloadParameter(Sema &S, ParmVarDecl *Payload, FunctionDecl *FD,
DXIL::PayloadAccessShaderStage stage) {
if (!Payload) {
// cought already during codgegen of the function
return false;
}
if (!Payload->getAttr<HLSLInOutAttr>()) {
// error: payload must be inout qualified
return false;
}
CXXRecordDecl *Decl = Payload->getType()->getAsCXXRecordDecl();
if (!Decl || Decl->isImplicit()) {
// error: not a user defined type decl
return false;
}
if (!Decl->hasAttr<HLSLRayPayloadAttr>()) {
S.Diag(Payload->getLocation(), diag::err_payload_requires_attribute)
<< Decl->getName();
return false;
}
return true;
}
class DXRShaderVisitor : public RecursiveASTVisitor<DXRShaderVisitor> {
public:
DXRShaderVisitor(Sema &S) : S(S) {}
void diagnose(TranslationUnitDecl *TU) { TraverseTranslationUnitDecl(TU); }
bool VisitFunctionDecl(FunctionDecl *Decl) {
auto attr = Decl->getAttr<HLSLShaderAttr>();
if (!attr)
return true;
StringRef shaderStage = attr->getStage();
if (StringRef("miss,closesthit,anyhit,raygeneration").count(shaderStage)) {
ParmVarDecl *Payload = nullptr;
if (shaderStage != "raygeneration")
Payload = Decl->getParamDecl(0);
DXIL::PayloadAccessShaderStage Stage =
DXIL::PayloadAccessShaderStage::Invalid;
if (shaderStage == "closesthit") {
Stage = DXIL::PayloadAccessShaderStage::Closesthit;
} else if (shaderStage == "miss") {
Stage = DXIL::PayloadAccessShaderStage::Miss;
} else if (shaderStage == "anyhit") {
Stage = DXIL::PayloadAccessShaderStage::Anyhit;
}
// Diagnose the payload parameter.
if (Payload) {
DiagnosePayloadParameter(S, Payload, Decl, Stage);
}
DxrShaderDiagnoseInfo Info;
Info.funcDecl = Decl;
Info.Payload = Payload;
Info.Stage = Stage;
std::set<const FunctionDecl *> VisitedFunctions;
DiagnosePayloadAccess(S, Info, {}, {}, VisitedFunctions);
}
return true;
}
private:
Sema &S;
};
} // namespace
namespace hlsl {
void DiagnoseRaytracingPayloadAccess(clang::Sema &S,
clang::TranslationUnitDecl *TU) {
DXRShaderVisitor visitor(S);
visitor.diagnose(TU);
}
void DiagnoseCallableEntry(Sema &S, FunctionDecl *FD,
llvm::StringRef StageName) {
if (!FD->getReturnType()->isVoidType())
S.Diag(FD->getLocation(), diag::err_shader_must_return_void) << StageName;
if (FD->getNumParams() != 1)
S.Diag(FD->getLocation(), diag::err_raytracing_entry_param_count)
<< StageName << FD->getNumParams()
<< /*Special message for callable.*/ 3;
else {
ParmVarDecl *Param = FD->getParamDecl(0);
if (!(Param->getAttr<HLSLInOutAttr>() ||
(Param->getAttr<HLSLOutAttr>() && Param->getAttr<HLSLInAttr>())))
S.Diag(Param->getLocation(), diag::err_payload_requires_inout)
<< /*payload|callable*/ 1 << Param;
QualType Ty = Param->getType().getNonReferenceType();
// Don't diagnose incomplete type here. Function parameters are
// checked in Sema::CheckParmsForFunctionDef.
if (!S.RequireCompleteType(Param->getLocation(), Ty, 0) &&
!(hlsl::IsHLSLCopyableAnnotatableRecord(Ty)))
S.Diag(Param->getLocation(), diag::err_payload_attrs_must_be_udt)
<< /*payload|attributes|callable*/ 2 << /*parameter %2|type*/ 0
<< Param;
}
return;
}
void DiagnoseMissOrAnyHitEntry(Sema &S, FunctionDecl *FD,
llvm::StringRef StageName,
DXIL::ShaderKind Stage) {
if (!FD->getReturnType()->isVoidType())
S.Diag(FD->getLocation(), diag::err_shader_must_return_void) << StageName;
unsigned ExpectedParams = Stage == DXIL::ShaderKind::Miss ? 1 : 2;
if (ExpectedParams != FD->getNumParams()) {
S.Diag(FD->getLocation(), diag::err_raytracing_entry_param_count)
<< StageName << FD->getNumParams() << ExpectedParams;
return;
}
ParmVarDecl *Param = FD->getParamDecl(0);
if (!(Param->getAttr<HLSLInOutAttr>() ||
(Param->getAttr<HLSLOutAttr>() && Param->getAttr<HLSLInAttr>()))) {
S.Diag(Param->getLocation(), diag::err_payload_requires_inout)
<< /*payload|callable*/ 0 << Param;
return;
}
if (FD->getNumParams() > 1) {
Param = FD->getParamDecl(1);
if (Param->getAttr<HLSLInOutAttr>() || Param->getAttr<HLSLOutAttr>()) {
S.Diag(Param->getLocation(), diag::err_attributes_requiers_in) << Param;
return;
}
}
for (unsigned Idx = 0; Idx < ExpectedParams && Idx < FD->getNumParams();
++Idx) {
Param = FD->getParamDecl(Idx);
QualType Ty = Param->getType().getNonReferenceType();
// Don't diagnose here, just continue if this fails. Function parameters are
// checked in Sema::CheckParmsForFunctionDef.
if (S.RequireCompleteType(Param->getLocation(), Ty, 0))
continue;
if (!(hlsl::IsHLSLCopyableAnnotatableRecord(Ty))) {
S.Diag(Param->getLocation(), diag::err_payload_attrs_must_be_udt)
<< /*payload|attributes|callable*/ Idx << /*parameter %2|type*/ 0
<< Param;
}
}
return;
}
void DiagnoseRayGenerationOrIntersectionEntry(Sema &S, FunctionDecl *FD,
llvm::StringRef StageName) {
if (!FD->getReturnType()->isVoidType())
S.Diag(FD->getLocation(), diag::err_shader_must_return_void) << StageName;
unsigned ExpectedParams = 0;
if (ExpectedParams != FD->getNumParams())
S.Diag(FD->getLocation(), diag::err_raytracing_entry_param_count)
<< StageName << FD->getNumParams() << ExpectedParams;
return;
}
void DiagnoseClosestHitEntry(Sema &S, FunctionDecl *FD,
llvm::StringRef StageName) {
if (!FD->getReturnType()->isVoidType())
S.Diag(FD->getLocation(), diag::err_shader_must_return_void) << StageName;
unsigned ExpectedParams = 2;
if (ExpectedParams != FD->getNumParams()) {
S.Diag(FD->getLocation(), diag::err_raytracing_entry_param_count)
<< StageName << FD->getNumParams() << ExpectedParams;
}
if (FD->getNumParams() == 0)
return;
ParmVarDecl *Param = FD->getParamDecl(0);
if (!(Param->getAttr<HLSLInOutAttr>() ||
(Param->getAttr<HLSLOutAttr>() && Param->getAttr<HLSLInAttr>()))) {
S.Diag(Param->getLocation(), diag::err_payload_requires_inout)
<< /*payload|callable*/ 0 << Param;
}
if (FD->getNumParams() > 1) {
Param = FD->getParamDecl(1);
if (Param->getAttr<HLSLInOutAttr>() || Param->getAttr<HLSLOutAttr>()) {
S.Diag(Param->getLocation(), diag::err_attributes_requiers_in) << Param;
}
}
for (unsigned Idx = 0; Idx < ExpectedParams && Idx < FD->getNumParams();
++Idx) {
Param = FD->getParamDecl(Idx);
QualType Ty = Param->getType().getNonReferenceType();
// Don't diagnose here, just continue if this fails. Function parameters are
// checked in Sema::CheckParmsForFunctionDef.
if (S.RequireCompleteType(Param->getLocation(), Ty, 0))
continue;
if (!(hlsl::IsHLSLCopyableAnnotatableRecord(Ty))) {
S.Diag(Param->getLocation(), diag::err_payload_attrs_must_be_udt)
<< /*payload|attributes|callable*/ Idx << /*parameter %2|type*/ 0
<< Param;
}
}
return;
}
} // namespace hlsl