[Vulkan 学习之路] 04 - 选妃环节:挑选物理设备与队列族

欢迎回来!上一集我们成功建立了与 Vulkan 驱动的"外交关系"(Instance)。今天,我们要进入实质性的阶段:挑选我们要用的显卡

在 OpenGL 中,你没得选,系统给你什么就是什么。但在 Vulkan 中,你可以遍历电脑上所有的 GPU(比如你的笔记本可能同时有 Intel 核显和 NVIDIA 独显),然后根据显存大小、特性支持甚至名字来"钦定"一个用来渲染。

这一步我们称之为选择 Physical Device (物理设备)

1. 物理设备 (VkPhysicalDevice)

首先,我们在 HelloVulkanApp 类中添加一个成员变量来存储我们要用的显卡句柄。

cpp 复制代码
// 物理显卡句柄(注意:这个对象会在 Instance 销毁时自动失效,不需要我们手动 vkDestroy)
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

挑选显卡的逻辑

我们需要编写一个 pickPhysicalDevice 函数。逻辑依然是标准的 Vulkan 流程:

  1. 问 Vulkan 有多少个显卡?

  2. 获取显卡列表。

  3. 遍历列表,找到一个符合我们要求的(比如支持图形渲染)。

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!");
    }
}

把这个函数加到 initVulkansetupDebugMessenger 的后面。

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 结构应该大致如下:

  1. 新增了 struct QueueFamilyIndices

  2. HelloVulkanApp 类中有了 pickPhysicalDeviceisDeviceSuitablefindQueueFamilies

  3. 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 (队列) 拿出来。

下一篇,我们将完成设备初始化的最后一步,真正的获得显卡的控制权!

详见:Physical devices and queue families - Vulkan Tutorial

相关推荐
一匹电信狗7 小时前
【LeetCode_547_990】并查集的应用——省份数量 + 等式方程的可满足性
c++·算法·leetcode·职场和发展·stl
Queenie_Charlie7 小时前
小陶的疑惑2
数据结构·c++·树状数组
Yvonne爱编码9 小时前
JAVA数据结构 DAY3-List接口
java·开发语言·windows·python
南宫码农9 小时前
我的电视 - Android原生电视直播软件 完整使用教程
android·开发语言·windows·电视盒子
Queenie_Charlie9 小时前
小陶与杠铃片
数据结构·c++·树状数组
CoderCodingNo9 小时前
【GESP】C++四级/五级练习题 luogu-P1223 排队接水
开发语言·c++·算法
sycmancia10 小时前
C++进阶01——示例
开发语言·c++
CoderCodingNo10 小时前
【GESP】C++五级/四级练习题 luogu-P1413 坚果保龄球
开发语言·c++·算法
阿猿收手吧!10 小时前
【C++】C++原子操作:compare_exchange_weak详解
java·jvm·c++
Trouvaille ~10 小时前
【Linux】网络编程基础(二):数据封装与网络传输流程
linux·运维·服务器·网络·c++·tcp/ip·通信