Vulkan 同步机制原理解析(6)

318 阅读59分钟

VULKAN 同步机制原理解析

一、Vulkan同步机制概述

Vulkan作为一款底层图形API,其设计哲学之一是显式同步。与OpenGL等API的隐式同步不同,Vulkan要求开发者手动管理GPU操作之间的同步关系,这虽然增加了编程复杂度,但也为性能优化提供了更大的空间。

Vulkan同步机制的核心目标是解决以下问题:

  • 资源竞争:多个GPU操作同时访问同一资源(如纹理、缓冲区)时的冲突
  • 执行顺序:确保GPU操作按预期顺序执行
  • 数据可见性:保证一个操作的结果能被后续操作正确读取
  • CPU-GPU协作:协调CPU与GPU之间的工作节奏

1.1 同步机制的核心组件

Vulkan提供了四种核心同步原语,它们分别适用于不同的同步场景:

  1. Pipeline Barrier(管线屏障):用于同一队列中不同管线阶段之间的同步
  2. Semaphore(信号量):用于不同队列之间或队列与呈现引擎之间的同步
  3. Fence( fences):用于CPU与GPU之间的同步
  4. Event(事件):用于GPU命令流内部或CPU与GPU之间的细粒度同步
graph TD
    A[Vulkan同步原语] --> B[Pipeline Barrier<br/>同一队列内同步]
    A --> C[Semaphore<br/>队列间/GPU内部同步]
    A --> D[Fence<br/>CPU-GPU同步]
    A --> E[Event<br/>细粒度条件同步]

1.2 同步机制的设计原则

Vulkan同步机制基于以下设计原则:

  • 显式声明:所有同步需求必须显式声明,API不会自动插入同步操作
  • 基于阶段:同步操作与GPU管线阶段紧密关联
  • 资源状态:资源的访问模式(读取/写入)必须明确声明
  • 最小同步:仅在必要时使用同步操作,过多的同步会降低性能
// 同步机制设计原则的体现:显式声明资源访问和管线阶段
void exampleSyncPrinciple(VkCommandBuffer cmdBuffer, VkImage image) {
    // 1. 定义图像内存屏障(显式声明)
    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    
    // 2. 明确源和目标管线阶段(基于阶段)
    barrier.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    barrier.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    
    // 3. 明确资源访问类型(资源状态)
    barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    
    // 4. 仅在必要时使用(最小同步)
    barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    barrier.image = image;
    barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
    
    // 应用屏障
    vkCmdPipelineBarrier(
        cmdBuffer,
        barrier.srcStageMask,
        barrier.dstStageMask,
        0,
        0, nullptr,
        0, nullptr,
        1, &barrier
    );
}

1.3 同步机制的重要性

正确的同步机制是Vulkan应用正确性和性能的关键:

  • 正确性:缺乏必要的同步会导致视觉错误、数据损坏甚至程序崩溃
  • 性能:不当的同步会严重影响性能,而优化的同步策略能显著提升性能
  • 可移植性:不同GPU硬件对未同步操作的处理可能不同,正确同步确保跨平台兼容性
graph TD
    A[正确的同步机制] --> B[视觉正确]
    A --> C[数据完整]
    A --> D[性能优化]
    A --> E[跨平台兼容]
    F[错误的同步机制] --> G[画面闪烁]
    F --> H[数据竞争]
    F --> I[性能低下]
    F --> J[平台相关行为]

接下来的章节将深入分析每种同步原语的工作原理、源码实现及最佳实践,帮助开发者全面掌握Vulkan同步机制。

二、PIPELINE BARRIER(管线屏障)原理解析

2.1 管线屏障的基本概念

Pipeline Barrier(管线屏障)是Vulkan中用于同一命令队列内部同步的基本机制。它能够控制命令缓冲区中命令的执行顺序,确保特定操作完成后再执行后续操作。

管线屏障主要解决两类问题:

  • 执行依赖:确保某些命令在其他命令之前完成执行
  • 内存依赖:确保一个操作写入的数据能被后续操作正确读取
// 管线屏障的基本结构和使用
void basicPipelineBarrier(VkCommandBuffer cmdBuffer) {
    // 1. 创建内存屏障(以缓冲区为例)
    VkBufferMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
    
    // 2. 设置源和目标管线阶段
    barrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;          // 源阶段:传输阶段
    barrier.dstStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;     // 目标阶段:顶点着色器阶段
    
    // 3. 设置访问掩码(内存操作类型)
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;            // 源操作:写入传输
    barrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;     // 目标操作:读取顶点属性
    
    // 4. 指定受影响的缓冲区
    barrier.buffer = vertexBuffer;
    barrier.offset = 0;
    barrier.size = VK_WHOLE_SIZE;  // 整个缓冲区
    
    // 5. 应用管线屏障
    vkCmdPipelineBarrier(
        cmdBuffer,
        barrier.srcStageMask,    // 源管线阶段掩码
        barrier.dstStageMask,    // 目标管线阶段掩码
        0,                       // 依赖_flags
        0, nullptr,              // 内存屏障数量和指针(通用)
        1, &barrier,             // 缓冲区内存屏障数量和指针
        0, nullptr               // 图像内存屏障数量和指针
    );
}

管线屏障的工作原理可以概括为:在命令流中插入一个同步点,所有源阶段的操作必须在该点之前完成,所有目标阶段的操作必须在该点之后开始

timeline
    title 管线屏障工作原理
    section 命令执行流
        传输操作(写入) : 0, 10
        管线屏障 : 10, 10
        顶点着色器(读取) : 10, 20

2.2 管线屏障的核心参数解析

管线屏障的行为由多个关键参数共同决定,理解这些参数是正确使用管线屏障的基础。

2.2.1 管线阶段掩码(Stage Mask)

管线阶段掩码指定了屏障影响的GPU管线阶段,分为源阶段掩码(srcStageMask)和目标阶段掩码(dstStageMask):

  • 源阶段掩码:指定必须完成的操作所在的管线阶段
  • 目标阶段掩码:指定需要等待屏障的操作所在的管线阶段
// 管线阶段掩码的示例
void stageMaskExamples() {
    // 1. 图形管线阶段
    VkPipelineStageFlags vertexStage = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;
    VkPipelineStageFlags fragmentStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    
    // 2. 传输阶段
    VkPipelineStageFlags transferStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    
    // 3. 计算阶段
    VkPipelineStageFlags computeStage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
    
    // 4. 多个阶段的组合
    VkPipelineStageFlags combinedStages = 
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | 
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    
    // 5. 特殊阶段
    VkPipelineStageFlags topOfPipe = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;    // 管线最开始
    VkPipelineStageFlags bottomOfPipe = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; // 管线最末端
}

常见的管线阶段及其用途:

管线阶段说明典型用途
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT管线最开始阶段作为最早的依赖源
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT顶点输入阶段读取顶点数据
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT顶点着色器阶段顶点变换
VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT几何着色器阶段几何处理
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT片段着色器阶段像素处理
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT颜色附件输出阶段渲染到帧缓冲
VK_PIPELINE_STAGE_TRANSFER_BIT传输阶段数据复制操作
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT计算着色器阶段通用计算
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT管线最末端阶段作为最后的依赖目标
graph LR
    A[TOP_OF_PIPE] --> B[VERTEX_INPUT]
    B --> C[VERTEX_SHADER]
    C --> D[GEOMETRY_SHADER]
    D --> E[RASTERIZATION]
    E --> F[FRAGMENT_SHADER]
    F --> G[COLOR_ATTACHMENT_OUTPUT]
    H[TRANSFER] -. 并行 .- C
    I[COMPUTE_SHADER] -. 并行 .- F
    G --> J[BOTTOM_OF_PIPE]
2.2.2 访问掩码(Access Mask)

访问掩码指定了在对应管线阶段中发生的内存访问类型,同样分为源访问掩码(srcAccessMask)和目标访问掩码(dstAccessMask):

  • 源访问掩码:指定在源阶段中对资源进行的内存操作
  • 目标访问掩码:指定在目标阶段中对资源进行的内存操作
// 访问掩码的示例
void accessMaskExamples() {
    // 1. 读取操作
    VkAccessFlags readAccess = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;  // 读取顶点属性
    
    // 2. 写入操作
    VkAccessFlags writeAccess = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;  // 写入颜色附件
    
    // 3. 传输操作
    VkAccessFlags transferRead = VK_ACCESS_TRANSFER_READ_BIT;    // 传输读取
    VkAccessFlags transferWrite = VK_ACCESS_TRANSFER_WRITE_BIT;  // 传输写入
    
    // 4. 计算操作
    VkAccessFlags computeWrite = VK_ACCESS_SHADER_WRITE_BIT;     // 着色器写入(包括计算)
}

常见的访问掩码及其用途:

访问掩码说明关联的管线阶段
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT读取顶点属性数据VERTEX_INPUT
VK_ACCESS_UNIFORM_READ_BIT读取 uniform 数据VERTEX_SHADER, FRAGMENT_SHADER 等
VK_ACCESS_SHADER_READ_BIT着色器读取(非 uniform)VERTEX_SHADER, FRAGMENT_SHADER 等
VK_ACCESS_SHADER_WRITE_BIT着色器写入VERTEX_SHADER, FRAGMENT_SHADER, COMPUTE_SHADER 等
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT写入颜色附件COLOR_ATTACHMENT_OUTPUT
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT读取颜色附件COLOR_ATTACHMENT_OUTPUT
VK_ACCESS_TRANSFER_READ_BIT传输操作读取TRANSFER
VK_ACCESS_TRANSFER_WRITE_BIT传输操作写入TRANSFER
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT写入深度/模板附件EARLY_FRAGMENT_TESTS, LATE_FRAGMENT_TESTS
2.2.3 依赖 Flags

管线屏障还可以通过 flags 参数设置一些特殊行为:

  • VK_DEPENDENCY_BY_REGION_BIT:指定依赖关系是按区域的,即图像的不同区域可以独立处理,这允许GPU进行更多优化
  • VK_DEPENDENCY_DEVICE_GROUP_BIT:指定依赖关系跨越设备组中的多个物理设备
  • VK_DEPENDENCY_VIEW_LOCAL_BIT:指定依赖关系局限于单个视图
// 依赖 flags 示例
void dependencyFlagsExample(VkCommandBuffer cmdBuffer) {
    // 创建一个按区域的图像屏障,允许GPU对不同区域进行并行处理
    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    // ... 其他屏障参数设置
    
    // 应用按区域依赖 flag
    vkCmdPipelineBarrier(
        cmdBuffer,
        barrier.srcStageMask,
        barrier.dstStageMask,
        VK_DEPENDENCY_BY_REGION_BIT,  // 按区域依赖
        0, nullptr,
        0, nullptr,
        1, &barrier
    );
}

2.3 三种类型的管线屏障

Vulkan提供了三种类型的管线屏障,分别用于不同类型资源的同步:

  1. 通用内存屏障(Global Memory Barrier):用于全局范围的同步,不特定于某个资源
  2. 缓冲区内存屏障(Buffer Memory Barrier):用于缓冲区资源的同步
  3. 图像内存屏障(Image Memory Barrier):用于图像资源的同步
2.3.1 通用内存屏障

通用内存屏障不关联到特定资源,用于全局范围内的内存操作同步,例如:

  • 确保所有之前的写入操作对后续操作可见
  • 同步整个内存系统的状态
// 通用内存屏障示例
void globalMemoryBarrier(VkCommandBuffer cmdBuffer) {
    // 通用内存屏障没有关联特定资源,用于全局同步
    vkCmdPipelineBarrier(
        cmdBuffer,
        // 源阶段:所有可能写入内存的阶段
        VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
        // 目标阶段:所有可能读取内存的阶段
        VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
        0,
        1, nullptr,  // 使用通用内存屏障
        0, nullptr,  // 不使用缓冲区屏障
        0, nullptr   // 不使用图像屏障
    );
}

通用内存屏障的应用场景有限,因为它会同步所有资源,可能导致不必要的性能开销。通常更推荐使用针对特定资源的屏障。

2.3.2 缓冲区内存屏障

缓冲区内存屏障用于同步对缓冲区资源的访问:

// 缓冲区内存屏障完整示例
void bufferMemoryBarrierExample(VkCommandBuffer cmdBuffer, VkBuffer buffer) {
    // 1. 定义缓冲区内存屏障
    VkBufferMemoryBarrier bufferBarrier{};
    bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
    
    // 2. 设置源和目标阶段
    bufferBarrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
    bufferBarrier.dstStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;
    
    // 3. 设置访问掩码
    bufferBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;  // 传输写入
    bufferBarrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;  // 顶点读取
    
    // 4. 指定缓冲区和范围
    bufferBarrier.buffer = buffer;
    bufferBarrier.offset = 0;  // 起始偏移
    bufferBarrier.size = VK_WHOLE_SIZE;  // 整个缓冲区
    
    // 5. 应用屏障
    vkCmdPipelineBarrier(
        cmdBuffer,
        bufferBarrier.srcStageMask,
        bufferBarrier.dstStageMask,
        0,
        0, nullptr,  // 不使用通用屏障
        1, &bufferBarrier,  // 使用缓冲区屏障
        0, nullptr   // 不使用图像屏障
    );
}

缓冲区屏障的典型应用流程:

  1. CPU将数据传输到缓冲区(传输阶段,写入操作)
  2. 插入缓冲区屏障
  3. 顶点着色器从缓冲区读取数据(顶点着色器阶段,读取操作)
sequenceDiagram
    participant CPU
    participant Transfer as 传输阶段
    participant Barrier as 缓冲区屏障
    participant VertexShader as 顶点着色器阶段
    
    CPU->>Transfer: 写入数据到缓冲区
    Transfer->>Barrier: 完成写入
    Barrier->>VertexShader: 允许读取
    VertexShader->>VertexShader: 从缓冲区读取数据
2.3.3 图像内存屏障

图像内存屏障用于同步对图像资源的访问,它比缓冲区屏障更复杂,因为图像有更多的状态(布局)需要管理:

// 图像内存屏障完整示例:从颜色附件

