[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

相关推荐
三万棵雪松2 小时前
【AI小智硬件程序(九)】
c++·人工智能·嵌入式·esp32·ai小智
HABuo2 小时前
【linux进程控制(一)】进程创建&退出-->fork&退出码详谈
linux·运维·服务器·c语言·c++·ubuntu·centos
半壶清水2 小时前
如何在IDEA中将JavaFX项目打包EXE文件
java·windows·intellij-idea·jar
想唱rap2 小时前
MySQL内置函数
linux·运维·服务器·数据库·c++·mysql
_oP_i2 小时前
Windows 下往 Elasticsearch 添加数据
大数据·windows·elasticsearch
玖釉-2 小时前
[Vulkan 学习之路] 10 - 掌握 SPIR-V:编写你的第一个着色器 (Shader Modules)
c++·windows·图形渲染
xiaoye-duck2 小时前
吃透C++类和对象(中):详解 Date 类的设计与实现
c++
玖釉-2 小时前
[Vulkan 学习之路] 03 - 你的守护天使:校验层 (Validation Layers)
c++·windows·图形渲染
冰暮流星2 小时前
c语言如何实现字符串复制替换
c语言·c++·算法