DirectX12_基础知识

Direct3D 是底层图形应用程序编程接口。我们可以通过它对GPU进行控制和编程,从而在渲染虚拟3D场景时获得硬件加速的效果。12 比 11 更加偏于底层,更能压榨硬件性能。从今天开始我们就会开始进行DX12龙书的学习,主要还是为了能够熟悉它的接口,并可以使用它进行编程,这样为我们未来的渲染器打好基础。

首先我们来说说DX12中一些特性(相对于Vulkan或OpenGL中有或没有的一些概念)。

一、组件对象模型(Component Object Model, COM)

COM的全称是Component Object Model,它让DX称为独立的编程语言并且向下兼容。我们通常使用特定的函数或者其它COM接口来获取COM接口引用的指针。另外,COM对接口是引用计数的,使用完毕后,需要调用Release方法来释放它们。为了管理COM对象的生命周期,Windows Runtime Library提供了wrl.h来管理COM对象的智能指针。龙书中主要用到了ComPtr三个方法:

  • Get: 返回其包含的COM接口。
  • GetAddressOf: 返回其包含的COM接口的指针指向的地址。
  • Reset: 将ComPtr接口设置为空指针,并且减少其包含的COM接口的引用计数。

例如在Office文档中,可以放入一个视频,然后这个东西就会显示成一张静态的图片。我们双击这个图片,就会启用支持这个的软件对其进行播放。然后后台程序会启动,但窗口被隐藏。Office当中的一小块客户区域传递给这个后台程序,让其负责处理这块区域的绘制和用户输入。也就是说,Office程序的WM_PAINT事件处理当中,将Office窗口的整个客户区域分割为由自己绘制和由OLE绘制的部分。由OLE绘制的部分通过COM技术传递给后台应用进行绘制。

二、纹理格式

组成纹理的数据元素必须是 DXGI_FORMAT 枚举类型中定义的特定格式。例如:

DXGI_FORMAT_R32G32B32_FLOAT: 每个元素由 332 位浮点数分量构成
DXGI_FORMAT_R8G8B8A8_SNORM: 每个元素由 48 位无符号分量构成
DXGI_FORMAT_R32G32_UINT 每个元素由 232 位无符号整数构成
... 

纹理是渲染时信息的主要储存介质,而不仅限于存储图像数据。

三、交换链

每一帧图像的绘制都需要一个过程,为了使这个过程对于使用者不可见,我们需要先将内容全部绘制到后台缓冲区的纹理中。在一帧的内容全部绘制完成时,前后台缓冲区对调,新的前台缓冲区为用户呈现内容,新的后台缓冲区开始绘制下一帧。

前后台缓冲区构成交换链(IDXGISwapChain)。使用两个缓冲区的叫做双缓冲。而且可以使用更多的缓冲区,例如使用 3 个缓冲区的叫三重缓冲。

四、深度缓冲

深度缓冲区是一种储存特定像素深度信息的缓冲区。其值为 0.0~1.0 之间。0.0 表示平截头体中最近能看到的物体,1.0 表示平截头体最远能看到的物体。

深度测试用于对比写入后台缓冲区的同一像素的不同深度值。较小的值(物体靠前)会最终写入后台缓冲区。

深度缓冲区也是一种纹理,所以在 DXGI_FORMAT 枚举类型中也定义了它能使用的格式:

DXGI_FORMAT_D32_FLOAT_S8X24_UINT 32位深度缓冲 + 8 位无符号模板缓冲 + 24 位对齐
DXGI_FORMAT_D32_FLOAT
DXGI_FORMAT_D24_UNORM_S8_UINT
DXGI_FORMAT_D16_UNORM

五、资源与描述符

GPU想要对资源进行读写,就需要将该资源先绑定的渲染管线上。GPU在使用资源的时候大致需要知道两件事:

  • 要使用的是哪个资源?
  • 该资源充当什么角色?

资源描述符主要解决上述两件事。通过资源描述符引用要使用的资源很好理解。主要是资源描述符还需要解释资源充当的角色。因此我们可以把资源描述符分类,其中常用的有:

  • CBV/SRV/UAV 常量缓冲区视图(constant buffer view)/ 着色器资源视图(shader resource
    view)/ 无序访问视图(unordered access view)
  • 采样器(sampler)用于纹理贴图
  • RTV 渲染目标视图(render target view)
  • DSV 深度/模板视图(depth/stencil view)

描述符堆表示特定类型描述符的一块内存

六、多重采样技术

书中主要介绍了两种方式:

  • 超级采样(Super Sample Anti-Aliasing,SSAA)
  • 多重采样(MultiSample Anti-Aliasing,MSAA)