// 图像内存屏障完整示例:从颜色附件输出切换到着色器可读格式
void imageMemoryBarrierExample(VkCommandBuffer cmdBuffer, VkImage image) {
    // 1. 定义图像内存屏障
    VkImageMemoryBarrier imageBarrier{};
    imageBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    
    // 2. 设置源和目标阶段
    imageBarrier.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    imageBarrier.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    
    // 3. 设置访问掩码
    imageBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;  // 颜色附件写入
    imageBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;             // 着色器读取
    
    // 4. 图像布局转换(重要!)
    imageBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;  // 旧布局
    imageBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;  // 新布局
    
    // 5. 指定图像和子资源范围
    imageBarrier.image = image;
    imageBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;  // 颜色通道
    imageBarrier.subresourceRange.baseMipLevel = 0;                        // 基础MIP级别
    imageBarrier.subresourceRange.levelCount = 1;                          // MIP级别数量
    imageBarrier.subresourceRange.baseArrayLayer = 0;                      // 基础数组层
    imageBarrier.subresourceRange.layerCount = 1;                          // 数组层数量
    
    // 6. 应用屏障
    vkCmdPipelineBarrier(
        cmdBuffer,
        imageBarrier.srcStageMask,
        imageBarrier.dstStageMask,
        0,
        0, nullptr,   // 不使用通用屏障
        0, nullptr,   // 不使用缓冲区屏障
        1, &imageBarrier  // 使用图像屏障
    );
}

图像内存屏障与缓冲区屏障的主要区别在于图像布局转换。Vulkan中的图像布局决定了图像在内存中的存储方式,不同的操作需要不同的布局:

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:优化用于颜色附件写入
  • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:优化用于着色器读取
  • VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:优化用于传输源
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:优化用于传输目标
  • VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:优化用于深度/模板附件
  • VK_IMAGE_LAYOUT_UNDEFINED:未定义布局,通常用于初始状态
graph TD
    A[UNDEFINED] --> B[COLOR_ATTACHMENT_OPTIMAL]
    A --> C[TRANSFER_DST_OPTIMAL]
    B --> D[SHADER_READ_ONLY_OPTIMAL]
    B --> E[TRANSFER_SRC_OPTIMAL]
    C --> D
    C --> E
    D --> F[TRANSFER_SRC_OPTIMAL]
    E --> B

图像屏障的典型应用流程:

  1. 渲染到图像作为颜色附件(颜色附件输出阶段,写入操作)
  2. 插入图像屏障,将图像布局从VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL转换为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
  3. 片段着色器读取该图像作为纹理(片段着色器阶段,读取操作)
sequenceDiagram
    participant RenderPass as 渲染通道
    participant Barrier as 图像屏障
    participant FragmentShader as 片段着色器
    
    RenderPass->>RenderPass: 渲染到图像(颜色附件)
    RenderPass->>Barrier: 完成渲染
    Barrier->>Barrier: 转换图像布局
    Barrier->>FragmentShader: 允许读取
    FragmentShader->>FragmentShader: 采样图像作为纹理

2.4 管线屏障的高级应用

2.4.1 多资源同步

一个管线屏障调用可以同时同步多个资源,只需在屏障调用中包含多个内存屏障:

// 同时同步多个```cpp
// 同时同步多个资源的管线屏障
void multiResourceBarrierExample(VkCommandBuffer cmdBuffer, VkBuffer buffer, VkImage image) {
    // 1. 准备缓冲区屏障
    VkBufferMemoryBarrier bufferBarrier{};
    bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
    bufferBarrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
    bufferBarrier.dstStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;
    bufferBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    bufferBarrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;
    bufferBarrier.buffer = buffer;
    bufferBarrier.offset = 0;
    bufferBarrier.size = VK_WHOLE_SIZE;
    
    // 2. 准备图像屏障
    VkImageMemoryBarrier imageBarrier{};
    imageBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    imageBarrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
    imageBarrier.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    imageBarrier.srcAccessMask = VK_ACCESS_ACCESS_TRANSFER_WRITE_BIT;
    imageBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    imageBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    imageBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageBarrier.image = image;
    imageBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
    
    // 3. 在一个屏障调用中同步多个资源
    vkCmdPipelineBarrier(
        cmdBuffer,
        // 源阶段掩码是所有源阶段的组合
        VK_PIPELINE_STAGE_TRANSFER_BIT,
        // 目标阶段掩码是所有目标阶段的组合
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        0,
        0, nullptr,
        1, &bufferBarrier,  // 缓冲区屏障
        1, &imageBarrier    // 图像屏障
    );
}

同时同步多个资源可以减少屏障调用的数量,提高效率。但需要注意的是,所有资源都将使用相同的源和目标阶段掩码,因此只有当多个资源需要相同的同步条件时才适合这样做。

graph
    A[管线屏障] --> B[缓冲区同步]
    A --> C[图像同步]
    B --> D[顶点着色器读取]
    C --> E[片段着色器读取]
2.4.2 子资源范围同步

对于大型图像或缓冲区,可以只同步部分子资源,而不是整个资源,从而提高性能:

// 同步图像的子资源范围
void imageSubresourceBarrier(VkCommandBuffer cmdBuffer, VkImage image) {
    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    barrier.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    barrier.image = image;
    
    // 只同步特定的MIP级别和数组层
    barrierrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    barrier.subresourceRange.baseMipLevel = 1;  // 从MIP级别1开始
    barrier.subresourceRange.levelCount = 2;    // 同步2个MIP级别
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 1;
    
    vkCmdPipelineBarrier(
        cmdBuffer,
        barrier.srcStageMask,
        barrier.dstStageMask,
        0,
        0, nullptr,
        0, nullptr,
        1, &barrier
    );
}

子资源同步特别适用于:

  • 大型纹理的部分更新
  • 纹理数组的单个图层更新
  • MIP映射的部分生成
graph TD
    A[完整图像] --> B[MIP级别0]
    A --> C[MIP级别1]
    A --> D[MIP级别2]
    A --> E[MIP级别3]
    B --> F[数组层0]
    B --> G[数组层1]
    C --> H[数组层0]
    C --> I[数组层1]
    
    style C fill:#ff9999,stroke:#333
    style H fill:#ff9999,stroke:#333
    style I fill:#ff9999,stroke:#333
2.4.3 复杂场景中的屏障组合

在复杂渲染场景中,通常需要组合使用多个管线屏障来处理不同的同步需求:

// 复杂场景中的多个屏障组合
void complexBarrierSequence(VkCommandBuffer cmdBuffer, VkBuffer buffer, VkImage image1, VkImage image2) {
    // 屏障1:同步缓冲区传输到顶点着色器
    VkBufferMemoryBarrier bufferBarrier{};
    // ... 设置缓冲区屏障参数 ...
    
    vkCmdPipelineBarrier(
        cmdBuffer,
        VK_PIPELINE_STAGE_TRANSFER_BIT,
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        0,
        0, nullptr,
        1, &bufferBarrier,
        0, nullptr
    );
    
    // 记录顶点着色器相关命令...
    
    // 屏障2:同步渲染到图像1完成
    VkImageMemoryBarrier imageBarrier1{};
    // ... 设置图像1屏障参数 ...
    
    vkCmdPipelineBarrier(
        cmdBuffer,
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
        0,
        0, nullptr,
        0, nullptr,
        1, &imageBarrier1
    );
    
    // 记录计算着色器使用图像1的命令...
    
    // 屏障3:同步计算结果到图像2
    VkImageMemoryBarrier imageBarrier2{};
    // ... 设置图像2屏障参数 ...
    
    vkCmdPipelineBarrier(
        cmdBuffer,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        0,
        0, nullptr,
        0, nullptr,
        1, &imageBarrier2
    );
    
    // 记录片段着色器使用图像2的命令...
}

复杂场景中的屏障序列通常遵循渲染流程的自然阶段:

  1. 资源加载和传输阶段
  2. 几何处理阶段
  3. 光照和计算阶段
  4. 最终渲染阶段

每个阶段之间插入适当的屏障,确保数据正确传递。

timeline
    title 复杂场景中的屏障序列
    section 命令流
        数据传输 : 0, 10
        缓冲区屏障 : 10, 10
        顶点处理 : 10, 20
        图像屏障1 : 20, 20
        计算处理 : 20, 35
        图像屏障2 : 35, 35
        片段处理 : 35, 50

2.5 管线屏障的性能考量

虽然管线屏障是确保正确性的必要手段,但过多或不当使用会严重影响性能。以下是一些性能优化建议:

2.5.1 最小化屏障数量

合并可以同时进行的同步操作,减少屏障调用次数:

// 不推荐:多个单独的屏障
void inefficientBarriers(VkCommandBuffer cmdBuffer, VkBuffer buf, VkImage img) {
    // 单独的缓冲区屏障
    vkCmdPipelineBarrier(cmdBuffer, ...);  // 仅同步缓冲区
    
    // 单独的图像屏障
    vkCmdPipelineBarrier(cmdBuffer, ...);  // 仅同步图像
}

// 推荐:合并为一个屏障
void efficientBarriers(VkCommandBuffer cmdBuffer, VkBuffer buf, VkImage img) {
    // 同时同步缓冲区和图像
    vkCmdPipelineBarrier(
        cmdBuffer,
        combinedSrcStages,  // 组合的源阶段
        combinedDstStages,  // 组合的目标阶段
        0,
        0, nullptr,
        1, &bufferBarrier,  // 缓冲区屏障
        1, &imageBarrier    // 图像屏障
    );
}
2.5.2 精确指定阶段和访问掩码

避免使用过于宽泛的阶段和访问掩码,如VK_PIPELINE_STAGE_ALL_COMMANDS_BIT

// 不推荐:过于宽泛的阶段掩码
void broadStageMask(VkCommandBuffer cmdBuffer, VkBuffer buffer) {
    VkBufferMemoryBarrier barrier{};
    barrier.srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;  // 过于宽泛
    barrier.dstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;  // 过于宽泛
    // ... 其他参数 ...
    
    vkCmdPipelineBarrier(cmdBuffer, ...);
}

// 推荐:精确的阶段掩码
void preciseStageMask(VkCommandBuffer cmdBuffer, VkBuffer buffer) {
    VkBufferMemoryBarrier barrier{};
    barrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;  // 精确的源阶段
    barrier.dstStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;  // 精确的目标阶段
    // ... 其他参数 ...
    
    vkCmdPipelineBarrier(cmdBuffer, ...);
}

过于宽泛的掩码会导致不必要的同步等待,降低GPU并行性。

2.5.3 利用按区域依赖

对于图像操作,尽可能使用VK_DEPENDENCY_BY_REGION_BIT flag,允许GPU对图像的不同区域进行并行处理:

// 使用按区域依赖提高并行性
void byRegionDependency(VkCommandBuffer cmdBuffer, VkImage image) {
    VkImageMemoryBarrier barrier{};
    // ... 其他参数设置 ...
    
    // 应用按区域依赖
    vkCmdPipelineBarrier(
        cmdBuffer,
        barrier.srcStageMask,
        barrier.dstStageMask,
        VK_DEPENDENCY_BY_REGION_BIT,  // 允许区域并行
        0, nullptr,
        0, nullptr,
        1, &barrier
    );
}

按区域依赖特别适用于:

  • 分块渲染(Tiled Rendering)
  • 图像的局部更新
  • 并行的图像过滤操作
graph TD
    A[图像] --> B[区域1]
    A --> C[区域2]
    A --> D[区域3]
    A --> E[区域4]
    
    B --> F[独立处理]
    C --> G[独立处理]
    D --> H[独立处理]
    E --> I[独立处理]
2.5.4 避免不必要的布局转换

图像布局转换可能会产生性能开销,应避免不必要的转换:

