Vulkan中的同步與緩存控制

1.Introduction

Vulkan 提供顯式的同步結構,允許 CPU 與 GPU 同步命令的執行。並且還可以控制 GPU 中命令的執行順序。所有執行的 Vulkan 命令都將進入隊列,並以某種未定義的順序“不間斷”執行。

有時,我們明確希望在執行新操作之前確保某些操作已完成。在編寫vulkan應用時,雖然對給定 VkQueue 的操作是線性發生的,但如果我們有多個VkQueue,則無法保證順序。爲此,以及爲了與 CPU 的通信,我們需要控制資源的訪問同步。

對於同一個資源的訪問之間的同步是vulkan應負責的內容之一,vulkan中一共提供瞭如下四種同步機制:

  1. Fence
  2. Semaphore
  3. Event
  4. 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章