17 KiB
RHI 模块设计问题分析报告(第二版)
1. 项目背景
本项目 RHI 模块参考 Unity 渲染架构设计,面向 Direct3D 12 和 Vulkan 等现代图形 API,目标是为引擎上层(SRP/RenderGraph)提供统一的渲染硬件抽象层,屏蔽 API 差异,实现跨后端移植。
当前已实现两个后端:D3D12 和 OpenGL。基础框架已有雏形(Reset/Close、TransitionBarrier、PipelineState),但存在被 OpenGL 风格带偏、不符合现代 D3D12/Vulkan 显式设计的关键问题。
2. 做得好的地方
- 有显式生命周期管理:
Reset()/Close()是 D3D12/Vulkan 风格,理解"录制-提交"的分离 - 考虑了资源状态转换:
TransitionBarrier()是现代 API 必须的,预留了接口 - 覆盖了图形+计算:
Draw/DrawIndexed/Dispatch都有,功能完整 - 预留了原生句柄:
GetNativeHandle()方便调试和特殊场景扩展
3. 核心问题(按严重程度排序)
3.1 最大的坑:字符串查找 SetUniform 是 OpenGL 风格
问题描述:当前接口使用字符串名称设置 Uniform/Texture:
virtual void SetUniformInt(const char* name, int value) = 0;
virtual void SetGlobalTexture(const char* name, RHIResourceView* texture) = 0;
这是典型的 OpenGL 立即模式风格,问题极大:
| 问题 | 说明 |
|---|---|
| 性能差 | 运行时通过字符串查找 uniform 位置,驱动开销大 |
| 无法多线程 | 字符串查找不是线程安全的,不利于多线程录制 CommandList |
| 接 Vulkan 不可能 | D3D12/Vulkan 用 DescriptorSet + PipelineLayout,根本没有"按名字设 uniform"的概念 |
根本原因:这是 OpenGL 风格的设计,没有对齐 D3D12/Vulkan 的显式 Descriptor 绑定模型。
D3D12/Vulkan 正确做法:
// D3D12: 通过 Root Signature + Descriptor Table 绑定
commandList->SetGraphicsRootDescriptorTable(rootIndex, gpuDescriptorHandle);
// Vulkan: 通过 DescriptorSet 绑定
vkCmdBindDescriptorSets(commandBuffer, ..., descriptorSet, ...);
修改建议:
// 新增:DescriptorSet 绑定接口(替代 SetUniform/SetGlobal)
virtual void SetGraphicsDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
virtual void SetComputeDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
3.2 缺少显式 RenderPass
问题描述:当前只有 SetRenderTargets(),没有 BeginRenderPass() / EndRenderPass():
// 当前设计
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
virtual void ClearRenderTarget(RHIResourceView* renderTarget, const float color[4]) = 0;
这是不符合现代 API 设计的:
| 问题 | 说明 |
|---|---|
| 不符合显式设计 | D3D12/Vulkan 要求显式定义渲染通道的开始和结束 |
| 清屏逻辑不规范 | 现代 API 推荐用 RenderPass 的 LoadOp(Clear/Load/DontCare)清屏 |
| Tile GPU 性能差 | 对移动端 Tile GPU 至关重要,隐式行为无法优化 |
根本原因:OpenGL 的 glClear() 是立即模式,没有 RenderPass 概念。
D3D12/Vulkan 正确做法:
// D3D12: Begin/End Render Pass
commandList->BeginRenderPass(...);
commandList->EndRenderPass();
// Vulkan: vkCmdBeginRenderPass / vkCmdEndRenderPass
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, ...);
vkCmdEndRenderPass(commandBuffer);
修改建议:
// 新增:显式渲染通道
virtual void BeginRenderPass(
RHIRenderPass* renderPass,
RHIFramebuffer* framebuffer,
const Rect& renderArea,
uint32_t clearValueCount,
const ClearValue* clearValues) = 0;
virtual void EndRenderPass() = 0;
// 移除 SetRenderTargets,改由 Framebuffer 定义
// 移除或废弃 ClearRenderTarget,改为 BeginRenderPass 的 LoadOp
3.2.1 ✅ 已完成:显式 RenderPass 实现
实现状态:2026-03-24 已完成
新增抽象:
| 类名 | 说明 |
|---|---|
RHIFramebuffer |
Framebuffer 基类,含 Initialize/Bind/GetHandle |
RHIRenderPass |
RenderPass 基类,含 AttachmentDesc |
D3D12Framebuffer |
D3D12 Framebuffer 实现 |
D3D12RenderPass |
D3D12 RenderPass 实现(使用 OMSetRenderTargets) |
OpenGLFramebuffer |
OpenGL 实现(继承自 RHIFramebuffer) |
OpenGLRenderPass |
OpenGL RenderPass 实现 |
RHICommandList 新增接口:
virtual void BeginRenderPass(RHIRenderPass* renderPass, RHIFramebuffer* framebuffer,
const Rect& renderArea, uint32_t clearValueCount,
const ClearValue* clearValues) = 0;
virtual void EndRenderPass() = 0;
AttachmentDesc 结构(定义在 RHIRenderPass.h):
struct AttachmentDesc {
Format format = Format::Unknown;
LoadAction loadOp = LoadAction::Undefined; // Undefined/Load/Clear
StoreAction storeOp = StoreAction::Store; // Store/Resolve/Discard
LoadAction stencilLoadOp = LoadAction::Undefined;
StoreAction stencilStoreOp = StoreAction::Undefined;
ClearValue clearValue;
};
实现说明:
- D3D12:使用
OMSetRenderTargets+ClearRenderTargetView/ClearDepthStencilView - OpenGL:使用
glBindFramebuffer+glClearBufferfv处理 LoadOp - 旧的
SetRenderTargets/ClearRenderTarget仍保留,向后兼容
3.3 动态状态太多,应该收敛到 PipelineState
问题描述:当前把 DepthStencilState、BlendState、PrimitiveTopology 都放在 CommandList 里动态设置:
virtual void SetDepthStencilState(const DepthStencilState& state) = 0;
virtual void SetBlendState(const BlendState& state) = 0;
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0;
这是OpenGL 风格的残留:
| 问题 | 说明 |
|---|---|
| 驱动开销大 | 现代 API 中这些状态大部分是 PipelineState 的一部分,是不可变的 |
| 不符合显式设计 | 动态状态应该只保留极少数(Viewport/Scissor/StencilRef/BlendFactor) |
| 状态不一致风险 | CommandList 动态设置的状态可能与 PSO 中定义的状态冲突 |
根本原因:OpenGL 是状态机,可以在任何时候设置任何状态。D3D12/Vulkan 把大部分状态打包到 PSO 中。
D3D12/Vulkan 正确做法:
DepthStencilState、BlendState、PrimitiveTopology在 PSO 创建时就确定- CommandList 只设置真正的动态状态:Viewport、Scissor、StencilRef、BlendFactor
修改建议:
// PipelineState 应该包含:
// - Shader
// - VertexLayout
// - RenderPass 兼容
// - DepthStencilState(不可变)
// - BlendState(不可变)
// - PrimitiveTopology(不可变)
// - 动态状态掩码
// CommandList 只保留必要的动态状态:
virtual void SetViewports(uint32_t count, const Viewport* viewports) = 0;
virtual void SetScissorRects(uint32_t count, const Rect* rects) = 0;
virtual void SetStencilRef(uint8_t ref) = 0;
virtual void SetBlendFactor(const float factor[4]) = 0;
// 移除:
virtual void SetDepthStencilState(const DepthStencilState& state) = 0; // 移到 PSO
virtual void SetBlendState(const BlendState& state) = 0; // 移到 PSO
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0; // 移到 PSO
3.4 ResourceView 类型不明确 ✅ 基本完成
问题描述:当前用 RHIResourceView* 代表所有视图:
virtual void SetVertexBuffer(uint32_t slot, RHIResourceView* buffer, ...) = 0;
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
这会导致:
| 问题 | 说明 |
|---|---|
| 类型不安全 | 容易把 SRV 当成 RTV 传,运行时才发现错误 |
| 后端处理麻烦 | D3D12/Vulkan 对不同视图有严格区分(RTV/DSV/SRV/UAV/VBV/IBV) |
| 语义模糊 | RHIResourceView 到底是哪种视图?调用者必须查文档 |
根本原因:没有在类型层面区分不同视图的语义。
修改建议:定义明确的视图类型层次结构
// 基类
class RHIResourceView {};
// 具体视图类型
class RHIRenderTargetView : public RHIResourceView {};
class RHIDepthStencilView : public RHIResourceView {};
class RHIShaderResourceView : public RHIResourceView {};
class RHIUnorderedAccessView : public RHIResourceView {};
class RHIVertexBufferView : public RHIResourceView {};
class RHIIndexBufferView : public RHIResourceView {};
class RHIConstantBufferView : public RHIResourceView {};
实现状态:✅ 基本完成 - RHIResourceView.h 中定义了具体视图类型,但 RHICommandList 仍使用 RHIResourceView* 以保持兼容性
3.5 TransitionBarrier 针对 View 而非 Resource ✅ 已完成
问题描述:当前接口:
virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;
资源状态转换是针对 Resource(Buffer/Texture 本身) 的,不是针对 View 的。一张 Texture 可能创建多个 View(SRV、RTV、DSV),但状态转换是对整个 Texture 做的。
根本原因:没有区分 Resource 和 ResourceView 的概念。
修改建议:
// 新增 Resource 基类
class RHIResource {};
class RHIBuffer : public RHIResource {};
class RHITexture : public RHIResource {};
// 修改 TransitionBarrier
struct ResourceBarrier {
RHIResource* resource;
ResourceStates stateBefore;
ResourceStates stateAfter;
uint32_t subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
};
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
实现状态:✅ 已完成 - RHIResource.h 已创建,RHIBuffer 和 RHITexture 已继承自 RHIResource
问题描述:当前接口:
virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;
资源状态转换是针对 Resource(Buffer/Texture 本身) 的,不是针对 View 的。一张 Texture 可能创建多个 View(SRV、RTV、DSV),但状态转换是对整个 Texture 做的。
根本原因:没有区分 Resource 和 ResourceView 的概念。
修改建议:
// 新增 Resource 基类
class RHIResource {};
class RHIBuffer : public RHIResource {};
class RHITexture : public RHIResource {};
// 修改 TransitionBarrier
struct ResourceBarrier {
RHIResource* resource;
ResourceStates stateBefore;
ResourceStates stateAfter;
uint32_t subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
};
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
4. 其他需要修改的问题
4.1 SetGlobal* 是空操作
详见第一版文档,此处不再赘述。
4.2 PrimitiveType 和 PrimitiveTopology 冲突
详见第一版文档,此处不再赘述。
4.3 OpenGL 特有方法暴露
详见第一版文档,此处不再赘述。
4.4 缺少 Compute Pipeline 抽象
详见第一版文档,此处不再赘述。
4.5 RHIPipelineLayout 空壳
详见第一版文档,此处不再赘述。
5. 修改后的 CommandList 核心接口
class RHICommandList {
public:
virtual ~RHICommandList() = default;
// ========== 生命周期 ==========
virtual void Reset(RHICommandPool* pool = nullptr) = 0;
virtual void Close() = 0;
// ========== 资源状态转换 ==========
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
// ========== 渲染通道(新增)==========
virtual void BeginRenderPass(
RHIRenderPass* renderPass,
RHIFramebuffer* framebuffer,
const Rect& renderArea,
uint32_t clearValueCount,
const ClearValue* clearValues) = 0;
virtual void EndRenderPass() = 0;
// ========== 管线与描述符 ==========
virtual void SetPipelineState(RHIPipelineState* pso) = 0;
virtual void SetGraphicsDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
// ========== 动态状态(精简后)==========
virtual void SetViewports(uint32_t count, const Viewport* viewports) = 0;
virtual void SetScissorRects(uint32_t count, const Rect* rects) = 0;
virtual void SetStencilRef(uint8_t ref) = 0;
virtual void SetBlendFactor(const float factor[4]) = 0;
// ========== 顶点/索引 ==========
virtual void SetVertexBuffers(
uint32_t startSlot,
uint32_t count,
RHIVertexBufferView** buffers,
const uint64_t* offsets,
const uint32_t* strides) = 0;
virtual void SetIndexBuffer(
RHIIndexBufferView* buffer,
uint64_t offset,
IndexFormat format) = 0;
// ========== 绘制 ==========
virtual void Draw(uint32_t vertexCount, uint32_t instanceCount = 1,
uint32_t startVertex = 0, uint32_t startInstance = 0) = 0;
virtual void DrawIndexed(uint32_t indexCount, uint32_t instanceCount = 1,
uint32_t startIndex = 0, int32_t baseVertex = 0,
uint32_t startInstance = 0) = 0;
// ========== 计算 ==========
virtual void SetComputePipelineState(RHIPipelineState* pso) = 0;
virtual void SetComputeDescriptorSets(
uint32_t firstSet,
uint32_t count,
RHIDescriptorSet** descriptorSets,
RHIPipelineLayout* pipelineLayout) = 0;
virtual void Dispatch(uint32_t x, uint32_t y, uint32_t z) = 0;
// ========== 复制 ==========
virtual void CopyResource(RHIResource* dst, RHIResource* src) = 0;
// ========== 原生句柄 ==========
virtual void* GetNativeHandle() = 0;
};
6. 需要同时新增的抽象
6.1 RHIDescriptorSet
class RHIDescriptorSet {
public:
virtual ~RHIDescriptorSet() = default;
virtual void Update(uint32_t offset, RHIResourceView* view) = 0;
virtual void UpdateSampler(uint32_t offset, RHISampler* sampler) = 0;
virtual void* GetNativeHandle() = 0;
};
6.2 RHIRenderPass
struct AttachmentDesc {
Format format;
LoadAction loadOp = LoadAction::DontCare;
StoreAction storeOp = StoreAction::Store;
LoadAction stencilLoadOp = LoadAction::DontCare;
StoreAction stencilStoreOp = StoreAction::DontCare;
};
class RHIRenderPass {
public:
virtual ~RHIRenderPass() = default;
virtual bool Initialize(uint32_t colorAttachmentCount, const AttachmentDesc* colorAttachments,
const AttachmentDesc* depthStencilAttachment) = 0;
virtual void Shutdown() = 0;
virtual void* GetNativeHandle() = 0;
};
6.3 RHIFramebuffer
class RHIFramebuffer {
public:
virtual ~RHIFramebuffer() = default;
virtual bool Initialize(RHIRenderPass* renderPass, uint32_t attachmentCount,
RHIResourceView** attachments, uint32_t width, uint32_t height) = 0;
virtual void Shutdown() = 0;
virtual void* GetNativeHandle() = 0;
};
6.4 RHIResource 基类
class RHIResource {
public:
virtual ~RHIResource() = default;
virtual void* GetNativeHandle() = 0;
virtual ResourceStates GetState() const = 0;
virtual void SetState(ResourceStates state) = 0;
};
class RHIBuffer : public RHIResource { ... };
class RHITexture : public RHIResource { ... };
7. 问题优先级总结
| 优先级 | 问题 | 严重性 | 修复难度 | 状态 |
|---|---|---|---|---|
| 1 | 字符串查找 SetUniform 不符合 D3D12/Vulkan | 🔴 致命 | 高 | ❌ 未完成 |
| 2 | 缺少显式 RenderPass | 🔴 致命 | 高 | ✅ 已完成 |
| 3 | 动态状态太多 | 🔴 高 | 高 | ❌ 未完成 |
| 4 | ResourceView 类型不明确 | 🟡 中 | 中 | ✅ 基本完成 |
| 5 | TransitionBarrier 针对 View 而非 Resource | 🟡 中 | 中 | ✅ 已完成 |
| 6 | SetGlobal* 空操作 | 🟡 中 | 低 | ❌ 未完成 |
| 7 | OpenGL 特有方法暴露 | 🟡 中 | 高 | ❌ 未完成 |
| 8 | 缺少 Compute Pipeline 抽象 | 🟡 中 | 中 | ❌ 未完成 |
8. 总结
当前 RHI 模块的基础框架有 D3D12/Vulkan 的影子,但被 OpenGL 风格带偏了。最核心的问题是:
- 用字符串查找设 uniform —— 这是 OpenGL 风格,必须改成 DescriptorSet 绑定
- 缺少显式 RenderPass —— 现代 API 的基础,必须添加
- 动态状态太多 —— 应该收敛到 PSO,只保留必要的动态状态
修复这些问题后,RHI 抽象层将完全对齐 D3D12/Vulkan 的显式模型,未来接入 Vulkan 将会非常顺利。