// 不推荐:不必要的布局转换
void unnecessaryLayoutTransition(VkCommandBuffer cmdBuffer, VkImage image) {
    // 第一次转换:COLOR_ATTACHMENT -> SHADER_READ
    transitionImageLayout(cmdBuffer, image, 
                        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
                        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
    
    // 第二次转换:SHADER_READ -> COLOR_ATTACHMENT(不必要)
    transitionImageLayout(cmdBuffer, image,
                        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
                        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
}

// 推荐:重用现有布局
void reuseImageLayout(VkCommandBuffer cmdBuffer, VkImage image, VkImageLayout currentLayout) {
    if (currentLayout != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
        // 仅在需要时转换
        transitionImageLayout(cmdBuffer, image,
                            currentLayout,
                            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
    }
    // 使用图像...
}

通过合理规划渲染流程,可以最大限度地减少图像布局转换的次数。

2.6 管线屏障常见错误与解决方案

管线屏障是Vulkan中最容易出错的部分之一,以下是一些常见错误及其解决方案:

2.6.1 阶段掩码不匹配

错误:源阶段与目标阶段之间没有数据依赖关系,导致同步无效。

// 错误示例:阶段掩码不匹配
void mismatchedStageMasks(VkCommandBuffer cmdBuffer) {
    VkBufferMemoryBarrier barrier{};
    barrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;  // 源阶段:传输
    barrier.dstStageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;  // 目标阶段:计算
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;  // 不匹配计算阶段
    // ... 其他参数 ...
    
    vkCmdPipelineBarrier(cmdBuffer, ...);
}

解决方案:确保访问掩码与目标阶段匹配:

// 正确示例:阶段与访问掩码匹配
void matchedStageMasks(VkCommandBuffer cmdBuffer) {
    VkBufferMemoryBarrier barrier{};
    barrier.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
    barrier.dstStageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;  // 与计算阶段匹配
    // ... 其他参数 ...
    
    vkCmdPipelineBarrier(cmdBuffer, ...);
}
2.6.2 缺少必要的屏障

错误:在需要同步的操作之间没有插入屏障,导致数据竞争。

// 错误示例:缺少屏障
void missingBarrier(VkCommandBuffer cmdBuffer, VkBuffer buffer) {
    // 1. 写入缓冲区
    vkCmdUpdateBuffer(cmdBuffer, buffer, 0, size, data);
    
    // 缺少屏障!
    
    // 2. 立即读取缓冲区(可能读取到旧数据)
    vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &buffer, &offset);
    vkCmdDraw(cmdBuffer, ...);
}

解决方案:在写入和读取操作之间插入适当的屏障:

// 正确示例:添加必要的屏障
void correctBarrier(VkCommandBuffer cmdBuffer, VkBuffer buffer) {
    // 1. 写入缓冲区
    vkCmdUpdateBuffer(cmdBuffer, buffer, 0, size, data);
    
    // 2. 添加屏障
    VkBufferMemoryBarrier barrier{};
    // ... 设置屏障参数 ...
    vkCmdPipelineBarrier(cmdBuffer, ...);
    
    // 3. 读取缓冲区(确保读取到新数据)
    vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &buffer, &offset);
    vkCmdDraw(cmdBuffer, ...);
}
2.6.3 图像布局转换错误

错误:图像布局转换路径不正确,导致渲染错误或性能问题。

// 错误示例:不正确的布局转换
void incorrectLayoutTransition(VkCommandBuffer cmdBuffer, VkImage image) {
    VkImageMemoryBarrier barrier{};
    barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
    // 缺少必要的阶段和访问掩码设置
    barrier.srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    barrier.dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
    // ... 其他参数 ...
    
    vkCmdPipelineBarrier(cmdBuffer, ...);
}

解决方案:使用正确的阶段和访问掩码进行布局转换:

// 正确示例:正确的布局转换
void correctLayoutTransition(VkCommandBuffer cmdBuffer, VkImage image) {
    VkImageMemoryBarrier barrier{};
    barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
    // 设置正确的阶段和访问掩码
    barrier.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    barrier.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
    barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
    // ... 其他参数 ...
    
    vkCmdPipelineBarrier(cmdBuffer, ...);
}
2.6.4 过度同步

错误:使用过多的屏障或过于严格的同步条件,降低性能。

// 错误示例:过度同步
void overSynchronization(VkCommandBuffer cmdBuffer, VkBuffer buffer) {
    // 第一个屏障
    vkCmdPipelineBarrier(cmdBuffer, ...);
    
    // 几乎没有命令...
    
    // 第二个不必要的屏障
    vkCmdPipelineBarrier(cmdBuffer, ...);
}

解决方案:仅在必要时使用屏障,合并可以同时同步的操作:

// 正确示例:最小化同步
void minimalSynchronization(VkCommandBuffer cmdBuffer, VkBuffer buffer, VkImage image) {
    // 合并屏障,同时同步多个资源
    vkCmdPipelineBarrier(
        cmdBuffer,
        combinedSrcStages,
        combinedDstStages,
        0,
        0, nullptr,
        1, &bufferBarrier,
        1, &imageBarrier
    );
}

管线屏障是Vulkan同步机制的基础,正确理解和使用管线屏障对于开发高效、正确的Vulkan应用至关重要。下一章将介绍用于队列间同步的信号量机制。

三、SEMAPHORE(信号量)原理解析

3.1 信号量的基本概念

Semaphore(信号量)是Vulkan中用于不同队列之间队列与呈现引擎之间同步的机制。与管线屏障不同,信号量不直接控制管线阶段或资源访问,而是通过信号(signal)和等待(wait)机制协调不同操作序列的执行顺序。

信号量的工作原理基于简单的二元状态:

  • 未触发(unsignaled):初始状态,等待操作会阻塞
  • 已触发(signaled):操作完成后进入此状态,会释放等待的操作

信号量主要用于以下场景:

  • 图形队列与计算队列之间的同步
  • 图形队列与传输队列之间的同步
  • 应用程序与呈现引擎之间的同步(如交换链操作)
// 信号量的基本使用流程
void basicSemaphoreUsage(VkDevice device, VkQueue graphicsQueue, VkQueue computeQueue) {
    // 1. 创建信号量
    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    
    VkSemaphore computeDoneSemaphore;
    if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &computeDoneSemaphore) != VK_SUCCESS) {
        throw std::runtime_error("无法创建信号量!");
    }
    
    // 2. 录制计算命令缓冲区
    VkCommandBuffer computeCmdBuffer = recordComputeCommands();
    
    // 3. 提交计算命令,并设置信号量在计算完成后触发
    VkSubmitInfo computeSubmit{};
    computeSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    computeSubmit.commandBufferCount = 1;
    computeSubmit.pCommandBuffers = &computeCmdBuffer;
    computeSubmit.signalSemaphoreCount = 1;
    computeSubmit.pSignalSemaphores = &computeDoneSemaphore;  // 计算完成后触发
    
    vkQueueSubmit(computeQueue, 1, &computeSubmit, VK_NULL_HANDLE);
    
    // 4. 录制图形命令缓冲区
    VkCommandBuffer graphicsCmdBuffer = recordGraphicsCommands();
    
    // 5. 提交图形命令,并设置等待信号量触发
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_VERTEX_SHADER_BIT};
    
    VkSubmitInfo graphicsSubmit{};
    graphicsSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    graphicsSubmit.waitSemaphoreCount = 1;
    graphicsSubmit.pWaitSemaphores = &computeDoneSemaphore;  // 等待计算完成信号量
    graphicsSubmit.pWaitDstStageMask = waitStages;           // 等待的管线阶段
    graphicsSubmit.commandBufferCount = 1;
    graphicsSubmit.pCommandBuffers = &graphicsCmdBuffer;
    
    vkQueueSubmit(graphicsQueue, 1, &graphicsSubmit, VK_NULL_HANDLE);
    
    // 6. 清理
    vkQueueWaitIdle(graphicsQueue);
    vkQueueWaitIdle(computeQueue);
    vkDestroySemaphore(device, computeDoneSemaphore, nullptr);
}

信号量的工作流程可以概括为:

  1. 一个队列提交命令时指定要信号的信号量
  2. 另一个队列提交命令时指定要等待的信号量
  3. 当第一个队列的命令执行完成,信号量被触发
  4. 第二个队列在指定的管线阶段等待信号量,一旦信号量被触发就继续执行
timeline
    title 信号量工作流程
    section 计算队列
        执行计算命令 : 0, 20
        触发信号量 : 20, 20
    section 图形队列
        等待信号量 : 10, 20
        执行图形命令 : 20, 40

3.2 信号量的创建与销毁

信号量的创建过程相对简单,不需要复杂的参数配置:

// 信号量创建函数
VkSemaphore createSemaphore(VkDevice device) {
    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    
    // 可选:使用VkSemaphoreTypeCreateInfo指定信号量类型(Vulkan 1.2+)
    VkSemaphoreTypeCreateInfo semaphoreTypeInfo{};
    semaphoreTypeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
    semaphoreTypeInfo.semaphoreType = VK_SEMAPHORE_TYPE_BINARY;  // 二元信号量(默认)
    semaphoreTypeInfo.initialValue = 0;  // 初始值(0表示未触发)
    
    semaphoreInfo.pNext = &semaphoreTypeInfo;
    
    VkSemaphore semaphore;
    if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &semaphore) != VK_SUCCESS) {
        throw std::runtime_error("创建信号量失败!");
    }
    
    return semaphore;
}

// 信号量销毁函数
void destroySemaphore(VkDevice device, VkSemaphore semaphore) {
    vkDestroySemaphore(device, semaphore, nullptr);
}

Vulkan 1.2引入了两种信号量类型:

  • 二元信号量(VK_SEMAPHORE_TYPE_BINARY):最常用的类型,只有未触发和已触发两种状态
  • ** timeline信号量(VK_SEMAPHORE_TYPE_TIMELINE)**:具有单调递增的64位值,可用于更精细的同步控制
graph TD
    A[VkSemaphore] --> B[二元信号量<br/>VK_SEMAPHORE_TYPE_BINARY]
    A --> C[Timeline信号量<br/>VK_SEMAPHORE_TYPE_TIMELINE]
    B --> B1[未触发状态]
    B --> B2[已触发状态]
    C --> C1[64位单调递增值]
    C --> C2[可等待特定值]

3.3 二元信号量详解

二元信号量是最常用的信号量类型,它只有两种状态:未触发(unsignaled)和已触发(signaled)。

3.3.1 二元信号量的生命周期

二元信号量的典型生命周期:

  1. 创建时处于未触发状态
  2. 当关联的命令提交完成后,被自动触发(信号操作)
  3. 当等待它的命令开始执行时,被自动重置为未触发状态(等待操作)
// 二元信号量的完整使用示例
void binarySemaphoreExample(VkDevice device, VkQueue queueA, VkQueue queueB) {
    // 创建二元信号量
    VkSemaphore semaphore = createSemaphore(device);
    
    // 队列A的命令缓冲区
    VkCommandBuffer cmdBufferA = createCommandBufferA();
    
    // 提交队列A的命令,并在完成时触发信号量
    VkSubmitInfo submitA{};
    submitA.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitA.commandBufferCount = 1;
    submitA.pCommandBuffers = &cmdBufferA;
    submitA.signalSemaphoreCount = 1;
    submitA.pSignalSemaphores = &semaphore;
    
    vkQueueSubmit(queueA, 1, &submitA, VK_NULL_HANDLE);
    
    // 队列B的命令缓冲区
    VkCommandBuffer cmdBufferB = createCommandBufferB();
    
    // 提交队列B的命令,等待信号量触发
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT};
    
    VkSubmitInfo submitB{};
    submitB.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitB.waitSemaphoreCount = 1;
    submitB.pWaitSemaphores = &semaphore;
    submitB.pWaitDstStageMask = waitStages;
    submitB.commandBufferCount = 1;
    submitB.pCommandBuffers = &cmdBufferB;
    
    vkQueueSubmit(queueB, 1, &submitB, VK_NULL_HANDLE);
    
    // 等待所有操作完成
    vkQueueWaitIdle(queueA);
    vkQueueWaitIdle(queueB);
    
    // 清理
    destroyCommandBuffer(device, cmdBufferA);
    destroyCommandBuffer(device, cmdBufferB);
    destroySemaphore(device, semaphore);
}

二元信号量的状态转换:

stateDiagram
    [*] --> Unsignaled: 创建
    Unsignaled --> Signaled: 信号操作(命令完成)
    Signaled --> Unsignaled: 等待操作(开始执行)
    Unsignaled --> [*]: 销毁
    Signaled --> [*]: 销毁
3.3.2 多个信号量的使用

可以同时使用多个信号量来实现更复杂的同步逻辑:

// 多个信号量的使用示例
void multipleSemaphoresExample(VkDevice device, VkQueue graphicsQueue, VkQueue computeQueue, VkQueue transferQueue) {
    // 创建两个信号量
    VkSemaphore computeSemaphore = createSemaphore(device);
    VkSemaphore transferSemaphore = createSemaphore(device);
    
    // 1. 提交计算命令,完成后触发computeSemaphore
    VkCommandBuffer computeCmd = recordComputeCommands();
    VkSubmitInfo computeSubmit{};
    // ... 设置计算提交信息 ...
    computeSubmit.signalSemaphoreCount = 1;
    computeSubmit.pSignalSemaphores = &computeSemaphore;
    vkQueueSubmit(computeQueue, 1, &computeSubmit, VK_NULL_HANDLE);
    
    // 2. 提交传输命令,完成后触发transferSemaphore
    VkCommandBuffer transferCmd = recordTransferCommands();
    VkSubmitInfo transferSubmit{};
    // ... 设置传输提交信息 ...
    transferSubmit.signalSemaphoreCount = 1;
    transferSubmit.pSignalSemaphores = &transferSemaphore;
    vkQueueSubmit(transferQueue, 1, &transferSubmit, VK_NULL_HANDLE);
    
    // 3. 提交图形命令,等待两个信号量都触发
    VkCommandBuffer graphicsCmd = recordGraphicsCommands();
    
    // 要等待的信号量
    VkSemaphore waitSemaphores[] = {computeSemaphore, transferSemaphore};
    // 每个信号量对应的等待阶段
    VkPipelineStageFlags waitStages[] = {
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
    };
    
    VkSubmitInfo graphicsSubmit{};
    graphicsSubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    graphicsSubmit.waitSemaphoreCount = 2;
    graphicsSubmit.pWaitSemaphores = waitSemaphores;
    graphicsSubmit.pWaitDstStageMask = waitStages;
    graphicsSubmit.commandBufferCount = 1;
    graphicsSubmit.pCommandBuffers = &graphicsCmd;
    
    vkQueueSubmit(graphicsQueue, 1, &graphicsSubmit, VK_NULL_HANDLE);
    
    // 等待完成并清理
    // ...
}

多个信号量的等待是逻辑与的关系:只有当所有等待的信号量都被触发时,命令才会继续执行。

timeline
    title 多个信号量同步
    section 计算队列
        计算操作 : 0, 20
        触发computeSemaphore : 20, 20
    section 传输队列
        传输操作 : 0, 15
        触发transferSemaphore : 15, 15
    section 图形队列
        等待两个信号量 : 10, 20
        执行图形命令 : 20, 40

3.4 Timeline信号量详解

Vulkan 1.2引入的Timeline信号量(时间线信号量)提供了比二元信号量更灵活的同步机制。它使用64位整数作为计数器,支持更精细的同步控制。

3.4.1 Timeline信号量的基本使用

Timeline信号量通过一个单调递增的64位值来控制同步:

// Timeline信号量的基本使用
void timelineSemaphoreBasic(VkDevice device, VkQueue queueA, VkQueue queueB) {
    // 1. 创建Timeline信号量,初始值为0
    VkSemaphoreTypeCreateInfo timelineInfo{};
    timelineInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
    timelineInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
    timelineInfo.initialValue = 0;  // 初始值
    
    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    semaphoreInfo.pNext = &timelineInfo;
    
    VkSemaphore timelineSemaphore;
    if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &timelineSemaphore) != VK_SUCCESS) {
        throw std::runtime_error("创建Timeline信号量失败!");
    }
    
    // 2. 录制队列A的命令缓冲区
    VkCommandBuffer cmdBufferA = recordCommandBufferA();
    
    // 3. 提交队列A的命令,完成后将信号量值设置为1
    VkSemaphoreSignalInfo signalInfo{};
    signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO;
    signalInfo.semaphore = timelineSemaphore;
    signalInfo.value = 1;  // 信号量将被设置为1
    
    VkSubmitInfo submitA{};
    submitA.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitA.commandBufferCount = 1;
    submitA.pCommandBuffers = &cmdBufferA;
    submitA.signalSemaphoreCount = 1;
    submitA.pSignalSemaphores = &timelineSemaphore;
    submitA.pSignalSemaphoreValues = &signalInfo.value;  // 指定要设置的值
    
    vkQueueSubmit(queueA, 1, &submitA, VK_NULL_HANDLE);
    
    // 4. 录制队列B的命令缓冲区
    VkCommandBuffer cmdBufferB = recordCommandBufferB();
    
    // 5. 提交队列B的命令,等待信号量值达到1
    VkSemaphoreWaitInfo waitInfo{};
    waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
    waitInfo.semaphoreCount = 1;
    waitInfo.pSemaphores = &timelineSemaphore;
    uint64_t waitValue = 1;
    waitInfo.pValues = &waitValue;  // 等待值达到1
    
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT};
    
    VkSubmitInfo submitB{};
    submitB.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitB.waitSemaphoreCount = 1;
    submitB.pWaitSemaphores = &timelineSemaphore;
    submitB.pWaitDstStageMask = waitStages;
    submitB.pWaitSemaphoreValues = &waitValue;  // 指定等待值
    submitB.commandBufferCount = 1;
    submitB.pCommandBuffers = &cmdBufferB;
    
    vkQueueSubmit(queueB, 1, &submitB, VK_NULL_HANDLE);
    
    // 6. 等待完成并清理
    vkQueueWaitIdle(queueA);
    vkQueueWaitIdle(queueB);
    vkDestroySemaphore(device, timelineSemaphore, nullptr);
}

