闭关之 Vulkan 应用开发指南笔记(四):绘制、几何体&片段处理、同步和回读数据

28 篇文章 12 订阅

第8章 绘制

8.1 准备绘制

  • 准备绘制
    void vkCmdBeginRenderPass (
        VkCommandBuffer              commandBuffer,
        const VkRenderPassBeginInfo* pRenderPassBegin,
        VkSubpassContents            contents
    );
    
  • VkRenderPassBeginInfo
    typedef struct VkRenderPassBeginInfo 
    {
        //VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO 
        VkStructureType     sType;
        //nullptr
        const void*         pNext;
        VkRenderPass        renderPass;
        //被渲染的帧缓冲区
        VkFramebuffer       framebuffer;
        //可以选择只渲染到图像附件的一部分区域
        VkRect2D            renderArea;
        uint32_t            clearValueCount;
        //转变颜色
        const VkClearValue* pClearValues;
    } VkRenderPassBeginInfo; 
    
  • VkClearValue
    typedef union VkClearValue 
    {
        VkClearColorValue        color;
        VkClearDepthStencilValue depthStencil;
    } VkClearValue; 
    
  • VkClearColorValue
    typedef union VkClearColorValue 
    {
        float    float32[4];
        int32_t  int32[4];
        uint32_t uint32[4];
    } VkClearColorValue;
    
  • VkClearDepthStencilValue
    typedef struct VkClearDepthStencilValue 
    {
        float    depth;
        uint32_t stencil;
    } VkClearDepthStencilValue; 
    
  • 终止渲染通道中的渲染
    void vkCmdEndRenderPass (VkCommandBuffer commandBuffer); 
    

8.2 顶点数据

  • 把缓冲区当作顶点数据来用
    //要更新顶点缓冲区的非连续绑定范围,需要多次调用
    void vkCmdBindVertexBuffers (
        VkCommandBuffer     commandBuffer,
        //更新的第一个绑定的索引
        uint32_t            firstBinding,
        //一个给定的管线可能会引用很多个顶点缓冲
        uint32_t            bindingCount,
        //缓冲区对象
        const VkBuffer*     pBuffers,
        //偏移量的数组
        const VkDeviceSize* pOffsets
    );
    

8.3 索引绘制

  • 索引绘制
    void vkCmdDrawIndexed (
        VkCommandBuffer commandBuffer,
        uint32_t        indexCount,
        uint32_t        instanceCount,
        uint32_t        firstIndex,
        int32_t         vertexOffset,
        uint32_t        firstInstance
    ); 
    
  • 索引缓冲区绑定到命令缓冲区
    void vkCmdBindIndexBuffer (
        VkCommandBuffer commandBuffer,
        VkBuffer        buffer,
        VkDeviceSize    offset,
        //索引的类型
        //VK_INDEX_TYPE_UINT16
        //VK_INDEX_TYPE_UINT32
        VkIndexType     indexType
    );
    
  • 当调用 vkCmdDrawIndexed() 时,Vulkan 将从当前绑定的索引缓冲区中的如下索引位置开始读取数据
    • offset + firstIndex * sizeof(index)
  • 支持索引数值的最大范围
    • VkPhysicalDeviceLimits
      • maxDrawIndexedIndexValue
    • 至少为 2 24 − 1 2^{24}-1 2241

8.3.1 只用索引的绘制

  • 很多几何对象的局部顶点位置可以用 16 位、10 位,甚至 8 位数据来表示,并有足够的精度。3 个 10 位数据可以打包进一个 32 位的字中
    • VK_FORMAT_A2R10G10B10_SNORM_PACK32
  • 思想
    • 把顶点写在索引缓冲区
    • VS 时解包
    • Page 222

8.3.2 重置索引

  • 索引化绘制的另外一个特征允许你使用图元重启索引
    • 这个特殊的索引值可以存储进索引缓冲区里,用于通知一个新图元的开始
  • 图元重启特性开启
    • VkPipelineInputAssemblyStateCreateInfo
      • VkGraphicsPipelineCreateInfo
      typedef struct VkPipelineInputAssemblyStateCreateInfo 
      {
          VkStructureType                         sType;
          const void*                             pNext;
          VkPipelineInputAssemblyStateCreateFlags flags;
          VkPrimitiveTopology                     topology;
          //图元重启索引
          VkBool32                                primitiveRestartEnable;
      } VkPipelineInputAssemblyStateCreateInfo;
      
  • 索引类型的最大可能值的特殊值用作特殊重启标志
    • VK_INDEX_TYPE_UINT16 是 0xFFFF
    • VK_INDEX_TYPE_UINT32 是 0xFFFFFFFF
  • 如果没有启用图元重启,这个特殊的重置标志会被当作一个普通的顶点索引
  • 在把大型条带或者扇形绘制分解成许多小块时,重置索引很有用
  • 在有些架构上使用重置索引会影响性能,使用列表拓扑并展开索引缓冲区(不要试图用条带)可能会更好

8.4 实例化

  • vkCmdDraw()vkCmdDrawIndexed()firstInstanceinstanceCount
    • 用来控制实例化
  • 在一个着色器输入上使用内置的 InstanceIndex 修饰符来以当前实例的索引作为着色器的输入
    • 这个输入变量随后可以用来从 uniform 类型缓冲区中取出参
    • 通过编程计算每个实例的变化量
  • 使用实例化的顶点属性, 让 Vulkan 为顶点着色器提供每个实例特有的数据
  • 示例
  • Page 224

