【筆記_學習DirectX12(1)】第一章——初始化DirectX 12

https://www.3dgep.com/learning-directx-12-1

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 裏的接口非常簡單,用ID3D12CommandQueue::ExecuteCommandLists 和 ID3D12CommandQueue::Signal 就行了,以下是僞代碼:

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
  1. IsFenceComplete: 檢查Fence的最終值是否可以被讀取
  2. WaitForFenceValue: 保持CPU線程直到Fence完成
  3. 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.
  4. Render: 渲染一幀。除非某一幀的之前的Fence值拿到了,才能渲染那一幀

Render方法用於渲染場景,方法是使用所有必須的繪製(或計算指令)來填充指令列表。然後使用ExecuteCommandList方法來在指令隊列上使用這個指令列表,ExecuteCommandList並不會阻塞調用線程,在指令列表中的指令在GPU上執行並返回Caller前,它就會持續下去。

Signal方法會將一個Fence值附着到指令列表最後。在其他所有指令在GPU上執行完成之後,FenceObject纔會被賦予特定的值。對Signal的調用不會阻塞調用線程,而是僅返回值以等待命令列表中引用的任何(可寫)GPU資源重新使用。

Present方法將會讓渲染結果呈現到屏幕前。這個方法的返回值就是交換鏈中下一個需要被渲染的back buffer。當使用DXGI_SWAP_EFFECT_FLIP_DISCARD翻轉模型時,Present方法同樣不會阻塞主線程。因此,除非圖像已經展現在屏幕上,否則前一幀的back-buffer的資源將不可用。

爲了防止資源在被呈現到屏幕前就被重寫,CPU線程需要等待前一幀的Fence值能夠被獲取。WaitForFenceValue就是專門幹這件事的。

理解每個指令隊列都要追蹤它自己的fence是非常重要的。DX12定義了三種不同的指令隊列類型:

  1. Copy: 用於複製資源數據的指令 (CPU -> GPU, GPU -> GPU, GPU -> CPU).
  2. Compute: 在第一條基礎上還可以issue compute (dispatch) commands.
  3. 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值溢出。

...中間跳過一部分...

// 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 g_CommandList variable is used to store the pointer to the ID3D12GraphicsCommandList

ID3D12CommandAllocator將用於backing 內存用於把GPU指令放進指令列。與指令列不同,指令分配器在執行完所有被記錄下的指令前不能重新使用,否則將會導致DebugLayer的COMMAND_ALLOCATOR_SYNC錯誤。而g_CommandAllocators數組變量用於存儲指令分配器的引用。至少每個渲染幀都要有一個指令分配器在工作,也就是交換鏈的每個back buffer都至少要有一個指令分配器。

交換鏈的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將會描述每個紋理。g_RTVDescriptorHeap將會存儲descriptor heap。g_RTVDescriptorSize 是RTV descriptor的大小,每個供應商的大小都不同,比如因特爾,英偉達,AMD。所以要在一開始就定義好。

由於交換鏈的翻轉模式,back buffer的索引可能是無序的,所以需要用g_CurrentBackBufferIndex來定義當前back buffer的索引。

下面一些變量將用於保證GPU同步的正確性。

// 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物體的值到達特定值的通知。

還有一些變量用於定義交換鏈的參數。

// 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消息回調過程是一個例外,它需要一個前向聲明,以便可以使用回調函數來註冊窗口類。

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章