本节摘要:Vulkan 的世界由一组层级严格的核心对象构成:Instance 管全局、PhysicalDevice 是只读档案、Device 是执行账户、Queue 是办事窗口、CommandBuffer 是工单、Buffer 与 Image 是资产、各类同步原语是预约凭证。本节建立全册沿用的对象对照表与所有权层级,并用一次完整的提交流程把这些对象串起来。
Vulkan 的抽象模型可以这样定义:它是一套按所有权分层的对象系统,外层对象创建并约束内层对象,任何跨层越权都会被退回。本节给出的对照表是全册的「术语总账」——第 2 章的初始化、第 3 章的资源、第 5 章的命令、第 6 章的同步,都是往这张表里添条目。
按用途给核心对象分类,比按字母序死记有效得多。下表左列是对象,右列回答它在办事大厅里的角色。
| 对象 | 类型 | 大厅里的角色 | 生命周期归属 |
|---|---|---|---|
| VkInstance | 全局入口 | 大厅总入口,登记应用信息、启用全局扩展 | 进程级,最后销毁 |
| VkPhysicalDevice | 只读档案 | 受理网点名录:每块 GPU 一份能力档案 | 由 Instance 派生,不可销毁 |
| VkDevice | 执行账户 | 你的办事账户:声明用哪些能力、开哪些窗口 | 由 PhysicalDevice 创建 |
| VkQueue | 提交窗口 | 真正执行命令的柜台,按族分类 | 由 Device 派生,不可销毁 |
| VkCommandPool | 工单库 | 工单的印制与回收部门 | 显式创建销毁 |
| VkCommandBuffer | 工单 | 记录一串待执行命令的单据 | 由 Pool 分配 |
| VkBuffer / VkImage | 资产 | 显存里的数据资产(先领证后挂靠内存) | 显式创建销毁 |
| VkDeviceMemory | 库房 | 真实的显存块,资产必须挂靠其上 | 显式分配释放 |
| VkPipeline | 工艺执照 | 固化好的处理流程,创建昂贵、运行便宜 | 显式创建销毁 |
| VkFence | 完成回执 | CPU 可以等待的完成凭证 | 显式创建销毁 |
| VkSemaphore | 队列间凭证 | 队列之间的接力棒,CPU 不参与 | 显式创建销毁 |
| VkRenderPass / VkFramebuffer | 版式与相纸 | 附件的组织方式与具体图像绑定 | 显式创建销毁 |
这张表还隐含了三条铁律。第一条:档案只读——对 PhysicalDevice 只能查询,不能执行任何操作;第二条:资源不越级——Device 创建的东西不能拿到另一个 Device 上用;第三条:回执必须领——每个 vkCreate 调用都返回 VkResult,不检查它等于不看申请是否被受理。
把上表纵向看,就是一条从粗到细的所有权链。下图标注了每个节点的角色与去向,虚线是查询关系,实线是创建关系。

所有权的实际含义是「谁创建、谁销毁、谁能用」。Instance 销毁前必须先销毁它名下的一切;Device 创建的 Buffer 不能传给另一块 GPU 的 Device;Queue 从 Device 免费领取却不需要你销毁——因为它不是你申请来的,是账户自带的柜台。
概念只有动起来才好记。下面这段代码是把上图对象串成一次真实提交的骨架(错误处理从略,第 2 章会补全每一步):
// 链条起点:凭 Instance 枚举所有物理设备档案 uint32_t devCount = 0; vkEnumeratePhysicalDevices(instance, &devCount, NULL); std::vector<VkPhysicalDevice> physDevs(devCount); vkEnumeratePhysicalDevices(instance, &devCount, physDevs.data()); // 选定档案后开户:声明要用的队列族与扩展 VkDeviceQueueCreateInfo qInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; qInfo.queueFamilyIndex = 0; // 图形队列族,编号来自查询 qInfo.queueCount = 1; float prio = 1.0f; qInfo.pQueuePriorities = &prio; VkDeviceCreateInfo devInfo = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; devInfo.queueCreateInfoCount = 1; devInfo.pQueueCreateInfos = &qInfo; VkDevice device; vkCreateDevice(physDevs[0], &devInfo, NULL, &device); // 从账户领取窗口,再领工单并开始记录 VkQueue queue; vkGetDeviceQueue(device, 0, 0, &queue); VkCommandBuffer cmd; // (省略 Pool 创建与分配参数,第 5 章展开) vkBeginCommandBuffer(cmd, &beginInfo); vkCmdDraw(cmd, 3, 1, 0, 0); // 记录:画 3 个顶点 vkEndCommandBuffer(cmd); // 递交工单到窗口,同时领取完成回执 VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; vkQueueSubmit(queue, 1, &submit, fence); // fence 是 VkFence vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX); // CPU 等回执
读这段代码时注意两个细节。vkGetDeviceQueue 没有CreateInfo也没有销毁函数——Queue 是 Device 的固有部件,领出来就用。vkQueueSubmit 的最后一个参数是 VkFence,相当于递交工单时顺便预约了一张「办完通知单」;不预约的话,CPU 就没有任何合法途径知道 GPU 干完了没有。
背景:一位初学者照教程写完初始化,程序在 vkCreateDevice 处返回 VK_ERROR_INITIALIZATION_FAILED,验证层只给了一句「requested queue family out of range」。
操作:排错顺序沿对象链从上往下。先查 vkEnumeratePhysicalDevices——返回了设备,档案在。再查 vkGetPhysicalDeviceQueueFamilyProperties——输出显示队列族的数量为若干,而他硬编码的 queueFamilyIndex 是随便抄的某个数,超出了该设备的族总数。
结果:把族编号改成从查询结果里筛出的「支持 GRAPHICS 位」的那个族,创建通过。顺带他把「族编号写死」的三处调用统一改成从查询函数取值。
解读:Vulkan 的每个数值参数背后都有一个「必须先查询」的对应关系——族编号对应 QueueFamilyProperties,内存类型编号对应 MemoryProperties,格式支持对应 FormatProperties。报错信息表面在说参数越界,实质在说你跳过了查询手续。
变式:这类错误的通用排查法是「沿链回溯」:报错发生在创建账户,就回查它的输入全部来自哪些更上游的查询;上游查询的每个数组下标,都要确认不是凭空写死的。第 8 章的验证层会把这类问题的定位时间从小时缩短到分钟。
有两个容易混淆的边界值得单独说。其一,PhysicalDevice 与 Device 的分立不是多余:同一块 GPU 可以开多个用途不同的账户(比如一个开全套调试、一个求性能),档案与账户分离才办得到。其二,Queue 与 CommandBuffer 的分立也不是多余:工单在哪个窗口递交,决定它用哪块硬件,而「做什么」完全记录在工单里——「做什么」与「在哪做」被刻意解耦,这个解耦是多线程渲染的地基,第 6 章会反复用到。