進擊的 Vulkan 移動開發之 Command Buffer

Vulkan 開發的系列文章:數組

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

此篇文章繼續學習 Vulkan 中的組件:Command-Buffer 。微信

在前面的文章中,咱們已經建立了 InstanceDeviceQueue 三個組件,而且知道了 Queue 組件是用來和物理設備溝通的橋樑,而具體的溝經過程就須要 Command-Buffer (命令緩衝區)組件,它是若干命令的集合,咱們向 Queue 提交 Command-Buffer,而後才交由物理設備 GPU 進行處理。異步

Command-Pool 組件

在建立 Command-Buffer 以前,須要建立 Command-Pool 組件,從 Command-Pool 中去分配 Command-Buffer函數

仍是老套路,咱們須要先建立一個 VkXXXXCreateInfo 的結構體,結構體每一個參數的釋義仍是要多看官方的文檔。post

// 建立 Command-Pool 組件
    VkCommandPool command_pool;
    VkCommandPoolCreateInfo poolCreateInfo = {};
    poolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    // 能夠看到 Command-Pool 還和 Queue 相關聯
    poolCreateInfo.queueFamilyIndex = info.graphics_queue_family_index;
    // 標識命令緩衝區的一些行爲
    poolCreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
    // 具體建立函數的調用
    vkCreateCommandPool(info.device, &poolCreateInfo, nullptr, &command_pool);
複製代碼

有幾個參數須要注意:學習

  1. queueFamilyIndex 參數爲建立 Queue 時選擇的那個 queueFlagsVK_QUEUE_GRAPHICS_BIT 的索引,從 Command-Pool 中分配的的 Command-Buffer 必須提交到同一個 Queue 中。
  2. flags 有以下的選項,分別指定了 Command-Buffer 的不一樣特性:
