最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

Vulkan 學(xué)習(xí)記錄

2023-07-02 23:30 作者:Zweitestock  | 我要投稿

本學(xué)習(xí)記錄完全基于?https://vulkan-tutorial.com/。可能有很多地方理解不當(dāng)或錯(cuò)誤,請(qǐng)注意。

為了方便理解,我對(duì)教程代碼做了簡單的模塊化,每一分支對(duì)應(yīng)一節(jié)內(nèi)容。代碼地址:https://github.com/Zwei-Reverberate/Zw_Vulkan_Learning。

1. Project Structure

Vulkan 是一個(gè)基于 GPU 的跨平臺(tái)圖形 API,完全按照現(xiàn)代圖形架構(gòu)設(shè)計(jì)。

Vulkan 繪制是一個(gè)比較復(fù)雜的過程,我們希望將繪制的各個(gè)步驟模塊化。

1. Vulkan Procedure

以繪制一個(gè)三角形為例,讓我們簡要了解一下 Vulkan 的繪制流程。

1. Instance and physical device selection

構(gòu)建一個(gè) Vulkan 應(yīng)用程序從創(chuàng)建一個(gè) VkInstance 來配置 Vulkan API 開始。而一個(gè) VkInstance 的創(chuàng)建需要指明應(yīng)用程序所要用到的 API extensions。創(chuàng)建 VkInstance 之后,就可以查詢 Vulkan 支持的硬件并從中選取一個(gè)或多個(gè) VkPhysicalDevice 進(jìn)行操作??梢酝ㄟ^查詢?cè)O(shè)備屬性如 VRAM 大小,選擇一個(gè)適合的 device。

2. Logical device and queue families

選擇好硬件設(shè)備之后,需要?jiǎng)?chuàng)建一個(gè) VkDevice (logical device) 并對(duì)它更加具體的指定所要使用的 VkPhysicalDeviceFeatures,如 multi viewport rendering 和 64 bit floats。

你還需要指定所想使用的 queue families,Vulkan 的大多數(shù)操作,如 draw commands 和 memory operations 都是將它們提交到 VkQueue 中異步執(zhí)行的。Queues 由 queue families 分配,每個(gè) queue family 支持其 queues 中的一組特定操作。例如對(duì)于 graphics, compute 和 memory transfer operations 可能對(duì)應(yīng)不同的 queue families。

Queue families 的可用性也可以用于區(qū)分 physical device,支持 Vulkan 的設(shè)備可能不提供任何圖形功能,但是現(xiàn)在支持 Vulkan 的所有顯卡通常都支持常用的隊(duì)列操作。

3. Window suface and swap chain

我們常常需要?jiǎng)?chuàng)建一個(gè)窗口來顯示渲染的圖像。我們可以使用 GLFW 或是 SDL 這樣的庫來完成這一任務(wù)。 我們至少需要兩個(gè)組件來渲染一個(gè)窗口:一個(gè) window surface(VkSurfaceKHR) 和一個(gè) swap chain (VkSwapchainKHR)。(KHR 后綴表示它們是 Vulkan extension 的一部分)。 Vulkan API 本身是平臺(tái)無關(guān)的,這也是我們?yōu)槭裁词褂脴?biāo)準(zhǔn)化的 WSI(Window System Interface) extension 來和 window manager 交互。Surface 是對(duì)要呈現(xiàn)的窗口的跨平臺(tái)抽象,通常通過提供一個(gè) native window handle 的引用來實(shí)例化,例如 Windows 上的 HWND。幸運(yùn)的是,GLFW 庫中有一個(gè)內(nèi)置的函數(shù)來處理平臺(tái)特定的細(xì)節(jié)。 Swap chain 是渲染目標(biāo)的集合。它的目的是確保我們當(dāng)前渲染的圖像和當(dāng)前屏幕呈現(xiàn)的圖像不同,這可以使顯示的圖像具有連續(xù)性。繪制每一幀時(shí),我們必須向 swap chain 請(qǐng)求要渲染的圖像。當(dāng)這一幀繪制完成時(shí),將圖像返回 swap chain 以便后續(xù)顯示在屏幕上。渲染目標(biāo)的數(shù)量以及將渲染好的圖像呈現(xiàn)在屏幕上的圖像的條件取決于當(dāng)前的呈現(xiàn)模式,常見的模式有雙緩沖 (double buffer, vssync) 和三緩沖 (triple buffering)。 某些平臺(tái)允許你直接渲染到顯示器而無需通過 VK_KHR_display 和 VK_KHR_swapchain extension 與任何 window manager 進(jìn)行交互。

4. Image views and framebuffers

為了繪制從 swap chain 獲取的圖像,我們還需要將其包裝為 VkImageView 和 VkFramebuffer。 一個(gè) image view 引用一個(gè)圖像的特定部分,一個(gè) framebuffer 引用要用于顏色,深度和模板目標(biāo)的 image views。Swapchain 中可能有許多不同的圖像,可以預(yù)先為每個(gè)圖像都創(chuàng)建好 Image view 和 framebuffer,然后在繪制時(shí)選擇對(duì)應(yīng)的 image view 或 framebuffer。

5. Render passes

Vulkan 中的 render passes 描述了渲染操作中的圖像類型,它們將如何使用以及它們的內(nèi)容應(yīng)該如何被處理。 例如,在我們的三角形繪制程序中,我們將告訴 Vulkan 我們將使用單個(gè)圖像作為顏色目標(biāo),并希望在繪圖操作之前將其清除為純色。Render pass 僅僅描述圖像的類型,VkFramebuffer 實(shí)際上將特定的圖像綁定到這些插槽。

6. Graphics pipeline

Vulkan 中的渲染管線是通過創(chuàng)建一個(gè) VkPipeline 對(duì)象來設(shè)定的。它描述了顯卡的可配置狀態(tài),如視口大小和深度緩沖操作以及使用 VkShaderModule 對(duì)象的可編程狀態(tài)。VkShaderModule 對(duì)象是通過 shader 字節(jié)碼創(chuàng)建的。驅(qū)動(dòng)程序還需要知道管線中將使用哪些渲染目標(biāo)。 與現(xiàn)有的 API 相比,Vulkan 的顯著特點(diǎn)之一就是渲染管線的幾乎所有配置都需要提前設(shè)置。這意味著你若是想切換到不同的著色器或是稍微改變一下頂點(diǎn)布局,那么你需要完全重新創(chuàng)建渲染管線,這樣做的效率很低。這就迫使你提前創(chuàng)建出所有需要的渲染管線,在需要時(shí)直接使用已創(chuàng)建好渲染管線。渲染管線只有很少一部分配置可以動(dòng)態(tài)修改,如視口大小和清除顏色。所有狀態(tài)也需要有明確描述。 這樣做的好處類似于預(yù)編譯相比于即時(shí)編譯,驅(qū)動(dòng)程序有更大的優(yōu)化空間,運(yùn)行時(shí)性能更加可預(yù)測(cè)。

7. Command pools and command buffers

如前所述,我們要執(zhí)行的許多操作(例如繪圖操作)需要提交到 queue。這些操作首先需要記錄到 VkCommandBuffer 中,然后才能提交。這些 command buffers 是從與特定 queue family 關(guān)聯(lián)的 VkCommandPool 分配的。例如,若要繪制一個(gè)三角形,我們需要將以下操作記錄到一個(gè) command buffer:

  • Begin the render pass

  • Bind the graphics pipeline

  • Draw 3 vertices

  • End the render pass

因?yàn)?framebuffer 中的圖像取決于 swap chain 給我們的圖像,所以我們需要為每個(gè)可能的圖像記錄一個(gè) command buffer,并在繪制時(shí)選擇正確的圖像。另一種方法是每幀再次記錄 command buffer,但是效率不高。

8. Main loop

現(xiàn)在繪制命令已經(jīng)被包裝進(jìn) command buffer 中,主循環(huán)變得非常簡單。 我們首先使用 vkAcquireNextImageKHR 從 swap chain 獲取圖像,然后我們可以為該圖像選擇適當(dāng)?shù)?command buffer 并使用 vkQueueSubmit 執(zhí)行它。最后,我們將圖像返回 swap chain,以使用 vkQueuePresentKHR 呈現(xiàn)到屏幕上。 提交到 queues 的操作是異步執(zhí)行的,因此必須使用同步對(duì)象(如信號(hào)量)來確保正確的執(zhí)行順序。執(zhí)行繪制 command buffer 必須設(shè)置為等待圖像采集完成,否則我們可能開始渲染正在被讀取并顯示到屏幕上的圖像。反過來,vkQueuePresentKHR 調(diào)用需要等待完成渲染,為此,需要使用發(fā)出第二個(gè)信號(hào)量以通知渲染結(jié)束。

