-
Book Overview & Buying
-
Table Of Contents
Vulkan 3D Graphics Rendering Cookbook - Second Edition
By :
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.
We recommend reading the recipe Compiling Vulkan shaders at runtime in Chapter 1 before you proceed.
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.
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;
}
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");
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) {}
};
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)) };
}
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);
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,
};
}
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;
#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.