8.5 间接绘制

  • 不知道每一次绘制的准确参数情况
    • 几何物体的所有结构是已知的,但是顶点的个数和在顶点缓冲区的位置是未知的
      • 如某个对象永远以相同的方式渲染,但是细节层次可能随时间变化
    • 绘制命令由设备生成,而非主机
      • 顶点数据的个数和布局永远不会被主机所知
  • 绘制命令从设备可访问的内存获取参数
  • 它执行非索引化绘制,使用的参数包含在一个缓冲区
  • 执行非索引化的间接绘制
    void vkCmdDrawIndirect (
        VkCommandBuffer commandBuffer,
        VkBuffer        buffer,
        VkDeviceSize    offset,
        uint32_t        drawCount,
        uint32_t        stride
    ); 
    
  • VkDrawIndirectCommand
    typedef struct VkDrawIndirectCommand 
    {
        uint32_t vertexCount;
        uint32_t instanceCount;
        uint32_t firstVertex;
        uint32_t firstInstance;
    } VkDrawIndirectCommand;
    
  • 执行索引化的间接绘制
    void vkCmdDrawIndexedIndirect (
        VkCommandBuffer commandBuffer,
        VkBuffer        buffer,
        VkDeviceSize    offset,
        uint32_t        drawCount,
        uint32_t        stride
    ); 
    
  • VkDrawIndexedIndirectCommand
    typedef struct VkDrawIndexedIndirectCommand 
    {
        uint32_t indexCount;
        uint32_t instanceCount;
        uint32_t firstIndex;
        int32_t  vertexOffset;
        uint32_t firstInstance;
    } VkDrawIndexedIndirectCommand;
    
  • 关于应用详看 Page 225

第9章 几何体处理

9.1 表面细分

  • 来判定是否支持表面细分
    • VkPhysicalDeviceFeature
      • tessellationShader

9.1.1 表面细分配置

  • 两个着色器阶段
    • 表面细分控制着色器 (tessellation control shader)
      • 负责处理图元片的控制点,设置每个图元片的某些参数,然后将控制权交给固定功能细分模块
    • 表面细分评估着色器 (tessellation evaluation shader)
      • 的作用与顶点着色器类似,只不过它处理的是所有新生成的顶点
  • 表面细分的状态可以通过结合两组信息源来配置
  • VkPipelineTessellationStateCreateInfo
    typedef struct VkPipelineTessellationStateCreateInfo 
    {
    VkStructureType                        sType;
    const void*                            pNext;
    VkPipelineTessellationStateCreateFlags flags;
    //影响表面细分状态
    //用来设置组成每个图元片的控制点的数量
    uint32_t                               patchControlPoints;
    } VkPipelineTessellationStateCreateInfo;
    
  • Vulkan 将至少支持每个图元片包含 32 个控制点
  • 查询最大限制
    • VkPhysicalDeviceLimits
      • maxTessellationPatchSize
表面细分模式
  • 细分的两种主要模式
    • 将图元片当作矩形
    • 将图元片当作三角形
  • 每一个图元片都会有一个内部的和一个外部的细分等级集合
    • 外部的细分等级集合控制沿着图元片外部边缘的细分等级
    • 内部细分模式控制图元片内的细分等级
  • 四边形和三角形细分模式会生成三角形,等值线模式则会生成直线
  • 表面细分引擎生成单个的点
    • 图元片会按照正常方式细分,但是细分点并不会组合,而是被直接送入管线的余下阶段
    • polygonMode 设置为 VK_POLYGON_MODE_POINT
控制细分
  • 是控制相邻图元片的各条边如何排列
    • SpacingEqual
      • 每条边的细分等级会限制在区间 [1,maxLevel]
      • 向上取整为下一个较大的整数 n
      • 然后在质心空间下把边细分为等长的 n 段
    • SpacingFractionalEven
      • 每条边的细分等级会限制在区间 [2,maxLevel]
      • 向上取整为最近的偶数 n
      • 然后把边细分为等长的 n−2 段
    • SpacingFractionalOdd
      • 每条边的细分等级会限制在区间 [1,maxLevel − 1]
      • 向上取整为最近的奇数 n
      • 然后把边细分为等长的 n−2 段
    • 表面细分环绕顺序
      • layout (cw) in;
      • layout (ccw) in;

9.1.2 表面细分相关变量

  • 表面细分控制着色器传递到表面细分评估着色器的控制点的数量可以通过在表面细分控制着色器中使用输出布局限定符来设置
    • layout (vertices = 9) out;
  • 内部的和外部的细分等级分别用内置的 gl_TessLevelInnergl_TessLevelOuter 两个变量表示
  • 最大细分等级查询
    • VkPhysicalDeviceLimits
      • maxTessellationGenerationLevel
    • Vulkan 能够保证支持的最小值是 64
  • gl_InvocationID
    • 用于索引输出数组
  • 每一个输出顶点能够生成的组件总数
    • VkPhysicalDeviceLimits
      • maxTessellationControlTotalOutputComponents
    • 至少能够支持 2048 个
  • 表面细分评估着色器接收的组件总数
    • VkPhysicalDeviceLimits
      • maxTessellationEvaluationInputComponents
      • 至少是 64