9. Summary

簡而言之,繪制三角形的大致步驟是:

  • Create a VkInstance

  • Select a supported graphics card (VkphysicalDevice)

  • Create a VkDevice and VkQueue for drawing and presetation

  • Create a window, window surface and swap chain

  • Wrap the swap chain image into VkImageView

  • Create a render pass that specifies the render targets and usage

  • Create framebuffers for the render pass

  • Set up the graphics pipeline

  • Allocate and record a command buffer with the draw commands for every possible swap chain image

  • Draw frames by acquiring images, submitting the right draw command buffer and returning the images back to the swap chain

2. API concepts

1. Coding conventions

所有的 Vulkan 函數(shù),枚舉和結(jié)構(gòu)都被定義在 Vulkan SDK 的 vulkan.h 中。

Vulkan 中的許多結(jié)構(gòu)要求你使用 sType 成員明確指明結(jié)構(gòu)類型。pNext 結(jié)構(gòu)可以指向擴(kuò)展結(jié)構(gòu)。創(chuàng)建或銷毀對(duì)象的函數(shù)有一個(gè) VkAllocationCallbacks 參數(shù),允許你為驅(qū)動(dòng)程序的內(nèi)存使用自定義的分配器。

幾乎所有的函數(shù)都會(huì)返回一個(gè) VkResult,它要么是 VK_SUBESS,要么是一個(gè) error code。

2. Validation layers

如前所述,Vulkan 是為高性能和低驅(qū)動(dòng)程序開銷而設(shè)計(jì)的,因此默認(rèn)情況下它提供的錯(cuò)誤檢測(cè)和調(diào)試功能非常有限。驅(qū)動(dòng)程序會(huì)在發(fā)生錯(cuò)誤時(shí)直接崩潰,而不是返回一個(gè)錯(cuò)誤代碼。這可能導(dǎo)致對(duì)于某種顯卡可以工作,不會(huì)崩潰,但對(duì)于其它顯卡無法工作,驅(qū)動(dòng)程序崩潰。

Vulkan 允許你通過 validation layer 進(jìn)行一定的錯(cuò)誤檢查。驗(yàn)證層能幫開發(fā)者進(jìn)行參數(shù)驗(yàn)證(nullptr等)、內(nèi)存泄漏檢測(cè)、線程安全檢測(cè)、日志、Profiling,以及常見邏輯(runtime)錯(cuò)誤等,輔助開發(fā)者更好的 debug 不易發(fā)現(xiàn)的錯(cuò)誤和不規(guī)范的 API 調(diào)用。

2. Development environment

環(huán)境配置步驟:https://vulkan-tutorial.com/Development_environment

對(duì)應(yīng)分支:01_environment_configure

3. Set up

1. BaseCode

1. General structure

為了代碼的模塊化,我們特地將 GLFWwindow 單獨(dú)封裝為一個(gè)類,并在其中處理 GLFWwindow 的創(chuàng)建和銷毀工作,并提供一個(gè) get 接口,讓其他地方可以獲取其中封裝的 GLFWwindow* 指針。

建立一個(gè) VulkanApp 類負(fù)責(zé)整合所有的 vulkan 模塊以及 GLFWwindow。

2. Resource management

和 malloc 函數(shù)分配的內(nèi)存需要使用 free 釋放類似,使用 Vulkan API 創(chuàng)建的的對(duì)象也需要使用顯式地銷毀。不過,我們也可以選擇使用智能指針等手段進(jìn)行自動(dòng)資源管理。

Vulkan 對(duì)象可以直接通過類似 vkCreateXXXX 的函數(shù)創(chuàng)建,或是通過其他對(duì)象調(diào)用類似 vkAllocateXXXX 的函數(shù)創(chuàng)建。當(dāng)創(chuàng)建的對(duì)象不再使用時(shí),使用對(duì)應(yīng)的 vkDestroyXXXX ?或 vkFreeXXX 函數(shù)進(jìn)行清除操作。對(duì)于不同種類的對(duì)象這些函數(shù)的參數(shù)會(huì)有所不同,但是有一個(gè)參數(shù)是相同的:pAllocator。這是一個(gè)可選參數(shù),允許你通過這個(gè)參數(shù)來指定回調(diào)函數(shù)編寫自己的內(nèi)存分配器。

3. Integrating GLFW

我們使用 GLFW 創(chuàng)建窗口來顯示渲染結(jié)果,雖然 Vulkan 可以在沒有顯示窗口的情況下正常運(yùn)作。

對(duì)應(yīng)分支:02_base_code

2. Instance

1. Creating an instance

我們首先需要通過創(chuàng)建 instance 來初始化 Vulkan library。Instance 是應(yīng)用程序和 Vulkan library 之間的連接。創(chuàng)建 instance 涉及向驅(qū)動(dòng)程序指定有關(guān)你應(yīng)用程序的一些詳細(xì)信息。

為了代碼的模塊化,我們單獨(dú)將其封裝為一個(gè)類,并在其中處理它的創(chuàng)建和銷毀工作:

為了創(chuàng)建一個(gè) instance,我們填入一些應(yīng)用程序的相關(guān)信息:

如前所述,許多 Vulkan 中的結(jié)構(gòu)需要顯式地設(shè)定 sType 成員以指明結(jié)構(gòu)類型。此外,許多 Vulkan 結(jié)構(gòu)還有一個(gè) pNext 成員用來指明可能的拓展結(jié)構(gòu),現(xiàn)在我們并未使用,所以將其設(shè)為 nullptr。

Vulkan 中的很多信息不是通過函數(shù)參數(shù)傳遞而是通過結(jié)構(gòu)傳遞的。我們必須再填寫一個(gè)結(jié)構(gòu)來為創(chuàng)建 instance 提供足夠的信息。下面的這個(gè)結(jié)構(gòu)體是必須的,它告訴 Vulkan 的驅(qū)動(dòng)程序需要使用的全局?jǐn)U展和校驗(yàn)層。全局是指這里的設(shè)置對(duì)于整個(gè)應(yīng)用程序都有效,而不僅僅對(duì)一個(gè)設(shè)備有效:

接下來,我們需要指定需要的全局?jǐn)U展,如前所述,Vulkan 是平臺(tái)無關(guān)的 API,所以需要一個(gè)和窗口系統(tǒng)交互的擴(kuò)展。GLFW 庫包含了一個(gè)可以返回這一擴(kuò)展的函數(shù),我們可以直接使用它:

結(jié)構(gòu)的最后兩個(gè)成員變量用來指定全局的 validation layers:

createInfo.enabledLayerCount = 0;

我們現(xiàn)在有了 Vulkan 創(chuàng)建 instance 所需要的一切,可以直接使用 vkCreateInstance 來創(chuàng)建 instance::

可以看到,創(chuàng)建 Vulkan 對(duì)象的函數(shù)參數(shù)的一般形式是:

  • 一個(gè)包含了創(chuàng)建信息的結(jié)構(gòu)體指針

  • 一個(gè)自定義的 allocator 回調(diào)函數(shù),這里我們沒有使用自定義的 allocator,所以置 nullptr

  • 一個(gè)指向新對(duì)象 handle 的指針

如果正確執(zhí)行,那么創(chuàng)建的 instance 的 handle 將會(huì)存儲(chǔ)在 m_instance 中,返回一個(gè) VK_SUCESS,否則返回 error code。

2. Checking for extension support

關(guān)于 vkCreateInstance 一個(gè)可能的錯(cuò)誤是 VK_ERROR_EXTENSION_NOT_PRESENT。我們可以簡單地指定我們需要的擴(kuò)展,并在該錯(cuò)誤代碼返回時(shí)終止。這對(duì)于像窗口系統(tǒng)這樣必要的擴(kuò)展來說非常合適,但如果我們請(qǐng)求的擴(kuò)展是非必須的,有了很好,沒有的話,程序仍然可以運(yùn)行。這時(shí),我們?cè)撛趺醋瞿兀?/p>

為了在創(chuàng)建 instance 之前檢索支持的的擴(kuò)展列表,Vulkan 提供了 vkenumerateInstanceExtensionProperties 函數(shù)。通過它,我們可以獲取擴(kuò)展的個(gè)數(shù)以及擴(kuò)展的詳細(xì)信息。此外,它還允許我們指定 validation layer 來對(duì)擴(kuò)展進(jìn)行過濾,這里置為 nullptr。

我們首先需要知道擴(kuò)展的數(shù)量,以便分配合適的數(shù)組大小來存儲(chǔ)信息,可以通過下面的代碼來獲取擴(kuò)展的數(shù)量:

