物理设备与队列族
在创建实例之后,我们需要查找并选择系统中合适的物理设备,这通常指显卡。 你可以选择任意数量的显卡并同时使用它们,但在本教程中,我们只使用第一张满足我们需求的显卡。
相关概念:Vulkan-Guide [Querying Properties, Extensions, Features ...]
选择物理设备
1. 成员变量与辅助函数
首先在 m_debugMessenger
下方添加一个成员变量管理物理设备句柄:
vk::raii::PhysicalDevice m_physicalDevice{ nullptr };
然后添加一个函数 selectPhysicalDevice
,并在 initVulkan
函数中调用它:
void initVulkan() {
createInstance();
setupDebugMessenger();
selectPhysicalDevice();
}
void selectPhysicalDevice() {
}
2. 获取可用设备列表
void selectPhysicalDevice() {
// std::vector<vk::raii::PhysicalDevice>
const auto physicalDevices = m_instance.enumeratePhysicalDevices();
if(physicalDevices.empty()){
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
}
你可能见过
vk::raii::PhysicalDevices
类型,它末尾多了个s
。
他实际上继承了std::vector<vk::raii::PhysicalDevice>
,二者功能基本一致。
3. 设备适用性检查
现在我们需要评估每个设备,并检查它们是否适合满足要求。为此我们引入一个新函数:
bool isDeviceSuitable(const vk::raii::PhysicalDevice& physicalDevice) const {
return true;
}
4. 选择第一个合适的设备
遍历并挑选一个满足的物理设备,如果没有就抛出异常:
void selectPhysicalDevice() {
......
for (const auto& it : physicalDevices) {
if (isDeviceSuitable(it)) {
m_physicalDevice = it;
break;
}
}
if(m_physicalDevice == nullptr){
throw std::runtime_error("failed to find a suitable GPU!");
}
}
注意到 vk::raii::PhysicalDevice
可以直接拷贝赋值,这很特殊。
因为物理设备句柄实际由 vk::Instance
管理,所以 vk::raii::PhysicalDevice
销毁时没有调用任何 vkDestory
。
设备评估标准
1. 设备属性与特性查询
在 isDeviceSuitable
函数中,我们可以这样获取物理设备的属性:
vk::PhysicalDeviceProperties properties = physicalDevice.getProperties();
vk::PhysicalDeviceFeatures features = physicalDevice.getFeatures();
Properties 是 GPU 的基本属性,Features 则是 GPU 支持的可选功能。
Properties
: 基本设备属性,例如名称、类型和支持的 Vulkan 版本。Features
: 可选功能(如纹理压缩、64 位浮点数和多视口渲染)的支持。
后者尤为重要,因为不同显卡的功能集可能不同,比如某些手机 GPU 可能不支持网格着色器。 即使某个 GPU Feature 可用,我们也需要在后续逻辑设备创建时显式启用它们,否则 Vulkan 认为你不会用到这些功能。
假设我们的应用程序需要支持几何着色器的独立显卡。那么可以这样写
bool isDeviceSuitable(const vk::raii::PhysicalDevice& physicalDevice) const {
const auto properties = physicalDevice.getProperties();
const auto features = physicalDevice.getFeatures();
return features.geometryShader &&
properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu;
}
2. 评分机制示例
你还可以创建自己的评分机制,然后挑选最好的显卡,像这样:
int rateDeviceSuitability(const vk::raii::PhysicalDevice& physicalDevice) const {
const auto properties = physicalDevice.getProperties();
const auto features = physicalDevice.getFeatures();
// 必要性功能检查
if (!features.geometryShader) {
return 0;
}
int score = 0;
// 独立显卡加分
if (properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) {
score += 1000;
}
// 最大纹理尺寸影响图形质量加分
score += properties.limits.maxImageDimension2D;
return score;
}
注意!!
作为教程的开始部分,我们目前仅需要 Vulkan 支持,下面暂时使用这个最简单的判断函数:
bool isDeviceSuitable(const vk::raii::PhysicalDevice& physicalDevice) const {
return true;
}
在下一节,我们将讨论第一个真正需要检查的功能。
队列族管理
Vulkan 中的几乎每个操作,从绘制到上传纹理,都需要将命令提交到队列。
队列有不同的类型,这些类型源自不同的队列族,并且每个队列族仅允许一些特定的命令。 例如,可能有一个队列族仅允许处理计算命令,或者一个队列族仅允许与内存传输相关的命令。
更详细的概念:Vulkan-Guide [Queues]
1. 队列查找函数
我们需要添加一个新函数 findQueueFamilies
,用于查找我们需要的所有队列族。
现在我们只打算查找支持图形命令的队列族,因此该函数可能如下所示:
uint32_t findQueueFamilies(const vk::raii::PhysicalDevice& physicalDevice) const {
// 查找图像队列族
}
队列族的查找返回整数索引,由于我们后面需要找的队列不止一个,可以返回一个结构体:
struct QueueFamilyIndices {
uint32_t graphicsFamily;
};
QueueFamilyIndices findQueueFamilies(const vk::raii::PhysicalDevice& physicalDevice) const {
QueueFamilyIndices indices;
// 查找图像队列族
return indices;
}
2. 更好的队列存储
此函数可能找不到有用的队列族,但有时找不到也可以正常执行,比如我们希望使用具有专用传输队列族的设备,但不强制要求。
不应该使用魔术值来指示队列族的不存在,因为 uint32_t
的任何值都可能是有效的队列族索引,包括 0
。
所以这里使用 C++17 的 std::optional<>
数据结构来区分值存在与不存在的情况,它可以这样使用:
std::optional<uint32_t> graphicsFamily;
std::cout << graphicsFamily.has_value() << std::endl; // 0 (false)
graphicsFamily = 0;
std::cout << graphicsFamily.has_value() << std::endl; // 1 (true)
于是我们可以将代码修改成这样:
......
#include <optional>
......
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
};
QueueFamilyIndices findQueueFamilies(const vk::raii::PhysicalDevice& physicalDevice) const {
QueueFamilyIndices indices;
// Assign index to queue families that could be found
return indices;
}
3. 实现队列族查找
我们现在可以开始实现 findQueueFamilies
,使用 getQueueFamilyProperties
成员函数即可:
// std::vector<vk::QueueFamilyProperties>
const auto queueFamilies = physicalDevice.getQueueFamilyProperties();
vk::QueueFamilyProperties
只包含基本信息,包括支持的操作类型以及该族可创建的队列数量,但在这里已经足够了。
我们需要找到至少一个支持 vk::QueueFlagBits::eGraphics
的队列族(即图形队列族)。
for (int i = 0; const auto& queueFamily : queueFamilies) {
if (queueFamily.queueFlags & vk::QueueFlagBits::eGraphics) {
indices.graphicsFamily = i;
}
++i;
}
这里用到了C++20的初始化语句。
4. 改进设备适用性检查
现在可以在 isDeviceSuitable
函数中使用它作为检查,以确保设备具有我们需要的队列族:
bool isDeviceSuitable(const vk::raii::PhysicalDevice& physicalDevice) const {
const auto indices = findQueueFamilies(physicalDevice);
return indices.graphicsFamily.has_value();
}
为了更方便一点,我们可以在结构体中添加一个通用检查:
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
bool isComplete() const {
return graphicsFamily.has_value();
}
};
......
bool isDeviceSuitable(const vk::raii::PhysicalDevice& physicalDevice) const {
const auto indices = findQueueFamilies(physicalDevice);
return indices.isComplete();
}
我们现在可以使用它,在需要的时候从 findQueueFamilies
中提前退出:
for (int i = 0; const auto& queueFamily : queueFamilies) {
...
if (indices.isComplete()) break;
++i;
}
为什么不直接赋值后就退出?因为我们后面还需查找其他队列族。
测试
现在构建与运行代码,虽然程序还是和之前一样的效果,但不应报错。