ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RHI:跨平台图形API抽象层的设计与实现

RHI:跨平台图形API抽象层的设计与实现

1. RHI初印象:图形API的抽象层

第一次接触RHI这个概念是在研究跨平台渲染引擎架构时。当时我正在为一个工业仿真项目评估图形后端方案,发现不同硬件平台(Windows/DirectX、macOS/Metal、Linux/Vulkan)的API差异让代码维护成本激增。RHI(Render Hardware Interface)正是为解决这类问题而生的中间层设计。

简单来说,RHI就像图形编程世界的"翻译官"。它在上层渲染逻辑与底层图形API(如Vulkan、DX12)之间建立抽象层,开发者只需编写一套RHI调用代码,就能自动适配不同平台的本地API实现。这让我想起USB接口的设计理念——无论设备内部使用何种协议,对外都呈现统一的数据交互方式。

典型的RHI架构包含三个关键层级:

  1. 设备抽象层:封装GPU设备、命令队列等硬件资源
  2. 资源管理:统一管理纹理、缓冲区和着色器等资产
  3. 命令提交:将绘制指令转换为平台特定调用

以创建纹理为例,原生Vulkan需要20多行初始化代码,而通过RHI可能只需这样:

RHITextureDesc desc; desc.format = ERHIFormat::RGBA8_UNORM; desc.width = 1024; desc.height = 1024; RHITexture* texture = gRHI->CreateTexture(desc);

2. 为什么需要RHI:跨平台开发的现实困境

2015年苹果宣布弃用OpenGL转向Metal时,我参与维护的一个跨平台引擎突然面临重大架构调整。当时我们不得不同时维护OpenGL、DirectX 11和Metal三套渲染后端,每次新增渲染特性都要重复实现三次——这正是RHI要解决的核心痛点。

2.1 图形API的碎片化现状

现代图形API主要分为三大阵营:

  • Windows平台:DirectX 12(最新)、DirectX 11(遗留)
  • Apple生态:Metal(唯一选择)
  • 跨平台标准:Vulkan(桌面)、OpenGL ES(移动)

每个API都有独特的编程模型:

  • Vulkan强调显式控制和多线程
  • Metal深度集成Apple硬件特性
  • DX12采用COM对象模型

2.2 维护成本对比

假设要实现一个延迟渲染管线:

任务项多后端实现RHI方案
代码行数~3000行~1000行
新API适配周期2-3月2-3周
调试复杂度高中

在实际项目中,我们通过RHI将平台相关代码占比从40%降至8%,这还没算上避免的重复测试成本。

3. RHI实现关键技术点

3.1 抽象粒度的把控

设计RHI时最常见的误区是过度抽象。早期版本我们试图统一所有API特性,结果产生了大量无用的抽象层。后来总结出"80/20法则"——只对最常用的20%功能进行深度抽象,其余保留原生扩展点。

例如对资源屏障的处理:

// 基础抽象 RHITextureBarrier barrier; barrier.texture = texture; barrier.newState = ERHIResourceState::RenderTarget; gRHI->CmdResourceBarrier(cmdList, &barrier, 1); // 原生扩展(Vulkan专用) if (gRHI->GetAPI() == ERHIAPI::Vulkan) { VkMemoryBarrier vkBarrier = ...; vkCmdPipelineBarrier(vkCmdBuf, ...); }

3.2 多线程渲染架构

现代图形API都要求显式多线程支持,RHI需要封装这些差异。我们的方案是:

  1. 命令列表模型:

    • 主线程:录制RHICmdList
    • 工作线程:并行提交指令
    • RHI内部处理API线程安全
  2. 资源上传流程:

graph TD A[主线程准备数据] --> B[RHIBuffer创建暂存资源] B --> C[工作线程提交上传命令] C --> D[RHI内部处理GPU同步]

注意:实际项目中我们发现Metal对多线程支持较弱,需要特殊处理命令队列提交。

4. 主流引擎的RHI实现对比

4.1 Unreal Engine的FRHI

Epic的RHI实现堪称工业级典范:

  • 层级设计:

    1. FRHICommandList:命令抽象
    2. FRHIResource:资源基类
    3. 平台特定派生类(如FVulkanTexture)
  • 特色设计:

    • 延迟命令执行(Draw延迟到Submit)
    • 统一的Shader编译管道
    • 详细的GPU Profiling工具链

