作者:星隕
來源:
音視訊開發進階 在 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 中有一些固定的 調用套路 。
1.要建立某個對象,先提供一個包含建立資訊的對象。
2.建立時通過傳遞引用的方式來傳參。
接下來看看這個套路是如何應用在
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);
參考
這裡有一些不錯的參考位址和書籍:
https://www.zhihu.com/people/snowfox-68/activities https://www.zhihu.com/people/chen-yong-59-86/posts也可以參考我的項目實踐代碼:
https://github.com/glumes/vulkan_tutorial以上是個人的學習經驗,僅供參考,有講的不對之處,歡迎指出,也可以加我微信一起交流學習:
ezglumes
(備注部落格).
總結
敲一遍上述的代碼,會發現 Vulkan 在 API 調用上還是有迹可循的,重點是要了解了每個參數的含義,多結合官方的文檔來學習、實踐、
Vulkan 系列文章「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。