Vulkan 開發的系列文章:
在 Vulkan 的系列文章中出現過如下的圖片:
這張圖片很詳細的概括了 Vulkan 中的重要元件以及它們的工作流程,接下來的文章中會針對每個元件進行學習講解並配上相關的示例程式碼,首先是 Instance、Device 和 Queue 元件。
Instance 元件
在開始建立 Device 等元件之前,需要建立一個 VkInstance
物件。
通過 vkCreateInstance
方法建立 VKInstance
物件,以下是函式原型,在 <vulkan.h>
標頭檔案中。
// 宣告的函式指標的形式
typedef VkResult (VKAPI_PTR *PFN_vkCreateInstance)
(const VkInstanceCreateInfo* pCreateInfo, // 提供建立的資訊
const VkAllocationCallbacks* pAllocator, // 建立時的回撥函式
VkInstance* pInstance); // 建立的例項
複製程式碼
<vulkan.h> 的標頭檔案把函式通過 typedef
關鍵字宣告成了函式指標的形式,可能會有點難找。
在 Vulkan 的 API 中有一些固定的 呼叫套路 。
- 要建立某個物件,先提供一個包含建立資訊的物件。
- 建立時通過傳遞引用的方式來傳參。
接下來看看這個套路是如何應用在 VKInstance
物件上的。
在 vkCreateInstance
函式中看到有個名為 VkInstanceCreateInfo
型別的引數,這就是包含了 VKInstance
要建立的資訊。
它的引數資訊有點多:
typedef struct VkInstanceCreateInfo {
VkStructureType sType; // 一般為方法對應的型別
const void* pNext; // 一般為 null 就好了
VkInstanceCreateFlags flags; // 留著以後用的,設為 0 就好了
const VkApplicationInfo* pApplicationInfo; // 對應新的一個結構體 VkApplicationInfo
uint32_t enabledLayerCount; // layer 和 extension 用於除錯和擴充
const char* const* ppEnabledLayerNames;
uint32_t enabledExtensionCount;
const char* const* ppEnabledExtensionNames;
} VkInstanceCreateInfo;
複製程式碼
除了還需要建立一個 VkApplicationInfo
物件,還可以設定 Layer
和 Extension
。
其中:Layer
是用來錯誤校驗、除錯輸出的。為了提供效能,其中的方法之一就是減少驅動進行狀態、錯誤校驗,而 Vulkan 就把這一層單獨抽出來了。
Layer
在整個架構中的位置如上圖,Vulkan API 直接和驅動對話,而 Layer
處於應用和 Vulkan API 之間,供開發者進行除錯。
另外,Extension
就是 Vulkan 支援的擴充,最典型的就是 Vulkan 的跨平臺渲染顯示,就是通過擴充來完成的,比如在 Android、Windows 上使用 Vulkan 都需要使用不同的擴充才可以把內容顯示到螢幕上。
關於 Layer
和 Extension
後續再細說。
接著回到 VkApplicationInfo
結構體,也是建立 Instance 的必要引數之一。
typedef struct VkApplicationInfo {
VkStructureType sType;
const void* pNext;
const char* pApplicationName;
uint32_t applicationVersion;
const char* pEngineName;
uint32_t engineVersion;
uint32_t apiVersion;
} VkApplicationInfo;
複製程式碼
它的引數釋義就比較容易理解了,設定應用的名稱、版本號等,有了它們就可以建立 Instance
物件了,程式碼可以參考 這裡 。
具體的程式碼如下:
VkApplicationInfo app_info = {};
app_info.apiVersion = VK_API_VERSION_1_0;
app_info.applicationVersion = 1;
app_info.engineVersion = 1;
app_info.pNext = nullptr;
app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app_info.pEngineName = APPLICATION_NAME;
app_info.pApplicationName = APPLICATION_NAME;
VkInstanceCreateInfo instance_info = {};
// type 就是結構體的型別
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.pNext = nullptr;
instance_info.pApplicationInfo = &app_info;
instance_info.flags = 0;
// Extension and Layer 暫時不用,可空
instance_info.enabledExtensionCount = 0;
instance_info.ppEnabledExtensionNames = nullptr;
instance_info.ppEnabledLayerNames = nullptr;
instance_info.enabledLayerCount = 0;
VkResult result = vkCreateInstance(&instance_info, nullptr, &instance);
複製程式碼
當每呼叫一個建立函式後,返回的型別都是 VkResult
,只要 VkResult 大於 0 ,那麼執行就是成功的。
另外還有個引數是 VkAllocationCallbacks
,表示函式呼叫時的回撥,需要傳遞一個函式指標,在後面的各種呼叫中都會看到它的身影,如果有用到可以傳參,一般為 nullptr
就好了。
關於每個結構體,它每個引數的具體釋義,靠死記硬背是肯定不行的,參考 vkspec.pdf
書籍,裡面有對每個引數、結構體的詳細釋義。
Device 元件
有了 Instance
元件,就可以建立 Device
元件了,按照呼叫的套路,肯定還會有一個 VkDeviceCreateInfo
的結構體表示 Device
的建立資訊。
而 Device
具體指的是邏輯上的裝置,可以說是對物理裝置的一個邏輯上的封裝,而物理裝置就是 VkPhysicalDevice
物件。
在某些情況下,可能會具有多個物理裝置,如下圖所示,因此要先列舉一下所有的物理裝置:
LOGI("enumerate gpu device");
uint32_t gpu_size = 0;
// 第一次呼叫只為了獲得個數
VkResult res = vkEnumeratePhysicalDevices(instance, &gpu_size, nullptr);
複製程式碼
在 vkEnumeratePhysicalDevices
方法中,傳入的第二個引數為 gpu 的個數,第三個引數為 null,這樣的一次呼叫會返回 gpu 的個數到 gpu_size
變數。
vector<VkPhysicalDevice> gpus;
gpus.resize(gpu_size);
// vector.data() 方法轉換成指標型別
// 第二次呼叫獲得所有的資料
res = vkEnumeratePhysicalDevices(instance, &gpu_size, gpus.data());
複製程式碼
當再一次呼叫 vkEnumeratePhysicalDevices
函式時,第三個引數不為 null,而是相應的 VkPhysicalDevice
容器,那麼 gpus 會填充 gpu_size
個的 VkPhysicalDevice
物件。
這也算是 Vulkan API 呼叫的一個 固定套路 了,呼叫兩次來獲得資料,在後面的程式碼中也會經常看到這種方式。
有了 VkPhysicalDevice
物件之後,可以查詢 VkPhysicalDevice
上的一些屬性,以下函式都可以查詢相關資訊:
- vkGetPhysicalDeviceQueueFamilyProperties
- vkGetPhysicalDeviceMemoryProperties
- vkGetPhysicalDeviceProperties
- vkGetPhysicalDeviceImageFormatProperties
- vkGetPhysicalDeviceFormatProperties
在這裡需要用到的屬性是 QueueFamilyProperties
,獲得該屬性的方法呼叫方式和獲得 VkPhysicalDevice
資料方式一樣,也是一個兩次呼叫。
如果有裝置有多個 GPU,那麼這裡取第一個來獲取它的相關屬性:
// 第一次呼叫,獲得個數
uint32_t queue_family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(gpus[0], &queue_family_count, nullptr);
assert(queue_family_count != 0);
// 第二次呼叫,獲得實際資料
vector<VkQueueFamilyProperties> queue_family_props;
queue_family_props.resize(queue_family_count);
vkGetPhysicalDeviceQueueFamilyProperties(gpus[0], &queue_family_count, queue_family_props.data());
assert(queue_family_count != 0);
複製程式碼
QueueFamilyProperties
的結構體含義如下:
typedef struct VkQueueFamilyProperties {
VkQueueFlags queueFlags; // 標識位:表示 Queue 的功能
uint32_t queueCount;
uint32_t timestampValidBits;
VkExtent3D minImageTransferGranularity;
} VkQueueFamilyProperties;
複製程式碼
其中:queueFlags
表示該 Queue 的能力,有的 Queue 是用來渲染影象的,這個和我們的使用最為密切,還有的 Queue 是用來計算的。
具體的 Flag 標識如下:
typedef enum VkQueueFlagBits {
VK_QUEUE_GRAPHICS_BIT = 0x00000001, // 影象相關
VK_QUEUE_COMPUTE_BIT = 0x00000002, // 計算相關
VK_QUEUE_TRANSFER_BIT = 0x00000004,
VK_QUEUE_SPARSE_BINDING_BIT = 0x00000008,
VK_QUEUE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkQueueFlagBits;
typedef VkFlags VkQueueFlags;
複製程式碼
一般來說,我們用的是 queueFlags
為 VK_QUEUE_GRAPHICS_BIT
標識位的 Queue
。
那麼 Queue
究竟是什麼?
物理裝置可能會有多個 Queue
,不同的 Queue
對應不同的特性。
在文章最開始的圖中可以看到,Command-buffer
是提交到了 Queue
,Queue
再提交給 Device
去執行。Queue
可以看成是應用程式和物理裝置溝通的橋樑,我們在 Queue
上提交命令,然後再交由 GPU 去執行。
回到本小節的內容,建立 Device 元件,它的函式指標形式如下:
// 建立 Device 的函式指標
typedef VkResult (VKAPI_PTR *PFN_vkCreateDevice)
(VkPhysicalDevice physicalDevice, // 物理裝置
const VkDeviceCreateInfo* pCreateInfo, // 呼叫套路里面的 CreateInfo
const VkAllocationCallbacks* pAllocator,
VkDevice* pDevice); // 要建立的 Device 類
複製程式碼
建立一個 Device
物件,不僅需要指定具體的物理裝置 VkPhysicalDevice
,另外還需要該物理裝置上的 Queue
相關資訊。
在 VkDeviceCreateInfo
結構體中需要一個引數是 VkDeviceQueueCreateInfo
,它的建立如下:
// 建立 Queue 所需的相關資訊
VkDeviceQueueCreateInfo queue_info = {};
// 找到屬性為 VK_QUEUE_GRAPHICS_BIT 的索引
bool found = false;
for (unsigned int i = 0; i < queue_family_count; ++i) {
if (queue_family_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
queue_info.queueFamilyIndex = i;
found = true;
break;
}
}
float queue_priorities[1] = {0.0};
// 結構體的型別
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.pNext = nullptr;
queue_info.queueCount = 1;
// Queue 的優先順序
queue_info.pQueuePriorities = queue_priorities;
複製程式碼
接下來就可以完成 Queue
的建立:
// 建立 Device 所需的相關資訊類
VkDeviceCreateInfo device_info = {};
device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
device_info.pNext = nullptr;
// Device 所需的 Queue 相關資訊
device_info.queueCreateInfoCount = 1; // Queue 個數
device_info.pQueueCreateInfos = &queue_info; // Queue 相關資訊
// Layer 和 Extension 暫時為空,不影響執行,後續再補上
device_info.enabledExtensionCount = 0;
device_info.ppEnabledExtensionNames = NULL;
device_info.enabledLayerCount = 0;
device_info.ppEnabledLayerNames = NULL;
device_info.pEnabledFeatures = NULL;
res = vkCreateDevice(gpus[0], &device_info, nullptr, &device);
複製程式碼
Queue 元件
完成了 Device
建立之後,Queue
的建立也簡單多了,直接呼叫如下函式就好了:
typedef void (VKAPI_PTR *PFN_vkGetDeviceQueue)
(VkDevice device, // 建立的 Device 物件
uint32_t queueFamilyIndex, // queueFlags 為 VK_QUEUE_GRAPHICS_BIT 的索引
uint32_t queueIndex,
VkQueue* pQueue); // 要建立的 Queue
// 程式碼示例
vkGetDeviceQueue(info.device, info.graphics_queue_family_index, 0, &info.queue);
複製程式碼
元件銷燬
完成了 Instance
、Device
、Queue
元件的建立之後,還有一件要做的事情就是釋放它們,銷燬元件。
按照先進後出的方式進行銷燬,Instance
最先建立反而最後銷燬,和 Device
相關聯的 Queue
當 Device
銷燬了,Queue
也隨之銷燬了。
// 銷燬 Device
vkDestroyDevice(info.device, nullptr);
// 銷燬 Instance
vkDestroyInstance(info.instance, nullptr);
複製程式碼
參考
這裡有一些不錯的參考地址和書籍:
也可以參考我的專案實踐程式碼:
以上是個人的學習經驗,僅供參考,有講的不對之處,歡迎指出,也可以加我微信一起交流學習: zh_ying_13
(備註部落格).
有相關工作機會的求帶入坑~~~
總結
敲一遍上述的程式碼,會發現 Vulkan 在 API 呼叫上還是有跡可循的,重點是要理解了每個引數的含義,多結合官方的文件來學習、實踐、
歡迎關注微信公眾號:【紙上淺談】,獲得最新文章推送~~~