Skip to content

Commit 1f3c4fe

Browse files
committed
Refactor validation layer checks and feature chaining in Vulkan setup
- Replace `checkValidationLayerSupport` function with inlined logic for layer validation. - Improve clarity of Vulkan instance creation by directly validating required layers and extensions during initialization. - Introduce `vk::StructureChain` for cleaner and more efficient feature chaining in logical device creation. - Refactor debug messenger initialization to use non-pointer object and update Vulkan 1.3-specific features for better compatibility.
1 parent 4c566c7 commit 1f3c4fe

File tree

3 files changed

+139
-80
lines changed

3 files changed

+139
-80
lines changed

en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ raii context:
2626
----
2727
private:
2828
vk::raii::Context context;
29-
std::unique_ptr<vk::raii::Instance> instance;
29+
vk::raii::Instance instance = nullptr;
3030
----
3131

3232
Now, to create an instance, we'll first have to fill in a struct with some
@@ -77,14 +77,26 @@ that which we can pass to the struct:
7777

7878
[,c++]
7979
----
80+
// Get the required instance extensions from GLFW.
8081
uint32_t glfwExtensionCount = 0;
81-
const char** glfwExtensions;
82-
83-
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
82+
auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
83+
84+
// Check if the required GLFW extensions are supported by the Vulkan implementation.
85+
auto extensionProperties = context.enumerateInstanceExtensionProperties();
86+
for (uint32_t i = 0; i < glfwExtensionCount; ++i)
87+
{
88+
if (std::ranges::none_of(extensionProperties,
89+
[glfwExtension = glfwExtensions[i]](auto const& extensionProperty)
90+
{ return strcmp(extensionProperty.extensionName, glfwExtension) == 0; }))
91+
{
92+
throw std::runtime_error("Required GLFW extension not supported: " + std::string(glfwExtensions[i]));
93+
}
94+
}
8495
8596
vk::InstanceCreateInfo createInfo{
8697
.pApplicationInfo = &appInfo,
87-
.ppEnabledLayerNames = glfwExtensions};
98+
.enabledExtensionCount = glfwExtensionCount,
99+
.ppEnabledExtensionNames = glfwExtensions};
88100
----
89101

90102
The other missing piece is the Layers to enable. Here is where we'll talk

en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -104,26 +104,35 @@ constexpr bool enableValidationLayers = true;
104104
#endif
105105
----
106106

107-
We'll add a new function `checkValidationLayerSupport` that checks if all
108-
the requested layers are available. As the instance layers are returned as a
109-
std::array, they can be filtered via a single line and return the value
110-
using lambda functions.
111-
112-
[,c++]
113-
----
114-
bool checkValidationLayerSupport() {
115-
return (std::ranges::any_of(context.enumerateInstanceLayerProperties(),
116-
[]( vk::LayerProperties const & lp ) { return ( strcmp( "VK_LAYER_KHRONOS_validation", lp.layerName ) == 0 ); } ) );
117-
}
118-
----
119-
120-
We can now use this function in `createInstance`:
107+
We'll check if all the requested layers are available. We need to iterate
108+
through the requested layers and validate that all the required layers are
109+
supported by the Vulkan implementation. This check is performed directly in the
110+
`createInstance` function:
121111

122112
[,c++]
123113
----
124114
void createInstance() {
125-
if (enableValidationLayers && !checkValidationLayerSupport()) {
126-
throw std::runtime_error("validation layers requested, but not available!");
115+
constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle",
116+
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
117+
.pEngineName = "No Engine",
118+
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
119+
.apiVersion = vk::ApiVersion14 };
120+
121+
// Get the required layers
122+
std::vector<char const*> requiredLayers;
123+
if (enableValidationLayers) {
124+
requiredLayers.assign(validationLayers.begin(), validationLayers.end());
125+
}
126+
127+
// Check if the required layers are supported by the Vulkan implementation.
128+
auto layerProperties = context.enumerateInstanceLayerProperties();
129+
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
130+
return std::ranges::none_of(layerProperties,
131+
[requiredLayer](auto const& layerProperty)
132+
{ return strcmp(layerProperty.layerName, requiredLayer) == 0; });
133+
}))
134+
{
135+
throw std::runtime_error("One or more required layers are not supported!");
127136
}
128137
129138
...
@@ -138,11 +147,12 @@ validation layer names if they are enabled:
138147

