Rework how shaders are parsed/assembled
Summary
This issue will present a couple of suggestions to improve the handling of shaders to ease debugging and simplify the needed parsing and assembling work.
Motivation
While working on making OpenGL ES work with Splash, I noticed that whenever a shader is created, a bunch of steps are done, including:
- The shader code is processed to extract uniforms, their type, and comments/documentation
- Includes are processed similarly to how the C preprocessor works, but in OpenGL shaders
Another thing worth pointing out, is that OpenGL ES doesn't support uniform initialization, so we need some way to initialize those uniforms on the CPU.
Proposal
My proposal is to split shaders such that we have a programmatic representation of their data that will be easier to process. For example, a shader can be made up of the following parts: 2. Uniforms represented as a list of (type, name, initial value, documentation) tuples 3. Includes represented as a list of names or pointers to utility shaders/functions 4. The rest of the shader (in/out variables, shader main function) are included as a string.
Example:
Shader(
{"includeA", "includeB", ...}, // Include files for common functionality
{"defA", "defB", {"defC", 12}, ...}, // Defines
{
{Vec2, "myVec2", Vec2(0,0), "Documentation ..."},
{Sampler2D, "mySampler2D"},
...,
}, // Uniforms (with default values)
R"(
// Shader code
)"
)
When the shader is required, these parts will then be assembled into a shader to be sent to OpenGL.
This will remove the need to parse shaders to figure out their uniforms and docs, and can allow us to easily initialize uniforms on the CPU if needed.
Impact on already existing code
The changes proposed here might introduce some overhead related to the assembling of a whole shader out of the parts previously described, mostly in the form of string allocations. But this will hopefully be offset by the removal of regex matches and string operations which are done when parsing shaders. Another thing to mention is that this seems to be done once at the start of the program, so even if there's a slowdown, it won't be noticable for users.
Issues and possible solutions
Recently, I've met some shaders with code similar to this:
#ifdef SOME_DEF
uniform ...
#endif
Code similar to this would be hard to represent, although not impossible. A solution I have in my mind as I'm writing this section is:
{
{Vec2, "myVec2", Vec2(0,0), "Documentation ..."},
{Sampler2D, "mySampler2D", {/* no docs*/}, {"defA"}},
...,
}, // Uniforms
Where {"defA"}
defines a list (or it could be just one) of definitions that this uniform depends on. If the shader is built without this definition being present in the definitions list given to the shader, then the uniform will not be created.
Roadmap
- Discuss this proposal and the current state of shaders in Splash to reach some consensus on the design
- Once the current work on making splash work with OpenGL ES is done, make a prototype implementation
- Test the prototype implementation for issues and performance regressions
- Decide whether or not the prototype implementation should be merged or if more work on this idea is needed.