2019年12月23日 星期一

關於在 Vulkan 使用 Barrier 的心得

關於在 Vulkan 使用 Barrier 的心得

前言

  最近在 Vulkan 使用 Barrier 時發生了問題,我在 [ stackoverflow ] How to render to texture in Vulkan? 提出了問題,不過並沒有得到詳細的解答,但可以知道關鍵的問題在同步( Synchronization ),這裡把學習的過程做個紀錄。

內容

  在舊的API( OpenGL 與 Direct3D11 )時,命令是即時執行,所以同步的問題不會浮現上來,
但新的API( Vulkan 與 Direct3D12 )時,命令會"並行"執行,如下圖
命令執行的差異

如果需要等某一群的命令執行完的話,就會需要用到 Barrier 來等待命令都結束,來達成所謂的同步( Synchronization )。 個人使用 Barrier 在 Direct3D12 沒遇到問題,但 Vulkan 卻出了問題,所以本篇主力說明 Vulkan 的 Barrier。

  先來看看 Vulkan 的 Barrier ,如下
void vkCmdPipelineBarrier(
    VkCommandBuffer                             commandBuffer,
    VkPipelineStageFlags                        srcStageMask,
    VkPipelineStageFlags                        dstStageMask,
    VkDependencyFlags                           dependencyFlags,
    uint32_t                                    memoryBarrierCount,
    const VkMemoryBarrier*                      pMemoryBarriers,
    uint32_t                                    bufferMemoryBarrierCount,
    const VkBufferMemoryBarrier*                pBufferMemoryBarriers,
    uint32_t                                    imageMemoryBarrierCount,
    const VkImageMemoryBarrier*                 pImageMemoryBarriers);

最讓我困惑的是 srcStageMask 與 dstStageMask ,在 Direct3D12 的 Barrier 並沒有這一類的參數,所以使我困惑,這兩個參數的作用我在  BARRIERS IN VULKAN : THEY ARE NOT THAT DIFFICULT 裡找到了解答,先看下圖
Pipeline stage 與 Barrier

所謂的"Stage"指的是平行處理區塊,"srcStage"可以解釋成 Barrier 之前的平行處理區塊,"dstStage"可以解釋成 Barrier 之後的平行處理區塊,如果要簡單地完成一個等上一個平行處理區塊完成再執行之後的平行處理區塊,可以像以下
vkCmdPipelineBarrier(
  hCommandBuffer,
  VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
  VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
  0,
  0, nullptr,//MemoryBarriers
  0, nullptr,//BufferMemoryBarriers
  0, nullptr//ImageMemoryBarriers
);


這段可以解釋成等到 Barrier 之前的所有 DrawCall 都達到 VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT 狀態後,執行 MemoryBarriers 、 BufferMemoryBarriers 與 ImageMemoryBarriers ,但本例都是空值所以不需要執行,這些執行什麼時候要完成呢?在 Barrier 之後的所有 DrawCall 都達到 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT ,利用這樣的機制可以達到等待的效果。

  由於 render to texture 需要轉變 texture 的 layout ,有兩個時機,一是被當成 rendertarget 時要將 layout 從輸入變輸出,二是被當成輸入的 texture 時要將 layout 從輸出便輸入,這個做法在 Direct3D12 也有類似的作法,但問題也發生在這,當我企圖做這兩件事的時候不是發生繪製結果錯誤,就是得到 debug 的錯誤訊息 ,我試了將近一個禮拜,但還是無法得到正確的轉 layout 的作法,這問題到目前都還沒解決。

  在遇到上段所提及的問題後,由於想不到方法解決,所以在網上找到了範例,在 [ Github ] SaschaWillems/Vulkan 裡的 offscreen 專案,該範例實作了 render to texture ,但卻發現它完全沒用到 vkCmdPipelineBarrier() ,該專案改變 layout 是利用 render pass 的自帶機制,在 VkAttachmentDescription 裡可以指名輸入與輸出的 layout ,定義如下
typedef struct VkAttachmentDescription {
    VkAttachmentDescriptionFlags    flags;
    VkFormat                        format;
    VkSampleCountFlagBits           samples;
    VkAttachmentLoadOp              loadOp;
    VkAttachmentStoreOp             storeOp;
    VkAttachmentLoadOp              stencilLoadOp;
    VkAttachmentStoreOp             stencilStoreOp;
    VkImageLayout                   initialLayout;
    VkImageLayout                   finalLayout;
} VkAttachmentDescription;

看最後兩個參數 initialLayout 與 finalLayout ,這是一個自動轉 layout 的機制, initialLayout 代表的是要轉入前要轉的 layout , finalLayout  代表的是輸出後要轉的 layout ,目前我利用這個機制來轉 layout ,但我覺得這不是個好方法,為什麼這麼說呢?由於輸出的時候 render target 和 surface 使用的是不同的 layout ,就會需要兩個 render pass ,一個給 render target ,另一個給 surface,當輸出被交換了,render pass 也可能需要被交換,想當麻煩,但目前沒辦法找到用 vkCmdPipelineBarrier() 的正確轉法,只能先用這個方法替代。

  整體來說, Vulkan 的 barrier 做得比 Direcct3D12 還來得複雜,至少我在 Direct3D12 沒遇到問題,加上用 vkCmdPipelineBarrier() 轉 layout 的範例目前沒找到,只能用 render pass 得自帶機制來替代,這不是最好的解決方法,但目前只能這樣了。

參考資料

[ stackoverflow ] How to render to texture in Vulkan?
[ www.khronos.org ] vkCmdPipelineBarrier
Synchronization Examples
BARRIERS IN VULKAN : THEY ARE NOT THAT DIFFICULT
[ Github ] SaschaWillems/Vulkan

沒有留言:

張貼留言