Files
XCEngine/docs/api/_guides/RHI/Devices-Queues-CommandLists-And-Resource-Creation.md

237 lines
7.5 KiB
Markdown
Raw Normal View History

2026-03-29 01:36:53 +08:00
# 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](../../XCEngine/RHI/RHIFactory/RHIFactory.md) 选择后端并创建 [RHIDevice](../../XCEngine/RHI/RHIDevice/RHIDevice.md)。
2. 调用 `device->Initialize()` 初始化设备。
3. 读取 `GetCapabilities()` / `GetDeviceInfo()` 决定可用路径。
4. 通过 `device` 创建 [RHICommandQueue](../../XCEngine/RHI/RHICommandQueue/RHICommandQueue.md)。
5. 通过 `device` 创建 [RHICommandList](../../XCEngine/RHI/RHICommandList/RHICommandList.md)。
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
```cpp
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 等子系统;但在当前阶段,这种集中式设备接口是务实的。
## `RHICommandQueue` 和 `RHICommandList` 应该怎么分工理解
### `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::bufferType``TextureDesc::format``CommandQueueDesc::queueType` 都是整数类型。正确写法通常需要:
```cpp
desc.queueType = static_cast<uint32_t>(CommandQueueType::Direct);
```
### 3. `RHIFactory` 的后端选择受编译开关影响
当前真实情况是:
- `D3D12` 总能创建
- `OpenGL``XCENGINE_SUPPORT_OPENGL` 控制
- `Vulkan``XCENGINE_SUPPORT_VULKAN` 控制
- `Metal` 当前返回 `nullptr`
### 4. 抽象层并没有完全抹平后端差异
例如:
- `RHITypes.h` 里仍能看到 `RootSignatureDesc``DescriptorHeapDesc` 这类 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` 已经完全采用单一抽象范式
当前它混合了多种录制路径和状态绑定思路,阅读具体页面时要按“实现事实”理解。
## 从这里继续读什么
- [RHI](../../XCEngine/RHI/RHI.md)
- [RHIFactory](../../XCEngine/RHI/RHIFactory/RHIFactory.md)
- [RHIDevice](../../XCEngine/RHI/RHIDevice/RHIDevice.md)
- [RHICommandQueue](../../XCEngine/RHI/RHICommandQueue/RHICommandQueue.md)
- [RHICommandList](../../XCEngine/RHI/RHICommandList/RHICommandList.md)
- [RHITypes](../../XCEngine/RHI/RHITypes/RHITypes.md)
- [RHIEnums](../../XCEngine/RHI/RHIEnums/RHIEnums.md)