【Vulkan學習記錄-基礎篇-4】Vulkan中的同步機制

提醒:本文不含任何圖,讀起來可能有些枯燥,如果對Vulkan中的同步還沒有任何瞭解的,建議先參考後面給出的鏈接 [1][2][3]

在Vulkan中,對資源讀寫所需要做的同步是應用程序的職責,Vulkan本身只提供了很少的隱式同步機制,其餘的都需要在程序中顯式地使用Vulkan中的同步機制來實現。

提交順序

提交順序是Vulkan中的一個非常基本的概念,它本身並不具有任何同步的意義,但是不管是Vulkan提供的隱式同步,還是用戶要自己實現的顯式同步,都要以這個精確的概念爲前提。
在Vulkan中,用戶需要將命令寫入CommandBuffer中,然後把一個或多個CommandBuffer寫入到一個或多個VkSubmitInfo中,再把一個或多個VkSubmitInfo傳給vkQueueSubmit,讓Queue開始執行傳入的命令,由此,從高往低,提交順序爲:
1.在CPU上通過多次vkQueueSubmit提交了一系列命令,這些命令的提交順序爲調用vkQueueSubmit從前往後的順序,即先通過vkQueueSubmit提交的命令一定在後通過vkQueueSubmit提交的命令之前。

2.在同一次vkQueueSubmit中,傳入了一個或多個VkSubmitInfo,這些VkSubmitInfo中的命令,按照VkSubmitInfo的下標順序排列,即在pSubmits所指向的VkSubmitInfo數組中,下標靠前的VkSubmitInfo中所記錄的所有命令都在下標靠後的VkSubmitInfo中所記錄的所有命令之前。

3.在同一個VkSubmitInfo中,填入了一個或多個CommandBuffer,這些CommandBuffer中的命令的提交順序爲按照這些CommandBuffer的下標順序,類似2中的順序。

4.在同一個CommandBuffer中,所記錄的命令分爲兩種:
一是不在RenderPass中的命令,即除去所有在vkCmdBeginRenderPass和vkCmdEndRenderPass之間的命令,這些命令的提交順序爲按照在CPU上寫入CommandBuffer時的順序。
二是在RenderPass中的命令,在RenderPass中的命令,只定義在同一SubPass中的其他命令的提交順序,這些命令的提交順序也是按照在CPU上寫入CommandBuffer時的順序。注意,如果幾個命令在vkCmdBeginRenderPass和vkCmdEndRenderPass之間,但是它們不在同一SubPass中,那麼它們之間是不存在任何提交順序的。

Vulkan提供的隱式同步

有了提交順序的概念,就可以定義一些隱式的同步機制,即不需要用戶自己去實現,一定會默認遵循的同步。
Spec中提到的隱式同步有:

1.所有的Action類命令(Draw、Transfer、Clear、Copy等)以及顯示地使用同步機制的命令(這個在之後會介紹),這些命令在執行VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT時,會遵循提交順序。即這些命令開始執行的順序,是嚴格遵循提交順序的。(但這並不意味着這些命令結束執行時的順序會有什麼約束,所有的這些命令,到底是哪個先結束,隱式同步並沒有嚴格的規定,也就是說任何一個命令都有可能最先結束。)

2.所有的設置狀態類的命令(bind pipelines, descriptor sets, and buffers等),由於它們不需要在GPU上執行,它們只負責設置CPU上相應CommandBuffer的狀態,所以它們的執行順序,遵循它們在CPU上寫入CommandBuffer時的順序。

3.所有的Draw類命令在處理Primitive時,首先遵循提交順序,即先提交的Draw中的Primitive會先被處理。而在一個Draw內所提交的Primitive,會按照頂點和索引的下標順序執行。

4.ImageLayout的轉移,是通過ImageMemoryBarrier實現的(也是一種顯式的同步原語),它們遵循提交順序,即先提交的先轉移。

以上差不多就是Vulkan中所提供的隱式同步了,還有一些細節可以參考Spec6.2中相關內容。
其他的所有需要用到同步的情景(比如寫後寫問題,寫後讀問題,讀後寫問題),都需要手動地通過顯式的同步機制來實現。

Vulkan同步的基本概念