Timeline信号量的核心优势是可以等待特定的值,而不仅仅是"已触发"状态,这使得它可以用于更复杂的同步场景。

3.4.2 Timeline信号量的高级特性

Timeline信号量支持一些二元信号量不具备的高级特性:

  1. 直接从CPU触发:不需要提交命令就可以更新Timeline信号量的值
// 从CPU直接更新Timeline信号量的值
void cpuSignalTimeline(VkDevice device, VkSemaphore timelineSemaphore) {
    VkSemaphoreSignalInfo signalInfo{};
    signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO;
    signalInfo.semaphore = timelineSemaphore;
    signalInfo.value = 5;  // 将信号量值设置为5
    
    // 直接从CPU触发信号量更新
    if (vkSignalSemaphore(device, &signalInfo) != VK_SUCCESS) {
        throw std::runtime_error("CPU触发Timeline信号量失败!");
    }
}
  1. CPU等待Timeline信号量:CPU可以直接等待Timeline信号量达到特定值
// CPU等待Timeline信号量达到特定值
void cpuWaitTimeline(VkDevice device, VkSemaphore timelineSemaphore) {
    VkSemaphoreWaitInfo waitInfo{};
    waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO;
    waitInfo.semaphoreCount = 1;
    waitInfo.pSemaphores = &timelineSemaphore;
    uint64_t waitValue = 3;
    waitInfo.pValues = &waitValue;
    
    // 等待信号量达到值3,超时时间为1秒
    VkResult result = vkWaitSemaphores(device, &waitInfo, 1000000000);
    if (result == VK_TIMEOUT) {
        std::cout << "等待超时!" << std::endl;
    } else if (result != VK_SUCCESS) {
        throw std::runtime_error("等待Timeline信号量失败!");
    }
}
  1. 查询当前值:可以查询Timeline信号量的当前值
// 查询Timeline信号量的当前值
uint64_t getTimelineValue(VkDevice device, VkSemaphore timelineSemaphore) {
    uint64_t currentValue;
    if (vkGetSemaphoreCounterValue(device, timelineSemaphore, &currentValue) != VK_SUCCESS) {
        throw std::runtime_error("查询Timeline信号量值失败!");
    }
    return currentValue;
}

这些特性使得Timeline信号量非常适合CPU与GPU之间的细粒度同步,以及需要跟踪多个同步点的复杂场景。

graph TD
    A[Timeline信号量高级特性] --> B[CPU直接触发]
    A --> C[CPU直接等待]
    A --> D[查询当前值]
    B --> B1[无需提交命令]
    B --> B2[即时更新值]
    C --> C1[CPU等待GPU进度]
    C --> C2[设置超时时间]
    D --> D1[监控同步进度]
    D --> D2[动态调整等待条件]
3.4.3 Timeline信号量的多阶段同步

Timeline信号量特别适合需要多阶段同步的场景,例如复杂的渲染管线:

// 使用Timeline信号量进行多阶段同步
void multiStageSynchronization(VkDevice device, VkQueue queue) {
    // 创建Timeline信号量,初始值0
    VkSemaphore timelineSemaphore = createTimelineSemaphore(device, 0);
    
    // 阶段1:加载资源
    VkCommandBuffer loadCmd = recordLoadCommands();
    submitCommandWithTimelineSignal(queue, loadCmd, timelineSemaphore, 1);
    
    // 阶段2:生成光照贴图
    VkCommandBuffer lightmapCmd = recordLightmapCommands();
    submitCommandWithTimelineWaitAndSignal(
        queue, lightmapCmd, 
        timelineSemaphore, 1,  // 等待值1
        timelineSemaphore, 2   // 信号值2
    );
    
    // 阶段3:渲染场景
    VkCommandBuffer renderCmd = recordRenderCommands();
    submitCommandWithTimelineWaitAndSignal(
        queue, renderCmd,
        timelineSemaphore, 2,  // 等待值2
        timelineSemaphore, 3   // 信号值3
    );
    
    // 阶段4:后期处理
    VkCommandBuffer postProcessCmd = recordPostProcessCommands();
    submitCommandWithTimelineWaitAndSignal(
        queue, postProcessCmd,
        timelineSemaphore, 3,  // 等待值3
        timelineSemaphore, 4   // 信号值4
    );
    
    // CPU等待所有阶段完成
    cpuWaitTimeline(device, timelineSemaphore, 4);
    
    // 清理
    // ...
}

多阶段同步的时间线:

timeline
    title Timeline信号量多阶段同步
    section 信号量值
        初始值0 : 0, 0
        达到值1(加载完成) : 10, 10
        达到值2(光照完成) : 25, 25
        达到值3(渲染完成) : 45, 45
        达到值4(后期完成) : 55, 55
    section 命令执行
        加载资源 : 0, 10
        生成光照贴图 : 10, 25
        渲染场景 : 25, 45
        后期处理 : 45, 55

3.5 信号量与交换链同步

信号量最常见的应用场景之一是与交换链(Swapchain)配合,同步应用程序与呈现引擎的操作。

3.5.1 交换链图像获取与呈现同步

使用信号量确保正确的图像获取和呈现顺序:

// 交换链同步示例
void swapchainSynchronization(VkDevice device, VkQueue presentQueue, VkSwapchainKHR swapchain, 
                             VkCommandBuffer cmdBuffer, VkFramebuffer framebuffer) {
    // 创建两个信号量:一个用于图像可用,一个用于渲染完成
    VkSemaphore imageAvailableSemaphore = createSemaphore(device);
    VkSemaphore renderFinishedSemaphore = createSemaphore(device);
    
    // 1. 获取交换链图像,等待图像可用
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, 
                         imageAvailableSemaphore,  // 图像可用时触发
                         VK_NULL_HANDLE, &imageIndex);
    
    // 2. 提交渲染命令,等待图像可用信号量
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
    
    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = &imageAvailableSemaphore;
    submitInfo.pWaitDstStageMask = waitStages;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &cmdBuffer;
    submitInfo.signalSemaphoreCount = 1;
    submitInfo.pSignalSemaphores = &renderFinishedSemaphore;  // 渲染完成时触发
    
    vkQueueSubmit(presentQueue, 1, &submitInfo, VK_NULL_HANDLE);
    
    // 3. 呈现图像,等待渲染完成信号量
    VkPresentInfoKHR presentInfo{};
    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    presentInfo.waitSemaphoreCount = 1;
    presentInfo.pWaitSemaphores = &renderFinishedSemaphore;  // 等待渲染完成
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapchain;
    presentInfo.pImageIndices = &imageIndex;
    
    vkQueuePresentKHR(presentQueue, &presentInfo);
    
    // 4. 等待完成并清理
    vkQueueWaitIdle(presentQueue);
    vkDestroySemaphore(device, imageAvailableSemaphore, nullptr);
    vkDestroySemaphore(device, renderFinishedSemaphore, nullptr);
}

交换链同步的工作流程:

  1. vkAcquireNextImageKHR获取图像,当图像可用时触发imageAvailableSemaphore
  2. 渲染命令等待imageAvailableSemaphore,完成后触发renderFinishedSemaphore
  3. vkQueuePresentKHR等待renderFinishedSemaphore,然后呈现图像
sequenceDiagram
    participant App as 应用程序
    participant Swapchain as 交换链
    participant GPU as GPU
    participant Display as 显示器
    
    App->>Swapchain: vkAcquireNextImageKHR()
    Swapchain->>App: 提供图像索引
    App->>GPU: 提交渲染命令(等待imageAvailable)
    Swapchain-->>GPU: 触发imageAvailable信号量
    GPU->>GPU: 执行渲染命令
    GPU-->>Display: 触发renderFinished信号量
    Display->>Display: 呈现图像
3.5.2 三缓冲同步

为了提高性能,通常使用三缓冲(Triple Buffering)技术,配合信号量实现高效的帧同步:

// 三缓冲同步实现
class TripleBufferSync {
public:
    TripleBufferSync(VkDevice device, uint32_t bufferCount = 3) 
        : device(device), bufferCount(bufferCount) {
        // 为每个缓冲创建信号量
        imageAvailableSemaphores.resize(bufferCount);
        renderFinishedSemaphores.resize(bufferCount);
        
        for (uint32_t i = 0; i < bufferCount; i++) {
            imageAvailableSemaphores[i] = createSemaphore(device);
            renderFinishedSemaphores[i] = createSemaphore(device);
        }
        
        currentBuffer = 0;
    }
    
    ~TripleBufferSync() {
        // 销毁所有信号量
        for (uint32_t i = 0; i < bufferCount; i++) {
            vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
            vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
        }
    }
    
    // 获取下一个缓冲并等待图像可用
    uint32_t acquireNextImage(VkSwapchainKHR swapchain) {
        uint32_t imageIndex;
        // 使用当前缓冲的imageAvailable信号量
        vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, 
                             imageAvailableSemaphores[currentBuffer],
                             VK_NULL_HANDLE, &imageIndex);
        return imageIndex;
    }
    
    // 提交渲染命令
    void submitRenderCommands(VkQueue queue, VkCommandBuffer cmdBuffer) {
        VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
        
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.waitSemaphoreCount = 1;
        submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentBuffer];
        submitInfo.pWaitDstStageMask = waitStages;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &cmdBuffer;
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer];
        
        vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);
    }
    
    // 呈现当前缓冲
    void presentImage(VkQueue presentQueue, VkSwapchainKHR swapchain, uint32_t imageIndex) {
        VkPresentInfoKHR presentInfo{};
        presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
        presentInfo.waitSemaphoreCount = 1;
        presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer];
        presentInfo.swapchainCount = 1;
        presentInfo.pSwapchains = &swapchain;
        presentInfo.pImageIndices = &imageIndex;
        
        vkQueuePresentKHR(presentQueue, &presentInfo);
        
        // 切换到下一个缓冲
        currentBuffer = (currentBuffer + 1) % bufferCount;
    }
    
private:
    VkDevice device;
    uint32_t bufferCount;
    uint32_t currentBuffer;
    std::vector<VkSemaphore> imageAvailableSemaphores;
    std::vector<VkSemaphore> renderFinishedSemaphores;
};

三缓冲技术通过循环使用三个缓冲减少等待时间,提高渲染效率:

timeline
    title 三缓冲同步流程
    section 缓冲0
        等待图像可用 : 0, 5
        渲染中 : 5, 15
        呈现中 : 15, 25
    section 缓冲1
        等待图像可用 : 5, 10
        渲染中 : 10, 20
        呈现中 : 20, 30
    section 缓冲2
        等待图像可用 : 10, 15
        渲染中 : 15, 25
        呈现中 : 25, 35
    section GPU
        处理缓冲0 : 5, 15
        处理缓冲1 : 10, 20
        处理缓冲2 : 15, 25
    section 显示器
        显示缓冲0 : 15, 25
        显示缓冲1 : 20, 30
        显示缓冲2 : 25, 35

3.6 信号量的性能考量

信号量是实现队列间同步的关键机制,但不当使用也会导致性能问题:

3.6.1 信号量数量的优化

避免创建过多的信号量,尽量重用信号量对象:

// 不推荐:每次提交创建新信号量
void inefficientSemaphoreUsage(VkDevice device, VkQueue queue) {
    for (int i = 0; i < 100; i++) {
        // 每次循环创建新信号量(低效)
        VkSemaphore semaphore = createSemaphore(device);
        
        // 提交命令...
        
        vkDestroySemaphore(device, semaphore, nullptr);
    }
}

// 推荐:重用信号量对象
void efficientSemaphoreUsage(VkDevice device, VkQueue queue) {
    // 创建一次,多次重用
    VkSemaphore semaphore = createSemaphore(device);
    
    for (int i = 0; i < 100; i++) {
        // 重用已创建的信号量
        // 提交命令...
    }
    
    vkDestroySemaphore(device, semaphore, nullptr);
}

对于帧循环等场景,建议为每个帧缓冲预创建信号量,而不是每帧创建新的信号量。

3.6.2 等待阶段的选择

选择合适的等待阶段对性能至关重要,过早的等待会阻塞GPU管线:

// 不推荐:过早等待
void earlyWaitSemaphore(VkCommandBuffer cmdBuffer, VkSemaphore semaphore) {
    // 在管线早期阶段等待(可能导致不必要的阻塞)
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT};
    
    VkSubmitInfo submitInfo{};
    // ...
    submitInfo.pWaitDstStageMask = waitStages;
    // ...
}

// 推荐:在必要的最晚阶段等待
void lateWaitSemaphore(VkCommandBuffer cmdBuffer, VkSemaphore semaphore) {
    // 只在需要使用信号量保护的资源的阶段等待
    VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT};
    
    VkSubmitInfo submitInfo{};
    // ...
    submitInfo.pWaitDstStageMask = waitStages;
    // ...
}

等待阶段应该尽可能接近实际需要访问共享资源的阶段,以最大化GPU并行性。

3.6.3 二元信号量 vs Timeline信号量

选择合适的信号量类型:

  • 二元信号量:简单场景,开销较小
  • Timeline信号量:复杂同步场景,功能更强大但可能有更高开销
// 根据场景选择信号量类型
VkSemaphore createAppropriateSemaphore(VkDevice device, bool complexSync) {
    if (complexSync) {
        // 复杂同步使用Timeline信号量
        return createTimelineSemaphore(device, 0);
    } else {
        // 简单同步使用二元信号量
        return createBinarySemaphore(device);
    }
}

一般来说,对于交换链同步等简单场景,二元信号量足够且效率更高;而对于复杂的多阶段同步或需要CPU参与的同步,Timeline信号量更适合。

3.7 信号量常见错误与解决方案

信号量使用中常见的错误及其解决方案:

3.7.1 信号量重用错误

错误:在信号量仍被使用时重用它,导致同步失败。

