Skip to content

Commit 85632e8

Browse files
author
Tim Foley
authored
Add support for returning structures that contain opaque types (shader-slang#1835)
Introduction ============ Several of our target platforms share a concept of "opaque" types, including resources (`Texture2D`) and samplers (`SamplerState`), which are restricted in how they can be used. GLSL and SPIR-V place very severe restrictions, in that opaque types cannot be used for the type of: * (mutable) local variables * (mutable) global variables * structure fields * Function result/return * `out` or `inout` parameters The HLSL language allows all of these cases, but with the practical caveat that the compiler front-end must be able to statically analyze how opaque types have been used and "optimize away" all of the above cases. For example, it is legal to have a local variable of an opaque type, but at any point where the variable gets used it must be statically known which top-level shader parameter the variable refers to. Existing Work ============= In the Slang compiler we need to implement our own passes to detect these "illegal" uses of opaque types and legalize them. The work is basically broken into two distinct steps: * The existing `legalizeResourceTypes()` pass detects illegal types (e.g., a `struct` that has a field of type `Texture2D`) and replaces them with legal types, sometimes by splitting apart declarations (e.g., a parameter using such a `struct` type gets split into multiple parameters). At a high level, we can think of this as "exposing" opaque types so that they are not hidden inside of nested structures. * Next, the `specializeResourceOutputs()` pass detects calls to functions that output opaque types (whether by the function return value of `out` / `inout` parameters). The pass analyzes the body of such functions, and tries to isolate the logic that determines their resource-type outputs and hoise that logic into call sites (so that the opaque-type outputs can then be eliminated). This Change =========== One important missing case was that the type legalization step was incapable of legalizing types that appear in the result/return type of functions. The existing logic would simply diagnose an internal/unimplemented error if it ecountered a non-simple type in the return position. At a high-level, supporting this case seems simple enough. Given a function signature like: ``` struct Things { int a; Texture2D b; } Things myFunc(int x) { ... } ``` we want to split the result type into an "ordinary" result type and then `out` parameters for any opaque-type fields: ``` struct Things_Legal { int a; } Things_Legal myFunc(int x, out Texture2D result_b) { ... }; ``` Similarly, at a call site to a function like this: ``` Things t = myFunc(99); ``` we split the function result into ordinary and opaque-type parts, and pass the latter as `out` parameters: ``` Texture2D t_b; Things_Legal t = myFunc(99, /*out*/ t_b); ``` The main place where things get tricky is when dealing with `return` sites within the body of a function that needs legalization: ``` Things myFunc(int x) { ... Things things = ...; ... return things; } ``` In theory the answer is simple: a `return` translates into writes to the `out` parameters for any opaque-type data, followed by a return of the ordinary-type part: ``` Things_Legal myFunc(int x, out Texture2D result_b) { ... Things_Legal things = ...; Texture2D things_b = ...; ... result_b = things_b; return things; } ``` The sticking point here is that this step requires tracking data between the legalization of the parameter list for `myFunc` and legalization of the `return`s in its body, so that we can identify the `result_b` parameter to be able to write to it. The existing type legalization pass was not built with the idea that such communication is commonly needed; it assumes that each instruction can be legalized in isolation, so long as dependencies are respected. This change adds logic such that the `legalizeFunc()` step sets up a data structure that it used to represent information about how a function (and its parameter list) got legalized, so that the logic for a `return` can make use of that legalized information. Right now the information we track consists of just the list of parameters that were introduced to represent a return/result type. Testing ======= In order to confirm what features do/don't work, I added a set of tests that cover a cross-product of opaque type use cases: * The opaque type can be used in the function result type, an `out` parameter, or an `inout` parameter * The opaque type can be used "directly" or nested inside a `struct`. These tests are helpful to make sure we handle the most important cases, but it is worth noting that the coverage is still lacking in that we do not sufficiently test all the options for what the function body might do. An opaque-type function result could be derived from many different sources: * It could be a global shader parameter * It could be an `in` or `inout` parameter of the function itself * It could be wrapped up in one or more structure types * It could be wrapped up in one or more array types (such that the output of specialization needs to pass around array indices) * It could involve use of the type as a local variable (including passing it into other functions with result/`out`/`inout` outputs of opaque types) This change makes it so that we can handle the simplest cases involving result/return types with a wrapper `struct`, and adds test cases that confirm we handle several other cases for `out` and `inout` parameters. Gaining confidence that we cover all the cases that arise in practical shaders will require more work over following changes.
1 parent 731f1fc commit 85632e8

15 files changed

+1095
-121
lines changed

source/slang/slang-ir-legalize-types.cpp

+776-118
Large diffs are not rendered by default.

source/slang/slang-ir-specialize-resources.cpp

+23-2
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ struct ResourceOutputSpecializationPass
171171
oldFunc,
172172
newFunc);
173173

