DX12是DirectX11 SDK的继任者。DirectX12相比于11作了很多改进,比如在11中的资源管理中,驱动必须持续追踪被渲染管线使用的资源的生命周期,但这在很多时候都是不必要的,于是DX12中将这部分交给了程序开发者来实现他们所需要的资源管理方式。
DirectX 12 组件
DirectX SDK事实上是一些了API的合集。其中用于硬件加速3D图形渲染的是Direct3D。其余的包括Direct2D,用于支持高质量文字渲染的DIREACTWRIATE,用于提供优化后的线性代数方法的DIRECTXMATH,音频支持的XAudio,以及用于支持输入的XINPUT。
DirectX 12 图形管线

蓝色方块表示不可编程阶段,绿色表示可编程阶段
第一个阶段是Input-Assembler (IA),用于把用户自定义的顶点和索引缓存收集起来并组装成图元,比如线,三角形,或多边形。
Vertex Shader (VS)用于把顶点数据从物体空间转变为摄像机的裁剪空间,同样可用于骨骼动画或者逐顶点光照。
Hull Shader (HS) 用于决定输入的patch有多少应该进入Tessellation阶段。
Tessellator Stage 根据tessellation factors 将patch中的图元划分成更小的图元。
The Domain Shader (DS) stage is an optional shader stage and it computes the final vertex attributes based on the output control points from the hull shader and the interpolation coordinates from the tesselator stage [14]. The input to the domain shader is a single output point from the tessellator stage and the output is the computed attributes of the tessellated primitive.
Geometry Shader (GS)是可选的,输入是单个图元(一个顶点则是代表一个点,三个顶点则是三角形,两个顶点则一条线),然后丢弃这个图元,把这个图元转换为其他类型的图元,或者生成新的图元。
Stream Output (SO) 是个可选的固定管线,可以把图元数据返回到GPU中,这在粒子效果中很有用。
Rasterizer Stage (RS) 同样是固定管线阶段,用于Culling,即把屏幕看不到东西都丢弃,也用于把逐顶点属性插值并传递给玄素着色器。
Pixel Shader (PS)
Output-Merger (OM) stage combines the various types of output data (pixel shader output values, depth values, and stencil information) together with the contents of the currently bound render targets to produce the final pipeline result.
GPU 同步
GPU同步以前是交给驱动自动操作,但在Directx12中,开发者必须手动操作。尤其是当管理资源时,如果有对这个资源的指令未执行完成,那么此时释放这个资源就不安全。
The Fence object is used to synchronize commands issued to the Command Queue. The fence stores a single value that indicates the last value that was used to signal the fence. Although it is possible to use the same fence object with multiple command queues, it is not reliable to ensure the proper synchronization of commands across command queues. Therefore, it is advised to create at least one fence object for each command queue. Multiple command queues can wait on a fence to reach a specific value, but the fence should only be allowed to be signaled from a single command queue. In addition to the fence object, the application must also track a fence value that is used to signal the fence. An example of performing CPU-GPU synchronization using fences will be shown in the following sections.
Command List 用于处理复制,计算(分发)或绘制命令。与DX11不同的时,DX12里的所有命令都是延迟的,这些命令在CommandQueue上执行后才会进入GPU并运行。
Command Queue 在DirectX12 里的接口非常简单,用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | method IsFenceComplete( _fenceValue ) return fence->GetCompletedValue() >= _fenceValue end method method WaitForFenceValue( _fenceValue ) if ( !IsFenceComplete( _fenceValue ) fence->SetEventOnCompletion( _fenceValue, fenceEvent ) WaitForEvent( fenceEvent ) end if end method method Signal _fenceValue <- AtomicIncrement( fenceValue ) commandQueue->Signal( fence, _fenceValue ) return _fenceValue end method method Render( frameID ) _commandList <- PopulateCommandList( frameID ) commandQueue->ExecuteCommandList( _commandList ) _nextFrameID <- Present() fenceValues[frameID] = Signal() WaitForFenceValue( fenceValues[_nextFrameID] ) frameID <- _nextFrameID end method |
IsFenceComplete : 检查Fence的最终值是否可以被读取WaitForFenceValue : 保持CPU线程直到Fence完成Signal : 将一个Fence值插入命令队列,The fence used to signal the command queue will have it's completed value set when that value is reached in the command queue.Render : 渲染一帧。除非某一帧的之前的Fence值拿到了,才能渲染那一帧
Render方法用于渲染场景,方法是使用所有必须的绘制(或计算指令)来填充指令列表。然后使用ExecuteCommandList方法来在指令队列上使用这个指令列表,ExecuteCommandList并不会阻塞调用线程,在指令列表中的指令在GPU上执行并返回Caller前,它就会持续下去。
Signal方法会将一个Fence值附着到指令列表最后。在其他所有指令在GPU上执行完成之后,FenceObject才会被赋予特定的值。对Signal的调用不会阻塞调用线程,而是仅返回值以等待命令列表中引用的任何(可写)GPU资源重新使用。
Present方法将会让渲染结果呈现到屏幕前。这个方法的返回值就是交换链中下一个需要被渲染的back buffer。当使用
- Copy: 用于复制资源数据的指令 (CPU -> GPU, GPU -> GPU, GPU -> CPU).
- Compute: 在第一条基础上还可以issue compute (dispatch) commands.
- Direct: 在第Compute基础上还可以 draw commands.
事实上,GPU可能每种指令队列类型都有一条或几条工作队列,而且我们无法直到GPU到底有几条以及它是什么类型的。如果你打算创建多队列,你必须为每一个指令队列都创建一个fence物体并追踪这些fence值。

上面这张图,在主线程中有一些指令。例如,第一帧是帧N,此时这些指令列正在指令队列上执行,执行完成后,队列就会被赋给值N。然后Fence将被赋予特定值。
在Signal右边,WaitForFenceValue指令正在等待之前一帧(帧N-1)完成。由于之前一帧中的指令队列已经没有指令了,那么其他指令将会继续执行下去而不会stalling掉CPU线程。
N+1帧在CPU上建立,并且在直接指令队列上执行。在CPU继续之前,指令队列必须完成对从帧N来的资源的使用。因此,CPU必须一直等待,等待N的到来,也就是说与这些资源有关的指令队列已经完成了。
当与帧N的资源有关的指令队列完成,帧N+2就可以被建立然后执行。如果队列还需要处理来制帧N+1的指令,那么CPU也将继续等待下去。
这个例子展示了一个典型的双缓冲场景。你也许觉得三缓冲会更快,但事实上,当CPU分配指令比指令被执行还要快时,CPU必须在某一些时间点等待这些指令被执行完成。
如果你添加了一条额外的队列,那么又要麻烦一些, you must be careful not to signal the second queue with a fence value that is larger than, but could be completed before, a fence value that was used on another queue using the same fence object. 这样做会让主队列在获取到Fence之前就让其他的队列获取到了fence值。

上面这张图中,CPU执行来自帧N的指令列,并把的DirectQueue的值赋为N。同时,CPU把一个Dispatch指令给了ComputeQueue,并且把这个队列值设为N+1。如果ComputeQueue先完成,那么值就是N+1,然后DirectQueue完成,值又得是N,但这是错误的,Fence值不能减少!
这个故事的意义在于说明每个指令队列要跟踪自己的FenceObject,而Fence值也只能给特定的FenceObject。安全起见,Fence值不能减少。你不必但是Fence值超出限制又变回0。就是指令队列每帧赋值100次,每秒300帧,而64位无符号整型的范围可以让这个游戏运行1950万年而不让Fence值溢出。
...中间跳过一部分...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Window handle. HWND g_hWnd; // Window rectangle (used to toggle fullscreen state). RECT g_WindowRect; // DirectX 12 Objects ComPtr<ID3D12Device2> g_Device; ComPtr<ID3D12CommandQueue> g_CommandQueue; ComPtr<IDXGISwapChain4> g_SwapChain; ComPtr<ID3D12Resource> g_BackBuffers[g_NumFrames]; ComPtr<ID3D12GraphicsCommandList> g_CommandList; ComPtr<ID3D12CommandAllocator> g_CommandAllocators[g_NumFrames]; ComPtr<ID3D12DescriptorHeap> g_RTVDescriptorHeap; UINT g_RTVDescriptorSize; UINT g_CurrentBackBufferIndex; |
g_hWnd用于保存将要用于渲染图像的窗口。
当游戏在全屏和非全屏状态切换时,g_WindowRect用于储存非全屏状态下的窗口的大小。
DX12 device物体存储在g_Device中,指令队列储存在g_CommandQueue中。
IDXGOSwapChain4接口定义了交换链。交换链将会使用一定数量的back buffer资源来创建。为了让这些back buffer资源能够被转换到正确的状态,这些back buffer的指针将会被放在g_BackBuffers数组中。尽管这些back buffer事实上只是纹理,但仍然会被ID3D12Resource接口所引用。
GPU指令首先会被记录进ID3D12GraphicsCommandList里。通常一个用于记录GPU指令的的指令列将会用一个单独的线程。由于Demo使用了主线程来记录所有的GPU指令,, only a single command list is defined. The
交换链的back buffer纹理被Render Target View描述。RTV描述了GPU存储中的纹理的位置,长宽,以及类型。The RTV is used to clear the back buffers of the render target. In a later tutorial, the RTV will be used to render geometry to the screen.
在之前版本的DirectX中,RTV每次创建一个,但在DirectX12中,RTV现在被存储在Descriptor heaps中,这个heaps可以被看做一组descirptors或是views。

DirectX 12中的View也叫做descriptor。与View相同,descriptor也描述了一个资源。由于交换链会包含很多back buffer纹理,因此一个descriptor将会描述每个纹理。
由于交换链的翻转模式,back buffer的索引可能是无序的,所以需要用
1 2 3 4 5 | // Synchronization objects ComPtr<ID3D12Fence> g_Fence; uint64_t g_FenceValue = 0; uint64_t g_FrameFenceValues[g_NumFrames] = {}; HANDLE g_FenceEvent; |
g_Fence变量用于存储之前提到的fence物体。
要给指令队列赋值的Fence值存储在g_FenceValue这个变量中。对于可能正在使用指令队列的渲染帧来说,Fence值用来标记那些需要追踪的渲染队列,来保证任何被指令队列调用的资源没有被重写。而g_FrameFenceValues数组变量就用于在每一帧中追踪这些要被赋给指令队列的Fence。
如果Fence物体的值在一帧结束后没有到达指定的值,那么CPU线程将会等待直到到达哪个值。这个g_FenceEvent变量是event物体的一个handle,用于接收Fence物体的值到达特定值的通知。
还有一些变量用于定义交换链的参数。
1 2 3 4 5 6 7 | // By default, enable V-Sync. // Can be toggled with the V key. bool g_VSync = true; bool g_TearingSupported = false; // By default, use windowed mode. // Can be toggled with the Alt+Enter or F11 bool g_Fullscreen = false; |
g_VSync用于控制交换链是否应该在下一个垂直刷新前把渲染好的图像展示的屏幕上。默认情况下交换链展示方法将在下一次垂直刷新前被阻塞。这会让应用程序的帧率下降到与显示器的帧率一样。但如果把这个设置为false,可能会导致画面撕裂,即Screen Tearing。
GPU和显示器都要提供对不同刷新率的支持。g_Fullscreen用于检查渲染窗口是否全屏。
演示的源代码经过组织,以最大程度地减少需要向前声明的功能的数量。 Windows消息回调过程是一个例外,它需要一个前向声明,以便可以使用回调函数来注册窗口类。
1 | LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); |