Reputation: 341
I am implementing a basic 2D game engine using vulkan. I want to re-implement SpriteBatch from XNA/MonoGame, so basically I want to draw lots of textured quads with individual transformations (rotation, translation, scale).
My application works, but there are two issues:
vkQueueSubmit
returns DEVICE_LOST
, when there are more than just a few hundred drawsA test, drawing 30000 images with random rotation:
This looks as expected. View the test app through OBS, and you see this:
On high end hardware, the application also randomly crashes every few hours (DEVICE_LOST
). Right before the crash, the image that is presented on the window briefly looks like the one that can always be observed through OBS (flicker, corruption).
Validation layers are not reporting any errors or warnings. My initial suspicion is that I have messed up synchronization, in a way that causes slower hardware to crash with a higher chance.
My code is based on Sascha Willems vulkan tutorial (https://vulkan-tutorial.com/) (though heavily adapted). Here is a (heavily-reduced) version of how a frame looks like (I kept all vulkan commands and synchronization code):
vkWaitForFences(instance.Device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
auto aquireImageResult = vkAcquireNextImageKHR(instance.Device, swapChain.SwapChain, UINT64_MAX,
imageAvailableSemaphore[currentFrame], VK_NULL_HANDLE,
&imageIndex);
if (aquireImageResult == VK_ERROR_OUT_OF_DATE_KHR || aquireImageResult == VK_SUBOPTIMAL_KHR)
{
return ...
}
if (aquireImageResult != VK_SUCCESS)
{
return GenericFailure;
}
if (inFlightImages[imageIndex] != VK_NULL_HANDLE)
vkWaitForFences(instance.Device, 1, &inFlightImages[imageIndex], VK_TRUE, UINT64_MAX);
inFlightImages[imageIndex] = inFlightFences[currentFrame];
vkResetFences(instance.Device, 1, &inFlightFences[currentFrame]);
// We re-record command buffers every frame
auto& rootCommandBuffer = instance.CommandBuffers[imageIndex];
auto& frameBuffer = instance.FrameBuffers[imageIndex];
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0;
beginInfo.pInheritanceInfo = nullptr;
vkResetCommandBuffer(rootCommandBuffer, 0); // This is done implicitly on vkBeginCommandBuffer because of the flags on the command pool, but better do it explicitly
if (vkBeginCommandBuffer(rootCommandBuffer, &beginInfo) != VK_SUCCESS)
throw std::runtime_error("Failed to begin recording command buffer.");
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = instance.RenderPass;
renderPassInfo.framebuffer = frameBuffer;
renderPassInfo.renderArea.offset = { 0, 0 };
renderPassInfo.renderArea.extent = swapChain.Extent;
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(rootCommandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
// "Draws" are submitted in C# land - a struct of (image, trans_matrix, color) is written into a host_coherent buffer for each draw.
// Draw is being done with instanceCount = _numberOfDraws, geometrie is created in vertex shader (just a quad)
// Basically, these two commands are executed exaclty once per frame:
// Start C#
var setsToBeBound = stackalloc VkDescriptorSet[2] { _bufferDescriptorSet, _imageDescriptorSet };
VulkanNative.vkCmdBindDescriptorSets(_vkCommandBuffer, VkPipelineBindPoint.VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineHandle.Pipeline->VkPipelineLayout, 0, 2, setsToBeBound, 0, null);
VulkanNative.vkCmdDraw(_vkCommandBuffer, 4, (uint)_numberOfDraws, 0, 0);
// End C#
vkCmdEndRenderPass(rootCommandBuffer);
if (vkEndCommandBuffer(rootCommandBuffer) != VK_SUCCESS)
throw std::runtime_error("Failed to end command buffer recording.");
VkSemaphore waitSemaphores[] = { imageAvailableSemaphore[currentFrame] };
VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
VkSemaphore signalSemaphores[] = { renderFinishedSemaphore[currentFrame] };
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &rootCommandBuffer;
auto queueSubmitResult = vkQueueSubmit(instance.GraphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);
if (queueSubmitResult != VK_SUCCESS)
throw std::runtime_error(std::string("Failed to submit draw command buffer. Code: ") + std::to_string(queueSubmitResult)); // Here a crash can happen with code DEVICE_LOST
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = { swapChain.SwapChain };
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = nullptr; // Optional
auto presentResult = vkQueuePresentKHR(instance.PresentQueue, &presentInfo);
if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || presentResult == VK_SUBOPTIMAL_KHR)
{
instance.PendingResize = true;
}
else if (presentResult != VK_SUCCESS)
{
throw ...
}
currentFrame = (currentFrame + 1) % maxFramesInFlight;
What makes me question my sanity is that even if I insert a vkQueueWaitIdle
at the end/start of the frame, the bug still occurs (So it seems that this is not a bug due to the multiple-frames-in-flight design supported throughout the application).
Upvotes: 2
Views: 180