前言:本教程為筆者依據教程https://docs.vulkan.net.cn/spec/latest/index.html#_about進行Vulkan學習并結合自己的理解整理的筆記,供大家學習和參考。
(注意:代碼僅為片段,非完整程序)
學習前提:
支持Vulkan的顯卡以及驅動程序(NVIDIA,AMD,Intel)
具備C++編程經驗(熟悉RAII,初始化列表)
支持C++的編譯器(Visual Studio 2017+,GCC 7+)往期回顧:
Vulkan入門教程 | 第一部分:Vulkan簡介
一、概述
本教程將逐步解釋如何創建一個基礎的Vulkan實例,這是所有Vulkan程序的起點。Vulkan實例是應用程序與Vulkan驅動程序之間的連接橋梁,它相當于一個"會話",告訴系統"我的程序要使用Vulkan了,請為我分配相應的資源"。傳統的OpenGL采用全局狀態機模式,而Vulkan采用顯式的對象模型。這意味著:
- 顯式控制:你必須明確告訴系統你要做什么
- 多實例支持:同一個程序可以創建多個Vulkan實例
- 更好的錯誤檢測:每個操作都有明確的返回值
實例負責以下關鍵功能:
-
初始化Vulkan系統?- 加載驅動和底層資源
-
管理擴展和驗證層?- 控制額外功能和調試工具
-
查詢可用硬件?- 發現系統中的物理設備(GPU)
-
平臺抽象?- 提供跨不同操作系統的一致接口
二、完整流程
1、添加頭文件
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>#include <iostream>
#include <stdexcept>
#include <cstdlib>
(1)?#define GLFW_INCLUDE_VULKAN
是GLFW庫的配置宏,它的核心作用是啟用GLFW對Vulkan的原生支持。當定義此宏后,GLFW會在內部進行以下關鍵操作:
-
自動包含Vulkan核心頭文件
<vulkan/vulkan.h>
-
激活GLFW中與Vulkan相關的函數實現
-
建立GLFW窗口系統與Vulkan之間的連接橋梁
-
處理不同平臺上Vulkan頭文件的包含差異
沒有這個宏定義,GLFW將無法提供Vulkan所需的窗口表面擴展,導致實例創建失敗。它是連接GLFW窗口系統和Vulkan API的關鍵開關。
(2)#include <GLFW/glfw3.h>是GLFW庫的主頭文件,提供跨平臺的窗口創建和事件處理功能。在Vulkan開發中,它具體負責:
-
創建和管理操作系統原生窗口
-
處理鍵盤、鼠標等輸入事件
-
提供與Vulkan兼容的窗口表面
-
封裝不同操作系統的原生窗口API(Windows的Win32、macOS的Cocoa、Linux的X11/Wayland)
-
實現
glfwGetRequiredInstanceExtensions()
函數,獲取平臺特定的Vulkan擴展
GLFW抽象了底層平臺的窗口創建細節,使開發者能夠用統一的API創建適用于Vulkan的渲染窗口,無需編寫平臺特定的代碼。
(3)#include <iostream>是C++標準輸入輸出庫頭文件,主要用于錯誤報告和控制臺輸出。
(4)#include <stdexcept
>是C++標準異常處理頭文件,提供標準異常類。這個頭文件實現了錯誤處理的標準化,使代碼能通過統一的異常處理機制管理Vulkan API可能返回的各種錯誤。
(5)#include <cstdlib
>是C標準庫的C++封裝頭文件,提供程序基本控制功能:
-
定義程序退出碼
EXIT_SUCCESS
和EXIT_FAILURE
-
當捕獲到異常時,返回
EXIT_FAILURE
表示異常終止 -
程序正常結束時返回
EXIT_SUCCESS
-
提供內存管理、隨機數生成等基礎功能
2、類封裝的整體架構設計
class HelloTriangleApplication {
public:void run() {initWindow();//1、初始化階段initVulkan();//2、資源準備階段mainLoop();//3、運行階段cleanup();//4、清理階段}
private:GLFWwindow* window;//GLFW窗口指針VkInstance instance;//Vulkan實例句柄,這是我們與Vulkan驅動通信的橋梁// 其他私有成員...
};
類封裝遵循RAII原則,資源獲取即初始化,離開作用域自動清理,確保了資源的正確分配和釋放,保證即使程序出現異常,析構函數也會被調用。
run()
函數體現了一個經典的應用程序生命周期模式:初始化—>資源準備—>運行主循環—>清理資源,每個階段都可以獨立測試。
3、初始化窗口系統(GLFW)
const uint32_t WIDTH = 800;//定義窗口的寬度
const uint32_t HEIGHT = 600;//定義窗口的高度//類的私有成員函數
void initWindow() {glfwInit();//初始化GLFW庫glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);//不使用OpenGLglfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);//禁用窗口大小調整//創建實際的窗口,參數分別是寬度、高度、窗口標題、顯示器和共享上下文window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
?這個函數只負責窗口相關的初始化,不涉及任何Vulkan代碼。其中窗口大小變化涉及交換鏈重建等復雜概念,初學時選擇禁用窗口大小調整。
4、創建Vulkan實例
//類的私有成員函數
void initVulkan() {createInstance();
}void createInstance() {// 配置應用程序信息VkApplicationInfo appInfo{};appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;appInfo.pApplicationName = "Hello Triangle";appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);appInfo.pEngineName = "No Engine";appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);appInfo.apiVersion = VK_API_VERSION_1_0;// 獲取GLFW所需的Vulkan擴展uint32_t glfwExtensionCount = 0;const char** glfwExtensions;glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);// 配置實例創建信息VkInstanceCreateInfo createInfo{};createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;createInfo.pApplicationInfo = &appInfo;createInfo.enabledExtensionCount = glfwExtensionCount;createInfo.ppEnabledExtensionNames = glfwExtensions;createInfo.enabledLayerCount = 0; // 禁用驗證層// 創建Vulkan實例if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {throw std::runtime_error("failed to create instance!");}
}
目前只在Vulkan初始化函數initVulkan()中調用了createInstance(),這是Vulkan初始化的第一步,在完成的Vulkan應用中,這里還會包括物理設備選擇、邏輯設備創建等步驟。createInstance() 是創建實例的核心內容,分為以下幾個部分:
//第一部分:應用程序信息結構體
VkApplicationInfo appInfo{};//描述應用程序的基本信息
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;//結構體類型標識符,Vulkan的所有結構體都需要設置這個字段
appInfo.pApplicationName = "Hello Triangle";//應用程序名稱,可以被驅動程序用于優化
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);//應用程序版本號,使用VK_MAKE_VERSION宏創建
appInfo.pEngineName = "No Engine";//使用的引擎名稱,這里寫"No Engine"表示沒有使用特定引擎
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);//引擎版本號
appInfo.apiVersion = VK_API_VERSION_1_0;//應用程序期望使用的Vulkan API版本
sType是結構體類型標識符,Vulkan的所有結構體都需要設置這個字段。在Vulkan的C語言環境中,由于缺乏C++的類型系統,每個結構體都必須通過sType字段來聲明自己的類型。當這個結構體稍后被傳遞給驅動程序時,驅動程序會首先檢查這個字段,確認接收到的確實是一個ApplicationInfo結構體,然后才會按照相應的格式來解析其他字段。
為什么需要sType?
- 類型安全:Vulkan使用C語言編寫,沒有C++的類型系統,sType提供運行時類型檢查
- 版本兼容性:不同版本的Vulkan可能有不同的結構體布局,sType幫助驅動程序正確解析
- 擴展支持:未來的擴展可以通過sType識別新的結構體類型
//第二部分:實例創建信息結構體
VkInstanceCreateInfo createInfo{};//用于創建Vulkan實例的信息結構體
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;//同樣需要設置結構體類型標識符
createInfo.pApplicationInfo = &appInfo;//指向前面創建的應用程序信息結構體
這個階段開始構建實例創建的主控制結構。VkInstanceCreateInfo是整個創建過程的核心數據結構,它將收集所有必要的信息,然后一次性傳遞給實例創建函數。
createInfo.pApplicationInfo = &appInfo;這行代碼建立了創建信息與應用程序信息之間的引用關系。通過指針傳遞而不是值拷貝,避免了大量數據的復制開銷。當稍后調用vkCreateInstance時,驅動程序會通過這個指針訪問應用程序信息。pApplicationInfo指針的作用:
- 信息傳遞:將應用程序信息傳遞給實例創建過程
- 內存效率:避免復制大量數據,使用指針引用
- 可選性:pApplicationInfo可以為nullptr,但提供信息有助于優化
//第三部分:GLFW擴展配置
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);//GLFW告訴我們需要哪些Vulkan擴展才能與窗口系統交互
createInfo.enabledExtensionCount = glfwExtensionCount;//指定要啟用的擴展數量
createInfo.ppEnabledExtensionNames = glfwExtensions;//指定要啟用的擴展名稱列表
前三行代碼執行了一個需求查詢過程。GLFW作為窗口管理庫,了解當前平臺需要哪些Vulkan擴展才能實現窗口與Vulkan的集成。glfwGetRequiredInstanceExtensions函數的執行過程是這樣的:GLFW內部會檢測當前的操作系統和窗口系統,然后返回相應的擴展名稱列表。在Windows上,它可能返回VK_KHR_surface和VK_KHR_win32_surface;在Linux上,可能返回VK_KHR_surface和VK_KHR_xlib_surface。
GLFW需要的典型擴展:
- VK_KHR_surface:跨平臺窗口表面抽象
- VK_KHR_win32_surface(Windows)或VK_KHR_xlib_surface(Linux):平臺特定的表面擴展
后兩行代碼將GLFW提供的擴展需求注冊到創建信息中。enabledExtensionCount告訴驅動程序有多少個擴展需要啟用,ppEnabledExtensionNames提供了擴展名稱的字符串數組。
當驅動程序稍后處理創建請求時,會逐一檢查這些擴展是否可用,如果任何一個擴展不存在或不兼容,整個創建過程就會失敗。
//第四部分:驗證層配置
createInfo.enabledLayerCount = 0;//設置為0表示不啟用任何驗證層,適用于發布版本
這行代碼明確設置不啟用任何驗證層。驗證層是Vulkan提供的調試和開發輔助工具,它們會攔截API調用進行額外的檢查和驗證。將計數設置為0表示這是一個發布版本的配置,追求最佳性能而不是調試便利性。下一篇會詳細介紹驗證層,驗證層的攔截機制為:
應用程序調用 → 驗證層 → 驅動程序
? ? ?↑ ? ? ? ? ? ? ? ?↓
? ?返回結果 ? ? ? ?檢查參數/狀態
//第五部分:創建實例
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {throw std::runtime_error("failed to create instance!");//如果不等于VK_SUCCESS,說明創建失敗,拋出異常
}
?這段代碼執行了整個過程的核心系統調用。vkCreateInstance函數接收三個參數:
第一個參數&createInfo將前面精心準備的所有配置信息傳遞給驅動程序。驅動程序會詳細檢查這個結構體中的每一個字段,驗證應用程序的需求是否可以滿足。
第二個參數nullptr表示使用默認的內存分配器。Vulkan允許應用程序提供自定義的內存分配策略,但在這個簡單示例中使用系統默認的分配器。
第三個參數&instance是一個輸出參數,成功創建后,新實例的句柄會被寫入這個變量中。
函數調用完成后,代碼立即檢查返回值,
可能的返回值及含義:
- VK_SUCCESS:創建成功
- VK_ERROR_OUT_OF_HOST_MEMORY:主機內存不足
- VK_ERROR_OUT_OF_DEVICE_MEMORY:設備內存不足
- VK_ERROR_INITIALIZATION_FAILED:初始化失敗
- VK_ERROR_LAYER_NOT_PRESENT:請求的驗證層不可用
- VK_ERROR_EXTENSION_NOT_PRESENT:請求的擴展不可用
- VK_ERROR_INCOMPATIBLE_DRIVER:驅動程序不兼容
5、主循環
void mainLoop() {while (!glfwWindowShouldClose(window)) {//檢查窗口是否應該關閉glfwPollEvents();//處理窗口事件(如鍵盤、鼠標輸入)}
}
?標準的GLFW事件循環。
6、資源清理
void cleanup() {vkDestroyInstance(instance, nullptr);//銷毀Vulkan實例,釋放相關資源glfwDestroyWindow(window);//銷毀GLFW窗口glfwTerminate();//終止GLFW庫
}
?7、主函數和異常處理
int main() {HelloTriangleApplication app;//對象創建//如果運行過程中拋出任何繼承自std::exception的異常,程序會捕獲并處理try {app.run();//應用程序的主要執行函數} catch (const std::exception& e) {std::cerr << e.what() << std::endl;//錯誤信息輸出到標準錯誤流return EXIT_FAILURE;}return EXIT_SUCCESS;
}
使用異常處理機制,確保程序能夠優雅地處理錯誤,如果出現異常,打印錯誤信息并返回失敗狀態。
總結
當v
vkCreateInstance成功返回后,instance變量中就包含了一個有效的Vulkan實例句柄。這個句柄實際上是一個指向復雜內部結構的指針,該結構包含了:
- 所有啟用擴展的函數指針表
- 應用程序信息的副本
- 平臺特定的窗口系統集成代碼
- 內存分配器的配置信息
- 錯誤處理和調試回調的設置
此時,應用程序就獲得了訪問Vulkan功能的基礎憑證,可以進行后續的物理設備枚舉、邏輯設備創建等操作。實例的生命周期將貫穿整個應用程序運行期間,直到程序退出時在cleanup()函數中被銷毀。
這整個創建過程體現了Vulkan先配置后執行的設計哲學:應用程序需要詳細描述自己的需求和配置,然后系統根據這些描述一次性完成所有初始化工作,避免了運行時的不確定性和性能開銷。
該部分的完整代碼如下:
(1)無注釋版:
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>#include <iostream>
#include <stdexcept>
#include <cstdlib>const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;class HelloTriangleApplication {
public:void run() {initWindow();initVulkan();mainLoop();cleanup();}private:GLFWwindow* window;VkInstance instance;void initWindow() {glfwInit();glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);}void initVulkan() {createInstance();}void mainLoop() {while (!glfwWindowShouldClose(window)) {glfwPollEvents();}}void cleanup() {vkDestroyInstance(instance, nullptr);glfwDestroyWindow(window);glfwTerminate();}void createInstance() {VkApplicationInfo appInfo{};appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;appInfo.pApplicationName = "Hello Triangle";appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);appInfo.pEngineName = "No Engine";appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);appInfo.apiVersion = VK_API_VERSION_1_0;VkInstanceCreateInfo createInfo{};createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;createInfo.pApplicationInfo = &appInfo;uint32_t glfwExtensionCount = 0;const char** glfwExtensions;glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);createInfo.enabledExtensionCount = glfwExtensionCount;createInfo.ppEnabledExtensionNames = glfwExtensions;createInfo.enabledLayerCount = 0;if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {throw std::runtime_error("failed to create instance!");}}
};int main() {HelloTriangleApplication app;try {app.run();} catch (const std::exception& e) {std::cerr << e.what() << std::endl;return EXIT_FAILURE;}return EXIT_SUCCESS;
}
(2)詳細注釋版:
// 定義GLFW_INCLUDE_VULKAN宏,讓GLFW自動包含Vulkan頭文件
// 這樣我們就不需要單獨包含vulkan.h了
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>// 標準C++庫頭文件
#include <iostream> // 用于輸出錯誤信息
#include <stdexcept> // 用于異常處理
#include <cstdlib> // 用于EXIT_SUCCESS和EXIT_FAILURE常量// 定義窗口的寬度和高度常量
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;class HelloTriangleApplication {
public:// 主運行函數,按順序執行初始化、主循環和清理void run() {initWindow(); // 初始化GLFW窗口initVulkan(); // 初始化Vulkan相關資源mainLoop(); // 進入主事件循環cleanup(); // 清理所有資源}private:GLFWwindow* window; // GLFW窗口指針VkInstance instance; // Vulkan實例句柄,這是使用Vulkan API的入口點// 初始化GLFW窗口void initWindow() {// 初始化GLFW庫glfwInit();// 設置窗口提示:不使用任何圖形API(因為我們要使用Vulkan)glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);// 設置窗口提示:禁止窗口大小調整(簡化示例代碼)glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);// 創建窗口:寬度、高度、標題、監視器(nullptr表示窗口模式)、共享窗口(nullptr表示不共享)window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);}// 初始化Vulkan相關資源void initVulkan() {createInstance(); // 創建Vulkan實例}// 主事件循環void mainLoop() {// 持續運行直到用戶關閉窗口while (!glfwWindowShouldClose(window)) {glfwPollEvents(); // 處理窗口事件(如鍵盤、鼠標輸入)}}// 清理所有資源void cleanup() {// 銷毀Vulkan實例(必須在glfwTerminate之前調用)vkDestroyInstance(instance, nullptr);// 銷毀GLFW窗口glfwDestroyWindow(window);// 終止GLFW庫glfwTerminate();}// 創建Vulkan實例的核心函數void createInstance() {// 步驟1:填充應用程序信息結構體VkApplicationInfo appInfo{}; // {}初始化所有成員為零// 指定結構體類型(Vulkan中每個結構體都需要指定類型)appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;// 應用程序名稱(可選,但有助于驅動程序優化)appInfo.pApplicationName = "Hello Triangle";// 應用程序版本(使用VK_MAKE_VERSION宏創建版本號:主版本.次版本.補丁版本)appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);// 引擎名稱(如果使用游戲引擎的話)appInfo.pEngineName = "No Engine";// 引擎版本appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);// 要使用的Vulkan API版本appInfo.apiVersion = VK_API_VERSION_1_0;// 步驟2:填充實例創建信息結構體VkInstanceCreateInfo createInfo{};// 指定結構體類型createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;// 指向應用程序信息的指針createInfo.pApplicationInfo = &appInfo;// 步驟3:獲取GLFW需要的Vulkan擴展uint32_t glfwExtensionCount = 0; // 擴展數量const char** glfwExtensions; // 擴展名稱數組// GLFW告訴我們它需要哪些Vulkan擴展來創建窗口表面glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);// 將GLFW需要的擴展添加到實例創建信息中createInfo.enabledExtensionCount = glfwExtensionCount; // 擴展數量createInfo.ppEnabledExtensionNames = glfwExtensions; // 擴展名稱數組// 步驟4:驗證層設置(這里暫時設為0,不啟用任何驗證層)createInfo.enabledLayerCount = 0;// 步驟5:創建Vulkan實例// 參數:創建信息、自定義分配器(nullptr使用默認)、實例句柄的指針if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {// 如果創建失敗,拋出運行時異常throw std::runtime_error("failed to create instance!");}}
};// 主函數
int main() {HelloTriangleApplication app; // 創建應用程序對象try {app.run(); // 運行應用程序} catch (const std::exception& e) { // 捕獲異常std::cerr << e.what() << std::endl; // 輸出錯誤信息return EXIT_FAILURE; // 返回失敗狀態碼}return EXIT_SUCCESS; // 返回成功狀態碼
}
?歡迎學習和交流,歡迎指正!🌹🌹?