Files
XCEngine/docs/plan/end/RHI模块设计与实现/RHIFence.md
ssdfasd 08c01dd143 RHI: Refactor Fence module to pure timeline semantics
- Remove IsSignaled() from RHIFence interface (semantic inconsistency)
- Remove Reset() from OpenGL implementation (no D3D12 counterpart)
- OpenGL Fence now uses single GLsync + CPU counters for timeline simulation
- OpenGL Fence Initialize() now accepts uint64_t initialValue (was bool)
- Add comprehensive timeline semantics tests for all backends:
  - Signal increment/decrement scenarios
  - Multiple signals
  - Wait smaller than completed value
  - GetCompletedValue stages verification
- Update documentation to reflect actual implementation
2026-03-24 01:53:00 +08:00

433 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
RHIFence 设计GPU-CPU 同步的关键解析
\# RHIFence 设计GPU-CPU 同步的关键解析
\## 1. 概述
\### 1.1 什么是 RHIFence
RHIFence 全称 Render Hardware Interface Fence是现代跨平台图形引擎中负责实现 \*\*GPU 与 CPU 同步\*\*的核心抽象接口类。它属于引擎渲染硬件接口RHI层的核心组件核心价值是\*\*彻底屏蔽 D3D12、Vulkan、OpenGL、Metal 等底层图形 API 的同步机制差异\*\*,为引擎上层逻辑提供一套统一、通用、无平台耦合的同步控制接口,让上层引擎开发无需关注底层图形 API 的同步实现细节,专注于业务逻辑与渲染流程搭建。
\### 1.2 核心作用
\- \*\*GPU-CPU 精准同步\*\*:解决 CPU 与 GPU 异步执行导致的指令乱序问题,确保 GPU 渲染/计算任务按预期顺序执行,或让 CPU 按需等待 GPU 完成指定任务,避免并行执行冲突。
\- \*\*资源安全生命周期管理\*\*:防止 CPU 在 GPU 仍在使用纹理、模型、缓冲区等资源时,提前释放资源引发的程序崩溃、画面花屏、显存非法访问等致命问题。
\- \*\*多任务并行流程控制\*\*:支持批量任务同步、多帧并行调度、非阻塞任务进度查询,适配现代引擎多帧缓冲、流水线渲染的高性能架构。
\- \*\*跨平台一致性保障\*\*:统一不同图形 API 的同步调用逻辑,实现“一套同步代码,全平台运行”,大幅降低引擎跨平台适配的开发与维护成本。
\## 2. RHIFence 标准接口定义
\### 2.1 引擎通用接口源码示例
这是商业游戏引擎中最经典的 RHIFence 抽象接口定义,完全贴合上层调用需求,同时兼容所有底层图形 API 适配:
```cpp
\#pragma once
// 引入 RHI 层基础类型定义(包含 uint64\_t 等基础类型、平台宏定义)
\#include "RHITypes.h"
namespace XCEngine {
namespace RHI {
/// RHIFence 抽象接口GPU-CPU 同步核心抽象类
/// 所有平台底层 Fence 实现类均需继承此类,实现纯虚函数
class RHIFence {
public:
  /// 虚析构函数:确保子类析构时能正确调用自身析构逻辑,避免内存泄漏
  virtual \~RHIFence() = default;
  /// 显式关闭销毁接口:释放底层图形 API 相关句柄、显存、系统内存资源
  /// 区别于析构函数,用于引擎主动控制资源销毁时机,适配引擎资源池管理逻辑
  virtual void Shutdown() = 0;
  /// 无参信号触发:将 Fence 标记为已触发状态,适用于二元语义场景
  virtual void Signal() = 0;
  /// 带值信号触发:为 Fence 指定 uint64\_t 目标值,适用于数值语义场景
  /// 核心接口,标记 GPU 任务完成后的目标进度值
  virtual void Signal(uint64\_t value) = 0;
  /// 数值等待CPU 阻塞等待,直到 Fence 完成值大于等于指定目标值
  /// 数值语义核心等待接口
  virtual void Wait(uint64\_t value) = 0;
  /// 获取当前已完成值:非阻塞查询 GPU 当前执行到的 Fence 进度值
  /// 用于实时监控任务进度,不阻塞 CPU 执行
  virtual uint64\_t GetCompletedValue() const = 0;
  /// 获取底层原生句柄:返回对应图形 API 的原生 Fence 对象指针
  /// 用于引擎底层特殊扩展操作、平台专属调试、第三方库对接
  virtual void\* GetNativeHandle() = 0;
};
} // namespace RHI
} // namespace XCEngine
```
\### 2.2 接口全维度详解
| 接口名 | 核心功能说明 | 底层实现逻辑 | 实际工程应用场景 |
|--------|--------------|--------------|------------------|
| `virtual \~RHIFence() = default;` | 虚析构函数,保证多态析构,子类资源正常释放 | 编译器默认生成,确保子类重写析构时被正确调用 | 引擎退出、资源池销毁时,自动释放 Fence 相关资源 |
| `virtual void Shutdown() = 0;` | 主动释放底层 API 原生句柄、内存资源,显式资源清理 | 各平台分别实现D3D12 释放 ID3D12FenceVulkan 销毁 VkFenceOpenGL 删除 GLsync | 引擎主动回收闲置 Fence、场景切换、资源卸载时调用 |
| `virtual void Signal() = 0;` | 二元语义触发,仅标记“已完成/未完成”两种状态 | 底层调用对应 API 二元信号接口,切换 Fence 状态 | 简单单帧同步、轻量级 GPU 任务完成标记 |
| `virtual void Signal(uint64\_t value) = 0;` | 数值语义核心接口,指定 GPU 任务完成后的目标进度值 | D3D12/Metal 直接设置原生数值Vulkan 通过 Timeline Semaphore 赋值OpenGL 维护计数器映射 | 多帧并行标记、批量任务进度标注、帧缓冲同步 |
| `virtual void Wait(uint64\_t value) = 0;` | 数值等待CPU 阻塞至 Fence 完成值达标 | 原生数值 API 直接等待,二元 API 等待对应映射的同步对象 | 资源安全销毁前等待、CPU 读取 GPU 计算结果前等待 |
| `virtual uint64\_t GetCompletedValue() const = 0;` | 非阻塞查询当前完成进度,无 CPU 阻塞开销 | 原生数值 API 直接读取,二元 API 返回 CPU 维护的计数器 | 实时监控 GPU 任务进度、动态调整 CPU 提交节奏、性能调试 |
| `virtual void\* GetNativeHandle() = 0;` | 获取底层原生对象指针,暴露平台专属能力 | 返回对应 API 原生对象地址ID3D12Fence\*、VkFence、GLsync、MTLFence\* | 底层调试、平台专属优化、第三方图形库对接 |
\## 3. 核心概念:数值语义 vs 二元语义
RHIFence 的核心设计围绕\*\*语义类型\*\*展开,也是区分底层 API 差异、引擎上层统一调用的关键,必须彻底区分两种语义的核心特性与适用场景。
\### 3.1 二元语义Binary Fence
二元语义是最基础的同步语义,仅包含\*\*未触发\*\*和\*\*已触发\*\*两种绝对状态,无中间进度,类比为“普通电灯开关”,只有开和关两种状态,无法表示亮度等级。
\- 核心特点:无数值概念,仅标记任务是否完成,无法区分多任务、多帧的进度差异;
\- 底层限制同步对象多为一次性OpenGL或需手动重置Vulkan批量同步需创建多个同步对象
\- 适用场景:仅适用于简单单任务、单帧同步,无法满足现代引擎多帧并行需求。
\### 3.2 数值语义Timeline Fence/时间线语义)
数值语义是现代高性能引擎必备的高级同步语义,通过 \*\*uint64\_t 递增数值\*\*标记 GPU 任务进度,类比为“汽车里程表”,数值持续递增,可精准标记每一个任务、每一帧的完成进度。
\- 核心特点:单个 Fence 可标记无限多任务/多帧进度,支持非阻塞进度查询、批量等待;
\- 核心优势:无需创建多个同步对象,内存开销极低,代码逻辑极简,完美适配多帧缓冲架构;
\- 适用场景:全场景同步,尤其是多帧并行、资源安全管理、批量任务同步等核心工程场景。
\### 3.3 主流图形 API 语义支持对比
| 图形 API | 原生 Fence 语义 | 数值语义支持情况 | 底层实现细节说明 |
|----------|----------------|------------------|------------------|
| D3D12 | Timeline数值语义 | ✅ 原生原生支持,无额外开销 | 核心对象 ID3D12Fence 天生绑定 uint64\_t 数值,所有接口直接支持数值操作 |
| Vulkan | Binary二元语义 | ❌ 原生不支持,需通过 Timeline Semaphore 扩展模拟 | 原生 VkFence 仅二元状态Vulkan 1.2 后通过 VK\_KHR\_timeline\_semaphore 核心扩展实现数值能力 |
| OpenGL | BinaryGLsync | ❌ 纯二元语义,无任何原生数值支持 | 无 Fence 概念,仅 GLsync 同步对象,一次性使用,触发后无法重置,需频繁创建销毁 |
| Metal | Timeline数值语义 | ✅ 原生原生支持,与 D3D12 完全一致 | MTLFence 原生绑定 uint64\_t 数值,接口设计与 D3D12 高度趋同 |
\## 4. 各底层图形 API 适配实现详解
\### 4.1 D3D12原生数值语义实现
D3D12 是数值语义的标杆 API原生 Fence 完全为多帧并行、批量同步设计,是引擎 RHI 层数值语义的核心参考。
\#### 核心特性
\- 原生 ID3D12Fence 对象自带 uint64\_t 完成值,初始值可自定义(默认 0
\- 数值更新规则:\*\*CPU 主动指定目标值GPU 执行完指令队列后被动赋值\*\*GPU 绝不会自动 +1数值步长、更新时机完全由 CPU 控制;
\- 常见用法:每帧递增 1引擎通用惯例也可按任务批次自定义步长如 10、100仅需保证数值递增即可。
\#### 标准执行流程
1\. CPU 向 GPU 命令队列提交渲染/计算指令;
2\. CPU 调用 `CommandQueue->Signal(Fence, 目标值)`,告知 GPU 执行完指令后更新 Fence 数值;
3\. GPU 异步执行指令,执行完毕后自动将 Fence 完成值设为 CPU 指定的目标值;
4\. CPU 通过 `Wait(目标值)` 阻塞等待,或 `GetCompletedValue()` 非阻塞查询进度。
\#### 核心优势
单个 Fence 可管理全帧所有任务,无需额外同步对象,性能最优,逻辑最简。
\### 4.2 Vulkan二元语义模拟数值语义
Vulkan 原生 Fence 仅支持二元状态,但其设计理念为“最小化底层抽象”,通过 Timeline Semaphore 扩展补齐数值语义能力,是引擎 RHI 层适配的主流方案。
\#### 原生二元 Fence 限制
\- 仅支持 `未触发/已触发` 两种状态,无数值概念;
\- 可手动重置复用,比 OpenGL GLsync 更灵活,但批量同步仍需多个 Fence 管理。
\#### 数值语义模拟方案(引擎标准方案)
采用 Vulkan 1.2 核心扩展 `VK\_KHR\_timeline\_semaphore`,将时间线信号量封装为 RHIFence模拟数值语义
1\. 创建 `VK\_SEMAPHORE\_TYPE\_TIMELINE` 类型的信号量,初始值设为 0
2\. 提交指令队列时,绑定信号量并指定目标数值,替代原生 Fence
3\. CPU 等待信号量数值达标,实现与 D3D12 完全一致的数值等待逻辑;
4\. RHI 层屏蔽信号量与 Fence 的差异,上层仅感知数值语义接口。
\#### 备选模拟方案(低版本 Vulkan
维护 CPU 端 uint64\_t 计数器 + 二元 Fence 池,每个计数器值对应一个二元 Fence通过映射关系模拟数值进度仅适用于老旧设备兼容。
\### 4.3 OpenGL纯二元语义适配
OpenGL 无标准 Fence 概念,仅提供 `GLsync` 一次性同步对象,是适配难度最高、限制最多的 API
\#### 核心限制
\- `GLsync` 为一次性对象,触发后无法重置,必须销毁重建;
\- 无任何数值相关接口,仅支持 `glClientWaitSync` 阻塞等待、`glWaitSync` GPU 等待;
\- 仅支持 CPU 等待 GPU 指令完成,无 GPU 内部队列同步能力。
\#### 引擎适配方案
RHI 层维护 **CPU 端数值计数器 + 单个 GLsync**,通过等待旧 sync 后重建新 sync 的方式模拟数值语义:
1\. 调用 `Signal(value)` 时,若存在旧 sync 则等待其完成并销毁,然后更新 `signaledValue` 并创建新 GLsync
2\. 调用 `Wait(value)` 时,若 `completedValue >= value` 直接返回(已完成),否则等待 sync 完成并更新 `completedValue = signaledValue`
3\. 调用 `GetCompletedValue()` 时,检查 sync 状态,若已 signal 则返回 `signaledValue`,否则返回 `completedValue`
4\. 上层完全屏蔽 GLsync 管理逻辑,仅暴露数值语义接口。
\### 4.4 Metal原生数值语义实现
Metal 作为苹果平台专属图形 API完全对标 D3D12 设计,原生支持数值语义,适配逻辑极简:
\#### 核心特性
\- 核心对象 `MTLFence` 自带 `completedValue` 属性uint64\_t 数值类型;
\- 接口调用:`signalFence:value:` 设置目标值,`waitUntilCompletedValue:` 执行等待;
\- 数值规则与 D3D12 完全一致CPU 控制数值GPU 执行后赋值,无自动递增逻辑。
\#### 引擎适配
直接封装原生 `MTLFence` 接口,无需任何模拟逻辑,性能与 D3D12 持平。
\## 5. 数值语义核心工程应用场景
数值语义是现代引擎 RHIFence 的核心价值所在所有商业引擎UE/Unity的核心同步逻辑均依赖数值语义以下为最常用的工程场景
\### 5.1 资源安全销毁/回收(引擎最核心场景)
\#### 问题背景
GPU 执行指令存在延迟CPU 提交指令后不会等待 GPU 执行完毕,若此时 CPU 直接销毁 GPU 正在使用的资源纹理、模型、缓冲区GPU 会访问已释放显存,导致程序崩溃、画面花屏。
\#### 解决方案
采用\*\*三帧延迟销毁\*\*机制(引擎通用经验值):
1\. CPU 标记资源为“待销毁”,加入延迟回收队列;
2\. 记录当前 Fence 目标值 = 当前完成值 + 3
3\. CPU 等待 Fence 完成值 ≥ 目标值(确保 GPU 执行完 3 帧,彻底不再使用该资源);
4\. 等待完成后,真正释放资源,彻底规避显存非法访问。
\#### 为何选择 3 帧
现代引擎普遍采用\*\*三帧缓冲架构\*\*CPU 最多提前提交 3 帧指令,等待 3 帧可 100% 保证 GPU 完成所有涉及该资源的指令,是性能与安全性的最优平衡。
\### 5.2 跨帧批量数据同步
\#### 问题背景
GPU 执行粒子模拟、光照计算、物理演算等大规模计算任务时会拆分至多帧执行CPU 需等待所有帧计算完毕后,再读取结果,避免逐帧等待导致的 CPU 阻塞开销。
\#### 解决方案
CPU 提交多帧计算指令,为每帧指定递增 Fence 数值,最后等待最终帧数值达标,一次性读取所有计算结果,大幅减少 CPU 等待次数,提升整体运行效率。
\### 5.3 帧率节流与性能稳定
\#### 问题背景
主机、移动端等性能受限平台CPU 提交指令速度远快于 GPU 执行速度,会导致 GPU 指令队列积压,引发帧率波动、卡顿、发热过高问题。
\#### 解决方案
CPU 主动节流:每提交 3 帧指令,等待 GPU 完成对应 Fence 数值,再继续提交新指令,控制 GPU 队列长度,保证帧率稳定,平衡 CPU 与 GPU 负载。
\### 5.4 大场景切换与资源加载同步
\#### 问题背景
游戏大场景切换时需加载大量纹理、模型资源GPU 需多帧完成资源上传与初始化,若 CPU 提前切换场景,会出现资源未加载完毕导致的黑屏、花屏。
\#### 解决方案
CPU 提交多帧资源上传指令,标记 Fence 最终目标值,等待数值达标后再执行场景切换,确保所有资源 GPU 端初始化完毕,保证场景切换流畅无异常。
\## 6. 商业游戏引擎UE/UnityRHIFence 设计规范
Unreal Engine、Unity 作为成熟商业引擎,其 RHIFence 设计完全遵循\*\*上层数值语义统一,底层平台适配\*\*的核心原则,是行业标准设计:
\### 6.1 核心设计原则
\- \*\*上层接口统一暴露数值语义\*\*:引擎上层渲染、业务逻辑、插件均仅调用数值语义接口(`Signal(uint64\_t)``Wait(uint64\_t)``GetCompletedValue()`),完全屏蔽底层 API 差异;
\- \*\*底层平台差异化适配\*\*:原生支持数值语义的 APID3D12/Metal直接封装原生接口二元语义 APIVulkan/OpenGL通过扩展/计数器模拟数值语义,上层无感知;
\- \*\*资源池化管理\*\*Fence 对象统一由 RHI 层池化管理,避免频繁创建销毁带来的性能开销。
\### 6.2 各平台底层适配明细
| 引擎层级 | 接口规范 | 底层平台适配方案 |
|----------|----------|------------------|
| 引擎上层 | 纯数值语义接口,无任何平台相关代码 | 统一调用 `FRHIFence::Signal`/`Wait`UE`GraphicsFence`Unity |
| D3D12 平台 | 原生数值 Fence 直接封装 | 直接调用 ID3D12Fence 相关接口,无额外模拟开销 |
| Metal 平台 | 原生数值 Fence 直接封装 | 直接封装 MTLFence 接口,逻辑与 D3D12 完全一致 |
| Vulkan 平台 | Timeline Semaphore 模拟 | 采用 Vulkan 1.2+ 时间线信号量,封装为数值 Fence |
| OpenGL 平台 | 单个 GLsync + CPU 计数器模拟 | CPU 维护 signaledValue/completedValueSignal 时重建 GLsyncWait 时等待 sync 完成 |
\### 6.3 引擎选择数值语义的核心原因
1\. \*\*上层逻辑无平台耦合\*\*:一套同步代码适配全平台,开发维护成本极低;
2\. \*\*覆盖全场景同步需求\*\*:同时支持单帧、多帧、批量、非阻塞查询,二元语义无法实现;
3\. \*\*性能最优\*\*:原生数值 API 无额外开销,模拟方案经引擎极致优化,性能损失可忽略;
4\. \*\*生态兼容\*\*:第三方插件、渲染库均基于数值语义开发,保证生态兼容性。
\## 7. 核心总结与关键认知
\### 7.1 核心要点总结
1\. \*\*数值语义是现代引擎刚需\*\*:是多帧并行、资源安全管理、高性能同步的核心基础,二元语义仅能满足基础轻量同步,无法适配商业引擎需求;
2\. \*\*API 差异本质\*\*D3D12/Metal 原生支持数值语义Vulkan/OpenGL 需模拟,引擎 RHI 层的核心价值就是屏蔽这一差异;
3\. \*\*D3D12 数值关键认知\*\*Fence 数值绝非 GPU 自动递增,完全由 CPU 主动指定,每帧 +1 是引擎通用惯例,而非 API 强制规则;
4\. \*\*Vulkan 误区纠正\*\*Vulkan 并非不支持数值语义,而是通过 Timeline Semaphore 实现,并非原生 Fence 支持;
5\. \*\*三帧等待的意义\*\*:用短暂 CPU 阻塞,换取资源绝对安全,是现代引擎三帧缓冲架构的标准配套方案。
\### 7.2 工程开发关键注意事项
\- 永远不要在 GPU 未完成任务时释放资源,必须通过 Fence 等待完成;
\- 优先使用数值语义接口,避免二元语义导致的多对象管理混乱;
\- Vulkan 平台优先使用 Timeline Semaphore 模拟,放弃老旧二元 Fence 池方案;
\- OpenGL 平台需做好 GLsync 的销毁管理,每次 Signal 前确保旧 sync 已等待完成并销毁;
\- 非阻塞查询优先使用 `GetCompletedValue()`,减少 CPU 阻塞耗时。