01. The-Forge 入門教程 - 繪制三角形
學(xué)習(xí)一個(gè)新的圖形API或者中間件,第一件事一般就是繪制最簡(jiǎn)單的圖元,三角形。
The-Forge API基于dx12/vulkan設(shè)計(jì),如果曾經(jīng)有學(xué)習(xí)過 DX12/Vulkan,上手會(huì)非常容易。當(dāng)然如果沒有接觸過也沒關(guān)系,使用過The-Forge之后,返回去上手DX12/Vulkan也會(huì)變得非常簡(jiǎn)單。如果只接觸過openGL,那么這個(gè)教程對(duì)你可能會(huì)有些痛苦。openGL API的設(shè)計(jì)思路,和dx12/vulkan的差異還是太大了。
推薦一個(gè)vulkan教程: https://vulkan-tutorial.com/
新建窗口
本教程基于The-Forge第一個(gè)Demo?01_Transformations
?精簡(jiǎn)而來,編譯時(shí)選擇 DebugDx 版本。The-Forge開發(fā)時(shí)沒有使用CMake/premake/ninja等構(gòu)建工具,而是手工維護(hù)各個(gè)平臺(tái)的構(gòu)建,因此,并沒有use as lib的相關(guān)教程。對(duì)于對(duì)build,compile,link不熟悉的同學(xué),這個(gè)步驟可能要有些痛苦了。
新建一個(gè)空工程
添加 The-Forge 根目錄到 include path
添加?The-Forge\Examples_3\Unit_Tests\PC Visual Studio 2017\x64\DebugDx 到 library path
將下列庫(kù)添加為依賴:Xinput9_1_0.libws2_32.libgainputstatic.libRendererDX12.libOS.lib
添加Preprocessor Definition:?USE_MEMORY_TRACKING; _DEBUG ;_WINDOWS ;DIRECT3D12
做好這些準(zhǔn)備工作之后,就可以開始寫代碼了

有了這個(gè)基礎(chǔ)框架就可以運(yùn)行了,運(yùn)行之后應(yīng)該是直接閃退,因?yàn)檫€缺少一些dll文件,把DebugDx下的?amd_ags_x64.dll;WinPixEventRunTime.dll;dxcompiler.dll;dxil.dll 拷貝到exe所在路徑,然后再次運(yùn)行,就可以看到窗口了

最后的extern和main函數(shù),也可以用DEFINE_APPLICATION_MAIN代替。
程序生命周期
IApp.h中,有如下虛函數(shù)

查看WindowsBase.cpp中WindowsMain函數(shù)的實(shí)現(xiàn),可以知道程序的實(shí)際生命周期如下

創(chuàng)建Renderer
添加一個(gè)全局變量:?Renderer* pRenderer = nullptr;
在Init函數(shù)中添加:

在Exit函數(shù)中添加:removeRenderer(pRenderer);
Renderer對(duì)象的作用,類似dx12/vulkan中的device
創(chuàng)建GraphicsQueue
添加全局變量:Queue* ? pGraphicsQueue = nullptr;
在Init函數(shù)中添加:

在Exit函數(shù)中添加:removeQueue(pRenderer, pGraphicsQueue);

注意:在Init中創(chuàng)建對(duì)象的順序,和Exit中銷毀對(duì)象的順序相反,也就是說,后創(chuàng)建的對(duì)象要更先銷毀。
給只用過openGL的同學(xué)講一下,在dx12/vulkan/metal等現(xiàn)代圖形API中,device,fence,buffer,semaphore,descriptor等的管理,從驅(qū)動(dòng)移交到了開發(fā)者手中,意味著開發(fā)者要做更多的事情。力量越大,責(zé)任就越大。
現(xiàn)代圖形API一般支持多種Queue:Graphics,Compute,Cpoy(dx12)/Blit(Vulkan),不同的Queue意味的功能有所不同,比如Graphics Queue支持所有的Stage(VS,PS,DS,HS,GS,CS),而Compute Queue可能只支持CS一種Stage。實(shí)際開發(fā)時(shí)某些硬件運(yùn)行開發(fā)者創(chuàng)建多個(gè)Queue,比如創(chuàng)建一個(gè)Graphics Queue和一個(gè)Compute Queue,在渲染的同時(shí)做一些通用計(jì)算任務(wù),也就是所謂的asynccompute。
創(chuàng)建CmdPool,Cmd,F(xiàn)ence,Semaphore
添加全局變量:
const uint32_t gImageCount = 3;
CmdPool* pCmdPools[gImageCount] = { nullptr };
Cmd* ? ? pCmds[gImageCount] = { nullptr };
Fence* ? ? ? ?pRenderCompleteFences[gImageCount] = { nullptr };
Semaphore* ? ?pImageAcquiredSemaphore = nullptr;
Semaphore* ? ?pRenderCompleteSemaphores[gImageCount] = { nullptr };
在Init函數(shù)中添加:

