Working with a shader in Vulkan

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:

  • Vertex shaders: The graphics pipeline executes the vertex shader as a result of the primitive assembly. It is used to process geometric vertices. It manipulates the data that may be required by the subsequent shaders (if enabled), such as lighting information by the fragment shader.
  • Tessellation shaders: This is a vertex-processing stage. When enabled, it subdivides the patches of vertex data into smaller primitives governed by the control and evaluate shaders.
  • Geometry shaders: This shader when enabled has the capability to produce new geometry at the execution time by using the output from the previous shader stages (tessellation and vertex shaders).
  • Fragment shaders: The rasterizer (fixed function) produces the fragments using the processed vertices from the previous stages. These fragments are then processed by the fragment shader, which is responsible for coloring them.
  • Compute shaders: Compute shaders are invoked in the workgroup for computing arbitrary information in a massive parallel computation format. The compute pipeline only consists of the compute shader.

Unlike OpenGL, where GLSL is the official shading language, Vulkan uses SPIR-V, which is a whole new approach to shaders and computing kernels.

Introduction to SPIR-V

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.

Introduction to SPIR-V

The following diagram used from Khronos's official SPIR-V specification provides an overview of the SPIR-V file format:

Introduction to SPIR-V

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.

Compiling a GLSL shader into SPIR-V

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.

Offline compilation with the glslangValidator executable

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:

  • Location: The 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.
  • Usage: Its usage is defined as glslangValidator [option]... [file]....

Here, each file ends in .<stage>, where <stage> is one of the following:

Stage

Meaning

.conf

To provide an optional configuration file that replaces the default configuration

.vert

For a vertex shader

.tesc

For a tessellation control shader

.tese

For a tessellation evaluation shader

.geom

For a geometry shader

.frag

For a fragment shader

.comp

For a compute shader

  • An example: Refer to the following instruction to compile the GLSL source file into the SPIR-V format (.spv):
    • Open the terminal, go to the source folder (let's say the one containing the vertex shader 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

Note

The glslangValidator.exe can also be built using the LunarG SDK glslang source code.

Online compilation with SPIR-V tool libraries

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:

  • Location: Locate the SPIR-V-tool folder using the <Vulkan SDK Path ><Vulkan SDK Version>glslang path.
  • CMake: The 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.
  • Required libraries: The following libraries (on Windows) are required in order to support the online compilation of the GLSL files into SPIR-V:
    • SPIRV.lib
    • glslang.lib
    • OGLCompiler.lib
    • OSDependent.lib
    • HLSL.lib

Implementing a shader

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:

  • A project name: Give the recipe a proper name and set Vulkan SDK path:
      set (Recipe_Name "7e_ShadersWithSPIRV")
  • Header files: Include the header file specified in the glslang directory. This will allow us to include the SPIRV/GlslangToSpv.h required in the source program:
  • Static libraries: From the Vulkan-SDK-compiled projects, we need SPIRV, glslang, HLSL OGLCompiler, and OSDependent static libraries.
  • The link library path: Provide the path to the solution where it can pick the static libraries specified in the preceding point. The variable name, VULKAN_PATH, specifies the path for the Lunar SDK:
  • The following CMake code adds the required libraries for GLSL to SPIR-V conversion. You can convert a GLSL code into .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:

  1. Initialize the GLSL language shader library using glslang::InitializeProcess(). Note that this function needs to be called exactly once per process before you use anything else.
  2. Convert the GLSL shader code into a SPIR-V shader byte array. This includes the following: 
    • Parsing the GLSL source code
    • Adding the parse shader to the program object
    • Linking to the project object
    • Using glslang::GlslangToSpv() to covert the compiled binary shader into the SPIR-V format

  3. Use the converted SPIR-V shader intermediate binary code to create the shader module using vkCreateShaderModule().
  4. Finalize the processing using 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

device

This is the logical device handle to which the shader module is associated.

pCreateInfo

This is the pointer to the VkShaderModuleCreateInfo structure object.

pAllocator

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.

pShaderModule

This refers to the created VkShaderModule object.

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

type

This refers to the type of the structure. This must be of the type VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO.

next

This is either NULL or a pointer to an extension-specific structure.

flags

This is reserved for future use.

codeSize

This refers to the source code size in bytes. The source is pointed by code.

code

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

device

This is the logical device to be used to destroy the shader module object.

shaderModule

This is the handle of the shader module that needs to be destroyed.

allocator

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.78.83