139148
[,c++]
140149
----
141-
std::vector<char const *> enabledLayers;
142-
if (enableValidationLayers) {
143-
enabledLayers.assign(validationLayers.begin(), validationLayers.end());
144-
}
145-
vk::InstanceCreateInfo createInfo({}, &appInfo, enabledLayers, {});
150+
vk::InstanceCreateInfo createInfo{
151+
.pApplicationInfo = &appInfo,
152+
.enabledLayerCount = static_cast<uint32_t>(requiredLayers.size()),
153+
.ppEnabledLayerNames = requiredLayers.data(),
154+
.enabledExtensionCount = 0,
155+
.ppEnabledExtensionNames = nullptr };
146156
----
147157

148158
If the check was successful then `vkCreateInstance` should not ever return a
@@ -171,12 +181,6 @@ std::vector<const char*> getRequiredExtensions() {
171181
uint32_t glfwExtensionCount = 0;
172182
auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
173183
174-
std::vector<vk::ExtensionProperties> props = context.enumerateInstanceExtensionProperties();
175-
if (const auto propsIterator = std::ranges::find_if(props, []( vk::ExtensionProperties const & ep ) { return strcmp( ep.extensionName, vk::EXTDebugUtilsExtensionName ) == 0; } ); propsIterator == props.end() )
176-
{
177-
std::cout << "Something went very wrong, cannot find VK_EXT_debug_utils extension" << std::endl;
178-
exit( 1 );
179-
}
180184
std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
181185
if (enableValidationLayers) {
182186
extensions.push_back(vk::EXTDebugUtilsExtensionName );
@@ -268,7 +272,7 @@ you want. Add a class member for this handle right under `instance`:
268272

269273
[,c++]
270274
----
271-
std::unique_ptr<vk::raii::DebugUtilsMessengerEXT> debugMessenger;
275+
vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr;
272276
----
273277

274278
Now add a function `setupDebugMessenger` to be called from `initVulkan` right
@@ -293,9 +297,12 @@ We'll need to fill in a structure with details about the messenger and its callb
293297
----
294298
vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError );
295299
vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation );
296-
297-
vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT({}, severityFlags, messageTypeFlags, &debugCallback);
298-
debugMessenger = std::make_unique<vk::raii::DebugUtilsMessengerEXT>( *instance, debugUtilsMessengerCreateInfoEXT );
300+
vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{
301+
.messageSeverity = severityFlags,
302+
.messageType = messageTypeFlags,
303+
.pfnUserCallback = &debugCallback
304+
};
305+
debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT);
299306
----
300307

