Files
XCEngine/docs/api/_guides/RHI/Devices-Queues-CommandLists-And-Resource-Creation.md
2026-03-29 01:36:53 +08:00

7.5 KiB
Raw Blame History

Devices, Queues, Command Lists, And Resource Creation

这篇指南解决什么问题

如果只看 RHI 目录下的类型名,很容易知道“有设备、有队列、有命令列表”,但不容易知道:

  • 当前真实初始化顺序是什么
  • 哪些对象负责创建,哪些对象负责提交
  • 生命周期应该怎么收尾
  • 这层抽象和 Unity、Unreal、原生图形 API 分别是什么关系

这篇指南专门把这些问题讲清楚。

先建立正确心智模型

XCEngine::RHI 不是最终给游戏逻辑直接使用的渲染 API它是更靠近后端实现的一层基础渲染抽象。

更准确地说:

  • 它比 Unity 常见 gameplay 层接触到的渲染接口更底层。
  • 它比原生 D3D12 / OpenGL / Vulkan API 更统一。
  • 它更像商业引擎内部的 render backend layer。

如果拿成熟引擎做类比:

  • 和 Unity 的关系:更接近 SRP / 图形后端内部会接触的那一层,而不是 MonoBehaviour 侧的普通渲染入口。
  • 和 Unreal 的关系:更接近低层 RHI 的思路。

这么设计的好处是:

  • 后端 bring-up 更直接
  • 图形测试可以绕开更高层 renderer直接验证抽象层
  • 上层 renderer 有明确的后端适配边界

代价是:

  • 使用显式、样板多
  • 生命周期管理要求更严格
  • 当前还会看到一部分后端痕迹和未完全收敛的接口

当前最真实的使用路径

按源码和测试总结,当前推荐按下面顺序理解:

  1. RHIFactory 选择后端并创建 RHIDevice
  2. 调用 device->Initialize() 初始化设备。
  3. 读取 GetCapabilities() / GetDeviceInfo() 决定可用路径。
  4. 通过 device 创建 RHICommandQueue
  5. 通过 device 创建 RHICommandList
  6. 通过 device 创建 buffer、texture、resource view、render pass、framebuffer、descriptor pool / set、pipeline state 等对象。
  7. 在 command list 上 Reset() 后录制命令,再 Close()
  8. 通过 queue 提交命令并用 fence 同步。
  9. 对所有对象执行 Shutdown(),最后 delete

一个最小化示意流程

下面这段更接近“当前抽象层的使用骨架”,不是完整 renderer

using namespace XCEngine::RHI;

RHIDevice* device = RHIFactory::CreateRHIDevice(RHIType::D3D12);
if (device == nullptr) {
    return;
}

RHIDeviceDesc deviceDesc = {};
deviceDesc.enableDebugLayer = true;

if (!device->Initialize(deviceDesc)) {
    delete device;
    return;
}

CommandQueueDesc queueDesc = {};
queueDesc.queueType = static_cast<uint32_t>(CommandQueueType::Direct);
RHICommandQueue* queue = device->CreateCommandQueue(queueDesc);

CommandListDesc cmdDesc = {};
cmdDesc.commandListType = static_cast<uint32_t>(CommandQueueType::Direct);
RHICommandList* cmd = device->CreateCommandList(cmdDesc);

BufferDesc vbDesc = {};
vbDesc.size = 1024;
vbDesc.stride = sizeof(float) * 8;
vbDesc.bufferType = static_cast<uint32_t>(BufferType::Vertex);
RHIBuffer* vertexBuffer = device->CreateBuffer(vbDesc);

cmd->Reset();
cmd->SetPrimitiveTopology(PrimitiveTopology::TriangleList);
cmd->Close();

vertexBuffer->Shutdown();
delete vertexBuffer;
cmd->Shutdown();
delete cmd;
queue->Shutdown();
delete queue;
device->Shutdown();
delete device;