知道了擴(kuò)展的數(shù)量后,就可以分配數(shù)組來存儲(chǔ)擴(kuò)展信息:

我們可以查詢擴(kuò)展的詳情:

vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());

每個(gè) VkExtensionProperties 結(jié)構(gòu)都包含擴(kuò)展的名稱和版本信息:

3. Cleaning up

代碼分支:03_instance

3. Validation layers

1. What are validation layers?

Validation layers 本身并不在 Vulkan 的渲染流程之中,它只是用于 Vulkan 程序的校驗(yàn)和查錯(cuò)。

Vulkan 的 API 是以最小的驅(qū)動(dòng)開銷為理念設(shè)計(jì)的,所以默認(rèn)情況下其錯(cuò)誤檢查非常有限。即使像是枚舉值設(shè)置錯(cuò)誤或是將空指針傳遞給所需參數(shù)這樣簡單的錯(cuò)誤通常也不會(huì)顯式處理,只會(huì)導(dǎo)致程序崩潰或未定義行為。Vulkan 需要我們顯式地定義每一個(gè)操作,所以很容易犯一些小錯(cuò)誤,比如使用了一個(gè)新的 GPU 特性,卻忘記在 logic device 創(chuàng)建時(shí)請(qǐng)求這一特性。

然而,這并不意味著這些檢查不能添加到 API 中。Vulkan 引入了 validation layers 來解決。Validation layers 是可選的可以用來在 vulkan API 函數(shù)調(diào)用上進(jìn)行附加操作的組件。validation layers 中常見的操作是:

  • Checking the values of parameters against the specification to detect misuse

  • Tracking creation and destruction of objects to find resource leaks

  • Checking thread safety by tracking the threads that calls originate from

  • Logging every call and its parameters to the standard output

  • Tracing Vulkan calls for profiling and replaying

以下是一個(gè)在 diagnostics validation layer 中的函數(shù)實(shí)現(xiàn)示例:

這些 validation layers 可以任意包含你想要的調(diào)式功能??梢栽诔绦虻?debug 版本開啟它們,在 release 版本關(guān)閉它們。

Vulkan 庫本身沒有提供任何內(nèi)建的 validation layers, 但 LunarG 的 Vulkan SDK 提供了一個(gè) validation layer 的實(shí)現(xiàn)。該 validation layer 只能在安裝了 Vulkan SDK 的設(shè)備上使用。Vulkan 可以使用 instance 和 device specific 兩種不同的 validation layers。更加推薦使用 instance valition layer。

2. Using validation layers

Validation layers 需要通過指定名稱來啟用。所有的可用的標(biāo)準(zhǔn) validation 都綁定在 SDK 的一個(gè)稱為 VK_LAYER_KHRONOS_validation 的 layer 中。

我們將其名稱存儲(chǔ)為全局變量,并設(shè)定 bool 變量來控制其不同模式下是否啟用:

我們使用封裝一個(gè)類單獨(dú)管理 validation layers,并將其耦合進(jìn) instance。這樣做的理由是 validation layer 的大部分工作都是針對(duì) instance 的。

我們首先為 Validation 類添加一個(gè)靜態(tài)成員函數(shù) checkValidationLayerSupport 以檢查所有的 validation layers 是否可用:

然后,我們?cè)?instance ?的 create 函數(shù)中調(diào)用一次這個(gè)函數(shù):

現(xiàn)在在 debug 模式下運(yùn)行程序,確保沒有錯(cuò)誤發(fā)生。如果有,就要查看幫助文檔尋找錯(cuò)誤原因。

最后,修改 VkInstanceCreateInfo 結(jié)構(gòu)體信息以在 validation layers 啟用時(shí)包含 validation layer 的名稱。

如果 validation layers 檢查成功,vkCreateInstance 就不應(yīng)返回 VK_ERROR_LAYER_NOT_PRESENT 錯(cuò)誤。

3. Message callback

默認(rèn)情況下,validation layers 會(huì)將調(diào)試信息打印到標(biāo)準(zhǔn)輸出,但我們也可以通過在程序中提供顯式的回調(diào)來自己處理它們。

要在程序中設(shè)置回調(diào)以處理消息和相關(guān)細(xì)節(jié),我們必須使用 VK_EXT_debug_utils extension 設(shè)置回調(diào)函數(shù)來接受調(diào)試信息。

我們首先創(chuàng)建一個(gè)靜態(tài) getRequiredExtensions 函數(shù)根據(jù)是否啟用 validation layers 返回需要的 extensions 列表。

GLFW 指定的擴(kuò)展是必需的,但調(diào)式報(bào)告相關(guān)的 extensions 可根據(jù)條件添加。代碼中我們使用了等價(jià)于 “VK_EXT_debug_utils” 的 VK_EXT_DEBUG_UTILS_EXTENSION_NAME 宏用來避免打字時(shí)的手誤。

我們?cè)?instance 的 create 函數(shù)中加入此函數(shù)的調(diào)用:

運(yùn)行程序確保沒有收到 VK_ERROR_EXTENSION_NOT_PRESENT 錯(cuò)誤。

現(xiàn)在我們來完成接受調(diào)試信息的回調(diào)函數(shù)。同樣為 Validation 類加入一個(gè)靜態(tài)成員函數(shù),

第一個(gè)入?yún)⒅付讼⒓?jí)別,它可能是以下這些值:

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: 診斷信息

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: 資源創(chuàng)建之類的信息

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: 警告信息

  • VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: 不合法可能造成崩潰的操作信息

這些值經(jīng)過一定設(shè)計(jì),可以使用比較運(yùn)算符來過濾處理一定級(jí)別以上的調(diào)試信息:

第二個(gè)參數(shù) messageType 可以是下面這些值:

  • VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT: 發(fā)生了一些與規(guī)范和性能無關(guān)的事件

  • VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT: 出現(xiàn)了違反規(guī)范的情況或發(fā)生了一個(gè)可能的錯(cuò)誤

  • VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT: 進(jìn)行了可能影響 Vulkan 性能的行為

第三個(gè)參數(shù) pCallbackData 是一個(gè)指向 VkDebugUtilsMessengerCallbackDataEXT 的結(jié)構(gòu)體指針,這一結(jié)構(gòu)體包含了下面這些非常重要的成員:

  • pMessage: 一個(gè)以 null 結(jié)尾的包含調(diào)試信息的字符串

  • pObjects: 存儲(chǔ)有和消息相關(guān)的 Vulkan 對(duì)象句柄的數(shù)組

  • objectCount: 數(shù)組中的對(duì)象個(gè)數(shù)

最后一個(gè)參數(shù) pUserData 是一個(gè)指向我們?cè)O(shè)置回調(diào)函數(shù)時(shí)傳遞的數(shù)據(jù)的指針。

回調(diào)函數(shù)返回一個(gè) bool 值,用來指示是否應(yīng)該中止觸發(fā) validation layers 消息的 Vulkan 調(diào)用。如果回調(diào)函數(shù)返回 true,則調(diào)用中止并觸發(fā) VK_ERROR_VALIDATION_FAILED_EXT 錯(cuò)誤。通常,只在測(cè)試 validation layers 本身時(shí)返回 true,其余情況下,回調(diào)函數(shù)應(yīng)該返回 VK_FALSE。

定義完回調(diào)函數(shù), 接下來要做的就是設(shè)置 Vulkan 使用這一回調(diào)函數(shù)。我們需要一個(gè) VkDebugUtilsMessengerEXT 對(duì)象來存儲(chǔ)回調(diào)函數(shù)信息,然后將它提交給 Vulkan 完成回調(diào)函數(shù)的設(shè)置。我們將其設(shè)定為 VkValidation 的成員變量。

VkDebugUtilsMessengerEXT debugMessenger;

并為其提供相應(yīng)的 get 和 set 方法以便在外部對(duì)其進(jìn)行訪問。

我們?cè)?VkcoreInstance 類中添加 setupDebugMessenger() 函數(shù),并在最外層的 vulkanapp 類的 initCoreVulkan 函數(shù)中對(duì)其進(jìn)行調(diào)用:

現(xiàn)在,在 VkValidation 類中添加一個(gè) populateDebugMessengerCreateInfo() 函數(shù)用來填寫 VkDebugUtilsMessengerCreateInfoEXT 結(jié)構(gòu)的信息:

messageSeverity 域用來指定回調(diào)函數(shù)處理的消息級(jí)別。在這里,我們?cè)O(shè)置回調(diào)函數(shù)處理除了 VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 的所有級(jí)別的消息,這使得我們的回調(diào)函數(shù)可以接收到可能的問題信息,同時(shí)忽略掉冗長的一般調(diào)試信息。

