WaitForGPUSwapchain() doesn't wait (Vulkan)

Hello everyone

I’m working on the VSync input latency of my game. In my understanding, that’s what SDL_WaitForGPUSwapchain() is for, to sync the start of the game loop with the framerate:

SDL_WaitForGPUSwapchain()
-- VBLANK --
measureTime()
handleEvents()
updateState()
SDL_AcquireGPUSwapchainTexture()
sendRenderCommands()

However, this doesn’t work on my system (Mint 22.1, X11, Vulkan, AMD Radeon). It seems that SDL_WaitForGPUSwapchain() doesn’t block at all. Instead, SDL_AcquireGPUSwapchainTexture() blocks for the entire frame regardless, even though it should set swapchain to NULL and return immediately when it isn’t ready.

Minimal example:

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

int main(int argc, char** argv)
{
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* window = SDL_CreateWindow("test", 640, 360, 0);
  SDL_GPUDevice* device = SDL_CreateGPUDevice(0xFF, true, "vulkan");

  SDL_assert(window);
  SDL_assert(device);
  SDL_assert(SDL_ClaimWindowForGPUDevice(device, window));

  while (true) {
    SDL_Event event;
    while (SDL_PollEvent(&event))
      if (event.type == SDL_EVENT_QUIT)
        return 0;

    SDL_GPUCommandBuffer* command = SDL_AcquireGPUCommandBuffer(device);
    SDL_GPUTexture* swapchain;

Sint64 start = SDL_GetTicksNS();
    SDL_assert(SDL_WaitForGPUSwapchain(device, window));
Sint64 wait = SDL_GetTicksNS();
    SDL_assert(SDL_AcquireGPUSwapchainTexture(command, window, &swapchain, NULL, NULL));
Sint64 aquire = SDL_GetTicksNS();

    SDL_Log("Wait: %8li  Acquire: %8li", wait - start, aquire - wait);

    if (swapchain) {
      float pulse = 0.5f + 0.5f*SDL_cosf(SDL_GetTicks()/1000.0f);
      SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(command,
        &(SDL_GPUColorTargetInfo){
        .texture     = swapchain,
        .clear_color = (SDL_FColor){0, pulse, 1, 1},
        .load_op     = SDL_GPU_LOADOP_CLEAR,
        .store_op    = SDL_GPU_STOREOP_STORE}, 1, NULL);
      SDL_EndGPURenderPass(pass);
      SDL_SubmitGPUCommandBuffer(command);
    }
    else {
      SDL_Log("NULL swapchain");
      SDL_CancelGPUCommandBuffer(command);
    }
  }
  return 0;
}

Relevant output:

SDL revision: SDL-release-3.4.8-0-gd9d553670
SDL chose video backend 'x11'
SDL chose gpu backend 'vulkan'
Validation layers enabled, expect debug level performance!
Vulkan Device: AMD Radeon RX 6400 (RADV NAVI24)
Vulkan Driver: radv Mesa 25.2.8-0ubuntu0.24.04.1
...
Wait:    10960  Acquire: 16470012
Wait:    52030  Acquire: 16439692
Wait:    17530  Acquire: 16603893
...

From what I can tell, the blocking happens here. I come from OpenGL, so I’m not very familiar with low-level synchronization. But it seems that the fence disagrees with the semaphore on whether the swapchain is ready or not?

Am I just doing this incorrectly? Or have I encountered an actual issue? Any help is appreciated.

SDL_WaitForGPUSwapchain() does literally what it says: it waits until a swapchain texture is available. This is only tangentially related to vsync, because your system almost certainly has multiple textures in the swapchain.

SDL_AcquireGPUSwapchainTexture() blocking sounds like a bug.

You should probably use SDL_WaitAndAcquireGPUSwapchainTexture()

But once the queue is full (SDL_SetGPUAllowedFramesInFlight()), the next texture only becomes available when the oldest one is displayed, which is tied to VSync, or not?

The problem with SDL_WaitAndAcquireGPUSwapchainTexture() is that the events have to be processed before the blocking, which adds unnecessary input latency.

There is a Unity blog post about their improved frame timing and input latency. The pseudo-code in my first post is based on the Unity approach. So in theory, the SDL already provides the necessary functionality for an optimal loop, it just doesn’t seem to work as expected.