在使用同步類命令時,往往會填一些讓人不太容易理解的參數,比如看pipeline barrier以及image memory 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);
typedef struct VkImageMemoryBarrier {
    VkStructureType            sType;
    const void*                pNext;
    VkAccessFlags              srcAccessMask;
    VkAccessFlags              dstAccessMask;
    VkImageLayout              oldLayout;
    VkImageLayout              newLayout;
    uint32_t                   srcQueueFamilyIndex;
    uint32_t                   dstQueueFamilyIndex;
    VkImage                    image;
    VkImageSubresourceRange    subresourceRange;
} VkImageMemoryBarrier;

可以看到這裏面有很多莫名其妙的參數,比如VkPipelineStageFlags、VkAccessFlags、VkImageLayout等,初看時,可能會認爲這些東西和同步根本沒有任何關係,所謂同步,是想讓一個命令的某個過程,一定要等待另一個命令的某個過程完成後才能開始執行,所以這些參數有什麼意義呢?

VkPipelineStageFlags
Vulkan中並不提供任何命令級別的同步,即明確地指定某兩個命令之間需要滿足什麼同步。所有需要同步的命令,都會將其執行過程劃分爲若干個階段,所有的命令都會在流水線上執行,只是不同類型的命令,它們的階段劃分是不同的。當我們在Vulkan中使用同步機制時,都是以流水線階段爲單位,即某個流水線階段上執行的所有命令,會在當前階段暫停,等待另一個流水線階段上的所有命令在相應的階段執行完全後,再開始執行。VkPipelineStageFlags就代表流水線階段,在Spec 6.1.2節中給出了它的所有可能取值,以及對於不同類型命令的流水線劃分規則。

VkAccessFlags
Vulkan中的同步不僅控制操作執行的順序,還要控制緩存的寫回,即內存數據的同步。什麼意思呢?不管是CPU還是GPU,存儲系統都是按級劃分的,比如主存、L2 Cache、L1 Cache等等,爲了簡化討論,我們就假設只有這三級的存儲結構(然而實際肯定是要更加複雜一些的)。從左往右,讀寫的速率會依次提升,但是價格也會依次增高,所包含的數據量也會依次減少。觀察一下VkAccessFlags的取值(Spec 6.1.3節),它們的命名都是按照:VK_ACCESS_ + Stage + Resource Read/Write ,代表對內存進行讀寫,所以VkAccessFlags是爲了表達流水線階段對於內存的讀寫操作。
每一個這樣的Access,都會從某一個L1 Cache上讀或寫數據,具體地選擇哪一個L1 Cache來進行讀或寫,由驅動決定。那麼這樣就會有一個問題,假如某一個資源,比如一個Buffer,在某一次Access中被修改了,那麼那一次的寫操作只會對相應的那個L1 Cache有效果,在此後的某個Access中,需要再讀這個Buffer的相關數據,如果只做執行過程間的同步(設置一個同步,讓這次的讀操作在前一個Access的寫操作完成後再執行),這次讀到的很有可能就不是這個Buffer的最新的值,因爲儘管這次讀操作確實是在寫操作完成之後才執行的,但是那一次寫操作只將數據寫到了L1 Cache上,我們需要讓這次讀操作用到的L1 Cache也要包含的是修改以後得到的值纔行。所以你會發現,所有的MemoryBarrier,都要包含VkAccessFlags,對於MemoryBarrier,它所控制的同步執行過程爲:
①等待srcStageMask所代表的流水線階段上的所有操作執行完成
②等待srcAccessMask所代表的內存操作available
③等待相應的available內存對於dstAccessMask所代表的內存操作是visible的
④喚醒dstStageMask所代表的流水線階段上的所有操作
那麼什麼叫做available和visible呢?在spec上說的也比較含糊,我也沒有找到它們的精確定義,以下是我個人的猜測
讓某一個內存操作available,指的是這一次操作,可以被某一個內存區域(host、device等)所看到(注意這裏的用語——可以看到,我猜測這裏是將這次操作寫回到了某一個全局可見的Cache或者主存或者跟可以通過BUS傳遞到主存的某塊內存中,但是還並沒有寫回到後面這個Access所用到的L1 Cache中)
而visible指的就是讓一個available的內存,它的最新值寫入到了該Access所要用到的相應的L1 Cache中。

上述討論也說明一點,Vulkan中的所有同步,大體上可以分爲兩種,一種是操作執行類的同步,即操作執行的先後順序,另一種是內存類的同步,即讓內存數據在各種不同的L1 Cache之間同步,當進行內存數據的顯式同步時,都需要用到某一類MemoryBarrier。