messageType 域用來指定回調(diào)函數(shù)處理的消息類型。在這里,我們?cè)O(shè)置處理所有類型的消息。

pfnUserCallback 域是一個(gè)指向回調(diào)函數(shù)的指針。pUserData 是一個(gè)指向用戶自定義數(shù)據(jù)的指針。它是可選的,這個(gè)指針?biāo)傅牡刂窌?huì)被作為回調(diào)函數(shù)的參數(shù),用來向回調(diào)函數(shù)傳遞用戶數(shù)據(jù)。

還有其他許多方法可以配置 validation layers 消息和 debug callbacks。

VkDebugUtilsMessengerCreateInfoEXT 這個(gè)結(jié)構(gòu)體應(yīng)該被當(dāng)作參數(shù)傳遞給 vkCreateDebugUtilsMessengerEXT 函數(shù)以創(chuàng)建 VkDebugUtilsMessengerEXT 對(duì)象。不幸的是,由于這是一個(gè) extension functioin,所以不會(huì)自動(dòng)加載,我們必須手動(dòng)地使用 vkGetInstanceProcAddr 手動(dòng)地尋找它的地址。我們將創(chuàng)建我們自己的 proxy function 在后臺(tái)處理這個(gè)問題。我們?cè)?VkcoreInstance 中添加一個(gè)靜態(tài)成員函數(shù):

有了以上準(zhǔn)備,我們現(xiàn)在可以完成 setupDebugMessenger() 函數(shù):

VkDebugUtilsMessengerEXT 對(duì)象需要使用 vkDestroyDebugUtilsMessengerEXT 函數(shù)進(jìn)行清理。和 vkCreateDebugUtilsMessengerEXT 函數(shù)一樣,它也需要顯式地被加載,所以我們同樣在 VkcoreInstance 類中添加一個(gè)靜態(tài)成員函數(shù)來處理這件事情:

同時(shí),在 VkcoreInstance 的 destroy() 對(duì)其進(jìn)行調(diào)用:

4. Debugging instance creation and destruction

vkCreateDebugUtilsMessengerEXT 調(diào)用需要?jiǎng)?chuàng)建一個(gè)有效的 instance,并且在銷毀 instance 之前調(diào)用 vkDestroyDebugUtilsMessengerEXT。這樣導(dǎo)致的問題是 vkCreateInstance 和 vkDestroyInstance 之中的問題無法被調(diào)試。

有一種方式可以專門為這兩個(gè)函數(shù)創(chuàng)建一個(gè)單獨(dú)的 debug utils messager。需要在 VkInstanceCreateInfo 的 pNext 擴(kuò)展字段中簡單地傳遞一個(gè)指向 VkDebugUtilsMessengerCreateInfoEXT 結(jié)構(gòu)的指針:

debugCreateInfo 變量放在 if 語句之外,以確保它不會(huì)在 vkCreateInstance 調(diào)用之前被銷毀。使用此方法創(chuàng)建一個(gè)額外的 debug messager,它將在 vkCreateInstance 和 vkDestroyInstance 期間自動(dòng)使用,并在之后清理。

5. Testing

在 VkcoreInstance 的 destroy 中注釋下面的代碼:

//DestroyDebugUtilsMessengerEXT(m_instance, m_debugMessenger, nullptr);

運(yùn)行程序并退出,退出時(shí)命令行就會(huì)打印出 validation layer 的錯(cuò)誤信息。

(注意是要退出才能看到錯(cuò)誤信息,不然程序就一直在執(zhí)行 MainLoop)

代碼分支:04_validation

4. Physical devices and queue families

1. Selecting a physical device

通過一個(gè) VKInstance 初始化 Vulkan library 之后,我們需要在系統(tǒng)中尋找并選擇一張顯卡。可以選擇任意數(shù)量的顯卡并同時(shí)使用。在這里選擇滿足需求的第一張顯卡。

為此,我們封裝一個(gè) VkcorePhysicalDevice 類進(jìn)行管理:

并將其作為指針加入 VulkanApp 的成員變量。在 initVulkan() 函數(shù)中調(diào)用 pickPhysicalDevice() 函數(shù)。

2. Queue Families

幾乎 Vulkan 中的每個(gè)操作,都需要將命令提交到 Queue。不同類型的 Queue 分屬不同的 Queue Familes,并且每個(gè) Queue Family 只允許執(zhí)行特定的一部分指令,比如,可能存在只允許執(zhí)行計(jì)算相關(guān)指令的 Queue Family 或者只允許執(zhí)行內(nèi)存?zhèn)鬏斚嚓P(guān)指令的 Queue Family。

我們需要檢查設(shè)備支持哪些 Queue Family。同時(shí),我們需要找到我們所有需要的 Queue Family 來支持我們想要使用的所有指令。

我們建立一個(gè)結(jié)構(gòu)體 QueueFamilyIndices:

optional 是一個(gè)包裝器,在為其分配內(nèi)容之前,不包含任何值??梢允褂?has_value() 查詢它是否包含值。

我們添加一個(gè)新函數(shù) findQueueFamilies 來查找我們需要的 Queue Familes:

3. Base device suitability checks

為 VkcorePhysicalDevice 類添加一個(gè)函數(shù) isDeviceSuitable(),用來驗(yàn)證 Physical Device 是否合適:

然后據(jù)此,完成 pickPhysicalDevice() 函數(shù):

至此,我們已經(jīng)完成了 physical device 的查找。

代碼分支:05_physical_device

5. Logical device and queues

1. Introduction

選擇好了 physical device,我們需要設(shè)置一個(gè) logical device 與之交互。可以為同一 physical device 創(chuàng)建多個(gè) logical device。

我們同樣封裝一個(gè)類來管理:

并將其 create() 調(diào)用加入 initCoreVulkan() 函數(shù)中。

2. Specifying the queues to be created

Logical device 的創(chuàng)建同樣需要在 struct 指定一些細(xì)節(jié)。

VkDeviceQueueCreateInfo 描述了單個(gè) queue family 所需要的隊(duì)列數(shù)。現(xiàn)在我們只對(duì)具有圖形功能的隊(duì)列有興趣。

Vulkan 允許使用 0.0 到 1.0 之間的浮點(diǎn)數(shù)為 Queue 分配優(yōu)先級(jí)以影響命令緩沖區(qū)執(zhí)行的調(diào)度。即使只有一個(gè) Queue,這也是必須的:

3. Specifying used device features

VkPhysicalDeviceFeatures 指定了支持的一系列功能,比如幾何著色器。我們暫時(shí)不包含任何特殊的東西,但一旦開始使用 Vulkan 做更多有趣的事情時(shí),我們就會(huì)指定相應(yīng)的內(nèi)容??梢酝ㄟ^ vkGetPhysicalDeviceFeatures 函數(shù)查詢到指定的功能。

VkPhysicalDeviceFeatures deviceFeatures{};

4. Creating the logical device

有了前兩個(gè)結(jié)構(gòu),我們來填寫 VkDeviceCreateInfo 結(jié)構(gòu):

首先添加指向 Queue 創(chuàng)建信息和 device features 結(jié)構(gòu)的指針:

其余信息與 VkInstanceCreateInfo 相似,需要指定擴(kuò)展和 validation layers。不同之處在于這次是對(duì)于設(shè)備的:

然后,調(diào)用 vkCreateDevice 函數(shù)來實(shí)例化 logical device。參數(shù)是與之交互的 physical device,剛剛指定的 queue 及使用信息 ,可選的分配回調(diào)指針和指向用于存儲(chǔ) logical device 的 handle。

最后,設(shè)定一個(gè) destroy() 函數(shù),用于進(jìn)行相關(guān)的銷毀,并在 cleanUp() 中進(jìn)行調(diào)用:

5. Retrieving queue handles

Queues 和 logical device 一起創(chuàng)建,但是我們還沒有它們交互的 handle,所以我們?cè)?VkcoreLogicalDevice 類中添加 一個(gè)成員變量來存儲(chǔ)圖形 Queue 的 handle。

當(dāng) device 被銷毀時(shí),device queue 會(huì)被隱式清理,所以不需要進(jìn)行銷毀處理。

我們可以使用 vkGetDeviceQueue 函數(shù)來檢索每個(gè) queue family 的 queue handle。參數(shù)是 logical device,queue family,queue index,和指向存儲(chǔ) queue handle 的指針。因?yàn)槲覀冎皇菑倪@個(gè) family 中創(chuàng)建一個(gè) queue,所以簡單地使用索引 0。

