RHI(Render Hardware Interface)是 UE 渲染里最值得先读懂的一层:上面 RDG、材质系统全都对着它编程,下面 DX12/Vulkan/Metal 各自为政。读懂它要回答一个问题:为什么 UE 不直接在渲染线程调 DX12,非要中间隔一层命令翻译。源码:RHI/Public/RHICommandList.h、D3D12RHI/Private/D3D12Commands.cpp(UE5.3)。
一图看懂分层
RenderThread / TaskGraph 任务
→ FRHICommandList(录制抽象:Draw/Copy/SetPSO…,不碰平台 API)
→ RHIThread 取出命令 → FD3D12CommandContext 翻译
→ ID3D12GraphicsCommandList(平台命令列表)
→ GPU 执行
注意中间那步"翻译"不是装饰。FRHICommandList 录的是引擎语义的命令(带 UE 的资源类型、视口状态约定),RHIThread 上的 Execute 阶段才把它们展开成平台调用。双层结构让"录"和"提交"跑在不同线程——上一篇渲染线程模型里 RT 与 RHI-T 的解耦,物理载体就是这套命令内存。
FRHICommandList:命令即数据
FRHICommandList(RHICommandList.h)的核心是 FRHICommandListBase 持有的命令内存池:每条命令是一个 POD 结构 + 一段内联存储,录命令就是在这个池子里做 placement new:
// RHICommandList.h 的命令录制骨架(UE5.3)
template<typename TCmd, typename... Args>
void FRHICommandListBase::AllocCommand(Args&&... args)
{
void* Mem = Alloc(sizeof(TCmd)); // 从环形命令内存池切一块
new (Mem) TCmd(Forward<Args>(args)...); // 构造 POD 命令,不调用任何图形 API
}
所有 RHIDrawIndexedPrimitive、RHISetRenderTarget 之类最终都落到这。重要推论:
- 录命令几乎零驱动开销。没碰 D3D12,没碰 Vulkan,纯粹是向内存写结构体。所以 RT 上录 1000 个 draw 的成本远低于想象,真正贵的在 RHI 线程翻译和 GPU 执行。
- 命令的生命周期由栅栏管理。命令池内存不能提前复用——RHI 线程提交完、GPU 读完之后才能回收,对应
FRHICommandListExecutor的 Latch 阶段。手动扩展 RHI 时最容易踩的坑就是自己分配命令内存导致 RT 和 RHI-T 竞争。
FRHICommandListImmediate 是特殊变体:跳过命令池,直接在当前线程执行到平台 API。留给"必须立刻生效"的场合(如跨帧同步的查询读取),代价是失去线程解耦,所以 RDG 里大部分 pass 用的是 deferred list,只有 fence/flip 用 immediate。
平台后端:一套接口、三份翻译
每个平台后端实现同一组虚函数(FD3D12DynamicRHI、FVulkanDynamicRHI、FMetalDynamicRHI),进程启动时按平台/配置选一份(-dx12/-vulkan、DefaultEngine.ini 的 RHI 设置)。渲染代码全程只看见 GDynamicRHI 接口指针。
翻译层的活不只是"换个函数名"。拿 PSO(Pipeline State Object)说:DX12 的 ID3D12PipelineState 要把 UE 的 FGraphicsPipelineStateInitializer 里几十个字段(RT 格式、混合状态、深度模板、光栅化、VS/PS 字节码……)一一填进 D3D12_GRAPHICS_PIPELINE_STATE_DESC。Vulkan 那边对应的 VkGraphicsPipelineCreateInfo 字段布局和语义都有差异(比如 Vulkan 的 dynamic state 允许视口后期绑定,DX12 不行),翻译层要抹平这些能力差距。
PSO 缓存:抽象层也救不了的痛点
UE 经过 RHI 缓存 PSO(r.PSODiskCache 等),但 DX12/Vulkan 下首次见到新 PSO 仍可能卡顿——因为管线编译是驱动层的,RHI 再抽象也绕不过。UE 的对策是把"见过的 PSO 组合"收集成 .upipelinecache 文件,预热时批量编译。为什么主机平台不卡、PC 卡:主机的 PSO 格式和驱动行为是固定的,PC 上驱动会按硬件分派编译路径,同样的状态描述在不同 GPU 上编译耗时差一个量级。这是 RHI 层设计得再好也解决不了的那一半问题,属于现代 API 的结构性代价(DX11 时代驱动后台线程偷偷编译,卡顿被摊平了,换到显式 API 后这笔账露出来了)。
什么时候需要读懂这层
日常写材质/玩法用不到 RHI,但三种情况绕不开:给引擎加自定义渲染 pass(要决定哪些状态进 PSO initializer);跨平台性能对齐(同一场景 Vulkan 比 DX12 慢,得分别看后端翻译出的命令序列);追 RHI 线程的 stall(stat rhi 里 Execute 段长,说明翻译/提交重,往下就要看后端实现了)。