From f5bf7991e6f96e19dc58d62a726aaf41c284f989 Mon Sep 17 00:00:00 2001 From: swinston Date: Mon, 21 Jul 2025 19:37:09 -0700 Subject: [PATCH 1/2] address feedback given #105 and start reviewing the rest of the tutorial for similar instances that might require further explainer text. --- ...3_Physical_devices_and_queue_families.adoc | 98 +++++++++++++++---- .../04_Logical_device_and_queues.adoc | 32 ++++++ 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc b/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc index 72db501b..1f9fa4f7 100644 --- a/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc @@ -199,6 +199,83 @@ void pickPhysicalDevice() { throw std::runtime_error("failed to find a suitable GPU!"); } } + +=== Understanding the nested lambda functions + +The `pickPhysicalDevice()` function above uses several nested lambda functions, which might look complex if you're not familiar with this C++ feature. Let's break down what's happening: + +==== What are lambda functions? + +Lambda functions (or lambda expressions) are a C++ feature that allows you to define anonymous functions inline. They're especially useful for short operations that you don't need to define as separate named functions. + +The basic syntax of a lambda is: +```cpp +[capture-list](parameters) { body } +``` + +- The `capture-list` specifies which variables from the surrounding scope are accessible inside the lambda +- `parameters` are the input parameters, just like in regular functions +- `body` contains the code that will be executed + +==== The main device selection lambda + +In our `pickPhysicalDevice()` function, we use `std::ranges::find_if` with a lambda to find the first suitable device: + +```cpp +const auto devIter = std::ranges::find_if(devices, + [&](auto const & device) { + // Lambda body that checks if the device is suitable + // ... + return isSuitable; + }); +``` + +The `[&]` capture list means this lambda can access all variables from the +surrounding scope by reference. This is necessary because we need to access +the `deviceExtensions` variable. NB: this can be replaced by the explicit +capture of deviceExtnesions variable, however, the & (capture of everything) is +convenience andclarity as goals in mind. + +==== Nested lambda for finding graphics queue family + +Inside the main lambda, we have another lambda that checks if a queue family supports graphics operations: + +```cpp +const auto qfpIter = std::ranges::find_if(queueFamilies, + []( vk::QueueFamilyProperties const & qfp ) { + return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0); + }); +``` + +This lambda doesn't need to capture any variables (empty `[]`), as it only uses its parameter `qfp`. + +==== Nested lambda for checking extension support + +Further down, we have another lambda that checks if a specific extension is supported: + +```cpp +auto extensionIter = std::ranges::find_if(extensions, + [extension](auto const & ext) { + return strcmp(ext.extensionName, extension) == 0; + }); +``` + +This lambda captures the `extension` variable by value (`[extension]`) so it can compare it with each available extension. + +==== Why use nested lambdas? + +Nested lambdas are used here for a few reasons: + +1. **Code organization**: Each lambda handles a specific part of the device selection criteria. +2. **Reusability**: The same pattern (using `std::ranges::find_if` with a predicate) is used for different checks. +3. **Efficiency**: We avoid creating separate named functions for these small operations. +4. **Readability**: Once you understand the pattern, it becomes easier to see what each part is checking for. + +While nested lambdas can make code more concise, they can also make it harder + to read for those not used to modern C++ idioms. That's why we're providing + this explanation to help you understand the pattern. Throughout this + tutorial, we will attempt to use only modern C++ to keep with a one language + paradigm for ease of use/familiarity. ---- In the next section, we'll discuss the first real required feature to check for. @@ -214,24 +291,11 @@ commands or one that only allows memory transfer related commands. We need to check which queue families are supported by the device and which one of these supports the commands that we want to use. Right now we are only -going to look for a queue that supports graphics commands, so the code -could look like this: - -[,c++] ----- -uint32_t findQueueFamilies(VkPhysicalDevice device) { - // find the index of the first queue family that supports graphics - std::vector queueFamilyProperties = physicalDevice->getQueueFamilyProperties(); - - // get the first index into queueFamilyProperties which supports graphics - auto graphicsQueueFamilyProperty = - std::find_if( queueFamilyProperties.begin(), - queueFamilyProperties.end(), - []( vk::QueueFamilyProperties const & qfp ) { return qfp.queueFlags & vk::QueueFlagBits::eGraphics; } ); +going to look for a queue that supports graphics commands. - return static_cast( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) ); -} ----- +When selecting a physical device, we've already seen how to check for graphics support in our +`pickPhysicalDevice()` function. In the next chapter, we'll implement a more detailed approach +to find the queue family index and create a queue from it. Great, that's all we need for now to find the right physical device! The next step is to xref:./04_Logical_device_and_queues.adoc[create a logical device] 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 176869ab..491dddb1 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 @@ -41,10 +41,42 @@ structs again, of which the first one will be `VkDeviceQueueCreateInfo`. This structure describes the number of queues we want for a single queue family. Right now we're only interested in a queue with graphics capabilities. +=== Finding the graphics queue family index + +Before we can create a queue, we need to find which queue family supports graphics operations. Let's add code to find the graphics queue family index: + [,c++] ---- +// Get all queue family properties from the physical device std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); +// Find the first queue family that supports graphics operations +auto graphicsQueueFamilyProperty = std::ranges::find_if(queueFamilyProperties, []( auto const & qfp ) + { return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0); } ); +assert(graphicsQueueFamilyProperty != queueFamilyProperties.end() && "No graphics queue family found!"); + +// Calculate the index of the graphics queue family +auto graphicsIndex = static_cast( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) ); +---- + +Let's break down this code to understand what it's doing: + +1. First, we get all queue family properties from the physical device using `getQueueFamilyProperties()`. + +2. Then, we use `std::ranges::find_if` to find the first queue family that supports graphics operations: + - `std::ranges::find_if` takes a collection and a predicate function (the lambda), and returns an iterator to the first element that satisfies the predicate. + - The lambda function `[]( auto const & qfp ) { ... }` checks if a queue family has graphics capabilities. + - Inside the lambda, we use a bitwise AND operation `&` to check if the `queueFlags` include the `eGraphics` flag. + - The comparison `!= static_cast(0)` ensures the result is non-zero, meaning the graphics flag is set. + +3. We use `assert` to verify that we found a queue family with graphics support. + +4. Finally, we calculate the index of the graphics queue family using `std::distance`, which gives us the position of the iterator in the collection. + +Now that we have the graphics queue family index, we can use it to create our queue: + +[,c++] +---- vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = graphicsIndex }; ---- From b9c2eaa796d6228309f16fb33f539b830755953d Mon Sep 17 00:00:00 2001 From: swinston Date: Tue, 22 Jul 2025 10:18:07 -0700 Subject: [PATCH 2/2] address asciidoc syntax mistakes. --- ...3_Physical_devices_and_queue_families.adoc | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc b/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc index 1f9fa4f7..78f05a4d 100644 --- a/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc +++ b/en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc @@ -199,6 +199,7 @@ void pickPhysicalDevice() { throw std::runtime_error("failed to find a suitable GPU!"); } } +---- === Understanding the nested lambda functions @@ -209,9 +210,11 @@ The `pickPhysicalDevice()` function above uses several nested lambda functions, Lambda functions (or lambda expressions) are a C++ feature that allows you to define anonymous functions inline. They're especially useful for short operations that you don't need to define as separate named functions. The basic syntax of a lambda is: -```cpp + +[source,cpp] +---- [capture-list](parameters) { body } -``` +---- - The `capture-list` specifies which variables from the surrounding scope are accessible inside the lambda - `parameters` are the input parameters, just like in regular functions @@ -221,14 +224,15 @@ The basic syntax of a lambda is: In our `pickPhysicalDevice()` function, we use `std::ranges::find_if` with a lambda to find the first suitable device: -```cpp +[source,cpp] +---- const auto devIter = std::ranges::find_if(devices, [&](auto const & device) { // Lambda body that checks if the device is suitable // ... return isSuitable; }); -``` +---- The `[&]` capture list means this lambda can access all variables from the surrounding scope by reference. This is necessary because we need to access @@ -240,12 +244,13 @@ convenience andclarity as goals in mind. Inside the main lambda, we have another lambda that checks if a queue family supports graphics operations: -```cpp +[source,cpp] +---- const auto qfpIter = std::ranges::find_if(queueFamilies, []( vk::QueueFamilyProperties const & qfp ) { return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast(0); }); -``` +---- This lambda doesn't need to capture any variables (empty `[]`), as it only uses its parameter `qfp`. @@ -253,12 +258,13 @@ This lambda doesn't need to capture any variables (empty `[]`), as it only uses Further down, we have another lambda that checks if a specific extension is supported: -```cpp +[source,cpp] +---- auto extensionIter = std::ranges::find_if(extensions, [extension](auto const & ext) { return strcmp(ext.extensionName, extension) == 0; }); -``` +---- This lambda captures the `extension` variable by value (`[extension]`) so it can compare it with each available extension. @@ -276,7 +282,6 @@ While nested lambdas can make code more concise, they can also make it harder this explanation to help you understand the pattern. Throughout this tutorial, we will attempt to use only modern C++ to keep with a one language paradigm for ease of use/familiarity. ----- In the next section, we'll discuss the first real required feature to check for.