vkGetDeviceQueue(m_device, indices.m_graphicsFamily.value(), 0, &m_graphicsQueue);

代碼分支:06_logical_device

4. Presentation

1. Window surface

因?yàn)?Vulkan 是一個(gè)平臺(tái)無關(guān)的 API,它不能直接與 Window 系統(tǒng)建立連接。我們要在 Vulkan 和 windows 系統(tǒng)之間建立連接用以將結(jié)果呈現(xiàn)在屏幕上。

我們需要使用 WSI(Window System Integration) 擴(kuò)展。這里我們使用的 VK_KHR_surface, 它公開一個(gè) VkSurfaceKHR 對(duì)象,該對(duì)象代表一種抽象類型的表面可呈現(xiàn)渲染圖像。我們使用 GLFW 來得到 VkSurfaceKHR 對(duì)象。

VK_KHR_surface 是一個(gè) instance 級(jí)別的擴(kuò)展,它包含在 glfwGetRequiredInstanceExtensions 獲取的擴(kuò)展列表中。該列表還包含了一些其他的 WSI 擴(kuò)展。

Window surface 需要在 instance 創(chuàng)建后立即創(chuàng)建。因?yàn)樗鼤?huì)影響到 physical device 的選擇。

1. Window surface creation

我們同樣封裝一個(gè)類來管理:

并將它的指針加入到 VulkanApp 類中,并用 glfw 提供的接口完成 surface 的創(chuàng)建函數(shù):

并在 initVulkan 函數(shù)中進(jìn)行調(diào)用。

同時(shí),我們需要對(duì)其進(jìn)行銷毀:

我們需要確保 surface 在 instance 之前進(jìn)行銷毀。

2. Querying for presentation support

雖然 Vulkan 可能支持 windows 系統(tǒng)集成,但并不意味著系統(tǒng)中的每個(gè)設(shè)備都支持它。因此我們需要擴(kuò)展 isDeviceSuitable 函數(shù)以確保設(shè)備可以呈現(xiàn)出 surface 的圖像。

因?yàn)槌尸F(xiàn)是對(duì)于 Queue 的功能,因此問題實(shí)際上是找到支持呈現(xiàn)我們創(chuàng)建的 surface 的 queue family。

支持圖形命令的 queue family 和支持演示的 queue family 實(shí)際上有可能不重疊,因此我們需要修改 QueueFamilyIndices,添加成員變量存儲(chǔ)呈現(xiàn) queue family 的索引。

std::optional<uint32_t> presentFamily;

接下來,修改 findQueueFamilies 函數(shù)以查找呈現(xiàn)相關(guān)的 queue family 并存儲(chǔ)其索引:

請(qǐng)注意,它們很可能最終成為同一個(gè) queue family,但在整個(gè)程序中,我們會(huì)將它們視為獨(dú)立的 queue,以實(shí)現(xiàn)統(tǒng)一的方法。不過,可以添加邏輯首選支持同一 queue 中的繪圖和呈現(xiàn)的 physical device,以提高性能。

3. Creating the presentation queue

接下來修改 logical device 的創(chuàng)建過程。我們需要在其中創(chuàng)建呈現(xiàn)(present) queue,并檢索對(duì)應(yīng)的 handle。

為此,我們?yōu)?VkcoreLogicalDevice 類添加一個(gè)成員變量:

VkQueue m_presentQueue;

接下來使用一個(gè) set 創(chuàng)建來自兩個(gè) queue family 的 queue:

如果 queue family 相同,則兩個(gè) handle 很可能具有相同的值。

代碼分支:07_window_surface

2. Swap chain

Vulkan 沒有“默認(rèn)幀緩沖區(qū)”的概念,因此我們需要一個(gè) infrastructure 來將存儲(chǔ)我們渲染的 buffers,然后才將它呈現(xiàn)到屏幕上。

這在 Vulkan 中稱為 swap chain,必須顯式創(chuàng)建。

Swap chain 的本質(zhì)是一個(gè)等待呈現(xiàn)在屏幕上的圖像 queue。目的使屏幕顯示和屏幕刷新率同步。

1. Checking for swap chain support

并非所有的顯卡都能直接將圖像呈現(xiàn)到屏幕上,例如為服務(wù)器設(shè)計(jì)的顯卡可能就沒有任何的顯示輸出。所以我們必須在啟用 VK_KHR_swapchain 擴(kuò)展之前校驗(yàn)是否支持。我們?cè)?isDeviceSuitable 中完成這項(xiàng)工作。

首先在 appenum 中聲明所需的擴(kuò)展列表,類似于要啟用 validation layers 的列表:

接著,在 VkcorePhysicalDevice 類中創(chuàng)建 checkDeviceExtensionSupport 函數(shù),并在 isDeviceSuitable 添加其調(diào)用:

這里選擇使用字符串來表示未確認(rèn)的所需擴(kuò)展。這樣我們就可以在枚舉可用擴(kuò)展的序列時(shí)輕松地勾選它們

2. Enabling device extensions

啟用 swap chain 需要 VK_KHR_swapchain 先啟用擴(kuò)展:

createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();

3. Querying details of swap chain support

僅僅查詢 swap chains 是否可用還不夠,因?yàn)樗赡芘c window surface 不兼容,因此我們還要查詢更詳細(xì)的信息。

我們需要檢查三種屬性:

  • 基本的 surface 功能(swap chain 中圖像的最小/最大數(shù)量,圖像的最小/最大寬度和高度)

  • surface 格式(像素格式,色彩空間)

  • 可用的顯示模式

類似于 findQueueFamilies,我們使用一個(gè)結(jié)構(gòu)體來傳遞這些信息:

然后在 isDeviceSuitable() 函數(shù)中調(diào)用它:


return indices.isComplete() && extensionsSupported && swapChainAdequate;

4. Choosing the right settings for the swap chain

如果滿足了上文 swapChainAdequate 的條件,就繼續(xù)為 swap chain 選擇正確的設(shè)置。這通過幾個(gè)函數(shù)來完成,確定以下三種類型的設(shè)置:

  • surface 格式(顏色深度)

  • 呈現(xiàn)模式(“交換”圖像到屏幕的條件)

  • 交換范圍(swap chain 中圖像的分辨率)

對(duì)于這些設(shè)置中的每一個(gè),我們都會(huì)在腦海中有一個(gè)理想值,如果它可用,我們將使用它,否則我們將創(chuàng)建一些邏輯來找到下一個(gè)最好的選擇。

我們首先定義出一個(gè)專門的類來管理 swapChain:

1. Surface format

添加一個(gè)成員函數(shù) chooseSwapSurfaceFormat(),入?yún)⑴c SwapChainSupportDetails 的 formats 相對(duì)應(yīng):

每個(gè) VVkSurfaceFormatKHR 對(duì)象都包含一個(gè) format 和 一個(gè) colorSpace 成員。

format 指定了 color 的 channels 和 types。例如 VK_FORMAT_B8G8R8A8_SRGB 的意思是以 B, G, R, A 通道的順序,每個(gè)通道存 8bit,總共存 32bit 的方式對(duì)應(yīng)每個(gè)像素。

color space 成員使用 VK_COLOR_SPACE_SRGB_NONLINEAR_KHR 標(biāo)志指示是否支持 SRGB 顏色空間。

對(duì)于 color space 來說,我們將使用 SRGB(如果可用),因?yàn)樗軠?zhǔn)確,且?guī)缀跏菆D像的標(biāo)準(zhǔn)色彩空間。對(duì)于 format 來說,常見的 SRGB 格式是 VK_FORMAT_B8G8R8A8_SRGB。

2. Presentation mode

Presentation mode 可以說是 swap chain 最重要的設(shè)置,因?yàn)樗砹似聊簧巷@示圖像的實(shí)際條件。Vulkan 中有四種可能的呈現(xiàn)模式:

  • VK_PRESENT_MODE_IMMEDIATE_KHR:應(yīng)用程序提交的圖像會(huì)立即傳輸?shù)狡聊簧?,這可能會(huì)導(dǎo)致撕裂

  • VK_PRESENT_MODE_FIFO_KHR:swap chain 是一個(gè)隊(duì)列,當(dāng)顯示刷新時(shí),顯示從隊(duì)列的前面獲取圖像,程序在隊(duì)列的后面插入渲染圖像。如果隊(duì)列已滿,則程序必須等待。這與現(xiàn)代游戲中的垂直同步最為相似。刷新顯示的時(shí)刻稱為“垂直空白”

  • VK_PRESENT_MODE_FIFO_RELAXED_KHR:如果應(yīng)用程序遲到并且隊(duì)列在最后一個(gè)垂直空白處為空,則此模式與之前的模式不同。而不是等待下一個(gè)垂直空白,圖像最終到達(dá)時(shí)立即傳輸。這可能會(huì)導(dǎo)致可見的撕裂

  • VK_PRESENT_MODE_MAILBOX_KHR:這是第二種模式的另一種變體。當(dāng)隊(duì)列已滿時(shí),不會(huì)阻塞應(yīng)用程序,而是將已經(jīng)排隊(duì)的圖像簡單地替換為較新的圖像。此模式可用于盡可能快地渲染幀,同時(shí)仍然避免撕裂,從而比標(biāo)準(zhǔn)垂直同步產(chǎn)生更少的延遲問題。這就是俗稱的“三重緩沖”,雖然單獨(dú)存在三個(gè)緩沖區(qū)并不一定意味著幀率被解鎖

