Loading a SPIR-V shader program
Standard, Portable Intermediate Representation - V (SPIR-V) is an intermediate language designed and standardized by the Khronos Group for shaders. It is intended to be a compiler target for a number of different languages. In the Vulkan API, shaders are required to be compiled to SPIR-V before they can be loaded. SPIR-V is intended to provide developers with the freedom to develop their shaders in any language they want (as long as it can be compiled to SPIR-V), and avoid the need for an OpenGL (or Vulkan) implementation to provide compilers for multiple languages.
Support for SPIR-V shader binaries was added to OpenGL core with version 4.6, but is also available via the ARB_gl_spirv
extension for earlier OpenGL versions.
Currently, the Khronos Group provides a reference compiler for compiling GLSL to SPIR-V. It is available on GitHub at https://github.com/KhronosGroup/glslang.
In this recipe, we'll go through the steps involved in precompiling a GLSL shader pair to SPIR-V, and then load it into an OpenGL program.
Getting ready
Download and compile the OpenGL shader validator from https://github.com/KhronosGroup/glslang. Make sure that the glslangValidator
binary is available in your PATH
command line. In this example, we'll use the shader pair located in the basic.vert.glsl
andbasic.frag.glsl
files.
Note that you'll need to use explicit locations for all of your input/output variables in the shaders. For details, see the Linking a shader program recipe.
Note
All variables used for input/output interfaces (in/out variables) must have a location assigned.
How to do it...
Start by compiling the shader pair into SPIR-V using the glslangValidator
tool:
glslangValidator -G -o basic.vert.spv basic.vert.glsl glslangValidator -G -o basic.frag.spv basic.frag.glsl
If successful, this produces the basic.vert.spv
and basic.frag.spv
SPIR-V output files.
To load your SPIR-V shaders into an OpenGL program, use glShaderBinary
and glSpecializeShader
. With glShaderBinary
, use GL_SHADER_BINARY_FORMAT_SPIR_V
as the binary format:
GLuint vertShader = glCreateShader(GL_VERTEX_SHADER); // Load the shader into a std::vector std::ifstream inStream("basic.vert.spv", std::ios::binary); std::istreambuf_iterator<char> startIt(inStream), endIt; std::vector<char> buffer(startIt, endIt); inStream.close(); // Load using glShaderBinary glShaderBinary(1, &vertShader, GL_SHADER_BINARY_FORMAT_SPIR_V, buffer.data(), buffer.size()); // Specialize the shader (specify the entry point) glSpecializeShader( vertShader, "main", 0, 0, 0); // Check for success/failure GLint status; glGetShaderiv(vertShader, GL_COMPILE_STATUS, &status); if( GL_FALSE == status ) { // Loading failed... }
The process is nearly exactly the same for the fragment shader; just use GL_FRAGMENT_SHADER
instead of GL_VERTEX_SHADER
on the first line.
Finally, we create the program object, attach the shaders, and link. This process is identical to that shown in the Linking a shader program recipe, so we won't reproduce it here.
How it works...
The glShaderBinary
function provides us with the ability to load shaders that have been compiled to the SPIR-V format. This part is fairly straightforward.
The function that might be a bit more confusing is glSpecializeShader
. We are required to call this function before the shader stage can be linked. This call is needed because a single SPIR-V file can have multiple entry points, and SPIR-V files can have specialization constants, which are parameters that the user can provide before it is compiled into native code.
At a minimum, we need to define the entry point for our shader. Since the source language is GLSL, the entry point is main
. We specify the entry point(s) via the second argument. For GLSL, we simply use the main
constant string. The last three parameters can be used to define the specialization constants. The first of the three is the number of constants, the next is a pointer to an array of constant indices, and the last is a pointer to an array of constant values.
The process of specializing an SPIR-V shader is similar to compiling a GLSL shader. Before calling glSpecializeShader
, or if specialization fails, the compile status will be GL_FALSE
. If specialization succeeds, the compile status will be GL_TRUE
. As with GLSL shaders, we can query the shader info log to get detailed error messages (see the Compiling a shader recipe).
There's more...
SPIR-V appears to be the future of shader programming in the Vulkan/OpenGL space. However, GLSL is not going away anytime soon. GLSL compilers still ship with OpenGL and there's currently no sign that they will be removed or deprecated. The OpenGL specification still considers GLSL to be the primary shading language.
However, if you're interested in getting on board with SPIR-V early, or you have an interest in moving toward Vulkan, it might be valuable to you to start working with SPIR-V now in OpenGL. Fortunately, that's possible, at least in recent versions of OpenGL.
The future of SPIR-V is very bright. There is already a (mostly complete) compiler for HLSL that targets SPIR-V, and it is likely that other languages will be developed soon. It's an exciting time for shader programming!
See also
- The
chapter01/scenebasic.cpp
file in the example code - The Compiling a shader recipe
- The Linking a shader program recipe