diff options
| author | Tim Foley <tfoleyNV@users.noreply.github.com> | 2019-02-05 16:47:25 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-02-05 16:47:25 -0800 |
| commit | 60cc9f24c4bec54561bea873ee943aa3d0973dc2 (patch) | |
| tree | 16e404be181eba50d7d770f373d07cb17d9ac64d /source/slang/ir-link.cpp | |
| parent | 314795b5d8ff5845624f93e152face325659dd0c (diff) | |
Allow entry points to have explicit generic parameters (#826)
* Allow entry points to have explicit generic parameters
Prior to this change, the Slang implementation required users to use global `type_param` declarations in order to specialize a full shader. For example:
```hlsl
type_param L : ILight;
ParameterBlock<L> gLight;
[shader("fragment")]
float4 fs(...)
{ ... gLight.doSomething() ... }
```
With this change we can rewrite code like the above using explicit generics, plus the ability to have `uniform` entry-point parameters:
```hlsl
[shader("fragment")]
float4 fs<L : ILight>(
uniform ParameterBlock<L> light,
...)
{ ... light.doSomething() ... }
```
Having this support in place should make it possible for us to eliminate global generic type parameters and the complications they cause (both at a conceptual and implementation level).
The most central and visible piece of the change is that `EntryPointRequest` now holds a `DeclRef<FuncDecl>` instead of just ` RefPtr<FuncDecl>`, which allows it to refer to a specialization of a generic function.
Various places in the code that refer to the `EntryPointRequest::decl` member now use a `getFuncDecl()` or `getFuncDeclRef()` method as appropriate (see `compiler.h`).
In order to fill in the new data, the `findAndValidateEntryPoint` function has been greaterly overhauled.
The changes to its operation include:
* The by-name lookup step for the entry point function has been adapted to accept either a function or a generic function.
* The generic argument strings provided by API or command line are no longer parsed all the way to `Type`s, but instead just to `Expr`s in the first pass.
* There are now two cases for checking the global generic arguments against their matching parameters. The first case is the new one, where we plug the generic argument `Expr`s into the explicit generic parameters of an entry point (that case re-uses existing semantic checking logic). The second case is the pre-existing code for dealing with global generic type arguments.
The `lower-to-ir.cpp` logic for hadling entry points then had to be extended. Making it deal with a full `DeclRef` instead of just a `Decl` was the easy part (just call `emitDeclRef` instead of `ensureDecl`).
The more interesting bits were:
* We need to carefully add the `IREntryPointDecoration` to the nested function and not the generic in the case where we have a generic entry point. There is a handy `getResolvedInstForDecorations` that can extract the return value for an IR generic so that we can decorate the right hting.
* We need to make sure that in the case where we emit a `specialize` instruction (which normally wouldn't get a linkage decoration), we attach an `[export(...)]` decoration to it with the mangled name of the decl-ref, so that it can be found during the linking step.
The IR linking step is then slightly more complicated because the mangled entry point name could either refer directly to an `IRFunc` or to a `specialize` instruction for a generic entry point. The logic was refactored to first clone the entry point symbol without concern for which case it is (the old code was specific to functions), and then *if* the result is a `specialize` instruction, we attempt to run generic specialization on-demand.
That on-demand specialization is a bit of a kludge, but it deals with the fact that all the downstream passing only expect to see an `IRFunc`. A future cleanup might try to split out that specialization step into its own pass, which ends up being a limited form of the specialization pass.
Since I was already having to touch a lot of the code around IR linking, I went ahead and refactored the signature of the operations. I eliminated the need for the caller to create, pass in, and then destroy an `IRSpecializationState` (really an IR *linking* state), and replaced it with a structure local to the pass (that data structure was a remnant of an older approach in the compiler), and then also renamed the main operation to `linkIR` to reflect what it is doing in our conceptual flow.
Smaller changes made along the way include:
* Refactored `visitGenericAppExpr` to create a subroutine `checkGenericAppWithCheckedArgs` so that it can be used by the entry-point validation logic described above).
* Refactored the declarations around the IR passes in `emitEntryPoint()` (`emit.cpp`), to show that things are more self-contained than they used to be (e.g., that the `TypeLegalizationContext` is now only needed by one pass).
* Refactored the generic specialization code so that there is a stand-along free function that can perform specialization on a `specialize` instruction without all the other context being required. This is only to support the limited specialization that needs to be done as part of linking.
* Updated the `global-type-param.slang` test to actually test entry-point generic parameters. In a later pass we can/should rework all the tests/examples for global type parameters over to use explicit entry-point generic parameters (at which point we should rename the tests as well). For now I am leaving thigns with just one test case, with the expectation that bugs will be found and ironed out as we expand to more tests.
* fixup
* Fixup: don't leave entry-point decorations on stuff we don't want to keep
The IR `[entryPoint]` decoration is effectively a "keep this alive" decoration, which means that attaching it to something we don't intend to keep around can lead to Bad Things.
The approach to generic entry points was attaching `[entryPoint]` to the underlying `IRFunc` because that seemed to make sense, but that meant that the `specialize` instruction at global scope scould instantiate that generic and then keep it alive, even if the resulting function wouldn't be valid according to the language rules.
As a quick fix, I'm attaching `[entryPoint]` to the `specialize` instruction instead in such cases, and then re-attaching it to the result of explicit specialization during linking.
* Port most of remaining test and rename global type parameters
This change ports as many as possible of the existing tests for global type parameters over to use entry-point generic parameters instead. For the most part this is a mechanical change.
A few test cases remain using global generic parameters, as does the `model-viewer` example application.
The reason for this is that the shaders have either or both the following features:
* A vertex and fragment shader that can/shold agree on their parameters
* A type declaration (e.g., a `struct`) that is dependent on one of the generic type parameters
In these cases, it would really only make sense to switch to explicit parameters once we support shader entry points nested inside of a `struct` type, so that we can use an outer generic `struct` as a mechanism to scope the entry points and other type-dependent declrations.
Since global-scope type parameters need to persist for at least a bit longer, I went ahead and renamed all the use sites over to use `type_param` for consistency.
Diffstat (limited to 'source/slang/ir-link.cpp')
| -rw-r--r-- | source/slang/ir-link.cpp | 147 |
1 files changed, 85 insertions, 62 deletions
diff --git a/source/slang/ir-link.cpp b/source/slang/ir-link.cpp index dba4fc2d1..35e0f46b8 100644 --- a/source/slang/ir-link.cpp +++ b/source/slang/ir-link.cpp @@ -659,13 +659,29 @@ void cloneFunctionCommon( } } +// We will forward-declare the subroutine for eagerly specializing +// an IR-level generic to argument values, because `specializeIRForEntryPoint` +// needs to perform this operation even though it is logically part of +// the later generic specialization pass. +// +IRInst* specializeGeneric( + IRSpecialize* specializeInst); + IRFunc* specializeIRForEntryPoint( IRSpecContext* context, EntryPointRequest* entryPointRequest, EntryPointLayout* entryPointLayout) { - // Look up the IR symbol by name - auto mangledName = getMangledName(entryPointRequest->decl); + // We start by looking up the IR symbol that + // matches the mangled name given to the + // function we want to emit. + // + // Note: the function decl-ref may refer to + // a specialization of a generic function, + // so that the mangled name of the decl-ref is + // not the same as the mangled name of the decl. + // + auto mangledName = getMangledName(entryPointRequest->getFuncDeclRef()); RefPtr<IRSpecSymbol> sym; if (!context->getSymbols().TryGetValue(mangledName, sym)) { @@ -674,40 +690,68 @@ IRFunc* specializeIRForEntryPoint( } // TODO: deal with the case where we might - // have multiple versions... + // have multiple (profile-overloaded) versions... + // + auto originalVal = sym->irGlobalValue; + + // We will start by cloning the entry point reference + // like any other global value. + // + auto clonedVal = cloneGlobalValue(context, originalVal); - auto globalValue = sym->irGlobalValue; - if (globalValue->op != kIROp_Func) + // In the case where the user is requesting a specialization + // of a generic entry point, we have a bit of a problem. + // + // This function is expected to return an `IRFunc` and + // subsequent passes expect to find, e.g., layout information + // attached to the parameters of such a func. + // + // In the generic case, the `clonedValue` won't be an + // `IRFunc`, but instead an `IRSpecialize`. + // + if(auto clonedSpec = as<IRSpecialize>(clonedVal)) { - SLANG_UNEXPECTED("expected an IR function"); + // The Right Thing to do here is to perform some + // amount of generic specialization, at least + // until we get back an `IRFunc`. + // + // The dangerous thing is that the generic specialization + // pass can, in principle, change the signature of + // functions, so that attaching parameter layout + // information *after* specialization might not work. + // + // The compromise we make here is to directly + // invoke the logic for specializing a generic. + // + // In theory this isn't valid, because there is no + // way we can register the specialized function we + // create so that it would be re-used by other instantiations + // with the same arguments (because we cannot be + // sure the generic arguments are themselves fully specialized) + // + // In practice this isn't really a problem, because + // we don't want to share the definition between + // an entry point and an ordinary function anyway. + // + clonedVal = specializeGeneric(clonedSpec); + } + + auto clonedFunc = as<IRFunc>(clonedVal); + if(!clonedFunc) + { + SLANG_UNEXPECTED("expected entry point to be a function"); return nullptr; } - auto originalFunc = (IRFunc*)globalValue; - - // Create a clone for the IR function - auto clonedFunc = context->builder->createFunc(); - - // Note: we do *not* register this cloned declaration - // as the cloned value for the original symbol. - // This is kind of a kludge, but it ensures that - // in the unlikely case that the function is both - // used as an entry point and a callable function - // (yes, this would imply recursion...) we actually - // have two copies, which lets us arbitrarily - // transform the entry point to meet target requirements. - // - // TODO: The above statement is kind of bunk, though, - // because both versions of the function would have - // the same mangled name... :( - // We need to clone all the properties of the original - // function, including any blocks, their parameters, - // and their instructions. - cloneFunctionCommon(context, clonedFunc, originalFunc); + if( !clonedFunc->findDecorationImpl(kIROp_EntryPointDecoration) ) + { + context->builder->addEntryPointDecoration(clonedFunc); + } // We need to attach the layout information for // the entry point to this declaration, so that // we can use it to inform downstream code emit. + // context->builder->addLayoutDecoration( clonedFunc, entryPointLayout); @@ -1166,13 +1210,14 @@ struct IRSpecializationState } }; -IRSpecializationState* createIRSpecializationState( +LinkedIR linkIR( EntryPointRequest* entryPointRequest, ProgramLayout* programLayout, CodeGenTarget target, TargetRequest* targetReq) { - IRSpecializationState* state = new IRSpecializationState(); + IRSpecializationState stateStorage; + auto state = &stateStorage; state->programLayout = programLayout; state->target = target; @@ -1232,6 +1277,8 @@ IRSpecializationState* createIRSpecializationState( context->globalVarLayouts.AddIfNotExists(mangledName, globalVarLayout); } + context->builder->setInsertInto(context->getModule()->getModuleInst()); + // for now, clone all unreferenced witness tables // // TODO: This step should *not* be needed with the current IR @@ -1242,39 +1289,9 @@ IRSpecializationState* createIRSpecializationState( if (sym.Value->irGlobalValue->op == kIROp_WitnessTable) cloneGlobalValue(context, (IRWitnessTable*)sym.Value->irGlobalValue); } - return state; -} - -void destroyIRSpecializationState(IRSpecializationState* state) -{ - delete state; -} - -IRModule* getIRModule(IRSpecializationState* state) -{ - return state->irModule; -} - -IRFunc* specializeIRForEntryPoint( - IRSpecializationState* state, - EntryPointRequest* entryPointRequest) -{ - auto translationUnit = entryPointRequest->getTranslationUnit(); - auto originalIRModule = translationUnit->irModule; - if (!originalIRModule) - { - // We should already have emitted IR for the original - // translation unit, and it we don't have it, then - // we are now in trouble. - return nullptr; - } - - auto context = state->getContext(); - auto newProgramLayout = state->newProgramLayout; auto entryPointLayout = findEntryPointLayout(newProgramLayout, entryPointRequest); - // Next, we make sure to clone the global value for // the entry point function itself, and rely on // this step to recursively copy over anything else @@ -1317,13 +1334,19 @@ IRFunc* specializeIRForEntryPoint( context->builder->addLayoutDecoration(clonedType, taggedUnionTypeLayout); } - - // TODO: *technically* we should consider the case where // we have global variables with initializers, since // these should get run whether or not the entry point // references them. - return irEntryPoint; + + // Now that we've cloned the entry point and everything + // it refers to, we can package up the data we return + // to the caller. + // + LinkedIR linkedIR; + linkedIR.module = state->irModule; + linkedIR.entryPoint = irEntryPoint; + return linkedIR; } |
