LukasBanana / LLGL

Low Level Graphics Library (LLGL) is a thin abstraction layer for the modern graphics APIs OpenGL, Direct3D, Vulkan, and Metal
BSD 3-Clause "New" or "Revised" License
2.05k stars 139 forks source link

Multi submission of command buffer results in flickering #75

Closed mikex86 closed 1 year ago

mikex86 commented 2 years ago

LLGL::CommandBufferFlags::MultiSubmit seems to imply, that it should be possible to record a command buffer once and resubmit it multiple times to prevent CPU cycles being wasted dispatching draw calls to the GPU while nothing has changed.

The results of testing out this theory made me wonder whether or not the results I was seeing were intended behaviour. Tested with Vulkan.

    LLGL::CommandBuffer *renderCmd;
    {
        LLGL::CommandBufferDescriptor commandBufferDescriptor{
                .flags =(LLGL::CommandBufferFlags::MultiSubmit)
        };
        renderCmd = renderSystem->CreateCommandBuffer(commandBufferDescriptor);
    }
    renderCmd->Begin();
    {
        renderCmd->SetViewport(renderContext->GetResolution());
        renderCmd->SetPipelineState(*pipeline);
        renderCmd->SetVertexBuffer(*vertexBuffer);
        renderCmd->SetClearColor(LLGL::ColorRGBAf(0.1f, 0.1f, 0.1f));

        renderCmd->BeginRenderPass(*renderContext);

        renderCmd->Clear(LLGL::ClearFlags::Color);
        renderCmd->Draw(3, 0);

        renderCmd->EndRenderPass();
    }
    renderCmd->End();
    while (window.ProcessEvents()) {
        queue->Submit(*renderCmd);
        renderContext->Present();
    }

This little example produces nothing more than the clear color flashing between black and the gray specified in the command buffer. The triangle, which renders just fine when the command buffer is re-recorded every frame in the main loop, is no where to be seen with this code.

However, some experimentation revealed even weirder behaviour: Re-recording the command buffer for the first three frames of the application and then continuing to use it works just fine. If the command buffer is altered at a later point in time, it requires only 2 repeated recordings (with a Submit call and a renderContext->Present() in between) for the changes to be displayed without flickering. Re-recording the command buffer less than 3 times for the initial frames of the loop, or less than 2 times at a later point in time will result in flickering back and forth between what seem to be the previously recorded draw calls and what actually should be displayed.

I think this might be happening because a double buffered swap chain also has two command buffers. When a new frame is forced to render, it will cycle back to the previous command buffer. If the command buffer contents have not been updated in a frame, no new image should be acquired from the swap chain.

Should it be possible to re-submit command buffers in this way? Am I using LLGL incorrectly?

LukasBanana commented 2 years ago

This does look like a bug and I also never had the time to experiment a lot with multi-submit buffers unfortunately. I'm also out on vacation for the next four weeks, so won't have time to look into that. For the time being, I recommend wrapping the command buffer recording into a function and call it every frame for single submit command buffers until the issue is identified and fixed. Or use the GL backend until then, the multi-submit buffer implementation there should work fine.

ceeac commented 1 year ago

I believe the behaviour is most likely present because in VKSwapChain::GetPresentableImageIndex() the call to vkAcquireNextImageKHR only signals the semaphore to vkQueuePresentKHR but not to the command buffer(s) that render to the swap chain image. From https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#vkAcquireNextImageKHR:

The presentation engine may not have finished reading from the image at the time it is acquired, so the application must use semaphore and/or fence to ensure that the image layout and contents are not modified until the presentation engine reads have completed.

Currently, the following can happen:

LukasBanana commented 1 year ago

This has been fixed with the new design of the BeginRenderPass interface (better late than never). I'll have to come up with an example just for multi-submit command buffers, but you basically have to record multiple command buffers when you render into a swap-chain. One command buffer for each swap-chain back-buffer:

LLGL::CommandBuffer cmdBuffers[3];
for (std::uint32_t swapBufferIndex = 0; swapBufferIndex < swapChain->GetNumSwapBuffers(); ++swapBufferIndex) {
  auto& renderCmd = cmdBuffers[swapBufferIndex];
  renderCmd = renderer->CreateCommandBuffer(LLGL::CommandBufferFlags::MultiSubmit);

  renderCmd->Begin();
  renderCmd->BeginRenderPass(*swapChain, nullptr, 0, nullptr, swapBufferIndex);

  /* Render into currently selected back buffer (via 'swapBufferIndex') ... */

  renderCmd->EndRenderPass();
  renderCmd->End();
}

while (/* ... */) {
  // Submit command buffer that encoded the back buffer that is currently active in the swap-chain
  queue->Submit(*cmdBuffers[swapChain->GetCurrentSwapIndex()]);
  swapChain->Present();
}

Command buffers that only render into render-targets (not swap-chains) don't have to be recorded for multiple states since regular render-targets don't have a chain of internal buffers.