I've read several articles on the CPU-GPU (using fences) and GPU-GPU (using semaphores) synchronization mechanisms, but still got trouble to understand how I should implement a simple render-loop.
Please take a look at the simple render()
function below. If I got it right, the minimal requirement is that we ensure the GPU-GPU synchronization between vkAcquireNextImageKHR
, vkQueueSubmit
and vkQueuePresentKHR
by a single set of semaphores image_available
and rendering_finished
as I've done in the example code below.
However, is this really safe? All operations are asynchronous. So, is it really safe to "reuse" the image_available
semaphore in a subsequent call of render()
again even though the signal request from the previous call hasn't fired yet? I would think it's not, but, on the other hand, we're using the same queues (don't know if it matters where the graphics and presentation queue are actually the same) and operations inside a queue should be sequentially consumed ... But if I got it right, they might not be consumed "as a whole" and could be reordered ...
The second thing is that (again, unless I'm missing something) I clearly should use one fence per swap chain image to ensure that the operation on the image corresponding to the image_index
of the call to render()
has finished. But does that mean that I necessarily need to do a
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]);
before my call to vkAcquireNextImageKHR
? And do I then need dedicated image_available
and rendering_finished
semaphores per swap chain image? Or maybe per frame? Or maybe per command buffer/pool? I'm really confused ...
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");
}
}
EDIT: As suggested in the answers below, assume we have k
"frames in flight" and hence k
instances of the semaphores and the fence used in the code above, which I will denote by m_image_available[i]
, m_rendering_finished[i]
and m_fence[i]
for i = 0, ..., k - 1
. Let i
denote the current index of the frame in flight, which is increased by 1
after each invocation of render()
, and j
denote the number of invocations of render()
, starting from j = 0
.
Now, assume the swap chain contains three images.
- If
j = 0
, theni = 0
and the first frame in flight is using swap chain image0
- In the same way, if
j = a
, theni = a
and thea
th frame in flight is using swap chain imagea
, fora= 2, 3
- Now, if
j = 3
, theni = 3
, but since the swap chain image only has three images, the fourth frame in flight is using swap chain image0
again. I wonder whether this is problematic or not. I guess it's not, since the wait/signal semaphoresm_image_available[3]
/m_rendering_finished[3]
, used in the calls ofvkAcquireNextImageKHR
,vkQueueSubmit
andvkQueuePresentKHR
in this invocation ofrender()
, are dedicated to this particular frame in flight. - If we reach
j = k
, theni = 0
again, since there are onlyk
frames in flight. Now we potentially wait at the beginning ofrender()
, if the call tovkQueuePresentKHR
from the first invocation (i = 0
) ofrender()
hasn't signaledm_fence[0]
yet.
So, besides my doubts described in the third bullet point above, the only question which remains is why I shouldn't take k
as large as possible? What I theoretically could imagine is that if we are submitting work to the GPU in a quicker fashion than the GPU is able to consume, the used queue(s) might continually grow and eventually overflow (is there some kind of "max commands in queue" limit?).