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-specialize.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-specialize.cpp')
| -rw-r--r-- | source/slang/ir-specialize.cpp | 249 |
1 files changed, 149 insertions, 100 deletions
diff --git a/source/slang/ir-specialize.cpp b/source/slang/ir-specialize.cpp index 144cc008f..0da06580f 100644 --- a/source/slang/ir-specialize.cpp +++ b/source/slang/ir-specialize.cpp @@ -29,6 +29,14 @@ namespace Slang // simplifications/specializations of one category can open // up opportunities for transformations in the other categories. +struct SpecializationContext; + +IRInst* specializeGenericImpl( + IRGeneric* genericVal, + IRSpecialize* specializeInst, + IRModule* module, + SpecializationContext* context); + struct SpecializationContext { // For convenience, we will keep a pointer to the module @@ -203,112 +211,26 @@ struct SpecializationContext // If no existing specialization is found, we need // to create the specialization instead. + // This mostly amounts to evaluating the generic as + // if it were a function being called. // - // Effectively this amounts to "calling" the generic - // on its concrete argument values and computing the - // result it returns. - // - // For now, all of our generics consist of a single - // basic block, so we can "call" them just by - // cloning the instructions in their single block - // into the global scope, using an environment for - // cloning that maps the generic parameters to - // the concrete arguments that were provided - // by the `specialize(...)` instruction. - // - IRCloneEnv env; - - // We will walk through the parameters of the generic and - // register the corresponding argument of the `specialize` - // instruction to be used as the "cloned" value for each - // parameter. - // - // Suppose we are looking at `specialize(g, a, b, c)` and `g` has - // three generic parameters: `T`, `U`, and `V`. Then we will - // be initializing our environment to map `T -> a`, `U -> b`, - // and `V -> c`. + // We will use a free function to do the actual work + // of evaluating the generic, so that the logic + // can be re-used in other cases that need to + // do one-off specialization. // - UInt argCounter = 0; - for( auto param : genericVal->getParams() ) - { - UInt argIndex = argCounter++; - SLANG_ASSERT(argIndex < specializeInst->getArgCount()); - - IRInst* arg = specializeInst->getArg(argIndex); - - env.mapOldValToNew.Add(param, arg); - } + IRInst* specializedVal = specializeGenericImpl(genericVal, specializeInst, module, this); - // We will set up an IR builder for insertion - // into the global scope, at the same location - // as the original generic. - // - IRBuilder builderStorage; - IRBuilder* builder = &builderStorage; - builder->sharedBuilder = &sharedBuilderStorage; - builder->setInsertBefore(genericVal); - // Now we will run through the body of the generic and - // clone each of its instructions into the global scope, - // until we reach a `return` instruction. + // The value that was returned from evaluating + // the generic is the specialized value, and we + // need to remember it in our dictionary of + // specializations so that we don't instantiate + // this generic again for the same arguments. // - for( auto bb : genericVal->getBlocks() ) - { - // We expect a generic to only ever contain a single block. - // - SLANG_ASSERT(bb == genericVal->getFirstBlock()); - - // We will iterate over the non-parameter ("ordinary") - // instructions only, because parameters were dealt - // with explictly at an earlier point. - // - for( auto ii : bb->getOrdinaryInsts() ) - { - // The last block of the generic is expected to end with - // a `return` instruction for the specialized value that - // comes out of the abstraction. - // - // We thus use that cloned value as the result of the - // specialization step. - // - if( auto returnValInst = as<IRReturnVal>(ii) ) - { - auto specializedVal = findCloneForOperand(&env, returnValInst->getVal()); - - // The value that was returned from evaluating - // the generic is the specialized value, and we - // need to remember it in our dictionary of - // specializations so that we don't instantiate - // this generic again for the same arguments. - // - genericSpecializations.Add(key, specializedVal); - - return specializedVal; - } - - // For any instruction other than a `return`, we will - // simply clone it completely into the global scope. - // - IRInst* clonedInst = cloneInst(&env, builder, ii); - - // Any new instructions we create during cloning were - // not present when we initially built our work list, - // so we need to make sure to consider them now. - // - // This is important for the cases where one generic - // invokes another, because there will be `specialize` - // operations nested inside the first generic that refer - // to the second. - // - addToWorkList(clonedInst); - } - } + genericSpecializations.Add(key, specializedVal); - // If we reach this point, something went wrong, because we - // never encountered a `return` inside the body of the generic. - // - SLANG_UNEXPECTED("no return from generic"); - UNREACHABLE_RETURN(nullptr); + return specializedVal; } // The logic for generating a specialization of an IR generic @@ -1302,4 +1224,131 @@ void specializeModule( context.processModule(); } + +IRInst* specializeGenericImpl( + IRGeneric* genericVal, + IRSpecialize* specializeInst, + IRModule* module, + SpecializationContext* context) +{ + // Effectively, specializing a generic amounts to "calling" the generic + // on its concrete argument values and computing the + // result it returns. + // + // For now, all of our generics consist of a single + // basic block, so we can "call" them just by + // cloning the instructions in their single block + // into the global scope, using an environment for + // cloning that maps the generic parameters to + // the concrete arguments that were provided + // by the `specialize(...)` instruction. + // + IRCloneEnv env; + + // We will walk through the parameters of the generic and + // register the corresponding argument of the `specialize` + // instruction to be used as the "cloned" value for each + // parameter. + // + // Suppose we are looking at `specialize(g, a, b, c)` and `g` has + // three generic parameters: `T`, `U`, and `V`. Then we will + // be initializing our environment to map `T -> a`, `U -> b`, + // and `V -> c`. + // + UInt argCounter = 0; + for( auto param : genericVal->getParams() ) + { + UInt argIndex = argCounter++; + SLANG_ASSERT(argIndex < specializeInst->getArgCount()); + + IRInst* arg = specializeInst->getArg(argIndex); + + env.mapOldValToNew.Add(param, arg); + } + + // We will set up an IR builder for insertion + // into the global scope, at the same location + // as the original generic. + // + SharedIRBuilder sharedBuilderStorage; + sharedBuilderStorage.module = module; + sharedBuilderStorage.session = module->getSession(); + + IRBuilder builderStorage; + IRBuilder* builder = &builderStorage; + builder->sharedBuilder = &sharedBuilderStorage; + builder->setInsertBefore(genericVal); + + // Now we will run through the body of the generic and + // clone each of its instructions into the global scope, + // until we reach a `return` instruction. + // + for( auto bb : genericVal->getBlocks() ) + { + // We expect a generic to only ever contain a single block. + // + SLANG_ASSERT(bb == genericVal->getFirstBlock()); + + // We will iterate over the non-parameter ("ordinary") + // instructions only, because parameters were dealt + // with explictly at an earlier point. + // + for( auto ii : bb->getOrdinaryInsts() ) + { + // The last block of the generic is expected to end with + // a `return` instruction for the specialized value that + // comes out of the abstraction. + // + // We thus use that cloned value as the result of the + // specialization step. + // + if( auto returnValInst = as<IRReturnVal>(ii) ) + { + auto specializedVal = findCloneForOperand(&env, returnValInst->getVal()); + return specializedVal; + } + + // For any instruction other than a `return`, we will + // simply clone it completely into the global scope. + // + IRInst* clonedInst = cloneInst(&env, builder, ii); + + // Any new instructions we create during cloning were + // not present when we initially built our work list, + // so we need to make sure to consider them now. + // + // This is important for the cases where one generic + // invokes another, because there will be `specialize` + // operations nested inside the first generic that refer + // to the second. + // + if( context ) + { + context->addToWorkList(clonedInst); + } + } + } + + // If we reach this point, something went wrong, because we + // never encountered a `return` inside the body of the generic. + // + SLANG_UNEXPECTED("no return from generic"); + UNREACHABLE_RETURN(nullptr); +} + +IRInst* specializeGeneric( + IRSpecialize* specializeInst) +{ + auto baseGeneric = as<IRGeneric>(specializeInst->getBase()); + SLANG_ASSERT(baseGeneric); + if(!baseGeneric) return specializeInst; + + auto module = specializeInst->getModule(); + SLANG_ASSERT(module); + if(!module) return specializeInst; + + return specializeGenericImpl(baseGeneric, specializeInst, module, nullptr); +} + + } // namespace Slang |
