Vulkan Cookbook 第四章 7 設定影象記憶體屏障
設定影象記憶體屏障
譯者注:示例程式碼點選此處
建立影象用於各種目的,它們用作紋理,通過描述符集將它們繫結到管線,作為渲染目標,或者作為交換鏈中的可呈現影象。我們可以將資料複製到影象或從影象複製資料(這些也是在影象建立期間定義的獨特用法)。
在我們開始將影象用作任何目的之前,每次想要更改給定影象的當前使用情況時,都需要知道驅動程式有關此操作的資訊。我們通過使用在命令緩衝區記錄期間設定的影象記憶體屏障來完成此操作。
做好準備
出於本節內容的目的,引入了自定義結構型別ImageTransition。它有以下定義:
struct ImageTransition { VkImage Image; VkAccessFlags CurrentAccess; VkAccessFlags NewAccess; VkImageLayout CurrentLayout; VkImageLayout NewLayout; uint32_t CurrentQueueFamily; uint32_t NewQueueFamily; VkImageAspectFlags Aspect; };
CurrentAccess和NewAccess成員定義了給定影象在屏障之前的記憶體操作型別以及屏障之後的型別。
在Vulkan中用於不同目的影象可能具有不同的記憶體組織。換句話說,記憶體可以針對不同影象使用特定佈局。當我們想要以不同的方式開始使用佈局時,需要改變這個記憶體佈局。這是通過CurrentLayout和NewLayout完成的。
如果影象以獨佔共享模式建立,則記憶體屏障還准許我們轉移佇列族所有權。在CurrentQueueFamily成員中我們定義了一個族的索引,在NewQueueFamily中我們需要為將在屏障之後使用的影象的佇列定義一個族的索引。當我們不想轉移所有權時,還可以為兩者使用VK_QUEUE_FAMILY_IGNORED特殊值。
Aspect(方面)成員定了影象的使用“上下文”。我們可以從顏色,深度或模板方面進行選擇。
怎麼做...
1.需要為每個要設定屏障的影象準備引數。將它們儲存在名為image_transitions的std::vector<ImageTransition> 型別向量中。
·Image為影象控制代碼
·CurrentAccess為到目前為止影象的記憶體操作型別
·NewAccess為在屏障之後對影象執行的記憶體操作型別
·CurrentLayout為當前影象的記憶體佈局
·NewLayout為屏障之後改變的影象記憶體佈局
·CurrentQueueFamily為到目前為止引用影象的佇列族的索引(如果我們不想傳輸佇列所有權,則為VK_QUEUE_FAMILY_IGNORED值)。
·NewQueueFamily為從現在起將引用的影象的佇列族索引(如果不想轉移佇列所有權,則為VK_QUEUE_FAMILY_IGNORED值)。譯者注:這裡的佇列族我想也是從屏障之後發生改變,但是原文這句並沒有寫屏障(barrier)之後,太不嚴謹了!
·Aspect為影象方面(顏色,深度或模板)。
2.建立一個名為image_memory_barriers的std::vector<VkImageMemoryBarrier>型別向量變數。
3.將image_transitions變數的每個元素新增到image_memory_barriers向量。對新新元素的成員使用如下值:
·sType為VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER
·pNext為nullptr
·srcAccessMask為CurrentAccess
·dstAccessMask為NewAccess
·oldLayout為CurrentLayout
·newLayout為NewLayout
·srcQueueFamilyIndex為CurrentQueueFamily
·dstQueueFamilyIndex為NewQueueFamily
·image為影象控制代碼
·對subresourceRange的成員設定如下值:
·aspectMask為Aspect
·baseMipLevel為0
·levelCount為VK_REMAINING_MIP_LEVELS
·baseArrayLayer為0
·layerCount為VK_REMAINING_ARRAY_LAYERS
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, 0, nullptr, static_cast<uint32_t>(image_memory_barriers.size()), &image_memory_barriers[0] )並在第一個引數中提供命令緩衝區的控制代碼。第二和第三個引數中提供generating_stages和consume_stages變數。應該在倒數第二個引數中提供image_memory_barriers向量的元素數量,並且最後一個引數應該指向image_memory_barriers向量的第一個元素。
這個怎麼運作...
在Vulkan中,操作在管線中處理。既使操作的處理需要按照提交的順序開始,管線的某些部分仍然可以同時執行,但有時,我們可能需要同步這些操作並告訴驅動程式我們希望其中一些操作等待其他操作的結果。
提示:記憶體屏障用於定義命令緩衝區執行中的時刻,後續命令應等待先前的命令完成其操作。它們還會導致這些操作的結果對其他操作可見。
記憶體操作需要屏障才能在以後的命令中可見。如果作業系統將資料寫入影象並進一步操作將從中讀取,我們需要使用影象記憶體屏障。反之亦然,覆蓋影象資料的操作應該等待早期操作停止從中讀取資料。在這兩種情況下,如果不這樣做,將使影象的內容無效。但是這種情況儘可能少,否則我們的應用可能會遭到效能損失。這是因為命令緩衝區執行中的這種暫停導致圖形硬體處理管線的停頓,因此浪費了時間:
影象記憶體屏障還用於定義影象使用方式的變化。這種使用變化通常還要求我們同步提交的操作,這就是為什麼需要通過記憶體屏障來完成。為了更改影象的用法,我們需要定義在屏障之前和之後對影象執行的記憶體操作型別(記憶體訪問)。我們還指定了屏障之前的記憶體佈局以及屏障之後應該如何佈局記憶體。這是因為影象在用於不同目的時可能具有不同的記憶體組織。例如,從著色器內的影象採集資料可能需要對它們進行快取記憶體,使得相鄰紋素在記憶體中是鄰居,但是,當記憶體線性佈局時,可以更快地將資料寫入影象。這就是為什麼在Vulkan中引入影象佈局的原因。每個影象使用都有自己的指定佈局。有一個通用佈局,可用於所有目的。但是,簡易不要使用常規佈局,因為這可能會影響某些硬體平臺的效能。
提示:為獲得最佳效能,建議使用指定的影象記憶體佈局以用於特定用途,但如果過於頻繁執行佈局轉換則必須小心。
定義用法更改的引數是通過VkImageMemoryBarrier型別的變數指定的,如下所示:
std::vector<VkImageMemoryBarrier> image_memory_barriers;
for( auto & image_transition : image_transitions ) {
image_memory_barriers.push_back( {
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
nullptr,
image_transition.CurrentAccess,
image_transition.NewAccess,
image_transition.CurrentLayout,
image_transition.NewLayout,
image_transition.CurrentQueueFamily,
image_transition.NewQueueFamily,
image_transition.Image,
{
image_transition.Aspect,
0,
VK_REMAINING_MIP_LEVELS,
0,
VK_REMAINING_ARRAY_LAYERS
}
} );
}
但是為了使屏障正常工作,我們需要定義到目前為止使用影象的管道階段,以及從現在開始使用的影象:
在上圖中,我們可以看到兩個管線屏障的例子。在左側,顏色由片段著色器生成,並且在此進行所有片段測試(深度測試,混合)之後顏色資料被寫入影象。然後在連續命令的頂點著色器中使用該影象,這樣的設定很可能在管線中產生停頓。
右側的示例顯示了影象命令中的另一個依賴項。這裡資料被寫入頂點著色器中的資源。然後下一個命令的片段著色器使用這些資料。這一次,頂點著色器的所有例項很可能在下一個命令的片段著色器開始執行之前完成其作業。這就是為什麼重要的是降低管道屏障的數量,並且如果需要正確設定繪製命令並選擇屏障的管道階段,屏障的參分為生成階段和消費階段,呼叫如下函式為我們的影象集設定屏障。
譯者注:上面這個例子作者寫的不明不白的,看到後面明白了再回來補充解釋。
if( image_memory_barriers.size() > 0 ) {
vkCmdPipelineBarrier( command_buffer, generating_stages, consuming_stages, 0, 0, nullptr, 0, nullptr, static_cast<uint32_t>(image_memory_barriers.size()), image_memory_barriers.data() );
}
如果影象以相同的方式多次使用,並且在兩者之前沒有用於其他目的,則在實際使用影象之前我們不需要設定屏障,我們將它們設定為使用更改的訊號,而不是使用自身。
譯者注:我們將它們設定為使用更改的訊號,而不是使用自身。原文We set it to signal the usage change, not the usage itself。暫時沒看懂是什麼鳥意思。好像只有之前訊號量裡提到過signal,但是好像和這裡沒有關係。先不管了向後看