Vulkan 教程--Overview

原文地址 Vulkan-tutorial 。


Origin of Vulkan


和其他圖形API一樣,Vulkan也被設計成跨平臺。但是以前的這些圖形API在設計時和當時的顯卡關係密切,只是提供了一些可配置的固定功能。編程人員不得不在顯卡廠商的憐憫之下,使用所謂標準格式的頂點數據來勉強進行光照( lighting )和着色( shading )操作。

隨着顯卡架構的成熟,它們開始提供越來越多的可編程功能,這些新功能都以某種方式集成到現有的API中。這就導致了不理想的抽象和編程時更多的猜測。也導致了遊戲爲了獲得更好的性能而更新驅動,當然,有時也是爲了自身的利益。因爲這些驅動的複雜性,應用開發者還必須解決不同廠商間的不兼容問題。比如Shader的格式。除了這些情況外,在過去的十年中,我們也看到具有強大顯卡的嵌入式移動設備。這些移動設備根據電量( energy  )和空間的需求(space requirements)具有不同的GPU架構。其中一個顯著地例子就是 tiled rendering ,通過給予編程人員更多的控制,來從改善的性能中受益。之前的API 的另一個限制就是不支持多線程,這在CPU端往往是造成瓶頸的重要原因。

Vulkan 根據現代顯卡的特點,從頭開始設計,從而解決了這些問題。它通過讓編程人員使用更多的API來明確自己的意圖,從而減輕驅動的負擔,並且支持多線程和並行的提交命令。它將Shader代碼轉換成字節碼(byte code format)解決了不同廠商間Shader不兼容的問題。最後一點,它承認了當代顯卡的計算能力,並將圖形( graphics )和計算( compute )功能集成到了一個API中。


What it takes to draw a triangle

讓我們來大致看一下Vulkan是如何一步一步來渲染一個三角形的。以下幾個概念在後續章節裏還會詳細介紹,這裏只是想讓你對各個概念之間的聯繫有個宏觀的瞭解。

Step 1 - Instance and physical device selection

Vulkan 通過創建VkInstance 引入API來開始整個Vulkan 應用。在創建Instance時,你可以配置你的應用和將來要使用的API擴展。Instance 創建之後,就可以獲取平臺上Vulkan所支持的硬件,然後從中選擇一個或多個VkPhysicalDevice來使用,每個的device ,你都可以獲取它的屬性(properties)和能力(capbilities),比如VRAM 大小, 然後選擇一個適合你需求的device。 比如,你想要一個專用顯卡(dedicated graphics cards) 。


Step 2 - Logical device and queue families

獲取你想要的硬件設備(hardware device)後,就可以通過 VkPhysicalDeviceFeatures來描述你所需要的顯卡特性,像多視圖渲染( multi viewport rendering )和使用64bit的float等,然後根據這些特性創建VkDevice( logical device )。當然,也可以告訴VkDevice你想要使用何種隊列。Vulkan中的大多數操作,如繪畫命令和內存操作,都要提交到VkQueue中,在VkQueue中異步執行這些命令。隊列從隊列家族(queue families)中分配,每一種隊列支持一組特定的命令或操作。比如,可能存在一些不同種類的隊列,它們分別支持圖形操作、計算操作、內存轉移操作。隊列的這種特點也可以成爲你你選擇VkPhysicalDevice 的依據。Vulkan可能會支持一些不具有圖形操作的顯卡,不過請放心,目前Vulkan支持的顯卡基本上都已支持你感興趣的各種操作。


Step 3 - Window surface and swap chain

除非你只想離線渲染( offline rendering ) ,否則就必須創建一個將渲染結果顯示到屏幕上的窗口(window)。 你可以使用本地平臺的API來創建window ,或者使用像GLFW和SDL這樣的庫。在這篇教程中我們選擇GLFW,這一點你將在後續的教程中看到。

我們還需要另外兩個組件才能對window進行渲染: 一個window surafce ( VkSurfaceKHR )和一個 swap chain( VkSwapChainKHR ), 注意KHR後綴表示這些對象是Vulkan的擴展。Vulkan是跨平臺的,這也是爲什麼我們要使用WSI(Window system interface )擴展 來和窗口管理器(Window manager)進行交互。Surface 是對window 的一個抽象,通常他需要window 的引用來創建,比如windows上的HWND ,幸運的是GLFW的內置函數能夠自動爲我們解決不同平臺間的差異。

Swap chain是渲染目標的一個集合,它最簡單的功能就是:保證正在渲染的image  和 現在顯示在屏幕上的image 是兩個不同的image。保證image渲染完畢後才能進行顯示十分重要。每次我們想要畫一個幀時,都必須從swap chain裏請求一個image 來渲染,繪畫完畢後,再將它返回到到swpa chain,以便在某個時間後顯示到屏幕上。渲染的目標數量以及渲染完畢後顯示到屏幕上的時機用present mode 來表示。常見的present mode有雙緩衝和三緩衝,我們將在創建swap chain時再詳細討論這個問題。


Step 4 - Image views and framebuffers

先使用VkImageView和VkFrameBuffer將image包裹起來,然後才能將內容畫到image上。imageView 引用一個image要被使用的特定部分,而framebuffer引用imageView ,把它當做color 、depth和stencil的目標使用。因爲swap chain裏可以有多個image ,所以我們先發制人:爲每一個image 創建一個imageView和framebuffer ,然後在繪畫階段選擇一個正確的來使用。


Step 5 - Render passes

Render pass描述了在渲染階段要使用的image類型、如何使用以及如何處理image的內容。在我們的示例三角形應用中,我們告訴Vulkan,要使用一個image 作爲color的目標,並且希望它在繪畫操作前被塗成純色。請注意,Render pass只是描述要使用的image類型,而framebuffer( 通過綁定image )纔是要使用的image實體。


