1. 程式人生 > >Vulkan Cookbook 第四章 3 設定緩衝區記憶體屏障

Vulkan Cookbook 第四章 3 設定緩衝區記憶體屏障

設定緩衝區記憶體屏障

譯者注:示例程式碼點選此處

緩衝區可用於各種目的。對於每個緩衝區,我們可以上傳資料或從中複製資料通過描述符集將緩衝區繫結到管線。並在著色器中將其用作資料來源,或者可以在著色器中將資料儲存在緩衝區中。

我們不僅在緩衝區建立期間,而且在預期使用之前必須向驅動程式通知使用目的。當我們已經使用了一個緩衝區並且從現在開始希望以不同的方式使用它時,必須告訴驅動程式緩衝區使用目的的變化。這是通過緩衝記憶體屏障完成的。在命令緩衝區記錄期間,它們被設定為管線屏障的一部分(請參閱第三章,命令緩衝區和同步中的開始命令緩衝區記錄操作)。

譯者注:恩。。稍微有點亂,捋一捋。屏障(barriers)是用於指定一個緩衝區內的起始位(offset)和尺寸(size)區域的用途,在特定管線階段才可以使用這一快取區域。注意!!!緩衝區(butter)是VkBuffer型別和命令緩衝區(command_buffer)是

VkCommandBuffer不是一回事!!緩衝區是用於存放資料的,而命令緩衝區是儲存命令的!命令緩衝區見第三章建立命令池分配命令緩衝區等相關章節

做好準備...

出於本節內容的目的,我們將使用名為BufferTransition的自定義結果型別:其定義如下:

struct BufferTransition { 
  VkBuffer      Buffer;
  VkAccessFlags CurrentAccess;
  VkAccessFlags NewAccess;
  uint32_t      CurrentQueueFamily;
  uint32_t      NewQueueFamily;
};

通過這種結構,我們將定義想要用於緩衝記憶體屏障的引數。在CurrentAccess和NewAccess中,我們分別儲存有關緩衝區當前如何使用以及之後如何使用的資訊(被定義為給定緩衝區的記憶體操作型別)。當我們想要將所有權從一個佇列族的轉譯到另一個族時,將使用CurrentQueueFamily和NewQueueFamily成員。但只是當緩衝區建立期間指定了獨佔共享模式時才需要這樣做。

譯者注:看來Vulkan驅動並沒有幫我儲存當前的CurrentAccess和CurrentQueueFamily狀態,這大概是出於通用性和執行效率的考慮?切換的時候我們需要告訴他當前狀態才可以?還是我有什麼理解錯誤了。

怎麼做...

1.為每個緩衝區準備引數,將它們儲存在名為buffer_transitions的std::vector<BufferTransition>型別變數裡。對於每個緩衝區,儲存以下引數:
    1.對Buffer欄位設定緩衝區的控制代碼。
    2.CurrentAccess設定到目前為止緩衝區的記憶體操作型別
    3.NewAccess設定從現在開始(在屏障之後)將在緩衝區上執行的記憶體操作型別。
    4.CurrentQueueFamily設定到目前為止一直引用緩衝區的佇列族索引(如果不想轉移佇列所有權,則為VK_QUEUE_FAMILY_IGNORED值)。
 5.NewQueueFamily成員中從現在起將引用緩衝區的佇列族索引(如果不想轉移佇列所有權,則為VK_QUEUE_FAMILY_IGNORED值)。
2.建立一個名稱為buffer_memory_barriers的std::vector<VkBufferMemoryBarrier>型別變數。
3.將buffer_transitions每個元素新增到buffer_memory_barriers中。對新元素的成員使用以下值:
    ·sType為VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER
    ·pNext為nullptr
    ·srcAccessMask為CurrentAccess
    ·dstAccessMask為NewAccess
    ·srcQueueFamilyIndex為CurrentQueueFamily
    ·dstQueueFamilyIndex為NewQueueFamily
    ·buffer為緩衝區的控制代碼
    ·offset為0
    ·size為VK_WHOLE_SIZE