图元片的相关变量
  • 图元片输出主要用于以下两个目的
    • 存储每个图元片数据,把数据从表面细分控制着色器传递到表面细分评估着色器
    • 在同一个图元片内的不同的表面细分控制着色器调用中共享数据
  • 声明一个图元片的输出变量
    • patch out myVariable;
  • 同步一个图元片内的调用
    • 表面细分控制着色器中调用内置函数 barrier() 即可生成这些指令
表面细分评估着色器
  • 表面细分控制着色器执行结束并把细分系数传递给固定功能细分单元之后后,图元片内部会产
    生新的顶点并且在图元片空间中给顶点分配质心坐标
  • 每一个新生成的顶点都会调用一次表面细分评估着色器
  • 能够生成的数据总量
    • VkPhysicalDeviceLimits
      • maxTessellationEvaluationOutputComponents

9.1.3 表面细分示例:置换贴图

  • Page 242

9.2 几何着色器

  • 是否支持几何着色器
    • VkPhysicalDeviceFeatures
      • geometryShader
  • 几何着色器必须包含以下信息
    • 输入图元类型,它必须是点、线(邻接线)、三角形(邻接三角形)中的一种
    • 输出图元类型,它必须是点、线或者三角形条带中的一种
    • 期望在一次几何着色器调用中生成的顶点的最大数量
      layout (triangles) in;
      layout (triangle_strip) out;
      layout (max_vertices = 3) out;
      
  • 单次几何着色器调用能够生成的最大顶点数量
    • VkPhysicalDeviceLimits
      • maxGeometryOutputVertices
    • 至少为 256
  • 几何着色器输出的每一个顶点能够生成的最大组件数量
    • maxGeometryOutputComponents
    • 至少为 64
  • 几何着色器的输出来自如下两个位置
    • gl_PerVertex 输入块声明中包含的内置输入声明
    • 用户自定义的与前面一个着色器阶段中的输出声明相对应的输入
  • 输入几何着色器的所有组件总数
    • VkPhysicalDeviceLimits
      • maxGeometryInputComponents
    • 至少 64 个

9.2.1 图元裁剪

9.2.2 几何着色器实例化

  • 一种非常高效的实例化机制
  • GLSL 着色器中使用 invocations 输入布局限定符改变几何着色器执行次数
    • layout (invocations = 8) in;
  • 调用编号可以通过 GLSL 内置变量 gl_InvocationID 获取
  • 几何着色器的最大调用次数依赖于具体实现
    • 至少为 32
    • VkPhysicalDeviceLimits
      • maxGeometryShaderInvocations

9.3 可编程顶点尺寸

  • 点能够按照以下 3 种方式之一进行光栅化
    • 仅通过顶点着色器和片段着色器渲染
      • 设置图元的拓扑为 VK_PRIMITIVE_TOPOLOGY_POINT_LIST
    • 启用表面细分
      • 通过使用 SPIR-V 的执行模式 PointMode 修饰表面细分着色器入口点置表面细分为点模式
    • 使用一个生成点的几何着色器
  • 带 PointSize 修饰符的输出变量中的值必须在设备支持的尺寸范围内
    • VkPhysicalDeviceLimits
      • pointSizeRange
  • 点最终的像素尺寸会量化为一个与设备相关的尺度
    • VkPhysicalDeviceLimits
      • pointSizeGranularity
  • 把一个编译时常量写入 PointSize 会让很多 Vulkan 实现变得更高效

9.4 线的宽度以及光栅化

  • Vulkan 实现可能以两种方式之一渲染线段
    • 严格的和非严格的
  • 设置线宽
    void vkCmdSetLineWidth (
        VkCommandBuffer commandBuffer,
        float           lineWidth
    );
    
  • 支持的最小和最大线宽
    • VkPhysicalDeviceLimits
      • lineWidthRange
    • Vulkan 实现至少会支持 1~8 像素的线宽

9.5 用户裁剪和剔除

  • 使用 ClipDistance 修饰符修饰输出变量,能够产生裁剪距离变量
    • 支持的裁剪距离的最大数量至少为 8
      • VkPhysicalDeviceLimits
        • maxClipDistances
  • 裁剪距离也可用作片段着色器的输入
    • 在大多数实现中裁剪都是在图元级别执行的
  • 通过使用裁剪距离能够实现另一种裁剪方式
    • 对应用于跨图元顶点的裁剪距离进行插值
  • 要使用剔除距离,声明一个带 CullDistance 修饰符的输出变量即可
    • 至少支持 8 个
      • VkPhysicalDeviceLimits
        • maxCullDistances
  • 很多设备会有一个单次可用距离值的数量的综合限制
    • VkPhysicalDeviceLimits
      • ClipAndCullDistances

9.6 视口变换

  • 至少视口的最大尺寸保证是 4096×4096 像素
    • VkPhysicalDeviceLimits
      • maxViewportDimensions
  • 视口可偏移的最大限度
    • VkPhysicalDeviceLimits
      • viewportBoundsRange
  • 视口状态也是可以动态设置的
    • VK_DYNAMIC_STATE_VIEWPORT
  • 动态设置视口边界
    void vkCmdSetViewport (
        VkCommandBuffer   commandBuffer,
        uint32_t          firstViewport, 
        uint32_t          viewportCount,
        const VkViewport* pViewports
    );
    
  • 当前管线支持的视口数量
    • VkPipelineViewportStateCreateInfo
      • viewportCount
  • 设备支持的视口总数
    • VkPhysicalDeviceLimits
      • maxViewports
    • 那么这个值至少是 16
  • 几何着色器可以通过对输出变量使用 ViewportIndex 修饰符来选择视口索引
    • gl_ViewportIndex
  • 把几何体投影到多个视口的简单方法就是,让几何着色器的执行次数与管线中的视口数量相同
    • 示例 Page 266

