天天看點

進擊的 Vulkan 移動開發之 Instance & Device & QueueInstance 元件Device 元件Queue 元件元件銷毀參考總結

作者:星隕

來源:

音視訊開發進階 在 Vulkan 的系列文章中出現過如下的圖檔:
進擊的 Vulkan 移動開發之 Instance & Device & QueueInstance 元件Device 元件Queue 元件元件銷毀參考總結
這張圖檔很詳細的概括了 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 就把這一層單獨抽出來了。

進擊的 Vulkan 移動開發之 Instance &amp; Device &amp; QueueInstance 元件Device 元件Queue 元件元件銷毀參考總結

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

在某些情況下,可能會具有多個實體裝置,如下圖所示,是以要先枚舉一下所有的實體裝置:

進擊的 Vulkan 移動開發之 Instance &amp; Device &amp; QueueInstance 元件Device 元件Queue 元件元件銷毀參考總結
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 系列文章
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
進擊的 Vulkan 移動開發之 Instance &amp; Device &amp; QueueInstance 元件Device 元件Queue 元件元件銷毀參考總結

繼續閱讀