174-
// At first `newFunc` is a directclone of `oldFunc`, and thus doesn't
174+
// At first `newFunc` is a direct clone of `oldFunc`, and thus doesn't
175175
// solve any of our problems. We will traverse `oldFunc` and specialize
176176
// it as needed, while also collecting information that will allow
177177
// us to rewrite call sites.
@@ -468,8 +468,29 @@ struct ResourceOutputSpecializationPass
468468
//
469469
// Any failures along the way cause the whole process to fail.
470470

471-
for( auto param : func->getParams() )
471+
// Note: We are introducing new parameters at the same time as we
472+
// iterate over the parameter list, so we cannot just use the
473+
// `func->getParams()` convenience accessor. Instead, we manually
474+
// iterate over the parameters in a way that avoids invalidation
475+
// if we remove the `param` we are working on.
476+
//
477+
// Note: it might seem odd that we are modifying `func` but will
478+
// still bail out on any errors. You might ask: isn't there a chance
479+
// that we will end up with the function in a partially-modified state?
480+
//
481+
// The important thing to remember is that `func` is *copy* of the
482+
// original function, so any modifications we make to it do not
483+
// affect the original, so that if we *do* have to bail out we can
484+
// leave any call sites intact as calls to the original. The result
485+
// is that bailing out here may leave the new/copied function in
486+
// a state where it isn't useful, but it also won't have any uses,
487+
// and can be eliminated later.
488+
//
489+
IRParam* nextParam = nullptr;
490+
for( IRParam* param = func->getFirstParam(); param; param = nextParam )
472491
{
492+
nextParam = param->getNextParam();
493+
473494
ParamInfo paramInfo;
474495
SLANG_RETURN_ON_FAIL(maybeSpecializeParam(param, paramInfo, outFuncInfo));
475496
outFuncInfo.oldParams.add(paramInfo);

source/slang/slang-legalize-types.h

+33-1
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ struct LegalVal
509509
}
510510

511511
static LegalVal implicitDeref(LegalVal const& val);
512-
LegalVal getImplicitDeref();
512+
LegalVal getImplicitDeref() const;
513513

514514
static LegalVal pair(RefPtr<PairPseudoVal> pairInfo);
515515
static LegalVal pair(
@@ -566,6 +566,30 @@ struct WrappedBufferPseudoVal : LegalValImpl
566566
LegalElementWrapping elementInfo;
567567
};
568568

569+
//
570+
571+
/// Information about a function that has been legalized
572+
///
573+
/// This type is used to track any information about the function
574+
/// and its signature that might be relevant to the legalization
575+
/// of instructions inside the function body.
576+
///
577+
struct LegalFuncInfo : RefObject
578+
{
579+
/// Any parameters that were added to the function signature
580+
/// to represent the function result after legalization.
581+
///
582+
/// It is possible that the result type of a function needed
583+
/// to be split into multiple types, and as a result a single
584+
/// function result couldn't return all of them.
585+
///
586+
/// This array is a list of `out` parameters created to represent
587+
/// additional function results. Because they are `out` parameters,
588+
/// each is a *pointer* to a value of the relevant type.
589+
///
590+
List<IRInst*> resultParamVals;
591+
};
592+
569593
//
570594

571595
/// Context that drives type legalization
@@ -601,6 +625,14 @@ struct IRTypeLegalizationContext
601625

602626
Dictionary<IRType*, LegalType> mapTypeToLegalType;
603627