第10章 片段处理

10.1 裁剪测试

  • 该测试只是简单地判断片段是否位于帧缓冲区的指定矩形区域内
  • 帧缓冲区的全尺寸视口和裁剪区域的区别
    • 视口变换改变图元在帧缓冲区里的位置
    • 视口区域会影响裁剪
  • 裁剪测试总是运行在片段着色器之前
  • 修改裁剪区域
    void vkCmdSetScissor(
      VkCommandBuffer commandBuffer, 
      uint32_t        firstScissor,
      uint32_t        scissorCount,
      const VkRect2D* pScissors
    );
    

10.2 深度和模板测试

  • 深度和模板操作
    typedef struct VkPipelineDepthStencilStateCreateInfo 
    {
      VkStructureType                        sType; 
      const void*                            pNext;
      VkPipelineDepthStencilStateCreateFlags flags;
      //启用深度测试
      VkBool32                               depthTestEnable;
      //启用深度缓冲区
      VkBool32                               depthWriteEnable;
      //比较操作,Page 271 表格
      VkCompareOp                            depthCompareOp;
      //深度测试范围
      //当前深度缓冲区深度值与指定的范围比较,在范围内就通过
      VkBool32                               depthBoundsTestEnable;
      //启用模板测试
      VkBool32                               stencilTestEnable;
      //模板测试可以对图元正面和背面进行不同测试
      VkStencilOpState                       front;
      VkStencilOpState                       back;
      //最大范围与最小范围
      float                                  minDepthBounds;
      float                                  maxDepthBounds;
    } VkPipelineDepthStencilStateCreateInfo;
    

10.2.1 深度测试

  • 深度测试的最大最小范围设置
    void vkCmdSetDepthBounds (
      VkCommandBuffer commandBuffer,
      float           minDepthBounds,
      float           maxDepthBounds
    ); 
    
  • 是否支持深度范围测试
    • VkPhysicalDeviceFeatures
      • depthBounds
  • 深度偏差
    typedef struct VkPipelineRasterizationStateCreateInfo
    {
      VkStructureType                         sType;
      const void*                             pNext;
      VkPipelineRasterizationStateCreateFlags flags;
      VkBool32                                depthClampEnable;
      VkBool32                                rasterizerDiscardEnable;
      VkPolygonMode                           polygonMode;
      VkCullModeFlags                         cullMode;
      VkFrontFace                             frontFace;
      //深度偏差
      VkBool32                                depthBiasEnable;
      //偏差方程系数 方程:Page274
      float                                   depthBiasConstantFactor;
      float                                   depthBiasClamp;
      float                                   depthBiasSlopeFactor;
      float                                   lineWidth;
    } VkPipelineRasterizationStateCreateInfo;
    
  • 设置深度偏差方程参数
    void vkCmdSetDepthBias (
      VkCommandBuffer commandBuffer,
      float           depthBiasConstantFactor,
      float           depthBiasClamp,
      float           depthBiasSlopeFactor
    ); 
    

10.2.2 模板测试

  • 模板测试可以对图元正面和背面进行不同测试
    typedef struct VkStencilOpState 
    {
      //VkStencilOp 可执行操作:Page 275
      //深度测试成功,模板测试失败执行
      VkStencilOp failOp;
      //深度测试成功,模板测试成功执行 
      VkStencilOp passOp;
      //深度测试失败,执行,跳过模板测试
      VkStencilOp depthFailOp;
      //比较模板参考值和缓冲区里的值
      VkCompareOp compareOp;
      //比较用的资源
      uint32_t    compareMask;
      uint32_t    writeMask;
      uint32_t    reference;
    } VkStencilOpState;
    
  • 模板测试参数设置
    void vkCmdSetStencilReference (
      VkCommandBuffer    commandBuffer,
      //是否将新的状态应用到正面、背面或者双面
      VkStencilFaceFlags faceMask,
      uint32_t           reference
    );
    
    void vkCmdSetStencilCompareMask (
      VkCommandBuffer    commandBuffer,
      //是否将新的状态应用到正面、背面或者双面
      VkStencilFaceFlags faceMask,
      uint32_t           compareMask
    );
    
    void vkCmdSetStencilWriteMask (
      VkCommandBuffer    commandBuffer,
      //是否将新的状态应用到正面、背面或者双面
      VkStencilFaceFlags faceMask,
      uint32_t           writeMask
    );
    

10.2.3 早期片段测试

  • 在片段着色器之后进行深度和模板测试
    • 片段着色器通过着色器内置变量 FragDepth 来修改当前片段的深度值
    • 片段着色器有其他副作用,比如存储到图像中
    • 如果片段着色器使用 SPIR-V 的 OpKill 指令丢弃当前片,深度缓存不会更新
      • discard()
  • 如果上述情况不存在,可以在片段着色器之前进行深度和模板测试
    • 启动
      • 在片段着色器的 SPIR-V 代码的函数入口点使用 EarlyFragmentTest 修饰符
    • 用一个预渲染的深度图像进行深度测试
      • Page 277

