From 1f3c4fef5943e70d8fef61bf5b639e8ee97618dc Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 8 Jul 2025 09:17:44 -0700 Subject: [PATCH] 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. --- .../00_Setup/01_Instance.adoc | 22 +++- .../00_Setup/02_Validation_layers.adoc | 118 ++++++++++++------ .../04_Logical_device_and_queues.adoc | 79 ++++++------ 3 files changed, 139 insertions(+), 80 deletions(-) diff --git a/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc b/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc index 26f97ad0..b6ef04e8 100644 --- a/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/01_Instance.adoc @@ -26,7 +26,7 @@ raii context: ---- private: vk::raii::Context context; -std::unique_ptr instance; +vk::raii::Instance instance = nullptr; ---- 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: [,c++] ---- +// Get the required instance extensions from GLFW. uint32_t glfwExtensionCount = 0; -const char** glfwExtensions; - -glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); +auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +// Check if the required GLFW extensions are supported by the Vulkan implementation. +auto extensionProperties = context.enumerateInstanceExtensionProperties(); +for (uint32_t i = 0; i < glfwExtensionCount; ++i) +{ + if (std::ranges::none_of(extensionProperties, + [glfwExtension = glfwExtensions[i]](auto const& extensionProperty) + { return strcmp(extensionProperty.extensionName, glfwExtension) == 0; })) + { + throw std::runtime_error("Required GLFW extension not supported: " + std::string(glfwExtensions[i])); + } +} vk::InstanceCreateInfo createInfo{ .pApplicationInfo = &appInfo, - .ppEnabledLayerNames = glfwExtensions}; + .enabledExtensionCount = glfwExtensionCount, + .ppEnabledExtensionNames = glfwExtensions}; ---- The other missing piece is the Layers to enable. Here is where we'll talk diff --git a/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc b/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc index 59e00b7a..bc70a9e3 100644 --- a/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/02_Validation_layers.adoc @@ -104,26 +104,35 @@ constexpr bool enableValidationLayers = true; #endif ---- -We'll add a new function `checkValidationLayerSupport` that checks if all -the requested layers are available. As the instance layers are returned as a -std::array, they can be filtered via a single line and return the value -using lambda functions. - -[,c++] ----- -bool checkValidationLayerSupport() { - return (std::ranges::any_of(context.enumerateInstanceLayerProperties(), - []( vk::LayerProperties const & lp ) { return ( strcmp( "VK_LAYER_KHRONOS_validation", lp.layerName ) == 0 ); } ) ); -} ----- - -We can now use this function in `createInstance`: +We'll check if all the requested layers are available. We need to iterate +through the requested layers and validate that all the required layers are +supported by the Vulkan implementation. This check is performed directly in the +`createInstance` function: [,c++] ---- void createInstance() { - if (enableValidationLayers && !checkValidationLayerSupport()) { - throw std::runtime_error("validation layers requested, but not available!"); + constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle", + .applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION( 1, 0, 0 ), + .apiVersion = vk::ApiVersion14 }; + + // Get the required layers + std::vector requiredLayers; + if (enableValidationLayers) { + requiredLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Check if the required layers are supported by the Vulkan implementation. + auto layerProperties = context.enumerateInstanceLayerProperties(); + if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) { + return std::ranges::none_of(layerProperties, + [requiredLayer](auto const& layerProperty) + { return strcmp(layerProperty.layerName, requiredLayer) == 0; }); + })) + { + throw std::runtime_error("One or more required layers are not supported!"); } ... @@ -138,11 +147,12 @@ validation layer names if they are enabled: [,c++] ---- -std::vector enabledLayers; -if (enableValidationLayers) { - enabledLayers.assign(validationLayers.begin(), validationLayers.end()); -} -vk::InstanceCreateInfo createInfo({}, &appInfo, enabledLayers, {}); +vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(requiredLayers.size()), + .ppEnabledLayerNames = requiredLayers.data(), + .enabledExtensionCount = 0, + .ppEnabledExtensionNames = nullptr }; ---- If the check was successful then `vkCreateInstance` should not ever return a @@ -171,12 +181,6 @@ std::vector getRequiredExtensions() { uint32_t glfwExtensionCount = 0; auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); - std::vector props = context.enumerateInstanceExtensionProperties(); - if (const auto propsIterator = std::ranges::find_if(props, []( vk::ExtensionProperties const & ep ) { return strcmp( ep.extensionName, vk::EXTDebugUtilsExtensionName ) == 0; } ); propsIterator == props.end() ) - { - std::cout << "Something went very wrong, cannot find VK_EXT_debug_utils extension" << std::endl; - exit( 1 ); - } std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); if (enableValidationLayers) { extensions.push_back(vk::EXTDebugUtilsExtensionName ); @@ -268,7 +272,7 @@ you want. Add a class member for this handle right under `instance`: [,c++] ---- -std::unique_ptr debugMessenger; +vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; ---- 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 ---- vk::DebugUtilsMessageSeverityFlagsEXT severityFlags( vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError ); vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags( vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation ); - -vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT({}, severityFlags, messageTypeFlags, &debugCallback); -debugMessenger = std::make_unique( *instance, debugUtilsMessengerCreateInfoEXT ); +vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{ + .messageSeverity = severityFlags, + .messageType = messageTypeFlags, + .pfnUserCallback = &debugCallback + }; +debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); ---- 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: [,c++] ---- void createInstance() { - constexpr auto appInfo = vk::ApplicationInfo("Hello Triangle", 1, "No Engine", 1, vk::ApiVersion11); - auto extensions = getRequiredExtensions(); - std::vector enabledLayers; + constexpr vk::ApplicationInfo appInfo{ .pApplicationName = "Hello Triangle", + .applicationVersion = VK_MAKE_VERSION( 1, 0, 0 ), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION( 1, 0, 0 ), + .apiVersion = vk::ApiVersion14 }; + + // Get the required layers + std::vector requiredLayers; if (enableValidationLayers) { - enabledLayers.assign(validationLayers.begin(), validationLayers.end()); + requiredLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Check if the required layers are supported by the Vulkan implementation. + auto layerProperties = context.enumerateInstanceLayerProperties(); + if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) { + return std::ranges::none_of(layerProperties, + [requiredLayer](auto const& layerProperty) + { return strcmp(layerProperty.layerName, requiredLayer) == 0; }); + })) + { + throw std::runtime_error("One or more required layers are not supported!"); } - vk::InstanceCreateInfo createInfo({}, &appInfo, enabledLayers.size(), enabledLayers.data(), extensions.size(), extensions.data()); - instance = std::make_unique(context, createInfo); + + // Get the required extensions. + auto requiredExtensions = getRequiredExtensions(); + + // Check if the required extensions are supported by the Vulkan implementation. + auto extensionProperties = context.enumerateInstanceExtensionProperties(); + for (auto const & requiredExtension : requiredExtensions) + { + if (std::ranges::none_of(extensionProperties, + [requiredExtension](auto const& extensionProperty) + { return strcmp(extensionProperty.extensionName, requiredExtension) == 0; })) + { + throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension)); + } + } + + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(requiredLayers.size()), + .ppEnabledLayerNames = requiredLayers.data(), + .enabledExtensionCount = static_cast(requiredExtensions.size()), + .ppEnabledExtensionNames = requiredExtensions.data() }; + instance = vk::raii::Instance(context, createInfo); } ---- diff --git a/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc b/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc index f24eddc9..176869ab 100644 --- a/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.adoc @@ -77,50 +77,40 @@ start doing more interesting things with Vulkan. vk::PhysicalDeviceFeatures deviceFeatures; ---- -== Specifying any extra features or updates we'd like our device to support +== Enabling additional device features -Vulkan is built to be backwards compatible. Thus, if you do nothing else, -you will get a Vulkan instance just as it was originally released in Vulkan 1 -.0. This is necessary as code written back then would need to still work so -any additional features must be new code which would need to be turned on. -So, let's tell Vulkan that we use some of the more modern features which make - it easier to work with. +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. -First, let's query for the features of the physical device: +In Vulkan, features are organized into different structures based on when they were introduced or what functionality they relate to. For example: +- Basic features are in `vk::PhysicalDeviceFeatures` +- Vulkan 1.3 features are in `vk::PhysicalDeviceVulkan13Features` +- Extension-specific features are in their own structures (like `vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT`) -[,c++] ----- - // query for Vulkan 1.3 features - auto features = physicalDevice.getFeatures2(); ----- +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. -Now, let's tell Vulkan that there's a few features we wish to use, by turning - on dynamicRendering and the extendedDynamicState. +The C++ Vulkan API provides a helper template called `vk::StructureChain` that makes this process easier. Let's see how to use it: [,c++] ---- -vk::PhysicalDeviceVulkan13Features vulkan13Features; -vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures; -vulkan13Features.dynamicRendering = vk::True; -extendedDynamicStateFeatures.extendedDynamicState = vk::True; +// Create a chain of feature structures +vk::StructureChain featureChain = { + {}, // vk::PhysicalDeviceFeatures2 (empty for now) + {.dynamicRendering = true }, // Enable dynamic rendering from Vulkan 1.3 + {.extendedDynamicState = true } // Enable extended dynamic state from the extension +}; ---- -Note that these are just variables that we created, and aren't part of the -device create info logic structure. In Vulkan, all structures have a .pNext -member variable which allows for chaining. So, we need to tell each object -about the next object in the chain like this: +Here's what's happening in this code: -[,c++] ----- -vulkan13Features.pNext = &extendedDynamicStateFeatures; -features.pNext = &vulkan13Features; ----- +1. We create a `vk::StructureChain` with three different feature structures. +2. For each structure in the chain, we provide an initializer: + - The first structure (`vk::PhysicalDeviceFeatures2`) is left empty with `{}` + - In the second structure, we enable the `dynamicRendering` feature from Vulkan 1.3 + - In the third structure, we enable the `extendedDynamicState` feature from an extension -Note here that features is the same object that we got when we queried the -physical device for the features. Each pNext member variable points to the -next feature structure in the chain. So all that's left is to ensure that -when we create the logical device with a VkDeviceCreateInfo structure we set -.pNext in that structure to the pointer to the features. +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. + +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. == Specifying device extensions @@ -140,16 +130,29 @@ The `VK_KHR_swapchain` extension is required for presenting rendered images to t == Creating the logical device -With the previous structures in place, we can start filling in the main -`VkDeviceCreateInfo` structure. +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: [,c++] ---- -vk::DeviceCreateInfo deviceCreateInfo{ .pNext = &features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &deviceQueueCreateInfo }; -deviceCreateInfo.enabledExtensionCount = deviceExtensions.size(); -deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); +vk::DeviceCreateInfo deviceCreateInfo{ + .pNext = &featureChain.get(), + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &deviceQueueCreateInfo, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data() +}; ---- +Reviewing how we connect our feature chain to the device creation process: + +1. The `featureChain.get()` method retrieves a reference to the first structure in our chain (the `vk::PhysicalDeviceFeatures2` structure). + +2. We assign this reference to the `pNext` field of the `deviceCreateInfo` structure. + +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. + +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. + The remainder of the information bears a resemblance to the `VkInstanceCreateInfo` struct and requires you to specify extensions and validation layers. The difference is that these are device-specific this time.