VULKAN 同步机制原理解析
一、Vulkan同步机制概述
Vulkan作为一款底层图形API,其设计哲学之一是显式同步。与OpenGL等API的隐式同步不同,Vulkan要求开发者手动管理GPU操作之间的同步关系,这虽然增加了编程复杂度,但也为性能优化提供了更大的空间。
Vulkan同步机制的核心目标是解决以下问题:
- 资源竞争:多个GPU操作同时访问同一资源(如纹理、缓冲区)时的冲突
- 执行顺序:确保GPU操作按预期顺序执行
- 数据可见性:保证一个操作的结果能被后续操作正确读取
- CPU-GPU协作:协调CPU与GPU之间的工作节奏
1.1 同步机制的核心组件
Vulkan提供了四种核心同步原语,它们分别适用于不同的同步场景:
- Pipeline Barrier(管线屏障):用于同一队列中不同管线阶段之间的同步
- Semaphore(信号量):用于不同队列之间或队列与呈现引擎之间的同步
- Fence( fences):用于CPU与GPU之间的同步
- 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提供了三种类型的管线屏障,分别用于不同类型资源的同步:
- 通用内存屏障(Global Memory Barrier):用于全局范围的同步,不特定于某个资源
- 缓冲区内存屏障(Buffer Memory Barrier):用于缓冲区资源的同步
- 图像内存屏障(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 // 不使用图像屏障
);
}
缓冲区屏障的典型应用流程:
- CPU将数据传输到缓冲区(传输阶段,写入操作)
- 插入缓冲区屏障
- 顶点着色器从缓冲区读取数据(顶点着色器阶段,读取操作)
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
图像屏障的典型应用流程:
- 渲染到图像作为颜色附件(颜色附件输出阶段,写入操作)
- 插入图像屏障,将图像布局从
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL转换为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - 片段着色器读取该图像作为纹理(片段着色器阶段,读取操作)
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的命令...
}
复杂场景中的屏障序列通常遵循渲染流程的自然阶段:
- 资源加载和传输阶段
- 几何处理阶段
- 光照和计算阶段
- 最终渲染阶段
每个阶段之间插入适当的屏障,确保数据正确传递。
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);
}
信号量的工作流程可以概括为:
- 一个队列提交命令时指定要信号的信号量
- 另一个队列提交命令时指定要等待的信号量
- 当第一个队列的命令执行完成,信号量被触发
- 第二个队列在指定的管线阶段等待信号量,一旦信号量被触发就继续执行
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 二元信号量的生命周期
二元信号量的典型生命周期:
- 创建时处于未触发状态
- 当关联的命令提交完成后,被自动触发(信号操作)
- 当等待它的命令开始执行时,被自动重置为未触发状态(等待操作)
// 二元信号量的完整使用示例
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信号量支持一些二元信号量不具备的高级特性:
- 直接从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信号量失败!");
}
}
- 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信号量失败!");
}
}
- 查询当前值:可以查询Timeline信号量的当前值
// 查询Timeline信号量的当前值
uint64_t getTimelineValue(VkDevice device, VkSemaphore timelineSemaphore) {
uint64_t currentValue;
if (vkGetSemaphoreCounterValue(device, timelineSemaphore, ¤tValue) != 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);
}
交换链同步的工作流程:
vkAcquireNextImageKHR获取图像,当图像可用时触发imageAvailableSemaphore- 渲染命令等待
imageAvailableSemaphore,完成后触发renderFinishedSemaphore 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的工作流程:
- CPU创建Fence并提交关联了该Fence的GPU命令
- GPU执行命令
- CPU可以选择等待Fence或查询其状态
- 当GPU完成命令执行,Fence被触发
- 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的工作流程:
- 创建Event(初始状态为未触发)
- 在命令缓冲区中插入等待Event的指令
- 提交命令缓冲区,GPU执行到等待指令时会暂停
- 当满足条件时(可以是CPU或GPU操作),触发Event
- 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等待操作中的内存屏障确保了:
- 等待操作前后的内存访问顺序
- 数据在不同管线阶段之间的正确可见性
- 图像布局的正确转换
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中最灵活的同步原语,适用于各种复杂的同步场景。但这种灵活性也带来了更高的使用复杂度和潜在的性能开销,应在确实需要时才使用。