超级采样使用四倍于屏幕分辨率的后台缓冲区和深度缓冲区,然后在降采样的时候以四个像素为一组计算平均值。

采用4X多重采样,也是需要四倍于屏幕分辨率的后台缓冲区和深度缓冲区,但在降采样的时候直接计算像素中心的颜色,然后根据可视性和覆盖性将信息分享给子像素。

相当于超级采样每个子像素都要算一遍。而多重采样是每个像素算一遍然后把结果按条件分给子像素。

子像素是指在像素内部再次细分出用于采样的子像素。在上述条件下,四倍于屏幕分辨率,则屏幕的每个像素下就有4个用户采用的子像素。

七、命令队列&命令列表&命令分配器

每个GPU都至少维护一个命令队列(command queue)。CPU可以通过命令列表(command list)将命令提交到GPU的命令队列中。

命令分配器保存由CPU通过命令列表添加的命令,在提交到命令队列中时,队列也会引用分配器中的命令。

命令提交到命令队列中时并不会立即执行。而是添加到执行列表排号。

命令列表和分配器不是线程安全的,不过命令队列是线程安全的。所以可以在多个线程分别创建命令列表和分配器,而在提交命令时使用相同的命令队列。

八、CPU与GPU的同步问题

由于两个处理器并行工作,就必然会遇到需要同步的情况。此时我们需要运用围栏点(fence point)功能。

围栏点的逻辑简单来讲就是 CPU 向 GPU 执行路径上添加一个围栏点,然后 CPU 可以在需要和 GPU 同步的时候使用该围栏点的值等待 GPU。如果 GPU 执行到该围栏点值处会触发事件,从而达到同步的目的。

九、资源驻留

Direct3D 12 可以主动管理资源在显存中的驻留情况。可以将短时间不用的资源清出以节省缓存。

十、资源转换屏障(transition resource barrier)

假如我们要对某个资源进行先写后读两种操作。但是如果 GPU 在写还没开始时或未完成时就开始读资源明显就会出问题(资源冒险 resource hazard)。为了解决这个问题,Direct3D 引入了一种状态机制。在操作资源的时候更改资源的状态。GPU 会根据资源状态来调整行为避免资源冒险的产生。

十一、多线程

11.1 D3D11多线程渲染基本原理

在D3D11中,对于多线程渲染的支持主要可以概括为几个“小点”:
首先利用ID3D11Device::CreateDeferredContext方法创建一个(或多个)延迟渲染(Deferred Device Context)的设备上下文接口ID3D11DeviceContext;
接着利用这个Deferred Device Context接口调用诸如:IASetPrimitiveTopology、IASetInputLayout、RSSetState、OMSetBlendState 、OMSetDepthStencilState、DrawIndexed、DrawIndexedInstanced(最后两个为Draw Call)等等方法(可以统称为命令)像往常一样进行渲染调用;
所有的渲染调用都结束后,接着调用ID3D11DeviceContext::FinishCommandList方法,得到一个ID3D11CommandList接口;
最终通过即时设备接口(Immediate Device Context)的ID3D11DeviceContext::ExecuteCommandList方法执行这个Command List;
当所有的Command List都Execute完成之后,就可以调用IDXGISwapChain::Present方法呈现最终渲染画面了。
其中最最吸引人的就是Deferred Device Context的ID3D11DeviceContext接口可以有多个,并且每一个可以在不同的CPU线程(Windows线程)中分别记录命令(Command),然后提交给Immediate Device Context所对应的CPU线程(Windows线程)进行Execute(MSDN中称之为Queues commands),然后也在同样的线程中调用Present即可。这也就是D3D11多线程渲染的全部核心奥秘了。
当然这不是全貌,只是一个示意性的核心原理说明,但至少说明使用D3D11加入多线程渲染在编程原理和具体实现上其实并不复杂。在D3D11中命令列表中的命令(其实主要是CPU发送给GPU执行的命令)是被快速记录下来,而不是立即执行的(包括那些可怕的被称之为Draw Call的性能杀手,当然一定要记得升级你的显卡驱动程序),直到你调用ExecuteCommandList方法(调用即返回,不等待)才被GPU真正的执行,此时那些使用延迟渲染设备接口的CPU线程以及主渲染线程(不一定是进程的主线程,此处是指调用Immediate Device Context::ExecuteCommandList方法的线程)又可以去干别的事情了,比如继续下一帧的输入变换、碰撞检测、物理变换、动画矩阵调色板准备、光照准备等等,从而为记录形成新的命令列表做准备。而此时GPU就忙碌的开始执行渲染命令了。这也就是延迟渲染设备名字的真正含义。最终这就形成了CPU和GPU同时都在忙碌的高效渲染效果。
而在拥有D3D11多线程渲染之前,CPU和GPU的工作就好像两个人打台球一样,一个击球时,另一个只能在旁边观望(CPU线程在Draw Call上等待GPU完成渲染)。而D3D11引入的多线程渲染,不但让CPU和GPU可以同时处于忙碌状态,更让现代多核CPU及多GPU并行执行任务的能力得到根本上的解放。由此也可以看出来多线程渲染也是为解放生产力而生!

