summaryrefslogtreecommitdiffstats
path: root/source/slang/slang-ir-collect-global-uniforms.cpp
blob: aad0de44f0c8f3cbe986be86831b1c061f247dc6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
// 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> 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<IRGlobalParam>(key);
        case kIROp_MakeExistential:
        case kIROp_WrapExistential:
            return as<IRGlobalParam>(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<IRParameterGroupTypeLayout>(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<IRStructTypeLayout>(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<GlobalParams>`.
        //
        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<IRStructFieldLayoutAttr*> orderedFields;

        if (target == CodeGenTarget::CUDASource)
        {
            // For CUDA: separate regular and unsized array fields
            List<IRStructFieldLayoutAttr*> regularFields;
            List<IRStructFieldLayoutAttr*> unsizedArrayFields;

            for (auto fieldLayoutAttr : fieldAttrs)
            {
                auto globalParam =
                    _getGlobalParamFromLayoutFieldKey(fieldLayoutAttr->getFieldKey());
                if (globalParam && as<IRUnsizedArrayType>(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<IRStructFieldLayoutAttr>(
                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<IRUse*> 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<IRStructFieldLayoutAttr>(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<IRNumThreadsDecoration>(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<GlobalParams>`, 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