10.3 多重采样渲染

  • 两种通用方式可以生成多重采样图像
    • 多重采样
    • 超采样
      • 开销高于多重采样
  • 大多数 Vulkan 实现所支持的采样次数为 1~8 或者 1~16
  • 不同格式支持的采样数不同,查询
    VkResult vkGetPhysicalDeviceImageFormatProperties (
      VkPhysicalDevice         physicalDevice,
      VkFormat                 format,
      VkImageType              type,
      VkImageTiling            tiling,
      VkImageUsageFlags        usage,
      VkImageCreateFlags       flags,
      VkImageFormatProperties* pImageFormatProperties
    );
    
  • 采样分布
    • VkPhyicalDeviceLimits
      • standardSampleLocations
        • 均匀采样还是非标准采样

10.3.1 采样率着色

  • 启用
    • VkPipelineMultiSampleStateCreateInfo
      • sampleShadingEnable
        • 设置为 true
      • minSampleShading
        • 采样率
      • alphaToCoverage
        • 使用 alpha 设置覆盖率

10.3.2 多重采样解析

  • 两种解析图像的方法
    • 将非多重采样图像包含到结构体 VkSubpassDescription
      • pResolveAttachments
        • 就是解析后的图像
    • 显式地将多重采样图像解析成单重采样图像
      • 调用函数
        void vkCmdResolveImage (
          VkCommandBuffer commandBuffer,
          VkImage               srcImage,
          VkImageLayout         srcImageLayout,
          VkImage               dstImage,
          VkImageLayout         dstImageLayout,
          uint32_t              regionCount,
          //图像解析区域(可以部分解析)
          const VkImageResolve* pRegions
        ); 
        
      • VkImageResolve
        typedef struct VkImageResolve {
          VkImageSubresourceLayers srcSubresource;
          VkOffset3D               srcOffset;
          VkImageSubresourceLayers dstSubresource;
          VkOffset3D               dstOffset;
          VkExtent3D               extent;
        } VkImageResolve;
        

10.4 逻辑操作

  • 允许在片段着色器的输出与颜色附件内容之间进行逻辑操作
  • 启用
    typedef struct VkPipelineColorBlendStateCreateInfo 
    {
      VkStructureType                            sType;
      const void*                                pNext;
      VkPipelineColorBlendStateCreateFlags       flags;
      //启用
      VkBool32                                   logicOpEnable;
      //操作 Page 282
      VkLogicOp                                  logicOp;
      uint32_t                                   attachmentCount;
      const VkPipelineColorBlendAttachmentState* pAttachments;
      float                                      blendConstants[4];
    } VkPipelineColorBlendStateCreateInfo;
    

10.5 片段着色器的输出

  • 输出多个颜色附件
    layout (location = 0) out vec4 o_color1;
    layout (location = 1) out vec4 o_color2;
    layout (location = 5) out vec4 o_color3;
    
  • 片段着色器输出指定的最大位置数是与设备相关
    • 至少写入 4 个颜色附件
    • VkPhysicalDeviceLimits
      • maxFragmentOutputAttachments

10.6 颜色混合

  • 片段着色器输出的附件合并操作
    typedef struct VkPipelineColorBlendAttachmentState 
    {
      //开启混合
      VkBool32              blendEnable;
      //混合因子 Page 287
      VkBlendFactor         srcColorBlendFactor;
      VkBlendFactor         dstColorBlendFactor;
      //混合算法设置 Page 286
      VkBlendOp             colorBlendOp;
      VkBlendFactor         srcAlphaBlendFactor;
      VkBlendFactor         dstAlphaBlendFactor;
      VkBlendOp             alphaBlendOp;
      VkColorComponentFlags colorWriteMask;
    } VkPipelineColorBlendAttachmentState; 
    
  • 如果混合是静态的需要设置
    • VkPipelineColorBlendStateCreateInfo
      • blendConstants
  • 如果颜色混合常量状态配置成动态的
    • 改变混合常量
      void vkCmdSetBlendConstants (
        VkCommandBuffer commandBuffer,
        const float     blendConstants[4]
      );
      
  • 双源混合
    • 两个由片段着色器输出的源颜色都同时指向相同的附件位置, 但是使用不同的颜色索引
    • 不是 Vulkan 实现必须支持的
      • VkGetPhysicsDeviceFeature
        • VkPhysicalDeviceFeatures
          • dualSrcBlend
    • 如果支持,至少一个附件可用于双源混合模式

第11章 同步

  • Vulkan 提供的 3 种主要的同步原语类型
    • 栅栏(fence)
      • 当主机需要等待设备完成某次提交中的大量工作时使用,通常需要操作系统的协助
    • 事件(event)
      • 表示一个细粒度的同步原语,可由主机或者设备发出
        • 当设备发出信号时,可以在命令缓冲区中通知它,并且在管线中的特定点上可以由设备等待它
    • 信号量(semaphore)
      • 用于控制设备上的不同队列对资源的所有权
        • 它们可用于同步不同队列上可能异步执行的工作