4.2 Unity的SRP

Unity的Scriptable Render Pipeline采用另一种思路:

  • 基于C#的轻量级RHI
  • 更强调可编程管线配置
  • 通过RenderGraph管理资源依赖

对比测试同一场景(百万三角形绘制):

指标Unreal FRHIUnity SRP
CPU耗时(ms)2.13.8
内存占用(MB)11289
API调用次数47132

5. 手把手实现最小化RHI

下面用约200行C++代码演示RHI核心框架:

5.1 基础接口定义

enum class ERHIAPI { Vulkan, DX12, Metal }; class IRHI { public: virtual ~IRHI() = default; // 设备管理 virtual bool Init() = 0; virtual ERHIAPI GetAPI() const = 0; // 资源创建 virtual RHITexture* CreateTexture(const RHITextureDesc&) = 0; virtual RHIBuffer* CreateBuffer(const RHIBufferDesc&) = 0; // 命令提交 virtual RHICmdList* CreateCmdList() = 0; virtual void SubmitCmdLists(RHICmdList** lists, uint32_t count) = 0; };

5.2 Vulkan实现示例

class VulkanRHI : public IRHI { VkDevice m_device; VkQueue m_graphicsQueue; public: bool Init() override { VkInstanceCreateInfo instInfo = {...}; vkCreateInstance(&instInfo, nullptr, &m_instance); // 设备初始化省略... return true; } RHITexture* CreateTexture(const RHITextureDesc& desc) override { VkImageCreateInfo imageInfo = {}; imageInfo.format = ConvertFormat(desc.format); imageInfo.extent = {desc.width, desc.height, 1}; // ...其他参数设置 VkImage image; vkCreateImage(m_device, &imageInfo, nullptr, &image); return new VulkanTexture(image); } };

5.3 使用示例

// 初始化 IRHI* gRHI = CreateVulkanRHI(); gRHI->Init(); // 渲染循环 RHICmdList* cmd = gRHI->CreateCmdList(); cmd->Begin(); cmd->SetRenderTarget(backbuffer); cmd->ClearRenderTarget(Color::Blue); // ...绘制命令 cmd->End(); gRHI->SubmitCmdLists(&cmd, 1);

6. 进阶开发技巧与坑点指南

6.1 资源生命周期管理

我们曾遇到一个棘手Bug:Metal纹理在RHI对象析构后仍被引用。解决方案是引入引用计数:

class RHITexture { std::atomic<uint32_t> m_refCount; public: void AddRef() { m_refCount++; } void Release() { if (--m_refCount == 0) delete this; } };

6.2 多平台Shader编译

不同平台的Shader编译工具链差异巨大:

  • HLSL:需DXC编译器
  • Metal:需Xcode命令行工具
  • SPIR-V:需glslangValidator

建议的方案:

  1. 开发期保存原始HLSL/GLSL
  2. 构建时调用对应平台工具链编译
  3. 运行时加载平台特定字节码

6.3 性能调优经验

通过RHI的抽象层可能引入额外开销,关键优化点:

  1. 命令批处理:

    • 合并相邻的状态设置调用
    • 使用内存池分配临时资源
  2. 数据驱动渲染:

// 优化前:逐对象设置材质 for (auto& obj : objects) { cmd->SetMaterial(obj.material); cmd->DrawMesh(obj.mesh); } // 优化后:按材质排序批处理 std::sort(objects.begin(), objects.end(), [](auto& a, auto& b) { return a.material->GetID() < b.material->GetID(); });

7. 现代图形API的新挑战

随着DirectX 12 Ultimate和Vulkan 1.3的发布,RHI需要适配新特性:

  1. 光线追踪统一标准:

    • DXR、Vulkan RT、Metal RT的抽象
    • 加速结构管理
  2. 网格着色器:

RHIMeshShaderDesc desc; desc.taskShader = taskShader; desc.meshShader = meshShader; gRHI->CmdDispatchMesh(cmdList, desc);
  1. 可变速率着色:
    • 封装不同API的VRS实现
    • 提供统一的着色率控制接口

在实际集成这些特性时,我们发现Metal的实现往往滞后于Windows平台,这时RHI的抽象层价值更加凸显——可以通过软件模拟保持功能可用性。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进