It’s been a while since Igalia is working on bringing SPIR-V to mesa OpenGL. Alejandro PiƱeiro has already given a talk on the status of the ARB_gl_spirv extension development that was very well received at FOSDEM 2018 . Anyone interested in technical information can watch the video recording here: https://youtu.be/wXr8-C51qeU.
So far, I haven’t been involved with the extension development myself, but as I am looking forward to work on it in the future, I decided to familiarize myself with the use of SPIR-V in OpenGL. In this post, I will try to demonstrate with a short OpenGL example how to use this extension after I briefly explain what SPIR-V is, and why it is important to bring it to OpenGL.
So, what is SPIR-V and why is it useful?
SPIR-V is an intermediate language for defining shaders. With the use of an external compiler, shaders written in any shading language (for example GLSL or HLSL) can be converted to SPIR-VĀ (see more here: https://www.khronos.org/opengl/wiki/SPIR-V and here: https://www.khronos.org/registry/spir-v/ and here: https://www.khronos.org/spir/). The obvious advantages for the OpenGL programs are speed, less complexity and portability:
With SPIR-V, the graphics program and the driver can avoid the overhead of parsing, compiling and linking the shaders. Also, it is easy to re-use shaders written in different shading languages. For example an OpenGL program for Linux can use an HLSL shader that was originally written for a Vulkan program for Windows, by loading its SPIR-V representation. The only requirement is that the OpenGL implementation and the driver support the ARB_gl_spirv extension.
OpenGL – SPIR-V example
So, here’s an example OpenGL program that loads SPIR-V shaders by making use of the OpenGL ARB_gl_spirv extension:Ā https://github.com/hikiko/gl4
The example makes use of a vertex and a pixel shader written in GLSL 450.Ā Some notes on it:
1- I had to write the shaders in a way compatible with theĀ SPIR-V extension:
First of all, I had to forget about the traditional attribute locations, varyings and uniform locations. Each shader had to contain all the necessary information for linking. This means that I had to specify which are the input and output attributes/varyings at each shader stage and their locations/binding points inside the GLSL program. I’ve done this using the layout qualifier. I also placed the uniforms inĀ Uniform Buffer Objects (UBO) as standalone uniforms are not supported when using SPIR-V 1. I couldn’t use UniformBlockBinding because again is not supported when using SPIR-V shaders (see ARB_gl_spirvĀ : issues : 24 to learn why).
In the following tables you can see a side-by-side comparison of the traditional GLSL shaders I would use with older GLSL versions and the GLSL 450 shaders I used for this example:
Vertex shader:
before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
uniform mat4 mvpmat, mvmat, projmat; uniform mat3 normmat; uniform vec3 light_pos; attribute vec4 attr_vertex; attribute vec3 attr_normal; attribute vec2 attr_texcoord; varying vec3 vpos, norm, ldir; varying vec2 texcoord; void main() { gl_Position = mvpmat * attr_vertex; vpos = (mvmat * attr_vertex).xyz; norm = normmat * attr_normal; texcoord = attr_texcoord * vec2(2.0, 1.0); } |
after:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#version 450 layout(std140, binding = 0) uniform matrix_state { mat4 vmat; mat4 projmat; mat4 mvmat; mat4 mvpmat; vec3 light_pos; } matrix layout(location = 0) in vec4 attr_vertex; layout(location = 1) in vec3 attr_normal; layout(location = 2) in vec2 attr_texcoord; layout(location = 3) out vec3 vpos; layout(location = 4) out vec3 norm; layout(location = 5) out vec3 ldir; layout(location = 6) out vec2 texcoord; void main() { gl_Position = matrix.mvpmat * attr_vertex; vpos = (matrix.mvmat * attr_vertex).xyz; norm = mat3(matrix.mvmat) * attr_normal; texcoord = attr_texcoord * vec2(2.0, 1.0); ldir = matrix.light_pos - vpos; } |
Pixel shader:
before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
uniform sampler2D tex; varying vec3 vpos, norm, ldir; varying vec2 texcoord; void main() { vec4 texel = texture2D(tex, texcoord); vec3 vdir = -normalize(vpos); vec3 n = normalize(norm); vec3 l = normalize(ldir); vec3 h = normalize(vdir + ldir); float ndotl = max(dot(n, l), 0.0); float ndoth = max(dot(n, h), 0.0); vec3 diffuse = texel.rgb * ndotl; vec3 specular = vec3(1.0, 1.0, 1.0) * pow(ndoth, 50.0); gl_FragColor.rgb = diffuse + specular; gl_FragColor.a = texel.a; } |
after:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#version 450 layout(binding = 0) uniform sampler2D tex; layout(location = 3) in vec3 vpos; layout(location = 4) in vec3 norm; layout(location = 5) in vec3 ldir; layout(location = 6) in vec2 texcoord; layout(location = 0) out vec4 color; void main() { vec4 texel = texture(tex, texcoord); vec3 vdir = -normalize(vpos); vec3 n = normalize(norm); vec3 l = normalize(ldir); vec3 h = normalize(vdir + ldir); float ndotl = max(dot(n, l), 0.0); float ndoth = max(dot(n, h), 0.0); vec3 diffuse = texel.rgb * ndotl; vec3 specular = vec3(1.0, 1.0, 1.0) * pow(ndoth, 50.0); color.rgb = diffuse + specular; color.a = texel.a; } |
As you can see, in the modern GLSL version I’ve set the locationĀ for every attribute and varying as well as the binding number for the uniform buffer objects and the opaque types of the fragment shader (sampler2D) inside the shader. Also, the output varyings of the vertex stage (out variables) useĀ the same locations with the equivalent input varyings of the fragment stage (in variables). There are also some minor changes in the language syntax: I can’t make use of the deprecated gl_FragColor and texture2DĀ functions anymore.
2- GLSL to SPIR-V HOWTO:
First of all we need a GLSL to SPIR-V compiler.
On Linux, we can use the Khronos’s glslangValidator by checking out the code from this repository:
https://github.com/KhronosGroup/glslang and installing it locally. Then, we can do something like:
1 |
glslangValidator -G -V -S vertex.glsl -o spirv/vertex.spv |
for each stage (glslangValidator -h for more options). Note that -G is used to compile the shaders targeting the OpenGL platform. It should be avoided when the shaders will be ported to other platforms.
I found easier to add some rules in the project’s Makefile (https://github.com/hikiko/gl4/blob/master/Makefile) to compile the shaders automatically.
3- Loading, specializing and using the SPIR-V shaders
To use the SPIR-V shadersĀ an OpenGL program must:
- load the SPIR-V shaders
- specialize them
- create the shader program
Loading the shaders is quite simple, we only have to specify the size of the SPIR-V content and the content by calling:
1 |
glShaderBinary(1, &sdr, GL_SHADER_BINARY_FORMAT_SPIR_V_ARB, buf, fsz); |
where sdr is our shader, buf is a buffer that contains the SPIR-V we loaded from the file and fsz the contents size (file size). Check out the load_shader function here:Ā https://github.com/hikiko/gl4/blob/master/main.c (the case when SPIRV is defined).
We can thenĀ specializeĀ the shaders using the function glSpecializeShaderARBĀ that allows us to set the shader’s entry point (which is the function from which the execution begins) and the number of constants as well as the constants that will be used by this function. In our example the execution starts from the main function that is void, therefore we set “main”, 0, 0Ā for each shader. Note that I load the glSpecializeShaderARB function at runtime because the linker couldn’t find it, you might not need to do this in your program.
Before we create the shader program it’s generally useful to perform some error checking:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
glGetShaderiv(sdr, GL_COMPILE_STATUS, &status); if(status) { printf("successfully compiled shader: %s\n", fname); } else { printf("failed to compile shader: %s\n", fname); } glGetShaderiv(sdr, GL_INFO_LOG_LENGTH, &loglen); if(loglen > 0 && (buf = malloc(loglen + 1))) { glGetShaderInfoLog(sdr, loglen, 0, buf); buf[loglen] = 0; printf("%s\n", buf); free(buf); } |
In the code snippet above, I used the driver’s compiler (as we would use it if we had just compiled the GLSL code) to validate the shader’s SPIR-V representation.
Then, I created and used the shader programĀ as usually (see load_program of main.c). And that was all.
To run the example program you can clone it from here: https://github.com/hikiko/gl4, and supposing that you have installed the glslangValidator mentioned before you can run:
1 2 |
make ./test |
inside the gl4/ directory.
[1]: Standalone uniforms with explicit locations can also be accepted but since this feature is not supported in other platforms (like Vulkan) the shaders that use it won’t be portable.