Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,88 @@ void pickPhysicalDevice() {
}
----

=== 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:

[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
- `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:

[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
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:

[source,cpp]
----
const auto qfpIter = std::ranges::find_if(queueFamilies,
[]( vk::QueueFamilyProperties const & qfp ) {
return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(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:

[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.

==== 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.

== Queue families
Expand All @@ -214,24 +296,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<vk::QueueFamilyProperties> 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<uint32_t>( 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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<vk::QueueFamilyProperties> 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<vk::QueueFlags>(0); } );
assert(graphicsQueueFamilyProperty != queueFamilyProperties.end() && "No graphics queue family found!");

// Calculate the index of the graphics queue family
auto graphicsIndex = static_cast<uint32_t>( 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<vk::QueueFlags>(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 };
----

Expand Down
Loading