diff options
| author | Tim Foley <tfoleyNV@users.noreply.github.com> | 2020-12-11 08:50:43 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-12-11 08:50:43 -0800 |
| commit | 992778e25c444932921ce92fe7934893b2aca35f (patch) | |
| tree | 4351c61079da5586c5f469dc8c989364c7a2bd4e /source/slang/slang-ir-link.cpp | |
| parent | 4337338ed2d9525b4638f32c6b91ef61b69e41cd (diff) | |
Add first steps toward a "capability" system (#1636)
* Add first steps toward a "capability" system
We already have cases in the stdlib where we mark declarations as being specific to certain targets, e.g.:
```
// My ordinary function to add two numbers.
// Works everywhere.
//
void myFunc(int a, int b) { return a + b; }
// On the "coolgpu" target, we can use a secret intrinsic
// that adds numbers even faster!
//
__specialized_for_target(coolgpu)
void myFunc(int a, int b) { return __secretIntrinsic(a, b); }
```
The existing logic for dealing with these modifiers (`__specialized_for_target` and `__target_intrinsic`) was almost entirely string-based. We would turn the chosen compilation target into a string, and then use that to try and search for the "best" definition of a function at a few steps:
* During IR linking, we always pick one definition of an `[import]`ed function, and that definition will be the one with the "best" target-specialization modifier (if any)
* During final code generation, we always look up the "best" target-intrinsic modifier, and use it as the template for the code we output.
This change preserves the basic flow there, but replaces the ad hoc string-based logic with something a bit more principled, in terms of a new `CapabilitySet` type.
A `CapabilitySet` represents a set of zero or more atomic features (here represented as `CapabilityAtom`s). What a `CapabilitySet` means depends on how and where it is used:
* A compilation target implies a `CapabilitySet` where the contents of the set are the features the target *supports*.
* A `CapabilitySet` attached to a declaration (or a modifier on that declaration) describes a set of feature that declaration *requires*.
The current implementation of `CapabilitySet` is wasteful and inefficient, but that is something we can iterate on over time.
In practice, most of the current code only ever uses capability sets that are either empty (because they represent a function with no specific requirements) or singleton (because they represent asingle atomic capability like "is a GLSL target," "is an HLSL target," etc.).
The main goal here was to put in the skeleton of a new system, including some of the features it might need down the line, and then to leave changes that eventually use the greater flexibility for later. Eventually, the capability system should encompass:
* Differences between shader model versions, GLSL versions, SPIR-V versions, etc. (currently tracked with other modifiers)
* Optional extensions, and functions that are made available only with certain extensions (currently tracked with other modifiers)
* Front-end checking that the call graph of a program doesn't violate any capability-requirements (e.g., having a GLSL+HLSL portable function call a GLSL-only subroutine)
* Hypothetically we can also try to fold stage-specific (vertex-only, fragment-only, etc.) functions into this system, but doing so would require more linker cleverness if we allow overloading on stages (since we might have to clone a caller if it calls through to a callee with multiple stage-specific versions)
One important complication that the system has to deal with just because of the "do what I mean" nature of the current compiler is that somethings a current Slang user might compile for target X and specify version N, but then use a function that actually requires version N+1 of that target. Currently the Slang compiler silently "upgrades" the version(s) used by user code in these cases, because it is often what users want in cross-compilation scenarios.
Dealing with the "silent upgrade" situation requires us to be a little careful and sometimes pick a "best" capability set that doesn't appear to be supported on our target. Refining that system and potentially getting rid of the "do what I mean" behavior over time could be a goal for future changes.
* fixup: handle case where value is incompatible during linking
Diffstat (limited to 'source/slang/slang-ir-link.cpp')
| -rw-r--r-- | source/slang/slang-ir-link.cpp | 165 |
1 files changed, 73 insertions, 92 deletions
diff --git a/source/slang/slang-ir-link.cpp b/source/slang/slang-ir-link.cpp index c96286eec..e4c1bad85 100644 --- a/source/slang/slang-ir-link.cpp +++ b/source/slang/slang-ir-link.cpp @@ -1,6 +1,7 @@ // slang-ir-link.cpp #include "slang-ir-link.h" +#include "slang-capability.h" #include "slang-ir.h" #include "slang-ir-insts.h" #include "slang-mangle.h" @@ -37,6 +38,9 @@ struct IRSharedSpecContext // The code-generation target in use CodeGenTarget target; + // The API-level target request + TargetRequest* targetReq = nullptr; + // The specialized module we are building RefPtr<IRModule> module; @@ -915,79 +919,35 @@ IRFunc* specializeIRForEntryPoint( // Get a string form of the target so that we can // use it to match against target-specialization modifiers // -// TODO: We shouldn't be using strings for this. -String getTargetName(IRSpecContext* context) +CapabilitySet getTargetCapabilities(IRSpecContext* context) { - switch( context->shared->target ) - { - case CodeGenTarget::HLSL: - return "hlsl"; - - case CodeGenTarget::GLSL: - return "glsl"; - - case CodeGenTarget::CSource: - return "c"; - - case CodeGenTarget::CPPSource: - return "cpp"; - - case CodeGenTarget::CUDASource: - return "cuda"; - - case CodeGenTarget::SPIRV: - return "spirv"; - - - default: - SLANG_UNEXPECTED("unhandled case"); - UNREACHABLE_RETURN("unknown"); - } + return context->getShared()->targetReq->getTargetCaps(); } -// How specialized is a given declaration for the chosen target? -enum class TargetSpecializationLevel -{ - specializedForOtherTarget = 0, - notSpecialized, - specializedForTarget, -}; - -TargetSpecializationLevel getTargetSpecialiationLevel( - IRInst* inVal, - String const& targetName) + /// Get the most appropriate ("best") capability requirements for `inVal` based on the `targetCaps`. +static CapabilitySet _getBestSpecializationCaps( + IRInst* inVal, + CapabilitySet const& targetCaps) { - // HACK: Currently the front-end is placing modifiers related - // to target specialization on nodes like functions, even when - // those functions are being returned by a generic. This - // means that we need to try and inspect the value being - // returned by the generic if we are looking at a generic. - IRInst* val = inVal; - while( auto genericVal = as<IRGeneric>(val) ) - { - auto firstBlock = genericVal->getFirstBlock(); - if(!firstBlock) break; + IRInst* val = getResolvedInstForDecorations(inVal); - auto returnInst = as<IRReturnVal>(firstBlock->getLastInst()); - if(!returnInst) break; + // If the instruction has no target-related decorations, + // then it is implied to be an unspecialized, target-independent + // declaration. + // + // Such a declaration amounts to an empty set of capabilities. + // + if(!val->findDecoration<IRTargetDecoration>()) + return CapabilitySet::makeEmpty(); - val = returnInst->getVal(); + if( auto targetDecoration = findBestTargetDecoration(inVal, targetCaps) ) + { + return targetDecoration->getTargetCaps(); } - - TargetSpecializationLevel result = TargetSpecializationLevel::notSpecialized; - for(auto dd : val->getDecorations()) + else { - if(dd->op != kIROp_TargetDecoration) - continue; - - auto decoration = (IRTargetDecoration*) dd; - if(String(decoration->getTargetName()) == targetName) - return TargetSpecializationLevel::specializedForTarget; - - result = TargetSpecializationLevel::specializedForOtherTarget; + return CapabilitySet::makeInvalid(); } - - return result; } // Is `newVal` marked as being a better match for our @@ -1006,43 +966,61 @@ bool isBetterForTarget( return true; } - String targetName = getTargetName(context); - // For right now every declaration might have zero or more - // modifiers, representing the targets for which it is specialized. - // Each modifier has a single string "tag" to represent a target. - // We thus decide that a declaration is "more specialized" by: + // decorations, representing the capabilities for which it is specialized. + // Each decorations has a `CapabilitySet` to represent what it requires of a target. + // + // We need to look at all the candidate declarations for a symbol + // and pick the one that has the "most specialized" set of capabilities + // for our chosen target. // - // - Does it have a modifier with a tag with the string for the current target? - // If yes, it is the most specialized it can be. + // In principle, this should be as simple as: // - // - Does it have a no tags? Then it is "unspecialized" and that is okay. + // * Ignore all decorations with capability sets that aren't subsets + // of the capabilities of our target. // - // - Does it have a modifier with a tag for a *different* target? - // If yes, then it shouldn't even be usable on this target. + // * From the remaining decorations, pick the one that is a superset + // of all the others (and give an ambiguity error if there is + // no unique "best" option). // - // Longer term a better approach is to think of this in terms - // of a "disjunction of conjunctions" that is: + // In practice, the choice is complicated by the way that we currently + // have the compiler automatically deduce dependencies on extensions + // or other features that were not included as part of the target + // description by the user. // - // (A and B and C) or (A and D) or (E) or (F and G) ... + // In order to preserve the ability to infer more specialized requirements + // than what the target includes, we change the two steps slightly: // - // A code generation target would then consist of a - // conjunction of individual tags: + // * Ignore all decorations with capability sets that are *incompatible* + // with the capabilities of our target, such that they could never be + // used together. // - // (HLSL and SM_4_0 and Vertex and ...) + // * From all the remaining decorations, pick the one that is "better" + // than all the others in that it is either a supserset of each other + // candidate, or for each feature that another candidate requires, + // it requires a "better" feature that covers the same ground. // - // A declaration is *applicable* on a target if one of - // its conjunctions of tags is a subset of the target's. + // Note: This approach isn't really sound, so we are likely to have + // to tweak it over time. Most notably, we probably need/want to + // push back on the automatic inference of extensions/versions in + // the compiler as much as possible. // - // One declaration is *better* than another on a target - // if it is applicable and its tags are a superset - // of the other's. + CapabilitySet targetCaps = getTargetCapabilities(context); + CapabilitySet newCaps = _getBestSpecializationCaps(newVal, targetCaps); + CapabilitySet oldCaps = _getBestSpecializationCaps(oldVal, targetCaps); - auto newLevel = getTargetSpecialiationLevel(newVal, targetName); - - auto oldLevel = getTargetSpecialiationLevel(oldVal, targetName); - if(newLevel != oldLevel) - return UInt(newLevel) > UInt(oldLevel); + // If either value returned an invalid capability set, it implies + // that it cannot be used on this target at all, and the other + // value should be considered better by default. + // + // Note: if both of the candidate values we have are incompatible + // with our target, then it doesn't matter which we favor. + // + if(newCaps.isInvalid()) return false; + if(oldCaps.isInvalid()) return true; + + if(newCaps != oldCaps) + return newCaps.implies(oldCaps); // All preceding factors being equal, an `[export]` is better // than an `[import]`. @@ -1300,7 +1278,8 @@ void initializeSharedSpecContext( IRSharedSpecContext* sharedContext, Session* session, IRModule* module, - CodeGenTarget target) + CodeGenTarget target, + TargetRequest* targetReq) { SharedIRBuilder* sharedBuilder = &sharedContext->sharedBuilderStorage; @@ -1318,6 +1297,7 @@ void initializeSharedSpecContext( sharedBuilder->module = module; sharedContext->module = module; sharedContext->target = target; + sharedContext->targetReq = targetReq; } struct IRSpecializationState @@ -1372,7 +1352,8 @@ LinkedIR linkIR( sharedContext, compileRequest->getSession(), nullptr, - target); + target, + targetReq); state->irModule = sharedContext->module; |
