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

glumes發表於2019-01-07

Vulkan 開發的系列文章:

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

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

進擊的 Vulkan 移動開發之 Instance & 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 物件,還可以設定 LayerExtension

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

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

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 物件。

在某些情況下,可能會具有多個物理裝置,如下圖所示,因此要先列舉一下所有的物理裝置:

進擊的 Vulkan 移動開發之 Instance & 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;
複製程式碼

一般來說,我們用的是 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 呼叫上還是有跡可循的,重點是要理解了每個引數的含義,多結合官方的文件來學習、實踐、

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

掃碼關注

相關文章