-
Book Overview & Buying
-
Table Of Contents
Vulkan 3D Graphics Rendering Cookbook - Second Edition
By :
A Vulkan pipeline is an implementation of an abstract graphics pipeline, which is a sequence of operations that transform vertices and rasterize the resulting image. Essentially, it’s like a single snapshot of a “frozen” OpenGL state. Vulkan pipelines are mostly immutable, meaning multiple Vulkan pipelines should be created to allow different data paths through the graphics pipeline. In this recipe, we will learn how to create a Vulkan pipeline suitable for rendering a colorful triangle and explore how low-level and verbose Vulkan can be wrapped into a simple, high-level interface.
To get all the basic information about Vulkan pipelines, we recommend reading Vulkan Cookbook by Pawel Lapinski which was published by Packt, or the Vulkan Tutorial series by Alexander Overvoorde https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics/Introduction.
For additional information on descriptor set layouts, check out the chapter https://vulkan-tutorial.com/Uniform_buffers/Descriptor_layout_and_buffer.
Vulkan pipelines require Vulkan shader modules. Check the previous recipe Initializing Vulkan shader modules before going through this recipe.
Let’s dive into how to set up a Vulkan pipeline suitable for our triangle rendering application. Due to the verbosity of the Vulkan API, this recipe will be one of the longest. We will begin with the high-level code in our demo application, Chapter02/02_HelloTriangle, and work our way through to the internals of LightweightVK down to the Vulkan API. In the following chapters, we will explore more details, such as dynamic states, multisampling, vertex input, and more.
Let’s take a look at the initialization and the main loop of Chapter02/02_HelloTriangle.
GLFWwindow* window =
lvk::initWindow("Simple example", width, height);
std::unique_ptr<lvk::IContext> ctx =
lvk::createVulkanContextWithSwapchain(window, width, height, {});
lvk::RenderPipelineHandle is an opaque handle that manages a collection of VkPipeline objects, and lvk::Holder is a RAII wrapper that automatically disposes of handles when they go out of scope. The method createRenderPipeline() takes a RenderPipelineDesc structure, which contains the data necessary to configure a rendering pipeline. For our first triangle demo, we aim to keep things as minimalistic as possible, so we simply set the vertex and fragment shaders, and define the format of a color attachment. This is the absolute minimum required to render something into a swapchain image.
Holder<lvk::ShaderModuleHandle> vert = loadShaderModule(
ctx, "Chapter02/02_HelloTriangle/src/main.vert");
Holder<lvk::ShaderModuleHandle> frag = loadShaderModule(
ctx, "Chapter02/02_HelloTriangle/src/main.frag");
Holder<lvk::RenderPipelineHandle> rpTriangle =
ctx->createRenderPipeline({
.smVert = vert,
.smFrag = frag,
.color = { { .format = ctx->getSwapchainFormat() } } });
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
glfwGetFramebufferSize(window, &width, &height);
if (!width || !height) continue;
lvk::ICommandBuffer& buf = ctx->acquireCommandBuffer();
cmdBeginRendering() wraps Vulkan 1.3 dynamic rendering functionality, which enables rendering directly into Vulkan images without explicitly creating any render passes or framebuffer objects. It takes a description of a render pass lvk::RenderPass and a description of a framebuffer lvk::Framebuffer. We will explore it in more detail in subsequent chapters. Here, we use the current swapchain texture as the first color attachment and clear it to a white color before rendering using the attachment load operation LoadOp_Clear, which corresponds to VK_ATTACHMENT_LOAD_OP_CLEAR in Vulkan. The store operation is set to StoreOp_Store by default.
buf.cmdBeginRendering(
{.color = {{ .loadOp = LoadOp_Clear, .clearColor = {1,1,1,1} }}},
{.color = {{ .texture = ctx->getCurrentSwapchainTexture() }}});
cmdDraw(), which is a wrapper on top of vkCmdDraw(). You may have noticed that we did not use any index or vertex buffers at all. We will see why in a moment as we look into GLSL shaders. The command cmdEndRendering() corresponds to vkCmdEndRendering() from Vulkan 1.3.
buf.cmdBindRenderPipeline(rpTriangle);
buf.cmdPushDebugGroupLabel("Render Triangle", 0xff0000ff);
buf.cmdDraw(3);
buf.cmdPopDebugGroupLabel();
buf.cmdEndRendering();
ctx->submit(buf, ctx->getCurrentSwapchainTexture());
}
Let’s take a look at the GLSL shaders.
gl_VertexIndex, which gets incremented automatically for every subsequent vertex, to return hardcoded values for positions and vertex colors.
#version 460
layout (location=0) out vec3 color;
const vec2 pos[3] = vec2[3](
vec2(-0.6, -0.4), vec2(0.6, -0.4), vec2(0.0, 0.6) );
const vec3 col[3] = vec3[3](
vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0) );
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
color = col[gl_VertexIndex];
}
#version 460
layout (location=0) in vec3 color;
layout (location=0) out vec4 out_FragColor;
void main() {
out_FragColor = vec4(color, 1.0);
}
The application should render a colorful triangle as in the following picture.

