资讯动态

Vulkan物理设备与队列族详解:从枚举到逻辑设备创建实践

发布时间:2026/10/2 15:01:44 来源:尧图企业网站定制
我第一次按Vulkan教程走到“Physical devices and queue families”这一小节时心里挺疑惑的明明只是画个三角形为什么拿到 VkInstance 之后不直奔管线反而先花大篇幅讨论显卡枚举和队列族后来把这一章啃完才明白Vulkan 和 OpenGL 最大的区别就藏在这里——它拒绝替你决定任何事包括用哪块 GPU、走哪条队列。这篇文章就围绕这个 setup 阶段展开我会先讲为什么 Vulkan 要把“选显卡”这件事交给你再拆解物理设备枚举、队列族查询、设备评分这三个核心环节最后给一份完整的验证代码和常见坑。卡在 vkEnumeratePhysicalDevices 或者对 queueFamilyIndex 一脸懵的人看完应该能把这段彻底跑通。1. 从“一行代码拿显卡”到“亲手挑GPU”Vulkan为什么这么折腾1.1 OpenGL 时代显卡离你有多远用过 OpenGL 的人都知道想知道当前跑在哪块显卡上一行代码就出来了const GLubyte* renderer glGetString(GL_RENDERER);驱动会替你把 CPU 端的渲染命令绑到最合适的GPU上你根本不用管是哪个设备在干活。在台式机上这通常没问题但在双显卡笔记本上就开始微妙了——OpenGL 上下文默认创建在哪块卡上很多时候是驱动的内部策略你只能被动接受。D3D11 的情况类似CreateDevice 传一个适配器指针传 nullptr 就能让系统自动挑一个。这对开发效率是好事但对诊断问题来说是个黑盒你无法确定渲染到底走了哪条硬件路径也无法精确控制多 GPU 场景下的设备选择。1.2 Vulkan 的核心理念把决定权交还给程序Vulkan 从设计上就反着来。它的核心思路是驱动不再替你“包办”而是把硬件能力完整暴露给你由你根据当前任务抉择。所以你创建完 VkInstance 之后第一个真正面对硬件的操作就是枚举物理设备Physical Device——也就是把机器上所有支持 Vulkan 的 GPU 全部列出来自己挑一张用。这套设计的好处有两点。第一是可预测性你选的设备是确定的跑的驱动路径是确定的出问题能复现第二是适配性你可以同时接多张卡让画面渲染走独立显卡、物理计算走另一张、数据上传再走第三条队列把硬件资源拆开用。代价就是学习曲线陡峭。画三角形这个“Hello World”在 Vulkan 里就必须先过“选卡 选队列族”这道关。别嫌麻烦这一章真正决定了后面所有命令是怎么提交到 GPU 的值得认真对待。2. 枚举物理设备第一次真正“摸到”你的GPU2.1 vkEnumeratePhysicalDevices 的两阶段调用拿到 VkInstance 之后第一步是调用vkEnumeratePhysicalDevices。它的签名大概长这样VkResult vkEnumeratePhysicalDevices( VkInstance instance, uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices );注意 Vulkan 里几乎所有“枚举”类函数都长一个样第一次传空指针拿数量分配好数组后再调用一次拿数据。这是 Vulkan 的经典两阶段模式后面枚举队列族、枚举扩展名全都一个套路。uint32_t deviceCount 0; vkEnumeratePhysicalDevices(instance, deviceCount, nullptr); std::vectorVkPhysicalDevice devices(deviceCount); vkEnumeratePhysicalDevices(instance, deviceCount, devices.data());有个细节值得提第一次调用之后设备数量理论上可能变化比如别的程序动态挂了新的GPU。所以严谨的做法是第二次调用结束后检查返回值是不是VK_SUCCESS如果返回VK_INCOMPLETE说明数组太小、数据被截断了。实际开发中这种概率极低但你要排查诡异问题时知道有这件事。比较常见的坑反而是 deviceCount 为 0。渲染用的电脑一般不会这样但如果你在虚拟机里跑 Vulkan宿主机没把 GPU 直通给虚拟机或者驱动没装好vkEnumeratePhysicalDevices就可能返回设备数为 0。这时候别急着往下写先检查驱动。2.2 VkPhysicalDeviceProperties把句柄翻译成信息VkPhysicalDevice 本身只是一个不透明句柄你没法从里面直接读出显卡名字。要拿到设备信息得调用vkGetPhysicalDevicePropertiesVkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(device, props); std::cout 设备名称: props.deviceName std::endl; std::cout 设备类型: props.deviceType std::endl; std::cout 驱动版本: props.driverVersion std::endl; std::cout API 版本: VK_VERSION_MAJOR(props.apiVersion) . VK_VERSION_MINOR(props.apiVersion) . VK_VERSION_PATCH(props.apiVersion) std::endl;deviceType是枚举值比较常用的几个取值含义典型场景VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU集成显卡Intel UHD、AMD APUVK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU独立显卡NVIDIA、AMD 独显VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU虚拟显卡云主机 / 虚拟机VK_PHYSICAL_DEVICE_TYPE_CPUCPU 模拟渲染软件渲染器如 SwiftShader这一章的用途是让你清楚自己手里有几张卡、每张卡什么型号。真正决定“选哪张卡”的逻辑还要参考设备特性VkPhysicalDeviceFeatures和队列族。下一节就进入队列族。3. 队列族GPU内部的“专用通道”才是真正干活的地方3.1 队列、队列族与硬件调度GPU 里有硬件队列Queue命令缓冲Command Buffer提交之后就是进到队列里被执行的。但队列不是一堆一视同仁的槽位而是被硬件分成了若干“族”Queue Family。同一个族里的队列能力相同不同族之间能力可能完全不同。我常用的类比是工厂车间里的几条产线有的产线专门做图形渲染Graphics有的专门做通用计算Compute有的专门做内存拷贝Transfer。你手里有的指令得挑对产线否则根本跑不了。画三角形需要的是图形指令所以在队列族里要找带VK_QUEUE_GRAPHICS_BIT标志的那个族。以后你如果做 GPU 计算、做数据上传还得回头找计算族和传输族——这也是为什么教程在 setup 阶段就反复强调队列族因为后面每一步都要用到queueFamilyIndex这个索引。3.2 用代码查出显卡有哪些队列族查询函数同样是两阶段写法uint32_t queueFamilyCount 0; vkGetPhysicalDeviceQueueFamilyProperties(device, queueFamilyCount, nullptr); std::vectorVkQueueFamilyProperties queueFamilies(queueFamilyCount); vkGetPhysicalDeviceQueueFamilyProperties(device, queueFamilyCount, queueFamilies.data());VkQueueFamilyProperties 结构体的核心字段是这几个queueFlags能力位常见组合是VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BITqueueCount这个族里有几条队列创建逻辑设备时可以申请的数量不能超过它timestampValidBits时间戳查询精度位数相关功能不常用先不用管minImageTransferGranularity传输操作的最小粒度涉及图像拷贝时才有意义拿到之后筛选支持图形能力的族并记下来它对应的索引int graphicsFamily -1; for (uint32_t i 0; i queueFamilies.size(); i) { if (queueFamilies[i].queueFlags VK_QUEUE_GRAPHICS_BIT) { graphicsFamily static_castint(i); break; } } if (graphicsFamily -1) { throw std::runtime_error(没有找到支持图形渲染的队列族); }这里用的是位与不是。因为一个队列族经常同时支持多种能力你要是写queueFlags VK_QUEUE_GRAPHICS_BIT碰到“图形传输”双标志的族就直接漏判了。我见过有人在这个地方排查半天最后发现就是这么个符号的事。4. 设备评分在双显卡机器上自动选出最合适的物理设备4.1 无脑选第一个的隐性风险如果你机器上只有一张显卡那选第一个设备没问题。但双显卡机器越来越多——笔记本基本是核显独显的组合台式机也可能插了多张卡。Vulkan 枚举设备的顺序由驱动决定你无法保证第一个就是独立显卡。在某些机器上vkEnumeratePhysicalDevices返回的第一张往往是核显。如果程序直接选第一个那么后面创建的交换链、渲染管线全都跑在核显上性能会非常难看而你以为自己用的独显。所以真正的项目里必须写一套选择逻辑不能偷懒。还有一层即使只有一张卡也可能遇到虚拟显卡比如某些云桌面环境。这种设备能不能满足画三角形的需求是另一回事评分时也得考虑进去。4.2 从“简单判断”到“评分函数”最朴素的策略是优先选独立显卡没有独立显卡才接受集成显卡。直接这样写VkPhysicalDevice pickPhysicalDevice(const std::vectorVkPhysicalDevice devices) { for (const auto device : devices) { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(device, props); if (props.deviceType VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { bool graphicsSupported false; // 同时确认队列族支持 graphics略 return device; } } return devices.front(); }这种做法够用但扩展性一般。你以后要是想根据显存大小、纹理最大尺寸、几何着色器支持等条件做综合权衡就得用评分函数。我给一个稍微成熟的版本int rateDevice(VkPhysicalDevice device) { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(device, props); VkPhysicalDeviceFeatures features; vkGetPhysicalDeviceFeatures(device, features); int score 0; if (props.deviceType VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { score 1000; } else if (props.deviceType VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) { score 100; } // 纹理尺寸越大通常卡越强用于打破同分情况 score static_castint(props.limits.maxImageDimension2D / 100); // 如果后续要用几何着色器这里可以直接淘汰不支持的特性的设备 if (!features.geometryShader) { // 按需处理不是所有程序都需要 } return score; }选卡时遍历所有设备取分数最高者VkPhysicalDevice bestDevice VK_NULL_HANDLE; int bestScore 0; for (const auto device : devices) { int score rateDevice(device); if (score bestScore) { bestDevice device; bestScore score; } } if (bestDevice VK_NULL_HANDLE) { throw std::runtime_error(没有找到可用的物理设备); }这里我没加“必须高于某个阈值”的判断因为画三角形的需求太低任何能枚举出来的卡基本都满足。阈值判断通常留到设备特性检查比如你必须用几何着色器而某张卡不支持那就可以直接把这个设备排除掉。5. 从物理设备到队列创建逻辑设备前的关键衔接5.1 VkDeviceQueueCreateInfo把索引翻译成可用队列物理设备和队列族都选好之后下一步要创建逻辑设备VkDevice。逻辑设备才是你真正用来提交命令、创建管线的对象它跟物理设备的关系可以理解成“进程里的句柄”和“底层硬件资源”的关系。创建逻辑设备时必须告诉 Vulkan 你要创建队列以及队列属于哪个队列族。这里用 VkDeviceQueueCreateInfoVkDeviceQueueCreateInfo queueInfo{}; queueInfo.sType VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueInfo.queueFamilyIndex graphicsFamily; queueInfo.queueCount 1; float queuePriority 1.0f; queueInfo.pQueuePriorities queuePriority;queuePriority 是 0.0 到 1.0 的浮点表示同族队列之间的相对调度优先级。我们只申请一条队列全部填 1.0f 就行。如果你申请多条队列pQueuePriorities 数组的长度必须等于 queueCount否则行为未定义。然后填设备创建信息VkDeviceCreateInfo deviceInfo{}; deviceInfo.sType VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceInfo.queueCreateInfoCount 1; deviceInfo.pQueueCreateInfos queueInfo; // 需要交换链扩展时在这里补 ppEnabledExtensionNames VkDevice device; if (vkCreateDevice(physicalDevice, deviceInfo, nullptr, device) ! VK_SUCCESS) { throw std::runtime_error(创建逻辑设备失败); }创建成功之后用vkGetDeviceQueue把队列句柄取出来VkQueue queue; vkGetDeviceQueue(device, graphicsFamily, 0, queue);后两个参数分别是队列族索引和队列索引。因为我们在 queueInfo 里只创建了一条队列队列索引就是 0。5.2 一份可以直接跑通的验证程序前面把 API 一个个拆开讲了这里我把完整的验证代码放在一起。它做的事情是创建 VkInstance → 枚举物理设备 → 打印设备信息 → 查询队列族 → 选出独立显卡 → 创建逻辑设备并取图形队列。不需要窗口不需要交换链纯控制台就能跑。#include vulkan/vulkan.h #include iostream #include vector #include stdexcept static int rateDevice(VkPhysicalDevice device) { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(device, props); int score 0; if (props.deviceType VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { score 1000; } else if (props.deviceType VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) { score 100; } score static_castint(props.limits.maxImageDimension2D / 100); return score; } static int findGraphicsFamily(VkPhysicalDevice device) { uint32_t count 0; vkGetPhysicalDeviceQueueFamilyProperties(device, count, nullptr); std::vectorVkQueueFamilyProperties families(count); vkGetPhysicalDeviceQueueFamilyProperties(device, count, families.data()); for (uint32_t i 0; i count; i) { if (families[i].queueFlags VK_QUEUE_GRAPHICS_BIT) { return static_castint(i); } } return -1; } int main() { VkApplicationInfo appInfo{}; appInfo.sType VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName device-setup-demo; 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 instInfo{}; instInfo.sType VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instInfo.pApplicationInfo appInfo; VkInstance instance; if (vkCreateInstance(instInfo, nullptr, instance) ! VK_SUCCESS) { throw std::runtime_error(创建 VkInstance 失败); } uint32_t deviceCount 0; vkEnumeratePhysicalDevices(instance, deviceCount, nullptr); if (deviceCount 0) { throw std::runtime_error(没有找到任何 Vulkan 物理设备); } std::vectorVkPhysicalDevice devices(deviceCount); vkEnumeratePhysicalDevices(instance, deviceCount, devices.data()); VkPhysicalDevice best VK_NULL_HANDLE; int bestScore 0; for (const auto device : devices) { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(device, props); std::cout 发现设备: props.deviceName std::endl; std::cout 类型: props.deviceType std::endl; int score rateDevice(device); std::cout 评分: score std::endl; if (score bestScore) { bestScore score; best device; } } int graphicsFamily findGraphicsFamily(best); if (graphicsFamily 0) { throw std::runtime_error(选定设备不支持图形队列); } std::cout 选定设备, 图形队列族索引: graphicsFamily std::endl; VkDeviceQueueCreateInfo queueInfo{}; queueInfo.sType VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueInfo.queueFamilyIndex graphicsFamily; queueInfo.queueCount 1; float priority 1.0f; queueInfo.pQueuePriorities priority; VkDeviceCreateInfo deviceInfo{}; deviceInfo.sType VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceInfo.queueCreateInfoCount 1; deviceInfo.pQueueCreateInfos queueInfo; VkDevice device; if (vkCreateDevice(best, deviceInfo, nullptr, device) ! VK_SUCCESS) { throw std::runtime_error(创建逻辑设备失败); } VkQueue queue; vkGetDeviceQueue(device, graphicsFamily, 0, queue); std::cout 逻辑设备和图形队列创建成功 std::endl; vkDestroyDevice(device, nullptr); vkDestroyInstance(instance, nullptr); return 0; }我机器上跑出来的输出大致是这样发现设备: NVIDIA GeForce RTX 2060 类型: 2 (DISCRETE_GPU) 评分: 1043 发现设备: Intel(R) UHD Graphics 630 类型: 1 (INTEGRATED_GPU) 评分: 141 选定设备, 图形队列族索引: 0 逻辑设备和图形队列创建成功注意 Intel 核显在这里也有 141 分但如果独立显卡在场它必然拿不到最高分。这套逻辑在大多数双显卡机器上能稳定选到正确设备。这里我刻意没有启用任何设备扩展比如 VK_KHR_swapchain因为那属于交换链章节的内容。后面真正要把三角形渲染到窗口时记得回来在VkDeviceCreateInfo的ppEnabledExtensionNames里加上交换链扩展否则vkCreateDevice会直接失败。6. 这一章最容易踩的坑6.1 双显卡笔记本选到了核显这是最常见的。如果你在笔记本上跑 Vulkan 程序发现帧率上不去先回去看控制台打印的设备名——很可能vkEnumeratePhysicalDevices返回的第一张卡就是核显。我前面给出的评分函数能避开这个问题但如果你还是图省事直接取devices[0]建议立刻改成评分制。另外注意Windows 上有些机器核显和独显的枚举顺序跟 BIOS 设置有关不是固定的换一台机器顺序可能就变了。还有一种情况是独显和核显都枚举到了但VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU判断正确最终选对了卡。这是理想情况。怕就怕驱动只暴露了核显的 Vulkan 支持独显没有 Vulkan 驱动——这时候设备列表里根本没有它评分函数也无能为力只能去装驱动。6.2 默认队列族0不一定支持Graphics我见过不止一个人默认“第一个队列族就是图形队列族”。也确实很多显卡的第一个队列族同时带 Graphics 和 Transfer 标志所以这么写碰巧能跑。但这不是 Vulkan 规范承诺的行为。某些混合架构的 GPU或者驱动版本不同第一个队列族可能只有 Transfer 或 Compute。你如果硬把图形命令提交到不支持 Graphics 的队列轻则报错重则设备丢失VK_ERROR_DEVICE_LOST。排查的时候把你枚举到的所有队列族 flags 都打出来逐个检查别用“第一个”代替“第一个支持图形的”。6.3 创建逻辑设备时的队列参数错误vkCreateDevice阶段有三个地方容易出错第一个queueFamilyIndex 填错。它必须来自之前查询队列族得到的真实索引不能拍脑袋写个 0。第二个queueCount 超过该队列族的 queueCount。某个族只有 2 条队列你非要申请 4 条驱动不会帮你扩容。创建逻辑设备时大概率会失败或行为未定义。第三个queuePriorities 数组长度对不上 queueCount。申请多条队列时这个数组必须有足够多的元素否则读取越界。这条也是初学者最容易忽略的。我自己调试时通常会把队列族查询结果完整打印出来确认数量再写死参数。宁可多打几行日志也别对着报错猜。6.4 后续接入Surface时的队列族不一致这一条严格来说是交换链章节的内容但我现在就想提醒窗口系统接入之后你不仅需要图形队列族还需要一个支持“呈现”Present的队列族也就是能够把渲染结果提交给窗口的队列。在绝大多数设备上支持 Graphics 的队列族同时支持 Present所以图形队列族可以直接复用。但也有例外——某些平台、某些驱动组合下图形队列族不支持 Present专门负责 Present 的是另一个队列族。如果你没提前查创建交换链时会卡在那里。查询方法是用vkGetPhysicalDeviceSurfaceSupportKHR(device, queueFamilyIndex, surface, supported)surface 是窗口系统关联出来的表面对象。画三角形教程里通常会在选设备阶段同时收集图形队列族索引和呈现队列族索引这就是为什么你会发现很多教程代码里有两个 index 变量。一旦出现图形和呈现在不同队列族的情况创建逻辑设备时就得在pQueueCreateInfos里同时传入两个 VkDeviceQueueCreateInfo后续提交命令和呈现图像各用各的队列。这里提前知道有这回事真遇到时就不慌。我实际操作中的体会是setup 阶段宁可多打印、多检查也别急着往下冲。设备枚举和队列族选择是后面所有 Vulkan 代码的地基这里选错了后面根本无从谈起。把本文的验证代码跑通再继续画三角形的下一步你会踏实很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