-
Notifications
You must be signed in to change notification settings - Fork 14
Shader Modularity #1
Description
Metal Shading Language
Metal Shading Language has "Function Constants," (Section 4.10 of the Metal Shading Language 2.1 spec) where you can say something like
constant int a [[function_constant(0)]];
and then there's a two-phase preparation of the shader:
- Compile the source of the shader by calling MTLDevice.makeLibrary(options:) where you specify the preprocessor macros
- Specialize the shader by specifying MTLLibrary.makeFunction(constantValues:) where you specify the function constants
GLSL
Similarly, GLSL (and SPIR-V) have "Specialization Constants," (Section 7.2.1 of the OpenGL 4.6 Core spec, and section 4.11 of the GLSL 4.6 spec) where you can say something like
layout (constant_id = 0) const int a = 42;
and then there's a three-phase preparation of the shader:
- Specify preprocessor macros by manually generating strings of the form "#define foo 5" and injecting them into the GLSL source string at the right place
glShaderSource()
which presumably does some compilationglSpecializeShader()
GLSL (but not SPIR-V?) also has a concept of subroutines (Section 7.10 of the OpenGL 4.6 Core spec, and section 6.1.2 of the GLSL 4.6 spec), which are just like specialization constants, except for functions. You can say
subroutine float MySignature(float a, float b);
subroutine(MySignature) float foo(float a, float b) { ... }
subroutine(MySignature) float bar(float a, float b) { ... }
subroutine uniform MySignature myUniformName;
...
float r = myUniformName(3.3, 4.4);
and the OpenGL API hooks up one of foo()
/bar
to myUniformName
. Notably, this happens after compilation, and the value for myUniformName
could change each frame / draw call.
HLSL
AFAICT, HLSL doesn't have anything like function constants, specialization constants, or subroutines, but instead has preprocessor macros. You can specify these as arguments to D3DPreprocess().
Language | Generics / Templates | Specialization Constants | Subroutines | Preprocessor Macros | Polymorphism |
---|---|---|---|---|---|
HLSL | ❌ | ❌ | ❌ | ✅ | ❌ |
GLSL | ❌ | ✅ | ✅ | ✅ | ❌ |
SPIR-V | ❌ | ✅ | ❌ | ❌ | ❌ |
MSL | ✅ | ✅ | ❌ | ✅ | ✅ |
WHLSL | ❌ | ❓ | ❓ | ❌ | ❌ |
These constant values lead to better performance than regular constants because they can cause dead code to be removed before the shader ever hits the GPU. This is important for ubershaders, where most of the code will end up being removed.
WHLSL does not include a preprocessor because of the additional complexity it brings. Similarly, on the last WebGPU call where we discussed shading languages, we agreed to remove the ability for user-defined structs/functions to accept type arguments.
The big game engines (Unity, Unreal, etc.) often don't have a single shader to run; instead, they often have families of related shaders. For example, an engine's shader might describe a forward-rendering algorithm, but leave the BRDF and lighting model up to the specific app linking with it. Generics, specialization constants, and preprocessor macros are all ways of making this easier.
Given that HLSL has been successful without specialization constants, perhaps they aren't necessary. On the other hand, WHLSL doesn't have the mechanism that people usually use to specialize HLSL shaders (preprocessor macros). GLSL has both specialization constants and subroutines, but GLSL ES has removed both of those features, so perhaps this entire problem isn't very important. We should figure out what to do here.