還要注意,並不是所有的同步情景一定都得用到內存類的同步,比如讀後寫問題,這個情景下,只需要控制寫操作在讀操作之後進行就可以了,讀操作不涉及到任何內存的修改,因此不需要將修改以後的內存變得available、visible之類的操作。

VkImageLayout
Vulkan中的同步原語還可以有附加的作用。比如一個VkImageMemoryBarrier,它附帶的oldLayout和newLayout參數,可以實現讓相應的Image進行Layout的轉移。每一個Image都會處於某一個Layout下,一些操作只能在特定的流水線階段對特定Layout下的Image才能使用,所以我們需要在程序執行過程的每一個階段細緻地設置每一個Image的Layout,Layout的轉移就是通過這樣的一個同步機制來實現的。這樣你可能會有疑問,爲什麼Vulkan會將這樣一個操作設計在一個同步原語中呢?事實上Layout的轉移,無非也就是將相應的Image數據通過不同的壓縮、排列等方式經過變換以後寫到內存中的其他位置以供特殊的需求使用,即Layout的轉移實際就是內存的讀寫操作,因此它需要用到內存數據的同步。Spec中明確指出,Layout進行轉移時,相應的Image的內存一定得是available的,一個ImageMemoryBarrier所包含的同步執行過程爲:
①等待srcStageMask所代表的流水線階段上的所有操作執行完成
②等待srcAccessMask所代表的內存操作available
③進行ImageLayout Transition
④等待相應的available內存對於dstAccessMask所代表的內存操作是visible的
⑤喚醒dstStageMask所代表的流水線階段上的所有操作

Vulkan中不存在只在一個CommandBuffer內有效的同步機制,所有的同步,在全局上都應該認爲是對一個Queue中的所有命令有效果。
下面來逐個介紹Vulkan中的同步原語,它們的作用都是大體相同的,即控制 操作執行的同步或者內存數據的同步,不同之處在於它們的粒度以及作用的範圍。

下面來逐個介紹Vulkan中的各個同步原語

Fence

Fence用於同步渲染隊列和CPU之間的同步,它有兩種狀態——signaled和unsignaled。
在創建Fence時可以指定它的初始狀態;
在調用vkQueueSubmit時,可以傳入一個Fence,這樣當Queue中的所有命令都被完成以後,Fence就會被設置成signaled的狀態;
通過調用vKResetFences可以讓一個Fence恢復成unsignaled的狀態;

vkWaitForFences會讓CPU在當前位置被阻塞掉,然後一直等待到它接受的Fence變爲signaled的狀態,這樣就可以實現在某個渲染隊列內的所有任務被完成後,CPU再執行某些操作的同步情景。

舉一個具體的例子:假如現在SwapChain中一共有3個Image,然後創建了3個CommandBuffer分別代表在渲染到相應Image時所需要執行的所有命令。在每一幀渲染時,我們需要獲取當前需要渲染到的Image的編號,然後使用對應的CommandBuffer,傳入渲染隊列中,執行渲染命令。那麼現在就有一個問題,一個CommandBuffer,如果它還沒有被執行完全,那麼它是不能夠再次被開始執行的。也就是說上面所說的那個獲取CommandBuffer後,把它傳入渲染隊列執行的這樣一個CPU上的操作一定要在這個CommandBuffer在上一次被執行完全以後纔可以執行。所以這裏就遇到了一個渲染隊列和CPU之間的一個同步情景,此時可以對每個CommandBuffer分別設置一個Fence來實現這樣的一種同步,大體的實現如下(這裏用到了Semaphore,不過可以先只關注Fence):

	void draw()
	{
		// Get next image in the swap chain (back/front buffer)
		VK_CHECK_RESULT(swapChain.acquireNextImage(presentCompleteSemaphore, &currentBuffer));

		// Use a fence to wait until the command buffer has finished execution before using it again
		VK_CHECK_RESULT(vkWaitForFences(device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX));
		VK_CHECK_RESULT(vkResetFences(device, 1, &waitFences[currentBuffer]));

		// Pipeline stage at which the queue submission will wait (via pWaitSemaphores)
		VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
		// The submit info structure specifices a command buffer queue submission batch
		VkSubmitInfo submitInfo = {};
		submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
		submitInfo.pWaitDstStageMask = &waitStageMask;									// Pointer to the list of pipeline stages that the semaphore waits will occur at
		submitInfo.pWaitSemaphores = &presentCompleteSemaphore;							// Semaphore(s) to wait upon before the submitted command buffer starts executing
		submitInfo.waitSemaphoreCount = 1;												// One wait semaphore																				
		submitInfo.pSignalSemaphores = &renderCompleteSemaphore;						// Semaphore(s) to be signaled when command buffers have completed
		submitInfo.signalSemaphoreCount = 1;											// One signal semaphore
		submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];					// Command buffers(s) to execute in this batch (submission)
		submitInfo.commandBufferCount = 1;												// One command buffer

		// Submit to the graphics queue passing a wait fence
		VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentBuffer]));
		
		// Present the current buffer to the swap chain
		// Pass the semaphore signaled by the command buffer submission from the submit info as the wait semaphore for swap chain presentation
		// This ensures that the image is not presented to the windowing system until all commands have been submitted
		VK_CHECK_RESULT(swapChain.queuePresent(queue, currentBuffer, renderCompleteSemaphore));
	}