11.1 栅栏

  • 栅栏常常对应于一个操作系统提供的本地同步原语
    • 所以通常当线程等待栅栏时可能会休眠,这样就能节能
      • 等待多个命令缓冲区执行完毕
      • 将渲染完的帧展示给用户
  • 创建栅栏
    VkResult vkCreateFence (
    VkDevice                     device,
    const VkFenceCreateInfo*     pCreateInfo,
    //用于分配栅栏所需的主机内存
    const VkAllocationCallbacks* pAllocator,
    VkFence*                     pFence
    );
    
  • VkFenceCreateInfo
    typedef struct VkFenceCreateInfo 
    {
      //VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
      VkStructureType    sType;
      //nullptr
      const void*        pNext;
      //栅栏的行为
      //VK_FENCE_CREATE_SIGNALED_BIT
      //如果设置,初始状态是有信号的
      //未设置,则无信号
      VkFenceCreateFlags flags;
    } VkFenceCreateInfo;
    
  • 销毁栅栏
    void vkDestroyFence (
      VkDevice                     device,
      VkFence                      fence,
      const VkAllocationCallbacks* pAllocator
    );
    
  • 栅栏应用于任何接受栅栏参数的命令中 (队列)
    • 发起在队列上执行的工作
    VkResult vkQueueSubmit (
      VkQueue             queue,
      uint32_t            submitCount,
      const VkSubmitInfo* pSubmits,
      //当队列引发的所有工作都完成后,把字段 fence 指定的栅栏对象设置为有信号的状态
      VkFence             fence
    ); 
    
  • 设备可以直接向栅栏对象发送信号
  • 设备通过中断或者其他硬件机制向操作系统发送信号,操作系统再改变栅栏的状态
  • 判断栅栏的状态
    VkResult vkGetFenceStatus (
      VkDevice device,
      VkFence  fence
    );
    
    • VK_SUCCESS
      • 栅栏当前是有信号的状态
    • VK_NOT_READY
      • 栅栏当前是无信号的状态
    • 可能会引发错误,进而自旋
    • 避免自旋应调用 vkWaitForFences
  • vkWaitForFences
    VkResult vkWaitForFences (
      VkDevice       device,
      uint32_t       fenceCount,
      const VkFence* pFences,
      VkBool32       waitAll,
      //超时设置
      uint64_t       timeout
    );
    
    • 可以等待任何数量的栅栏对象
    • 可以等待所有栅栏有信号
    • 也可以等待任意一个变为有信号返回
      • waitAll
    • 检查多个栅栏比 vkGetFenceStatus 高效
    • 允许超时
    • 返回值
      • VK_SUCCESS
      • VK_TIMEOUT
  • 要重置一个或者多个栅栏为无信号的状态
    VkResult vkResetFences (
      VkDevice       device,
      uint32_t       fenceCount,
      const VkFence* pFences
    ); 
    
  • 栅栏用途
    • 防止主机修改正在被设备使用的数据,或设备可能即将使用的数据
  • 3 种机制来实现主机和设备之间的同步,并确保主机在设备使用缓冲区里的数据之前不重写数据
    • 调用 vkQueueWaitIdle()来保证所有提交到队列的工作已完成
    • 使用一个栅栏,该栅栏和使用这些数据的提交任务相关联,并且在重写缓冲区的内容之前等待这个栅栏
    • 将缓冲区细分为 4 等份每一份关联一个栅栏,在重写每一份之前,等待关联的栅栏
      • 示例:Page 293

11.2 事件

  • 事件对象代表了一个细粒度的同步原语,用于精确地界定管线里发生的操作
  • 事件有两种状态
    • 有信号状态和无信号状态
  • 可以显式地向事件发送信号或者进行重置
  • 设备不但可以直接地操作事件的状态,还可以在管线里的特定时间点上进行操作
  • 创建事件对象
    VkResult vkCreateEvent (
      VkDevice                     device,
      const VkEventCreateInfo*     pCreateInfo,
      const VkAllocationCallbacks* pAllocator,
      VkEvent*                     pEvent
    );
    
  • VkEventCreateInfo
    typedef struct VkEventCreateInfo 
    {
      //VK_STRUCTURE_TYPE_EVENT_CREATE_INFO
      VkStructureType    sType;
      //nullptr
      const void*        pNext;
      //0
      VkEventCreateFlags flags;
    } VkEventCreateInfo;
    
  • 释放事件对象
    void vkDestroyEvent (
      VkDevice                     device,
      VkEvent                      event,
      const VkAllocationCallbacks* pAllocator
    );
    
  • 事件的初始状态是无信号的或者重置的
  • 主机上改变事件的状态
    VkResult vkSetEvent (
      VkDevice device,
      VkEvent  event
    );
    
  • 当主机设置事件对象后,事件对象的状态就立即变成了有信号的状态。如果另一个线程正在通过调用 vkCmdWaitEvents() 等待这个事件对象,该线程将立即变为非阻塞状态
  • 重置事件状态
    VkResult vkResetEvent (
      VkDevice device,
      VkEvent  event
    );
    
  • 获取事件状态
    VkResult vkGetEventStatus (
      VkDevice device,
      VkEvent  event
    ); 
    
    • 返回值
      • VK_EVENT_SET
        • 指定的事件已设置或者处于有信号的状态
      • VK_EVENT_RESET
        • 指定的事件已重置或者处于无信号的状态
  • 除了在循环里自旋以等待 vkGetEventStatus() 返回 VK_EVENT_SET 之外,主机没有办法等待事件对象
    • 该自旋非常低效,如果确实需要这么做,需要和系统配合
      • 例如,使当前线程休眠,或者在查询事件对象状态之间做些其他有用的事情
  • 事件对象也可以由设备操作
    void vkCmdSetEvent (
      VkCommandBuffer      commandBuffer,
      VkEvent              event,
      VkPipelineStageFlags stageMask
    );
    
  • 设备操作重置事件
    void vkCmdResetEvent (
      VkCommandBuffer      commandBuffer,
      VkEvent              event,
      VkPipelineStageFlags stageMask
    ); 
    
  • 设备不能直接获取事件对象的状态,但是能等待一个或者多个事件
    void vkCmdWaitEvents (
      //指定哪个命令缓冲区会中止运行,以等待事件
      VkCommandBuffer              commandBuffer,
      //需要等待的事件的数量
      uint32_t                     eventCount,
      const VkEvent*               pEvents,
      //在哪些管线阶段会通知事件
      VkPipelineStageFlags         srcStageMask,
      //在哪些阶段等待事件会变为有信号状态
      //在 dstStageMask 指定的阶段,等待肯定会发生
      VkPipelineStageFlags         dstStageMask,
      //变为有信号的状态执行内存操作
      uint32_t                     memoryBarrierCount,
      const VkMemoryBarrier*       pMemoryBarriers,
      uint32_t                     bufferMemoryBarrierCount,
      const VkBufferMemoryBarrier* pBufferMemoryBarriers,
      uint32_t                     imageMemoryBarrierCount,
      const VkImageMemoryBarrier*  pImageMemoryBarriers
    ); 
    