Step 6 - Graphics pipeline

在Vulkan中Graphics Pipeline 通過創建VkPipeline對象來建立。它描述了一些顯卡不可編程部分的可配置狀態(configurable state ),比如viewport的大小和depth buffer操作等,以及用VkShaderModule表示的可編程部分。VkShaderModule對象用着色器的字節碼來創建。驅動需要知道哪些渲染目標將在pipeline中使用,而這些目標就是我們在Render pass中定義的image。

Vulkan和現存的其他圖形API最顯著地區別就是:幾乎所有不可編程部分的配置都要在pipeline創建前提前完成。這就意味着如果你想換一個着色器(shader)或者僅僅改變一些頂點的佈局(vertex layout) ,那麼你必須重新創建pipeline 。這也意味着你必須提前創建很多pipeline,來應對渲染過程中不同組合的配置。只有很少的一些配置你可以動態改變,比如viewport 的大小和celar 的顏色等。Pipeline中所有的配置狀態你必須顯示的進行定義,比如,顏色混合就沒有爲你提供默認的配置。

它給我們帶來的一個好處就如同提前編譯和當場編譯一樣,驅動將有更多優化的機會和對運行時性能做更多的預測。


Step 7 - Command pools and command buffers

之前也提到,Vulkan中的命令(原文是operation )必須提交到對應的隊列才能執行。這些命令首先要記錄到VkCommandBuffer中,然後才能提交的到隊列。這些commandBuffer來自於一個commandPool,而CommandPool關聯一種具有特定命令的隊列。

畫一個三角形,我們需要以下幾個步驟來記錄commandBuffer:

  1. 開始render pass
  2. 綁定graphics pipeline
  3. 畫三個頂點
  4. 結束render pass。
FrameBuffer中的image取決於swap chain返回給我們的是哪一個,所以我們必須爲每一個可能的image記錄一個commandBuffer,然後在繪畫階段選擇正確的那個來運行。另外一種方法是每一幀都記錄一個commandBuffer,但這種方法性能不高。

Step 8 - Main loop

現在繪畫命令已經放在CommandBuffer中, Main loop就變得非常簡單了。首先使用 vkAcquireNextImageKHR.從swap chain裏獲取一個image ,然後根據這個image選擇對應的commandBuffer ,使用vkQueueSubmit執行這個commandBuffer ,最後使用vkQueuePresentKHR將這個Image 返回到swap chain準備顯示。

隊列中的命令採用異步的方式來執行,因此我們必須採用像信號量(semaphore)這樣的同步對象,來確保程序執行的正確順序。繪畫操作必須在獲取image完成後才能進行,否則就會出現當前渲染和顯示共用一個image的情況。vkQueuePresentKHR必須等待渲染結束才能進行交替使用, 這就需要我們再使用一個semaphore來等待渲染結束。


Summary

通過上述大致的講解,想必你已經對畫三角形的過程有了基本的認識。然而我們真正的程序卻包含更多的步驟,像分配vertex buffer和uniform buffer 以及上傳紋理圖片等,我們將在後續的章節一一介紹它們。因爲vulkan具有相當陡峭的學習曲線,我們打算先從簡單入手。這裏我們將耍點小計倆,打算先把頂點座標硬編碼到Shader裏,而不是直接使用veretx buffer ,因爲管理vertex buffer 需要你先對command buffer 有一定了解。


總之,爲了畫這個三角形,我們需要:

  1. 創建VkInstance。
  2. 選擇一個顯卡(VkPhysicalDevice)
  3. 爲繪畫和顯示創建一個VkDevice和VkQueue。
  4. 創建一個window、window surface 和 swap chain。
  5. 用image view 包裹swap chain裏的image。
  6. 創建一個render pass ,用它來定義渲染目標和目標的用法。
  7. 爲render pass 創建一個frameBuffer。
  8. 創建graphics pipeline。
  9. 爲每一個可能的swap chain image 繪畫命令分配和記錄command buffer。
  10. 通過獲取的image 來draw frame,提交正確的繪畫command buffer,最後將繪畫結果(image)返回到swap chain。

API concepts

Coding conventions

Vulkan的所有函數、枚舉變量和結構體都定義在vulkan.h裏,而且包含在Vulkan SDK中 。函數以小寫的vk爲前綴,枚舉類型和結構體以Vk爲前綴,而枚舉值以VK爲前綴。Vulkan API嚴重依賴結構體作爲函數參數。例如,對象的創建將遵循如下方式:

<span style="font-size:18px;">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::cerr << "failed to create object" << std::endl;
    return false;
}</span>

在Vulkan中很多結構都要你定義它的類型sType。pNext 指向一個擴展的結構,在本教程中它總是nullptr。創建和銷燬對象的函數要求一個 VkAllocationCallbacks,我們也將它置位nullptr。

所有函數的返回值是一個 VkResult 類型的枚舉,它要麼是VK_SUCCESS表示成功,要麼是其他錯誤值。

Validation layers


(後面有一節專門講Validation layers, 這裏只提取了原文的主要意思)
之前也說過 Vulkan是高性能低驅動負擔的API,這也就意味着它的錯誤檢測十分有限。如果你做錯了什麼事,他就會直接crash掉,而不是返回一個錯誤碼。Vulakn允許你爲錯誤檢測添加擴展,這就是Validation layers。它像是嵌套在你方法調用裏的代碼片段一樣,跟蹤參數和內存安全。你可以編寫自己的Validation layers ,但Vulkan SDK 爲你提供了一些標準的Validation layers供你開發使用。所以其實它比OPengl 和Direct3D 更容易查找到錯誤。



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