由此可見Fence是一種比較粗粒度的同步原語,另一個需要關心的問題是:上面只提到了它在操作執行方面的同步,而Vulkan中非常重要的另一種——內存數據的同步,Fence能不能做到呢?

事實上,Fence也具備這種內存數據同步的功能,但是並不需要手動地指定,在使用Fence時,如果它一旦被signaled,那麼使用這個Fence的Queue中的所有的命令如果涉及到了對內存的修改,那麼這些Memory Access就一定會再signaled之前在Device上變得available(注意只是在Device上有這個效果,如果在CPU上讀相關的內存數據,並不能保證讀到的是最新的值,所以如果確保CPU也能夠獲取最新的值的話,就需要再用上其他的同步原語)

Semaphore

Semaphore用於渲染隊列每次提交的一批命令(batch)之間的同步,和Fence一樣,它也有兩種狀態:signaled和unsignaled。
調用vkQueueSubmit提交命令時,會填充VkSubmitInfo結構,而這個結構體中需要填入pWaitSemaphores、pSignalSemaphores、pWaitDstStageMask,表示此次提交的所有命令在執行到pWaitDstStageMask時,要停下,必須要等待pWaitSemaphores所指向的所有Semaphore的狀態變成signaled時纔可以繼續執行,此次提交的所有命令結束以後,pSignalSemaphores所指向的所有Semaphore的狀態都會被設置成signaled。

可以看到Fence和Semaphore都會在vkQueueSubmit時作爲參數傳入,不同之處是,Fence用於阻塞CPU直到Queue中的命令執行結束(GPU、CPU之間的同步),而Semaphore用於不同的命令提交之間的同步(GPU、GPU之間的同步)

在Fence中給出的那段代碼中,使用了兩個Semaphore,用於控制queue提交的present命令(注意swapChain.queuePresent()的實現也是通過queue提交了一個執行present的命令)和render命令之間的同步:在渲染時,需要將渲染的結果寫入到ColorAttachment中,我們必須要等待上一次把這個ColorAttachment給present到屏幕上的命令結束以後,纔可以完成這個寫入操作;
並且,將當前幀渲染結果顯示到屏幕上的這個present命令,必須要等到當前幀的render命令完全執行結束以後,纔可以開始執行。

和Fence一樣,Semaphore也是一種粗粒度的同步,它本身也提供了隱含的內存數據的同步:
1.當讓一個semaphore變成signaled時:semaphore之前的所有命令涉及到的內存寫操作,都會在semaphore變成signaled之前,達到available的狀態
2.當等待一個semaphore變成signaled時:在semaphore變成signaled之後,所有暫停的命令被重新喚醒繼續執行之前,所有此後相關的Memory Access,都會達到visible的狀態。
也就是說在使用Fence和Semaphore時,一般是不需要對GPU上有關的任何Memory Access做同步處理,這些都會被自動完成。但是,這些隱含的同步只是針對GPU的,CPU上所需要的內存數據同步操作必須由應用程序顯式完成,比如:當CPU需要讀一個經由GPU修改過的內存數據,就需要加一個MemoryBarrier來確保CPU讀到的是最新的數據。