在Exit函數(shù)中添加:

Cmd就是驅(qū)動(dòng)中的command buffer,實(shí)際渲染時(shí),可能要經(jīng)常創(chuàng)建和銷毀command buffer,因此設(shè)計(jì)了CmdPool,使用內(nèi)存池來循環(huán)使用Cmd buffer。CmdPool/Fence/Semaphore的數(shù)量要大于等于交換鏈緩沖數(shù)量,保證資源沒有沖突。
Fence和Semaphore則是用來控制狀態(tài)同步的重要方式
fence:Fence提供了一種粗粒度的,從Device向Host單向傳遞信息的機(jī)制。在Queue submit的時(shí)候,可以附加帶上一個(gè)Fence對(duì)象。之后Host就可以使用這個(gè)對(duì)象來查詢之前提交的狀態(tài)了
Semaphore:Semaphore用以同步不同的queue之間,或者同一個(gè)queue不同的submission之間的執(zhí)行順序。Semaphore只在Device上生效。也因此不同于fence,沒有重置或者等待semaphore的api
創(chuàng)建Shader,VertexBuffer,RootSignature
添加全局變量:
Shader* ? pTriangleShader = nullptr;
Buffer* ? pTriangleVertexBuffer = nullptr;
RootSignature* pRootSignature = nullptr;
shader
在Init函數(shù)中添加:

在Exit函數(shù)中添加:removeShader(pRenderer, pTriangleShader);
vertex shader

pixel shader

shader文件拷貝到<exe目錄>/shader/dx下
RootSignature
在Init函數(shù)中添加:

在Exit函數(shù)中添加:removeRootSignature(pRenderer, pRootSignature);
RootSignature是DX中的概念,用來告訴GPU該如何解析CPU傳過來的buffer

等價(jià)于vulkan中的VkDescriptorSetLayout
VertexBuffer
在Init函數(shù)中添加:

在Exit函數(shù)中添加:?removeResource(pTriangleVertexBuffer);
創(chuàng)建SwapChain
添加全局變量:SwapChain* ? ?pSwapChain = nullptr;
新建一個(gè)函數(shù)?addSwapChain :

在Load函數(shù)中添加:

在Unload函數(shù)中添加:removeSwapChain(pRenderer, pSwapChain);
注意,這次是Load和Unload函數(shù)
在Unload和Exit函數(shù)的第一行添加:waitQueueIdle(pGraphicsQueue);
SwapChain就是管理前后緩沖的對(duì)象。opengl的話,只需要調(diào)用 EGL/WGL/GLX...?提供的 swapBuffer 接口即可,但是在vulkan/dx12上,交換鏈的控制也轉(zhuǎn)交給了開發(fā)者。
創(chuàng)建Pipeline
添加全局變量:?Pipeline* pTrianglePipeline = nullptr;
在Load函數(shù)中添加:

在Unload函數(shù)中添加:removePipeline(pRenderer, pTrianglePipeline);
pipeline估計(jì)有dx使用經(jīng)驗(yàn)的同學(xué)會(huì)比較熟悉,但是只有openGL經(jīng)驗(yàn)的話可能就比較陌生了
Draw Triangle
經(jīng)過了漫長(zhǎng)的準(zhǔn)備工作,終于到了繪制階段了。
添加全局變量:uint32_t gFrameIndex = 0;
并在Draw函數(shù)中添加:gFrameIndex = (gFrameIndex + 1) % gImageCount;
整個(gè)程序并不需要在Update函數(shù)中實(shí)現(xiàn)功能
首先使用acquiescenceNextImage,獲取backbuffer的RT

等待上一幀完成

Record command buffer

command buffer的內(nèi)容在beginCmd和engCmd之間,可以分成bind RT,clear,draw triangle
接下來向Graphics queue提交剛剛生成的command buffer

queueSubmit會(huì)等acquireNextImage發(fā)出pImageAcquiredSemaphore信號(hào)后開始執(zhí)行,在queueSubmit運(yùn)行完成后,會(huì)發(fā)出renderCompleteSemaphore和renderCompleteFence信號(hào)
最后是present

present等待queueSubmit完成后發(fā)出的renderCompleteSemaphore信號(hào)
在OpenGL中,submit和present都在swapbuffer的過程中由驅(qū)動(dòng)隱式完成
現(xiàn)在,點(diǎn)擊運(yùn)行,就可以看到你的第一個(gè)三角形
