文章目录
概述
包含D3D初始化以及硬件原理等基础知识
内容
组件对象模型(COM)
COM相当于是一个接口,也可以当做一个C++类,它封装了很多底层的细节,COM接口的所有功能都是从IUnknow这个COM接口继承来的,所有COM接口都以“I”开头
COM出现的目的是为了解决二级制兼容性问题,为了更好地在不同平台与编译器上加载dll文件而不需更改源代码或影响较小,但为了解决这个问题导致了COM的实现异常复杂,因此初学者不需掌握其基本原理,会用就行。
因为不同的编译器对内存管理函数的实现方法是不同的,一旦在dll文件的某个类中新添加了一个变量,就有可能导致用户访问越界,即使规定好在哪些地方使用delete对内存进行释放也有可能出现内存释放错误
因此实现二进制兼容就需要注意:由dll的函数控制分配内存而不是让用户使用new,由dll的release函数控制释放内存而不是由用户使用,使用引用计数控制内存释放时机,这就是IUnknow接口的基本原理(在这个接口中,只有引用计数加,引用计数减,向下转型检查这几个类型的函数)
以IUnknow为例的基本函数:
为接口分配空间并返回指针
比如:D3D12CreatDevice等函数
转型函数:
QueryInterface函数
这是一个IUnknow里的一个纯虚函数,用于检测能否转型为GUID所指向的类型,不行就返回E_NOTIMPL可以就返回S_OK并将转换后的指针作为参数返回并且增加引用计数等
create函数
微软统一的通用create函数被封装为
HRESULT CoCreateInstance(
[in] REFCLSID rclsid,
[in] LPUNKNOWN pUnkOuter,
[in] DWORD dwClsContext,
[in] REFIID riid,
[out] LPVOID *ppv
);
第一个参数是待创建组件的CLSID(类标识符)
第二个参数用于聚合组件,D3D并不涉及这个内容
第三个参数用于限定所创建组件的执行上下文,D3D不涉及
第四个参数是组件上待使用接口的IID(接口标识符)
第五个参数用于接收返回的指向此接口的指针
然而D3D中的COM技术创建接口并不需要CLSID并且都有特定函数用于create,而不是依赖于最基本的CoCreateInstance,而IID可以使用msvc的关键字__uuidof获取,无需查询CLSID
注:组件就是已编译好的一块用于调用来实现某种功能的东西
COM接口实例的创建与销毁:
获取指向COM的指针不能使用new关键字,而是要依赖于特定函数或者另一COM接口,在销毁这一实例时,也应该调用它的Release方法
Windows运行时库对COM的管理
Windows运行时库提供了用于管理COM的Microsoft::WRL::ComPtr类,它相当于是一个智能指针,当一个ComPtr实例超出作用域范围时,它会自动调用相应对象的Release方法,以下是常用的3个ComPtr方法:
//Get:返回一个指向此底层COM接口的指针。常用此方法将COM接口的指针传递给函数
ComPtr<ID3D12RootSignature> mRootSignature;//初始化一个指向ID3D12RootSignature的ComPtr
//以下有一个需要获取ID3D12RootSignature*类型的参数的函数
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
//GetAddressOf:返回指向此底层COM接口的指针的地址。用于传参
ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
//示例函数
ThrowIfFailed(md3dDevice->CreatCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.GetAddressOf()));
//Reset:将此ComPtr实例设置为nullptr释放与之相关的所有引用(并减少其底层COM接口的引用计数),与直接将目标实例直接赋值为nullptr效果相同
纹理格式
2D纹理是一种由数据元素构成的矩阵,它的用途之一是存储2D图像数据,在这种情况下,纹理的每一个元素储存的是一个像素(这个像素并不是指的是显示器上的像素,而是抽象地指一个有固定长宽的色块)的颜色,还有一些纹理的高级应用,比如法线贴图,这种纹理中每个元素储存的是一个3D向量,除此之外,纹理格式还可能有多个mipmap层级,因此纹理格式并不只是储存图像数据这么简单,但现在暂时不做讨论
GPU会对它们进行特殊处理,比如使用过滤器或多重采样,纹理只能存储DXGI_FORMAT枚举类型中描述的特定格式的数据元素,比如
DXGI_FORMAT_R32G32B32_FLOAT//每个元素由3个32位浮点数分量构成
DXGI_FORMAT_R16G16B16A16_UNORM//每个元素由4个16位分量构成,每个分量被映射到[0,1]区间
DXGI_FORMAT_R32G32_UNIT//每个元素由2个32位无符号整型分量构成
DXGI_FORMAT_R8G8B8A8_UNORM//每个元素由4个8位无符号分量构成,每个分量被映射到[0,1]区间
DXGI_FORMAT_R8G8B8A8_SNORM//每个元素由4个8位有符号分量构成,每个分量被映射到[-1,1]区间
DXGI_FORMAT_R8G8B8A8_SINT//每个元素由4个8位有符号整型分量构成,每个分量被映射到[-128,127]区间
DXGI_FORMAT_R8G8B8A8_UNIT//每个元素由4个8位无符号整型分量构成,每个分量被映射到[0,255]区间
RGBA分别表示红色、绿色、蓝色和透明度,但是实际上并不一定拿这些格式去储存颜色信息,比如DXGI_FORMAT_R32G32B32_FLOAT可以用来存储三维向量,除此之外,无类型(typeless)的格式可以用来预留内存,当纹理被绑定到渲染流水线时,再去具体解释它的数据类型
交换链与页面翻转
为了防止多种因素(比如GPU性能不稳定,绘制速度慢等)导致的画面闪烁,引入了交换链这种缓冲机制,原理是先在一个缓冲区绘制屏幕图案(前台缓冲区),在显示这一帧时去绘制下一帧(后台缓冲区),绘制完成时互换两缓冲区(通过交换指针),前台缓冲区与后台缓冲区构成了交换链,后台缓冲区变为前台缓冲区的过程叫做呈现,这种有两个缓冲区的情况也被叫做双缓冲,当然也有三重缓冲的例子,但一般来说双缓冲就够了(但建议FPS游戏使用三缓冲)
三缓冲的优势是能够增加帧率并在一定条件下降低延迟,但是非常可惜,D3D并不支持三缓冲(而OpenGL支持),常常与三重缓冲同时出现的一种叫做垂直同步的技术,这种技术能够遏制屏幕撕裂(这种现象出现的原因是屏幕刷新率与显卡性能不匹配,显卡渲染速度过快或过慢都会导致这种现象)但会提高延迟并且有不好的观感(因为强制地把显卡渲染速度和屏幕刷新率绑定在了一起,显卡会等待屏幕导致显示滞后),因此现在出现的一些其他技术(比如自适应显示屏刷新率)
现在的一部分游戏有OpenGL模式和D3D模式,因此可以打开三重缓冲
深度缓冲
深度缓冲区也被称为Z缓冲(Z指的是Z坐标),是一种2D纹理资源,它储存了摄像机与像素的距离,深度值的取值范围为[0.0,1.0],0.0表示摄像机能看到的最近的物体,1.0指的是摄像机能看到的最远的物体
摄像机的观察范围是一个视锥体,由于屏幕一般为长方形,视锥体是一个四棱台,常称这个台体为平截头体
深度缓冲区的元素与后台缓冲区内的像素一一对应,后台缓冲区有多少个像素深度缓冲区就有多少个元素
开始渲染之前,后台缓冲区被清理成默认颜色,深度缓冲区的所有深度值被清除为1.0,深度缓冲区算法对确定深度缓冲区对应像素深度是对物体在摄像机上的位置存在竞争时(向摄像机投影,有重叠)使用了深度测试的方法,深度值小的通过测试,深度值大的被掩盖
由于深度缓冲区也是一种纹理,因此它只支持特定的数据格式,可用的数据格式包括:
DXGI_FORMAT_D32_FLOAT_S8X24_UINT//共占用64位,其中32位指定一个浮点型深度缓冲区,8位分配给模板缓冲区并将该元素映射到[0,255]区间,剩下的24位用于对齐
DXGI_FORMAT_D32_FLOAT//指定一个32位浮点型深度缓冲区
DXGI_FORMAT_D24_UNORM_S8_UINT//指定一个无符号24位深度缓冲区,并将该元素映射到[0,1]区间,另有8位分配给模板缓冲区,并将此元素映射到[0,255]区间
DXGI_FORMAT_D16_UNORM//指定一个无符号16位缓冲区,把该元素映射到[0,1]区间
关于模板缓冲区的内容暂时不提及,指定模板缓冲区后,它与深度缓冲区总是有很紧密的联系
资源与描述符
在渲染过程中GPU会对资源进行读写,在发出绘制命令之前,需要将本次绘制调用相关的资源绑定到渲染流水线上,一些资源可能在每次调用时都有变化,所以每次也要按需更新绑定
然而GPU并不与渲染流水线直接绑定,而是要通过一种名为描述符的对象间接进行引用,总的来讲,描述符是一个中间层
视图与描述符是同义词,在较早些的版本中,称描述符为视图
描述符的类型
类型指明了资源的具体作用,列举一些比较常用的描述符
CBV:常量缓冲区视图
SRV:着色器资源视图
UAV:无序访问视图
sampler:采样器资源
RTV:渲染目标视图资源
DSV:深度/模板视图资源
描述符堆
其中存有一系列的描述符,相当于是一个描述符数组,每种类型的描述符都有一个描述符堆,此外也可以为同类型的描述符创建多个描述符堆
创建描述符的最佳时机为初始化期间,因为创建描述符时会进行一些类型的检测和验证工作,所以最好不要在运行时才创建
抗锯齿(反走样)技术
反走样技术,也叫抗锯齿技术,由于屏幕中的像素不能做到无穷小,所以比较细的线会呈阶梯状的锯齿,这样会影响观感,因此出现了一些减轻这样现象的技术,以下是两种抗锯齿技术
超级采样技术(SSAA)
使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲区,当向屏幕显示时,会将后台缓冲区的像素进行四个一组的解析(也叫降采样),每组用平均值的方法得到一组比较平滑的像素颜色
但超级采样技术操作开销高昂,它将处理像素的数量以及内存都增加了4倍,但能够使画面更加精细
多重采样技术(MSAA)
这种技术将一个像素分割为几个子像素,并跨子像素共享一些计算信息,从而比超级采样开销更低。
以4X多重采样为例,它把一个像素分割为4个子像素,并同样使用4倍于屏幕分辨率的后台缓冲区,但多重采样计数并不计算所有的像素,而是测试像素的中心处的颜色(虽然也许并不能直接测试中心处,但应该是有某些算法去间接确定),并将得到的颜色信息分享给每个子像素,进行运算(取平均值得到一个中间色)后确定出整个像素的颜色
计算图像颜色是图形流水线中开销最大的步骤之一,尽管MSAA能够做到开销较小的同时实现一定的抗锯齿效果,但是其效果并没有SSAA好
FSR2
AMD的一种提高帧数的方案,很广泛的支持了新旧型号的AMD显卡和部分英伟达显卡,画质较低,不如SMAA,不同版本有不同原理,但总的来说就是拿画质换取了帧数,并且会使用CPU的资源
SMAA
英伟达的一种提高画质与帧率的方案,使用AI模型对帧进行分析,在真实的两帧之间加入用AI分析得到的几个帧,优点是画质高,帧率高,但缺点是图标和文字的变形,且只支持一些比较新的显卡
DLSS
是TAA与AI渲染结合的一种技术,作用是通过降低游戏内的渲染分辨率,同时再通过人工智能算法模型和AI加速硬件单元(Tensor Core)来拉伸输出画面,提高显示分辨率,例如使用1080P的渲染分辨率再通过AI算法和Tensor Core运算输出4K(2160P)的显示分辨率,以此来达成提升帧数的目的。
这个描述和FSR类似,但DLSS远比FSR优秀,不过,DLSS只支持英伟达显卡
TAA
时间性抗锯齿,应用得很广泛,是一种基于着色器的算法,使用运动矢量组合前后两帧并对前一帧进行采样处理,对每一个像素进行一个抖动操作,当连续多帧数据混合后就相当于对每个像素进行了多次采样,而不需要对每一帧每一个像素进行多次采样,从而实现减少计算量,但由于TAA会盲目跟随移动物体的移动矢量,这也导致TAA会让画面细节变模糊。
D3D的多重采样实现
DXGI_SAMPLE_DESC结构体
定义
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
UINT Quality
}DXGI_SAMPLE_DESC
Count指定了每个像素的采样次数Quality指定了用户期望的图像的质量级别(但对于不同的硬件厂商来说,这个词语的意义也有甚至很大的不同),采样次数越多或质量级别越高,渲染质量越好但渲染操作的代价也就越高,质量级别的范围取决于纹理格式和每个像素的采样数量
根据给定的纹理格式和采样数量,能够用ID3D12Device::CheckFeatureSupport方法查询到对应的质量级别
typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS{
DXGI_FORMAT Format;
UINT SampleCount;
D3D_MULTYSAMPLE_QUALITY_LEVEL_FLAGS Flags;
UINT NumQuanlityLevels;
}D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format=mBackBufferFormat;
msQualityLevels.SampleCount=4;
msQualityLevels.Flags=D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels=0;
ThrowFailed(md3dDevice->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,//这个既可以当作输出也可以当作输入参数,作为输入参数时,必须指定纹理格式,采样数量以及希望查询的多重采样所支持的标志(即立flag),函数执行之后就会在第二个参数上返回图像质量级别
sizeof(msQualityLevels)));
对于某种纹理格式和采样数量的组合来说,其质量级别的有效范围是0至NumQualityLevels-1
每个像素的最大采样数量是D3D12_MAX_MULTISAMPLE_SAMPLE_COUNT,值为32
通常将采样数量设置为4或8以保证性能,若不使用多重采样,可以设置采样数量为1,质量级别为0
功能级别
从D3D11开始引入了功能级别的概念(在代码中使用枚举类型D3D_FEATURE_LEVEL表示),以下参数对应于D3D9到D3D11的各种版本,在D3D12中同样对这个做出了更新(但估计这本书沿袭了很多D3D11的内容,书中的代码并没有更新)
enum D3D_FEATURE_LEVEL
{
D3D_FEATURE_LEVEL_9_1 =0x9100,
D3D_FEATURE_LEVEL_9_2 =0x9200,
D3D_FEATURE_LEVEL_9_3 =0x9300,
D3D_FEATURE_LEVEL_10_0 =0xa100,
D3D_FEATURE_LEVEL_10_1 =0xa200,
D3D_FEATURE_LEVEL_11_0 =0xb000,
D3D_FEATURE_LEVEL_11_1 =0xb100,
D3D_FEATURE_LEVEL_12_0 =0xc000,
D3D_FEATURE_LEVEL_12_1 =0xc100
}D3D_FEATURE_LEVEL;
为了程序的兼容性,应当从新到旧依次进行检测硬件所能够支持的功能级别
DirectX图形基础结构(DXGI)
也译作图形基础设施,它的设计目的是想让不同的图形API中所有共同的底层任务能够借助一组通用API来处理,比如说屏幕显示用的交换链接口IDXGISwapChain还有切换全屏与窗口模式等内容,还定义了D3D支持的各种表面格式信息(DXGI_FORMAT)
IDXGIFactory是DXGI的关键接口之一,主要用于创建IDXGISwapChain接口以及枚举显示适配器,显示适配器即进行图形运算等功能的设备,比如显卡,一台设备可以有多个显示适配器,适配器用接口IDXGIAdapter来表示,在Windows8以后的版本存在软件显示适配器,只要硬件显示适配器以及驱动不出问题,它一般没有任何功能
用于显示的设备被称为显示输出实例,用IDXGIOutput接口表示,一个显示适配器可以与多个显示输出设备相关联
每种显示设备都有它支持的显示模式,可以用DXGI_MODE_DESC结构体的数据成员表示
typedef struct DXGI_MODE_DESC
{
UINT Width; ` //分辨率宽度
UINT Hight; //分辨率高度
DXGI_RATIONAL RefrshRate; //刷新率单位为hz
DXGI_FORMAT Format; //显示格式
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //逐行还是隔行扫描
DXGI_MODE_SCALING Scaling; //图像相对于屏幕拉伸
}DXGI_MODE_DESC;
typedef struct DXGI_RATIONAL
{
UINT Numerator;
UINT Denominator;
}DXGI_RATIONAL;
typedef enum DXGI_MODE_SCANLINE_ORDER
{
DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED =0,//未指明
DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE =1,//逐行扫描
DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST=2,//隔行扫描,优先扫描奇数行(因为将一行称为一场,也叫奇数场)
DXGI_MODE_SCANLINE_ORDER_LOWER_FILED_FIRST=3//隔行扫描,优先扫描偶数行
}DXGI_MODE_SCANLINE_ORDER;
typedef enum DXGI_MODE_SCALING
{
DXGI_MODE_SCALING_UNSPECIFIED =0,
DXGI_MODE_SCALING_CENTERED =1,//不做缩放,将图像显示在屏幕正中
DXGI_MODE_SCALING_STRETCHED =2//根据屏幕分辨率对图像进行拉伸缩放
}DXGI_MODE_SCALING;
进入全屏模式时,枚举显示模式非常重要,为了全屏模式效果最佳,要求指定的显示模式与显示器支持的显示模式完全匹配
DXGI的更多资料在DXGI Overveiw中进行查询
检测功能支持
检测功能支持的函数:
HRESULT ID3D12Device::CheckFeatureSupport(D3D12_FEATURE Feature,
void *pFeatureSupportData,
UINT FeatureSupportDataSize);
以下是对这个函数的参数的解释
Feature:传入的是枚举类型D3D_FEATURE的一个成员,用于指定我们希望检测的功能支持类型
D3D12_FEATURE_D3D12_OPTIONS:检测当前驱动对D3D12各种功能的支持情况
D3D12_FEATURE_ARCHITECTURE:检测图形适配器中GPU的硬件体系架构特性
D3D12_FEATURE_FEATURE_LEVELS:检测对功能级别的支持情况
D3D12_FEATURE_FORMAT_SUPPORT:检测对给定纹理格式的支持情况
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:检测对多重采样的支持情况
pFeatureSupportData:指向某种数据结构的指针,该结构中存有检索到的特定功能支持的信息,具体类型取决于Feature参数
当Feature参数为D3D12_FEATURE_D3D12_OPTIONS时
则传回一个D3D12_FEATURE_DATA_D3D12_FEATURE_D3D12_OPTIONS实例
当Feature参数为D3D12_FEATURE_ARCHITECTURE时
则传回一个D3D12_FEATURE_DATA_ARCHITECTURE实例
当Feature参数为D3D12_FEATURE_FEATURE_LEVELS时
则传回一个D3D12_FEATURE_FEATURE_LEVELS实例
当Feature参数为D3D12_FEATURE_FORMAT_SUPPORT时
则传回一个D3D12_FEATURE_DATA_FORMAT_SUPPORT实例
当Feature参数为D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS时
则传回一个D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS实例
FeatureSupportDataSize:传回pFeatureSupportData参数中的数据结构的大小
CheckFeatureSupport的第二个参数兼有输入与输出属性,作为输入时,先要指定功能级别数组的元素的个数(NumFeatureLevels),再令(pFeatureLevelsRequested)指针指向功能级别数组,其中应包括希望检测的一系列硬件支持功能级别。最后此函数将用MaxSupportedFeatureLevel字段返回当前硬件支持的最高功能级别
资源驻留
游戏中的资源(比如各种纹理和3D网格),但某些资源不需要一直储存在显存中,比如说某些不会立即使用的地图数据,可以把这些地图数据先从显存中删除掉
在D3D12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况(即residency,无论资源是否本已位于显存中,都能够对其进行管理。在D3D11中则是系统自动管理)
应避免短时间内在显存中进出相同的资源,清出的资源应在短时间内不会再次使用
控制资源驻留的方法:
HRESULT ID3D12Device::MakeResident(UINT NumObjects,
ID3D12Pageable *const *ppObjects);
HRESULT ID3D12Device::Evict(UINT NumObjects,
ID3D12Pageable *const *ppObjects);
这两种方法的第二个参数都是ID3DPageable资源数组,第一个参数则表示该数组中资源的数量
CPU与GPU的交互(最佳运行时关系)
最好让CPU与GPU同时运作,少同步,因为同步势必会使某一方等待另一方,浪费空闲资源
为了达到这样的目的,有一些措施:
命令队列与命令列表
每个GPU至少维护着一个命令队列(是一个环形缓冲区),借助D3DAPI,CPU可以使用命令列表将命令提交到命令队列中去(D3D11支持立即渲染以及延迟渲染,立即渲染是将缓冲区中的内容直接借助驱动层发往GPU执行,后者与文中介绍的命令列表相似,到了D3D12就完全取消了立即渲染,完全采用了命令列表-命令队列模型,使多个命令列表同时记录命令以发挥多核心优势)
当命令被提交到命令队列时,并不会被GPU立即执行,而是等待前面的命令执行完毕后才执行
当命令列表为空时,GPU会等待CPU,当命令列表已满时,CPU会等待GPU
在D3D12中,命令队列抽象为ID3D12CommandQueue接口,需要通过填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列,再调用ID3D12Device::CreatCommandQueue方法来创建队列
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc={};
queueDesc.Type=D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags=D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc,
IID_PPV_ARGS(&mCommandQueue)));
IID_PPV_ARGS辅助宏的定义如下:
IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
其中,__uuidof(**(ppType))将获取(**(ppType))的COM接口ID(即GUID),上述代码获得了ID3D12CommandQueue接口的COM ID。这个辅助宏的本质是将ppType强制转换为void**类型。这个辅助宏很常用,因为在调用D3D12中创建接口实例的API时,大多都有一个参数是类型为void**的待创建接口COM ID
ExecuteCommandLists是一种常用的ID3D12CommandQueue接口方法,可以使用它来将命令列表里的命令添加到命令队列中:
void ID3D12CommandQueue::ExecuteCommandLists(UINT Count,
ID3D12CommandList *const *ppComandLists);
GPU将从数组中的第一个命令列表开始顺序执行
ID3D12GraphicsCommandList接口封装了一系列图形渲染命令,继承自ID3D12CommandList接口
以下是向命令列表添加设置视口、清除渲染目标视图和发起绘制调用的命令
mCommandList->RSSetVeiwPorts(1,&mScreenVeiwPorts);//第一个参数是视口数量,第二个参数是视口参数列表
mCommandList->ClearRenderTargetVeiw(mBackBufferVeiw,
Colors::LightSteelBlue,0,nullptr);
//第一个参数是渲染目标环境,第二个参数是清除颜色值,第三个参数与第四个参数默认0与nullptr
mCommand->DrawIndexedInstanced(36,1,0,0,0);
//第一个参数是indexbuffer中用于绘制的下标数,第二个参数是从indexbuffer中开始绘制的下标位置,其余的默认0即可
上面的代码只是把命令添加到命令列表而不是添加到命令队列,需要调用ExecuteCommandLists方法才能把命令列表的内容放到命令队列中
当所有的命令都被放入命令列表后,需要调用ID3D12GraphicsCommandList::Close方法来结束命令的记录:
mCommandList->Close();
在提交之前必须将其关闭
有一个与命令列表相关的内存管理类接口ID3D12CommandAllocator,记录在命令列表中的命令实际上是储存在关联的命令分配器中的,通过ID3D12CommandQueue::ExecuteCommandList方法执行命令列表时,命令队列就会引用分配器中的命令,而命令分配器由ID3D12Device接口创建:
HRESULT ID3D12Device::CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
void **ppCommandAllocator);
/*
其中第一个参数type:
指定与此命令分配器相关联的命令列表类型,以常用的两个为例:
D3D12_COMMAND_LIST_TYPE_DIRECT:存储了一系列可以使GPU直接执行的命令
D3D12_COMMAND_LIST_TYPE_BUNDLE:将命令列表打包(bundle),由于构建命令列表时会产生一定的CPU开销,D3D提供了一种优化方案,允许将一系列命令打包,当打包完成(命令记录完成)后,驱动会对其中的命令进行预处理,使它们在被执行的过程中得到优化。因此在初始化阶段就应该用包记录命令。若在构建某些命令列表时发现那会很占用时间就可以考虑使用打包技术进行优化,但因为D3D12的绘制API效率很高,一般来说不会用到这种技术。
第二个参数riid:
待创建ID3D12CommandAllocator接口的COM ID。
第三个参数ppCommandAllocator:
输出指向所创建命令列表分配器(这也是一个指向堆中空间的指针)的指针
*/
命令列表同样由ID3D12Device创建:
HRESULT ID3D12Device::CreateCommandList(UINT nodeMask,
D3D12_COMMAND_LIST_TYPE type,
ID3D12CommandAllocator *pCommandAllocator,
ID3D12PipelineState *pInitialState,
REFIID riid,
void **ppCommandList);
/*
第一个参数nodeMask:
对于仅有一个GPU的系统而言,要将此值设置为0;对具有多GPU的系统来说,此节点掩码(nodeMask)指定的是与所建命令列表相关联的物理GPU,但这里只考虑单GPU的情况
注:可以使用ID3D12Device::GetNodeCount方法查询系统中GPU适配器节点(物理GPU)的数量
第二个参数type:
命令列表的类型,与上述相同
第三个参数pCommandAllocator:与所建命令列表相关联的命令分配器,它的类型必须与所创命令列表的类型相匹配
第四个参数pInitialState:指定命令列表的渲染流水线的初始状态,对于打包技术来说可以将此设为nullptr,另外设为nullptr也适用于不进行任何绘制操作的情况,比如进行初始化
第五个参数riid:
待创建ID3D12CommandList接口的COM ID
第六个参数ppCommandList:输出指向所建命令列表的指针,这是一个输出属性的参数
*/
可以创建出多个命令列表,但是不能同时使用它们,因为命令列表会占用命令分配器,只有关闭其他所有命令列表时才可以对一个命令列表记录命令,注意,在创建与重置(reset)一个命令列表时,它们都是打开的状态
命令列表的重置
在调用ID3D12CommandQueue::ExecuteCommandList©方法(C是一个命令列表)后,可以通过调用ID3D12GraphicsCommandList::Reset方法安全地对命令列表C占用的相关底层内存进行复用来记录新的命令集(将命令列表恢复至刚创建的状态),这两个函数的参数相互对应,完全相同,在调用这个方法前,应该将操作的命令列表关闭,否则将报错
上述重置列表命令并不会影响已经在命令队列里的命令,因为相关的命令分配器仍然在维护着其内存中被命令队列引用的相关命令
还有另外一种reset函数:ID3D12CommandAllocator::Reset(),这个函数也是对内存进行复用的,但与上一个reset不同,它会将命令列表清空并保留命令列表的容量
官方文档中如此描述:与 ID3D12CommandAllocator::Reset 不同,可以在仍在调用命令列表时调用ID3D12GraphicsCommandList::Reset。 典型的模式是提交一个命令列表,然后立即将其重置为对另一个命令列表重复使用分配的内存。
所以,ID3D12CommandAllocator::Reset()函数是对命令列表进行内存复用且命令队列中的命令不会被命令分配器维持着,所以当命令队列中仍然有命令未执行时,千万不要调用这个函数
CPU与GPU的同步
举例来说,由于CPU不会因命令队列导致阻塞而进行等待,当GPU还在位置p1绘制R时CPU已经将下一步R的位置p2覆盖到内存中时,就会发生位置的错误,R所处的p1位置变成了p2但实际上的位置没有变化
当然,这种错误在程序有比较优秀的错误修正机制的情况下通常并不会发生,但CPU与GPU的不协调会引发一些其它方面的致命错误,为了规避这些问题而编写一堆复杂的代码不如从根本上解决问题,因此我们可以使CPU强制等待GPU,直到达到某个指定的围栏点(fence point)为止,这种方法称为刷新命令队列,可以通过围栏来实现这一点
围栏用ID3D12Fence接口来表示,创建一个围栏对象:
HRESULT ID3D12Device::CreateFence(UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
void **ppFence);
/*
每个围栏对象都维护着一个UINT64类型的值,此为用于标识围栏点的整数,一开始时,将此值设置为0,每当需要标记一个新的围栏点时就将它加一
第二个参数是用于生成指定围栏的flags
第三个参数是围栏接口D3D12Fence的GUID
第四个参数是返回的指向围栏接口的指针
*/
typedef enum D3D12_FENCE_FLAGS {
D3D12_FENCE_FLAG_NONE = 0,//未指定任何选项
D3D12_FENCE_FLAG_SHARED = 0x1,//围栏是共享的
D3D12_FENCE_FLAG_SHARED_CROSS_ADAPTER = 0x2,//围栏与另一个GPU适配器共享
D3D12_FENCE_FLAG_NON_MONITORED = 0x4//围栏属于非受监视类型,仅当适配器不支持受监视围栏或与不支持受监视围栏的适配器共享围栏时,才应该使用不受监视的围栏
} ;
示例代码:
UINT64 mCurrentFence=0;
void D3DApp::FlushCommandQueue()
{
mCurrentFence++;//增加围栏值,接下来将命令标记到此围栏点
//向命令队列中添加一条用来设置新围栏点的命令
//由于这条命令需要交给GPU处理(GPU端修改围栏值),所以在GPU中处理完命令队列中此Signal()以前的所有命令之前它并不会设置新的围栏点
//注:ID3D12CommandQueue::Signal在GPU端设置围栏点,而ID3D12Fence::Signal在CPU端设置围栏点
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),mCurrentFence));
//在CPU端等待GPU,直到后者执行完这个围栏点之前的所有命令
if(mFence->GetCompletedValue()<mCurrentFence)
{
HANDLE eventHandle=CreateEventEx(nullptr,false,false,EVENT_ALL_ACCESS);
//若GPI命中当前围栏(即执行到Signal命令,修改了围栏值),则激发预定事件
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence,eventHandle));
//等待GPU命中围栏,激发事件
WaitForSingleObject(eventHandle,INFINITE);
CloseHandle(eventHandle);
}
}
不仅可以在渲染每一帧时刷新一次命令队列,也可以在GPU初始化命令前进行刷新,也可以用这种方法确定命令队列执行完毕以进行Reset操作,所以在几乎所有时间都可以用这个方法,但因为这种方法浪费CPU资源,以后还会讲一些更好用的同步方式
资源转换
为了实现常见的渲染效果,一般对资源R进行先写后读的操作,但当对资源的写操作还未完成时就进行读操作,就会导致资源冒险,为了解决这个问题,D3D提供了一组相关状态
资源在创建开始时就会处于一种默认状态,该默认状态一直持续到应用程序通过D3D对其进行转换操作为另外一种状态为止。
通过命令列表设置转换资源屏障数组即可指定资源的转换。在代码中,资源屏障用D3D12_RESOURCE_BARRIER结构体表示,下列辅助函数将根据用户给出的资源和指定的前后转换状态
返回对应的转换资源屏障描述:
struct CD3DX12_RESOURCE_BARRIER:public D3D12_RESOURCE_BARRIER
{
//...
static inline CD3DX12_RESOURCE_BARRIER Translation(
_In_ID3D12Rresource* pResource,
D3D12_RESOURCE_STATES stateBefore,
D3D12_RESOURCE_STATES stateAfter,
UINT subresource=D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
D3D12_RESOURCE_BARRIER_FLAG flags=D3D12_RESOURCE_BARRIER_FLAG_NONE)
{
CD3DX12_RESOURCES_BARRIER result;
ZeroMemory(&result,sizeof(result));
D3D12_RESOURCE_BARRIER &barrier=result;
result.Type=D3D12_RESOURCE_BARRIER_TYPE_TYPE_TRANSLATION;
result.Flags=flags;
barrier.Translation.pResource=pResource;
barrier.Translation.StateBefore=stateBefore;
barrier.Translation.StateAfter=stateAfter;
barrier.Translation.Subresource=subresource;
return result;
}
//...
}
CD3DX12_RESOURCE_BARRIER继承自D3D12_RESOURCE_BARRIER结构体并添加了一些方法,D3D12中有许多结构体都有其对应的扩展辅助结构体,可以从微软的官方网站下载d3dx12.h头文件来使用这些功能性更强的结构体
此辅助函数的用法如下:
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Translation(
CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET));
这段代码将以图片形式显示在屏幕中的纹理,从呈现状态转换为渲染状态,可以将此资源屏障转换看作是一条告知GPU某资源状态正在转换的命令,所以在后续的命令中,GPU就会采取必要的措施以防资源冒险
命令与多线程
D3D12的设计目标是为用户提供一个高效的多线程环境,命令列表也是一种发挥D3D多线程优势的途径,对于内含很多物体的大场景而言,仅构建一个命令列表来绘制整个场景会占用不少CPU时间,因此可以并行构建多个命令列表的思路
注意:
-
命令列表并非自由线程对象,也就是说,多线程既不能同时共享相同的命令列表也不能同时调用同一命令列表的方法,所以每个命令列表通常只能使用自己的命令列表
-
命令分配器也不是线程自由的对象,多线程不能同时共享一个命令分配器也不能同时调用同一命令分配器的方法,所以,每个线程只使用自己的命令分配器
-
命令队列是线程自由对象,多线程可以同时访问同一个命令队列也能同时调用其方法,也能同时提交命令列表
-
出于性能原因,应用程序必须在初始化期间指出用于并行记录命令的命令列表最大数量
可以查阅SDK的D3D12Mutithreading示例学习并行生成命令列表
声明
大部分内容来自《DirectX12 3D游戏开发实战》,部分内容来自网络,侵删