1.Introduction
Vulkan 提供顯式的同步結構,允許 CPU 與 GPU 同步命令的執行。並且還可以控制 GPU 中命令的執行順序。所有執行的 Vulkan 命令都將進入隊列,並以某種未定義的順序“不間斷”執行。
有時,我們明確希望在執行新操作之前確保某些操作已完成。在編寫vulkan應用時,雖然對給定 VkQueue 的操作是線性發生的,但如果我們有多個VkQueue,則無法保證順序。爲此,以及爲了與 CPU 的通信,我們需要控制資源的訪問同步。
對於同一個資源的訪問之間的同步是vulkan應負責的內容之一,vulkan中一共提供瞭如下四種同步機制:
- Fence
- Semaphore
- Event
- Barrier
用以同步host/device之間,queues之間,queue submissions之間,以及一個單獨的command buffer的commands之間的同步。
2.Fence
2.1Fence概述
首先我們介紹最簡單的Fence。一句話總結,Fence提供了一種粗粒度的,從Device向Host單向傳遞信息的機制,即GPU -> CPU。
Host可以使用Fence來查詢通過vkQueueSubmit/vkQueueBindSparse所提交的操作是否完成。簡言之,在vkQueueSubmit/vkQueueBindSparse的時候,可以附加帶上一個Fence對象。之後就可以使用這個對象來查詢之前提交的狀態了。
example:
VkResult vkQueueSubmit( VkQueue queue, uint32_t submitCount, const VkSubmitInfo* pSubmits, VkFence fence);
其中,最後一個參數可以是一個有效的fence對象,當然,也可以指定爲VK_NULL_HANDLE,標明不需要Fence。有趣的是,在vkQueueSubmit的時候,如果給定一個有效的fence對象,但是不提交任何信息,即submitCount爲0,那麼同樣也可以算作一次成功的提交,等待之前所有提交到queue的任務都完成後,這個fence也就signaled了。這種使用方式提供了一種機制,可以讓我們查詢一個queue現在到底忙不忙(即提交後直接查詢這個fence的狀態,如果是signaled,證明不忙;如果unsignaled,證明之前提交的任務還沒有完成)。
Fence本身只有兩種狀態,unsignaled或者signaled,大致可以認爲fence是觸發態還是未觸發態。當使用vkCreateFence創建fence對象的時候,如果在標誌位上填充了VkFenceCreateFlagBits的VK_FENCE_CREATE_SIGNALED_BIT,那麼創建出來的fence就是signaled狀態,否則都是unsignaled狀態的。銷燬一個fence對象需要使用vkDestroyFence。
伴隨着vkQueueSubmit/vkQueueBindSparse一起提交的fence對象,可以使用vkGetFenceStatus來查詢fence的狀態。注意vkGetFenceStatus是非阻塞的,如果fence處於signaled狀態,這個API返回VK_SUCCESS,否則,立即返回VK_NOT_READY。
當然,fence被觸發到signaled狀態,必須存在一種方法,將之轉回到unsignaled狀態,這個功能由vkResetFences完成,這個API一次可以將多個fence對象轉到unsignaled狀態。這個API結合VK_FENCE_CREATE_SIGNALED_BIT位,可以達到一種類似於C中do {} while;的效果,即loop的代碼有着一致的表現:loop開始之前,所有的fence都創建位signaled狀態,每次loop開始的時候,所用到的fence都由這個API轉到unsignaled狀態,伴隨着submit提交過去。
等待一個fence,除了使用vkGetFenceStatus輪詢之外,還有一個API vkWaitForFences提供了阻塞式地查詢方法。這個API可以等待一組fence對象,直到其中至少一個,或者所有的fence都處於signaled狀態,或者超時(時間限制由參數給出),纔會返回。如果超時的時間設置爲0,則這個API簡單地看一下是否滿足前兩個條件,然後根據情況選擇返回VK_SUCCESS,或者(雖然沒有任何等待)VK_TIMEOUT。
簡而言之,對於一個fence對象,Device會將其從unsignaled轉到signaled狀態,告訴Host一些工作已經完成。所以fence使用在Host/Device之間的,且是一種比較粗粒度的同步機制。
2.2.Fence實例
void executeQueue( VkCommandBuffer cmd ) { const VkCommandBuffer cmds[] = { cmd }; VkFenceCreateInfo fenceInfo; VkFence drawFence; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceInfo.pNext = nullptr; fenceInfo.flags = 0; vkCreateFence( gDevice, &fenceInfo, nullptr, &drawFence ); VkPipelineStageFlags pipeStageFlags = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; VkSubmitInfo submitInfo[1] = {}; submitInfo[0].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo[0].pNext = nullptr; submitInfo[0].waitSemaphoreCount = 0; submitInfo[0].pWaitSemaphores = nullptr; submitInfo[0].pWaitDstStageMask = &pipeStageFlags; submitInfo[0].commandBufferCount = 1; submitInfo[0].pCommandBuffers = cmds; submitInfo[0].signalSemaphoreCount = 0; submitInfo[0].pSignalSemaphores = nullptr; HR( vkQueueSubmit( gQueue, 1, submitInfo, drawFence ) ); VkResult res; do { res = vkWaitForFences( gDevice, 1, &drawFence, VK_TRUE, 100000000 ); } while( res == VK_TIMEOUT ); vkDestroyFence( gDevice, drawFence, nullptr ); }
3.Semaphore
3.1.VkSemaphore概述
VkSemaphore用以同步不同的queue之間,或者同一個queue不同的submission之間的執行順序。即GPU -> GPU。
類似於fence,semaphore也有signaled和unsignaled的狀態之分。然而由於在queue之間或者內部做同步都是device自己控制,所以一個semaphore的初始狀態也就不重要了。所以,vkCreateSemaphore(3)就簡單地不用任何額外參數創建一個semaphore對象,然後vkDestroySemaphore(3)可以用來銷燬一個semaphore對象。不同於fence,沒有重置或者等待semaphore的api,因爲semaphore只對device有效。
在device上使用semaphore的最典型的場景,就是通過vkQueueSubmit提交command buffer時候,所需要的參數由VkSubmitInfo()提交
typedef struct VkSubmitInfo { VkStructureType sType; const void* pNext; uint32_t waitSemaphoreCount; const VkSemaphore* pWaitSemaphores; const VkPipelineStageFlags* pWaitDstStageMask; uint32_t commandBufferCount; const VkCommandBuffer* pCommandBuffers; uint32_t signalSemaphoreCount; const VkSemaphore* pSignalSemaphores; } VkSubmitInfo;
通過不同的參數搭配,可以達到如下效果:所提交的command buffer將在執行到每個semaphore等待階段時候,檢查並等待每個對應的wait semaphore數組中的semaphore是否被signal, 且等到command buffer執行完畢以後,將所有signal semaphore數組中的semaphore都signal起來。
通過這種方式,實際上提供了一種非常靈活的同步queue之間或者queue內部不同command buffer之間的方法,通過組合使用semaphore,AP可以顯式地指明不同command buffer之間的資源依賴關係,從而可以讓driver在遵守這個依賴關係的前提下,最大程度地並行化,以提高GPU的利用效率。
基本邏輯:
一些 Vulkan 操作(如 VkQueueSubmit)支持 Signal 或 Wait 信標。
如果將其設置爲 Signal a semaphore,這意味着該操作將在執行時立即“鎖定”該信標,並在執行完成後解鎖。
如果將其設置爲 Wait on a semaphore,則表示操作將等到該信標解鎖後纔開始執行。
3.2.VkSemaphore實例
僞代碼:
VkSemaphore Task1Semaphore; VkSemaphore Task2Semaphore; VkOperationInfo OpAlphaInfo; // Operation Alpha will signal the semaphore 1 OpAlphaInfo.signalSemaphore = Task1Semaphore; VkDoSomething(OpAlphaInfo); VkOperationInfo OpBetaInfo; // Operation Beta signals semaphore 2, and waits on semaphore 1 OpBetaInfo.signalSemaphore = Task2Semaphore; OpBetaInfo.waitSemaphore = Task1Semaphore; VkDoSomething(OpBetaInfo); VkOperationInfo OpGammaInfo; //Operation gamma waits on semaphore 2 OpGammaInfo.waitSemaphore = Task2Semaphore; VkDoSomething(OpGammaInfo);
參考鏈接
- Vulkan Guide學習(1.5): https://zhuanlan.zhihu.com/p/451194569
- vulkan中的同步和緩存控制之一,fence和semaphore: https://zhuanlan.zhihu.com/p/24817959
- C++ (Cpp) vkCreateFence示例: https://cpp.hotexamples.com/zh/examples/-/-/vkCreateFence/cpp-vkcreatefence-function-examples.html