進擊的 Vulkan 移動開發之 Instance & Device & Queue

Vulkan 開發的系列文章:git

  1. 進擊的 Vulkan 移動開發(一)之此生前世
  1. 進擊的 Vulkan 移動開發(二)之談談對渲染流程的理解

在 Vulkan 的系列文章中出現過以下的圖片:github

這張圖片很詳細的歸納了 Vulkan 中的重要組件以及它們的工做流程,接下來的文章中會針對每一個組件進行學習講解並配上相關的示例代碼,首先是 Instance、Device 和 Queue 組件。api

Instance 組件

在開始建立 Device 等組件以前,須要建立一個 VkInstance 對象。微信

經過 vkCreateInstance 方法建立 VKInstance 對象,如下是函數原型,在 <vulkan.h> 頭文件中。架構

// 聲明的函數指針的形式
typedef VkResult (VKAPI_PTR *PFN_vkCreateInstance) (const VkInstanceCreateInfo* pCreateInfo, // 提供建立的信息 const VkAllocationCallbacks* pAllocator, // 建立時的回調函數 VkInstance* pInstance);                // 建立的實例
複製代碼

<vulkan.h> 的頭文件把函數經過 typedef 關鍵字聲明成了函數指針的形式,可能會有點難找。app

在 Vulkan 的 API 中有一些固定的 調用套路函數

  1. 要建立某個對象,先提供一個包含建立信息的對象。
  2. 建立時經過傳遞引用的方式來傳參。

接下來看看這個套路是如何應用在 VKInstance 對象上的。post

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 對象,還能夠設置 LayerExtension

其中:Layer 是用來錯誤校驗、調試輸出的。爲了提供性能,其中的方法之一就是減小驅動進行狀態、錯誤校驗,而 Vulkan 就把這一層單獨抽出來了。

Layer 在整個架構中的位置如上圖,Vulkan API 直接和驅動對話,而 Layer 處於應用和 Vulkan API 之間,供開發者進行調試。

另外,Extension 就是 Vulkan 支持的拓展,最典型的就是 Vulkan 的跨平臺渲染顯示,就是經過拓展來完成的,好比在 Android、Windows 上使用 Vulkan 都須要使用不一樣的拓展才能夠把內容顯示到屏幕上。

關於 LayerExtension 後續再細說。

接着回到 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;
複製代碼

通常來講,咱們用的是 queueFlagsVK_QUEUE_GRAPHICS_BIT 標識位的 Queue

那麼 Queue 到底是什麼?

物理設備可能會有多個 Queue,不一樣的 Queue 對應不一樣的特性。

在文章最開始的圖中能夠看到,Command-buffer 是提交到了 QueueQueue 再提交給 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);

複製代碼

組件銷燬

完成了 InstanceDeviceQueue 組件的建立以後,還有一件要作的事情就是釋放它們,銷燬組件。

按照先進後出的方式進行銷燬,Instance 最早建立反而最後銷燬,和 Device 相關聯的 QueueDevice 銷燬了,Queue 也隨之銷燬了。

// 銷燬 Device
    vkDestroyDevice(info.device, nullptr);
    // 銷燬 Instance
    vkDestroyInstance(info.instance, nullptr);
複製代碼

參考

這裏有一些不錯的參考地址和書籍:

  1. www.zhihu.com/people/snow…
  2. www.zhihu.com/people/chen…

也能夠參考個人項目實踐代碼:

github.com/glumes/vulk…

以上是我的的學習經驗,僅供參考,有講的不對之處,歡迎指出,也能夠加我微信一塊兒交流學習: zh_ying_13 (備註博客).

有相關工做機會的求帶入坑~~~

總結

敲一遍上述的代碼,會發現 Vulkan 在 API 調用上仍是有跡可循的,重點是要理解了每一個參數的含義,多結合官方的文檔來學習、實踐、

歡迎關注微信公衆號:【紙上淺談】,得到最新文章推送~~~

掃碼關注
相關文章
相關標籤/搜索