Integrating glslang for runtime shader compilation
In my Vulkan engine, I needed a tool to compile GLSL into SPIR-V at runtime, so here’s a tutorial of how I did it with glslang
.
The official glslang
standalone program source is a great resource for integrating glslang
.
Intializing and finalizing glslang
First, you need to include ShaderLang.h
:
#include <glslang/Public/ShaderLang.h>
To intialize glslang
, you can simply call glslang::IntializeProcess()
. It needs to be called exactly once per process before using anything else.
void Initialize() {
// ...
glslang::InitializeProcess();
// ...
}
To finalize glslang
, you need to call glslang::FinalizeProcess()
.
void Finalize() {
// ...
glslang::FinalizeProcess();
// ...
}
You can also use glslang::GetGlslVersionString()
to get glslang
supported GLSL version.
void Intialize() {
// ...
printf("glslang GLSL version: %s", glslang::GetGlslVersionString());
// ...
}
Compiling GLSL Shaders
Create a glslang::TShader
object
To compile GLSL shaders, you need a glslang::TShader
object and specify which stage the shader belongs to.
std::vector<uint32_t> CompileGLSLToSPRIV(const std::string &source) {
const EShLanguage stage = EShLangVertex;
// or EShLangFragment, EShLangGeometry, EShLangCompute, etc.
glslang::TShader shader(stage);
// ...
}
Feeding shader source code
After creating the shader object, you can set source strings and preamble
.
preamble
is added to the front of the shader source, but it doesn’t work the same as string concatenation (putting #version
directive in preamble
doesn’t work). I usually use it for enabling extensions like #extension GL_GOOGLE_include_directive : require
.
std::vector<uint32_t> CompileGLSLToSPRIV(const std::string &source) {
// ...
glslang::TShader shader(stage);
shader.setStrings(source.c_str(), 1);
shader.setPreamble("#extension GL_GOOGLE_include_directive : require\n");
// ...
}
ARB_shading_language_include
and GL_GOOGLE_include_directive
are two different extensions, and ARB_shading_language_include
doesn’t work for glslang
.
Setting input and output settings
Then you need to pass in three settings: EnvInput
, EnvClient
, and EnvTarget
.
According to the comments in ShaderLang.h
, these three settings represent:
setEnvInput(EShSource langguageFamily, EShLanguage stage, EShClient dialect, int dialectVersion)
: The input source language and stage.setEnvClient(EShClient client, EShTargetClientVersion version)
: The client APi and its version.setEnvTarget(EShTargetLanguage lang, EShTargetLanguageVersion version)
: The target language and its version. Currentlylang
only supportsEShTargetSpv
for SPIR-V.
For example, I have a vertex shader source written in GLSL, and I’m using the Vulkan dialect of GLSL and version 100 of GL_KHR_vulkan_glsl
extension.
I want to compile for Vulkan, and my Vulkan version is 1.3. I want it to produce SPIR-V binaries, and I want SPIR-V 1.0 (the latest version is 1.6 but I want better compatibility).
So I would continue with:
std::vector<uint32_t> CompileGLSLToSPRIV(const std::string &source) {
// ...
shader.setEnvInput(glslang::EShSourceGlsl, EShLangVertex, glslang::EShClientVulkan, 100);
shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_3);
shader.setEnvTarget(glslang::EshTargetSpv, glslang::EShTargetSpv_1_0);
// ...
}
Parsing and linking the shader
For parsing shader, we need to specify a TBulitInResource
to tell glslang
how many resources the shader is allowed to use. This can be a SUPER long list (check ResourceLimits.cpp), but luckily we can simply use the default config.
Here’s how to parse the shader:
// ...
#include <glslang/Public/ResourceLimits.h>
// ...
std::vector<uint32_t> CompileGLSLToSPRIV(const std::string &source) {
// ...
shader.parse(
GetDefaultResources(), // default TBuiltInResource from ResourceLimits.h
100, // default version
false, // not forward compatible
EShMsgDefault // report default error/warning messages
);
// There're overloads for extra options like includers but we'll skip that for now
printf("Parsing shader: %s\n", shader.getInfoLog()); // get the log for parsing the shader
// ...
}
After that, we can simply create a glslang::TProgram
object and link the shader:
std::vector<uint32_t> CompileGLSLToSPRIV(const std::string &source) {
// ...
glslang::TProgram program;
program.addShader(&shader);
program.link(EShMsgDefault); // link and report default error/warning messages
printf("Linking program: %s\n", program.getInfoLog()); // get the log for linking the program
// ...
}
Export SPIR-V bytes
If everything goes smoothly, we can now export the generated SPIR-V bytes.
First we need to include the utility function:
#include <glslang/SPIRV/GlslangToSpv.h>
It’s pretty straight forward to get the result:
std::vector<uint32_t> CompileGLSLToSPRIV(const std::string &source) {
// ...
// get the vertex stage glslang intermediate representation from the program
glslang::TIntermediate *intermediate = program.getIntermediate(EShVertex);
std::vector<uint32_t> spriv; // the vector for the output
glslang::GlslangToSpv(*intermediate, spirv); // convert the glslang intermediate into SPIR-V bytes
return spirv; // Usually the result is optimized with RVO so don't worry about copying
}
You can now pass the SPIR-V bytes to VkShaderModuleCreateInfo
(or probably glShaderBinary
? I don’t know if anyone is actually using this 🙂).