Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Vulkan 3D Graphics Rendering Cookbook

You're reading from   Vulkan 3D Graphics Rendering Cookbook Implement expert-level techniques for high-performance graphics with Vulkan

Arrow left icon
Product type Paperback
Published in Feb 2025
Publisher Packt
ISBN-13 9781803248110
Length 714 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Alexey Medvedev Alexey Medvedev
Author Profile Icon Alexey Medvedev
Alexey Medvedev
Sergey Kosarevsky Sergey Kosarevsky
Author Profile Icon Sergey Kosarevsky
Sergey Kosarevsky
Arrow right icon
View More author details
Toc

Table of Contents (14) Chapters Close

Preface 1. Establishing a Build Environment 2. Getting Started with Vulkan FREE CHAPTER 3. Working with Vulkan Objects 4. Adding User Interaction and Productivity Tools 5. Working with Geometry Data 6. Physically Based Rendering Using the glTF 2.0 Shading Model 7. Advanced PBR Extensions 8. Graphics Rendering Pipeline 9. glTF Animations 10. Image-Based Techniques 11. Advanced Rendering Techniques and Optimizations 12. Other Books You May Enjoy
13. Index

Initializing Vulkan shader modules

The Vulkan API consumes shaders in the form of compiled SPIR-V binaries. We already learned how to compile shaders from GLSL source code to SPIR-V using the open-source glslang compiler from Khronos. In this recipe, we will learn how to use GLSL shaders and precompiled SPIR-V binaries in Vulkan.

Getting ready

We recommend reading the recipe Compiling Vulkan shaders at runtime in Chapter 1 before you proceed.

How to do it...