// 错误示例:信号量重用错误
void incorrectSemaphoreReuse(VkDevice device, VkQueue queue) {
    VkSemaphore semaphore = createSemaphore(device);
    
    // 第一次提交:使用信号量
    VkSubmitInfo submit1{};
    // ...
    vkQueueSubmit(queue, 1, &submit1, VK_NULL_HANDLE);
    
    // 错误:在第一次提交完成前就重用信号量
    VkSubmitInfo submit2{};
    // ... 使用同一个semaphore ...
    vkQueueSubmit(queue, 1, &submit2, VK_NULL_HANDLE);  // 危险!
}

解决方案:确保信号量不再被使用后再重用,或使用信号量池:

// 正确示例:安全重用信号量
void correctSemaphoreReuse(VkDevice device, VkQueue queue) {
    VkSemaphore semaphore = createSemaphore(device);
    
    // 第一次提交
    VkFence fence1 = createFence(device);
    VkSubmitInfo submit1{};
    // ...
    vkQueueSubmit(queue, 1, &submit1, fence1);
    
    // 等待第一次提交完成
    vkWaitForFences(device, 1, &fence1, VK_TRUE, UINT64_MAX);
    vkResetFences(device, 1, &fence1);
    
    // 安全重用信号量
    VkSubmitInfo submit2{};
    // ... 使用同一个semaphore ...
    vkQueueSubmit(queue, 1, &submit2, VK_NULL_HANDLE);
    
    // 清理
    vkDestroyFence(device, fence1, nullptr);
    vkDestroySemaphore(device, semaphore, nullptr);
}
3.7.2 信号量与队列不匹配

错误:在不兼容的队列之间使用信号量同步。

// 错误示例:队列兼容性问题
void queueCompatibilityIssue(VkDevice device, VkQueue queueA, VkQueue queueB) {
    // 如果queueA和queueB属于不同的队列族且不兼容
    VkSemaphore semaphore = createSemaphore(device);
    
    // 从queueA信号
    // ...
    
    // 向queueB等待(如果队列族不兼容,可能失败)
    // ...
}

解决方案:确保使用的队列族之间是兼容的,或使用设备范围的信号量:

// 正确示例:使用设备范围的信号量
void deviceScopeSemaphore(VkDevice device, VkQueue queueA, VkQueue queueB) {
    // 创建设备范围的信号量(Vulkan 1.1+)
    VkSemaphoreTypeCreateInfo semaphoreTypeInfo{};
    semaphoreTypeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
    semaphoreTypeInfo.semaphoreType = VK_SEMAPHORE_TYPE_BINARY;
    
    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    semaphoreInfo.pNext = &semaphoreTypeInfo;
    semaphoreInfo.flags = VK_SEMAPHORE_CREATE_DEVICE_ONLY_BIT;  // 设备范围
    
    VkSemaphore semaphore;
    vkCreateSemaphore(device, &semaphoreInfo, nullptr, &semaphore);
    
    // 现在可以安全地在不同队列间使用
    // ...
}
3.7.3 交换链同步缺失

错误:在交换链操作中缺少必要的信号量同步。

// 错误示例:交换链同步缺失
void missingSwapchainSync(VkDevice device, VkQueue queue, VkSwapchainKHR swapchain) {
    // 获取图像,但没有使用信号量
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, 
                         VK_NULL_HANDLE,  // 没有信号量
                         VK_NULL_HANDLE, &imageIndex);
    
    // 提交渲染命令,没有等待信号量
    VkSubmitInfo submitInfo{};
    // ... 没有等待信号量 ...
    vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);
    
    // 呈现图像,没有等待渲染完成
    VkPresentInfoKHR presentInfo{};
    // ... 没有等待信号量 ...
    vkQueuePresentKHR(queue, &presentInfo);  // 可能呈现未完成的图像!
}

解决方案:始终在交换链操作中使用信号量同步:

// 正确示例:完整的交换链同步
void correctSwapchainSync(VkDevice device, VkQueue queue, VkSwapchainKHR swapchain) {
    VkSemaphore imageAvailable = createSemaphore(device);
    VkSemaphore renderFinished = createSemaphore(device);
    
    // 1. 获取图像,使用imageAvailable信号量
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, 
                         imageAvailable, VK_NULL_HANDLE, &imageIndex);
    
    // 2. 提交渲染命令,等待imageAvailable,信号renderFinished
    VkSubmitInfo submitInfo{};
    // ... 设置等待和信号量 ...
    vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);
    
    // 3. 呈现图像,等待renderFinished
    VkPresentInfoKHR presentInfo{};
    // ... 设置等待信号量 ...
    vkQueuePresentKHR(queue, &presentInfo);
    
    // 清理
    // ...
}
graph TD
    A[信号量常见错误] --> B[信号量重用错误]
    A --> C[队列兼容性问题]
    A --> D[交换链同步缺失]
    
    B --> B1[等待前一次使用完成]
    B --> B2[使用信号量池]
    C --> C1[确保队列族兼容]
    C --> C2[使用设备范围信号量]
    D --> D1[获取图像时使用信号量]
    D --> D2[呈现时等待渲染完成]

信号量是Vulkan中实现队列间同步的核心机制,掌握信号量的使用对于开发高性能的多队列Vulkan应用至关重要。下一章将介绍用于CPU与GPU同步的Fence机制。

四、FENCE(围栏)原理解析

4.1 Fence的基本概念

Fence(围栏)是Vulkan中用于CPU与GPU之间同步的机制。与主要用于GPU内部同步的信号量不同,Fence允许CPU知道GPU上的操作何时完成。

Fence的核心功能:

  • 让CPU等待GPU完成特定操作
  • 允许CPU查询GPU操作的完成状态
  • 用于同步CPU和GPU的工作节奏

Fence与信号量的主要区别:

  • Fence主要用于CPU与GPU之间的同步
  • 信号量主要用于GPU内部(队列之间)的同步
  • Fence可以被CPU显式重置,而二元信号量会在等待时自动重置
  • Fence有明确的状态查询接口,CPU可以主动检查其状态
