Vulkan 01 簡介
這一章介紹Vulkan,然后看看怎么繪制第一個三角形。后面的章節(jié)會詳細(xì)介紹。
Vulkan的起源
和之前的圖形接口OpenGL一樣,Vulkan也是一個跨平臺的GPU接口。以前的接口的問題是它們局限于固定功能。編程人員必須用標(biāo)準(zhǔn)的格式提交頂點(diǎn)數(shù)據(jù),且只能靠GPU制造商提供的光照和著色選項。
隨著顯卡的發(fā)展,它們已經(jīng)能提供更多的可編程能力。在不推翻原有API構(gòu)架的基礎(chǔ)上,通過增補(bǔ)來提供新的API,結(jié)果是一個導(dǎo)致一個越來越不不理想的接口。最近幾年移動端GPU使用Tile Based Rendering可以降低功耗。舊的API的另一個問題是它們對多線程渲染支持不好。
Vulkan重新設(shè)計了一套圖形接口。編程人員可以清楚的指定GPU的工作,并且多個線程并行的創(chuàng)建和提交命令。Vulkan只使用一個專門的offline著色器編譯器,減少了各個廠商自行實現(xiàn)的on the fly編譯器的不一致性。最后,它把graphics functionality和compute functionality合并成為一套統(tǒng)一的API。
如何繪制一個三角形
簡要介紹vulkan如何初始化和繪制第一個三角形。
1 - 實例(Instance)和物理設(shè)備選擇
Vulkan程序始于VkInstance(),它創(chuàng)建一個vulkan實例。
實例創(chuàng)建后,就可以查詢機(jī)器上支持vulkan的硬件,并且選擇一個或多個VkPhysicalDevice來使用。你可以查詢?nèi)鏥Ram大小、設(shè)備能力,最后選擇你喜歡的設(shè)備,例如優(yōu)先選擇獨(dú)顯。
2 - 邏輯設(shè)備和queue family
決定選哪個設(shè)備后,你應(yīng)該創(chuàng)建一個VkDevice(邏輯設(shè)備),來代表那個設(shè)備。
你還需要指定要使用那個queue family。vulkan可以執(zhí)行的操作,如繪制命令命令、內(nèi)存操作,都是提交到VkQueue之后異步執(zhí)行的。圖形命令,計算命令,內(nèi)存?zhèn)鬏敳僮?,都有各自對?yīng)的隊列類型。顯卡可以只支持某些類型的隊列而不是全部,但是現(xiàn)在的顯卡基本都支持所有的隊列類型了。
3 - window surface和swap chain
除非你只想做離屏渲染(offscreen rendering),你都需要創(chuàng)建一個窗體來顯示渲染結(jié)果。
你可以用操作系統(tǒng)提供的API創(chuàng)建窗口,也可以用封裝后的庫例如GLFW和SDL。
我們將使用GLFW。
我們需要這兩個組件:window surface(VkSurfaceKHR)和 swap chain(VkSwapChainKHR)。
注意后綴KHR,它表示這些類型是vulkan的擴(kuò)展部分。因為vulkan api本身是和平臺無關(guān)的,所以我們需要使用標(biāo)準(zhǔn)的WSI(Window System Interface)擴(kuò)展來和窗口交互。Surface是一個跨平臺的對窗體的抽象,它在初始化時需要一個本地類型的窗口handle作參數(shù),例如windows的HWND。好消息是GLFW已經(jīng)幫我們處理了平臺相關(guān)的細(xì)節(jié)。
Swap chain是一系列的render target。它的基本目的是保證當(dāng)我們在渲染到一個緩存區(qū)時,這個緩沖區(qū)沒有被用來顯示。這很重要,這樣才能保住只有已經(jīng)渲染完了的圖像會被用來顯示。
當(dāng)我們渲染完一幀后,這個圖像被返還給swap chain等待顯示。Render target的數(shù)量和present條件取決于當(dāng)前設(shè)置的模式。通常的模式是雙緩沖(vsync)以及三層緩沖。
4 - image view和framebuffer
要在從swap chain得到一個image上繪制,你必須把它包裹進(jìn)VkImageView和VkFrameBuffer。Image view用來指定image的那個部分將被使用,framebuffer用來包裝用于渲染的顏色view、深度view和stencil view。因為swap chain里面可能有很多image,我們預(yù)先給它們各自都創(chuàng)建image view和framebuffer,然后在繪圖時選擇對應(yīng)的那個。
5 - render pass
Vulkan的render pass用來描述渲染操作中使用的image類型,它們會如何被使用,如何對待它們的內(nèi)容。在我們的第一個渲染三角形的程序中,我們將告訴vulkan我們會只使用一個image作為color target。
6 - graphics pipeline
創(chuàng)建一個VkPipeline對象,它代表顯示卡的那些可配置的狀態(tài),例如viewport size和深度緩沖的參數(shù)。創(chuàng)建一個VkShaderModule對象,來代表著色器。
和OpenGL最大的不同是,幾乎所有的可配置選項都必須預(yù)先設(shè)置。意思是,如果你需要切換到另一個著色器或者稍稍的改變頂點(diǎn)布局,你需要完全的重新創(chuàng)建graphics pipeline。這意味著你必須你將預(yù)先創(chuàng)建很多個VkPipeline的對象。只有少數(shù)一些基本的設(shè)置,如viewport size和clear color可以動態(tài)的改變,所有狀態(tài)都需要明確描述,例如沒有默認(rèn)的顏色混合模式。
好消息是,因為這樣做是ahead-of-time編譯而不是just-in-time編譯,所以驅(qū)動有更多的信息可以用于優(yōu)化,程序的性能會更高。
7 - command pool和command buffer
如前面提到的,vulkan的很多操作如繪圖命令,都是先提交到隊列。命令要先存到VkCommandBuffer中,然后才能提交。這些緩沖是使用VkCommandPool創(chuàng)建的,并且需要指定隊列類型。要繪制一個三角形,我們需要如下操作:
● 開始render pass
● 綁定graphics pipeline
● 繪制3個頂點(diǎn)
● 結(jié)束render pass
因為framebuffer中的圖像取決于swap chain給我們的image,因此我們需要為每個可能的image存一個command buffer,然后在繪制時選擇對應(yīng)的command buffer。另一個方法時在每一幀都存command buffer,但不是最有效的。
8 - 主循環(huán)
當(dāng)繪制命令包裹進(jìn)command buffer后,主循環(huán)就很直截了當(dāng)了。我們先從swap chain中獲得一個image(調(diào)用函數(shù)vkAcquireNextImageKHR)。之后我們可以選擇適合這個image的command buffer,并且用vkQueueSubmit提交命令。最后我們把image還給swap chain用于展示,用vkQueuePresentKHR函數(shù)。
提交的指令被異步的執(zhí)行。因此我們需要使用同步對象例如信號量(semaphore)來保證非沖突訪問。必須在image獲取成功后才能開始執(zhí)行command buffer,否則可能導(dǎo)致image正在被讀取用于展示,但程序卻企圖在上面繪制。另外,vkQueuePresentKHR調(diào)用需要等待渲染結(jié)束,因此我們需要第二個信號量,它在渲染結(jié)束后被產(chǎn)生信號。
Vulkan API的一些概念
所有vulkan的函數(shù),枚舉類和結(jié)構(gòu)體都定義在vulkan.h頭文件中。Vulkan SDK是由LunarG開發(fā)的。
函數(shù)帶有vk前綴,枚舉和結(jié)構(gòu)體類型有Vk前綴,枚舉類型的值有VK_前綴。
API大量使用結(jié)構(gòu)體來傳遞參數(shù)。
例如,對象創(chuàng)建基本上是如下形式:
VkXXXCreateInfo createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.foo = ...;
createInfo.bar = ...;
VkXXX object;
if(vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS)
{
? ? std::err << "failed to create object" << std::endl;
? ? return false;
}
Vulkan的很多結(jié)構(gòu)體需要在sType字段指定結(jié)構(gòu)體的類型。pNext字段可能指向一個擴(kuò)展結(jié)構(gòu)但是在本教程中將一直是nullptr。創(chuàng)建和銷毀對象的函數(shù),會有一個VkAllocationCallbacks的參數(shù)允許你使用自定義的內(nèi)存分配器,本教程中也全部使用nullptr。
幾乎所有的函數(shù)都返回要么VK_SUCCESS或者一個錯誤碼。
驗證層(Validation layers)
如前面提到的,vulkan被設(shè)計成高效的和低驅(qū)動負(fù)擔(dān)的。所以它之后很有限的錯誤檢查和調(diào)試能力的。如果程序?qū)懙牟粚Γ?jīng)常會直接崩潰而不是返回錯誤代碼,可能更糟糕的是,它好像在你的顯卡可以工作但是在別的顯卡上會完全失敗。
Vulkan允許你使用驗證層來做大量的檢查工作。驗證層是插入到API和顯示驅(qū)動間的代碼,用來做檢查工作,例如函數(shù)參數(shù)檢查,跟蹤內(nèi)存管理問題。好的是你可以在開發(fā)是啟用它,然后在發(fā)布時完全關(guān)閉它使程序沒有這些額外負(fù)擔(dān)。任何人都可以編寫自己的驗證層,但是LunarG的vulkan sdk提供了一套標(biāo)準(zhǔn)的驗證層。此外,你還需要注冊一個回調(diào)函數(shù)來接收這個層返回的調(diào)試消息。
接下來只差一步就可以開始寫代碼了,那就是配置開發(fā)環(huán)境。