Let’s take a look at our next demo application, Chapter02/02_HelloTriangle, to learn the high-level LightweightVK API for shader modules. There’s a method createShaderModule() in IContext that does the work and a helper function loadShaderModule() which makes it easier to use.

  1. The helper function loadShaderModule() is defined in shared/Utils.cpp. It detects the shader stage type from the file name extension and calls createShaderModule() with the appropriate parameters.
    Holder<ShaderModuleHandle> loadShaderModule(
      const std::unique_ptr<lvk::IContext>& ctx,
      const char* fileName)
    {
      const std::string code = readShaderFile(fileName);
      const lvk::ShaderStage stage =
        lvkShaderStageFromFileName(fileName);
      Holder<ShaderModuleHandle> handle =
        ctx->createShaderModule({ code.c_str(), stage,
         (std::string("Shader Module: ") + fileName).c_str() });
      return handle;
    }
    
  2. In this way, given a pointer to IContext, Vulkan shader modules can be created from GLSL shaders as follows, where codeVS and codeFS are null-terminated strings holding the vertex and fragment shader source code, respectively.
    Holder<ShaderModuleHandle> vert = loadShaderModule(
      ctx, "Chapter02/02_HelloTriangle/src/main.vert");
    Holder<ShaderModuleHandle> frag = loadShaderModule(
      ctx, "Chapter02/02_HelloTriangle/src/main.frag");
    
  3. The parameter of createShaderModule() is a structure ShaderModuleDesc containing all properties required to create a Vulkan shader module. If the dataSize member field is non-zero, the data field is treated as a binary SPIR-V blob. If dataSize is zero, data is treated as a null-terminated string containing GLSL source code.
    struct ShaderModuleDesc {
      ShaderStage stage = Stage_Frag;
      const char* data = nullptr;
      size_t dataSize = 0;
      const char* debugName = "";
      ShaderModuleDesc(const char* source, lvk::ShaderStage stage,
        const char* debugName) : stage(stage), data(source),
        debugName(debugName) {}
      ShaderModuleDesc(const void* data, size_t dataLength,
        lvk::ShaderStage stage, const char* debugName) :
        stage(stage), data(static_cast<const char*>(data)),
        dataSize(dataLength), debugName(debugName) {}
    };
    
  4. Inside VulkanContext::createShaderModule(), we handle the branching for textual GLSL and binary SPIR-V shaders. An actual VkShaderModule object is stored in a pool, which we will discuss in subsequent chapters.
    struct ShaderModuleState final {
      VkShaderModule sm = VK_NULL_HANDLE;
      uint32_t pushConstantsSize = 0;
    };
    Holder<ShaderModuleHandle>
      VulkanContext::createShaderModule(const ShaderModuleDesc& desc)
    {
      Result result;
      ShaderModuleState sm = desc.dataSize ?
        createShaderModuleFromSPIRV(
          desc.data, desc.dataSize, desc.debugName, &result) :
        createShaderModuleFromGLSL(
          desc.stage, desc.data, desc.debugName, &result); // text
      return { this, shaderModulesPool_.create(std::move(sm)) };
    }
    
  5. The creation of a Vulkan shader module from a binary SPIR-V blob looks as follows. Error checking is omitted for simplicity.
    ShaderModuleState VulkanContext::createShaderModuleFromSPIRV(
      const void* spirv,
      size_t numBytes,
      const char* debugName,
      Result* outResult) const
    {
      VkShaderModule vkShaderModule = VK_NULL_HANDLE;
      const VkShaderModuleCreateInfo ci = {
        .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
        .codeSize = numBytes,
        .pCode = (const uint32_t*)spirv,
      };
      vkCreateShaderModule(vkDevice_, &ci, nullptr, &vkShaderModule);
    
  6. There’s one important trick here. We will need the size of push constants in the shader to initialize our Vulkan pipelines later. Here, we use the SPIRV-Reflect library to introspect the SPIR-V code and retrieve the size of the push constants from it.
      SpvReflectShaderModule mdl;
      SpvReflectResult result =
        spvReflectCreateShaderModule(numBytes, spirv, &mdl);
      LVK_ASSERT(result == SPV_REFLECT_RESULT_SUCCESS);
      SCOPE_EXIT {
        spvReflectDestroyShaderModule(&mdl);
      };
      uint32_t pushConstantsSize = 0;
      for (uint32_t i = 0; i < mdl.push_constant_block_count; ++i) {
        const SpvReflectBlockVariable& block =
          mdl.push_constant_blocks[i];
        pushConstantsSize = std::max(pushConstantsSize, block.offset + block.size);
      }
      return {
        .sm = vkShaderModule,
        .pushConstantsSize = pushConstantsSize,
      };
    }
    
  7. The VulkanContext::createShaderModuleFromGLSL() function invokes compileShader(), which we learned about in the recipe Compiling Vulkan shaders at runtime in Chapter 1 to create a SPIR-V binary blob. It then calls the aforementioned createShaderModuleFromSPIRV() to create an actual VkShaderModule. Before doing so, it injects a bunch of textual source code into the provided GLSL code. This is done to reduce code duplication in the shader. Things like declaring GLSL extensions and helper functions for bindless rendering are injected here. The injected code is quite large, and we will explore it step by step in subsequent chapters. For now, you can find it in lightweightvk/lvk/vulkan/VulkanClasses.cpp.
    ShaderModuleState VulkanContext::createShaderModuleFromGLSL(
      ShaderStage stage,
      const char* source,
      const char* debugName,
      Result* outResult) const
    {
      const VkShaderStageFlagBits vkStage = shaderStageToVkShaderStage(stage);
      std::string sourcePatched;
    
  8. The automatic GLSL code injection happens only when the provided GLSL shader does not contain the #version directive. This allows you to override the code injection and provide complete GLSL shaders manually.
      if (strstr(source, "#version ") == nullptr) {
        if (vkStage == VK_SHADER_STAGE_TASK_BIT_EXT ||
            vkStage == VK_SHADER_STAGE_MESH_BIT_EXT) {
          sourcePatched += R"(
          #version 460
          #extension GL_EXT_buffer_reference : require
          // ... skipped a lot of injected code
        }
        sourcePatched += source;
        source = sourcePatched.c_str();
      }
      const glslang_resource_t glslangResource =
        lvk::getGlslangResource(
          getVkPhysicalDeviceProperties().limits);
      std::vector<uint8_t> spirv;
      const Result result = lvk::compileShader(
        vkStage, source, &spirv, &glslangResource);
      return createShaderModuleFromSPIRV(
        spirv.data(), spirv.size(), debugName, outResult);
    }
    

Now that our Vulkan shader modules are ready to be used with Vulkan pipelines, let’s learn how to do that in the next recipe.

You have been reading a chapter from
Vulkan 3D Graphics Rendering Cookbook - Second Edition
Published in: Feb 2025
Publisher: Packt
ISBN-13: 9781803248110
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Visually different images