Files
XCEngine/docs/used/RHI_Design_Issues.md
ssdfasd 16e2065c6c Unified logging: Replace LogSystem with EditorConsoleSink
- Created EditorConsoleSink (implements ILogSink interface)
- EditorConsoleSink stores logs in memory buffer (max 1000 entries)
- Added to Debug::Logger in Application::Initialize()
- ConsolePanel now reads from EditorConsoleSink via static GetInstance()
- Removed separate LogSystem singleton
- Removed editor/src/Core/LogEntry.h (no longer needed)

Now Editor and Engine share the same Debug::Logger, with ConsolePanel
displaying logs via EditorConsoleSink.
2026-03-25 16:13:02 +08:00

22 KiB
Raw Blame History

RHI 模块设计问题分析报告(第二版)

1. 项目背景

本项目 RHI 模块参考 Unity 渲染架构设计,面向 Direct3D 12Vulkan 等现代图形 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

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.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 新增接口

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 新增工厂方法

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()

// 当前设计
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
virtual void ClearRenderTarget(RHIResourceView* renderTarget, const float color[4]) = 0;

这是不符合现代 API 设计的:

问题 说明
不符合显式设计 D3D12/Vulkan 要求显式定义渲染通道的开始和结束
清屏逻辑不规范 现代 API 推荐用 RenderPass 的 LoadOpClear/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 正确做法

  • DepthStencilStateBlendStatePrimitiveTopologyPSO 创建时就确定
  • 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.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* 代表所有视图:

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;

资源状态转换是针对 ResourceBuffer/Texture 本身) 的,不是针对 View 的。一张 Texture 可能创建多个 ViewSRV、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 已创建,RHIBufferRHITexture 已继承自 RHIResource

问题描述:当前接口:

virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;

资源状态转换是针对 ResourceBuffer/Texture 本身) 的,不是针对 View 的。一张 Texture 可能创建多个 ViewSRV、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* 是空操作 已完成

问题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 特有方法暴露 已完成

实现状态2026-03-25 已完成

问题OpenGLDevice 和 OpenGLCommandList 暴露了 OpenGL 特有方法,违反"上层只调用抽象接口"原则。

修复内容

  1. OpenGLDevice 内部化方法

    • 移除 GetTextureUnitAllocator()GetUniformBufferManager() 公开访问(移到 private
    • 移除 SwapBuffers() 公开方法OpenGLSwapChain 作为 friend 仍可访问)
    • GetPresentationDC()GetGLContext() 保留在 private通过 friend 访问
  2. 新增抽象接口

    • MakeContextCurrent() - 封装 wglMakeCurrent 操作
    • GetNativeContext() - 返回 GL Context 供 RenderDoc 使用
  3. 保留的逃生舱方法

    • OpenGLCommandList 中的 PrimitiveType 相关方法是显式的 OpenGL 逃生舱
    • 这些方法使用 OpenGL 特有类型,文档化为"backend-specific escape hatch"
    • 抽象层正确使用 PrimitiveTopology 枚举

修改的文件

  • engine/include/XCEngine/RHI/OpenGL/OpenGLDevice.h - 内部化特有方法,新增 MakeContextCurrent/GetNativeContext
  • engine/src/RHI/OpenGL/OpenGLDevice.cpp - 实现 MakeContextCurrent
  • tests/RHI/OpenGL/unit/test_device.cpp - 移除 SwapBuffers 测试
  • tests/RHI/OpenGL/integration/minimal/main.cpp - 使用 MakeContextCurrent
  • tests/RHI/OpenGL/integration/triangle/main.cpp - 使用 MakeContextCurrent
  • tests/RHI/OpenGL/integration/quad/main.cpp - 使用 MakeContextCurrent
  • tests/RHI/OpenGL/integration/sphere/main.cpp - 使用 MakeContextCurrent

说明

  • PrimitiveType 枚举在 OpenGL 专用逃生舱方法中使用是可接受的,因为这些是显式的后端特定接口
  • 抽象层正确使用 PrimitiveTopology,不存在类型泄漏问题

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 风格带偏了。最核心的问题是:

  1. 用字符串查找设 uniform —— 这是 OpenGL 风格,必须改成 DescriptorSet 绑定
  2. 缺少显式 RenderPass —— 现代 API 的基础,必须添加
  3. 动态状态太多 —— 应该收敛到 PSO只保留必要的动态状态

修复这些问题后RHI 抽象层将完全对齐 D3D12/Vulkan 的显式模型,未来接入 Vulkan 将会非常顺利。