So I tried github copilot for the first time…
Honestly, I think the chat/conversation feature is more valuable than the code suggestion. It helps you work through a problem like you would with another engineer except they are at an almost omnipotent level.
Transcript of my chat with copilot
Me
What is a good approach to VkPhysicalDevice selection
GitHub Copilot
The selection of a VkPhysicalDevice
in a Vulkan application is a crucial step as it represents the GPU that will be used for rendering. The selection process typically involves the following steps:
- Enumerate the physical devices available on the system using
vkEnumeratePhysicalDevices
. - For each physical device, check if it supports the features your application needs. This can include checking the type of the device (discrete GPU, integrated GPU, etc.), checking if it supports the required Vulkan version, checking if it supports the required Vulkan extensions, and checking if it has enough memory and compute units for your needs.
- Score each device based on your application’s specific needs. For example, you might prefer a discrete GPU over an integrated one, or a device that supports a higher Vulkan version, or a device with more memory, etc.
- Select the device with the highest score.
Here is a basic example of how you can implement this in C++:
VkPhysicalDevice selectPhysicalDevice(VkInstance instance) {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
std::multimap<int, VkPhysicalDevice> candidates;
for (const auto& device : devices) {
int score = scoreDevice(device);
candidates.insert(std::make_pair(score, device));
}
// Check if the best candidate is suitable at all
if (candidates.rbegin()->first > 0) {
return candidates.rbegin()->second;
} else {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
In this code, scoreDevice
is a function that calculates a score for a device based on your application’s specific needs. You can modify this function to suit your needs.
Me
Can you design a composable functional approach to scoreDevice for me? An acceptable solution should allow for multiple implementations of a scoreDevice algorithm which are composable onto each other.
GitHub Copilot
Sure, a composable functional approach to scoreDevice
can be achieved by creating a series of scoring functions that each handle a specific aspect of the scoring. These functions can then be composed together to create the final scoring function. Here’s a basic example in C++:
First, define a type for your scoring functions. These functions should take a VkPhysicalDevice
and return an int
:
using ScoringFunction = std::function<int(VkPhysicalDevice)>;
Next, define your individual scoring functions. For example, you might have one function for scoring based on the device type, and another for scoring based on whether the device supports a geometry shader:
ScoringFunction scoreDeviceType = [](VkPhysicalDevice device) {
VkPhysicalDeviceProperties properties;
vkGetPhysicalDeviceProperties(device, &properties);
if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
return 1000;
} else {
return 0;
}
};
ScoringFunction scoreGeometryShader = [](VkPhysicalDevice device) {
VkPhysicalDeviceFeatures features;
vkGetPhysicalDeviceFeatures(device, &features);
if (features.geometryShader) {
return 500;
} else {
return 0;
}
};
Finally, you can create a function to compose these scoring functions together. This function takes a list of scoring functions and returns a new scoring function that sums the scores from each individual function:
ScoringFunction composeScoringFunctions(const std::vector<ScoringFunction>& functions) {
return [functions](VkPhysicalDevice device) {
int totalScore = 0;
for (const auto& function : functions) {
totalScore += function(device);
}
return totalScore;
};
}
You can now create a scoreDevice
function by composing the individual scoring functions:
ScoringFunction scoreDevice = composeScoringFunctions({scoreDeviceType, scoreGeometryShader});
This scoreDevice
function can now be used in the same way as your original scoreDevice
function, but it has the advantage of being composed of smaller, simpler functions that can be tested and modified independently.
Me
This looks good, but what if I did not want to use lambdas?
GitHub Copilot
If you prefer not to use lambdas, you can define the scoring functions as regular functions or as methods in a class. Here’s an example of how you can do this:
First, define the type for your scoring functions:
using ScoringFunction = int(*)(VkPhysicalDevice);
Next, define your individual scoring functions as regular functions:
int scoreDeviceType(VkPhysicalDevice device) {
VkPhysicalDeviceProperties properties;
vkGetPhysicalDeviceProperties(device, &properties);
if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
return 1000;
} else {
return 0;
}
}
int scoreGeometryShader(VkPhysicalDevice device) {
VkPhysicalDeviceFeatures features;
vkGetPhysicalDeviceFeatures(device, &features);
if (features.geometryShader) {
return 500;
} else {
return 0;
}
}
Then, create a function to compose these scoring functions together:
ScoringFunction composeScoringFunctions(const std::vector<ScoringFunction>& functions) {
return [functions](VkPhysicalDevice device) {
int totalScore = 0;
for (const auto& function : functions) {
totalScore += function(device);
}
return totalScore;
};
}
Finally, create a scoreDevice
function by composing the individual scoring functions:
ScoringFunction scoreDevice = composeScoringFunctions({scoreDeviceType, scoreGeometryShader});
This scoreDevice
function can now be used in the same way as your original scoreDevice
function, but it has the advantage of being composed of smaller, simpler functions that can be tested and modified independently.
Me
is there a generic variety of this compose function we have created that is available in the STL?
GitHub Copilot
Yes, there is a generic way to compose functions in C++ using the STL, but it’s not as straightforward as having a single function that does it. You can use std::bind
and std::function
to create a composition of functions. However, this doesn’t work as cleanly as the custom compose function we created earlier, especially when you want to compose multiple functions together.
Here’s an example of how you can use std::bind
and std::function
to compose two functions together:
#include <functional>
int scoreDeviceType(VkPhysicalDevice device) {
// scoring logic here
}
int scoreGeometryShader(VkPhysicalDevice device) {
// scoring logic here
}
std::function<int(VkPhysicalDevice)> compose(std::function<int(VkPhysicalDevice)> f, std::function<int(VkPhysicalDevice)> g) {
return [=](VkPhysicalDevice device) { return f(device) + g(device); };
}
int main() {
auto scoreDevice = compose(scoreDeviceType, scoreGeometryShader);
// Now you can use scoreDevice as a composed function
}
This will work for composing two functions, but if you want to compose more than two, you would need to nest calls to compose
, which can get messy.
In C++20, there’s a proposal for std::compose
which would make function composition as easy as it is in functional programming languages, but it’s not yet part of the standard library.
Me
You mentioned std::bind in your last example, but I do not see it being used anywhere.
GitHub Copilot
I apologize for the confusion. You’re correct, the example provided did not use std::bind
. It was a mistake in the explanation. The std::bind
function is used to bind one or more arguments to a function, and it returns a function object that can be called later. However, in the context of composing functions, std::bind
is not typically used. Instead, function composition in C++ is often achieved through the use of lambdas, as shown in the previous examples.
Me
could we extend our new compose method by using templates, so that it may compose other types of functions together?
GitHub Copilot
Yes, you can use templates to create a more generic compose function that can handle different types of functions. Here’s an example of how you can do this:
template<typename Func>
auto compose(Func f) {
return [f](auto... args) { return f(args...); };
}
template<typename Func1, typename Func2, typename... Funcs>
auto compose(Func1 f1, Func2 f2, Funcs... funcs) {
return [f1, f2, funcs...](auto... args) {
return f1(compose(f2, funcs...)(args...));
};
}
In this code, the first compose
function is a base case that simply returns the function f
. The second compose
function is a variadic template that takes at least two functions. It creates a lambda that calls the first function with the result of the composition of the rest of the functions.
This compose
function can be used to compose functions of different types together. For example:
auto addOne = [](int x) { return x + 1; };
auto multiplyByTwo = [](int x) { return x * 2; };
auto subtractThree = [](int x) { return x - 3; };
auto composedFunction = compose(addOne, multiplyByTwo, subtractThree);
int result = composedFunction(5); // result is 5
In this example, composedFunction
is a function that first subtracts 3 from its argument, then multiplies the result by 2, and finally adds 1.
Me
I very much like this last solution, can you now apply it to our device selection use case with examples?
GitHub Copilot
Sure, let’s apply the generic compose function to the scoreDevice
function in your Vulkan application.
First, we’ll need to modify the scoreDevice
function to be a composition of smaller scoring functions. We’ll create two scoring functions: scoreDeviceType
and scoreGeometryShader
.
int scoreDeviceType(VkPhysicalDevice device) {
VkPhysicalDeviceProperties properties;
vkGetPhysicalDeviceProperties(device, &properties);
if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
return 1000;
} else {
return 0;
}
}
int scoreGeometryShader(VkPhysicalDevice device) {
VkPhysicalDeviceFeatures features;
vkGetPhysicalDeviceFeatures(device, &features);
if (features.geometryShader) {
return 500;
} else {
return 0;
}
}
Next, we’ll define the generic compose function using templates:
template<typename Func>
auto compose(Func f) {
return [f](auto... args) { return f(args...); };
}
template<typename Func1, typename Func2, typename... Funcs>
auto compose(Func1 f1, Func2 f2, Funcs... funcs) {
return [f1, f2, funcs...](auto... args) {
return f1(compose(f2, funcs...)(args...));
};
}
Now, we can create a scoreDevice
function by composing the individual scoring functions:
auto scoreDevice = compose(scoreDeviceType, scoreGeometryShader);
Finally, we’ll need to modify the selectPhysicalDevice
function to use the new scoreDevice
function:
VkPhysicalDevice selectPhysicalDevice(VkInstance instance) {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
std::multimap<int, VkPhysicalDevice> candidates;
for (const auto& device : devices) {
int score = scoreDevice(device);
candidates.insert(std::make_pair(score, device));
}
// Check if the best candidate is suitable at all
if (candidates.rbegin()->first > 0) {
return candidates.rbegin()->second;
} else {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
This scoreDevice
function can now be used in the same way as your original scoreDevice
function, but it has the advantage of being composed of smaller, simpler functions that can be tested and modified independently.
Me
Awesome, this is almost perfect, but one last thing. Could you modify the selectPhysicalDevice method to be passed a scoreDevice method instead of using a top level defined method. Then, summarize our entire final solution for me.
GitHub Copilot
Sure, we can modify the selectPhysicalDevice
method to accept a scoring function as a parameter. This will make the function more flexible and allow you to use different scoring functions if needed. Here’s how you can do it:
VkPhysicalDevice selectPhysicalDevice(VkInstance instance, std::function<int(VkPhysicalDevice)> scoreDevice) {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
std::multimap<int, VkPhysicalDevice> candidates;
for (const auto& device : devices) {
int score = scoreDevice(device);
candidates.insert(std::make_pair(score, device));
}
// Check if the best candidate is suitable at all
if (candidates.rbegin()->first > 0) {
return candidates.rbegin()->second;
} else {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
Now, you can pass the scoreDevice
function when calling selectPhysicalDevice
:
auto scoreDevice = compose(scoreDeviceType, scoreGeometryShader);
auto physicalDevice = selectPhysicalDevice(instance, scoreDevice);
To summarize, we have created a more flexible and modular solution for scoring and selecting a physical device in a Vulkan application. We have defined individual scoring functions (scoreDeviceType
and scoreGeometryShader
), and a generic compose
function that can combine these scoring functions into a single function. We then pass this composed function to the selectPhysicalDevice
function, which uses it to score and select the best physical device. This approach allows for easier testing and modification of the scoring logic, as each scoring function is independent and the composition is done dynamically at runtime.
Me
My apologies, summarize was the incorrect word, could you show me the entire final code with multiple usage examples?
GitHub Copilot
Sure, here is the final code with the changes we discussed:
#include <algorithm>
#include <iostream>
#include <optional>
#include <vector>
#include <vulkan/vulkan.h>
#include <functional>
int scoreDeviceType(VkPhysicalDevice device) {
VkPhysicalDeviceProperties properties;
vkGetPhysicalDeviceProperties(device, &properties);
if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
return 1000;
} else {
return 0;
}
}
int scoreGeometryShader(VkPhysicalDevice device) {
VkPhysicalDeviceFeatures features;
vkGetPhysicalDeviceFeatures(device, &features);
if (features.geometryShader) {
return 500;
} else {
return 0;
}
}
template<typename Func>
auto compose(Func f) {
return [f](auto... args) { return f(args...); };
}
template<typename Func1, typename Func2, typename... Funcs>
auto compose(Func1 f1, Func2 f2, Funcs... funcs) {
return [f1, f2, funcs...](auto... args) {
return f1(compose(f2, funcs...)(args...));
};
}
VkPhysicalDevice selectPhysicalDevice(VkInstance instance, std::function<int(VkPhysicalDevice)> scoreDevice) {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
std::multimap<int, VkPhysicalDevice> candidates;
for (const auto& device : devices) {
int score = scoreDevice(device);
candidates.insert(std::make_pair(score, device));
}
// Check if the best candidate is suitable at all
if (candidates.rbegin()->first > 0) {
return candidates.rbegin()->second;
} else {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
int main() {
VkInstance instance = createInstance(); // Assuming createInstance() is defined elsewhere
auto scoreDevice = compose(scoreDeviceType, scoreGeometryShader);
VkPhysicalDevice physicalDevice = selectPhysicalDevice(instance, scoreDevice);
// You can also create different scoring functions and use them
auto scoreDevice2 = compose([](VkPhysicalDevice device) { /* scoring logic */ }, scoreGeometryShader);
VkPhysicalDevice physicalDevice2 = selectPhysicalDevice(instance, scoreDevice2);
}
In this code, we first define the individual scoring functions scoreDeviceType
and scoreGeometryShader
. We then define a generic compose
function that can combine these scoring functions into a single function. The selectPhysicalDevice
function is modified to accept a scoring function as a parameter. In the main
function, we create a scoreDevice
function by composing scoreDeviceType
and scoreGeometryShader
, and pass this function to selectPhysicalDevice
to select the best physical device. We also show an example of creating a different scoring function and using it to select a physical device.
Me
This looks great, but looking at the code it is not clear to me if our compose implementation is suming the return values of the composed methods as I would expect. Additionally, it seems like there is no way for one of the composed methods to short circuit the entire thing if it comes across a requirement that can not be satisfied. Would you agree with my assesment?
GitHub Copilot
Yes, you are correct in your assessment. The current implementation of the compose
function does not sum the return values of the composed functions. Instead, it passes the return value of one function as the argument to the next function. This is a common pattern in function composition, but it may not be what you want in this case.
Additionally, you are correct that the current implementation does not provide a way for one of the composed functions to short-circuit the entire process. If one of the functions returns a value that indicates a requirement is not satisfied, the other functions will still be called.
If you want the compose
function to sum the return values of the composed functions and to stop evaluating as soon as one of the functions returns a value that indicates a requirement is not satisfied, you would need to modify the compose
function. Here’s an example of how you could do this:
template<typename Func>
auto compose(Func f) {
return [f](auto... args) { return f(args...); };
}
template<typename Func1, typename Func2, typename... Funcs>
auto compose(Func1 f1, Func2 f2, Funcs... funcs) {
return [f1, f2, funcs...](auto... args) {
int result = compose(f2, funcs...)(args...);
if (result == 0) { // If a requirement is not satisfied
return 0;
}
return f1(args...) + result;
};
}
In this code, the compose
function checks the result of the composed functions. If the result is 0, it returns 0 immediately, short-circuiting the rest of the evaluation. Otherwise, it adds the result of f1(args...)
to the result and returns the sum.
Me
Awesome, this works great! I definately would have arrived to this conclussion on my own, but you saved me a few hours for sure, thanks.
GitHub Copilot
You’re welcome! I’m glad I could assist you. If you have any more questions or need further help in the future, don’t hesitate to ask. Happy coding!