Skip to content

项目初始化

理论

Command List

命令列表(Command List),抽象为ID3D12CommandList。但该接口只是一个通用的抽象基类,实际包含的方法非常少,在应用时通常会使用更具体的接口。

ID3D12CommandList通常用于统一管理命令队列,由于它是父类,它的数组允许其元素是不同类型的命令队列。
通常,录制命令时使用更具体的CommandList类型指针(即CommandList的子类),提交命令时使用通用的ComandList指针。不需要显示转换,只需要把具体的CommandList指针加入通用的CommandList列表并提交到命令队列即可,编译器会自行处理。

当命令全部加入命令列表后,使用Close()方法关闭命令列表,并使用m_commandQueue.ExcuteCommandLists()提交到命令队列中。

GraphicsCommandList

API文档:ID3D12GraphicsCommandList (d3d12.h) - Win32 apps | Microsoft Learn

继承自CoomandList,封装了一系列图形渲染指令,这些方法都只是将命令加入命令列表,而不是立刻执行。

Command Queue

API文档:ID3D12CommandQueue (d3d12.h) - Win32 apps | Microsoft Learn

命令队列(Command Queue)本质是环形缓冲区,在dx12中CPU通过命令列表将命令提交到这个队列中,而命令不会在提交的同时立刻执行

初始化

命令队列在DX12中被抽象为ID3D12CommandQueue接口,创建命令队列前需要先创建对应的描述符。

Command Queue 初始化流程
//声明命令队列
ComPtr<ID3D12CommandQueue> m_commandQueue;

//指定命令队列描述符
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;    //特殊行为标志,位掩码
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;    //命令队列类型,包括DIRECT、BUNDLE、COMPUTE和COPY四种

//通过设备创建命令队列,参数分别是命令队列描述符、接口类型和存放命令队列实例的地址
ThrowIfFailed(m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue)));

这里使用了IID_PPV_ARGS,这是WindowsSDK中一个宏,作用是将接口类型转换成GUID并传入接口地址。

常用方法

  • ExecuteCommandLists()

    用于将命令列表数组里的命令添加到命令队列中,一次性可以添加多个命令列表,GPU会按数组中的索引开始顺序执行。。有如下参数:

    • UINT Count :命令列表数组中命令列表的数量
    • ID3D12CommandList *const *ppCommandLists:待执行的命令数组,指向命令列表数组的第一个元素。
    调用例
    1
    2
    3
    4
    5
    6
    7
    8
    // 1. 准备 ComPtr 列表
    ComPtr<ID3D12GraphicsCommandList> cmdList; 
    
    // 2. 提交时,用 Get() 拿到原始指针,放进数组
    ID3D12CommandList* ppLists[] = { cmdList.Get() };
    
    // 3. 数组名 ppLists 会自动转换成 ID3D12CommandList* const*
     m_CommandQueue->ExecuteCommandLists(1, ppLists);
    

Command Allocator

API文档:ID3D12CommandAllocator (d3d12.h) - Win32 apps | Microsoft Learn

命令分配器。存储命令的接口,可以与多个命令列表关联,但同一个命令分配器同一时间,只允许有一个命令列表处于录制状态且与该命令分配器关联。
逻辑关系上,可以把Coomand List视为笔,Command Allocatoe视为纸。
命令列表创建和重置时默认处于打开状态,因此不能连续创建命令列表并将其加入Allocator

创建与销毁CommandAllocator的开销极为昂贵,通用的做法是创建与Swap Chain的缓冲区数量相同数量的Command Allocator,每个缓冲区的录制统一使用一个。在确认GPU执行完命令分配器中的命令后,可以通过Reset()方法重置Allocator。

初始化Command Allocator需要使用ID3D12Device。

初始化Command Allocator
1
2
3
4
5
m_device->CreateCommandAllocator(
    D3D12_COMMAND_LIST_TYPE type,
    REFIID riid,
    void **ppCommandAllocator
)

其中:

  • type

    指定与分配器关联的命令列表的类型,常用的有两种:

    • D3D12_COMMAND_LIST_TYPE_DIRECT
    • D3D12_COMMAND_LIST_TYPE_BUNDLE
  • rrid:接口的com id。
  • ppCommandAllocator:输出指针

flushing the command queue &Fench

API文档:ID3D12Fence(d3d12.h)- Win32 应用 | Microsoft Learn --- ID3D12Fence (d3d12.h) - Win32 apps | Microsoft Learn

