Skip to content

Commit f5bf799

Browse files
committed
address feedback given #105 and start reviewing the rest of the tutorial for similar instances that might require further explainer text.
1 parent 3d5207c commit f5bf799

File tree

2 files changed

+113
-17
lines changed

2 files changed

+113
-17
lines changed

en/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.adoc

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,83 @@ void pickPhysicalDevice() {
199199
throw std::runtime_error("failed to find a suitable GPU!");
200200
}
201201
}
202+
203+
=== Understanding the nested lambda functions
204+
205+
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:
206+
207+
==== What are lambda functions?
208+
209+
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.
210+
211+
The basic syntax of a lambda is:
212+
```cpp
213+
[capture-list](parameters) { body }
214+
```
215+
216+
- The `capture-list` specifies which variables from the surrounding scope are accessible inside the lambda
217+
- `parameters` are the input parameters, just like in regular functions
218+
- `body` contains the code that will be executed
219+
220+
==== The main device selection lambda
221+
222+
In our `pickPhysicalDevice()` function, we use `std::ranges::find_if` with a lambda to find the first suitable device:
223+
224+
```cpp
225+
const auto devIter = std::ranges::find_if(devices,
226+
[&](auto const & device) {
227+
// Lambda body that checks if the device is suitable
228+
// ...
229+
return isSuitable;
230+
});
231+
```
232+
233+
The `[&]` capture list means this lambda can access all variables from the
234+
surrounding scope by reference. This is necessary because we need to access
235+
the `deviceExtensions` variable. NB: this can be replaced by the explicit
236+
capture of deviceExtnesions variable, however, the & (capture of everything) is
237+
convenience andclarity as goals in mind.
238+
239+
==== Nested lambda for finding graphics queue family
240+
241+
Inside the main lambda, we have another lambda that checks if a queue family supports graphics operations:
242+
243+
```cpp
244+
const auto qfpIter = std::ranges::find_if(queueFamilies,
245+
[]( vk::QueueFamilyProperties const & qfp ) {
246+
return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(0);
247+
});
248+
```
249+
250+
This lambda doesn't need to capture any variables (empty `[]`), as it only uses its parameter `qfp`.
251+
252+
==== Nested lambda for checking extension support
253+
254+
Further down, we have another lambda that checks if a specific extension is supported:
255+
256+
```cpp
257+
auto extensionIter = std::ranges::find_if(extensions,
258+
[extension](auto const & ext) {
259+
return strcmp(ext.extensionName, extension) == 0;
260+
});
261+
```
262+
263+
This lambda captures the `extension` variable by value (`[extension]`) so it can compare it with each available extension.
264+
265+
==== Why use nested lambdas?
266+
267+
Nested lambdas are used here for a few reasons:
268+
269+
1. **Code organization**: Each lambda handles a specific part of the device selection criteria.
270+
2. **Reusability**: The same pattern (using `std::ranges::find_if` with a predicate) is used for different checks.
271+
3. **Efficiency**: We avoid creating separate named functions for these small operations.
272+
4. **Readability**: Once you understand the pattern, it becomes easier to see what each part is checking for.
273+
274+
While nested lambdas can make code more concise, they can also make it harder
275+
to read for those not used to modern C++ idioms. That's why we're providing
276+
this explanation to help you understand the pattern. Throughout this
277+
tutorial, we will attempt to use only modern C++ to keep with a one language
278+
paradigm for ease of use/familiarity.
202279
----
203280

204281
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.
214291

215292
We need to check which queue families are supported by the device and which one
216293
of these supports the commands that we want to use. Right now we are only
217-
going to look for a queue that supports graphics commands, so the code
218-
could look like this:
219-
220-
[,c++]
221-
----
222-
uint32_t findQueueFamilies(VkPhysicalDevice device) {
223-
// find the index of the first queue family that supports graphics
224-
std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice->getQueueFamilyProperties();
225-
226-
// get the first index into queueFamilyProperties which supports graphics
227-
auto graphicsQueueFamilyProperty =
228-
std::find_if( queueFamilyProperties.begin(),
229-
queueFamilyProperties.end(),
230-
[]( vk::QueueFamilyProperties const & qfp ) { return qfp.queueFlags & vk::QueueFlagBits::eGraphics; } );
294+
going to look for a queue that supports graphics commands.
231295

232-
return static_cast<uint32_t>( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) );
233-
}
234-
----
296+
When selecting a physical device, we've already seen how to check for graphics support in our
297+
`pickPhysicalDevice()` function. In the next chapter, we'll implement a more detailed approach
298+
to find the queue family index and create a queue from it.
235299

236300
Great, that's all we need for now to find the right physical device! The next
237301
step is to xref:./04_Logical_device_and_queues.adoc[create a logical device]

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,42 @@ structs again, of which the first one will be `VkDeviceQueueCreateInfo`. This
4141
structure describes the number of queues we want for a single queue family.
4242
Right now we're only interested in a queue with graphics capabilities.
4343

44+
=== Finding the graphics queue family index
45+
46+
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:
47+
4448
[,c++]
4549
----
50+
// Get all queue family properties from the physical device
4651
std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice.getQueueFamilyProperties();
4752
53+
// Find the first queue family that supports graphics operations
54+
auto graphicsQueueFamilyProperty = std::ranges::find_if(queueFamilyProperties, []( auto const & qfp )
55+
{ return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(0); } );
56+
assert(graphicsQueueFamilyProperty != queueFamilyProperties.end() && "No graphics queue family found!");
57+
58+
// Calculate the index of the graphics queue family
59+
auto graphicsIndex = static_cast<uint32_t>( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) );
60+
----
61+
62+
Let's break down this code to understand what it's doing:
63+
64+
1. First, we get all queue family properties from the physical device using `getQueueFamilyProperties()`.
65+
66+
2. Then, we use `std::ranges::find_if` to find the first queue family that supports graphics operations:
67+
- `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.
68+
- The lambda function `[]( auto const & qfp ) { ... }` checks if a queue family has graphics capabilities.
69+
- Inside the lambda, we use a bitwise AND operation `&` to check if the `queueFlags` include the `eGraphics` flag.
70+
- The comparison `!= static_cast<vk::QueueFlags>(0)` ensures the result is non-zero, meaning the graphics flag is set.
71+
72+
3. We use `assert` to verify that we found a queue family with graphics support.
73+
74+
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.
75+
76+
Now that we have the graphics queue family index, we can use it to create our queue:
77+
78+
[,c++]
79+
----
4880
vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = graphicsIndex };
4981
----
5082

0 commit comments

Comments
 (0)