628+
/// Map a function to information about how it was legalized.
629+
///
630+
/// Note that entries are only created if there is somehting for them
631+
/// to represent, so many functions may lack entries in this map even
632+
/// after legalization.
633+
///
634+
Dictionary<IRFunc*, RefPtr<LegalFuncInfo>> mapFuncToInfo;
635+
604636
IRBuilder* getBuilder() { return builder; }
605637

606638
/// Customization point to decide what types are "special."
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// inout-param-opaque-type-in-struct.slang
2+
3+
// Test that a function/method can have an `out` parameter of
4+
// aggregate type that includes an opaque type
5+
6+
//TEST(compute):COMPARE_COMPUTE:
7+
8+
struct Things
9+
{
10+
int first;
11+
RWStructuredBuffer<int> rest;
12+
}
13+
14+
//TEST_INPUT:set C = new { {1, ubuffer(data=[2 3 4 5], stride=4)}, {6, ubuffer(data=[7 8 9 10], stride=4)} }
15+
cbuffer C
16+
{
17+
Things gX;
18+
Things gY;
19+
}
20+
21+
void swap(
22+
inout Things a,
23+
inout Things b)
24+
{
25+
Things t = a;
26+
a = b;
27+
b = t;
28+
}
29+
30+
int eval(Things t, int val)
31+
{
32+
return t.first*256 + t.rest[val];
33+
}
34+
35+
int test(int val)
36+
{
37+
Things f = gX;
38+
Things g = gY;
39+
40+
swap(f, g);
41+
42+
return (eval(f,val) << 16) + eval(g,val);
43+
}
44+
45+
//TEST_INPUT:set gOutput = out ubuffer(data=[0 0 0 0], stride=4)
46+
RWStructuredBuffer<int> gOutput;
47+
48+
[numthreads(4, 1, 1)]
49+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
50+
{
51+
uint tid = dispatchThreadID.x;
52+
int inVal = tid;
53+
int outVal = test(inVal);
54+
gOutput[tid] = outVal;
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
6070102
2+
6080103
3+
6090104
4+
60A0105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// inout-param-opaque-type.slang
2+
3+
// Test that a function/method can have an `out` parameter of opaque type
4+
5+
//TEST(compute):COMPARE_COMPUTE:
6+
7+
//TEST_INPUT:set gX = ubuffer(data=[16 17 18 19], stride=4)
8+
RWStructuredBuffer<int> gX;
9+
10+
//TEST_INPUT:set gY = ubuffer(data=[3 6 9 12], stride=4)
11+
RWStructuredBuffer<int> gY;
12+
13+
void swap(
14+
inout RWStructuredBuffer<int> a,
15+
inout RWStructuredBuffer<int> b)
16+
{
17+
RWStructuredBuffer<int> t = a;
18+
a = b;
19+
b = t;
20+
}
21+
22+
int test(int val)
23+
{
24+
RWStructuredBuffer<int> f = gX;
25+
RWStructuredBuffer<int> g = gY;
26+
27+
swap(f, g);
28+
29+
return f[val] * 256 + g[val];
30+
}
31+
32+
//TEST_INPUT:set gOutput = out ubuffer(data=[0 0 0 0], stride=4)
33+
RWStructuredBuffer<int> gOutput;
34+
35+
[numthreads(4, 1, 1)]
36+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
37+
{
38+
uint tid = dispatchThreadID.x;
39+
int inVal = tid;
40+
int outVal = test(inVal);
41+
gOutput[tid] = outVal;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
310
2+
611
3+
912
4+
C13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// out-opaque-type-in-struct.slang
2+
3+
// Test that a function/method can have an `out` parameter of
4+
// aggregate type that includes an opaque type
5+
6+
//TEST(compute):COMPARE_COMPUTE:
7+
8+
struct Things
9+
{
10+
int first;
11+
RWStructuredBuffer<int> rest;
12+
}
13+
14+
//TEST_INPUT:set gThings = new Things { 1, ubuffer(data=[2 3 4 5], stride=4) }
15+
ConstantBuffer<Things> gThings;
16+
17+
void getThings(out Things outThings)
18+
{
19+
outThings = gThings;
20+
}
21+
22+
int test(int val)
23+
{
24+
Things things;
25+
getThings(things);
26+
return things.first * (16 << val) + things.rest[val];
27+
}
28+
29+
//TEST_INPUT:set gOutput = out ubuffer(data=[0 0 0 0], stride=4)
30+
RWStructuredBuffer<int> gOutput;
31+
32+
[numthreads(4, 1, 1)]
33+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
34+
{
35+
uint tid = dispatchThreadID.x;
36+
int inVal = tid;
37+
int outVal = test(inVal);
38+
gOutput[tid] = outVal;
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
12
2+
23
3+
44
4+
85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// out-opaque-type.slang
2+
3+
// Test that a function/method can have an `out` parameter of opaque type
4+
5+
//TEST(compute):COMPARE_COMPUTE:
6+
7+
//TEST_INPUT:set gThings = ubuffer(data=[16 17 18 19], stride=4)
8+
RWStructuredBuffer<int> gThings;
9+
10+
11+
void getThings(out RWStructuredBuffer<int> things)
12+
{
13+
things = gThings;
14+
}
15+
16+
int test(int val)
17+
{
18+
RWStructuredBuffer<int> t;
19+
getThings(t);
20+
return t[val];
21+
}
22+
23+
//TEST_INPUT:set gOutput = out ubuffer(data=[0 0 0 0], stride=4)
24+
RWStructuredBuffer<int> gOutput;
25+
26+
[numthreads(4, 1, 1)]
27+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
28+
{
29+
uint tid = dispatchThreadID.x;
30+
int inVal = tid;
31+
int outVal = test(inVal);
32+
gOutput[tid] = outVal;
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
10
2+
11
3+
12
4+
13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// return-opaque-type-in-struct.slang
2+
3+
// Test that a function/method can return a value of
4+
// aggregate type that includes an opaque type
5+
6+
//TEST(compute):COMPARE_COMPUTE:
7+
8+
struct Things
9+
{
10+
int first;
11+
RWStructuredBuffer<int> rest;
12+
}
13+
14+
//TEST_INPUT:set gThings = new Things { 1, ubuffer(data=[2 3 4 5], stride=4) }
15+
ConstantBuffer<Things> gThings;
16+
17+
Things getThings()
18+
{
19+
return gThings;
20+
}
21+
22+
int test(int val)
23+
{
24+
let things = getThings();
25+
return things.first * (16 << val) + things.rest[val];
26+
}
27+
28+
//TEST_INPUT:set gOutput = out ubuffer(data=[0 0 0 0], stride=4)
29+
RWStructuredBuffer<int> gOutput;
30+
31+
[numthreads(4, 1, 1)]
32+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
33+
{
34+
uint tid = dispatchThreadID.x;
35+
int inVal = tid;
36+
int outVal = test(inVal);
37+
gOutput[tid] = outVal;
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
12
2+
23
3+
44
4+
85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// return-opaque-type.slang
2+
3+
// Test that a function/method can return a value of an opaque type.
4+
5+
//TEST(compute):COMPARE_COMPUTE:
6+
7+
struct Stuff
8+
{
9+
RWStructuredBuffer<int> things;
10+
11+
RWStructuredBuffer<int> getThings() { return things; }
12+
}
13+
14+
//TEST_INPUT:set gStuff = new Stuff { ubuffer(data=[16 17 18 19], stride=4) }
15+
ConstantBuffer<Stuff> gStuff;
16+
17+
int test(int val)
18+
{
19+
return gStuff.getThings()[val];
20+
}
21+
22+
//TEST_INPUT:set gOutput = out ubuffer(data=[0 0 0 0], stride=4)
23+
RWStructuredBuffer<int> gOutput;
24+
25+
[numthreads(4, 1, 1)]
26+
void computeMain(uint3 dispatchThreadID : SV_DispatchThreadID)
27+
{
28+
uint tid = dispatchThreadID.x;
29+
int inVal = tid;
30+
int outVal = test(inVal);
31+
gOutput[tid] = outVal;
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
10
2+
11
3+
12
4+
13

0 commit comments

Comments
 (0)