創(chuàng)建一個(gè)成員函數(shù) chooseSwapPresentMode() 用于選擇 Presentation mode。如果有足夠的資源,我們優(yōu)先選擇 VK_PRESENT_MODE_MAILBOX_KHR。它允許我們渲染盡可能新的圖像,同時(shí)保持一個(gè)較低的延遲。


3. Swap extent

Swap extent 是 swap chain 圖像的分辨率,它幾乎總是完全等于我們正在繪制的窗口的分辨率。

我們使用 glfwGetFramebufferSize 查詢窗口像素的分辨率,然后再將其和最小和最大的圖像范圍進(jìn)行匹配。

添加一個(gè)成員函數(shù) chooseSwapExtent() 用以選擇 swap extent:

5. Creating the swap chain

有了以上輔助函數(shù)幫助我們選擇創(chuàng)建 swap chain 的信息,我們來完成 swap chain 的 create 函數(shù)。

在 create 函數(shù)的末尾,我們新增了三個(gè)成員變量 sm_swapChainImages, m_swapChainImageFormat, m_swapChainExtent 用于后續(xù)使用。

同時(shí),完成對(duì)應(yīng)的銷毀函數(shù):

代碼分支:08_swap_chain

3. Imge views

在上一節(jié)創(chuàng)建 swap chain 的過程中,我們保留了用 vector 存儲(chǔ)的 vkImage 對(duì)象。現(xiàn)在我們想在渲染管線中使用它。為此,我們需要?jiǎng)?chuàng)建 VkImageView 對(duì)象。VkImageView 描述了如何訪問圖像以及要訪問圖像的哪一部分。例如,是否應(yīng)將其視為沒有任何 mipmapping 級(jí)別的 2D 紋理深度紋理。

為此,我們同樣封裝一個(gè)類來管理 image views:

同時(shí),完成對(duì)應(yīng)的 destroy() 函數(shù):

代碼分支:09_image_views

5. Graphics pipeline basics

1. Introduction

Graphics pipeline 是將 mesh 的 vertices 和 textures 渲染成像素的一系列操作。

  • input assembler

    input assembler 從指定的緩沖區(qū)收集原始頂點(diǎn)數(shù)據(jù),并且還可以使用索引緩沖區(qū)來重復(fù)使用某些元素,避免同一頂點(diǎn)信息的重復(fù)存儲(chǔ)

  • vertex shader

    vertex shader 對(duì)于每個(gè)頂點(diǎn)執(zhí)行計(jì)算,通常會(huì)將頂點(diǎn)位置從模型坐標(biāo)系轉(zhuǎn)換到屏幕坐標(biāo)系

  • tessellation shader

    tessellation shader 使用某些規(guī)則細(xì)分幾何體以提高 mesh ?質(zhì)量

  • geometry shader

    geometry shader 在每個(gè)圖元(三角形、線、點(diǎn))上執(zhí)行計(jì)算??梢詠G棄圖元或者輸出更多的圖元。這和 tessellation shader 類似,但更加靈活。

  • rasterization stage

    rasterization stage 將圖元離散化為 fragments。任何在屏幕之外的 fragments 都會(huì)被丟棄,并且 vertex shader 輸出的屬性會(huì)在 fragments 間進(jìn)行插值

  • fragment shader

    fragment shader 在每個(gè)被保留的 fragment 上執(zhí)行計(jì)算,并確定將 fragment 寫入哪個(gè)緩沖區(qū)及使用哪些顏色和深度值

  • color blending stage

    color blending stage 應(yīng)用操作來混合映射到幀緩沖區(qū)中相同像素的不同 fragment。fragment 可以簡單地相互覆蓋、相加或根據(jù)透明度混合

Vulkan 種 graphics pipeline 幾乎是完全不可變的,如果想更改 shader,綁定不同的 framebuffers,或是更改 blend 函數(shù),則必須重新創(chuàng)建 graphics pipeline。這樣的缺點(diǎn)是需要?jiǎng)?chuàng)建許多 graphics pipeline 來表示在渲染操作中使用的所有不同狀態(tài)的組合。優(yōu)點(diǎn)是因?yàn)樵?pipeline 中所有操作都是預(yù)先知道的,因此驅(qū)動(dòng)程序可以更好地進(jìn)行優(yōu)化。

2. Shader modules

Vulkan 中著色器代碼必須以 SPIR-V 字節(jié)碼格式指定。而不是 GLSL 和 HLSL 等可以閱讀的語法。

不過我們?nèi)匀豢梢允褂?GLSL 書寫我們的著色器,然后在 Vulkan SDK 中使用 glslangValidator.exe 去把它編譯成字節(jié)碼。

1. Vertex Shader

Vertex shader 為每個(gè)傳入的頂點(diǎn)執(zhí)行計(jì)算。輸入的頂點(diǎn)屬性包括 world position, color, normal, texture coordinate 等,輸出的是裁剪坐標(biāo)以及需要傳遞給 fragment shader 的屬性。經(jīng)過 vertex shader 計(jì)算后,這些值將被傳遞給 rasterizer 進(jìn)行插值以產(chǎn)生平滑的過渡。

Vertex shader 輸出的裁剪坐標(biāo)是一個(gè)四維向量,我們將整個(gè)向量除以最后一個(gè)分量將其轉(zhuǎn)化為 NDC (normalized device coordinates) 坐標(biāo),它的范圍在 (-1, 1) 之間。

我們先將頂點(diǎn)坐標(biāo)硬編碼在 vertex shader 中:

2. Fragment shader

對(duì)于 fragment shader,我們同樣先簡單處理:

3. Compiling the shaders

現(xiàn)在,我們需要將 shader 轉(zhuǎn)換為 SPIR-V 字節(jié)碼。

編寫如下 bat 文件并執(zhí)行:

D:/Vulkan/Bin/glslc.exe triangle.vert -o trianglevert.spv
D:/Vulkan/Bin/glslc.exe triangle.frag -o trianglefrag.spv
pause

會(huì)在我們編寫 shader 的同一目錄下生成對(duì)應(yīng)的 spv 文件。

4. Manage graphics pipeline

后文的內(nèi)容都會(huì)基于 graphics pipeline 展開,因此,我們需要封裝一個(gè)類專門負(fù)責(zé)管理:

5. Loading a shader

對(duì)于 shader,我們同樣封裝一個(gè)類來進(jìn)行管理:

添加一個(gè)靜態(tài)成員函數(shù) realFile 用于讀取 shader 文件:

6. Creating shader modules

為 VkShader 類添加 create 和 createShaderModule 函數(shù):

我們現(xiàn)在有了我們的 shader 結(jié)構(gòu),并且以枚舉的形式區(qū)分了不同類型的 shader,現(xiàn)在需要在 VkGraphicsPipeline 中引入 shader 結(jié)構(gòu)。

我們添加兩個(gè)成員變量分別代表 vertex shader 和 fragment shader:

std::shared_ptr<VkShader> m_vertexShader;
std::shared_ptr<VkShader> m_fragmentSahder;

據(jù)此,我們可以先填寫一部分的 graphics pipeline 的創(chuàng)建函數(shù):

代碼分支:10_shader_modules

3. Fixed functions

較舊的圖形 API 為 graphics pipeline 的大部分階段提供默認(rèn)狀態(tài),但是在 Vulkan 中,必須明確設(shè)定大多數(shù) graphics pipeline 的狀態(tài)。因?yàn)樗鼘⒈缓姹旱揭粋€(gè)不可變的 pipeline 狀態(tài)對(duì)象中。

1. Dynamic state