// Fence的基本使用流程
void basicFenceUsage(VkDevice device, VkQueue queue) {
    // 1. 创建Fence,初始状态为未触发
    VkFenceCreateInfo fenceInfo{};
    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fenceInfo.flags = 0;  // 初始状态:未触发
    
    VkFence fence;
    if (vkCreateFence(device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
        throw std::runtime_error("创建Fence失败!");
    }
    
    // 2. 录制命令缓冲区
    VkCommandBuffer cmdBuffer = recordCommandBuffer();
    
    // 3. 提交命令,并关联Fence
    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &cmdBuffer;
    
    // 提交命令,当命令完成时Fence会被触发
    if (vkQueueSubmit(queue, 1, &submitInfo, fence) != VK_SUCCESS) {
        throw std::runtime_error("命令提交失败!");
    }
    
    // 4. CPU等待Fence被触发(等待GPU完成)
    // 等待超时时间为1秒(1e9纳秒)
    if (vkWaitForFences(device, 1, &fence, VK_TRUE, 1000000000) != VK_SUCCESS) {
        throw std::runtime_error("等待Fence超时!");
    }
    
    // 5. 重置Fence,以便再次使用
    if (vkResetFences(device, 1, &fence) != VK_SUCCESS) {
        throw std::runtime_error("重置Fence失败!");
    }
    
    // 6. 可以再次使用该Fence提交其他命令
    // ...
    
    // 7. 清理
    vkDestroyFence(device, fence, nullptr);
}

Fence的工作流程:

  1. CPU创建Fence并提交关联了该Fence的GPU命令
  2. GPU执行命令
  3. CPU可以选择等待Fence或查询其状态
  4. 当GPU完成命令执行,Fence被触发
  5. CPU检测到Fence被触发后,可以安全地访问GPU修改的资源
timeline
    title Fence工作流程
    section CPU
        提交命令并关联Fence : 0, 5
        等待Fence触发 : 5, 25
        继续执行 : 25, 30
    section GPU
        执行命令 : 5, 25
        触发Fence : 25, 25

4.2 Fence的创建与状态管理

Fence的创建和状态管理相对直接,但需要注意一些细节:

4.2.1 Fence的创建

Fence可以创建为初始触发或初始未触发状态:

// 创建不同初始状态的Fence
VkFence createFenceWithState(VkDevice device, bool initiallySignaled) {
    VkFenceCreateInfo fenceInfo{};
    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    
    // 设置初始状态
    if (initiallySignaled) {
        fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;  // 初始触发状态
    } else {
        fenceInfo.flags = 0;  // 初始未触发状态
    }
    
    VkFence fence;
    if (vkCreateFence(device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
        throw std::runtime_error("创建Fence失败!");
    }
    
    return fence;
}

初始触发状态的Fence常用于需要立即满足等待条件的场景,例如初始化阶段。

4.2.2 Fence的状态查询

CPU可以主动查询Fence的当前状态:

// 查询Fence状态
bool isFenceSignaled(VkDevice device, VkFence fence) {
    VkResult result = vkGetFenceStatus(device, fence);
    
    if (result == VK_SUCCESS) {
        // Fence已触发
        return true;
    } else if (result == VK_NOT_READY) {
        // Fence未触发
        return false;
    } else {
        // 发生错误
        throw std::runtime_error("查询Fence状态失败!");
    }
}

// 轮询Fence状态(非阻塞)
void pollFenceStatus(VkDevice device, VkFence fence) {
    while (true) {
        if (isFenceSignaled(device, fence)) {
            std::cout << "Fence已触发!" << std::endl;
            break;
        } else {
            std::cout << "Fence未触发,继续等待..." << std::endl;
            // 短暂休眠,避免CPU占用过高
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
}

状态查询允许CPU在不阻塞的情况下检查GPU操作是否完成,这在需要同时处理其他任务时非常有用。

4.2.3 Fence的重置

Fence在被触发后需要重置才能再次使用:

// 重置Fence
void resetFencesExample(VkDevice device, std::vector<VkFence>& fences) {
    // 可以同时重置多个Fence
    if (vkResetFences(device, static_cast<uint32_t>(fences.size()), fences.data()) != VK_SUCCESS) {
        throw std::runtime_error("重置Fence失败!");
    }
    std::cout << "已重置" << fences.size() << "个Fence" << std::endl;
}

// Fence重用示例
void reuseFenceExample(VkDevice device, VkQueue queue) {
    VkFence fence = createFenceWithState(device, false);
    
    for (int i = 0; i < 5; i++) {
        // 重置Fence(如果是第一次使用则不需要,但重置也无害)
        vkResetFences(device, 1, &fence);
        
        // 录制并提交命令
        VkCommandBuffer cmdBuffer = recordCommandBufferForFrame(i);
        
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &cmdBuffer;
        
        vkQueueSubmit(queue, 1, &submitInfo, fence);
        
        // 等待完成
        vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
        
        // 处理结果...
    }
    
    vkDestroyFence(device, fence, nullptr);
}

Fence必须在每次使用前重置,否则如果它处于触发状态,提交命令时会立即被视为完成。

stateDiagram
    [*] --> Unsignaled: 创建(无标志)
    [*] --> Signaled: 创建(带VK_FENCE_CREATE_SIGNALED_BIT)
    Unsignaled --> Signaled: GPU完成命令
    Signaled --> Unsignaled: vkResetFences()
    Unsignaled --> [*]: 销毁
    Signaled --> [*]: 销毁

4.3 Fence的等待操作

Fence提供了多种等待方式,以适应不同的同步需求:

4.3.1 等待单个Fence

最基本的用法是等待单个Fence被触发:

// 等待单个Fence
void waitForSingleFence(VkDevice device, VkFence fence, uint64_t timeoutNs = UINT64_MAX) {
    // 等待Fence被触发
    // 第三个参数为VK_TRUE表示等待所有指定的Fence
    VkResult result = vkWaitForFences(device, 1, &fence, VK_TRUE, timeoutNs);
    
    if (result == VK_SUCCESS) {
        std::cout << "Fence已触发,继续执行" << std::endl;
    } else if (result == VK_TIMEOUT) {
        std::cout << "等待Fence超时" << std::endl;
    } else {
        throw std::runtime_error("等待Fence失败!");
    }
}

超时参数可以控制等待的最长时间(以纳秒为单位):

  • 0:不等待,立即返回
  • UINT64_MAX:无限等待,直到Fence被触发
  • 其他值:最多等待指定的纳秒数
4.3.2 等待多个Fence

可以同时等待多个Fence,并指定等待条件(所有Fence触发或任一Fence触发):

// 等待多个Fence
void waitForMultipleFences(VkDevice device, const std::vector<VkFence>& fences, bool waitAll) {
    // 等待多个Fence
    // waitAll为VK_TRUE表示等待所有Fence,为VK_FALSE表示等待任一Fence
    VkResult result = vkWaitForFences(
        device, 
        static_cast<uint32_t>(fences.size()), 
        fences.data(), 
        waitAll ? VK_TRUE : VK_FALSE, 
        UINT64_MAX  // 无限等待
    );
    
    if (result == VK_SUCCESS) {
        std::cout << (waitAll ? "所有Fence已触发" : "至少一个Fence已触发") << std::endl;
    } else {
        throw std::runtime_error("等待多个Fence失败!");
    }
}

// 等待多个Fence的不同策略
void multipleFenceStrategies(VkDevice device, VkFence fenceA, VkFence fenceB, VkFence fenceC) {
    std::vector<VkFence> fences = {fenceA, fenceB, fenceC};
    
    // 策略1:等待所有Fence触发
    waitForMultipleFences(device, fences, true);
    std::cout << "所有Fence都已触发,继续处理..." << std::endl;
    
    // 重置Fence并重新使用...
    
    // 策略2:等待任一Fence触发
    waitForMultipleFences(device, fences, false);
    std::cout << "至少一个Fence已触发,继续处理..." << std::endl;
}

等待多个Fence时的两种策略:

  • 等待所有(waitAll = VK_TRUE):适用于需要所有操作完成后才能继续的场景
  • 等待任一(waitAll = VK_FALSE):适用于可以处理部分完成结果的场景
timeline
    title 等待多个Fence的两种策略
    section Fence A
        触发 : 10, 10
    section Fence B
        触发 : 20, 20
    section Fence C
        触发 : 15, 15
    section 等待所有Fence
        等待中 : 0, 20
        完成等待 : 20, 20
    section 等待任一Fence
        等待中 : 0, 10
        完成等待 : 10, 10
4.3.3 非阻塞等待

通过结合状态查询和短暂休眠,可以实现非阻塞的等待方式:

// 非阻塞等待Fence
bool nonBlockingFenceWait(VkDevice device, VkFence fence, uint64_t maxWaitMs = 1000) {
    auto startTime = std::chrono::high_resolution_clock::now();
    
    while (true) {
        // 检查Fence状态
        VkResult result = vkGetFenceStatus(device, fence);
        
        if (result == VK_SUCCESS) {
            // Fence已触发
            return true;
        } else if (result != VK_NOT_READY) {
            // 发生错误
            return false;
        }
        
        // 检查是否超时
        auto currentTime = std::chrono::high_resolution_clock::now();
        auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
            currentTime - startTime
        ).count();
        
        if (elapsedMs >= maxWaitMs) {
            // 超时
            return false;
        }
        
        // 短暂休眠,避免CPU占用过高
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
}

// 使用非阻塞等待处理其他任务
void processWhileWaiting(VkDevice device, VkFence fence) {
    std::cout << "等待GPU操作完成,同时处理其他任务..." << std::endl;
    
    while (true) {
        // 检查Fence是否已触发
        if (isFenceSignaled(device, fence)) {
            std::cout << "GPU操作完成!" << std::endl;
            break;
        }
        
        // 处理其他CPU任务
        performOtherCPUTasks();
        
        // 短暂休眠
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
}

非阻塞等待适用于CPU在等待GPU时还有其他任务可以处理的场景,能提高整体效率。

4.4 Fence在帧同步中的应用

Fence在帧同步中有着广泛的应用,确保CPU不会超前于GPU太多,避免内存耗尽。

4.4.1 基本帧同步

使用Fence确保每帧渲染完成后再开始下一帧的准备工作:

// 基本帧同步
void basicFrameSynchronization(VkDevice device, VkQueue queue, uint32_t frameCount) {
    // 创建Fence
    VkFence frameFence = createFenceWithState(device, true);  // 初始触发状态
    
    for (uint32_t frame = 0; frame < frameCount; frame++) {
        // 等待上一帧完成
        vkWaitForFences(device, 1, &frameFence, VK_TRUE, UINT64_MAX);
        vkResetFences(device, 1, &frameFence);
        
        // 准备帧数据(CPU工作)
        prepareFrameData(frame);
        
        // 录制命令缓冲区
        VkCommandBuffer cmdBuffer = recordFrameCommands(frame);
        
        // 提交命令,关联Fence
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &cmdBuffer;
        
        vkQueueSubmit(queue, 1, &submitInfo, frameFence);
        
        // 呈现帧(如果需要)
        presentFrame(frame);
    }
    
    // 等待最后一帧完成
    vkWaitForFences(device, 1, &frameFence, VK_TRUE, UINT64_MAX);
    
    // 清理
    vkDestroyFence(device, frameFence, nullptr);
}

基本帧同步确保了帧的严格顺序执行,但可能限制了CPU和GPU的并行性。

timeline
    title 基本帧同步
    section CPU
        准备帧1 : 0, 5
        准备帧2 : 25, 30
        准备帧3 : 50, 55
    section GPU
        渲染帧1 : 5, 25
        渲染帧2 : 30, 50
        渲染帧3 : 55, 75
    section Fence
        帧1完成 : 25, 25
        帧2完成 : 50, 50
        帧3完成 : 75, 75
4.4.2 多缓冲帧同步

为了提高CPU和GPU的并行性,通常使用多缓冲(Multi-buffering)技术:

// 多缓冲帧同步实现
class MultiBufferSync {
public:
    MultiBufferSync(VkDevice device, VkQueue queue, uint32_t bufferCount = 3)
        : device(device), queue(queue), bufferCount(bufferCount), currentBuffer(0) {
        // 为每个缓冲创建Fence,初始状态为触发
        fences.resize(bufferCount);
        for (uint32_t i = 0; i < bufferCount; i++) {
            fences[i] = createFenceWithState(device, true);
        }
        
        // 为每个缓冲创建命令缓冲区
        cmdBuffers.resize(bufferCount);
        for (uint32_t i = 0; i < bufferCount; i++) {
            cmdBuffers[i] = createCommandBuffer();
        }
    }
    
    ~MultiBufferSync() {
        // 等待所有缓冲完成
        vkWaitForFences(device, bufferCount, fences.data(), VK_TRUE, UINT64_MAX);
        
        // 销毁Fence和命令缓冲区
        for (uint32_t i = 0; i < bufferCount; i++) {
            vkDestroyFence(device, fences[i], nullptr);
            destroyCommandBuffer(cmdBuffers[i]);
        }
    }
    
    // 处理一帧
    void processFrame(uint32_t frameIndex) {
        // 获取当前缓冲索引
        uint32_t bufferIndex = currentBuffer;
        
        // 等待当前缓冲的Fence
        vkWaitForFences(device, 1, &fences[bufferIndex], VK_TRUE, UINT64_MAX);
        vkResetFences(device, 1, &fences[bufferIndex]);
        vkResetCommandBuffer(cmdBuffers[bufferIndex], 0);
        
        // 准备帧数据(CPU工作)
        prepareFrameData(frameIndex, bufferIndex);
        
        // 录制命令
        recordFrameCommands(cmdBuffers[bufferIndex], frameIndex, bufferIndex);
        
        // 提交命令
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &cmdBuffers[bufferIndex];
        
        vkQueueSubmit(queue, 1, &submitInfo, fences[bufferIndex]);
        
        // 呈现帧
        presentFrame(frameIndex, bufferIndex);
        
        // 切换到下一缓冲
        currentBuffer = (currentBuffer + 1) % bufferCount;
    }
    
private:
    VkDevice device;
    VkQueue queue;
    uint32_t bufferCount;
    uint32_t currentBuffer;
    std::vector<VkFence> fences;
    std::vector<VkCommandBuffer> cmdBuffers;
    // ... 其他辅助函数和成员 ...
};

多缓冲技术允许CPU和GPU并行工作:当GPU渲染第n帧时,CPU可以准备第n+1帧的数据,从而提高整体效率。

timeline
    title 三缓冲帧同步
    section CPU
        准备帧1 : 0, 5
        准备帧2 : 5, 10
        准备帧3 : 10, 15
        准备帧4 : 25, 30
    section GPU
        渲染帧1 : 5, 20
        渲染帧2 : 10, 25
        渲染帧3 : 15, 30
        渲染帧4 : 30, 45
    section Fence 0
        完成帧1 : 20, 20
        完成帧4 : 45, 45
    section Fence 1
        完成帧2 : 25, 25
    section Fence 2
        完成帧3 : 30, 30
4.4.3 与交换链结合的多缓冲同步

将Fence与交换链和信号量结合,实现高效的渲染和呈现同步:

// 与交换链结合的多缓冲同步
class SwapchainMultiBufferSync {
public:
    SwapchainMultiBufferSync(VkDevice device, VkQueue graphicsQueue, VkQueue presentQueue, 
                            VkSwapchainKHR swapchain, uint32_t imageCount)
        : device(device), graphicsQueue(graphicsQueue), presentQueue(presentQueue),
          swapchain(swapchain), imageCount(imageCount), currentFrame(0) {
        // 为每个帧创建Fence、信号量
        inFlightFences.resize(imageCount);
        imageAvailableSemaphores.resize(imageCount);
        renderFinishedSemaphores.resize(imageCount);
        
        for (uint32_t i = 0; i < imageCount; i++) {
            inFlightFences[i] = createFenceWithState(device, true);  // 初始触发
            imageAvailableSemaphores[i] = createSemaphore(device);
            renderFinishedSemaphores[i] = createSemaphore(device);
        }
    }
    
    ~SwapchainMultiBufferSync() {
        // 等待所有操作完成
        vkWaitForFences(device, imageCount, inFlightFences.data(), VK_TRUE, UINT64_MAX);
        
        // 清理资源
        for (uint32_t i = 0; i < imageCount; i++) {
            vkDestroyFence(device, inFlightFences[i], nullptr);
            vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
            vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
        }
    }
    
    // 渲染一帧
    void renderFrame() {
        // 等待当前帧的Fence
        vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
        vkResetFences(device, 1, &inFlightFences[currentFrame]);
        
        // 获取交换链图像
        uint32_t imageIndex;
        vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, 
                             imageAvailableSemaphores[currentFrame],
                             VK_NULL_HANDLE, &imageIndex);
        
        // 重置并录制命令缓冲区
        VkCommandBuffer cmdBuffer = getCommandBuffer(currentFrame);
        vkResetCommandBuffer(cmdBuffer, 0);
        recordRenderCommands(cmdBuffer, imageIndex);
        
        // 提交渲染命令
        VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
        
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.waitSemaphoreCount = 1;
        submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentFrame];
        submitInfo.pWaitDstStageMask = waitStages;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &cmdBuffer;
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame];
        
        vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);
        
        // 呈现图像
        VkPresentInfoKHR presentInfo{};
        presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
        presentInfo.waitSemaphoreCount = 1;
        presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];
        presentInfo.swapchainCount = 1;
        presentInfo.pSwapchains = &swapchain;
        presentInfo.pImageIndices = &imageIndex;
        
        vkQueuePresentKHR(presentQueue, &presentInfo);
        
        // 移动到下一帧
        currentFrame = (currentFrame + 1) % imageCount;
    }
    
private:
    VkDevice device;
    VkQueue graphicsQueue;
    VkQueue presentQueue;
    VkSwapchainKHR swapchain;
    uint32_t imageCount;
    uint32_t currentFrame;
    
    std::vector<VkFence> inFlightFences;
    std::vector<VkSemaphore> imageAvailableSemaphores;
    std::vector<VkSemaphore> renderFinishedSemaphores;
    // ... 其他成员和辅助函数 ...
};

这种组合同步机制是Vulkan应用中最常见的帧同步方式,它结合了Fence(CPU-GPU同步)和信号量(GPU内部同步)的优势。

4.5 Fence的性能考量

Fence虽然是CPU-GPU同步的重要机制,但不当使用也会影响性能:

4.5.1 减少不必要的等待

避免在不必要的地方等待Fence,以保持CPU和GPU的并行性:

// 不推荐:不必要的Fence等待
void unnecessaryFenceWait(VkDevice device, VkQueue queue, VkFence fence) {
    // 提交命令
    submitCommands(queue, cmdBuffer, fence);
    
    // 不必要的等待:GPU正在渲染,CPU可以做其他工作
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
    
    // 做一些与渲染结果无关的CPU工作
    performUnrelatedCPUTasks();
}

// 推荐:重叠CPU和GPU工作
void overlapCPUandGPU(VkDevice device, VkQueue queue, VkFence fence) {
    // 提交命令
    submitCommands(queue, cmdBuffer, fence);
    
    // 在GPU渲染的同时,CPU处理其他任务
    performUnrelatedCPUTasks();
    
    // 当需要渲染结果时才等待
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
    
    // 处理渲染结果
    processRenderResults();
}

最大化CPU和GPU并行性的关键是尽可能重叠它们的工作,只在必要时才同步。

4.5.2 Fence数量的优化

为每个帧或任务创建独立的Fence,但避免过度创建:

// 不推荐:过多的Fence创建
void tooManyFences(VkDevice device, VkQueue queue, uint32_t taskCount) {
    for (uint32_t i = 0; i < taskCount; i++) {
        // 每次任务创建新Fence(低效)
        VkFence fence = createFence(device);
        
        // 提交任务...
        
        // 等待完成...
        vkDestroyFence(device, fence, nullptr);
    }
}

// 推荐:Fence池重用
class FencePool {
public:
    FencePool(VkDevice device, uint32_t initialSize = 8) : device(device) {
        // 预创建一定数量的Fence
        for (uint32_t i = 0; i < initialSize; i++) {
            fences.push_back(createFenceWithState(device, true));
        }
    }
    
    ~FencePool() {
        // 销毁所有Fence
        for (auto fence : fences) {
            vkDestroyFence(device, fence, nullptr);
        }
    }
    
    // 获取一个可用的Fence
    VkFence acquireFence() {
        std::lock_guard<std::mutex> lock(mutex);
        
        // 查找已触发的Fence
        for (size_t i = 0; i < fences.size(); i++) {
            if (isFenceSignaled(device, fences[i])) {
                vkResetFences(device, 1, &fences[i]);
                return fences[i];
            }
        }
        
        // 如果没有可用Fence,创建新的
        VkFence newFence = createFenceWithState(device, false);
        fences.push_back(newFence);
        return newFence;
    }
    
private:
    VkDevice device;
    std::vector<VkFence> fences;
    std::mutex mutex;
};

Fence池可以减少频繁创建和销毁Fence的开销,特别适合需要大量临时Fence的场景。

4.5.3 超时设置的选择

合理设置Fence等待的超时时间:

// 合理设置超时时间
void合理设置超时时间
void reasonableTimeout(VkDevice device, VkFence fence) {
    // 1. 对于关键操作,使用较长超时或无限等待
    VkResult result = vkWaitForFences(device, 1, &fence, VK_TRUE, 5000000000);  // 5秒
    if (result == VK_TIMEOUT) {
        // 关键操作超时,可能需要处理错误
        handleCriticalTimeout();
    }
    
    // 2. 对于非关键操作,使用较短超时
    result = vkWaitForFences(device, 1, &fence, VK_TRUE, 100000000);  // 100毫秒
    if (result == VK_TIMEOUT) {
        // 非关键操作超时,可以跳过或稍后重试
        skipOrRetryNonCriticalOperation();
    }
    
    // 3. 对于轮询操作,使用0超时(立即返回)
    result = vkWaitForFences(device, 1, &fence, VK_TRUE, 0);
    if (result == VK_NOT_READY) {
        // 操作未完成,继续其他工作
        continueOtherWork();
    }
}

超时时间的选择应根据操作的重要性和预期执行时间来决定:

  • 关键操作:较长超时或无限等待
  • 非关键操作:较短超时
  • 轮询检查:零超时
graph TD
    A[选择超时时间] --> B{操作类型}
    B -->|关键操作| C[较长超时或无限等待]
    B -->|非关键操作| D[较短超时]
    B -->|轮询检查| E[零超时]
    C --> C1[确保操作完成]
    C --> C2[可能阻塞较长时间]
    D --> D1[快速响应超时]
    D --> D2[可能需要重试机制]
    E --> E1[不阻塞CPU]
    E --> E2[需要循环检查]

4.6 Fence常见错误与解决方案