4.獲取命令緩衝區的控制代碼並將其儲存在名為command_buffer的VkCommandBuffer變數中。
5.確保command_buffer控制代碼所代表的命令緩衝區處於記錄狀態(為命令緩衝區啟用了記錄操縱)。
6.建立名為generating_stages的VkPipelineStageFlags位欄位型別變數。在此變數中,儲存表示到目前為止一直使用緩衝區的管線階段的值。
7.建立名為consume_stages的位欄位型別VkPipelineStageFlags的變數。在此變數中,儲存表示管線階段的值,緩衝區將在屏障之後使用。
8.呼叫vkCmdPipelineBarrier(command_buffer, generating_stages, consuming_stages, 0, 0, nullptr, 
 static_cast<uint32_t>(buffer_memory_barriers.size()), &buffer_memory_barriers[0], 0, nullptr )。在第一個引數中提供命令緩衝區的控制代碼,並且分別在第二個和第三個引數中提供generating_stages和consume_stages變數。buffer_memory_barriers向量的元素個數應該在第七個引數中提供,第八個引數應該指向第一個buffer_memory_barriers向量的元素。

這個怎麼運作...

在Vulkan中,提交到佇列的操作按順序執行,但它們是獨立的,有時某些操作可能會在之前的操作完成之前啟動。這種並行執行是當前圖形硬體最重要的效能因素之一,但有時候,一些操作應該等待早期操作的結果是至關重要的:這就是記憶體屏障派上用場的時候。

譯者注:提交到佇列的操作按順序執行,但它們是獨立的原文In Vulkan, operations that are submitted to queues are executed in order, but they are independent.為何順序執行它們還是獨立的?佇列操作是順序執行操作,而操作是獨立的看上去比較矛盾,難道在佇列中的操作還能並行嗎,但是前一句已經否定了並行。那看來這裡說的並行是指的佇列是並行的,所以操作是獨立的。也可能我理解錯了,要看看後面的章節再說。

提示:記憶體屏障用於定義命令緩衝區執行中的時刻,後續命令應該等待先前的命令完成其工作。它們還會導致這些操作的結果對其他操作可見。

譯者注:上面的提示也許並不全面,好像記憶體屏障是規定了緩衝區的範圍和這個範圍的目的以及這個範圍在命令緩衝區執行中可見的時刻,否則還要offset和size做什麼?

通過記憶體屏障(memory barriers),我們指定緩衝區的使用方式以及哪些管線階段一直執行到我們設定的屏障的位置。接下來需要定義在屏障之後的使用方式以及哪些管線階段將使用它。有了這些資訊,驅動程式可以暫停需要等待早期操作結果變為可用的操作,但執行根本不會引用緩衝區的操作

譯者注:原文In the case of buffers, through memory barriers, we specify how the buffer was used and which pipeline stages were using it up to the moment in which we placed a barrier. Next we need to define which pipeline stages will be using it and how, after the barrier. With this information, the driver can pause operations that need to wait for the results of earlier operations to become available, but execute operations that won't reference the buffer at all.我們指定緩衝區的使用方式以及哪些管線階段一直執行到我們設定的屏障的位置。這裡所說的的哪些管線表示的是generating_stages?,而一直執行到我們設定的屏障的位置,這個位置是哪裡?目前看好像是consuming_stages的位置。那這句話的意思是我們指定緩衝區的使用方式以及generating_stages設定的管線階段一直執行到consuming_stages管線的位置?還要定義在consuming_stages之後的記憶體使用目的。還有這個但執行根本不會引用緩衝區的操作是說的在記憶體不可能用的時間段操作不會引用特定緩衝區? 這部分作者寫的真是有很大的歧義,generating_stages和consuming_stages還是位欄位如果我指定了多個管線階段會怎樣?需要先看了後續章節再考慮清楚。如果你有什麼想法請回復。法克!