为了避免CPU和GPU在并行计算时不同步,导致CPU提前覆写GPU需要使用的数据,DX12使用Fence(围栏)来进行CPU与GPU之间的同步。
其思路是强制CPU进行等待,直到GPU执行完命令达到指定的fence point为止。

初始化fence
1
2
3
4
5
m_device->CreateFence(UINT64 InitialValue,                    
                      D3D12_FENCE_FLAGS Flag, 
                      REFIID riid,
                      void **ppFence;
                     );

每个Fence对象维护一个UINT64类型的值InitialValue,用来标识Fence point。
创建Fence对象时,InitialValue应传入\(0\);每当需要标记一个新的Fence point时,InitialValue的值就加\(1\)

资源转换 Resource Barrier

为了避免资源转换未完成时GPU就执行相关达到读写操作,DX12使用一系列状态标记资源。
需要注意资源转换和跟踪状态这一行为有一定开销,应该谨慎地使用。

命令与多线程

Command List、Command Allocator都不是线程自由对象,每个线程只能使用各自的对象。

Command Queue是线程自由对象,且多个线程可以同时向Command Queue提交Command List。

创建项目

D3D12项目通常是桌面程序,在新建项目时应该选择:

image-20260326140106079

并选择“桌面应用程序(.exe)”,勾选”空项目“:

image-20260326140221126

需要注意,对于桌面程序,其程序主入口通常是WINAPI WinMain而不是main

1
2
3
4
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int showCmd)
{
    return 0;
}

支撑文件

WinInclude.h

设置一个头文件以引用在整个项目中都会引用到的依赖库,并在后续其它文件中都引用该头文件,以避免多次引用同一个库导致重复定义和其它异常。

在这里将其命名为WinInclude.h,放在/Support/下,通常一个D3D12项目的WinInclude.h文件至少需要包含以下内容:

支撑文件
#pragma once           //允许且仅允许该头文件被include一次

#define NOMINMAX         //禁用windows.h 中max和min的宏定义

#ifndef WIN32_LEAN_AND_MEAN       //编译优化,仅引用windows.h中核心功能
#define WIN32_LEAN_AND_MEAN            
#endif

#include <windows.h>

#include <d3d12.h>          //d3d12
#include <dxgi1_6.h>
#include <D3Dcompiler.h>
#include <DirectXMath.h>   //数学运算   
#include "d3dx12.h"        //d3d12的辅助

#include <string>
#include <wrl.h>
#include <shellapi.h>

//在Debug中开启增强的错误检查和内存泄露监控。未开启的情况下,D3D12不会提供任何有关报错的信息
#ifdef _DEBUG            
#include <d3d12sdklayers.h>
#include <dxgidebug.h>
#endif

项目属性

打开项目属性,分步进行以下设置:

依赖项

选择配置属性->链接器->输入->附加依赖项,增加d3d12.lib;dxgi.lib;dxguid.lib

image-20260326142531522

  • d3d12.lib

    D3D12的核心静态库,提供与GPU交互的底层接口与函数,用于创建Deivce、Commond Queue、PSO和Assets(Buffer/Textrue)等。

    常用于初始化d3d12设备,管理渲染管线、提交绘制命令、同步GPU和CPU等。

  • dxgi.lib

    DXGI的静态库,负责图形的硬件抽象层,提供了与GPU、SwapChain、显示模式等的相关功能。

    常用于枚举GPU适配器、创建SwapChain、查询显示器支持的显示模式等。

  • dxguid.lib

    包含DXGI和Direct3D接口的GUID定义,用于唯一标识COM接口。

包含目录

选择配置属性->VC++目录->包含目录,添加当前项目的根目录,以便在项目中include。

image-20260326150106762

浮点模型和SSE2

选择配置属性->C/C++->代码生成->浮点模型,选择快速(/fp:fast)。

同一页面找到启用增强指令集,选择流式处理SIMD拓展2(/arch:SSE2);这一步仅在x86平台是必须的,x64默认支持SSE2。

image-20260326162637057

D3D初始化

总体流程:

  1. 创建ID3D12Device实例
  2. 创建ID3D12Fence实例,并查询Descriptor大小
  3. 检查设备对4X MSAA的支持情况
  4. 依次构建Command Queue、Command Allocator和Command List。
  5. 描述并创建Swap Chain。
  6. 创建Descriptor Heap。
  7. 调整后台缓冲区大小,创建RTV(渲染目标视图)
  8. 创建深度/模板缓冲区和关联视图
  9. 设置视口和裁剪矩形。

调试