11.3 信号量

  • 信号量代表了可以被硬件以原子方式设置和重置的标记
  • 信号量不能被设备显式地通知或者等待
    • 它们由队列操作,通知并在队列操作上等待
      • 例如 vkQueueSubmit()
  • 创建信号量对象
    VkResult vkCreateSemaphore (
      VkDevice                     device,
      const VkSemaphoreCreateInfo* pCreateInfo,
      const VkAllocationCallbacks* pAllocator,
      VkSemaphore*                 pSemaphore
    ); 
    
  • VkSemaphoreCreateInfo
    typedef struct VkSemaphoreCreateInfo 
    {
      //VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO
      VkStructureType        sType;
      //nullptr
      const void*            pNext;
      //0
      VkSemaphoreCreateFlags flags;
    } VkSemaphoreCreateInfo;
    
  • 销毁信号量
    void vkDestroySemaphore (
      VkDevice                     device,
      VkSemaphore                  semaphore,
      const VkAllocationCallbacks* pAllocator
    );
    
  • 信号量对象不允许显式地设置、重置和等待
  • 用来同步不同队列对资源的访问,从而形成向设备提交工作的完整部分
  • vkQueueSubmit
    VkResult vkQueueSubmit 
    (
      VkQueue             queue,
      uint32_t            submitCount,
      const VkSubmitInfo* pSubmits,
      VkFence             fence
    );
    
  • VkSubmitInfo
    typedef struct VkSubmitInfo 
    {
      VkStructureType             sType;
      const void*                 pNext;
      uint32_t                    waitSemaphoreCount;
      //队列等待的信号量
      const VkSemaphore*          pWaitSemaphores;
      const VkPipelineStageFlags* pWaitDstStageMask;
      uint32_t                    commandBufferCount;
      const VkCommandBuffer*      pCommandBuffers;
      uint32_t                    signalSemaphoreCount;
      //完成后发送型号
      const VkSemaphore*          pSignalSemaphores;
    } VkSubmitInfo; 
    
  • 信号量是外部同步的,必须保证信号量不能同时用于两个不同线程中的不同队列中
  • 可以将一定量的命令缓冲区提交给只负责计算的队列,这个队列随后会在完成时通知信号量
    • 该信号量出现在等待列表中,等待第二次提交到图形队列里
  • 信号量的相同同步机制在其他方面也有运用
    • 稀疏内存

第12章 回读数据

12.1 查询

  • Vulkan 读取统计数据的主要机制是依靠查询对象(query object)
    • 查询对象通过池进行创建和管理
      • 每个对象实际上是池里的一个槽(slot)
      • 而不是一个单独管理的离散对象
  • 查询池创建
    VkResult vkCreateQueryPool (
      VkDevice                     device,
      const VkQueryPoolCreateInfo* pCreateInfo,
      const VkAllocationCallbacks* pAllocator,
      VkQueryPool*                 pQueryPool
    ); 
    
  • VkQueryPoolCreateInfo
    typedef struct VkQueryPoolCreateInfo 
    {
      //VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO
      VkStructureType               sType;
      //nullptr
      const void*                   pNext;
      //0
      VkQueryPoolCreateFlags        flags;
      //查询的类型
      VkQueryType                   queryType;
      uint32_t                      queryCount;
      //如何获取统计信息
      VkQueryPipelineStatisticFlags pipelineStatistics;
    } VkQueryPoolCreateInfo;
    
  • VkQueryType
    • VK_QUERY_TYPE_OCCLUSION
      • 遮挡查询
      • 统计通过了深度和模板测试的样本数
    • VK_QUERY_TYPE_PIPELINE_STATISTICS
      • 管线统计查询
      • 统计在设备的运行过程中产生的各种统计信息
    • VK_QUERY_TYPE_TIMESTAMP
      • 时间戳查询
      • 测量执行一个命令缓冲区里的命令所花费的时间
  • 销毁查询池
    void vkDestroyQueryPool (
      VkDevice                     device,
      VkQueryPool                  queryPool,
      const VkAllocationCallbacks* pAllocator
    );
    
  • 重置池
    void vkCmdResetQueryPool (
      VkCommandBuffer commandBuffer,
      VkQueryPool     queryPool,
      uint32_t        firstQuery,
      uint32_t        queryCount
    ); 
    

