[英]Do I need dedicated fences/semaphores per swap chain image, per frame or per command pool in Vulkan?
我已經閱讀了幾篇關於 CPU-GPU(使用圍欄)和 GPU-GPU(使用信號量)同步機制的文章,但仍然無法理解我應該如何實現一個簡單的渲染循環。
請看下面的簡單render()
function。 如果我做對了,最低要求是我們通過一組信號量image_available
和rendering_finished
確保vkAcquireNextImageKHR
、 vkQueueSubmit
和vkQueuePresentKHR
之間的 GPU-GPU 同步,正如我在下面的示例代碼中所做的那樣。
然而,這真的安全嗎? 所有操作都是異步的。 那么,即使先前調用的信號請求尚未觸發,在隨后的render()
調用中再次“重用” image_available
信號量真的安全嗎? 我認為不是,但是另一方面,我們使用的是相同的隊列(不知道圖形和表示隊列實際上是否相同是否重要),並且隊列內的操作應該按順序使用.. . 但是,如果我做對了,它們可能不會“作為一個整體”被消耗,並且可以重新排序......
第二件事是(同樣,除非我遺漏了什么)我顯然應該為每個交換鏈圖像使用一個柵欄,以確保與調用render()
的image_index
對應的圖像上的操作已經完成。 但這是否意味着我一定需要做一個
if (vkWaitForFences(device(), 1, &fence[image_index_of_last_call], VK_FALSE, std::numeric_limits<std::uint64_t>::max()) != VK_SUCCESS)
throw std::runtime_error("vkWaitForFences");
vkResetFences(device(), 1, &fence[image_index_of_last_call]);
在我打電話給vkAcquireNextImageKHR
之前? 然后我是否需要每個交換鏈圖像專用image_available
和rendering_finished
信號量? 或者也許每幀? 或者也許每個命令緩沖區/池? 我真的很困惑...
void render()
{
std::uint32_t image_index;
switch (vkAcquireNextImageKHR(device(), swap_chain().handle(),
std::numeric_limits<std::uint64_t>::max(), m_image_available, VK_NULL_HANDLE, &image_index))
{
case VK_SUBOPTIMAL_KHR:
case VK_SUCCESS:
break;
case VK_ERROR_OUT_OF_DATE_KHR:
on_resized();
return;
default:
throw std::runtime_error("vkAcquireNextImageKHR");
}
static VkPipelineStageFlags constexpr wait_destination_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit_info{};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.waitSemaphoreCount = 1;
submit_info.pWaitSemaphores = &m_image_available;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &m_rendering_finished;
submit_info.pWaitDstStageMask = &wait_destination_stage_mask;
if (vkQueueSubmit(graphics_queue().handle, 1, &submit_info, VK_NULL_HANDLE) != VK_SUCCESS)
throw std::runtime_error("vkQueueSubmit");
VkPresentInfoKHR present_info{};
present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present_info.waitSemaphoreCount = 1;
present_info.pWaitSemaphores = &m_rendering_finished;
present_info.swapchainCount = 1;
present_info.pSwapchains = &swap_chain().handle();
present_info.pImageIndices = &image_index;
switch (vkQueuePresentKHR(presentation_queue().handle, &present_info))
{
case VK_SUCCESS:
break;
case VK_ERROR_OUT_OF_DATE_KHR:
case VK_SUBOPTIMAL_KHR:
on_resized();
return;
default:
throw std::runtime_error("vkQueuePresentKHR");
}
}
編輯:正如下面的答案所建議的,假設我們有k
個“飛行中的幀”,因此有k
個信號量實例和上面代碼中使用的柵欄,我將用m_image_available[i]
、 m_rendering_finished[i]
和m_fence[i]
對於i = 0, ..., k - 1
。 令i
表示飛行中幀的當前索引,在每次調用render()
后增加1
, j
表示調用render()
的次數,從j = 0
開始。
現在,假設交換鏈包含三個圖像。
j = 0
,則i = 0
並且飛行中的第一幀使用交換鏈圖像0
j = a
,則i = a
並且飛行中的第a
幀正在使用交換鏈圖像a
,對於a= 2, 3
j = 3
,則i = 3
,但由於交換鏈圖像只有三個圖像,所以飛行中的第四幀再次使用交換鏈圖像0
。 我想知道這是否有問題。 我猜不是這樣,因為在調用render()
時調用vkAcquireNextImageKHR
、 vkQueueSubmit
和vkQueuePresentKHR
中使用的等待/信號量m_image_available[3]
/ m_rendering_finished[3]
專用於飛行中的這個特定幀。j = k
,那么i = 0
再次,因為只有k
幀在飛行。 現在我們可能會在render()
的開頭等待,如果從第一次調用( i = 0
)的render()
調用vkQueuePresentKHR
還沒有發出m_fence[0]
的信號。 所以,除了我在上面第三個要點中描述的懷疑之外,唯一剩下的問題是為什么我不應該盡可能大地取k
? 我理論上可以想象的是,如果我們以比 GPU 能夠消耗的速度更快的方式向 GPU 提交工作,則使用的隊列可能會不斷增長並最終溢出(隊列中是否存在某種“最大命令“ 限制?)。
如果我做對了,最低要求是我們通過一組信號量 image_available 和 rendering_finished 確保 vkAcquireNextImageKHR、vkQueueSubmit 和 vkQueuePresentKHR 之間的 GPU-GPU 同步,正如我在下面的示例代碼中所做的那樣。
是的,你沒看錯。 您通過vkAcquireNextImageKHR
提交獲取要渲染的新圖像的願望。 一旦要渲染的圖像可用,表示引擎就會發出m_image_available
信號量的信號。 但是您已經提交了指令。
接下來,您通過submit_info
向圖形隊列提交一些命令。 即它們也已經提交給 GPU 並在那里等待,直到m_image_available
信號量接收到它的信號。
此外,將表示指令提交給表示引擎,該指令表示它需要等待直到submit_info
命令通過等待m_rendering_finished
信號量完成的依賴關系。
即一切都已提交。 如果尚未發出任何信號,則所有內容都位於某些 GPU 緩沖區中並等待信號。
現在,如果您的代碼直接循環回到render()
function 並重新使用相同的m_image_available
和m_rendering_finished
信號量,它只會在您非常幸運的情況下工作,即如果所有信號量在您再次使用它們之前已經發出信號。
vkAcquireNextImageKHR
的規格說明如下:
如果信號量不是 VK_NULL_HANDLE 它不能有任何未完成的信號或等待操作掛起
等待二進制信號量的行為也會取消該信號量的信號。
即確實,您需要在 CPU上等待,直到您確定之前使用相同vkAcquireNextImageKHR
信號量的m_image_available
已完成。
是的,您已經做對了:您需要為傳遞給vkQueueSubmit
的內容使用柵欄。 如果您不在 CPU 上進行同步,您將在 GPU 上進行更多工作(這是一個問題),並且您正在重復使用的信號量可能無法及時正確地取消信號(這是一個問題)。
經常做的是將信號量和柵欄相乘,例如每個成3個,並按順序使用這些同步對象集,以便在GPU上並行處理更多工作。 Vulkan 教程在其渲染和演示一章中很好地描述了這一點。 7:59開始的本次講座中還使用 animation 進行了解釋。
因此,首先,正如您正確提到的,信號量嚴格用於 GPU-GPU 同步,例如,確保一批命令(一個提交)在另一批命令開始之前完成。 這在這里用於將渲染命令與呈現命令同步,以便呈現引擎知道何時呈現呈現的圖像。
Fences 是 CPU-GPU 同步的主要工具。 您在隊列提交中放置一個柵欄,然后在 CPU 端等待它,然后再繼續。 這通常在這里完成,這樣我們就不會在前一幀尚未完成時排隊任何新的渲染/呈現命令。
但這是否意味着我一定需要做一個
if (vkWaitForFences(device(), 1, &fence[image_index_of_last_call], VK_FALSE, std::numeric_limits<std::uint64_t>::max()) != VK_SUCCESS)
throw std::runtime_error("vkWaitForFences");
vkResetFences(device(), 1, &fence[image_index_of_last_call]);
在我打電話給 vkAcquireNextImageKHR 之前?
是的,您的代碼中肯定需要這個,否則您的信號量將不安全,並且您可能會遇到驗證錯誤。
一般來說,如果你想讓你的 CPU 等到你的 GPU 完成前一幀的渲染,你將只有一個柵欄和一對信號量。 您還可以通過隊列或設備的 waitIdle 命令替換柵欄。 但是,在實踐中,您不希望停止 CPU 並同時記錄下一幀的命令。 這是通過飛行中的幀完成的。 這僅僅意味着對於飛行中的每一幀(即可以與 GPU 上的執行並行記錄的幀數),您有一個柵欄和一對同步該特定幀的信號量。
因此,從本質上講,為了讓您的渲染循環正常工作,您需要在每幀飛行中使用一對信號量 + 柵欄,與交換鏈圖像的數量無關。 但是,請注意,當前幀索引(飛行中的幀)和圖像索引(交換鏈)通常不會相同,除非您使用與飛行中的幀相同數量的交換鏈圖像。 這是因為呈現引擎可能會根據您的呈現模式為您提供亂序的交換鏈圖像。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.