301308
The `messageSeverity` field allows you to specify all the types of
@@ -324,14 +331,51 @@ We can now re-use this in the `createInstance` function:
324331
[,c++]
325332
----
326333
void createInstance() {
327-
constexpr auto appInfo = vk::ApplicationInfo("Hello Triangle", 1, "No Engine", 1, vk::ApiVersion11);
328-
auto extensions = getRequiredExtensions();
329-
std::vector<char const *> enabledLayers;
334+
constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle",
335+
.applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ),
336+
.pEngineName = "No Engine",
337+
.engineVersion = VK_MAKE_VERSION( 1, 0, 0 ),
338+
.apiVersion = vk::ApiVersion14 };
339+
340+
// Get the required layers
341+
std::vector<char const*> requiredLayers;
330342
if (enableValidationLayers) {
331-
enabledLayers.assign(validationLayers.begin(), validationLayers.end());
343+
requiredLayers.assign(validationLayers.begin(), validationLayers.end());
344+
}
345+
346+
// Check if the required layers are supported by the Vulkan implementation.
347+
auto layerProperties = context.enumerateInstanceLayerProperties();
348+
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
349+
return std::ranges::none_of(layerProperties,
350+
[requiredLayer](auto const& layerProperty)
351+
{ return strcmp(layerProperty.layerName, requiredLayer) == 0; });
352+
}))
353+
{
354+
throw std::runtime_error("One or more required layers are not supported!");
332355
}
333-
vk::InstanceCreateInfo createInfo({}, &appInfo, enabledLayers.size(), enabledLayers.data(), extensions.size(), extensions.data());
334-
instance = std::make_unique<vk::raii::Instance>(context, createInfo);
356+
357+
// Get the required extensions.
358+
auto requiredExtensions = getRequiredExtensions();
359+
360+
// Check if the required extensions are supported by the Vulkan implementation.
361+
auto extensionProperties = context.enumerateInstanceExtensionProperties();
362+
for (auto const & requiredExtension : requiredExtensions)
363+
{
364+
if (std::ranges::none_of(extensionProperties,
365+
[requiredExtension](auto const& extensionProperty)
366+
{ return strcmp(extensionProperty.extensionName, requiredExtension) == 0; }))
367+
{
368+
throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension));
369+
}
370+
}
371+
372+
vk::InstanceCreateInfo createInfo{
373+
.pApplicationInfo = &appInfo,
374+
.enabledLayerCount = static_cast<uint32_t>(requiredLayers.size()),
375+
.ppEnabledLayerNames = requiredLayers.data(),
376+
.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size()),
377+
.ppEnabledExtensionNames = requiredExtensions.data() };
378+
instance = vk::raii::Instance(context, createInfo);
335379
}
336380
----
337381

en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -77,50 +77,40 @@ start doing more interesting things with Vulkan.
7777
vk::PhysicalDeviceFeatures deviceFeatures;
7878
----
7979

80-
== Specifying any extra features or updates we'd like our device to support
80+
== Enabling additional device features
8181

82-
Vulkan is built to be backwards compatible. Thus, if you do nothing else,
83-
you will get a Vulkan instance just as it was originally released in Vulkan 1
84-
.0. This is necessary as code written back then would need to still work so
85-
any additional features must be new code which would need to be turned on.
86-
So, let's tell Vulkan that we use some of the more modern features which make
87-
it easier to work with.
82+
Vulkan is designed to be backwards compatible, which means that by default, you only get access to the basic features that were available in Vulkan 1.0. To use newer features, you need to explicitly request them during device creation.
8883

89-
First, let's query for the features of the physical device:
84+
In Vulkan, features are organized into different structures based on when they were introduced or what functionality they relate to. For example:
85+
- Basic features are in `vk::PhysicalDeviceFeatures`
86+
- Vulkan 1.3 features are in `vk::PhysicalDeviceVulkan13Features`
87+
- Extension-specific features are in their own structures (like `vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT`)
9088

91-
[,c++]
92-
----
93-
// query for Vulkan 1.3 features
94-
auto features = physicalDevice.getFeatures2();
95-
----
89+
To enable multiple sets of features, Vulkan uses a concept called "structure chaining." Each feature structure has a `pNext` field that can point to another structure, creating a chain of feature requests.
9690

97-
Now, let's tell Vulkan that there's a few features we wish to use, by turning
98-
on dynamicRendering and the extendedDynamicState.
91+
The C++ Vulkan API provides a helper template called `vk::StructureChain` that makes this process easier. Let's see how to use it:
9992

