While the Vulkan swapchain was a difficult component to implement, it gives us the prequisites for some of the remaining components needed to form a renderer. In this article we will progress through the following topics:
Reminder: I’ll be using the word colour a lot throughout these topics - don’t forget that I use the American spelling color in code, but I use the spelling colour in editorial text. Same goes for the other kinds of words like this :)
The main render loop for Vulkan is composed of the interactions between a number of different Vulkan components. We will go into detail about these interactions over the next few articles as we write the rest of the rendering code.
Having a render context class which is separate from our Vulkan context class allows us to encapsulate all the potentially volatile rendering components together - making it easier to recreate them in situations where they become invalid. One such scenario where the rendering components become invalid is if the size of the application window surface changes. I’ll cover the recreation topic in more depth in a subsequent article. For now we will focus on authoring a basic render context class.
Create vulkan-render-context.hpp
and vulkan-render-context.cpp
in application/vulkan
. Enter the following into the header:
#pragma once
#include "../../core/internal-ptr.hpp"
#include "../../core/sdl-window.hpp"
#include "vulkan-device.hpp"
#include "vulkan-physical-device.hpp"
#include "vulkan-surface.hpp"
namespace ast
{
struct VulkanRenderContext
{
VulkanRenderContext(const ast::SDLWindow& window,
const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSurface& surface);
private:
struct Internal;
ast::internal_ptr<Internal> internal;
};
} // namespace ast
Initially the render context will need the related components required to create a swapchain as this class will now become the owner of the swapchain.
Enter the following into the implementation:
#include "vulkan-render-context.hpp"
#include "vulkan-swapchain.hpp"
using ast::VulkanRenderContext;
struct VulkanRenderContext::Internal
{
const ast::VulkanSwapchain swapchain;
Internal(const ast::SDLWindow& window,
const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSurface& surface)
: swapchain(ast::VulkanSwapchain(window, physicalDevice, device, surface)) {}
};
VulkanRenderContext::VulkanRenderContext(const ast::SDLWindow& window,
const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSurface& surface)
: internal(ast::make_internal_ptr<Internal>(window, physicalDevice, device, surface)) {}
The implementation is quite straight forward - really it is just creating and holding a swapchain instance. We will add more components during this article.
Our Vulkan context class will hold an instance of our new render context and will no longer directly create the swapchain itself. Edit vulkan-context.cpp
and exchange the swapchain header for our new render context header:
#include "vulkan-swapchain.hpp"
becomes:
#include "vulkan-render-context.hpp"
Replace the existing swapchain member field and construction code with a VulkanRenderContext
instead:
struct VulkanContext::Internal
{
...
const ast::VulkanSwapchain swapchain;
Internal() : ...
swapchain(ast::VulkanSwapchain(window, physicalDevice, device, surface))
becomes:
struct VulkanContext::Internal
{
...
const ast::VulkanRenderContext renderContext;
Internal() : ...
renderContext(ast::VulkanRenderContext(window, physicalDevice, device, surface))
Run the application to make sure it still works the same way as before.
I would highly recommend reading the following links about render passes - what they are for and an overview of their behaviours:
We must have a render pass otherwise we won’t really be able to render anything. There are some prerequisite bits of information we need to create our render pass - some of which we already have in our Vulkan implementation and some which we’ll need to introduce in this article. In particular we will need:
Swapchain colour format
We already have the colour format for our swapchain held privately in the VulkanSwapchainFormat format
field inside vulkan-swapchain.cpp
. All we need to do is add a way to get the colour format publicly. Open vulkan-swapchain.hpp
and add a new public function definition:
namespace ast
{
struct VulkanSwapchain
{
...
const vk::Format& getColorFormat() const;
...
In vulkan-swapchain.cpp
add the implementation of this new function to return the colorFormat
component of the internal format
field:
const vk::Format& VulkanSwapchain::getColorFormat() const
{
return internal->format.colorFormat;
}
Multi sampling level
The multi sampling level - or anti aliasing level as it is also known - comes from our physical device. We will choose a multi sampling level by querying the properties of the physical device we chose during initialisation. We will have to write some extra code in our physical device class to achieve this.
Open vulkan-physical-device.hpp
and add a new function definition to expose the multi sampling level that can be used. Note that it is of the type vk::SampleCountFlagBits
which is an enumeration of all the possible values - the multi sampling level isn’t simply a number:
vk::SampleCountFlagBits getMultiSamplingLevel() const;
Since we can compute this value when initialising the physical device, we’ll add a new field to the internal implementation - edit vulkan-physical-device.cpp
and add the following field and constructor code into the Internal
struct:
struct VulkanPhysicalDevice::Internal
{
...
const vk::SampleCountFlagBits multiSamplingLevel;
Internal(const vk::Instance& instance)
: ...
multiSamplingLevel(::getMultiSamplingLevel(physicalDevice)) {}
Add the <stack>
header at the top of our file to allow us to use a std::stack
component:
#include <stack>
Now jump to the anonymous namespace and write a new free function named getMultiSamplingLevel
:
namespace
{
...
vk::SampleCountFlagBits getMultiSamplingLevel(const vk::PhysicalDevice& physicalDevice)
{
static const std::string logTag{"ast::VulkanPhysicalDevice::getMultiSamplingLevel"};
vk::PhysicalDeviceProperties properties{physicalDevice.getProperties()};
vk::SampleCountFlags supportedSampleCounts{properties.limits.framebufferColorSampleCounts};
std::stack<vk::SampleCountFlagBits> preferredSampleCounts;
preferredSampleCounts.push(vk::SampleCountFlagBits::e1);
preferredSampleCounts.push(vk::SampleCountFlagBits::e2);
preferredSampleCounts.push(vk::SampleCountFlagBits::e4);
preferredSampleCounts.push(vk::SampleCountFlagBits::e8);
while (!preferredSampleCounts.empty())
{
// Take the sample count at the top of the stack and see if it is supported.
vk::SampleCountFlagBits sampleCount{preferredSampleCounts.top()};
if (supportedSampleCounts & sampleCount)
{
return sampleCount;
}
// If our preferred sample count is not found, pop the stack ready for the next iteration.
preferredSampleCounts.pop();
}
// If none of our sample counts is found, multi sampling is not supported on this device ...
throw std::runtime_error(logTag + ": Multi sampling not supported.");
}
}
We start off by obtaining the physical device properties then querying them for the framebufferColorSampleCounts
limit:
vk::SampleCountFlagBits getMultiSamplingLevel(const vk::PhysicalDevice& physicalDevice)
{
static const std::string logTag{"ast::VulkanPhysicalDevice::getMultiSamplingLevel"};
vk::PhysicalDeviceProperties properties{physicalDevice.getProperties()};
vk::SampleCountFlags supportedSampleCounts{properties.limits.framebufferColorSampleCounts};
We then create a stack which holds our preferred levels of multi sampling to allow us to pop each one looking for matching support. Note that I’ve only gone as high as multi sampling level of 8 which is reasonably high on most average hardware. You could try to go higher if desired or make it configurable by a user so they could choose instead but we will simply use the highest available multi sampling level if it’s supported up to a maximum of e8
:
std::stack<vk::SampleCountFlagBits> preferredSampleCounts;
preferredSampleCounts.push(vk::SampleCountFlagBits::e1);
preferredSampleCounts.push(vk::SampleCountFlagBits::e2);
preferredSampleCounts.push(vk::SampleCountFlagBits::e4);
preferredSampleCounts.push(vk::SampleCountFlagBits::e8);
Note: High multi sampling levels can impact performance. You may find that low end platforms such as mobile devices might perform better with lower multi sampling levels at the cost of image quality.
The stack is then walked, popping and checking each element until we find a supported multi sampling count or don’t find any. It would be very unlikely (not sure if it’s even possible) to not have at least e1
support, but our code will cope regardless. If we find a match we return it and we are done. You might remember we took a similar approach when writing the swapchain to choose a presentation mode:
while (!preferredSampleCounts.empty())
{
// Take the sample count at the top of the stack and see if it is supported.
vk::SampleCountFlagBits sampleCount{preferredSampleCounts.top()};
if (supportedSampleCounts & sampleCount)
{
return sampleCount;
}
// If our preferred sample count is not found, pop the stack ready for the next iteration.
preferredSampleCounts.pop();
}
Of course if our entire stack is exhausted and no match was found, then ultimately we can’t use Vulkan on whatever device this is:
throw std::runtime_error(logTag + ": Multi sampling not supported.");
Depth testing format
We need to supply the render pass with the colour format to apply when performing depth testing. The article I mentioned earlier here https://vulkan-tutorial.com/Depth_buffering explains a lot about this topic. We will be choosing to use the VK_FORMAT_D32_SFLOAT
format for depth testing exposed to us via the Vulkan C++ header as vk::Format::eD32Sfloat
.
All the Vulkan formats are described here: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkFormat.html. Searching that site for VK_FORMAT_D32_SFLOAT
produces the following definition:
“VK_FORMAT_D32_SFLOAT specifies a one-component, 32-bit signed floating-point format that has 32-bits in the depth component.”
We will need to check if the format is supported by our physical device before assuming it is available then keep it as a field within our vulkan-physical-device
class for easy reference. Edit vulkan-physical-device.hpp
and add the following function definition to allow other code to ask for the depth format:
vk::Format getDepthFormat() const;
Update vulkan-physical-device.cpp
, adding a new member field named depthFormat
, initialising it via the getDepthFormat
function:
struct VulkanPhysicalDevice::Internal
{
...
const vk::Format depthFormat;
Internal(const vk::Instance& instance)
: ...
depthFormat(::getDepthFormat(physicalDevice)) {}
Next, add the getDepthFormat
free function into the anonymous namespace:
namespace
{
...
vk::Format getDepthFormat(const vk::PhysicalDevice& physicalDevice)
{
static const std::string logTag{"ast::VulkanPhysicalDevice::getDepthFormat"};
vk::FormatProperties formatProperties{physicalDevice.getFormatProperties(vk::Format::eD32Sfloat)};
if (formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment)
{
return vk::Format::eD32Sfloat;
}
throw std::runtime_error(logTag + ": 32 bit signed depth stencil format not supported.");
}
}
This function firstly fetches the formatProperties
for the given physical device that are specific to the eD32Sfloat
format which is the one we want to use for depth testing. It then checks if the format properties has the eDepthStencilAttachment
capability which is required to use the format for depth testing. If support is not found, we throw an exception.
Lastly we need to add the public function implementation to the bottom of vulkan-physical-device.cpp
:
vk::Format VulkanPhysicalDevice::getDepthFormat() const
{
return internal->depthFormat;
}
We now have all the components needed to instantiate a render pass. A render pass is comprised of one or more vk::AttachmentDescription
configurations, each having a corresponding vk::AttachmentReference
. The attachment references are then fed into a subpass which is what the render pass object will execute during rendering. We will also define a vk::SubpassDependency
configuration to describe the relationship between the subpasses. This is particularly important when we have more than one subpass in the same render pass instance - though we will just have one subpass for this article.
Our render pass will have the following:
vk::UniqueRenderPass
instance - we will store the resulting render pass instance as an internal field.If you feel lost here don’t fret - I found (still finding!!) the relationships between these attachments and subpasses to be quite challenging to sink into my brain. The following site describes a lot about the subpass system: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkSubpassDescription.html.
The main thing is that a render pass can have one or more subpasses, and the subpasses are the actual stages of operations that will be performed during rendering. A subpass itself can have different kinds of attachments - colour, depth testing and multi sampling (syntactically this is called the resolve attachment as it resolves the final state of the output image and may not necessarily be due to multi sampling).
With all that said, we will now stand up a new class to encapsulate the render pass. Create vulkan-render-pass.hpp
and vulkan-render-pass.cpp
in application/vulkan
. Edit the header with the following:
#pragma once
#include "../../core/internal-ptr.hpp"
#include "vulkan-device.hpp"
#include "vulkan-physical-device.hpp"
#include "vulkan-swapchain.hpp"
namespace ast
{
struct VulkanRenderPass
{
VulkanRenderPass(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSwapchain& swapchain);
private:
struct Internal;
ast::internal_ptr<Internal> internal;
};
} // namespace ast
Note that we need the device as it is the component that creates the render pass. We need the physical device to find out what the multi sampling level and depth formats are. We need the swapchain to find out the colour format to use when rendering to the colour attachment.
Now enter the following into vulkan-render-pass.cpp
- note we will work through the createRenderPass
function separately so we can spend time on its details:
#include "vulkan-render-pass.hpp"
using ast::VulkanRenderPass;
namespace
{
vk::UniqueRenderPass createRenderPass(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSwapchain& swapchain)
{
}
} // namespace
struct VulkanRenderPass::Internal
{
const vk::UniqueRenderPass renderPass;
Internal(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSwapchain& swapchain)
: renderPass(::createRenderPass(physicalDevice, device, swapchain)) {}
};
VulkanRenderPass::VulkanRenderPass(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSwapchain& swapchain)
: internal(ast::make_internal_ptr<Internal>(physicalDevice, device, swapchain)) {}
createRenderPass function
We will walk slowly through the createRenderPass
function and I’ll try to explain the parts of it as we go.
First up, we query the swapchain for its colour format - we need to know this so our render pass can output its rendering result in a compatible format for consumption by the swapchain. If we used a different colour format to the swapchain’s colour format they would be incompatible. We also query the physical device for its multi sampling level and depth format using the functions we added earlier in this article:
namespace
{
vk::UniqueRenderPass createRenderPass(const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSwapchain& swapchain)
{
vk::Format colorFormat{swapchain.getColorFormat()};
vk::SampleCountFlagBits multiSamplingLevel{physicalDevice.getMultiSamplingLevel()};
vk::Format depthFormat{physicalDevice.getDepthFormat()};
Colour attachment
The first attachment is the colour attachment. Like all attachments, it is defined through a vk::AttachmentDescription
object:
vk::AttachmentDescription colorAttachment{
vk::AttachmentDescriptionFlags(), // Flags
colorFormat, // Format
multiSamplingLevel, // Samples
vk::AttachmentLoadOp::eClear, // Load operation
vk::AttachmentStoreOp::eStore, // Store operation
vk::AttachmentLoadOp::eDontCare, // Stencil load operation
vk::AttachmentStoreOp::eDontCare, // Stencil store operation
vk::ImageLayout::eUndefined, // Initial layout
vk::ImageLayout::eColorAttachmentOptimal}; // Final layout
The configuration comprises of:
colorFormat
: Apply the same colour format as the swapchain to its output.multiSamplingLevel
: Apply the multi sampling level supported by the current physical device to the output.eClear
: The initial state for the colour attachment is to clear its content.eStore
: The result of this attachment should be stored otherwise it wouldn’t persist as output.eDontCare
because this attachment is a colour attachment and doesn’t do any kind of depth testing operations.eUndefined
: There will be no initial layout at the start of operations for this attachment. You can read more about image layouts here: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkImageLayout.html.eColorAttachmentOptimal
: Use specifically for colour attachments.Remembering that each attachment description should be coupled with an attachment reference, we next write the attachment reference for our colour attachment:
vk::AttachmentReference colorAttachmentReference{
0, // Attachment index
vk::ImageLayout::eColorAttachmentOptimal}; // Layout
The attachment index of 0
will inform the consumer of the reference to look at position 0 in an array of attachments to find this one. The layout reflects that this is a colour attachment.
Depth testing attachment
Next up is our attachment to describe how to do depth testing within the render pass:
vk::AttachmentDescription depthTestingAttachment{
vk::AttachmentDescriptionFlags(), // Flags
depthFormat, // Format
multiSamplingLevel, // Samples
vk::AttachmentLoadOp::eClear, // Load operation
vk::AttachmentStoreOp::eDontCare, // Store operation
vk::AttachmentLoadOp::eDontCare, // Stencil load operation
vk::AttachmentStoreOp::eDontCare, // Stencil store operation
vk::ImageLayout::eUndefined, // Initial layout
vk::ImageLayout::eDepthStencilAttachmentOptimal}; // Final layout
While similar to our colour attachment there are a few subtle differences:
depthFormat
: This time the colour format to use is the depthFormat
from our physical device.multiSamplingLevel
: The depth testing attachment must have the same multi sampling level as the colour attachment.eClear
: Initially the attachment should clear itself before use.eDontCare
: We don’t write the depth testing attachment to any kind of output image so we don’t care.eDontCare
: We aren’t concerned about storing stencil operations.eUndefined
: No initial layout.eDepthStencilAttachmentOptimal
: This declares that the attachment is for the purposes of depth testing.And of course we need an accompanying attachment reference:
vk::AttachmentReference depthTestingAttachmentReference{
1, // Attachment index
vk::ImageLayout::eDepthStencilAttachmentOptimal}; // Layout
This time you might notice the attachment index is 1
whereas our colour attachment index was 0
. This specifies that the depth testing attachment description can be found at position 1 in the list of attachments (we will create the list of attachments soon).
Multi sampling attachment
Our multi sampling attachment actually goes into a property named resolve attachment because it is used to resolve the final image output for the subpass. The multi sampling would be applied when resolving the final image to suppress aliasing in the image:
vk::AttachmentDescription multiSamplingAttachment{
vk::AttachmentDescriptionFlags(), // Flags
colorFormat, // Format
vk::SampleCountFlagBits::e1, // Samples
vk::AttachmentLoadOp::eDontCare, // Load operation
vk::AttachmentStoreOp::eStore, // Store operation
vk::AttachmentLoadOp::eDontCare, // Stencil load operation
vk::AttachmentStoreOp::eDontCare, // Stencil store operation
vk::ImageLayout::eUndefined, // Initial layout
vk::ImageLayout::ePresentSrcKHR}; // Final layout
The properties are:
colorFormat
: Again, this should be the same as the colour format for the swapchain because multi sampling still emits an output image just like the colour attachment does.e1
: A bit odd since this attachment is the multi sampling attachment but effectively saying that the output of the multi sampling attachment isn’t expected to be itself multi sampled again.eDontCare
: No special load conditions needed.eStore
: The output of this attachment should be stored, as it will become the final image forwarded to the renderer presentation queue.eDontCare
: This attachment performs no depth testing related functions.eUndefined
: Same as for the other attachments.ePresentSrcKHR
: This one is a bit interesting, the final image layout for the multi sampling attachment is in a layout that will become the source for presentation to the screen.And for the attachment reference:
vk::AttachmentReference multiSamplingAttachmentReference{
2, // Attachment index
vk::ImageLayout::eColorAttachmentOptimal}; // Layout
Here we are specifying 2
as the index position where the attachment description can be found and the layout is eColorAttachmentOptimal
because the output of the multi sampling is still effectively a colour image.
List of all attachments
We now create a list (technically a std::array
) holding our three attachments so it can be passed into another configuration object soon. Observe that the order of the attachments reflects the attachment index
values we specified in each of the attachment references. If the attachment indices don’t correlate to the correct attachment descriptions in the array our render pass will not function properly:
std::array<vk::AttachmentDescription, 3> attachments{
colorAttachment,
depthTestingAttachment,
multiSamplingAttachment};
Subpass
We have created all the attachments we need, but a render pass needs at least one subpass. Our subpass will stitch together all the attachments to describe how to perform its operations:
vk::SubpassDescription subpass{
vk::SubpassDescriptionFlags(), // Flags
vk::PipelineBindPoint::eGraphics, // Pipeline bind point
0, // Input attachment count
nullptr, // Input attachments
1, // Color attachments count
&colorAttachmentReference, // Color attachments
&multiSamplingAttachmentReference, // Resolve attachments
&depthTestingAttachmentReference, // Depth stencil attachments
0, // Preserve attachments count
nullptr}; // Preserve attachments
The properties are:
eGraphics
: This describes what stage of the rendering pipeline this subpass should execute. Other options are compute
and ray tracing
which is something supported by more contemporary video hardware (at the time of writing).0
/ nullptr
: We don’t use any input attachments. The subsequent property is nullptr
due to this.1
: We only have one colour attachment which we just wrote.&colorAttachmentReference
: Notice that we are adding the colour attachment reference, not the attachment description.&multiSamplingAttachmentReference
: This is our multi sampling attachment which resolves the final output image.&depthTestingAttachmentReference
: This is our depth testing attachment.0
/ nullptr
: We don’t use these.Subpass dependencies
We have a subpass and even though we only have one of them, we must define its dependencies. If we had more than one subpass, the dependencies would allow Vulkan to orchestrate the order of operations amongst them:
vk::SubpassDependency subpassDependency{
0, // Source subpass index
0, // Destination subpass index
vk::PipelineStageFlagBits::eColorAttachmentOutput, // Source access mask
vk::PipelineStageFlagBits::eColorAttachmentOutput, // Destination access mask
vk::AccessFlags(), // Source access flags
vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite, // Destination access flags
vk::DependencyFlags()}; // Dependency flags
The important properties:
0
: We only have one subpass so it can be found at the first position in the list of subpasses for the render pass instance.eColorAttachmentOutput
: This controls the point in time when the subpass should be considered. Go to https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkPipelineStageFlagBits.html to learn about the different pipeline stage flags. In particular our dependency is defined as:“VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT specifies the stage of the pipeline after blending where the final color values are output from the pipeline. This stage also includes subpass load and store operations and multisample resolve operations for framebuffer attachments with a color or depth/stencil format.”
vk::AccessFlags()
: Default flags nothing special needed.vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite
: The destination flags require the ability to both read and write to the colour attachment during its operation.Render pass creation info
We can now use all the components we’ve configured to describe the actual render pass itself through a vk::RenderPassCreateInfo
object.
vk::RenderPassCreateInfo renderPassCreateInfo{
vk::RenderPassCreateFlags(), // Flags
static_cast<uint32_t>(attachments.size()), // Attachment count
attachments.data(), // Attachments
1, // Subpass count
&subpass, // Subpasses
1, // Dependency count
&subpassDependency}; // Dependencies
static_cast<uint32_t>(attachments.size())
: Remember we created an array earlier which contains all of our attachment descriptions - this property specifies how many there were.attachments.data()
: This is the memory location of the array. Coupled with the attachment count, Vulkan will know how many attachment descriptions to read from the memory location.1
: We only defined one subpass, so …&subpass
: The memory location to read in the subpass configurations.1
: Yep, we only made one of those too …&subpassDependency
: The memory location to read in the subpass dependencies.Create render pass
Ok, finally we can create and return the render pass, by invoking the correct method on our device:
return device.getDevice().createRenderPassUnique(renderPassCreateInfo);
We have enough of our render pass written now to instantiate one in our Vulkan render context. Pop open vulkan-render-context.cpp
and add the render pass header:
#include "vulkan-render-pass.hpp"
Then go to the Internal
struct and add a field to hold a render pass and create it in the constructor:
struct VulkanRenderContext::Internal
{
const ast::VulkanSwapchain swapchain;
const ast::VulkanRenderPass renderPass;
Internal(const ast::SDLWindow& window,
const ast::VulkanPhysicalDevice& physicalDevice,
const ast::VulkanDevice& device,
const ast::VulkanSurface& surface)
: swapchain(ast::VulkanSwapchain(window, physicalDevice, device, surface)),
renderPass(ast::VulkanRenderPass(physicalDevice, device, swapchain)) {}
};
Run your application and once again you won’t see anything different but if it boots up with Vulkan successfully initialised you know that our render pass was created correctly.
Phew … you might want to stop here for a minute and absorb all that - maybe have a coffee or a sleep. There is still a fair bit more to go but we are chipping away at it!!
It is worth a quick note that the code we are writing at the moment is still executed during the constructor of the Vulkan application class. This means that any exceptions that propagate out will cause our main engine to fall back to the OpenGL implementation. We have a few more things to initialise during construction but eventually we will reach a point where we stop running the code during construction and instead defer it through our scene class during the application loop.
I have broken the renderer implementation into multiple articles to avoid them getting too long. In the next article we will revisit our render pass class and add frame buffers to it which are required to fulfill the render pass -> swapchain image relationship.
The code for this article can be found here.
Continue to Part 23: Vulkan frame buffers.
End of part 22