欢迎回来!上一集我们成功建立了与 Vulkan 驱动的"外交关系"(Instance)。今天,我们要进入实质性的阶段:挑选我们要用的显卡。
在 OpenGL 中,你没得选,系统给你什么就是什么。但在 Vulkan 中,你可以遍历电脑上所有的 GPU(比如你的笔记本可能同时有 Intel 核显和 NVIDIA 独显),然后根据显存大小、特性支持甚至名字来"钦定"一个用来渲染。
这一步我们称之为选择 Physical Device (物理设备)。
1. 物理设备 (VkPhysicalDevice)
首先,我们在 HelloVulkanApp 类中添加一个成员变量来存储我们要用的显卡句柄。
cpp
// 物理显卡句柄(注意:这个对象会在 Instance 销毁时自动失效,不需要我们手动 vkDestroy)
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
挑选显卡的逻辑
我们需要编写一个 pickPhysicalDevice 函数。逻辑依然是标准的 Vulkan 流程:
-
问 Vulkan 有多少个显卡?
-
获取显卡列表。
-
遍历列表,找到一个符合我们要求的(比如支持图形渲染)。
cpp
void pickPhysicalDevice() {
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());
// 遍历所有显卡,找一个能用的
for (const auto& device : devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
if (physicalDevice == VK_NULL_HANDLE) {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
把这个函数加到 initVulkan 中 setupDebugMessenger 的后面。
2. 核心难点:队列族 (Queue Families)
那么,什么叫"符合我们要求 (isDeviceSuitable)"的显卡?
这就要引出 Vulkan 的指挥系统了。在 Vulkan 里,所有的命令(比如"画个三角形"、"把图片存入显存")都不是直接执行的,而是提交给 队列 (Queue) 执行的。
但是,不同的队列有不同的特长。显卡内部就像一个大工厂,分成了不同的部门(队列族):
-
Graphics Queue Family: 专门负责画图的部门。
-
Compute Queue Family: 专门负责通用计算的部门。
-
Transfer Queue Family: 专门负责搬运数据的部门。
我们需要找的显卡,必须至少包含一个支持 Graphics(图形)操作的队列族。否则,这显卡只能拿来挖矿(计算),不能拿来玩游戏(画图)。
使用 std::optional 管理索引
因为队列族的索引是非负整数,为了区分"没找到"和"找到了索引为0的队列",我们引入 C++17 的神器 std::optional。
记得在文件头部添加:
cpp
#include <optional>
定义一个结构体来存放我们要找的队列族索引:
cpp
struct QueueFamilyIndices {
// std::optional 就像一个盒子,可能里面有值(int),也可能是空的
std::optional<uint32_t> graphicsFamily;
bool isComplete() {
return graphicsFamily.has_value();
}
};
3. 寻找队列族 (findQueueFamilies)
现在我们来写寻找队列族的逻辑。这需要调用 vkGetPhysicalDeviceQueueFamilyProperties。
cpp
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
QueueFamilyIndices indices;
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
int i = 0;
for (const auto& queueFamily : queueFamilies) {
// 检查这个队列族是否支持 Graphics 操作
if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
indices.graphicsFamily = i;
}
if (indices.isComplete()) {
break;
}
i++;
}
return indices;
}
4. 完善检查函数 (isDeviceSuitable)
回到之前的检查函数,现在我们可以利用刚才写的 findQueueFamilies 来判断显卡是否合格了。
cpp
bool isDeviceSuitable(VkPhysicalDevice device) {
// 1. 这一步是必须的:确保显卡有图形队列
QueueFamilyIndices indices = findQueueFamilies(device);
return indices.isComplete();
// 扩展知识:在这里你其实还可以查询显卡的名字、显存大小等
// VkPhysicalDeviceProperties deviceProperties;
// vkGetPhysicalDeviceProperties(device, &deviceProperties);
// std::cout << "Checking GPU: " << deviceProperties.deviceName << std::endl;
}
5. 运行测试
此时,你的 main.cpp 结构应该大致如下:
-
新增了
struct QueueFamilyIndices。 -
HelloVulkanApp类中有了pickPhysicalDevice、isDeviceSuitable和findQueueFamilies。 -
initVulkan顺序:Instance -> DebugMessenger -> pickPhysicalDevice。cpp#define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h> #include <iostream> #include <stdexcept> #include <vector> #include <cstring> #include <cstdlib> #include <optional> const uint32_t WIDTH = 800; const uint32_t HEIGHT = 600; const std::vector<const char*> validationLayers = { "VK_LAYER_KHRONOS_validation" }; #ifdef NDEBUG const bool enableValidationLayers = false; #else const bool enableValidationLayers = true; #endif VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); if (func != nullptr) { return func(instance, pCreateInfo, pAllocator, pDebugMessenger); } else { return VK_ERROR_EXTENSION_NOT_PRESENT; } } void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); if (func != nullptr) { func(instance, debugMessenger, pAllocator); } } struct QueueFamilyIndices { std::optional<uint32_t> graphicsFamily; bool isComplete() { return graphicsFamily.has_value(); } }; class HelloTriangleApplication { public: void run() { initWindow(); initVulkan(); mainLoop(); cleanup(); } private: GLFWwindow* window; VkInstance instance; VkDebugUtilsMessengerEXT debugMessenger; VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; void initWindow() { glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); } void initVulkan() { createInstance(); setupDebugMessenger(); pickPhysicalDevice(); } void mainLoop() { while (!glfwWindowShouldClose(window)) { glfwPollEvents(); } } void cleanup() { if (enableValidationLayers) { DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); } vkDestroyInstance(instance, nullptr); glfwDestroyWindow(window); glfwTerminate(); } void createInstance() { if (enableValidationLayers && !checkValidationLayerSupport()) { throw std::runtime_error("validation layers requested, but not available!"); } VkApplicationInfo appInfo{}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = "Hello Triangle"; appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.pEngineName = "No Engine"; appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.apiVersion = VK_API_VERSION_1_0; VkInstanceCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; auto extensions = getRequiredExtensions(); createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); createInfo.ppEnabledExtensionNames = extensions.data(); VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; if (enableValidationLayers) { createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); populateDebugMessengerCreateInfo(debugCreateInfo); createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo; } else { createInfo.enabledLayerCount = 0; createInfo.pNext = nullptr; } if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { throw std::runtime_error("failed to create instance!"); } } void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; createInfo.pfnUserCallback = debugCallback; } void setupDebugMessenger() { if (!enableValidationLayers) return; VkDebugUtilsMessengerCreateInfoEXT createInfo; populateDebugMessengerCreateInfo(createInfo); if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { throw std::runtime_error("failed to set up debug messenger!"); } } void pickPhysicalDevice() { 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()); for (const auto& device : devices) { if (isDeviceSuitable(device)) { physicalDevice = device; break; } } if (physicalDevice == VK_NULL_HANDLE) { throw std::runtime_error("failed to find a suitable GPU!"); } } bool isDeviceSuitable(VkPhysicalDevice device) { QueueFamilyIndices indices = findQueueFamilies(device); return indices.isComplete(); } QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { QueueFamilyIndices indices; uint32_t queueFamilyCount = 0; vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount); vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); int i = 0; for (const auto& queueFamily : queueFamilies) { if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { indices.graphicsFamily = i; } if (indices.isComplete()) { break; } i++; } return indices; } std::vector<const char*> getRequiredExtensions() { uint32_t glfwExtensionCount = 0; const char** glfwExtensions; glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); if (enableValidationLayers) { extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } return extensions; } bool checkValidationLayerSupport() { uint32_t layerCount; vkEnumerateInstanceLayerProperties(&layerCount, nullptr); std::vector<VkLayerProperties> availableLayers(layerCount); vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); for (const char* layerName : validationLayers) { bool layerFound = false; for (const auto& layerProperties : availableLayers) { if (strcmp(layerName, layerProperties.layerName) == 0) { layerFound = true; break; } } if (!layerFound) { return false; } } return true; } static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; return VK_FALSE; } }; int main() { HelloTriangleApplication app; try { app.run(); } catch (const std::exception& e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }
运行程序:
依然是黑窗口(别急,我们在搭地基)。但如果程序没有抛出 "failed to find a suitable GPU!" 异常,恭喜你!你的代码成功识别了你的显卡,并确认了它拥有绘图能力。

给 Windows 用户的特别提示:
如果你是双显卡笔记本(Intel 集显 + NVIDIA 独显),目前的逻辑 break 会导致它选中列表中第一个合格的显卡。通常驱动程序会把独显排在前面,或者是集显。
在本教程的简单场景下,集显和独显都能跑。但如果你想强制选独显,可以在 isDeviceSuitable 里加一个判断:deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU。
下一步
选好了物理设备,我们还不能直接指挥它。我们需要基于这个物理设备创建一个 Logical Device (逻辑设备) ,并从队列族中把那个我们要用的 Queue (队列) 拿出来。
下一篇,我们将完成设备初始化的最后一步,真正的获得显卡的控制权!