D3D12 教程:命令提交 - 构建 GPU 工作流水线
在 D3D12 中,命令提交是驱动 GPU 执行渲染任务的核心环节。 理解命令队列、命令列表和命令分配器之间的关系,以及如何有效地组织和提交命令,对于构建高性能 D3D12 应用程序至关重要。
1. D3D12 命令管理的核心概念
D3D12 使用命令队列 (Command Queue)、命令列表 (Command List) 和命令分配器 (Command Allocator) 来管理 GPU 的工作负载。 它们协同工作,形成一个高效的命令提交流水线:
命令队列 (Command Queue): GPU 上的命令执行队列。 CPU 将需要 GPU 执行的命令列表提交到命令队列。 GPU 按照提交顺序执行命令队列中的命令列表。 D3D12 支持多种命令队列类型,例如:
DIRECT
: 用于执行图形渲染和通用计算命令。COMPUTE
: 专门用于执行计算着色器 (Compute Shader) 命令。COPY
: 用于执行资源复制命令。
命令列表 (Command List): 记录一系列 GPU 命令的缓冲区。 命令列表在 CPU 端创建和填充,然后提交到命令队列由 GPU 执行。 命令列表必须关联到一个命令分配器。
命令分配器 (Command Allocator): 为命令列表分配内存的 D3D12 对象。 每个命令列表都必须从一个命令分配器中分配内存。 命令分配器可以重置 (Reset),重置后可以被命令列表重新使用,而无需重新创建。
2. d3d12_command
类:命令管理器的封装
d3d12_command
类封装了 D3D12 命令队列、命令列表和命令分配器的创建和管理,提供了一套简化的接口用于命令录制和提交。
2.1 构造函数 (d3d12_command
)
构造函数负责创建命令队列、命令分配器和命令列表:
C++
explicit d3d12_command(ID3D12Device8 *const device, D3D12_COMMAND_LIST_TYPE type)
参数:
device
: D3D12 设备指针 (ID3D12Device8*
),用于创建命令相关对象。type
: 命令队列类型 (D3D12_COMMAND_LIST_TYPE
),指定创建的命令队列和命令列表的类型 (DIRECT, COMPUTE, COPY)。
主要步骤:
创建命令队列:
C++
D3D12_COMMAND_QUEUE_DESC desc{}; desc.Type = type; DXCall(device->CreateCommandQueue(&desc, IID_PPV_ARGS(&_cmd_queue)));
使用
D3D12_COMMAND_QUEUE_DESC
结构体描述命令队列属性,然后调用device->CreateCommandQueue
创建命令队列。创建命令分配器 (帧缓冲区):
C++
for (u32 i{ 0 }; i < frame_buffer_count; ++i) { DXCall(hr = device->CreateCommandAllocator(type, IID_PPV_ARGS(&frame.cmd_allocator))); }
根据帧缓冲区大小 (
frame_buffer_count
) 循环创建命令分配器。 帧缓冲区技术允许 CPU 在 GPU 执行前一帧命令的同时,准备下一帧的命令数据,提高并行性。创建命令列表:
C++
DXCall(hr = device->CreateCommandList(0, type, _cmd_frames[0].cmd_allocator, nullptr, IID_PPV_ARGS(&_cmd_list))); DXCall(_cmd_list->Close());
使用第一个命令分配器 (
_cmd_frames[0].cmd_allocator
) 创建命令列表。device->CreateCommandList
创建的命令列表初始状态为 "recording" (正在录制命令),_cmd_list->Close()
将其关闭,使其回到 "closed" 状态,等待后续使用。
2.2 begin_frame()
函数:开始帧命令录制
begin_frame()
函数在每一帧渲染开始时调用,用于重置命令分配器和命令列表,为新帧的命令录制做准备:
C++
void begin_frame()
{
command_frame& frame{ _cmd_frames[_frame_index] };
frame.wait(); // 等待上一帧 GPU 命令执行完成 (当前为空,需要完善同步机制)
DXCall(frame.cmd_allocator->Reset());
DXCall(_cmd_list->Reset(frame.cmd_allocator, nullptr));
}
frame.wait()
: [待完善] 此处应实现帧同步机制,例如使用 Fence 或 Event,确保 CPU 在 GPU 执行完成上一帧命令后再开始录制新帧命令。 当前wait()
函数为空,表示 CPU 不等待 GPU,这在实际渲染循环中可能会导致资源竞争和错误。frame.cmd_allocator->Reset()
: 重置当前帧的命令分配器。 重置命令分配器非常高效,它可以快速回收之前帧使用的命令内存,而无需重新分配。_cmd_list->Reset(frame.cmd_allocator, nullptr)
: 重置命令列表,使其与当前帧的命令分配器关联,并准备好接收新的命令。 第二个参数pPipelineState
用于设置初始 Pipeline State Object (PSO), 这里传入nullptr
表示不设置,可以在后续的命令录制中再指定 PSO。
2.3 end_frame()
函数:结束帧命令录制并提交
end_frame()
函数在每一帧渲染结束后调用,用于关闭命令列表并将录制好的命令提交到命令队列:
C++
void end_frame()
{
DXCall(_cmd_list->Close());
ID3D12CommandList *const cmd_lists[]{ _cmd_list };
_cmd_queue->ExecuteCommandLists(_countof(cmd_lists), &cmd_lists[0]);
_frame_index = (_frame_index + 1) % frame_buffer_count;
}
_cmd_list->Close()
: 关闭命令列表,表示当前帧的命令录制完成。 关闭后的命令列表才可以被提交到命令队列执行。_cmd_queue->ExecuteCommandLists(_countof(cmd_lists), &cmd_lists[0])
: 将命令列表数组 (cmd_lists
) 中的命令提交到命令队列 (_cmd_queue
)。ExecuteCommandLists
是 CPU 提交命令到 GPU 的关键函数。 GPU 将从命令队列中取出命令列表并开始执行。_frame_index = (_frame_index + 1) % frame_buffer_count
: 更新帧索引,指向下一个可用的命令帧 (命令分配器),实现帧缓冲区的循环使用。
2.4 release()
函数:资源释放
release()
函数负责释放 d3d12_command
类中创建的所有 D3D12 资源,包括命令队列、命令列表和命令分配器:
C++
void release()
{
core::release(_cmd_queue);
core::release(_cmd_list);
for (u32 i{ 0 }; i < frame_buffer_count; ++i)
{
_cmd_frames[i].release();
}
}
3. 帧缓冲区 (Frame Buffer) 和命令并行
d3d12_command
类使用了帧缓冲区技术,通过创建多个命令帧 (command_frame
结构体数组 _cmd_frames
) 来提高 CPU 和 GPU 的并行性。 每个 command_frame
包含一个命令分配器。 在每一帧渲染时, d3d12_command
会循环使用这些命令帧。
帧缓冲区的优势在于:
CPU-GPU 并行: CPU 可以在 GPU 执行前一帧命令的同时,使用另一个命令帧的命令分配器开始录制下一帧的命令。 从而提高 CPU 和 GPU 的利用率,减少 CPU 等待 GPU 的时间。
资源复用: 命令分配器可以被重置和复用,避免了频繁的资源创建和销毁,提高了效率。
4. 总结
d3d12_command
类为 D3D12 命令提交管理提供了一个基础框架。 它封装了命令队列、命令列表和命令分配器的创建 1 和管理,并使用了帧缓冲区技术来提高 CPU-GPU 并行性。