在 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)。

  • 主要步骤:

    1. 创建命令队列:

      C++

      D3D12_COMMAND_QUEUE_DESC desc{};
      desc.Type = type;
      DXCall(device->CreateCommandQueue(&desc, IID_PPV_ARGS(&_cmd_queue)));
      

      使用 D3D12_COMMAND_QUEUE_DESC 结构体描述命令队列属性,然后调用 device->CreateCommandQueue 创建命令队列。

    2. 创建命令分配器 (帧缓冲区):

      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 执行前一帧命令的同时,准备下一帧的命令数据,提高并行性。

    3. 创建命令列表:

      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 并行性。