12.1.1 执行查询

  • 查询通过在命令缓冲区中包含特定的开始-停止指令
    • vkCmdBeginQuery()
      void vkCmdBeginQuery (
        VkCommandBuffer     commandBuffer,
        VkQueryPool         queryPool,
        uint32_t            query,
        //控制查询执行的额外标志位
        //VK_QUERY_CONTROL_PRECISE_BIT 获取精确结果,会有额外开销
        //未设置返回近似结果
        VkQueryControlFlags flags
      );
      
    • vkCmdEndQuery()
      void vkCmdEndQuery (
        VkCommandBuffer commandBuffer,
        VkQueryPool     queryPool,
        uint32_t        query
      );
      
    • 必须成对出现
  • 从池中的一个或者多个查询中获取结果
    VkResult vkGetQueryPoolResults (
      VkDevice           device,
      VkQueryPool        queryPool,
      uint32_t           firstQuery,
      uint32_t           queryCount,
      size_t             dataSize,
      //指向的主机内存
      void*              pData,
      //间隔写入
      VkDeviceSize       stride,
      //查询的类型将决定什么会写入内存中
      VkQueryResultFlags flags
    );
    
  • VkQueryResultFlags
    • VK_QUERY_RESULT_64_BIT
      • 结果将会以 64 位的数量返回
      • 否则,以 32 位的数量返回
    • VK_QUERY_RESULT_WAIT_BIT
      • 将会在查询可用前一直等待
      • 否则,将会返回状态编码,来指出要查询的命令是否执行完毕
    • VK_QUERY_RESULT_WITH_AVAILABILITY_BIT
      • 当查询没有就绪时,在调用时 Vulkan 会写入一个 0
      • 同时任何就绪的查询都会返回一个非零的结果。
    • VK_QUERY_RESULT_PARTIAL_BIT
      • 在查询包括的指令执行完成前就将当前的值写入结果缓冲区中
  • 将查询结果写入缓冲区对象
    void vkCmdCopyQueryPoolResults (
      VkCommandBuffer    commandBuffer,
      VkQueryPool        queryPool,
      uint32_t           firstQuery,
      uint32_t           queryCount,
      VkBuffer           dstBuffer,
      VkDeviceSize       dstOffset,
      VkDeviceSize       stride,
      VkQueryResultFlags flags
    );
    
    • 必须以同步方式访问写入缓冲区的对象结果,该访问使用屏障来完成
遮挡查询
  • 计数是通过深度和模板测试的片段数
  • 应用场景
    • 将场景的一部分渲染到深度缓冲区中
      • 例如,建筑物或地形
    • Page 308
管线统计信息查询
  • pipelineStatistics
    • 前缀
      • VK_QUERY_PIPELINE_STATISTIC_
    • …INPUT_ASSEMBLY_VERTICES_BIT
      • 对图形管线顶点装配阶段组装的顶点数进行计数
    • …INPUT_ASSEMBLY_PRIMITIVES_BIT
      • 对图形管线图元组装阶段装配的完整图元数进行计数
    • …VERTEX_SHADER_INVOCATIONS_BIT
      • 统计图形管线中顶点着色器的总调用次数
      • 可能与组装的顶点数不同
    • …GEOMETRY_SHADER_INVOCATIONS_BIT
      • 统计图形管线中几何着色器的总调用次数
    • …GEOMETRY_SHADER_PRIMITIVES_BIT
      • 统计几何着色器生成的图元总数
    • …CLIPPING_INVOCATIONS_BIT
      • 统计进入图形管线裁剪阶段的图元数
      • 如果一个图元可以完全丢弃而不进行裁剪,这个计数器不会递增
    • …CLIPPING_PRIMITIVES_BIT
      • 统计裁剪时生成的图元数量
    • …FRAGMENT_SHADER_INVOCATIONS_BIT
      • 统计片段着色器的总调用次数
    • …TESSELLATION_CONTROL_SHADER_PATCHES_BIT
      • 对由细分控制着色器处理的图元片总数进行统计
    • …TESSELLATION_EVALUATION_SHADER_INVOCATIONS_BIT:
      • 在细分过程中每次调用细分评估着色器时递增
    • …COMPUTE_SHADER_INVOCATIONS_BIT
      • 统计计算着色器调用的总数

12.1.2 计时查询

  • 测量在命令缓冲区中执行命令所花费的时间
  • 将当前设备时间写入查询池
    void vkCmdWriteTimestamp (
      VkCommandBuffer         commandBuffer,
      //指定的管线阶段将当前设备时间写入指定的查询
      VkPipelineStageFlagBits pipelineStage,
      VkQueryPool             queryPool,
      uint32_t                query
    ); 
    

12.2 通过主机读取数据

  • 示例
    • Page 311

第十三章 多通道渲染

    • 类似 FrameGraph
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值