1.2 关键术语与抽象模型


1.2 关键术语与抽象模型

本节摘要: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,不检查它等于不看申请是否被受理。

二、所有权层级与查询链

把上表纵向看,就是一条从粗到细的所有权链。下图标注了每个节点的角色与去向,虚线是查询关系,实线是创建关系。

图 1-1:核心对象所有权层级与角色标注

图 1-1:核心对象所有权层级与角色标注

所有权的实际含义是「谁创建、谁销毁、谁能用」。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 章会反复用到。

本节要点回顾

  • 对象总账:核心对象按「入口、档案、账户、窗口、工单、资产、执照、凭证」归位,每类有明确的生命周期归属。
  • 三条铁律:档案只读、资源不越级、回执必须领,违反任何一条都会在验证层现形。
  • 所有权链:Instance 到 PhysicalDevice 到 Device 到 Queue 与资源,创建关系是实线、查询关系是虚线,跨层越权非法。
  • 一次提交的链条:枚举、开户、领窗口、记工单、递工单、等回执,六步覆盖全册大部分章节的用武之地。
  • 查询先行:所有「编号类」参数都必须来自对应的查询接口,写死编号是初学者第一大错源。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U