Fence使用中常见的错误及其解决方案:

4.6.1 忘记重置Fence

错误:在重用Fence前忘记重置,导致错误的同步行为。

// 错误示例:忘记重置Fence
void forgotToResetFence(VkDevice device, VkQueue queue) {
    VkFence fence = createFenceWithState(device, false);
    
    // 第一次提交
    submitCommands(queue, cmdBuffer1, fence);
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
    
    // 错误:没有重置Fence就再次使用
    submitCommands(queue, cmdBuffer2, fence);  // Fence已经处于触发状态!
    
    // 这次等待会立即返回,即使命令可能尚未完成
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);  // 错误!
}

解决方案:每次重用Fence前必须重置:

// 正确示例:使用前重置Fence
void resetFenceBeforeUse(VkDevice device, VkQueue queue) {
    VkFence fence = createFenceWithState(device, false);
    
    // 第一次提交
    submitCommands(queue, cmdBuffer1, fence);
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
    
    // 正确:重置Fence后再使用
    vkResetFences(device, 1, &fence);
    submitCommands(queue, cmdBuffer2, fence);
    
    // 等待第二次提交完成
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
}
4.6.2 过度同步

错误:不必要地等待Fence,限制了CPU和GPU的并行性。

// 错误示例:过度同步
void overSynchronization(VkDevice device, VkQueue queue, VkFence fence) {
    // 提交GPU任务1
    submitTask1(queue, fence);
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);  // 等待完成
    
    // 提交GPU任务2(不依赖任务1)
    submitTask2(queue, fence);
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);  // 不必要的等待
}

解决方案:只在必要时等待,允许独立任务并行执行:

// 正确示例:最小化同步
void minimalSynchronization(VkDevice device, VkQueue queue) {
    VkFence fence1 = createFence(device);
    VkFence fence2 = createFence(device);
    
    // 提交GPU任务1
    submitTask1(queue, fence1);
    
    // 不等待任务1完成,直接提交独立的任务2
    submitTask2(queue, fence2);
    
    // 当需要两个任务的结果时再等待
    std::vector<VkFence> fences = {fence1, fence2};
    vkWaitForFences(device, 2, fences.data(), VK_TRUE, UINT64_MAX);
}
4.6.3 Fence与信号量混淆

错误:混淆Fence和信号量的用途,在GPU内部同步中使用Fence。

// 错误示例:混淆Fence和信号量
void confuseFenceAndSemaphore(VkDevice device, VkQueue queueA, VkQueue queueB) {
    // 错误:试图用Fence同步两个GPU队列
    VkFence fence = createFence(device);
    
    // 队列A提交命令
    submitToQueueA(queueA, cmdBufferA, fence);
    
    // 队列B尝试等待Fence(这不起作用!)
    VkSubmitInfo submitB{};
    // ... 错误地使用Fence作为等待对象 ...
    vkQueueSubmit(queueB, 1, &submitB, VK_NULL_HANDLE);
}

解决方案:正确区分Fence和信号量的用途:

// 正确示例:正确使用Fence和信号量
void correctFenceAndSemaphoreUsage(VkDevice device, VkQueue queueA, VkQueue queueB) {
    // 用信号量同步GPU队列
    VkSemaphore semaphore = createSemaphore(device);
    
    // 队列A提交命令,完成后信号量触发
    VkSubmitInfo submitA{};
    // ...
    submitA.signalSemaphoreCount = 1;
    submitA.pSignalSemaphores = &semaphore;
    vkQueueSubmit(queueA, 1, &submitA, VK_NULL_HANDLE);
    
    // 队列B等待信号量
    VkSubmitInfo submitB{};
    // ...
    submitB.waitSemaphoreCount = 1;
    submitB.pWaitSemaphores = &semaphore;
    vkQueueSubmit(queueB, 1, &submitB, VK_NULL_HANDLE);
    
    // 用Fence让CPU等待队列B完成
    VkFence fence = createFence(device);
    submitFinalTask(queueB, finalCmdBuffer, fence);
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
}
graph TD
    A[Fence常见错误] --> B[忘记重置Fence]
    A --> C[过度同步]
    A --> D[与信号量混淆]
    
    B --> B1[每次使用前调用vkResetFences]
    C --> C1[只在必要时等待]
    C --> C2[允许独立任务并行]
    D --> D1[Fence用于CPU-GPU同步]
    D --> D2[信号量用于GPU内部同步]

Fence是CPU与GPU同步的关键机制,正确使用Fence可以确保CPU安全地访问GPU处理后的资源,同时最大化CPU和GPU的并行性。下一章将介绍Vulkan中最灵活的同步原语——Event。

五、EVENT(事件)原理解析

5.1 Event的基本概念

Event(事件)是Vulkan中最灵活的同步原语,它可以被CPU或GPU触发,也可以被CPU或GPU等待。Event提供了细粒度的同步控制,适用于复杂的同步场景。

Event的主要特点:

  • 可以被CPU和GPU共同控制
  • 支持更精细的同步粒度
  • 可以在命令缓冲区录制期间或执行期间触发
  • 可用于实现条件执行逻辑

Event与其他同步原语的区别:

  • 比管线屏障更灵活,可以在命令流中任意位置触发和等待
  • 比信号量和Fence提供更细粒度的控制
  • 支持GPU内部的条件执行
// Event的基本使用流程
void basicEventUsage(VkDevice device, VkCommandBuffer cmdBuffer) {
    // 1. 创建Event
    VkEventCreateInfo eventInfo{};
    eventInfo.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO;
    
    VkEvent event;
    if (vkCreateEvent(device, &eventInfo, nullptr, &event) != VK_SUCCESS) {
        throw std::runtime_error("创建Event失败!");
    }
    
    // 2. 在命令缓冲区中记录等待Event的命令
    vkCmdWaitEvents(
        cmdBuffer,
        1, &event,  // 要等待的Event
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,  // 等待的源阶段
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,  // 等待的目标阶段
        0, nullptr,  // 内存屏障
        0, nullptr,  // 缓冲区屏障
        0, nullptr   // 图像屏障
    );
    
    // 3. 记录需要等待Event后执行的命令
    recordCommandsAfterEvent(cmdBuffer);
    
    // 4. 提交命令缓冲区
    submitCommandBuffer(cmdBuffer);
    
    // 5. 在适当的时候,由CPU触发Event
    // 可以在提交命令后,根据CPU计算结果决定何时触发
    performCPUComputations();
    vkSetEvent(device, event);  // 触发Event,允许GPU继续执行
    
    // 6. 等待所有操作完成
    waitForCommandBufferCompletion();
    
    // 7. 清理
    vkDestroyEvent(device, event, nullptr);
}

Event的工作流程:

  1. 创建Event(初始状态为未触发)
  2. 在命令缓冲区中插入等待Event的指令
  3. 提交命令缓冲区,GPU执行到等待指令时会暂停
  4. 当满足条件时(可以是CPU或GPU操作),触发Event
  5. GPU检测到Event被触发后,继续执行后续命令
timeline
    title Event工作流程
    section CPU
        提交命令缓冲区 : 0, 5
        执行CPU计算 : 5, 20
        触发Event : 20, 20
    section GPU
        执行命令 : 5, 10
        等待Event : 10, 20
        继续执行命令 : 20, 30

5.2 Event的创建与状态管理

Event的创建和状态管理相对简单,但提供了丰富的操作方式:

5.2.1 Event的创建

Event的创建非常简单,不需要复杂的参数:

// 创建Event
VkEvent createEvent(VkDevice device) {
    VkEventCreateInfo eventInfo{};
    eventInfo.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO;
    // 可选:设置标志
    // eventInfo.flags = ...;
    
    VkEvent event;
    if (vkCreateEvent(device, &eventInfo, nullptr, &event) != VK_SUCCESS) {
        throw std::runtime_error("创建Event失败!");
    }
    
    return event;
}

Vulkan 1.1及以上版本还支持设备范围的Event,适用于跨队列族同步:

// 创建设备范围的Event(Vulkan 1.1+)
VkEvent createDeviceScopeEvent(VkDevice device) {
    VkEventCreateInfo eventInfo{};
    eventInfo.sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO;
    eventInfo.flags = VK_EVENT_CREATE_DEVICE_ONLY_BIT;  // 设备范围的Event
    
    VkEvent event;
    if (vkCreateEvent(device, &eventInfo, nullptr, &event) != VK_SUCCESS) {
        throw std::runtime_error("创建设备范围Event失败!");
    }
    
    return event;
}
5.2.2 Event的状态操作

Event的状态可以通过CPU或GPU操作来改变:

// CPU操作Event状态
void cpuControlEvent(VkDevice device, VkEvent event) {
    // 触发Event
    if (vkSetEvent(device, event) != VK_SUCCESS) {
        throw std::runtime_error("CPU触发Event失败!");
    }
    
    // 查询Event状态
    VkResult status = vkGetEventStatus(device, event);
    if (status == VK_EVENT_SET) {
        std::cout << "Event已触发" << std::endl;
    } else if (status == VK_EVENT_RESET) {
        std::cout << "Event已重置" << std::endl;
    } else {
        throw std::runtime_error("查询Event状态失败!");
    }
    
    // 重置Event
    if (vkResetEvent(device, event) != VK_SUCCESS) {
        throw std::runtime_error("CPU重置Event失败!");
    }
}
// GPU操作Event状态
void gpuControlEvent(VkCommandBuffer cmdBuffer, VkEvent event) {
    // 在命令缓冲区中记录触发Event的命令
    vkCmdSetEvent(
        cmdBuffer,
        event,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT  // 触发Event的管线阶段
    );
    
    // 在命令缓冲区中记录重置Event的命令
    vkCmdResetEvent(
        cmdBuffer,
        event,
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT  // 重置Event的管线阶段
    );
}

Event的状态转换:

stateDiagram
    [*] --> Reset: 创建
    Reset --> Set: vkSetEvent() 或 vkCmdSetEvent()
    Set --> Reset: vkResetEvent() 或 vkCmdResetEvent()
    Reset --> [*]: 销毁
    Set --> [*]: 销毁

与Fence不同,Event可以被多次触发和重置,无需重新创建,这使它非常适合需要重复使用的同步场景。

5.3 Event的等待操作

Event提供了灵活的等待机制,可以在命令流中的任意位置插入等待点:

5.3.1 GPU等待Event

GPU等待Event是最常见的用法,允许GPU命令流暂停直到Event被触发:

// GPU等待Event示例
void gpuWaitForEvent(VkCommandBuffer cmdBuffer, VkEvent event) {
    // 定义等待Event时的内存屏障
    std::vector<VkMemoryBarrier> memoryBarriers;
    std::vector<VkBufferMemoryBarrier> bufferBarriers;
    std::vector<VkImageMemoryBarrier> imageBarriers;
    
    // 添加必要的内存屏障(根据具体同步需求)
    VkBufferMemoryBarrier bufferBarrier{};
    bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
    bufferBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
    bufferBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    // ... 设置其他缓冲区屏障参数 ...
    bufferBarriers.push_back(bufferBarrier);
    
    // 记录等待Event的命令
    vkCmdWaitEvents(
        cmdBuffer,
        1, &event,  // 要等待的Event
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,  // 等待开始的管线阶段
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,  // 等待结束的管线阶段
        static_cast<uint32_t>(memoryBarriers.size()), memoryBarriers.data(),
        static_cast<uint32_t>(bufferBarriers.size()), bufferBarriers.data(),
        static_cast<uint32_t>(imageBarriers.size()), imageBarriers.data()
    );
    
    // 等待Event触发后才会执行的命令
    recordDependentCommands(cmdBuffer);
}

vkCmdWaitEvents命令中的管线阶段参数定义了:

  • 源阶段掩码:等待开始前必须完成的管线阶段
  • 目标阶段掩码:等待结束后继续执行的管线阶段

这允许精细控制命令流中等待操作的位置和影响范围。

5.3.2 CPU等待Event

CPU也可以等待Event被触发,这提供了另一种CPU与GPU同步的方式:

// CPU等待Event触发
bool cpuWaitForEvent(VkDevice device, VkEvent event, uint64_t timeoutNs = UINT64_MAX) {
    // 循环检查Event状态,直到触发或超时
    auto startTime = std::chrono::high_resolution_clock::now();
    
    while (true) {
        VkResult status = vkGetEventStatus(device, event);
        
        if (status == VK_EVENT_SET) {
            // Event已触发
            return true;
        } else if (status != VK_EVENT_RESET) {
            // 发生错误
            return false;
        }
        
        // 检查是否超时
        auto currentTime = std::chrono::high_resolution_clock::now();
        auto elapsedNs = std::chrono::duration_cast<std::chrono::nanoseconds>(
            currentTime - startTime
        ).count();
        
        if (elapsedNs >= timeoutNs) {
            // 超时
            return false;
        }
        
        // 短暂休眠,减少CPU占用
        std::this_thread::sleep_for(std::chrono::microseconds(10));
    }
}

CPU等待Event与等待Fence的区别:

  • Event可以由CPU或GPU触发,而Fence只能由GPU触发
  • Event等待通常需要CPU轮询,而Fence可以通过vkWaitForFences高效等待
  • Event提供更灵活的触发时机,但可能有更高的CPU开销
5.3.3 多个Event的等待

可以同时等待多个Event,提供更复杂的同步逻辑:

// 等待多个Event
void waitForMultipleEvents(VkCommandBuffer cmdBuffer, const std::vector<VkEvent>& events) {
    // 记录等待多个Event的命令
    vkCmdWaitEvents(
        cmdBuffer,
        static_cast<uint32_t>(events.size()), events.data(),
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,  // 源阶段
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,  // 目标阶段
        0, nullptr,  // 内存屏障
        0, nullptr,  // 缓冲区屏障
        0, nullptr   // 图像屏障
    );
    
    // 所有Event都触发后执行的命令
    // ...
}

等待多个Event时,GPU会暂停直到所有Event都被触发,这与信号量的等待行为类似。

timeline
    title 多个Event等待
    section Event A
        触发 : 10, 10
    section Event B
        触发 : 15, 15
    section Event C
        触发 : 20, 20
    section GPU
        执行命令 : 0, 5
        等待所有Event : 5, 20
        继续执行 : 20, 30

5.4 Event的典型应用场景

Event的灵活性使其适用于多种复杂的同步场景:

5.4.1 CPU驱动的条件渲染

Event可以实现由CPU决策控制GPU渲染流程:

