Double buffering issue

Synopsis

I have a program that pipes video out the screen directly via KMS. This program has a double-buffering scheme that modesets two frame buffers, which were allocated via libGBM. The implicit synchronization of drmModeSetCrtc() to the blanking interval essentially acts as a form of VSync. OpenGL is used to render each video frame, and the render target frame buffer is switched via eglMakeCurrent(). The buffers are locked before being modeset.

  /* Main loop */
  for (;;)
  {
    /* First buffer swap-in */
    eglMakeCurrent(egldpy, eglSur1, eglSur1, eglctx);
    edraw();
    eglSwapBuffers(egldpy, eglSur1);
    gbmbo = gbm_surface_lock_front_buffer(gbmSur1);
    if (gbmbo == NULL)
      fprintf(stderr, "%s: [GBM] Failed to lock buffer 2.\n", progname);
    drmModeSetCrtc(gpufd, drm_crtcid, fb1, 0, 0, &drm_connid, 1, &drm_mode);
    gbm_surface_release_buffer(gbmSur1, gbmbo);
    gbmbo = NULL;

    /* Second buffer swap-in */
    eglMakeCurrent(egldpy, eglSur2, eglSur2, eglctx);
    edraw();
    eglSwapBuffers(egldpy, eglSur2);
    gbmbo = gbm_surface_lock_front_buffer(gbmSur2);
    if (gbmbo == NULL)
      fprintf(stderr, "%s: [GBM] Failed to lock buffer 1.\n", progname);
    drmModeSetCrtc(gpufd, drm_crtcid, fb2, 0, 0, &drm_connid, 1, &drm_mode);
    gbm_surface_release_buffer(gbmSur2, gbmbo);
    gbmbo = NULL;
  }

The problem

On Intel GPUs, this double buffering scheme appears to work perfectly fine. However, on NVIDIA GPUs with the NVIDIA driver, this scheme causes the video to stagger. I have one theory on why this may be the case - maybe the GPU’s state is being reset every time eglMakeCurrent() is called?

Either way, a staggering, laggy video player, doesn’t make for a good experience. Is there something fundamentally wrong with my approach, or is it an issue with the driver itself?

– EDIT: The render target frame buffer is switched via eglMakeCurrent(), not eglSwapBuffers()

Best solution I’ve found

The solution below this solution is somewhat poor. I mistakenly reused the gbmbo variable for both buffers, so it made that solution more complicated. Here is a better solution:

  /* Outside of main loop */
  gbm_surface_release_buffer(gbmSur1, gbmbo);
  gbmbo = NULL;

  /* Main loop init */
  for (;;)
  {
    /* First buffer swap-in */
    gbm_surface_release_buffer(gbmSur1, gbmbo2);
    gbmbo2 = NULL;
    edraw();
    eglSwapBuffers(egldpy, eglSur1);
    gbmbo = gbm_surface_lock_front_buffer(gbmSur1);
    if (gbmbo == NULL)
      fprintf(stderr, "%s: [GBM] Failed to lock buffer 2.\n", progname);
    drmModeSetCrtc(gpufd, drm_crtcid, fb2, 0, 0, &drm_connid, 1, &drm_mode);

    /* Second buffer swap-in */
    gbm_surface_release_buffer(gbmSur1, gbmbo);
    gbmbo = NULL;
    edraw();
    eglSwapBuffers(egldpy, eglSur1);
    gbmbo2 = gbm_surface_lock_front_buffer(gbmSur1);
    if (gbmbo2 == NULL)
      fprintf(stderr, "%s: [GBM] Failed to lock buffer 1.\n", progname);
    drmModeSetCrtc(gpufd, drm_crtcid, fb1, 0, 0, &drm_connid, 1, &drm_mode);
  }

Old solution

I have found a solution, although I am not sure if it is the “proper” one. This will allow for the GPU to swap buffers without staggering, or deadlocking. It is as follows:

  /* Main loop init */
  for (;;)
  {
    /* First buffer swap-in */
    gbm_surface_release_buffer(gbmSur1, gbmbo);
    gbmbo = NULL;
    edraw();
    eglSwapBuffers(egldpy, eglSur1);
    gbmbo = gbm_surface_lock_front_buffer(gbmSur1);
    if (gbmbo == NULL)
      fprintf(stderr, "%s: [GBM] Failed to lock buffer 2.\n", progname);
    drmModeSetCrtc(gpufd, drm_crtcid, fb2, 0, 0, &drm_connid, 1, &drm_mode);
    gbm_surface_release_buffer(gbmSur1, gbmbo);
    gbmbo = NULL;

    /* Second buffer swap-in */
    gbm_surface_release_buffer(gbmSur1, gbmbo2);
    gbmbo2 = NULL;
    edraw();
    eglSwapBuffers(egldpy, eglSur1);
    gbmbo = gbm_surface_lock_front_buffer(gbmSur1);
    if (gbmbo == NULL)
      fprintf(stderr, "%s: [GBM] Failed to lock buffer 1.\n", progname);
    drmModeSetCrtc(gpufd, drm_crtcid, fb1, 0, 0, &drm_connid, 1, &drm_mode);
    gbm_surface_release_buffer(gbmSur1, gbmbo2);
    gbmbo2 = NULL;
  }

Beforehand, I did not know that the GBM surface had several buffers attached to it. Therefore, I was using two GBM surfaces as render target buffers, instead of using one GBM surface with two render target buffers. This likely caused the NVIDIA driver to reset the GPU context a lot, as I was using eglMakeCurrent() to switch between render targets, which likely hinted at the driver that I wanted to use a separate context entirely, rather than a different surface.

Program initialization (extra info)

The initial frame is drawn. Then, eglSwapBuffers() is called.

  /* Initial draw */
  einit();
  edraw();
  eglSwapBuffers(egldpy, eglSur1);

Next, the first BO is retrieved by locking a front buffer.

  /* Lock buffer */
  gbmbo = gbm_surface_lock_front_buffer(gbmSur1);
  if(gbmbo == NULL)
  {
    fprintf(stderr, "%s: [GBM] Failed to lock buffer 1 for first time.\n",
          progname);
    return 1;
  }

The first frame buffer is added


    /* First framebuffer */
    if (drmModeAddFB(gpufd, width, height, 24, 32, stride, handle,
                     &fb1))
    {
      fprintf(stderr, "%s: [DRM] Failed to add framebuffer.\n", progname);
      return 1;
    }

Those three steps are then repeated for the second buffer. If you draw and call eglSwapBuffers() before you call gbm_surface_lock_buffer() a second time, a second buffer will automatically be given.

Note for Intel drivers

This solution will cause visual artifacts on Intel. For some reason, eglSwapBuffers() behavior seems to be different for each driver (the current render target buffer ends up swapped), so if you wish to prevent this, the frame buffers on each drmModeSetCrtc() function should be swapped.

(it probably wasn’t a driver issue)