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

18 KiB
Raw Blame History

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 适配:


\#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/WaitUEGraphicsFenceUnity |

| 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 阻塞耗时。