# RHI 模块设计问题分析报告(第二版) ## 1. 项目背景 本项目 RHI 模块参考 Unity 渲染架构设计,面向 **Direct3D 12** 和 **Vulkan** 等现代图形 API,目标是为引擎上层(SRP/RenderGraph)提供统一的渲染硬件抽象层,屏蔽 API 差异,实现跨后端移植。 当前已实现两个后端:D3D12 和 OpenGL。基础框架已有雏形(Reset/Close、TransitionBarrier、PipelineState),但存在**被 OpenGL 风格带偏、不符合现代 D3D12/Vulkan 显式设计**的关键问题。 --- ## 2. 做得好的地方 1. **有显式生命周期管理**:`Reset()` / `Close()` 是 D3D12/Vulkan 风格,理解"录制-提交"的分离 2. **考虑了资源状态转换**:`TransitionBarrier()` 是现代 API 必须的,预留了接口 3. **覆盖了图形+计算**:`Draw` / `DrawIndexed` / `Dispatch` 都有,功能完整 4. **预留了原生句柄**:`GetNativeHandle()` 方便调试和特殊场景扩展 --- ## 3. 核心问题(按严重程度排序) ### 3.1 最大的坑:字符串查找 SetUniform 是 OpenGL 风格 **问题描述**:当前接口使用字符串名称设置 Uniform/Texture: ```cpp 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 正确做法**: ```cpp // D3D12: 通过 Root Signature + Descriptor Table 绑定 commandList->SetGraphicsRootDescriptorTable(rootIndex, gpuDescriptorHandle); // Vulkan: 通过 DescriptorSet 绑定 vkCmdBindDescriptorSets(commandBuffer, ..., descriptorSet, ...); ``` **修改建议**: ```cpp // 新增: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.1.1 ✅ 已完成:DescriptorSet 抽象 **实现状态**:2026-03-25 已完成 **新增抽象**: | 类名 | 说明 | |------|------| | `RHIDescriptorSet` | DescriptorSet 基类,含 Update/UpdateSampler/GetNativeHandle/GetBindingCount/GetBindings | | `RHIDescriptorPool` | DescriptorPool 基类,含 AllocateSet/FreeSet | | `D3D12DescriptorSet` | D3D12 实现,使用 DescriptorHeap 分配 | | `D3D12DescriptorHeap` | 扩展支持 AllocateSet/FreeSet | | `OpenGLDescriptorSet` | OpenGL 实现,使用 TextureUnitAllocator | | `OpenGLDescriptorPool` | OpenGL 实现 | **RHICommandList 新增接口**: ```cpp 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; ``` **RHIDevice 新增工厂方法**: ```cpp virtual RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc& desc) = 0; virtual RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool* pool, const DescriptorSetDesc& desc) = 0; ``` **实现说明**: - D3D12:使用 `D3D12DescriptorHeap` 的 GPU/CPU descriptor 分配 - OpenGL:使用 `TextureUnitAllocator` 分配 texture unit - 旧的 `SetUniform*/SetGlobal*` 仍保留,向后兼容 --- ### 3.2 缺少显式 RenderPass **问题描述**:当前只有 `SetRenderTargets()`,没有 `BeginRenderPass()` / `EndRenderPass()`: ```cpp // 当前设计 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 正确做法**: ```cpp // D3D12: Begin/End Render Pass commandList->BeginRenderPass(...); commandList->EndRenderPass(); // Vulkan: vkCmdBeginRenderPass / vkCmdEndRenderPass vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, ...); vkCmdEndRenderPass(commandBuffer); ``` **修改建议**: ```cpp // 新增:显式渲染通道 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 新增接口**: ```cpp virtual void BeginRenderPass(RHIRenderPass* renderPass, RHIFramebuffer* framebuffer, const Rect& renderArea, uint32_t clearValueCount, const ClearValue* clearValues) = 0; virtual void EndRenderPass() = 0; ``` **AttachmentDesc 结构**(定义在 RHIRenderPass.h): ```cpp 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 里动态设置: ```cpp 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 **修改建议**: ```cpp // 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.3.1 ✅ 已完成:动态状态移至 PSO **实现状态**:2026-03-25 已完成 **移除的无效方法**: | 方法 | 问题 | |------|------| | `SetDepthStencilState` | D3D12 中是空实现(TODO),调用无效;OpenGL 中直接调用 GL 函数但状态应通过 PSO 配置 | | `SetBlendState` | D3D12 中只设置 BlendFactor,其他参数被忽略;OpenGL 中直接调用 GL 函数但状态应通过 PSO 配置 | **保留的真正动态状态**: | 方法 | 说明 | |------|------| | `SetViewport/SetViewports` | 真正的动态状态 | | `SetScissorRect/SetScissorRects` | 真正的动态状态 | | `SetStencilRef` | D3D12 动态状态 | | `SetBlendFactor` | D3D12 动态状态 | **修改的文件**: - `RHICommandList.h` - 移除 SetDepthStencilState/SetBlendState 纯虚方法 - `D3D12CommandList.h/cpp` - 移除 SetDepthStencilState/SetBlendState 实现 - `OpenGLCommandList.h/cpp` - 移除 SetDepthStencilState/SetBlendState 实现 - `tests/RHI/unit/test_command_list.cpp` - 移除相关测试 - `tests/RHI/OpenGL/unit/test_command_list.cpp` - 移除相关测试 **说明**: - D3D12 中 PSO 是不可变的,SetDepthStencilState/SetBlendState 调用原本就是无效代码 - OpenGL 中状态通过 OpenGLPipelineState::Apply() 在绑定 PSO 时应用 - PrimitiveTopology 保留在 CommandList,因为 D3D12 允许动态改变 topology type --- ### 3.4 ResourceView 类型不明确 ✅ 基本完成 **问题描述**:当前用 `RHIResourceView*` 代表所有视图: ```cpp 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` 到底是哪种视图?调用者必须查文档 | **根本原因**:没有在类型层面区分不同视图的语义。 **修改建议**:定义明确的视图类型层次结构 ```cpp // 基类 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 ✅ 已完成 **问题描述**:当前接口: ```cpp virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0; ``` 资源状态转换是针对 **Resource(Buffer/Texture 本身)** 的,不是针对 View 的。一张 Texture 可能创建多个 View(SRV、RTV、DSV),但状态转换是对整个 Texture 做的。 **根本原因**:没有区分 Resource 和 ResourceView 的概念。 **修改建议**: ```cpp // 新增 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` **问题描述**:当前接口: ```cpp virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0; ``` 资源状态转换是针对 **Resource(Buffer/Texture 本身)** 的,不是针对 View 的。一张 Texture 可能创建多个 View(SRV、RTV、DSV),但状态转换是对整个 Texture 做的。 **根本原因**:没有区分 Resource 和 ResourceView 的概念。 **修改建议**: ```cpp // 新增 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* 是空操作 ✅ 已完成 **问题**:SetGlobal* 方法(SetGlobalInt/SetGlobalFloat/SetGlobalVec3/SetGlobalVec4/SetGlobalMat4/SetGlobalTexture)在 D3D12 和 OpenGL 中只是缓存值,从不提交到 GPU。 **实现状态**:2026-03-25 已完成 - 移除所有 SetGlobal* 方法 **移除的方法**(共6个): | 方法 | 问题 | |------|------| | `SetGlobalInt` | 只缓存到 unordered_map,从未提交到 GPU | | `SetGlobalFloat` | 只缓存到 unordered_map,从未提交到 GPU | | `SetGlobalVec3` | 只缓存到 unordered_map,从未提交到 GPU | | `SetGlobalVec4` | 只缓存到 unordered_map,从未提交到 GPU | | `SetGlobalMat4` | 只缓存到 unordered_map,从未提交到 GPU | | `SetGlobalTexture` | 只缓存到 unordered_map,从未提交到 GPU | **移除的缓存成员变量**: - D3D12CommandList: `m_globalIntCache`, `m_globalFloatCache`, `m_globalVec3Cache`, `m_globalVec4Cache`, `m_globalMat4Cache`, `m_globalTextureCache` - OpenGLCommandList: 同上 **说明**: - `SetGlobal*` 从未被代码库中任何地方调用(死代码) - `SetUniform*` 方法已正常工作,使用 shader reflection + 实际 GPU 绑定 - 移除后无功能损失 ### 4.2 PrimitiveType 和 PrimitiveTopology 冲突 详见第一版文档,此处不再赘述。 ### 4.3 OpenGL 特有方法暴露 详见第一版文档,此处不再赘述。 ### 4.4 缺少 Compute Pipeline 抽象 详见第一版文档,此处不再赘述。 ### 4.5 RHIPipelineLayout 空壳 详见第一版文档,此处不再赘述。 --- ## 5. 修改后的 CommandList 核心接口 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 基类 ```cpp 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 风格带偏**了。最核心的问题是: 1. **用字符串查找设 uniform** —— 这是 OpenGL 风格,必须改成 DescriptorSet 绑定 2. **缺少显式 RenderPass** —— 现代 API 的基础,必须添加 3. **动态状态太多** —— 应该收敛到 PSO,只保留必要的动态状态 修复这些问题后,RHI 抽象层将完全对齐 D3D12/Vulkan 的显式模型,未来接入 Vulkan 将会非常顺利。