vulkan

Vulkan validation error: "Semaphore must not have any pending operations." from vkAcquireNextImageKHR


I recently forked an old Vulkan project of mine, written in Java with LWJGL. While I was testing it to start modifying it I noticed the following validation error, generated on each frame:

[Renderer] DEBUG com.f03.juleng.engine.rendering.Renderer -- Acquiring next image. Frame: 6
[Renderer] ERROR com.f03.juleng.engine.rendering.VulkanInstance -- VkDebugUtilsCallback: Validation Error: [ VUID-vkAcquireNextImageKHR-semaphore-01779 ] Object 0: handle = 0xab64de0000000020, type = VK_OBJECT_TYPE_SEMAPHORE; | MessageID = 0x5717e75b | vkAcquireNextImageKHR():  Semaphore must not have any pending operations. The Vulkan spec states: If semaphore is not VK_NULL_HANDLE it must not have any uncompleted signal or wait operations pending (https://vulkan.lunarg.com/doc/view/1.3.283.0/linux/1.3-extensions/vkspec.html#VUID-vkAcquireNextImageKHR-semaphore-01779)
[Renderer] DEBUG com.f03.juleng.engine.rendering.Renderer -- Submitting present queue. Frame: 6
[Renderer] DEBUG com.f03.juleng.engine.rendering.Renderer -- Presenting next image. Frame: 6

I could not remember this error being present in my old project, and that's strange because I didn't make any changes to the engine itself. I just migrated the project from Windows to Linux. Also, the problem does NOT crash the program, which runs normally. It's just that it floods the console, which is a sign that something is wrong...

This is the code in the SwapChain class:

    boolean acquire() {
        boolean outOfDate = false;
        try (MemoryStack stack = MemoryStack.stackPush()) {
            IntBuffer ip = stack.mallocInt(1);

            // NEXT LINE IS WHAT CAUSES THE ERROR
            int result = KHRSwapchain.vkAcquireNextImageKHR(
                    device.getLogicalDevice(), 
                    swapChain, ~0L,
                    semaphores[frame].acquisitionSemaphore().getSemaphore(), 
                    MemoryUtil.NULL, 
                    ip);
            
            if (result == KHRSwapchain.VK_ERROR_OUT_OF_DATE_KHR) {
                outOfDate = true;
            } else if (result == KHRSwapchain.VK_SUBOPTIMAL_KHR) {
                // TODO
            } else if (result != VK_SUCCESS) {
                throw new RuntimeException("Failed to acquire image: " + result);
            }
            
            frame = ip.get(0);
        }

        return !outOfDate;
    }
    
    boolean present(Queue queue){
        boolean resize = false;
        try (MemoryStack stack = MemoryStack.stackPush()) {
            
            VkPresentInfoKHR present = VkPresentInfoKHR.calloc(stack)
                    .sType(KHRSwapchain.VK_STRUCTURE_TYPE_PRESENT_INFO_KHR)
                    .pWaitSemaphores(stack.longs(semaphores[frame].completeSemaphore().getSemaphore()))
                    .swapchainCount(1)
                    .pSwapchains(stack.longs(swapChain))
                    .pImageIndices(stack.ints(frame));

            int result = KHRSwapchain.vkQueuePresentKHR(queue.getQueue(), present);
            
            if (result == KHRSwapchain.VK_ERROR_OUT_OF_DATE_KHR) {
                resize = true;
            } else if (result == KHRSwapchain.VK_SUBOPTIMAL_KHR) {
                // TODO
            } else if (result != VK_SUCCESS) {
                throw new RuntimeException("Failed to present KHR: " + result);
            }
        }
        
        frame = (frame + 1) % imageViews.length;
        
        return resize;
    }

The semaphore creation happens in the constructor:

    semaphores = new SyncSemaphores[nImages];
    for(int i = 0; i < nImages; i++){
        semaphores[i] = new SyncSemaphores(device);
    }

These are the queue submit functions:

    void submit(Queue queue) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            int idx = swapChain.currentFrame();
            CommandBuffer commandBuffer = commandBuffers[idx];
            Fence currentFence = fences[idx];
            SwapChain.SyncSemaphores syncSemaphores = swapChain.getSyncSemaphores()[idx];
            queue.submit(stack.pointers(commandBuffer.getCommandBuffer()),
                    stack.longs(syncSemaphores.acquisitionSemaphore().getSemaphore()),
                    stack.ints(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT),
                    stack.longs(syncSemaphores.completeSemaphore().getSemaphore()), 
                    currentFence);
        }
    }

