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 and the GLSL 450 shaders I used for this example:


Vertex shader:

before:

after:


Pixel shader:

before:

after:


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:

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:

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:

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.