Figure 2.2: Hello Triangle
We learned how to draw a triangle with Vulkan using LightweightVK. It is time to look under the hood and find out how this high-level render pipeline management interface is implemented via Vulkan.
To get to the underlying Vulkan implementation, we have to peel a few layers one by one. When we want to create a graphics pipeline in our application, we call the member function IContext::createRenderPipeline() which is implemented in VulkanContext. This function takes in a structure lvk::RenderPipelineDesc which describes our rendering pipeline. Let’s take a closer look at it.
VkPipeline object. It starts with the topology and vertex input descriptions, followed by shader modules for all supported shader stages. While LightweightVK supports mesh shaders, in this book, we will use only vertex, fragment, geometry, and tessellation shaders.
struct RenderPipelineDesc final {
Topology topology = Topology_Triangle;
VertexInput vertexInput;
ShaderModuleHandle smVert;
ShaderModuleHandle smTesc;
ShaderModuleHandle smTese;
ShaderModuleHandle smGeom;
ShaderModuleHandle smTask;
ShaderModuleHandle smMesh;
ShaderModuleHandle smFrag;
SpecializationConstantDesc specInfo = {};
const char* entryPointVert = "main";
const char* entryPointTesc = "main";
const char* entryPointTese = "main";
const char* entryPointGeom = "main";
const char* entryPointTask = "main";
const char* entryPointMesh = "main";
const char* entryPointFrag = "main";
8. We do not store the number of used attachments here. Instead, we use a short helper function to calculate how many attachments we actually have.
ColorAttachment color[LVK_MAX_COLOR_ATTACHMENTS] = {};
uint32_t getNumColorAttachments() const {
uint32_t n = 0;
while (n < LVK_MAX_COLOR_ATTACHMENTS &&
color[n].format != Format_Invalid) n++;
return n;
}
Format depthFormat = Format_Invalid;
Format stencilFormat = Format_Invalid;
CullMode cullMode = lvk::CullMode_None;
WindingMode frontFaceWinding = lvk::WindingMode_CCW;
PolygonMode polygonMode = lvk::PolygonMode_Fill;
StencilState backFaceStencil = {};
StencilState frontFaceStencil = {};
uint32_t samplesCount = 1u;
uint32_t patchControlPoints = 0;
float minSampleShading = 0.0f;
const char* debugName = "";
};
When we call VulkanContext::createRenderPipeline(), it performs sanity checks on RenderPipelineDesc and stores all the values in the RenderPipelineState struct. LightweightVK pipelines cannot be directly mapped one-to-one to VkPipeline objects because the actual VkPipeline objects have to be created lazily. For example, LightweightVK manages Vulkan descriptor set layouts automatically. Vulkan requires a descriptor set layout to be specified for a pipeline object. To address this, the data stored in RenderPipelineState is used to lazily create actual VkPipeline objects through the function VulkanContext::getVkPipeline(). Let’s take a look at how this mechanism works, with error checking and some minor details omitted for simplicity.
RenderPipelineState structure contains some precached values to avoid reinitializing them every time a new Vulkan pipeline object is created. All shader modules must remain alive as long as any pipeline that uses them is still in use.
class RenderPipelineState final {
RenderPipelineDesc desc_;
uint32_t numBindings_ = 0;
uint32_t numAttributes_ = 0;
VkVertexInputBindingDescription
vkBindings_[VertexInput::LVK_VERTEX_BUFFER_MAX] = {};
VkVertexInputAttributeDescription
vkAttributes_[VertexInput::LVK_VERTEX_ATTRIBUTES_MAX] = {};
VkDescriptorSetLayout lastVkDescriptorSetLayout_ =
VK_NULL_HANDLE;
VkShaderStageFlags shaderStageFlags_ = 0;
RenderPipelineState owns a pipeline layout, a pipeline, and memory storage for specialization constants.
VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
VkPipeline pipeline_ = VK_NULL_HANDLE;
void* specConstantDataStorage_ = nullptr;
};
With all data structures in place, we are now ready to go through the implementation of VulkanContext::createRenderPipeline(). Most of the error checking code is skipped for the sake of brevity.
Holder<RenderPipelineHandle> VulkanContext::createRenderPipeline(
const RenderPipelineDesc& desc, Result* outResult)
{
const bool hasColorAttachments =
desc.getNumColorAttachments() > 0;
const bool hasDepthAttachment =
desc.depthFormat != Format_Invalid;
const bool hasAnyAttachments =
hasColorAttachments || hasDepthAttachment;
if (!LVK_VERIFY(hasAnyAttachments)) return {};
if (!LVK_VERIFY(desc.smVert.valid())) return {};
if (!LVK_VERIFY(desc.smFrag.valid())) return {};
RenderPipelineState rps = {.desc_ = desc};
bufferAlreadyBound. Everything else is a very trivial conversion code from our high-level data structures to Vulkan.
const lvk::VertexInput& vstate = rps.desc_.vertexInput;
bool bufferAlreadyBound[LVK_VERTEX_BUFFER_MAX] = {};
rps.numAttributes_ = vstate.getNumAttributes();
for (uint32_t i = 0; i != rps.numAttributes_; i++) {
const VertexInput::VertexAttribute& attr = vstate.attributes[i];
rps.vkAttributes_[i] = { .location = attr.location,
.binding = attr.binding,
.format =
vertexFormatToVkFormat(attr.format),
.offset = (uint32_t)attr.offset };
if (!bufferAlreadyBound[attr.binding]) {
bufferAlreadyBound[attr.binding] = true;
rps.vkBindings_[rps.numBindings_++] = {
.binding = attr.binding,
.stride = vstate.inputBindings[attr.binding].stride,
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX };
}
}
RenderPipelineDesc into local memory storage owned by the pipeline. This simplifies RenderPipelineDesc management on the application side, allowing it to be destroyed after the pipeline is created.
if (desc.specInfo.data && desc.specInfo.dataSize) {
rps.specConstantDataStorage_ =
malloc(desc.specInfo.dataSize);
memcpy(rps.specConstantDataStorage_,
desc.specInfo.data, desc.specInfo.dataSize);
rps.desc_.specInfo.data = rps.specConstantDataStorage_;
}
return {this, renderPipelinesPool_.create(std::move(rps))};
}
Now we can create actual Vulkan pipelines. Well, almost. A couple of very long code snippets await us. These are the longest functions in the entire book, but we have to go through them at least once. Though, error checking is skipped to simplify things a bit.
getVkPipeline() functions retrieves a RenderPipelineState struct from a pool using a provided pipeline handle.
VkPipeline VulkanContext::getVkPipeline(
RenderPipelineHandle handle)
{
lvk::RenderPipelineState* rps =
renderPipelinesPool_.get(handle);
if (!rps) return VK_NULL_HANDLE;
VkPipeline object has changed. Our implementation uses Vulkan descriptor indexing to manage all textures in a large descriptor set and creates a descriptor set layout to store them. When new textures are created, there might not be enough space to store them, requiring the creation of a new descriptor set layout. Every time this happens, we have to delete the old VkPipeline and VkPipelineLayout objects and create new ones.
if (rps->lastVkDescriptorSetLayout_ != vkDSL_) {
deferredTask(std::packaged_task<void()>(
[device = getVkDevice(), pipeline = rps->pipeline_]() {
vkDestroyPipeline(device, pipeline, nullptr); }));
deferredTask(std::packaged_task<void()>(
[device = getVkDevice(), layout = rps->pipelineLayout_]() {
vkDestroyPipelineLayout(device, layout, nullptr); }));
rps->pipeline_ = VK_NULL_HANDLE;
rps->lastVkDescriptorSetLayout_ = vkDSL_;
}
if (rps->pipeline_ != VK_NULL_HANDLE) {
return rps->pipeline_;
}
formatToVkFormat(), convert LightweightVK enumerations to Vulkan.
VkPipelineLayout layout = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
const RenderPipelineDesc& desc = rps->desc_;
const uint32_t numColorAttachments =
desc_.getNumColorAttachments();
VkPipelineColorBlendAttachmentState
colorBlendAttachmentStates[LVK_MAX_COLOR_ATTACHMENTS] = {};
VkFormat colorAttachmentFormats[LVK_MAX_COLOR_ATTACHMENTS] ={};
for (uint32_t i = 0; i != numColorAttachments; i++) {
const lvk::ColorAttachment& attachment = desc_.color[i];
colorAttachmentFormats[i] =
formatToVkFormat(attachment.format);
if (!attachment.blendEnabled) {
colorBlendAttachmentStates[i] =
VkPipelineColorBlendAttachmentState{
.blendEnable = VK_FALSE,
.srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO,
.colorBlendOp = VK_BLEND_OP_ADD,
.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE,
.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
.alphaBlendOp = VK_BLEND_OP_ADD,
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT |
VK_COLOR_COMPONENT_A_BIT,
};
} else {
colorBlendAttachmentStates[i] =
VkPipelineColorBlendAttachmentState{
.blendEnable = VK_TRUE,
.srcColorBlendFactor = blendFactorToVkBlendFactor(
attachment.srcRGBBlendFactor),
.dstColorBlendFactor = blendFactorToVkBlendFactor(
attachment.dstRGBBlendFactor),
.colorBlendOp = blendOpToVkBlendOp(attachment.rgbBlendOp),
.srcAlphaBlendFactor = blendFactorToVkBlendFactor(
attachment.srcAlphaBlendFactor),
.dstAlphaBlendFactor = blendFactorToVkBlendFactor(
attachment.dstAlphaBlendFactor),
.alphaBlendOp =
blendOpToVkBlendOp(attachment.alphaBlendOp),
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT |
VK_COLOR_COMPONENT_A_BIT,
};
}
}
VkShaderModule objects from the pool using opaque handles. We will discuss how pools work in the next chapters. Here all we have to know is that they allow fast conversion of an integer handle into the actual data associated with it. The geometry shader is optional. We skip all other shaders here for the sake of brevity.
const VkShaderModule* vert =
ctx_->shaderModulesPool_.get(desc_.smVert);
const VkShaderModule* geom =
ctx_->shaderModulesPool_.get(desc_.smGeom);
const VkShaderModule* frag =
ctx_->shaderModulesPool_.get(desc_.smFrag);
const VkPipelineVertexInputStateCreateInfo ciVertexInputState =
{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
.vertexBindingDescriptionCount = rps->numBindings_,
.pVertexBindingDescriptions = rps->numBindings_ ?
rps->vkBindings_ : nullptr,
.vertexAttributeDescriptionCount = rps->numAttributes_,
.pVertexAttributeDescriptions =
rps->numAttributes_ ? rps->vkAttributes_ : nullptr,
};
VkSpecializationInfo structure to describe specialization constants for this graphics pipeline.
VkSpecializationMapEntry
entries[LVK_SPECIALIZATION_CONSTANTS_MAX] = {};
const VkSpecializationInfo si =
lvk::getPipelineShaderStageSpecializationInfo(
desc.specInfo, entries);
VkPipelineLayout object for this pipeline. Use the current descriptor set layout stored in VulkanContext. Here one descriptor set layout vkDSL_ is duplicated multiple times to create a pipeline layout. This is necessary to ensure compatibility with MoltenVK which does not allow aliasing of different descriptor types. Push constant sizes are retrieved from precompiled shader modules as was described in the previous recipe Initializing Vulkan shader modules.
const VkDescriptorSetLayout dsls[] =
{ vkDSL_, vkDSL_, vkDSL_, vkDSL_ };
const VkPushConstantRange range = {
.stageFlags = rps->shaderStageFlags_,
.offset = 0,
.size = pushConstantsSize,
};
const VkPipelineLayoutCreateInfo ci = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = (uint32_t)LVK_ARRAY_NUM_ELEMENTS(dsls),
.pSetLayouts = dsls,
.pushConstantRangeCount = pushConstantsSize ? 1u : 0u,
.pPushConstantRanges = pushConstantsSize ? &range:nullptr,
};
vkCreatePipelineLayout(vkDevice_, &ci, nullptr, &layout);
More information
Here’s a snippet to retrieve precalculated push constant sizes from shader modules:
#define UPDATE_PUSH_CONSTANT_SIZE(sm, bit) if (sm) { \
pushConstantsSize = std::max(pushConstantsSize, \
sm->pushConstantsSize); \
rps->shaderStageFlags_ |= bit; }
rps->shaderStageFlags_ = 0;
uint32_t pushConstantsSize = 0;
UPDATE_PUSH_CONSTANT_SIZE(vertModule,
VK_SHADER_STAGE_VERTEX_BIT);
UPDATE_PUSH_CONSTANT_SIZE(tescModule,
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT);
UPDATE_PUSH_CONSTANT_SIZE(teseModule,
VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT);
UPDATE_PUSH_CONSTANT_SIZE(geomModule,
VK_SHADER_STAGE_GEOMETRY_BIT);
UPDATE_PUSH_CONSTANT_SIZE(fragModule,
VK_SHADER_STAGE_FRAGMENT_BIT);
#undef UPDATE_PUSH_CONSTANT_SIZE
VkPipeline objects is encapsulated in VulkanPipelineBuilder, which provides reasonable default values for all the numerous Vulkan data members that we do not want to set. Those familiar with Java will recognize a typical Builder design pattern here.
lvk::vulkan::VulkanPipelineBuilder()
// from Vulkan 1.0
.dynamicState(VK_DYNAMIC_STATE_VIEWPORT)
.dynamicState(VK_DYNAMIC_STATE_SCISSOR)
.dynamicState(VK_DYNAMIC_STATE_DEPTH_BIAS)
.dynamicState(VK_DYNAMIC_STATE_BLEND_CONSTANTS)
// from Vulkan 1.3
.dynamicState(VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE)
.dynamicState(VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE)
.dynamicState(VK_DYNAMIC_STATE_DEPTH_COMPARE_OP)
.dynamicState(VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE)
.primitiveTopology(
topologyToVkPrimitiveTopology(desc.topology))
.rasterizationSamples(
getVulkanSampleCountFlags(desc.samplesCount,
getFramebufferMSAABitMask()), desc.minSampleShading)
.polygonMode(polygonModeToVkPolygonMode(desc_.polygonMode))
.stencilStateOps(VK_STENCIL_FACE_FRONT_BIT,
stencilOpToVkStencilOp(desc_.frontFaceStencil.stencilFailureOp),
stencilOpToVkStencilOp(desc_.frontFaceStencil.depthStencilPassOp),
stencilOpToVkStencilOp(desc_.frontFaceStencil.depthFailureOp),
compareOpToVkCompareOp(desc_.frontFaceStencil.stencilCompareOp))
.stencilStateOps(VK_STENCIL_FACE_BACK_BIT,
stencilOpToVkStencilOp(desc_.backFaceStencil.stencilFailureOp),
stencilOpToVkStencilOp(desc_.backFaceStencil.depthStencilPassOp),
stencilOpToVkStencilOp(desc_.backFaceStencil.depthFailureOp),
compareOpToVkCompareOp(desc_.backFaceStencil.stencilCompareOp))
.stencilMasks(VK_STENCIL_FACE_FRONT_BIT, 0xFF,
desc_.frontFaceStencil.writeMask,
desc_.frontFaceStencil.readMask)
.stencilMasks(VK_STENCIL_FACE_BACK_BIT, 0xFF,
desc_.backFaceStencil.writeMask,
desc_.backFaceStencil.readMask)
.shaderStage(lvk::getPipelineShaderStageCreateInfo(
VK_SHADER_STAGE_VERTEX_BIT,
vertModule->sm, desc.entryPointVert, &si))
.shaderStage(lvk::getPipelineShaderStageCreateInfo(
VK_SHADER_STAGE_FRAGMENT_BIT,
fragModule->sm, desc.entryPointFrag, &si))
.shaderStage(geomModule ?
lvk::getPipelineShaderStageCreateInfo(
VK_SHADER_STAGE_GEOMETRY_BIT,
geomModule->sm, desc.entryPointGeom, &si) :
VkPipelineShaderStageCreateInfo{ .module = VK_NULL_HANDLE } )
.cullMode(cullModeToVkCullMode(desc_.cullMode))
.frontFace(windingModeToVkFrontFace(desc_.frontFaceWinding))
.vertexInputState(vertexInputStateCreateInfo_)
.colorAttachments(colorBlendAttachmentStates,
colorAttachmentFormats, numColorAttachments)
.depthAttachmentFormat(formatToVkFormat(desc.depthFormat))
.stencilAttachmentFormat(formatToVkFormat(desc.stencilFormat))
.patchControlPoints(desc.patchControlPoints)
VulkanPipelineBuilder::build() method, which creates a VkPipeline object and we can store it in our RenderPipelineState structure together with the pipeline layout.
.build(vkDevice_, pipelineCache_, layout, &pipeline, desc.debugName);
rps->pipeline_ = pipeline;
rps->pipelineLayout_ = layout;
return pipeline;
}
The last method we want to explore here is VulkanPipelineBuilder::build() which is pure Vulkan. Let’s take a look at it to conclude the pipeline creation process.
VkPipelineDynamicStateCreateInfo.
VkResult VulkanPipelineBuilder::build(
VkDevice device,
VkPipelineCache pipelineCache,
VkPipelineLayout pipelineLayout,
VkPipeline* outPipeline,
const char* debugName)
{
const VkPipelineDynamicStateCreateInfo dynamicState = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = numDynamicStates_,
.pDynamicStates = dynamicStates_,
};
nullptr if the viewport and scissor states are dynamic. We are definitely happy to make the most of this opportunity.
const VkPipelineViewportStateCreateInfo viewportState = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.viewportCount = 1,
.pViewports = nullptr,
.scissorCount = 1,
.pScissors = nullptr,
};
VkPipelineRenderingCreateInfo is necessary for the Vulkan 1.3 dynamic rendering feature.
const VkPipelineColorBlendStateCreateInfo colorBlendState = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.logicOpEnable = VK_FALSE,
.logicOp = VK_LOGIC_OP_COPY,
.attachmentCount = numColorAttachments_,
.pAttachments = colorBlendAttachmentStates_,
};
const VkPipelineRenderingCreateInfo renderingInfo = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR,
.colorAttachmentCount = numColorAttachments_,
.pColorAttachmentFormats = colorAttachmentFormats_,
.depthAttachmentFormat = depthAttachmentFormat_,
.stencilAttachmentFormat = stencilAttachmentFormat_,
};
VkGraphicsPipelineCreateInfo and call vkCreateGraphicsPipelines().
const VkGraphicsPipelineCreateInfo ci = {
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = &renderingInfo,
.flags = 0,
.stageCount = numShaderStages_,
.pStages = shaderStages_,
.pVertexInputState = &vertexInputState_,
.pInputAssemblyState = &inputAssembly_,
.pTessellationState = &tessellationState_,
.pViewportState = &viewportState,
.pRasterizationState = &rasterizationState_,
.pMultisampleState = &multisampleState_,
.pDepthStencilState = &depthStencilState_,
.pColorBlendState = &colorBlendState,
.pDynamicState = &dynamicState,
.layout = pipelineLayout,
.renderPass = VK_NULL_HANDLE,
.subpass = 0,
.basePipelineHandle = VK_NULL_HANDLE,
.basePipelineIndex = -1,
};
vkCreateGraphicsPipelines(
device, pipelineCache, 1, &ci, nullptr, outPipeline);
numPipelinesCreated_++;
}
This code concludes the pipeline creation process. In addition to the very simple example in Chapter02/02_HelloTriangle, we created a slightly more elaborate app in Chapter02/03_GLM to demonstrate how to use multiple render pipelines to render a rotating cube with a wireframe overlay. This app uses the GLM library for matrix math. You can check it out in Chapter02/03_GLM/src/main.cpp, where it uses cmdPushConstants() to animate the cube and specialization constants to use the same set of shaders for both solid and wireframe rendering. It should look as shown in the following screenshot.

