Shaders are the means by which the programmable stage is controlled in the graphics and compute pipeline.
The graphics pipeline includes vertex, tessellation, geometry, and fragment shaders. Collectively, the first four-vertex, tessellation control, tessellation evaluation, and geometry shaders-are responsible for the vertex-processing stages. These are followed by the fragment shader, which is executed after rasterization.
Here's a bit about the graphics pipeline shaders:
Unlike OpenGL, where GLSL is the official shading language, Vulkan uses SPIR-V, which is a whole new approach to shaders and computing kernels.
Unlike OpenGL shading language (GLSL), which is a human-readable form for a shader, Vulkan totally relies on SPIR-V, which is a low-level binary intermediate representation (IR). As the high-level language is no more a requirement for SPIR-V, it reduces the driver complexity significantly. This allows an application to accept any high-level language (GLSL or HLSL) as long as the GPU vendor provides the compiler to convert it into the SPIR-V form.
SPIR-V is a binary intermediate language used to represent graphical shader stages and compute kernels for multiple APIs. It stores the information in a stream of 32-bit words. It consists of mainly two parts: header and payload. The header consists of the first five slots (5 x 4 bytes = 20 bytes) that are helpful in recognizing the input source as the SPIR-V input stream. However, the payload represents variable length instructions containing the source data.
SPIR-V is a platform-independent intermediate language that can be used for multiple languages that feed multiple drivers under the hood, as shown in the following diagram. Compilers to SPIR-V exist for multiple source languages, such as GLSL or HLSL, or even for reading compute kernels. The shader module provides multiple entry points that can provide good wins in terms of shader code size and disk I/O requirement.
The following diagram used from Khronos's official SPIR-V specification provides an overview of the SPIR-V file format:
Although SPIR-V is a high-level language, it is also simple enough to bypass all of the text or string parsing, which is a good thing to achieve higher performance. According to the official specification, SPIR-V first encodes a set of annotations and decorations and then a collection of functions. Each function encodes a control flow graph (CFG) of basic blocks with additional instructions to preserve source-code-structured control flow. Load/store instructions are used to access declared variables, which include all of the I/O. Intermediate results bypassing load/store use the static single assignment (SSA) representation. Data objects are represented logically with hierarchical-type information. There is no flattening of aggregates or assignments to physical register banks and so on. Selectable addressing models establish whether general pointers may be used or whether memory access is purely logical.
In this section, we will implement a very simple vertex and fragment shader in GLSL and convert it into the SPIR-V form in order to utilize it in our Vulkan program. There are two ways in which the GLSL shader can be converted into the SPIR-V binary form-offline and online. The former uses an executable to covert a GLSL source into the SPIR-V native format file; this file is then injected into the running Vulkan program. The latter makes use of a dynamic library to compile the GLSL into the SPIR-V form.
The precompiled Lunar-G SDK's binaries include the glslangValidator
executable, which can be used to convert GLSL into SPIR-V's .spv
format. This does not require runtime compilation of the data and can be injected upfront. In this approach, the developers cannot change the GLSL program, and seeing the effects that take place upfront, it has to be recompiled again with every change added. This way is suitable for product releases where not to many changes are expected.
For a development cycle, where frequent changes are expected, the online method is suitable. Refer to the next section for more information on this. Also, refer to the following points:
glslangValidator
can be located in <Vulkan SDK Path ><Vulkan SDK Version>Bin
on Windows. For a 32-bit platform implementation, refer to the Bin32
folder instead.glslangValidator [option]... [file]...
.Here, each file ends in .<stage>
, where <stage>
is one of the following:
Stage |
Meaning |
|
To provide an optional configuration file that replaces the default configuration |
|
For a vertex shader |
|
For a tessellation control shader |
|
For a tessellation evaluation shader |
|
For a geometry shader |
|
For a fragment shader |
|
For a compute shader |
.spv
):Tri.vert
), and type the following command; this will result into the Tri-Vert.spv
SPIR-V format:
glslangValidator.exe -V Tri.vert -o Tri-Vert.spv
The Lunar SDK also provides on-the-fly compilation using GLslang libraries. We need to compile these libraries from the SDK source code and include them in our source project. This exposes some special APIs that can be used to pass the GLSL shader source code into the project, making it available to the Vulkan shader module in the SPIR-V format behind the curtains.
In order to build the source code and compile them into libraries, you need the following:
<Vulkan SDK Path ><Vulkan SDK Version>glslang
path.GLslang
folder contains CMakelists.txt
, which can be used to build the platform-specific project. After you build CMake, the following projects will be created: glslang
, glslangValidator
, OGLCompiler
, OSDependent
, SPIRV
, and spirv-remap
. Upon the compilation of the project in the debug and release mode, it will produce the necessary static libraries in the destination build folder.SPIRV.lib
glslang.lib
OGLCompiler.lib
OSDependent.lib
HLSL.lib
It's time to bring some shader capabilities in our example. This section will help us achieve this step by step. Follow the instructions given here:
Go to the sample application's CMakeLists.txt
file and make the following changes:
set (Recipe_Name "7e_ShadersWithSPIRV")
glslang
directory. This will allow us to include the SPIRV/GlslangToSpv.h
required in the source program:SPIRV
, glslang
, HLSL OGLCompiler
, and OSDependent
static libraries.VULKAN_PATH
, specifies the path for the Lunar SDK:.spv
form in two ways:
a) Offline: Using Vulkan SDK's glslangValidator.exe
, this tool does not require additional libraries, the Vulkan-1.lib
is more than enough. This mode is useful when the project is in the deployment stage where shaders are not undergoing the development cycle's dynamic changes
b) Online: Auto-converting GLSL into .spv
on runtime using glslang helper functions. At project development stage, the developers do not need to compile the GLSL shader offline for desire shader code changes. The glslang helper functions automatically compiles the GLSL into .spv form. This mode requires a few libraries these are: SPIRV, glslang, OGLCompiler, OSDependent, HLSL.Using CMake's BUILD_SPV_ON_COMPILE_TIME
variable you can choose either of the desired options to convert the GLSL shader into .spv
form. For more information, please add the below code in existing CMakeLists.txt
and follow through the inline comments:# BUILD_SPV_ON_COMPILE_TIME - accepted value ON or OFF # ON - Reads the GLSL shader file and auto convert in SPIR-V form (.spv). This requires additional libraries support from VulkanSDK like SPIRV glslang OGLCompiler OSDependent HLSL # OFF - Only reads .spv files, which need to be compiled offline using glslangValidator.exe. #For example: glslangValidator.exe <GLSL file name> -V -o <output filename in SPIR-V(.spv) form> option(BUILD_SPV_ON_COMPILE_TIME "BUILD_SPV_ON_COMPILE_TIME" OFF) if(BUILD_SPV_ON_COMPILE_TIME) # Preprocessor flag allows the solution to use glslang library functions add_definitions(-DAUTO_COMPILE_GLSL_TO_SPV) # GLSL - use Vulkan SDK's glslang library for compling GLSL to SPV # This does not require offline coversion of GLSL shader to SPIR-V(.spv) form set(GLSLANGDIR "${VULKAN_PATH}/glslang") get_filename_component(GLSLANG_PREFIX "${GLSLANGDIR}" ABSOLUTE) # Check if glslang directory exists if(NOT EXISTS ${GLSLANG_PREFIX}) message(FATAL_ERROR "Necessary glslang components do not exist: "${GLSLANG_PREFIX}) endif() # Include the glslang directory include_directories( ${GLSLANG_PREFIX} ) # If compiling GLSL to SPV using we need the following libraries set(GLSLANG_LIBS SPIRV glslang OGLCompiler OSDependent HLSL) # Generate the list of files to link, per flavor. foreach(x ${GLSLANG_LIBS}) list(APPEND VULKAN_LIB_LIST debug ${x}d optimized ${x}) endforeach() # Note: While configuring CMake for glslang we created the binaries in a "build" folder inside ${VULKAN_PATH}/glslang. # Therefore, you must edit the below lines for your custom path like <Your binary path>/OGLCompilersDLL, <Your binary path>/OSDependent/Windows link_directories(${VULKAN_PATH}/glslang/build/OGLCompilersDLL ) link_directories(${VULKAN_PATH}/glslang/build/glslang/ OSDependent/Windows) link_directories(${VULKAN_PATH}/glslang/build/glslang) link_directories(${VULKAN_PATH}/glslang/build/SPIRV) link_directories(${VULKAN_PATH}/glslang/build/hlsl) endif()
Implement the user-defined shader custom class called VulkanShader
in VulkanShader.h/.cpp
to manage all the shader activities. Refer to the implementation here:
/************** VulkanShader.h **************/ #pragma once #include "Headers.h" // Shader class managing the shader conversion, compilation, linking class VulkanShader { public: VulkanShader() {} // Constructor ~VulkanShader() {} // Destructor // Entry point to build the shaders void buildShader(const char *vertShaderText, const char *fragShaderText); // Convert GLSL shader to SPIR-V shader bool GLSLtoSPV(const VkShaderStageFlagBits shaderType, const char *pshader, std::vector<unsigned int> &spirv); // Kill the shader when not required void destroyShaders(); // Type of shader language. This could be - EShLangVertex, // Tessellation Control, Tessellation Evaluation, Geometry, // Fragment and Compute EShLanguage getLanguage (const VkShaderStageFlagBits shaderType); // Initialize the TBuitInResource void initializeResources(TBuiltInResource &Resources); // Vk structure storing vertex & fragment shader information VkPipelineShaderStageCreateInfo shaderStages[2]; };
The user-defined VulkanShader
class exposes the buildShader()
function, which allows you to inject the GLSL shaders in order to compile them into the Vulkan project. Behind the curtains, the function makes use of the GLslang SPIR-V library functions to convert them into the native form.
This function builds the shader in four steps:
glslang::InitializeProcess()
. Note that this function needs to be called exactly once per process before you use anything else.glslang::GlslangToSpv()
to covert the compiled binary shader into the SPIR-V format
vkCreateShaderModule()
.glslang::FinalizeProcess()
during the tear down. This function must be called once per process.This function takes two arguments as parameter input, specifying the vertex and fragment shader. The shaders are used to create the shader module VkShaderModule
with the help of the vkCreateShaderModule()
API. This API intakes the VkPipelineShaderStageCreateInfo
control structure as primary input containing the necessary information of the shader.
For more information, follow the syntax and parameter description given here:
VKAPI_ATTR VkResult VKAPI_CALL vkCreateShaderModule( VkDevice device, const VkShaderModuleCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkShaderModule* pShaderModule);
The following table describes the various fields of this function:
Parameters |
Description |
|
This is the logical device handle to which the shader module is associated. |
|
This is the pointer to the |
|
This controls the host memory allocation process. For more information, refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan. |
|
This refers to the created |
typedef struct VkShaderModuleCreateInfo { VkStructureType type; const void* next; VkShaderModuleCreateFlags flags; size_t codeSize; const uint32_t* code; } VkShaderModuleCreateInfo;
The following table describes the various fields of this structure:
Parameters |
Description |
|
This refers to the type of the structure. This must be of the type |
|
This is either |
|
This is reserved for future use. |
|
This refers to the source code size in bytes. The source is pointed by |
|
This refers to the source code that will be used to create the shader module. |
The following code describes the implementation of the shader used in the sample example. The implementation is done in the buildShader()
function; this helper function presently supports only vertex and fragment shaders, whose source code is passed as parameters into this function in the GLSL form. The following is the implementation:
/************** VulkanShader.cpp **************/ // Helper function intaking the GLSL vertex and fragment shader. // It prepares the shaders to be consumed in the SPIR-V format // with the help of glslang library helper functions. void VulkanShader::buildShader(const char *vertShaderText, const char *fragShaderText) { VulkanDevice* deviceObj = VulkanApplication::GetInstance() ->deviceObj; VkResult result; bool retVal; // Fill in the control structure to push the necessary // details of the shader. std::vector<unsigned int> vertexSPV; shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_ SHADER_STAGE_CREATE_INFO; shaderStages[0].pNext = NULL; shaderStages[0].pSpecializationInfo = NULL; shaderStages[0].flags = 0; shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; shaderStages[0].pName = "main"; glslang::InitializeProcess(); retVal = GLSLtoSPV(VK_SHADER_STAGE_VERTEX_BIT, vertShaderText, vertexSPV); assert(retVal); VkShaderModuleCreateInfo moduleCreateInfo; moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; moduleCreateInfo.pNext = NULL; moduleCreateInfo.flags = 0; moduleCreateInfo.codeSize = vertexSPV.size() * sizeof(unsigned int); moduleCreateInfo.pCode = vertexSPV.data(); result = vkCreateShaderModule(deviceObj->device, &moduleCreateInfo, NULL, &shaderStages[0].module); assert(result == VK_SUCCESS); std::vector<unsigned int> fragSPV; shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_ SHADER_STAGE_CREATE_INFO; shaderStages[1].pNext = NULL; shaderStages[1].pSpecializationInfo = NULL; shaderStages[1].flags = 0; shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; shaderStages[1].pName = "main"; retVal = GLSLtoSPV(VK_SHADER_STAGE_FRAGMENT_BIT, fragShaderText, fragSPV); assert(retVal); moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER _MODULE_CREATE_INFO; moduleCreateInfo.pNext = NULL; moduleCreateInfo.flags = 0; moduleCreateInfo.codeSize = fragSPV.size() * sizeof(unsigned int); moduleCreateInfo.pCode = fragSPV.data(); result = vkCreateShaderModule(deviceObj->device, &moduleCreateInfo, NULL, &shaderStages[1].module); assert(result == VK_SUCCESS); glslang::FinalizeProcess(); }
The vkCreateShaderModule()
takes the SPIR-V form as an input; therefore, we need the GLslang helper function to convert the GLSL shaders into the native format binary form supported by SPIR-V.
Before you call any glslang function, the library needs to be initialized once per process. This is done by calling glslang::InitializeProcess()
. Next, the input shader is converted into the stream of SPIR-V bits using the user-defined function VulkanShader::GLSLtoSPV()
, as mentioned in the following code:
bool VulkanShader::GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char *pshader, std::vector<unsigned int> &spirv) { glslang::TProgram& program = *new glslang::TProgram; const char *shaderStrings[1]; TBuiltInResource Resources; initializeResources(Resources); // Enable SPIR-V and Vulkan rules when parsing GLSL EShMessages messages = (EShMessages)(EShMsgSpvRules | EShMsgVulkanRules); EShLanguage stage = findLanguage(shader_type); glslang::TShader* shader = new glslang::TShader(stage); shaderStrings[0] = pshader; shader->setStrings(shaderStrings, 1); if (!shader->parse(&Resources, 100, false, messages)) { puts(shader->getInfoLog()); puts(shader->getInfoDebugLog()); return false; } program.addShader(shader); // Link the program and report if errors... if (!program.link(messages)) { puts(shader->getInfoLog()); puts(shader->getInfoDebugLog()); return false; } glslang::GlslangToSpv(*program.getIntermediate(stage), spirv); return true; }
The GLSLtoSPV
function needs to be compiled for each type of shader to convert its GLSL source code into its SPIR-V form. First it creates an empty shader object and initializes the shader resource structure. Using the shader type passed to the GLSLtoSPV
function parameter, determine the shader language using the findLanguage()
helper function (see the following code snippet) and create a TShader
shader object. Pass the GLSL shader source code to this shader and parse it to check for any potential issues. In case errors are found, report the error to help the user rectify it:
EShLanguage VulkanShader::findLanguage(const VkShaderStageFlagBits shader_type) { switch (shader_type) { case VK_SHADER_STAGE_VERTEX_BIT: return EShLangVertex; case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT: return EShLangTessControl; case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: return EShLangTessEvaluation; case VK_SHADER_STAGE_GEOMETRY_BIT: return EShLangGeometry; case VK_SHADER_STAGE_FRAGMENT_BIT: return EShLangFragment; case VK_SHADER_STAGE_COMPUTE_BIT: return EShLangCompute; default: printf("Unknown shader type specified: %d. Exiting!", shaderType); exit(1); } }
Add the shader object in the program object to compile and link it. Upon the successful linking of the glslang::GlslangToSpv()
API called, convert the GLSL source program into the intermediate shader (.spv
) form.
Finally, upon exiting from the application, don't forget to delete the shaders. The shader can be destroyed using the vkDestroyShaderModule()
API. Refer to the following implementation for more information:
void VulkanShader::destroyShaders() { VulkanApplication* appObj = VulkanApplication::GetInstance(); VulkanDevice* deviceObj = appObj->deviceObj; vkDestroyShaderModule(deviceObj->device, shaderStages[0].module, NULL); vkDestroyShaderModule(deviceObj->device, shaderStages[1].module, NULL); }
Here is the syntax of the vkDestroyShaderModule()
API:
VKAPI_ATTR void VKAPI_CALL vkDestroyShaderModule( VkDevice device, VkShaderModule shaderModule, const VkAllocationCallbacks* allocator);
The following table describes the various fields of this API:
Parameters |
Description |
|
This is the logical device to be used to destroy the shader module object. |
|
This is the handle of the shader module that needs to be destroyed. |
|
This controls the host memory deallocation process. For more information, refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan. |
52.15.78.83