Vulkan 開發的系列文章:數組
此篇文章繼續學習 Vulkan 中的組件:Command-Buffer 。微信
在前面的文章中,咱們已經建立了 Instance
、Device
、Queue
三個組件,而且知道了 Queue
組件是用來和物理設備溝通的橋樑,而具體的溝經過程就須要 Command-Buffer
(命令緩衝區)組件,它是若干命令的集合,咱們向 Queue
提交 Command-Buffer
,而後才交由物理設備 GPU 進行處理。異步
在建立 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);
複製代碼
有幾個參數須要注意:學習
queueFamilyIndex
參數爲建立 Queue
時選擇的那個 queueFlags
爲 VK_QUEUE_GRAPHICS_BIT
的索引,從 Command-Pool
中分配的的 Command-Buffer
必須提交到同一個 Queue
中。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-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
剛剛建立時,它就是處於初始化的狀態。今後狀態,能夠達到 Recording
狀態,另外,若是重置以後,也會回到該狀態。
調用 vkBeginCommandBuffer
方法從 Initial
狀態進入到該狀態。一旦進入該狀態後,就能夠調用 vkCmd*
等系列方法記錄命令。
調用 vkEndCommandBuffer
方法從 Recording
狀態進入到該狀態,此狀態下,Command-Buffer
能夠提交或者重置。
把 Command-Buffer
提交到 Queue
以後,就會進入到該狀態。此狀態下,物理設備可能正在處理記錄的命令,所以不要在此時更改 Command-Buffer
,當處理結束後,Command-Buffer
可能會回到 Executable
狀態或者 Invalid
狀態。
一些操做會使得 Command-Buffer
進入到此狀態,該狀態下,Command-Buffer
只能重置、或者釋放。
如今能夠嘗試着記錄一些命令,提交到 Queue
上了,命令記錄的調用過程以下圖:
在 vkBeginCommandBuffer
和 vkEndCommandBuffer
方法之間能夠記錄和渲染相關的命令,這裏先不考慮中間的過程,直接建立提交。
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;
複製代碼
直接調用 vkEndCommandBuffer
方法就能夠結束記錄,此時就能夠提交了。
vkEndCommandBuffer(commandBuffer[0]);
複製代碼
經過 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 中有 Semaphore
、Fences
、Event
、Barrier
四種機制來保證同步。
簡單說一下 Semaphore
和 Fence
。
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
機制。Semaphore
和 Fence
有相同之處,可是使用場景卻不同,就如圖所示。
Semaphore
和 Fence
的建立過程以下,和以往的 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
的過程,省略了 Semaphores
和 Fences
的同步機制,固然也能夠把它們加上。
在 vkQueueSubmit
的最後一個參數設置爲了 VK_NULL_HANDLE
,這是 Vulkan 中設置爲 NULL
的一個方法(實際上是設置了一個整數 0 ),也能夠設置了 Fence
,表示咱們要等待該 Command-Buffer
在 Queue
執行結束,雖然說 Command-Buffer
也能夠經過 Semaphore
來表示執行結束,但這兩種方式的使用場景不同。
回到 Fence
的建立過程,其中有一個 flags
參數表示 Fence
的狀態,有以下兩種狀態:
當 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 去執行具體的操做,固然具體執行是一個異步的過程,須要用到同步機制。
Semaphore
和 Fence
均可以實現同步,但使用場景不一樣。
歡迎關注微信公衆號:【紙上淺談】,得到最新文章推送~~~