雖然 pipeline 的大部分狀態(tài)需要烘焙到 pipeline state 中,但改變一小部分的狀態(tài)而不重建 pipeline 是可以做到的。例如視口的大小,線寬和 blend constants 這些狀態(tài)。如果想要?jiǎng)討B(tài)設(shè)定這些狀態(tài)并將保存在外面,需要填寫 VkPipelineDynamicStateCreateInfo 結(jié)構(gòu)體。這個(gè)過程仍在 VkGraphicsPipeline 的 create 函數(shù)中。

這種做法將使這些值的配置被忽略。我們可以并且必須在繪制的時(shí)候指定這些數(shù)據(jù)。對(duì)于 viewport 和 scissor state 等來說這樣的設(shè)置更加靈活,同時(shí)也更加復(fù)雜。

2. Vertex input

VkPipelineVertexInputStateCreateInfo 結(jié)構(gòu)描述了將傳遞給頂點(diǎn)著色器的頂點(diǎn)數(shù)據(jù)的格式。它大致以兩種方式描述這一點(diǎn):

  • Bindings:數(shù)據(jù)之間的間距和數(shù)據(jù)是按逐頂點(diǎn)的方式還是按逐實(shí)例的方式 進(jìn)行組織

  • Attribute descriptions:傳遞給頂點(diǎn)著色器的屬性類型,用于將屬性綁定到頂點(diǎn)著色器中的變量

不過我們現(xiàn)在是在 vertex shader 中對(duì)頂點(diǎn)數(shù)據(jù)進(jìn)行了硬編碼,所以在這里先進(jìn)行簡單處理:

3. Input assembly

VkPipelineInputAssemblyStateCreateInfo 結(jié)構(gòu)描述了兩件事:將從點(diǎn)點(diǎn)繪制什么樣的幾何圖形,以及 primitive restart should be enabled。

前者在 topology 成員中指定,可以具有如下值:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST: points from vertices

  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST: line from every 2 vertices without reuse

  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: the end vertex of every line is used as start vertex for the next line

  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: triangle from every 3 vertices without reuse

  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: the second and third vertex of every triangle are used as first two vertices of the next triangle

通常,頂點(diǎn)是按照索引順序從 vertex buffer 中加載的,但是使用 element buffer 的話就可以自己指定使用自己的索引。這樣做可以執(zhí)行優(yōu)化,比如重復(fù)使用頂點(diǎn)。如果將 primitiveRestartEnable 成員設(shè)置為 VK_TRUE,則可以 通過特殊索引中的 0xFFFF 或者 0xFFFFFFFF 分解拓?fù)淠J街械闹g和三角形。

我們準(zhǔn)備繪制三角形,所以使用以下定義:

4. Viewports and scissors

Viewport ?是 framebuffer 將被渲染輸出的區(qū)域。大多數(shù)情況下都是從 (0, 0) 到 (width, height)。

Swap chain 的圖像大小可能和窗口大小不同,swap chain 圖像在之后會(huì)被用作 frame buffer,所以我們?cè)谶@里設(shè)定 viewport 的大小為 swap chain 圖像的大小。

Viewport 定義了圖像到 frame buffer 的映射關(guān)系,裁剪矩形定義了哪一區(qū)域的像素實(shí)際被存儲(chǔ)在 frame buffer。任何位于裁剪矩形之外的像素都會(huì)在光柵化時(shí)被丟棄。

如上文所言,我們動(dòng)態(tài)地指定視口和裁剪矩形,這使得我們可以并且必須在繪制時(shí)指定二者,而無需在改變二者時(shí)重新建立 pipeline。

我們?cè)谏衔闹幸呀?jīng)為 viewport 和 scissor 開啟了動(dòng)態(tài) state,這里只需要在 pipeline 創(chuàng)建時(shí)指定其計(jì)數(shù):

5. Rasterizer

Rasterizer 從頂點(diǎn)著色器中獲取由頂點(diǎn)組成的幾何體,并將其轉(zhuǎn)換為要由片段著色器著色的片段。它還執(zhí)行 depth testing, face culling 和 the scissor test。并且可以設(shè)定是否使用線框渲染。

所有的這些都由 VkPipelineRasterizationStateCreateInfo 結(jié)構(gòu)配置。

6. Multisampling

VkPipelineMultisampleStateCreateInfo 結(jié)構(gòu)配置多重采樣,這是進(jìn)行抗鋸齒的方法之一。現(xiàn)在暫時(shí)禁用。

7. Depth and stencil testing

如果想要使用深度緩沖或模板緩沖,需要配置 VkPipelineDepthStencilStateCreateInfo?,F(xiàn)在暫時(shí)不需要。

8. Color blending

Fragment shader 返回顏色以后,需要將其與 framebuffer 中已有的顏色組合,這就是 color blending。有兩種方式可以實(shí)現(xiàn):

  • Mix the old and new value to produce a final color

  • Combine the old and new value using a bitwise operation

9. Pipeline layout

為了在 shader 中使用 uniform 變量(常常用于將變換矩陣傳遞給 shader),我們需要在管線創(chuàng)建期間通過 VkPipelineLayout 對(duì)象指定。但現(xiàn)在不使用它們。我們需要在 VkGraphicsPipeline 中將其存為成員變量,因?yàn)槲覀冃枰谙鄳?yīng)的 destroy 函數(shù)中銷毀它們:

代碼分支:11_fixed_functions

4. Render passes

1. Setup

在 pipeline 創(chuàng)建之前,我們需要告訴 Vulkan 渲染時(shí)將使用的 framebuffer attachments。我們需要指定將有多少顏色和深度緩沖區(qū),以及每個(gè)緩沖區(qū)如何進(jìn)行采樣和處理。所有這些信息都被封裝在 render pass 對(duì)象中。

我們封裝一個(gè)類用以專門管理 renderpass:

2. Attachment description

目前,我們只有一個(gè)代表 swap chain 圖像的 color buffer attachment:

loadOp 和 storeOp 確定在渲染之前如何處理 attachment 中的數(shù)據(jù),對(duì)于 loadOp 我們有以下選擇:

  • VK_ATTACHMENT_LOAD_OP_LOAD:保留 attachment 的現(xiàn)有內(nèi)容

  • VK_ATTACHMENT_LOAD_OP_CLEAR:使用一個(gè)常量來清除 attachment 的內(nèi)容

  • VK_ATTACHMENT_LOAD_OP_DONT_CARE:attachment 現(xiàn)有內(nèi)容未定義或者我們并不關(guān)心

我們使用第二種選擇,在繪制新的 frame 之前將 frame buffer 清除為黑色。

storeOp 有以下兩種選擇:

  • VK_ATTACHMENT_STORE_OP_STORE:將渲染內(nèi)容存儲(chǔ)在內(nèi)存中,稍后可以讀取、

  • VK_ATTACHMENT_STORE_OP_DONT_CARE:渲染后,不讀取 frame buffer 的內(nèi)容

我們想在屏幕上看到渲染的圖像,所以選擇第一種。

colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

loadOp 和 storeOp 的設(shè)置對(duì)于顏色緩沖和深度緩沖生效。stencilLoadOp / stencilStoreOp 對(duì)于模板緩沖生效,我們現(xiàn)在沒有使用模板緩沖,所以此設(shè)置其不關(guān)心即可:

colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

Vulkan 中的 texture 和 frame buffer 由特定像素格式的 VkImage 對(duì)象來表示,圖像的像素在內(nèi)存中的分布取決于我們對(duì)圖像進(jìn)行的操作。一些常見的圖像內(nèi)存布局是:

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:圖像被用作 color attachment

  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:圖像在 swap chain 中用于呈現(xiàn)

  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:圖像被用作復(fù)制操作的目標(biāo)

在這里我們對(duì)初始 layout 使用未定義 layout,意味著我們不關(guān)心以前的 layout。而渲染后使用 swap chain 進(jìn)行呈現(xiàn)。所以 final layout 使用 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR。

3. Subpasses and attachment references

單個(gè) render pass 可能包括多個(gè) subpass。Subpass 是后續(xù)渲染操作,依賴于先前 render pass 中 frame buffer 的內(nèi)容。例如,許多疊加的后期處理效果就是在上一次的處理結(jié)果上進(jìn)行的。如果將后續(xù)的一些操作組合到一個(gè) subpass 中,Vulkan 就能夠?qū)ζ鋬?yōu)化節(jié)省內(nèi)存。我們現(xiàn)在只使用單個(gè) subpass。

每個(gè) subpass 可以引用一個(gè)或者多個(gè) attachment,這些引用的 attachment 是通過 VkAttachmentReference 結(jié)構(gòu)指定的:

subPass 使用 VkSubpassDescription 結(jié)構(gòu)進(jìn)行描述:

4. Render pass

現(xiàn)在我們已經(jīng)設(shè)置好 attachment 和引用它的 subpass,現(xiàn)在可以來創(chuàng)建 render pass 本身。

