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.
- The helper function
loadShaderModule()
is defined inshared/Utils.cpp
. It detects the shader stage type from the file name extension and callscreateShaderModule()
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; }
- In this way, given a pointer to
IContext
, Vulkan shader modules can be created from GLSL shaders as follows, wherecodeVS
andcodeFS
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");
- The parameter of
createShaderModule()
is a structureShaderModuleDesc
containing all properties required to create a Vulkan shader module. If thedataSize
member field is non-zero, thedata
field is treated as a binary SPIR-V blob. IfdataSize
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) {} };
- Inside
VulkanContext::createShaderModule()
, we handle the branching for textual GLSL and binary SPIR-V shaders. An actualVkShaderModule
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)) }; }
- 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);
- 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, }; }
- The
VulkanContext::createShaderModuleFromGLSL()
function invokescompileShader()
, 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 aforementionedcreateShaderModuleFromSPIRV()
to create an actualVkShaderModule
. 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 inlightweightvk/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;
- 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.