物理设备和队列族
选择物理设备
通过 VkInstance 初始化 Vulkan 库后,我们需要在系统中查找并选择支持我们所需功能的显卡。实际上,我们可以选择任意数量的显卡并同时使用它们,但在本教程中,我们将坚持使用第一个满足我们需求的显卡。
我们将添加一个函数 pickPhysicalDevice,并在 initVulkan 函数中调用它。
void initVulkan() {
createInstance();
setupDebugMessenger();
pickPhysicalDevice();
}
void pickPhysicalDevice() {
}
我们最终选择的显卡将存储在一个 VkPhysicalDevice 句柄中,作为新的类成员添加。
vk::raii::PhysicalDevice physicalDevice;
列出显卡与列出扩展非常相似,首先查询数量。
auto devices = instance.enumeratePhysicalDevices()
如果没有支持 Vulkan 的设备,那么继续下去就没有意义了。
if (devices.empty()) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
现在我们需要评估每个设备,并检查它们是否适合我们想要执行的操作,因为并非所有显卡都是相同的。我们将检查是否有任何物理设备满足我们将添加到该函数中的要求。
for (const auto& device : devices) {
physicalDevice = std::make_unique<vk::raii::PhysicalDevice>(device);
break;
}
基本设备适用性检查
要评估设备的适用性,我们可以从查询一些细节开始。基本的设备属性,如名称、类型和支持的 Vulkan 版本,可以使用 vkGetPhysicalDeviceProperties 查询。
auto deviceProperties = device.getProperties();
对可选功能的支持,如纹理压缩、64 位浮点数和多视口渲染(对 VR 有用),可以使用 vkGetPhysicalDeviceFeatures 查询:
auto deviceFeatures = device.getFeatures();
还有更多可以从设备查询的细节,我们将在后面讨论设备内存和队列族(参见下一节)。
例如,假设我们认为我们的应用程序仅适用于支持几何着色器的专用显卡。那么 isDeviceSuitable 函数将如下所示:
bool isDeviceSuitable(VkPhysicalDevice device) {
auto deviceProperties = device.getProperties();
auto deviceFeatures = device.getFeatures();
if (deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu &&
deviceFeatures.geometryShader) {
physicalDevice = std::make_unique<vk::raii::PhysicalDevice>(device);
break;
}
}
您不仅可以检查设备是否合适并选择第一个合适的设备,还可以为每个设备打分并选择得分最高的设备。这样,您可以通过给专用显卡更高的分数来优先选择它,但如果只有集成 GPU 可用,则回退到集成 GPU。您可以实现类似以下的内容:
#include <map>
...
void pickPhysicalDevice() {
auto devices = vk::raii::PhysicalDevices( *instance );
if (devices.empty()) {
throw std::runtime_error( "failed to find GPUs with Vulkan support!" );
}
// 使用有序映射自动按分数递增排序候选设备
std::multimap<int, vk::raii::PhysicalDevice> candidates;
for (const auto& device : devices) {
auto deviceProperties = device.getProperties();
auto deviceFeatures = device.getFeatures();
uint32_t score = 0;
// 专用 GPU 具有显著的性能优势
if (deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) {
score += 1000;
}
// 纹理的最大可能尺寸影响图形质量
score += deviceProperties.limits.maxImageDimension2D;
// 应用程序无法在没有几何着色器的情况下运行
if (!deviceFeatures.geometryShader) {
continue;
}
candidates.insert(std::make_pair(score, device));
}
// 检查最佳候选设备是否完全合适
if (candidates.rbegin()->first > 0) {
physicalDevice = std::make_unique<vk::raii::PhysicalDevice>(candidates.rbegin()->second);
} else {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
您不需要为本教程实现所有这些,但这是为了让您了解如何设计设备选择过程。当然,您也可以显示选择的名称并允许用户选择。
因为我们刚开始,Vulkan 1.3 支持是我们唯一需要的,因此我们将搜索它以及我们实际要演示的扩展:
std::vector<const char*> deviceExtensions = {
vk::KHRSwapchainExtensionName,
vk::KHRSpirv14ExtensionName,
vk::KHRSynchronization2ExtensionName,
vk::KHRCreateRenderpass2ExtensionName
};
void pickPhysicalDevice() {
std::vector<vk::raii::PhysicalDevice> devices = instance.enumeratePhysicalDevices();
const auto devIter = std::ranges::find_if(devices,
[&](auto const & device) {
auto queueFamilies = device.getQueueFamilyProperties();
bool isSuitable = device.getProperties().apiVersion >= VK_API_VERSION_1_3;
const auto qfpIter = std::ranges::find_if(queueFamilies,
[]( vk::QueueFamilyProperties const & qfp )
{
return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(0);
} );
isSuitable = isSuitable && ( qfpIter != queueFamilies.end() );
auto extensions = device.enumerateDeviceExtensionProperties( );
bool found = true;
for (auto const & extension : deviceExtensions) {
auto extensionIter = std::ranges::find_if(extensions, [extension](auto const & ext) {return strcmp(ext.extensionName, extension) == 0;});
found = found && extensionIter != extensions.end();
}
isSuitable = isSuitable && found;
printf("\n");
if (isSuitable) {
physicalDevice = device;
}
return isSuitable;
});
if (devIter == devices.end()) {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
在下一节中,我们将讨论第一个真正需要检查的功能。
队列族
之前已经简要提到过,Vulkan 中的几乎每个操作,从绘制到上传纹理,都需要将命令提交到队列。有不同类型的队列,它们来自不同的*队列族*,每个队列族只允许一部分命令。例如,可能有一个队列族只允许处理计算命令,或者一个只允许与内存传输相关的命令。
我们需要检查设备支持哪些队列族,以及其中哪些支持我们想要使用的命令。现在我们只寻找支持图形命令的队列,因此代码可能如下所示:
uint32_t findQueueFamilies(VkPhysicalDevice device) {
// 找到第一个支持图形的队列族的索引
std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice->getQueueFamilyProperties();
// 获取 queueFamilyProperties 中第一个支持图形的索引
auto graphicsQueueFamilyProperty =
std::find_if( queueFamilyProperties.begin(),
queueFamilyProperties.end(),
[]( vk::QueueFamilyProperties const & qfp ) { return qfp.queueFlags & vk::QueueFlagBits::eGraphics; } );
return static_cast<uint32_t>( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) );
}
很好,这就是我们现在选择正确的物理设备所需的全部内容!下一步是 创建一个逻辑设备 来与之交互。