237 lines
7.5 KiB
Markdown
237 lines
7.5 KiB
Markdown
|
|
# 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)
|