Figure 2.3: GLM usage example
If you are familiar with older versions of Vulkan, you might have noticed that in this recipe we completely left out any references to render passes. They are also not mentioned in any of the data structures. The reason for that is that we use Vulkan 1.3 dynamic rendering functionality, which allows VkPipeline objects to operate without needing a render pass.
In case you want to implement a similar wrapper for older versions of Vulkan and without using the VK_KHR_dynamic_rendering extension, you can maintain a “global” collection of render passes in an array inside VulkanContext and add an integer index of a corresponding render pass as a data member to RenderPipelineDynamicState. Since we can use only a very restricted number of distinct rendering passes — let’s say a maximum of 256 — the index can be saved as uint8_t. This would enable us to store them in array inside VulkanContext.
If you want to explore an actual working implementation of this approach, take a look at Meta’s IGL library https://github.com/facebook/igl/blob/main/src/igl/vulkan/RenderPipelineState.h and check out how renderPassIndex is handled there.
Now let’s jump to the next Chapter 3 to learn how to use Vulkan in a user-friendly way to build more interesting examples.
Unlock this book’s exclusive benefits nowThis book comes with additional benefits designed to elevate your learning experience. |
![]() |
|
Note: Have your purchase invoice ready before you begin. |
Change the font size
Change margin width
Change background colour