这里最值得注意的是最后那段清理代码。当前 RHI 层不是自动 RAII 托管风格,而是显式关闭、显式释放。

为什么 RHIDevice 会这么“大”

初看 RHIDevice 很容易觉得它过于庞大,因为它几乎包揽了所有对象创建:

  • 资源
  • 队列和命令列表
  • shader / pipeline
  • descriptor pool / set
  • render pass / framebuffer
  • resource view

但这其实很符合低层图形后端的发展路径。因为这些对象本来就都依赖同一套 native device / context / backend state把创建权集中到设备上有几个直接好处

  • 上层只依赖一个统一入口
  • 设备能力与对象创建天然放在一起
  • 后端实现更容易把 native handle、allocator、capabilities 串起来

以后如果引擎演进到更成熟阶段,可以再考虑拆分 allocator、descriptor manager、pipeline cache 等子系统;但在当前阶段,这种集中式设备接口是务实的。

RHICommandQueueRHICommandList 应该怎么分工理解

RHICommandList

负责“录制要做什么”。

它当前可以记录:

  • barrier
  • render pass
  • resource binding
  • draw / draw indexed
  • dispatch
  • clear
  • copy

RHICommandQueue

负责“把录制好的东西交给 GPU 执行,并管理同步”。

它当前还暴露了:

  • fence signal / wait
  • idle 等待
  • frame index
  • timestamp frequency

这和显式图形 API 的常规心智模型一致。

需要提前知道的几个实现事实

1. 所有权是裸指针

当前工厂和设备的大部分创建接口都返回裸指针。测试中普遍采用:

  1. 创建
  2. 使用
  3. Shutdown()
  4. delete

不要把这层接口自动代入 shared_ptr / unique_ptr 语义。

2. 很多描述符字段不是强类型枚举字段

例如 BufferDesc::bufferTypeTextureDesc::formatCommandQueueDesc::queueType 都是整数类型。正确写法通常需要:

desc.queueType = static_cast<uint32_t>(CommandQueueType::Direct);

3. RHIFactory 的后端选择受编译开关影响

当前真实情况是:

  • D3D12 总能创建
  • OpenGLXCENGINE_SUPPORT_OPENGL 控制
  • VulkanXCENGINE_SUPPORT_VULKAN 控制
  • Metal 当前返回 nullptr

4. 抽象层并没有完全抹平后端差异

例如:

  • RHITypes.h 里仍能看到 RootSignatureDescDescriptorHeapDesc 这类 D3D12 命名
  • RHICommandList 同时保留 render pass 路径和 SetRenderTargets() 路径
  • RHICommandQueue::ExecuteCommandLists() 仍是 void**

正确的工程心态不是要求它现在就绝对纯净,而是接受它正处在“可用、可扩展、逐步收敛”的阶段。

和 Unity 风格设计该怎么对应

如果你更熟悉 Unity需要避免一个常见误区不要把这层直接理解成“Unity 的 public rendering API”。

更准确的对应关系是:

  • Unity gameplay / Component 层 对应 XCEngine 更高层的 Rendering、组件系统和未来的 renderer 封装
  • Unity 底层图形设备 / SRP 内部命令缓冲 才更接近当前 RHI

这种设计的工程收益是:

  • renderer 能做更细粒度控制
  • 后端替换成本更低
  • 图形测试不需要依赖完整场景系统

常见误用

忘记 Shutdown()

当前很多对象不能只靠析构做资源回收,至少从测试约定看不应该这么用。

把所有 RHIType 都当成可用后端

Metal 目前不是可创建后端,OpenGL / Vulkan 也不是所有构建都可用。

RHITypes 当成纯中立 ABI

它是 C++ 内部描述符集合,不是稳定二进制协议。

以为 RHICommandList 已经完全采用单一抽象范式

当前它混合了多种录制路径和状态绑定思路,阅读具体页面时要按“实现事实”理解。

从这里继续读什么