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
|
// slang-ir-wrap-structured-buffers.cpp
#include "slang-ir-wrap-structured-buffers.h"
#include "slang-ir-insts.h"
#include "slang-ir.h"
namespace Slang
{
// Suppose a programmer wrote HLSL/Slang code like this:
//
// StructuredBuffer<float4x4> gBuffer;
//
// ... gBuffer[index] ...
// ... gBuffer.Load(index) ...
//
// Further suppose that they specified that row-major
// matrix layout should be used. It might surprise such
// a user that the fxc and dxc compilers both use
// column-major layout for the matrices in `gBuffer`,
// and there is no way to change that fact.
//
// In contrast, we want Slang to respect the matrix layout
// setting given by the user in all cases, so we need to
// find a way to force fxc and dxc to do what we want.
//
// Fortunately, fxc and dxc *do* respect the row-major
// layout setting for code like the following:
//
// struct Wrapper { float4x4 wrapped; }
// StructuredBuffer<Wrapper> gBuffer;
//
// ... gBuffer[index].wrapped ...
// ... gBuffer.Load(index).wrapped ...
//
// The role of the pass in this file is to transform IR
// for code like the first case into IR for code like the
// second case.
//
// The main thing that makes this pass challenging is
// recognizing calls to `StructuredBuffer<T>` builtins
// so that we can transform them to append the `.wrapped`
// field reference.
// As is typical, we will wrap up our IR pass in a
// "context" structure type.
//
struct WrapStructuredBuffersContext
{
IRModule* m_module = nullptr;
// We process a module by processing all its instructions, recursively.
//
void processModule() { processInstRec(m_module->getModuleInst()); }
void processInstRec(IRInst* inst)
{
processInst(inst);
for (auto child : inst->getChildren())
processInstRec(child);
}
void processInst(IRInst* inst)
{
// We will frame our processing as a search for types
// of the form `*StructuredBuffer<matrixType>` where
// `matrixType` is any matrix type.
//
// If the instruction we are looking at doesn't have
// the right form, then we will skip it.
//
auto oldStructuredBufferType = as<IRHLSLStructuredBufferTypeBase>(inst);
if (!oldStructuredBufferType)
return;
auto matrixType = as<IRMatrixType>(oldStructuredBufferType->getElementType());
if (!matrixType)
return;
// Having found a `*StructuredBuffer<M>` we will now
// need an IR builder to help us construct the wrapper code.
//
IRBuilder builderStorage(m_module);
auto builder = &builderStorage;
// We begin by constructing a structure type that wraps
// our `matrixType`, into something like:
//
// struct Wrapper { matrixType wrapped; }
//
builder->setInsertBefore(oldStructuredBufferType);
auto wrappedFieldKey = getWrappedFieldKey(builder);
auto wrapperStruct = getWrapperStruct(builder, matrixType);
// Now that we have our wrapper struct, we can construct a type
// that is `*StructuredBuffer<wrapperStruct>` and use it to
// replace `*StructuredBuffer<matrixType>`
//
// Note: we are constructing a *new* type instead of modifying
// the old one in-place because eventually when we do deduplication
// of types/constants more consistently it might cause problems
// to modify a tyep in a way that changes its hash code.
//
// TODO: the above statement is still slippery, since we are still
// replacing one type A with another type B globally, and doing
// so could affect any type that in turn referenced A...
//
auto newStructuredBufferType =
builder->getType(oldStructuredBufferType->getOp(), wrapperStruct);
oldStructuredBufferType->replaceUsesWith(newStructuredBufferType);
// Any values that used our old structured bufer type
// now have the new structured buffer type, but that
// means that operations on them might be getting
// applied incorrectly.
//
// For example, if we had a call like:
//
// float4x4 m = gBuffer.Load(someIndex);
//
// the result of the `Load` call is now `wrapperStruct`
// and we can't assign that to a matrix-type variable.
//
// we need to invetigate values of our structured
// buffer type, and then investigate operations that
// are using those values, to see if we can find the
// ones we need to rewrite.
//
// We can find values of `newStructuredBufferType` by
// scanning through its IR uses, since values of that
// type are using it as a (type) operand.
//
traverseUses(
newStructuredBufferType,
[&](IRUse* typeUse)
{
// There might be uses of `newStructuredBufferType` where
// it isn't being used as the type of a value, so we
// start by weeding out the ones we don't care about.
//
auto valueOfStructuredBufferType = typeUse->getUser();
if (valueOfStructuredBufferType->getFullType() != newStructuredBufferType)
return;
// Now we have some `valueOfStructuredBufferType`. In our running
// example, this might be `gBuffer`, which is an `IRGlobalParam`.
//
// We don't need to change anything about `gBuffer` itself, since
// replacing `oldStructuredBufferType` with `newStructuredBufferType`
// already replaced the type of `gBuffer`.
//
// Instead, we want to look for instructions that *use* the buffer,
// because these could be calls to intrinsic functions like
// `RWStructuredBuffer.Load`
//
traverseUses(
valueOfStructuredBufferType,
[&](IRUse* valueUse)
{
// we are only interested in instructions that are calls,
// with at least one argument, where the first argument
// is our `valueOfStructuredBufferType`. These
// are calls that could potentially be intrinsic
// operations on `*StructuredBuffer`.
//
auto user = valueUse->getUser();
switch (user->getOp())
{
case kIROp_StructuredBufferLoad:
case kIROp_StructuredBufferLoadStatus:
case kIROp_RWStructuredBufferStore:
case kIROp_RWStructuredBufferLoadStatus:
case kIROp_RWStructuredBufferGetElementPtr:
break;
default:
return;
}
builder->setInsertAfter(user);
auto oldResultType = user->getDataType();
// First we care about the case for `Load`, which
// will return the element type, which would be
// a matrix type.
//
if (as<IRMatrixType>(oldResultType))
{
// We know that the call to `Load` should now
// return our wrapper struct type, so we will
// go ahead and modify its type to be correct.
//
auto newResultType = wrapperStruct;
builder->setDataType(user, newResultType);
// Next, we need to make sure to extract the
// field from the wrapper struct, so that
// we get back to a value of the expected
// type.
//
// This logic takes something like:
//
// WrapperStruct call = gBuffer.Load(index);
//
// and follows it with:
//
// float4x4 newVal = call.wrapped;
//
auto newVal =
builder->emitFieldExtract(oldResultType, user, wrappedFieldKey);
// Any code that used the value of `call` should
// now use `newVal` instead...
//
user->replaceUsesWith(newVal);
//
// ... except for one important gotcha, which is
// that `newVal` itself used `call`, and replacing
// `call` with `newVal` results in `newVal` using
// itself as one of its operands.
//
// It is a bit of a kludge, but we fix the situation
// by just setting the appropriate operand again.
//
// TODO: it might be helpful to have a variant
// of `replaceUsesWith` that can handle cases like
// this.
//
newVal->setOperand(0, user);
}
//
// The second interesting case is the `ref` accessor
// in `operator[]` for a `RWStructuredBuffer`, which
// at the IR level returns a *pointer* to the buffer
// element type.
//
else if (auto oldPtrType = as<IRPtrTypeBase>(oldResultType))
{
auto pointeeType = oldPtrType->getValueType();
if (as<IRMatrixType>(pointeeType))
{
// At this point we know that the intrinsic
// operation returned a pointer to a matrix,
// which seems like a good indications that
// it is our `operator[]` and it should now
// return a pointer to the wrapper struct
// instead.
//
// The logic here is almost identical to the
// non-pointer case above, so please refer
// there if you want the comments.
auto newResultType =
builder->getPtrType(oldPtrType->getOp(), wrapperStruct);
builder->setDataType(user, newResultType);
auto newVal =
builder->emitFieldAddress(oldResultType, user, wrappedFieldKey);
user->replaceUsesWith(newVal);
newVal->setOperand(0, user);
}
}
});
});
}
/// Get the struture field "key" to use for generated wrappers
IRStructKey* getWrappedFieldKey(IRBuilder* builder)
{
// We will re-use the same field key for all of the
// wrapper structs we might create, so that all of
// the new field access operations will use the same
// field name to make their purpose more clear.
//
// TODO: It might be useful to give the field key
// a name that is indicative of its purpose so
// that people won't be confused why their code
// has been transformed and now there is this
// `._S2` in the middle of their expressions.
if (!m_wrappedFieldKey)
{
m_wrappedFieldKey = builder->createStructKey();
}
return m_wrappedFieldKey;
}
/// Lazily created and cached field "key" to use for wrapper structs.
IRStructKey* m_wrappedFieldKey = nullptr;
/// Get the wrapper struct to use for a particular `matrixType`.
IRStructType* getWrapperStruct(IRBuilder* builder, IRMatrixType* matrixType)
{
// TODO: Because our type de-duplication isn't perfect right now,
// it is possible that there could be more than one equivalent
// matrix type, and thus more than one equivalent structured buffer
// type using that matrix type. As a result, if we just generate
// one new `struct` per IR structured-buffer type, we could end up
// with conflicts when a buffer with one IR type gets passed to
// a function with another (equivalent) IR type.
//
// The right fix here is to cache and look up these structure
// tyeps based on the scalar type (opcode) and row/column counts
// of the given `matrixType`.
//
// For now I will ignore this issue and hope that we can address
// the more fundamental issue of type deduplication before this
// choice comes back to bite me.
auto wrappedFieldKey = getWrappedFieldKey(builder);
auto wrapperStruct = builder->createStructType();
builder->createStructField(wrapperStruct, wrappedFieldKey, matrixType);
return wrapperStruct;
}
};
void wrapStructuredBuffersOfMatrices(IRModule* module)
{
WrapStructuredBuffersContext context;
context.m_module = module;
context.processModule();
}
} // namespace Slang
|