summaryrefslogtreecommitdiff
path: root/source/slang/slang-ir-generics-lowering-context.cpp
diff options
context:
space:
mode:
authorTim Foley <tfoleyNV@users.noreply.github.com>2020-11-19 01:26:43 -0800
committerGitHub <noreply@github.com>2020-11-19 01:26:43 -0800
commit4459d4428761b0581b221c52eaea595d1b257a9f (patch)
treeff2f3558afb82ee0d1ce0e956b647b0a5e053a9e /source/slang/slang-ir-generics-lowering-context.cpp
parentb59451020eee59cd52e4d8231360ebed4fc59adb (diff)
Unify handling of static and dynamic dispatch for interfaces (#1612)
Overview ======== Prior to this change, we had two different code generation strategies for interface/existential types in Slang, that didn't always play nicely together: * The "legacy" static specialization approach could handle plugging in an arbitrary concrete type for an existential type parameter (including types with resources, etc.), but wouldn't work well with things like a `StructuredBuffer<>` of an interface type, and requires somewhat counter-intuitive layout rules to make work. * The new dynamic dispatch approach produces simpler, more easily understood layouts by assuming that values of interface type can fit into a fixed number of bytes. The tradeoff there is that it cannot handle types that include resources (only POD types). The goal of this change is to make it so that the two strategies can co-exist. In particular, in cases where a shader is amenable to both static specialization and dynamic dispatch, the type layouts should agree. In order to make the type layouts agree, we: * Declare that *all* values of existential type reserve storage according to the dynamic-dispatch rules (so 16 bytes for the RTTI and witness-table information, plus whatever bytes are needed to story "any value" of a conforming type). * Then we modify the "legacy" layout rules so that if a value of concrete type can fit in the reserved "any value" space for a given interface, then it is laid out there exactly like the dynamic dispatch rules would do. Otherwise, we fall back to the previous legacy rules (since we don't need to agree with the dynamic-dispatch layout on types that can't be used with dynamic dispatch). Details ======= * Renamed `ExistentialBox` to `BoundInterfaceType` to better clarify how it relates to `BindExistentialsType` * Unconditionally apply the `lowerGenerics` pass during emit, since it is now responsible for aspects of the lowering of existential types when specialization is used. * Made IR type layout take the target into account, so that the layout of resource types can vary by target (e.g., being POD on some targets, and invalid on others) * Cleaned up some issues around using global shader parameters as the "key" for their layout information in the global-scope layout (only comes up when there are global-scope `uniform` parameters) * Made there be a default any-value size (16) instead of making it be an error to leave out. This was the simplest option; we could try to go back to having an error, but we'd need to only issue it if we are sure a type/interface is being used with dynamic dispatch, since static dispatch doesn't have to obey the restrictions. * Changed lowering of existential types to tuples so that bound interfaces where the concrete type won't fit use a "pseudo-pointer" instead of an "any-value" to hold the payload * Changed IR type legalization to handle the "pseudo-pointer" case and apply layout information from an interface type over to the payload part when static specialization was used. * Changed some details of how witness tables were being lowered, so that we didn't have to create "proxy" witness tables for the constraints on associated types (just use the actual requirement entries we generate) * Changed witness tables so that they know the subtype doing the conforming * Added logic so that we don't generate pack/unpack logic and witness table wrapper functions for types that are incompatible with any-value/dynamic dispatch for a given interface. * Changed the core AST-level type layout logic to use the dynamic-dispatch layout in case things fit, and the legacy static specialization case when things don't (while also reserving space for the dynamic-dispatch fields) * Changed a bunch of test cases for static specialization to properly use the new layout (which introduces new buffers in some cases, and moves data around in others). Future Work =========== The experience of trying to reconcile our older way of handling interface-type specialization with our newer model (that supports dynamic dispatch) makes it clear that we really need to make similar changes to our handling of generic type parameters on entry points and at the global scope. A future change should make it so that a global type parameter is lowered with a type layout similar to a value parameter of interface type, including the RTTI and witness-table pieces, and just leaving out the "any value" piece. A similar translation strategy should apply to entry-point generic parameters (mirroring how we lower generic functions for dynamic dispatch already), and value specialization parameters. Co-authored-by: Yong He <yonghe@outlook.com>
Diffstat (limited to 'source/slang/slang-ir-generics-lowering-context.cpp')
-rw-r--r--source/slang/slang-ir-generics-lowering-context.cpp174
1 files changed, 155 insertions, 19 deletions
diff --git a/source/slang/slang-ir-generics-lowering-context.cpp b/source/slang/slang-ir-generics-lowering-context.cpp
index 4c4224295..5ae19391b 100644
--- a/source/slang/slang-ir-generics-lowering-context.cpp
+++ b/source/slang/slang-ir-generics-lowering-context.cpp
@@ -66,7 +66,7 @@ namespace Slang
// For now the only type info we encapsualte is type size.
IRSizeAndAlignment sizeAndAlignment;
- getNaturalSizeAndAlignment((IRType*)typeInst, &sizeAndAlignment);
+ getNaturalSizeAndAlignment(targetReq, (IRType*)typeInst, &sizeAndAlignment);
builder->addRTTITypeSizeDecoration(result, sizeAndAlignment.size);
// Give a name to the rtti object.
@@ -118,12 +118,19 @@ namespace Slang
}
if (anyValueSize == kInvalidAnyValueSize)
{
- sink->diagnose(type->sourceLoc, Diagnostics::dynamicInterfaceLacksAnyValueSizeAttribute, type);
+ // We could conceivably make it an error to have an associated type
+ // without an `[anyValueSize(...)]` attribute, but then we risk
+ // producing error messages even when doing 100% static specialization.
+ //
+ // It is simpler to use a reasonable default size and treat any
+ // type without an explicit attribute as using that size.
+ //
+ anyValueSize = kDefaultAnyValueSize;
}
return builder->getAnyValueType(anyValueSize);
}
- IRType* SharedGenericsLoweringContext::lowerType(IRBuilder* builder, IRInst* paramType, const Dictionary<IRInst*, IRInst*>& typeMapping)
+ IRType* SharedGenericsLoweringContext::lowerType(IRBuilder* builder, IRInst* paramType, const Dictionary<IRInst*, IRInst*>& typeMapping, IRType* concreteType)
{
if (!paramType)
return nullptr;
@@ -137,7 +144,6 @@ namespace Slang
return builder->getRTTIHandleType();
}
- IRIntegerValue anyValueSize = kInvalidAnyValueSize;
switch (paramType->op)
{
case kIROp_WitnessTableType:
@@ -151,19 +157,28 @@ namespace Slang
{
if (isBuiltin(anyValueSizeDecor->getConstraintType()))
return (IRType*)paramType;
- anyValueSize = getInterfaceAnyValueSize(anyValueSizeDecor->getConstraintType(), paramType->sourceLoc);
+ auto anyValueSize = getInterfaceAnyValueSize(anyValueSizeDecor->getConstraintType(), paramType->sourceLoc);
return builder->getAnyValueType(anyValueSize);
}
- sink->diagnose(paramType, Diagnostics::unconstrainedGenericParameterNotAllowedInDynamicFunction, paramType);
- return builder->getAnyValueType(kInvalidAnyValueSize);
+ // We could conceivably make it an error to have a generic parameter
+ // without an `[anyValueSize(...)]` attribute, but then we risk
+ // producing error messages even when doing 100% static specialization.
+ //
+ // It is simpler to use a reasonable default size and treat any
+ // type without an explicit attribute as using that size.
+ //
+ return builder->getAnyValueType(kDefaultAnyValueSize);
}
case kIROp_ThisType:
+ {
+
if (isBuiltin(cast<IRThisType>(paramType)->getConstraintType()))
return (IRType*)paramType;
- anyValueSize = getInterfaceAnyValueSize(
+ auto anyValueSize = getInterfaceAnyValueSize(
cast<IRThisType>(paramType)->getConstraintType(),
paramType->sourceLoc);
return builder->getAnyValueType(anyValueSize);
+ }
case kIROp_AssociatedType:
{
return lowerAssociatedType(builder, paramType);
@@ -172,12 +187,97 @@ namespace Slang
{
if (isBuiltin(paramType))
return (IRType*)paramType;
- // An existential type translates into a tuple of (AnyValue, WitnessTable, RTTI*)
- anyValueSize = getInterfaceAnyValueSize(paramType, paramType->sourceLoc);
+
+ // In the dynamic-dispatch case, a value of interface type
+ // is going to be packed into the "any value" part of a tuple.
+ // The size of the "any value" part depends on the interface
+ // type (e.g., it might have an `[anyValueSize(8)]` attribute
+ // indicating that 8 bytes needs to be reserved).
+ //
+ auto anyValueSize = getInterfaceAnyValueSize(paramType, paramType->sourceLoc);
+
+ // If there is a non-null `concreteType` parameter, then this
+ // interface type is one that has been statically bound (via
+ // specialization parameters) to hold a value of that concrete
+ // type.
+ //
+ IRType* pendingType = nullptr;
+ if( concreteType )
+ {
+ // Because static specialization is being used (at least in part),
+ // we do *not* have a guarantee that the `concreteType` is one
+ // that can fit into the `anyValueSize` of the interface.
+ //
+ // We will use the IR layout logic to see if we can compute
+ // a size for the type, which can lead to a few different outcomes:
+ //
+ // * If a size is computed successfully, and it is smaller than or
+ // equal to `anyValueSize`, then the concrete value will fit into
+ // the reserved area, and the layout will match the dynamic case.
+ //
+ // * If a size is computed successfully, and it is larger than
+ // `anyValueSize`, then the concrete value cannot fit into the
+ // reserved area, and it needs to be stored out-of-line.
+ //
+ // * If size cannot be computed, then that implies that the type
+ // includes non-ordinary data (e.g., a `Texture2D` on a D3D11
+ // target), and cannot possible fit into the reserved area
+ // (which consists of only uniform bytes). In this case, the
+ // value must be stored out-of-line.
+ //
+ IRSizeAndAlignment sizeAndAlignment;
+ Result result = getNaturalSizeAndAlignment(targetReq, concreteType, &sizeAndAlignment);
+ if(SLANG_FAILED(result) || (sizeAndAlignment.size > anyValueSize))
+ {
+ // If the value must be stored out-of-line, we construct
+ // a "pseudo pointer" to the concrete type, and the
+ // constructed tuple will contain such a pseudo pointer.
+ //
+ // Semantically, the pseudo pointer behaves a bit like
+ // a pointer to the concrete type, in that it can be
+ // (pseudo-)dereferenced to produce a value of the chosen
+ // type.
+ //
+ // In terms of layout, the pseudo pointer occupies no
+ // space in the parent tuple/type, and will be automatically
+ // moved out-of-line by a later type legalization pass.
+ //
+ pendingType = builder->getPseudoPtrType(concreteType);
+ }
+ }
+
auto anyValueType = builder->getAnyValueType(anyValueSize);
auto witnessTableType = builder->getWitnessTableIDType((IRType*)paramType);
auto rttiType = builder->getRTTIHandleType();
- auto tupleType = builder->getTupleType(rttiType, witnessTableType, anyValueType);
+
+ IRType* tupleType = nullptr;
+ if( !pendingType )
+ {
+ // In the oridnary (dynamic) case, an existential type decomposes
+ // into a tuple of:
+ //
+ // (RTTI, witness table, any-value).
+ //
+ tupleType = builder->getTupleType(rttiType, witnessTableType, anyValueType);
+ }
+ else
+ {
+ // In the case where static specialization mandateds out-of-line storage,
+ // an existential type decomposes into a tuple of:
+ //
+ // (RTTI, witness table, pseudo pointer, any-value)
+ //
+ tupleType = builder->getTupleType(rttiType, witnessTableType, pendingType, anyValueType);
+ //
+ // Note that in each of the cases, the third element of the tuple
+ // is a representation of the value being stored in the existential.
+ //
+ // Also note that each of these representations has the same
+ // size and alignment when only "ordinary" data is considered
+ // (the pseudo-pointer will eventually be legalized away, leaving
+ // behind a tuple with equivalent layout).
+ }
+
return tupleType;
}
case kIROp_lookup_interface_method:
@@ -196,12 +296,20 @@ namespace Slang
interfaceType,
lookupInterface->getRequirementKey());
SLANG_ASSERT(reqVal && reqVal->op == kIROp_AssociatedType);
- return lowerType(builder, reqVal, typeMapping);
+ return lowerType(builder, reqVal, typeMapping, nullptr);
}
- case kIROp_ExistentialBoxType:
+ case kIROp_BoundInterfaceType:
{
- auto existentialBoxType = static_cast<IRExistentialBoxType*>(paramType);
- return lowerType(builder, existentialBoxType->getInterfaceType(), typeMapping);
+ // A bound interface type represents an existential together with
+ // static knowledge that the value stored in the extistential has
+ // a particular concrete type.
+ //
+ // We handle this case by lowering the underlying interface type,
+ // but pass along the concrete type so that it can impact the
+ // layout of the interface type.
+ //
+ auto boundInterfaceType = static_cast<IRBoundInterfaceType*>(paramType);
+ return lowerType(builder, boundInterfaceType->getInterfaceType(), typeMapping, boundInterfaceType->getConcreteType());
}
default:
{
@@ -209,7 +317,7 @@ namespace Slang
List<IRInst*> loweredOperands;
for (UInt i = 0; i < paramType->getOperandCount(); i++)
{
- loweredOperands.add(lowerType(builder, paramType->getOperand(i), typeMapping));
+ loweredOperands.add(lowerType(builder, paramType->getOperand(i), typeMapping, nullptr));
if (loweredOperands.getLast() != paramType->getOperand(i))
translated = true;
}
@@ -237,12 +345,40 @@ namespace Slang
IRIntegerValue SharedGenericsLoweringContext::getInterfaceAnyValueSize(IRInst* type, SourceLoc usageLocation)
{
+ SLANG_UNUSED(usageLocation);
+
if (auto decor = type->findDecoration<IRAnyValueSizeDecoration>())
{
return decor->getSize();
}
- sink->diagnose(type->sourceLoc, Diagnostics::dynamicInterfaceLacksAnyValueSizeAttribute, type);
- sink->diagnose(usageLocation, Diagnostics::seeInterfaceUsage, type);
- return kInvalidAnyValueSize;
+
+ // We could conceivably make it an error to have an interface
+ // without an `[anyValueSize(...)]` attribute, but then we risk
+ // producing error messages even when doing 100% static specialization.
+ //
+ // It is simpler to use a reasonable default size and treat any
+ // type without an explicit attribute as using that size.
+ //
+ return kDefaultAnyValueSize;
+ }
+
+
+ bool SharedGenericsLoweringContext::doesTypeFitInAnyValue(IRType* concreteType, IRInterfaceType* interfaceType)
+ {
+ auto anyValueSize = getInterfaceAnyValueSize(interfaceType, interfaceType->sourceLoc);
+
+ IRSizeAndAlignment sizeAndAlignment;
+ Result result = getNaturalSizeAndAlignment(targetReq, concreteType, &sizeAndAlignment);
+ if(SLANG_FAILED(result) || (sizeAndAlignment.size > anyValueSize))
+ {
+ // The value does not fit, either because it is too large,
+ // or because it includes types that cannot be stored
+ // in uniform/ordinary memory for this target.
+ //
+ return false;
+ }
+
+ return true;
}
+
}