A short OpenGL / SPIRV example.

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 (left column) and the GLSL 450 shaders I used for this example (right column):

Vertex shader:

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 *
   vpos = (mvmat * attr_vertex).xyz;
   norm = normmat * attr_normal;
   texcoord = attr_texcoord
               * vec2(2.0, 1.0);
#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:

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;
#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.


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:

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:

  1. load the SPIR-V shaders
  2. specialize them
  3. 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:

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:

	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);

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 usual (see load_program of main.c). And that’s it.

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:


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.