還有一點值得注意的是,在討論Fence和Semaphore時,都提到了vkQueueSubmit函數,這個函數本身也是隱含了一個內存數據的同步的:就是CPU上所有的內存修改操作,都會在GPU讀寫之前,對GPU而言變成available的,並且對於所有之後GPU上的MemoryAccess,它們都是visible的。

Event

Event用於同步提交到同一隊列的不同命令,或者同步CPU和隊列。它同樣也具有兩種狀態——signaled和unsignaled,與Fence不同的是,它的狀態改變既可以在CPU上完成,也可以在GPU上完成,並且它是一種細粒度的同步機制。注意:Event不能用於不同隊列的命令之間的同步。

在CPU上,可以調用vkSetEvent來使一個Event變成Signaled的狀態;可以調用vkResetEvent來使一個Event變成Unsignaled的狀態;可以調用vkGetEventStatus來獲取一個Event的當前狀態,可以利用這個狀態來對CPU進行阻塞。

而在GPU上:
1.可以通過vkCmdSetEvent命令來使得一個Event變成Signaled狀態,此時該命令附帶了一個操作執行同步:根據提交順序,所有在該命令之前的所有命令都必須在此次把Event設置Signaled狀態之前完成。
2.可以通過vkCmdResetEvent命令來使得一個Event變成Unsignaled狀態,此時該命令附帶了一個操作執行同步:根據提交順序,所有在該命令之前的所有命令都必須在此次把Event設置Unsignaled狀態之前完成。

這裏有一點非常需要注意:vkCmdSetEvent和vkCmdResetEvent不能夠在一個RenderPass內被執行,原因可以參見[4]。
3.

void vkCmdWaitEvents(
VkCommandBuffer                             commandBuffer,
uint32_t                                    eventCount,
const VkEvent*                              pEvents,
VkPipelineStageFlags                        srcStageMask,
VkPipelineStageFlags                        dstStageMask,
uint32_t                                    memoryBarrierCount,
const VkMemoryBarrier*                      pMemoryBarriers,
uint32_t                                    bufferMemoryBarrierCount,
const VkBufferMemoryBarrier*                pBufferMemoryBarriers,
uint32_t                                    imageMemoryBarrierCount,
const VkImageMemoryBarrier*                 pImageMemoryBarriers);

該函數直接顯式定義一個同步(比較繁瑣):
首先是操作執行的同步:
A:pEvents所指向的所有Event被Signal的操作,以及這些Event被Signal之前的所有命令在指定的srcStageMask時的操作
B:根據提交順序,在這條命令之後的所有命令在指定的dstStageMask時的操作
B一定要在A完成之後才能開始進行

而內存數據的同步則來自於後面6個參數所給出的MemoryBarrier,如果不傳入任何MemoryBarrier,那麼就不具有任何的內存數據的同步。

來看一個例子:
1.vkCmdDispatch
2.vkCmdDispatch
3.vkCmdSetEvent(event, srcStageMask = COMPUTE)
4.vkCmdDispatch
5.vkCmdWaitEvent(event, dstStageMask = COMPUTE)
6.vkCmdDispatch
7.vkCmdDispatch

那麼這裏實現的就是:{6,7}命令在指定的stage要停下來,等{1,2}在指定的stage的所有操作都完成後纔可以繼續重新開始執行。而命令{4}並不受此次同步的影響,它的執行過程是不受到約束的。
注意這裏省略掉了內存數據的同步,如果{6,7}中所要讀/寫的數據,被{1,2}修改過的話,就需要在命令{5}中傳入MemoryBarrier來實現這個同步。

關於MemoryBarrier馬上就會解釋。

Barrier

pipeline 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);

現在再看應該比較輕鬆了。它本身定義了一個操作執行的同步:
A:在這條命令之前的所有的命令在srcStageMask所指示的stage的操作;
B:在這條命令之後的所有的命令在dstStageMask所指示的stage的操作
B一定要在A執行完全後纔可以開始執行。
同時後6個參數代表的MemoryBarrier實現內存數據的同步。
可以發現PipelineBarrier和Event非常像,區別在於Event的Signal和Wait可以在兩個地方,而PipelineBarrier直接將命令序列一分爲二。

關注一下參數dependencyFlags:
如果它的取值中包含VK_DEPENDENCY_BY_REGION_BIT,那麼任何涉及到 framebuffer-space的同步,都是framebuffer-local的,什麼意思呢?