And in the Queue class:

    public void submit(PointerBuffer commandBuffers, LongBuffer waitSemaphores, IntBuffer dstStageMasks, 
            LongBuffer signalSemaphores, Fence fence) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            VkSubmitInfo submitInfo = VkSubmitInfo.calloc(stack)
                    .sType(VK_STRUCTURE_TYPE_SUBMIT_INFO)
                    .pCommandBuffers(commandBuffers)
                    .pSignalSemaphores(signalSemaphores);
            if (waitSemaphores != null) {
                submitInfo.waitSemaphoreCount(waitSemaphores.capacity())
                        .pWaitSemaphores(waitSemaphores)
                        .pWaitDstStageMask(dstStageMasks);
            } else {
                submitInfo.waitSemaphoreCount(0);
            }
            long fenceHandle = fence != null ? fence.getFence(): VK_NULL_HANDLE;

            vkCheck(vkQueueSubmit(queue, submitInfo, fenceHandle),
                    "Failed to submit command to queue", logger);
        }
    }

And this is the rendering function, called every frame:

    private void render(){
        lock.lock();
        try {
            while(paused.get()){
                pause.awaitUninterruptibly();
            }
        } finally {
            lock.unlock();
        }
        
        logger.debug("Acquiring next image. Frame: " + swapChain.currentFrame());
        if(!swapChain.acquire() || recreateSwapChain.get()){
            resize(); // SwapChain out of date. Recreate.
            swapChain.acquire();
            recreateSwapChain.set(false);
        }
        
        updateApplicationRendering();
        for(IRenderer subRenderer : subRenderers.values()){
            subRenderer.render();
        }
        logger.debug("Submitting present queue. Frame: " + swapChain.currentFrame());
        sys.submit(presentQueue);
        
        logger.debug("Presenting next image. Frame: " + swapChain.currentFrame());
        if(swapChain.present(graphicsQueue)){
            recreateSwapChain.set(true);
        }
    }

I already tried disabling the intermediate application rendering code, but it remained the same.

My questions are two:


Solution

  • I had temporarily bypassed the problem presentQueue.waitIdle() before acquiring a new image from the SwapChain, but it blocks the current thread preventing it from rendering the next frame until the current frame is presented. This is likely to cause performance issues. So after some research I solved the problem simply by waiting on the current fence. The render function becomes:

    private void render(){
        lock.lock();
        try {
            while(paused.get()){
                pause.awaitUninterruptibly();
            }
        } finally {
            lock.unlock();
        }
        
        //presentQueue.waitIdle(); // Temporary fix.
        // This solves the problem. Waits for the fence at the 
        // current frame index.
        sys.waitForFence(); 
        sys.resetFence();
        
        int imageIndex;
        if((imageIndex = swapChain.acquire()) < 0 || recreateSwapChain.get()){
            resize(); // SwapChain out of date. Recreate.
            swapChain.acquire();
            recreateSwapChain.set(false);
        }
        
        updateApplicationRendering();
        for(IRenderer subRenderer : subRenderers.values()){
            subRenderer.render();
        }
        sys.submit(presentQueue);
        
        if(swapChain.present(graphicsQueue, imageIndex)){
            recreateSwapChain.set(true);
        }
    }
    

    Thanks to @KarlShultz for the suggestion. This tutorial may be useful.