填寫完 create 函數(shù),我們來完成 destroy 函數(shù):

代碼分支:12_renderpass

5. Conclusion

現(xiàn)在我們可以結(jié)合前面所有章節(jié)的結(jié)構(gòu)和對(duì)象來創(chuàng)建 pipeline。我們現(xiàn)在擁有的對(duì)象類型有:

  • Shader stages: the shader modules that define the functionality of the programmable stages of the graphics pipeline

  • Fixed-function state: all of the structures that define the fixed-function stages of the pipeline, like input assembly, rasterizer, viewport and color blending

  • Pipeline layout: the uniform and push values referenced by the shader that can be updated at draw time

  • Render pass: the attachments referenced by the pipeline stages and their usage

所有這些結(jié)合起來完全定義了 pipeline 的功能。我們現(xiàn)在往 VkcoreGraphicsPipeline 里面添加一個(gè) VkPipeline 成員變量, 并來最終完成 其 create() 函數(shù):

同時(shí)更新 destroy 函數(shù):

vkDestroyPipeline(pLogicalDevice->getDevice(), m_graphicsPipeline, nullptr);

代碼分支:13_graphics_pipeline

6. Drawing

1. Framebuffers

我們必須為 swap chains 中的所有圖像創(chuàng)建一個(gè) framebbuffer,并在繪制時(shí)使用與檢索到的 image 對(duì)應(yīng)的 framebuffer。

我們使用一個(gè)類用于專門管理 frame buffers:

并填寫完成相應(yīng)的 create 和 destroy 函數(shù):

代碼分支:14_frame_buffers

2. Command buffers

Vulkan 中的命令,如繪圖和內(nèi)存操作不是直接使用函數(shù)調(diào)用執(zhí)行的。我們必須在 command buffer 對(duì)象中記錄要執(zhí)行的所有操作。這樣做的好處是,當(dāng)我們準(zhǔn)備好告訴 Vulkan 我們想做什么時(shí),所有的命令都會(huì)一起提交,Vulkan 可以更有效地處理這些命令,因?yàn)樗鼈兌际峭瑫r(shí)可用的。此外,如果需要,可以在多個(gè)線程中進(jìn)行命令記錄。

1. Command pools

在創(chuàng)建 command buffers 之前,我們必須先創(chuàng)建 command pool。Command pool 用于管理 command buffers ?的內(nèi)存。

我們同樣封裝一個(gè)類用于專門管理 command pool:

完成其 create() 和 destroy() 函數(shù):

2. Command buffer allocation

我們現(xiàn)在來分配 command buffers。

同樣使用一個(gè)類來進(jìn)行管理:

3. Command buffer recording

添加一個(gè) recordCommandBuffer 函數(shù),它將我們想要執(zhí)行的命令寫入 ?command buffers。

我們總是通過調(diào)用 vkBeginCommandBuffer 來開始記錄 command buffer。

代碼分支:15_command_buffer

3. Rendering and presentation

1. Outline of a frame

在 VulkanApp 類中添加一個(gè) drawFrame() 函數(shù),并在 mainLoop() 函數(shù)中調(diào)用。

在 Vulkan 中渲染一幀通常由以下步驟組成:

  • 等待上一幀完成

  • 從 swap chain 獲取圖像

  • 記錄一個(gè)將場(chǎng)景繪制到圖像的 command buffer

  • 提交 command buffer

  • 呈現(xiàn) swap chain image

2. Synchronization

Vulkan 的核心設(shè)計(jì)理念是 GPU 上的執(zhí)行同步是明確的。操作的順序由我們使用的各種同步原語來定義。這些同步原語告訴驅(qū)動(dòng)程序我們所希望的事物運(yùn)行的順序。許多在 GPU 上執(zhí)行的 Vulkan API 是異步的,這些函數(shù)會(huì)在操作完成前返回。

所以我們需要顯式地排列一些事件的順序,因?yàn)樗鼈儼l(fā)生在 GPU 上:

  • 從 swap chain 獲取圖像

  • 執(zhí)行在獲取到的圖像上的繪制命令

  • 將圖像呈現(xiàn)到屏幕上顯示,并將其返回 swap chain

以上事件都是使用單個(gè)函數(shù)調(diào)用啟動(dòng)的,但都是異步執(zhí)行的。這些函數(shù)會(huì)在操作實(shí)際完成前返回,且執(zhí)行的順序也是未定義的。我們不希望這樣的結(jié)果,因?yàn)槊恳豁?xiàng)操作都依賴于前一項(xiàng)操作的完成。所以我們需要使用原語實(shí)現(xiàn)所期望的順序。

1. Semaphores

Semaphore 用于在隊(duì)列操作之間添加順序。隊(duì)列操作是指我們提交到隊(duì)列的工作,可以是在 command buffer 中,也可以是從我們稍后將看到的函數(shù)中。隊(duì)列的示例是圖形隊(duì)列和演示隊(duì)列。Semaphore 于在同一隊(duì)列內(nèi)和不同隊(duì)列之間對(duì)工作進(jìn)行排序。

Vulkan 中有兩種信號(hào)量,binary 和 timeline。我們只討論 binary。

一個(gè) semaphore 要么是 unsignaled 狀態(tài)的,要么是 signaled 狀態(tài)的。從一開始,它處于 unsignaled 狀態(tài)。

我們使用 semaphore 對(duì)隊(duì)列操作進(jìn)行排序的方式是在一個(gè)隊(duì)列操作中提供與另一個(gè)隊(duì)列信號(hào)量相等的 semaphore。前者作為 "signal",后者作為 "wait"。

例如,假設(shè)我們有 semaphore S 以及要按順序執(zhí)行的隊(duì)列操作 A 和 B,我們告訴 Vulkan 的是,操作 A 在執(zhí)行完成時(shí)將 semaphore S 發(fā)出 "signal",而操作 B 在開始執(zhí)行之前維持 semaphore S 的 "wait" 狀態(tài)。當(dāng)操作 A 完成時(shí),操作 B 收到信號(hào)才能開始執(zhí)行。

2. Fences

Fences 同樣是為了同步。它用于 CPU 上命令的執(zhí)行。如果 CPU 想知道 GPU 何時(shí)完成某些操作,我們可以使用 fences。

同 semaphores 類似,fences 要么是 unsignaled 狀態(tài)的,要么是 signaled 狀態(tài)的。每當(dāng)我們提交要執(zhí)行的工作時(shí),我們都可以為該工作附加一個(gè) fence,工作完成后,fence 置為 signaled 狀態(tài)。這樣我們就可以保證當(dāng)某項(xiàng)特定的工作完成之后,CPU 能夠獲知。

上面的意思是 A 完成之后,CPU 才能繼續(xù)執(zhí)行。但一般來說,我們并不想讓 CPU 置于閑置狀態(tài)。

總的來說,semaphores 用于指定 GPU 上操作的執(zhí)行順序,而 fences 用于保持 CPU 和 GPU 的同步。

3. What to choose

我們需要在兩個(gè)地方應(yīng)用同步:swap chain 操作和等待前一幀完成。對(duì)于前者,我們使用 semaphore,因?yàn)樗l(fā)生在 GPU 上。對(duì)于后者,我們使用 fence 使 CPU 等待,這樣我們每次就不會(huì)繪制超過一幀。

3. Creating the synchronization objects

我們需要一個(gè) semaphore 來表示已從 swap chain 獲取圖像并準(zhǔn)備好渲染,另一個(gè) semaphore 表示渲染已完成并可以進(jìn)行顯示。還需要一個(gè) fence 來確保每次僅渲染一幀。

我們使用一個(gè)類來管理同步工作:

同時(shí)完成其 create 和 destroy 函數(shù):

4. Draw frame

現(xiàn)在來完成 drawFrme() 函數(shù):

至此,我們已經(jīng)繪制出了一個(gè)完整的三角形。

代碼分支: 16_rendering_presentation



Vulkan 學(xué)習(xí)記錄的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國家法律
崇明县| 静宁县| 无极县| 大余县| 东明县| 乌海市| 淮北市| 奇台县| 将乐县| 昌吉市| 克山县| 青河县| 房山区| 通辽市| 亳州市| 高唐县| 桃江县| 桐梓县| 合阳县| 类乌齐县| 通河县| 来安县| 德兴市| 淅川县| 蕉岭县| 舞阳县| 淮阳县| 永安市| 泗水县| 宁波市| 抚松县| 尉犁县| 手游| 镇雄县| 灵山县| 明溪县| 抚顺县| 兴仁县| 宁阳县| 金坛市| 娄烦县|