diff options
Diffstat (limited to 'source/slang/slang-ir-lower-binding-query.cpp')
| -rw-r--r-- | source/slang/slang-ir-lower-binding-query.cpp | 610 |
1 files changed, 610 insertions, 0 deletions
diff --git a/source/slang/slang-ir-lower-binding-query.cpp b/source/slang/slang-ir-lower-binding-query.cpp new file mode 100644 index 000000000..d0128d365 --- /dev/null +++ b/source/slang/slang-ir-lower-binding-query.cpp @@ -0,0 +1,610 @@ +// slang-ir-lower-tuple-types.cpp + +#include "slang-ir-lower-tuple-types.h" +#include "slang-ir.h" +#include "slang-ir-insts.h" + +// The pass in this file lowers the `getRegisterIndex()` and +// `getSpaceIndex()` intrinsics, by replacing them with literal +// values derived from the binding information on shader parameters. +// +// If these operations are applied to a global shader parameter, +// then we can simply read the binding information from that parameter +// and use it directly. +// +// Otherwise, we expect that the opaque object (resource/sampler/etc.) +// being referenced was passed down into the current function from +// a caller. We thus introduce new function parameters after the +// resource in question, transforming, e.g., this: +// +// void doThings( +// float a, +// Texture2D t, +// float b ) +// { +// ... __getRegisterIndex(t) ... +// ... __getSpaceIndex(t) ... +// } +// ... +// doThings(myTexture); +// +// into this: +// +// void doThings( +// float a, +// Texture2D t, +// uint t_index, +// uint t_space, +// float b ) +// { +// ... t_index ... +// ... t_space ... +// } +// ... +// doThings(myTexture, __getRegisterIndex(myTexture), __getRegisterSpace(myTexture)); +// +// At that point we have removed the invocations of `getRegisterIndex` +// and `getRegisterSpace` in the callee function, but introduced new +// invocations in the caller function, so we need to iterate until +// we eventually either bottom out at a global shader parameter, or +// run into a context that we cannot simplify. + +namespace Slang +{ + // There are a ton of passes we've implemented now that use + // some basic work-list structures, and it seems a bit silly + // to be writing that code intermixed with the actual algorithm. + // + // For this file, we break the common work-list functionality + // out into a base type that we can re-use in specific passes. + // + struct WorkListPass + { + public: + IRModule* module; + DiagnosticSink* sink; + + protected: + + // The base type needs to abstract over how the + // concrete pass will process each instruction + // that gets placed into the work list. + + virtual void processInst(IRInst* inst) = 0; + + // Otherwise, the implementation of the work list + // itself is straightforward, and not anything + // that hasn't been seen in other files. + + List<IRInst*> workList; + HashSet<IRInst*> workListSet; + + void addToWorkList(IRInst* inst) + { + if (workListSet.contains(inst)) + return; + + workList.add(inst); + workListSet.add(inst); + } + + void processWorkList() + { + while (workList.getCount() != 0) + { + IRInst* inst = workList.getLast(); + + workList.removeLast(); + workListSet.remove(inst); + + processInst(inst); + + for (auto child = inst->getLastChild(); child; child = child->getPrevInst()) + { + addToWorkList(child); + } + } + } + + // As long as we are factoring out repeated cruft, + // it seems reasonable to *also* deal with the + // frequent need to buffer up instructions to + // be deleted when a pass is complete. + + List<IRInst*> toBeDeleted; + HashSet<IRInst*> toBeDeletedSet; + + void addToBeDeleted(IRInst* inst) + { + if (toBeDeletedSet.contains(inst)) + return; + + toBeDeleted.add(inst); + toBeDeletedSet.add(inst); + } + + void processDeletions() + { + for (auto inst : toBeDeleted) + { + inst->removeAndDeallocate(); + } + toBeDeleted.clear(); + toBeDeletedSet.clear(); + } + }; + + // The concrete pass will then be a specialization of + // the base work-list abstraction. + // + struct BindingQueryLoweringContext : public WorkListPass + { + // All of the intrinsics we will be processing use + // the same result type (`uint`), so it is helpful + // to cache a pointer to the IR type at the start + // of the pass and re-use it. + // + IRType* indexType = nullptr; + + void processModule() + { + IRBuilder builder(module); + indexType = builder.getUIntType(); + + // Processing the module consists of recursively + // processing all the instructions in one pass, + // and then potentially revisiting instructions + // that had new intrinsics added to their bodies. + // + addToWorkList(module->getModuleInst()); + processWorkList(); + } + + void processInst(IRInst* inst) + { + // For this pass, we really only care about + // our binding query instructions. + // + if (auto query = as<IRBindingQuery>(inst)) + { + processQueryInst(query); + } + } + + void processQueryInst(IRBindingQuery* inst) + { + // Processing one of the query instructions is conceptually + // simple: we find a compute a value to replace it with, + // and then simply *replace* the instruction. + // + auto replacementValue = findOrComputeReplacementValueFor(inst); + if (!replacementValue) + { + // If we cannot find or compute a replacement value, + // then we need to treat it as an error, since the + // binding query intrinsics don't admit any reasonable + // runtime implementation. + // + sink->diagnose( + inst, + Diagnostics::opaqueReferenceMustResolveToGlobal); + return; + } + + inst->replaceUsesWith(replacementValue); + inst->removeAndDeallocate(); + } + + // We want to cache the results of computing the binding + // information for an opaque-type value, in case doing + // so required adding or modifying code. + // + // For that purpose, we introduce a simple data structure + // to hold the two pieces of binding information we + // care about. + // + struct OpaqueValueInfo + { + IRInst* registerIndex = nullptr; + IRInst* registerSpace = nullptr; + }; + + IRInst* findOrComputeReplacementValueFor(IRBindingQuery* query) + { + // Finding the replacement for a given query instruction + // then amounts to computing (or caching) the binding + // information for the opaque-type value it queries, + // and then projecting out the appropriate field. + + auto opaqueValue = query->getOpaqueValue(); + auto opaqueValueInfo = findOrComputeOpaqueValueInfo(opaqueValue); + + switch (query->getOp()) + { + default: + SLANG_UNEXPECTED("unhandled binding query instruction type"); + UNREACHABLE_RETURN(query); + + case kIROp_GetRegisterIndex: + return opaqueValueInfo.registerIndex; + + case kIROp_GetRegisterSpace: + return opaqueValueInfo.registerSpace; + } + } + + // The information will be cached in a dictionary, + // keyed on the opaque-type value that the information + // was computed for. + // + Dictionary<IRInst*, OpaqueValueInfo> mapOpaqueValueToInfo; + + // Looking up the cached information (if any) is a simple + // matter of using the dictionary. + // + // (We have a distinct operation for lookup vs. the + // memo-cached lookup below, because we may want to + // query this information while computing an entry, + // and we don't want to introduce potential recursion. + // + OpaqueValueInfo* findOpaqueValueInfo(IRInst* opaqueValue) + { + return mapOpaqueValueToInfo.tryGetValue(opaqueValue); + } + + OpaqueValueInfo findOrComputeOpaqueValueInfo(IRInst* opaqueValue) + { + if (auto foundInfo = findOpaqueValueInfo(opaqueValue)) + return *foundInfo; + + // If there is no information registered in the cache, we + // compute it on-demand. + // + // Note that there is no potential for circularity, so + // long as the implementation of `computeOpaqueValueInfo` + // does not itself call `findOrComputeValueInfo`. + // + auto computedInfo = computeOpaqueValueInfo(opaqueValue); + mapOpaqueValueToInfo.add(opaqueValue, computedInfo); + return computedInfo; + } + + // We are now (finally) getting into the meat of what this + // pass needs to do. Given an instruction with an opaque + // type, we need to try to compute the register and space + // it is bound to, or conspire to have that information + // passed along. + // + OpaqueValueInfo computeOpaqueValueInfo(IRInst* opaqueValue) + { + if (auto globalParam = as<IRGlobalParam>(opaqueValue)) + { + // The simple/base case is when we have a global shader + // parameter that has layout information attached. + // + // Note that this pass needs to run late enough that + // shader parameters declared at other scopes will have + // been massaged into the appropriate form. + // + if (auto layoutDecoration = globalParam->findDecoration<IRLayoutDecoration>()) + { + if (auto layout = as<IRVarLayout>(layoutDecoration->getLayout())) + { + // We expect any shader parameter of an opaque type + // to have a relevant resource kind, but it isn't + // too hard to code defensively. We will iterate + // over the resource kinds that are present and + // take the first one that represents an opaque type. + // + for (auto offsetAttr : layout->getOffsetAttrs()) + { + switch (offsetAttr->getResourceKind()) + { + default: + break; + + case LayoutResourceKind::ShaderResource: + case LayoutResourceKind::UnorderedAccess: + case LayoutResourceKind::ConstantBuffer: + case LayoutResourceKind::SamplerState: + case LayoutResourceKind::DescriptorTableSlot: + { + IRBuilder builder(module); + + OpaqueValueInfo info; + info.registerIndex = builder.getIntValue(indexType, offsetAttr->getOffset()); + info.registerSpace = builder.getIntValue(indexType, offsetAttr->getSpace()); + return info; + } + break; + } + } + } + } + } + else if (auto param = as<IRParam>(opaqueValue)) + { + // The other very interesting case is when the opaque-type + // value is an `IRParam`, which indicates that it is either + // a function parameter or a phi node of a basic block. + // + // Either way, we always expect a parameter to appear as + // a child of a block. + // + auto block = as<IRBlock>(param->getParent()); + SLANG_ASSERT(block); + + // When rewriting call sites, we will need to know the + // index of `param` within the parameter list. + // + Index paramIndex = -1; + { + Count paramCounter = 0; + for (auto p : block->getParams()) + { + Index i = paramCounter++; + if (p == param) + { + paramIndex = i; + break; + } + } + SLANG_ASSERT(paramIndex >= 0); + } + + // In either case (function parameter or block parameter), + // we will insert additional parameters after the original + // parameter, so that the register index and space can + // be passed along explicitly. + // + IRBuilder builder(module); + + // We create new parameters to pass along the register index/space, + // and manually insert them where we want them in the parameter list. + // + auto registerIndexParam = builder.createParam(builder.getUIntType()); + auto registerSpaceParam = builder.createParam(builder.getUIntType()); + // + registerSpaceParam->insertAfter(param); + registerIndexParam->insertAfter(param); + + // We would like for the newly-introduced parameters to have + // nice human-readable names, if the original parameter did. + // + if (auto nameHintDecoration = param->findDecoration<IRNameHintDecoration>()) + { + String hint; + hint.append(nameHintDecoration->getName()); + hint.append("."); + builder.addNameHintDecoration(registerIndexParam, (hint + "index").getUnownedSlice()); + builder.addNameHintDecoration(registerSpaceParam, (hint + "space").getUnownedSlice()); + } + + // Similarly, the new parameters should get debugging-related + // source location information from the original parameter, + // if it had any. + // + registerIndexParam->sourceLoc = param->sourceLoc; + registerSpaceParam->sourceLoc = param->sourceLoc; + + // Now we need to scan for the places that the function or block + // that the parameter belongs to gets referenced. At each such + // location, we will pass along arguments to match the additional + // parameters. + // + if (!block->getPrevBlock()) + { + // If this is the first block in the parent function, + // then this is a function parameter, and we will + // iterate over call sites of the function and rewrite + // them to pass along arguments for the new parameters. + // + auto func = block->getParent(); + + for (auto use = func->firstUse; use; use = use->nextUse) + { + auto user = use->getUser(); + if (auto call = as<IRCall>(user)) + { + if (call->getCallee() == func) + { + rewriteCall(call, paramIndex); + } + } + } + } + else + { + // If this is a block parameter, we will iterate over + // the instructions that branch to the block, and rewrite + // their argument lists, similar to what we do for function calls. + // + for (auto use = block->firstUse; use; use = use->nextUse) + { + auto user = use->getUser(); + if (auto branch = as<IRUnconditionalBranch>(user)) + { + if (branch->getTargetBlock() == block) + { + rewriteBranch(branch, paramIndex); + } + } + } + } + + // The new parameters that we introduced will be used to + // replace any binding query intrinsics applied to + // this opaque value. + // + OpaqueValueInfo info; + info.registerIndex = registerIndexParam; + info.registerSpace = registerSpaceParam; + return info; + } + + // By default we find that we cannot query binding information + // for the given instruction. + OpaqueValueInfo info; + return info; + } + + // In our IR, there isn't a lot of difference between a `call` + // and an `unconditionalBranch`; indeed, this is part of what + // motivates the use of `IRParam`s for both function parameters + // and phi nodes. + // + // However, while both blocks and functions use the same `IRParam` + // representation, we (currently) do not have a common base + // between the `call` and `unconditionalBranch` instructions. + // + // Rather than have duplicate logic between the two cases, we + // simply observe that for our purposes rewriting either a + // `call` or an `unconditionalBranch` amounts to doing + // special-case work on *one* operand of the original, while + // copying over all the other operands as-is. + // + // Given this observation, we can bottleneck both calls and + // branches into a common worker routine by passing down + // the instruction to be rewritten and a pointer to the + // `IRUse` for the one "interesting" operand. + + void rewriteCall(IRCall* oldCall, Index paramIndex) + { + rewriteCallOrBranch( + oldCall, + oldCall->getArgs() + paramIndex); + } + + void rewriteBranch(IRUnconditionalBranch* oldBranch, Index paramIndex) + { + rewriteCallOrBranch( + oldBranch, + oldBranch->getArgs() + paramIndex); + } + + void rewriteCallOrBranch( + IRInst* oldCallOrBranch, + IRUse* oldOperandToRewrite) + { + // Our goal here is to generate a new version of + // `oldCallOrBranch` that copies over most of the + // operands as-is, but introduces our rewrites + // around the chosen operand. + + IRBuilder builder(module); + builder.setInsertBefore(oldCallOrBranch); + + // We capture the old operand list as a range of + // `IRUse`s, and set up a fresh list to hold the + // new operands. + // + auto oldOperandsBegin = oldCallOrBranch->getOperands(); + auto oldOperandsEnd = oldOperandsBegin + oldCallOrBranch->getOperandCount(); + // + List<IRInst*> newOperands; + + // All of the operands that precede the interesting + // one can be copied over from the old list to the + // new one as-is. + // + for (auto u = oldOperandsBegin; u < oldOperandToRewrite; ++u) + { + auto operand = u->get(); + newOperands.add(operand); + } + + // Next we look at the value of the "intersting" + // operand, knowing that we need to pass along + // not only the original value but also the + // binding information. + // + IRInst* arg = oldOperandToRewrite->get(); + IRInst* registerIndex = nullptr; + IRInst* registerSpace = nullptr; + + // As a simple optimization, if we have *already* + // computed and cached binding information for + // the argument, we can re-use that information + // here and now. + // + if (auto info = findOpaqueValueInfo(arg)) + { + registerIndex = info->registerIndex; + registerSpace = info->registerSpace; + } + else + { + // If there is no cached information for + // the argument, we choose *not* to make + // a recursive call into `findOrComputeOpaqueValueInfo`. + // + // Instead we will simply emit additional + // binding query intrinsics into the body + // of the caller (right before the call site), + // and add those instructions to our work + // list, to be eliminated later. + // + registerIndex = builder.emitIntrinsicInst( + indexType, + kIROp_GetRegisterIndex, + 1, &arg); + registerSpace = builder.emitIntrinsicInst( + indexType, + kIROp_GetRegisterSpace, + 1, &arg); + // + addToWorkList(registerIndex); + addToWorkList(registerSpace); + } + + // Whether we have found existing binding information, + // or emitted new intrinsics, we are now ready + // to append the argument and its binding information + // to the new operand list. + // + newOperands.add(arg); + newOperands.add(registerIndex); + newOperands.add(registerSpace); + + // Any operands of the original instruction that come + // after the one we rewrite can be copied over as-is. + // + // Note: we don't currently have any operands that would + // appear after the arguments of a `call` or `branch`, + // but the fact that we encode `IRAttr`s on an instruction + // as additional (trailing) operands means that this could + // conceivably happen at some point. + // + for (auto u = oldOperandToRewrite + 1; u < oldOperandsEnd; ++u) + { + auto operand = u->get(); + newOperands.add(operand); + } + + // Once we've built up the new operand list, we can emit + // a new instruction that has the same opcode and type, + // with the new operands, and then use it to replace + // the existing instruction. + // + auto newCallOrBranch = builder.emitIntrinsicInst( + oldCallOrBranch->getFullType(), + oldCallOrBranch->getOp(), + newOperands.getCount(), + newOperands.getBuffer()); + + oldCallOrBranch->transferDecorationsTo(newCallOrBranch); + oldCallOrBranch->replaceUsesWith(newCallOrBranch); + oldCallOrBranch->removeAndDeallocate(); + } + }; + + void lowerBindingQueries( + IRModule* module, + DiagnosticSink* sink) + { + BindingQueryLoweringContext context; + context.module = module; + context.sink = sink; + context.processModule(); + } +} |