所謂的framebuffer-space,就是指以下4個STAGE:

VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT

任何同步原語,只要包含以上四種STAGE之一,我們就可以說這個同步是涉及到framebuffer-space的。
而根framebuffer-space有關的同步,指的是流水線在對framebuffer執行讀寫時必須要遵循的同步,framebuffer中的讀寫都是以framebuffer-region爲單位進行,一個framebuffer-region指的是(x,y,layer,sample),如果一個同步是framebuffer-local的:那麼B中的任何對某一framebuffer-region的操作,只會在A中的同一位置的framebuffer-region的操作結束後開始執行,不同framebuffer-region之間不存在同步關係。而如果一個同步是framebuffer-global的,那麼對一個framebuffer上的所有framebuffer-region都是存在同步關係的,也即B中的任何一個操作,都必須要等到A中所有的framebuffer-region被處理完全以後才能開始執行。

還有一點非常重要:如果在一個subpass內使用vkCmdPipelineBarrier,那麼必須讓這個subpass所在的renderpass爲它聲明一個self-dependency,即必須存在一個VkSubpassDependency,它的srcSubpass和dstSubpass都是這個subpass的index(Spec 6.6.1),原因可以參考[4]

[3]中給出了大量的使用Barrier的例子,可以仔細閱讀。

Memory Barrier
前面反覆提及過了,內存數據的同步需要使用Memory Barrier完成,然而MemoryBarrier的作用不止於此,它還可以進行QueueFamily的轉移、ImageLayout的轉移。
ImageLayout的轉移前面已經說過了,這裏介紹一下QueueFamily的轉移:在創建Vulkan中的資源時,都指定了一個Queue,表示當前創建的這個資源的所有權是指定的這個Queue的,如果在創建時指定了VkSharingMode爲VK_SHARING_MODE_EXCLUSIVE,那麼這個資源如果要想被某些命令使用,那麼這些命令提交到的隊列就必須得具有它的所有權,如果沒有就必須進行QueueFamily的轉移。

Vulkan中一共有三種MemoryBarrier:

typedef struct VkMemoryBarrier {
    VkStructureType    sType;
    const void*        pNext;
    VkAccessFlags      srcAccessMask;
    VkAccessFlags      dstAccessMask;
} VkMemoryBarrier;
typedef struct VkBufferMemoryBarrier {
    VkStructureType    sType;
    const void*        pNext;
    VkAccessFlags      srcAccessMask;
    VkAccessFlags      dstAccessMask;
    uint32_t           srcQueueFamilyIndex;
    uint32_t           dstQueueFamilyIndex;
    VkBuffer           buffer;
    VkDeviceSize       offset;
    VkDeviceSize       size;
} VkBufferMemoryBarrier;
typedef struct VkImageMemoryBarrier {
    VkStructureType            sType;
    const void*                pNext;
    VkAccessFlags              srcAccessMask;
    VkAccessFlags              dstAccessMask;
    VkImageLayout              oldLayout;
    VkImageLayout              newLayout;
    uint32_t                   srcQueueFamilyIndex;
    uint32_t                   dstQueueFamilyIndex;
    VkImage                    image;
    VkImageSubresourceRange    subresourceRange;
} VkImageMemoryBarrier;

可以看到後兩種Barrier無非就是針對Buffer和Image添加了一些參數,以及加入了QueueFamily的轉移、ImageLayout的轉移。
此前說過了,有關內存數據的讀寫,我們要關注此前對它的寫操作是否是Available的,當前對它的讀寫操作是不是visible的,以確定當前的讀寫操作能夠獲取最新的值。
那麼這裏三個barrier所共同都需要制定的srcAccessMask和dstAccessMask就具備此含義。所有的MemoryBarrier都需要搭配PipelineBarrier或者Event使用,PipelineBarrier和Event都定義了執行操作的Stage,並給出了操作的同步,那麼在MemoryBarrier中,我們就需要指定好在相應的Stage中的Access,這樣就可以保證此前的對內存的Access一定會available,並且對後面的dstAccessMask是visible的。

本文一定有不少不嚴謹或者是不正確的地方,希望讀者能不吝斧正。

相關鏈接:
[1]Vulkan Specification
[2]Yet another blog explaining Vulkan synchronization
[3]Synchronization Examples
[4]Why some commands can be recorded only outside of a render pass?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章