// CPU驱动的条件渲染
void cpuDrivenConditionalRendering(VkDevice device, VkCommandBuffer cmdBuffer) {
    VkEvent conditionMetEvent = createEvent(device);
    
    // 1. 记录GPU命令,等待Event
    vkCmdWaitEvents(
        cmdBuffer,
        1, &conditionMetEvent,
        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        0, nullptr,
        0, nullptr,
        0, nullptr
    );
    
    // 2. 记录条件性执行的渲染命令
    recordConditionalRenderCommands(cmdBuffer);
    
    // 3. 提交命令缓冲区
    submitCommandBuffer(cmdBuffer);
    
    // 4. CPU执行计算并决定是否触发Event
    bool condition = performCPUCalculation();
    if (condition) {
        // 条件满足,触发Event,允许GPU执行条件渲染命令
        vkSetEvent(device, conditionMetEvent);
    } else {
        // 条件不满足,不触发Event,GPU将一直等待(需要超时机制)
        std::cout << "条件不满足,不执行渲染命令" << std::endl;
    }
    
    // 清理
    vkDestroyEvent(device, conditionMetEvent, nullptr);
}

这种模式适用于需要CPU进行复杂决策后才确定渲染内容的场景,如AI驱动的动态内容生成。

5.4.2 GPU内部的复杂同步

Event可以实现GPU内部更复杂的同步模式,如流水线式处理:

// GPU流水线处理同步
void gpuPipelineSynchronization(VkCommandBuffer cmdBuffer) {
    // 创建两个Event用于阶段间同步
    VkEvent stage1Complete = createEvent(device);
    VkEvent stage2Complete = createEvent(device);
    
    // 阶段1:数据预处理
    recordDataPreprocessing(cmdBuffer);
    
    // 标记阶段1完成
    vkCmdSetEvent(cmdBuffer, stage1Complete, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
    
    // 阶段2:等待阶段1完成,然后进行光照计算
    vkCmdWaitEvents(
        cmdBuffer,
        1, &stage1Complete,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
        0, nullptr,
        0, nullptr,
        0, nullptr
    );
    recordLightingCalculation(cmdBuffer);
    
    // 标记阶段2完成
    vkCmdSetEvent(cmdBuffer, stage2Complete, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
    
    // 阶段3:等待阶段2完成,然后进行最终渲染
    vkCmdWaitEvents(
        cmdBuffer,
        1, &stage2Complete,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        0, nullptr,
        0, nullptr,
        0, nullptr
    );
    recordFinalRendering(cmdBuffer);
    
    // 清理
    vkDestroyEvent(device, stage1Complete, nullptr);
    vkDestroyEvent(device, stage2Complete, nullptr);
}

这种流水线同步模式允许GPU以并行方式处理不同阶段的任务,提高整体效率。

5.4.3 CPU与GPU的数据依赖同步

当CPU和GPU需要交换数据并存在依赖关系时,Event是理想的同步选择:

// CPU与GPU数据依赖同步
void cpuGpuDataDependency(VkDevice device, VkQueue queue, VkCommandBuffer cmdBuffer, VkBuffer sharedBuffer) {
    VkEvent dataReadyEvent = createEvent(device);
    VkEvent processingCompleteEvent = createEvent(device);
    
    // 1. CPU准备初始数据
    prepareInitialData(sharedBuffer);
    
    // 2. 标记数据准备就绪
    vkSetEvent(device, dataReadyEvent);
    
    // 3. GPU等待数据就绪,然后处理数据
    vkCmdWaitEvents(
        cmdBuffer,
        1, &dataReadyEvent,
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
        0, nullptr,
        0, nullptr,
        0, nullptr
    );
    recordDataProcessingCommands(cmdBuffer, sharedBuffer);
    
    // 4. GPU处理完成后标记Event
    vkCmdSetEvent(cmdBuffer, processingCompleteEvent, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
    
    // 5. 提交GPU命令
    submitCommandBuffer(cmdBuffer, queue);
    
    // 6. CPU等待GPU处理完成
    if (cpuWaitForEvent(device, processingCompleteEvent)) {
        // 7. CPU处理GPU的结果
        processGpuResults(sharedBuffer);
    }
    
    // 清理
    vkDestroyEvent(device, dataReadyEvent, nullptr);
    vkDestroyEvent(device, processingCompleteEvent, nullptr);
}

这种双向同步模式适用于需要CPU和GPU协同处理数据的场景,如交互式模拟。

sequenceDiagram
    participant CPU
    participant GPU
    participant Buffer as 共享缓冲区
    
    CPU->>Buffer: 准备初始数据
    CPU->>GPU: 触发dataReadyEvent
    GPU->>GPU: 等待dataReadyEvent
    GPU->>Buffer: 处理数据
    GPU->>CPU: 触发processingCompleteEvent
    CPU->>CPU: 等待processingCompleteEvent
    CPU->>Buffer: 处理GPU结果

5.5 Event的性能考量

Event提供了最大的灵活性,但也可能带来性能开销,使用时需要注意:

5.5.1 避免过度使用

Event的灵活性使其容易被过度使用,导致性能下降:

// 不推荐:过度使用Event
void overuseEvents(VkCommandBuffer cmdBuffer) {
    // 创建过多不必要的Event
    std::vector<VkEvent> events;
    for (int i = 0; i < 10; i++) {
        events.push_back(createEvent(device));
    }
    
    // 为每个小操作都添加Event等待(低效)
    for (int i = 0; i < 10; i++) {
        recordSmallOperation(cmdBuffer, i);
        vkCmdSetEvent(cmdBuffer, events[i], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
        vkCmdWaitEvents(cmdBuffer, 1, &events[i], ...);
    }
}

// 推荐:合理使用Event
void合理使用Event
void useEventsJudiciously(VkCommandBuffer cmdBuffer) {
    // 只为关键同步点使用Event
    VkEvent majorStageComplete = createEvent(device);
    
    // 执行一系列相关操作
    recordRelatedOperations(cmdBuffer);
    
    // 只在主要阶段边界使用Event
    vkCmdSetEvent(cmdBuffer, majorStageComplete, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
    
    // ...
}

Event的使用应限于确实需要其灵活性的场景,简单的同步应优先使用管线屏障或信号量。

5.5.2 选择合适的触发和等待方式

根据场景选择CPU或GPU触发/等待Event:

// 选择合适的Event操作方式
void chooseProperEventOperations(VkDevice device, VkCommandBuffer cmdBuffer) {
    // 场景1:GPU内部同步 - 使用GPU触发和等待
    VkEvent gpuSyncEvent = createEvent(device);
    vkCmdSetEvent(cmdBuffer, gpuSyncEvent, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
    vkCmdWaitEvents(cmdBuffer, 1, &gpuSyncEvent, ...);
    
    // 场景2:CPU驱动的条件 - 使用CPU触发,GPU等待
    VkEvent cpuTriggerEvent = createEvent(device);
    vkCmdWaitEvents(cmdBuffer, 1, &cpuTriggerEvent, ...);
    // CPU端:vkSetEvent(device, cpuTriggerEvent);
    
    // 场景3:等待GPU结果 - 使用GPU触发,CPU等待
    VkEvent gpuResultEvent = createEvent(device);
    vkCmdSetEvent(cmdBuffer, gpuResultEvent, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
    // CPU端:cpuWaitForEvent(device, gpuResultEvent);
}

不同操作方式的性能特点:

  • GPU触发+GPU等待:最高效,完全在GPU内部处理
  • CPU触发+GPU等待:中等效率,适合CPU控制GPU流程
  • GPU触发+CPU等待:效率较低(需要轮询),仅在必要时使用
5.5.3 结合内存屏障使用

Event等待操作应与适当的内存屏障结合使用,确保数据可见性:

// 结合内存屏障使用Event
void eventWithMemoryBarriers(VkCommandBuffer cmdBuffer, VkEvent event, VkBuffer buffer, VkImage image) {
    // 定义内存屏障
    VkBufferMemoryBarrier bufferBarrier{};
    bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
    bufferBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
    bufferBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    bufferBarrier.buffer = buffer;
    bufferBarrier.offset = 0;
    bufferBarrier.size = VK_WHOLE_SIZE;
    
    VkImageMemoryBarrier imageBarrier{};
    imageBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    imageBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    imageBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    imageBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    imageBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageBarrier.image = image;
    imageBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
    
    // 等待Event并应用内存屏障
    vkCmdWaitEvents(
        cmdBuffer,
        1, &event,
        VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        0, nullptr,
        1, &bufferBarrier,
        1, &imageBarrier
    );
}

Event等待操作中的内存屏障确保了:

  1. 等待操作前后的内存访问顺序
  2. 数据在不同管线阶段之间的正确可见性
  3. 图像布局的正确转换

5.6 Event常见错误与解决方案

Event使用中的常见错误及其解决方案:

5.6.1 忘记处理未触发的Event

错误:Event未被触发,导致GPU永久等待。

// 错误示例:未处理未触发的Event
void unhandledUnsignaledEvent(VkDevice device, VkCommandBuffer cmdBuffer) {
    VkEvent event = createEvent(device);
    
    // 记录等待Event的命令
    vkCmdWaitEvents(cmdBuffer, 1, &event, ...);
    
    // 提交命令缓冲区
    submitCommandBuffer(cmdBuffer);
    
    // 错误:在某些条件下忘记触发Event
    if (someCondition) {
        vkSetEvent(device, event);
    }
    // 如果someCondition为false,Event永远不会触发,GPU将一直等待
}

解决方案:确保Event总会被触发,或设置超时机制:

// 正确示例:处理未触发的Event
void handleUnsignaledEvent(VkDevice device, VkCommandBuffer cmdBuffer, VkQueue queue) {
    VkEvent event = createEvent(device);
    VkFence fence = createFence(device);
    
    // 记录等待Event的命令
    vkCmdWaitEvents(cmdBuffer, 1, &event, ...);
    
    // 提交命令缓冲区,关联Fence
    submitCommandBuffer(cmdBuffer, queue, fence);
    
    // 设置超时检查
    auto startTime = std::chrono::high_resolution_clock::now();
    const uint64_t timeoutMs = 1000;  // 1秒超时
    
    // 根据条件触发Event
    if (someCondition) {
        vkSetEvent(device, event);
    } else {
        // 条件不满足,等待一段时间后强制触发Event
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        vkSetEvent(device, event);  // 确保触发,避免GPU永久等待
    }
    
    // 等待命令完成
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
    
    // 清理
    vkDestroyEvent(device, event, nullptr);
    vkDestroyFence(device, fence, nullptr);
}
5.6.2 Event与内存屏障配合不当

错误:Event等待时没有正确设置内存屏障,导致数据可见性问题。

// 错误示例:内存屏障缺失
void missingMemoryBarriers(VkCommandBuffer cmdBuffer, VkEvent event, VkBuffer buffer) {
    // 记录写入缓冲区的命令
    recordWriteBufferCommands(cmdBuffer, buffer);
    
    // 触发Event
    vkCmdSetEvent(cmdBuffer, event, VK_PIPELINE_STAGE_TRANSFER_BIT);
    
    // 等待Event,但没有内存屏障
    vkCmdWaitEvents(
        cmdBuffer,
        1, &event,
        VK_PIPELINE_STAGE_TRANSFER_BIT,
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        0, nullptr,
        0, nullptr,  // 错误:缺少缓冲区屏障
        0, nullptr
    );
    
    // 读取缓冲区(可能读取到旧数据)
    recordReadBufferCommands(cmdBuffer, buffer);
}

解决方案:在Event等待操作中添加适当的内存屏障:

// 正确示例:添加内存屏障
void properMemoryBarriers(VkCommandBuffer cmdBuffer, VkEvent event, VkBuffer buffer) {
    // 记录写入缓冲区的命令
    recordWriteBufferCommands(cmdBuffer, buffer);
    
    // 触发Event
    vkCmdSetEvent(cmdBuffer, event, VK_PIPELINE_STAGE_TRANSFER_BIT);
    
    // 定义缓冲区屏障
    VkBufferMemoryBarrier bufferBarrier{};
    bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
    bufferBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    bufferBarrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;
    bufferBarrier.buffer = buffer;
    bufferBarrier.offset = 0;
    bufferBarrier.size = VK_WHOLE_SIZE;
    
    // 等待Event并应用内存屏障
    vkCmdWaitEvents(
        cmdBuffer,
        1, &event,
        VK_PIPELINE_STAGE_TRANSFER_BIT,
        VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
        0, nullptr,
        1, &bufferBarrier,  // 正确:添加缓冲区屏障
        0, nullptr
    );
    
    // 安全读取缓冲区
    recordReadBufferCommands(cmdBuffer, buffer);
}
5.6.3 不必要的CPU轮询

错误:过度使用CPU轮询等待Event,占用过多CPU资源。

// 错误示例:过度轮询
void excessivePolling(VkDevice device, VkEvent event) {
    // 错误:没有延迟的紧密轮询
    while (vkGetEventStatus(device, event) != VK_EVENT_SET) {
        // 没有休眠,占用100% CPU
    }
}

解决方案:添加适当的休眠时间,或考虑使用Fence替代:

// 正确示例:优化轮询
void optimizedPolling(VkDevice device, VkEvent event) {
    // 初始快速轮询
    for (int i = 0; i < 10; i++) {
        if (vkGetEventStatus(device, event) == VK_EVENT_SET) {
            return;
        }
        std::this_thread::sleep_for(std::chrono::microseconds(10));
    }
    
    // 之后增加休眠时间
    while (vkGetEventStatus(device, event) != VK_EVENT_SET) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
}

// 更好的方案:使用Fence替代
void useFenceInstead(VkDevice device, VkQueue queue, VkFence fence) {
    // 提交命令并关联Fence
    submitCommands(queue, cmdBuffer, fence);
    
    // 使用高效的Fence等待
    vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
}
graph TD
    A[Event常见错误] --> B[未处理未触发的Event]
    A --> C[内存屏障配合不当]
    A --> D[不必要的CPU轮询]
    
    B --> B1[确保Event总会被触发]
    B --> B2[设置超时机制]
    C --> C1[在等待操作中添加适当的内存屏障]
    C --> C2[确保数据可见性]
    D --> D1[添加合理的休眠时间]
    D --> D2[适合场景使用Fence]

Event是Vulkan中最灵活的同步原语,适用于各种复杂的同步场景。但这种灵活性也带来了更高的使用复杂度和潜在的性能开销,应在确实需要时才使用。