10093
[,c++]
10194
----
102-
vk::PhysicalDeviceVulkan13Features vulkan13Features;
103-
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures;
104-
vulkan13Features.dynamicRendering = vk::True;
105-
extendedDynamicStateFeatures.extendedDynamicState = vk::True;
95+
// Create a chain of feature structures
96+
vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features, vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT> featureChain = {
97+
{}, // vk::PhysicalDeviceFeatures2 (empty for now)
98+
{.dynamicRendering = true }, // Enable dynamic rendering from Vulkan 1.3
99+
{.extendedDynamicState = true } // Enable extended dynamic state from the extension
100+
};
106101
----
107102

108-
Note that these are just variables that we created, and aren't part of the
109-
device create info logic structure. In Vulkan, all structures have a .pNext
110-
member variable which allows for chaining. So, we need to tell each object
111-
about the next object in the chain like this:
103+
Here's what's happening in this code:
112104

113-
[,c++]
114-
----
115-
vulkan13Features.pNext = &extendedDynamicStateFeatures;
116-
features.pNext = &vulkan13Features;
117-
----
105+
1. We create a `vk::StructureChain` with three different feature structures.
106+
2. For each structure in the chain, we provide an initializer:
107+
- The first structure (`vk::PhysicalDeviceFeatures2`) is left empty with `{}`
108+
- In the second structure, we enable the `dynamicRendering` feature from Vulkan 1.3
109+
- In the third structure, we enable the `extendedDynamicState` feature from an extension
118110

119-
Note here that features is the same object that we got when we queried the
120-
physical device for the features. Each pNext member variable points to the
121-
next feature structure in the chain. So all that's left is to ensure that
122-
when we create the logical device with a VkDeviceCreateInfo structure we set
123-
.pNext in that structure to the pointer to the features.
111+
The `vk::StructureChain` template automatically connects these structures together by setting up the `pNext` pointers between them. This saves us from having to manually link each structure to the next one.
112+
113+
When we create the logical device later, we'll pass a pointer to the first structure in this chain, which will allow Vulkan to see all the features we want to enable.
124114

125115
== Specifying device extensions
126116

@@ -140,16 +130,29 @@ The `VK_KHR_swapchain` extension is required for presenting rendered images to t
140130

141131
== Creating the logical device
142132

143-
With the previous structures in place, we can start filling in the main
144-
`VkDeviceCreateInfo` structure.
133+
With all the necessary information prepared, we can now create the logical device. We need to fill in the `vk::DeviceCreateInfo` structure and connect our feature chain to it:
145134

146135
[,c++]
147136
----
148-
vk::DeviceCreateInfo deviceCreateInfo{ .pNext = &features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &deviceQueueCreateInfo };
149-
deviceCreateInfo.enabledExtensionCount = deviceExtensions.size();
150-
deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data();
137+
vk::DeviceCreateInfo deviceCreateInfo{
138+
.pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
139+
.queueCreateInfoCount = 1,
140+
.pQueueCreateInfos = &deviceQueueCreateInfo,
141+
.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size()),
142+
.ppEnabledExtensionNames = deviceExtensions.data()
143+
};
151144
----
152145

146+
Reviewing how we connect our feature chain to the device creation process:
147+
148+
1. The `featureChain.get<vk::PhysicalDeviceFeatures2>()` method retrieves a reference to the first structure in our chain (the `vk::PhysicalDeviceFeatures2` structure).
149+
150+
2. We assign this reference to the `pNext` field of the `deviceCreateInfo` structure.
151+
152+
3. Since all the structures in our feature chain are already connected (thanks to `vk::StructureChain`), Vulkan will be able to see all the features we want to enable by following the chain of `pNext` pointers.
153+
154+
This approach allows us to request multiple sets of features in a clean and organized way. Vulkan will process each structure in the chain and enable the requested features during device creation.
155+
153156
The remainder of the information bears a resemblance to the
154157
`VkInstanceCreateInfo` struct and requires you to specify extensions and
155158
validation layers. The difference is that these are device-specific this time.

0 commit comments

Comments
 (0)