11.2 D3D12多线程渲染基本原理

而在D3D12中多线程渲染与D3D11中是异曲同工的:
首先在D3D12中利用命令队列(Command Queue 接口:ID3D12CommandQueue)代替了ID3D11DeviceContext接口,更准确的说是代替了Immediate Device Context的ID3D11DeviceContext接口,在D3D12中不论什么队列(Queues)都需要自己创建,而不是像D3D11中Immediate Device Context伴随ID3D11Device一同被创建。
在D3D12中Command Queue被进一步细分为D3D12_COMMAND_LIST_TYPE_DIRECT(直接命令队列), D3D12_COMMAND_LIST_TYPE_BUNDLE(捆绑包), D3D12_COMMAND_LIST_TYPE_COMPUTE(计算命令队列),D3D12_COMMAND_LIST_TYPE_COPY(复制命令队列), D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE(视频解码命令队列), D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS(视频处理命令队列)。
不论什么命令队列,它们本质上就是用来执行命令列表的,相当于D3D11中的Immediate Device Context,同样也是调用ExecuteCommandLists方法来执行命令列表。因此从其丰富的种类就可以感受到D3D12中命令队列本身就已经开始大大扩展了。
在D3D12中也有与D3D11中相类似的命令列表的概念,具体是用ID3D12GraphicsCommandList接口来表达,它就相当于D3D11中的Deferred Device Context。当然其内涵比在D3D11中要丰富的多,在D3D11中记录命令列表是由Deferred Device Context越庖代俎的,也就是我们按逻辑调用一堆ID3D11DeviceContext的方法,结束的时候使用FinishCommandList方法得到一个命令队列的接口ID3D11CommandList,而这个接口几乎没什么方法,仅仅是个“概念标志物”,或者直白的说就是仅仅代表GPU上的一个命令队列而已,其自身并没有什么方法可供调用。而在D3D12中ID3D12GraphicsCommandList却包含了几乎所有的可供放入命令队列中的命令方法(几乎全部是渲染相关的方法),并且在D3D12中命令队列本身是被创建的,使用的是ID3D12Device::CreateCommandList方法,有了这个接口以后你就可以在对应的其它CPU线程中按照渲染逻辑调用ID3D12GraphicsCommandList接口的方法生成一个命令列表(Command List)了,通常这个过程被称作“记录(或录制)”一个命令列表。同样录制只是说记录了你调用的顺序和使用的资源(CPU从内存传入显存),而不是立即执行这些方法,这些方法都会快速返回,因为是CPU调用这些方法,这个过程可以想象为你到餐馆去点菜,并不是你点一个菜,就做一个菜上一个菜再点下一个,而是生成一个菜单(Command Lists),统一提交到厨房(Queues Commands & Execute)。对应于不同的命令队列,命令列表也分为很多种类,基本上就是有多少种命令队列,就有多少种命令列表。
最后在命令列表(Command Lists)记录完成后,与D3D11中相同,提交到对应的命令队列(Command Queues)上去执行(ExecuteCommandLists)即可。
在命令队列执行命令列表的对应关系上,D3D12中基本的原则就是直接命令队列几乎可以执行所有种类的命令列表,而其它的命令队列只能执行对应种类的命令列表,如复制命令队列原则上只执行复制命令列表。
最终当所有的命令列表都执行结束后,主渲染线程(这时往往指的就是运行直接命令队列的CPU线程了)再调用Present方法将最终画面呈现出来即可。
需要注意的就是在D3D12中最终的Execute Command Lists操作也是立即返回,此时CPU线程可以进行其它的操作,而GPU就同时忙着执行各种渲染命令了,只有到所有都结束后才调用Present。 当然这很好理解,在所有渲染没有完成之前,后台缓冲区里还不是我们想要的最终画面。
由此可以看出,至少从原理上来说D3D12与D3D11多线程渲染框架基本是一致的。都是通过在不同的CPU线程中录制命令列表(Command Lists),最后再统一执行(Execute)的方式完成多线程渲染。并且都从根本上屏蔽了令人发指的Draw Call同步调用,而改为CPU和GPU完全异步执行的方式(并行!),从而在整体渲染效率和性能上获得巨大的提升。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值