第6章 着色器和管线
6.1 GLSL 概述
- 对 GLSL 的修改允许它用来产生 Vulkan 可用的 SPIR-V 着色器,这些修改记录在 GL_KHR_ vulkan_glsl 扩展的文档中
- 内置函数
any()
和all()
分别用来判断数组中是否有一个为 true 以及是否所有的元素都是 true
6.2 SPIR-V 概述
6.2.1 如何表示 SPIR-V
- 略
把 SPIR-V 传递给 Vulkan
- 创建着色器模块对象
VkResult vkCreateShaderModule ( VkDevice device, const VkShaderModuleCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkShaderModule* pShaderModule );
VkShaderModuleCreateInfo
typedef struct VkShaderModuleCreateInfo { //为 VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //为 0 VkShaderModuleCreateFlags flags; //SPIR-V 模块的大小 size_t codeSize; const uint32_t* pCode; } VkShaderModuleCreateInfo;
- 销毁并释放资源
void vkDestroyShaderModule ( VkDevice device, VkShaderModule shaderModule, const VkAllocationCallbacks* pAllocator );
6.3 管线
- 在 Vulkan 中有两种管线
- 计算
- 图形
6.3.1 计算管线
- 本地工作组和全局工作组都是三维的。
- 本地工作组的尺寸在计算着色器内部设置。在 GLSL 中,使用 layout 限定符
layout (local_size_x = 4, local_size_y = 5, local_size_z 6) in;
- 计算着色器的本地工作组的最大尺寸一般来说比较小,仅仅要求 x 和 y 维度上至少有 128 次 调用,z 维度上至少有 64 次调用
- 工作组的总“体积” (在 x、y 和 z 三个方向上的上限的乘积) 有额外的限制,仅仅要求至少有 128 次调用。尽管许多实现支持更高的上限,当想要超出最小值时,你需要总是查询这些上限
- 查询工作组的最大尺寸
vkGetPhysicalDeviceProperties()
VkPhysicalDeviceLimits
maxComputeWorkGroupSize
- 本地工作组里的最大调用次数
maxComputeWorkGroupInvocations
6.3.2 创建管线
- 创建一个或多个管线
VkResult vkCreateComputePipelines ( VkDevice device, //用来加速管线创建的一个对象的句柄 VkPipelineCache pipelineCache, uint32_t createInfoCount, //个新管线的参数信息 const VkComputePipelineCreateInfo* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines );
VkComputePipelineCreateInfo
typedef struct VkComputePipelineCreateInfo { //VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineCreateFlags flags; //包含着色器本身的信息 VkPipelineShaderStageCreateInfo stage; VkPipelineLayout layout; VkPipeline basePipelineHandle; int32_t basePipelineIndex; } VkComputePipelineCreateInfo;
VkPipelineShaderStageCreateInfo
typedef struct VkPipelineShaderStageCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineShaderStageCreateFlags flags; //管线创建的阶段 //VK_SHADER_STAGE_COMPUTE_BIT VkShaderStageFlagBits stage; //着色器模块的句柄 VkShaderModule module; //表示这个特别的管线的入口点 const char* pName; //包含特化一个着色器所需的信息 const VkSpecializationInfo* pSpecializationInfo; } VkPipelineShaderStageCreateInfo;
6.3.3 特化常量
- “特化”是指构建着色器时将一些常量编译进去
- Vulkan 实现会延迟管线代码的最终生成时间
- 直到调用
vkCreateComputePipelines()
函数
- 直到调用
- 这允许特化常量的值在着色器优化的最后通道中才考虑
- 特化常量的典型应用包括 (详细解释 Page 146)
- 通过分支产生特殊执行路径
- 通过 switch 语句产生的特殊情形
- 循环展开
- 常量折叠
- 运算符简化
- 常量可以被创建管线时传递的新值覆盖
typedef struct VkSpecializationInfo { //需要设置新值的特化常量的个数 uint32_t mapEntryCount; //表示特化常量 const VkSpecializationMapEntry* pMapEntries; //数据大小 size_t dataSize; //原生数据 const void* pData; } VkSpecializationInfo;
VkSpecializationMapEntry
typedef struct VkSpecializationMapEntry { //特化常量的 ID, 使用 constant_id 布局限定符来设置值 uint32_t constantID; //原生数据偏移量 uint32_t offset; 原生数据大小 size_t size; } VkSpecializationMapEntry;
- 销毁管线对象
void vkDestroyPipeline ( VkDevice device, VkPipeline pipeline, const VkAllocationCallbacks* pAllocator );
6.3.4 加速管线的创建
- 创建管线可能是应用程序开销最大的操作之一
- 管线缓存
VkResult vkCreatePipelineCache ( VkDevice device, const VkPipelineCacheCreateInfo* pCreateInfo, const VkAllocationCallbacks * pAllocator, VkPipelineCache* pPipelineCache );
VkPipelineCacheCreateInfo
typedef struct VkPipelineCacheCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO VkStructureType sType; //nullptr const void * pNext; //0 VkPipelineCacheCreateFlags flags; size_t initialDataSize; //存在程序上一次运行产生的数据的地址 const void * pInitialData; } VkPipelineCacheCreateInfo;
- 从缓存中取出数据
VkResult vkGetPipelineCacheData ( VkDevice device, VkPipelineCache pipelineCache, //内存区域的大小 size_t* pDataSize, //缓存数据的内存区域 void* pData );
- 可以调用
vkGetPipelineCacheData()
两次来存储所有的缓存数据 - 将管线缓存数据保存到文件中
- Page 149
- 缓存数据文件头的定义
- Page 149
- 合并两个缓存对象
- 在多个线程中创建管线时,特别有用
VkResult vkMergePipelineCaches ( VkDevice device, //目标缓存的句柄 VkPipelineCache dstCache, //待融合缓存的个数 uint32_t srcCacheCount, const VkPipelineCache* pSrcCaches );
- 销毁管线缓存对象
void vkDestroyPipelineCache ( VkDevice device, VkPipelineCache pipelineCache, const VkAllocationCallbacks* pAllocator );
6.3.5 绑定管线
- 把管线绑定到一个命令缓冲区
void vkCmdBindPipeline ( VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline );
- 每一个命令缓冲区上有两个管线绑定点
- 图形绑定点
- VK_PIPELINE_BIND_POINT_GRAPHICS
- 计算绑定点
- VK_PIPELINE_BIND_POINT_COMPUTE
- 图形绑定点
6.4 执行工作
- 使用计算管线分发全局工作组
void vkCmdDispatch ( VkCommandBuffer commandBuffer, uint32_t x, uint32_t y, uint32_t z );
- 间接分发
- 在工作组中分发的个数来自缓冲区对象
- 允许分发大小在命令缓冲区构建之后计算
- 使用一个缓冲区来间接分发,然后用主机重写缓冲区里的内容
void vkCmdDispatchIndirect ( VkCommandBuffer commandBuffer, //工作组存储在 3 个连续的 uint32_t 类型的变量 VkBuffer buffer, //偏移量 VkDeviceSize offset );
- 缓冲区中的参数实质上代表了一个结构体
typedef struct VkDispatchIndirectCommand { uint32_t x; uint32_t y; uint32_t z; } VkDispatchIndirectCommand;
6.5 在着色器中访问资源
- 应用程序中的着色器以两种方式来使用和产生数据
- 通过和固定功能的硬件进行交互
- 直接读取和写入资源
6.5.1 描述符集
- 描述符集是作为整体绑定到管线的资源的集合
- 可以同时将多个集合绑定到一个管线
- 每一个集合都有一个布局
- 布局描述了集合中资源的排列顺序和类型
- 两个拥有相同布局的集合被视为兼容的和可相互交换的
- 管线布局
- 可被管线访问的集合的集合组成的对象
- 管线通过参照这个管线布局对象来创建
- 对于描述符集兼容的两个管线布局,它们必须符合如下两点
- 使用相同的推送常量范围
- 按照相同顺序使用相同的描述符集布局 (或等同的布局)
- 符集布局和管线布局之间的关系
- Page 153
- 创建描述符集布局对象
VkResult vkCreateDescriptorSetLayout ( VkDevice device, const VkDescriptorSetLayoutCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorSetLayout* pSetLayout );
VkDescriptorSetLayoutCreateInfo
typedef struct VkDescriptorSetLayoutCreateInfo { //VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkDescriptorSetLayoutCreateFlags flags; uint32_t bindingCount; //把资源绑定到描述符集里的绑定点 const VkDescriptorSetLayoutBinding* pBindings; } VkDescriptorSetLayoutCreateInfo;
VkDescriptorSetLayoutBinding
typedef struct VkDescriptorSetLayoutBinding { //每个着色器可访问的资源都具有一个绑定序号 // uint32_t binding; //绑定点的描述符的类型 VkDescriptorType descriptorType; uint32_t descriptorCount; VkShaderStageFlags stageFlags; const VkSampler* pImmutableSamplers; } VkDescriptorSetLayoutBinding;
VkDescriptorType
- VK_DESCRIPTOR_TYPE_SAMPLER
- 采样器是一个可以用来在从图像读入数据时执行诸如过滤、采样坐标转换等操作的对象
- VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
- 被采样的图像是一种图像,可用来和采样器连接,为着色器提供过滤过的数据
- VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
- 图像-采样器联合对象是采样器与图像的一个配对
- 总是使用同一个采样器来对这个图像做采样,在一些架构上会更加高效
- VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
- 存储图像是不可以被采样器使用却可以写入的图像
- VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER
- uniform 纹素缓冲区是一种缓冲区,里面填充了同构格式化数据
- 不能被着色器写入
- 如果知道该缓冲区的内容是不变的,那么某些 Vulkan 实现可以优化对该缓冲区的访问
- uniform 纹素缓冲区是一种缓冲区,里面填充了同构格式化数据
- VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER
- 存储纹素缓冲区是一种包含格式化数据的缓冲区
- 与 uniform 纹素缓冲区非常像,但是可以写入该存储缓冲区
- VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER 和 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
- 数据是未格式化的,通过着色器里声明的结构体来描述
- VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC和VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC
- 包含了起始偏移量和大小,把描述符集绑定到管线时传入,而不是当把描述符绑定到集时传入
- 这允许单个集合中的单个缓冲区高频地更新
- VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT
- 输入附件是一种特殊类型的图像
- 它的内容是由图形管线里同一个图像上的早期操作所生成的
- VK_DESCRIPTOR_TYPE_SAMPLER
- 建议你不要创建稀疏填充的集合,因为这会浪费设备资源
- 把两个或多个描述符集打包成管线可以使用的形式
VkResult vkCreatePipelineLayout ( VkDevice device, const VkPipelineLayoutCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkPipelineLayout* pPipelineLayout );
VkPipelineLayoutCreateInfo
typedef struct VkPipelineLayoutCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_ LAYOUT_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineLayoutCreateFlags flags; //描述符集布局的数量 uint32_t setLayoutCount; const VkDescriptorSetLayout* pSetLayouts; //推送常量 uint32_t pushConstantRangeCount; //推送常量 const VkPushConstantRange* pPushConstantRanges; } VkPipelineLayoutCreateInfo;
- 一次可绑定的描述符集的最大数至少为 4
vkGetPhysicalDeviceProperties()
VkPhysicalDeviceLimits
maxBoundDescriptorSets
- 管线资源限制表格
- Page 158
- 如果两个管线布局对于前面几个集合使用相同(或者等同的)的集合布局,而对于后面的集合使用不相同的集合布局,那么认为这两个管线布局是部分兼容的
- 在两个部分兼容的管线之间切换时,无须重新绑定任何集合,直到管线共享布局的地方
- 如果你有一系列的资源并想在全局范围内访问
- 比如包含每帧所需常量的 uniform 块
- 或者在每个着色器都需要访问的纹理
- 就把这些资源放在第一个集合里
- 频繁改变的资源可以放到高序号的集合里
- 销毁管线布局
void vkDestroyPipelineLayout ( VkDevice device, VkPipelineLayout pipelineLayout, const VkAllocationCallbacks* pAllocator );
- 销毁描述符集布局对象
void vkDestroyDescriptorSetLayout ( VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const VkAllocationCallbacks* pAllocator );
6.5.2 绑定资源到描述符集
- 资源是通过描述符表示的,绑定到管线上的顺序是
- 先把描述符绑定到集合上
- 然后把描述符集绑定到管线
- 创建描述符缓存池
VkResult vkCreateDescriptorPool ( VkDevice device, const VkDescriptorPoolCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorPool* pDescriptorPool );
VkDescriptorPoolCreateInfo
typedef struct VkDescriptorPoolCreateInfo { //VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //唯一定义VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT //应用程序可以释放从池中获取的单个描述符 //如果不打算把单个描述符归还给缓存池,把 flags 设置为 0 即可 VkDescriptorPoolCreateFlags flags; //可从池中分配的集合数量的最大值 uint32_t maxSets; //可以存储在集合中的每种类型资源可用的描述符个数 uint32_t poolSizeCount; const VkDescriptorPoolSize* pPoolSizes; } VkDescriptorPoolCreateInfo;
VkDescriptorPoolSize
typedef struct VkDescriptorPoolSize { //资源的类型 VkDescriptorType type; //池中该种类型资源的个数 uint32_t descriptorCount; } VkDescriptorPoolSize;
- 创建描述符集对象
VkResult vkAllocateDescriptorSets ( VkDevice device, const VkDescriptorSetAllocateInfo* pAllocateInfo, VkDescriptorSet* pDescriptorSets );
VkDescriptorSetAllocateInfo
typedef struct VkDescriptorSetAllocateInfo { //VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO VkStructureType sType; //nullptr const void* pNext; //可以从中分配集合的描述符缓存池的句柄 //在外部保持同步 VkDescriptorPool descriptorPool; //创建的集合的个数 uint32_t descriptorSetCount; //每一个集合的布局 const VkDescriptorSetLayout* pSetLayouts; } VkDescriptorSetAllocateInfo;
- 释放一个或者多个描述符集
VkResult vkFreeDescriptorSets ( VkDevice device, VkDescriptorPool descriptorPool, uint32_t descriptorSetCount, const VkDescriptorSet* pDescriptorSets );
- 重置缓存池
VkResult vkResetDescriptorPool ( VkDevice device, VkDescriptorPool descriptorPool, //为 0 VkDescriptorPoolResetFlags flags );
- 销毁缓冲池对象
void vkDestroyDescriptorPool( VkDevice device, VkDescriptorPool descriptorPool, const VkAllocationCallbacks* pAllocator );
- 直接写入描述符集或者从另一个描述符集复制绑定,来把资源绑定到描述符集
void vkUpdateDescriptorSets ( VkDevice device, uint32_t descriptorWriteCount, const VkWriteDescriptorSet* pDescriptorWrites, //描述符复制的次数 uint32_t descriptorCopyCount, const VkCopyDescriptorSet* pDescriptorCopies );
VkWriteDescriptorSet
typedef struct VkWriteDescriptorSet { //VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET VkStructureType sType; //nullptr const void* pNext; //目标描述符集 VkDescriptorSet dstSet; //绑定索引 uint32_t dstBinding; //绑定资源类型的数组,更新起始的索引 uint32_t dstArrayElement; //需要更新的连续描述符个数 uint32_t descriptorCount; //更新的资源的类型 VkDescriptorType descriptorType; //图像资源 const VkDescriptorImageInfo* pImageInfo; //缓冲区资源 const VkDescriptorBufferInfo* pBufferInfo; const VkBufferView* pTexelBufferView; } VkWriteDescriptorSet;
VkDescriptorImageInfo
typedef struct VkDescriptorImageInfo { VkSampler sampler; //视图 VkImageView imageView; //所期望的布局 VkImageLayout imageLayout; } VkDescriptorImageInfo;
VkDescriptorBufferInfo
typedef struct VkDescriptorBufferInfo { VkBuffer buffer; VkDeviceSize offset; //如果是 uniform, 需要小于或者等于 maxUniformBufferRange VkDeviceSize range; } VkDescriptorBufferInfo;
maxUniformBufferRange
和maxStorageBufferRange
上限分别保证至少是 16384 与 2 27 2^{27} 227minUniformBufferOffsetAlignment
和minStorageBufferOffsetAlignment
要保证最多是 256 字节VkCopyDescriptorSet
typedef struct VkCopyDescriptorSet { //VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET VkStructureType sType; //nullptr const void* pNext; //源的描述符集的句柄 VkDescriptorSet srcSet; //绑定索引 uint32_t srcBinding; //描述符数组 uint32_t srcArrayElement; //源的描述符集的句柄 VkDescriptorSet dstSet; //绑定索引 uint32_t dstBinding; //描述符数组 uint32_t dstArrayElement; uint32_t descriptorCount; } VkCopyDescriptorSet;
6.5.3 绑定描述符集
- 把描述符集绑定到命令缓冲区
void vkCmdBindDescriptorSets ( VkCommandBuffer commandBuffer, //VK_PIPELINE_BIND_POINT_COMPUTE //or //VK_PIPELINE_BIND_POINT_GRAPHICS VkPipelineBindPoint pipelineBindPoint, //使用的管线布局 VkPipelineLayout layout, //管线布局可访问的集合的一个子集 uint32_t firstSet, uint32_t descriptorSetCount, const VkDescriptorSet* pDescriptorSets, //动态 uniform 或着色器存储绑定的偏移量 //动态偏移量的个数 uint32_t dynamicOffsetCount, //类型为 32 位偏移量的数组 const uint32_t* pDynamicOffsets );
6.5.4 uniform、纹素和存储缓冲区
- 着色器可以直接通过 3 种资源访问缓冲区内存的内容
- uniform 块提供了对存储在缓冲区对象中常量(只读)数据的快速访问
- 着色器块提供了对缓冲区对象的读写访问, 支持原子操作
- 纹素缓冲区提供了对存储格式化纹素数据的长线性数组的访问能力
uniform 和着色器块
- 默认情况下
- uniform 块使用 std140 规则
- 着色器块使用 std430 规则
layout (set = 0, binding = 1) uniform my_uniform_buffer_t { float foo; vec4 bar; int baz[42]; } my_uniform_buffer; layout (set = 0, binding = 2) buffer my_storage_buffer_t { int peas; float carrots; vec3 potatoes[99]; } my_storage_buffer;
纹素缓冲区
- 在数据读取时可进行格式转换
- 纹素缓冲区是只读的
layout (set = 0, binding = 3) uniform samplerBuffer my_float_texel_buffer; layout (set = 0, binding = 4) uniform isamplerBuffer my_signed_texel_buffer; layout (set = 0, binding = 5) uniform usamplerBuffer my_unsigned_texel_buffer;
- 中纹素缓冲区要求的最小上限是 65535 个元素
- 1D 纹素要求的最小尺寸是 4096 纹素
6.5.5 推送常量
- 不需要存储在内存里
- 由 Vulkan 自身持有和更新
- 的新值可以被直接从命令缓冲区推送到管线
- 推送常量
VkPipelineLayoutCreateInfo
VkPushConstantRange
typedef struct VkPushConstantRange { //能看到常量的管线阶段 VkShaderStageFlags stageFlags; uint32_t offset; uint32_t size; } VkPushConstantRange;
- 向多个着色阶段传递一个常量也许要求广播它,并会消耗很多资源
- GLSL 里声明推送常量
layout (push_constant) uniform my_push_constants_t { int bourbon; int scotch; int beer; } my_push_constants;
- 要更新一个或者多个推送常量
void vkCmdPushConstants ( VkCommandBuffer commandBuffer, VkPipelineLayout layout, //不更新没有包含的阶段来提升性能 VkShaderStageFlags stageFlags, //第一个常量在虚拟块内的偏移量 uint32_t offset, //更新的量的大小 uint32_t size, //数据 const void* pValues );
- 推送常量在逻辑上按照 std430 布局规则在内存中存储
- 推送常量总共可用的空间至少是 128 字节
- 对于两个 4×4 矩阵来说足够
- 把推送常量当作稀缺资源
- 优先使用正常的 uniform 块来存储大数据结构
- 将推送常量用作整数或者更新非常频繁的数据
6.5.6 采样图像
- 当着色器从图像中读数据时,它们可以使用两种方式
- 执行原始加载,从图像的指定位置直接读取格式化的或非格式化的数据
- 使用采样器对图像采样
- 采样可以包括如下操作
- 在图像坐标上做基础变换,
- 过滤纹素来向着色器返回光滑的图像数据
- 采样可以包括如下操作
- 创建采样器对象
VkResult vkCreateSampler ( VkDevice device, const VkSamplerCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSampler* pSampler );
VkSamplerCreateInfo
typedef struct VkSamplerCreateInfo { //VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkSamplerCreateFlags flags; //过滤模式 //VK_FILTER_NEAREST //VK_FILTER_LINEAR VkFilter magFilter; VkFilter minFilter; //多重细节层 //VK_SAMPLER_MIPMAP_MODE_NEAREST //VK_SAMPLER_MIPMAP_MODE_LINEAR VkSamplerMipmapMode mipmapMode; //纹理坐标的变换方式 //VK_SAMPLER_ADDRESS_MODE_REPEAT //VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT //VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE //VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER //VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE VkSamplerAddressMode addressModeU; VkSamplerAddressMode addressModeV; VkSamplerAddressMode addressModeW; //浮点型偏移量, 作用于 mipmap float mipLodBias; //各向异性过滤 //各向异性过滤通常在投影范围内做采样,而不是在固定的 2×2 范围内 VkBool32 anisotropyEnable; //范围是 1.0 到设备允许的最大值 //maxSamplerAnisotropy float maxAnisotropy; //比较模式 VkBool32 compareEnable; //ALWAYS //NEVER //LESS //LESS_OR_EQUAL //EQUAL //NOT_EQUAL //GREATER //GREATER_OR_EQUAL //Page 179 VkCompareOp compareOp; //在带有 mipmap 的图像中,采样器可配置成只在一个层级子集中采样 //被限制的 mipmap 范围通过 minLod 和 maxLod 指定 //要在整个 mipmap 链上做采样,设置 minLod 为 0.0,并设置 maxLod 为最高层 float minLod; float maxLod; // VkBorderColor borderColor; //当设置为 VK_TRUE 时,表示图像用于采样的坐标以原生纹素为单位,而不是 uv //使用限制 //minFilter 和 magFilter 必须是相同的 //mipmapMode 必须是 VK_SAMPLER_MIPMAP_MODE_NEAREST //anisotropyEnable 和 compareEnable 必须是 VK_FALSE VkBool32 unnormalizedCoordinates; } VkSamplerCreateInfo;
- 一个设备上可以创建的采样器个数的上限取决于 Vulkan 实现。保证的是至少有 4000 个
VkPhysicalDeviceLimits
maxSamplerAllocationCount
- 销毁采样器
void vkDestroySampler ( VkDevice device, VkSampler sampler, const VkAllocationCallbacks* pAllocator );
第7章 图形管线
7.1 逻辑图形管线
- 管线 (Page 181)
- 绘制
- 输入装配
- 顶点着色器
- 细分控制着色器
- 产生细分因子和其他逐图片元(patch)数 据(被固定功能细分引擎使用)
- 细分图元生成
- 固定功能阶段使用在细分控制着色器中产生的 细分因子,来把图片元分解成许多更小的、更简单的图元,以供细分评估着色器使用
- 细分评估着色器
- 这个着色阶段运行在细分图元生成器产生的每一个顶点上。它和顶点着 色器的操作类似——除了输入顶点是生成的之外,而非从内存读取的
- 几何着色器
- 图元组装
- 裁剪和剔除
- 光栅器
- 前置片段操作
- 包括深度和模板测试(当开启了这两个测试时)。
- 片段装配
- 片段装配阶段接受光栅器的输出,以及任何逐片段数 据,将这些信息作为一组,发送给片段着色阶段
- 片段着色器
- 后置片段操作
- 片段着色器会修改本应该在前置片段操作中使用的数据。 在这种情况下,这些前置片段操作转移到后置片段阶段中执行
- 颜色混合
- 颜色操作接受片段着色器和后置片段操作的结果,并使用它们更新帧缓冲区。 颜色操作包括混合与逻辑操作
- 绘制命令
void vkCmdDraw ( VkCommandBuffer commandBuffer, //附加到管线的顶点的数量 uint32_t vertexCount, //1, 或实例绘制物体 uint32_t instanceCount, //可以从非零个顶点或者实例开始绘制 uint32_t firstVertex, uint32_t firstInstance );
7.2 渲染通道
- 创建渲染通道对象
VkResult vkCreateRenderPass ( VkDevice device, const VkRenderPassCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkRenderPass* pRenderPass );
VkRenderPassCreateInfo
typedef struct VkRenderPassCreateInfo { //VK_STRUCTURE_TYPE_RENDERPASS_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkRenderPassCreateFlags flags; uint32_t attachmentCount; //组定义和渲染通道关联的多个附件 const VkAttachmentDescription* pAttachments; uint32_t subpassCount; const VkSubpassDescription* pSubpasses; uint32_t dependencyCount; //依赖信息 //当在一个渲染通道中有多个子通道时 //Vulkan 可推算出一个附件依赖了哪些附件 //这需要通过跟踪附件引用,并查找输入和输出完成 //当无法自动跟踪时,使用该字段定义依赖信息 const VkSubpassDependency* pDependencies; } VkRenderPassCreateInfo;
VkAttachmentDescription
typedef struct VkAttachmentDescription { //VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT 附件可能和同一个渲染通道引用的其他附件使用相同的内存 //这告诉 Vulkan 不要做任何可能导致附件的数据不一致的事情 //一般情况为 0 VkAttachmentDescriptionFlags flags; VkFormat format; //图像中采样的次数 //不使用多次采用:VK_SAMPLE_COUNT_1_BIT。 VkSampleCountFlagBits samples; //下面 4 个指定了在渲染通道的开始与结束时如何处理附件 //VK_ATTACHMENT_LOAD_OP_LOAD 表示附件里已经有数据了,仍想继续对它进行渲染 //VK_ATTACHMENT_LOAD_OP_CLEAR 在渲染通道开始时清除附件的内容 //VK_ATTACHMENT_LOAD_OP_DONT_CARE 渲染通道开始时不关心附件的内容 VkAttachmentLoadOp loadOp; //VK_ATTACHMENT_STORE_OP_STORE 让 Vulkan 保留附件的内容以供稍后使用 //VK_ATTACHMENT_STORE_OP_DONT_CARE 在渲染通道结束后不需要附件的内容 VkAttachmentStoreOp storeOp; VkAttachmentLoadOp stencilLoadOp; VkAttachmentStoreOp stencilStoreOp; //渲染通道开始期望图像是什么布局 VkImageLayout initialLayout; //在渲染通道结束时期望图像是什么布局 VkImageLayout finalLayout; } VkAttachmentDescription;
- 定义子通道
typedef struct VkSubpassDescription { //0 VkSubpassDescriptionFlags flags; //现阶段为 VK_PIPELINE_BIND_POINT_GRAPHICS VkPipelineBindPoint pipelineBindPoint; //个、输入附件,可以从中读出数据 uint32_t inputAttachmentCount; const VkAttachmentReference* pInputAttachments; uint32_t colorAttachmentCount; //颜色附件是写入输出数据的附件 const VkAttachmentReference* pColorAttachments; //解析附件是对多重采样图像数据进行解析后存储的附件 const VkAttachmentReference* pResolveAttachments; const VkAttachmentReference* pDepthStencilAttachment; uint32_t preserveAttachmentCount; //如果希望附件的生存周期跨越一个子通道但并不被子通道直接引用 //该引用将阻止 Vulkan 进行任何可能改动这些附件内容的优化 const uint32_t* pPreserveAttachments; } VkSubpassDescription;
VkAttachmentReference
typedef struct VkAttachmentReference { //附件数组的索引 uint32_t attachment; //图像布局 VkImageLayout layout; } VkAttachmentReference;
- 单个子通道可以渲染输出的颜色附件的最大个数
maxColorAttachments
- 保证最少支持 4 个
VkSubpassDependency
typedef struct VkSubpassDependency { //源子通道 uint32_t srcSubpass; //目标子通道 uint32_t dstSubpass; //指定了源子通道的哪些管线阶段产生数据 VkPipelineStageFlags srcStageMask; //指定了目标子通道的哪些管线阶段使用数据 VkPipelineStageFlags dstStageMask; //如何访问数据 VkAccessFlags srcAccessMask; VkAccessFlags dstAccessMask; VkDependencyFlags dependencyFlags; } VkSubpassDependency;
- 销毁渲染通道
void vkDestroyRenderPass ( VkDevice device, VkRenderPass renderPass, const VkAllocationCallbacks* pAllocator );
7.3 帧缓冲区
- 帧缓冲区影响管线的最后几个阶段
- 深度和模板测试
- 混合
- 逻辑操作
- 多采样
- 等
- 帧缓冲区对象通过使用渲染通道的引用来创建
- 可以和任何有类似的附件排布方式的渲染通道一同使用
- 创建
VkResult vkCreateFramebuffer ( VkDevice device, const VkFramebufferCreateInfo* pCreateInfo, //如果要求使用主机内存,就将会用到 pAllocator 所指向的分配器 const VkAllocationCallbacks* pAllocator, VkFramebuffer* pFramebuffer );
VkFramebufferCreateInfo
typedef struct VkFramebufferCreateInfo { //VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkFramebufferCreateFlags flags; VkRenderPass renderPass; //数组的长度 uint32_t attachmentCount; const VkImageView* pAttachments; //必须指定帧缓冲区的维度 uint32_t width; uint32_t height; uint32_t layers; } VkFramebufferCreateInfo;
- 支持的帧缓冲区最大尺寸依赖于设备
VkPhysicalDeviceLimits
maxFramebufferWidth
maxFramebufferHeight
maxFramebufferLayers
- vulkan 标准保证最小支持的宽度和高度是 4096 像素
- 层数至少为 256
- 大多数桌面级硬件支持 16384 像素的宽度和高度
- 2048 层
- 无附件帧缓冲区
- 存储图像
- 遮挡查询
- 无须在任何地方存储渲染结果
- 销毁帧缓冲
void vkDestroyFramebuffer ( VkDevice device, VkFramebuffer framebuffer, const VkAllocationCallbacks* pAllocator );
- 销毁帧缓冲区对象并不影响附着到它上面的图像
- 图像可以同时附着到多个帧缓冲区上
7.4 创建一个简单的图形管线
- 创建图形管线
VkResult vkCreateGraphicsPipelines ( VkDevice device, //管线缓存 VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkGraphicsPipelineCreateInfo* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines );
VkGraphicsPipelineCreateInfo
typedef struct VkGraphicsPipelineCreateInfo { //VK_GRAPHICS_PIPELINE_CREATE_INFO VkStructureType sType; //nullptr,可以使用扩展 const void* pNext; //管线如何使用的信息 VkPipelineCreateFlags flags; uint32_t stageCount; //把着色器传递到管线的目标位置 //描述了一个着色阶段 const VkPipelineShaderStageCreateInfo* pStages; //顶点输入状态 const VkPipelineVertexInputStateCreateInfo* pVertexInputState; //输入组装接受顶点数据 const VkPipelineInputAssemblyStateCreateInfo* pInputAssemblyState; //细分状态 const VkPipelineTessellationStateCreateInfo* pTessellationState; //视口状态 const VkPipelineViewportStateCreateInfo* pViewportState; //光栅化状态 const VkPipelineRasterizationStateCreateInfo* pRasterizationState; //多重采样状态 const VkPipelineMultisampleStateCreateInfo* pMultisampleState; //深度和模板状态 const VkPipelineDepthStencilStateCreateInfo* pDepthStencilState; //颜色混合状态 const VkPipelineColorBlendStateCreateInfo* pColorBlendState; //动态状态 const VkPipelineDynamicStateCreateInfo* pDynamicState; VkPipelineLayout layout; VkRenderPass renderPass; uint32_t subpass; VkPipeline basePipelineHandle; int32_t basePipelineIndex; } VkGraphicsPipelineCreateInfo;
VkPipelineCreateFlags
- VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT
- 管线将不会在性能严苛的程序中使用
- VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT 和 VK_PIPELINE_CREATE_DERIVATIVE_BIT
- 衍生管线
- 可把类似的管线分为一组, 告诉 Vulakn 你将在它们之间快速切换
- VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT
- 为这个新管线创建衍生管线
- VK_PIPELINE_CREATE_DERIVATIVE_ BIT
- 告诉 Vulkan 这个管线就是一个管线
- VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT
7.4.1 图形着色器阶段
VkPipelineShaderStageCreateInfo
typedef struct VkPipelineShaderStageCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineShaderStageCreateFlags flags; VkShaderStageFlagBits stage; VkShaderModule module; const char* pName; const VkSpecializationInfo* pSpecializationInfo; } VkPipelineShaderStageCreateInfo;
- 管线最多由 5 个着色器阶段组成
- 顶点着色器
- VK_SHADER_STAGE_VERTEX_BIT
- 细分控制着色器
- VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT
- 细分评估着色器
- VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT
- 几何着色器
- VK_SHADER_STAGE_GEOMETRY_BIT
- 片段着色器
- VK_SHADER_STAGE_FRAGMENT_BIT
- 顶点着色器
7.4.2 顶点输入状态
VertexInputStateCreateInfo
typedef struct VkPipelineVertexInputStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineVertexInputStateCreateFlags flags; //是管线使用的顶点绑定的个数 uint32_t vertexBindingDescriptionCount; const VkVertexInputBindingDescription* pVertexBindingDescriptions; uint32_t vertexAttributeDescriptionCount; //顶点属性都 const VkVertexInputAttributeDescription* pVertexAttributeDescriptions; } VkPipelineVertexInputStateCreateInfo;
VkVertexInputBindingDescription
typedef struct VkVertexInputBindingDescription { //结构体描述的绑定的索引 uint32_t binding; //数组的步长 uint32_t stride; //遍历数组的方式 //实例索引 VK_VERTEX_INPUT_RATE_VERTEX //顶点索引 VK_VERTEX_INPUT_RATE_INSTANCE。 VkVertexInputRate inputRate; } VkVertexInputBindingDescription;
VkVertexInputBindingDescription
数组声明的最后一个绑定索引必须比设备支持的最大绑定数要小- Vulkan 标准保证最少支持 16 个
VkPhysicalDeviceLimits
maxVertexInputBindings
- stride 的最大值都是由 Vulkan 实现决定的,
- Vulkan 标准保证至少是 2048 字节
VkPhysicalDeviceLimits
maxVertexInputBindingStride
VkVertexInputAttributeDescription
typedef struct VkVertexInputAttributeDescription { //属性的位置 uint32_t location; //绑定缓冲区的位置 uint32_t binding; //顶点数据的格式 VkFormat format; //每个数据的偏移量 uint32_t offset; } VkVertexInputAttributeDescription;
- 每一个属性在数据结构内的偏移量也有上限
- Vulkan 标准保证至少是 2047 字节
VkPhysicalDeviceLimits
maxVertexInput
7.4.3 输入组装
VkPipelineInputAssemblyStateCreateInfo
typedef struct VkPipelineInputAssemblyStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineInputAssemblyStateCreateFlags flags; //图元拓扑类型 VkPrimitiveTopology topology; //允许条带与扇形被切除和重启 VkBool32 primitiveRestartEnable; } VkPipelineInputAssemblyStateCreateInfo;
VkPrimitiveTopology
- VK_PRIMITIVE_TOPOLOGY_POINT_LIST
- 每一个顶点用来构造一个独立的点
- VK_PRIMITIVE_TOPOLOGY_LINE_LIST
- 顶点以成对的形式分组,每一对形成一条线段
- VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
- 把每 3 个顶点分为一组,组成一个三角形
- VK_PRIMITIVE_TOPOLOGY_LINE_STRIP
- 前两个顶点构成一条线段
- 每一个新的顶点连接前一个处理的顶点构成一条新的线段
- VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP
- 前 3 个顶点构成一个三角形
- 每一个接下来的顶点和前面处理的两个顶点构成一个新的三角形
- VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN
- 前 3 个顶点构成一个三角形
- 每个接下来的顶点和上一个顶点、本次绘制的第一个顶点构成新的三角形
- VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY
- 一次绘制中每 4 个顶点构成单个图元
- 中间两个顶点构成线段
- 把第一个和最后一个顶点传递给几何着色器
- VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY
- 一次绘制中 4 个顶点构成单个图元
- 中间的两个顶点构成线段
- 第一个和最后一个顶点被当作邻接信息传递给几何着色器
- VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY
- 每组 6 个顶点构成单个图元
- 每组的第 1 个、第 3 个、第 5 个构成一个三角形
- 第 2 个、第 4 个、第 6 个作为邻接信息传递给几何着色器
- VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY
- 以前 6 个顶点作为开始的条带构成了一个带有邻接信息的三角形
- 每两个新顶点构成一个新的三角形
- 奇数序号的顶点构成三角形,偶数序号的顶点提供邻接信息
- VK_PRIMITIVE_TOPOLOGY_POINT_LIST
7.4.4 细分状态
VkPipelineTessellationStateCreateInfo
typedef struct VkPipelineTessellationStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineTessellationStateCreateFlags flags; //设置了将分组到一个图元的控制点的个数 uint32_t patchControlPoints; } VkPipelineTessellationStateCreateInfo;
7.4.5 视口状态
VkPipelineViewportStateCreateInfo
typedef struct VkPipelineViewportStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineViewportStateCreateFlags flags; //视口的个数 uint32_t viewportCount; //每一个视口的尺寸 const VkViewport* pViewports; uint32_t scissorCount; //设置裁剪矩形 const VkRect2D* pScissors; } VkPipelineViewportStateCreateInfo;
- 视口变换是 Vulkan 管线中在栅格化之前的最后一个坐标变换
- 如果支持多视口,下限是 16 个
VkPhysicalDeviceLimits
maxViewports
7.4.6 光栅化状态
VkPipelineRasterizationStateCreateInfo
typedef struct VkPipelineRasterizationStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineRasterizationStateCreateFlags flags; //开启或关闭深度夹持(哈哈) //本该被远近平面裁剪掉的片段投射到这些平面上 //可用来填充因为裁剪造成的空洞 VkBool32 depthClampEnable; //关闭光栅化,光栅器将不会运行,将不会产生图元 VkBool32 rasterizerDiscardEnable; //自动地把三角形转化为点或者直线 VkPolygonMode polygonMode; //剔除 //VK_CULL_MODE_FRONT_BIT //VK_CULL_MODE_BACK_BIT VkCullModeFlags cullMode; //三角形的朝向由顶点的绕序 //VK_FRONT_FACE_COUNTER_CLOCKWISE //VK_FRONT_FACE_CLOCKWISE VkFrontFace frontFace; //下面四个参数控制了深度偏移这一特性 VkBool32 depthBiasEnable; float depthBiasConstantFactor; float depthBiasClamp; float depthBiasSlopeFactor; //置线段图元的宽度 //一些 Vulkan 实现并不支持宽线段,会忽略这个字段 //设置为 1.0 以外的值时也许会运行得相当缓慢 //建议设置为 1 不变 //线段的最大宽度,Vulkan 标准保证至少支持 8 像素 float lineWidth; } VkPipelineRasterizationStateCreateInfo;
VkPolygonMode
VK_POLYGON_MODE_FILL
- 三角形将会被绘制为实心的,内部的每一个点都会创建一个片段
VK_POLYGON_MODE_LINE
- 每一个三角形的每一条边都变为线段
- 绘制几何物体的线框模式时这个模式很有用
VK_POLYGON_MODE_POINT
- 每一个顶点绘制为一个点
7.4.7 多重采样状态
VkPipelineMultisampleStateCreateInfo
typedef struct VkPipelineMultisampleStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineMultisampleStateCreateFlags flags; VkSampleCountFlagBits rasterizationSamples; VkBool32 sampleShadingEnable; float minSampleShading; const VkSampleMask* pSampleMask; VkBool32 alphaToCoverageEnable; VkBool32 alphaToOneEnable; } VkPipelineMultisampleStateCreateInfo;
- 当进行多重采样时,颜色和深度-模板附件必须是多重采样图像
7.4.8 深度和模板状态
-
深度测试在片段着色器运行之后发生
-
要在深度测试之前运行片段着色器,可以在片段着色器入口设置 SPIR-V EarlyFragmentTests 执行模式
-
VkPipelineDepthStencilStateCreateInfo
typedef struct VkPipelineDepthStencilStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineDepthStencilStateCreateFlags flags; //深度测试启用 第 10 章 VkBool32 depthTestEnable; VkBool32 depthWriteEnable; VkCompareOp depthCompareOp; VkBool32 depthBoundsTestEnable; VkBool32 stencilTestEnable; VkStencilOpState front; VkStencilOpState back; float minDepthBounds; float maxDepthBounds; } VkPipelineDepthStencilStateCreateInfo;
-
深度和模板测试可以在片段着色器运行之前或之后进行。默认情况下,深度测试在片段着 色器运行之后发生
7.4.9 颜色混合状态
- 这个阶段负责把片段写入颜色附件
VkPipelineColorBlendStateCreateInfo
typedef struct VkPipelineColorBlendStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //0 VkPipelineColorBlendStateCreateFlags flags; //色器的输出和颜色附件的内容之间是否进行逻辑操作 第十章 VkBool32 clogicOpEnable; VkLogicOp logicOp; uint32_t attachmentCount; const VkPipelineColorBlendAttachmentState* pAttachments; float blendConstants[4]; } VkPipelineColorBlendStateCreateInfo;
VkPipelineColorBlendAttachmentState
//第 10 章 typedef struct VkPipelineColorBlendAttachmentState { VkBool32 blendEnable; VkBlendFactor srcColorBlendFactor; VkBlendFactor dstColorBlendFactor; VkBlendOp colorBlendOp; VkBlendFactor srcAlphaBlendFactor; VkBlendFactor dstAlphaBlendFactor; VkBlendOp alphaBlendOp; //控制了把输出图像的哪个通道写入附件中 VkColorComponentFlags colorWriteMask; } VkPipelineColorBlendAttachmentState;
7.5 动态状态
VkPipelineDynamicStateCreateInfo
typedef struct VkPipelineDynamicStateCreateInfo { //VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO VkStructureType sType; //nullptr const void* pNext; //为 0 VkPipelineDynamicStateCreateFlags flags; //动态状态的个数 uint32_t dynamicStateCount; //想要使用对应的动态状态设置命令来改变状态 const VkDynamicState* pDynamicStates; } VkPipelineDynamicStateCreateInfo;
VkDynamicState
VK_DYNAMIC_STATE_VIEWPOR
- 视口矩形是动态的
- 使用
vkCmdSetViewport()
更新
VK_DYNAMIC_STATE_SCISSOR
- 裁剪矩形是动态的
vkCmdSetScissor()
VK_DYNAMIC_STATE_LINE_WIDTH
- 线段宽度是动态的
vkCmdSetLineWidth()
VK_DYNAMIC_STATE_DEPTH_BIAS
- 深度偏移参数是动态的
vkCmdSetDepthBias()
- VK_DYNAMIC_STATE_BLEND_CONSTANTS
- 颜色混合常量是动态的
vkCmdSetBlendConstants()
- VK_DYNAMIC_STATE_DEPTH_BOUNDS
- 深度界限参数是动态的
vkCmdSetDepthBounds()
- VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK、VK_DYNAMIC_STATE_STENCIL_WRITE_MASK 和 VK_DYNAMIC_STATE_STENCIL_REFERENCE
- 对应的模板参数是动态的
vkCmdSetStencilCompareMask()
vkCmdSetStencilWriteMask()
vkCmdSetStencilReference()
- 动态和静态状态的有效性
- Page 211
- 最佳实践是,首先绑定管线,然后绑定任何相关状态,以避免潜在的未定义行为发生