// slang-ir-collect-global-uniforms.cpp #include "slang-ir-collect-global-uniforms.h" #include "slang-ir-insts.h" namespace Slang { // This file implements a pass that takes input code like: // // uniform int gA; // uniform float gB; // // void main() { ... gA + gB ... } // // and transforms it into code like: // // struct GlobalParams // { // int gA; // float gB; // } // // ConstantBuffer globalParams; // // void main() { ... globalParams.gA + globalParams.gB ... } // // The main consequence of this transformation is that we can support // global `uniform` shader parameters of "ordinary" data types when // compiling for targets that don't directly support that feature // (e.g., GLSL/SPIR-V). // // In addition, on targets that already support a similar transformation // (e.g., when compiling to DXBC/DXIL via fxc/dxc), making the `globalParams` // constant buffer explicit allows us to control the binding that is // assigned to it using our existing logic for automatic layout, rather than // being left at the whims of the undocumented defaults of those compilers. // // A final consequence of this pass is that for targets where *all* // shader parameters use "ordinary" data types (because there are no // non-first-class types), we end up with a conveniently packaged up // single parameter and type that encapsulates all of the shader inputs. // struct CollectGlobalUniformParametersContext { // In orderto perform our transformation, we need access to the module // to be transformed, as well as the layout information representing // the global-scope shader parameters. // IRModule* module; IRVarLayout* globalScopeVarLayout; CodeGenTarget target = CodeGenTarget::Unknown; IRGlobalParam* _getGlobalParamFromLayoutFieldKey(IRInst* key) { switch (key->getOp()) { case kIROp_GlobalParam: return cast(key); case kIROp_MakeExistential: case kIROp_WrapExistential: return as(key->getOperand(0)); default: return nullptr; } } // This is a relatively simple pass, and it is all driven // by a single subroutine. // void processModule() { if (!globalScopeVarLayout) { return; } // We start by looking at the layout that was computed for the global-scope // parameters to determine how the parameters are supposed to be pacakged. // // This step relies on the earlier layout computation logic to have implemented // any target-specific policies around how the global-scope parametesr are // to be passed, and therefore we avoid trying to make any target-specific // decisions in this pass. // auto globalScopeTypeLayout = globalScopeVarLayout->getTypeLayout(); auto globalParamsTypeLayout = globalScopeTypeLayout; // One example of a difference that might appear in the global-scope layout // depending on the target is that the global-scope parameters might end // up just pacakged as a `struct`, *or* they might be packaged up in a // `ConstantBuffer<...>` or other parameter group that wraps that `struct`. // IRParameterGroupTypeLayout* globalParameterGroupTypeLayout = as(globalParamsTypeLayout); if (globalParameterGroupTypeLayout) { // In the case where there is a wrapping `ConstantBuffer<...>`, we want to // get at the element type of that constant buffer, because it represents // the packaged-up `struct` that we want to produce. // globalParamsTypeLayout = globalParameterGroupTypeLayout->getElementVarLayout()->getTypeLayout(); } // As a special case (in order to avoid disruption to any downstream passes), // if the layout for the global-scope parameters doesn't include any "ordinary" // data (represented as `LayoutResourceKind::Uniform`), then we will not do // the "packaging up" step at all. // // This means that the current pass will not change the results for a majority // of targets (notably, all the current graphics APIs) *unless* global shader // parameters are declared that use "ordinary' data. // // TODO: eventually we should remove this special case, and confirm that the resulting // logic works across all shaders (it should). Doing so will be a necessary // step if want to start applying the packaging-up of global-scope parameters on // a per-module basis. // if (!globalParameterGroupTypeLayout && !globalParamsTypeLayout->findSizeAttr(LayoutResourceKind::Uniform)) return; // We expect that the layout for the global-scope parameters is always // computed for a `struct` type. // auto globalParamsStructTypeLayout = as(globalParamsTypeLayout); SLANG_ASSERT(globalParamsStructTypeLayout); // We need to construct a single IR parameter that will represent // the collected global-scope parameters. The `IRBuilder` we construct // for this will also be used when replacing the individual parameters. // IRBuilder builderStorage(module); IRBuilder* builder = &builderStorage; builder->setInsertInto(module->getModuleInst()); // The packaged-up global parameters will be turned into fields of // a new global IR `struct` type, which we give a name of `GlobalParams` // so that it is identifiable in the output. // // Note: the equivalent in fxc/dxc is the `$Globals` constant buffer. // auto wrapperStructType = builder->createStructType(); builder->addNameHintDecoration( wrapperStructType, UnownedTerminatedStringSlice("GlobalParams")); builder->addBinaryInterfaceTypeDecoration(wrapperStructType); // If the computed layout used a bare `struct` type, then we will use // our `GlobalParams` struct as-is, but if the layout involved an // implicitly defined `ConstantBuffer<...>`, this is where we construct // the type `ConstantBuffer`. // IRType* wrapperParamType = wrapperStructType; if (globalParameterGroupTypeLayout) { auto wrapperParamGroupType = builder->getConstantBufferType( wrapperStructType, builder->getType(kIROp_DefaultBufferLayoutType)); wrapperParamType = wrapperParamGroupType; } // Now that we've determined what the type of the new single global parameter // should be, we can go ahead and emit it into the IR module. // // We will call the implicit parameter for all the globals `globalParams`. // IRGlobalParam* wrapperParam = builder->createGlobalParam(wrapperParamType); builder->addLayoutDecoration(wrapperParam, globalScopeVarLayout); builder->addNameHintDecoration(wrapperParam, UnownedTerminatedStringSlice("globalParams")); // With the setup work out of the way, we can iterate over the global // parameters that were present in the layout information (they are // represented as the fields of the global-scope `struct` layout). // // For CUDA targets, we need to ensure unsized arrays come last to satisfy // the layout constraint in slang-ir-layout.cpp auto fieldAttrs = globalParamsStructTypeLayout->getFieldLayoutAttrs(); // Create ordered field list - for CUDA, put unsized arrays last List orderedFields; if (target == CodeGenTarget::CUDASource) { // For CUDA: separate regular and unsized array fields List regularFields; List unsizedArrayFields; for (auto fieldLayoutAttr : fieldAttrs) { auto globalParam = _getGlobalParamFromLayoutFieldKey(fieldLayoutAttr->getFieldKey()); if (globalParam && as(globalParam->getDataType())) { unsizedArrayFields.add(fieldLayoutAttr); } else { regularFields.add(fieldLayoutAttr); } } // Add regular fields first, then unsized arrays for (auto field : regularFields) orderedFields.add(field); for (auto field : unsizedArrayFields) orderedFields.add(field); } else { // For other targets: preserve original order for (auto field : fieldAttrs) orderedFields.add(field); } for (auto fieldLayoutAttr : orderedFields) { // We expect the IR layout pass to have encoded field per-field // layout so that the "key" for the field is the corresponding // global shader parameter. // Save the original global param before replacement. auto globalParam = _getGlobalParamFromLayoutFieldKey(fieldLayoutAttr->getFieldKey()); auto globalParamLayout = fieldLayoutAttr->getLayout(); // Set insert position to a valid instruction under the global parent scope so we can // create struct keys. builder->setInsertAfter(fieldLayoutAttr->getFieldKey()); // This global parameter needs to be turned into a field of the global // parameter structure type, and that field will need a key. // auto fieldKey = builder->createStructKey(); // In order to make sure that the existing IR layout information for // the global scope remains valid, we will swap out the key in the // per-field layout information to reference the key we created // instead of the existing parameter (which we will be removing). // fieldLayoutAttr = as( builder->replaceOperand(fieldLayoutAttr->getOperands(), fieldKey)); // If the given parameter doesn't contribute to uniform/ordinary usage, then // we can safely leave it at the global scope and potentially avoid a lot // of complications that might otherwise arise (that is, we don't need to worry // about downstream passes that might have worked for a simple global parameter, // but that would not work for one nested inside a structure. // // TODO: It would be more consistent and robust to *always* wrap up // these global parameters appropriately, and ensure that all the downstream // passes can handle that case, since they would need to do so in general. // if (!globalParamLayout->getTypeLayout()->findSizeAttr(LayoutResourceKind::Uniform)) continue; SLANG_ASSERT(globalParam); // The new structure field will need to have whatever decorations // had been put on the global parameter (notably including any name hint) // globalParam->transferDecorationsTo(fieldKey); // Now we can add a field to the `GlobalParams` type that // will stand in for the parameter: it will have the key we // just generated, and the type of the original parameter. // auto globalParamType = globalParam->getFullType(); builder->createStructField(wrapperStructType, fieldKey, globalParamType); // Next we need to replace the uses of the parameter will // logic to extract the appropriate field from the aggregated // parameter. // // Unlike trivial cases that can work with `replaceAllUsesWith`, // we are going to need to different code for each use, and that // potentially puts us in the bad case of modifying the use-def // information while also iterating it. // // To worka around the problem, we will make a copy of the list of // uses and work with that instead. // List uses; for (auto use = globalParam->firstUse; use; use = use->nextUse) { uses.add(use); } for (auto use : uses) { auto user = use->user; // There is an annoying gotcha here, in that we are using // global shader parameters themselves (the `IRGlobalParam`s) // to represent their "keys" in the layout objects that // represent the layout of the global scope. // // We don't want to replace the reference to the global // parameter in one of these layouts with a reference // to a field of our new collected parameter, and instead // want to replace such a reference with the key for that // field. // // TODO: We should probably be assigning keys to global // parameters, and using those keys in the layout instructions // instead of directly using the parameters. The parameters // could then have a decoration to assocaite them with their // key. // // TODO: Alternatively, we could considering doing this // kind of collection work earlier, on a per-module basis, // so that we don't need to perform collection as a back-end step. // (Note that the main sticking point there is explicit layout // markers on global parameters, that stop the entire parameter // range for a module from being contiguous). // if (auto layoutAttr = as(user)) { builder->replaceOperand(layoutAttr->getOperands(), fieldKey); continue; } // NumThreadsDecoration may sometimes be the user for a global // parameter. This occurs when the parameter was supposed to be // a specialization constant, but isn't due to that not being // supported for the target. These can be skipped here and // diagnosed later. if (as(user)) { continue; } // For each use site for the global parameter, we will // insert new code right before the instruction that uses // the parameter. // // TODO: In some cases we might want to emit a single load of // a global parameter at the start of a function, rather // than individual loads at multiple points in the body // of a function. Ideally we can/should annotate the // `globalParams` parameter with the equivalent of `__restrict__` // so that these loads can be merged/moved without concern // for aliasing. // builder->setInsertBefore(user); IRInst* value = nullptr; if (globalParameterGroupTypeLayout) { // If the global parameters are being placed in a // `ConstantBuffer`, then we need to // emit an instruction to compute a pointer to the // desired field, and then load from it. // auto ptrType = builder->getPtrType(globalParamType); auto fieldAddr = builder->emitFieldAddress(ptrType, wrapperParam, fieldKey); value = builder->emitLoad(globalParamType, fieldAddr); } else { // If the global parameters are being bundled in a // plain old `struct`, then we simple want to emit // an instruction to extract the desired field. // value = builder->emitFieldExtract(globalParamType, wrapperParam, fieldKey); } // Whatever replacement value we computed, we need // to install it as the value to be used at the use site. // use->set(value); } // Once we've replaced all uses of the global parameter, // we can remove it from the IR module completely. // globalParam->removeAndDeallocate(); } } }; void collectGlobalUniformParameters( IRModule* module, IRVarLayout* globalScopeVarLayout, CodeGenTarget target) { CollectGlobalUniformParametersContext context; context.module = module; context.globalScopeVarLayout = globalScopeVarLayout; context.target = target; context.processModule(); } } // namespace Slang