diff options
| author | Tim Foley <tfoleyNV@users.noreply.github.com> | 2020-07-15 09:31:27 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-15 09:31:27 -0700 |
| commit | 723c9b1b3607ba910abbeb72f4f13bdff3cbd502 (patch) | |
| tree | 387ecf8c0a3324ebeb8361bb1abda08f8589721d /source/slang/slang-ir-explicit-global-init.cpp | |
| parent | 48f26ef082fa3b0c2a02dc57585f7e43210bbb63 (diff) | |
Remove KernelContext wrapper from CPU/CUDA emit (#1440)
* Remove KernelContext wrapper from CPU/CUDA emit
Currently, the CPU and CUDA C++ targets rely on a `KernelContext` type that is generated during emit, as a way to provide implicit access to things that were global in the input Slang code, but that can't actually be emitted as globals in the target language (because the semantics of global declarations differ).
For example, input like:
```hlsl
ConstantBuffer<Stuff> gStuff; // shader parameter
groupshared int gData[1024]; // thread-group shared variable
static int gCounter = 0; // "thread-local" global-scope variable
void subroutine() { ... }
[shader("compute")] void computeMain() { ... }
```
would translate to output C++ for CPU a bit like:
```c++
struct KernelContext
{
ConstantBuffer<Stuff> gStuff;
int gData[1024];
int gCounter = 0;
void subroutine() { ... }
void computeMain() { ... }
};
```
Note that both `computeMain()` and `subroutine()` are non-`static` members functions on `KernelContext`, so they have an implicit `this` parameter of type `KernelContext`, which allows the bodies of those functions to implicitly reference `gStuff`, etc. by name in their bodies.
Because `KernelContext::computeMain()` is a member function, we end up emitting an additional global-scope function to expose the entry point to the outside world, and that function is responsible for declaring a local `KernelContext` and invoking the generated entry point on it.
This approach has several important drawbacks:
* It complicates the emit logic for CPU and CUDA, with many special cases around when/how things get emitted
* It complicates the implementation of dynamic dispatch, because what seems like a function pointer in Slang IR needs to be a pointer-to-member-function in C++.
* It makes it difficult to have a non-kernel-oriented mode of compilation for CPU where a Slang function with a given signature gets output as a C++ CPU function with the "same" signature (not wrapped up as a member function of `KernelContext`.
This change makes a step toward addressing these issues by making the introducing of the `KernelContext` type be something that is done in an explicit IR pass instead of being handled as part of the last-mile emit logic.
The most important change is the removal of code related to `KernelContext` from the `slang-emit-{cpp,cuda}.{h,cpp}` files, with the equivalent logic instead being handled in a new pass in `slang-ir-explicit-global-context.{h,cpp}`. It should be noted that further cleanups to the emit logic should now be possible; in particular, both the CPU and CUDA emit paths are manually sequencing the `EmitAction`s instead of relying on the default logic, but at this point they should be able to just use the default. The additional cleanups are left for future work.
The explicit IR pass does more or less what one would expect: it identifies global-scope entities (global variables and parameters) that need to be wrapped and turns them into fields of a `KernelContext` type. It then modifies all entry points to initialize a `KernelContext` as part of their startup. Finally, any code that used to refer to the global entities is changed to refer to a field of the context, with the context passed via new function parameters (the new parameter is only added to functions that need it for now).
Transforming global variables into fields of a `KernelContext` type in the IR pass ends up dropping their initial-value expressions (since those were attached as basic blocks on the `IRGlobalVar`). To avoid breaking code that relies on global-scope (but thread-local) variables, this change also adds an explicit pass that takes the initialization logic on all global variables and moves it to explicit logic that runs at the start of every entry point in a linked module (`slang-ir-explicit-global-init.{h,cpp}`). This pass would also be useful when we get back to direct SPIR-V emit, since SPIR-V also requires initialization logic for globals to be emitted into entry points.
One complication that arises when the IR is introducing the types for entry-point parameters, global-scope parameters, and the `KernelContext` type is that it becomes harder for the emit logic to utter the names of those types (they might not even have names, since `IRNameHint`s might get stripped). This created a problem since the wrapper operations that were being generated for CPU were taking `void*` parameters and casting them to the appropriate type. To work around this issue, we have added an explicit IR pass (`slang-ir-entry-point-raw-ptr-params.{h,cpp}`) that transforms the signature of entry points so that any pointer parameters instead become raw pointer (`void*`) parameters, with the casting being handled inside the entry point itself.
One consequence of all the above changes is that for the CUDA target we no longer need a wrapper function to invoke the generated entry point any more, because the IR function for the entry point ends up having the correct/expected signature already. This is also the case for CPU when it comes to the `*_Thread` wrapper function, but this change doesn't try to eliminate the wrapper because of a belief that the `*_Thread`-level interface is going away anyway.
Because the IR is now responsible for ensuring the signature of the IR entry point for CUDA and CPU is what is expected, I needed to modify the `slang-ir-entry-point-uniforms` pass to always create an explicit parameter for the entry point uniforms when compiling for CUDA/CPU, even if there were no `uniform` parameters on the entry point as written. This also ended up requiring some tweaks to the parameter layout logic to ensure that CPU/CUDA targets always treat `ConstantBuffer<T>` as a `T*` even in the case where `T` is an empty `struct` type (which happens when we construct a `struct` type to represent the uniform parameters of an entry point with no uniform parameters...).
There are several future changes that can/should build on this work:
* We should change the generated signatures for CUDA kernels, so that they don't rely on `KernelContext` for global-scope parameters. At that point we can avoid generating a `KernelContext` at all for CUDA, except when a program uses global-scope thread-local variables.
* We should figure out how to make the "ABI" for dynamic-dispatch calls ensure that the kernel context is either always passed, or always *not* passed. Making a hard-and-fast rule as part of the calling convention for dynamic calls would ensure that they access through the context continues to work with dynamic calls (this change might break it in some cases).
* We should figure out how to handle the layout for the `KernelContext` in cases where a program is composed of multiple separately-compiled modules. Right now the layout of the `KernelContext` requires global knowledge (as does the pass that introduces explicit initialization for global-scope thread-locals).
* We should try to further clean up the CPU/CUDA C++ emit logic to fall back on the default emit behavior more, now that the various special-case approaches that were taken are no longer needed
* fixup: restore build files to default configuration
Diffstat (limited to 'source/slang/slang-ir-explicit-global-init.cpp')
| -rw-r--r-- | source/slang/slang-ir-explicit-global-init.cpp | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/source/slang/slang-ir-explicit-global-init.cpp b/source/slang/slang-ir-explicit-global-init.cpp new file mode 100644 index 000000000..07397902e --- /dev/null +++ b/source/slang/slang-ir-explicit-global-init.cpp @@ -0,0 +1,207 @@ +// slang-ir-explicit-global-init.cpp +#include "slang-ir-explicit-global-init.h" + +#include "slang-ir-insts.h" + +namespace Slang +{ + +// This pass is responsible for taking code in a form like: +// +// static int gCounter = 1; +// +// void computeMain() +// { +// ... +// int tmp = gCounter++; +// } +// +// and transforming it so that the initialization of global +// variables is performed explicitly at the start of each +// entry-point funciton: +// +// static int gCounter; +// +// void computeMain() +// { +// gCounter = 1; +// ... +// int tmp = gCounter++; +// } +// +// Transforming the code in this way may be required for targets +// that do not support initial-value expressions on global +// variables (e.g., SPIR-V is such a target). It can also be +// useful as a pre-process before other transformations that +// might work with global variables, because after this change +// there cannot be any global variables with initializers. + +struct MoveGlobalVarInitializationToEntryPointsPass +{ + IRModule* m_module; + + SharedIRBuilder* m_sharedBuilder; + + // In the Slang IR, a global variable represents a pointer + // to the storage for the variable but it *also* encodes + // the logic used to compute the initial value of that + // variable. This works because `IRGlobalVar` is a subtype + // of `IRGlobalValueWithCode`, which is also the base + // type of `IRFunc`. Thus a global variable behaves a + // bit like a function, which just happens to compute + // the initial value for the variable. + // + // Part of the work in this pass will be to split those + // two pars of the variable, so that we end up with + // a global variable with not initialization logic, + // plus an ordinary `IRFunc` to compute the initial + // value. + // + // We will compute this split representation and then + // hold onto it so that we can use it for injecting + // the initialization logic into entry points. + // + struct GlobalVarInfo + { + IRGlobalVar* globalVar = nullptr; + IRFunc* initFunc = nullptr; + }; + List<GlobalVarInfo> m_globalVarsWithInit; + + void processModule(IRModule* module) + { + m_module = module; + + SharedIRBuilder sharedBuilder(module); + m_sharedBuilder = &sharedBuilder; + + // We start by looking for global variables with + // initialization logic in the IR, and processing + // each to produce a split variable (now without + // initialization) and function (to compute the + // initial value). + // + for( auto inst : m_module->getGlobalInsts() ) + { + auto globalVar = as<IRGlobalVar>(inst); + if(!globalVar) + continue; + + auto firstBlock = globalVar->getFirstBlock(); + if(!firstBlock) + continue; + + processGlobalVarWithInit(globalVar, firstBlock); + } + + // Then we loop over all the entry points in the + // module and modify them to explicitly initialize + // all the global variables that were identified + // and processed in the first pass. + // + for( auto inst : m_module->getGlobalInsts() ) + { + auto func = as<IRFunc>(inst); + if(!func) + continue; + + if(!func->findDecoration<IREntryPointDecoration>()) + continue; + + processEntryPoint(func); + } + } + + void processGlobalVarWithInit(IRGlobalVar* globalVar, IRBlock* firstBlock) + { + IRBuilder builder(m_sharedBuilder); + builder.setInsertBefore(globalVar); + + // Becaue an `IRGlobalVar` reprsents a pointer to the storage + // for the variable, we need to extract the underlying value + // type from the pointer type. + // + auto valueType = globalVar->getDataType()->getValueType(); + + // We are going to construct an explicit IR function to compute + // the initial value of the variable. That function will alway + // take zero parameters. + // + auto initFunc = builder.createFunc(); + initFunc->setFullType(builder.getFuncType(0, nullptr, valueType)); + + // The basic blocks under teh `IRGlobalVar` define its initialization + // logic, and we can simply move those blocks over to the new + // `IRFunc` to define its behavior. + // + // As a result, the `globalVar` will no longer have its own + // initialization logic, which is a postcondition this pass + // needed to guarantee. + // + IRBlock* nextBlock = nullptr; + for( IRBlock* block = firstBlock; block; block = nextBlock ) + { + nextBlock = block->getNextBlock(); + + block->removeFromParent(); + block->insertAtEnd(initFunc); + } + + // We need to remember the variable and the assocaited + // initial-value function so that we can iterate over + // them in the per-entry-point logic below. + // + GlobalVarInfo info; + info.globalVar = globalVar; + info.initFunc = initFunc; + m_globalVarsWithInit.add(info); + } + + void processEntryPoint(IRFunc* entryPointFunc) + { + // We can only process entry point definitions, not declarations. + // + auto firstBlock = entryPointFunc->getFirstBlock(); + if(!firstBlock) + return; + + // We are going to insert initiailization logic at the start + // of the first block of the entry point. + // + IRBuilder builder(m_sharedBuilder); + builder.setInsertBefore(firstBlock->getFirstOrdinaryInst()); + + for( auto globalVarInfo : m_globalVarsWithInit ) + { + // The earlier step split each global variable into + // a variable with no initialization logic, plus a function + // that can be called to compute the initial value. + // + auto globalVar = globalVarInfo.globalVar; + auto initFunc = globalVarInfo.initFunc; + + // Because the `IRGlobalVar` represents a pointer to + // storage, we need to get the pointed-to type to + // get the type of the initial value. + // + auto valType = globalVar->getDataType()->getValueType(); + + // We compute the initial value for the variable by calling + // the initial-value function with no arguments, and then + // we store that value into the corresponding global. + // + auto initVal = builder.emitCallInst(valType, initFunc, 0, nullptr); + builder.emitStore(globalVar, initVal); + } + } +}; + + /// Move initialization logic off of global variables and onto each entry point +void moveGlobalVarInitializationToEntryPoints( + IRModule* module) +{ + MoveGlobalVarInitializationToEntryPointsPass pass; + pass.processModule(module); +} + +} |
