blob: 5a54e55378987928692d0c93be1e7f510e25037e [file] [edit]
//===- SPIRVLegalizeResourceBinding.cpp - Legalize resource bindings ----*- C++
//-*-===//
//
// 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 pass legalizes the @llvm.spv.resource.handlefromimplicitbinding
// and @llvm.spv.resource.handlefromheap intrinsics by replacing them with a
// call to @llvm.spv.resource.handlefrombinding.
// It also replaces any @llvm.spv.resource.counterhandlefromimplicitbinding and
// @llvm.spv.resource.counterhandlefromheap intrinsics with calls to
// @llvm.spv.resource.counterhandlefrombinding.
//
//===----------------------------------------------------------------------===//
#include "SPIRV.h"
#include "llvm/ADT/BitVector.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstVisitor.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/IntrinsicsSPIRV.h"
#include "llvm/IR/Module.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include <vector>
using namespace llvm;
namespace {
class SPIRVLegalizeResourceBindingImpl {
public:
bool runOnModule(Module &M);
private:
void collectBindingInfo(Module &M);
uint32_t getAndReserveFirstUnusedBinding(uint32_t DescSet);
bool replaceImplicitBindingCalls(Module &M);
bool replaceHeapBindingCalls(Module &M);
// A map from descriptor set to a bit vector of used binding numbers.
std::vector<BitVector> UsedBindings;
// Set to true by collectBindingInfo() if there are possibly any implicit
// binding or heap binding calls in the module (if the module contains a
// declaration of implicit binding or heap intrinsic).
bool MayHaveImplicitBindings = false;
bool MayHaveHeapBindings = false;
};
class SPIRVLegalizeResourceBindingLegacy : public ModulePass {
public:
static char ID;
SPIRVLegalizeResourceBindingLegacy() : ModulePass(ID) {}
StringRef getPassName() const override {
return "SPIRV Legalize Resource Binding";
}
bool runOnModule(Module &M) override {
return SPIRVLegalizeResourceBindingImpl().runOnModule(M);
}
};
static uint32_t getDescSet(const CallInst *CI) {
uint32_t DescSetArgIdx;
switch (CI->getIntrinsicID()) {
case Intrinsic::spv_resource_handlefromimplicitbinding:
DescSetArgIdx = 1;
break;
case Intrinsic::spv_resource_counterhandlefromimplicitbinding:
DescSetArgIdx = 2;
break;
default:
llvm_unreachable("CallInst is not an implicit binding intrinsic");
}
return cast<ConstantInt>(CI->getArgOperand(DescSetArgIdx))->getZExtValue();
}
// Collect all of the bindings used by llvm.spv.resource.handlefrombinding
// and llvm.spv.resource.counterhandlefrombinding calls. Also check if there
// are any implicit binding calls.
void SPIRVLegalizeResourceBindingImpl::collectBindingInfo(Module &M) {
auto addBinding = [&](uint32_t DescSet, uint32_t Binding) {
if (UsedBindings.size() <= DescSet) {
UsedBindings.resize(DescSet + 1);
UsedBindings[DescSet].resize(64);
}
if (UsedBindings[DescSet].size() <= Binding) {
UsedBindings[DescSet].resize(2 * Binding + 1);
}
UsedBindings[DescSet].set(Binding);
};
auto collectBinding = [&](Function &F, uint32_t ArgDescSetIdx,
uint32_t ArgBindingIdx) {
for (User *U : F.users()) {
if (CallInst *CI = dyn_cast<CallInst>(U)) {
const uint32_t DescSet =
cast<ConstantInt>(CI->getArgOperand(ArgDescSetIdx))->getZExtValue();
const uint32_t Binding =
cast<ConstantInt>(CI->getArgOperand(ArgBindingIdx))->getZExtValue();
addBinding(DescSet, Binding);
}
}
};
for (Function &F : M) {
if (!F.isDeclaration())
continue;
switch (F.getIntrinsicID()) {
case Intrinsic::spv_resource_handlefrombinding:
collectBinding(F, /*ArgDescSetIdx*/ 0, /*ArgBindingIdx*/ 1);
break;
case Intrinsic::spv_resource_counterhandlefrombinding:
collectBinding(F, /*ArgDescSetIdx*/ 1, /*ArgBindingIdx*/ 2);
break;
case Intrinsic::spv_resource_handlefromimplicitbinding:
case Intrinsic::spv_resource_counterhandlefromimplicitbinding:
MayHaveImplicitBindings = true;
break;
case Intrinsic::spv_resource_handlefromheap:
case Intrinsic::spv_resource_counterhandlefromheap:
MayHaveHeapBindings = true;
break;
default:
break;
}
}
}
uint32_t SPIRVLegalizeResourceBindingImpl::getAndReserveFirstUnusedBinding(
uint32_t DescSet) {
if (UsedBindings.size() <= DescSet) {
UsedBindings.resize(DescSet + 1);
UsedBindings[DescSet].resize(64);
}
int NewBinding = UsedBindings[DescSet].find_first_unset();
if (NewBinding == -1) {
NewBinding = UsedBindings[DescSet].size();
UsedBindings[DescSet].resize(2 * NewBinding + 1);
}
UsedBindings[DescSet].set(NewBinding);
return NewBinding;
}
// Replace the implicit binding call with a new call using explicit binding.
static void replaceWithHandleFromBinding(Module &M, CallInst *CI,
uint32_t DescSet, uint32_t Binding,
Value *RangeOp, Value *IndexOp,
Value *Name) {
assert((CI->getIntrinsicID() ==
Intrinsic::spv_resource_handlefromimplicitbinding ||
CI->getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) &&
"unexpected binding intrinsic");
IRBuilder<> Builder(CI);
Value *DescSetOp = Builder.getInt32(DescSet);
Value *BindingOp = Builder.getInt32(Binding);
Function *NewFunc = Intrinsic::getOrInsertDeclaration(
&M, Intrinsic::spv_resource_handlefrombinding, {CI->getType()});
CallInst *NewCI = Builder.CreateCall(
NewFunc, {DescSetOp, BindingOp, RangeOp, IndexOp, Name});
NewCI->setCallingConv(CI->getCallingConv());
CI->replaceAllUsesWith(NewCI);
CI->eraseFromParent();
}
// Replace the implicit counter binding call with a new call using explicit
// binding.
static void replaceWithCounterHandleFromBinding(Module &M, CallInst *CI,
uint32_t DescSet,
uint32_t Binding) {
assert(
(CI->getIntrinsicID() ==
Intrinsic::spv_resource_counterhandlefromimplicitbinding ||
CI->getIntrinsicID() == Intrinsic::spv_resource_counterhandlefromheap) &&
"unexpected binding intrinsic");
IRBuilder<> Builder(CI);
Value *DescSetOp = Builder.getInt32(DescSet);
Value *BindingOp = Builder.getInt32(Binding);
Value *MainHandle = CI->getArgOperand(0);
Type *OverloadTys[] = {CI->getType(), MainHandle->getType()};
Function *NewFunc = Intrinsic::getOrInsertDeclaration(
&M, Intrinsic::spv_resource_counterhandlefrombinding, OverloadTys);
CallInst *NewCI =
Builder.CreateCall(NewFunc, {MainHandle, DescSetOp, BindingOp});
NewCI->setCallingConv(CI->getCallingConv());
CI->replaceAllUsesWith(NewCI);
CI->eraseFromParent();
}
bool SPIRVLegalizeResourceBindingImpl::replaceImplicitBindingCalls(Module &M) {
// Collect all implicit binding calls.
SmallVector<std::pair<uint32_t, CallInst *>> IBCalls;
bool Changed = false;
for (Function &F : M) {
if (!F.isDeclaration())
continue;
uint32_t OrderIdIdx;
if (F.getIntrinsicID() == Intrinsic::spv_resource_handlefromimplicitbinding)
OrderIdIdx = 0;
else if (F.getIntrinsicID() ==
Intrinsic::spv_resource_counterhandlefromimplicitbinding)
OrderIdIdx = 1;
else
continue;
for (User *U : F.users()) {
if (CallInst *CI = dyn_cast<CallInst>(U)) {
ConstantInt *OrderId = cast<ConstantInt>(CI->getArgOperand(OrderIdIdx));
IBCalls.emplace_back(OrderId->getZExtValue(), CI);
}
}
}
if (IBCalls.empty())
return false;
// Sort the collected calls by their order ID.
llvm::sort(IBCalls, llvm::less_first());
// Assign bindings based on the order ID. Same order ID gets the same binding.
// Also make sure that calls with the same order ID have the same descriptor
// set.
uint32_t LastOrderId = -1;
uint32_t LastBinding = -1;
uint32_t LastDescSet = -1;
for (auto &[OrderId, CI] : IBCalls) {
uint32_t Binding;
uint32_t DescSet = getDescSet(CI);
if (OrderId == LastOrderId) {
if (DescSet != LastDescSet)
report_fatal_error("Implicit binding calls with the same order ID must "
"have the same descriptor set");
Binding = LastBinding;
} else {
Binding = getAndReserveFirstUnusedBinding(DescSet);
}
// Replace the implicit binding call with an explicit binding call.
if (CI->getIntrinsicID() ==
Intrinsic::spv_resource_handlefromimplicitbinding)
replaceWithHandleFromBinding(M, CI, DescSet, Binding,
CI->getArgOperand(2), CI->getArgOperand(3),
CI->getArgOperand(4));
else
replaceWithCounterHandleFromBinding(M, CI, DescSet, Binding);
Changed = true;
LastOrderId = OrderId;
LastBinding = Binding;
LastDescSet = DescSet;
}
return Changed;
}
bool moduleContainsConstantString(Module &M, StringRef Str) {
for (GlobalVariable &GV : M.globals()) {
if (!GV.hasInitializer())
continue;
if (ConstantDataArray *CDA =
dyn_cast<ConstantDataArray>(GV.getInitializer())) {
if (CDA->isString() && CDA->getAsCString() == Str)
return true;
}
}
return false;
}
// Creates unique global variable for the heap name string, making
// sure that each heap name string is unique within the module.
GlobalVariable *createHeapNameString(Module &M, StringRef Name) {
SmallString<32> GlobalStringName(Name);
uint32_t HeapNameLen = Name.size();
StringRef HeapName;
for (unsigned Suffix = 1;; ++Suffix) {
GlobalStringName.append(".str");
if (!M.getNamedValue(GlobalStringName)) {
// Make sure the module does not already have a constant
// string with this value.
HeapName = GlobalStringName.substr(0, HeapNameLen);
if (!moduleContainsConstantString(M, HeapName))
break;
}
GlobalStringName.resize(Name.size());
raw_svector_ostream(GlobalStringName) << '.' << Suffix;
HeapNameLen = GlobalStringName.size();
}
Constant *Init = ConstantDataArray::getString(M.getContext(), HeapName);
GlobalVariable *HeapNameGV = new GlobalVariable(
M, Init->getType(), /*isConstant=*/true, GlobalValue::PrivateLinkage,
Init, GlobalStringName, /*InsertBefore=*/nullptr,
GlobalVariable::NotThreadLocal, /*AddressSpace=*/0);
HeapNameGV->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
HeapNameGV->setAlignment(Align(1));
return HeapNameGV;
}
// The SPIR-V backend represents dynamic resources as unbounded resource arrays.
// This function scans the module for calls to
// `llvm.spv.resource.handlefromheap` and groups them according to whether they
// create CBV/SRV/UAV resources or samplers. It also collects calls to
// `llvm.spv.resource.counterhandlefromheap` intrinsics that form a third group.
//
// The function assigns the first available binding to each non-empty heap group
// in this order: CBV/SRV/UAV resources, samplers, and counters. For each group,
// it will replace the heap intrinsic calls with their explicit
// `handlefrombinding` equivalents using the assigned binding.
//
// The function does not actually create the unbounded resource-array globals
// itself. It only assigns a unique name that is shared by all resources
// belonging to the same heap type; the existing `SPIRVInstructionSelector` will
// create the globals.
//
// Because the CBV/SRV/UAV group can contain resources of different types, these
// resources must be represented by separate arrays, one for each unique
// resource type. All of these arrays will use the same binding and therefore
// overlap.
bool SPIRVLegalizeResourceBindingImpl::replaceHeapBindingCalls(Module &M) {
// First we collect all used heap binding declarations and group them based on
// their kind.
SmallVector<Function *, 8> CbvSrvUavs;
SmallVector<Function *, 8> Samplers;
SmallVector<Function *, 8> Counters;
bool Changed = false;
for (Function &F : M) {
if (!F.isDeclaration() || F.user_empty())
continue;
if (F.getIntrinsicID() == Intrinsic::spv_resource_handlefromheap) {
TargetExtType *ResType = cast<TargetExtType>(F.getReturnType());
if (ResType->getName() == "spirv.Sampler")
Samplers.emplace_back(&F);
else
CbvSrvUavs.emplace_back(&F);
} else if (F.getIntrinsicID() ==
Intrinsic::spv_resource_counterhandlefromheap) {
Counters.emplace_back(&F);
} else
continue;
}
if (CbvSrvUavs.empty() && Samplers.empty() && Counters.empty())
return false;
// Heap resources are always mapped to descriptor set 0 as an unbounded
// runtime array.
constexpr uint32_t DescSet = 0;
Value *Zero =
llvm::ConstantInt::get(llvm::Type::getInt32Ty(M.getContext()), 0);
if (!CbvSrvUavs.empty()) {
// For CBV/UAV/SRV resources we need to create a different
// ResourceDescriptorHeap name for each unique resource type. They will all
// share the same binding and will overlap.
uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
SmallDenseMap<TargetExtType *, GlobalVariable *> ResourceDescriptorHeaps;
for (Function *F : CbvSrvUavs) {
TargetExtType *ResType = cast<TargetExtType>(F->getReturnType());
GlobalVariable *HeapNameGV = nullptr;
auto It = ResourceDescriptorHeaps.find(ResType);
if (It == ResourceDescriptorHeaps.end()) {
HeapNameGV = createHeapNameString(M, "ResourceDescriptorHeap");
[[maybe_unused]] auto [InsertedIt, Inserted] =
ResourceDescriptorHeaps.try_emplace(ResType, HeapNameGV);
assert(Inserted && "resource heap name already exists");
} else {
HeapNameGV = It->second;
}
for (User *U : make_early_inc_range(F->users())) {
if (CallInst *CI = dyn_cast<CallInst>(U)) {
Value *HeapIdx = CI->getArgOperand(0);
replaceWithHandleFromBinding(M, CI, DescSet, Binding, Zero, HeapIdx,
HeapNameGV);
Changed = true;
}
}
F->eraseFromParent();
}
}
if (!Samplers.empty()) {
uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
// The Sampler handle type should be the same for all samplers
// (target("spirv.Sampler")).
[[maybe_unused]] TargetExtType *SamplerHandleType =
cast<TargetExtType>(Samplers.front()->getReturnType());
GlobalVariable *HeapNameGV =
createHeapNameString(M, "SamplerDescriptorHeap");
for (Function *F : Samplers) {
assert(F->getReturnType() == SamplerHandleType &&
"sampler handle type mismatch");
for (User *U : make_early_inc_range(F->users())) {
if (CallInst *CI = dyn_cast<CallInst>(U)) {
Value *HeapIdx = CI->getArgOperand(0);
replaceWithHandleFromBinding(M, CI, DescSet, Binding, Zero, HeapIdx,
HeapNameGV);
Changed = true;
}
}
F->eraseFromParent();
}
}
if (!Counters.empty()) {
uint32_t Binding = getAndReserveFirstUnusedBinding(DescSet);
[[maybe_unused]] Type *CounterHandleTy = Counters.front()->getReturnType();
for (Function *F : Counters) {
// The counter handle type should be the same for all resource types
// that have a counter (target("spirv.VulkanBuffer", i32, 12, 1)).
assert(F->getReturnType() == CounterHandleTy &&
"counter handle type mismatch");
for (User *U : make_early_inc_range(F->users())) {
if (CallInst *CI = dyn_cast<CallInst>(U)) {
replaceWithCounterHandleFromBinding(M, CI, DescSet, Binding);
Changed = true;
}
}
F->eraseFromParent();
}
}
return Changed;
}
bool SPIRVLegalizeResourceBindingImpl::runOnModule(Module &M) {
collectBindingInfo(M);
bool Changed = false;
if (MayHaveImplicitBindings)
Changed |= replaceImplicitBindingCalls(M);
if (MayHaveHeapBindings)
Changed |= replaceHeapBindingCalls(M);
return Changed;
}
} // namespace
PreservedAnalyses
SPIRVLegalizeResourceBindingPass::run(Module &M, ModuleAnalysisManager &AM) {
return SPIRVLegalizeResourceBindingImpl().runOnModule(M)
? PreservedAnalyses::none()
: PreservedAnalyses::all();
}
char SPIRVLegalizeResourceBindingLegacy::ID = 0;
INITIALIZE_PASS(SPIRVLegalizeResourceBindingLegacy,
"legalize-spirv-resource-binding",
"Legalize SPIR-V resource bindings", false, false)
ModulePass *llvm::createSPIRVLegalizeResourceBindingPass() {
return new SPIRVLegalizeResourceBindingLegacy();
}