typedef enum VkCommandPoolCreateFlagBits {
    VK_COMMAND_POOL_CREATE_TRANSIENT_BIT = 0x00000001,
    VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT = 0x00000002,
    VK_COMMAND_POOL_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandPoolCreateFlagBits;
複製代碼
  • VK_COMMAND_POOL_CREATE_TRANSIENT_BITui

    • 表示該 Command-Buffer 的壽命很短,可能在短期內被重置或釋放
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BITspa

    • 表示從 Command-Pool 中分配的 Command-Buffer 能夠經過 vkResetCommandBuffer 或者 vkBeginCommandBuffer 方法進行重置,若是沒有設置該標識位,就不能調用 vkResetCommandBuffer 方法進行重置。

Command-Buffer 組件

接下來就是從 Command-Pool 中分配 Command-Buffer,經過 VkCommandBufferAllocateInfo 函數。3d

首先須要一個 VkCommandBufferAllocateInfo 結構體表示分配所須要的信息。指針

typedef struct VkCommandBufferAllocateInfo {
    VkStructureType         sType;
    const void*             pNext;
    VkCommandPool           commandPool;    // 對應上面建立的 command-pool
    VkCommandBufferLevel    level;
    uint32_t                commandBufferCount; // 建立的個數
} VkCommandBufferAllocateInfo;
複製代碼

這裏有個參數也要注意:

  • VkCommandBufferLevel 指定 Command-Buffer 的級別。

有以下級別可使用:

typedef enum VkCommandBufferLevel {
    VK_COMMAND_BUFFER_LEVEL_PRIMARY = 0,
    VK_COMMAND_BUFFER_LEVEL_SECONDARY = 1,
    VK_COMMAND_BUFFER_LEVEL_BEGIN_RANGE = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
    VK_COMMAND_BUFFER_LEVEL_END_RANGE = VK_COMMAND_BUFFER_LEVEL_SECONDARY,
    VK_COMMAND_BUFFER_LEVEL_RANGE_SIZE = (VK_COMMAND_BUFFER_LEVEL_SECONDARY - VK_COMMAND_BUFFER_LEVEL_PRIMARY + 1),
    VK_COMMAND_BUFFER_LEVEL_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferLevel;
複製代碼

通常來講,使用 VK_COMMAND_BUFFER_LEVEL_PRIMARY 就行了。

具體建立代碼以下:

VkCommandBuffer commandBuffer[2];
    VkCommandBufferAllocateInfo command_buffer_allocate_info{};
    command_buffer_allocate_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    command_buffer_allocate_info.commandPool = command_pool;
    command_buffer_allocate_info.commandBufferCount = 2;
    command_buffer_allocate_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    vkAllocateCommandBuffers(info.device, &command_buffer_allocate_info, commandBuffer);
複製代碼

Command-Buffer 的生命週期

建立了 Command-Buffer 以後,來了解一下它的生命週期,以下圖:

  • Initial 狀態

Command-Buffer 剛剛建立時,它就是處於初始化的狀態。今後狀態,能夠達到 Recording 狀態,另外,若是重置以後,也會回到該狀態。

  • Recording 狀態

調用 vkBeginCommandBuffer 方法從 Initial 狀態進入到該狀態。一旦進入該狀態後,就能夠調用 vkCmd* 等系列方法記錄命令。

  • Executable 狀態

調用 vkEndCommandBuffer 方法從 Recording 狀態進入到該狀態,此狀態下,Command-Buffer 能夠提交或者重置。

  • Pending 狀態

Command-Buffer 提交到 Queue 以後,就會進入到該狀態。此狀態下,物理設備可能正在處理記錄的命令,所以不要在此時更改 Command-Buffer,當處理結束後,Command-Buffer 可能會回到 Executable 狀態或者 Invalid 狀態。

  • Invalid 狀態

一些操做會使得 Command-Buffer 進入到此狀態,該狀態下,Command-Buffer 只能重置、或者釋放。

Command-Buffer 的記錄與提交

如今能夠嘗試着記錄一些命令,提交到 Queue 上了,命令記錄的調用過程以下圖:

vkBeginCommandBuffervkEndCommandBuffer 方法之間能夠記錄和渲染相關的命令,這裏先不考慮中間的過程,直接建立提交。

begin 階段

VkCommandBufferBeginInfo beginInfo = {};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
        vkBeginCommandBuffer(commandBuffer[0], &beginInfo);
複製代碼

首先,仍是須要建立一個 VkCommandBufferBeginInfo 結構體用來表示 Command-Buffer 開始的信息。

這裏要注意的參數是 flags ,表示 Command-Buffer 的用途,

typedef enum VkCommandBufferUsageFlagBits {
    VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT = 0x00000001,
    VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT = 0x00000002,
    VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT = 0x00000004,
    VK_COMMAND_BUFFER_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferUsageFlagBits;
複製代碼
  • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
    • 表示該 Command-Buffer 只使用提交一次,用完以後就會被重置,而且每次提交時都須要從新記錄

end 階段

直接調用 vkEndCommandBuffer 方法就能夠結束記錄,此時就能夠提交了。

vkEndCommandBuffer(commandBuffer[0]);
複製代碼

buffer 提交

經過 vkQueueSubmit 方法將 Command-Buffer 提交到 Queue 上。

一樣的仍是須要建立一個 VkSubmitInfo 結構體:

typedef struct VkSubmitInfo {
    VkStructureType                sType;
    const void*                    pNext;
    uint32_t                       waitSemaphoreCount;  // 等待的 Semaphore 數量
    const VkSemaphore*             pWaitSemaphores;     // 等待的 Semaphore 數組指針
    const VkPipelineStageFlags*    pWaitDstStageMask;       // 在哪一個階段進行等待
    uint32_t                       commandBufferCount;  // 提交的 Command-Buffer 數量
    const VkCommandBuffer*         pCommandBuffers;      // 具體的 Command-Buffer 數組指針
    uint32_t                       signalSemaphoreCount;    //執行結束後通知的 Semaphore 數量
    const VkSemaphore*             pSignalSemaphores;       //執行結束後通知的 Semaphore 數組指針
} VkSubmitInfo;
複製代碼

它的參數比較多,而且涉及到 Command-Buffer 之間的同步關係了,這裏簡單說一下,後面再細說這一塊。

以下圖,Vulkan 中有 SemaphoreFencesEventBarrier 四種機制來保證同步。

簡單說一下 SemaphoreFence

  • Semaphore

    • Semaphore 的做用主要是用來向 Queue 中提交 Command-Buffer 時實現同步。好比說某個 Command-Buffer-B 在執行的某個階段中須要等待另外一個 Command-Buffer-A 執行成功後的結果,同時 Command-Buffer-C 在某階段又要要等待 Command-Buffer-B 的執行結果,那麼就應該使用 Semaphore 機制實現同步;
    • 此時 Command-Buffer-B 提交到 Queue 時就須要兩個 VkSemaphor ,一個表示它須要等待的 Semaphore,而且指定在哪一個階段等待;一個是它執行結束後發出通知的 Semaphore
  • Fence

    • Fence 的做用主要是用來保證物理設備和應用程序之間的同步,好比說向 Queue 中提交了 Command-Buffer 後,具體的執行交由物理設備去完成了,這是一個異步的過程,而應用程序若是要等待執行結束,就要使用 Fence 機制。

SemaphoreFence 有相同之處,可是使用場景卻不同,就如圖所示。

SemaphoreFence 的建立過程以下,和以往的 Vulkan 建立對象的調用方式沒有太大區別:

// 建立 Semaphore
    VkSemaphore imageAcquiredSemaphore;
    VkSemaphoreCreateInfo semaphoreCreateInfo = {};
    semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    vkCreateSemaphore(info.device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphore);

    // 建立 Fence
    VkFence drawFence;
    VkFenceCreateInfo fenceCreateInfo = {};
    fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    // 該參數表示 Fence 的狀態,若是不設置或者爲 0 表示 unsignaled state
    fence_info.flags = 0; 
    vkCreateFence(info.device, &fenceCreateInfo, nullptr, &drawFence);
複製代碼

繼續回到 VkSubmitInfo 結構體中,若是隻是簡單的提交 Command-Buffer,那就不須要考慮 Semaphore 這些同步機制了,把相應的參數都設置爲 nullptr,或者直接不設置也行,最後提交就行了,代碼以下:

// 簡單的提交過程
    // 開始記錄
    VkCommandBufferBeginInfo beginInfo1 = {};
    beginInfo1.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo1.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    vkBeginCommandBuffer(commandBuffer[0], &beginInfo1);

    // 省略中間的 vkCmdXXXX 系列方法
    // 結束記錄
    vkEndCommandBuffer(commandBuffer[0]);

    VkSubmitInfo submitInfo1 = {};
    submitInfo1.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    // pWaitSemaphores 和 pSignalSemaphores 都不設置,只是提交
    submitInfo1.commandBufferCount = 1;
    submitInfo1.pCommandBuffers = &commandBuffer[0];

    // 注意最後的參數 臨時設置爲 VK_NULL_HANDLE,也能夠設置爲 Fence 來同步
    vkQueueSubmit(info.queue, 1, &submitInfo1, VK_NULL_HANDLE);
複製代碼

以上就完成了 Command-Buffer 提交到 Queue 的過程,省略了 SemaphoresFences 的同步機制,固然也能夠把它們加上。

vkQueueSubmit 的最後一個參數設置爲了 VK_NULL_HANDLE ,這是 Vulkan 中設置爲 NULL 的一個方法(實際上是設置了一個整數 0 ),也能夠設置了 Fence ,表示咱們要等待該 Command-BufferQueue 執行結束,雖然說 Command-Buffer 也能夠經過 Semaphore 來表示執行結束,但這兩種方式的使用場景不同。

回到 Fence 的建立過程,其中有一個 flags 參數表示 Fence 的狀態,有以下兩種狀態:

  • signaled state
    • 若是 flags 參數爲 VK_FENCE_CREATE_SIGNALED_BIT 則表示建立後處於該狀態。
  • unsignaled state
    • 默認的狀態。

vkQueueSubmit 的最後參數傳入 Fence 後,就能夠經過 Fence 等待該 Command-Buffer 執行結束。

// wait fence to enter the signaled state on the host
// 錯誤的 waitForFences 使用,由於它並非一個阻塞的方法
// VkResult res = vkWaitForFences(info.device, 1, &fence, VK_TRUE, UINT64_MAX);
    VkResult res;
    do {
        res = vkWaitForFences(info.device, 1, &fence, VK_TRUE, UINT64_MAX);
    } while (res == VK_TIMEOUT);
複製代碼

vkWaitForFences 方法會等待 Fence 進入 signaled state 狀態,該方法的調用要放在 while 循環中,由於它並非一個阻塞的方法,能夠理解成一個狀態查詢,若是結果不對,返回的是 VK_TIMEOUT,結果知足要求才返回 VK_SUCCESS

Command-Buffer 執行結束後,傳入的 Fence 參數就會從 unsignaled state 進入到 signaled state ,從而觸發 vkWaitForFences 調用結束循環,代表執行結束了。

這就是 Fence 的使用,至於 Command-Buffer 之間經過 Semaphore 來同步的示例,詳見後續文章。

總結

本篇文章主要講解了 Command-Buffer 的使用和提交,而且涉及到了 Vulkan 的一些同步機制。

具體和渲染有關的操做,都要在 Command-Buffer 之間記錄,結束記錄以後提交給 Queue ,讓 GPU 去執行具體的操做,固然具體執行是一個異步的過程,須要用到同步機制。

SemaphoreFence 均可以實現同步,但使用場景不一樣。

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

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