緩衝區的用法只能在建立期間定義。每個用法對應於可以通過其訪問緩衝區內容的記憶體操作的型別。以下是支援的記憶體訪問型別列表:
     ·當緩衝區的內容是間接繪製的資料來源時使用VK_ACCESS_INDIRECT_COMMAND_READ_BIT
     ·當緩衝區的內容在繪製操作期間用於索引時使用VK_ACCESS_INDEX_READ_BIT
     ·當緩衝區是在繪製期間讀取的頂點屬性索引源時VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT
     ·當緩衝區作為統一緩衝區通過著色器訪問時使用VK_ACCESS_UNIFORM_READ_BIT
     ·當可以在著色器內讀取緩衝區時(但不能作為統一緩衝區)使用VK_ACCESS_SHADER_READ_BIT
     ·當著色器將資料寫入緩衝區時使用VK_ACCESS_SHADER_WRITE_BIT
     ·當我們想要從緩衝區賦值資料時使用VK_ACCESS_TRANSFER_READ_BIT
     ·當我們想要將資料複製到緩衝區時使用VK_ACCESS_TRANSFER_WRITE_BIT
     ·當應用程式將讀取緩衝區的內容時(通過記憶體對映)使用VK_ACCESS_HOST_READ_BIT
     ·當應用程式將資料寫入緩衝區時(通過記憶體對映)使用VK_ACCESS_HOST_WRITE_BIT
     ·當以上未指定的任何其他方式讀取緩衝區的記憶體時使用VK_ACCESS_MEMORY_READ_BIT
     ·當以上未指定的任何其他方式寫入緩衝區的記憶體時使用VK_ACCESS_MEMORY_WRITE_BIT

記憶體操作需要屏障才能在之後的命令中可見。沒有它們,讀取緩衝區內容的命令可能會在內容被前面的操作正確寫入之前開始讀取它們。但是命令緩衝區執行中的這種中斷會導致圖形硬體處理管線中的停頓。不幸的是,這可能會影響我們應用程式的效能:

提示:我們應該在儘可能少的屏障中儘可能多的聚合緩衝區和所有權轉換

譯者注:上面那張圖沒看懂,他的意思是命令緩衝區被分到多個佇列去處理了?

要為緩衝區設定記憶體屏障,我們需要準備型別為VkBufferMemoryBarrier的變數。如果可能,我們應該在一個記憶體屏障中聚合多個緩衝區的資料。這就是為什麼VkBufferMemoryBarrier型別元素的向量看起來非常有用,可以像這樣填充:

std::vector<VkBufferMemoryBarrier> buffer_memory_barriers;

for( auto & buffer_transition : buffer_transitions ) {   
  buffer_memory_barriers.push_back( {
    VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, 
    nullptr, 
    buffer_transition.CurrentAccess, 
    buffer_transition.NewAccess,
    buffer_transition.CurrentQueueFamily, 
    buffer_transition.NewQueueFamily, 
    buffer_transition.Buffer,
    0,
    VK_WHOLE_SIZE 
  } );
}

接下來,我們在命令緩衝區中設定記憶體屏障。這是在命令緩衝區的記錄操作期間完成的:

if( buffer_memory_barriers.size() > 0 ) {
  vkCmdPipelineBarrier( command_buffer, generating_stages, consuming_stages, 0, 0, nullptr, static_cast<uint32_t>(buffer_memory_barriers.size()), buffer_memory_barriers.data(), 0, nullptr );
}

在屏障中,我們指定在屏障之後執行的命令的哪個管線階段應該等待屏障之前的執行的命令的哪個管線階段的結果。

記住,只有在使用改變時,我們才需要設定一個屏障。如果屏障區多次用於同一目的,我們不需要這麼做。假設想要從兩個不同的資源將資料複製兩次到緩衝區。首先需要設定一個屏障,通知驅動程式我們將執行涉及VK_ACCESS_TRANSFER_WRITE_BIT型別的記憶體訪問操作。之後,我們可以按照希望的方式將資料複製到緩衝區。接下來如果我們想要使用緩衝區,例如作為頂點緩衝區(在渲染期間頂點屬性的來源),需要設定另一個屏障,指示我們將從緩衝區中讀取頂點屬性的資料,這是由VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT表示。當完成繪製並且緩衝區將用於另一個目的時,及時我們想要再次將資料複製到緩衝區,還需要使用適當的引數設定記憶體屏障。

還有更多…

我們不需要為整個緩衝區設定屏障,只能為緩衝區的部分記憶體做這件事。為此,我們需要為給定緩衝區定義VkBufferMemoryBarrier型別變數的offset和size成員指定適當的值。通過這些成員定義了記憶體開始的內容,以及我們想要定義屏障的記憶體大小。這些值以計算機單位(位元組)指定。