From 08c01dd143aaafff58741d85527a5edfab025bff Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 24 Mar 2026 01:53:00 +0800 Subject: [PATCH] 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 --- docs/plan/end/RHI模块设计与实现/RHIFence.md | 432 ++++++++++++++++++ docs/plan/end/RHI模块设计与实现/总览.md | 63 +++ .../include/XCEngine/RHI/D3D12/D3D12Fence.h | 2 - .../include/XCEngine/RHI/OpenGL/OpenGLFence.h | 20 +- engine/include/XCEngine/RHI/RHIFence.h | 1 - engine/src/RHI/D3D12/D3D12Fence.cpp | 1 - engine/src/RHI/OpenGL/OpenGLFence.cpp | 88 ++-- tests/RHI/D3D12/unit/test_fence.cpp | 71 +++ tests/RHI/OpenGL/unit/test_fence.cpp | 87 +++- tests/RHI/unit/test_fence.cpp | 99 +++- 10 files changed, 769 insertions(+), 95 deletions(-) create mode 100644 docs/plan/end/RHI模块设计与实现/RHIFence.md create mode 100644 docs/plan/end/RHI模块设计与实现/总览.md diff --git a/docs/plan/end/RHI模块设计与实现/RHIFence.md b/docs/plan/end/RHI模块设计与实现/RHIFence.md new file mode 100644 index 00000000..69e5cbe0 --- /dev/null +++ b/docs/plan/end/RHI模块设计与实现/RHIFence.md @@ -0,0 +1,432 @@ +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 释放 ID3D12Fence,Vulkan 销毁 VkFence,OpenGL 删除 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 | Binary(GLsync) | ❌ 纯二元语义,无任何原生数值支持 | 无 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/Unity)RHIFence 设计规范 + +Unreal Engine、Unity 作为成熟商业引擎,其 RHIFence 设计完全遵循\*\*上层数值语义统一,底层平台适配\*\*的核心原则,是行业标准设计: + + + +\### 6.1 核心设计原则 + +\- \*\*上层接口统一暴露数值语义\*\*:引擎上层渲染、业务逻辑、插件均仅调用数值语义接口(`Signal(uint64\_t)`、`Wait(uint64\_t)`、`GetCompletedValue()`),完全屏蔽底层 API 差异; + +\- \*\*底层平台差异化适配\*\*:原生支持数值语义的 API(D3D12/Metal)直接封装原生接口;二元语义 API(Vulkan/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/completedValue,Signal 时重建 GLsync,Wait 时等待 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 阻塞耗时。 + + + diff --git a/docs/plan/end/RHI模块设计与实现/总览.md b/docs/plan/end/RHI模块设计与实现/总览.md new file mode 100644 index 00000000..10c5a7ae --- /dev/null +++ b/docs/plan/end/RHI模块设计与实现/总览.md @@ -0,0 +1,63 @@ +# RHI 渲染模块设计文档 +## 1. 项目背景 +本项目旨在参考 Unity 渲染架构,为已有的 **OpenGL** 、**Direct3D 12** 和 **Vulkan** 图形 API 后端设计统一的**渲染硬件抽象层(RHI)**,屏蔽 API 差异,实现引擎上层逻辑与底层图形 API 的解耦。 + +## 2. 核心设计理念 +### 2.1 总体原则 +**求同存异,分层抽象,特性降级,底层逃逸** +- **求同存异**:优先提取 API 共性作为核心抽象,差异部分通过模拟/降级处理 +- **分层抽象**:通过清晰的层级结构隔离 API 差异 +- **特性降级**:对高级特性提供能力检测和替代方案 +- **底层逃逸**:允许直接访问原生 API 以满足极端需求 + +### 2.2 差异分类与处理策略 +| 差异类型 | 典型示例 | 处理方案 | +|----------|----------|----------| +| 概念命名不同但本质相似 | D3D12 `CommandList` ≈ OpenGL 状态机绘制 | 直接统一抽象 | +| 显式控制 vs 隐式管理 | D3D12 `DescriptorHeap` vs OpenGL 纹理单元 | 提供"自动模式"+"显式模式" | +| API 独有高级特性 | D3D12 光线追踪、网格着色器 | 特性检测 + 降级方案 + 底层逃逸 | + + +## 3. RHI 分层架构 +### 3.1 通用分层模型 +``` +┌─────────────────────────────────┐ +│ 引擎功能层(场景/材质/光照) │ +├─────────────────────────────────┤ +│ 渲染管线层(SRP/Render Graph) │ +├─────────────────────────────────┤ +│ RHI 抽象层(统一接口) │ ← 核心设计重点 +├─────────────────────────────────┤ +│ API 后端层(D3D12/OpenGL) │ +├─────────────────────────────────┤ +│ 驱动/硬件层 │ +└─────────────────────────────────┘ +``` + +### 3.2 层级职责说明 +1. **引擎功能层**:提供场景管理、材质系统、光照等高级功能接口 +2. **渲染管线层**:定义渲染流程(前向/延迟渲染) +3. **RHI 抽象层**:统一图形 API 接口,屏蔽差异 +4. **API 后端层**:针对具体 API 的实现 +5. **驱动/硬件层**:最终执行渲染指令 + + +## 4. RHI 抽象基类设计 +include/XCEngine/RHI/ +├── RHIEnums.h # 通用枚举 +├── RHITypes.h # 通用结构体 +├── RHICapabilities.h # 硬件能力检测 +├── RHIDevice.h # 设备抽象(核心入口) +├── RHICommandQueue.h # 命令队列抽象 +├── RHICommandList.h # 命令列表抽象 +├── RHIBuffer.h # 缓冲区抽象 +├── RHITexture.h # 纹理抽象 +├── RHIShader.h # 着色器抽象 +├── RHIPipelineLayout.h # 管线布局抽象(替代 RootSignature) +├── RHIPipelineState.h # 管线状态抽象 +├── RHISwapChain.h # 交换链抽象 +├── RHIFence.h # 同步栅栏抽象 +├── RHIDescriptorPool.h # 描述符池抽象 +└── RHIFactory.h # RHI 工厂类 + + diff --git a/engine/include/XCEngine/RHI/D3D12/D3D12Fence.h b/engine/include/XCEngine/RHI/D3D12/D3D12Fence.h index ef2f4af7..310c600b 100644 --- a/engine/include/XCEngine/RHI/D3D12/D3D12Fence.h +++ b/engine/include/XCEngine/RHI/D3D12/D3D12Fence.h @@ -22,7 +22,6 @@ public: void Signal(uint64_t value) override; void Wait(uint64_t value) override; uint64_t GetCompletedValue() const override; - bool IsSignaled() const override { return m_fence->GetCompletedValue() >= m_signalValue; } void* GetEventHandle() { return m_eventHandle; } void* GetNativeHandle() override { return m_fence.Get(); } @@ -31,7 +30,6 @@ public: private: ComPtr m_fence; void* m_eventHandle; - uint64_t m_signalValue = UINT64_MAX; }; } // namespace RHI diff --git a/engine/include/XCEngine/RHI/OpenGL/OpenGLFence.h b/engine/include/XCEngine/RHI/OpenGL/OpenGLFence.h index 243bb205..8ece1a44 100644 --- a/engine/include/XCEngine/RHI/OpenGL/OpenGLFence.h +++ b/engine/include/XCEngine/RHI/OpenGL/OpenGLFence.h @@ -1,44 +1,34 @@ #pragma once #include +#include #include "../RHIFence.h" namespace XCEngine { namespace RHI { -enum class FenceStatus { - Signaled, - Unsignaled, - Error -}; - class OpenGLFence : public RHIFence { public: OpenGLFence(); ~OpenGLFence() override; - bool Initialize(bool signaled = false); + bool Initialize(uint64_t initialValue = 0); void Shutdown() override; void Signal() override; void Signal(uint64_t value) override; void Wait(uint64_t value) override; - void Reset(); - bool IsSignaled() const override; - FenceStatus GetStatus() const; uint64_t GetCompletedValue() const override; - uint64_t GetCurrentValue() const { return m_fenceValue; } void* GetNativeHandle() override; private: void* m_sync; - uint64_t m_fenceValue; - uint64_t m_completedValue; - bool m_signaled; + std::atomic m_signaledValue; + std::atomic m_completedValue; }; } // namespace RHI -} // namespace XCEngine +} // namespace XCEngine \ No newline at end of file diff --git a/engine/include/XCEngine/RHI/RHIFence.h b/engine/include/XCEngine/RHI/RHIFence.h index 47a7a423..54b4d65d 100644 --- a/engine/include/XCEngine/RHI/RHIFence.h +++ b/engine/include/XCEngine/RHI/RHIFence.h @@ -15,7 +15,6 @@ public: virtual void Signal(uint64_t value) = 0; virtual void Wait(uint64_t value) = 0; virtual uint64_t GetCompletedValue() const = 0; - virtual bool IsSignaled() const = 0; virtual void* GetNativeHandle() = 0; }; diff --git a/engine/src/RHI/D3D12/D3D12Fence.cpp b/engine/src/RHI/D3D12/D3D12Fence.cpp index abcb86c1..188bb17e 100644 --- a/engine/src/RHI/D3D12/D3D12Fence.cpp +++ b/engine/src/RHI/D3D12/D3D12Fence.cpp @@ -38,7 +38,6 @@ void D3D12Fence::Signal() { } void D3D12Fence::Signal(uint64_t value) { - m_signalValue = value; m_fence->Signal(value); } diff --git a/engine/src/RHI/OpenGL/OpenGLFence.cpp b/engine/src/RHI/OpenGL/OpenGLFence.cpp index 00ba9929..5b674d4b 100644 --- a/engine/src/RHI/OpenGL/OpenGLFence.cpp +++ b/engine/src/RHI/OpenGL/OpenGLFence.cpp @@ -4,21 +4,19 @@ namespace XCEngine { namespace RHI { -OpenGLFence::OpenGLFence() +OpenGLFence::OpenGLFence() : m_sync(nullptr) - , m_fenceValue(0) - , m_completedValue(0) - , m_signaled(false) { + , m_signaledValue(0) + , m_completedValue(0) { } OpenGLFence::~OpenGLFence() { Shutdown(); } -bool OpenGLFence::Initialize(bool signaled) { - m_fenceValue = signaled ? 1 : 0; - m_completedValue = m_fenceValue; - m_signaled = signaled; +bool OpenGLFence::Initialize(uint64_t initialValue) { + m_signaledValue.store(initialValue, std::memory_order_release); + m_completedValue.store(initialValue, std::memory_order_release); return true; } @@ -27,64 +25,56 @@ void OpenGLFence::Shutdown() { glDeleteSync(static_cast(m_sync)); m_sync = nullptr; } + m_signaledValue.store(0, std::memory_order_release); + m_completedValue.store(0, std::memory_order_release); } void OpenGLFence::Signal() { - glFlush(); - m_fenceValue++; - m_signaled = true; + Signal(1); } void OpenGLFence::Signal(uint64_t value) { - glFlush(); - m_fenceValue = value; - m_completedValue = value; - m_signaled = true; - if (m_sync) { - glDeleteSync(static_cast(m_sync)); - } - m_sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); -} - -void OpenGLFence::Wait(uint64_t timeoutNs) { - if (m_signaled && m_sync) { - GLsync sync = static_cast(m_sync); - glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED); - } - glFinish(); - m_completedValue = m_fenceValue; - m_signaled = true; -} - -void OpenGLFence::Reset() { if (m_sync) { + glClientWaitSync(static_cast(m_sync), GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED); glDeleteSync(static_cast(m_sync)); m_sync = nullptr; } - m_signaled = false; + + glFlush(); + + m_signaledValue.store(value, std::memory_order_release); + m_sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); } -bool OpenGLFence::IsSignaled() const { - return m_signaled; -} - -FenceStatus OpenGLFence::GetStatus() const { - if (!m_sync) { - return m_signaled ? FenceStatus::Signaled : FenceStatus::Unsignaled; +void OpenGLFence::Wait(uint64_t value) { + uint64_t currentCompleted = m_completedValue.load(std::memory_order_acquire); + if (currentCompleted >= value) { + return; } - GLsync sync = static_cast(m_sync); - GLint status = 0; - glGetSynciv(sync, GL_SYNC_STATUS, sizeof(status), nullptr, &status); - - if (status == GL_SIGNALED) { - return FenceStatus::Signaled; + if (m_sync) { + glClientWaitSync(static_cast(m_sync), GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED); + glDeleteSync(static_cast(m_sync)); + m_sync = nullptr; } - return FenceStatus::Unsignaled; + + uint64_t signaled = m_signaledValue.load(std::memory_order_acquire); + m_completedValue.store(signaled, std::memory_order_release); } uint64_t OpenGLFence::GetCompletedValue() const { - return m_completedValue; + if (!m_sync) { + return m_completedValue.load(std::memory_order_acquire); + } + + GLint status = 0; + glGetSynciv(static_cast(m_sync), GL_SYNC_STATUS, sizeof(status), nullptr, &status); + + if (status == GL_SIGNALED) { + return m_signaledValue.load(std::memory_order_acquire); + } + + return m_completedValue.load(std::memory_order_acquire); } void* OpenGLFence::GetNativeHandle() { @@ -95,4 +85,4 @@ void* OpenGLFence::GetNativeHandle() { } } // namespace RHI -} // namespace XCEngine +} // namespace XCEngine \ No newline at end of file diff --git a/tests/RHI/D3D12/unit/test_fence.cpp b/tests/RHI/D3D12/unit/test_fence.cpp index ec4e4004..f7cec69e 100644 --- a/tests/RHI/D3D12/unit/test_fence.cpp +++ b/tests/RHI/D3D12/unit/test_fence.cpp @@ -105,6 +105,77 @@ TEST_F(D3D12TestFixture, Fence_Signal_Multiple) { EXPECT_EQ(fence->GetCompletedValue(), value3); CloseHandle(eventHandle); + fence->Shutdown(); + delete fence; +} + +TEST_F(D3D12TestFixture, Fence_Timeline_SignalIncrement) { + auto* fence = new D3D12Fence(); + ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0)); + + fence->Signal(1); + fence->Wait(1); + EXPECT_GE(fence->GetCompletedValue(), 1u); + + fence->Signal(5); + fence->Wait(5); + EXPECT_GE(fence->GetCompletedValue(), 5u); + + fence->Signal(10); + fence->Wait(10); + EXPECT_GE(fence->GetCompletedValue(), 10u); + + fence->Shutdown(); + delete fence; +} + +TEST_F(D3D12TestFixture, Fence_Timeline_SignalDecrement) { + auto* fence = new D3D12Fence(); + ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0)); + + fence->Signal(5); + fence->Wait(5); + EXPECT_GE(fence->GetCompletedValue(), 5u); + + fence->Signal(3); + fence->Wait(3); + EXPECT_GE(fence->GetCompletedValue(), 3u); + + fence->Shutdown(); + delete fence; +} + +TEST_F(D3D12TestFixture, Fence_Timeline_MultipleSignals) { + auto* fence = new D3D12Fence(); + ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0)); + + fence->Signal(10); + fence->Wait(10); + EXPECT_GE(fence->GetCompletedValue(), 10u); + + fence->Signal(20); + fence->Wait(20); + EXPECT_GE(fence->GetCompletedValue(), 20u); + + fence->Signal(30); + fence->Wait(30); + EXPECT_GE(fence->GetCompletedValue(), 30u); + + fence->Shutdown(); + delete fence; +} + +TEST_F(D3D12TestFixture, Fence_Timeline_WaitSmallerThanCompleted) { + auto* fence = new D3D12Fence(); + ASSERT_TRUE(fence->Initialize(GetDevice()->GetDevice(), 0)); + + fence->Signal(5); + fence->Wait(5); + + fence->Wait(3); + + EXPECT_GE(fence->GetCompletedValue(), 5u); + fence->Shutdown(); delete fence; } \ No newline at end of file diff --git a/tests/RHI/OpenGL/unit/test_fence.cpp b/tests/RHI/OpenGL/unit/test_fence.cpp index 6d7c267c..9e9c0d9a 100644 --- a/tests/RHI/OpenGL/unit/test_fence.cpp +++ b/tests/RHI/OpenGL/unit/test_fence.cpp @@ -6,10 +6,9 @@ using namespace XCEngine::RHI; TEST_F(OpenGLTestFixture, Fence_Initialize_Unsignaled) { OpenGLFence fence; - bool result = fence.Initialize(false); + bool result = fence.Initialize(0); ASSERT_TRUE(result); - EXPECT_EQ(fence.GetStatus(), FenceStatus::Unsignaled); fence.Shutdown(); } @@ -17,7 +16,7 @@ TEST_F(OpenGLTestFixture, Fence_Initialize_Unsignaled) { TEST_F(OpenGLTestFixture, Fence_Initialize_Signaled) { OpenGLFence fence; - bool result = fence.Initialize(true); + bool result = fence.Initialize(1); ASSERT_TRUE(result); @@ -26,36 +25,100 @@ TEST_F(OpenGLTestFixture, Fence_Initialize_Signaled) { TEST_F(OpenGLTestFixture, Fence_Signal_Wait) { OpenGLFence fence; - fence.Initialize(false); + fence.Initialize(0); fence.Signal(1); fence.Wait(1); - EXPECT_TRUE(fence.IsSignaled()); EXPECT_EQ(fence.GetCompletedValue(), 1u); fence.Shutdown(); } -TEST_F(OpenGLTestFixture, Fence_IsSignaled_ReturnsState) { +TEST_F(OpenGLTestFixture, Fence_Timeline_SignalIncrement) { OpenGLFence fence; - fence.Initialize(false); + fence.Initialize(0); fence.Signal(1); fence.Wait(1); + EXPECT_EQ(fence.GetCompletedValue(), 1u); - EXPECT_TRUE(fence.IsSignaled()); + fence.Signal(5); + fence.Wait(5); + EXPECT_EQ(fence.GetCompletedValue(), 5u); + + fence.Signal(10); + fence.Wait(10); + EXPECT_EQ(fence.GetCompletedValue(), 10u); fence.Shutdown(); } -TEST_F(OpenGLTestFixture, Fence_GetStatus_ReturnsCorrect) { +TEST_F(OpenGLTestFixture, Fence_Timeline_SignalDecrement) { OpenGLFence fence; - fence.Initialize(false); + fence.Initialize(0); - FenceStatus status = fence.GetStatus(); - EXPECT_TRUE(status == FenceStatus::Signaled || status == FenceStatus::Unsignaled); + fence.Signal(5); + fence.Wait(5); + EXPECT_EQ(fence.GetCompletedValue(), 5u); + + fence.Signal(3); + fence.Wait(3); + EXPECT_EQ(fence.GetCompletedValue(), 3u); fence.Shutdown(); } + +TEST_F(OpenGLTestFixture, Fence_Timeline_MultipleSignals) { + OpenGLFence fence; + fence.Initialize(0); + + fence.Signal(10); + fence.Wait(10); + + fence.Signal(20); + fence.Wait(20); + + fence.Signal(30); + fence.Wait(30); + + EXPECT_EQ(fence.GetCompletedValue(), 30u); + + fence.Shutdown(); +} + +TEST_F(OpenGLTestFixture, Fence_Timeline_WaitSmallerThanCompleted) { + OpenGLFence fence; + fence.Initialize(0); + + fence.Signal(5); + fence.Wait(5); + + fence.Wait(3); + + EXPECT_GE(fence.GetCompletedValue(), 5u); + + fence.Shutdown(); +} + +TEST_F(OpenGLTestFixture, Fence_Timeline_GetCompletedValue_Stages) { + OpenGLFence fence; + fence.Initialize(0); + + EXPECT_EQ(fence.GetCompletedValue(), 0u); + + fence.Signal(1); + fence.Wait(1); + EXPECT_EQ(fence.GetCompletedValue(), 1u); + + fence.Signal(10); + fence.Wait(10); + EXPECT_EQ(fence.GetCompletedValue(), 10u); + + fence.Signal(5); + fence.Wait(5); + EXPECT_EQ(fence.GetCompletedValue(), 5u); + + fence.Shutdown(); +} \ No newline at end of file diff --git a/tests/RHI/unit/test_fence.cpp b/tests/RHI/unit/test_fence.cpp index feaa1442..366e2c52 100644 --- a/tests/RHI/unit/test_fence.cpp +++ b/tests/RHI/unit/test_fence.cpp @@ -64,27 +64,13 @@ TEST_P(RHITestFixture, Fence_GetCompletedValue) { EXPECT_EQ(fence->GetCompletedValue(), 0u); fence->Signal(1); + fence->Wait(1); EXPECT_EQ(fence->GetCompletedValue(), 1u); fence->Shutdown(); delete fence; } -TEST_P(RHITestFixture, Fence_IsSignaled) { - FenceDesc desc = {}; - desc.initialValue = 0; - - RHIFence* fence = GetDevice()->CreateFence(desc); - ASSERT_NE(fence, nullptr); - - EXPECT_FALSE(fence->IsSignaled()); - fence->Signal(1); - EXPECT_TRUE(fence->IsSignaled()); - - fence->Shutdown(); - delete fence; -} - TEST_P(RHITestFixture, Fence_GetNativeHandle) { FenceDesc desc = {}; desc.initialValue = 0; @@ -97,3 +83,86 @@ TEST_P(RHITestFixture, Fence_GetNativeHandle) { fence->Shutdown(); delete fence; } + +TEST_P(RHITestFixture, Fence_Timeline_SignalIncrement) { + FenceDesc desc = {}; + desc.initialValue = 0; + + RHIFence* fence = GetDevice()->CreateFence(desc); + ASSERT_NE(fence, nullptr); + + fence->Signal(1); + fence->Wait(1); + EXPECT_GE(fence->GetCompletedValue(), 1u); + + fence->Signal(5); + fence->Wait(5); + EXPECT_GE(fence->GetCompletedValue(), 5u); + + fence->Signal(10); + fence->Wait(10); + EXPECT_GE(fence->GetCompletedValue(), 10u); + + fence->Shutdown(); + delete fence; +} + +TEST_P(RHITestFixture, Fence_Timeline_SignalDecrement) { + FenceDesc desc = {}; + desc.initialValue = 0; + + RHIFence* fence = GetDevice()->CreateFence(desc); + ASSERT_NE(fence, nullptr); + + fence->Signal(5); + fence->Wait(5); + EXPECT_GE(fence->GetCompletedValue(), 5u); + + fence->Signal(3); + fence->Wait(3); + EXPECT_GE(fence->GetCompletedValue(), 3u); + + fence->Shutdown(); + delete fence; +} + +TEST_P(RHITestFixture, Fence_Timeline_MultipleSignals) { + FenceDesc desc = {}; + desc.initialValue = 0; + + RHIFence* fence = GetDevice()->CreateFence(desc); + ASSERT_NE(fence, nullptr); + + fence->Signal(10); + fence->Wait(10); + EXPECT_GE(fence->GetCompletedValue(), 10u); + + fence->Signal(20); + fence->Wait(20); + EXPECT_GE(fence->GetCompletedValue(), 20u); + + fence->Signal(30); + fence->Wait(30); + EXPECT_GE(fence->GetCompletedValue(), 30u); + + fence->Shutdown(); + delete fence; +} + +TEST_P(RHITestFixture, Fence_Timeline_WaitSmallerThanCompleted) { + FenceDesc desc = {}; + desc.initialValue = 0; + + RHIFence* fence = GetDevice()->CreateFence(desc); + ASSERT_NE(fence, nullptr); + + fence->Signal(5); + fence->Wait(5); + + fence->Wait(3); + + EXPECT_GE(fence->GetCompletedValue(), 5u); + + fence->Shutdown(); + delete fence; +} \ No newline at end of file