3.4 描述符布局、池与集合


文档摘要

3.4 描述符布局、池与集合 本节摘要:描述符是着色器访问资源的点名册:布局声明册子格式,池是批量供料的仓库,集合是按册子填好的具体名单。本节讲清三层对象的创建与配对规则、更新的正确姿势,以及「每帧覆盖更新」与帧并发之间的时序陷阱。 为什么渲染命令里看不到任何纹理指针?因为指针在 Vulkan 里换了一种存在形式——描述符。着色器写的 sampler2D、uniform buffer,运行时都要靠描述符集(descriptor set)把它们与具体资源对上号。本节是第 3 章的柜台环节:资产(3.2)与库房(3.1)都备好了,现在办理把它们递交给着色器的手续。 一、三层对象,各管一摊 描述符系统的三层结构值得先立起来。

3.4 描述符布局、池与集合

本节摘要:描述符是着色器访问资源的点名册:布局声明册子格式,池是批量供料的仓库,集合是按册子填好的具体名单。本节讲清三层对象的创建与配对规则、更新的正确姿势,以及「每帧覆盖更新」与帧并发之间的时序陷阱。

为什么渲染命令里看不到任何纹理指针?因为指针在 Vulkan 里换了一种存在形式——描述符。着色器写的 sampler2D、uniform buffer,运行时都要靠描述符集(descriptor set)把它们与具体资源对上号。本节是第 3 章的柜台环节:资产(3.2)与库房(3.1)都备好了,现在办理把它们递交给着色器的手续。

一、三层对象,各管一摊

描述符系统的三层结构值得先立起来。VkDescriptorSetLayout 是册子模板:声明有哪些条目、各是什么类型、绑在哪个槽位、可见于哪些着色阶段。VkDescriptorPool 是供料仓库:创建时申报总量(多少个集合、每种类型多少条),之后从池里批量领取集合。VkDescriptorSet 是按模板填好的具体名单:每个槽位指向一个缓冲视图或图像视图(3.2 节的取景框在此派上用场)。

三层的约束关系是一条链:管线布局引用集合布局(第 4 章组装管线时),集合必须从支持对应类型的池里领取,集合的每个槽位写入与布局声明严丝合缝。验证层对这条链的核对极其严格——槽位号差一、类型不匹配、没写就绑,都会当场退件。

// 册子模板:一条 uniform 缓冲(顶点着色器可见)加一条组合图像采样器(片段可见) VkDescriptorSetLayoutBinding binds[2] = {}; binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo lci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; lci.bindingCount = 2; lci.pBindings = binds; VkDescriptorSetLayout layout; vkCreateDescriptorSetLayout(device, &lci, NULL, &layout); // 供料仓库:申报要撑起整个会话的量 VkDescriptorPoolSize sizes[2] = {}; sizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; sizes[0].descriptorCount = 1024; sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sizes[1].descriptorCount = 1024; VkDescriptorPoolCreateInfo pci = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; pci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; // 允许单册归还 pci.maxSets = 1024; pci.poolSizeCount = 2; pci.pPoolSizes = sizes; VkDescriptorPool pool; vkCreateDescriptorPool(device, &pci, NULL, &pool);

二、领册、填册、交册

集合从池里按布局领取,然后用一次写入调用填好所有槽位。填册的核心是 vkUpdateDescriptorSets,它按「写信息数组」批量执行:

VkDescriptorSetAllocateInfo ai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; ai.descriptorPool = pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &layout; VkDescriptorSet set; vkAllocateDescriptorSets(device, &ai, &set); VkDescriptorBufferInfo bufInfo = { perObjectUBO, 0, sizeof(ObjectParams) }; VkDescriptorImageInfo imgInfo = { sampler, texView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; VkWriteDescriptorSet writes[2] = {}; writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[0].dstSet = set; writes[0].dstBinding = 0; writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[0].descriptorCount = 1; writes[0].pBufferInfo = &bufInfo; writes[1] = writes[0]; writes[1].dstBinding = 1; writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[1].pImageInfo = &imgInfo; vkUpdateDescriptorSets(device, 2, writes, 0, NULL); // 交册:绑定到命令缓冲,槽位号与管线布局对齐 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &set, 0, NULL);

两个规则值得强调。其一,图像描述符必须携带布局:写入时的 imageLayout 要与资源实际状态一致(通常 SHADER_READ_ONLY_OPTIMAL),状态对不上是「采样到垃圾数据」类疑难案的常见根源。其二,更新与在途使用的关系:规范允许更新「未被待执行命令引用」的槽位;一个集合正被 GPU 使用时改写它,行为未定义——这个坑值得单独展开。

图 3-3:描述符三层结构与绑定链

图 3-3:描述符三层结构与绑定链

三、案例:每帧更新与帧并发的冲突

背景:按「每帧为每个对象写一次 uniform 描述符」实现后,画面上随机出现「A 物体顶着 B 物体的变换」串号现象,且越忙越频繁。

操作:排查更新时序后发现根因:CPU 已经跑到第 N 加二帧改写集合,而第 N 帧的渲染命令还在 GPU 队列里没执行——改写发生在「集合被待执行命令引用」期间,正是规范未定义区。改造方案按在途帧数分册:每个在途帧一个集合(2.5 节帧栅栏轮换的同一套框架),CPU 永远只写「对应帧的栅栏未触发就跳过」的那一册;对象数据本体也按帧分槽(3.1 的方案 B 直写内存同样分槽)。

结果:串号消失。在途帧数从二调到三时同步对象随之加套,框架无需再改。

解读:这是 Vulkan 与老 API 心智差异最大的地方之一:老 API 的常量更新是即时语义,Vulkan 的更新与执行是异步的两条时间线。「谁还在用这份名单」的答案不由直觉给,由栅栏给——这也预告了第 6 章的中心议题。

变式:对象数量大时「每对象一册」会让集合数爆炸,正确解法是把变换数据合并成一条大缓冲,用动态偏移(vkCmdBindDescriptorSets 的 pDynamicOffsets)在绑定层选槽,全场景共享一册。偏移的对齐要求来自设备的 minUniformBufferOffsetAlignment,照例查询取得。

四、要点回顾

  • 三层链条:布局声明格式、池管配额供给、集合装具体指向,管线布局把册子模板焊进管线。
  • 填册两规矩:图像描述符携带的布局要与资源状态一致;被待执行命令引用的槽位不可改写。
  • 在途分册:按帧栅栏轮换的集合套装是「每帧更新」的标准解,串号案的根源是两条时间线撞车。
  • 动态偏移省册:大数据缓冲加绑定时偏移,能把每对象一册压成全场景一册。
  • 池配额要预算:maxSets 与各类型 descriptorCount 是硬上限,按「在途帧数乘每帧用量」留余量申报。

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